mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-14 17:38:19 +00:00
Compare commits
8
Commits
8cb3f1a8f0
...
260a738ce4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
260a738ce4 | ||
|
|
e1fc11cb54 | ||
|
|
1db83d12ff | ||
|
|
ddab996b50 | ||
|
|
5990310e99 | ||
|
|
c4eab38866 | ||
|
|
f89811c116 | ||
|
|
1d337f065b |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -26,19 +26,28 @@
|
||||
|
||||
Читать только нужную строку, а не все документы подряд.
|
||||
|
||||
| Задача | Документ |
|
||||
| --------------------------------------------- | ---------------------------------- |
|
||||
| Слои, поток данных, новый инструмент/поле | `docs/architecture.md` |
|
||||
| Какие проверки запускать, пороги, CI | `docs/quality-gates.md` |
|
||||
| Правка самих гейтов: «почему так, а не иначе» | `docs/decisions.md` |
|
||||
| Порядок работы, нагрузка на машину | `docs/agent-workflow.md` |
|
||||
| Locator-ы, fixtures, browser matrix | `docs/testing-strategy.md` |
|
||||
| Детали lint-правил и токенов | `web/eslint-plugins/README.md` |
|
||||
| Задачи и идеи | `docs/backlog.md` |
|
||||
| Актуальный список инструментов | `docs/tools-map.md` |
|
||||
| Правка полей и раскладки по волнам | `docs/plan-fields-audit-fixes.md` |
|
||||
| Ручная приёмка UI | `docs/checklist-manual-testing.md` |
|
||||
| Индекс остальных документов | `docs/README.md` |
|
||||
| Задача | Документ |
|
||||
| ---------------------------------------------- | ---------------------------------- |
|
||||
| Слои, поток данных, новый инструмент/поле | `docs/architecture.md` |
|
||||
| Какие проверки запускать, пороги, CI | `docs/quality-gates.md` |
|
||||
| Правка самих гейтов: «почему так, а не иначе» | `docs/decisions.md` |
|
||||
| Порядок работы, нагрузка на машину | `docs/agent-workflow.md` |
|
||||
| Locator-ы, fixtures, browser matrix | `docs/testing-strategy.md` |
|
||||
| Детали lint-правил и токенов | `web/eslint-plugins/README.md` |
|
||||
| Задачи и идеи | `docs/backlog.md` |
|
||||
| Актуальный список инструментов | `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
@@ -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).
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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). Всё остальное
|
||||
неразрешимое — ошибка.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
+123
-102
@@ -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()`)
|
||||
|
||||
---
|
||||
|
||||
@@ -137,64 +144,68 @@ 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
@@ -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
@@ -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-паттерн, риск
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -105,13 +105,25 @@ 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` |
|
||||
| Проект | Движок | Спецификации |
|
||||
| ----------------- | -------- | ------------------------------------------------------------------------ |
|
||||
| `chromium` | Chromium | все e2e |
|
||||
| `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
@@ -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
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "easy-png-tools",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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/` не читаются.
|
||||
|
||||
|
||||
@@ -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
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
+5
-23
@@ -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>
|
||||
+1
-1
@@ -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
-1
@@ -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;
|
||||
+7
-21
@@ -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
-1
@@ -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
-1
@@ -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,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>
|
||||
<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>
|
||||
+3
-17
@@ -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"
|
||||
value={current.color}
|
||||
oninput={(e) => set("color", (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</span>
|
||||
<ColorSwatchInput
|
||||
value={current.color}
|
||||
onchange={(v) => set("color", v)}
|
||||
/>
|
||||
</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"
|
||||
value={current.color}
|
||||
disabled={!current.enabled}
|
||||
oninput={(e) => set("color", (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</label>
|
||||
<ColorSwatchInput
|
||||
value={current.color}
|
||||
disabled={!current.enabled}
|
||||
onchange={(v) => set("color", v)}
|
||||
/>
|
||||
<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);
|
||||
|
||||
@@ -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,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
-1
@@ -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;
|
||||
@@ -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>
|
||||
+21
-30
@@ -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);
|
||||
+5
-5
@@ -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";
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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 {
|
||||
+1
-1
@@ -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;
|
||||
+15
-40
@@ -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>
|
||||
+2
-2
@@ -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
-1
@@ -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,
|
||||
+2
-2
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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,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];
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
window.dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "G-MEQJ7JHRQQ");
|
||||
@@ -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
Reference in New Issue
Block a user