Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c7736dbc3 | ||
|
|
902ff61e8d |
@@ -0,0 +1,39 @@
|
|||||||
|
name: Verify
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: verify-${{ gitea.repository }}-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 45
|
||||||
|
env:
|
||||||
|
CI: "true"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
cache: npm
|
||||||
|
- name: Select declared npm version
|
||||||
|
run: npm install --global npm@11.17.0
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Audit runtime dependencies
|
||||||
|
run: npm audit --omit=dev --audit-level=moderate
|
||||||
|
- name: Check, test, and build
|
||||||
|
run: npm run check
|
||||||
|
- name: Install browser engines
|
||||||
|
run: npx playwright install --with-deps chromium firefox webkit
|
||||||
|
- name: Browser tests
|
||||||
|
run: npm run test:browser
|
||||||
@@ -3,6 +3,25 @@
|
|||||||
All notable changes follow Keep a Changelog. Versions follow Semantic
|
All notable changes follow Keep a Changelog. Versions follow Semantic
|
||||||
Versioning.
|
Versioning.
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-09-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added source-fidelity duplicate and delete operations for safe, ID-free
|
||||||
|
subtrees, with reference guards and undo/redo integration.
|
||||||
|
- Declared SVG/project input, SVG/SVGZ/raster/project export and optional worker
|
||||||
|
capability profiles for Toolbox handoff.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Updated DOMPurify to 3.4.14 so the sanitized SVG projection includes the
|
||||||
|
upstream namespace-confusion fix tracked as GHSA-55q2-fjhq-7xh7.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Added the requested exact reference-implementation review and adoption
|
||||||
|
record.
|
||||||
|
|
||||||
## [0.1.0] - 2026-08-02
|
## [0.1.0] - 2026-08-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -19,4 +38,5 @@ Versioning.
|
|||||||
- Deterministic standalone release packaging and Toolbox Portal assembly smoke
|
- Deterministic standalone release packaging and Toolbox Portal assembly smoke
|
||||||
test.
|
test.
|
||||||
|
|
||||||
|
[0.2.0]: https://git.add-ideas.de/lotobo/svg-tools/releases/tag/v0.2.0
|
||||||
[0.1.0]: https://git.add-ideas.de/lotobo/svg-tools/releases/tag/v0.1.0
|
[0.1.0]: https://git.add-ideas.de/lotobo/svg-tools/releases/tag/v0.1.0
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ editing projection, a canvas overlay and inspectors from that source. Files are
|
|||||||
processed in the browser; the app has no backend, upload endpoint, telemetry or
|
processed in the browser; the app has no backend, upload endpoint, telemetry or
|
||||||
runtime CDN dependency.
|
runtime CDN dependency.
|
||||||
|
|
||||||
Version 0.1.0 delivers the structured viewer/editor milestone (MVP 1) and
|
Version 0.2.0 delivers the structured viewer/editor milestone (MVP 1) and
|
||||||
tested vertical slices of the path, transform, reference, optimization,
|
tested vertical slices of the path, transform, reference, optimization,
|
||||||
security, accessibility, animation and export milestones. The later milestone
|
security, accessibility, animation and export milestones. The later milestone
|
||||||
lists below remain a roadmap, not a claim of complete vector-editor coverage.
|
lists below remain a roadmap, not a claim of complete vector-editor coverage.
|
||||||
@@ -26,7 +26,7 @@ lists below remain a roadmap, not a claim of complete vector-editor coverage.
|
|||||||
- No editor namespace or persistent editor attribute is added to normal SVG
|
- No editor namespace or persistent editor attribute is added to normal SVG
|
||||||
exports.
|
exports.
|
||||||
|
|
||||||
## Implemented in 0.1.0
|
## Implemented in 0.2.0
|
||||||
|
|
||||||
- open, drop and paste SVG; magic-byte SVGZ import; new-document templates;
|
- open, drop and paste SVG; magic-byte SVGZ import; new-document templates;
|
||||||
- XML editor, ranged diagnostics, structure tree, filtering and synchronized
|
- XML editor, ranged diagnostics, structure tree, filtering and synchronized
|
||||||
@@ -34,6 +34,8 @@ lists below remain a roadmap, not a claim of complete vector-editor coverage.
|
|||||||
- safe canvas, zoom, pan, grid, selected bounds and last-valid state;
|
- safe canvas, zoom, pan, grid, selected bounds and last-valid state;
|
||||||
- document dimensions, `viewBox`, `preserveAspectRatio`, arbitrary attributes,
|
- document dimensions, `viewBox`, `preserveAspectRatio`, arbitrary attributes,
|
||||||
fill, stroke, opacity, rotate, flip, translate, scale and skew;
|
fill, stroke, opacity, rotate, flip, translate, scale and skew;
|
||||||
|
- undoable subtree duplicate/delete for ID-free nodes, with duplicate-ID and
|
||||||
|
external-reference guards rather than silently breaking the document;
|
||||||
- exact undo/redo and explicit source-diff previews;
|
- exact undo/redo and explicit source-diff previews;
|
||||||
- parsing of `M L H V C S Q T A Z`, absolute/relative and repeated groups;
|
- parsing of `M L H V C S Q T A Z`, absolute/relative and repeated groups;
|
||||||
- anchors, cubic and quadratic controls, derived smooth controls, arc controls,
|
- anchors, cubic and quadratic controls, derived smooth controls, arc controls,
|
||||||
@@ -69,7 +71,7 @@ derived control intentionally expands shorthand to explicit normalized geometry.
|
|||||||
Visual path edits currently serialize the selected `d` attribute in normalized
|
Visual path edits currently serialize the selected `d` attribute in normalized
|
||||||
absolute form; surrounding source is untouched.
|
absolute form; surrounding source is untouched.
|
||||||
|
|
||||||
Not yet implemented: multi-node modes, delete/convert/join/break/combine,
|
Not yet implemented: multi-node modes, path join/break/combine,
|
||||||
subpath-start changes, a pen tool, snapping, simplification, boolean operations,
|
subpath-start changes, a pen tool, snapping, simplification, boolean operations,
|
||||||
round corners and stroke-to-path.
|
round corners and stroke-to-path.
|
||||||
|
|
||||||
@@ -159,7 +161,8 @@ should keep `Cross-Origin-Resource-Policy: same-origin`. The Toolbox Portal
|
|||||||
|
|
||||||
## Source and licence
|
## Source and licence
|
||||||
|
|
||||||
Corresponding source and inspected revisions are recorded in [SOURCE.md](SOURCE.md).
|
Corresponding source and inspected revisions are recorded in [SOURCE.md](SOURCE.md)
|
||||||
|
and [docs/REFERENCE_IMPLEMENTATIONS.md](docs/REFERENCE_IMPLEMENTATIONS.md).
|
||||||
Third-party packages and adoption decisions are recorded in
|
Third-party packages and adoption decisions are recorded in
|
||||||
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and [LICENSES](LICENSES/README.md).
|
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and [LICENSES](LICENSES/README.md).
|
||||||
Project-authored fixtures contain no third-party artwork or text.
|
Project-authored fixtures contain no third-party artwork or text.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Corresponding source and provenance
|
# Corresponding source and provenance
|
||||||
|
|
||||||
The corresponding source for SVG Tools 0.1.0 is:
|
The corresponding source for SVG Tools 0.2.0 is:
|
||||||
|
|
||||||
https://git.add-ideas.de/lotobo/svg-tools/src/tag/v0.1.0
|
https://git.add-ideas.de/lotobo/svg-tools/src/tag/v0.2.0
|
||||||
|
|
||||||
Build from that tag with Node.js 22 and the exact `package-lock.json`:
|
Build from that tag with Node.js 22 and the exact `package-lock.json`:
|
||||||
|
|
||||||
@@ -18,15 +18,15 @@ dependency inventory and notices.
|
|||||||
## Revisions inspected
|
## Revisions inspected
|
||||||
|
|
||||||
| Project | Revision/version inspected | Decision |
|
| Project | Revision/version inspected | Decision |
|
||||||
| ------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||||
| SVG Tools | empty public `lotobo/svg-tools` repository before this initial commit | implemented here |
|
| SVG Tools | empty public `lotobo/svg-tools` repository before this initial commit | implemented here |
|
||||||
| Toolbox SDK/contract/AppShell/testkit | `ef2dab4b46c61812c9a877d8a19fe497b4a4630a`, packages 0.2.3 | adopted |
|
| Toolbox SDK/contract/AppShell/testkit | `bc91659a424aaaadc88fff3a98a2cc2412237386`, packages 0.3.0 | adopted |
|
||||||
| Toolbox Portal | `55b2b12434465511586eaa2d0a43e81770868738`, tag v0.9.3 | release/UX contract reference; Portal 0.10.0 integrates the artifact |
|
| Toolbox Portal | `55b2b12434465511586eaa2d0a43e81770868738`, tag v0.9.3 | release/UX contract reference; Portal 0.10.0 integrates the artifact |
|
||||||
| SVG-Edit | `244a26c88e1ab1c32911c5b3637e214d7a7d8b25` | interaction/implementation reference only; no source copied |
|
| SVG-Edit | `244a26c88e1ab1c32911c5b3637e214d7a7d8b25` | interaction/implementation reference only; no source copied |
|
||||||
| SVG Path Editor | `937d75a83b6be2bdda11d02b9b3594841315223a` | path interaction reference only; no source copied |
|
| SVG Path Editor | `937d75a83b6be2bdda11d02b9b3594841315223a` | path interaction reference only; no source copied |
|
||||||
| SVGPathCommander | `9aa91dd2119ee6a65b1807d55d33e86b1ccb27fc` | inspected, not adopted; project-authored path core is smaller and source-ranged |
|
| SVGPathCommander | `9aa91dd2119ee6a65b1807d55d33e86b1ccb27fc` | inspected, not adopted; project-authored path core is smaller and source-ranged |
|
||||||
| SVGO | npm 4.0.2, published `gitHead` `b2309cf541aee11634eb653157b0ff86ab326e98` | adopted in a worker; GitHub clone was not required for shipped bytes |
|
| SVGO | npm 4.0.2, published `gitHead` `b2309cf541aee11634eb653157b0ff86ab326e98` | adopted in a worker; GitHub clone was not required for shipped bytes |
|
||||||
| DOMPurify | `9365501773d6665aaf334d8afa55081b9930a684`, npm 3.4.12 | adopted as one layer of the projection policy |
|
| DOMPurify | npm 3.4.14, lockfile integrity `sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==` | adopted as one layer of the projection policy |
|
||||||
| css-tree | npm 3.2.1 | adopted for parsed CSS policy/value checks |
|
| css-tree | npm 3.2.1 | adopted for parsed CSS policy/value checks |
|
||||||
| Boolean/geometry engine | none | not adopted; boolean operations are not claimed |
|
| Boolean/geometry engine | none | not adopted; boolean operations are not claimed |
|
||||||
| Raster engine | browser SVG image decoder and Canvas 2D | no third-party raster engine shipped |
|
| Raster engine | browser SVG image decoder and Canvas 2D | no third-party raster engine shipped |
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ the packages.
|
|||||||
|
|
||||||
| Project | Version | Licence | Role | Source |
|
| Project | Version | Licence | Role | Source |
|
||||||
| ----------------------- | ------- | --------------------- | -------------------------------------------------------- | ------------------------------------------------- |
|
| ----------------------- | ------- | --------------------- | -------------------------------------------------------- | ------------------------------------------------- |
|
||||||
| Toolbox contract | 0.2.3 | Apache-2.0 | manifest/context types | https://git.add-ideas.de/lotobo/toolbox-sdk |
|
| Toolbox contract | 0.3.0 | Apache-2.0 | manifest/context types | https://git.add-ideas.de/lotobo/toolbox-sdk |
|
||||||
| Toolbox shell React | 0.2.3 | Apache-2.0 | shared app shell/theme/help/source actions | https://git.add-ideas.de/lotobo/toolbox-sdk |
|
| Toolbox shell React | 0.3.0 | Apache-2.0 | shared app shell/theme/help/source actions | https://git.add-ideas.de/lotobo/toolbox-sdk |
|
||||||
| CodeMirror commands | 6.10.4 | MIT | editor commands/keymap | https://code.haverbeke.berlin/codemirror/commands |
|
| CodeMirror commands | 6.10.4 | MIT | editor commands/keymap | https://code.haverbeke.berlin/codemirror/commands |
|
||||||
| CodeMirror XML language | 6.1.0 | MIT | XML language support | https://github.com/codemirror/lang-xml |
|
| CodeMirror XML language | 6.1.0 | MIT | XML language support | https://github.com/codemirror/lang-xml |
|
||||||
| CodeMirror language | 6.12.4 | MIT | highlighting/folding | https://code.haverbeke.berlin/codemirror/language |
|
| CodeMirror language | 6.12.4 | MIT | highlighting/folding | https://code.haverbeke.berlin/codemirror/language |
|
||||||
@@ -24,7 +24,7 @@ the packages.
|
|||||||
| CodeMirror view | 6.43.6 | MIT | source editor UI | https://code.haverbeke.berlin/codemirror/view |
|
| CodeMirror view | 6.43.6 | MIT | source editor UI | https://code.haverbeke.berlin/codemirror/view |
|
||||||
| Lezer XML | 1.0.6 | MIT | ranged XML syntax diagnostics | https://github.com/lezer-parser/xml |
|
| Lezer XML | 1.0.6 | MIT | ranged XML syntax diagnostics | https://github.com/lezer-parser/xml |
|
||||||
| css-tree | 3.2.1 | MIT | parsed CSS security and animation-value checks | https://github.com/csstree/csstree |
|
| css-tree | 3.2.1 | MIT | parsed CSS security and animation-value checks | https://github.com/csstree/csstree |
|
||||||
| DOMPurify | 3.4.12 | MPL-2.0 OR Apache-2.0 | first sanitization layer | https://github.com/cure53/DOMPurify |
|
| DOMPurify | 3.4.14 | MPL-2.0 OR Apache-2.0 | first sanitization layer | https://github.com/cure53/DOMPurify |
|
||||||
| fflate | 0.8.3 | MIT | bounded SVGZ decompression and deterministic compression | https://github.com/101arrowz/fflate |
|
| fflate | 0.8.3 | MIT | bounded SVGZ decompression and deterministic compression | https://github.com/101arrowz/fflate |
|
||||||
| React | 19.2.6 | MIT | application UI | https://github.com/facebook/react |
|
| React | 19.2.6 | MIT | application UI | https://github.com/facebook/react |
|
||||||
| React DOM | 19.2.6 | MIT | browser rendering | https://github.com/facebook/react |
|
| React DOM | 19.2.6 | MIT | browser rendering | https://github.com/facebook/react |
|
||||||
@@ -67,7 +67,7 @@ as runtime modules in the release ZIP.
|
|||||||
|
|
||||||
| Package | Version | Licence |
|
| Package | Version | Licence |
|
||||||
| ----------------------------- | ------- | ---------- |
|
| ----------------------------- | ------- | ---------- |
|
||||||
| `@add-ideas/toolbox-testkit` | 0.2.3 | Apache-2.0 |
|
| `@add-ideas/toolbox-testkit` | 0.3.0 | Apache-2.0 |
|
||||||
| `@eslint/js` | 10.0.1 | MIT |
|
| `@eslint/js` | 10.0.1 | MIT |
|
||||||
| `@playwright/test` | 1.61.1 | Apache-2.0 |
|
| `@playwright/test` | 1.61.1 | Apache-2.0 |
|
||||||
| `@testing-library/jest-dom` | 6.9.1 | MIT |
|
| `@testing-library/jest-dom` | 6.9.1 | MIT |
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Reference implementations
|
||||||
|
|
||||||
|
This record complements [`SOURCE.md`](../SOURCE.md). Revisions were inspected
|
||||||
|
on 2026-08-01/02 while implementing SVG Tools 0.1.0. Reference code, fixtures,
|
||||||
|
assets, branding and explanatory text were not copied or adapted; the shipped
|
||||||
|
path, transform, security and test code is project-authored.
|
||||||
|
|
||||||
|
| Reference | Exact revision/version | Licence | Purpose and decision |
|
||||||
|
| ---------------- | ------------------------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Toolbox SDK | `bc91659a424aaaadc88fff3a98a2cc2412237386` / packages 0.3.0 | Apache-2.0 | Adopted through its published contract, AppShell and testkit APIs. |
|
||||||
|
| Toolbox Portal | `55b2b12434465511586eaa2d0a43e81770868738` (v0.9.3 before integration) | AGPL-3.0-only | Studied for release assembly, nested deployment, catalogue and shell conventions. SVG Tools is consumed only as an immutable release artifact. |
|
||||||
|
| SVG-Edit | `244a26c88e1ab1c32911c5b3637e214d7a7d8b25` | MIT | Interaction and implementation reference only. Its canvas/document model was not adopted because canonical SVG source must remain authoritative. |
|
||||||
|
| SVG Path Editor | `937d75a83b6be2bdda11d02b9b3594841315223a` | MIT | Path interaction reference only. Its source was not copied; SVG Tools uses its own source-ranged path model. |
|
||||||
|
| SVGPathCommander | `9aa91dd2119ee6a65b1807d55d33e86b1ccb27fc` | MIT | Evaluated for parsing and geometry, then not shipped. The initial project-authored core is smaller and retains source-command fragments. |
|
||||||
|
| DOMPurify | npm 3.4.14 / lockfile-pinned tarball integrity | MPL-2.0 OR Apache-2.0 | Adopted as the first projection-sanitization layer, followed by application-owned SVG, URL and parsed-CSS policy checks. |
|
||||||
|
| SVGO | npm 4.0.2, published `gitHead` `b2309cf541aee11634eb653157b0ff86ab326e98` | MIT | Adopted behind a cancelable application worker. Direct GitHub cloning was unavailable in the build environment, so the exact locked npm source identity is recorded. |
|
||||||
|
| css-tree | npm 3.2.1 | MIT | Adopted for stylesheet, declaration and animation-value parsing. |
|
||||||
|
| fflate | npm 0.8.3 | MIT | Adopted for deterministic SVGZ output and bounded streaming decompression. |
|
||||||
|
|
||||||
|
Boxy SVG and SVGViewer were consulted only as public product/interaction
|
||||||
|
references. Their proprietary source and assets were not used.
|
||||||
|
|
||||||
|
No boolean geometry engine was adopted: PathBool.js was considered too early
|
||||||
|
for a mandatory production dependency, while Paper.js and PathKit would add a
|
||||||
|
large secondary geometry/runtime model before the required fixture and fuzz
|
||||||
|
spikes. No third-party raster engine was adopted; 0.1.0 uses the browser SVG
|
||||||
|
decoder and Canvas 2D behind explicit limits. These decisions deliberately
|
||||||
|
leave boolean operations and deterministic cross-browser rasterization on the
|
||||||
|
roadmap rather than overstating support.
|
||||||
@@ -73,6 +73,10 @@ and DOMPurify remain dependency/security boundaries. Bounded data URLs may still
|
|||||||
decode expensive images within the raster-pixel limits. CSP header regressions
|
decode expensive images within the raster-pixel limits. CSP header regressions
|
||||||
can break the fixed controller even when the content remains inert.
|
can break the fixed controller even when the content remains inert.
|
||||||
|
|
||||||
|
DOMPurify is pinned in the lockfile and its SVG policy is covered by the
|
||||||
|
project-authored adversarial suite. Dependency updates must run both
|
||||||
|
`npm run test:security` and the browser tests before release.
|
||||||
|
|
||||||
Report vulnerabilities privately through the repository owner/contact before
|
Report vulnerabilities privately through the repository owner/contact before
|
||||||
opening a public issue when disclosure could expose users. Include the SVG,
|
opening a public issue when disclosure could expose users. Include the SVG,
|
||||||
browser, deployment headers and observed network/execution behavior without
|
browser, deployment headers and observed network/execution behavior without
|
||||||
|
|||||||
Generated
+20
-20
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "svg-tools",
|
"name": "svg-tools",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "svg-tools",
|
"name": "svg-tools",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.3",
|
"@add-ideas/toolbox-contract": "0.3.0",
|
||||||
"@add-ideas/toolbox-shell-react": "0.2.3",
|
"@add-ideas/toolbox-shell-react": "0.3.0",
|
||||||
"@codemirror/commands": "6.10.4",
|
"@codemirror/commands": "6.10.4",
|
||||||
"@codemirror/lang-xml": "6.1.0",
|
"@codemirror/lang-xml": "6.1.0",
|
||||||
"@codemirror/language": "6.12.4",
|
"@codemirror/language": "6.12.4",
|
||||||
@@ -19,14 +19,14 @@
|
|||||||
"@codemirror/view": "6.43.6",
|
"@codemirror/view": "6.43.6",
|
||||||
"@lezer/xml": "1.0.6",
|
"@lezer/xml": "1.0.6",
|
||||||
"css-tree": "3.2.1",
|
"css-tree": "3.2.1",
|
||||||
"dompurify": "3.4.12",
|
"dompurify": "3.4.14",
|
||||||
"fflate": "0.8.3",
|
"fflate": "0.8.3",
|
||||||
"react": "19.2.6",
|
"react": "19.2.6",
|
||||||
"react-dom": "19.2.6",
|
"react-dom": "19.2.6",
|
||||||
"svgo": "4.0.2"
|
"svgo": "4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@add-ideas/toolbox-testkit": "0.2.3",
|
"@add-ideas/toolbox-testkit": "0.3.0",
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
"@playwright/test": "1.61.1",
|
"@playwright/test": "1.61.1",
|
||||||
"@testing-library/jest-dom": "6.9.1",
|
"@testing-library/jest-dom": "6.9.1",
|
||||||
@@ -54,18 +54,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@add-ideas/toolbox-contract": {
|
"node_modules/@add-ideas/toolbox-contract": {
|
||||||
"version": "0.2.3",
|
"version": "0.3.0",
|
||||||
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.2.3/toolbox-contract-0.2.3.tgz",
|
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.3.0/toolbox-contract-0.3.0.tgz",
|
||||||
"integrity": "sha512-T0PVSuMT40GjTDfQJhEEY3ZawQq8zz1/ry95JdKI6W39CdLacaRXdGnEpDCMHt+jUbf1Jz7Nat/M5dFCgKVM9A==",
|
"integrity": "sha512-dKrK7BjOFwqJaBfJuhKxZKIld4sH0AKjEn6a0yLnbdMUFY+fFv4VSLGV2tNSBD016gumc2iNqOjUj/ld7x4rtA==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@add-ideas/toolbox-shell-react": {
|
"node_modules/@add-ideas/toolbox-shell-react": {
|
||||||
"version": "0.2.3",
|
"version": "0.3.0",
|
||||||
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.2.3/toolbox-shell-react-0.2.3.tgz",
|
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.3.0/toolbox-shell-react-0.3.0.tgz",
|
||||||
"integrity": "sha512-DT5lQDH48BFkFcmFLZnQh7+Cm73JzBPcmp5WzUXypfkUXpEyDYHzaXgmW4kZ0edSwh4RK4sPmx+JPtK0X4aKCQ==",
|
"integrity": "sha512-74p6JzAOG0YCAKdlc1hLofV4ZIko7vb448S75cIiM88PKm93EHl5VD7g8YVyfM56Ui97UY9dmy+Whiq4sGzpsg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.3"
|
"@add-ideas/toolbox-contract": "0.3.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18 <20",
|
"react": ">=18 <20",
|
||||||
@@ -73,13 +73,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@add-ideas/toolbox-testkit": {
|
"node_modules/@add-ideas/toolbox-testkit": {
|
||||||
"version": "0.2.3",
|
"version": "0.3.0",
|
||||||
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.2.3/toolbox-testkit-0.2.3.tgz",
|
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.3.0/toolbox-testkit-0.3.0.tgz",
|
||||||
"integrity": "sha512-sq1MwhKWfFKen+N+124hl74qQimRSvmQ9sOU7jdcI+2qCKZ67+2B8rWyezeV80uTFu4Jv6deHksfYQ/tKNV6XQ==",
|
"integrity": "sha512-4Fk+oSvZFspOMIXr8Xy040nhAaBsIQAzsGyXWSpjn3+k3yBKq7nB1r5zCHhsXzfdLzvPDAx2KcmSNOhM330D9w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.3"
|
"@add-ideas/toolbox-contract": "0.3.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"toolbox-check": "dist/cli.js"
|
"toolbox-check": "dist/cli.js"
|
||||||
@@ -2330,9 +2330,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.4.12",
|
"version": "3.4.14",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.14.tgz",
|
||||||
"integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
|
"integrity": "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==",
|
||||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@types/trusted-types": "^2.0.7"
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "svg-tools",
|
"name": "svg-tools",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Inspect, edit, optimize and transform SVG documents locally in the browser.",
|
"description": "Inspect, edit, optimize and transform SVG documents locally in the browser.",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"author": "Albrecht Degering",
|
"author": "Albrecht Degering",
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
"release:artifact": "npm run check && npm run test:security && npm run test:browser && npm run package:release -- --force && npm run portal:smoke"
|
"release:artifact": "npm run check && npm run test:security && npm run test:browser && npm run package:release -- --force && npm run portal:smoke"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.3",
|
"@add-ideas/toolbox-contract": "0.3.0",
|
||||||
"@add-ideas/toolbox-shell-react": "0.2.3",
|
"@add-ideas/toolbox-shell-react": "0.3.0",
|
||||||
"@codemirror/commands": "6.10.4",
|
"@codemirror/commands": "6.10.4",
|
||||||
"@codemirror/lang-xml": "6.1.0",
|
"@codemirror/lang-xml": "6.1.0",
|
||||||
"@codemirror/language": "6.12.4",
|
"@codemirror/language": "6.12.4",
|
||||||
@@ -51,14 +51,14 @@
|
|||||||
"@codemirror/view": "6.43.6",
|
"@codemirror/view": "6.43.6",
|
||||||
"@lezer/xml": "1.0.6",
|
"@lezer/xml": "1.0.6",
|
||||||
"css-tree": "3.2.1",
|
"css-tree": "3.2.1",
|
||||||
"dompurify": "3.4.12",
|
"dompurify": "3.4.14",
|
||||||
"fflate": "0.8.3",
|
"fflate": "0.8.3",
|
||||||
"react": "19.2.6",
|
"react": "19.2.6",
|
||||||
"react-dom": "19.2.6",
|
"react-dom": "19.2.6",
|
||||||
"svgo": "4.0.2"
|
"svgo": "4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@add-ideas/toolbox-testkit": "0.2.3",
|
"@add-ideas/toolbox-testkit": "0.3.0",
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
"@playwright/test": "1.61.1",
|
"@playwright/test": "1.61.1",
|
||||||
"@testing-library/jest-dom": "6.9.1",
|
"@testing-library/jest-dom": "6.9.1",
|
||||||
|
|||||||
+20
-2
@@ -18,7 +18,25 @@ export default defineConfig({
|
|||||||
timeout: 180_000,
|
timeout: 180_000,
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
{
|
||||||
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
|
name: "chromium",
|
||||||
|
testIgnore: /responsive\.spec\.ts/,
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "firefox",
|
||||||
|
testIgnore: /responsive\.spec\.ts/,
|
||||||
|
use: { ...devices["Desktop Firefox"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "webkit",
|
||||||
|
testIgnore: /responsive\.spec\.ts/,
|
||||||
|
use: { ...devices["Desktop Safari"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mobile-chromium",
|
||||||
|
testMatch: /responsive\.spec\.ts/,
|
||||||
|
use: { ...devices["Pixel 5"] },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
+16
-6
@@ -3,7 +3,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.svg-tools",
|
"id": "de.add-ideas.svg-tools",
|
||||||
"name": "SVG Tools",
|
"name": "SVG Tools",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Inspect, edit, optimize and transform SVG documents locally in the browser.",
|
"description": "Inspect, edit, optimize and transform SVG documents locally in the browser.",
|
||||||
"entry": "./",
|
"entry": "./",
|
||||||
"icon": "./favicon.svg",
|
"icon": "./favicon.svg",
|
||||||
@@ -24,16 +24,26 @@
|
|||||||
},
|
},
|
||||||
"requirements": {
|
"requirements": {
|
||||||
"secureContext": false,
|
"secureContext": false,
|
||||||
"workers": true,
|
"workers": false,
|
||||||
"indexedDb": false,
|
"indexedDb": false,
|
||||||
"crossOriginIsolated": false,
|
"crossOriginIsolated": false,
|
||||||
"topLevelContext": false
|
"topLevelContext": false
|
||||||
},
|
},
|
||||||
"privacy": {
|
"io": {
|
||||||
"processing": "local",
|
"accepts": [
|
||||||
"fileUploads": false,
|
{ "mediaType": "image/svg+xml", "extensions": [".svg", ".svgz"] },
|
||||||
"telemetry": false
|
{ "mediaType": "application/json", "extensions": [".svgtools.json"] }
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
{ "mediaType": "image/svg+xml", "extensions": [".svg", ".svgz"] },
|
||||||
|
{ "mediaType": "image/png", "extensions": [".png"] },
|
||||||
|
{ "mediaType": "image/jpeg", "extensions": [".jpg"] },
|
||||||
|
{ "mediaType": "image/webp", "extensions": [".webp"] },
|
||||||
|
{ "mediaType": "application/json", "extensions": [".svgtools.json"] }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
"capabilities": { "required": [], "optional": ["workers", "canvas"] },
|
||||||
|
"privacy": { "processing": "local", "fileUploads": true, "telemetry": false },
|
||||||
"source": {
|
"source": {
|
||||||
"repository": "https://git.add-ideas.de/lotobo/svg-tools",
|
"repository": "https://git.add-ideas.de/lotobo/svg-tools",
|
||||||
"license": "GPL-3.0-or-later"
|
"license": "GPL-3.0-or-later"
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export interface InspectorProps {
|
|||||||
value: string | null,
|
value: string | null,
|
||||||
mergeKey?: string,
|
mergeKey?: string,
|
||||||
) => void;
|
) => void;
|
||||||
|
onDuplicateNode: () => void;
|
||||||
|
onDeleteNode: () => void;
|
||||||
onCommitPath: (data: string, label: string) => void;
|
onCommitPath: (data: string, label: string) => void;
|
||||||
onTransformPreview: (matrix: Matrix | null) => void;
|
onTransformPreview: (matrix: Matrix | null) => void;
|
||||||
onApplyTransform: (matrix: Matrix, mode: "attribute" | "bake") => void;
|
onApplyTransform: (matrix: Matrix, mode: "attribute" | "bake") => void;
|
||||||
@@ -186,9 +188,13 @@ function DocumentPanel({
|
|||||||
function ElementPanel({
|
function ElementPanel({
|
||||||
node,
|
node,
|
||||||
onSetAttribute,
|
onSetAttribute,
|
||||||
|
onDuplicateNode,
|
||||||
|
onDeleteNode,
|
||||||
}: {
|
}: {
|
||||||
node: SemanticSvgNode | undefined;
|
node: SemanticSvgNode | undefined;
|
||||||
onSetAttribute: InspectorProps["onSetAttribute"];
|
onSetAttribute: InspectorProps["onSetAttribute"];
|
||||||
|
onDuplicateNode: () => void;
|
||||||
|
onDeleteNode: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [newName, setNewName] = useState("");
|
const [newName, setNewName] = useState("");
|
||||||
const [newValue, setNewValue] = useState("");
|
const [newValue, setNewValue] = useState("");
|
||||||
@@ -207,6 +213,24 @@ function ElementPanel({
|
|||||||
<span className="element-glyph"><{node.name}></span>
|
<span className="element-glyph"><{node.name}></span>
|
||||||
<span>{node.id ? `#${node.id}` : "No ID"}</span>
|
<span>{node.id ? `#${node.id}` : "No ID"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="button-row">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="secondary-button"
|
||||||
|
disabled={!node.parentKey}
|
||||||
|
onClick={onDuplicateNode}
|
||||||
|
>
|
||||||
|
Duplicate element
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="secondary-button danger-button"
|
||||||
|
disabled={!node.parentKey}
|
||||||
|
onClick={onDeleteNode}
|
||||||
|
>
|
||||||
|
Delete element
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<h3>Common styling</h3>
|
<h3>Common styling</h3>
|
||||||
<div className="field-grid">
|
<div className="field-grid">
|
||||||
<Field
|
<Field
|
||||||
@@ -898,14 +922,13 @@ export function Inspector(props: InspectorProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="inspector-tabs"
|
className="inspector-tabs"
|
||||||
role="tablist"
|
role="group"
|
||||||
aria-label="Inspector sections"
|
aria-label="Inspector sections"
|
||||||
>
|
>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
aria-pressed={props.activeTab === tab.id}
|
||||||
aria-selected={props.activeTab === tab.id}
|
|
||||||
className={props.activeTab === tab.id ? "is-active" : ""}
|
className={props.activeTab === tab.id ? "is-active" : ""}
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => props.onTabChange(tab.id)}
|
onClick={() => props.onTabChange(tab.id)}
|
||||||
@@ -914,7 +937,7 @@ export function Inspector(props: InspectorProps) {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="inspector-body" role="tabpanel">
|
<div className="inspector-body">
|
||||||
<fieldset disabled={props.disabled} className="inspector-fieldset">
|
<fieldset disabled={props.disabled} className="inspector-fieldset">
|
||||||
{props.activeTab === "document" ? (
|
{props.activeTab === "document" ? (
|
||||||
<DocumentPanel
|
<DocumentPanel
|
||||||
@@ -923,7 +946,12 @@ export function Inspector(props: InspectorProps) {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{props.activeTab === "element" ? (
|
{props.activeTab === "element" ? (
|
||||||
<ElementPanel node={node} onSetAttribute={props.onSetAttribute} />
|
<ElementPanel
|
||||||
|
node={node}
|
||||||
|
onSetAttribute={props.onSetAttribute}
|
||||||
|
onDuplicateNode={props.onDuplicateNode}
|
||||||
|
onDeleteNode={props.onDeleteNode}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{props.activeTab === "path" ? (
|
{props.activeTab === "path" ? (
|
||||||
<PathPanel
|
<PathPanel
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { CommandHistory, createTransaction } from "../commands/history";
|
|||||||
import { geometryDiagnostics } from "../diagnostics/geometry";
|
import { geometryDiagnostics } from "../diagnostics/geometry";
|
||||||
import type {
|
import type {
|
||||||
SemanticSvgDocument,
|
SemanticSvgDocument,
|
||||||
|
SemanticSvgNode,
|
||||||
SourcePatch,
|
SourcePatch,
|
||||||
SvgDiagnostic,
|
SvgDiagnostic,
|
||||||
} from "../document/document.types";
|
} from "../document/document.types";
|
||||||
@@ -49,6 +50,7 @@ import {
|
|||||||
} from "../structure/reference-index";
|
} from "../structure/reference-index";
|
||||||
import { APPLICATION_VERSION } from "../version";
|
import { APPLICATION_VERSION } from "../version";
|
||||||
import { STARTER_SVG } from "../app/sample";
|
import { STARTER_SVG } from "../app/sample";
|
||||||
|
import { defaultSvgLimits, utf8ByteLength } from "../app/limits";
|
||||||
import { CanvasPane, type CanvasView } from "./CanvasPane";
|
import { CanvasPane, type CanvasView } from "./CanvasPane";
|
||||||
import { Inspector, type InspectorTab } from "./Inspector";
|
import { Inspector, type InspectorTab } from "./Inspector";
|
||||||
import { SourceEditor } from "./SourceEditor";
|
import { SourceEditor } from "./SourceEditor";
|
||||||
@@ -608,6 +610,115 @@ export function Workbench() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectedSubtree = (): SemanticSvgNode[] => {
|
||||||
|
if (!selectedNode) return [];
|
||||||
|
const result: SemanticSvgNode[] = [];
|
||||||
|
const visit = (node: SemanticSvgNode) => {
|
||||||
|
result.push(node);
|
||||||
|
for (const childKey of node.childKeys) {
|
||||||
|
const child = snapshot.semantic.nodes.get(childKey);
|
||||||
|
if (child) visit(child);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(selectedNode);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const duplicateSelectedNode = () => {
|
||||||
|
if (visualDisabled || !selectedNode?.parentKey) return;
|
||||||
|
try {
|
||||||
|
const subtree = selectedSubtree();
|
||||||
|
const ids = subtree.flatMap((node) => (node.id ? [node.id] : []));
|
||||||
|
if (ids.length)
|
||||||
|
throw new Error(
|
||||||
|
`Duplicate is refused because the subtree owns ${ids.length} ID(s). Rename or remove them first so references cannot become ambiguous.`,
|
||||||
|
);
|
||||||
|
const fragment = source.slice(
|
||||||
|
selectedNode.sourceRange.full.from,
|
||||||
|
selectedNode.sourceRange.full.to,
|
||||||
|
);
|
||||||
|
const lineStart = Math.max(
|
||||||
|
source.lastIndexOf("\n", selectedNode.sourceRange.full.from - 1) + 1,
|
||||||
|
source.lastIndexOf("\r", selectedNode.sourceRange.full.from - 1) + 1,
|
||||||
|
);
|
||||||
|
const indentation =
|
||||||
|
/^\s*/u.exec(
|
||||||
|
source.slice(lineStart, selectedNode.sourceRange.full.from),
|
||||||
|
)?.[0] ?? "";
|
||||||
|
const insert = `${snapshot.semantic.preferences.newline}${indentation}${fragment}`;
|
||||||
|
if (
|
||||||
|
utf8ByteLength(source) + utf8ByteLength(insert) >
|
||||||
|
defaultSvgLimits.sourceHardBytes
|
||||||
|
)
|
||||||
|
throw new RangeError(
|
||||||
|
"Duplicate would exceed the 20 MiB SVG source limit.",
|
||||||
|
);
|
||||||
|
commitPatches(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
from: selectedNode.sourceRange.full.to,
|
||||||
|
to: selectedNode.sourceRange.full.to,
|
||||||
|
insert,
|
||||||
|
label: `Duplicate <${selectedNode.name}>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
`Duplicate <${selectedNode.name}>`,
|
||||||
|
undefined,
|
||||||
|
snapshot.revision,
|
||||||
|
);
|
||||||
|
setStatus(
|
||||||
|
`Duplicated <${selectedNode.name}> without introducing editor metadata.`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error instanceof Error ? error.message : "Duplicate failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSelectedNode = () => {
|
||||||
|
if (visualDisabled || !selectedNode?.parentKey) return;
|
||||||
|
try {
|
||||||
|
const subtree = selectedSubtree();
|
||||||
|
const subtreeKeys = new Set(subtree.map((node) => node.key));
|
||||||
|
const references = buildReferenceIndex(snapshot.semantic);
|
||||||
|
for (const node of subtree) {
|
||||||
|
if (!node.id) continue;
|
||||||
|
const externalIncoming = (
|
||||||
|
references.incomingById.get(node.id) ?? []
|
||||||
|
).filter((edge) => !subtreeKeys.has(edge.sourceKey));
|
||||||
|
if (externalIncoming.length)
|
||||||
|
throw new Error(
|
||||||
|
`Delete is refused because #${node.id} has ${externalIncoming.length} reference(s) from outside the selected subtree.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const parentKey = selectedNode.parentKey;
|
||||||
|
commitPatches(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
from: selectedNode.sourceRange.full.from,
|
||||||
|
to: selectedNode.sourceRange.full.to,
|
||||||
|
insert: "",
|
||||||
|
label: `Delete <${selectedNode.name}>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
`Delete <${selectedNode.name}>`,
|
||||||
|
undefined,
|
||||||
|
snapshot.revision,
|
||||||
|
);
|
||||||
|
setAnimations((current) =>
|
||||||
|
current.filter(
|
||||||
|
(animation) => !subtreeKeys.has(animation.targetNodeKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setSelectedKey(parentKey);
|
||||||
|
selectedRef.current = parentKey;
|
||||||
|
setStatus(
|
||||||
|
`Deleted <${selectedNode.name}> and its ${subtree.length - 1} descendant(s).`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error instanceof Error ? error.message : "Delete failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const applyTransform = (matrix: Matrix, mode: "attribute" | "bake") => {
|
const applyTransform = (matrix: Matrix, mode: "attribute" | "bake") => {
|
||||||
if (visualDisabled || !selectedNode) return;
|
if (visualDisabled || !selectedNode) return;
|
||||||
try {
|
try {
|
||||||
@@ -848,6 +959,7 @@ export function Workbench() {
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
type="file"
|
type="file"
|
||||||
|
aria-label="Open SVG or SVG Tools project"
|
||||||
accept=".svg,.svgz,.svgtools.json,image/svg+xml,application/gzip,application/json"
|
accept=".svg,.svgz,.svgtools.json,image/svg+xml,application/gzip,application/json"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const file = event.currentTarget.files?.[0];
|
const file = event.currentTarget.files?.[0];
|
||||||
@@ -1104,6 +1216,8 @@ export function Workbench() {
|
|||||||
onTabChange={setActiveTab}
|
onTabChange={setActiveTab}
|
||||||
onPathEditingChange={setPathEditing}
|
onPathEditingChange={setPathEditing}
|
||||||
onSetAttribute={setAttribute}
|
onSetAttribute={setAttribute}
|
||||||
|
onDuplicateNode={duplicateSelectedNode}
|
||||||
|
onDeleteNode={deleteSelectedNode}
|
||||||
onCommitPath={commitPath}
|
onCommitPath={commitPath}
|
||||||
onTransformPreview={setTransformPreview}
|
onTransformPreview={setTransformPreview}
|
||||||
onApplyTransform={applyTransform}
|
onApplyTransform={applyTransform}
|
||||||
|
|||||||
@@ -183,6 +183,15 @@ a:focus-visible,
|
|||||||
color: var(--toolbox-accent);
|
color: var(--toolbox-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.danger-button {
|
||||||
|
border-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--toolbox-danger) 55%,
|
||||||
|
var(--toolbox-border)
|
||||||
|
);
|
||||||
|
color: var(--toolbox-danger);
|
||||||
|
}
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
width: 2.25rem;
|
width: 2.25rem;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.svg-tools",
|
"id": "de.add-ideas.svg-tools",
|
||||||
"name": "SVG Tools",
|
"name": "SVG Tools",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Inspect, edit, optimize and transform SVG documents locally in the browser.",
|
"description": "Inspect, edit, optimize and transform SVG documents locally in the browser.",
|
||||||
"entry": "./",
|
"entry": "./",
|
||||||
"icon": "./favicon.svg",
|
"icon": "./favicon.svg",
|
||||||
@@ -24,14 +24,31 @@
|
|||||||
},
|
},
|
||||||
"requirements": {
|
"requirements": {
|
||||||
"secureContext": false,
|
"secureContext": false,
|
||||||
"workers": true,
|
"workers": false,
|
||||||
"indexedDb": false,
|
"indexedDb": false,
|
||||||
"crossOriginIsolated": false,
|
"crossOriginIsolated": false,
|
||||||
"topLevelContext": false
|
"topLevelContext": false
|
||||||
},
|
},
|
||||||
|
"io": {
|
||||||
|
"accepts": [
|
||||||
|
{ "mediaType": "image/svg+xml", "extensions": [".svg", ".svgz"] },
|
||||||
|
{ "mediaType": "application/json", "extensions": [".svgtools.json"] }
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
{ "mediaType": "image/svg+xml", "extensions": [".svg", ".svgz"] },
|
||||||
|
{ "mediaType": "image/png", "extensions": [".png"] },
|
||||||
|
{ "mediaType": "image/jpeg", "extensions": [".jpg"] },
|
||||||
|
{ "mediaType": "image/webp", "extensions": [".webp"] },
|
||||||
|
{ "mediaType": "application/json", "extensions": [".svgtools.json"] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"required": [],
|
||||||
|
"optional": ["workers", "canvas"]
|
||||||
|
},
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"processing": "local",
|
"processing": "local",
|
||||||
"fileUploads": false,
|
"fileUploads": true,
|
||||||
"telemetry": false
|
"telemetry": false
|
||||||
},
|
},
|
||||||
"source": {
|
"source": {
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
export const APPLICATION_VERSION = "0.1.0";
|
export const APPLICATION_VERSION = "0.2.0";
|
||||||
export const DOMPURIFY_VERSION = "3.4.12";
|
export const DOMPURIFY_VERSION = "3.4.14";
|
||||||
export const SVGO_VERSION = "4.0.2";
|
export const SVGO_VERSION = "4.0.2";
|
||||||
export const SVG_PATH_COMMANDER_VERSION = "2.2.1";
|
export const SVG_PATH_COMMANDER_VERSION = "2.2.1";
|
||||||
export const CSS_TREE_VERSION = "3.2.1";
|
export const CSS_TREE_VERSION = "3.2.1";
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("keeps the primary workspace inside a narrow viewport", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/deep/nested/svg/");
|
||||||
|
await expect(page.locator("main").first()).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.locator("main .loading, main .workbench-loading"),
|
||||||
|
).toHaveCount(0);
|
||||||
|
|
||||||
|
const widths = await page.evaluate(() => ({
|
||||||
|
content: document.documentElement.scrollWidth,
|
||||||
|
viewport: document.documentElement.clientWidth,
|
||||||
|
}));
|
||||||
|
expect(widths.viewport).toBeLessThanOrEqual(430);
|
||||||
|
expect(widths.content).toBeLessThanOrEqual(widths.viewport + 1);
|
||||||
|
});
|
||||||
@@ -67,7 +67,7 @@ test("synchronizes tree selection, source patches, undo and redo", async ({
|
|||||||
const pathRow = page.getByRole("treeitem").filter({ hasText: "path" }).last();
|
const pathRow = page.getByRole("treeitem").filter({ hasText: "path" }).last();
|
||||||
await pathRow.click();
|
await pathRow.click();
|
||||||
await expect(pathRow).toHaveAttribute("aria-selected", "true");
|
await expect(pathRow).toHaveAttribute("aria-selected", "true");
|
||||||
await page.getByRole("tab", { name: "Element" }).click();
|
await page.getByRole("button", { name: "Element" }).click();
|
||||||
|
|
||||||
const fill = page.getByLabel("Fill", { exact: true });
|
const fill = page.getByLabel("Fill", { exact: true });
|
||||||
await fill.fill("#123456");
|
await fill.fill("#123456");
|
||||||
@@ -80,6 +80,30 @@ test("synchronizes tree selection, source patches, undo and redo", async ({
|
|||||||
await expect(page.locator(".cm-content")).toContainText('fill="#123456"');
|
await expect(page.locator(".cm-content")).toContainText('fill="#123456"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("duplicates and deletes ID-free nodes while protecting referenced IDs", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/deep/nested/svg/");
|
||||||
|
await page.getByPlaceholder("Filter elements…").fill("stop");
|
||||||
|
await page.getByRole("treeitem").filter({ hasText: "stop" }).first().click();
|
||||||
|
await page.getByRole("button", { name: "Element" }).click();
|
||||||
|
await page.getByRole("button", { name: "Duplicate element" }).click();
|
||||||
|
await expect(page.locator(".cm-content")).toContainText(
|
||||||
|
/stop-color="#725cff"[\s\S]*stop-color="#725cff"/u,
|
||||||
|
);
|
||||||
|
await page.getByRole("button", { name: "Delete element" }).click();
|
||||||
|
await expect(page.locator(".cm-content")).not.toContainText(
|
||||||
|
/stop-color="#725cff"[\s\S]*stop-color="#725cff"/u,
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByPlaceholder("Filter elements…").fill("sun");
|
||||||
|
await page.getByRole("treeitem").filter({ hasText: "sun" }).click();
|
||||||
|
await page.getByRole("button", { name: "Duplicate element" }).click();
|
||||||
|
await expect(page.locator(".workbench-footer")).toContainText(
|
||||||
|
/Duplicate is refused/u,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("connects to a valid same-origin Toolbox catalogue from the nested build", async ({
|
test("connects to a valid same-origin Toolbox catalogue from the nested build", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user