Compare commits

..
8 Commits
101 changed files with 1793 additions and 1215 deletions
@@ -0,0 +1,23 @@
name: "Setup Cocogitto"
description: "Installs a specific version of Cocogitto and cleans up the workspace"
inputs:
version:
description: "Cocogitto version to install"
required: true
default: "7.0.0"
runs:
using: "composite"
steps:
- name: Install Cocogitto
shell: bash
run: |
VERSION="${{ inputs.version }}"
URL="https://github.com/cocogitto/cocogitto/releases/download/${VERSION}/cocogitto-${VERSION}-x86_64-unknown-linux-musl.tar.gz"
curl -LOf "$URL"
tar -xzf "cocogitto-${VERSION}-x86_64-unknown-linux-musl.tar.gz"
sudo mv $(find . -name cog -type f -executable) /usr/local/bin/
git clean -fd
cog --version
+77
View File
@@ -0,0 +1,77 @@
name: Bump version
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: write
concurrency:
group: bump
cancel-in-progress: false
jobs:
verify:
uses: ./.github/workflows/verify.yml
bump:
needs: verify
runs-on: ubuntu-latest
outputs:
bumped: ${{ steps.check-bump.outputs.bumped }}
new_version: ${{ steps.get-tag.outputs.version }}
steps:
- name: Generate token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.XBOCT_BOT_ID }}
private-key: ${{ secrets.XBOCT_BOT_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ steps.generate_token.outputs.token }}
fetch-depth: 0
- name: Setup Cocogitto
uses: ./.github/actions/setup-cocogitto
- name: Check if bump is needed
id: check-bump
run: |
BUMP_OUTPUT=$(cog bump --auto --dry-run 2>&1)
if [[ "$BUMP_OUTPUT" == *"No conventional commits"* ]]; then
echo "No bump needed."
echo "bumped=false" >> "$GITHUB_OUTPUT"
else
echo "Changes detected, proceeding..."
echo "bumped=true" >> "$GITHUB_OUTPUT"
fi
- name: Bump and push
if: steps.check-bump.outputs.bumped == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
cog bump --auto
git push origin main --tags
- name: Get new version
id: get-tag
if: steps.check-bump.outputs.bumped == 'true'
run: |
VERSION=$(git describe --tags --abbrev=0)
echo "Detected version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
release:
needs: bump
if: needs.bump.outputs.bumped == 'true'
uses: ./.github/workflows/release.yml
with:
version: ${{ needs.bump.outputs.new_version }}
secrets: inherit
+2 -28
View File
@@ -17,34 +17,7 @@ concurrency:
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ^11.20.0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
pnpm-lock.yaml
web/pnpm-lock.yaml
- name: Install root dependencies
run: pnpm install --frozen-lockfile
- name: Install web dependencies
working-directory: web
run: pnpm install --frozen-lockfile
- name: Run quality gate
run: pnpm verify
uses: ./.github/workflows/verify.yml
build:
needs: verify
@@ -77,6 +50,7 @@ jobs:
run: pnpm build
env:
BASE_PATH: /easy-png-tools
PUBLIC_GA_ENABLED: "true"
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
+1 -29
View File
@@ -16,35 +16,7 @@ concurrency:
jobs:
verify:
name: Fast quality gate
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ^11.20.0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
pnpm-lock.yaml
web/pnpm-lock.yaml
- name: Install root dependencies
run: pnpm install --frozen-lockfile
- name: Install web dependencies
working-directory: web
run: pnpm install --frozen-lockfile
- name: Run quality gate
run: pnpm verify
uses: ./.github/workflows/verify.yml
e2e:
name: E2E (non-blocking baseline)
+58
View File
@@ -0,0 +1,58 @@
name: GitHub Release
on:
workflow_call:
inputs:
version:
required: true
type: string
workflow_dispatch:
inputs:
version:
description: "Версия для релиза (например, v0.2.0)"
required: true
type: string
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Generate token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.XBOCT_BOT_ID }}
private-key: ${{ secrets.XBOCT_BOT_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ steps.generate_token.outputs.token }}
fetch-depth: 0
- name: Setup Cocogitto
uses: ./.github/actions/setup-cocogitto
- name: Sync and reset
run: |
git fetch origin main --tags --force
git reset --hard origin/main
- name: Get changelog
run: |
cog changelog "${{ inputs.version }}" > GITHUB_CHANGELOG.md
if [ ! -s GITHUB_CHANGELOG.md ]; then
echo "Release ${{ inputs.version }}" > GITHUB_CHANGELOG.md
echo "::warning title=Changelog::Changelog was empty, used default text."
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.version }}
name: Release ${{ inputs.version }}
body_path: GITHUB_CHANGELOG.md
token: ${{ steps.generate_token.outputs.token }}
+39
View File
@@ -0,0 +1,39 @@
name: Verify
on:
workflow_call:
permissions:
contents: read
jobs:
verify:
name: Fast quality gate
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ^11.20.0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
pnpm-lock.yaml
web/pnpm-lock.yaml
- name: Install root dependencies
run: pnpm install --frozen-lockfile
- name: Install web dependencies
working-directory: web
run: pnpm install --frozen-lockfile
- name: Run quality gate
run: pnpm verify
+10 -1
View File
@@ -27,7 +27,7 @@
Читать только нужную строку, а не все документы подряд.
| Задача | Документ |
| --------------------------------------------- | ---------------------------------- |
| ---------------------------------------------- | ---------------------------------- |
| Слои, поток данных, новый инструмент/поле | `docs/architecture.md` |
| Какие проверки запускать, пороги, CI | `docs/quality-gates.md` |
| Правка самих гейтов: «почему так, а не иначе» | `docs/decisions.md` |
@@ -38,8 +38,17 @@
| Актуальный список инструментов | `docs/tools-map.md` |
| Правка полей и раскладки по волнам | `docs/plan-fields-audit-fixes.md` |
| Ручная приёмка UI | `docs/checklist-manual-testing.md` |
| Направления развития, фазы 1–11 | `docs/roadmap.md` |
| SEO/GEO-план (адреса, `noindex`, sitemap) | `docs/plan-seo.md` |
| API/PWA/шрифты | `docs/plan-platform.md` |
| Переименование бренда и покупка домена | `docs/plan-domain.local.md` † |
| Сырые UX-находки по инструментам (вход аудита) | `docs/tools-audit.md` |
| Архивирование и удаление старых документов | `docs/archive/README.md` |
| Индекс остальных документов | `docs/README.md` |
† `docs/plan-domain.local.md` вне git, в клоне отсутствует — в этом случае
задачу ищут по `docs/plan-seo.md` §6.
## Инварианты, которые линтер не проверяет
- **Сначала искать.** Новая константа, тип или утилита не заводится, пока не
+242
View File
@@ -0,0 +1,242 @@
# Changelog
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
- - -
## [v0.1.1](https://github.com/Ku6epXBOCTuK/easy-png-tools/compare/1db83d12ff717bc2e282fe21d35d282f261147f5..v0.1.1) - 2026-10-04
#### Bug Fixes
- google analytics only on production - ([e1fc11c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/e1fc11cb5491a6f5be5c823140830672ea669a62)) - Ku6epXBOCTuK
- shapes offset calculate - ([1db83d1](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/1db83d12ff717bc2e282fe21d35d282f261147f5)) - Ku6epXBOCTuK
- - -
## [v0.1.0](https://github.com/Ku6epXBOCTuK/easy-png-tools/compare/fdf7e2a3cd68d6693f1c5581796fafaf8939dad4..v0.1.0) - 2026-10-04
#### Features
- change id - remove png intents - ([7b7e12f](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/7b7e12fafcbd7444be61426cd0b90104de9de40f)) - Ku6epXBOCTuK
- move routes to slugs - ([d09c954](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/d09c95469494ec5f243f1981f6bc08b6e9fa6eb7)) - Ku6epXBOCTuK
- separate i18n dict pages - ([9776e1d](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/9776e1d3251761347440ed43d57fef851b2af150)) - Ku6epXBOCTuK
- separate slug pages from tools - ([7a3498c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/7a3498c173daa63f0a9ec88f09e10798525be9bd)) - Ku6epXBOCTuK
- add source aware resolver, update reset, update generator - ([5f77c09](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/5f77c097e2d58d1826a10f05217af973c4de7e81)) - Ku6epXBOCTuK
- remove v1 routes and libs - ([16d4288](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/16d42889120566e45008fa94285fd518b37f967f)) - Ku6epXBOCTuK
- add new dics\translations - ([9a27388](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/9a27388119bb1b14bd759a830a9444be6c498e1b)) - Ku6epXBOCTuK
- add icon for split tool, update docs - ([b91c1ab](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/b91c1ab7ee5bad72a90d2c66543a2e04e8f8c70e)) - Ku6epXBOCTuK
- add ui for new split tool - ([2be8871](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/2be887193084b0fc87f16c110264bad58dc7279c)) - Ku6epXBOCTuK
- add executor support for multifiles result - ([adb5785](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/adb578540f95ccafd3d0e8c419760da1cb031dcb)) - Ku6epXBOCTuK
- add split to parts to geometry registry - ([a7a42d0](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/a7a42d0091af3cc8e61c16962c4000d5c0bd8bdd)) - Ku6epXBOCTuK
- add error texts - ([e1c3d0a](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/e1c3d0a95ffd1b1a1245a88bf8db247c28fc0b64)) - Ku6epXBOCTuK
- add download zip with result files - ([ceee9ad](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/ceee9adead94c437785ede7053e4563a7cbf5054)) - Ku6epXBOCTuK
- add split to parts core tool - ([d94c5d1](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/d94c5d1297bfbc35b08feab28b565eab152217f4)) - Ku6epXBOCTuK
- add files tool result - ([ee5f660](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/ee5f660572676edf829faca3a238796468ab2c12)) - Ku6epXBOCTuK
- rename old -> v1 folder - ([33e3f3c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/33e3f3c4f035c0daeae8de687cd464b1572242e5)) - Ku6epXBOCTuK
- flatten lib/preview -> lib folder - ([1e5e096](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/1e5e096d5d61b5ca69aaf196928868ad67f2f4f5)) - Ku6epXBOCTuK
- flatten components/kit -> components folder - ([f8a1da6](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f8a1da6ff79be1a22e0046db7c3701110479011a)) - Ku6epXBOCTuK
- move preview pages to root - ([45dc54b](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/45dc54b6aa136f753de16856a2378fec5ecbd10f)) - Ku6epXBOCTuK
- move old root routes to /v1 routes - ([b6318f0](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/b6318f03ab60cea602606d88dd94ed3d0601dd0f)) - Ku6epXBOCTuK
- update filter tools to new types - ([d6706ab](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/d6706abea22cd9fa587f377338200032cce0f38a)) - Ku6epXBOCTuK
- update alpha and colors tools to new types - ([bea010d](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/bea010dd907e1c5ffe0b3b2f43c5db6efac9d348)) - Ku6epXBOCTuK
- update geometry tools to new types - ([2937974](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/2937974fe732690ca58e4d693e5c1a69ef4bfc90)) - Ku6epXBOCTuK
- update text tools to new types - ([1e85fe3](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/1e85fe36a636a7ab4e4daf48c2877ba9e91eeccb)) - Ku6epXBOCTuK
- update convert and analyze tools to new types - ([60c7d61](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/60c7d61daba60f656522e3ab45ffb87149910e69)) - Ku6epXBOCTuK
- update generate tool to new types - ([6b0b064](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6b0b0646d51d3b3f74e4d132df2bfd44be411530)) - Ku6epXBOCTuK
- update schema components - ([b6db1c8](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/b6db1c8f19ceb15c6a5e81f4108c9a25dc180525)) - Ku6epXBOCTuK
- update executor for tools - ([ba8a518](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/ba8a51886129d3c128d21ee625a8c3a0d16ae5b0)) - Ku6epXBOCTuK
- update tools types - ([292e53c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/292e53c9c76d4e02efa4990b5d62868a197d39c7)) - Ku6epXBOCTuK
- add text tools - ([e5b76ca](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/e5b76ca3e11eb0313988ea1d49578adb6f24bf1e)) - Ku6epXBOCTuK
- add text core tools and views - ([a497857](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/a49785764b39af1707be5990b5adcb43eb7356c9)) - Ku6epXBOCTuK
- add output format for download - ([c0cebdc](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/c0cebdc81f229fc2fab57b40c48b2fb6dd84f598)) - Ku6epXBOCTuK
- add generate tools - ([b3d460a](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/b3d460a951f69b505b153ae2e1af5596f8321049)) - Ku6epXBOCTuK
- add multiple colors field - ([6be9d10](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6be9d10c42580a984b78e95fb0e317b2d4d6993a)) - Ku6epXBOCTuK
- add analyze, generate, text tools and update plan - ([cc70750](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/cc70750727aaf5d5c758aa64fa577262a5197b9b)) - Ku6epXBOCTuK
- add alpha tools - ([e2735c5](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/e2735c52abf1e3968b24e334bb3d52c4e7f4dcf3)) - Ku6epXBOCTuK
- add geometry tools - ([d383cf2](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/d383cf29e30ad3722b754eb07cad10251af784d3)) - Ku6epXBOCTuK
- add filters tools - ([bfad0c7](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/bfad0c7d020fe8e19a5919ed42011710b955a1df)) - Ku6epXBOCTuK
- add colors tools - ([6750d5a](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6750d5a373c9f10b44fa75c7c407f12e2d8d6680)) - Ku6epXBOCTuK
- add layout groupt to tools - ([e662d1d](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/e662d1df248469f4e554b39a4e42d25a3296192d)) - Ku6epXBOCTuK
- add tool schema layout - ([d48262e](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/d48262e48269785c7edd414868fd952ff6d85f5f)) - Ku6epXBOCTuK
- add gradient control component - ([cad9bf6](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/cad9bf6cfd48aef22d27610d735a3e76bd4baaab)) - Ku6epXBOCTuK
- add plate (under text) field component - ([b99ccf5](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/b99ccf5d7808bdd3bca7349b10e1b3858c7e56c3)) - Ku6epXBOCTuK
- add font style field component - ([0a7c4a8](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/0a7c4a84c9009ab907579dc1750d8bd203d34f67)) - Ku6epXBOCTuK
- add position9 field component - ([f24ccf9](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f24ccf924031c7384b22c83087401fbdfbfc4125)) - Ku6epXBOCTuK
- add offset field component - ([0c28925](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/0c28925673facc40500c3c5e8e63d94db617f94b)) - Ku6epXBOCTuK
- add two colors field control - ([19f2e13](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/19f2e13b8874b60bb0d34d2d0ce8a575bfd9fe5c)) - Ku6epXBOCTuK
- update use dimension tools - ([f286044](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f286044ab92773af36b712d847b4eccc1f31d81b)) - Ku6epXBOCTuK
- add dimension tools for new flow - ([2601aa5](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/2601aa5b5c9e6d5c20a4df6d1eab2a0a68126f60)) - Ku6epXBOCTuK
- add registry tools for new preview flow - ([1e9b354](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/1e9b35406d8e3fd4071ba72d1dc6a043a0bf318f)) - Ku6epXBOCTuK
- double executor to use with preview - ([520a1e1](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/520a1e1d37f4b63692ca906c06bdf694e297f0ca)) - Ku6epXBOCTuK
- add new registry for preview design - ([a19d3db](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/a19d3db929f5d840900f1190c9efd10bb0595548)) - Ku6epXBOCTuK
- add single tool schema rendering - ([1c74629](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/1c746299c9780a237cff2197c8b6b46310bc49d1)) - Ku6epXBOCTuK
- add new registry schema for new ui - ([1ae4b36](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/1ae4b36c9c2b0268a39486c935220cad9290cfb7)) - Ku6epXBOCTuK
- css audit fixes - ([dbeffac](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/dbeffac082f56990fa1d08089b00e79da28b276c)) - Ku6epXBOCTuK
- WIP: add css fixes - ([2ef4709](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/2ef47090387fc47bd6c93365c7471532402e514d)) - Ku6epXBOCTuK
- add refs css audit - ([1831c3d](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/1831c3d10051510842fbf1a591c95d39cabf3ce4)) - Ku6epXBOCTuK
- fix tools pages, dom audit fix - ([9ef6ae0](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/9ef6ae03c69a8b343bc8f05dac832876b9ea184e)) - Ku6epXBOCTuK
- add preview catalog and catalog group and header - ([b695f5d](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/b695f5d58c2e78f7d076d175ae819811b874cfe1)) - Ku6epXBOCTuK
- fix list tools page to correspond refs - ([7578a18](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/7578a185b64e48c970e27c18c580b4055ba5c23b)) - Ku6epXBOCTuK
- fix demo page to correspond refs - ([d29ec73](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/d29ec73322d6caf3d7ebff3305b2cf20cb0f0fbb)) - Ku6epXBOCTuK
- add new simple components - ([c0f48ec](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/c0f48ecd9762f0c507933409525f5e22bf759a76)) - Ku6epXBOCTuK
- some refactor, fix some design - ([65f12c9](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/65f12c931baec26732a6fc3f39284bd423a3af0b)) - Ku6epXBOCTuK
- add link to preview design - ([40eee98](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/40eee984daeb2d82af375ff322c66a6c0e7ca869)) - Ku6epXBOCTuK
- update tool page - add step card steps - ([d373cc0](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/d373cc0daf8672c1febb3c3a844cb62714ff1dc7)) - Ku6epXBOCTuK
- add dropzone, update tool page - ([a16a7b9](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/a16a7b92c737273b27fd2611a77c65b746d63231)) - Ku6epXBOCTuK
- add single tool pages, wiring with toolcard - ([f990ad6](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f990ad67671662637acc6b890145a2d2b0109bf0)) - Ku6epXBOCTuK
- separate topbar, fix important styles - ([f4216e6](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f4216e6868f975e511395c729359109a9a001a5c)) - Ku6epXBOCTuK
- add demo page - preselected pipeline - ([adcc255](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/adcc2551073d0a57517b655239100d807e643fe7)) - Ku6epXBOCTuK
- add list tools page - ([73118ea](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/73118ea393c60d69108be3a08d72608c937465b5)) - Ku6epXBOCTuK
- add main page - ([e9578fc](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/e9578fc15e179b9ae5df3c0f9e1690f95b113a26)) - Ku6epXBOCTuK
- add ui kit page - ([06d185a](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/06d185add66864662b0a173183f69c22078dc098)) - Ku6epXBOCTuK
- add ui components - ([8437c95](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/8437c95a28343d6b24971d41776b8fd65e831810)) - Ku6epXBOCTuK
- add ui components - ([ac75665](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/ac75665deb78d9b9459e4c34f4a9f9719a027c63)) - Ku6epXBOCTuK
- add new design primitives - ([0acfa3a](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/0acfa3aeb923fe7cbe95846055d2e199a62257e4)) - Ku6epXBOCTuK
- add new design2.css, add fonts - ([418a091](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/418a091505d80e4b2798a80740906d2d215c2488)) - Ku6epXBOCTuK
- add compression tools - ([83d3ce1](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/83d3ce11986c908ea64c54c23bbfba0395cc838d)) - Ku6epXBOCTuK
- add quantize tools - ([501d2dd](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/501d2dda4e186b2f7ece3f1be41b552393ca18a3)) - Ku6epXBOCTuK
- add text and generation tools - ([2b67414](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/2b67414e79ef646b78bf6372d35e2262099d65a0)) - Ku6epXBOCTuK
- add pixel effects core tools - ([56ebe5b](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/56ebe5b53be2bd797911faf285b5fe7b414358a1)) - Ku6epXBOCTuK
- add geometry masks - ([84cc56c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/84cc56cc6cf6627382d4f76f464c787a90181308)) - Ku6epXBOCTuK
- add shape masks - ([d65adcd](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/d65adcd16cb9bbd888b356784b46489e611f6ace)) - Ku6epXBOCTuK
- add masks core tools - ([553601a](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/553601acc1ec4bb5a5fd74b50e60afb09070dd5d)) - Ku6epXBOCTuK
- add channel tools - ([5c37eb2](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/5c37eb2e7b6bc41a219550e8be237697266ce5a1)) - Ku6epXBOCTuK
- add pallette tools - ([db02cbd](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/db02cbd16558a367d328184b96f570ec692e5d67)) - Ku6epXBOCTuK
- add watermarks and text tools - ([116d7c7](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/116d7c72aac7ba2463f249c962251ee9d4a56744)) - Ku6epXBOCTuK
- add text and date draw tools - ([54af762](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/54af7626d6615c379e841e38e528fc9572d11196)) - Ku6epXBOCTuK
- add text draw core - ([38e7cd2](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/38e7cd20d28e7fc26d3cbfb3ec960a2b136d1202)) - Ku6epXBOCTuK
- add demo page - ([6e0647d](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6e0647dfbf9eb68d980ae8d76d6affdc9f1965d6)) - Ku6epXBOCTuK
- inline tool page - parameters beetween source and result images - ([148e4b1](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/148e4b195739774c5e892dbfd16f5a83617b93dc)) - Ku6epXBOCTuK
- add theme switcher, dark theme - ([f1f5bd4](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f1f5bd4409797cb6c28fdee55e3dbd73ac6ce5f0)) - Ku6epXBOCTuK
- add base path for deploy - ([6d770dc](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6d770dc642cd6110bb4ab3d7af9c9364bb29d0bb)) - Ku6epXBOCTuK
- search use both languages and remove diacrites - ([c04a1fc](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/c04a1fc1dc4d266f47164e735668a53e14c2d61b)) - Ku6epXBOCTuK
- use i18n in components - ([3960368](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/396036802c1ee1e8fe52877826b22b9a3a427bee)) - Ku6epXBOCTuK
- base english language, also fallback to english - ([74a18d9](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/74a18d91a77f3a049683f73122279eb9392a61df)) - Ku6epXBOCTuK
- move all common messages to i18n (ru\en) - ([2301b0e](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/2301b0e1654874744d2ddf72b878997aa461620b)) - Ku6epXBOCTuK
- add i18n - dict, en\ru, state - ([8255d33](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/8255d330988f9f6f98c51407e6e98613f7306150)) - Ku6epXBOCTuK
- add tool registry, icons - ([8745812](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/8745812a5f077f715b003ca4022d20a72eb783ba)) - Ku6epXBOCTuK
- add io extensions - svg, jpeg - ([0909e14](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/0909e14cde8ba6ab7249e54bc84bdc4a095cbf8d)) - Ku6epXBOCTuK
- add core tools - transfor, light, effects - ([7ef4b90](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/7ef4b908e2f314e776ffd10f904085ac2eb4f3ea)) - Ku6epXBOCTuK
- add tools into registry, add icons - ([a2dd288](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/a2dd288851572b9634664f647e5c76a7f8184869)) - Ku6epXBOCTuK
- add alpha and morphology core tools - ([cb5ccf6](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/cb5ccf6da8d3f21590e481a617507a9f899090e8)) - Ku6epXBOCTuK
- add tool search cards - ([5e44a45](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/5e44a454571e99a33d019f5eacfaed840a00c8c5)) - Ku6epXBOCTuK
- add tool popularity and icons - ([76f1770](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/76f1770286258097000ee18a9d11adc6b55ae08c)) - Ku6epXBOCTuK
- combine tool in single card - ([702b962](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/702b96261d0266fd904fa740f5a4bdab12ffa24e)) - Ku6epXBOCTuK
- move tool executor to web worker - ([9fbb90b](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/9fbb90b6418641154c40017ca03d7fd3bcec1452)) - Ku6epXBOCTuK
- add registry - background, blur, sharpen - ([c143011](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/c14301130f23e9a4787f13d26b0bbf1a14bd084e)) - Ku6epXBOCTuK
- add background and convolution core tools - ([fd5236c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/fd5236c19b5a6daba78d8780d3c217e92d33f74f)) - Ku6epXBOCTuK
- restore old toolchain button - ([91b3492](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/91b3492231974d3b67b694a313c1f91dcf5b545d)) - Ku6epXBOCTuK
- add chain tools workflow - ([306c26a](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/306c26a3602d64a6a24ed938f20c04882f4637c4)) - Ku6epXBOCTuK
- add tool search component, add chainable flag - ([c93a8e4](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/c93a8e493ef8464d1b1ee96869b6f99ddc0a583a)) - Ku6epXBOCTuK
- add step picker\list\editor - ([8532eb9](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/8532eb98c3f985ed4ee9ba0c7a51eabe0f86dc3c)) - Ku6epXBOCTuK
- add pipeline for multi instrument flow - ([23c985b](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/23c985bde90cdbc24af5f1c137c0153e98658408)) - Ku6epXBOCTuK
- add registry - ([6fae98b](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6fae98b25c9db23f27effdd6c73914cad5a9a647)) - Ku6epXBOCTuK
- add core operations - ([f9a4973](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f9a4973b471b19f2aeaa8b71bf56bff2fd4de18d)) - Ku6epXBOCTuK
- add core operations - ([7fa2c52](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/7fa2c52bb83538898398460de5ed520a26372c02)) - Ku6epXBOCTuK
- add text\hex conver tools - ([17b0c68](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/17b0c68eb52cf8e4e495bd01754ba8e7d3130202)) - Ku6epXBOCTuK
- add color picker glass - ([96c37aa](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/96c37aabc568896f7a279069d0c65e6ff4f794bb)) - Ku6epXBOCTuK
- add color picker - ([7b79996](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/7b799969c4bc0229de490921f29a15b84387ee21)) - Ku6epXBOCTuK
- add slider field - ([3701fc8](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/3701fc80451f94989d412a954715b1ffe9333836)) - Ku6epXBOCTuK
- add show mask workflow - ([aa5372b](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/aa5372b771ef0834c2221f045c27cdd4570a1c41)) - Ku6epXBOCTuK
- reactive tool flow, with debounce - ([d0ee4ec](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/d0ee4ec77f7999655e55c79df50b98a68d3fab9a)) - Ku6epXBOCTuK
- update drop file flow - ([caf71ec](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/caf71ec8f0418ebe43d9e04b007e9c3379864afd)) - Ku6epXBOCTuK
- add ctrl+v flow - ([f576701](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f576701baabbd0172550efe18c422650e0d15ee1)) - Ku6epXBOCTuK
- update - use design system components - ([c7b56bb](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/c7b56bbda79b67d4e13c5062c55266c7b6de2c5c)) - Ku6epXBOCTuK
- add design system components - ([15e2e01](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/15e2e010ec3227dae0786ebf81afe71ed0c8b011)) - Ku6epXBOCTuK
- add design system tokens - ([3620c5f](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/3620c5fc309e840eea5ac6927534b50c2957928f)) - Ku6epXBOCTuK
- add route pages - ([3ec1199](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/3ec11999d82c118b72416d11fd0aa6b2698359f6)) - Ku6epXBOCTuK
- add svelte components - ([c022555](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/c0225552e4d5970b81a3dba51e357821da38977b)) - Ku6epXBOCTuK
- add instruments registry - ([719bda2](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/719bda2e225331d04da35b7a35fb980b2af63f00)) - Ku6epXBOCTuK
- core instruments - ([9ccc9f9](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/9ccc9f94acb5cc9d6f5ab1c13a7b3641160e3edd)) - Ku6epXBOCTuK
- core types and io - ([4ec2b41](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/4ec2b418fd001bf0c0d95534cd38988b6a44cd1d)) - Ku6epXBOCTuK
- initial sveltekit - ([85f595f](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/85f595f14b9cd059723d4ae84265b6a6e1633a67)) - Ku6epXBOCTuK
#### Bug Fixes
- show version in footer from manifest file - ([f89811c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f89811c116fbff42d0323ed811104bdce06dc68d)) - Ku6epXBOCTuK
- update crop tool max sizes - 2000x1200 - ([becf03f](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/becf03f4165e744e05b4e39d906b72a4804f6094)) - Ku6epXBOCTuK
- use base path for favicon - ([44cfca9](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/44cfca92ce2eb2cef5119fb3701719f78d97f579)) - Ku6epXBOCTuK
- worker use only i18n keys instead of full i18n - ([c2f3838](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/c2f3838485a78b12246a480cda564feb5a19255e)) - Ku6epXBOCTuK
- update i18n - update registry for tools - ([371387c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/371387ce81569921f5e143daec81275b63a1bab4)) - Ku6epXBOCTuK
- update i18n - update schema fields descriptions - ([08c9fbc](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/08c9fbce3b21a7530bba12d8cdaa77f4286e69ae)) - Ku6epXBOCTuK
- update i18n - tool group descriptions - ([4de42b1](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/4de42b1e402a17f43992b132d8d3d97b869fc0d9)) - Ku6epXBOCTuK
- update i18n - text verdicts from analyze tools - ([372babc](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/372babc3b741e0fd5e10709b4b6e3e70589b6074)) - Ku6epXBOCTuK
- update i18n - fields components - ([4b44bc4](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/4b44bc439ce2a36baf1c3f68fde8776d31ccb3f1)) - Ku6epXBOCTuK
- update i18n - schema components - ([6c88a23](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6c88a23e4882bebd20e9bd7cc45c70f15818d5cb)) - Ku6epXBOCTuK
- update i18n - catalog, nav, common components - ([a4c85c0](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/a4c85c08140bd802bfb0dc2952ea475d4dca48f5)) - Ku6epXBOCTuK
- update i18n text in pages and layout - ([e069709](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/e06970997e5a402139d2f2b9c78cab8310cad293)) - Ku6epXBOCTuK
- update lang toggle component - ([1097186](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/10971864872fb305bbcad4e6470b7afac8fa1312)) - Ku6epXBOCTuK
- restart tool after change source - ([5cd2a58](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/5cd2a586ce70116730cc9ce11653943194992454)) - Ku6epXBOCTuK
- update design - colors, remove texts - ([f2829de](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f2829de216f48f0d2bc5b35c0a1bf34a51ad10e8)) - Ku6epXBOCTuK
- disable download button while running jobs, auto run generator jobs - ([0c2de3f](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/0c2de3f191cb9cfd10c4186cd16db7db2f8545bf)) - Ku6epXBOCTuK
- separate preview tile component - ([35df076](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/35df07673211f2734c4f52ab4e232580006efc9c)) - Ku6epXBOCTuK
- source image checker background - ([0b107c7](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/0b107c7d93a8dd27b8ad40c2c73a70518a45056f)) - Ku6epXBOCTuK
- update topbar links - ([f7623ab](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f7623ab3bbc3d3998938e918e963219039f97f88)) - Ku6epXBOCTuK
- v1 layout - ([85effce](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/85effce7d9ae940858fda6ed3f4b8336799ac93c)) - Ku6epXBOCTuK
- css fixes - ([420d03f](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/420d03f58c3ed26f0dd5658cdc9391f881847ba1)) - Ku6epXBOCTuK
- lint errors - ([6f1771c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6f1771cb07da8b43b3ef79c8ebb4033f429b491c)) - Ku6epXBOCTuK
- update tool card component - route resolve issue - ([987f453](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/987f4535c8609e25ae27db192ba598683604803f)) - Ku6epXBOCTuK
- resolve always text input issue, separate to simple components - ([a1438e6](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/a1438e6f7f227f7489465070c02abdbb495a9dd1)) - Ku6epXBOCTuK
- add compress\decrease colors presets - ([37a4a05](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/37a4a0576c505ee3a87c0886e92faa9521092097)) - Ku6epXBOCTuK
- update tools list - show all tools - ([d1a386f](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/d1a386f92f2e9e7fa859cef950ca68d5f3729194)) - Ku6epXBOCTuK
- update segmented component styles - ([a24a182](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/a24a182d1813f9bc882d3dff2cbf58a0396c799c)) - Ku6epXBOCTuK
- increase font size - ([1e63eaa](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/1e63eaa03bc7d4c6ee21eb88cd0dff4cb54b4922)) - Ku6epXBOCTuK
- update field component - ([3d0f8eb](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/3d0f8ebb100294b970e9b45a854126ca031df9d7)) - Ku6epXBOCTuK
- update download button style - ([ef74cab](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/ef74cabf1e8bea77e88e0e29c802e02ee57eaf3d)) - Ku6epXBOCTuK
- update button component styles - ([c590bdf](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/c590bdf91a896fab0fd8a3e347a67db639be26c7)) - Ku6epXBOCTuK
- update css variables, update badge component - ([4794523](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/47945230846bacc4063bf8fc21620fefe57f3099)) - Ku6epXBOCTuK
- update some styles - ([191ca71](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/191ca71553187b45efb6bc26fb278a1e10b93aeb)) - Ku6epXBOCTuK
- change style, demo page visual looks as done - ([749b97c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/749b97cb4f55b145067bc169da8d4d08848ee4e3)) - Ku6epXBOCTuK
- resolve new audit styles - ([80def1f](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/80def1f38043b3fdd081fa8c8b3558c6c6f01ce5)) - Ku6epXBOCTuK
- update font styles - ([0b13d4c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/0b13d4c9e829e5a0c1d2e8be210ae9ba659bbe39)) - Ku6epXBOCTuK
- update style, resolve old css leaks issue - ([586c66a](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/586c66a159a9a4e30ae414070ffa83f06d9209ed)) - Ku6epXBOCTuK
- resolve style issues - ([bbd5532](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/bbd5532529257262faa133d4cd5e7dee0f943927)) - Ku6epXBOCTuK
- step fields styles - ([8eff9a9](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/8eff9a94836fa2a6219888a5599a3fa011699d9b)) - Ku6epXBOCTuK
- update styles - ([2c70c44](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/2c70c445d5ec601945b842663cfbc81222fdc91c)) - Ku6epXBOCTuK
- update preview styles - ([6c8f7b2](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6c8f7b2d8b1eaca7f0cee708f0f1e81d0d59e25e)) - Ku6epXBOCTuK
- update top bar styles - ([86b1c81](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/86b1c813e72bc07f7f07e534b54faa4f6d69e142)) - Ku6epXBOCTuK
- resolve design issues from refs audit - ([0d95acb](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/0d95acba8933280b32e54a3f13d014aa464a2e71)) - Ku6epXBOCTuK
- remove tile label doubling - ([6959abb](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6959abb15926edab7817006a42cfabfa436c04e8)) - Ku6epXBOCTuK
- update design with references - ([5093b50](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/5093b50f5cfea72f6298e9dd5a2ae934e741b75a)) - Ku6epXBOCTuK
- update search flow - first tool search - ([3c1c5b2](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/3c1c5b2b3ae6fe8bdf576a70acb6b449edaf0197)) - Ku6epXBOCTuK
- some small css fixes - ([5e512f0](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/5e512f0bdd34f6329b54f111f81950acbebb2569)) - Ku6epXBOCTuK
- get names from categories - ([96b8c00](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/96b8c0012988e4586718a11a77419b7752edeb80)) - Ku6epXBOCTuK
- base en - ([6787201](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6787201425b87256369003b97df299704ed75975)) - Ku6epXBOCTuK
- resolve transform core tools, add tests - ([ebc3c33](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/ebc3c33c12b58be7413378ff6e44677cf76f1a64)) - Ku6epXBOCTuK
- use toolcard as links at list page - ([200e971](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/200e971df503e6ba6a460bdd24f47cd617dbbc7c)) - Ku6epXBOCTuK
- update layout - ([4c5b52a](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/4c5b52a9ad13aadd425e9c5571120f356bf631be)) - Ku6epXBOCTuK
- update user input flow (keep aspect ratio) - ([da55328](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/da5532849198d3040a56f71a69fe6af6ae5b988f)) - Ku6epXBOCTuK
- resolve loading image error - ([4b70866](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/4b7086693cfd06ab88d47f22314913aa9326a470)) - Ku6epXBOCTuK
#### Refactoring
- separate components - ([c4eab38](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/c4eab388662b927aa5a6527be75350686e903865)) - Ku6epXBOCTuK
- add slug to tools - ([931ee27](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/931ee27b056a641b0adec58d0016347cd6afbc18)) - Ku6epXBOCTuK
- separate schema preview logic - ([6ab5989](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6ab59897d10dd391c549a3c1e6d9df6fc7623288)) - Ku6epXBOCTuK
- separate schema fields logic - ([cdb23fe](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/cdb23fe1c113f5bcc41228b9508e68a65a2746b6)) - Ku6epXBOCTuK
- executor protocol, fallback, debounce - ([0933f1c](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/0933f1cdeab3998de628eb1a5443399304e088ae)) - Ku6epXBOCTuK
- change v1 to import own copy of i18n - ([939038f](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/939038ffc02fc618c5e53a8589a803417149f327)) - Ku6epXBOCTuK
- update path registry-new to registry - ([75f6891](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/75f6891e7c4285b4a63515b426f88f3fe87bb6ab)) - Ku6epXBOCTuK
- set root layout, rename css, fix links in lint rules \ etc - ([e970b47](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/e970b477205ea02dc96c67dac19bbc36660aab85)) - Ku6epXBOCTuK
- move old libs and components to old folder - ([3834dad](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/3834dadd03f2946fa679e4cfb737d7cb486e588b)) - Ku6epXBOCTuK
- update css vars lint rules and fix them - ([e3d50f2](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/e3d50f2128e5f27c834455f5c11de0dd6364d9fe)) - Ku6epXBOCTuK
- fix lint css vars rules - ([2d24f51](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/2d24f51fd5bf94408f1ec515575fc700bfd74ae3)) - Ku6epXBOCTuK
- isolate old and preview versions, add lint rule - ([e57fadd](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/e57faddfdef9e75312b5a81df56e6450fb82bff4)) - Ku6epXBOCTuK
- update field schema types - ([743ae81](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/743ae817ee47b8bf2826ee9876e046babd7d811f)) - Ku6epXBOCTuK
- separate schema fields components - ([4a13bfb](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/4a13bfbd1684d095d10f9403bde88359174456c7)) - Ku6epXBOCTuK
- separate registry file - ([58e75f2](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/58e75f23345b956d4068ff16ef90805031eae1c8)) - Ku6epXBOCTuK
- move form fields to separate folder - ([6ec3e75](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/6ec3e75df4dc22728a234dab77c4bf69316ce9c1)) - Ku6epXBOCTuK
- move components - ([e9de7a4](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/e9de7a4bf8244a278bb44e8eb84c35db7ee38965)) - Ku6epXBOCTuK
- update panel component - ([7e94205](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/7e94205866f8c9d25ebb548f3c3fa87f42557e25)) - Ku6epXBOCTuK
- update panel component - ([2f758a8](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/2f758a88af234fc2ceefca024d6abfdd9ee14608)) - Ku6epXBOCTuK
- separate top bar component - ([991fe69](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/991fe69381e7018bb977991fb6ce2ba99c87c53d)) - Ku6epXBOCTuK
- update footer component - ([11c4cce](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/11c4ccece436aae2081c426d3e31fb272a464904)) - Ku6epXBOCTuK
- icon and download button use button component - ([8a6f879](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/8a6f87938d9179728e55642119ab575ff9891b93)) - Ku6epXBOCTuK
- update pages and refs audit - ([86e7b72](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/86e7b7250d5a35eabdd9c9a463ec7999fb7f641a)) - Ku6epXBOCTuK
- move old design to separate layout tree - ([f08af41](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/f08af41568f9694543703fdcc936118d8f20e342)) - Ku6epXBOCTuK
- separate tool page for A\B testing - ([d70cbad](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/d70cbad0d2d804386b2d5d53673553bf65ca323c)) - Ku6epXBOCTuK
- add error messages i18n - ([486b99d](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/486b99dbb8ec9383de36d2e6cd88c9fec08c7434)) - Ku6epXBOCTuK
- use standalone tool run executor - ([1b3bad4](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/1b3bad46d1be4a04311f46400c078542026315ff)) - Ku6epXBOCTuK
- change show mask flow - checkbox in params - ([3a5acfb](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/3a5acfbc452a2a2fc350dc4e803b0846473779c2)) - Ku6epXBOCTuK
- separate tool page - ([5560435](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/5560435b55cb232b978f4c20a813f3d964e66b79)) - Ku6epXBOCTuK
- dropzone update - ([7afd07e](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/7afd07e8c148a317456ee864685258e40cab484d)) - Ku6epXBOCTuK
- remove unused styles - ([a9234d2](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/a9234d27de7d19f3a0489ebe1da1d310371a4287)) - Ku6epXBOCTuK
#### Style
- fix formatting - ([3c10147](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/3c10147ad85650ed9e9f63850fc9581400e2d427)) - Ku6epXBOCTuK
- fix prettier ignore\config - ([9dccdd9](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/9dccdd99a0f7b6ef187627cf8ec4811b648bdd22)) - Ku6epXBOCTuK
- format docs markdown - ([64e35f4](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/64e35f4fdfedf331e79309c49ddab936b2a263da)) - Ku6epXBOCTuK
- fix formatting via prettier - ([ada96b3](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/ada96b3534a20a2c5c671cef3ca0146ada743afa)) - Ku6epXBOCTuK
- - -
Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto).
+9 -8
View File
@@ -12,16 +12,16 @@ and open source.
transparency/alpha, color, channel decomposition, geometry, filters,
pixel-property masks, generation, text/watermarks, analysis & verdicts — the
current list lives in [`docs/tools-map.md`](docs/tools-map.md)
- **Pipeline system (workspace)** — load image → chain multiple tool steps →
sequential application → download result; pipelines saved to localStorage,
exportable as JSON
- **Web Worker execution** — heavy operations run off the main thread with
automatic fallback to direct calls
- **i18n** — full Russian and English localization
- **Dark/Light themes** — persisted to localStorage
- **Pure TypeScript core** — `lib/core/` operates on
`ImageData`/`Uint8ClampedArray` with no DOM dependency
- **Fully static** — no backend, no analytics, no tracking
- **Fully static** — no backend; the image never leaves your browser. The only
third-party request is a Google Analytics page-view counter — it never sees
the file or its contents; the boundary and its limits are in
`docs/plan-seo.md` §0
## Tech Stack
@@ -65,8 +65,9 @@ pnpm verify # full fast quality gate — the pre-review gate
pnpm build # static production export → web/build/
```
Полный список команд и матрица «какие проверки запускать при каком изменении» —
в `AGENTS.md` и `docs/quality-gates.md`.
For the full command list and the "which checks to run for which change" matrix,
see [`AGENTS.md`](AGENTS.md) and
[`docs/quality-gates.md`](docs/quality-gates.md).
## Architecture
@@ -74,8 +75,8 @@ Everything is built around the **two-layer registry** (`web/src/lib/registry/`):
a tool is a self-contained implementation `{id, schema, input, result, run}`
(`registry/tools/`), and a page is `{slug, title, description, category, steps}`
(`registry/pages/`) that names the address and the tools to run — one tool can
back several pages. Pages, forms, and pipelines are generated from it, and all
execution goes through a single `executor.execute` entry point.
back several pages. Pages and forms are generated from it, and all execution
goes through a single `executor.execute` entry point.
Core image processing lives in `web/src/lib/core/` and operates directly on
`ImageData`, with no browser API dependencies.
+19
View File
@@ -0,0 +1,19 @@
branch_whitelist = ["main"]
ignore_merge_commits = true
tag_prefix = "v"
pre_bump_hooks = ["node scripts/sync-version.mjs {{version}}"]
[changelog]
path = "CHANGELOG.md"
template = "remote"
remote = "github.com"
repository = "easy-png-tools"
owner = "Ku6epXBOCTuK"
[commit_types]
build = { omit_from_changelog = true }
chore = { omit_from_changelog = true }
ci = { omit_from_changelog = true }
docs = { omit_from_changelog = true }
test = { omit_from_changelog = true }
+4 -3
View File
@@ -15,6 +15,7 @@
| Документ | Когда читать | Статус |
| ---------------------------------- | -------------------------------------------------- | ------ |
| `AGENTS.md` | Всегда: контракт агента | active |
| `docs/README.md` | Индекс документов, правила ведения документации | active |
| `docs/architecture.md` | Слои, поток данных, новый инструмент/поле/маршрут | active |
| `docs/quality-gates.md` | Какие проверки запускать, пороги, coverage, CI | active |
| `docs/decisions.md` | Правка самих гейтов; чтение CI после пуша | active |
@@ -60,9 +61,9 @@
`web/src/lib/registry/tools/`, адрес и имя страницы (`slug`) — в
`web/src/lib/registry/pages/`; исторические названия в старых планах не
действуют (`docs/architecture.md`, раздел 3).
- Поиск по `docs/` задевает `docs/archive/` (объём больше, чем у активных
документов вместе). Для правок production-кода архив не нужен: grep по
`docs/*.md` без `archive/`.
- Поиск по `docs/` задевает `docs/archive/`: там вынесены завершённые планы и
исследования, поэтому grep по `docs/` даёт больше шума, чем пользы. Для правок
production-кода архив не нужен: grep по `docs/*.md` без `archive/`.
- Проверка ссылок в документации не автоматизирована; ожидаемо неразрешимыми
считаются ссылки на `docs/plan-domain.local.md` (файл вне git). Всё остальное
неразрешимое — ошибка.
+8 -3
View File
@@ -12,7 +12,10 @@
4. Определить границу изменения: production UI, shared-модуль или инфраструктура
— `AGENTS.md`, раздел «Границы изменений». Изменение shared-кода требует
проверки production-потребителей.
5. Не добавлять исключения линтера ради отдельных модулей без записи в baseline.
5. Не добавлять исключения линтера ради отдельных модулей: осознанные
отступления живут в конфиге правила (`allowPaths`, `allowWords`,
`ignoreMissingPatterns`, `allowDefaultProject` —
`web/eslint-plugins/README.md`), а не в самих проверяемых файлах.
## 2. Порядок изменения
@@ -27,8 +30,10 @@
warnings и чтение CI — в `docs/quality-gates.md`. Здесь только то, что относится
к работе агента.
`lint:all` может завершиться успешно при warnings. Это не означает чистый
baseline: нужно отдельно показать число errors, warnings и token warnings.
`lint:all` роняет gate на любых ESLint warnings (`--max-warnings=0`).
Единственное, что он пропускает, — unused-token сообщения (неблокирующие).
Поэтому «команда прошла» не равно «нечего чинить»: нужно отдельно показать число
errors, warnings и token warnings.
E2E запускается отдельно от быстрых проверок, потому что поднимает build и
статический сервер. Для UI-изменений сначала выбирается минимальный
+61 -35
View File
@@ -16,10 +16,13 @@ web/src/lib/
catalog.ts # группировка страниц и счётчики для /list-tools
categories.ts # id/названия категорий
tool-icons.ts # slug страницы → иконка
core/ # чистый TS: операции над ImageData, без DOM
core/ # операции над ImageData; без DOM, кроме io.ts/domText.ts
executor/ # единственная точка исполнения инструментов
i18n/ # словари ru/en, t(), списки для i18n-правил линтера
components/ # UI (fields/, layout/, ui/, schema-компоненты)
i18n/ # словари ru/en, t(), состояние локали
components/ # UI по слоям: ui/ примитивы, layout/ каркас,
# catalog/ каталог, schema/ view инструмента,
# fields/ поля (+ fields/schema/ контролы схемы),
# display/ типографика и карточки, feedback/ состояния
zip.ts # сборка мультифайлового результата (1 → many)
theme.svelte.ts # light/dark + localStorage
web/src/routes/
@@ -30,9 +33,15 @@ web/src/routes/
```
Правило зависимостей: `components/**` и `routes/**` знают про `registry/**`,
`executor/**` и `core/**`; `core/**` не знает ни о ком и не зависит от DOM;
`registry/**` описывает данные и знает только `core/**` и `registry-schema.ts`.
Нарушение этого порядка — повод не заводить импорт, а инвертировать зависимость.
`executor/**` и `core/**`; `core/**` не знает ни о ком; `registry/**` описывает
данные и знает только `core/**` и `registry-schema.ts`. Нарушение этого порядка
— повод не заводить импорт, а инвертировать зависимость.
Исключение из «core без DOM» — `core/io.ts` (декодирование файла, кодирование,
скачивание) и `core/domText.ts` (canvas-рендер текста и эмодзи). Они трогают
`document`, `createImageBitmap` и `HTMLCanvasElement`, поэтому не работают в
node-Vitest и исключены из coverage-гейта (`docs/quality-gates.md`, раздел
Coverage). Всё остальное в `core/**` — чистые функции над `ImageData`.
### Чего в коде нет
@@ -43,11 +52,14 @@ web/src/routes/
него нет. Модель страницы уже хранит список `steps`, но исполняет первый шаг,
а `registry.test.ts` требует, чтобы шаг был ровно один: композиция формы из
полей двух инструментов — отдельная работа, описанная как фаза 11
`docs/roadmap.md`. Под неё зарезервированы `components/StepCard.svelte`
(сейчас импортируется только витриной `/kit`) и ключи `chain.*`,
`errors.badPipelineShape`, `errors.noSteps`.
- **Разделения страниц, которые делят один инструмент** — сводит 14
convert-страниц на один инструмент; задача в `docs/backlog.md`.
`docs/roadmap.md`. Под неё зарезервированы
`components/display/StepCard.svelte` (сейчас импортируется только витриной
`/kit`) и ключи `chain.*`, `errors.badPipelineShape`,
`errors.pipelineVersion`, `errors.noSteps`.
- **Разделения страниц, которые делят один инструмент** — сводит все
convert-страницы на один инструмент; задача в `docs/backlog.md`. Пока
`registry.test.ts` не требует, чтобы на инструмент ссылались две страницы, а
на практике не ссылается ни одна.
Задачи по обоим пунктам — в `docs/backlog.md`; архитектурные решения по
пайплайну — `docs/roadmap.md`.
@@ -67,7 +79,7 @@ registry/{tools,pages}/*.ts
├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол)
├─► SchemaSourceTile/SchemaTextSource.svelte вход
└─► SchemaPreview + schema-preview-model.ts вид превью
│ buildSchemaPreviewModel(inputMode, resultKind, …) → source/result/hasResult
│ buildSchemaPreviewModel(input) → { sourceValue, resultValue, formatValue, hasResult }
▼
executor.execute(ctx) единственная точка исполнения
│ сериализация параметров → worker → worker-handler → tool.run(ctx)
@@ -104,9 +116,9 @@ registry/{tools,pages}/*.ts
| `id` | внутреннее имя инструмента, уникально; ключ `tools.<id>.*` и `toolId` в worker |
| `schema` | `toolSchema({ fields, layout?, meta? })` |
| `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе |
| `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт |
| `domOnly` | инструмент требует DOM (canvas); превью-executor запускает его напрямую |
| `output` | mime/ext/quality для скачивания (не у всех) |
| `result`? | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт |
| `domOnly`? | инструмент требует DOM (canvas); превью-executor запускает его напрямую |
| `output`? | mime/ext/quality для скачивания (не у всех) |
| `run(ctx)` | единственный метод: `ctx` → `ToolResult` |
`Page` (записи в `registry/pages/<group>.ts`):
@@ -121,15 +133,22 @@ registry/{tools,pages}/*.ts
Кто по чему адресуется: `slug` — маршрут, имя файла, `TOOL_ICONS`, поиск по
каталогу; `id` — ключ `tools.<id>.*` (подписи полей, `options`, `results`),
`toolId` worker-протокола и запись пайплайна. Один инструмент может обслуживать
несколько страниц, одна страница — состоять из нескольких инструментов.
`toolId` worker-протокола и запись пайплайна. Модель допускает, что один
инструмент обслуживает несколько страниц, а страница состоит из нескольких
инструментов; на практике пока каждая страница ссылается на свой инструмент, а
исполняется первый (и единственный) шаг.
Инструмент работает с любым поддержанным форматом, поэтому png-интент живёт
только в `slug`: `id: "crop"` → страница `slug: "crop-png"` → `crop-png.png`.
Правило имени: снять суффикс `-png` и в середине имени (`png-to-base64` →
`to-base64`, `X-to-png` → `from-X`, `verify-is-png` → `verify-png`); следствие —
Правило имени `id`: снять суффикс `-png` и в середине имени (`png-to-base64` →
`to-base64`, `X-to-png` → `from-X`, `verify-is-png` → `verify-png`). Правило
относится только к `id`: у `slug` своя история и SEO-значение, поэтому
переименование `id` не трогает адреса, а переименование `slug` меняет адрес, имя
файла, ключ `pages.*`, иконку и запись в `docs/tools-map.md`.
файла, ключ `pages.*`, иконку и запись в `docs/tools-map.md`. Слаги, сложившиеся
до разделения реестра, правилу не соответствуют (`convert-png-to-jpg`,
`change-png-opacity`, `png-is-grayscale`, `verify-is-png` и другие) — это не
ошибка, менять их без SEO-решения нельзя. Форму slug проверяет
`registry.test.ts` (только форма, не смысл).
Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых
случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS`
@@ -154,15 +173,17 @@ registry/{tools,pages}/*.ts
допустимому виду. Правка поведения полей идёт здесь, а не в компоненте.
Новый вид поля требует: спека + фабрика `field.*` → запись в `fieldSpecs` →
контрол в `components/fields/schema/` → ключ в `FIELDS` → дефолт и санитизация →
контрол (`components/fields/schema/`) → ключ в `FIELDS` → дефолт и санитизация →
i18n-подписи → тест в `registry-schema.test.ts`.
## 5. Исполнение
`executor/executor.ts` — фабрика `createExecutor(factories?)` плюс готовый
экспорт `execute`. Контракт асинхронный (`Promise`), параметры сериализуются,
исполнение уходит в `executor.worker.ts`; `worker-handler.ts` разбирает
сообщение и зовёт `run`. Если worker недоступен — прямой вызов.
`executor/executor.ts` — фабрика `createExecutor({ workerFactory? })` плюс
готовый экспорт `execute`. Контракт асинхронный (`Promise`), параметры
сериализуются по `executor/protocol.ts`, исполнение уходит в
`executor.worker.ts`; `worker-handler.ts` разбирает сообщение и зовёт `run`.
Если worker недоступен — прямой вызов. UI импортирует `execute` из
`executor/index.ts`.
Отсюда следует: подключение wasm позже означает замену реализации **внутри**
исполнителя, а не правку компонентов.
@@ -170,13 +191,16 @@ i18n-подписи → тест в `registry-schema.test.ts`.
## 6. i18n
Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с
`LOCALES`/`BASE_LOCALE`). Пользовательский текст живёт только в словарях и
читается через `t()`. Секции разделены по слоям: `pages.<slug>.title` и
`pages.<slug>.description` — тексты страницы (в базовой локали секция пустая, EN
берётся из реестра), `tools.<id>.options/params/results` — подписи полей и
тексты вердиктов инструмента, `fields.*`/`groups.*` — общие подписи схемы.
Инварианты паритета ключей, плейсхолдеров и запрет хардкода проверяются
линтером, детали — `web/eslint-plugins/README.md`.
`LOCALES`/`BASE_LOCALE`, `t.ts` с `t()`, `locale.svelte.ts` с состоянием
локали). Пользовательский текст живёт только в словарях и читается через `t()`.
Списки ключей и допустимых атрибутов для i18n-правил линтера лежат не здесь, а в
`web/eslint-plugins/i18n/lists.js`. Секции разделены по слоям:
`pages.<slug>.title` и `pages.<slug>.description` — тексты страницы (в базовой
локали секция пустая, EN берётся из реестра),
`tools.<id>.options/params/results` — подписи полей и тексты вердиктов
инструмента, `fields.*`/`groups.*` — общие подписи схемы. Инварианты паритета
ключей, плейсхолдеров и запрет хардкода проверяются линтером, детали —
`web/eslint-plugins/README.md`.
## 7. Точки расширения
@@ -204,7 +228,7 @@ i18n-подписи → тест в `registry-schema.test.ts`.
`from-base64`, `is-grayscale`).
4. **Описать схему** через `field.*`, задать `layout`, если поля нужно
сгруппировать. Качество кодирования (`quality`) — не поле страницы, а
настройка скачивания (`docs/plan-seo.md` §1, S1f).
настройка скачивания (`docs/plan-seo.md` §4, S1f).
5. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в
`core/`, без DOM.
6. **Добавить страницу** в `registry/pages/<group>.ts`: `slug` с адресным
@@ -221,8 +245,10 @@ i18n-подписи → тест в `registry-schema.test.ts`.
«Реализовано», формат строки — в шапке раздела. Карта перечисляет **`slug`
страниц** из `registry/pages/`, то есть адреса. Проверяет
`web/src/lib/registry/tools-map-doc.test.ts`: slug без страницы и страница
без записи в карте роняют тест. Обоснование и границы автоматизации —
`docs/decisions.md`, раздел 4.
без записи в карте роняют тест; разделы «Можно добавить» и «Идеи»
проверяются в обратную сторону — реализованный slug в плане тоже роняет
тест, поэтому при переезде инструмента из плана в реестр его вынимают
оттуда. Обоснование и границы автоматизации — `docs/decisions.md`, раздел 4.
Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для
инструмента, меняющего пользовательский поток, — e2e (см. раздел 3
+71 -68
View File
@@ -1,36 +1,38 @@
# Backlog
> Статус: **active**. Продуктовые, UX- и инфраструктурные задачи. Здесь только
> **открытые** задачи: выполненные удаляются, а критичные для проекта факты из
> них переносятся в тематический документ (архитектура, quality gates, lint,
> e2e) по мере закрытия.
>
> «Что сознательно не вводим и почему» живёт в `docs/decisions.md`, архитектура
> — в `docs/architecture.md`. Raw-находки UX-аудита — `docs/tools-audit.md`, их
> разбивка на волны — `docs/plan-fields-audit-fixes.md`.
>
> Ids инструментов сверять с `web/src/lib/registry/`; полный список —
> `docs/tools-map.md`.
> Статус: **active**. Открытые задачи. Подробные планы, разбор решений и сырые
> находки — `docs/plan-seo.md`, `docs/plan-platform.md`,
> `docs/plan-fields-audit-fixes.md`, `docs/tools-audit.md`, `docs/decisions.md`.
## lint и прочие правила
- [ ] `tseslint.config(...)` deprecated: сигнатура
`(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray` помечена
устаревшей, нужен переход на `defineConfig` из `typescript-eslint` —
`web/eslint.config.js`.
устаревшей, нужен переход на `defineConfig` из `eslint/config` (ESLint
core, а не `typescript-eslint`) — `web/eslint.config.js`.
- [ ] удалить мёртвые `tools.*.params` из root i18n после проверки потребителей;
до этого действует pattern-based policy-исключение
(`web/eslint-plugins/README.md`, раздел `dict-consistency`)
## Мелочи всякие
Порядок правок полей и раскладки — `docs/plan-fields-audit-fixes.md`, сами
находки («что плохо», с датой аудита) — `docs/tools-audit.md`. Здесь — как это
исправить; находка в аудите не отменяет задачу здесь.
- [ ] Пересмотреть тесты - исправить всякие хрупкие, которые проверяют
количество инструментов, точное совпадение текстов, поиск по
`img[alt="Result image"]`, svelte классам и тому подобные
- [ ] split-into-parts-png - увеличить максимальные значения, плохо работает для
нарезки ассетов
- [ ] crop, resize (но скорее всего все инструменты) - при загрузке нового
изображения сбрасывает размеры, настроенные пользователем
- [ ] split-into-parts-png, resize (может еще какие-то) - возможность указывать
размеры из заранее одобренного списка размеров - 16х16, 32х32, 64х64 как
минимум, может еще какие-то размеры добавить, единый список из предыдущей
задачи - (для инди разработчиков, стандартные тайлы часто делаются)
- [ ] e2e: `i18n.spec.ts` и `text-and-verdicts.spec.ts` дублируют локаторы из
`helpers/page.ts` вместо вызова — правило «спека зовёт helpers»
(`docs/testing-strategy.md`, раздел 5)
- [ ] мёртвый i18n-ключ `noParams` («нет параметров — результат готов»): его
ничем не читают. Либо показывать подсказку для инструментов с пустой
схемой (FA-E1), либо удалить ключ из обеих локалей
- [ ] Пересмотреть тесты — убрать точные совпадения текстов и любые селекторы,
привязанные к разметке или svelte-классам (сейчас остались:
`tool-flows.spec.ts`, `schema-tool-strings.test.ts`)
- [ ] нужен инструмент для удаления фона по выбранной точке - удалять внутри
выделенной области
- [ ] сделать конвертацию в смайлики\награды для твитча (6 размеров или выбор
@@ -63,8 +65,6 @@
строку для svg - форматирование или подсветку синтаксиса??? для bin\hex -
форматирование по колонкам??? для hex\bytes\rgba - пример в placeholder
- [ ] сделать загрузку картинки исходника по ctrl+v и drag-and-drop (dropzone)
- [ ] square-mask-png - проверить работу - параметры неверно считаются. width -
100, height - 100, x - 100, y - 100, обрезает 50% на 50% картинку
- [ ] прикрутить кастомный color picker, добавить прозрачность (range?)
- [ ] проверить настройки range slider на crop-png - слишком большие значения
- [ ] для range slider field - добавить поле для ввода цифр и кнопку сброса
@@ -72,48 +72,43 @@
- [ ] для keep Aspect field - пересчитывать второе поле, учитывать какое было
изменено
- [ ] обновить страницу kit или сделать storybook
- [ ] Свести convert-страницы на один инструмент `convert` — реализация уже
умеет «скопировать пиксели, декодировать вход, отдать в заданном формате»,
а страницы различаются только форматом вывода. **Зависит от S1f**
(`docs/plan-seo.md` §1): пока `quality` живёт в схеме страницы, у
`convert-png-to-bmp` и `convert-png-to-jpg` разные наборы полей, и один
инструмент на них не сойдётся. Качество — настройка кнопки Download
(«Download as JPEG»), а не поле страницы; после его переезда пресеты схемы
не нужны, страницы различаются только `output` и `params` шага. Модель
страницы к этому готова: `Page.steps` — список инструментов, один
инструмент может обслуживать много страниц (`docs/architecture.md`, раздел
3).
- [ ] Свести convert-страницы (`convert-png-to-jpg`, `convert-png-to-webp`,
`png-to-bmp`) на один инструмент `convert` — зависит от S1f
(`docs/plan-seo.md` §4). Мешает не только `quality` (он живёт в схеме
инструмента), а сама модель: `output` с mime/ext — свойство `Tool`, а
`PageStep` умеет только `{ id, params? }`, поэтому страницы на общий `id`
получат одинаковый формат вывода. Нужно научить шаг переопределять формат
вывода
- [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop,
удаление, добавление инструментов. переход один инструмент - pipeline без
перезагрузки
- [ ] `CatalogToolbar`: фильтры используют сырой `<button>`, а не `ui/Button` —
единый контрол для всей библиотеки
(`web/src/lib/components/CatalogToolbar.svelte`).
- [ ] Вынести `ToneVariant`/варианты в общий модуль: `const`-объект с тонами
продублирован в `ui/Badge.svelte` и кнопках
(`web/src/lib/components/ui/Badge.svelte`).
- [ ] `PreviewTile`: canvas должен сам выбирать соотношение сторон в диапазоне
16:9…9:16 вместо фиксированного
(`web/src/lib/components/layout/PreviewTile.svelte`).
- [ ] `ThemeToggle`: свои анимации иконок и своя кнопка вместо `IconButton`
(`web/src/lib/components/ui/ThemeToggle.svelte`).
- [ ] Радиусы бордеров не совпадают с референсом: в `refs` радиусы были 0, а мы
взяли ненулевой `--radius-m` для всех бордеров. Проверить токен против
референсов; возможно он должен быть 0, а скругления оставить только там,
где они реально есть в дизайне.
- [ ] `Footer`: версию брать из `package.json`, а не писать `v0.1.0` вручную
(`web/src/lib/components/layout/Footer.svelte`).
- [ ] `SchemaTextResult` вердикт-бейдж рисует руками: свои классы
`badge`/`badge-tone--{tone}` и свой CSS в `<style>` вместо
`ui/Badge.svelte` — тон и разметка бейджа живут в двух местах
(`web/src/lib/components/schema/SchemaTextResult.svelte`).
- [ ] `PreviewTile`: высота плитки задана только `min-height: clamp(...)`,
поэтому она не следует за пропорциями результата — широкий результат
уезжает в узкую полосу, а высокий упирается в `max-height: 100vh`. Нужно,
чтобы соотношение сторон выбиралось по результату в диапазоне 16:9…9:16
(`web/src/lib/components/schema/PreviewTile.svelte`).
- [ ] Радиусы бордеров: `--radius-m` ненулевой и применён ко всем бордерам
(`app.css`, `SchemaToolView.svelte`). Решить, где скругления реально есть
в дизайне, а где должен быть 0
## Переезд
- [ ] множественные числа (часть/части/частей; «N параметров», счётчики) —
`tPlural()` и формат `PluralForms`; детали и критерии — в этом backlog
функция `tPlural(path, n)` и форма `PluralForms` (обязательное `other`,
опциональные `one`/`few`/`many`); первые потребители — `part`/`parts` в
подписи результата и счётчики параметров. **Критерий приёмки:** у ключа с
plurals есть все формы, для которых нужен перевод, `other` никогда не
пустой, число подставляется в текст без склейки
## SEO/GEO
Общий план: `docs/plan-seo.md` (S1–S4). Старт возможен — пока только domain-free
часть S1. Полное содержание этапов не дублируется здесь, только идентификаторы.
- [ ] переименование бренда + домен: чек-лист в локальном файле
`plan-domain.local.md` (в git не входит, в клоне отсутствует; подробности
и порядок — `docs/plan-seo.md` §6, R1/R2)
@@ -127,16 +122,16 @@
(`docs/plan-seo.md` §5)
- [ ] sitemap.xml из реестра + `Sitemap:` в robots.txt (S1b/S1c)
- [ ] Seo.svelte: canonical + OG-теги, per-tool OG-картинки (S1d/S1e)
- [ ] контент страницы инструмента: seo-блок в словарях, H1/how-to/FAQ/related
(S2)
- [ ] контент страницы инструмента: seo-блок в словарях, how-to/FAQ/related (S2)
- [ ] JSON-LD (SoftwareApplication/FAQPage/Breadcrumb) + llms.txt (S3)
- [ ] GSC + приватная аналитика (Plausible/Umami/CF), дистрибуция (S4)
- [ ] дистрибуция: закрепить репо, alternative.to, Product Hunt, Reddit/HN (S4c)
- [ ] GSC: подключить домен, отправить sitemap, мониторинг индексации (S4a)
- [ ] GA4: cookieless-конфигурация, отключить рекламные audiences и signals,
`anonymize_ip` — сейчас в `web/src/routes/+layout.svelte` голый
`gtag('config')` (S4b)
## Платформа (API/PWA)
Общий план: `docs/plan-platform.md`; WASM-ядро, эталоны и CLI —
`docs/roadmap.md` (фазы 2–8). Здесь только треки, которых нет в roadmap.
- [ ] PWA: manifest + service worker, оффлайн-режим, установка (после S1)
- [ ] API: спайк серверного ядра (нативный Rust vs edge-wasm), ключи, free tier
(после wasm-фаз 5–7)
@@ -145,8 +140,15 @@
## Качество production UI
Политика проверок — `docs/quality-gates.md`, e2e — `docs/testing-strategy.md`.
- [ ] `/` и `/list-tools` выходят без `<title>`: `svelte:head` есть только на
странице инструмента, а готовые ключи `home.defaultTitle`,
`catalog.pageTitle`, `catalog.metaDescription` не читает ни один компонент
(S1d)
- [ ] **Баг e2e:** `tool-flows.spec.ts:179` создаёт контекст через
`browser.newContext()`, который не наследует `use.baseURL` из
`playwright.config.ts`, — относительный `goto('/tools/flip-png')` внутри
`openTool` не резолвится, и тест «работает без Web Worker (fallback)»
падает на всех движках. В CI это не видно: job `e2e` неблокирующий
- [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного
решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение.
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
@@ -154,9 +156,8 @@
## Идеи
Идеи 1–5 и 15 относятся к **пайплайну (цепочкам шагов)**, которого в коде пока
нет: это фаза 11 `docs/roadmap.md`. Идеи 6–14 — к существующим инструментам и
UI. Оценки (S/M/L) — оценка труда, не приоритет.
Идеи 1–5 и 15 — про пайплайн (фаза 11 `docs/roadmap.md`). Идеи 6–14 и 16 — про
существующие инструменты и UI. Оценки (S/M/L) — оценка труда, не приоритет.
1. **Менять первый инструмент цепочки** — оценка M. Сейчас базовый инструмент
зафиксирован при открытии страницы; хочется заменить его на другой без
@@ -171,8 +172,8 @@ UI. Оценки (S/M/L) — оценка труда, не приоритет.
3. **Несколько цепочек: сохранение и загрузка** — оценка M. Именованные цепочки
в localStorage (список, создать/переименовать/удалить), быстрое переключение.
Формат экспортного JSON расширить опциональным полем имени; обратной
совместимости со старыми выгрузками не делаем (см. задачу про `id`/`slug`
выше — формат ещё не устоялся).
совместимости со старыми выгрузками не делаем — формат не устоялся: он
появится вместе с фазой 11 `docs/roadmap.md` и до её старта может измениться.
4. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив
картинок (распаковка в браузере), прогон текущего или выбранного сохранённого
@@ -192,9 +193,11 @@ UI. Оценки (S/M/L) — оценка труда, не приоритет.
(`OutputFormat`: mime/ext/qualityParamId) и кодирование по нему в
`SchemaToolView.download()` — `docs/architecture.md`, раздел 3. Что осталось
сверх S1f:
- экспорт с лимитом размера → закрывает отложенную страницу
`reduce-to-size-png` (целевой KB, бинарный поиск по k из квантования).
Проработать вместе с достройкой download-флоу.
- экспорт с лимитом размера → «сжатие до целевого КБ»: для lossy это quality
в селекторе формата, для PNG — бинарный поиск по k из квантования. Нужна ли
отдельная страница под этот интент — открытый вопрос с вордстатом, имя не
выбрано (`docs/plan-seo.md` §5). Проработать вместе с достройкой
download-флоу.
7. **Страница `watermark-image-png` (водяной знак картинкой)** — оценка M. Нужна
overlay-механика: второй источник изображения (getOverlay/store), параметры
@@ -259,7 +262,7 @@ UI. Оценки (S/M/L) — оценка труда, не приоритет.
- частичные ошибки: один битый файл не должен ронять весь шаг — нужен
per-file отчёт, а не один `ToolError` на шаг;
- имена частей на каждом шаге и Download для набора (сейчас zip по
`tool.id`, `SchemaToolView.download()`);
`page.slug`, `SchemaToolView.download()`);
- обратная сторона: распаковка zip в браузере, порядок файлов, поведение
many → 1 → many в одной цепочке. Зависит от идеи 10 (остальные потребители
1 → many) и идеи 4 (batch по архиву), `docs/roadmap.md` фаза 8
+11 -9
View File
@@ -1,8 +1,9 @@
# Чек-лист ручного тестирования production UI
> Вспомогательный документ к e2e-тестам (`web/e2e/`, `pnpm --dir web test:e2e`)
> и стратегии `docs/testing-strategy.md`. Автоматизированная часть закрывает
> «нет падений/ошибок»; здесь — то, что руками, и что Playwright не покрывает.
> Статус: **active**. Вспомогательный документ к e2e-тестам (`web/e2e/`,
> `pnpm --dir web test:e2e`) и стратегии `docs/testing-strategy.md`.
> Автоматизированная часть закрывает «нет падений/ошибок»; здесь — то, что
> руками, и что Playwright не покрывает.
>
> Перед прогоном: `pnpm --dir web build`, поднять локально `web/` (dev или build
> → `node scripts/serve-static.mjs`).
@@ -23,7 +24,7 @@
(авто-покрытие: `web/e2e/png-fixtures.spec.ts`).
- [ ] Анимированный/APNG, если попадётся — не ломает конвейер.
- [ ] Повреждённый/не-PNG файл — понятная ошибка, нет краша (авто-чека в
`pipeline.spec.ts` и `png-fixtures.spec.ts`).
`tool-flows.spec.ts` и `png-fixtures.spec.ts`).
- [ ] PNG с битой CRC / 16-bit — как ведут себя анализаторы (ориентация, размер,
прозрачность); CRC/16-bit авто-покрыты в `png-fixtures.spec.ts`.
@@ -72,8 +73,9 @@
## D. Производительность и отзывчивость
- [ ] Долгие авто-рераны на больших картинках (jpeg-artifacts, dithering,
quantize на 4К) — UI не фризит, канселяция/дебаунс срабатывает.
- [ ] Долгие авто-рераны на больших картинках (`jpeg-artifacts-png`,
`dithering-png`, `quantize-png` на 4К) — UI не фризит, канселяция/дебаунс
срабатывает.
- [ ] Скролл страницы при длинных настройках (например, text-инструменты) — нет
залипаний, панели не прыгают.
- [ ] Вкладка не «съедает» память при 10+ повторах blur/sharpen на мегапикселе
@@ -82,9 +84,9 @@
## E. Визуальный слой (доделываем по ходу)
- [ ] Сетки/выравнивание на брейкпоинтах 640 / 800 / 1100 (mobile/tablet/
desktop) на всех 4 маршрутах.
- [ ] Тема light/dark: контраст вердиктов (Yes/No), статус «LIVE PREVIEW»,
мета-инфо бликов не даёт.
desktop) на всех маршрутах: `/`, `/list-tools`, `/kit`, любая `/tools/*`.
- [ ] Тема light/dark: контраст вердиктов (Yes/No), подпись панели превью
(«Предпросмотр»), мета-инфо бликов не даёт.
- [ ] Иконки в каталоге на всех категориях — не «бьются» (missing icon).
- [ ] Язык RU/EN на превью-страницах — переключение без регресса рендера.
+42 -5
View File
@@ -47,11 +47,11 @@
которых 14 — ложные («30 дней» политики архива, «9 опций» описания поля, «4
проекта» факт из `playwright.config.ts», цитата самого правила).
- Правило всё равно неполно: ловит «122 инструмента», но не «инструментов —
122», не «более 100», не «≈120»; на каждое обходное движение нужна правка
правила, а allowlist легитимных чисел становится вторым источником долга.
Худший исход — не «шумно», а «вроде гейт, а толку нет»: срабатывания на пустом
месте забейзелят, и через месяц правило перестанет ловить настоящие нарушения.
- Правило всё равно неполно: ловит «M инструментов», но не «инструментов — M»,
не «более 100», не «≈120»; на каждое обходное движение нужна правка правила, а
allowlist легитимных чисел становится вторым источником долга. Худший исход —
не «шумно», а «вроде гейт, а толку нет»: срабатывания на пустом месте
забейзелят, и через месяц правило перестанет ловить настоящие нарушения.
Видимое блокирующее правило, которое на деле ничего не проверяет, хуже
отсутствия правила.
- Соблюдение правила дешевле обхода: «список — в `docs/tools-map.md`» стоит
@@ -82,6 +82,19 @@
про парсер, а не молча перестанет что-либо проверять. Входит в обычный
`pnpm --dir web test`; новых гейтов и конфигов не добавляет.
Граница автоматизации: парсер читает разделы «Реализовано», «Можно добавить» и
«Идеи», но проверяет их в разных направлениях. Раздел 1 сверяется с реестром
обеими сторонами (slug без страницы и страница без записи). Разделы 2 и 3
сверяются только в одну сторону: там slug'и намеренно несуществующие, поэтому
проверяется, что **ни один из них не является реальной страницей** —
реализованный инструмент, оставшийся в плане, роняет тест. Проверять «у плана
есть запись в реестре» нельзя по построению: список плановый, а не фактический.
Плановые разделы устроены прозой, поэтому их разбор другой: берутся все токены
нужной формы из всего буллита, а не только из «головы» до тире. Следствие для
авторов карты: реализованные страницы в планах называются прозой между
буллитами, а не внутри них.
## 5. Visual snapshots — не вводим
`toHaveScreenshot` сравнивает страницу с PNG-baseline из прошлого коммита, а не
@@ -170,3 +183,27 @@ PR. Актуальные длительности — в логе прогона
Альтернатива — `allowPaths: ["pages"]` — была бы слабее: она разрешила бы пустую
секцию `pages` в любой локали, то есть пропущенный перевод страницы прошёл бы
линтер молча. Пустая секция в EN и есть то, что делает правило точным.
## 12. Бамп версии и changelog — при пуше в main, а не в PR
Версионирование и changelog генерирует бот (`bump.yml`, cocogitto) на push в
`main`, а не на pull request.
- При PR-боте параллельные PR бампили бы одну и ту же версию и конфликтовали по
`CHANGELOG.md`; коммиты бота внутри чужого PR — шум, а версия росла бы за ещё
не влитый код. Репозиторий trunk-based: коммиты идут прямо в `main`, деплой и
так запускается на push.
- Цикл: push в `main` → `verify.yml` (переиспользуемый gate) → `cog bump --auto`
бампит версии в обоих `package.json` (футер сайта читает `web/package.json`),
обновляет `CHANGELOG.md`, ставит тег и пушит без `--skip-ci`, чтобы версионный
коммит заново запустил деплой и сайт показал актуальную версию. Зацикливание
исключено dry-run проверкой: после версионного коммита conventional-коммитов
после тега нет, бот выходит.
- Пуш идёт токеном GitHub App (`XBOCT_BOT_ID`/`XBOCT_BOT_PRIVATE_KEY`), а не
`GITHUB_TOKEN`: пуш стандартным токеном не запускает workflow, и деплой с
новой версией не случился бы.
- `concurrency: bump` без `cancel-in-progress`: два бампа параллельно запушили
бы конфликтующие версии.
- Пересмотреть, если появятся внешние контрибьюторы и поток пойдёт через PR —
тогда боту понадобится режим «черновик релиза в PR» или squash-стратегия с
conventional-заголовком PR.
+118 -97
View File
@@ -16,13 +16,20 @@
> которые в бэклоге, здесь не дублируются: этот документ задаёт **порядок**,
> backlog — **учёт** (`docs/backlog.md`).
>
> **Инструменты в этом документе названы по адресам страниц (slug).** В коде у
> страницы `slug` (`registry/pages/`), у вызываемого инструмента — `id` без
> png-интента (`crop-png` → `id: "crop"`, `png-to-hsl` → `to-hsl`), модель —
> `docs/architecture.md`, раздел 3. Названия из `docs/tools-audit.md` вида
> `color-wheel-generator`, `webp-to-png` в реестре отсутствуют (реальная
> страница — `color-wheel-png`): при переносе пункта аудита в шаг сверять с
> `PAGES`.
> **В этом документе инструменты названы по `id`** (`registry/tools/`), а не по
> адресу страницы: поля и раскладка принадлежат схеме инструмента, а страница
> только ссылается на список шагов (`Page.steps`), где сейчас ровно один
> инструмент, а в будущем может быть несколько. Поэтому «у страницы `crop-png`
> есть поля x/y» неверно — поля есть у инструмента `crop`, а страница лишь
> показывает его. `id` не несёт png-интента: страница `crop-png` вызывает
> `id: "crop"`, страница `png-to-hsl` — `id: "to-hsl"` (модель —
> `docs/architecture.md`, раздел 3).
>
> **Как переносить пункты аудита.** `docs/tools-audit.md` назван по страницам,
> потому что это то, что видит пользователь. При переносе пункта в шаг мапить
> slug → `id` через `page.steps[0].id` в `registry/pages/`. Названия аудита вида
> `color-wheel-generator` и `webp-to-png` в реестре отсутствуют (реальная
> страница — `color-wheel-png`, инструмент — `color-wheel`).
## 0. Ключевое решение: select → buttons
@@ -124,9 +131,9 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
- `web/src/lib/registry-schema.ts` — `NumberSpec.variant?: "seed"`
- `web/src/lib/components/fields/schema/RangeControl.svelte` — условный рендер
- `web/src/lib/registry/filters.ts` — `randomizePixels`, `addNoise`: поле seed →
`{ ...field.number(...), spec: { ...spec, variant: "seed" } }` (или
пересоздать через `field.seed()`)
- `web/src/lib/registry/tools/filters.ts` — `randomizePixels`, `addNoiseTool`:
поле seed → `{ ...field.number(...), spec: { ...spec, variant: "seed" } }`
(или пересоздать через `field.seed()`)
---
@@ -138,63 +145,67 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
### FA-B1. geometry.ts
| Инструмент | Поле | Опции |
| ----------------------- | -------- | ----------------------------------------- |
| rotate-png | angle | 90° / 180° / 270° |
| flip-png | axis | horizontal / vertical |
| swap-orientation-png | target | portrait / landscape |
| symmetric-copy-png | axis | vertical / horizontal |
| symmetric-copy-png | keepSide | left / right / top / bottom |
| change-aspect-ratio-png | ratio | 1:1 / 4:3 / 3:4 / 3:2 / 2:3 / 16:9 / 9:16 |
| change-aspect-ratio-png | mode | crop / pad |
| ------------------- | -------- | ----------------------------------------- |
| rotate | angle | 90° / 180° / 270° |
| flip | axis | horizontal / vertical |
| swap-orientation | target | portrait / landscape |
| symmetric-copy | axis | vertical / horizontal |
| symmetric-copy | keepSide | left / right / top / bottom |
| change-aspect-ratio | ratio | 1:1 / 4:3 / 3:4 / 3:2 / 2:3 / 16:9 / 9:16 |
| change-aspect-ratio | mode | crop / pad |
**Исключение:** `change-canvas-size-png` (anchor, 9 опций) — оставить
**Исключение:** `change-canvas-size` (anchor, 9 опций) — оставить
`field.select()`, т.к. 9 кнопок в ряд не поместятся. Аналогично `position9` (уже
отдельный компонент `PositionControl`).
### FA-B2. color.ts
| Инструмент | Поле | Опции |
| ------------------------ | --------- | ------------------------------------ |
| dithering-png | pattern | floyd-steinberg / bayer |
| extract-channel-png | channel | red / green / blue |
| swap-channels-png | pair | r-g / r-b / g-b |
| decrease-color-count-png | maxColors | 2 / 4 / 8 / 16 / 32 / 64 / 128 / 256 |
| -------------------- | --------- | ---------------------------------------------------- |
| dithering | pattern | floyd-steinberg / bayer |
| extract-channel | channel | red / green / blue |
| swap-channels | pair | r-g / r-b / g-b |
| decrease-color-count | maxColors | 2 / 4 / 8 / 16 / 32 / 44 / 64 / 96 / 128 / 192 / 256 |
**Исключение:** channel spaces (hsl/hsv/... component) — 3-6 опций, но это
**Исключение:** инструменты разложения каналов (`to-hsl`, `to-hsv`, `to-hsi`,
`to-cmyk`, `to-ycbcr`, `to-lab`, поле `component`) — 3-6 опций, но это
динамические инструменты, оставить `select`.
### FA-B3. analyze.ts
| Инструмент | Поле | Опции |
| ------------------------- | ---- | ------------------ |
| show-transparent-png | mode | binary / highlight |
| show-grayscale-pixels-png | mode | (тот же) |
| show-color-pixels-png | mode | (тот же) |
| light-pixel-mask-png | mode | (тот же) |
| dark-pixel-mask-png | mode | (тот же) |
| --------------------- | ---- | ------------------ |
| show-transparent | mode | binary / highlight |
| show-grayscale-pixels | mode | (тот же) |
| show-color-pixels | mode | (тот же) |
| light-pixel-mask | mode | (тот же) |
| dark-pixel-mask | mode | (тот же) |
| unique-color-mask | mode | (тот же) |
Все используют общий `maskBaseFields` — правка в одном месте.
Пять инструментов из шести берут `mode` из общего `maskBaseFields` — правка в
одном месте. Исключение — `show-transparent`: он переопределяет `mode` своими
опциями и дефолтом `highlight`, поэтому его `mode` правится отдельно.
### FA-B4. filters.ts
| Инструмент | Поле | Опции |
| ------------- | ---- | ------------ |
| add-noise-png | mode | mono / color |
| ---------- | ---- | ------------ |
| add-noise | mode | mono / color |
### FA-B5. generate.ts
| Инструмент | Поле | Опции |
| ------------------ | ------------ | --------------------- |
| color-spectrum-png | direction | horizontal / vertical |
| step-colors-png | layout | grid / strip |
| complementary-png | layout | grid / strip |
| triadic-png | layout | grid / strip |
| tetradic-png | layout | grid / strip |
| analogous-png | layout | grid / strip |
| monochromatic-png | layout | grid / strip |
| shades-png | layout | grid / strip |
| sort-colors-png | layout | grid / strip |
| mix-colors-png | (нет select) | — |
| -------------- | ------------ | --------------------- |
| color-spectrum | direction | horizontal / vertical |
| step-colors | layout | grid / strip |
| complementary | layout | grid / strip |
| triadic | layout | grid / strip |
| tetradic | layout | grid / strip |
| analogous | layout | grid / strip |
| monochromatic | layout | grid / strip |
| shades | layout | grid / strip |
| sort-colors | layout | grid / strip |
| mix-colors | (нет select) | — |
---
@@ -203,12 +214,12 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
### FA-C1. Source-aware defaults для dimension-полей — реализовано
Source-aware defaults реализованы: общий resolver в `registry-schema`
(`applySourceDefaults`). Для `resize-png` и `crop-png` дефолт после загрузки
равен размерам текущего source; до загрузки используется положительный fallback
`1×1`. Один-sided `0` у resize сохраняет режим auto. Проверено тестом
(`applySourceDefaults`). Для `resize` и `crop` дефолт после загрузки равен
размерам текущего source; до загрузки используется положительный fallback `1×1`.
Один-sided `0` у resize сохраняет режим auto. Проверено тестом
`registry.test.ts` («resize и crop получают размеры текущего source»).
### FA-C2. symmetric-copy-png: "keep side" не работает
### FA-C2. symmetric-copy: "keep side" не работает
Баг в `run()`: `keepSide` не учитывает `axis`. При `axis: "vertical"` (удвоение
ширины) работают только `left`/`right`, `top`/`bottom` не имеют смысла. Нужно:
@@ -257,55 +268,63 @@ resolver.
### FA-C4. verify-is-png: неверное название — решение принято, шаг не выполнен
Проверяет текст, не PNG-файл. Варианты:
Находка про **страницу**, а не про инструмент: страница `verify-is-png` обещает
проверку PNG-файла, а проверяет текст (base64 / data-uri). Сам инструмент назван
честно — `id: "verify-png"`, png-интент в нём означает входные данные. Адрес
страницы — `slug` `verify-is-png` (`docs/architecture.md`, раздел 3), поэтому
«неверное название» относится к `slug`, а не к `id`.
- `title` → "Verify PNG data", `description` → уточнить; `id` оставить — он же
адрес страницы, переименование ломает ссылку
- либо полное переименование `id` → `verify-is-png-data` с редиректом со старого
URL (нужен вордстат, иначе страница выпадает из индекса)
Варианты:
**Решение:** поменять title и description, id не трогать. Переименование `id`
затрагивает адрес — с ним связана задача разведения `id`/`slug`
(`docs/backlog.md`), поэтому в одиночку не делается.
- `title` → «Verify PNG data», `description` → уточнить; `slug` и `id` не
трогать
- либо переименовать `slug` → `verify-png-data` с редиректом со старого URL
(нужен вордстат, иначе страница выпадает из индекса)
**Решение:** поменять title и description, имя страницы не трогать.
Переименование `slug` меняет адрес, имя файла, ключи `pages.*` и иконку, то есть
это операция с SEO-последствиями, а не строчка в реестре — она не делается в
одиночку, вместе с решением по `docs/plan-seo.md` §5.
---
## 4. Волна D — обязательные фиксы (новые параметры)
### FA-D1. remove-alpha-channel-png: выбор цвета фона
### FA-D1. remove-alpha-channel: выбор цвета фона
Сейчас захардкожен `#ffffff`. Добавить `field.color({ default: "#ffffff" })`.
Файл: `alpha.ts`, схема `removeAlphaChannelSchema`.
### FA-D2. extract-alpha-mask-png: галочка «инвертировать»
### FA-D2. extract-alpha-mask: галочка «инвертировать»
Добавить `field.checkbox({ label: "fields.invertMask", default: false })`. В
`run()`: если `invert`, вызвать `invertAlpha()` после `extractAlphaMask()`.
Файл: `alpha.ts`.
### FA-D3. change-canvas-size-png: position9 вместо select
### FA-D3. change-canvas-size: position9 вместо select
`anchor` сейчас `field.select()` с 9 опциями → `field.position9()`.
Файл: `geometry.ts`. Уже есть `PositionControl`.
### FA-D4. Generators: добавить height
### FA-D4. Generate: добавить height
Инструменты без `size`/`height`:
- blend-two-png — добавить
- blend-two — добавить
`height: field.slider({ min: 1, max: 1024, default: 512 })`
- step-colors-png — аналогично
- complementary, triadic, tetradic, analogous, monochromatic, shades,
sort-colors — все используют `paletteBaseSchema` (общий объект), добавить
`height` туда
- mix-colors-png — добавить height
- step-colors — аналогично
- complementary, triadic, tetradic, analogous, monochromatic, shades — все
используют `paletteBaseSchema` (общий объект), добавить `height` туда
- `sort-colors` — своя схема, `paletteBaseSchema` не использует, `height`
добавлять вручную
- mix-colors — добавить height
Файл: `generate.ts`.
### FA-D5. add-border-png: прозрачность цвета
### FA-D5. add-border: прозрачность цвета
Текущий `field.color()` не поддерживает alpha. Пока что: оставить как есть
(прозрачность не поддерживается нативным color picker). Отметить в аудите как
@@ -322,24 +341,24 @@ blocked.
Файл: `SchemaToolView.svelte` — показать подсказку если `schema.fields` пуст.
### FA-E2. quantize-png: пресеты
### FA-E2. quantize: пресеты
`field.slider` с max 64 → добавить пресеты (8, 16, 32, 64) как кнопки под
слайдером. Либо `field.buttons()` с 4 опциями вместо слайдера.
### FA-E3. trim-empty-space-png: 0-254 → проценты
### FA-E3. trim-empty-space: 0-254 → проценты
Текущий `min: 0, max: 254` — нестандартно. Добавить переключатель px/% (аналог
шага FA-A1 — `variant: "alpha-threshold"` в NumberSpec).
### FA-E4. add-text-png: plate offset
### FA-E4. add-text: plate offset
Plate снизу имеет большой отступ — добавить галочку "compact plate" или
уменьшить дефолтный padding.
### FA-E5. emoji-to-png: выбор эмодзи
### FA-E5. from-emoji: выбор эмодзи
Поле `text` → добавить группу часто используемых эмодзи как кнопки- пресеты над
Поле `emoji` → добавить группу часто используемых эмодзи как кнопки-пресеты над
полем ввода.
---
@@ -365,44 +384,46 @@ Plate снизу имеет большой отступ — добавить г
round-corners — px→%): нужен компонент переключателя единиц в слайдере.
Требует правки `RangeControl` + всех схем — в волны A–E не входит, нужен
отдельный этап.
- **Smoothing checkbox** для масок (circle, square, star, wavy, round-corners,
invert-alpha): нужен `field.checkbox("smoothing")` + правка core (shapes.ts,
alpha). Отложен — требует визуального тестирования.
- **Smoothing checkbox** для масок (circle-mask, square-mask, star-mask,
wavy-mask, round-corners, invert-alpha): нужен `field.checkbox("smoothing")` +
правка core (shapes.ts, alpha). Отложен — требует визуального тестирования.
- **Vignette**: выбор центра + цвет + feather — 3 новых параметра. Требует
normative решения по UI.
- **Remove-background**: выбор точки удаления — либо параметр, либо отдельный
инструмент.
- **Precision/quality**: select в resize (bilinear/bicubic/nearest) — нужен ли
выбор алгоритма?
- **Метод масштабирования в `resize`**: поля в схеме нет (только `size` и
`keepAspect`), поэтому это уже не правка селекта, а новое поле — нужен ли
выбор алгоритма (bilinear/bicubic/nearest)?
- **Placeholders**: "нет параметров" — какой текст/компонент показывать.
### 7.2 Операционные (тест-кейсы, описания)
- find-contour-png: расширять на толщину линии (требует правки core)
- make-thicker-png: расширять картинку на толщину линии
- make-thinner-png: уменьшать от краев, fix выступа
- feather-edges-png: расширять картинку на толщину линии
- clean-edges-png: найти тест-кейс, возможно увеличить максимум
- despeckle-alpha-png: найти более заметный тест-кейс
- close-holes-png: проверить поведение на больших полупрозрачных областях
- auto-contrast-png: найти тестовый кейс
- sharpen-png: добавить тест-кейс
- center-by-alpha-png: найти тест-кейс
- png-is-grayscale: улучшить видимость результата
- find-contour: расширять на толщину линии (требует правки core)
- make-thicker: расширять картинку на толщину линии
- make-thinner: уменьшать от краев, fix выступа
- feather-edges: расширять картинку на толщину линии
- clean-edges: найти тест-кейс, возможно увеличить максимум
- despeckle-alpha: найти более заметный тест-кейс
- close-holes: проверить поведение на больших полупрозрачных областях
- auto-contrast: найти тестовый кейс
- sharpen: добавить тест-кейс
- center-by-alpha: найти тест-кейс
- is-grayscale: улучшить видимость результата
### 7.3 Дизайн-вопросы
- add-padding-png vs add-border-png: разница неочевидна, нужен review
- change-aspect-ratio-png: не видно что меняется — рамка результата
- watermark-tile-png: заполняет не полностью
- sepia-png: нужна ли настройка силы эффекта?
- posterize-png: настройка для каждого канала отдельно?
- pixelate-png: разные алгоритмы?
- swap-channels-png: обдумать flow через более простые инструменты
- add-padding vs add-border: разница неочевидна, нужен review
- change-aspect-ratio: не видно что меняется — рамка результата
- watermark-tile: заполняет не полностью
- sepia: нужна ли настройка силы эффекта?
- posterize: настройка для каждого канала отдельно?
- pixelate: разные алгоритмы?
- swap-channels: обдумать flow через более простые инструменты
### 7.4 Аудит полей (компоненты)
- Range slider: поле ввода + кнопки +/- + кнопка сброса
- Color picker: пипетка с картинки, прозрачность
- Width/height с keep-aspect-ratio: единый компонент
- Position offset: отдельные слайдеры x/y (уже сделано в `OffsetControl`)
- Position offset: отдельные числовые поля X/Y (уже сделано в
`fields/schema/OffsetControl.svelte`)
+23 -19
View File
@@ -3,7 +3,7 @@
> **Статус: active.** Этот план добавляет к `docs/roadmap.md` то, чего там нет:
> **API** (фаза 9, возможная монетизация) и **PWA** (фаза 10, оффлайн +
> установка), плюс шрифты (раздел 4 — частично относится к фазе 8, поэтому
> решения-phase 8 здесь дублируются). WASM-ядро, эталоны и CLI — фазы 2–8
> решения фазы 8 здесь дублируются). WASM-ядро, эталоны и CLI — фазы 2–8
> `docs/roadmap.md`, они здесь не пересказываются. Оба трека стартуют после
> wasm-фаз; PWA не ждёт API и может идти раньше.
@@ -11,19 +11,20 @@
- **Одно ядро — три потребителя.** Rust-крейт `core/` (roadmap фаза 4) даёт:
wasm-пакет для сайта (фаза 5), нативный CLI (фаза 6), серверный слой API (фаза
9). Единый контракт — JSON-пайплайн `{tool, params}` (заложен в roadmap §0,
реализуется в фазе 11); эталонные файлы и harness (фазы 2, 3, 7) гарантируют
одинаковый результат во всех рантаймах.
9). Единый контракт — JSON-пайплайн `{id, params}` (заложен в roadmap §0 и
совпадает с `Page.steps`, реализуется в фазе 11); эталонные файлы и harness
(фазы 2, 3, 7) гарантируют одинаковый результат во всех рантаймах.
- **API — нативный Rust на сервере, не wasm.** В браузере wasm нужен ради
доставки и песочницы; на сервере крейт линкуется напрямую — быстрее и проще.
Исключение — edge-воркеры (см. спайк ниже): там исполняется тот же
wasm-артефакт фазы 5.
- **PWA не трогает SEO-схему.** Сайт остаётся полностью prerendered-статикой;
service worker добавляет оффлайн-слой поверх и не отвечает за HTML ботовам.
- **Монетизация не ломает позиционирование.** Браузерные инструменты — бесплатны
и без трекинга; API — отдельный продукт для автоматизации/бизнеса. Файлы в API
обрабатываются в памяти: без хранения и логирования содержимого — продолжение
«privacy»-истории.
- **Монетизация не ломает позиционирование.** Браузерные инструменты —
бесплатные, картинка обрабатывается в браузере и никуда не отправляется.
Счётчик посещений GA4 этому не противоречит (границы — `docs/plan-seo.md` §0).
API — отдельный продукт для автоматизации/бизнеса. Файлы в API обрабатываются
в памяти: без хранения и логирования содержимого.
## 1. Фаза 9 — API (после фаз 5–7)
@@ -40,7 +41,7 @@
запросам/размеру файла. Только после стабильного free tier.
- [ ] **[A5]** Документация как лендинг: `/api` — индексируемая страница
(туториал, примеры curl/JS), OpenAPI, ссылка из `llms.txt`. Dev-аудитория
— источник бэклинков (план-seo S4c).
— источник бэклинков (`docs/plan-seo.md`, S4c).
- Гейт: один и тот же пайплайн даёт пиксельно идентичный результат в браузере,
CLI и API (harness фазы 7 расширяется третьим рантаймом).
@@ -68,17 +69,17 @@
фича — фаза 11 того же документа.
- PWA — после S1 `plan-seo.md`; API — после фаз 5–7.
- Синергия с дистрибуцией: «works offline, installable» и публичный API —
аргументы для Product Hunt / alternative.to / GitHub (plan-seo S4c) и для
GEO-текстов (`llms.txt`, описания категорий).
аргументы для Product Hunt / alternative.to / GitHub (`docs/plan-seo.md`, S4c)
и для GEO-текстов (`llms.txt`, описания категорий).
## 4. Шрифты: лицензирование и DOM-free рендер текста
Текст — один из canvas-зависимых классов, которые roadmap (фаза 8) держит в TS
(там их четыре: текст, градиенты, штампы, `ctx.filter`); для текста выбран курс
на реимплементацию, остальные классы не переносятся. Для CLI/API текст всё равно
придётся рендерить в Rust, поэтому решение: **перевести text-инструменты в
ядро** (в браузере canvas-рендер остаётся TS-фолбэком для A/B). Два вопроса —
легальность шрифтов в дистрибутиве и рендер без DOM.
(перечень — там же); для текста выбран курс на реимплементацию, остальные классы
не переносятся. Для CLI/API текст всё равно придётся рендерить в Rust, поэтому
решение: **перевести text-инструменты в ядро** (в браузере canvas-рендер
остаётся TS-фолбэком для A/B). Два вопроса — легальность шрифтов в дистрибутиве
и рендер без DOM.
**Когда начинать:** блока F1–F4 нет — они не привязаны к фазе roadmap, но F5/F6
без них бессмысленны (нечего рендерить). Реализуется всё это в фазе 8, поэтому
@@ -102,15 +103,18 @@
`pnpm verify` по правилам `docs/quality-gates.md` (решение о fail-режиме —
`docs/decisions.md`). Браузер грузит шрифты из бандла, не из ОС и не с
`fonts.googleapis.com`-CDN (сторонний запрос = утечка IP пользователя,
противоречит «no tracking»), — одинаковый результат у всех пользователей и
во всех рантаймах.
лишняя зависимость от чужой инфраструктуры), — одинаковый результат у всех
пользователей и во всех рантаймах.
- [ ] **[F4]** Кириллица: приоритет семействам с полным покрытием — Liberation
Sans, PT Sans/PT Serif, Noto Sans, Inter, Roboto (OFL/Apache), DejaVu Sans
(пермиссивная лицензия Bitstream Vera: редистрибуция и коммерческое
использование разрешены) — закрывает и backlog-вопрос про «только с
кириллицей».
- **Стартовый набор** (все OFL/Apache, с кириллицей; состав не догма —
расширяется по запросам, каждый проходит паспорт F3):
расширяется по запросам, каждый проходит паспорт F3). Сейчас в бандле сайта
лежат `@fontsource/ibm-plex-sans` и `@fontsource/ibm-plex-mono` (тоже OFL) — в
наборе ниже их нет, и при введении паспорта F3 их судьбу нужно решить явно:
оставить или заменить.
- базовый sans: **Inter** (дефолт add-text), **PT Sans** / **Golos Text**
(профессиональная русская типографика), **Noto Sans** (фолбэк покрытия);
- serif: **PT Serif**, **Lora**, **Playfair Display** (display);
+59 -37
View File
@@ -24,8 +24,9 @@
> схему сайта они не меняют.
>
> **Слаги в этом документе:** `jpg-to-png`, `crop-jpeg` и подобное — интенты и
> URL конкурента, а не id наших инструментов. Наши id — в `docs/tools-map.md`; в
> тексте ниже реальные id инструментов приводятся в бэктиках.
> URL конкурента, а не наши страницы. Наши страницы — в `docs/tools-map.md`
> (карта перечисляет `slug`, не `id`); в тексте ниже названия инструментов
> приводятся по этим слагам, потому что для SEO важны адреса.
## 0. Решения
@@ -72,9 +73,14 @@
`registry/pages/`. Схему не менять без отдельного SEO-плана: после индексации
переименование URL означает 301 и потерю позиций. Разделение инструмента и
страницы (решение выше) адресов не меняет.
- **Без GA и рекламных скриптов:** аналитика — self-hosted Plausible/Umami или
Cloudflare Web Analytics. «No tracking» — часть позиционирования, её нельзя
подрывать собственными скриптами.
- **Аналитика — GA4, в режиме «только агрегаты».** GA4 уже стоит в проде
(`web/src/app.html`, prerender-оболочка, значит на каждой странице). Решение —
не выпиливать, а ограничить: cookieless-конфигурация, без рекламных audiences,
signals и ремаркетинга. Граница приватности проходит по данным изображения: GA
не видит ни файла, ни его содержимого, ни результата инструмента — только факт
открытия страницы. **Рекламу и ремаркетинг не ставим** — они противоречат
позиционированию, в отличие от счётчика посещений. Формулировку «без трекинга»
на публичных страницах не использовать: она теперь неточна, см. §1 и S4b.
- **Честные методы.** Никакого клонированного текста, дорвеев, PBN. Контент — по
каждому инструменту, с ручной вычиткой.
@@ -105,10 +111,10 @@
| `rel=canonical` | нет | S1 |
| OG/Twitter-теги, OG-картинки | нет | S1 |
| Канонический домен в абсолютных URL | нет, деплой на GitHub Pages с базой | S1 |
| Контент страницы (H1-текст, how-to, FAQ, related) | только title/description | S2 |
| Контент страницы (how-to, FAQ, related) | title, robots и отрендеренный H1 | S2 |
| JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 |
| `llms.txt`, хабы категорий с текстом | нет | S3 |
| GSC + приватная аналитика | нет | S4 |
| GSC + доводка GA до cookieless-конфигурации | GA4 стоит, GSC не подключён | S4 |
Строка `noindex` — первая не потому, что её сложнее всего, а потому, что она
блокирует всё остальное: пока тег стоит, остальные строки таблицы не дают
@@ -150,7 +156,7 @@
ссылок на инструменты. Но это этап S3e и только после того, как написано ядро
S2: транзакционная страница приносит конверсию, информационная — только
трафик. Тонкие FAQ-страницы «про всё» не заводить — несколько сильных
материалов с ссылками на 3–5 инструментов каждый.
материалов с ссылками на несколько связанных инструментов.
6. **Дистрибуция.** GitHub-репо (звёзды/форки = сигналы), alternative.to,
Product Hunt, Reddit по интенту — реальный путь набрать первые бэклинки без
бюджетов.
@@ -175,16 +181,20 @@
- [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на
существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд,
файл `static/og/[slug].png`, ссылка из `Seo.svelte`. Относительные пути
(`/og/crop-png.png`) работают без домена; при деплое на собственном домене
файл `static/og/[slug].png`, ссылка из `Seo.svelte`. Путь к картинке
строить через `resolve()` из `$app/paths` (как favicon в
`+layout.svelte`), а не литералом `/og/…`: под `BASE_PATH` корневой путь
ломается. Домен для этого не нужен; при деплое на собственном домене
OG-картинки будут абсолютными и корректными.
- [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality
для lossy) для всех инструментов — механика `Tool.output` уже есть
(`docs/backlog.md`, «Идеи» №6, UX-райз «Download»). Продуктовая основа
«image tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести
параллельно с текущими quality-задачами. Побочно снимает `quality` из схем
страниц — после этого 14 convert-страниц сводятся на один инструмент
(`docs/backlog.md`).
**конвертеров** — после этого convert-страницы сводятся на один инструмент
(`docs/backlog.md`). Не из всех схем: у `jpeg-artifacts` `quality` —
функциональное поле (сила артефактов), а `output` у инструмента нет, так
что селектор формата его не покрывает.
- [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов
формат исходника (disable) — чтобы `convert-png-to-jpg` не мог сохранить в
png. Существующие страницы не трогать: `convert-png-to-jpg`,
@@ -198,10 +208,12 @@
`https://<юзер>/<repo>/…` на корневые URL (301). Делается одновременно с
S1d (canonical), чтобы Google проиндексировал сразу с правильным доменом.
- [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в
`build/` из `TOOLS` реестра (`web/src/lib/registry`) + статических
страниц. Вызов из `pnpm build` после `vite build` (adapter-static пишет в
`build/`, дописываем файл там же). Origin — из env (`PUBLIC_SITE_ORIGIN`),
учёт опционального `BASE_PATH`.
`build/` из `PAGES` реестра (`web/src/lib/registry`) + статических
страниц. Взять адреса у страниц, а не у инструментов: страниц может быть
несколько на один инструмент, у инструмента адреса нет. Вызов из
`pnpm build` после `vite build` (adapter-static пишет в `build/`,
дописываем файл там же). Origin — из env (`PUBLIC_SITE_ORIGIN`), учёт
опционального `BASE_PATH`.
- [ ] **[S1c]** `robots.txt`: `Sitemap:` + закрыть служебный `/kit`; после
cleanup — вернуть минимальный allow-all.
- [ ] **[S1d]** Компонент `Seo.svelte` (kit): canonical (абсолютный URL по
@@ -211,9 +223,9 @@
> **Порядок S1:** S1e/S1f/S1g → покупка домена → S1a+S1b+S1c+S1d → **S1h**
> (снятие `noindex`) → S2-контент. Гейты: `pnpm --dir web build` зелёный; в
> `build/` лежит корректный `sitemap.xml` со всеми инструментами; canonical/OG
> присутствуют на всех страницах; `noindex` отсутствует в собранном HTML
> (`pnpm --dir web build` и проверка `build/tools/*/index.html`);
> `build/` лежит корректный `sitemap.xml` со всеми страницами инструментов;
> canonical/OG присутствуют на всех страницах; `noindex` отсутствует в собранном
> HTML (`pnpm --dir web build` и проверка `build/tools/*/index.html`);
> `pnpm --dir web test:e2e` — по правилам `docs/quality-gates.md`, в CI job
> `e2e` non-blocking, поэтому его статус читается по отдельному job
> (`docs/decisions.md`, раздел 10).
@@ -229,9 +241,9 @@
(`ol`), описание параметров (из схемы), FAQ, related tools (перелинковка
внутри категории + 2–3 «соседних» интента).
- [ ] **[S2c]** Хлебные крошки (Каталог → Категория → Инструмент) в UI.
- [ ] **[S2d]** Первая волна контента — топ-30 инструментов ядра (приоритет по
вордстату: transparent png, jpg→png, compress, resize, crop, rotate,
circle crop, add text, pixelate, grayscale…).
- [ ] **[S2d]** Первая волна контента — топ приоритетных инструментов ядра
(приоритет по вордстату: transparent png, jpg→png, compress, resize, crop,
rotate, circle crop, add text, pixelate, grayscale…); отбор — §5.
- Гейты: на каждой странице инструмента — уникальный текст ≥ 300 слов, H1
совпадает с интентом, FAQ ≥ 3, related ≥ 3; текст в статике (prerender), не в
рантайме.
@@ -266,8 +278,12 @@
- [ ] **[S4a]** Google Search Console: собственность на домен, отправка sitemap,
мониторинг индексации и запросов. Бесплатно и обязательно.
- [ ] **[S4b]** Аналитика без трекинга: self-hosted Plausible/Umami либо
Cloudflare Web Analytics. GA/ЯМ не ставим (позиционирование).
- [ ] **[S4b]** Довести GA4 до решения §0: cookieless-конфигурация, отключённые
рекламные audiences и signals, `anonymize_ip`, запрет на передачу
параметров с данными картинки. Мусорные попадания уже отсечены: тег
подключается только при `PUBLIC_GA_ENABLED=true` (выставлен в
deploy-workflow, `+layout.svelte`). GA/ЯМ как замена счётчику не
рассматриваем.
- [ ] **[S4c]** Дистрибуция: закрепить GitHub-репо (README со скриншотами),
подача на alternative.to (страница против конкурента), Product Hunt,
тематические Reddit/HN-треды. Бэклинки — главный дефицит для конкуренции.
@@ -279,19 +295,23 @@
## 5. Приоритизация инструментов (первая волна контента)
Источник — `docs/tools-map.md` (наш состав и замер по конкуренту — §2). Ids ниже
— **фактические id реестра**; первая волна — топ-интенты, где конкуренты
ранжируются, а у нас уже есть инструмент и он не хуже:
Источник — `docs/tools-map.md` (наш состав и замер по конкуренту — §2). Слаги
ниже — **фактические `slug` страниц** из `docs/tools-map.md` §1; первая волна —
топ-интенты, где конкуренты ранжируются, а у нас уже есть инструмент и он не
хуже:
- прозрачность/фон: create-empty-png, remove-background-png, change-png-opacity,
set-alpha-channel-png
set-alpha-channel-png, round-corners-png
- конвертация: convert-png-to-jpg, convert-png-to-webp, png-to-bmp, svg-to-png,
png-to-base64
- геометрия: crop-png, resize-png, rotate-png, round-corners-png, flip-png,
change-aspect-ratio-png
- цвет/фильтры: grayscale-png, invert-colors-png, pixelate-png, add-text-png,
watermark-tile-png
- генерация: linear-gradient-png, create-empty-png
- геометрия: crop-png, resize-png, rotate-png, flip-png, change-aspect-ratio-png
- цвет/фильтры: grayscale-png, invert-colors-png, pixelate-png
- текст: add-text-png, watermark-tile-png
- генерация: linear-gradient-png
> Группы здесь — по категории страницы в реестре (`registry/pages/`), а не по
> смыслу: `round-corners-png` лежит в `alpha`, `add-text-png` и
> `watermark-tile-png` — в `text`.
> **Открытый вопрос по интентам «из формата в PNG».** У конкурента есть
> `jpg-to-png`, `webp-to-png`, `gif-to-png` как отдельные страницы; у нас таких
@@ -380,9 +400,10 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
- [ ] **[R1]** Механический переезд бренда на новое имя. Список файлов и строк с
хардкодом — в локальном плане (раздел «Где живёт бренд в коде»); здесь
только SEO-ограничения. **НЕ трогать:** адреса страниц инструментов
(`/tools/crop-png`), ключи localStorage (`easy-png-tools:*` — или
мигрировать отдельной мелочью), историю `docs/archive`. Бренд должен стать
i18n-константой, а не тремя независимыми хардкодами.
(`/tools/crop-png`) и localStorage-ключи — `theme` и `locale` лежат без
префикса бренда, поэтому переименование с ними не столкнётся (проверено в
`theme.svelte.ts` и `i18n/locale.svelte.ts`); историю `docs/archive`.
Бренд должен стать i18n-константой, а не тремя независимыми хардкодами.
- [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и
перевести прод на него без `BASE_PATH` (§0). Домен нужен до S1-domain:
canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`.
@@ -396,7 +417,8 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
- Клонирование всего каталога конкурента (низкокачественные дубли + нет ресурсов
на контент) — вместо этого ядро + обоснованные хвосты.
- RU-локаль в индексе до решения о структуре `/ru/` + hreflang.
- GA/реклама/трекинг — противоречит позиционированию.
- Реклама, ремаркетинг, pixels сторонних сетей — противоречит позиционированию.
Счётчик посещений GA4 — не противоречит, условия ограничений — §0 и S4b.
- Дорвеи, программные зеркала, покупные ссылки.
- Массово-сгенерированные форматные клоны (`crop-jpeg`/`crop-webp` как копии
`crop-png` с подстановкой формата в шаблонный текст) — doorway-паттерн, риск
+4 -2
View File
@@ -98,8 +98,10 @@ download, темы или локализации. Изменения runtime fix
pattern-based policy для временно неиспользуемых `tools.*.params`; это не
молчаливый `allowPaths`-список.
`lint:all` может завершиться успешно при warnings — это не означает чистый
baseline. Агент показывает число errors, warnings и token warnings отдельно.
`lint:all` может завершиться успешно, напечатав unused-token сообщения: это
единственный неблокирующий сигнал в нём. ESLint warnings, наоборот, роняют gate,
поэтому «прошёл» ≠ «нечего чинить». Агент показывает число errors, warnings и
token warnings раздельно.
## CI
+6 -5
View File
@@ -1,6 +1,6 @@
# План развития: easy-png-tools
> **Статус:** фаза 1 выполнена — сайт живёт на типизированном реестре,
> **Статус: active.** Фаза 1 выполнена — сайт живёт на типизированном реестре,
> production UI — корневые маршруты. Текущий фокус: SEO S1-free
> (`docs/plan-seo.md`) + открытые задачи (`docs/backlog.md`). Домен нужен для
> SEO (S1-domain). Фазы 2–8 — будущие; 9–10 спланированы в
@@ -19,16 +19,17 @@
слои) — `docs/architecture.md`, разделы 2–3.
- **Набор утилит для изображений, всё в браузере** (SvelteKit +
`adapter-static`), данные не покидают машину.
`adapter-static`): файл картинки не покидает машину. Счётчик посещений GA4
этому не противоречит — границы `docs/plan-seo.md`, §0.
- **Первый и главный приоритет — полностью рабочий сайт**: максимум простых
инструментов, UX/UI, пайплайны (несколько инструментов подряд), описания
инструментов.
- **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData` —
эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки
Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт.
- **Пайплайн = JSON-список шагов** `{tool, params}`. Формат зафиксирован этим
решением и станет общим для браузера, wasm и CLI; реализуется в фазе 11, не в
фазе 1.
- **Пайплайн = JSON-список шагов** `{id, params}` — формат шага совпадает с
полем `Page.steps`. Формат зафиксирован этим решением и станет общим для
браузера, wasm и CLI; реализуется в фазе 11, не в фазе 1.
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из
TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций,
**perceptual diff** для canvas-зависимых (правила сравнения — раздел 3).
+16 -4
View File
@@ -106,12 +106,24 @@ non-blocking E2E не считается заменой локальному smo
`web/playwright.config.ts` запускает четыре проекта:
| Проект | Движок | Спецификации |
| ----------------- | -------- | ---------------------------------------------------------------------- |
| ----------------- | -------- | ------------------------------------------------------------------------ |
| `chromium` | Chromium | все e2e |
| `firefox` | Firefox | `pipeline`, `png-fixtures`, `text-and-verdicts` |
| `webkit` | WebKit | `pipeline`, `png-fixtures`, `text-and-verdicts` |
| `mobile-chromium` | Chromium | `navigation`, `catalog`, `generators`, `pipeline`, 390×844, `hasTouch` |
| `firefox` | Firefox | `tool-flows`, `png-fixtures`, `text-and-verdicts` |
| `webkit` | WebKit | `tool-flows`, `png-fixtures`, `text-and-verdicts` |
| `mobile-chromium` | Chromium | `navigation`, `catalog`, `generators`, `tool-flows`, 390×844, `hasTouch` |
- Название `tool-flows.spec.ts` описывает содержание: сквозной поток одного
инструмента (загрузка → правка параметра → результат → download) на наборе
представительных страниц. Пайплайна в коде нет (`docs/architecture.md`, раздел
1), поэтому спеку нельзя называть `pipeline` — при её переименовании поправить
`browserContractTests` и `mobileTests` в `web/playwright.config.ts` и
`allowDefaultProject` в `web/eslint.config.js`.
- Рядом живёт `tools-smoke.spec.ts` — дешёвый проход по выборке страниц («выдал
результат без ошибок»): по одной-двум на категорию, а не по каждому
инструменту. Остальные страницы закрыты точечными спеками (`generators`,
`text-and-verdicts`, `tool-flows`). `tool-flows` отвечает за более глубокие
проверки, `tools-smoke` — за дешёвый прогон подряд; дублировать их цели не
стоит.
- Новые engine-специфичные фичи сначала добавляются в `chromium`, потом
решается, попадает ли spec в `browserContractTests`.
- Расхождения декодеров не ужесточаются и не скрываются skip-ом: они описаны в
+29 -16
View File
@@ -3,19 +3,24 @@
> Статус: **active**. **Дата прохода: 2026-09-13** (инструменты и поля). Здесь
> только «что плохо» — наблюдения, без порядка работ и без критериев приёмки.
> Новый проход — новая дата сверху; закрытая находка не удаляется, а помечается
> ссылкой на задачу в `docs/backlog.md`.
> ссылкой на задачу в `docs/backlog.md` или на уже сделанное в коде.
>
> **Слаги сверены с реестром** на дату прохода (`docs/tools-map.md` §1).
> Отдельно: `webp-to-png` и `color-wheel-generator` в коде отсутствуют (реальная
> страница — `color-wheel-png`), а `png-info` — ещё не заведённая страница
> (`docs/backlog.md`, «Идеи»). Такие пункты переименовываются по `PAGES`, а не
> resurrect'ятся. Перед работой по любому пункту — найти соответствующую
> страницу в `web/src/lib/registry/pages/` или `docs/tools-map.md`; ищи по
> смыслу: категория в заголовке раздела + ключевое слово в пункте.
>
> **Заголовки разделов ниже — это id категорий из `web/src/lib/categories.ts`**,
> по ним и ищи страницу в `registry/pages/<group>.ts`. Раздела `convert` в
> аудите нет: конвертация проверялась вместе с остальными инструментами,
> отдельного прохода по ней не было.
>
> Как исправлять — `docs/backlog.md` (задачи) и
> `docs/plan-fields-audit-fixes.md` (порядок правок полей). Находка в аудите и
> задача в бэклоге — не дубль: пока задачи нет, находка просто висит.
>
> Ids ниже **не сверены с реестром** и часть из них в коде отсутствует
> (`webp-to-png`, `color-wheel-generator`, `png-info`, `png-to-hsl`). Перед
> работой по любому пункту — найти соответствующий инструмент в
> `web/src/lib/registry/` или `docs/tools-map.md`; несуществующий id не
> resurrect'ить, а переименовать пункт. Чтобы не сверять вручную каждый раз, ищи
> по смыслу: категория в заголовке раздела + ключевое слово в пункте →
> `docs/tools-map.md`, где id уже фактические.
## везде
@@ -23,7 +28,7 @@
- инструменты без параметров выглядят странно, пересмотреть, может добавить
какую-то надпись "нет параметров"
- удаление\маски и т.д. - надо добавить галочку сглаживание
- производительность: почему flip заметно тормозит, но нет индикации работы
- производительность: почему flip-png заметно тормозит, но нет индикации работы
инструмента (также sharpen-png)
- производительность: исследовать, есть ли смысл на инструментах геометрии
выводить рамку результата до полноценной работы (например на skew \ rotate
@@ -31,7 +36,9 @@
## аудит инструментов
### transparency
### alpha
(в UI раздел называется «Transparency» / «Прозрачность»)
- add-stroke-png: добавить галочку "расширить на толщину линии"
- find-contour-png: если толщина линии более 1px - круг боле с искажениями, надо
@@ -64,7 +71,7 @@
полупрозрачной областью - закрывает всё, по описанию должен закрывать только
небольшие дырки
### colors
### color
- two-colors-png: в range slider добавить описание что это luminance threshold
- gamma-png: проверить правильность работы - яркое изображение выглядит не
@@ -95,8 +102,10 @@
- add-border-png: прозрачность цвета (аудит полей)
- fit-on-background-png: цвет
- change-canvas-size-png: использовать position9 вместо селекта
- resize-png: по умолчанию размеры 0х0, надо поменять; выбор метода
масштабирования
- resize-png: по умолчанию размеры 0х0 — сделано, дефолт берётся из source
(`defaultFromSource`), до загрузки 1×1. Выбор метода масштабирования — поля в
схеме нет вовсе, это не правка селекта, а новое поле
(`docs/plan-fields-audit-fixes.md` §7.1)
- crop-png: offset - слишком большие значения
- rotate-png: кнопки вместо select
- flip-png: кнопки вместо select
@@ -143,7 +152,9 @@
- png-is-grayscale: результат плохо виден, нужно ли копирование на полный
вердикт? может отдельные поля - 0\1, yes\no?
### generators
### generate
(в UI раздел называется «Generate» / «Генерация»)
- color-spectrum-png: кнопки вместо select
- draw-grid-png: выбор цвета фона
@@ -166,7 +177,9 @@
## аудит полей
- у полей plate, position9 и font-style вторая подпись дублирует первую
- position offset: должны быть отдельные range slider на x и y
- position offset: сделано — отдельные числовые поля X и Y в
`fields/schema/OffsetControl.svelte` (не range slider); задача на px/% вместо
них открыта, `docs/plan-fields-audit-fixes.md` §7.1
- range slider: поле ввода, кнопки + и -, кнопка сброса на умолчание
- range slider with presets: слайдер, как выше, добавочные кнопки с пресетами
- color picker: пикер с картинки (как исходника, так и результата), прозрачность
+167 -152
View File
@@ -1,187 +1,190 @@
# Карта инструментов: реализовано · добавить · идеи
> Статус: **active**. Живой документ для сверки с каталогом.
>
> **Карта перечисляет `slug` страниц (`web/src/lib/registry/pages/`) — то есть
> адреса вида `/tools/crop-png`:** это `slug: "crop-png"` у страницы, которая
> вызывает инструмент `id: "crop"`. Внутренние имена инструментов png-интента не
> держат, поэтому они от карты отделены; модель — `docs/architecture.md`,
> раздел 3. Источник правды — реестр. Этот список навигационный: сверять перед
> использованием. Формат записи: `slug — параметры через запятую`. Сравнение с
> каталогом источника (объём, нишевые клоны серий Logo*/Icon*/Stamp*Signature*)
> — замер в `docs/plan-seo.md`, §2; здесь он не дублируется.
>
> Статус: **active**. Навигационный список: что уже есть по адресам
> `/tools/<slug>`, что можно добавить, что отложено. Источник правды — реестр
> (`web/src/lib/registry/`), модель страницы и инструмента —
> `docs/architecture.md`, раздел 3; сверяться с реестром перед использованием.
> Количество инструментов здесь не фиксируется — оно выводится из раздела 1 и из
> `TOOLS` в `registry/index.ts`, и дублировать его цифрой значит заставлять себя
> обновлять документ при каждом добавлении (`AGENTS.md`).
> `PAGES` (`AGENTS.md`).
>
> **Как читать строку.** `slug` — адрес страницы и имя скачиваемого файла;
> страница вызывает инструмент через `steps[0].id`, и у `id` нет png-интента
> (`crop-png` → `id: "crop"`, `png-to-hsl` → `id: "to-hsl"`). Имена полей — это
> id полей из `schema.fields` инструмента; составные поля (`size`, `pair`,
> `offset`, `style`, `plate`, `gradient`) в скобках раскрыты, но не выдуманы как
> отдельные поля. Сравнение с каталогом источника (объём, нишевые клоны серий
> Logo\*/Icon\*/Stamp\*) — замер в `docs/plan-seo.md` §2; здесь он не
> дублируется.
>
> **Формат строки (проверяется `web/src/lib/registry/tools-map-doc.test.ts`):**
> всё **до первого тире** `—` — это slug страниц через `/` или `,`; всё после
> тире — свободное описание параметров и не разбирается. Второй slug нельзя
> оставлять после тире, иначе он не попадёт в разбор и страница будет считаться
> неописанной.
---
## 1. Реализовано
Ids приведены к фактическим в `web/src/lib/registry/pages/`.
Slugs приведены к фактическим в `web/src/lib/registry/pages/`. Разделы идут по
файлу реестра; поля — по схеме инструмента, который страница вызывает.
**Формат строки (проверяется тестом
`web/src/lib/registry/tools-map-doc.test.ts`):** в буллите всё **до первого
тире** `—` — это slug страниц через `/` или `,`; всё после тире — свободное
описание параметров и не разбирается. Второй slug нельзя оставлять после тире,
иначе он не попадёт в разбор и страница будет считаться неописанной.
### Конвертация (`convert.ts`)
### Конвертация (`convert`)
Отдельных «из формата в PNG» инструментов **нет**: входной формат выбирается
загрузкой файла, любой инструмент принимает PNG/JPG/WebP/GIF/BMP. Все конвертеры
исходят из PNG.
- svg-to-png — width результата (текстовый вход: SVG-код)
- png-to-bmp — 24-бит, фон вместо альфы
- convert-png-to-jpg — background, quality
- convert-png-to-webp — quality
- png-to-base64 / base64-to-png — строка
- png-to-data-uri / data-uri-to-png — строка
- png-to-hex / hex-to-png — rrggbbaa по строкам / tokens + width
- png-to-bytes / bytes-to-png — десятичные RGBA-байты по строкам / tokens +
width
- png-to-rgb-values / rgb-values-to-png — rgba(r,g,b,a) по пикселям / числа +
width
- `convert-png-to-jpg` — background, quality
- `convert-png-to-webp` — quality
- `png-to-bmp` — без параметров (24-бит, фон вместо альфы)
- `svg-to-png` — width (ширина результата; вход — SVG-код)
- `png-to-base64`, `base64-to-png` — без параметров (строка)
- `png-to-data-uri`, `data-uri-to-png` — без параметров (строка)
- `png-to-hex`, `hex-to-png` — вход `width`; rrggbbaa по строкам / tokens
- `png-to-bytes`, `bytes-to-png` — вход `width`; десятичные RGBA-байты по
строкам / tokens
- `png-to-rgb-values`, `rgb-values-to-png` — вход `width`; rgba(r,g,b,a) по
пикселям / числа
### Прозрачность (`alpha.ts`)
### Прозрачность (`alpha`)
- change-png-opacity — percent
- set-alpha-channel-png — percent
- remove-alpha-channel-png — без параметров (фон `#ffffff` захардкожен, см.
`docs/plan-fields-audit-fixes.md` D1)
- extract-alpha-mask-png — без параметров
- invert-alpha-png — без параметров
- remove-background-png — color, tolerance, outerOnly, smooth
- remove-color-from-png — targetColor, tolerance
- add-stroke-png — color, thickness
- find-contour-png — color, thickness
- make-thicker-png / make-thinner-png — radius
- harden-alpha-png — threshold
- feather-edges-png — radius (размытие только альфы)
- clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного)
- despeckle-alpha-png / close-holes-png — radius
- center-by-alpha-png — без параметров
- round-corners-png — radius
- circle-mask-png — size (диаметр, % меньшей стороны), offsetX, offsetY
- square-mask-png — widthPct, heightPct, offsetX, offsetY
- star-mask-png — points, innerRadius, size, rotation, offsetX, offsetY
- wavy-mask-png — size, amplitude, waves, phase, offsetX, offsetY
- `add-stroke-png` — color, thickness
- `find-contour-png` — color, thickness
- `remove-color-from-png` — targetColor, tolerance
- `remove-background-png` — color, tolerance, outerOnly, smooth
- `make-thicker-png`, `make-thinner-png` — radius
- `harden-alpha-png` — threshold
- `feather-edges-png` — radius (размытие только альфы)
- `clean-edges-png` — radius (defringe: RGB от ближайшего непрозрачного)
- `despeckle-alpha-png`, `close-holes-png` — radius
- `round-corners-png` — radius (% от половины меньшей стороны)
- `invert-alpha-png` — без параметров
- `extract-alpha-mask-png` — без параметров
- `set-alpha-channel-png` — percent
- `remove-alpha-channel-png` — без параметров (фон `#ffffff` захардкожен,
`docs/plan-fields-audit-fixes.md` FA-D1)
- `circle-mask-png` — size (диаметр, % меньшей стороны), offset (x/y)
- `square-mask-png` — widthPct, heightPct, offset (x/y)
- `star-mask-png` — points, innerRadius, size, rotation, offset (x/y)
- `wavy-mask-png` — size, amplitude, waves, phase, offset (x/y)
### Цвет (`color.ts`)
### Цвет (`color`)
- grayscale-png / invert-colors-png / sepia-png / auto-contrast-png — без
- `grayscale-png`, `invert-colors-png`, `sepia-png`, `auto-contrast-png` — без
параметров
- adjust-brightness-contrast-png — brightness, contrast
- change-png-hue — degrees
- extract-channel-png — channel (r/g/b)
- swap-channels-png — pair (r-g/r-b/g-b)
- black-and-white-png — threshold
- posterize-png — levels
- two-colors-png — lightColor, darkColor, threshold
- temperature-png — percent
- gamma-png — value
- tint-png — color, strength
- quantize-png — colors (k, median-cut)
- decrease-color-count-png — maxColors (пресеты 2…256)
- custom-palette-png — colors (hex через запятую, ближайший цвет)
- dithering-png — colors (k), pattern (Floyd–Steinberg / Bayer 4×4)
- `adjust-brightness-contrast-png` — brightness, contrast
- `change-png-opacity` — percent
- `change-png-hue` — degrees
- `temperature-png` — percent
- `gamma-png` — value
- `tint-png` — color, strength
- `two-colors-png` — pair (светлый/тёмный), threshold
- `black-and-white-png` — threshold
- `posterize-png` — levels
- `quantize-png` — colors (k, median-cut)
- `decrease-color-count-png` — maxColors (2…256)
- `custom-palette-png` — colors (hex через запятую, ближайший цвет)
- `dithering-png` — colors (k), pattern (Floyd–Steinberg / Bayer 4×4)
- `extract-channel-png` — channel (r/g/b)
- `swap-channels-png` — pair (r-g/r-b/g-b)
### Разложение каналов (`color.ts`)
### Разложение каналов (`color`)
- png-to-hsl, png-to-hsv, png-to-hsi, png-to-cmyk, png-to-ycbcr, png-to-lab —
component (h/s/l и т.п.), display (`gray` | `space-as-rgb`)
- `png-to-hsl`, `png-to-hsv`, `png-to-hsi`, `png-to-cmyk`, `png-to-ycbcr`,
`png-to-lab` — component (h/s/l и т.п.), display (`gray` | `space-as-rgb`)
### Геометрия (`geometry.ts`)
### Геометрия (`geometry`)
- resize-png — width/height (размеры source по умолчанию; 0=авто для одной
стороны), keepAspect
- crop-png — x, y, width/height (полная область source по умолчанию)
- rotate-png — angle (90/180/270)
- flip-png — axis (h/v)
- skew-png — degX, degY
- rotate-free-png — angle
- zoom-png — scale
- shift-png — offsetX, offsetY, color фона
- add-padding-png — padding, transparent, color
- add-border-png — thickness, color
- fit-on-background-png — width, height, transparent, color
- tile-png — columns, rows
- split-into-parts-png — columns, rows (мультифайловый вывод, zip)
- trim-empty-space-png — threshold (альфа)
- change-canvas-size-png — width, height, anchor (3×3)
- change-aspect-ratio-png — ratio (пресеты), mode (crop/pad)
- swap-orientation-png — target (portrait/landscape)
- symmetric-copy-png — axis, keepSide
- `resize-png` — size (по умолчанию размер source, 0 по одной стороне = авто),
keepAspect
- `crop-png` — x, y, size (по умолчанию вся область source)
- `rotate-png` — angle (90/180/270)
- `rotate-free-png` — angle
- `flip-png` — axis (h/v)
- `skew-png` — degX, degY
- `zoom-png` — scale
- `shift-png` — offsetX, offsetY, color
- `add-padding-png` — padding, transparent, color
- `add-border-png` — thickness, color
- `fit-on-background-png` — size, transparent, color
- `tile-png` — columns, rows
- `split-into-parts-png` — columns, rows (мультифайловый вывод, zip)
- `trim-empty-space-png` — threshold (по альфе)
- `change-canvas-size-png` — size, anchor (3×3)
- `change-aspect-ratio-png` — ratio (пресеты), mode (crop/pad)
- `swap-orientation-png` — target (portrait/landscape)
- `symmetric-copy-png` — axis, keepSide
- `center-by-alpha-png` — без параметров
### Фильтры (`filters.ts`)
### Фильтры (`filters`)
- blur-png — radius
- sharpen-png — strength
- vignette-png — strength
- jpeg-artifacts-png — quality (имитация пережатия jpg/webp)
- pixelate-png — blockSize (закрывает и их Color Blocks)
- randomize-pixels-png — blockSize, seed
- add-noise-png — amount, mode (mono/color), seed
- silhouette-png — color, threshold
- `blur-png` — radius
- `sharpen-png` — strength
- `vignette-png` — strength
- `jpeg-artifacts-png` — quality (имитация пережатия jpg/webp; поле
функциональное, у инструмента нет `output` — `docs/plan-seo.md` S1f)
- `pixelate-png` — blockSize
- `randomize-pixels-png` — blockSize, seed
- `add-noise-png` — amount, mode (mono/color), seed
- `silhouette-png` — color, threshold
### Анализ и вердикты (`analyze.ts`)
### Анализ и вердикты (`analyze`)
- png-file-size — без параметров (DOM-инструмент: кодирует в PNG и отдаёт
- `png-file-size` — без параметров (DOM-инструмент: кодирует в PNG и отдаёт
размер)
- png-is-transparent / png-is-grayscale / png-orientation — текстовый вердикт
- verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре
- `png-is-transparent`, `png-is-grayscale`, `png-orientation` — без параметров,
текстовый вердикт
- `verify-is-png` — без параметров (вход — текст: base64/data-uri), вердикт по
сигнатуре
### Маски по свойствам пикселей (`analyze.ts`)
### Маски по свойствам пикселей (`analyze`)
- show-transparent-png — color, opacity (подсветка прозрачных/полупрозрачных)
- show-grayscale-pixels-png — tolerance, mode (binary/highlight),
highlightColor, highlightOpacity
- show-color-pixels-png — tolerance, mode, highlightColor, highlightOpacity
- light-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity
- dark-pixel-mask-png — threshold, mode, highlightColor, highlightOpacity
- unique-color-mask-png — rarity (макс. повторов), mode, highlightColor,
highlightOpacity
- extract-color-from-png — color, tolerance (оставить близкие, остальное
прозрачным)
- `show-transparent-png` — mode, color, opacity
- `show-grayscale-pixels-png` — mode, tolerance, color, opacity
- `show-color-pixels-png` — mode, tolerance, color, opacity
- `light-pixel-mask-png` — mode, threshold, color, opacity
- `dark-pixel-mask-png` — mode, threshold, color, opacity
- `unique-color-mask-png` — mode, rarity (макс. повторов), color, opacity
- `extract-color-from-png` — color, tolerance
### Генерация (`generate.ts`)
### Генерация (`generate`)
- create-empty-png — width, height, transparent, color
- single-color-png — width, height, color
- random-noise-png — width, height, seed
- linear-gradient-png — width, height, fromColor, toColor, direction
- color-spectrum-png — width, height, direction, saturation, lightness
- random-colors-png — width, height, blockSize, seed
- draw-grid-png — width, height, cols, rows, lineWidth, color, transparentBg
- placeholder-png — width, height, backgroundColor, color, showText
- blend-two-png — pair (from/to), width
- step-colors-png — pair (from/to), steps, width
- emoji-to-png — emoji, size
- text-to-png — text, fontSize, font, bold, color, transparentBg,
- `create-empty-png` — size, transparent, color
- `single-color-png` — size, color
- `random-noise-png` — size, seed
- `linear-gradient-png` — size, gradient (от/к, направление)
- `color-spectrum-png` — size, direction, saturation, lightness
- `random-colors-png` — size, blockSize, seed
- `draw-grid-png` — size, cols, rows, lineWidth, color, transparentBg
- `placeholder-png` — size, backgroundColor, color, showText
- `blend-two-png` — pair (от/к), width
- `step-colors-png` — pair (от/к), steps, width, layout (grid/strip)
- `color-wheel-png` — size, lightness
- `mix-colors-png` — colors, width
- `sort-colors-png` — colors, order (hue/brightness/saturation), width, layout
- `emoji-to-png` — emoji, size
- `text-to-png` — text, style (шрифт/размер/цвет/жирность), transparentBg,
backgroundColor, padding
- color-wheel-png — size, lightness
- complementary-png / triadic-png / tetradic-png / analogous-png /
monochromatic-png / shades-png — общая база палитры: baseColor, width, layout;
`shades-png` дополнительно count, depth
- mix-colors-png — colors, width
- sort-colors-png — colors, порядок (hue/brightness/saturation), width
- `complementary-png`, `triadic-png`, `tetradic-png`, `analogous-png`,
`monochromatic-png`, `shades-png` — baseColor, width, layout; у `analogous`
ещё spread и count, у `monochromatic` — count и range, у `shades` — count и
depth
### Текст (`text.ts`)
### Текст (`text`)
- add-text-png — text, fontSize, color, font, bold, position (3×3), margin,
plate, plateColor, plateOpacity
- date-stamp-png — format, fontSize, color, font, bold, position, margin, plate,
plateColor, plateOpacity
- watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY,
font, bold
- `add-text-png` — text, style (шрифт/размер/цвет/жирность), position (3×3),
margin, plate (плашка, её цвет и прозрачность)
- `date-stamp-png` — format, style, position, margin, plate
- `watermark-tile-png` — text, style, opacity, angle, stepX, stepY
---
## 2. Можно добавить — из onlinepngtools
Всё из этого списка **в реестре отсутствует**.
Всё из этого списка **в реестре отсутствует** — это план, а не факт. Список
проверяется `web/src/lib/registry/tools-map-doc.test.ts` в обратную сторону:
реализованный slug, оставшийся здесь, роняет тест.
### Разложение каналов — остаток
@@ -216,8 +219,11 @@ Ids приведены к фактическим в `web/src/lib/registry/pages/
### Сжатие и качество
- compress, reduce-to-size, pick-a-color, extract-barcode — **не реализованы**;
заведены в `docs/backlog.md` (reduce-to-size зависит от UX-райза «Download»).
Пипетка для выбора цвета уже есть в превью, отдельная страница не планируется.
заведены в `docs/backlog.md`. Сжатие до целевого размера — прежде всего
селектор формата в Download (S1f, `docs/plan-seo.md` §4): для lossy это
quality-ручка, а для PNG — квантование палитры. Нужен ли отдельный инструмент
под этот интент, решается по вордстату, имя страницы пока не выбрано. Пипетка
для выбора цвета уже есть в превью, отдельная страница не планируется.
Извлечение штрихкодов — HARD, вне планов.
---
@@ -227,12 +233,21 @@ Ids приведены к фактическим в `web/src/lib/registry/pages/
Развёрнутые описания этих идей живут в `docs/backlog.md`, раздел «Идеи»; здесь
только список.
Буллиты этого раздела, как и раздела 2, перечисляют **несуществующие** страницы:
уже реализованные в них не называются, иначе тест решит, что slug пора
переносить в раздел 1. Реализованные примеры пишутся прозой между буллитами, как
ниже.
- **Region-инструменты** — нужен selection-компонент на превью: censor-region,
erase-region, pixelate-area, blur-area, sharpen-area, reverse-colors-area.
Один раз делаем selection — получаем шесть инструментов.
- **Мультифайловый выход** — механизм «результат = набор файлов» (zip)
реализован на `split-into-parts-png`. Остальные 1→many: gif-to-frames,
separate-colors, multiply-grid-as-files — подключаются по мере нужды.
- **Мультифайловый выход** — остальные потребители механизма «результат = набор
файлов»: gif-to-frames, separate-colors, multiply-grid-as-files — подключаются
по мере нужды.
Механизм уже реализован на `split-into-parts-png` (`docs/architecture.md`,
раздел 7), он в разделе 1.
- **Анимационные эффекты** — slow-reveal, fade-in/out, disappearing, scrolling:
это видео/GIF на выходе, а не PNG. Отдельное решение о формате результата.
- **HARD-хвост** — glitch-art, extract-signature, handwritten→digital,
@@ -240,6 +255,6 @@ Ids приведены к фактическим в `web/src/lib/registry/pages/
- **Нишевые серии** (logo/icon/stamp/signature — замер в шапке документа) —
сознательно не копируем: это обычные операции над конкретным контентом, у нас
они доступны через базовые инструменты + цепочки.
- **Входные «из формата в PNG»** (jpg-to-png, webp-to-png) — решение и его SEO-
последствия описаны в разделе 1 выше; отдельные страницы под эти интенты —
предмет решения в `docs/plan-seo.md` §5.
- **Входные «из формата в PNG»** (jpg-to-png, webp-to-png) — решение и его
SEO-последствия описаны в `docs/plan-seo.md` §5; отдельные страницы под эти
интенты — предмет решения там же.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "easy-png-tools",
"version": "0.1.0",
"version": "0.1.1",
"description": "",
"main": "index.js",
"scripts": {
+16
View File
@@ -0,0 +1,16 @@
import { readFileSync, writeFileSync } from "node:fs";
const version = process.argv[2];
if (!version) {
console.error("usage: node scripts/sync-version.mjs <version>");
process.exit(1);
}
for (const file of ["package.json", "web/package.json"]) {
const raw = readFileSync(file, "utf8");
const manifest = JSON.parse(raw);
manifest.version = version;
const indent = raw.match(/(?<=\n)([^\S\n]+)"/)?.[1] ?? 2;
const eol = raw.endsWith("\n") ? "\n" : "";
writeFileSync(file, JSON.stringify(manifest, null, indent) + eol);
}
+6 -2
View File
@@ -175,8 +175,9 @@ report-only (`warn`), baseline — 0 срабатываний.
- **Неиспользуемые токены**: выводится **варнинг** (определены, но нигде не
используются) — выход он не меняет. Проверка видит только ссылки `var(--x)`,
поэтому чтение токена из JS (`getPropertyValue`) она не учтёт; решение
оставить проверку неблокирующей зафиксировано в `docs/quality-gates.md`.
Провалами (exit 1) считаются parity, hct-авторство и резолв `var()`.
оставить проверку неблокирующей — `docs/decisions.md`, раздел 6 (как оно
выглядит в гейте — `docs/quality-gates.md`). Провалами (exit 1) считаются
parity, hct-авторство и резолв `var()`.
## Тесты кастомных линт-правил
@@ -187,6 +188,9 @@ report-only (`warn`), baseline — 0 срабатываний.
`no-token-definition-in-svelte`) через `RuleTester` со строковыми кейсами;
`no-undefined-in-svelte` — через `Linter` API, т.к. читает словарь токенов из
`__fixtures__/src/app.css` (не из реального `src/app.css`).
- `__tests__/interface-props.test.ts` и
`__tests__/no-string-union-alias.test.ts` — конвенции плагина `conventions`
(интерфейс пропсов, запрет union-алиасов).
- Хелпер `__tests__/helpers.ts` собирает `Linter` с `cwd = __fixtures__` —
`process.cwd()` не трогается, реальные `src/` не читаются.
+1 -1
View File
@@ -63,7 +63,7 @@ export default tseslint.config(
"e2e/helpers/page.ts",
"e2e/navigation.spec.ts",
"e2e/catalog.spec.ts",
"e2e/pipeline.spec.ts",
"e2e/tool-flows.spec.ts",
"e2e/text-and-verdicts.spec.ts",
"e2e/tools-smoke.spec.ts",
"e2e/generators.spec.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "web",
"private": true,
"version": "0.0.1",
"version": "0.1.1",
"type": "module",
"scripts": {
"dev": "vite dev",
+2 -2
View File
@@ -1,7 +1,7 @@
import { defineConfig } from "playwright/test";
const browserContractTests = [
"**/pipeline.spec.ts",
"**/tool-flows.spec.ts",
"**/png-fixtures.spec.ts",
"**/text-and-verdicts.spec.ts",
];
@@ -10,7 +10,7 @@ const mobileTests = [
"**/navigation.spec.ts",
"**/catalog.spec.ts",
"**/generators.spec.ts",
"**/pipeline.spec.ts",
"**/tool-flows.spec.ts",
];
export default defineConfig({
-14
View File
@@ -4,20 +4,6 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" />
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-MEQJ7JHRQQ"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-MEQJ7JHRQQ");
</script>
<script>
(() => {
try {
@@ -1,62 +0,0 @@
<script lang="ts">
import { Upload } from "@lucide/svelte";
import { t } from "$lib/i18n/t";
import DownloadButton from "./ui/DownloadButton.svelte";
interface Props {
inputMode: "image" | "text" | "none";
canDownload: boolean;
running: boolean;
onupload: (file: File) => void;
ondownload: () => void;
}
let { inputMode, canDownload, running, onupload, ondownload }: Props =
$props();
</script>
<div class="actions">
{#if inputMode === "image"}
<label class="btn upload">
<Upload size={14} />
{t("actions.openImage")}
<input
type="file"
accept="image/*"
onchange={(e) => {
const f = (e.target as HTMLInputElement).files?.[0];
if (f) onupload(f);
}}
/>
</label>
{/if}
{#if canDownload}
<DownloadButton
label={t("actions.downloadResult")}
onclick={ondownload}
disabled={running}
/>
{/if}
</div>
<style>
.actions {
display: flex;
align-items: center;
gap: var(--space-l);
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
gap: var(--space-m);
padding: var(--space-m) var(--space-l);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-m);
font: var(--font-size-s) var(--font-mono);
color: var(--color-text-muted);
cursor: pointer;
}
.upload input {
display: none;
}
</style>
@@ -1,5 +1,6 @@
<script lang="ts">
import { Funnel, Search } from "@lucide/svelte";
import ChipButton from "$lib/components/ui/ChipButton.svelte";
import { t } from "$lib/i18n/t";
interface Props {
@@ -33,14 +34,11 @@
<div class="catalog-filters">
<Funnel size={15} />
{#each FILTERS as f (f.value)}
<button
type="button"
class:active={category === f.value}
aria-pressed={category === f.value}
<ChipButton
label={f.label}
active={category === f.value}
onclick={() => (category = f.value)}
>
{f.label}
</button>
/>
{/each}
</div>
</div>
@@ -81,20 +79,4 @@
flex-wrap: wrap;
color: var(--color-text-muted);
}
.catalog-filters button {
border: var(--size-border) solid var(--color-border);
background: var(--color-panel);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
text-transform: uppercase;
padding: var(--space-s) var(--space-m);
border-radius: var(--radius-m);
cursor: pointer;
}
.catalog-filters button.active {
background: var(--color-main);
color: var(--color-background);
border-color: var(--color-main);
}
</style>
@@ -2,7 +2,7 @@
import { resolve } from "$app/paths";
import { ArrowUpRight } from "@lucide/svelte";
import type { Component } from "svelte";
import Icon from "./ui/Icon.svelte";
import Icon from "$lib/components/ui/Icon.svelte";
interface Props {
title: string;
@@ -1,7 +1,7 @@
<script lang="ts">
import { Copy } from "@lucide/svelte";
import { t } from "$lib/i18n/t";
import IconButton from "./ui/IconButton.svelte";
import IconButton from "$lib/components/ui/IconButton.svelte";
interface Props {
code: string;
@@ -1,5 +1,6 @@
<script lang="ts">
import { Ellipsis, GripVertical, X } from "@lucide/svelte";
import IconButton from "$lib/components/ui/IconButton.svelte";
import { t } from "$lib/i18n/t";
import type { Snippet } from "svelte";
@@ -26,14 +27,13 @@
</div>
<div class="step-tools">
{#if tools}{@render tools()}{/if}
<button
type="button"
class="step-remove"
aria-label={t("chain.removeStepAria")}
<IconButton
icon={X}
label={t("chain.removeStepAria")}
variant="clear"
size="s"
onclick={() => onremove?.()}
>
<X size={16} />
</button>
/>
<Ellipsis size={17} />
</div>
</div>
@@ -81,24 +81,10 @@
align-items: center;
gap: var(--space-l);
}
.step-remove {
border: 0;
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
line-height: inherit;
& :global(svg) {
display: block;
line-height: inherit;
}
}
.step-card :global(.drag) {
margin-top: var(--space-xl);
color: var(--color-border);
}
.step-remove:hover {
color: var(--color-danger);
}
.step-body > :not(.step-heading) {
padding: var(--space-l);
}
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Component, Snippet } from "svelte";
import Icon from "./ui/Icon.svelte";
import Icon from "$lib/components/ui/Icon.svelte";
interface Props {
title: string;
@@ -1,6 +1,6 @@
<script lang="ts">
import StatusDot from "./StatusDot.svelte";
import MonoLabel from "./MonoLabel.svelte";
import MonoLabel from "$lib/components/display/MonoLabel.svelte";
interface Props {
label: string;
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Snippet } from "svelte";
import MonoLabel from "../MonoLabel.svelte";
import MonoLabel from "$lib/components/display/MonoLabel.svelte";
interface Props {
label: string;
@@ -1,4 +1,5 @@
<script lang="ts">
import ChipButton from "$lib/components/ui/ChipButton.svelte";
import { t } from "$lib/i18n/t";
interface Preset {
@@ -49,10 +50,12 @@
</div>
<div class="angle-presets">
{#each presets as preset (preset.value)}
<button
class:active={preset.value === value}
onclick={() => onchange(preset.value)}>{preset.label}</button
>
<ChipButton
label={preset.label}
variant="outline"
active={preset.value === value}
onclick={() => onchange(preset.value)}
/>
{/each}
</div>
</div>
@@ -88,22 +91,4 @@
grid-template-columns: repeat(4, 1fr);
gap: var(--space-m);
}
.angle-presets button {
padding: var(--space-m) 0;
color: var(--color-text-muted);
background: var(--color-background);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
cursor: pointer;
}
.angle-presets button:hover {
color: var(--color-text);
border-color: var(--color-main);
}
.angle-presets button.active {
color: var(--color-main);
border-color: var(--color-main);
}
</style>
@@ -1,5 +1,6 @@
<script lang="ts">
import type { CheckboxSpec, FieldSpec } from "$lib/registry-schema";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -15,30 +16,16 @@
);
</script>
<label class="control checkbox-control">
<span>{label}</span>
<Control element="label" {label} row>
<input
type="checkbox"
class="checkbox-field"
checked={current}
onchange={(e) => onchange?.((e.target as HTMLInputElement).checked)}
/>
</label>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.checkbox-control {
display: flex;
justify-content: space-between;
align-items: center;
}
.checkbox-field {
width: var(--space-xl);
height: var(--space-xl);
@@ -1,5 +1,7 @@
<script lang="ts">
import type { ColorSpec, FieldSpec } from "$lib/registry-schema";
import ColorSwatchInput from "./ColorSwatchInput.svelte";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -13,47 +15,6 @@
const current = $derived(typeof value === "string" ? value : sp.default);
</script>
<label class="control color-control">
<span>{label}</span>
<span class="color-row">
<span class="swatch" style="background:{current}"></span>
<input
type="color"
value={current}
oninput={(e) => onchange?.((e.target as HTMLInputElement).value)}
/>
</span>
</label>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.color-control > span:first-child {
justify-content: flex-start;
}
.color-row {
display: flex;
align-items: center;
gap: var(--space-m);
}
.swatch {
width: var(--space-xxl);
height: var(--space-xxl);
border-radius: var(--radius-s);
border: var(--size-border) solid var(--color-border);
}
input[type="color"] {
width: 100%;
height: var(--space-xxl);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
background: var(--color-background);
cursor: pointer;
}
</style>
<Control element="label" {label}>
<ColorSwatchInput value={current} onchange={(v) => onchange?.(v)} />
</Control>
@@ -5,6 +5,8 @@
ColorPairSpec,
FieldSpec,
} from "$lib/registry-schema";
import ColorSwatchInput from "./ColorSwatchInput.svelte";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -28,45 +30,26 @@
}
</script>
<div class="control color-pair-control">
<span class="pair-label">{label}</span>
<Control {label} caps>
<div class="pair-field">
<label class="pair-axis">
<span class="swatch" style="background:{current.from}"></span>
<input
type="color"
<ColorSwatchInput
value={current.from}
oninput={(e) => setColor("from", (e.target as HTMLInputElement).value)}
onchange={(v) => setColor("from", v)}
/>
<span class="axis-label">{t("ui.from")}</span>
</label>
<label class="pair-axis">
<span class="swatch" style="background:{current.to}"></span>
<input
type="color"
<ColorSwatchInput
value={current.to}
oninput={(e) => setColor("to", (e.target as HTMLInputElement).value)}
onchange={(v) => setColor("to", v)}
/>
<span class="axis-label">{t("ui.to")}</span>
</label>
</div>
</div>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.pair-label {
color: var(--color-text);
font-size: var(--font-size-s);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
.pair-field {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -74,25 +57,6 @@
}
.pair-axis {
display: grid;
grid-template-columns: var(--space-xxl) 1fr;
align-items: center;
gap: var(--space-m);
}
.swatch {
width: var(--space-xxl);
height: var(--space-xxl);
border-radius: var(--radius-s);
border: var(--size-border) solid var(--color-border);
}
.pair-axis input[type="color"] {
width: 100%;
height: var(--space-xxl);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
background: var(--color-background);
cursor: pointer;
}
.axis-label {
grid-column: 1 / -1;
}
</style>
@@ -0,0 +1,44 @@
<script lang="ts">
interface Props {
value: string;
disabled?: boolean;
onchange?: (value: string) => void;
}
let { value, disabled = false, onchange }: Props = $props();
</script>
<span class="swatch-field">
<span class="swatch" style="background:{value}"></span>
<input
type="color"
{value}
{disabled}
oninput={(e) => onchange?.((e.target as HTMLInputElement).value)}
/>
</span>
<style>
.swatch-field {
display: grid;
grid-template-columns: var(--space-xxl) 1fr;
align-items: center;
gap: var(--space-m);
}
.swatch {
width: var(--space-xxl);
height: var(--space-xxl);
border-radius: var(--radius-s);
border: var(--size-border) solid var(--color-border);
}
input[type="color"] {
width: 100%;
height: var(--space-xxl);
padding: 0;
background: var(--color-background);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
cursor: pointer;
}
</style>
@@ -1,10 +1,14 @@
<script lang="ts">
import { X } from "@lucide/svelte";
import Button from "$lib/components/ui/Button.svelte";
import IconButton from "$lib/components/ui/IconButton.svelte";
import { t } from "$lib/i18n/t";
import type {
ColorList,
ColorListSpec,
FieldSpec,
} from "$lib/registry-schema";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -41,8 +45,7 @@
}
</script>
<div class="control colors-control">
<span class="ctrl-label">{label}</span>
<Control {label} caps>
<div class="chips">
{#each current as hex, i (i)}
<div class="chip">
@@ -55,38 +58,28 @@
/>
</label>
<span class="hex">{hex}</span>
<button
type="button"
class="remove"
<IconButton
icon={X}
label={t("ui.removeColor")}
variant="clear"
size="s"
onclick={() => removeColor(i)}
disabled={current.length <= 1}
aria-label={t("ui.removeColor")}
>
×
</button>
/>
</div>
{/each}
</div>
<button type="button" class="add" onclick={addColor}
>{t("ui.addColor")}</button
>
<div class="add-row">
<Button
label={t("ui.addColor")}
variant="outline"
size="s"
onclick={addColor}
/>
</div>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.ctrl-label {
color: var(--color-text);
font-size: var(--font-size-s);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
.chips {
display: flex;
flex-wrap: wrap;
@@ -121,39 +114,8 @@
color: var(--color-text-muted);
font-size: var(--font-size-s);
}
.remove {
.add-row {
display: flex;
align-items: center;
justify-content: center;
width: var(--space-xl);
height: var(--space-xl);
border: none;
border-radius: var(--radius-s);
background: none;
color: var(--color-text-muted);
font-size: var(--font-size-s);
cursor: pointer;
}
.remove:hover:not(:disabled) {
color: var(--color-text);
background: var(--color-background-muted);
}
.remove:disabled {
opacity: 0.4;
cursor: default;
}
.add {
justify-self: start;
padding: var(--space-s) var(--space-m);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-m);
background: none;
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
cursor: pointer;
}
.add:hover {
color: var(--color-text);
border-color: var(--color-text-muted);
justify-content: flex-start;
}
</style>
@@ -0,0 +1,57 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
label: string;
element?: "div" | "label";
caps?: boolean;
row?: boolean;
trailing?: Snippet;
children: Snippet;
}
let {
label,
element = "div",
caps = false,
row = false,
trailing,
children,
}: Props = $props();
</script>
<svelte:element this={element} class="control" class:control--row={row}>
<span class="label" class:caps>
{label}
{#if trailing}{@render trailing()}{/if}
</span>
{@render children()}
</svelte:element>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.control--row {
display: flex;
justify-content: space-between;
align-items: center;
}
.label {
display: flex;
justify-content: space-between;
}
.caps {
color: var(--color-text);
font-size: var(--font-size-s);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
</style>
@@ -5,6 +5,7 @@
DimensionSpec,
FieldSpec,
} from "$lib/registry-schema";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -34,8 +35,7 @@
}
</script>
<div class="control dimension-control">
<span class="dimension-label">{label}</span>
<Control {label} caps>
<div class="dimension-field">
<label class="dimension-axis">
<span>{t("ui.width")}</span>
@@ -60,23 +60,9 @@
/>
</label>
</div>
</div>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.dimension-label {
color: var(--color-text);
font-size: var(--font-size-s);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
.dimension-field {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -6,6 +6,8 @@
FontStyle,
FontStyleSpec,
} from "$lib/registry-schema";
import ColorSwatchInput from "./ColorSwatchInput.svelte";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -43,8 +45,7 @@
];
</script>
<div class="control font-style-control">
<span class="fs-label">{label}</span>
<Control {label} caps>
<div class="fs-grid">
<label class="fs-cell">
<span class="fs-sub">{t("ui.font")}</span>
@@ -73,15 +74,10 @@
</label>
<label class="fs-cell">
<span class="fs-sub">{t("ui.color")}</span>
<span class="fs-color">
<span class="swatch" style="background:{current.color}"></span>
<input
class="fs-color-input"
type="color"
<ColorSwatchInput
value={current.color}
oninput={(e) => set("color", (e.target as HTMLInputElement).value)}
onchange={(v) => set("color", v)}
/>
</span>
</label>
<label class="fs-cell fs-bold">
<span class="fs-sub">{t("ui.bold")}</span>
@@ -92,23 +88,9 @@
/>
</label>
</div>
</div>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.fs-label {
color: var(--color-text);
font-size: var(--font-size-s);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
.fs-grid {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -122,8 +104,7 @@
font-size: var(--font-size-s);
}
.fs-select,
.fs-input,
.fs-color-input {
.fs-input {
width: 100%;
padding: var(--space-m) var(--space-l);
background: var(--color-background);
@@ -132,23 +113,6 @@
color: var(--color-text);
font: var(--font-size-s) var(--font-mono);
}
.fs-color {
display: grid;
grid-template-columns: var(--space-xxl) 1fr;
align-items: center;
gap: var(--space-m);
}
.swatch {
width: var(--space-xxl);
height: var(--space-xxl);
border-radius: var(--radius-s);
border: var(--size-border) solid var(--color-border);
}
.fs-color-input {
height: var(--space-xxl);
padding: 0;
cursor: pointer;
}
.fs-bold {
display: flex;
justify-content: space-between;
@@ -1,5 +1,7 @@
<script lang="ts">
import AngleControl from "./AngleControl.svelte";
import ColorSwatchInput from "./ColorSwatchInput.svelte";
import Control from "./Control.svelte";
import { t } from "$lib/i18n/t";
import type { FieldSpec, Gradient, GradientSpec } from "$lib/registry-schema";
@@ -29,46 +31,21 @@
}
</script>
<div class="control gradient-control">
<span class="gradient-label">{label}</span>
<Control {label} caps>
<div class="gradient-colors">
<label class="color-field">
<span>{t("ui.from")}</span>
<span class="swatch" style="background:{current.from}"></span>
<input
type="color"
value={current.from}
oninput={(e) => set("from", (e.target as HTMLInputElement).value)}
/>
<ColorSwatchInput value={current.from} onchange={(v) => set("from", v)} />
</label>
<label class="color-field">
<span>{t("ui.to")}</span>
<span class="swatch" style="background:{current.to}"></span>
<input
type="color"
value={current.to}
oninput={(e) => set("to", (e.target as HTMLInputElement).value)}
/>
<ColorSwatchInput value={current.to} onchange={(v) => set("to", v)} />
</label>
</div>
<AngleControl value={current.angle} onchange={(a) => set("angle", a)} />
</div>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.gradient-label {
color: var(--color-text);
font-size: var(--font-size-s);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
.gradient-colors {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -76,22 +53,8 @@
}
.color-field {
display: grid;
grid-template-columns: auto var(--space-xxl) 1fr;
grid-template-columns: auto 1fr;
align-items: center;
gap: var(--space-m);
}
.swatch {
width: var(--space-xxl);
height: var(--space-xxl);
border-radius: var(--radius-s);
border: var(--size-border) solid var(--color-border);
}
.color-field input[type="color"] {
width: 100%;
height: var(--space-xxl);
padding: 0;
background: var(--color-background);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
}
</style>
@@ -1,5 +1,6 @@
<script lang="ts">
import type { FieldSpec, Offset, OffsetSpec } from "$lib/registry-schema";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -23,8 +24,7 @@
}
</script>
<div class="control offset-control">
<span class="offset-label">{label}</span>
<Control {label} caps>
<div class="offset-field">
<label class="offset-axis">
<span>X</span>
@@ -49,23 +49,9 @@
/>
</label>
</div>
</div>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.offset-label {
color: var(--color-text);
font-size: var(--font-size-s);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
.offset-field {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -1,6 +1,8 @@
<script lang="ts">
import { t } from "$lib/i18n/t";
import type { FieldSpec, Plate, PlateSpec } from "$lib/registry-schema";
import ColorSwatchInput from "./ColorSwatchInput.svelte";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -28,8 +30,7 @@
}
</script>
<div class="control plate-control">
<span class="plate-label">{label}</span>
<Control {label} caps>
<div class="plate-row {current.enabled ? '' : 'off'}">
<label class="plate-enabled">
<span>{t("ui.backingPlate")}</span>
@@ -39,15 +40,11 @@
onchange={(e) => set("enabled", (e.target as HTMLInputElement).checked)}
/>
</label>
<label class="plate-color">
<span class="swatch" style="background:{current.color}"></span>
<input
type="color"
<ColorSwatchInput
value={current.color}
disabled={!current.enabled}
oninput={(e) => set("color", (e.target as HTMLInputElement).value)}
onchange={(v) => set("color", v)}
/>
</label>
<label class="plate-opacity">
<span>
{t("ui.opacity")}
@@ -65,23 +62,9 @@
/>
</label>
</div>
</div>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.plate-label {
color: var(--color-text);
font-size: var(--font-size-s);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
.plate-row {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -101,26 +84,6 @@
height: var(--space-xl);
accent-color: var(--color-main);
}
.plate-color {
display: grid;
grid-template-columns: var(--space-xxl) 1fr;
align-items: center;
gap: var(--space-m);
}
.swatch {
width: var(--space-xxl);
height: var(--space-xxl);
border-radius: var(--radius-s);
border: var(--size-border) solid var(--color-border);
}
.plate-color input[type="color"] {
width: 100%;
height: var(--space-xxl);
padding: 0;
background: var(--color-background);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-s);
}
.plate-opacity {
display: grid;
gap: var(--space-m);
@@ -5,6 +5,7 @@
type FieldSpec,
type Position9Spec,
} from "$lib/registry-schema";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -23,8 +24,7 @@
);
</script>
<div class="control position-control">
<span class="position-label">{label}</span>
<Control {label} caps>
<div class="grid">
{#each POSITION9_VALUES as position (position)}
<button
@@ -38,23 +38,9 @@
</button>
{/each}
</div>
</div>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.position-label {
color: var(--color-text);
font-size: var(--font-size-s);
letter-spacing: var(--space-text-l);
text-transform: uppercase;
}
.grid {
display: inline-grid;
grid-template-columns: repeat(3, var(--space-xl));
@@ -1,5 +1,6 @@
<script lang="ts">
import type { FieldSpec, NumberSpec, SliderSpec } from "$lib/registry-schema";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -19,11 +20,10 @@
}
</script>
<label class="control">
<span>
{label}
<Control element="label" {label}>
{#snippet trailing()}
<output>{current}</output>
</span>
{/snippet}
<input
aria-label={label}
type="range"
@@ -33,22 +33,10 @@
value={current}
oninput={handle}
/>
</label>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.control > span {
display: flex;
justify-content: space-between;
}
.control output {
output {
color: var(--color-text);
}
input[type="range"] {
@@ -1,6 +1,7 @@
<script lang="ts">
import { optionLabel } from "$lib/i18n/schema-tool-strings";
import type { FieldSpec, SelectSpec } from "$lib/registry-schema";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -20,8 +21,7 @@
);
</script>
<label class="control">
<span>{label}</span>
<Control element="label" {label}>
<select
class="select-field"
value={current}
@@ -33,17 +33,9 @@
</option>
{/each}
</select>
</label>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.select-field {
width: 100%;
padding: var(--space-m) var(--space-l);
@@ -1,5 +1,6 @@
<script lang="ts">
import type { FieldSpec, TextSpec } from "$lib/registry-schema";
import Control from "./Control.svelte";
interface Props {
label: string;
@@ -13,8 +14,7 @@
const current = $derived(typeof value === "string" ? value : sp.default);
</script>
<label class="control">
<span>{label}</span>
<Control element="label" {label}>
<input
class="text-field"
type="text"
@@ -22,17 +22,9 @@
placeholder={sp.placeholder}
oninput={(e) => onchange?.((e.target as HTMLInputElement).value)}
/>
</label>
</Control>
<style>
.control {
display: grid;
gap: var(--space-m);
margin-bottom: var(--space-xl);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
}
.text-field {
width: 100%;
padding: var(--space-m) var(--space-l);
+7 -1
View File
@@ -1,5 +1,11 @@
<script lang="ts">
import { version } from "../../../../package.json";
const versionLabel = `v${version}`;
</script>
<footer class="preview-footer">
<span>easy-png-tools <b class="version">v0.1.0</b></span>
<span>easy-png-tools <b class="version">{versionLabel}</b></span>
<span>© 2026</span>
</footer>
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Snippet } from "svelte";
import PanelHeading from "../PanelHeading.svelte";
import PanelHeading from "./PanelHeading.svelte";
interface Props {
title: string;
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Snippet } from "svelte";
import MonoLabel from "./MonoLabel.svelte";
import MonoLabel from "$lib/components/display/MonoLabel.svelte";
interface Props {
title: string;
+3 -3
View File
@@ -1,8 +1,8 @@
<script lang="ts">
import { resolve } from "$app/paths";
import LangToggle from "../ui/LangToggle.svelte";
import ThemeToggle from "../ui/ThemeToggle.svelte";
import TopNav from "../ui/TopNav.svelte";
import LangToggle from "$lib/components/ui/LangToggle.svelte";
import ThemeToggle from "$lib/components/ui/ThemeToggle.svelte";
import TopNav from "./TopNav.svelte";
interface Props {
theme: "light" | "dark";
@@ -0,0 +1,37 @@
<script lang="ts">
import DownloadButton from "$lib/components/ui/DownloadButton.svelte";
import UploadButton from "$lib/components/ui/UploadButton.svelte";
import { t } from "$lib/i18n/t";
interface Props {
inputMode: "image" | "text" | "none";
canDownload: boolean;
running: boolean;
onupload: (file: File) => void;
ondownload: () => void;
}
let { inputMode, canDownload, running, onupload, ondownload }: Props =
$props();
</script>
<div class="actions">
{#if inputMode === "image"}
<UploadButton label={t("actions.openImage")} onfile={onupload} />
{/if}
{#if canDownload}
<DownloadButton
label={t("actions.downloadResult")}
onclick={ondownload}
disabled={running}
/>
{/if}
</div>
<style>
.actions {
display: flex;
align-items: center;
gap: var(--space-l);
flex-wrap: wrap;
}
</style>
@@ -9,19 +9,20 @@
import { t } from "$lib/i18n/t";
import { RotateCcw } from "@lucide/svelte";
import type { Component } from "svelte";
import DimensionField from "./fields/DimensionField.svelte";
import CheckboxControl from "./fields/schema/CheckboxControl.svelte";
import ColorControl from "./fields/schema/ColorControl.svelte";
import ColorPairControl from "./fields/schema/ColorPairControl.svelte";
import ColorsControl from "./fields/schema/ColorsControl.svelte";
import RangeControl from "./fields/schema/RangeControl.svelte";
import SelectControl from "./fields/schema/SelectControl.svelte";
import TextControl from "./fields/schema/TextControl.svelte";
import OffsetControl from "./fields/schema/OffsetControl.svelte";
import PositionControl from "./fields/schema/PositionControl.svelte";
import FontStyleControl from "./fields/schema/FontStyleControl.svelte";
import PlateControl from "./fields/schema/PlateControl.svelte";
import GradientControl from "./fields/schema/GradientControl.svelte";
import Button from "$lib/components/ui/Button.svelte";
import DimensionField from "$lib/components/fields/schema/DimensionField.svelte";
import CheckboxControl from "$lib/components/fields/schema/CheckboxControl.svelte";
import ColorControl from "$lib/components/fields/schema/ColorControl.svelte";
import ColorPairControl from "$lib/components/fields/schema/ColorPairControl.svelte";
import ColorsControl from "$lib/components/fields/schema/ColorsControl.svelte";
import RangeControl from "$lib/components/fields/schema/RangeControl.svelte";
import SelectControl from "$lib/components/fields/schema/SelectControl.svelte";
import TextControl from "$lib/components/fields/schema/TextControl.svelte";
import OffsetControl from "$lib/components/fields/schema/OffsetControl.svelte";
import PositionControl from "$lib/components/fields/schema/PositionControl.svelte";
import FontStyleControl from "$lib/components/fields/schema/FontStyleControl.svelte";
import PlateControl from "$lib/components/fields/schema/PlateControl.svelte";
import GradientControl from "$lib/components/fields/schema/GradientControl.svelte";
interface FieldControlProps {
label: string;
@@ -86,10 +87,13 @@
{/each}
<div class="panel-foot">
<button class="reset-btn" onclick={onreset}>
<RotateCcw size={14} />
{t("ui.reset")}
</button>
<Button
icon={RotateCcw}
label={t("ui.reset")}
variant="clear"
size="s"
onclick={onreset}
/>
<span class="auto-note"><i></i> {t("ui.autoUpdate")}</span>
</div>
@@ -120,19 +124,6 @@
padding-top: var(--space-l);
border-top: var(--size-border) solid var(--color-border);
}
.reset-btn {
display: inline-flex;
align-items: center;
gap: var(--space-m);
background: none;
border: none;
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
cursor: pointer;
}
.reset-btn:hover {
color: var(--color-text);
}
.auto-note i {
width: var(--size-border-thick);
height: var(--size-border-thick);
@@ -1,9 +1,9 @@
<script lang="ts">
import MetaList from "$lib/components/MetaList.svelte";
import { buildSchemaPreviewModel } from "$lib/components/schema-preview-model";
import SchemaActions from "$lib/components/SchemaActions.svelte";
import SchemaResultTile from "$lib/components/SchemaResultTile.svelte";
import SchemaSourceTile from "$lib/components/SchemaSourceTile.svelte";
import MetaList from "$lib/components/display/MetaList.svelte";
import { buildSchemaPreviewModel } from "./schema-preview-model";
import SchemaActions from "./SchemaActions.svelte";
import SchemaResultTile from "./SchemaResultTile.svelte";
import SchemaSourceTile from "./SchemaSourceTile.svelte";
import type { PixelImage } from "$lib/core/types";
import { t } from "$lib/i18n/t";
import type { FileResult, InputMode, ResultKind } from "$lib/registry";
@@ -5,7 +5,7 @@
import type { FileResult, ResultKind } from "$lib/registry";
import { Check } from "@lucide/svelte";
import SchemaTextResult from "./SchemaTextResult.svelte";
import PreviewTile from "./layout/PreviewTile.svelte";
import PreviewTile from "./PreviewTile.svelte";
interface Props {
resultKind?: ResultKind;
@@ -3,7 +3,7 @@
import type { PixelImage } from "$lib/core/types";
import { t } from "$lib/i18n/t";
import type { InputMode } from "$lib/registry";
import PreviewTile from "./layout/PreviewTile.svelte";
import PreviewTile from "./PreviewTile.svelte";
import SchemaTextSource from "./SchemaTextSource.svelte";
interface Props {
@@ -2,7 +2,7 @@
import { verdictText, verdictTone } from "$lib/i18n/schema-tool-strings";
import { t } from "$lib/i18n/t";
import { Copy, Download, FileText } from "@lucide/svelte";
import IconButton from "./ui/IconButton.svelte";
import IconButton from "$lib/components/ui/IconButton.svelte";
interface Props {
value: string;
@@ -1,5 +1,6 @@
<script lang="ts">
import { ArrowUp, Upload } from "@lucide/svelte";
import Button from "$lib/components/ui/Button.svelte";
import { t } from "$lib/i18n/t";
interface Props {
@@ -36,15 +37,21 @@
}}></textarea>
<div class="actions">
{#if onsample}
<button type="button" class="secondary" onclick={onsample}>
<Upload size={14} />
{t("textSource.trySample")}
</button>
<Button
icon={Upload}
label={t("textSource.trySample")}
variant="outline"
size="s"
onclick={onsample}
/>
{/if}
<button type="button" class="render" onclick={onrender} {disabled}>
<ArrowUp size={14} />
{t("textSource.render")}
</button>
<Button
icon={ArrowUp}
label={t("textSource.render")}
size="s"
onclick={onrender}
{disabled}
/>
</div>
</div>
@@ -76,36 +83,4 @@
gap: var(--space-m);
justify-content: flex-end;
}
.render {
display: inline-flex;
align-items: center;
gap: var(--space-m);
padding: var(--space-m) var(--space-l);
border: var(--size-border) solid var(--color-main);
border-radius: var(--radius-m);
background: var(--color-main);
color: var(--color-background);
font: var(--font-size-s) var(--font-mono);
cursor: pointer;
}
.render:disabled {
opacity: 0.6;
cursor: default;
}
.secondary {
display: inline-flex;
align-items: center;
gap: var(--space-m);
padding: var(--space-m) var(--space-l);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-m);
background: none;
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
cursor: pointer;
}
.secondary:hover {
color: var(--color-text);
border-color: var(--color-text-muted);
}
</style>
@@ -1,6 +1,6 @@
<script lang="ts">
import SchemaFields from "$lib/components/SchemaFields.svelte";
import SchemaPreview from "$lib/components/SchemaPreview.svelte";
import SchemaFields from "./SchemaFields.svelte";
import SchemaPreview from "./SchemaPreview.svelte";
import { debounce } from "$lib/core/debounce";
import { ToolError, type ErrorVars } from "$lib/core/errors";
import { decodeFile, downloadBlob, encode } from "$lib/core/io";
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { makeImage } from "../core/test-helpers";
import { makeImage } from "../../core/test-helpers";
import {
buildSchemaPreviewModel,
type PreviewTranslate,
@@ -1,5 +1,5 @@
import type { PixelImage } from "../core/types";
import type { FileResult, InputMode, ResultKind } from "../registry";
import type { PixelImage } from "$lib/core/types";
import type { FileResult, InputMode, ResultKind } from "$lib/registry";
export type PreviewTranslate = (key: string) => string;
+46 -2
View File
@@ -1,6 +1,10 @@
<script lang="ts">
import type { Component } from "svelte";
import { ButtonVariantDefine, type ButtonVariant } from "../define";
import {
ButtonVariantDefine,
type ButtonVariant,
type ButtonSize,
} from "./define";
import Icon from "./Icon.svelte";
interface Props {
@@ -8,21 +12,27 @@
ariaLabel?: string;
onclick?: () => void;
variant?: ButtonVariant;
size?: ButtonSize;
icon?: Component<{ size?: number; class?: string }>;
disabled?: boolean;
type?: "button" | "submit";
}
let {
label,
ariaLabel = label,
onclick,
variant = ButtonVariantDefine.PRIMARY,
size = "m",
icon,
disabled = false,
type = "button",
}: Props = $props();
</script>
<button
class="btn btn--{variant}"
class="btn btn--{variant} btn--{size}"
class:btn--icon={!label}
{type}
{disabled}
aria-label={ariaLabel}
onclick={() => onclick?.()}
@@ -51,12 +61,36 @@
color: var(--color-background);
}
.btn--s {
gap: var(--space-m);
padding: var(--space-m) var(--space-l);
border-width: var(--size-border);
border-radius: var(--radius-m);
font-size: var(--font-size-s);
font-family: var(--font-mono);
font-weight: normal;
}
.btn--s.btn--icon {
padding: var(--space-s);
}
.btn--outline {
background: transparent;
color: var(--color-main-tint);
border-color: var(--color-main-tint);
}
.btn--s.btn--outline {
color: var(--color-text-muted);
border-color: var(--color-border);
}
.btn--s.btn--outline:hover {
color: var(--color-text);
border-color: var(--color-text-muted);
}
.btn--accent {
background: var(--color-accent);
color: var(--color-background);
@@ -69,6 +103,16 @@
border-color: var(--color-danger);
}
.btn--clear {
background: none;
border-color: transparent;
color: var(--color-text-muted);
}
.btn--clear:hover {
color: var(--color-text);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
@@ -0,0 +1,56 @@
<script lang="ts">
interface Props {
label: string;
active?: boolean;
variant?: "filled" | "outline";
onclick?: () => void;
}
let { label, active = false, variant = "filled", onclick }: Props = $props();
</script>
<button
type="button"
class="chip chip--{variant}"
class:active
aria-pressed={active}
onclick={() => onclick?.()}
>
{label}
</button>
<style>
.chip {
border: var(--size-border) solid var(--color-border);
background: var(--color-panel);
color: var(--color-text-muted);
font: var(--font-size-s) var(--font-mono);
letter-spacing: var(--space-text-m);
text-transform: uppercase;
padding: var(--space-s) var(--space-m);
border-radius: var(--radius-m);
cursor: pointer;
}
.chip--filled.active {
background: var(--color-main);
color: var(--color-background);
border-color: var(--color-main);
}
.chip--outline {
padding: var(--space-m) 0;
background: var(--color-background);
border-radius: var(--radius-s);
text-transform: none;
}
.chip--outline:hover {
color: var(--color-text);
border-color: var(--color-main);
}
.chip--outline.active {
color: var(--color-main);
border-color: var(--color-main);
}
</style>
+16 -2
View File
@@ -1,6 +1,10 @@
<script lang="ts">
import type { Component } from "svelte";
import { ButtonVariantDefine, type ButtonVariant } from "../define";
import {
ButtonVariantDefine,
type ButtonVariant,
type ButtonSize,
} from "./define";
import Button from "./Button.svelte";
interface Props {
@@ -8,6 +12,7 @@
label: string;
onclick?: () => void;
variant?: ButtonVariant;
size?: ButtonSize;
disabled?: boolean;
}
@@ -16,8 +21,17 @@
label,
onclick,
variant = ButtonVariantDefine.PRIMARY,
size = "m",
disabled = false,
}: Props = $props();
</script>
<Button label="" {onclick} {variant} {disabled} {icon} ariaLabel={label} />
<Button
label=""
{onclick}
{variant}
{size}
{disabled}
{icon}
ariaLabel={label}
/>
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { Moon, Sun } from "@lucide/svelte";
import { t } from "$lib/i18n/t";
import IconButton from "../ui/IconButton.svelte";
import IconButton from "./IconButton.svelte";
interface Props {
theme: "light" | "dark";
@@ -0,0 +1,47 @@
<script lang="ts">
import { Upload } from "@lucide/svelte";
import Icon from "./Icon.svelte";
interface Props {
label: string;
accept?: string;
onfile: (file: File) => void;
}
let { label, accept = "image/*", onfile }: Props = $props();
</script>
<label class="upload">
<Icon icon={Upload} size={14} />
{label}
<input
type="file"
{accept}
onchange={(e) => {
const f = (e.target as HTMLInputElement).files?.[0];
if (f) onfile(f);
}}
/>
</label>
<style>
.upload {
display: inline-flex;
align-items: center;
gap: var(--space-m);
padding: var(--space-m) var(--space-l);
border: var(--size-border) solid var(--color-border);
border-radius: var(--radius-m);
font: var(--font-size-s) var(--font-mono);
color: var(--color-text-muted);
cursor: pointer;
}
.upload:hover {
color: var(--color-text);
border-color: var(--color-text-muted);
}
.upload input {
display: none;
}
</style>
@@ -8,3 +8,11 @@ export const ButtonVariantDefine = {
export type ButtonVariant =
(typeof ButtonVariantDefine)[keyof typeof ButtonVariantDefine];
export const ButtonSizeDefine = {
M: "m",
S: "s",
} as const;
export type ButtonSize =
(typeof ButtonSizeDefine)[keyof typeof ButtonSizeDefine];
+28
View File
@@ -59,4 +59,32 @@ describe("renderShape", () => {
expect(shifted.data[3]).toBe(0);
expect(shifted.data[4 + 3]).toBe(255);
});
// Регрессия старого бага square-mask: полный размер без смещения обязан
// сохранять квадратную картинку целиком.
it("квадрат 100% без смещения сохраняет всё квадратное изображение", () => {
const out = renderShape(
makeImage(4, 4, new Array(16).fill([255, 255, 255, 255])),
boxTest(0.5, 0.5),
);
for (let i = 3; i < out.data.length; i += 4) expect(out.data[i]).toBe(255);
});
// Смещение — доля свободного места: ±0.5 прижимает фигуру к краю при любом
// её размере, фигура не уезжает за холст.
it("максимальное смещение прижимает фигуру к краю", () => {
const out = renderShape(
makeImage(4, 4, new Array(16).fill([255, 255, 255, 255])),
boxTest(0.25, 0.25),
0.5,
0.5,
{ x: 0.25, y: 0.25 },
);
const opaqueAt = (x: number, y: number) =>
out.data[(y * 4 + x) * 4 + 3] === 255;
expect(opaqueAt(3, 3)).toBe(true);
expect(opaqueAt(2, 2)).toBe(true);
expect(opaqueAt(1, 1)).toBe(false);
expect(opaqueAt(2, 1)).toBe(false);
});
});
+15 -3
View File
@@ -3,6 +3,12 @@ import { createPixelImage } from "./types";
export type ShapeTest = (nx: number, ny: number) => boolean;
/** Полуразмер фигуры в нормированных координатах теста, по осям. */
export interface ShapeExtent {
x: number;
y: number;
}
/**
* Координаты теста: нормированные к половине меньшей стороны изображения,
* центр в (0,0), ось Y вниз как в пикселях.
@@ -54,18 +60,24 @@ export function wavyTest(
/**
* Вырезает фигуру из изображения: внутри фигуры сохраняются исходные пиксели
* (с их альфой), снаружи альфа обнуляется. Смещение задаётся в долях меньшей стороны.
* (с их альфой), снаружи альфа обнуляется. Смещение задаётся в долях свободного
* места по каждой оси: ±0.5 прижимает фигуру к краю независимо от её размера,
* поэтому фигура никогда не уезжает за холст. Свободное место считается от
* extent (полуразмера фигуры); без extent смещение считается от точки.
*/
export function renderShape(
img: PixelImage,
test: ShapeTest,
offsetXFrac = 0,
offsetYFrac = 0,
extent: ShapeExtent = { x: 0, y: 0 },
): PixelImage {
const out = createPixelImage(img.width, img.height);
const minDim = Math.min(img.width, img.height);
const cx = img.width / 2 + offsetXFrac * minDim;
const cy = img.height / 2 + offsetYFrac * minDim;
const freeX = Math.max(0, img.width / (2 * minDim) - extent.x);
const freeY = Math.max(0, img.height / (2 * minDim) - extent.y);
const cx = img.width / 2 + offsetXFrac * 2 * freeX * minDim;
const cy = img.height / 2 + offsetYFrac * 2 * freeY * minDim;
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const di = (y * img.width + x) * 4;
+57 -13
View File
@@ -8,30 +8,35 @@ const MAP_PATH = fileURLToPath(
);
const SECTION_START = /^## 1\.\s/;
const SECTION_END = /^## 2\.\s/;
const SECTION_PLANNED = /^## 2\.\s/;
// Все slug страниц содержат дефис, поэтому формат строгий: это отсекает слова
// описания вроде `median-cut` из «colors (k, median-cut)».
const ID_SHAPE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
const ID_TOKEN = /[a-z][a-z0-9]*(?:-[a-z0-9]+)+/g;
type Mention = { id: string; line: number };
/**
* Буллиты раздела «1. Реализовано» вместе с номером первой строки.
* Перенесённые строки приклеиваются к буллету: иначе slug, оказавшийся на
* следующей строке, молча выпадал бы из разбора и страница считалась бы
* неописанной.
* Буллиты раздела вместе с номером первой строки. Перенесённые строки
* приклеиваются к буллету: иначе slug, оказавшийся на следующей строке, молча
* выпадал бы из разбора и страница считалась бы неописанной.
*/
function implementedBullets(
function sectionBullets(
markdown: string,
startPattern: RegExp,
endPattern: RegExp | null,
): { text: string; line: number }[] {
const lines = markdown.split("\n");
const start = lines.findIndex((l) => SECTION_START.test(l));
const end = lines.findIndex((l) => SECTION_END.test(l));
const start = lines.findIndex((l) => startPattern.test(l));
const end =
endPattern === null
? lines.length
: lines.findIndex((l) => endPattern.test(l));
if (start === -1 || end === -1 || end <= start) {
throw new Error(
"docs/tools-map.md: не найден раздел «1. Реализовано» или «2.» — " +
"тест разбирает только реализованные инструменты",
`docs/tools-map.md: не найден раздел по ${startPattern} — тест не может ` +
"определить границы разделов карты",
);
}
@@ -47,7 +52,7 @@ function implementedBullets(
!/^#{1,6}\s/.test(text) &&
!/^[-*]\s/.test(text);
if (isBullet) {
bullets.push({ text: text.slice(2), line: start + i + 1 });
bullets.push({ text: text.slice(2), line: i + 1 });
} else if (isContinuation) {
previous.text += ` ${text}`;
}
@@ -61,7 +66,11 @@ function implementedBullets(
*/
function mentionedIds(markdown: string): Mention[] {
const found: Mention[] = [];
for (const { text, line } of implementedBullets(markdown)) {
for (const { text, line } of sectionBullets(
markdown,
SECTION_START,
SECTION_PLANNED,
)) {
const head = text.split("—")[0];
for (const raw of head.split(/[/,]/)) {
const id = raw.trim().replace(/`/g, "");
@@ -71,14 +80,35 @@ function mentionedIds(markdown: string): Mention[] {
return found;
}
/**
* Плановые разделы «2. Можно добавить» и «3. Идеи» устроены прозой: slug стоит
* где угодно в буллите, после тире и в скобках. Поэтому берём все токены
* нужной формы из всего текста буллита, а не из «головы» до тире.
*/
function plannedIds(markdown: string): Mention[] {
const found: Mention[] = [];
for (const { text, line } of sectionBullets(
markdown,
SECTION_PLANNED,
null,
)) {
for (const id of text.replace(/`/g, "").match(ID_TOKEN) ?? []) {
found.push({ id, line });
}
}
return found;
}
describe("docs/tools-map.md ↔ страницы реестра", () => {
const markdown = readFileSync(MAP_PATH, "utf8");
const mentioned = mentionedIds(markdown);
const planned = plannedIds(markdown);
const registrySlugs = new Set(PAGES.map((page) => page.slug));
it("разбирает непустой список slug (парсер не сломался молча)", () => {
expect(implementedBullets(markdown).length).toBeGreaterThan(0);
expect(mentioned.length).toBeGreaterThan(0);
expect(new Set(mentioned.map((m) => m.id)).size).toBeGreaterThan(50);
expect(new Set(planned.map((m) => m.id)).size).toBeGreaterThan(10);
});
it("в карте нет slug, которых нет в реестре", () => {
@@ -116,4 +146,18 @@ describe("docs/tools-map.md ↔ страницы реестра", () => {
}
expect(dupes, `повторяющиеся slug:\n${dupes.join("\n")}`).toEqual([]);
});
it("плановый slug уже не реализован (перенос из «2./3.» в «1.»)", () => {
const stale = planned.filter((m) => registrySlugs.has(m.id));
expect(
stale,
stale
.map(
(m) =>
`${m.id} (docs/tools-map.md:${m.line}) — страница уже есть в реестре, ` +
"перенеси её из планового раздела в «1. Реализовано»",
)
.join("\n"),
).toEqual([]);
});
});
+4
View File
@@ -139,6 +139,7 @@ const circleMask: Tool<CircleMaskParams> = {
circleTest(p.size / 200),
p.offset.x / 100,
p.offset.y / 100,
{ x: p.size / 200, y: p.size / 200 },
),
),
};
@@ -197,6 +198,7 @@ const squareMask: Tool<SquareMaskParams> = {
boxTest(p.widthPct / 200, p.heightPct / 200),
p.offset.x / 100,
p.offset.y / 100,
{ x: p.widthPct / 200, y: p.heightPct / 200 },
),
),
};
@@ -271,6 +273,7 @@ const starMask: Tool<StarMaskParams> = {
starTest(p.points, p.innerRadius / 100, p.size / 200, p.rotation),
p.offset.x / 100,
p.offset.y / 100,
{ x: p.size / 200, y: p.size / 200 },
),
),
};
@@ -345,6 +348,7 @@ const wavyMask: Tool<WavyMaskParams> = {
wavyTest(p.size / 200, p.amplitude / 200, p.waves, p.phase),
p.offset.x / 100,
p.offset.y / 100,
{ x: (p.size + p.amplitude) / 200, y: (p.size + p.amplitude) / 200 },
),
),
};
+11
View File
@@ -31,6 +31,10 @@
let crumb = $derived(toCrumb(page.url.pathname));
// GA подключается только в production-деплое: флаг выставляется в
// deploy-workflow, локальные и preview-сборки тег не получают.
const gaEnabled = import.meta.env.PUBLIC_GA_ENABLED === "true";
function toCrumb(path: string): string {
if (path === "/") return "/ " + t("header.home");
if (path === "/list-tools") return "/ " + t("header.catalog");
@@ -46,6 +50,13 @@
<svelte:head>
<link rel="icon" href={`${resolve("/")}favicon.svg`} />
{#if gaEnabled}
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-MEQJ7JHRQQ"
></script>
<script src={`${resolve("/")}ga.js`}></script>
{/if}
</svelte:head>
<main class="preview-root" data-theme={theme}>
+4 -4
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { PREVIEW_GROUPS, PREVIEW_TOTAL } from "$lib/catalog";
import CatalogGroup from "$lib/components/CatalogGroup.svelte";
import CatalogHeader from "$lib/components/CatalogHeader.svelte";
import CatalogToolbar from "$lib/components/CatalogToolbar.svelte";
import ToolCard from "$lib/components/ToolCard.svelte";
import CatalogGroup from "$lib/components/catalog/CatalogGroup.svelte";
import CatalogHeader from "$lib/components/catalog/CatalogHeader.svelte";
import CatalogToolbar from "$lib/components/catalog/CatalogToolbar.svelte";
import ToolCard from "$lib/components/catalog/ToolCard.svelte";
import {
pageDescription,
pageTitle,
+33 -10
View File
@@ -1,26 +1,28 @@
<script lang="ts">
import CheckerCanvas from "$lib/components/CheckerCanvas.svelte";
import CodeBlock from "$lib/components/CodeBlock.svelte";
import EmptyState from "$lib/components/EmptyState.svelte";
import ToolCard from "$lib/components/catalog/ToolCard.svelte";
import CheckerCanvas from "$lib/components/display/CheckerCanvas.svelte";
import CodeBlock from "$lib/components/display/CodeBlock.svelte";
import ImageCard from "$lib/components/display/ImageCard.svelte";
import MetaList from "$lib/components/display/MetaList.svelte";
import SettingGroup from "$lib/components/display/SettingGroup.svelte";
import SettingsFooter from "$lib/components/display/SettingsFooter.svelte";
import StepCard from "$lib/components/display/StepCard.svelte";
import EmptyState from "$lib/components/feedback/EmptyState.svelte";
import StatusLine from "$lib/components/feedback/StatusLine.svelte";
import CheckboxField from "$lib/components/fields/CheckboxField.svelte";
import ColorField from "$lib/components/fields/ColorField.svelte";
import SelectField from "$lib/components/fields/SelectField.svelte";
import SliderField from "$lib/components/fields/SliderField.svelte";
import TextField from "$lib/components/fields/TextField.svelte";
import ImageCard from "$lib/components/ImageCard.svelte";
import Panel from "$lib/components/layout/Panel.svelte";
import MetaList from "$lib/components/MetaList.svelte";
import SettingGroup from "$lib/components/SettingGroup.svelte";
import SettingsFooter from "$lib/components/SettingsFooter.svelte";
import StatusLine from "$lib/components/StatusLine.svelte";
import StepCard from "$lib/components/StepCard.svelte";
import ToolCard from "$lib/components/ToolCard.svelte";
import Badge from "$lib/components/ui/Badge.svelte";
import Button from "$lib/components/ui/Button.svelte";
import ChipButton from "$lib/components/ui/ChipButton.svelte";
import DownloadButton from "$lib/components/ui/DownloadButton.svelte";
import IconButton from "$lib/components/ui/IconButton.svelte";
import Segmented from "$lib/components/ui/Segmented.svelte";
import Toggle from "$lib/components/ui/Toggle.svelte";
import UploadButton from "$lib/components/ui/UploadButton.svelte";
import { Download, ImageOff, Plus, Trash2 } from "@lucide/svelte";
import { PAGES } from "$lib/registry";
@@ -52,6 +54,26 @@
<Button variant="outline" onclick={() => {}} label="Outline" />
<Button variant="accent" onclick={() => {}} label="Accent" />
<Button variant="danger" onclick={() => {}} label="Danger" />
<Button variant="clear" onclick={() => {}} label="Clear" />
</SettingGroup>
<SettingGroup label="size s">
<Button size="s" onclick={() => {}} label="Primary" />
<Button size="s" variant="outline" onclick={() => {}} label="Outline" />
<Button size="s" variant="clear" onclick={() => {}} label="Clear" />
<IconButton size="s" icon={Plus} label="Add" onclick={() => {}} />
<IconButton
size="s"
variant="clear"
icon={Trash2}
label="Delete"
onclick={() => {}}
/>
</SettingGroup>
<SettingGroup label="chips">
<ChipButton label="All" active onclick={() => {}} />
<ChipButton label="Convert" onclick={() => {}} />
<ChipButton label="0°" variant="outline" active onclick={() => {}} />
<ChipButton label="90°" variant="outline" onclick={() => {}} />
</SettingGroup>
<SettingGroup label="states">
<Button disabled onclick={() => {}} label="Disabled" />
@@ -71,6 +93,7 @@
</SettingGroup>
</div>
<SettingsFooter>
<UploadButton label="Open image" onfile={() => {}} />
<DownloadButton label="Download PNG" onclick={() => {}} />
</SettingsFooter>
</Panel>
+4 -4
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { PREVIEW_GROUPS, PREVIEW_TOTAL } from "$lib/catalog";
import CatalogGroup from "$lib/components/CatalogGroup.svelte";
import CatalogHeader from "$lib/components/CatalogHeader.svelte";
import CatalogToolbar from "$lib/components/CatalogToolbar.svelte";
import ToolCard from "$lib/components/ToolCard.svelte";
import CatalogGroup from "$lib/components/catalog/CatalogGroup.svelte";
import CatalogHeader from "$lib/components/catalog/CatalogHeader.svelte";
import CatalogToolbar from "$lib/components/catalog/CatalogToolbar.svelte";
import ToolCard from "$lib/components/catalog/ToolCard.svelte";
import {
pageDescription,
pageTitle,
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import EmptyState from "$lib/components/EmptyState.svelte";
import SchemaToolView from "$lib/components/SchemaToolView.svelte";
import EmptyState from "$lib/components/feedback/EmptyState.svelte";
import SchemaToolView from "$lib/components/schema/SchemaToolView.svelte";
import { t } from "$lib/i18n/t";
import { getPageBySlug, getTool } from "$lib/registry";
import { SlidersHorizontal as ToolIcon } from "@lucide/svelte";
+6
View File
@@ -0,0 +1,6 @@
window.dataLayer = window.dataLayer || [];
function gtag() {
window.dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-MEQJ7JHRQQ");
+2
View File
@@ -11,6 +11,8 @@ const base: "" | `/${string}` =
: (`/${rawBase.replace(/^\/+/, "").replace(/\/+$/, "")}` as `/${string}`);
export default defineConfig({
// PUBLIC_* попадают в import.meta.env — флаги окружения вроде GA.
envPrefix: ["VITE_", "PUBLIC_"],
ssr: {
noExternal: ["@lucide/svelte"],
},

Some files were not shown because too many files have changed in this diff Show More