From 39d9802daa60d8f924f15ed6ba9111b561ec3a59 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Sun, 2 Aug 2026 16:31:49 +0200 Subject: [PATCH] feat: introduce local-first SVG workbench --- .gitignore | 19 + .npmrc | 1 + .prettierignore | 8 + CHANGELOG.md | 22 + LICENSE | 674 +++ LICENSES/README.md | 14 + README.md | 177 + SOURCE.md | 37 + THIRD_PARTY_NOTICES.md | 99 + docs/ACCESSIBILITY.md | 26 + docs/ANIMATION.md | 28 + docs/ARCHITECTURE.md | 53 + docs/DOCUMENT_MODEL.md | 45 + docs/OPTIMIZATION.md | 34 + docs/PATH_EDITOR.md | 42 + docs/SECURITY.md | 79 + docs/TRANSFORM_MODEL.md | 41 + eslint.config.mjs | 43 + index.html | 17 + package-lock.json | 4453 +++++++++++++++++ package.json | 85 + playwright.config.ts | 24 + public/canvas-frame-controller.js | 84 + public/favicon.svg | 13 + public/toolbox-app.json | 49 + scripts/generate-toolbox-manifest.mjs | 69 + scripts/package-release.mjs | 240 + scripts/portal-assembly-smoke.mjs | 158 + scripts/prepare-release-files.mjs | 95 + scripts/serve-test.mjs | 97 + src/App.tsx | 42 + src/accessibility/audit.ts | 130 + src/animation/animation.types.ts | 23 + src/animation/preview.ts | 40 + src/animation/validation.ts | 342 ++ src/app/limits.ts | 65 + src/app/sample.ts | 18 + src/commands/history.ts | 155 + src/components/AppErrorBoundary.tsx | 42 + src/components/CanvasPane.tsx | 579 +++ src/components/HelpDialog.tsx | 91 + src/components/Inspector.tsx | 965 ++++ src/components/SourceEditor.tsx | 164 + src/components/StructureTree.tsx | 264 + src/components/Workbench.tsx | 1219 +++++ src/components/WorkflowDialogs.tsx | 741 +++ src/diagnostics/geometry.ts | 144 + src/document/document.types.ts | 115 + src/document/source-parser.ts | 830 +++ src/document/source-patcher.ts | 127 + src/domain/affine.ts | 437 ++ src/domain/bake-transform.ts | 451 ++ src/domain/path.ts | 1059 ++++ src/domain/transform-chain.ts | 81 + src/export/derived-svg-export.ts | 184 + src/export/file-name.ts | 54 + src/export/raster-export.ts | 223 + src/export/svg-export.ts | 122 + src/format/diff.ts | 66 + src/format/formatter.ts | 78 + src/main.tsx | 9 + src/optimization/optimization.types.ts | 31 + src/optimization/optimizer-client.ts | 113 + src/optimization/optimizer.worker.ts | 48 + src/optimization/profiles.ts | 153 + src/project/project-format.ts | 295 ++ src/security/sanitize-svg.ts | 432 ++ src/security/security.types.ts | 18 + src/structure/reference-index.ts | 251 + src/styles.css | 1476 ++++++ src/test/setup.ts | 65 + src/toolbox/manifest.source.json | 49 + src/toolbox/manifest.ts | 4 + src/version.ts | 5 + tests/animation/preview.test.ts | 70 + tests/animation/validation.test.ts | 126 + tests/browser/svg-tools.spec.ts | 148 + tests/commands/history.test.ts | 79 + tests/document/resource-limits.test.ts | 112 + tests/document/source-parser.test.ts | 170 + tests/domain/affine.test.ts | 92 + tests/domain/bake-transform.test.ts | 114 + tests/domain/path.test.ts | 168 + tests/domain/transform-chain.test.ts | 45 + tests/export/derived-svg-export.test.ts | 65 + tests/export/export.test.ts | 120 + tests/format/format-and-diff.test.ts | 34 + tests/optimization/optimizer-client.test.ts | 129 + tests/optimization/profiles.test.ts | 60 + tests/project/project-format.test.ts | 165 + tests/security/adversarial.test.ts | 72 + tests/security/sanitize.test.ts | 148 + .../structure/reference-accessibility.test.ts | 83 + tsconfig.app.json | 27 + tsconfig.json | 7 + tsconfig.node.json | 24 + vite.config.ts | 18 + 97 files changed, 20702 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 LICENSES/README.md create mode 100644 README.md create mode 100644 SOURCE.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 docs/ACCESSIBILITY.md create mode 100644 docs/ANIMATION.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/DOCUMENT_MODEL.md create mode 100644 docs/OPTIMIZATION.md create mode 100644 docs/PATH_EDITOR.md create mode 100644 docs/SECURITY.md create mode 100644 docs/TRANSFORM_MODEL.md create mode 100644 eslint.config.mjs create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 public/canvas-frame-controller.js create mode 100644 public/favicon.svg create mode 100644 public/toolbox-app.json create mode 100644 scripts/generate-toolbox-manifest.mjs create mode 100644 scripts/package-release.mjs create mode 100644 scripts/portal-assembly-smoke.mjs create mode 100644 scripts/prepare-release-files.mjs create mode 100644 scripts/serve-test.mjs create mode 100644 src/App.tsx create mode 100644 src/accessibility/audit.ts create mode 100644 src/animation/animation.types.ts create mode 100644 src/animation/preview.ts create mode 100644 src/animation/validation.ts create mode 100644 src/app/limits.ts create mode 100644 src/app/sample.ts create mode 100644 src/commands/history.ts create mode 100644 src/components/AppErrorBoundary.tsx create mode 100644 src/components/CanvasPane.tsx create mode 100644 src/components/HelpDialog.tsx create mode 100644 src/components/Inspector.tsx create mode 100644 src/components/SourceEditor.tsx create mode 100644 src/components/StructureTree.tsx create mode 100644 src/components/Workbench.tsx create mode 100644 src/components/WorkflowDialogs.tsx create mode 100644 src/diagnostics/geometry.ts create mode 100644 src/document/document.types.ts create mode 100644 src/document/source-parser.ts create mode 100644 src/document/source-patcher.ts create mode 100644 src/domain/affine.ts create mode 100644 src/domain/bake-transform.ts create mode 100644 src/domain/path.ts create mode 100644 src/domain/transform-chain.ts create mode 100644 src/export/derived-svg-export.ts create mode 100644 src/export/file-name.ts create mode 100644 src/export/raster-export.ts create mode 100644 src/export/svg-export.ts create mode 100644 src/format/diff.ts create mode 100644 src/format/formatter.ts create mode 100644 src/main.tsx create mode 100644 src/optimization/optimization.types.ts create mode 100644 src/optimization/optimizer-client.ts create mode 100644 src/optimization/optimizer.worker.ts create mode 100644 src/optimization/profiles.ts create mode 100644 src/project/project-format.ts create mode 100644 src/security/sanitize-svg.ts create mode 100644 src/security/security.types.ts create mode 100644 src/structure/reference-index.ts create mode 100644 src/styles.css create mode 100644 src/test/setup.ts create mode 100644 src/toolbox/manifest.source.json create mode 100644 src/toolbox/manifest.ts create mode 100644 src/version.ts create mode 100644 tests/animation/preview.test.ts create mode 100644 tests/animation/validation.test.ts create mode 100644 tests/browser/svg-tools.spec.ts create mode 100644 tests/commands/history.test.ts create mode 100644 tests/document/resource-limits.test.ts create mode 100644 tests/document/source-parser.test.ts create mode 100644 tests/domain/affine.test.ts create mode 100644 tests/domain/bake-transform.test.ts create mode 100644 tests/domain/path.test.ts create mode 100644 tests/domain/transform-chain.test.ts create mode 100644 tests/export/derived-svg-export.test.ts create mode 100644 tests/export/export.test.ts create mode 100644 tests/format/format-and-diff.test.ts create mode 100644 tests/optimization/optimizer-client.test.ts create mode 100644 tests/optimization/profiles.test.ts create mode 100644 tests/project/project-format.test.ts create mode 100644 tests/security/adversarial.test.ts create mode 100644 tests/security/sanitize.test.ts create mode 100644 tests/structure/reference-accessibility.test.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4898ec6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +node_modules/ +dist/ +release/ +coverage/ +playwright-report/ +test-results/ +*.tsbuildinfo +.DS_Store +.env +.env.* +!.env.example +public/CHANGELOG.md +public/LICENSE +public/LICENSES/ +public/README.md +public/SOURCE.md +public/THIRD_PARTY_LICENSES.txt +public/THIRD_PARTY_NOTICES.md +public/docs/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..7e977af --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@add-ideas:registry=https://git.add-ideas.de/api/packages/lotobo/npm/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..599f740 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +dist +release +coverage +playwright-report +test-results +package-lock.json +public/toolbox-app.json +public/THIRD_PARTY_LICENSES.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d62e7e2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +All notable changes follow Keep a Changelog. Versions follow Semantic +Versioning. + +## [0.1.0] - 2026-08-02 + +### Added + +- Initial static, local-first SVG Tools workbench using Toolbox AppShell 0.2.3. +- Source/tree/canvas/inspector synchronization with exact source patches, + last-valid invalid-source handling and application undo/redo. +- Opaque-origin sanitized preview and project-authored hostile SVG tests. +- Standard path parsing and first-class anchor/control/arc handles. +- Transform analysis, preview and supported primitive/path baking. +- Reference, geometry, security and accessibility diagnostics. +- Worker-based SVGO profiles with cancellation and non-destructive diff. +- SVG, SVGZ, derived SVG, raster and deterministic project exports. +- Deterministic standalone release packaging and Toolbox Portal assembly smoke + test. + +[0.1.0]: https://git.add-ideas.de/lotobo/svg-tools/releases/tag/v0.1.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/LICENSES/README.md b/LICENSES/README.md new file mode 100644 index 0000000..344d14b --- /dev/null +++ b/LICENSES/README.md @@ -0,0 +1,14 @@ +# Licence inventory + +SVG Tools is `GPL-3.0-or-later`; the full project licence is at `../LICENSE`. + +`THIRD_PARTY_NOTICES.md` records every direct and transitive runtime package, +its exact installed version, licence, source and role. The release preparation +script additionally collates the project-specific licence/notice files from the +exact locked `node_modules` tree into +`LICENSES/npm-runtime-licenses.txt` inside the static artifact. That generated +file retains each package name/version and its original text. + +Development-only packages are recorded separately in the notices and are not +shipped in the browser artifact. Reference repositories were inspected but no +reference source or assets were copied or adapted. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad5cba8 --- /dev/null +++ b/README.md @@ -0,0 +1,177 @@ +# SVG Tools + +SVG Tools is a static, local-first SVG workbench. It keeps the original SVG +source as the persistent document, then derives a semantic tree, a sanitized +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 +runtime CDN dependency. + +Version 0.1.0 delivers the structured viewer/editor milestone (MVP 1) and +tested vertical slices of the path, transform, reference, optimization, +security, accessibility, animation and export milestones. The later milestone +lists below remain a roadmap, not a claim of complete vector-editor coverage. + +## Working model + +- The CodeMirror XML document is canonical. Visual changes create exact source + patches and one application-level undo transaction. +- The semantic model records element and attribute source ranges, stable + in-memory keys, source preferences, metrics and diagnostics. +- Tree, source cursor and canvas selection are synchronized. +- Invalid edits remain visible in the source editor while the tree and canvas + retain the last valid revision. Visual editing is disabled until parsing + succeeds again. +- Comments, whitespace and unrelated source remain unchanged for targeted + edits. Prettify, sanitize and optimize are explicit whole-document previews. +- No editor namespace or persistent editor attribute is added to normal SVG + exports. + +## Implemented in 0.1.0 + +- open, drop and paste SVG; magic-byte SVGZ import; new-document templates; +- XML editor, ranged diagnostics, structure tree, filtering and synchronized + selection; +- safe canvas, zoom, pan, grid, selected bounds and last-valid state; +- document dimensions, `viewBox`, `preserveAspectRatio`, arbitrary attributes, + fill, stroke, opacity, rotate, flip, translate, scale and skew; +- 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; +- anchors, cubic and quadratic controls, derived smooth controls, arc controls, + nested-transform coordinate mapping, keyboard nudging, 50% segment splitting + and path reversal; +- transform-list parsing, ancestor matrices, invertibility diagnostics, preview + overlay, and atomic baking for paths, lines, polylines, polygons, rectangles, + circles and ellipses with disclosed conversion/stroke warnings; +- ID/reference indexing, broken/ambiguous/cyclic reference diagnostics and + atomic ID rename for local URL, href and ARIA references; +- SVGO 4.0.2 in a cancelable worker with conservative, balanced and aggressive + profiles, explicit optional plugins, sizes and source diff; +- non-destructive sanitization, geometry diagnostics, accessible-name audit and + undoable root title/description fixes; +- app-owned, validated CSS opacity animation preview in the isolated canvas and + explicit source application; +- exact SVG, deterministic SVGZ, sanitized SVG, optimized SVG, selected-object + SVG, symbol sprite, PNG, WebP, JPEG and deterministic project export. + +Well-formed SVG/XML content that the semantic model does not understand is +preserved in canonical source. The editing projection deliberately excludes +active or externally loading content. Standard rendering still follows the +browser's SVG implementation, so font, filter and text layout can differ across +browsers. + +## Path controls + +Explicit cubic controls are solid; controls reflected by `S` are dashed and +labelled derived. Quadratic commands show their single control, while `T` +derives the reflection of the preceding quadratic control. Arcs expose endpoint, +center and radius controls plus their flags in the command table. Moving a +derived control intentionally expands shorthand to explicit normalized geometry. +Visual path edits currently serialize the selected `d` attribute in normalized +absolute form; surrounding source is untouched. + +Not yet implemented: multi-node modes, delete/convert/join/break/combine, +subpath-start changes, a pen tool, snapping, simplification, boolean operations, +round corners and stroke-to-path. + +## Transform model and limits + +Transform drafts never change source. SVG Tools shows the local/ancestor chain, +candidate matrix, diagnostics, before/after overlay and source diff before one +atomic Apply. General rectangle and ellipse cases may convert to a path. Baking +is refused for unsupported elements rather than partially rewriting them. + +Stroke widths are not silently rewritten for non-conformal transforms; the +preview reports the consequence. Paint servers, markers, filters, clips, masks, +text and shared definitions are not flattened or cloned. Group flattening, +text-to-path and complete paint-server coordinate remapping remain future work. + +## Security and privacy + +Imported source is never inserted into the application DOM. DOMPurify plus an +application policy produces a separate editing projection. Scripts, +`foreignObject`, event handlers, native SVG animation, navigation and unsafe +namespaces are removed; external/executable URLs and unsafe CSS resource forms +are neutralized. Only unique local fragments and bounded raster data URLs are +eligible for rendering. + +The projection runs in an opaque-origin iframe (`sandbox="allow-scripts"`) with +a restrictive child CSP. Its small self-hosted controller accepts a random +channel and messages from the parent window only, then removes its own script +element. Production servers must return the two documented cross-origin headers +for `canvas-frame-controller.js`; see [docs/SECURITY.md](docs/SECURITY.md). +Original source always remains available. Sanitization and optimization require +an explicit preview and Apply. These controls reduce the documented threat +surface; they are not a universal SVG sanitizer for every embedding context. + +Resource limits cover source bytes, decompressed SVGZ output, elements, depth, +attributes, text, path commands, CSS, references, animation/filter counts, +embedded data, history, optimization time and raster pixels. + +## Accessibility + +The current evidence-based audit checks the root accessible name (`title`, +`aria-label` or `aria-labelledby`), missing description, invalid/ambiguous ARIA +references, focusable elements without names and selected contrast evidence. +Safe root title/description fixes are undoable. This is not a complete browser +accessible-name computation and does not establish WCAG conformance. + +## Projects and persistence + +Normal work is memory-only. SVG Tools does not write source or recent-file data +to browser storage. A `.svgtools.json` project is explicit, deterministic JSON +containing canonical source, selection/expanded state, active panel, zoom, pan, +grid and app-owned animation definitions. Import validates schema, sizes, +numbers, arrays and every animation field before use. + +## Development + +Node.js 22 or newer and npm are required. + +```sh +npm ci +npm run dev +npm run typecheck +npm run lint +npm run format:check +npm test +npm run test:security +npm run test:browser +npm run build +npm run toolbox:check +``` + +`vite.config.ts` uses `base: './'`; the same build works standalone, in Toolbox +context and below a nested path. `npm run release:artifact` performs the complete +quality gate, creates a deterministic ZIP plus SHA-256 sidecar, and exercises a +temporary Toolbox Portal assembly. Portal consumes that immutable ZIP; it does +not build this repository. + +For the opaque canvas controller, a production server must return: + +```text +Access-Control-Allow-Origin: * +Cross-Origin-Resource-Policy: cross-origin +``` + +only for the app-relative `canvas-frame-controller.js`. All other app resources +should keep `Cross-Origin-Resource-Policy: same-origin`. The Toolbox Portal +0.10.0 Nginx policy contains this URI-scoped exception. + +## Source and licence + +Corresponding source and inspected revisions are recorded in [SOURCE.md](SOURCE.md). +Third-party packages and adoption decisions are recorded in +[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and [LICENSES](LICENSES/README.md). +Project-authored fixtures contain no third-party artwork or text. + +SVG Tools is licensed under `GPL-3.0-or-later`. See [LICENSE](LICENSE). + +## Roadmap + +The smallest next slice is multi-node path editing: node selection and modes, +shape-preserving deletion/insertion, segment conversion and open/join/break +operations under nested transforms. Later slices add drawing tools, grouping, +alignment/snapping, gradients/markers/filters, symbol management, advanced +geometry and broader isolated SMIL/CSS animation inspection. + +Detailed models and limitations live in [docs](docs/ARCHITECTURE.md). diff --git a/SOURCE.md b/SOURCE.md new file mode 100644 index 0000000..f8393c0 --- /dev/null +++ b/SOURCE.md @@ -0,0 +1,37 @@ +# Corresponding source and provenance + +The corresponding source for SVG Tools 0.1.0 is: + +https://git.add-ideas.de/lotobo/svg-tools/src/tag/v0.1.0 + +Build from that tag with Node.js 22 and the exact `package-lock.json`: + +```sh +npm ci +npm run release:artifact +``` + +No generated bundle is the preferred source form. No runtime code is loaded +from a CDN. The release ZIP contains the licence, corresponding-source pointer, +dependency inventory and notices. + +## Revisions inspected + +| Project | Revision/version inspected | Decision | +| ------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| 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 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 Path Editor | `937d75a83b6be2bdda11d02b9b3594841315223a` | path interaction reference only; no source copied | +| 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 | +| DOMPurify | `9365501773d6665aaf334d8afa55081b9930a684`, npm 3.4.12 | adopted as one layer of the projection policy | +| css-tree | npm 3.2.1 | adopted for parsed CSS policy/value checks | +| 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 | +| Compression fallback | fflate 0.8.3 | adopted for bounded SVGZ read/write | + +Boxy SVG and SVGViewer were consulted only as public product references. Their +source, assets, branding, layout and text were not copied. All test SVG strings +and visual assets in this repository are project-authored. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..801f79d --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,99 @@ +# Third-party notices + +The versions below are the exact installed versions in `package-lock.json` for +SVG Tools 0.1.0. “Shipped” means code or data can be present in the browser +bundle. No listed source was copied into project-authored files; packages are +consumed through their public APIs and normal bundling. + +The deterministic release contains a generated +`LICENSES/npm-runtime-licenses.txt` with each shipped package's original licence +or notice file. Repository links are the upstream source locations declared by +the packages. + +## Direct runtime dependencies + +| 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 shell React | 0.2.3 | 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 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 search | 6.7.1 | MIT | editor search | https://code.haverbeke.berlin/codemirror/search | +| CodeMirror state | 6.7.1 | MIT | editor state | https://code.haverbeke.berlin/codemirror/state | +| 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 | +| 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 | +| 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 DOM | 19.2.6 | MIT | browser rendering | https://github.com/facebook/react | +| SVGO | 4.0.2 | MIT | worker-based optional optimization | https://github.com/svg/svgo | + +## Transitive runtime dependencies + +| Package | Version | Licence | Source | +| ---------------------------- | ------- | ------------- | ------------------------------------------------------- | +| `@codemirror/autocomplete` | 6.20.3 | MIT | https://code.haverbeke.berlin/codemirror/autocomplete | +| `@lezer/common` | 1.5.2 | MIT | https://github.com/lezer-parser/common | +| `@lezer/highlight` | 1.2.3 | MIT | https://github.com/lezer-parser/highlight | +| `@lezer/lr` | 1.4.10 | MIT | https://code.haverbeke.berlin/lezer/lr | +| `@marijn/find-cluster-break` | 1.0.3 | MIT | https://code.haverbeke.berlin/marijn/find-cluster-break | +| `@types/trusted-types` | 2.0.7 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped | +| `boolbase` | 1.0.0 | ISC | https://github.com/fb55/boolbase | +| `commander` | 11.1.0 | MIT | https://github.com/tj/commander.js | +| `crelt` | 1.0.7 | MIT | https://code.haverbeke.berlin/marijn/crelt | +| `css-select` | 5.2.2 | BSD-2-Clause | https://github.com/fb55/css-select | +| `css-what` | 6.2.2 | BSD-2-Clause | https://github.com/fb55/css-what | +| `csso` | 5.0.5 | MIT | https://github.com/css/csso | +| `dom-serializer` | 2.0.0 | MIT | https://github.com/cheeriojs/dom-serializer | +| `domelementtype` | 2.3.0 | BSD-2-Clause | https://github.com/fb55/domelementtype | +| `domhandler` | 5.0.3 | BSD-2-Clause | https://github.com/fb55/domhandler | +| `domutils` | 3.2.2 | BSD-2-Clause | https://github.com/fb55/domutils | +| `entities` | 4.5.0 | BSD-2-Clause | https://github.com/fb55/entities | +| `mdn-data` | 2.27.1 | CC0-1.0 | https://github.com/mdn/data | +| `nth-check` | 2.1.1 | BSD-2-Clause | https://github.com/fb55/nth-check | +| `picocolors` | 1.1.1 | ISC | https://github.com/alexeyraspopov/picocolors | +| `sax` | 1.6.1 | BlueOak-1.0.0 | https://github.com/isaacs/sax-js | +| `scheduler` | 0.27.0 | MIT | https://github.com/facebook/react | +| `source-map-js` | 1.2.1 | BSD-3-Clause | https://github.com/7rulnik/source-map-js | +| `style-mod` | 4.1.3 | MIT | https://github.com/marijnh/style-mod | +| `w3c-keyname` | 2.2.8 | MIT | https://github.com/marijnh/w3c-keyname | + +## Development-only dependencies + +These packages are required to build, check or test source and are not shipped +as runtime modules in the release ZIP. + +| Package | Version | Licence | +| ----------------------------- | ------- | ---------- | +| `@add-ideas/toolbox-testkit` | 0.2.3 | Apache-2.0 | +| `@eslint/js` | 10.0.1 | MIT | +| `@playwright/test` | 1.61.1 | Apache-2.0 | +| `@testing-library/jest-dom` | 6.9.1 | MIT | +| `@testing-library/react` | 16.3.2 | MIT | +| `@testing-library/user-event` | 14.6.1 | MIT | +| `@types/css-tree` | 2.3.11 | MIT | +| `@types/node` | 25.8.0 | MIT | +| `@types/react` | 19.2.14 | MIT | +| `@types/react-dom` | 19.2.3 | MIT | +| `@vitejs/plugin-react` | 6.0.2 | MIT | +| `eslint` | 10.4.0 | MIT | +| `eslint-plugin-react-hooks` | 7.1.1 | MIT | +| `eslint-plugin-react-refresh` | 0.5.2 | MIT | +| `fast-check` | 4.9.0 | MIT | +| `globals` | 17.6.0 | MIT | +| `jsdom` | 29.1.1 | MIT | +| `prettier` | 3.8.3 | MIT | +| `typescript` | 6.0.3 | Apache-2.0 | +| `typescript-eslint` | 8.59.3 | MIT | +| `vite` | 8.2.0 | MIT | +| `vitest` | 4.1.6 | MIT | + +## Reviewed but not shipped + +SVG-Edit, SVG Path Editor and SVGPathCommander were inspected at the revisions +in `SOURCE.md`; no code, fixture, asset or explanatory text was copied or +adapted. Boxy SVG and SVGViewer were product references only. No boolean +geometry library, licensed parser, external raster engine, font or optional +Wasm module is included. All hostile and semantic fixtures are project-authored. diff --git a/docs/ACCESSIBILITY.md b/docs/ACCESSIBILITY.md new file mode 100644 index 0000000..b13ce89 --- /dev/null +++ b/docs/ACCESSIBILITY.md @@ -0,0 +1,26 @@ +# Accessibility audit + +The audit is evidence-based and intentionally narrower than a browser or +assistive-technology accessible-name computation. + +Implemented checks include: + +- root accessible name evidence from direct `title`, `aria-label` or a uniquely + resolved `aria-labelledby` target; +- missing root description (`desc`/`aria-describedby` evidence); +- missing, duplicate or ambiguous ARIA ID references; +- focusable graphical/link elements without local naming evidence; +- selected fill/stroke contrast evidence where simple literal colours can be + evaluated; +- reference diagnostics that can invalidate accessible relationships. + +Findings identify severity, rule, evidence, limitation and a suggested fix. +Safe automatic fixes insert a root `title` or `desc` through an exact source +patch, preserve surrounding source and are undoable. + +Limitations: the audit does not compute CSS cascade, rendered visibility, +language, reading order, browser/AT mappings, text contrast over arbitrary +backgrounds, keyboard interaction quality or author intent. It does not certify +WCAG conformance. Generated placeholder title/description text must be replaced +with an author-appropriate description and tested with target browsers and +assistive technology. diff --git a/docs/ANIMATION.md b/docs/ANIMATION.md new file mode 100644 index 0000000..b00652f --- /dev/null +++ b/docs/ANIMATION.md @@ -0,0 +1,28 @@ +# Animation + +Native SVG animation elements are discovered/countable in source but removed +from every rendered editing projection, so imported SMIL and event-triggered +animation cannot execute automatically. Imported scripts are never supported. + +Version 0.1.0 implements a deliberately small app-owned animation slice: + +- add an opacity animation to the selected element; +- enable/disable/remove definitions; +- preview generated CSS only in the opaque-origin canvas; +- persist definitions in an explicit `.svgtools.json` project; +- apply validated CSS plus stable target IDs to source in one transaction. + +One shared validator is used for project import, preview and Apply. It bounds +definition/keyframe counts and strings, requires finite ordered offsets and +bounded timing/iteration values, allowlists property/kind/direction/fill mode, +accepts only safe easing grammar and parses every CSS value with css-tree. URLs, +resource functions, structural CSS/XML characters and parser raw nodes are +rejected even for disabled definitions. Selectors and keyframe identifiers are +escaped/generated by the app. + +The preview has play/pause as an enable toggle but no timeline, seek or native +SMIL editing. Duration/keyframes are not yet exposed as a general UI. Transform, +colour, motion-path and compatible path-morph authoring, animation discovery +panels and conversion between CSS/SMIL remain future milestones. Applying CSS +changes the SVG source intentionally and may affect downstream consumers that +do not support the same CSS animation features. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..643dfd1 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,53 @@ +# Architecture + +## Boundaries + +SVG Tools is a static Vite application. React owns controls and projections, +not the document. Canonical SVG source is the only persistent document +representation. Normal sessions stay in memory; explicit downloads are the +only persistence boundary. + +```text +canonical source + ├─ ranged XML scan + DOM parse → semantic document → tree/inspectors + ├─ security policy + DOMPurify → editing projection → opaque iframe + └─ source patches/transactions ← visual commands and explicit workflows +``` + +The last valid semantic document and projection remain available while current +source is invalid. Revision checks reject stale visual, worker or dialog results. + +## Representations + +1. **Source document:** exact user text, including comments, quotes and + whitespace. +2. **Semantic document:** SVG elements, attributes, source ranges, in-memory + keys, relationships, preferences, metrics and diagnostics. +3. **Editing projection:** cloned, sanitized SVG with temporary mapping + attributes. It is never exported as current SVG. +4. **Preview:** the projection in a sandboxed opaque-origin iframe, plus a + parent-owned overlay for bounds and path controls. + +Imported source is never rendered through React or inserted into the main DOM. +The iframe controller is a fixed packaged asset, accepts only its parent and a +random per-instance channel, and removes its script element after startup. + +## Commands and workers + +Every accepted edit is a transaction containing exact before/after source, +patches, selection and revision. Undo validates the current source before +restoring exact text. Fast consecutive source/key nudge edits may merge under a +bounded merge key. + +SVGO executes in a dedicated module worker. Jobs have IDs, timeout, +supersession, AbortSignal cancellation and immediate deterministic rejection. +Parsing currently uses bounded synchronous browser XML facilities because its +`XMLDocument` projection is not transferable; large inputs are throttled and +hard-limited. Rasterization uses a sanitized Blob URL and Canvas 2D. + +## Release boundary + +`base: './'` keeps every asset relocatable. The independent ZIP is built and +checked here. Toolbox Portal consumes the immutable ZIP and checksum through a +release lock; it never compiles SVG Tools source. AppShell reads optional +Toolbox context but standalone operation is the default fallback. diff --git a/docs/DOCUMENT_MODEL.md b/docs/DOCUMENT_MODEL.md new file mode 100644 index 0000000..23c3963 --- /dev/null +++ b/docs/DOCUMENT_MODEL.md @@ -0,0 +1,45 @@ +# Document model + +## Source ranges + +The lexical scan records exact half-open offsets for each opening/full element, +name and quoted attribute. CodeMirror's Lezer XML parser supplies ranged syntax +errors; a separate stack scan rejects structures browser DOM parsers might +repair. The DOM parse supplies namespace and decoded semantic values. Element +order must match the lexical tokens or parsing fails with a source-map mismatch. + +DOCTYPE text remains in canonical source but is removed before DOM parsing. +Entity declarations are rejected for the semantic model. External entities are +never resolved. + +## Stable keys + +A unique explicit SVG `id` becomes `id:`. Otherwise a key hashes structural +path, source offset, local name and optional ID; collision suffixes are +in-memory. Reparsing retains explicit-ID identity and best-effort structural +identity. Keys appear only as temporary `data-svg-tools-node` attributes in the +editing projection and are stripped from exports. + +## Semantic nodes + +Nodes retain namespace/name, parent and children, depth, ID/classes, decoded +attributes, exact attribute ranges, render status and leaf text. The document +also records indentation/newline/quote preference, ordered keys, diagnostics +and bounded metrics. + +Unknown well-formed source remains canonical even when there is no inspector. +A targeted command patches only its attribute/tag range. Whole-source +serialization occurs only for explicit format, sanitize or optimize workflows, +each with a source diff. + +## Invalid state + +Every source update gets a monotonically increasing revision. If current source +is invalid, diagnostics refer to that text while the canvas/tree use the last +valid snapshot and all visual controls are disabled. Once the same current +revision parses and sanitizes successfully it atomically replaces the snapshot. + +Resource limits can make otherwise well-formed source invalid for this app. +Limits cover bytes, structure, path/CSS/reference/animation/filter counts, +embedded data and total text. This prevents unbounded model construction but is +not a promise that every file below a limit is inexpensive on every device. diff --git a/docs/OPTIMIZATION.md b/docs/OPTIMIZATION.md new file mode 100644 index 0000000..ee8c62b --- /dev/null +++ b/docs/OPTIMIZATION.md @@ -0,0 +1,34 @@ +# Optimization + +SVG Tools ships SVGO 4.0.2 (`gitHead` +`b2309cf541aee11634eb653157b0ff86ab326e98`) inside a module worker. The exact +version and active plugin list are displayed with every result. + +## Profiles + +- **Conservative:** cleanup and safe numeric/style normalization while retaining + IDs, metadata, viewBox, dimensions and accessibility content. +- **Balanced:** additional structural cleanup with protected identifiers and + accessibility-sensitive content. +- **Aggressive:** explicitly lossy/structural choices for users who accept the + disclosed risk. + +Optional plugins are selected explicitly in the dialog; plugin names are mapped +to an application allowlist rather than passed through from imported data. See +`src/optimization/profiles.ts` for the authoritative ordered lists. + +The worker returns source, exact before/after UTF-8 byte counts, version and +plugins. The main thread rejects stale job IDs. Closing/canceling, a superseding +request or AbortSignal immediately rejects the old promise, removes listeners, +clears its timer and terminates its worker. A 30-second safety timeout is the +final boundary. + +Optimization never mutates source automatically. A result remains a candidate +with size summary, source diff and side-by-side isolated visual comparison until +Apply. Applying checks the source revision and produces one undoable transaction. + +Even conservative SVGO passes can change insignificant lexical form and browser +edge behavior. Aggressive plugins can change IDs, metadata, editorability or +rendering. Visual comparison cannot prove semantic equivalence, especially for +fonts, filters, animation and external consumer behavior. Keep original source +and use project/domain fixtures for important artwork. diff --git a/docs/PATH_EDITOR.md b/docs/PATH_EDITOR.md new file mode 100644 index 0000000..807725d --- /dev/null +++ b/docs/PATH_EDITOR.md @@ -0,0 +1,42 @@ +# Path editor + +The project-authored path core accepts every SVG path command: `M/m`, `L/l`, +`H/h`, `V/v`, `C/c`, `S/s`, `Q/q`, `T/t`, `A/a` and `Z/z`, including repeated +parameter groups, implicit lines after move, packed arc flags, exponents, +relative forms and multiple subpaths. It rejects non-finite values, malformed +arity/flags and paths beyond the command limit. + +Segments resolve to absolute geometry while retaining source command/form and +fragment offsets for the command table. Serialization intentionally normalizes +edited geometry; no editor metadata is emitted. + +## Controls + +- Every segment endpoint is an anchor. +- Cubics show both controls. `S` reflects the previous cubic control and marks + the resulting handle as derived/dashed until edited. +- Quadratics show their one control. `T` reflects the previous quadratic + control and marks it derived. +- Arcs expose endpoint, radii and derived center controls and show rotation, + large-arc and sweep flags in the command panel. +- Handle radii and strokes use screen-oriented overlay styling so they remain + usable across zoom levels. + +The full selected-element/ancestor transform chain maps local path geometry into +root space. Pointer coordinates use its inverse, including an active preview +matrix; non-invertible chains disable handles. Dragging commits one transaction +on release; arrow keys use 1, Shift+10 or Alt+0.1 units and merge safely. + +Segment splitting uses the mathematical midpoint: linear interpolation for +lines, de Casteljau for quadratic/cubic curves, and arc subdivision preserving +the ellipse. Path reversal reverses every supported segment/subpath and adjusts +controls/sweep where required. + +## Current limits + +The 0.1.0 UI supplies drag/nudge, split at 50%, reverse and a read-only numeric +command table. It does not yet supply node modes, multi-selection, deletion, +conversion, open/close, join/break/combine, subpath-start changes, arbitrary +split parameter, pen creation, snapping, simplification or boolean operations. +Visual editing expands shorthand/relative commands to normalized absolute data; +the dialog/help discloses that source-form change. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..8830dfc --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,79 @@ +# Security model + +SVG is active content. Threats include script/event execution, navigation, +external fetches and tracking, CSS resource resolution, `foreignObject`, native +animation triggers, namespace confusion, entity/DOCTYPE processing, parser +differentials, oversized compressed/data content and algorithmic complexity. + +## Source and projection + +Canonical source is inert text and remains available even when unsafe. A +separate projection is built from a cloned semantic document. DOMPurify's SVG +profiles are followed by an application policy that: + +- removes `script`, `foreignObject`, `animate`, `animateMotion`, + `animateTransform`, `set`, foreign namespaces and all `on*` attributes; +- disables links/navigation and executable or external URL attributes; +- allows only unique local fragment references and bounded base64 raster image + data (`png`, `jpeg`, `gif`, `webp`, `avif`); +- parses style declarations/stylesheets with css-tree and rejects `@import`, + unsafe legacy properties, parse failures, non-local URLs, `expression()` and + resource-producing functions such as `image-set()` and `paint()`; +- reports findings with source ranges wherever a semantic node/range exists. + +DOCTYPE is excluded before DOM parsing; entity declarations invalidate the +semantic projection. Sanitized export removes temporary mapping attributes. +Sanitize is never applied to canonical source without preview and acceptance. + +## Isolated preview + +The projection is an iframe `srcdoc` with `sandbox="allow-scripts"` and no +`allow-same-origin`, giving it an opaque origin. Its child CSP is: + +```text +default-src 'none'; connect-src 'none'; object-src 'none'; frame-src 'none'; +base-uri 'none'; form-action 'none'; img-src data: blob:; +style-src 'unsafe-inline'; script-src +``` + +The sole script is the packaged `canvas-frame-controller.js`. It is loaded with +anonymous CORS, accepts messages only from `parent` with a random channel, +validates message fields and removes its script element. The parent validates +both `event.source` and channel. + +Because a sandbox without same-origin treats the script request as cross-origin, +serve this one app-relative asset with: + +```text +Access-Control-Allow-Origin: * +Cross-Origin-Resource-Policy: cross-origin +``` + +Keep `Cross-Origin-Resource-Policy: same-origin` for other files. Use an Nginx +`map`/header value, not a nested `location` that would drop inherited security +headers. Toolbox Portal 0.10.0 implements and tests this exception. + +## Limits and tests + +Hard limits are defined in `src/app/limits.ts`. SVGZ uses streaming +decompression and stops once output crosses the source limit. Workers are +revisioned, timed out and cancelable. Tree/diagnostic rendering is capped. + +Project-authored tests cover script, handlers, links, external image/use/filter, +CSS imports/URLs/parser failures/resource functions, native animation, +namespaces, duplicate IDs/cycles, malformed entities/XML, depth/attribute/path +and data limits. Browser tests assert no script callback and no request to the +hostile domain. + +## Limitations and reporting + +This policy protects this application's projections and exports; it is not a +general guarantee for arbitrary downstream embedding. Browser SVG/CSS parsers +and DOMPurify remain dependency/security boundaries. Bounded data URLs may still +decode expensive images within the raster-pixel limits. CSP header regressions +can break the fixed controller even when the content remains inert. + +Report vulnerabilities privately through the repository owner/contact before +opening a public issue when disclosure could expose users. Include the SVG, +browser, deployment headers and observed network/execution behavior without +sensitive user files. diff --git a/docs/TRANSFORM_MODEL.md b/docs/TRANSFORM_MODEL.md new file mode 100644 index 0000000..3cd5573 --- /dev/null +++ b/docs/TRANSFORM_MODEL.md @@ -0,0 +1,41 @@ +# Transform model + +SVG transform functions are parsed without changing order: `matrix`, +`translate`, `scale`, `rotate` (with optional center), `skewX` and `skewY`. +The inspector shows each selected/ancestor source transform, accumulated matrix, +determinant/invertibility diagnostics and the candidate matrix. + +A draft combines translate, origin, rotate, scale and skew. It affects only the +iframe/overlay until Apply. The proposed source and rendering are shown before +one revision-checked transaction; undo restores the exact pre-apply text. + +## Bake support + +| Element | Supported result | +| --------------------- | ----------------------------------------------------------------------------------------------------------- | +| `path` | all segment points transformed; arcs retained for safe conformal cases or converted/disclosed when required | +| `line` | transformed endpoints, remains `line` | +| `polyline`, `polygon` | transformed point list, same element | +| `rect` | remains `rect` for axis-aligned safe cases; otherwise candidate path | +| `circle` | remains circle under conformal scale; otherwise explicit ellipse/path as required | +| `ellipse` | remains ellipse for safe axis-aligned cases; otherwise path | + +Numbers remain full precision internally and are serialized with stable bounded +precision only at the explicit operation boundary. Invalid/non-finite geometry, +negative radii and unsupported element types fail before patch creation. + +## Policies and limits + +- Non-conformal transforms with a visible stroke produce a warning; stroke + width/vector-effect is not silently rewritten. +- General arc conversion is disclosed in the preview. +- Shared gradients, patterns, markers, filters, masks and clips are indexed but + not cloned or remapped during bake. +- Text transforms remain attributes; text-to-path is not implemented. +- Group flattening and partial multi-child baking are not implemented. +- Consolidation is available through an appended matrix candidate; a full + editable transform-list UI/decomposition is future work. + +Browser rendering, particularly text and filters, is the visual oracle; the +unit suite checks matrix and geometry invariants but does not claim pixel-perfect +equivalence for every renderer. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..f930094 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,43 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: [ + "dist", + "release", + "coverage", + "test-results", + "playwright-report", + ], + }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2023, + globals: { + ...globals.browser, + ...globals.worker, + }, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, + }, + { + files: ["scripts/**/*.mjs", "playwright.config.ts"], + languageOptions: { globals: globals.node }, + }, +); diff --git a/index.html b/index.html new file mode 100644 index 0000000..6288e9d --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + + + + SVG Tools + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5f96238 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4453 @@ +{ + "name": "svg-tools", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "svg-tools", + "version": "0.1.0", + "license": "GPL-3.0-or-later", + "dependencies": { + "@add-ideas/toolbox-contract": "0.2.3", + "@add-ideas/toolbox-shell-react": "0.2.3", + "@codemirror/commands": "6.10.4", + "@codemirror/lang-xml": "6.1.0", + "@codemirror/language": "6.12.4", + "@codemirror/search": "6.7.1", + "@codemirror/state": "6.7.1", + "@codemirror/view": "6.43.6", + "@lezer/xml": "1.0.6", + "css-tree": "3.2.1", + "dompurify": "3.4.12", + "fflate": "0.8.3", + "react": "19.2.6", + "react-dom": "19.2.6", + "svgo": "4.0.2" + }, + "devDependencies": { + "@add-ideas/toolbox-testkit": "0.2.3", + "@eslint/js": "10.0.1", + "@playwright/test": "1.61.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "16.3.2", + "@testing-library/user-event": "14.6.1", + "@types/css-tree": "2.3.11", + "@types/node": "25.8.0", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "6.0.2", + "eslint": "10.4.0", + "eslint-plugin-react-hooks": "7.1.1", + "eslint-plugin-react-refresh": "0.5.2", + "fast-check": "4.9.0", + "globals": "17.6.0", + "jsdom": "29.1.1", + "prettier": "3.8.3", + "typescript": "6.0.3", + "typescript-eslint": "8.59.3", + "vite": "8.2.0", + "vitest": "4.1.6" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@add-ideas/toolbox-contract": { + "version": "0.2.3", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.2.3/toolbox-contract-0.2.3.tgz", + "integrity": "sha512-T0PVSuMT40GjTDfQJhEEY3ZawQq8zz1/ry95JdKI6W39CdLacaRXdGnEpDCMHt+jUbf1Jz7Nat/M5dFCgKVM9A==", + "license": "Apache-2.0" + }, + "node_modules/@add-ideas/toolbox-shell-react": { + "version": "0.2.3", + "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", + "integrity": "sha512-DT5lQDH48BFkFcmFLZnQh7+Cm73JzBPcmp5WzUXypfkUXpEyDYHzaXgmW4kZ0edSwh4RK4sPmx+JPtK0X4aKCQ==", + "license": "Apache-2.0", + "dependencies": { + "@add-ideas/toolbox-contract": "0.2.3" + }, + "peerDependencies": { + "react": ">=18 <20", + "react-dom": ">=18 <20" + } + }, + "node_modules/@add-ideas/toolbox-testkit": { + "version": "0.2.3", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.2.3/toolbox-testkit-0.2.3.tgz", + "integrity": "sha512-sq1MwhKWfFKen+N+124hl74qQimRSvmQ9sOU7jdcI+2qCKZ67+2B8rWyezeV80uTFu4Jv6deHksfYQ/tKNV6XQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@add-ideas/toolbox-contract": "0.2.3" + }, + "bin": { + "toolbox-check": "dist/cli.js" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz", + "integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", + "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz", + "integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz", + "integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz", + "integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", + "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", + "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz", + "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz", + "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz", + "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz", + "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz", + "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz", + "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz", + "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz", + "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz", + "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz", + "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz", + "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz", + "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "2.0.0-alpha.3", + "@emnapi/runtime": "2.0.0-alpha.3", + "@napi-rs/wasm-runtime": "^1.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz", + "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz", + "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/css-tree": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-2.3.11.tgz", + "integrity": "sha512-aEokibJOI77uIlqoBOkVbaQGC9zII0A+JH1kcTNKW2CwyYWD8KM6qdo+4c77wD3wZOQfJuNWAr9M4hdk+YhDIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.3", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.9.tgz", + "integrity": "sha512-cp447VUsGS07+n1Dqf7YSQ8maeJrjEhaDxTm1ZefbqDtypHBC5GzGMQbklR6IPR13Y8OAJRHZWEMtZipJLCttg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.399", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.399.tgz", + "integrity": "sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-check": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.9.0.tgz", + "integrity": "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", + "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.142.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.1", + "@rolldown/binding-darwin-arm64": "1.2.1", + "@rolldown/binding-darwin-x64": "1.2.1", + "@rolldown/binding-freebsd-x64": "1.2.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", + "@rolldown/binding-linux-arm64-gnu": "1.2.1", + "@rolldown/binding-linux-arm64-musl": "1.2.1", + "@rolldown/binding-linux-ppc64-gnu": "1.2.1", + "@rolldown/binding-linux-s390x-gnu": "1.2.1", + "@rolldown/binding-linux-x64-gnu": "1.2.1", + "@rolldown/binding-linux-x64-musl": "1.2.1", + "@rolldown/binding-openharmony-arm64": "1.2.1", + "@rolldown/binding-wasm32-wasi": "1.2.1", + "@rolldown/binding-win32-arm64-msvc": "1.2.1", + "@rolldown/binding-win32-x64-msvc": "1.2.1" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz", + "integrity": "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3fbae97 --- /dev/null +++ b/package.json @@ -0,0 +1,85 @@ +{ + "name": "svg-tools", + "version": "0.1.0", + "description": "Inspect, edit, optimize and transform SVG documents locally in the browser.", + "license": "GPL-3.0-or-later", + "author": "Albrecht Degering", + "repository": { + "type": "git", + "url": "git+https://git.add-ideas.de/lotobo/svg-tools.git" + }, + "homepage": "https://git.add-ideas.de/lotobo/svg-tools", + "bugs": { + "url": "https://git.add-ideas.de/lotobo/svg-tools/issues" + }, + "private": true, + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "predev": "npm run manifest:generate", + "dev": "vite", + "prebuild": "npm run release:prepare && npm run manifest:generate", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc -b --pretty false", + "lint": "eslint . --max-warnings=0", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "vitest run", + "test:watch": "vitest", + "test:security": "vitest run tests/security", + "test:browser": "playwright test", + "manifest:generate": "node scripts/generate-toolbox-manifest.mjs", + "manifest:check": "node scripts/generate-toolbox-manifest.mjs --check", + "release:prepare": "node scripts/prepare-release-files.mjs", + "toolbox:check": "toolbox-check dist", + "package:release": "node scripts/package-release.mjs", + "portal:smoke": "node scripts/portal-assembly-smoke.mjs", + "check": "npm run manifest:check && npm run typecheck && npm run lint && npm run format:check && npm run test && npm run build && npm run toolbox:check", + "release:artifact": "npm run check && npm run test:security && npm run test:browser && npm run package:release -- --force && npm run portal:smoke" + }, + "dependencies": { + "@add-ideas/toolbox-contract": "0.2.3", + "@add-ideas/toolbox-shell-react": "0.2.3", + "@codemirror/commands": "6.10.4", + "@codemirror/lang-xml": "6.1.0", + "@codemirror/language": "6.12.4", + "@codemirror/search": "6.7.1", + "@codemirror/state": "6.7.1", + "@codemirror/view": "6.43.6", + "@lezer/xml": "1.0.6", + "css-tree": "3.2.1", + "dompurify": "3.4.12", + "fflate": "0.8.3", + "react": "19.2.6", + "react-dom": "19.2.6", + "svgo": "4.0.2" + }, + "devDependencies": { + "@add-ideas/toolbox-testkit": "0.2.3", + "@eslint/js": "10.0.1", + "@playwright/test": "1.61.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "16.3.2", + "@testing-library/user-event": "14.6.1", + "@types/css-tree": "2.3.11", + "@types/node": "25.8.0", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "6.0.2", + "eslint": "10.4.0", + "eslint-plugin-react-hooks": "7.1.1", + "eslint-plugin-react-refresh": "0.5.2", + "fast-check": "4.9.0", + "globals": "17.6.0", + "jsdom": "29.1.1", + "prettier": "3.8.3", + "typescript": "6.0.3", + "typescript-eslint": "8.59.3", + "vite": "8.2.0", + "vitest": "4.1.6" + }, + "packageManager": "npm@11.17.0" +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..60dbc27 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests/browser", + fullyParallel: false, + workers: 2, + timeout: 120_000, + expect: { timeout: 10_000 }, + reporter: [["list"]], + use: { + baseURL: "http://127.0.0.1:4173", + trace: "retain-on-failure", + }, + webServer: { + command: "npm run build && node scripts/serve-test.mjs", + url: "http://127.0.0.1:4173", + reuseExistingServer: !process.env.CI, + timeout: 180_000, + }, + projects: [ + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + ], +}); diff --git a/public/canvas-frame-controller.js b/public/canvas-frame-controller.js new file mode 100644 index 0000000..aeb302c --- /dev/null +++ b/public/canvas-frame-controller.js @@ -0,0 +1,84 @@ +(() => { + "use strict"; + + const controllerScript = document.currentScript; + const channel = document.documentElement.dataset.svgToolsChannel; + if (!channel) { + controllerScript?.remove(); + return; + } + + const send = (message) => parent.postMessage({ ...message, channel }, "*"); + const find = (key) => + typeof key === "string" + ? document.querySelector(`[data-svg-tools-node="${CSS.escape(key)}"]`) + : null; + const reportBox = (key) => { + const element = find(key); + let box = null; + try { + if (element && typeof element.getBBox === "function") { + const candidate = element.getBBox(); + if ( + [candidate.x, candidate.y, candidate.width, candidate.height].every( + Number.isFinite, + ) + ) { + box = { + x: candidate.x, + y: candidate.y, + width: candidate.width, + height: candidate.height, + }; + } + } + } catch { + // Some SVG elements do not expose geometry in every browser. + } + send({ type: "selection-box", key, box }); + }; + + document.addEventListener("click", (event) => { + const target = + event.target instanceof Element + ? event.target.closest("[data-svg-tools-node]") + : null; + const key = target?.getAttribute("data-svg-tools-node"); + if (key) send({ type: "select", key }); + if (event.target instanceof Element && event.target.closest("a")) { + event.preventDefault(); + } + }); + + addEventListener("message", (event) => { + if (event.source !== parent) return; + const message = event.data; + if (!message || message.channel !== channel) return; + if (message.type === "ping") send({ type: "ready" }); + if (message.type === "selection" && typeof message.key === "string") { + reportBox(message.key); + } + if ( + message.type === "path" && + typeof message.key === "string" && + typeof message.data === "string" + ) { + find(message.key)?.setAttribute("d", message.data); + reportBox(message.key); + } + if ( + message.type === "transform" && + typeof message.key === "string" && + (typeof message.value === "string" || message.value === null) + ) { + const element = find(message.key); + if (!element) return; + if (message.value) element.setAttribute("transform", message.value); + else element.removeAttribute("transform"); + reportBox(message.key); + } + }); + + send({ type: "ready" }); + controllerScript?.remove(); +})(); diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..4e3166f --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,13 @@ + + SVG Tools + A vector document with an editable curve and path nodes. + + + + + + + + + + diff --git a/public/toolbox-app.json b/public/toolbox-app.json new file mode 100644 index 0000000..8a6c8a8 --- /dev/null +++ b/public/toolbox-app.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://git.add-ideas.de/lotobo/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json", + "schemaVersion": 1, + "id": "de.add-ideas.svg-tools", + "name": "SVG Tools", + "version": "0.1.0", + "description": "Inspect, edit, optimize and transform SVG documents locally in the browser.", + "entry": "./", + "icon": "./favicon.svg", + "categories": ["graphics", "design", "developer"], + "tags": [ + "svg", + "vector", + "path", + "xml", + "transform", + "optimize", + "accessibility" + ], + "integration": { + "contextVersion": 1, + "launchModes": ["navigate", "new-tab"], + "embedding": "unsupported" + }, + "requirements": { + "secureContext": false, + "workers": true, + "indexedDb": false, + "crossOriginIsolated": false, + "topLevelContext": false + }, + "privacy": { + "processing": "local", + "fileUploads": false, + "telemetry": false + }, + "source": { + "repository": "https://git.add-ideas.de/lotobo/svg-tools", + "license": "GPL-3.0-or-later" + }, + "actions": [ + { + "id": "source", + "label": "Source", + "url": "https://git.add-ideas.de/lotobo/svg-tools" + } + ], + "assets": ["./canvas-frame-controller.js"] +} diff --git a/scripts/generate-toolbox-manifest.mjs b/scripts/generate-toolbox-manifest.mjs new file mode 100644 index 0000000..b123906 --- /dev/null +++ b/scripts/generate-toolbox-manifest.mjs @@ -0,0 +1,69 @@ +import { lstat, readFile, writeFile } from "node:fs/promises"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; +import { format } from "prettier"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const sourcePath = join(root, "src", "toolbox", "manifest.source.json"); +const outputPath = join(root, "public", "toolbox-app.json"); +const packagePath = join(root, "package.json"); +const applicationVersionPath = join(root, "src", "version.ts"); +const publicPath = join(root, "public"); +const checkOnly = process.argv.includes("--check"); + +const source = JSON.parse(await readFile(sourcePath, "utf8")); +const packageJson = JSON.parse(await readFile(packagePath, "utf8")); +const applicationVersionSource = await readFile(applicationVersionPath, "utf8"); +const applicationVersion = + /^export const APPLICATION_VERSION = "([^"]+)";$/mu.exec( + applicationVersionSource, + )?.[1]; + +if ( + source.version !== packageJson.version || + applicationVersion !== packageJson.version +) { + throw new Error( + `Version drift: manifest ${source.version}, application ${String(applicationVersion)}, package ${packageJson.version}`, + ); +} + +if ( + source.source?.repository !== "https://git.add-ideas.de/lotobo/svg-tools" || + source.source?.license !== "GPL-3.0-or-later" +) { + throw new Error("Manifest source identity is incomplete or inconsistent"); +} + +for (const asset of source.assets ?? []) { + if ( + typeof asset !== "string" || + !asset.startsWith("./") || + asset.includes("\\") || + asset.split("/").includes("..") + ) { + throw new Error(`Unsafe manifest asset path: ${JSON.stringify(asset)}`); + } + const details = await lstat(join(publicPath, asset.slice(2))).catch( + () => null, + ); + if (!details?.isFile() || details.isSymbolicLink()) { + throw new Error(`Manifest asset is missing or unsafe: ${asset}`); + } +} + +const serialized = await format(JSON.stringify(source), { + filepath: outputPath, +}); +if (checkOnly) { + const current = await readFile(outputPath, "utf8").catch(() => ""); + if (current !== serialized) { + throw new Error( + `${relative(root, outputPath)} is stale; run npm run manifest:generate`, + ); + } + console.log("Toolbox manifest is synchronized"); +} else { + await writeFile(outputPath, serialized); + console.log(`Generated ${relative(root, outputPath)}`); +} diff --git a/scripts/package-release.mjs b/scripts/package-release.mjs new file mode 100644 index 0000000..727b2bb --- /dev/null +++ b/scripts/package-release.mjs @@ -0,0 +1,240 @@ +#!/usr/bin/env node +import { createHash } from "node:crypto"; +import { + access, + lstat, + mkdir, + mkdtemp, + readFile, + readdir, + rename, + rm, + writeFile, +} from "node:fs/promises"; +import path from "node:path"; +import { deflateRawSync } from "node:zlib"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const packageJson = JSON.parse( + await readFile(path.join(root, "package.json"), "utf8"), +); +const input = path.join(root, "dist"); +const argument = (name, fallback) => { + const index = process.argv.indexOf(name); + return index >= 0 ? process.argv[index + 1] : fallback; +}; +const output = path.resolve( + root, + argument("--output", `release/svg-tools-${packageJson.version}.zip`), +); +const checksumOutput = `${output}.sha256`; +const force = process.argv.includes("--force"); + +if (path.extname(output).toLowerCase() !== ".zip") { + throw new Error("Release output must use a .zip extension"); +} +if (output === path.parse(output).root || output === root) { + throw new Error("Release output is not a safe file target"); +} + +const exists = (file) => + access(file).then( + () => true, + () => false, + ); +if (!force && (await exists(output))) { + throw new Error(`Release already exists (use --force): ${output}`); +} +if (!force && (await exists(checksumOutput))) { + throw new Error(`Checksum already exists (use --force): ${checksumOutput}`); +} + +const required = [ + "index.html", + "toolbox-app.json", + "favicon.svg", + "canvas-frame-controller.js", + "README.md", + "CHANGELOG.md", + "LICENSE", + "SOURCE.md", + "THIRD_PARTY_NOTICES.md", + "THIRD_PARTY_LICENSES.txt", + "LICENSES/README.md", + "LICENSES/npm-runtime-licenses.txt", +]; +for (const name of required) { + const details = await lstat(path.join(input, name)).catch(() => null); + if (!details?.isFile() || details.isSymbolicLink()) { + throw new Error(`Release input is missing a regular file: ${name}`); + } +} +const assets = await lstat(path.join(input, "assets")).catch(() => null); +if (!assets?.isDirectory() || assets.isSymbolicLink()) { + throw new Error("Release input is missing its assets directory"); +} + +const manifest = JSON.parse( + await readFile(path.join(input, "toolbox-app.json"), "utf8"), +); +if ( + manifest.id !== "de.add-ideas.svg-tools" || + manifest.version !== packageJson.version || + manifest.entry !== "./" || + manifest.icon !== "./favicon.svg" || + !manifest.assets?.includes("./canvas-frame-controller.js") +) { + throw new Error("Packaged Toolbox manifest identity/assets are invalid"); +} + +async function collect(directory, prefix = "") { + const files = []; + for (const entry of (await readdir(directory, { withFileTypes: true })).sort( + (left, right) => + left.name < right.name ? -1 : left.name > right.name ? 1 : 0, + )) { + const absolute = path.join(directory, entry.name); + const relative = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isSymbolicLink()) { + throw new Error(`Release input contains a symbolic link: ${relative}`); + } + if (entry.isDirectory()) files.push(...(await collect(absolute, relative))); + else if (entry.isFile()) files.push({ absolute, relative }); + else throw new Error(`Unsupported release entry: ${relative}`); + } + return files; +} + +const files = await collect(input); +if (files.length > 65_535) throw new Error("Release contains too many files"); +for (const { relative } of files) { + if ( + relative.endsWith(".map") || + /(?:^|\/)(?:\.env(?:\.|$)|id_rsa|id_ed25519|.*\.pem$|.*\.key$)/iu.test( + relative, + ) + ) { + throw new Error(`Forbidden release entry: ${relative}`); + } + if (relative.startsWith("/") || relative.split("/").includes("..")) { + throw new Error(`Unsafe release entry: ${relative}`); + } +} + +const indexHtml = await readFile(path.join(input, "index.html"), "utf8"); +if (/\b(?:src|href)=["']\//iu.test(indexHtml)) { + throw new Error("index.html contains a root-absolute asset reference"); +} + +const crcTable = new Uint32Array(256); +for (let index = 0; index < 256; index += 1) { + let value = index; + for (let bit = 0; bit < 8; bit += 1) { + value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; + } + crcTable[index] = value >>> 0; +} +function crc32(bytes) { + let value = 0xffffffff; + for (const byte of bytes) + value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8); + return (value ^ 0xffffffff) >>> 0; +} +function header(length) { + return Buffer.alloc(length); +} + +const localParts = []; +const centralParts = []; +let offset = 0; +for (const file of files) { + const source = await readFile(file.absolute); + const compressed = deflateRawSync(source, { level: 9 }); + const name = Buffer.from(file.relative, "utf8"); + const checksum = crc32(source); + if ( + source.byteLength > 0xffffffff || + compressed.byteLength > 0xffffffff || + offset > 0xffffffff + ) { + throw new Error("ZIP64 releases are not supported"); + } + + const local = header(30); + local.writeUInt32LE(0x04034b50, 0); + local.writeUInt16LE(20, 4); + local.writeUInt16LE(0x0800, 6); + local.writeUInt16LE(8, 8); + local.writeUInt16LE(0, 10); + local.writeUInt16LE(0x0021, 12); + local.writeUInt32LE(checksum, 14); + local.writeUInt32LE(compressed.byteLength, 18); + local.writeUInt32LE(source.byteLength, 22); + local.writeUInt16LE(name.byteLength, 26); + local.writeUInt16LE(0, 28); + localParts.push(local, name, compressed); + + const central = header(46); + central.writeUInt32LE(0x02014b50, 0); + central.writeUInt16LE(0x0314, 4); + central.writeUInt16LE(20, 6); + central.writeUInt16LE(0x0800, 8); + central.writeUInt16LE(8, 10); + central.writeUInt16LE(0, 12); + central.writeUInt16LE(0x0021, 14); + central.writeUInt32LE(checksum, 16); + central.writeUInt32LE(compressed.byteLength, 20); + central.writeUInt32LE(source.byteLength, 24); + central.writeUInt16LE(name.byteLength, 28); + central.writeUInt16LE(0, 30); + central.writeUInt16LE(0, 32); + central.writeUInt16LE(0, 34); + central.writeUInt16LE(0, 36); + central.writeUInt32LE((0o100644 << 16) >>> 0, 38); + central.writeUInt32LE(offset, 42); + centralParts.push(central, name); + offset += local.byteLength + name.byteLength + compressed.byteLength; +} +const centralOffset = offset; +const centralSize = centralParts.reduce( + (sum, part) => sum + part.byteLength, + 0, +); +if (centralOffset + centralSize > 0xffffffff) { + throw new Error("ZIP64 releases are not supported"); +} +const end = header(22); +end.writeUInt32LE(0x06054b50, 0); +end.writeUInt16LE(0, 4); +end.writeUInt16LE(0, 6); +end.writeUInt16LE(files.length, 8); +end.writeUInt16LE(files.length, 10); +end.writeUInt32LE(centralSize, 12); +end.writeUInt32LE(centralOffset, 16); +end.writeUInt16LE(0, 20); +const archive = Buffer.concat([...localParts, ...centralParts, end]); + +await mkdir(path.dirname(output), { recursive: true }); +const staging = await mkdtemp(path.join(path.dirname(output), ".svg-release-")); +const stagedArchive = path.join(staging, path.basename(output)); +const stagedChecksum = `${stagedArchive}.sha256`; +try { + await writeFile(stagedArchive, archive, { flag: "wx", mode: 0o644 }); + const digest = createHash("sha256").update(archive).digest("hex"); + await writeFile(stagedChecksum, `${digest} ${path.basename(output)}\n`, { + flag: "wx", + mode: 0o644, + }); + if (force) { + await rm(output, { force: true }); + await rm(checksumOutput, { force: true }); + } + await rename(stagedArchive, output); + await rename(stagedChecksum, checksumOutput); + process.stdout.write( + `Created ${path.relative(root, output)} (${archive.byteLength} bytes, ${files.length} files)\nSHA-256 ${digest}\n`, + ); +} finally { + await rm(staging, { recursive: true, force: true }); +} diff --git a/scripts/portal-assembly-smoke.mjs b/scripts/portal-assembly-smoke.mjs new file mode 100644 index 0000000..9397733 --- /dev/null +++ b/scripts/portal-assembly-smoke.mjs @@ -0,0 +1,158 @@ +#!/usr/bin/env node +import { createHash } from "node:crypto"; +import { spawn } from "node:child_process"; +import { + access, + mkdtemp, + readFile, + rm, + stat, + writeFile, +} from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const workspace = path.dirname(root); +const portal = path.resolve( + process.env.TOOLBOX_PORTAL_DIR ?? path.join(workspace, "toolbox-portal"), +); +const appPackage = JSON.parse( + await readFile(path.join(root, "package.json"), "utf8"), +); +const portalPackage = JSON.parse( + await readFile(path.join(portal, "package.json"), "utf8"), +); +const artifact = path.join( + root, + "release", + `svg-tools-${appPackage.version}.zip`, +); + +async function run(command, arguments_, cwd) { + await new Promise((resolve, reject) => { + const child = spawn(command, arguments_, { + cwd, + stdio: "inherit", + env: { ...process.env, CI: "1" }, + }); + child.once("error", reject); + child.once("exit", (code, signal) => { + if (code === 0) resolve(); + else + reject( + new Error( + `${command} exited with ${code ?? `signal ${signal ?? "unknown"}`}`, + ), + ); + }); + }); +} + +const details = await stat(artifact).catch(() => null); +if (!details?.isFile()) { + throw new Error(`Build the app release first: ${artifact}`); +} +const digest = createHash("sha256") + .update(await readFile(artifact)) + .digest("hex"); +const sidecar = await readFile(`${artifact}.sha256`, "utf8"); +if (!sidecar.startsWith(`${digest} ${path.basename(artifact)}`)) { + throw new Error("The app release checksum sidecar does not match the ZIP"); +} + +await access(path.join(portal, "scripts", "assemble.mjs")); +await run( + process.platform === "win32" ? "npm.cmd" : "npm", + ["run", "build"], + portal, +); + +const temporary = await mkdtemp( + path.join(os.tmpdir(), "svg-tools-portal-smoke-"), +); +try { + const example = JSON.parse( + await readFile( + path.join(portal, "release", "toolbox.lock.example.json"), + "utf8", + ), + ); + const lock = { + ...example, + releaseVersion: "0.10.0-smoke.0", + portalVersion: portalPackage.version, + apps: [ + { + id: "de.add-ideas.svg-tools", + version: appPackage.version, + artifact: pathToFileURL(artifact).href, + sha256: digest, + target: "svg", + }, + ], + }; + const lockFile = path.join(temporary, "toolbox.lock.json"); + const output = path.join(temporary, "assembled", "toolbox"); + const archive = path.join(temporary, "assembled", "toolbox.zip"); + await writeFile(lockFile, `${JSON.stringify(lock, null, 2)}\n`, { + flag: "wx", + }); + await run( + process.execPath, + [ + path.join(portal, "scripts", "assemble.mjs"), + "--lock", + lockFile, + "--portal-dist", + path.join(portal, "dist"), + "--output", + output, + "--archive", + archive, + ], + portal, + ); + + const catalogue = JSON.parse( + await readFile(path.join(output, "toolbox.catalog.json"), "utf8"), + ); + const manifest = JSON.parse( + await readFile( + path.join(output, "apps", "svg", "toolbox-app.json"), + "utf8", + ), + ); + const appIndex = await readFile( + path.join(output, "apps", "svg", "index.html"), + "utf8", + ); + if ( + catalogue.apps.length !== 1 || + catalogue.apps[0]?.manifest !== "./apps/svg/toolbox-app.json" || + manifest.id !== "de.add-ideas.svg-tools" || + manifest.version !== appPackage.version || + !manifest.assets?.includes("./canvas-frame-controller.js") || + /\b(?:src|href)=["']\//iu.test(appIndex) + ) { + throw new Error( + "Assembled SVG Tools identity or relocatable assets changed", + ); + } + for (const relative of [ + "apps/svg/favicon.svg", + "apps/svg/canvas-frame-controller.js", + "apps/svg/LICENSE", + "apps/svg/SOURCE.md", + "toolbox.release.json", + ]) { + const file = await stat(path.join(output, relative)).catch(() => null); + if (!file?.isFile()) throw new Error(`Assembly is missing ${relative}`); + } + process.stdout.write( + `Portal assembly smoke passed for SVG Tools ${appPackage.version} (${digest})\n`, + ); +} finally { + await rm(temporary, { recursive: true, force: true }); +} diff --git a/scripts/prepare-release-files.mjs b/scripts/prepare-release-files.mjs new file mode 100644 index 0000000..6261818 --- /dev/null +++ b/scripts/prepare-release-files.mjs @@ -0,0 +1,95 @@ +import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const publicDirectory = path.join(root, "public"); +const required = [ + "LICENSE", + "README.md", + "CHANGELOG.md", + "SOURCE.md", + "THIRD_PARTY_NOTICES.md", +]; + +await mkdir(publicDirectory, { recursive: true }); +for (const name of required) { + const source = path.join(root, name); + await readFile(source); + await cp(source, path.join(publicDirectory, name)); +} + +const publicLicenses = path.join(publicDirectory, "LICENSES"); +await rm(publicLicenses, { recursive: true, force: true }); +await cp(path.join(root, "LICENSES"), publicLicenses, { recursive: true }); +const publicDocs = path.join(publicDirectory, "docs"); +await rm(publicDocs, { recursive: true, force: true }); +await cp(path.join(root, "docs"), publicDocs, { recursive: true }); + +const lock = JSON.parse( + await readFile(path.join(root, "package-lock.json"), "utf8"), +); +const runtimeLicenseSections = []; +for (const [location, locked] of Object.entries(lock.packages ?? {}).sort( + ([left], [right]) => (left < right ? -1 : left > right ? 1 : 0), +)) { + if (!location.includes("node_modules/") || locked.dev === true) continue; + const packageDirectory = path.join(root, location); + const details = JSON.parse( + await readFile(path.join(packageDirectory, "package.json"), "utf8"), + ); + const candidates = (await readdir(packageDirectory)) + .filter((name) => /^(?:licen[cs]e|copying|notice)(?:\.|$)/iu.test(name)) + .sort(); + const texts = []; + for (const candidate of candidates) { + try { + texts.push( + `--- ${candidate} ---\n${await readFile(path.join(packageDirectory, candidate), "utf8")}`, + ); + } catch { + // Ignore directories or non-text aliases; another matching file may exist. + } + } + runtimeLicenseSections.push( + [ + "=".repeat(78), + `${details.name}@${details.version}`, + `Declared licence: ${details.license ?? locked.license ?? "See upstream package"}`, + `Installed from: ${location}`, + "=".repeat(78), + texts.length + ? texts.join("\n\n") + : "No package-local licence file was present; see THIRD_PARTY_NOTICES.md and the upstream source.", + ].join("\n"), + ); +} +await writeFile( + path.join(publicLicenses, "npm-runtime-licenses.txt"), + `${runtimeLicenseSections.join("\n\n")}\n`, +); + +const packageJson = JSON.parse( + await readFile(path.join(root, "package.json"), "utf8"), +); +const rows = [ + "# Runtime dependency licences", + "", + "Generated from the exact lock used for this build.", + "", + "| Package | Version | Licence |", + "| --- | --- | --- |", +]; +for (const name of Object.keys(packageJson.dependencies).sort()) { + const manifestPath = path.join(root, "node_modules", name, "package.json"); + const details = JSON.parse(await readFile(manifestPath, "utf8")); + rows.push( + `| \`${details.name}\` | ${details.version} | ${details.license ?? "See packaged notices"} |`, + ); +} +await writeFile( + path.join(publicDirectory, "THIRD_PARTY_LICENSES.txt"), + `${rows.join("\n")}\n`, +); + +console.log("Prepared static release notices"); diff --git a/scripts/serve-test.mjs b/scripts/serve-test.mjs new file mode 100644 index 0000000..0aa4fcf --- /dev/null +++ b/scripts/serve-test.mjs @@ -0,0 +1,97 @@ +import { createServer } from "node:http"; +import { readFile, stat } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", + "dist", +); +const nestedPrefix = "/deep/nested/svg/"; +const mediaTypes = new Map([ + [".css", "text/css; charset=utf-8"], + [".html", "text/html; charset=utf-8"], + [".js", "text/javascript; charset=utf-8"], + [".mjs", "text/javascript; charset=utf-8"], + [".json", "application/json; charset=utf-8"], + [".svg", "image/svg+xml"], + [".wasm", "application/wasm"], +]); +const catalogue = { + schemaVersion: 1, + id: "de.add-ideas.svg-tools.browser-test", + name: "SVG Tools browser-test Toolbox", + home: "./", + theme: { mode: "system", brand: "add·ideas" }, + apps: [{ manifest: "./toolbox-app.json", enabled: true }], +}; +const headers = { + "Content-Security-Policy": + "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; frame-src 'self' blob:; manifest-src 'self'", + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Resource-Policy": "same-origin", + "X-Content-Type-Options": "nosniff", +}; + +function safeFile(requestPath) { + const decoded = decodeURIComponent(requestPath); + const relative = decoded.startsWith(nestedPrefix) + ? decoded.slice(nestedPrefix.length) + : decoded.replace(/^\/+/, ""); + const normalized = path.posix.normalize(relative || "index.html"); + if ( + normalized === ".." || + normalized.startsWith("../") || + path.isAbsolute(normalized) + ) { + return null; + } + return path.join(root, normalized); +} + +const server = createServer(async (request, response) => { + try { + const url = new URL(request.url ?? "/", "http://127.0.0.1"); + if (url.pathname === "/toolbox.catalog.json") { + response.writeHead(200, { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-cache", + ...headers, + }); + response.end(JSON.stringify(catalogue)); + return; + } + let file = safeFile(url.pathname); + if (!file) { + response.writeHead(400).end("Bad request"); + return; + } + if ((await stat(file).catch(() => null))?.isDirectory()) { + file = path.join(file, "index.html"); + } + const content = await readFile(file); + const isCanvasController = + path.basename(file) === "canvas-frame-controller.js"; + response.writeHead(200, { + "Content-Type": + mediaTypes.get(path.extname(file)) ?? "application/octet-stream", + "Cache-Control": "no-cache", + ...headers, + ...(isCanvasController + ? { + "Access-Control-Allow-Origin": "*", + "Cross-Origin-Resource-Policy": "cross-origin", + } + : {}), + }); + response.end(content); + } catch { + response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Not found"); + } +}); + +server.listen(4173, "127.0.0.1", () => { + console.log("SVG Tools test server listening on http://127.0.0.1:4173"); +}); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..5290357 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,42 @@ +import { lazy, Suspense, useState } from "react"; +import { AppShell } from "@add-ideas/toolbox-shell-react"; +import "@add-ideas/toolbox-shell-react/styles.css"; +import "./styles.css"; +import { AppErrorBoundary } from "./components/AppErrorBoundary"; +import { HelpDialog } from "./components/HelpDialog"; +import { manifest } from "./toolbox/manifest"; + +const Workbench = lazy(async () => { + const module = await import("./components/Workbench"); + return { default: module.Workbench }; +}); + +export function App() { + const [helpOpen, setHelpOpen] = useState(false); + return ( + + setHelpOpen(true) }} + onContextError={(error) => { + console.warn( + "Toolbox context unavailable; continuing standalone.", + error, + ); + }} + > + + Preparing the local SVG workbench… +

+ } + > + +
+
+ setHelpOpen(false)} /> +
+ ); +} diff --git a/src/accessibility/audit.ts b/src/accessibility/audit.ts new file mode 100644 index 0000000..c8b8945 --- /dev/null +++ b/src/accessibility/audit.ts @@ -0,0 +1,130 @@ +import type { + SemanticSvgDocument, + SourcePatch, + SourceRange, +} from "../document/document.types"; +import { buildReferenceIndex } from "../structure/reference-index"; + +export interface AccessibilityFinding { + severity: "info" | "warning" | "error"; + rule: string; + nodeKey: string; + evidence: string; + limitation: string; + suggestedFix: string; + automaticFix: "add-title" | "add-description" | "mark-decorative" | null; + sourceRange?: SourceRange; +} + +export function auditAccessibility( + semantic: SemanticSvgDocument, +): AccessibilityFinding[] { + const root = semantic.nodes.get(semantic.rootKey)!; + const children = root.childKeys.map((key) => semantic.nodes.get(key)!); + const title = children.find((node) => node.localName === "title"); + const description = children.find((node) => node.localName === "desc"); + const role = root.attributes.role; + const labelledBy = root.attributes["aria-labelledby"]; + const label = root.attributes["aria-label"]; + const hidden = root.attributes["aria-hidden"] === "true"; + const findings: AccessibilityFinding[] = []; + if (!hidden && !title && !label && !labelledBy) { + findings.push({ + severity: "warning", + rule: "svg-accessible-name", + nodeKey: root.key, + evidence: "The root has no title, aria-label or aria-labelledby.", + limitation: + "The embedding page can provide an accessible name outside this file.", + suggestedFix: + "Add a concise root title or explicitly mark the image decorative.", + automaticFix: "add-title", + sourceRange: root.sourceRange.openTag, + }); + } + if (!hidden && !description) { + findings.push({ + severity: "info", + rule: "svg-description", + nodeKey: root.key, + evidence: "No root description element is present.", + limitation: + "Not every simple or decorative SVG needs a long description.", + suggestedFix: + "Add a description when the image communicates non-trivial content.", + automaticFix: "add-description", + sourceRange: root.sourceRange.openTag, + }); + } + if ( + !hidden && + role && + !["img", "graphics-document", "presentation", "none"].includes(role) + ) { + findings.push({ + severity: "info", + rule: "svg-role-review", + nodeKey: root.key, + evidence: `Root role is “${role}”.`, + limitation: + "Role validity depends on the embedding and interaction model.", + suggestedFix: "Review the role against the intended embedding context.", + automaticFix: null, + sourceRange: root.attributeRanges.role?.valueRange, + }); + } + const references = buildReferenceIndex(semantic); + for (const edge of references.edges.filter( + (candidate) => + candidate.attribute.startsWith("aria-") && + candidate.status !== "resolved", + )) { + const node = semantic.nodes.get(edge.sourceKey)!; + findings.push({ + severity: "error", + rule: "aria-reference", + nodeKey: edge.sourceKey, + evidence: `${edge.attribute} target “${edge.targetId}” is ${edge.status}.`, + limitation: "Only local SVG ID references are evaluated.", + suggestedFix: "Repair the referenced ID or remove the broken token.", + automaticFix: null, + sourceRange: node.attributeRanges[edge.attribute]?.valueRange, + }); + } + for (const node of semantic.nodes.values()) { + if (node.localName === "path" && /text/i.test(node.id ?? "")) { + findings.push({ + severity: "info", + rule: "text-as-path-review", + nodeKey: node.key, + evidence: + "A path ID suggests that visible text may have been outlined.", + limitation: "The audit cannot infer author intent from path geometry.", + suggestedFix: + "Prefer a text element when editable, selectable text is required.", + automaticFix: null, + sourceRange: node.sourceRange.openTag, + }); + } + } + return findings; +} + +export function accessibilityFixPatch( + semantic: SemanticSvgDocument, + fix: "add-title" | "add-description", + text: string, +): SourcePatch { + const root = semantic.nodes.get(semantic.rootKey)!; + const escaped = text + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); + const tag = fix === "add-title" ? "title" : "desc"; + return { + from: root.sourceRange.openTag.to, + to: root.sourceRange.openTag.to, + insert: `${semantic.preferences.newline}${semantic.preferences.indentation}<${tag}>${escaped}`, + label: fix === "add-title" ? "Add accessible title" : "Add description", + }; +} diff --git a/src/animation/animation.types.ts b/src/animation/animation.types.ts new file mode 100644 index 0000000..daa19e8 --- /dev/null +++ b/src/animation/animation.types.ts @@ -0,0 +1,23 @@ +export interface AnimationKeyframe { + offset: number; + value: string; + easing?: string; +} + +export interface AnimationDefinition { + id: string; + name: string; + targetNodeKey: string; + property: string; + kind: "attribute" | "style" | "transform"; + enabled: boolean; + keyframes: AnimationKeyframe[]; + timing: { + durationMs: number; + delayMs: number; + iterations: number | "infinite"; + direction: "normal" | "reverse" | "alternate" | "alternate-reverse"; + fillMode: "none" | "forwards" | "backwards" | "both"; + easing: string; + }; +} diff --git a/src/animation/preview.ts b/src/animation/preview.ts new file mode 100644 index 0000000..433e723 --- /dev/null +++ b/src/animation/preview.ts @@ -0,0 +1,40 @@ +import type { AnimationDefinition } from "./animation.types"; +import { buildAnimationCss } from "./validation"; + +function cssString(value: string): string { + return Array.from(value, (character) => { + if (character === "\\") return "\\\\"; + if (character === '"') return '\\"'; + const code = character.codePointAt(0)!; + if ( + code <= 0x1f || + code === 0x7f || + character === "<" || + character === ">" || + character === "&" + ) { + return `\\${code.toString(16)} `; + } + return character; + }).join(""); +} + +export function animationStyle( + definitions: readonly AnimationDefinition[], +): string { + return buildAnimationCss(definitions, { + keyframeNamePrefix: "svg-tools-animation", + selectorFor: (definition) => + `[data-svg-tools-node="${cssString(definition.targetNodeKey)}"]`, + }); +} + +export function withAnimationPreview( + sanitizedProjection: string, + definitions: readonly AnimationDefinition[], +): string { + const css = animationStyle(definitions); + if (!css) return sanitizedProjection; + const style = ``; + return sanitizedProjection.replace(/]*)>/iu, `${style}`); +} diff --git a/src/animation/validation.ts b/src/animation/validation.ts new file mode 100644 index 0000000..e02e8bf --- /dev/null +++ b/src/animation/validation.ts @@ -0,0 +1,342 @@ +import { parse as parseCss, walk as walkCss } from "css-tree"; +import { defaultSvgLimits } from "../app/limits"; +import type { AnimationDefinition, AnimationKeyframe } from "./animation.types"; + +const MAXIMUM_KEYFRAMES = 10_000; +const MAXIMUM_LABEL_LENGTH = 4_096; +const MAXIMUM_VALUE_LENGTH = 65_536; +const MAXIMUM_EASING_LENGTH = 256; +const MAXIMUM_DURATION_MS = 604_800_000; +const MAXIMUM_ITERATIONS = 1_000_000; + +const SAFE_PROPERTIES = new Set([ + "color", + "fill", + "filter", + "opacity", + "stroke", + "stroke-width", + "transform", + "transform-origin", + "visibility", +]); + +const DIRECTIONS = new Set([ + "normal", + "reverse", + "alternate", + "alternate-reverse", +]); +const FILL_MODES = new Set(["none", "forwards", "backwards", "both"]); +const KINDS = new Set(["attribute", "style", "transform"]); +const EASING_KEYWORDS = new Set([ + "linear", + "ease", + "ease-in", + "ease-out", + "ease-in-out", + "step-start", + "step-end", +]); +const RESOURCE_FUNCTIONS = new Set([ + "cross-fade", + "element", + "image", + "image-set", + "paint", + "src", + "-webkit-image-set", +]); +const CSS_NUMBER = "[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?"; +const CUBIC_BEZIER = new RegExp( + `^cubic-bezier\\(\\s*(${CSS_NUMBER})\\s*,\\s*(${CSS_NUMBER})\\s*,\\s*(${CSS_NUMBER})\\s*,\\s*(${CSS_NUMBER})\\s*\\)$`, + "iu", +); +const STEPS = + /^steps\(\s*(\d+)\s*(?:,\s*(jump-start|jump-end|jump-none|jump-both|start|end)\s*)?\)$/iu; + +export class AnimationValidationError extends Error { + readonly path: string; + readonly reason: string; + + constructor(reason: string, path: string) { + super(`${reason} (${path})`); + this.name = "AnimationValidationError"; + this.path = path; + this.reason = reason; + } +} + +function fail(reason: string, path: string): never { + throw new AnimationValidationError(reason, path); +} + +function record(value: unknown, path: string): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + fail("Expected an object", path); + } + return value as Record; +} + +function boundedText( + value: unknown, + path: string, + maximumLength: number, + allowEmpty = false, +): string { + if ( + typeof value !== "string" || + (!allowEmpty && value.length === 0) || + value.length > maximumLength + ) { + fail("Expected a bounded string", path); + } + return value; +} + +function finiteNumber( + value: unknown, + path: string, + minimum: number, + maximum: number, +): number { + if ( + typeof value !== "number" || + !Number.isFinite(value) || + value < minimum || + value > maximum + ) { + fail(`Expected a finite number from ${minimum} to ${maximum}`, path); + } + return value; +} + +function safeEasing(value: unknown, path: string): string { + const easing = boundedText(value, path, MAXIMUM_EASING_LENGTH).trim(); + if (EASING_KEYWORDS.has(easing.toLowerCase())) return easing; + + const bezier = CUBIC_BEZIER.exec(easing); + if (bezier) { + const values = bezier.slice(1).map(Number); + if ( + values.every(Number.isFinite) && + values[0]! >= 0 && + values[0]! <= 1 && + values[2]! >= 0 && + values[2]! <= 1 + ) { + return easing; + } + } + + const steps = STEPS.exec(easing); + if (steps) { + const count = Number(steps[1]); + if ( + Number.isSafeInteger(count) && + count >= 1 && + count <= MAXIMUM_ITERATIONS + ) { + return easing; + } + } + + fail("Unsupported or unsafe animation easing", path); +} + +function safeCssValue(value: unknown, path: string): string { + const css = boundedText(value, path, MAXIMUM_VALUE_LENGTH); + const hasUnsafeControlCharacter = Array.from(css).some((character) => { + const code = character.charCodeAt(0); + return ( + code <= 0x08 || + code === 0x0b || + code === 0x0c || + (code >= 0x0e && code <= 0x1f) || + code === 0x7f + ); + }); + if ( + /[\\<>&;{}@]/u.test(css) || + hasUnsafeControlCharacter || + /\/\*|\*\//u.test(css) + ) { + fail("Animation value contains unsafe CSS syntax", path); + } + + let parseFailed = false; + let unsafeResource = false; + try { + const ast = parseCss(css, { + context: "value", + positions: false, + onParseError: () => { + parseFailed = true; + }, + }); + walkCss(ast, (node) => { + if (node.type === "Url") unsafeResource = true; + if ( + node.type === "Function" && + (node.name.toLowerCase() === "expression" || + RESOURCE_FUNCTIONS.has(node.name.toLowerCase())) + ) { + unsafeResource = true; + } + if (node.type === "Raw") parseFailed = true; + }); + } catch { + parseFailed = true; + } + if (parseFailed) fail("Animation value is not valid CSS", path); + if (unsafeResource) { + fail("Animation values cannot resolve URLs or external resources", path); + } + return css; +} + +function keyframes(value: unknown, path: string): AnimationKeyframe[] { + if ( + !Array.isArray(value) || + value.length < 1 || + value.length > MAXIMUM_KEYFRAMES + ) { + fail("Expected one or more bounded keyframes", path); + } + let previousOffset = -1; + return value.map((entry, index) => { + const framePath = `${path}[${index}]`; + const frame = record(entry, framePath); + const offset = finiteNumber(frame.offset, `${framePath}.offset`, 0, 1); + if (offset < previousOffset) { + fail("Keyframe offsets must be ordered", path); + } + previousOffset = offset; + return { + offset, + value: safeCssValue(frame.value, `${framePath}.value`), + ...(frame.easing === undefined + ? {} + : { easing: safeEasing(frame.easing, `${framePath}.easing`) }), + }; + }); +} + +function definition(value: unknown, path: string): AnimationDefinition { + const item = record(value, path); + const property = boundedText(item.property, `${path}.property`, 64); + if (!SAFE_PROPERTIES.has(property)) { + fail( + `Animation property “${property}” is not application-safe`, + `${path}.property`, + ); + } + const kind = boundedText(item.kind, `${path}.kind`, 32); + if (!KINDS.has(kind)) fail("Unsupported animation kind", `${path}.kind`); + if (typeof item.enabled !== "boolean") { + fail("Expected a boolean", `${path}.enabled`); + } + + const timing = record(item.timing, `${path}.timing`); + const direction = boundedText( + timing.direction, + `${path}.timing.direction`, + 32, + ); + if (!DIRECTIONS.has(direction)) { + fail("Unsupported animation direction", `${path}.timing.direction`); + } + const fillMode = boundedText(timing.fillMode, `${path}.timing.fillMode`, 32); + if (!FILL_MODES.has(fillMode)) { + fail("Unsupported animation fill mode", `${path}.timing.fillMode`); + } + const iterations = + timing.iterations === "infinite" + ? "infinite" + : finiteNumber( + timing.iterations, + `${path}.timing.iterations`, + 0, + MAXIMUM_ITERATIONS, + ); + + return { + id: boundedText(item.id, `${path}.id`, MAXIMUM_LABEL_LENGTH), + name: boundedText(item.name, `${path}.name`, MAXIMUM_LABEL_LENGTH), + targetNodeKey: boundedText( + item.targetNodeKey, + `${path}.targetNodeKey`, + MAXIMUM_LABEL_LENGTH, + ), + property, + kind: kind as AnimationDefinition["kind"], + enabled: item.enabled, + keyframes: keyframes(item.keyframes, `${path}.keyframes`), + timing: { + durationMs: finiteNumber( + timing.durationMs, + `${path}.timing.durationMs`, + 1, + MAXIMUM_DURATION_MS, + ), + delayMs: finiteNumber( + timing.delayMs, + `${path}.timing.delayMs`, + -MAXIMUM_DURATION_MS, + MAXIMUM_DURATION_MS, + ), + iterations, + direction: direction as AnimationDefinition["timing"]["direction"], + fillMode: fillMode as AnimationDefinition["timing"]["fillMode"], + easing: safeEasing(timing.easing, `${path}.timing.easing`), + }, + }; +} + +export function validateAnimationDefinitions( + value: unknown, + path = "$.animations", +): AnimationDefinition[] { + if ( + !Array.isArray(value) || + value.length > defaultSvgLimits.maximumAnimations + ) { + fail("Expected a bounded animation array", path); + } + return value.map((entry, index) => definition(entry, `${path}[${index}]`)); +} + +export interface AnimationCssOptions { + keyframeNamePrefix: string; + selectorFor: (definition: AnimationDefinition, index: number) => string; +} + +export function buildAnimationCss( + definitions: readonly AnimationDefinition[], + options: AnimationCssOptions, +): string { + if (!/^[A-Za-z][A-Za-z0-9_-]*$/u.test(options.keyframeNamePrefix)) { + throw new Error("Animation keyframe prefix is not a safe CSS identifier"); + } + return validateAnimationDefinitions(definitions) + .filter((item) => item.enabled) + .map((item, index) => { + const name = `${options.keyframeNamePrefix}-${index}`; + const frames = item.keyframes + .map((frame) => { + const percent = Math.round(frame.offset * 100_000) / 1_000; + const frameEasing = frame.easing + ? ` animation-timing-function: ${frame.easing};` + : ""; + return `${percent}% { ${item.property}: ${frame.value};${frameEasing} }`; + }) + .join(" "); + const iterations = + item.timing.iterations === "infinite" + ? "infinite" + : String(item.timing.iterations); + const selector = options.selectorFor(item, index); + return `@keyframes ${name} { ${frames} }\n${selector} { animation: ${name} ${item.timing.durationMs}ms ${item.timing.easing} ${item.timing.delayMs}ms ${iterations} ${item.timing.direction} ${item.timing.fillMode}; }`; + }) + .join("\n"); +} diff --git a/src/app/limits.ts b/src/app/limits.ts new file mode 100644 index 0000000..cc28328 --- /dev/null +++ b/src/app/limits.ts @@ -0,0 +1,65 @@ +export interface SvgResourceLimits { + sourceSoftBytes: number; + sourceHardBytes: number; + maximumElements: number; + maximumDepth: number; + maximumAttributes: number; + maximumAttributeLength: number; + maximumTextLength: number; + maximumPathCommandsPerPath: number; + maximumPathCommandsTotal: number; + maximumCssRules: number; + maximumReferences: number; + maximumAnimations: number; + maximumFilterPrimitives: number; + maximumEmbeddedResourceBytes: number; + maximumHistoryEntries: number; + maximumHistoryBytes: number; + maximumOptimizationMs: number; + maximumRasterPixels: number; +} + +export const defaultSvgLimits: Readonly = { + sourceSoftBytes: 2 * 1024 * 1024, + sourceHardBytes: 20 * 1024 * 1024, + maximumElements: 100_000, + maximumDepth: 1_000, + maximumAttributes: 1_000_000, + maximumAttributeLength: 1_000_000, + maximumTextLength: 20 * 1024 * 1024, + maximumPathCommandsPerPath: 200_000, + maximumPathCommandsTotal: 1_000_000, + maximumCssRules: 100_000, + maximumReferences: 200_000, + maximumAnimations: 50_000, + maximumFilterPrimitives: 50_000, + maximumEmbeddedResourceBytes: 100 * 1024 * 1024, + maximumHistoryEntries: 500, + maximumHistoryBytes: 200 * 1024 * 1024, + maximumOptimizationMs: 30_000, + maximumRasterPixels: 100_000_000, +}; + +export const utf8ByteLength = (source: string): number => { + let bytes = 0; + for (let index = 0; index < source.length; index += 1) { + const code = source.charCodeAt(index); + if (code <= 0x7f) { + bytes += 1; + } else if (code <= 0x7ff) { + bytes += 2; + } else if ( + code >= 0xd800 && + code <= 0xdbff && + source.charCodeAt(index + 1) >= 0xdc00 && + source.charCodeAt(index + 1) <= 0xdfff + ) { + bytes += 4; + index += 1; + } else { + // UTF-8 encoders replace any unpaired surrogate with U+FFFD. + bytes += 3; + } + } + return bytes; +}; diff --git a/src/app/sample.ts b/src/app/sample.ts new file mode 100644 index 0000000..4343126 --- /dev/null +++ b/src/app/sample.ts @@ -0,0 +1,18 @@ +export const STARTER_SVG = ` + SVG Tools starter document + A small source-faithful vector editing example. + + + + + + + + + + SVG Tools +`; + +export const EMPTY_SVG = ` + Untitled SVG +`; diff --git a/src/commands/history.ts b/src/commands/history.ts new file mode 100644 index 0000000..f9b2e85 --- /dev/null +++ b/src/commands/history.ts @@ -0,0 +1,155 @@ +import { defaultSvgLimits, utf8ByteLength } from "../app/limits"; +import type { + DocumentTransaction, + SelectionState, + SourcePatch, + SvgDiagnostic, +} from "../document/document.types"; + +export interface HistorySnapshot { + past: readonly DocumentTransaction[]; + future: readonly DocumentTransaction[]; + bytes: number; +} + +const EMPTY_SELECTION: SelectionState = { + nodeKeys: [], + primaryNodeKey: null, +}; + +export function createTransaction(input: { + label: string; + baseRevision: number; + sourceBefore: string; + sourceAfter: string; + patches?: SourcePatch[]; + affectedNodeKeys?: string[]; + selectionBefore?: SelectionState; + selectionAfter?: SelectionState; + diagnostics?: SvgDiagnostic[]; + mergeKey?: string; +}): DocumentTransaction { + return { + id: globalThis.crypto?.randomUUID?.() ?? `transaction-${Date.now()}`, + label: input.label, + baseRevision: input.baseRevision, + sourceBefore: input.sourceBefore, + sourceAfter: input.sourceAfter, + sourcePatches: input.patches ?? [], + affectedNodeKeys: input.affectedNodeKeys ?? [], + selectionBefore: input.selectionBefore ?? EMPTY_SELECTION, + selectionAfter: input.selectionAfter ?? EMPTY_SELECTION, + diagnostics: input.diagnostics ?? [], + ...(input.mergeKey ? { mergeKey: input.mergeKey } : {}), + timestamp: Date.now(), + }; +} + +function transactionBytes(transaction: DocumentTransaction): number { + return ( + utf8ByteLength(transaction.sourceBefore) + + utf8ByteLength(transaction.sourceAfter) + ); +} + +export class CommandHistory { + readonly #maximumEntries: number; + readonly #maximumBytes: number; + #past: DocumentTransaction[] = []; + #future: DocumentTransaction[] = []; + #bytes = 0; + + constructor( + maximumEntries = defaultSvgLimits.maximumHistoryEntries, + maximumBytes = defaultSvgLimits.maximumHistoryBytes, + ) { + this.#maximumEntries = maximumEntries; + this.#maximumBytes = maximumBytes; + } + + get snapshot(): HistorySnapshot { + return { + past: [...this.#past], + future: [...this.#future], + bytes: this.#bytes, + }; + } + + get canUndo(): boolean { + return this.#past.length > 0; + } + + get canRedo(): boolean { + return this.#future.length > 0; + } + + commit(transaction: DocumentTransaction): void { + const previous = this.#past.at(-1); + const merge = + previous !== undefined && + Boolean(transaction.mergeKey) && + transaction.mergeKey === previous?.mergeKey && + transaction.timestamp - previous.timestamp < 1_000 && + previous.sourceAfter === transaction.sourceBefore; + if (merge && previous) { + this.#bytes -= transactionBytes(previous); + this.#past[this.#past.length - 1] = { + ...transaction, + id: previous.id, + sourceBefore: previous.sourceBefore, + selectionBefore: previous.selectionBefore, + sourcePatches: [ + ...previous.sourcePatches, + ...transaction.sourcePatches, + ], + }; + } else { + this.#past.push(transaction); + } + this.#bytes += transactionBytes(this.#past.at(-1)!); + this.#future = []; + this.#trim(); + } + + undo(currentSource: string): DocumentTransaction | null { + const transaction = this.#past.at(-1); + if (!transaction) return null; + if (transaction.sourceAfter !== currentSource) { + throw new Error("Undo rejected because the source revision is stale"); + } + this.#past.pop(); + this.#future.push(transaction); + this.#bytes -= transactionBytes(transaction); + return transaction; + } + + redo(currentSource: string): DocumentTransaction | null { + const transaction = this.#future.at(-1); + if (!transaction) return null; + if (transaction.sourceBefore !== currentSource) { + throw new Error("Redo rejected because the source revision is stale"); + } + this.#future.pop(); + this.#past.push(transaction); + this.#bytes += transactionBytes(transaction); + this.#trim(); + return transaction; + } + + clear(): void { + this.#past = []; + this.#future = []; + this.#bytes = 0; + } + + #trim(): void { + while ( + this.#past.length > this.#maximumEntries || + this.#bytes > this.#maximumBytes + ) { + const removed = this.#past.shift(); + if (!removed) break; + this.#bytes -= transactionBytes(removed); + } + } +} diff --git a/src/components/AppErrorBoundary.tsx b/src/components/AppErrorBoundary.tsx new file mode 100644 index 0000000..01e439c --- /dev/null +++ b/src/components/AppErrorBoundary.tsx @@ -0,0 +1,42 @@ +import { Component, type ErrorInfo, type ReactNode } from "react"; + +interface Props { + children: ReactNode; +} + +interface State { + error: Error | null; +} + +export class AppErrorBoundary extends Component { + state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo): void { + console.error( + "SVG Tools encountered an unrecoverable interface error", + error, + info, + ); + } + + render(): ReactNode { + if (!this.state.error) return this.props.children; + return ( +
+

SVG Tools could not continue

+

{this.state.error.message}

+ +
+ ); + } +} diff --git a/src/components/CanvasPane.tsx b/src/components/CanvasPane.tsx new file mode 100644 index 0000000..656c35c --- /dev/null +++ b/src/components/CanvasPane.tsx @@ -0,0 +1,579 @@ +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type KeyboardEvent, + type PointerEvent as ReactPointerEvent, +} from "react"; +import type { AnimationDefinition } from "../animation/animation.types"; +import { withAnimationPreview } from "../animation/preview"; +import type { SemanticSvgDocument } from "../document/document.types"; +import { + applyToPoint, + invert, + matrixToTransform, + multiply, + type Matrix, + type Point, +} from "../domain/affine"; +import { resolveTransformChain } from "../domain/transform-chain"; +import { + movePathHandle, + parsePathData, + pathHandles, + serializePathData, + type PathHandle, + type PathModel, +} from "../domain/path"; + +export interface CanvasPaneProps { + projection: string; + semantic: SemanticSvgDocument; + selectedKey: string | null; + disabled: boolean; + stale: boolean; + pathEditing: boolean; + showGrid: boolean; + transformPreview: Matrix | null; + animations: readonly AnimationDefinition[]; + animationPreview: boolean; + view: CanvasView; + onSelect: (nodeKey: string) => void; + onCommitPath: (data: string, mergeKey?: string) => void; + onPathError: (message: string) => void; + onShowGridChange: (show: boolean) => void; + onViewChange: (view: CanvasView) => void; +} + +export interface CanvasView { + zoom: number; + pan: { x: number; y: number }; +} + +interface ViewBox { + x: number; + y: number; + width: number; + height: number; +} + +interface SelectionBox { + x: number; + y: number; + width: number; + height: number; +} + +interface CanvasFrameMessage { + channel: string; + type: "ready" | "select" | "selection-box"; + key?: string; + box?: SelectionBox | null; +} + +function viewBoxFor(semantic: SemanticSvgDocument): ViewBox { + const root = semantic.nodes.get(semantic.rootKey)!; + const values = (root.attributes.viewBox ?? "") + .trim() + .split(/[\s,]+/u) + .map(Number); + if ( + values.length === 4 && + values.every(Number.isFinite) && + values[2]! > 0 && + values[3]! > 0 + ) { + return { + x: values[0]!, + y: values[1]!, + width: values[2]!, + height: values[3]!, + }; + } + const width = Number.parseFloat(root.attributes.width ?? "640"); + const height = Number.parseFloat(root.attributes.height ?? "480"); + return { + x: 0, + y: 0, + width: Number.isFinite(width) && width > 0 ? width : 640, + height: Number.isFinite(height) && height > 0 ? height : 480, + }; +} + +function frameHtml( + source: string, + channel: string, + controllerUrl: string, +): string { + const controllerOrigin = new URL(controllerUrl).origin; + const escapedControllerUrl = controllerUrl + .replaceAll("&", "&") + .replaceAll('"', """); + return `${source}`; +} + +export function CanvasPane({ + projection, + semantic, + selectedKey, + disabled, + stale, + pathEditing, + showGrid, + transformPreview, + animations, + animationPreview, + view, + onSelect, + onCommitPath, + onPathError, + onShowGridChange, + onViewChange, +}: CanvasPaneProps) { + const iframeRef = useRef(null); + const [frameChannel] = useState(() => globalThis.crypto.randomUUID()); + const [controllerUrl] = useState( + () => + new URL("./canvas-frame-controller.js", globalThis.location.href).href, + ); + const overlayRef = useRef(null); + const draftRef = useRef(null); + const dragRef = useRef<{ + handle: PathHandle; + base: PathModel; + pointerId: number; + } | null>(null); + const [draft, setDraft] = useState(null); + const [selectionBox, setSelectionBox] = useState(null); + const [frameRevision, setFrameRevision] = useState(0); + const selectedNode = selectedKey + ? semantic.nodes.get(selectedKey) + : undefined; + const transformChain = useMemo( + () => (selectedKey ? resolveTransformChain(semantic, selectedKey) : null), + [selectedKey, semantic], + ); + const displayedMatrix = useMemo( + () => + transformChain && transformPreview + ? multiply(transformChain.matrix, transformPreview) + : (transformChain?.matrix ?? null), + [transformChain, transformPreview], + ); + const displayedInverse = useMemo( + () => (displayedMatrix ? invert(displayedMatrix) : null), + [displayedMatrix], + ); + const sourcePath = + selectedNode?.localName === "path" + ? (selectedNode.attributes.d ?? "") + : null; + const parsedPath = useMemo(() => { + if (sourcePath === null) return null; + try { + return parsePathData(sourcePath); + } catch { + return null; + } + }, [sourcePath]); + const viewBox = useMemo(() => viewBoxFor(semantic), [semantic]); + const root = semantic.nodes.get(semantic.rootKey)!; + const preserveAspectRatio = + root.attributes.preserveAspectRatio ?? "xMidYMid meet"; + const renderedProjection = useMemo( + () => + animationPreview + ? withAnimationPreview(projection, animations) + : projection, + [animationPreview, animations, projection], + ); + const srcDoc = useMemo( + () => frameHtml(renderedProjection, frameChannel, controllerUrl), + [controllerUrl, frameChannel, renderedProjection], + ); + const postFrame = useCallback( + (message: Record) => { + iframeRef.current?.contentWindow?.postMessage( + { ...message, channel: frameChannel }, + "*", + ); + }, + [frameChannel], + ); + + useEffect(() => { + const receive = (event: MessageEvent) => { + if (event.source !== iframeRef.current?.contentWindow) return; + const message = event.data as Partial | null; + if (!message || message.channel !== frameChannel) return; + if (message.type === "ready") { + setFrameRevision((value) => value + 1); + return; + } + if (message.type === "select" && typeof message.key === "string") { + onSelect(message.key); + return; + } + if (message.type === "selection-box" && message.key === selectedKey) { + const box = message.box; + setSelectionBox( + box && [box.x, box.y, box.width, box.height].every(Number.isFinite) + ? box + : null, + ); + } + }; + globalThis.addEventListener("message", receive); + return () => globalThis.removeEventListener("message", receive); + }, [frameChannel, onSelect, selectedKey]); + + useEffect(() => { + const next = pathEditing && parsedPath ? structuredClone(parsedPath) : null; + draftRef.current = next; + dragRef.current = null; + let active = true; + queueMicrotask(() => { + if (active) setDraft(next); + }); + return () => { + active = false; + }; + }, [parsedPath, pathEditing, selectedKey]); + + useEffect(() => { + if (!selectedKey) { + queueMicrotask(() => setSelectionBox(null)); + return; + } + postFrame({ type: "selection", key: selectedKey }); + }, [draft, frameRevision, postFrame, selectedKey, transformPreview]); + + useEffect(() => { + if ( + pathEditing && + transformChain && + (!displayedInverse || transformChain.diagnostics.length) + ) { + onPathError( + transformChain.diagnostics[0] ?? + "Path handles cannot be edited through a non-invertible transform chain", + ); + } + }, [displayedInverse, onPathError, pathEditing, transformChain]); + + const selectionPoints = useMemo(() => { + if (!selectionBox || !displayedMatrix) return null; + return [ + { x: selectionBox.x, y: selectionBox.y }, + { x: selectionBox.x + selectionBox.width, y: selectionBox.y }, + { + x: selectionBox.x + selectionBox.width, + y: selectionBox.y + selectionBox.height, + }, + { x: selectionBox.x, y: selectionBox.y + selectionBox.height }, + ].map((point) => applyToPoint(displayedMatrix, point)); + }, [displayedMatrix, selectionBox]); + + useEffect(() => { + if (!selectedKey) return; + const original = selectedNode?.attributes.transform ?? ""; + const value = transformPreview + ? `${original}${original ? " " : ""}${matrixToTransform(transformPreview)}` + : original || null; + postFrame({ type: "transform", key: selectedKey, value }); + }, [frameRevision, postFrame, selectedKey, selectedNode, transformPreview]); + + const updateFramePath = (model: PathModel) => { + if (!selectedKey) return; + postFrame({ + type: "path", + key: selectedKey, + data: serializePathData(model), + }); + }; + + const toSvgPoint = (event: { + clientX: number; + clientY: number; + }): Point | null => { + const overlay = overlayRef.current; + const matrix = overlay?.getScreenCTM(); + if (!overlay || !matrix) return null; + const point = overlay.createSVGPoint(); + point.x = event.clientX; + point.y = event.clientY; + const transformed = point.matrixTransform(matrix.inverse()); + const rootPoint = { x: transformed.x, y: transformed.y }; + return displayedInverse ? applyToPoint(displayedInverse, rootPoint) : null; + }; + + const beginDrag = ( + event: ReactPointerEvent, + handle: PathHandle, + ) => { + if (disabled || !draftRef.current || !displayedInverse) return; + event.preventDefault(); + event.currentTarget.setPointerCapture(event.pointerId); + dragRef.current = { + handle, + base: structuredClone(draftRef.current), + pointerId: event.pointerId, + }; + }; + + const drag = (event: ReactPointerEvent) => { + const state = dragRef.current; + if (!state || state.pointerId !== event.pointerId) return; + const point = toSvgPoint(event); + if (!point) return; + try { + const next = movePathHandle(state.base, state.handle, point); + draftRef.current = next; + setDraft(next); + updateFramePath(next); + } catch (error) { + onPathError( + error instanceof Error + ? error.message + : "The path handle could not be moved", + ); + } + }; + + const finishDrag = (event: ReactPointerEvent) => { + if (dragRef.current?.pointerId !== event.pointerId) return; + dragRef.current = null; + if (draftRef.current) onCommitPath(serializePathData(draftRef.current)); + }; + + const nudge = ( + event: KeyboardEvent, + handle: PathHandle, + ) => { + if ( + !draftRef.current || + !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key) + ) + return; + event.preventDefault(); + const step = event.altKey ? 0.1 : event.shiftKey ? 10 : 1; + const delta = { + x: + event.key === "ArrowLeft" + ? -step + : event.key === "ArrowRight" + ? step + : 0, + y: event.key === "ArrowUp" ? -step : event.key === "ArrowDown" ? step : 0, + }; + try { + const current = pathHandles(draftRef.current).find( + (candidate) => candidate.id === handle.id, + ); + if (!current) return; + const next = movePathHandle(draftRef.current, current, { + x: current.point.x + delta.x, + y: current.point.y + delta.y, + }); + draftRef.current = next; + setDraft(next); + updateFramePath(next); + onCommitPath(serializePathData(next), "path-keyboard"); + } catch (error) { + onPathError( + error instanceof Error + ? error.message + : "The path handle could not be moved", + ); + } + }; + + const handles = draft ? pathHandles(draft) : []; + const controlLines = draft + ? draft.segments.flatMap((segment, index) => { + if (segment.kind === "C") { + return [ + { + id: `${index}:in`, + from: segment.from, + to: segment.control1, + derived: segment.derivedControl1, + }, + { id: `${index}:out`, from: segment.to, to: segment.control2 }, + ]; + } + if (segment.kind === "Q") { + return [ + { + id: `${index}:q-in`, + from: segment.from, + to: segment.control, + derived: segment.derivedControl, + }, + { id: `${index}:q-out`, from: segment.to, to: segment.control }, + ]; + } + return []; + }) + : []; + const projectPoint = (point: Point): Point => + displayedMatrix ? applyToPoint(displayedMatrix, point) : point; + const setZoom = (zoom: number) => onViewChange({ ...view, zoom }); + const panBy = (x: number, y: number) => + onViewChange({ ...view, pan: { x: view.pan.x + x, y: view.pan.y + y } }); + + return ( +
+
+
+

Sanitized projection

+

Canvas

+
+
+ + + + + + + + +
+
+ {stale ? ( +
+ Showing the last valid canvas revision. +
+ ) : null} +
+
+