From 0c1fc94b580af3977c2187bfcab3e87c042222cd Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 31 Aug 2026 21:21:35 +0200 Subject: [PATCH] feat: release Colour Tools 0.1.0 --- .gitignore | 9 + .npmrc | 1 + CHANGELOG.md | 30 + CONTRIBUTING.md | 52 + LICENSE | 674 +++ LICENSES/README.md | 10 + LICENSES/colorjs.io-MIT.txt | 21 + README.md | 128 + SECURITY.md | 50 + SOURCE.md | 31 + THIRD_PARTY_NOTICES.md | 25 + docs/ACCESSIBILITY.md | 19 + docs/ARCHITECTURE.md | 69 + docs/COLOUR-MATH.md | 94 + docs/PRIVACY-SECURITY.md | 60 + eslint.config.mjs | 40 + index.html | 19 + package-lock.json | 4024 +++++++++++++++++ package.json | 72 + playwright.config.ts | 24 + public/CHANGELOG.md | 30 + public/CONTRIBUTING.md | 52 + public/LICENSE | 674 +++ public/LICENSES/README.md | 10 + public/LICENSES/colorjs.io-MIT.txt | 21 + public/LICENSES/npm-runtime-licenses.txt | 514 +++ public/README.md | 128 + public/SECURITY.md | 50 + public/SOURCE.md | 31 + public/THIRD_PARTY_NOTICES.md | 25 + public/docs/ACCESSIBILITY.md | 19 + public/docs/ARCHITECTURE.md | 69 + public/docs/COLOUR-MATH.md | 94 + public/docs/PRIVACY-SECURITY.md | 60 + public/favicon.svg | 19 + public/manifest.webmanifest | 18 + public/sw.js | 79 + public/toolbox-app.json | 51 + scripts/generate-toolbox-manifest.mjs | 63 + scripts/package-release.mjs | 179 + scripts/prepare-release-files.mjs | 72 + scripts/serve-test.mjs | 80 + src/App.tsx | 42 + src/colour/accessibility.ts | 209 + src/colour/colorjs.d.ts | 4 + src/colour/compare.ts | 115 + src/colour/composite.ts | 244 + src/colour/format.ts | 268 ++ src/colour/gamut.ts | 65 + src/colour/index.ts | 48 + src/colour/internal.ts | 113 + src/colour/interpolate.ts | 170 + src/colour/palette.ts | 332 ++ src/colour/parse.ts | 320 ++ src/colour/types.ts | 249 + src/components/AppErrorBoundary.tsx | 35 + src/components/ColourField.tsx | 53 + src/components/CompositeLab.tsx | 401 ++ src/components/ContrastLab.tsx | 406 ++ src/components/ConverterLab.tsx | 327 ++ src/components/CopyButton.tsx | 51 + src/components/HelpDialog.tsx | 104 + src/components/ImagePickerLab.tsx | 669 +++ src/components/PaletteLab.tsx | 418 ++ src/components/PickerLab.tsx | 387 ++ src/components/StepsLab.tsx | 375 ++ src/components/Swatch.tsx | 30 + src/components/Workbench.tsx | 156 + src/hooks/useSavedPalette.ts | 97 + src/main.tsx | 20 + src/palette/bounds.ts | 170 + src/palette/decode.ts | 91 + src/palette/extract.ts | 224 + src/palette/format.ts | 21 + src/palette/index.ts | 23 + src/palette/oklab.ts | 64 + src/palette/sample.ts | 114 + src/palette/types.ts | 56 + src/palette/worker-client.ts | 102 + src/styles.css | 1760 +++++++ src/test/setup.ts | 25 + src/toolbox/manifest.source.json | 51 + src/toolbox/manifest.ts | 4 + src/version.ts | 1 + src/vite-env.d.ts | 1 + src/workers/palette.worker.ts | 31 + tests/browser/colour-tools.spec.ts | 201 + tests/colour/compare-palette.test.ts | 111 + tests/colour/composite.test.ts | 130 + .../colour/interpolate-accessibility.test.ts | 142 + tests/colour/parse-format.test.ts | 106 + tests/components/workbench.test.tsx | 70 + tests/palette/bounds.test.ts | 87 + tests/palette/decode.test.ts | 66 + tests/palette/extract.test.ts | 88 + tests/palette/sample.test.ts | 95 + tsconfig.app.json | 27 + tsconfig.json | 7 + tsconfig.node.json | 24 + vite.config.ts | 16 + 100 files changed, 17406 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 LICENSES/README.md create mode 100644 LICENSES/colorjs.io-MIT.txt create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 SOURCE.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 docs/ACCESSIBILITY.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/COLOUR-MATH.md create mode 100644 docs/PRIVACY-SECURITY.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/CHANGELOG.md create mode 100644 public/CONTRIBUTING.md create mode 100644 public/LICENSE create mode 100644 public/LICENSES/README.md create mode 100644 public/LICENSES/colorjs.io-MIT.txt create mode 100644 public/LICENSES/npm-runtime-licenses.txt create mode 100644 public/README.md create mode 100644 public/SECURITY.md create mode 100644 public/SOURCE.md create mode 100644 public/THIRD_PARTY_NOTICES.md create mode 100644 public/docs/ACCESSIBILITY.md create mode 100644 public/docs/ARCHITECTURE.md create mode 100644 public/docs/COLOUR-MATH.md create mode 100644 public/docs/PRIVACY-SECURITY.md create mode 100644 public/favicon.svg create mode 100644 public/manifest.webmanifest create mode 100644 public/sw.js 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/prepare-release-files.mjs create mode 100644 scripts/serve-test.mjs create mode 100644 src/App.tsx create mode 100644 src/colour/accessibility.ts create mode 100644 src/colour/colorjs.d.ts create mode 100644 src/colour/compare.ts create mode 100644 src/colour/composite.ts create mode 100644 src/colour/format.ts create mode 100644 src/colour/gamut.ts create mode 100644 src/colour/index.ts create mode 100644 src/colour/internal.ts create mode 100644 src/colour/interpolate.ts create mode 100644 src/colour/palette.ts create mode 100644 src/colour/parse.ts create mode 100644 src/colour/types.ts create mode 100644 src/components/AppErrorBoundary.tsx create mode 100644 src/components/ColourField.tsx create mode 100644 src/components/CompositeLab.tsx create mode 100644 src/components/ContrastLab.tsx create mode 100644 src/components/ConverterLab.tsx create mode 100644 src/components/CopyButton.tsx create mode 100644 src/components/HelpDialog.tsx create mode 100644 src/components/ImagePickerLab.tsx create mode 100644 src/components/PaletteLab.tsx create mode 100644 src/components/PickerLab.tsx create mode 100644 src/components/StepsLab.tsx create mode 100644 src/components/Swatch.tsx create mode 100644 src/components/Workbench.tsx create mode 100644 src/hooks/useSavedPalette.ts create mode 100644 src/main.tsx create mode 100644 src/palette/bounds.ts create mode 100644 src/palette/decode.ts create mode 100644 src/palette/extract.ts create mode 100644 src/palette/format.ts create mode 100644 src/palette/index.ts create mode 100644 src/palette/oklab.ts create mode 100644 src/palette/sample.ts create mode 100644 src/palette/types.ts create mode 100644 src/palette/worker-client.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 src/vite-env.d.ts create mode 100644 src/workers/palette.worker.ts create mode 100644 tests/browser/colour-tools.spec.ts create mode 100644 tests/colour/compare-palette.test.ts create mode 100644 tests/colour/composite.test.ts create mode 100644 tests/colour/interpolate-accessibility.test.ts create mode 100644 tests/colour/parse-format.test.ts create mode 100644 tests/components/workbench.test.tsx create mode 100644 tests/palette/bounds.test.ts create mode 100644 tests/palette/decode.test.ts create mode 100644 tests/palette/extract.test.ts create mode 100644 tests/palette/sample.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..177f475 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +release/*.zip +release/*.zip.sha256 +coverage/ +playwright-report/ +test-results/ +*.tsbuildinfo +.DS_Store 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/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c5782f7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes are documented here. + +## Unreleased + +## 0.1.0 - 2026-08-31 + +- Added a local CSS colour parser and converters for web, perceptual, + wide-gamut and developer colour formats, including bounded batch conversion. +- Added arbitrary RGBA layer compositing with Porter–Duff source-over, + per-layer opacity, standard blend modes, reordering, encoded-sRGB and + linear-light processing, and optional matte flattening. +- Added multi-stop colour ramps with positioned stops, nine interpolation + spaces, selectable hue routes, easing, premultiplied/straight alpha control + and CSS/JSON export. +- Added a large pointer and keyboard picker with alpha, exact CSS input, + browser-native colour input, local history and optional system EyeDropper. +- Added bounded local image decoding, coordinate and neighbourhood sampling, + deterministic perceptual palette extraction and Web Worker fallback. +- Added transparency-aware WCAG 2 contrast reporting and nearby passing colour + suggestions, Delta E comparison, gamut inspection/mapping and clearly + labelled colour-vision simulations. +- Added harmony, tint, shade and tone generation with local named palettes, + palette import and six design-token export formats. +- Added the shared Toolbox shell, system/light/dark themes, responsive layouts, + accessible navigation, nested-path deployment, offline application shell and + deterministic static release packaging. +- Added a GPL-3.0-or-later project licence and exact runtime dependency notices, + including Color.js 0.7.1 under MIT. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7b35535 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing + +Thank you for improving Colour Tools. Contributions should preserve its three +core properties: local-first processing, explicit colour semantics and a +relocatable static build. + +## Set up + +Use Node.js 22 or newer and npm 11 or newer. + +```sh +npm ci +npm run check +npm run test:browser +``` + +Run `npm run dev` for the development server. Do not commit `dist/`, `release/`, +local images, browser traces or editor secrets. + +## Expectations + +- Keep calculations pure and serialisable where practical. Add unit tests for + numeric boundaries, invalid input, alpha and out-of-gamut behaviour. +- Never add telemetry, remote image processing, remote fonts or implicit + network lookups. A feature that needs network access requires prior design + discussion and a plainly visible, opt-in boundary. +- Preserve values outside a target gamut until mapping is explicitly requested. + Name the transfer curve or working light for operations where it changes the + result. +- Bound file bytes, decoded dimensions, worker jobs, list sizes and generated + output. Release object URLs, workers and large buffers after use. +- Keep all application URLs relative. Test from the nested path used by the + Playwright server, not only `/`. +- Ensure every pointer interaction has a keyboard route and every colour-only + signal has a textual or structural equivalent. Use semantic controls before + custom roles. +- Document third-party runtime code in `THIRD_PARTY_NOTICES.md`, retain its + exact licence in `LICENSES/`, and confirm compatibility with GPLv3 or later. + +## Changes and review + +Keep a change focused and update `CHANGELOG.md` for user-visible behaviour. +Include the reasoning behind colour-math choices and cite a primary standard +where the choice is normative. Screenshots can help explain UI changes but do +not replace browser tests. + +Before proposing a change, run `npm run check` and `npm run test:browser`. +Tests must not contact external hosts. Security-sensitive reports should follow +[SECURITY.md](SECURITY.md) instead of a public issue. + +Contributions are accepted under the repository's +`GPL-3.0-or-later` licence. 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..042312d --- /dev/null +++ b/LICENSES/README.md @@ -0,0 +1,10 @@ +# Licence inventory + +Colour Tools is `GPL-3.0-or-later`; the full project licence is at `../LICENSE`. + +`THIRD_PARTY_NOTICES.md` identifies shipped runtime packages. Release +preparation collates the exact installed licence files for every locked runtime +package into `npm-runtime-licenses.txt`. + +`colorjs.io-MIT.txt` is retained separately because Color.js supplies the core +standards-aligned colour conversion primitives used by the application. diff --git a/LICENSES/colorjs.io-MIT.txt b/LICENSES/colorjs.io-MIT.txt new file mode 100644 index 0000000..74fc62b --- /dev/null +++ b/LICENSES/colorjs.io-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Lea Verou, Chris Lilley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fbf24c5 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# Colour Tools + +Colour Tools is a standalone, local-first browser workbench for converting, +compositing, interpolating, sampling, comparing and organising colours. It is +part of the +[add·ideas Toolbox](https://git.add-ideas.de/lotobo/toolbox-portal), but every +release also runs independently from a static HTTPS host or local preview. + +No colour, image, palette or usage event is sent to a server. Image decoding, +pixel sampling and palette extraction happen in the browser; the application +has no analytics, accounts, advertisements or remote lookup feature. + +## Workspaces + +- **Convert** parses CSS colours and presents copy-ready hex, RGB, HSL, HSV, + HWB, CMYK, Lab, LCH, OKLab, OKLCH, Display P3, Adobe RGB, ProPhoto RGB, + Rec. 2020 and XYZ representations. Wide-gamut coordinates remain intact + until an explicit display or gamut-mapping step. A batch converter isolates + invalid lines instead of silently dropping them. +- **Composite** calculates Porter–Duff source-over for an arbitrary ordered + stack of translucent colours. Each layer has its own alpha, opacity and + blend mode. The working-light choice makes encoded-sRGB and linear-light + results explicit, and an optional matte produces an opaque RGB result. +- **Steps** builds multi-stop colour ramps in perceptual, RGB or cylindrical + colour spaces. It supports positioned stops, five hue routes, easing, + premultiplied-alpha interpolation by default with a straight-alpha option, + and copy-ready CSS and JSON output. +- **Pick** provides a large keyboard- and pointer-operable HSV picker, alpha + control, exact CSS entry, browser-native colour input, recent picks and the + system EyeDropper API where the browser exposes it. +- **Image** accepts one bounded local PNG, JPEG, WebP, GIF or AVIF. It samples + an exact coordinate or a configurable average/median neighbourhood and + extracts two to twelve perceptual palette colours in a worker when + available. +- **Contrast** flattens transparency against a stated canvas, reports WCAG 2 + contrast thresholds, finds a nearby passing foreground, compares colours by + six Delta E methods, checks colour gamuts and previews four approximate + colour-vision deficiencies. +- **Palette** creates OKLCH harmonies, tints, shades and tones; keeps a named + palette in local browser storage; imports common text/JSON token forms; and + exports CSS, SCSS, JSON, Design Tokens JSON, JavaScript or CSV. + +Adding a generated or sampled colour to the palette is always explicit. The +saved palette is device-local and can be cleared from the Palette workspace or +by clearing site storage. + +## Colour behaviour + +Colour Tools keeps source coordinates outside display gamuts so conversions do +not destructively clip them. Browser previews and hex exports are mapped to +sRGB with an explicit indicator when mapping was needed. Compositing is carried +out in the selected sRGB working light, while interpolation defaults to OKLCH +for perceptual spacing. See [docs/COLOUR-MATH.md](docs/COLOUR-MATH.md) for the +formulas, bounds and terminology. + +Contrast pass/fail indicators follow WCAG 2 ratios: 3:1 for non-text UI and +large text, 4.5:1 for normal AA text and 7:1 for normal AAA text. They are an +engineering aid, not a complete accessibility audit; typography, state, +context and non-colour cues still need human review. + +## Local image handling + +Selecting an image decodes a bounded raster locally with `createImageBitmap` +where available. The image-element fallback uses a temporary in-memory object +URL. Decoded resources and any fallback URL are released when the image is +replaced, removed or the workspace is closed. Palette extraction uses a +deterministic OKLab clustering implementation and bounded sampling rather than +uploading the image or retaining a copy. Metadata outside the decoded raster +is not exposed or stored by the application. + +## Browser support + +Current evergreen Firefox, Chromium and Safari are the deployment target. +JavaScript modules, CSS custom properties, Canvas 2D and Web Workers are used. +The EyeDropper API is progressive enhancement and only appears where a browser +supports it. Clipboard writes, service workers and EyeDropper access may +require HTTPS and browser permission; all core calculations work without them. + +The UI supports system, light and dark themes through the shared Toolbox shell. +Tabs, picker controls and image sampling are keyboard operable, status changes +use live regions, and swatches are accompanied by machine-readable colour +values rather than relying on colour alone. + +## Offline application shell + +Production builds include a relative-scope web app manifest and a +subpath-aware service worker. After one successful load, the worker caches the +application shell and same-origin built assets, uses network-first navigation +with a cached fallback, serves cached static resources where possible and +removes obsolete cache versions. Service-worker support is optional; colour +operations never depend on a network connection. + +## Development + +Requirements: Node.js 22 or newer and npm 11 or newer. + +```sh +npm ci +npm run check +npm run test:browser +npm run dev +``` + +Vite uses `base: './'`, so `dist/` is relocatable to `/` or a nested Toolbox +path. `toolbox-check` validates standalone and contextual operation. + +The main implementation layers are described in +[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Contributions should follow +[CONTRIBUTING.md](CONTRIBUTING.md); please report security concerns according +to [SECURITY.md](SECURITY.md). + +## Release + +```sh +npm run release:artifact +``` + +This command checks the manifest, types, lint, formatting, unit tests, +production build, Toolbox contract and browser workflows, then creates a +deterministic `release/colour-tools-0.1.0.zip` and matching SHA-256 sidecar. +The archive is a ready-to-host static application and contains project and +third-party licence notices. + +## Licence + +Colour Tools is free software under `GPL-3.0-or-later`. See +[LICENSE](LICENSE). Runtime dependencies retain their respective licences; see +[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5e56297 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,50 @@ +# Security policy + +## Supported versions + +Security fixes are made on the current release line. Users should update to the +latest published tag and verify the release ZIP against its `.sha256` sidecar. + +## Reporting a vulnerability + +Please report a suspected vulnerability privately to the repository owner via +the contact channel shown on the +[Gitea profile](https://git.add-ideas.de/lotobo) or a private Gitea message. Do +not include private images, access tokens, unpublished exploit details or other +sensitive material in a public issue. + +Include the affected version, browser and operating system; a minimal local +reproduction; the expected impact; and whether the issue requires a crafted +image, palette or colour string. You should receive acknowledgement within +seven days. Coordinated disclosure is appreciated while a fix and release are +prepared. + +## Security model + +Colour Tools is a static browser application. It has no application backend, +account system, analytics endpoint or image-upload endpoint. All selected image +bytes are decoded and processed locally. Saved palette entries use browser +storage for the current origin. Clipboard, file picker, download and EyeDropper +actions happen only after a user gesture and remain governed by browser +permissions. + +File processing is deliberately bounded by encoded byte size, decoded pixel +count and dimensions. Image palette extraction samples a bounded number of +pixels and can run in a disposable worker. Object URLs and worker jobs are +cancelled or released when replaced. Input is displayed as text or parsed by +typed colour/image APIs; it is not evaluated as script or inserted as trusted +HTML. + +Production hosts should serve the headers in +[docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md), use HTTPS, keep MIME types +correct, disable content sniffing and avoid injecting third-party scripts. A +host can still observe ordinary HTTP metadata for the initial static-file +request; “local-first” describes application data processing, not anonymity +from the static host. + +## Out of scope + +Colour rendering differences between conforming browser/OS colour-management +pipelines are not vulnerabilities by themselves. Likewise, an approximate +colour-vision simulation is an educational preview, not a medical diagnosis or +a complete accessibility guarantee. diff --git a/SOURCE.md b/SOURCE.md new file mode 100644 index 0000000..7b05887 --- /dev/null +++ b/SOURCE.md @@ -0,0 +1,31 @@ +# Corresponding source and provenance + +The corresponding source for Colour Tools 0.1.0 is: + +https://git.add-ideas.de/lotobo/colour-tools/src/tag/v0.1.0 + +Build that tag with Node.js 22 and the exact dependency graph recorded in +`package-lock.json`: + +```sh +npm ci +npm run release:artifact +``` + +The generated JavaScript and CSS bundles are not the preferred form for +modification. No runtime code is loaded from a CDN. The production ZIP contains +this source pointer, the application licence, the dependency licence inventory, +and the applicable third-party notices. + +## Implementation provenance + +- The application is original project code and uses the shared Toolbox SDK + packages at version 0.2.3 for its manifest contract, shell, and build checks. +- Colour conversion and adaptation use Color.js 0.7.1. Its MIT licence is + reproduced in `LICENSES/colorjs.io-MIT.txt`. +- The compositing, interpolation, palette, image-sampling, contrast, comparison, + gamut, and colour-vision utilities in `src/colour` and `src/palette` were + implemented for this project. +- CSS Color 4 and WCAG 2.2 were used as public technical specifications. No + source code or branded assets were copied from the example websites that + informed the feature list. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..2cfadf9 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,25 @@ +# Third-party notices + +Release preparation collates licence files for the exact locked runtime +packages into `LICENSES/npm-runtime-licenses.txt` inside the static artifact. + +| Package | Version | Licence | Role | Source | +| -------------------------------- | ------- | ---------- | ------------------------------------ | ------------------------------------------- | +| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | manifest and context contract | https://git.add-ideas.de/lotobo/toolbox-sdk | +| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | shared shell, theme and actions | https://git.add-ideas.de/lotobo/toolbox-sdk | +| `colorjs.io` | 0.7.1 | MIT | parsing, conversion and colour maths | https://github.com/color-js/color.js | +| `react` | 19.2.8 | MIT | application UI | https://github.com/facebook/react | +| `react-dom` | 19.2.8 | MIT | browser rendering | https://github.com/facebook/react | + +`scheduler` 0.27.0, distributed by the React project under MIT, is the only +transitive runtime package. Development-only packages build and test the +source but are not shipped as runtime modules. + +The exact Color.js notice is retained at +`LICENSES/colorjs.io-MIT.txt`. Apache-2.0 and MIT texts from all installed +runtime packages are reproduced by the deterministic release-preparation step. + +CSS Color, CSS Compositing and Blending, WCAG and the linked web utilities that +informed the product requirements are specification or research references +only. Their source code, artwork and documentation wording are not bundled or +copied into Colour Tools. diff --git a/docs/ACCESSIBILITY.md b/docs/ACCESSIBILITY.md new file mode 100644 index 0000000..89b14a8 --- /dev/null +++ b/docs/ACCESSIBILITY.md @@ -0,0 +1,19 @@ +# Accessibility notes + +Colour Tools treats colour as data as well as appearance. Swatches are paired +with textual values, gamut/contrast status uses text and symbols in addition to +hue, and generated palettes remain usable without visually distinguishing +every swatch. + +Workspace navigation uses tabs with selected state and arrow/Home/End keyboard +navigation. Native inputs and labelled controls are preferred. The large picker +supports arrow-key adjustment and announces its saturation and brightness; +image sampling supports numeric coordinates and keyboard movement in addition +to pointer selection. Calculation and file statuses use live regions without +clearing the last valid result while a user is typing. + +The shared Toolbox shell provides system, light and dark themes. Focus rings, +touch targets, reflow and text contrast should be checked in all themes and at +400% zoom. A WCAG contrast result is not itself proof that the complete +interface conforms: test keyboard order, names/roles/values, error recovery, +high contrast modes, reduced motion and screen-reader output in context. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..dc86eed --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,69 @@ +# Architecture + +Colour Tools is a client-only React application built as relocatable static +files. The UI and calculation engine are intentionally separated so colour +operations can be tested without a browser and UI state can keep showing the +last valid result while an input is incomplete. + +## Layers + +1. `src/colour/` is the serialisable colour domain. It parses and formats + colours, converts gamuts, composites layers, interpolates stops, evaluates + contrast and Delta E, simulates colour-vision deficiencies, and builds or + exports palettes. Its public API is re-exported from `src/colour/index.ts`. +2. `src/palette/` contains browser-independent raster bounds, sampling and + deterministic OKLab clustering. `src/workers/palette.worker.ts` exposes the + expensive extraction path to a dedicated worker. +3. `src/components/` contains the seven workspaces and shared accessible + controls. Workspace selection is represented by a URL fragment, so a view + can be bookmarked without router or server support. +4. `src/hooks/` owns bounded device-local UI persistence. Image bytes are not + stored there. +5. `src/toolbox/` contains the checked manifest identity. The shared Toolbox + shell supplies navigation, contextual actions and theme behaviour without + changing the standalone application domain. + +## Colour values + +The engine's `ColourValue` is a plain object: colour-space identifier, three +numeric coordinates and alpha. It deliberately carries coordinates that may +sit outside a display gamut. Color.js 0.7.1 supplies standards-aligned parsing +and conversion primitives; Colour Tools wraps these in typed, bounded domain +operations and stable UI formats. + +Browser preview is a separate operation. It maps a value to sRGB and reports +whether mapping occurred. This keeps display limitations from mutating the +source colour used by later conversions or comparisons. + +## Raster pipeline + +The image picker validates the selected file before decode, rejects excessive +bytes/dimensions/pixels, then draws the decoded image to a same-origin in-memory +Canvas 2D surface. Sampling reads a bounded neighbourhood. Palette extraction +subsamples deterministically, converts points to OKLab and clusters them with a +fixed initialization and iteration bound. The worker client can fall back to +the same pure function when workers are unavailable. + +Temporary object URLs are revoked and outstanding extraction is aborted when an +image is replaced or removed. No image or extracted palette is transmitted. + +## Build and deployment + +Vite builds with a relative base. `scripts/generate-toolbox-manifest.mjs` +checks package, application and Toolbox versions plus immutable source +identity. `scripts/prepare-release-files.mjs` places project documentation and +the exact installed runtime licence inventory beside the executable app. + +The service worker is relative-scope and deployment-subpath aware. It is +progressive enhancement: the app remains usable when registration is rejected. +The deterministic packager rejects symbolic links, source maps, secret-like +filenames and root-absolute asset references before creating the release ZIP +and SHA-256 sidecar. + +## Testing boundaries + +Unit tests cover parsing, numerical operations, invalid inputs, raster bounds, +sampling and palette determinism. Browser smoke tests run the production build +under a restrictive policy at `/deep/nested/colour/`, exercise all major +workspaces and fail on uncaught errors or external network requests. The +Toolbox contract checker validates the static artifact independently. diff --git a/docs/COLOUR-MATH.md b/docs/COLOUR-MATH.md new file mode 100644 index 0000000..30a57a9 --- /dev/null +++ b/docs/COLOUR-MATH.md @@ -0,0 +1,94 @@ +# Colour mathematics and terminology + +This document records the choices that materially change a result. It is not a +replacement for CSS Color, Compositing and Blending, or WCAG. + +## Parsing and conversion + +The parser accepts CSS colour syntax supported by the installed Color.js +version, including hex and named colours; `rgb()`, `hsl()`, `hwb()`, Lab/LCH +and OKLab/OKLCH functions; and registered `color()` spaces. Conversion keeps +finite coordinates outside a target gamut. Hex and ordinary browser sRGB +previews require mapping; the UI labels when this happened. + +The converter exposes device-oriented and mathematical representations, but a +numeric tuple is meaningful only with its named colour space, white point, +transfer curve and alpha convention. CMYK output is a convenient mathematical +conversion, not a printer ICC separation or proof. + +## Alpha compositing + +For a source colour over a backdrop, with straight alpha `αs` and `αb`, output +alpha is: + +```text +αo = αs + αb × (1 − αs) +``` + +For each premultiplied channel under normal source-over: + +```text +Co = Cs × αs + Cb × αb × (1 − αs) +co = Co / αo when αo > 0 +``` + +Layers are accumulated from the bottom upward even though the editor displays +the top layer first. Layer opacity multiplies the colour's own alpha. Blend +modes are applied before source-over using the model in CSS Compositing and +Blending. + +“Encoded sRGB” applies the operation directly to sRGB-encoded channel values, +matching many quick web calculators. “Linear-light sRGB” first removes the +sRGB transfer curve and is generally the physically meaningful choice for +mixing emitted light. The two results are expected to differ. + +## Interpolation + +A ramp samples its first and last stop and distributes missing positions like a +CSS gradient. Each segment is interpolated in the selected space with the +chosen easing. Cylindrical spaces need an explicit hue route: shorter, longer, +increasing, decreasing or raw. The default premultiplies colour components by +alpha before mixing and unpremultiplies afterwards, preventing hidden channels +inside a transparent endpoint from tinting the ramp. A straight-alpha toggle +is available for intentionally reproducing that model. OKLCH is the UI default +because equal numeric progress tends to look more even than encoded RGB; it +does not guarantee that every intermediate colour lies in a display gamut. + +## Gamut mapping + +An in-gamut check does not modify the colour. Display mapping uses an OKLCH +chroma-reduction method by default; channel clipping is available at the engine +boundary where explicitly selected. The result includes Delta E OK from the +source so callers can quantify the change. A browser preview is always mapped +to sRGB because CSS support and physical displays differ. + +## Contrast and transparency + +Relative luminance and WCAG 2 contrast are evaluated after translucent +foreground/background colours are composited over the stated canvas (white by +default). The ratio is: + +```text +(Llighter + 0.05) / (Ldarker + 0.05) +``` + +The UI reports 3:1, 4.5:1 and 7:1 thresholds and searches for a nearby lighter +or darker passing foreground. A mathematical pass is only one part of an +accessibility review; font size/weight, focus states, disabled controls, +non-colour cues and actual display conditions remain relevant. + +## Perceptual difference and simulations + +Delta E 76, CMC, CIEDE2000, Delta E OK, ITP and Jz answer different historical +or application needs; values from different formulas are not interchangeable. +The colour-vision previews apply bounded matrices in linear sRGB and expose a +severity control. They are approximations for interface review, not a model of +every observer and not medical advice. + +## Palette extraction + +The raster extractor ignores sufficiently transparent pixels, converts bounded +samples to OKLab and clusters them deterministically. Reported coverage is the +share of accepted samples assigned to a cluster, not an exact segmentation of +every source pixel. Small antialiased regions and colour-managed decoding can +therefore influence the result. diff --git a/docs/PRIVACY-SECURITY.md b/docs/PRIVACY-SECURITY.md new file mode 100644 index 0000000..0dffc56 --- /dev/null +++ b/docs/PRIVACY-SECURITY.md @@ -0,0 +1,60 @@ +# Privacy, security and deployment + +## Data flow + +Colour input, saved palette entries, selected image bytes, decoded pixels and +generated exports stay in the browser. The application does not issue API +requests, load remote fonts, submit analytics or resolve remote colour +references. A saved palette belongs to the current browser origin and is +stored until the user clears it or browser storage is removed. Images are +in-memory only. + +The static host still receives ordinary requests for HTML, JavaScript, CSS, +icons and worker files on initial load and revalidation. Host logs, reverse +proxy logs and browser extension behaviour are outside the application's local +processing boundary. + +## Recommended headers + +Serve the static artifact over HTTPS and apply a policy equivalent to: + +```text +Content-Security-Policy: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self' +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Resource-Policy: same-origin +Permissions-Policy: camera=(), microphone=(), geolocation=(), usb=(), payment=() +Referrer-Policy: no-referrer +X-Content-Type-Options: nosniff +``` + +The shared Toolbox shell uses inline style properties for colour previews, so +`style-src 'unsafe-inline'` is currently required. Do not add +`script-src 'unsafe-inline'` or third-party script origins. `blob:` is required +for local image object URLs and may be used for a worker fallback. + +Serve `.js` as `text/javascript`, `.json` as `application/json`, `.webmanifest` +as `application/manifest+json`, and `.svg` as `image/svg+xml`. Hashed assets may +use a long immutable cache lifetime; keep `index.html`, `sw.js` and +`toolbox-app.json` on revalidation/no-cache so releases update predictably. + +## Reverse-proxy example + +For an nginx deployment rooted at `/apps/colour/`, use `try_files` only to +resolve actual static files and the directory index; do not rewrite missing +asset paths to HTML. The application itself uses relative paths and needs no +server-side router. Keep the `sw.js` scope at the application directory. + +If the app is assembled into toolbox-portal, use the portal's release lock and +assembly process instead of unpacking files into an existing live directory. +Verify the ZIP against its SHA-256 sidecar before assembly. + +## File limits and cleanup + +The image picker enforces encoded-byte, dimension and decoded-pixel limits +before expensive processing. Sampling radius and palette colour counts are +bounded. Object URLs, decoded buffers and worker requests are released or +cancelled when replaced. Browsers and image decoders remain security-sensitive +dependencies, so deploy current supported browser versions and publish patched +application releases promptly. + +See [SECURITY.md](../SECURITY.md) for private reporting instructions. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..60fe392 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,40 @@ +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..c25d7f7 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + Colour Tools + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..709de6b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4024 @@ +{ + "name": "colour-tools", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "colour-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", + "colorjs.io": "0.7.1", + "react": "19.2.8", + "react-dom": "19.2.8" + }, + "devDependencies": { + "@add-ideas/toolbox-testkit": "0.2.3", + "@eslint/js": "10.0.1", + "@playwright/test": "1.62.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "16.3.2", + "@testing-library/user-event": "14.6.1", + "@types/node": "25.9.5", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.5", + "@vitejs/plugin-react": "6.0.3", + "eslint": "10.7.0", + "eslint-plugin-react-hooks": "7.1.1", + "eslint-plugin-react-refresh": "0.5.2", + "fast-check": "4.9.0", + "globals": "17.7.0", + "jsdom": "29.1.1", + "prettier": "3.9.5", + "typescript": "6.0.3", + "typescript-eslint": "8.64.0", + "vite": "8.2.2", + "vitest": "4.1.11" + }, + "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/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "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.2.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", + "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "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.1", + "@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.10", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.10.tgz", + "integrity": "sha512-xBja6gaAaH2R2c7eNyl0TY4dhnnZ2uhj+KXpLdEQ6M/wuk9bYFZM8wY0ykw3VO4TgEJ56KGlerXS/9KBKVR/Cg==", + "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/@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.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "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/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", + "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/@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/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.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", + "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/type-utils": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "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.64.0", + "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.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", + "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "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.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", + "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.64.0", + "@typescript-eslint/types": "^8.64.0", + "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.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", + "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0" + }, + "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.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", + "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "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.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", + "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "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.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", + "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", + "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.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", + "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.64.0", + "@typescript-eslint/tsconfig-utils": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "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.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", + "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.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/visitor-keys": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", + "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "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.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "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.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "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.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "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.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "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/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.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "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.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "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/colorjs.io": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.7.1.tgz", + "integrity": "sha512-LY7OHnJZxHwT5UlzNa9bbhHHDbzB6yE5+3MIPwJEQKRvSCt/T4G7epsj+9j2BExUIIfXFIJGsIXUKdfrK9Q5tA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/color" + } + }, + "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/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-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "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.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/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/electron-to-chromium": { + "version": "1.5.417", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.417.tgz", + "integrity": "sha512-4T+DTDWuMPM4aHlHwWdAVCVWwp7LDilnhzkj+c/Lbj91XSQrLuOmZSLtS9Q4iIqjlPUbPOnC624zDVVHCHaolQ==", + "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.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "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.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "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.2", + "@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/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.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "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==", + "dev": true, + "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.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "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.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "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.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" + } + }, + "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==", + "dev": true, + "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/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.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "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.11", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz", + "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.11" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz", + "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==", + "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/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.64.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz", + "integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.64.0", + "@typescript-eslint/parser": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.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/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.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "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.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "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 || ^0.5.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.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "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.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "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-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.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "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..6aea290 --- /dev/null +++ b/package.json @@ -0,0 +1,72 @@ +{ + "name": "colour-tools", + "version": "0.1.0", + "description": "Convert, composite, compare and build colours locally in the browser.", + "license": "GPL-3.0-or-later", + "author": "Albrecht Degering", + "repository": { + "type": "git", + "url": "git+https://git.add-ideas.de/lotobo/colour-tools.git" + }, + "homepage": "https://git.add-ideas.de/lotobo/colour-tools", + "bugs": { + "url": "https://git.add-ideas.de/lotobo/colour-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: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", + "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:browser && npm run package:release -- --force" + }, + "dependencies": { + "@add-ideas/toolbox-contract": "0.2.3", + "@add-ideas/toolbox-shell-react": "0.2.3", + "colorjs.io": "0.7.1", + "react": "19.2.8", + "react-dom": "19.2.8" + }, + "devDependencies": { + "@add-ideas/toolbox-testkit": "0.2.3", + "@eslint/js": "10.0.1", + "@playwright/test": "1.62.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "16.3.2", + "@testing-library/user-event": "14.6.1", + "@types/node": "25.9.5", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.5", + "@vitejs/plugin-react": "6.0.3", + "eslint": "10.7.0", + "eslint-plugin-react-hooks": "7.1.1", + "eslint-plugin-react-refresh": "0.5.2", + "fast-check": "4.9.0", + "globals": "17.7.0", + "jsdom": "29.1.1", + "prettier": "3.9.5", + "typescript": "6.0.3", + "typescript-eslint": "8.64.0", + "vite": "8.2.2", + "vitest": "4.1.11" + }, + "packageManager": "npm@11.17.0" +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..2c0c008 --- /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: 1, + 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/CHANGELOG.md b/public/CHANGELOG.md new file mode 100644 index 0000000..c5782f7 --- /dev/null +++ b/public/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes are documented here. + +## Unreleased + +## 0.1.0 - 2026-08-31 + +- Added a local CSS colour parser and converters for web, perceptual, + wide-gamut and developer colour formats, including bounded batch conversion. +- Added arbitrary RGBA layer compositing with Porter–Duff source-over, + per-layer opacity, standard blend modes, reordering, encoded-sRGB and + linear-light processing, and optional matte flattening. +- Added multi-stop colour ramps with positioned stops, nine interpolation + spaces, selectable hue routes, easing, premultiplied/straight alpha control + and CSS/JSON export. +- Added a large pointer and keyboard picker with alpha, exact CSS input, + browser-native colour input, local history and optional system EyeDropper. +- Added bounded local image decoding, coordinate and neighbourhood sampling, + deterministic perceptual palette extraction and Web Worker fallback. +- Added transparency-aware WCAG 2 contrast reporting and nearby passing colour + suggestions, Delta E comparison, gamut inspection/mapping and clearly + labelled colour-vision simulations. +- Added harmony, tint, shade and tone generation with local named palettes, + palette import and six design-token export formats. +- Added the shared Toolbox shell, system/light/dark themes, responsive layouts, + accessible navigation, nested-path deployment, offline application shell and + deterministic static release packaging. +- Added a GPL-3.0-or-later project licence and exact runtime dependency notices, + including Color.js 0.7.1 under MIT. diff --git a/public/CONTRIBUTING.md b/public/CONTRIBUTING.md new file mode 100644 index 0000000..7b35535 --- /dev/null +++ b/public/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing + +Thank you for improving Colour Tools. Contributions should preserve its three +core properties: local-first processing, explicit colour semantics and a +relocatable static build. + +## Set up + +Use Node.js 22 or newer and npm 11 or newer. + +```sh +npm ci +npm run check +npm run test:browser +``` + +Run `npm run dev` for the development server. Do not commit `dist/`, `release/`, +local images, browser traces or editor secrets. + +## Expectations + +- Keep calculations pure and serialisable where practical. Add unit tests for + numeric boundaries, invalid input, alpha and out-of-gamut behaviour. +- Never add telemetry, remote image processing, remote fonts or implicit + network lookups. A feature that needs network access requires prior design + discussion and a plainly visible, opt-in boundary. +- Preserve values outside a target gamut until mapping is explicitly requested. + Name the transfer curve or working light for operations where it changes the + result. +- Bound file bytes, decoded dimensions, worker jobs, list sizes and generated + output. Release object URLs, workers and large buffers after use. +- Keep all application URLs relative. Test from the nested path used by the + Playwright server, not only `/`. +- Ensure every pointer interaction has a keyboard route and every colour-only + signal has a textual or structural equivalent. Use semantic controls before + custom roles. +- Document third-party runtime code in `THIRD_PARTY_NOTICES.md`, retain its + exact licence in `LICENSES/`, and confirm compatibility with GPLv3 or later. + +## Changes and review + +Keep a change focused and update `CHANGELOG.md` for user-visible behaviour. +Include the reasoning behind colour-math choices and cite a primary standard +where the choice is normative. Screenshots can help explain UI changes but do +not replace browser tests. + +Before proposing a change, run `npm run check` and `npm run test:browser`. +Tests must not contact external hosts. Security-sensitive reports should follow +[SECURITY.md](SECURITY.md) instead of a public issue. + +Contributions are accepted under the repository's +`GPL-3.0-or-later` licence. diff --git a/public/LICENSE b/public/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/public/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/public/LICENSES/README.md b/public/LICENSES/README.md new file mode 100644 index 0000000..042312d --- /dev/null +++ b/public/LICENSES/README.md @@ -0,0 +1,10 @@ +# Licence inventory + +Colour Tools is `GPL-3.0-or-later`; the full project licence is at `../LICENSE`. + +`THIRD_PARTY_NOTICES.md` identifies shipped runtime packages. Release +preparation collates the exact installed licence files for every locked runtime +package into `npm-runtime-licenses.txt`. + +`colorjs.io-MIT.txt` is retained separately because Color.js supplies the core +standards-aligned colour conversion primitives used by the application. diff --git a/public/LICENSES/colorjs.io-MIT.txt b/public/LICENSES/colorjs.io-MIT.txt new file mode 100644 index 0000000..74fc62b --- /dev/null +++ b/public/LICENSES/colorjs.io-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Lea Verou, Chris Lilley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/public/LICENSES/npm-runtime-licenses.txt b/public/LICENSES/npm-runtime-licenses.txt new file mode 100644 index 0000000..0914ee8 --- /dev/null +++ b/public/LICENSES/npm-runtime-licenses.txt @@ -0,0 +1,514 @@ +============================================================================== +@add-ideas/toolbox-contract@0.2.3 +Declared licence: Apache-2.0 +Installed from: node_modules/@add-ideas/toolbox-contract +============================================================================== +--- LICENSE --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + Copyright 2026 ADD Ideas + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +============================================================================== +@add-ideas/toolbox-shell-react@0.2.3 +Declared licence: Apache-2.0 +Installed from: node_modules/@add-ideas/toolbox-shell-react +============================================================================== +--- LICENSE --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + Copyright 2026 ADD Ideas + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +============================================================================== +colorjs.io@0.7.1 +Declared licence: MIT +Installed from: node_modules/colorjs.io +============================================================================== +--- LICENSE --- +MIT License + +Copyright (c) 2021 Lea Verou, Chris Lilley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +============================================================================== +react@19.2.8 +Declared licence: MIT +Installed from: node_modules/react +============================================================================== +--- LICENSE --- +MIT License + +Copyright (c) Meta Platforms, Inc. and affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +============================================================================== +react-dom@19.2.8 +Declared licence: MIT +Installed from: node_modules/react-dom +============================================================================== +--- LICENSE --- +MIT License + +Copyright (c) Meta Platforms, Inc. and affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +============================================================================== +scheduler@0.27.0 +Declared licence: MIT +Installed from: node_modules/scheduler +============================================================================== +--- LICENSE --- +MIT License + +Copyright (c) Meta Platforms, Inc. and affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/public/README.md b/public/README.md new file mode 100644 index 0000000..fbf24c5 --- /dev/null +++ b/public/README.md @@ -0,0 +1,128 @@ +# Colour Tools + +Colour Tools is a standalone, local-first browser workbench for converting, +compositing, interpolating, sampling, comparing and organising colours. It is +part of the +[add·ideas Toolbox](https://git.add-ideas.de/lotobo/toolbox-portal), but every +release also runs independently from a static HTTPS host or local preview. + +No colour, image, palette or usage event is sent to a server. Image decoding, +pixel sampling and palette extraction happen in the browser; the application +has no analytics, accounts, advertisements or remote lookup feature. + +## Workspaces + +- **Convert** parses CSS colours and presents copy-ready hex, RGB, HSL, HSV, + HWB, CMYK, Lab, LCH, OKLab, OKLCH, Display P3, Adobe RGB, ProPhoto RGB, + Rec. 2020 and XYZ representations. Wide-gamut coordinates remain intact + until an explicit display or gamut-mapping step. A batch converter isolates + invalid lines instead of silently dropping them. +- **Composite** calculates Porter–Duff source-over for an arbitrary ordered + stack of translucent colours. Each layer has its own alpha, opacity and + blend mode. The working-light choice makes encoded-sRGB and linear-light + results explicit, and an optional matte produces an opaque RGB result. +- **Steps** builds multi-stop colour ramps in perceptual, RGB or cylindrical + colour spaces. It supports positioned stops, five hue routes, easing, + premultiplied-alpha interpolation by default with a straight-alpha option, + and copy-ready CSS and JSON output. +- **Pick** provides a large keyboard- and pointer-operable HSV picker, alpha + control, exact CSS entry, browser-native colour input, recent picks and the + system EyeDropper API where the browser exposes it. +- **Image** accepts one bounded local PNG, JPEG, WebP, GIF or AVIF. It samples + an exact coordinate or a configurable average/median neighbourhood and + extracts two to twelve perceptual palette colours in a worker when + available. +- **Contrast** flattens transparency against a stated canvas, reports WCAG 2 + contrast thresholds, finds a nearby passing foreground, compares colours by + six Delta E methods, checks colour gamuts and previews four approximate + colour-vision deficiencies. +- **Palette** creates OKLCH harmonies, tints, shades and tones; keeps a named + palette in local browser storage; imports common text/JSON token forms; and + exports CSS, SCSS, JSON, Design Tokens JSON, JavaScript or CSV. + +Adding a generated or sampled colour to the palette is always explicit. The +saved palette is device-local and can be cleared from the Palette workspace or +by clearing site storage. + +## Colour behaviour + +Colour Tools keeps source coordinates outside display gamuts so conversions do +not destructively clip them. Browser previews and hex exports are mapped to +sRGB with an explicit indicator when mapping was needed. Compositing is carried +out in the selected sRGB working light, while interpolation defaults to OKLCH +for perceptual spacing. See [docs/COLOUR-MATH.md](docs/COLOUR-MATH.md) for the +formulas, bounds and terminology. + +Contrast pass/fail indicators follow WCAG 2 ratios: 3:1 for non-text UI and +large text, 4.5:1 for normal AA text and 7:1 for normal AAA text. They are an +engineering aid, not a complete accessibility audit; typography, state, +context and non-colour cues still need human review. + +## Local image handling + +Selecting an image decodes a bounded raster locally with `createImageBitmap` +where available. The image-element fallback uses a temporary in-memory object +URL. Decoded resources and any fallback URL are released when the image is +replaced, removed or the workspace is closed. Palette extraction uses a +deterministic OKLab clustering implementation and bounded sampling rather than +uploading the image or retaining a copy. Metadata outside the decoded raster +is not exposed or stored by the application. + +## Browser support + +Current evergreen Firefox, Chromium and Safari are the deployment target. +JavaScript modules, CSS custom properties, Canvas 2D and Web Workers are used. +The EyeDropper API is progressive enhancement and only appears where a browser +supports it. Clipboard writes, service workers and EyeDropper access may +require HTTPS and browser permission; all core calculations work without them. + +The UI supports system, light and dark themes through the shared Toolbox shell. +Tabs, picker controls and image sampling are keyboard operable, status changes +use live regions, and swatches are accompanied by machine-readable colour +values rather than relying on colour alone. + +## Offline application shell + +Production builds include a relative-scope web app manifest and a +subpath-aware service worker. After one successful load, the worker caches the +application shell and same-origin built assets, uses network-first navigation +with a cached fallback, serves cached static resources where possible and +removes obsolete cache versions. Service-worker support is optional; colour +operations never depend on a network connection. + +## Development + +Requirements: Node.js 22 or newer and npm 11 or newer. + +```sh +npm ci +npm run check +npm run test:browser +npm run dev +``` + +Vite uses `base: './'`, so `dist/` is relocatable to `/` or a nested Toolbox +path. `toolbox-check` validates standalone and contextual operation. + +The main implementation layers are described in +[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Contributions should follow +[CONTRIBUTING.md](CONTRIBUTING.md); please report security concerns according +to [SECURITY.md](SECURITY.md). + +## Release + +```sh +npm run release:artifact +``` + +This command checks the manifest, types, lint, formatting, unit tests, +production build, Toolbox contract and browser workflows, then creates a +deterministic `release/colour-tools-0.1.0.zip` and matching SHA-256 sidecar. +The archive is a ready-to-host static application and contains project and +third-party licence notices. + +## Licence + +Colour Tools is free software under `GPL-3.0-or-later`. See +[LICENSE](LICENSE). Runtime dependencies retain their respective licences; see +[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). diff --git a/public/SECURITY.md b/public/SECURITY.md new file mode 100644 index 0000000..5e56297 --- /dev/null +++ b/public/SECURITY.md @@ -0,0 +1,50 @@ +# Security policy + +## Supported versions + +Security fixes are made on the current release line. Users should update to the +latest published tag and verify the release ZIP against its `.sha256` sidecar. + +## Reporting a vulnerability + +Please report a suspected vulnerability privately to the repository owner via +the contact channel shown on the +[Gitea profile](https://git.add-ideas.de/lotobo) or a private Gitea message. Do +not include private images, access tokens, unpublished exploit details or other +sensitive material in a public issue. + +Include the affected version, browser and operating system; a minimal local +reproduction; the expected impact; and whether the issue requires a crafted +image, palette or colour string. You should receive acknowledgement within +seven days. Coordinated disclosure is appreciated while a fix and release are +prepared. + +## Security model + +Colour Tools is a static browser application. It has no application backend, +account system, analytics endpoint or image-upload endpoint. All selected image +bytes are decoded and processed locally. Saved palette entries use browser +storage for the current origin. Clipboard, file picker, download and EyeDropper +actions happen only after a user gesture and remain governed by browser +permissions. + +File processing is deliberately bounded by encoded byte size, decoded pixel +count and dimensions. Image palette extraction samples a bounded number of +pixels and can run in a disposable worker. Object URLs and worker jobs are +cancelled or released when replaced. Input is displayed as text or parsed by +typed colour/image APIs; it is not evaluated as script or inserted as trusted +HTML. + +Production hosts should serve the headers in +[docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md), use HTTPS, keep MIME types +correct, disable content sniffing and avoid injecting third-party scripts. A +host can still observe ordinary HTTP metadata for the initial static-file +request; “local-first” describes application data processing, not anonymity +from the static host. + +## Out of scope + +Colour rendering differences between conforming browser/OS colour-management +pipelines are not vulnerabilities by themselves. Likewise, an approximate +colour-vision simulation is an educational preview, not a medical diagnosis or +a complete accessibility guarantee. diff --git a/public/SOURCE.md b/public/SOURCE.md new file mode 100644 index 0000000..7b05887 --- /dev/null +++ b/public/SOURCE.md @@ -0,0 +1,31 @@ +# Corresponding source and provenance + +The corresponding source for Colour Tools 0.1.0 is: + +https://git.add-ideas.de/lotobo/colour-tools/src/tag/v0.1.0 + +Build that tag with Node.js 22 and the exact dependency graph recorded in +`package-lock.json`: + +```sh +npm ci +npm run release:artifact +``` + +The generated JavaScript and CSS bundles are not the preferred form for +modification. No runtime code is loaded from a CDN. The production ZIP contains +this source pointer, the application licence, the dependency licence inventory, +and the applicable third-party notices. + +## Implementation provenance + +- The application is original project code and uses the shared Toolbox SDK + packages at version 0.2.3 for its manifest contract, shell, and build checks. +- Colour conversion and adaptation use Color.js 0.7.1. Its MIT licence is + reproduced in `LICENSES/colorjs.io-MIT.txt`. +- The compositing, interpolation, palette, image-sampling, contrast, comparison, + gamut, and colour-vision utilities in `src/colour` and `src/palette` were + implemented for this project. +- CSS Color 4 and WCAG 2.2 were used as public technical specifications. No + source code or branded assets were copied from the example websites that + informed the feature list. diff --git a/public/THIRD_PARTY_NOTICES.md b/public/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..2cfadf9 --- /dev/null +++ b/public/THIRD_PARTY_NOTICES.md @@ -0,0 +1,25 @@ +# Third-party notices + +Release preparation collates licence files for the exact locked runtime +packages into `LICENSES/npm-runtime-licenses.txt` inside the static artifact. + +| Package | Version | Licence | Role | Source | +| -------------------------------- | ------- | ---------- | ------------------------------------ | ------------------------------------------- | +| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | manifest and context contract | https://git.add-ideas.de/lotobo/toolbox-sdk | +| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | shared shell, theme and actions | https://git.add-ideas.de/lotobo/toolbox-sdk | +| `colorjs.io` | 0.7.1 | MIT | parsing, conversion and colour maths | https://github.com/color-js/color.js | +| `react` | 19.2.8 | MIT | application UI | https://github.com/facebook/react | +| `react-dom` | 19.2.8 | MIT | browser rendering | https://github.com/facebook/react | + +`scheduler` 0.27.0, distributed by the React project under MIT, is the only +transitive runtime package. Development-only packages build and test the +source but are not shipped as runtime modules. + +The exact Color.js notice is retained at +`LICENSES/colorjs.io-MIT.txt`. Apache-2.0 and MIT texts from all installed +runtime packages are reproduced by the deterministic release-preparation step. + +CSS Color, CSS Compositing and Blending, WCAG and the linked web utilities that +informed the product requirements are specification or research references +only. Their source code, artwork and documentation wording are not bundled or +copied into Colour Tools. diff --git a/public/docs/ACCESSIBILITY.md b/public/docs/ACCESSIBILITY.md new file mode 100644 index 0000000..89b14a8 --- /dev/null +++ b/public/docs/ACCESSIBILITY.md @@ -0,0 +1,19 @@ +# Accessibility notes + +Colour Tools treats colour as data as well as appearance. Swatches are paired +with textual values, gamut/contrast status uses text and symbols in addition to +hue, and generated palettes remain usable without visually distinguishing +every swatch. + +Workspace navigation uses tabs with selected state and arrow/Home/End keyboard +navigation. Native inputs and labelled controls are preferred. The large picker +supports arrow-key adjustment and announces its saturation and brightness; +image sampling supports numeric coordinates and keyboard movement in addition +to pointer selection. Calculation and file statuses use live regions without +clearing the last valid result while a user is typing. + +The shared Toolbox shell provides system, light and dark themes. Focus rings, +touch targets, reflow and text contrast should be checked in all themes and at +400% zoom. A WCAG contrast result is not itself proof that the complete +interface conforms: test keyboard order, names/roles/values, error recovery, +high contrast modes, reduced motion and screen-reader output in context. diff --git a/public/docs/ARCHITECTURE.md b/public/docs/ARCHITECTURE.md new file mode 100644 index 0000000..dc86eed --- /dev/null +++ b/public/docs/ARCHITECTURE.md @@ -0,0 +1,69 @@ +# Architecture + +Colour Tools is a client-only React application built as relocatable static +files. The UI and calculation engine are intentionally separated so colour +operations can be tested without a browser and UI state can keep showing the +last valid result while an input is incomplete. + +## Layers + +1. `src/colour/` is the serialisable colour domain. It parses and formats + colours, converts gamuts, composites layers, interpolates stops, evaluates + contrast and Delta E, simulates colour-vision deficiencies, and builds or + exports palettes. Its public API is re-exported from `src/colour/index.ts`. +2. `src/palette/` contains browser-independent raster bounds, sampling and + deterministic OKLab clustering. `src/workers/palette.worker.ts` exposes the + expensive extraction path to a dedicated worker. +3. `src/components/` contains the seven workspaces and shared accessible + controls. Workspace selection is represented by a URL fragment, so a view + can be bookmarked without router or server support. +4. `src/hooks/` owns bounded device-local UI persistence. Image bytes are not + stored there. +5. `src/toolbox/` contains the checked manifest identity. The shared Toolbox + shell supplies navigation, contextual actions and theme behaviour without + changing the standalone application domain. + +## Colour values + +The engine's `ColourValue` is a plain object: colour-space identifier, three +numeric coordinates and alpha. It deliberately carries coordinates that may +sit outside a display gamut. Color.js 0.7.1 supplies standards-aligned parsing +and conversion primitives; Colour Tools wraps these in typed, bounded domain +operations and stable UI formats. + +Browser preview is a separate operation. It maps a value to sRGB and reports +whether mapping occurred. This keeps display limitations from mutating the +source colour used by later conversions or comparisons. + +## Raster pipeline + +The image picker validates the selected file before decode, rejects excessive +bytes/dimensions/pixels, then draws the decoded image to a same-origin in-memory +Canvas 2D surface. Sampling reads a bounded neighbourhood. Palette extraction +subsamples deterministically, converts points to OKLab and clusters them with a +fixed initialization and iteration bound. The worker client can fall back to +the same pure function when workers are unavailable. + +Temporary object URLs are revoked and outstanding extraction is aborted when an +image is replaced or removed. No image or extracted palette is transmitted. + +## Build and deployment + +Vite builds with a relative base. `scripts/generate-toolbox-manifest.mjs` +checks package, application and Toolbox versions plus immutable source +identity. `scripts/prepare-release-files.mjs` places project documentation and +the exact installed runtime licence inventory beside the executable app. + +The service worker is relative-scope and deployment-subpath aware. It is +progressive enhancement: the app remains usable when registration is rejected. +The deterministic packager rejects symbolic links, source maps, secret-like +filenames and root-absolute asset references before creating the release ZIP +and SHA-256 sidecar. + +## Testing boundaries + +Unit tests cover parsing, numerical operations, invalid inputs, raster bounds, +sampling and palette determinism. Browser smoke tests run the production build +under a restrictive policy at `/deep/nested/colour/`, exercise all major +workspaces and fail on uncaught errors or external network requests. The +Toolbox contract checker validates the static artifact independently. diff --git a/public/docs/COLOUR-MATH.md b/public/docs/COLOUR-MATH.md new file mode 100644 index 0000000..30a57a9 --- /dev/null +++ b/public/docs/COLOUR-MATH.md @@ -0,0 +1,94 @@ +# Colour mathematics and terminology + +This document records the choices that materially change a result. It is not a +replacement for CSS Color, Compositing and Blending, or WCAG. + +## Parsing and conversion + +The parser accepts CSS colour syntax supported by the installed Color.js +version, including hex and named colours; `rgb()`, `hsl()`, `hwb()`, Lab/LCH +and OKLab/OKLCH functions; and registered `color()` spaces. Conversion keeps +finite coordinates outside a target gamut. Hex and ordinary browser sRGB +previews require mapping; the UI labels when this happened. + +The converter exposes device-oriented and mathematical representations, but a +numeric tuple is meaningful only with its named colour space, white point, +transfer curve and alpha convention. CMYK output is a convenient mathematical +conversion, not a printer ICC separation or proof. + +## Alpha compositing + +For a source colour over a backdrop, with straight alpha `αs` and `αb`, output +alpha is: + +```text +αo = αs + αb × (1 − αs) +``` + +For each premultiplied channel under normal source-over: + +```text +Co = Cs × αs + Cb × αb × (1 − αs) +co = Co / αo when αo > 0 +``` + +Layers are accumulated from the bottom upward even though the editor displays +the top layer first. Layer opacity multiplies the colour's own alpha. Blend +modes are applied before source-over using the model in CSS Compositing and +Blending. + +“Encoded sRGB” applies the operation directly to sRGB-encoded channel values, +matching many quick web calculators. “Linear-light sRGB” first removes the +sRGB transfer curve and is generally the physically meaningful choice for +mixing emitted light. The two results are expected to differ. + +## Interpolation + +A ramp samples its first and last stop and distributes missing positions like a +CSS gradient. Each segment is interpolated in the selected space with the +chosen easing. Cylindrical spaces need an explicit hue route: shorter, longer, +increasing, decreasing or raw. The default premultiplies colour components by +alpha before mixing and unpremultiplies afterwards, preventing hidden channels +inside a transparent endpoint from tinting the ramp. A straight-alpha toggle +is available for intentionally reproducing that model. OKLCH is the UI default +because equal numeric progress tends to look more even than encoded RGB; it +does not guarantee that every intermediate colour lies in a display gamut. + +## Gamut mapping + +An in-gamut check does not modify the colour. Display mapping uses an OKLCH +chroma-reduction method by default; channel clipping is available at the engine +boundary where explicitly selected. The result includes Delta E OK from the +source so callers can quantify the change. A browser preview is always mapped +to sRGB because CSS support and physical displays differ. + +## Contrast and transparency + +Relative luminance and WCAG 2 contrast are evaluated after translucent +foreground/background colours are composited over the stated canvas (white by +default). The ratio is: + +```text +(Llighter + 0.05) / (Ldarker + 0.05) +``` + +The UI reports 3:1, 4.5:1 and 7:1 thresholds and searches for a nearby lighter +or darker passing foreground. A mathematical pass is only one part of an +accessibility review; font size/weight, focus states, disabled controls, +non-colour cues and actual display conditions remain relevant. + +## Perceptual difference and simulations + +Delta E 76, CMC, CIEDE2000, Delta E OK, ITP and Jz answer different historical +or application needs; values from different formulas are not interchangeable. +The colour-vision previews apply bounded matrices in linear sRGB and expose a +severity control. They are approximations for interface review, not a model of +every observer and not medical advice. + +## Palette extraction + +The raster extractor ignores sufficiently transparent pixels, converts bounded +samples to OKLab and clusters them deterministically. Reported coverage is the +share of accepted samples assigned to a cluster, not an exact segmentation of +every source pixel. Small antialiased regions and colour-managed decoding can +therefore influence the result. diff --git a/public/docs/PRIVACY-SECURITY.md b/public/docs/PRIVACY-SECURITY.md new file mode 100644 index 0000000..0dffc56 --- /dev/null +++ b/public/docs/PRIVACY-SECURITY.md @@ -0,0 +1,60 @@ +# Privacy, security and deployment + +## Data flow + +Colour input, saved palette entries, selected image bytes, decoded pixels and +generated exports stay in the browser. The application does not issue API +requests, load remote fonts, submit analytics or resolve remote colour +references. A saved palette belongs to the current browser origin and is +stored until the user clears it or browser storage is removed. Images are +in-memory only. + +The static host still receives ordinary requests for HTML, JavaScript, CSS, +icons and worker files on initial load and revalidation. Host logs, reverse +proxy logs and browser extension behaviour are outside the application's local +processing boundary. + +## Recommended headers + +Serve the static artifact over HTTPS and apply a policy equivalent to: + +```text +Content-Security-Policy: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self' +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Resource-Policy: same-origin +Permissions-Policy: camera=(), microphone=(), geolocation=(), usb=(), payment=() +Referrer-Policy: no-referrer +X-Content-Type-Options: nosniff +``` + +The shared Toolbox shell uses inline style properties for colour previews, so +`style-src 'unsafe-inline'` is currently required. Do not add +`script-src 'unsafe-inline'` or third-party script origins. `blob:` is required +for local image object URLs and may be used for a worker fallback. + +Serve `.js` as `text/javascript`, `.json` as `application/json`, `.webmanifest` +as `application/manifest+json`, and `.svg` as `image/svg+xml`. Hashed assets may +use a long immutable cache lifetime; keep `index.html`, `sw.js` and +`toolbox-app.json` on revalidation/no-cache so releases update predictably. + +## Reverse-proxy example + +For an nginx deployment rooted at `/apps/colour/`, use `try_files` only to +resolve actual static files and the directory index; do not rewrite missing +asset paths to HTML. The application itself uses relative paths and needs no +server-side router. Keep the `sw.js` scope at the application directory. + +If the app is assembled into toolbox-portal, use the portal's release lock and +assembly process instead of unpacking files into an existing live directory. +Verify the ZIP against its SHA-256 sidecar before assembly. + +## File limits and cleanup + +The image picker enforces encoded-byte, dimension and decoded-pixel limits +before expensive processing. Sampling radius and palette colour counts are +bounded. Object URLs, decoded buffers and worker requests are released or +cancelled when replaced. Browsers and image decoders remain security-sensitive +dependencies, so deploy current supported browser versions and publish patched +application releases promptly. + +See [SECURITY.md](../SECURITY.md) for private reporting instructions. diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..b27ec74 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..7570188 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,18 @@ +{ + "name": "Colour Tools", + "short_name": "Colour", + "description": "Convert, composite, compare and build colours locally.", + "start_url": "./", + "scope": "./", + "display": "standalone", + "background_color": "#f7f7fb", + "theme_color": "#2d2859", + "icons": [ + { + "src": "./favicon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..9cbf55d --- /dev/null +++ b/public/sw.js @@ -0,0 +1,79 @@ +const CACHE_PREFIX = "colour-tools-shell-"; +const CACHE_NAME = `${CACHE_PREFIX}0.1.0`; +const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then(async (cache) => { + await cache.addAll(CORE); + const response = await fetch("./"); + const html = await response.text(); + const assets = [...html.matchAll(/(?:src|href)="(\.\/assets\/[^"]+)"/g)] + .map((match) => match[1]) + .filter(Boolean); + await cache.addAll(assets); + }), + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all( + keys + .filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME) + .map((key) => caches.delete(key)), + ), + ) + .then(() => self.clients.claim()), + ); +}); + +self.addEventListener("fetch", (event) => { + if (event.request.method !== "GET") return; + const url = new URL(event.request.url); + if (url.origin !== self.location.origin) return; + const scope = new URL(self.registration.scope); + if (!url.pathname.startsWith(scope.pathname)) return; + + if (event.request.mode === "navigate") { + event.respondWith( + fetch(event.request) + .then((response) => { + if (response.ok) { + const copy = response.clone(); + void caches + .open(CACHE_NAME) + .then((cache) => cache.put(event.request, copy)); + } + return response; + }) + .catch(async () => { + const cached = await caches.match(event.request); + if (cached) return cached; + const shellUrl = new URL("./", self.registration.scope).href; + return (await caches.match(shellUrl)) ?? Response.error(); + }), + ); + return; + } + + event.respondWith( + caches.match(event.request).then( + (cached) => + cached ?? + fetch(event.request).then((response) => { + if (response.ok) { + const copy = response.clone(); + void caches + .open(CACHE_NAME) + .then((cache) => cache.put(event.request, copy)); + } + return response; + }), + ), + ); +}); diff --git a/public/toolbox-app.json b/public/toolbox-app.json new file mode 100644 index 0000000..da7363c --- /dev/null +++ b/public/toolbox-app.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://git.add-ideas.de/lotobo/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json", + "schemaVersion": 1, + "id": "de.add-ideas.colour-tools", + "name": "Colour Tools", + "version": "0.1.0", + "description": "Convert, composite, compare and build colours locally in the browser.", + "entry": "./", + "icon": "./favicon.svg", + "categories": ["graphics", "design", "developer"], + "tags": [ + "colour", + "color", + "rgb", + "rgba", + "oklch", + "palette", + "gradient", + "contrast", + "image" + ], + "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, + "label": "Colours and images stay in this browser; nothing is uploaded." + }, + "source": { + "repository": "https://git.add-ideas.de/lotobo/colour-tools", + "license": "GPL-3.0-or-later" + }, + "actions": [ + { + "id": "source", + "label": "Source", + "url": "https://git.add-ideas.de/lotobo/colour-tools" + } + ] +} diff --git a/scripts/generate-toolbox-manifest.mjs b/scripts/generate-toolbox-manifest.mjs new file mode 100644 index 0000000..11aa57b --- /dev/null +++ b/scripts/generate-toolbox-manifest.mjs @@ -0,0 +1,63 @@ +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 source = JSON.parse(await readFile(sourcePath, "utf8")); +const packageJson = JSON.parse( + await readFile(join(root, "package.json"), "utf8"), +); +const versionSource = await readFile(join(root, "src", "version.ts"), "utf8"); +const applicationVersion = + /^export const (?:APPLICATION|APP)_VERSION = "([^"]+)";$/mu.exec( + versionSource, + )?.[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.id !== "de.add-ideas.colour-tools" || + source.source?.repository !== + "https://git.add-ideas.de/lotobo/colour-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(root, "public", 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 (process.argv.includes("--check")) { + if ((await readFile(outputPath, "utf8").catch(() => "")) !== 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..e50fca5 --- /dev/null +++ b/scripts/package-release.mjs @@ -0,0 +1,179 @@ +#!/usr/bin/env node +import { createHash } from "node:crypto"; +import { execFile } from "node:child_process"; +import { + access, + chmod, + copyFile, + cp, + lstat, + mkdir, + mkdtemp, + readFile, + readdir, + rename, + rm, + utimes, + writeFile, +} from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; + +const execute = promisify(execFile); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const packageJson = JSON.parse( + await readFile(path.join(root, "package.json"), "utf8"), +); +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/colour-tools-${packageJson.version}.zip`), +); +const checksumOutput = `${output}.sha256`; +const force = process.argv.includes("--force"); + +if ( + path.extname(output).toLowerCase() !== ".zip" || + output === root || + output === path.parse(output).root +) + throw new Error("Release output is not a safe ZIP target"); + +const exists = (file) => + access(file).then( + () => true, + () => false, + ); +if (!force && ((await exists(output)) || (await exists(checksumOutput)))) + throw new Error("Release output already exists; use --force to replace it"); + +const input = path.join(root, "dist"); +for (const name of [ + "index.html", + "manifest.webmanifest", + "sw.js", + "toolbox-app.json", + "favicon.svg", + "README.md", + "CHANGELOG.md", + "CONTRIBUTING.md", + "LICENSE", + "SECURITY.md", + "SOURCE.md", + "THIRD_PARTY_NOTICES.md", + "LICENSES/README.md", + "LICENSES/colorjs.io-MIT.txt", + "LICENSES/npm-runtime-licenses.txt", + "docs/ACCESSIBILITY.md", + "docs/ARCHITECTURE.md", + "docs/COLOUR-MATH.md", + "docs/PRIVACY-SECURITY.md", +]) { + const details = await lstat(path.join(input, name)).catch(() => null); + if (!details?.isFile() || details.isSymbolicLink()) + throw new Error(`Release is missing a regular file: ${name}`); +} + +const manifest = JSON.parse( + await readFile(path.join(input, "toolbox-app.json"), "utf8"), +); +if ( + manifest.id !== "de.add-ideas.colour-tools" || + manifest.version !== packageJson.version || + manifest.entry !== "./" || + manifest.icon !== "./favicon.svg" || + manifest.source?.repository !== "https://git.add-ideas.de/lotobo/colour-tools" +) + throw new Error("Packaged Toolbox manifest identity is invalid"); + +const html = await readFile(path.join(input, "index.html"), "utf8"); +if (/\b(?:src|href)=["']\//iu.test(html)) + throw new Error("index.html contains a root-absolute asset reference"); + +async function collect(directory, prefix = "") { + const files = []; + for (const entry of (await readdir(directory, { withFileTypes: true })).sort( + (left, right) => + left.name === right.name ? 0 : left.name < right.name ? -1 : 1, + )) { + const absolute = path.join(directory, entry.name); + const relative = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isSymbolicLink()) + throw new Error(`Release 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 sourceFiles = await collect(input); +for (const file of sourceFiles) + if ( + file.relative.endsWith(".map") || + /(?:^|\/)(?:\.env(?:\.|$)|id_rsa|id_ed25519|.*\.pem$|.*\.key$)/iu.test( + file.relative, + ) || + file.relative.startsWith("/") || + file.relative.split("/").includes("..") + ) + throw new Error(`Forbidden release entry: ${file.relative}`); + +await mkdir(path.dirname(output), { recursive: true }); +const stagingRoot = await mkdtemp(path.join(os.tmpdir(), "colour-release-")); +const publicationRoot = await mkdtemp( + path.join(path.dirname(output), ".colour-publish-"), +); +const stagedTree = path.join(stagingRoot, "tree"); +const stagedArchive = path.join(stagingRoot, path.basename(output)); + +try { + await cp(input, stagedTree, { recursive: true }); + const timestamp = new Date("1980-01-01T00:00:00.000Z"); + for (const file of await collect(stagedTree)) { + await chmod(file.absolute, 0o644); + await utimes(file.absolute, timestamp, timestamp); + } + await execute( + "zip", + [ + "-X", + "-q", + "-9", + stagedArchive, + ...sourceFiles.map((file) => file.relative), + ], + { + cwd: stagedTree, + env: { ...process.env, TZ: "UTC" }, + maxBuffer: 1024 * 1024, + }, + ); + const archive = await readFile(stagedArchive); + const digest = createHash("sha256").update(archive).digest("hex"); + const stagedChecksum = `${stagedArchive}.sha256`; + await writeFile(stagedChecksum, `${digest} ${path.basename(output)}\n`, { + mode: 0o644, + }); + const publicationArchive = path.join(publicationRoot, path.basename(output)); + const publicationChecksum = `${publicationArchive}.sha256`; + await copyFile(stagedArchive, publicationArchive); + await copyFile(stagedChecksum, publicationChecksum); + if (force) { + await rm(output, { force: true }); + await rm(checksumOutput, { force: true }); + } + await rename(publicationArchive, output); + await rename(publicationChecksum, checksumOutput); + console.log( + `Created ${path.relative(root, output)} (${archive.byteLength} bytes, ${sourceFiles.length} files)\nSHA-256 ${digest}`, + ); +} finally { + await rm(stagingRoot, { recursive: true, force: true }); + await rm(publicationRoot, { recursive: true, force: true }); +} diff --git a/scripts/prepare-release-files.mjs b/scripts/prepare-release-files.mjs new file mode 100644 index 0000000..fed21b0 --- /dev/null +++ b/scripts/prepare-release-files.mjs @@ -0,0 +1,72 @@ +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 destination = path.join(root, "public"); +const required = [ + "LICENSE", + "README.md", + "CHANGELOG.md", + "CONTRIBUTING.md", + "SECURITY.md", + "SOURCE.md", + "THIRD_PARTY_NOTICES.md", +]; + +await mkdir(destination, { recursive: true }); +for (const name of required) { + await readFile(path.join(root, name)); + await cp(path.join(root, name), path.join(destination, name)); +} + +for (const directory of ["LICENSES", "docs"]) { + const output = path.join(destination, directory); + await rm(output, { recursive: true, force: true }); + await cp(path.join(root, directory), output, { recursive: true }); +} + +const lock = JSON.parse( + await readFile(path.join(root, "package-lock.json"), "utf8"), +); +const sections = []; + +for (const [location, locked] of Object.entries(lock.packages ?? {}).sort( + ([left], [right]) => (left === right ? 0 : left < right ? -1 : 1), +)) { + 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 and non-text aliases. */ + } + } + sections.push( + [ + "=".repeat(78), + `${details.name}@${details.version}`, + `Declared licence: ${details.license ?? locked.license ?? "See upstream"}`, + `Installed from: ${location}`, + "=".repeat(78), + texts.join("\n\n") || + "No package-local licence file was present; see THIRD_PARTY_NOTICES.md.", + ].join("\n"), + ); +} + +await writeFile( + path.join(destination, "LICENSES", "npm-runtime-licenses.txt"), + `${sections.join("\n\n").trimEnd()}\n`, +); +console.log("Prepared static release documentation and notices"); diff --git a/scripts/serve-test.mjs b/scripts/serve-test.mjs new file mode 100644 index 0000000..0a83bdb --- /dev/null +++ b/scripts/serve-test.mjs @@ -0,0 +1,80 @@ +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/colour/"; +const mediaTypes = new Map([ + [".css", "text/css; charset=utf-8"], + [".html", "text/html; charset=utf-8"], + [".js", "text/javascript; charset=utf-8"], + [".json", "application/json; charset=utf-8"], + [".webmanifest", "application/manifest+json; charset=utf-8"], + [".svg", "image/svg+xml"], + [".md", "text/markdown; charset=utf-8"], + [".txt", "text/plain; charset=utf-8"], + [".wasm", "application/wasm"], + [".png", "image/png"], + [".jpg", "image/jpeg"], + [".jpeg", "image/jpeg"], + [".webp", "image/webp"], + [".avif", "image/avif"], +]); +const headers = { + "Content-Security-Policy": + "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'", + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Resource-Policy": "same-origin", + "Permissions-Policy": + "camera=(), microphone=(), geolocation=(), usb=(), payment=()", + "Referrer-Policy": "no-referrer", + "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"); + 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); + response.writeHead(200, { + "Content-Type": + mediaTypes.get(path.extname(file)) ?? "application/octet-stream", + "Cache-Control": "no-cache", + ...headers, + }); + 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("Colour 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..dd3a995 --- /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 () => ({ + default: (await import("./components/Workbench")).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 colour workbench… +

+ } + > + +
+
+ setHelpOpen(false)} /> +
+ ); +} diff --git a/src/colour/accessibility.ts b/src/colour/accessibility.ts new file mode 100644 index 0000000..86b8a58 --- /dev/null +++ b/src/colour/accessibility.ts @@ -0,0 +1,209 @@ +import { + asColor, + clamp, + colorFrom, + lineariseSrgb, + srgbChannels, + toColourValue, +} from "./internal"; +import { compositeSourceOver } from "./composite"; +import { formatColour, toSrgbCss } from "./format"; +import { mapToGamut } from "./gamut"; +import { parseColour } from "./parse"; +import type { + ColourInput, + ColourValue, + ContrastOptions, + ContrastReport, + ContrastSuggestion, +} from "./types"; + +const WHITE: ColourValue = { space: "srgb", coords: [1, 1, 1], alpha: 1 }; + +export function flattenColour( + foreground: ColourInput, + background: ColourInput, +): ColourValue { + // Browser alpha compositing for ordinary CSS sRGB colours is performed in the + // encoded colour space; contrast must inspect the pixels users actually see. + return compositeSourceOver(foreground, background, { space: "srgb" }).colour; +} + +function opaqueCanvas(input: ColourInput | undefined): ColourValue { + const canvas = parseColour(input ?? WHITE); + return canvas.alpha >= 1 ? canvas : flattenColour(canvas, WHITE); +} + +function flattenedPair( + foreground: ColourInput, + background: ColourInput, + options: ContrastOptions, +): { foreground: ColourValue; background: ColourValue } { + const canvas = opaqueCanvas(options.canvas); + const flattenedBackground = flattenColour(background, canvas); + const flattenedForeground = flattenColour(foreground, flattenedBackground); + return { foreground: flattenedForeground, background: flattenedBackground }; +} + +export function relativeLuminance(input: ColourInput): number { + const [red, green, blue] = srgbChannels(parseColour(input)); + return ( + 0.2126 * lineariseSrgb(red) + + 0.7152 * lineariseSrgb(green) + + 0.0722 * lineariseSrgb(blue) + ); +} + +export function contrastRatio( + foreground: ColourInput, + background: ColourInput, + options: ContrastOptions = {}, +): number { + const pair = flattenedPair(foreground, background, options); + const first = relativeLuminance(pair.foreground); + const second = relativeLuminance(pair.background); + const lighter = Math.max(first, second); + const darker = Math.min(first, second); + return (lighter + 0.05) / (darker + 0.05); +} + +export function contrastReport( + foreground: ColourInput, + background: ColourInput, + options: ContrastOptions = {}, +): ContrastReport { + const foregroundValue = parseColour(foreground); + const backgroundValue = parseColour(background); + const pair = flattenedPair(foregroundValue, backgroundValue, options); + const ratio = contrastRatio(foregroundValue, backgroundValue, options); + return { + ratio, + foreground: foregroundValue, + background: backgroundValue, + flattenedForeground: pair.foreground, + flattenedBackground: pair.background, + passes: { + aaLarge: ratio >= 3, + aaaLarge: ratio >= 4.5, + aaNormal: ratio >= 4.5, + aaaNormal: ratio >= 7, + nonText: ratio >= 3, + }, + }; +} + +interface Candidate { + colour: ColourValue; + ratio: number; + difference: number; + lightness: number; +} + +function candidateAtLightness( + original: ColourValue, + oklch: readonly number[], + lightness: number, + background: ColourInput, + target: number, + options: ContrastOptions, +): Candidate | undefined { + const candidate = mapToGamut( + toColourValue( + colorFrom( + "oklch", + [lightness, oklch[1] ?? 0, oklch[2] ?? 0], + original.alpha, + ), + ), + ); + candidate.alpha = original.alpha; + const ratio = contrastRatio(candidate, background, options); + if (ratio + 1e-9 < target) return undefined; + return { + colour: candidate, + ratio, + difference: asColor(original).deltaEOK(asColor(candidate)), + lightness, + }; +} + +export function nearestPassingColour( + foreground: ColourInput, + background: ColourInput, + target = 4.5, + options: ContrastOptions = {}, +): ContrastSuggestion | null { + if (!Number.isFinite(target) || target < 1 || target > 21) { + throw new RangeError("Contrast target must be between 1 and 21."); + } + const original = parseColour(foreground); + const originalRatio = contrastRatio(original, background, options); + const originalOklch = asColor(original) + .to("oklch") + .coords.map((coordinate) => Number(coordinate ?? 0)); + const originalLightness = clamp(originalOklch[0] ?? 0); + if (originalRatio >= target) { + return { + colour: original, + css: toSrgbCss(original), + hex: formatColour(original, original.alpha < 1 ? "hex8" : "hex"), + ratio: originalRatio, + deltaEOK: 0, + direction: originalLightness >= 0.5 ? "lighter" : "darker", + }; + } + + let best: Candidate | undefined; + const consider = (candidate: Candidate | undefined): void => { + if (!candidate) return; + if ( + !best || + candidate.difference < best.difference - 1e-9 || + (Math.abs(candidate.difference - best.difference) <= 1e-9 && + candidate.ratio < best.ratio) + ) { + best = candidate; + } + }; + + for (let index = 0; index <= 400; index += 1) { + consider( + candidateAtLightness( + original, + originalOklch, + index / 400, + background, + target, + options, + ), + ); + } + if (!best) return null; + + const coarse = best as Candidate; + const lower = clamp(coarse.lightness - 1 / 400); + const upper = clamp(coarse.lightness + 1 / 400); + for (let index = 0; index <= 100; index += 1) { + const lightness = lower + ((upper - lower) * index) / 100; + consider( + candidateAtLightness( + original, + originalOklch, + lightness, + background, + target, + options, + ), + ); + } + + const chosen = best as Candidate; + return { + colour: chosen.colour, + css: toSrgbCss(chosen.colour), + hex: formatColour(chosen.colour, chosen.colour.alpha < 1 ? "hex8" : "hex"), + ratio: chosen.ratio, + deltaEOK: chosen.difference, + direction: chosen.lightness >= originalLightness ? "lighter" : "darker", + }; +} diff --git a/src/colour/colorjs.d.ts b/src/colour/colorjs.d.ts new file mode 100644 index 0000000..10d1cbc --- /dev/null +++ b/src/colour/colorjs.d.ts @@ -0,0 +1,4 @@ +declare module "colorjs.io/dist/color.js" { + export { default } from "colorjs.io"; + export type * from "colorjs.io"; +} diff --git a/src/colour/compare.ts b/src/colour/compare.ts new file mode 100644 index 0000000..52c2b2e --- /dev/null +++ b/src/colour/compare.ts @@ -0,0 +1,115 @@ +import { + asColor, + clamp, + encodeSrgb, + lineariseSrgb, + srgbChannels, +} from "./internal"; +import { mapToGamut } from "./gamut"; +import { parseColour } from "./parse"; +import type { + ColourCoordinates, + ColourInput, + ColourValue, + ColourVisionDeficiency, + ColourVisionOptions, + DeltaEMethod, +} from "./types"; + +export function deltaE( + first: ColourInput, + second: ColourInput, + method: DeltaEMethod = "2000", +): number { + const left = asColor(parseColour(first)); + const right = asColor(parseColour(second)); + switch (method) { + case "76": + return left.deltaE76(right); + case "cmc": + return left.deltaECMC(right); + case "2000": + return left.deltaE2000(right); + case "ok": + return left.deltaEOK(right); + case "itp": + return left.deltaEITP(right); + case "jz": + return left.deltaEJz(right); + } +} + +type Matrix = readonly [ + readonly [number, number, number], + readonly [number, number, number], + readonly [number, number, number], +]; + +// Full-severity Machado et al. matrices, applied to linear-light sRGB. +const VISION_MATRICES: Record = { + protanopia: [ + [0.152286, 1.052583, -0.204868], + [0.114503, 0.786281, 0.099216], + [-0.003882, -0.048116, 1.051998], + ], + deuteranopia: [ + [0.367322, 0.860646, -0.227968], + [0.280085, 0.672501, 0.047413], + [-0.01182, 0.04294, 0.968881], + ], + tritanopia: [ + [1.255528, -0.076749, -0.178779], + [-0.078411, 0.930809, 0.147602], + [0.004733, 0.691367, 0.3039], + ], + achromatopsia: [ + [0.2126, 0.7152, 0.0722], + [0.2126, 0.7152, 0.0722], + [0.2126, 0.7152, 0.0722], + ], +}; + +function multiply( + matrix: Matrix, + vector: ColourCoordinates, +): ColourCoordinates { + return matrix.map( + (row) => row[0] * vector[0] + row[1] * vector[1] + row[2] * vector[2], + ) as ColourCoordinates; +} + +export function simulateColourVision( + input: ColourInput, + deficiency: ColourVisionDeficiency, + options: ColourVisionOptions = {}, +): ColourValue { + const source = parseColour(input); + const severity = clamp(options.severity ?? 1); + const encoded = srgbChannels(source); + const linear = encoded.map(lineariseSrgb) as ColourCoordinates; + const simulated = multiply(VISION_MATRICES[deficiency], linear); + const mixed = linear.map( + (channel, index) => + channel + ((simulated[index] ?? channel) - channel) * severity, + ); + const result: ColourValue = { + space: "srgb", + coords: mixed.map(encodeSrgb) as ColourCoordinates, + alpha: source.alpha, + }; + return options.mapToSrgb === false + ? result + : mapToGamut(result, { method: "clip" }); +} + +export function simulateColourVisionSet( + input: ColourInput, + options: ColourVisionOptions = {}, +): Record { + return { + protanopia: simulateColourVision(input, "protanopia", options), + deuteranopia: simulateColourVision(input, "deuteranopia", options), + tritanopia: simulateColourVision(input, "tritanopia", options), + achromatopsia: simulateColourVision(input, "achromatopsia", options), + }; +} diff --git a/src/colour/composite.ts b/src/colour/composite.ts new file mode 100644 index 0000000..85f4379 --- /dev/null +++ b/src/colour/composite.ts @@ -0,0 +1,244 @@ +import { clamp, encodeSrgb, lineariseSrgb, srgbChannels } from "./internal"; +import { formatColour, toSrgbCss } from "./format"; +import { parseColour } from "./parse"; +import type { + BlendMode, + ColourCoordinates, + ColourInput, + ColourValue, + CompositeLayer, + CompositeOptions, + CompositeResult, + CompositingSpace, + SourceOverOptions, +} from "./types"; + +function luminosity([red, green, blue]: ColourCoordinates): number { + return 0.3 * red + 0.59 * green + 0.11 * blue; +} + +function saturation(channels: ColourCoordinates): number { + return Math.max(...channels) - Math.min(...channels); +} + +function clipColour(channels: ColourCoordinates): ColourCoordinates { + const lightness = luminosity(channels); + const minimum = Math.min(...channels); + const maximum = Math.max(...channels); + let output = [...channels] as ColourCoordinates; + if (minimum < 0) { + output = output.map( + (channel) => + lightness + ((channel - lightness) * lightness) / (lightness - minimum), + ) as ColourCoordinates; + } + if (maximum > 1) { + output = output.map( + (channel) => + lightness + + ((channel - lightness) * (1 - lightness)) / (maximum - lightness), + ) as ColourCoordinates; + } + return output; +} + +function setLuminosity( + channels: ColourCoordinates, + target: number, +): ColourCoordinates { + const difference = target - luminosity(channels); + return clipColour( + channels.map((channel) => channel + difference) as ColourCoordinates, + ); +} + +function setSaturation( + channels: ColourCoordinates, + target: number, +): ColourCoordinates { + const indices = [0, 1, 2] as const; + const sorted = [...indices].sort( + (first, second) => channels[first] - channels[second], + ); + const minimumIndex = sorted[0] ?? 0; + const middleIndex = sorted[1] ?? 1; + const maximumIndex = sorted[2] ?? 2; + const minimum = channels[minimumIndex]; + const maximum = channels[maximumIndex]; + const output: ColourCoordinates = [0, 0, 0]; + if (maximum > minimum) { + output[middleIndex] = + ((channels[middleIndex] - minimum) * target) / (maximum - minimum); + output[maximumIndex] = target; + } + output[minimumIndex] = 0; + return output; +} + +function softLight(backdrop: number, source: number): number { + if (source <= 0.5) + return backdrop - (1 - 2 * source) * backdrop * (1 - backdrop); + const d = + backdrop <= 0.25 + ? ((16 * backdrop - 12) * backdrop + 4) * backdrop + : Math.sqrt(backdrop); + return backdrop + (2 * source - 1) * (d - backdrop); +} + +function blendChannel( + backdrop: number, + source: number, + mode: BlendMode, +): number { + switch (mode) { + case "multiply": + return backdrop * source; + case "screen": + return backdrop + source - backdrop * source; + case "overlay": + return backdrop <= 0.5 + ? 2 * backdrop * source + : 1 - 2 * (1 - backdrop) * (1 - source); + case "darken": + return Math.min(backdrop, source); + case "lighten": + return Math.max(backdrop, source); + case "color-dodge": + return source >= 1 ? 1 : Math.min(1, backdrop / (1 - source)); + case "color-burn": + return source <= 0 ? 0 : 1 - Math.min(1, (1 - backdrop) / source); + case "hard-light": + return source <= 0.5 + ? 2 * backdrop * source + : 1 - 2 * (1 - backdrop) * (1 - source); + case "soft-light": + return softLight(backdrop, source); + case "difference": + return Math.abs(backdrop - source); + case "exclusion": + return backdrop + source - 2 * backdrop * source; + case "normal": + case "hue": + case "saturation": + case "color": + case "luminosity": + return source; + } +} + +function blend( + backdrop: ColourCoordinates, + source: ColourCoordinates, + mode: BlendMode, +): ColourCoordinates { + if (["hue", "saturation", "color", "luminosity"].includes(mode)) { + const nonSeparable = + mode === "hue" + ? setLuminosity( + setSaturation(source, saturation(backdrop)), + luminosity(backdrop), + ) + : mode === "saturation" + ? setLuminosity( + setSaturation(backdrop, saturation(source)), + luminosity(backdrop), + ) + : mode === "color" + ? setLuminosity(source, luminosity(backdrop)) + : setLuminosity(backdrop, luminosity(source)); + return nonSeparable.map((channel) => clamp(channel)) as ColourCoordinates; + } + return backdrop.map((channel, index) => + clamp(blendChannel(channel, source[index] ?? 0, mode)), + ) as ColourCoordinates; +} + +function channelsFor( + input: ColourInput, + space: CompositingSpace, +): ColourCoordinates { + const channels = srgbChannels(parseColour(input)); + return space === "linear-srgb" + ? (channels.map(lineariseSrgb) as ColourCoordinates) + : channels; +} + +function compositeValues( + source: ColourValue, + backdrop: ColourValue, + space: CompositingSpace, + mode: BlendMode, + opacity: number, +): ColourValue { + const sourceChannels = channelsFor(source, space); + const backdropChannels = channelsFor(backdrop, space); + const sourceAlpha = clamp(source.alpha * clamp(opacity)); + const backdropAlpha = clamp(backdrop.alpha); + const outputAlpha = sourceAlpha + backdropAlpha * (1 - sourceAlpha); + if (outputAlpha <= 0) return { space: "srgb", coords: [0, 0, 0], alpha: 0 }; + + const blended = blend(backdropChannels, sourceChannels, mode); + const output = sourceChannels.map((sourceChannel, index) => { + const backdropChannel = backdropChannels[index] ?? 0; + const blendChannelValue = blended[index] ?? sourceChannel; + const premultiplied = + sourceAlpha * (1 - backdropAlpha) * sourceChannel + + sourceAlpha * backdropAlpha * blendChannelValue + + (1 - sourceAlpha) * backdropAlpha * backdropChannel; + return premultiplied / outputAlpha; + }) as ColourCoordinates; + const encoded = + space === "linear-srgb" + ? (output.map(encodeSrgb) as ColourCoordinates) + : output; + return { + space: "srgb", + coords: encoded.map((channel) => clamp(channel)) as ColourCoordinates, + alpha: outputAlpha, + }; +} + +function result(colour: ColourValue): CompositeResult { + return { + colour, + css: toSrgbCss(colour), + hex: formatColour(colour, colour.alpha < 1 ? "hex8" : "hex"), + }; +} + +export function compositeSourceOver( + foreground: ColourInput, + background: ColourInput, + options: SourceOverOptions = {}, +): CompositeResult { + return result( + compositeValues( + parseColour(foreground), + parseColour(background), + options.space ?? "srgb", + options.blendMode ?? "normal", + options.opacity ?? 1, + ), + ); +} + +export function compositeLayers( + layers: readonly CompositeLayer[], + options: CompositeOptions = {}, +): CompositeResult { + if (layers.length === 0) + return result({ space: "srgb", coords: [0, 0, 0], alpha: 0 }); + const ordered = + options.order === "top-to-bottom" ? [...layers].reverse() : [...layers]; + let accumulator: ColourValue = { space: "srgb", coords: [0, 0, 0], alpha: 0 }; + for (const layer of ordered) { + accumulator = compositeValues( + parseColour(layer.colour), + accumulator, + options.space ?? "srgb", + layer.blendMode ?? "normal", + layer.opacity ?? 1, + ); + } + return result(accumulator); +} diff --git a/src/colour/format.ts b/src/colour/format.ts new file mode 100644 index 0000000..4002e12 --- /dev/null +++ b/src/colour/format.ts @@ -0,0 +1,268 @@ +import { + alphaText, + asColor, + clamp, + mappedColor, + numberText, + round, + srgbChannels, + toColourValue, +} from "./internal"; +import { parseColour } from "./parse"; +import type { + ColourFormat, + ColourInput, + ConversionRow, + FormatColourOptions, + SrgbPreview, +} from "./types"; + +const FORMAT_LABELS: Record = { + hex: "HEX", + hex8: "HEX + alpha", + rgb: "RGB", + rgba: "RGBA", + hsl: "HSL", + hsv: "HSV / HSB", + hwb: "HWB", + cmyk: "CMYK", + lab: "CIELAB", + lch: "CIELCH", + oklab: "OKLab", + oklch: "OKLCH", + p3: "Display P3", + rec2020: "Rec. 2020", + a98rgb: "Adobe RGB (1998)", + prophoto: "ProPhoto RGB", + "xyz-d50": "XYZ D50", + "xyz-d65": "XYZ D65", + css: "Original CSS", +}; + +export const DEFAULT_CONVERSION_FORMATS: readonly ColourFormat[] = [ + "hex", + "hex8", + "rgb", + "hsl", + "hsv", + "hwb", + "cmyk", + "lab", + "lch", + "oklab", + "oklch", + "p3", + "rec2020", +] as const; + +function byte(value: number): number { + return Math.round(clamp(value) * 255); +} + +function byteHex(value: number): string { + return byte(value).toString(16).padStart(2, "0"); +} + +function asHex(input: ColourInput, includeAlpha: boolean): string { + const value = parseColour(input); + const channels = srgbChannels(value); + const rgb = channels.map(byteHex).join(""); + return `#${rgb}${includeAlpha ? byteHex(value.alpha) : ""}`; +} + +function percent(value: number, precision: number): string { + return `${numberText(value, precision)}%`; +} + +function functional( + name: string, + coords: readonly string[], + alpha: number, + includeAlpha: boolean, +): string { + return `${name}(${coords.join(" ")}${includeAlpha && alpha < 1 ? ` / ${alphaText(alpha)}` : ""})`; +} + +function serialiseColorFunction( + space: string, + coords: readonly number[], + alpha: number, + precision: number, +): string { + return `color(${space} ${coords.map((coordinate) => numberText(coordinate, precision)).join(" ")}${ + alpha < 1 ? ` / ${alphaText(alpha)}` : "" + })`; +} + +export function formatColour( + input: ColourInput, + format: ColourFormat = "css", + options: FormatColourOptions = {}, +): string { + const precision = options.precision ?? 4; + const value = parseColour(input); + const alpha = value.alpha; + const includeAlpha = options.includeAlpha ?? alpha < 1; + + if (format === "hex") return asHex(value, false); + if (format === "hex8") return asHex(value, true); + + if (format === "rgb" || format === "rgba") { + const channels = srgbChannels(value, options.mapToSrgb ?? true).map(byte); + const forceAlpha = format === "rgba" || includeAlpha; + return forceAlpha + ? `rgba(${channels.join(", ")}, ${alphaText(alpha)})` + : `rgb(${channels.join(", ")})`; + } + + if (format === "cmyk") { + const [red, green, blue] = srgbChannels(value, options.mapToSrgb ?? true); + const black = 1 - Math.max(red, green, blue); + const denominator = 1 - black; + const cyan = denominator <= 1e-12 ? 0 : (1 - red - black) / denominator; + const magenta = + denominator <= 1e-12 ? 0 : (1 - green - black) / denominator; + const yellow = denominator <= 1e-12 ? 0 : (1 - blue - black) / denominator; + return functional( + "cmyk", + [cyan, magenta, yellow, black].map((channel) => + percent(channel * 100, precision), + ), + alpha, + includeAlpha, + ); + } + + const color = asColor(value); + if (format === "hsl" || format === "hsv" || format === "hwb") { + const converted = color.to(format); + const [hue, first, second] = converted.coords.map((coordinate) => + Number(coordinate ?? 0), + ); + return functional( + format, + [ + numberText(hue ?? 0, precision), + percent(first ?? 0, precision), + percent(second ?? 0, precision), + ], + alpha, + includeAlpha, + ); + } + + const space = + format === "p3" + ? "p3" + : format === "a98rgb" + ? "a98rgb" + : format === "prophoto" + ? "prophoto" + : format; + + if (format === "lab" || format === "lch") { + const converted = color.to(space); + const coords = converted.coords.map((coordinate) => + Number(coordinate ?? 0), + ); + return functional( + format, + [ + percent(coords[0] ?? 0, precision), + numberText(coords[1] ?? 0, precision), + numberText(coords[2] ?? 0, precision), + ], + alpha, + includeAlpha, + ); + } + if (format === "oklab" || format === "oklch") { + const converted = color.to(space); + const coords = converted.coords.map((coordinate) => + Number(coordinate ?? 0), + ); + return functional( + format, + [ + percent((coords[0] ?? 0) * 100, precision), + numberText(coords[1] ?? 0, precision), + numberText(coords[2] ?? 0, precision), + ], + alpha, + includeAlpha, + ); + } + if ( + ["p3", "rec2020", "a98rgb", "prophoto", "xyz-d50", "xyz-d65"].includes( + format, + ) + ) { + const converted = color.to(space); + const cssSpace = + format === "p3" + ? "display-p3" + : format === "a98rgb" + ? "a98-rgb" + : format === "prophoto" + ? "prophoto-rgb" + : format; + return serialiseColorFunction( + cssSpace, + converted.coords.map((coordinate) => + round(Number(coordinate ?? 0), precision), + ), + alpha, + precision, + ); + } + + if (format === "css") { + return color.toString({ precision, inGamut: options.mapToSrgb === true }); + } + + // Exhaustiveness safeguard for callers compiled against older declarations. + return color.toString({ precision, inGamut: false }); +} + +export function conversionRows( + input: ColourInput, + formats: readonly ColourFormat[] = DEFAULT_CONVERSION_FORMATS, +): ConversionRow[] { + return formats.map((format) => { + const value = formatColour(input, format); + return { + id: format, + label: FORMAT_LABELS[format], + value, + copyValue: value, + }; + }); +} + +export const formatRows = conversionRows; + +export function toSrgbPreview(input: ColourInput): SrgbPreview { + const value = parseColour(input); + const original = asColor(value); + const mapped = mappedColor(value, "srgb"); + const rgb = mapped.coords.map((coordinate) => + clamp(Number(coordinate ?? 0)), + ) as [number, number, number]; + const integerChannels = rgb.map(byte); + return { + css: + value.alpha < 1 + ? `rgb(${integerChannels.join(" ")} / ${alphaText(value.alpha)})` + : `rgb(${integerChannels.join(" ")})`, + hex: formatColour(toColourValue(mapped), value.alpha < 1 ? "hex8" : "hex", { + includeAlpha: value.alpha < 1, + }), + rgb, + alpha: value.alpha, + wasMapped: !original.inGamut("srgb"), + }; +} + +export function toSrgbCss(input: ColourInput): string { + return toSrgbPreview(input).css; +} diff --git a/src/colour/gamut.ts b/src/colour/gamut.ts new file mode 100644 index 0000000..df62efb --- /dev/null +++ b/src/colour/gamut.ts @@ -0,0 +1,65 @@ +import { asColor, toColourValue } from "./internal"; +import { parseColour } from "./parse"; +import { formatColour, toSrgbCss } from "./format"; +import type { + ColourInput, + ColourValue, + GamutMappingOptions, + GamutReport, + GamutSpaceReport, +} from "./types"; + +const GAMUT_LABELS: Readonly> = { + srgb: "sRGB", + p3: "Display P3", + rec2020: "Rec. 2020", + a98rgb: "Adobe RGB (1998)", + prophoto: "ProPhoto RGB", +}; + +export const DEFAULT_GAMUT_SPACES = ["srgb", "p3", "rec2020"] as const; + +export function mapToGamut( + input: ColourInput, + options: GamutMappingOptions = {}, +): ColourValue { + const target = options.target ?? "srgb"; + const method = options.method === "clip" ? "clip" : "oklch.c"; + const converted = asColor(parseColour(input)).to(target); + const mapped = converted.inGamut(target) + ? converted + : converted.toGamut({ space: target, method }); + return toColourValue(mapped.to(target)); +} + +function reportSpace(input: ColourValue, space: string): GamutSpaceReport { + const source = asColor(input); + const inGamut = source.inGamut(space); + const mapped = mapToGamut(input, { target: space }); + const format = + space === "p3" + ? "p3" + : space === "rec2020" + ? "rec2020" + : space === "a98rgb" + ? "a98rgb" + : space === "prophoto" + ? "prophoto" + : undefined; + return { + space, + label: GAMUT_LABELS[space] ?? space, + inGamut, + mapped, + mappedCss: format ? formatColour(mapped, format) : toSrgbCss(mapped), + deltaEOK: inGamut ? 0 : source.deltaEOK(asColor(mapped)), + }; +} + +export function gamutReport( + input: ColourInput, + spaces: readonly string[] = DEFAULT_GAMUT_SPACES, +): GamutReport { + const source = parseColour(input); + return { source, spaces: spaces.map((space) => reportSpace(source, space)) }; +} diff --git a/src/colour/index.ts b/src/colour/index.ts new file mode 100644 index 0000000..c121c8b --- /dev/null +++ b/src/colour/index.ts @@ -0,0 +1,48 @@ +export { + ColourParseError, + parseColour, + parseColourList, + tryParseColour, +} from "./parse"; +export { + DEFAULT_CONVERSION_FORMATS, + conversionRows, + formatColour, + formatRows, + toSrgbCss, + toSrgbPreview, +} from "./format"; +export { DEFAULT_GAMUT_SPACES, gamutReport, mapToGamut } from "./gamut"; +export { compositeLayers, compositeSourceOver } from "./composite"; +export { interpolateColourStops, interpolateStops } from "./interpolate"; +export { + contrastRatio, + contrastReport, + flattenColour, + nearestPassingColour, + relativeLuminance, +} from "./accessibility"; +export { + deltaE, + simulateColourVision, + simulateColourVisionSet, +} from "./compare"; +export { + colourHarmony, + exportCssVariables, + exportCsvPalette, + exportDesignTokens, + exportJsonPalette, + exportPalette, + exportScssVariables, + exportTailwindPalette, + generateHarmony, + normaliseTokenName, + parseNamedColourList, + parsePaletteList, + shades, + tints, + tones, +} from "./palette"; + +export type * from "./types"; diff --git a/src/colour/internal.ts b/src/colour/internal.ts new file mode 100644 index 0000000..ac0919a --- /dev/null +++ b/src/colour/internal.ts @@ -0,0 +1,113 @@ +import Color from "colorjs.io/dist/color.js"; + +import type { ColourCoordinates, ColourInput, ColourValue } from "./types"; + +export const EPSILON = 1e-9; + +export function clamp(value: number, minimum = 0, maximum = 1): number { + return Math.min(maximum, Math.max(minimum, value)); +} + +export function round(value: number, precision = 4): number { + const factor = 10 ** precision; + const result = Math.round((value + Number.EPSILON) * factor) / factor; + return Object.is(result, -0) ? 0 : result; +} + +export function numberText(value: number, precision = 4): string { + return String(round(value, precision)); +} + +export function alphaText(alpha: number, precision = 3): string { + return numberText(clamp(alpha), precision); +} + +export function finiteCoordinate(value: number | null | undefined): number { + return typeof value === "number" && Number.isFinite(value) ? value : 0; +} + +export function toColourValue(color: Color): ColourValue { + return { + space: color.spaceId, + coords: color.coords.map(finiteCoordinate) as ColourCoordinates, + alpha: clamp(Number.isFinite(color.alpha) ? color.alpha : 1), + }; +} + +export function isColourValue(value: unknown): value is ColourValue { + if (typeof value !== "object" || value === null) return false; + const candidate = value as Partial; + return ( + typeof candidate.space === "string" && + Array.isArray(candidate.coords) && + candidate.coords.length === 3 && + candidate.coords.every( + (coordinate) => + typeof coordinate === "number" && Number.isFinite(coordinate), + ) && + typeof candidate.alpha === "number" && + Number.isFinite(candidate.alpha) + ); +} + +export function asColor(input: ColourInput): Color { + if (typeof input === "string") return new Color(input); + return new Color(input.space, [...input.coords], clamp(input.alpha)); +} + +export function colorFrom( + space: string, + coords: readonly number[], + alpha = 1, +): Color { + return new Color( + space, + coords.map(finiteCoordinate) as ColourCoordinates, + clamp(alpha), + ); +} + +export function copyColour(value: ColourValue): ColourValue { + return { space: value.space, coords: [...value.coords], alpha: value.alpha }; +} + +export function mappedColor( + input: ColourInput, + target = "srgb", + method: "oklch.c" | "clip" = "oklch.c", +): Color { + const converted = asColor(input).to(target); + return converted.inGamut(target) + ? converted + : converted.toGamut({ space: target, method }); +} + +export function srgbChannels( + input: ColourInput, + map = true, +): ColourCoordinates { + const color = map ? mappedColor(input, "srgb") : asColor(input).to("srgb"); + return color.coords.map((coordinate) => + map ? clamp(finiteCoordinate(coordinate)) : finiteCoordinate(coordinate), + ) as ColourCoordinates; +} + +export function lineariseSrgb(channel: number): number { + return channel <= 0.04045 + ? channel / 12.92 + : ((channel + 0.055) / 1.055) ** 2.4; +} + +export function encodeSrgb(channel: number): number { + return channel <= 0.0031308 + ? 12.92 * channel + : 1.055 * channel ** (1 / 2.4) - 0.055; +} + +export function hueIndex(space: string): number | undefined { + if (["hsl", "hsv", "hwb", "okhsl", "okhsv"].includes(space)) return 0; + if (["lch", "oklch", "jzczhz", "hct"].includes(space)) return 2; + return undefined; +} + +export { Color }; diff --git a/src/colour/interpolate.ts b/src/colour/interpolate.ts new file mode 100644 index 0000000..f46651d --- /dev/null +++ b/src/colour/interpolate.ts @@ -0,0 +1,170 @@ +import { asColor, clamp, colorFrom, hueIndex, toColourValue } from "./internal"; +import { formatColour, toSrgbCss } from "./format"; +import { parseColour } from "./parse"; +import type { + ColourCoordinates, + ColourStop, + ColourValue, + EasingName, + HueInterpolation, + InterpolationOptions, + InterpolationStep, +} from "./types"; + +interface PositionedStop { + colour: ColourValue; + position: number; +} + +function ease(value: number, name: EasingName): number { + switch (name) { + case "ease-in": + return value * value; + case "ease-out": + return 1 - (1 - value) ** 2; + case "ease-in-out": + return value < 0.5 ? 2 * value * value : 1 - (-2 * value + 2) ** 2 / 2; + case "smoothstep": + return value * value * (3 - 2 * value); + case "linear": + return value; + } +} + +function normaliseStops( + stops: readonly ColourStop[], + shouldClamp: boolean, +): PositionedStop[] { + if (stops.length < 2) + throw new RangeError("At least two colour stops are required."); + const positions = stops.map((stop) => { + if (stop.position === undefined) return undefined; + if (!Number.isFinite(stop.position)) + throw new RangeError("Stop positions must be finite numbers."); + return shouldClamp ? clamp(stop.position) : stop.position; + }); + positions[0] ??= 0; + positions[positions.length - 1] ??= 1; + + let previousKnown = 0; + for (let index = 1; index < positions.length; index += 1) { + if (positions[index] === undefined) continue; + const start = positions[previousKnown] ?? 0; + const end = Math.max(start, positions[index] ?? start); + positions[index] = end; + const gap = index - previousKnown; + for (let missing = 1; missing < gap; missing += 1) { + positions[previousKnown + missing] = + start + ((end - start) * missing) / gap; + } + previousKnown = index; + } + + return stops.map((stop, index) => ({ + colour: parseColour(stop.colour), + position: positions[index] ?? 0, + })); +} + +function hueDelta(from: number, to: number, method: HueInterpolation): number { + if (method === "raw") return to - from; + const increasing = (((to - from) % 360) + 360) % 360; + const decreasing = increasing === 0 ? 0 : increasing - 360; + if (method === "increasing") return increasing; + if (method === "decreasing") return decreasing; + if (method === "shorter") return increasing <= 180 ? increasing : decreasing; + if (increasing === 0) return 360; + return increasing <= 180 ? decreasing : increasing; +} + +export function interpolateColourStops( + stops: readonly ColourStop[], + position: number, + options: InterpolationOptions = {}, +): ColourValue { + if (!Number.isFinite(position)) + throw new RangeError("Interpolation position must be finite."); + const shouldClamp = options.clamp ?? true; + const prepared = normaliseStops(stops, shouldClamp); + const sample = shouldClamp ? clamp(position) : position; + let left = prepared[0]; + let right = prepared[prepared.length - 1]; + if (!left || !right) + throw new RangeError("At least two colour stops are required."); + + if (sample <= left.position) right = prepared[1] ?? right; + else if (sample >= right.position) + left = prepared[prepared.length - 2] ?? left; + else { + for (let index = 1; index < prepared.length; index += 1) { + const candidate = prepared[index]; + if (candidate && sample <= candidate.position) { + left = prepared[index - 1] ?? left; + right = candidate; + break; + } + } + } + + const range = right.position - left.position; + const local = range === 0 ? 1 : (sample - left.position) / range; + const progress = ease( + shouldClamp ? clamp(local) : local, + options.easing ?? "linear", + ); + const space = options.space ?? "oklch"; + const leftColor = asColor(left.colour).to(space); + const rightColor = asColor(right.colour).to(space); + const leftCoords = leftColor.coords.map((coordinate) => + Number(coordinate ?? 0), + ) as ColourCoordinates; + const rightCoords = rightColor.coords.map((coordinate) => + Number(coordinate ?? 0), + ) as ColourCoordinates; + const hueCoordinate = hueIndex(space); + const alpha = + left.colour.alpha + (right.colour.alpha - left.colour.alpha) * progress; + const premultiplied = options.premultiplied ?? true; + const coords = leftCoords.map((coordinate, index) => { + if (index === hueCoordinate) { + return ( + coordinate + + hueDelta( + coordinate, + rightCoords[index] ?? coordinate, + options.hue ?? "shorter", + ) * + progress + ); + } + const rightCoordinate = rightCoords[index] ?? coordinate; + if (!premultiplied) { + return coordinate + (rightCoordinate - coordinate) * progress; + } + const mixed = + coordinate * left.colour.alpha * (1 - progress) + + rightCoordinate * right.colour.alpha * progress; + return alpha > 1e-12 ? mixed / alpha : 0; + }) as ColourCoordinates; + return toColourValue(colorFrom(space, coords, alpha)); +} + +export function interpolateStops( + stops: readonly ColourStop[], + count: number, + options: InterpolationOptions = {}, +): InterpolationStep[] { + if (!Number.isInteger(count) || count < 2 || count > 10_000) { + throw new RangeError("Step count must be an integer from 2 to 10,000."); + } + return Array.from({ length: count }, (_, index) => { + const position = index / (count - 1); + const colour = interpolateColourStops(stops, position, options); + return { + position, + colour, + css: toSrgbCss(colour), + hex: formatColour(colour, colour.alpha < 1 ? "hex8" : "hex"), + }; + }); +} diff --git a/src/colour/palette.ts b/src/colour/palette.ts new file mode 100644 index 0000000..0f8957c --- /dev/null +++ b/src/colour/palette.ts @@ -0,0 +1,332 @@ +import { asColor, clamp, colorFrom, toColourValue } from "./internal"; +import { formatColour } from "./format"; +import { mapToGamut } from "./gamut"; +import { interpolateColourStops } from "./interpolate"; +import { parseColour, tryParseColour } from "./parse"; +import type { + ColourFormat, + ColourInput, + ColourValue, + HarmonyType, + PaletteEntry, + PaletteExportFormat, + PaletteExportOptions, + PaletteScaleOptions, +} from "./types"; + +const HARMONY_OFFSETS: Record = { + complementary: [0, 180], + analogous: [-30, 0, 30], + "split-complementary": [0, 150, 210], + triadic: [0, 120, 240], + tetradic: [0, 60, 180, 240], + square: [0, 90, 180, 270], +}; + +export function colourHarmony( + input: ColourInput, + type: HarmonyType, +): ColourValue[] { + const source = parseColour(input); + const converted = asColor(source).to("oklch"); + const lightness = Number(converted.coords[0] ?? 0); + const chroma = Number(converted.coords[1] ?? 0); + const hue = Number(converted.coords[2] ?? 0); + return HARMONY_OFFSETS[type].map((offset) => + mapToGamut( + toColourValue( + colorFrom( + "oklch", + [lightness, chroma, (hue + offset + 360) % 360], + source.alpha, + ), + ), + ), + ); +} + +export const generateHarmony = colourHarmony; + +function scale( + input: ColourInput, + endpoint: ColourInput, + count: number, + options: PaletteScaleOptions, +): ColourValue[] { + if (!Number.isInteger(count) || count < 1 || count > 1_000) { + throw new RangeError("Palette count must be an integer from 1 to 1,000."); + } + const source = parseColour(input); + const values: ColourValue[] = options.includeBase ? [source] : []; + const includeEndpoint = options.includeEndpoint ?? false; + const denominator = includeEndpoint ? count : count + 1; + for (let index = 1; index <= count; index += 1) { + values.push( + interpolateColourStops( + [{ colour: source }, { colour: endpoint }], + index / denominator, + { space: options.space ?? "oklab" }, + ), + ); + } + return values; +} + +export function tints( + input: ColourInput, + count = 5, + options: PaletteScaleOptions = {}, +): ColourValue[] { + return scale(input, "#ffffff", count, options); +} + +export function shades( + input: ColourInput, + count = 5, + options: PaletteScaleOptions = {}, +): ColourValue[] { + return scale(input, "#000000", count, options); +} + +export function tones( + input: ColourInput, + count = 5, + options: PaletteScaleOptions = {}, +): ColourValue[] { + const source = parseColour(input); + const lightness = clamp(Number(asColor(source).to("oklab").coords[0] ?? 0)); + const neutral = toColourValue( + colorFrom("oklab", [lightness, 0, 0], source.alpha), + ); + return scale(source, neutral, count, options); +} + +function entryFromUnknown( + name: string, + value: unknown, +): PaletteEntry | undefined { + const candidate = + typeof value === "object" && value !== null + ? ((value as Record).$value ?? + (value as Record).value ?? + (value as Record).colour ?? + (value as Record).color) + : value; + if (typeof candidate !== "string") return undefined; + const result = tryParseColour(candidate); + return result.ok + ? { name, colour: result.value, source: candidate } + : undefined; +} + +function parseJsonPalette(input: string): PaletteEntry[] | undefined { + let parsed: unknown; + try { + parsed = JSON.parse(input) as unknown; + } catch { + return undefined; + } + if (Array.isArray(parsed)) { + return parsed.map((item, index) => { + if (typeof item === "string") + return { + name: `colour-${index + 1}`, + colour: parseColour(item), + source: item, + }; + if (typeof item !== "object" || item === null) + throw new TypeError(`Palette item ${index + 1} is invalid.`); + const record = item as Record; + const name = + typeof record.name === "string" ? record.name : `colour-${index + 1}`; + const entry = entryFromUnknown(name, record); + if (!entry) + throw new TypeError( + `Palette item “${name}” does not contain a valid colour.`, + ); + return entry; + }); + } + if (typeof parsed === "object" && parsed !== null) { + const entries: PaletteEntry[] = []; + const visit = (record: Record, path: string[]): void => { + for (const [name, value] of Object.entries(record)) { + if (name.startsWith("$")) continue; + const nextPath = [...path, name]; + const entry = entryFromUnknown(nextPath.join("-"), value); + if (entry) entries.push(entry); + else if ( + typeof value === "object" && + value !== null && + !Array.isArray(value) + ) { + visit(value as Record, nextPath); + } + } + }; + visit(parsed as Record, []); + if (entries.length > 0) return entries; + } + return undefined; +} + +export function parsePaletteList(input: string): PaletteEntry[] { + const trimmed = input.trim(); + if (!trimmed) return []; + const json = parseJsonPalette(trimmed); + if (json) return json; + + const cssBlock = trimmed.match(/^\s*(?::root|[^{}]+)\s*\{([\s\S]*)\}\s*$/); + const listText = cssBlock?.[1] ?? trimmed; + + const lines = listText + .split(/[\n;]/) + .map((line) => line.trim().replace(/,$/, "")) + .filter(Boolean); + return lines.map((line, index) => { + const whole = tryParseColour(line); + if (whole.ok) + return { name: `colour-${index + 1}`, colour: whole.value, source: line }; + const match = line.match(/^(--)?([^:=]+?)\s*[:=]\s*(.+)$/); + if (!match) + throw new TypeError(`Could not parse palette line ${index + 1}: ${line}`); + const name = (match[2] ?? `colour-${index + 1}`).trim(); + const source = (match[3] ?? "").trim(); + return { name, colour: parseColour(source), source }; + }); +} + +export const parseNamedColourList = parsePaletteList; + +export function normaliseTokenName(input: string): string { + const normalised = input + .normalize("NFKD") + .replace(/[\u0300-\u036f]/g, "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, "-") + .replace(/^-+|-+$/g, "") + .replace(/-{2,}/g, "-"); + return normalised || "colour"; +} + +function outputValue(entry: PaletteEntry, format: ColourFormat): string { + return formatColour( + entry.colour, + format === "hex" && entry.colour.alpha < 1 ? "hex8" : format, + ); +} + +function uniqueNames( + entries: readonly PaletteEntry[], +): Array<{ name: string; entry: PaletteEntry }> { + const used = new Map(); + return entries.map((entry) => { + const base = normaliseTokenName(entry.name); + const occurrence = (used.get(base) ?? 0) + 1; + used.set(base, occurrence); + return { name: occurrence === 1 ? base : `${base}-${occurrence}`, entry }; + }); +} + +export function exportCssVariables( + entries: readonly PaletteEntry[], + options: PaletteExportOptions = {}, +): string { + const prefix = normaliseTokenName(options.prefix ?? "colour"); + const lines = uniqueNames(entries).map( + ({ name, entry }) => + ` --${prefix}-${name}: ${outputValue(entry, options.format ?? "hex")};`, + ); + return `:root {\n${lines.join("\n")}\n}`; +} + +export function exportScssVariables( + entries: readonly PaletteEntry[], + options: PaletteExportOptions = {}, +): string { + const prefix = normaliseTokenName(options.prefix ?? "colour"); + return uniqueNames(entries) + .map( + ({ name, entry }) => + `$${prefix}-${name}: ${outputValue(entry, options.format ?? "hex")};`, + ) + .join("\n"); +} + +export function exportJsonPalette( + entries: readonly PaletteEntry[], + options: PaletteExportOptions = {}, +): string { + const value = Object.fromEntries( + uniqueNames(entries).map(({ name, entry }) => [ + name, + outputValue(entry, options.format ?? "hex"), + ]), + ); + return JSON.stringify(value, null, options.pretty === false ? undefined : 2); +} + +export function exportDesignTokens( + entries: readonly PaletteEntry[], + options: PaletteExportOptions = {}, +): string { + const value = Object.fromEntries( + uniqueNames(entries).map(({ name, entry }) => [ + name, + { $type: "color", $value: outputValue(entry, options.format ?? "hex") }, + ]), + ); + return JSON.stringify(value, null, options.pretty === false ? undefined : 2); +} + +export function exportTailwindPalette( + entries: readonly PaletteEntry[], + options: PaletteExportOptions = {}, +): string { + const value = Object.fromEntries( + uniqueNames(entries).map(({ name, entry }) => [ + name, + outputValue(entry, options.format ?? "hex"), + ]), + ); + return `export default ${JSON.stringify(value, null, options.pretty === false ? undefined : 2)};`; +} + +function csvCell(value: string): string { + return /[",\r\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value; +} + +export function exportCsvPalette( + entries: readonly PaletteEntry[], + options: PaletteExportOptions = {}, +): string { + return [ + "name,value", + ...uniqueNames(entries).map( + ({ name, entry }) => + `${csvCell(name)},${csvCell(outputValue(entry, options.format ?? "hex"))}`, + ), + ].join("\n"); +} + +export function exportPalette( + entries: readonly PaletteEntry[], + format: PaletteExportFormat = "css", + options: PaletteExportOptions = {}, +): string { + switch (format) { + case "css": + return exportCssVariables(entries, options); + case "scss": + return exportScssVariables(entries, options); + case "json": + return exportJsonPalette(entries, options); + case "tokens": + return exportDesignTokens(entries, options); + case "tailwind": + return exportTailwindPalette(entries, options); + case "csv": + return exportCsvPalette(entries, options); + } +} diff --git a/src/colour/parse.ts b/src/colour/parse.ts new file mode 100644 index 0000000..6578323 --- /dev/null +++ b/src/colour/parse.ts @@ -0,0 +1,320 @@ +import { + Color, + clamp, + copyColour, + isColourValue, + toColourValue, +} from "./internal"; +import type { + ColourInput, + ColourParseErrorCode, + ColourParseResult, + ColourValue, +} from "./types"; + +export class ColourParseError extends Error { + readonly code: ColourParseErrorCode; + readonly input: string; + + constructor(code: ColourParseErrorCode, message: string, input: string) { + super(message); + this.name = "ColourParseError"; + this.code = code; + this.input = input; + } +} + +function parseNumber(token: string, label: string, input: string): number { + const value = Number(token); + if (!Number.isFinite(value)) { + throw new ColourParseError( + "non-finite", + `${label} must be a finite number.`, + input, + ); + } + return value; +} + +function parseUnitInterval( + token: string, + label: string, + input: string, + allowHundred = true, +): number { + const trimmed = token.trim(); + const percentage = trimmed.endsWith("%"); + const value = parseNumber( + percentage ? trimmed.slice(0, -1) : trimmed, + label, + input, + ); + const normalised = + percentage || (allowHundred && value > 1) ? value / 100 : value; + if (normalised < 0 || normalised > 1) { + throw new ColourParseError( + "out-of-range", + `${label} must be between 0 and 100%.`, + input, + ); + } + return normalised; +} + +function parseAlpha(token: string | undefined, input: string): number { + return token === undefined + ? 1 + : parseUnitInterval(token, "Alpha", input, false); +} + +function parseAngle(token: string, input: string): number { + const match = token + .trim() + .match( + /^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)(deg|grad|rad|turn)?$/i, + ); + if (!match) + throw new ColourParseError( + "invalid-syntax", + "Hue must be a valid CSS angle.", + input, + ); + const value = parseNumber(match[1] ?? "", "Hue", input); + const unit = match[2]?.toLowerCase() ?? "deg"; + const degrees = + unit === "turn" + ? value * 360 + : unit === "rad" + ? (value * 180) / Math.PI + : unit === "grad" + ? value * 0.9 + : value; + return ((degrees % 360) + 360) % 360; +} + +function functionParts( + body: string, + expectedChannels = 3, +): { channels: string[]; alpha?: string } { + const slash = body.split("/"); + if (slash.length > 2) return { channels: [] }; + const channelText = slash[0]?.trim() ?? ""; + const commaParts = channelText.includes(",") + ? channelText.split(",").map((part) => part.trim()) + : channelText.split(/\s+/).filter(Boolean); + let alpha = slash[1]?.trim(); + if (commaParts.length === expectedChannels + 1 && alpha === undefined) + alpha = commaParts.pop(); + return { channels: commaParts, ...(alpha === undefined ? {} : { alpha }) }; +} + +function parseHsv(input: string): ColourValue | undefined { + const match = input.match(/^hs(?:v|b)a?\((.*)\)$/is); + if (!match) return undefined; + const parts = functionParts(match[1] ?? ""); + if (parts.channels.length !== 3) { + throw new ColourParseError( + "invalid-syntax", + "HSV needs hue, saturation and value channels.", + input, + ); + } + const hue = parseAngle(parts.channels[0] ?? "", input); + const saturation = + parseUnitInterval(parts.channels[1] ?? "", "Saturation", input) * 100; + const value = + parseUnitInterval(parts.channels[2] ?? "", "Value", input) * 100; + return { + space: "hsv", + coords: [hue, saturation, value], + alpha: parseAlpha(parts.alpha, input), + }; +} + +function parseCmyk(input: string): ColourValue | undefined { + const match = input.match(/^(?:device-)?cmyka?\((.*)\)$/is); + if (!match) return undefined; + const parts = functionParts(match[1] ?? "", 4); + if (parts.channels.length !== 4) { + throw new ColourParseError( + "invalid-syntax", + "CMYK needs cyan, magenta, yellow and black channels.", + input, + ); + } + const [cyan, magenta, yellow, black] = parts.channels.map((part, index) => + parseUnitInterval( + part, + ["Cyan", "Magenta", "Yellow", "Black"][index] ?? "Channel", + input, + ), + ); + const c = cyan ?? 0; + const m = magenta ?? 0; + const y = yellow ?? 0; + const k = black ?? 0; + return { + space: "srgb", + coords: [(1 - c) * (1 - k), (1 - m) * (1 - k), (1 - y) * (1 - k)], + alpha: parseAlpha(parts.alpha, input), + }; +} + +function parseBareRgb(input: string): ColourValue | undefined { + if (!/^\s*[+-]?(?:\d|\.)/.test(input) || !input.includes(",")) + return undefined; + const parts = input.split(",").map((part) => part.trim()); + if (parts.length < 3 || parts.length > 4 || parts.some((part) => part === "")) + return undefined; + const channels = parts.slice(0, 3).map((part, index) => { + const value = parseNumber( + part, + ["Red", "Green", "Blue"][index] ?? "Channel", + input, + ); + if (value < 0 || value > 255) { + throw new ColourParseError( + "out-of-range", + "RGB channels must be between 0 and 255.", + input, + ); + } + return value / 255; + }); + return { + space: "srgb", + coords: channels as [number, number, number], + alpha: parseAlpha(parts[3], input), + }; +} + +function normaliseStringInput(input: string): string { + const trimmed = input.trim(); + if (/^[\da-f]{3,8}$/i.test(trimmed) && [3, 4, 6, 8].includes(trimmed.length)) + return `#${trimmed}`; + if (/^0x[\da-f]{6,8}$/i.test(trimmed)) return `#${trimmed.slice(2)}`; + return trimmed; +} + +export function parseColour(input: ColourInput): ColourValue { + if (typeof input !== "string") { + if (!isColourValue(input)) { + throw new ColourParseError( + "invalid-syntax", + "Colour value is malformed.", + String(input), + ); + } + if (input.alpha < 0 || input.alpha > 1) { + throw new ColourParseError( + "out-of-range", + "Alpha must be between 0 and 1.", + JSON.stringify(input), + ); + } + try { + // Validate the public space identifier before returning a defensive copy. + new Color(input.space, [...input.coords], clamp(input.alpha)); + return copyColour(input); + } catch (error) { + throw new ColourParseError( + "unsupported-space", + error instanceof Error ? error.message : "Unsupported colour space.", + JSON.stringify(input), + ); + } + } + + const text = normaliseStringInput(input); + if (text === "") + throw new ColourParseError("empty", "Enter a colour.", input); + + const custom = parseHsv(text) ?? parseCmyk(text) ?? parseBareRgb(text); + if (custom) return custom; + + try { + return toColourValue(new Color(text)); + } catch (error) { + const detail = + error instanceof Error && error.message ? ` ${error.message}` : ""; + throw new ColourParseError( + "invalid-syntax", + `Could not parse “${text}”.${detail}`, + input, + ); + } +} + +export function tryParseColour(input: ColourInput): ColourParseResult { + try { + return { ok: true, value: parseColour(input) }; + } catch (error) { + if (error instanceof ColourParseError) { + return { + ok: false, + error: { code: error.code, message: error.message, input: error.input }, + }; + } + return { + ok: false, + error: { + code: "invalid-syntax", + message: "Could not parse this colour.", + input: String(input), + }, + }; + } +} + +function splitTopLevel(input: string): string[] { + const output: string[] = []; + let start = 0; + let depth = 0; + let quote = ""; + for (let index = 0; index < input.length; index += 1) { + const character = input[index] ?? ""; + if (quote) { + if (character === quote && input[index - 1] !== "\\") quote = ""; + continue; + } + if (character === '"' || character === "'") quote = character; + else if (character === "(") depth += 1; + else if (character === ")") depth = Math.max(0, depth - 1); + else if ( + depth === 0 && + (character === "\n" || character === ";" || character === ",") + ) { + const part = input.slice(start, index).trim(); + if (part) output.push(part); + start = index + 1; + } + } + const last = input.slice(start).trim(); + if (last) output.push(last); + return output; +} + +export function parseColourList(input: string): ColourValue[] { + const trimmed = input.trim(); + if (!trimmed) return []; + + const whole = tryParseColour(trimmed); + if (whole.ok) return [whole.value]; + + let values: unknown; + try { + values = JSON.parse(trimmed) as unknown; + } catch { + values = undefined; + } + if (Array.isArray(values)) { + return values.map((value) => + parseColour(typeof value === "string" ? value : (value as ColourValue)), + ); + } + + const parts = splitTopLevel(trimmed); + if (parts.length <= 1) + throw new ColourParseError(whole.error.code, whole.error.message, input); + return parts.map(parseColour); +} diff --git a/src/colour/types.ts b/src/colour/types.ts new file mode 100644 index 0000000..d1bc0da --- /dev/null +++ b/src/colour/types.ts @@ -0,0 +1,249 @@ +export type ColourCoordinates = [number, number, number]; + +/** A serialisable colour value. Coordinates use Color.js space conventions. */ +export interface ColourValue { + space: string; + coords: ColourCoordinates; + alpha: number; +} + +export type ColourInput = string | ColourValue; + +export type ColourParseErrorCode = + | "empty" + | "invalid-syntax" + | "unsupported-space" + | "non-finite" + | "out-of-range"; + +export interface ColourParseFailure { + ok: false; + error: { + code: ColourParseErrorCode; + message: string; + input: string; + }; +} + +export interface ColourParseSuccess { + ok: true; + value: ColourValue; +} + +export type ColourParseResult = ColourParseSuccess | ColourParseFailure; + +export type ColourFormat = + | "hex" + | "hex8" + | "rgb" + | "rgba" + | "hsl" + | "hsv" + | "hwb" + | "cmyk" + | "lab" + | "lch" + | "oklab" + | "oklch" + | "p3" + | "rec2020" + | "a98rgb" + | "prophoto" + | "xyz-d50" + | "xyz-d65" + | "css"; + +export interface FormatColourOptions { + precision?: number; + mapToSrgb?: boolean; + includeAlpha?: boolean; +} + +export interface ConversionRow { + id: ColourFormat; + label: string; + value: string; + copyValue: string; +} + +export interface SrgbPreview { + css: string; + hex: string; + rgb: ColourCoordinates; + alpha: number; + wasMapped: boolean; +} + +export type GamutMappingMethod = "oklch-chroma" | "clip"; + +export interface GamutMappingOptions { + target?: string; + method?: GamutMappingMethod; +} + +export interface GamutSpaceReport { + space: string; + label: string; + inGamut: boolean; + mapped: ColourValue; + mappedCss: string; + deltaEOK: number; +} + +export interface GamutReport { + source: ColourValue; + spaces: GamutSpaceReport[]; +} + +export type BlendMode = + | "normal" + | "multiply" + | "screen" + | "overlay" + | "darken" + | "lighten" + | "color-dodge" + | "color-burn" + | "hard-light" + | "soft-light" + | "difference" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + +export type CompositingSpace = "srgb" | "linear-srgb"; + +export interface CompositeLayer { + colour: ColourInput; + opacity?: number; + blendMode?: BlendMode; + name?: string; +} + +export interface CompositeOptions { + space?: CompositingSpace; + /** Array order. The default models a paint stack from its background upwards. */ + order?: "bottom-to-top" | "top-to-bottom"; +} + +export interface SourceOverOptions { + space?: CompositingSpace; + blendMode?: BlendMode; + /** Additional source opacity, multiplied with its own alpha. */ + opacity?: number; +} + +export interface CompositeResult { + colour: ColourValue; + css: string; + hex: string; +} + +export interface ColourStop { + colour: ColourInput; + /** Normalised position. Missing positions are distributed like a CSS gradient. */ + position?: number; + name?: string; +} + +export type InterpolationSpace = + | "srgb" + | "srgb-linear" + | "hsl" + | "hsv" + | "lab" + | "lch" + | "oklab" + | "oklch" + | "p3"; + +export type HueInterpolation = + "shorter" | "longer" | "increasing" | "decreasing" | "raw"; +export type EasingName = + "linear" | "ease-in" | "ease-out" | "ease-in-out" | "smoothstep"; + +export interface InterpolationOptions { + space?: InterpolationSpace; + hue?: HueInterpolation; + easing?: EasingName; + clamp?: boolean; + /** Interpolate premultiplied components to avoid transparent-colour fringes. Defaults to true. */ + premultiplied?: boolean; +} + +export interface InterpolationStep { + position: number; + colour: ColourValue; + css: string; + hex: string; +} + +export interface ContrastOptions { + /** Canvas below a translucent background. Defaults to white. */ + canvas?: ColourInput; +} + +export interface ContrastReport { + ratio: number; + foreground: ColourValue; + background: ColourValue; + flattenedForeground: ColourValue; + flattenedBackground: ColourValue; + passes: { + aaLarge: boolean; + aaaLarge: boolean; + aaNormal: boolean; + aaaNormal: boolean; + nonText: boolean; + }; +} + +export interface ContrastSuggestion { + colour: ColourValue; + css: string; + hex: string; + ratio: number; + deltaEOK: number; + direction: "lighter" | "darker"; +} + +export type DeltaEMethod = "76" | "cmc" | "2000" | "ok" | "itp" | "jz"; + +export type ColourVisionDeficiency = + "protanopia" | "deuteranopia" | "tritanopia" | "achromatopsia"; + +export interface ColourVisionOptions { + severity?: number; + mapToSrgb?: boolean; +} + +export type HarmonyType = + | "complementary" + | "analogous" + | "split-complementary" + | "triadic" + | "tetradic" + | "square"; + +export interface PaletteEntry { + name: string; + colour: ColourValue; + source?: string; +} + +export type PaletteExportFormat = + "css" | "scss" | "json" | "tokens" | "tailwind" | "csv"; + +export interface PaletteExportOptions { + format?: ColourFormat; + prefix?: string; + pretty?: boolean; +} + +export interface PaletteScaleOptions { + includeBase?: boolean; + includeEndpoint?: boolean; + space?: InterpolationSpace; +} diff --git a/src/components/AppErrorBoundary.tsx b/src/components/AppErrorBoundary.tsx new file mode 100644 index 0000000..6d0a474 --- /dev/null +++ b/src/components/AppErrorBoundary.tsx @@ -0,0 +1,35 @@ +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("Colour Tools encountered an interface error", error, info); + } + + render(): ReactNode { + if (!this.state.error) return this.props.children; + + return ( +
+

Colour Tools could not continue

+

{this.state.error.message}

+ +
+ ); + } +} diff --git a/src/components/ColourField.tsx b/src/components/ColourField.tsx new file mode 100644 index 0000000..e9f789e --- /dev/null +++ b/src/components/ColourField.tsx @@ -0,0 +1,53 @@ +import { toSrgbPreview, tryParseColour } from "../colour"; + +interface ColourFieldProps { + id: string; + label: string; + value: string; + onChange: (value: string) => void; + description?: string; + compact?: boolean; +} + +export function ColourField({ + id, + label, + value, + onChange, + description, + compact, +}: ColourFieldProps) { + const result = tryParseColour(value); + const preview = result.ok ? toSrgbPreview(result.value).css : "transparent"; + const errorId = `${id}-error`; + + return ( +
+ + + + {description && {description}} + {!result.ok && ( + + {result.error.message} + + )} +
+ ); +} diff --git a/src/components/CompositeLab.tsx b/src/components/CompositeLab.tsx new file mode 100644 index 0000000..eab36ba --- /dev/null +++ b/src/components/CompositeLab.tsx @@ -0,0 +1,401 @@ +import { useMemo, useState, type DragEvent } from "react"; +import { + compositeLayers, + toSrgbPreview, + tryParseColour, + type BlendMode, + type CompositingSpace, +} from "../colour"; +import { ColourField } from "./ColourField"; +import { CopyButton } from "./CopyButton"; + +interface LayerState { + id: string; + name: string; + colour: string; + opacity: number; + blendMode: BlendMode; + enabled: boolean; +} + +const BLEND_MODES: BlendMode[] = [ + "normal", + "multiply", + "screen", + "overlay", + "darken", + "lighten", + "color-dodge", + "color-burn", + "hard-light", + "soft-light", + "difference", + "exclusion", + "hue", + "saturation", + "color", + "luminosity", +]; + +let layerSequence = 4; +const makeLayer = ( + colour = "rgb(80 110 255 / 55%)", + name?: string, +): LayerState => ({ + id: `layer-${layerSequence++}`, + name: name ?? `Layer ${layerSequence - 1}`, + colour, + opacity: 1, + blendMode: "normal", + enabled: true, +}); + +export function CompositeLab({ + onAddColour, +}: { + onAddColour: (colour: string, name?: string) => void; +}) { + const [layers, setLayers] = useState([ + { ...makeLayer("rgb(240 52 89 / 65%)", "Rose"), id: "layer-1" }, + { ...makeLayer("rgb(36 128 255 / 55%)", "Blue"), id: "layer-2" }, + { ...makeLayer("rgb(250 196 65 / 45%)", "Gold"), id: "layer-3" }, + ]); + const [matte, setMatte] = useState("#ffffff"); + const [flatten, setFlatten] = useState(true); + const [space, setSpace] = useState("srgb"); + const [dragging, setDragging] = useState(null); + + const calculation = useMemo(() => { + const inputs = layers + .filter((layer) => layer.enabled) + .map((layer) => ({ + colour: layer.colour, + opacity: layer.opacity, + blendMode: layer.blendMode, + name: layer.name, + })); + if (flatten) + inputs.push({ + colour: matte, + opacity: 1, + blendMode: "normal", + name: "Matte", + }); + if (inputs.length === 0) + return { error: "Enable at least one layer.", result: null }; + if (inputs.some((layer) => !tryParseColour(layer.colour).ok)) { + return { + error: "Finish editing invalid layer colours to recalculate.", + result: null, + }; + } + try { + return { + error: null, + result: compositeLayers(inputs, { order: "top-to-bottom", space }), + }; + } catch (error) { + return { + error: + error instanceof Error + ? error.message + : "Could not composite these layers.", + result: null, + }; + } + }, [flatten, layers, matte, space]); + + function updateLayer(id: string, update: Partial) { + setLayers((current) => + current.map((layer) => + layer.id === id ? { ...layer, ...update } : layer, + ), + ); + } + + function moveLayer(from: number, to: number) { + if (to < 0 || to >= layers.length || from === to) return; + setLayers((current) => { + const copy = [...current]; + const [item] = copy.splice(from, 1); + if (!item) return current; + copy.splice(to, 0, item); + return copy; + }); + } + + function drop(event: DragEvent, index: number) { + event.preventDefault(); + if (dragging !== null) moveLayer(dragging, index); + setDragging(null); + } + + const preview = calculation.result + ? toSrgbPreview(calculation.result.colour) + : null; + + return ( +
+
+
+

Source-over, from two to many

+

Composite translucent colours

+

+ The first row is the topmost layer. Reorder, blend and flatten an + arbitrary RGBA stack to an opaque RGB result. +

+
+ +
+ +
+
+
+
+

Layer stack

+

+ Top to bottom · alpha in the colour and layer opacity are + multiplied. +

+
+ +
+
    + {layers.map((layer, index) => { + const parsed = tryParseColour(layer.colour); + const layerPreview = parsed.ok + ? toSrgbPreview(parsed.value).css + : "transparent"; + return ( +
  1. { + setDragging(index); + event.dataTransfer.effectAllowed = "move"; + }} + onDragEnd={() => setDragging(null)} + onDragOver={(event) => event.preventDefault()} + onDrop={(event) => drop(event, index)} + > + + +
    + + updateLayer(layer.id, { colour })} + compact + /> + + +
    +
    + + + + + +
    +
  2. + ); + })} +
+
+ + {flatten && ( + + )} +
+
+ + +
+
+ ); +} diff --git a/src/components/ContrastLab.tsx b/src/components/ContrastLab.tsx new file mode 100644 index 0000000..7dd19be --- /dev/null +++ b/src/components/ContrastLab.tsx @@ -0,0 +1,406 @@ +import { useMemo, useState } from "react"; +import { + contrastReport, + deltaE, + gamutReport, + nearestPassingColour, + simulateColourVision, + toSrgbPreview, + tryParseColour, + type ColourVisionDeficiency, + type DeltaEMethod, +} from "../colour"; +import { ColourField } from "./ColourField"; +import { CopyButton } from "./CopyButton"; + +const DEFICIENCIES: Array<{ id: ColourVisionDeficiency; label: string }> = [ + { id: "protanopia", label: "Protanopia" }, + { id: "deuteranopia", label: "Deuteranopia" }, + { id: "tritanopia", label: "Tritanopia" }, + { id: "achromatopsia", label: "Achromatopsia" }, +]; + +export function ContrastLab({ + onAddColour, +}: { + onAddColour: (colour: string, name?: string) => void; +}) { + const [foreground, setForeground] = useState("#2d2859"); + const [background, setBackground] = useState("#f8f5ee"); + const [canvas, setCanvas] = useState("#ffffff"); + const [target, setTarget] = useState(4.5); + const [method, setMethod] = useState("2000"); + const [severity, setSeverity] = useState(1); + + const analysis = useMemo(() => { + if ( + ![foreground, background, canvas].every( + (value) => tryParseColour(value).ok, + ) + ) + return null; + try { + const report = contrastReport(foreground, background, { canvas }); + const suggestion = nearestPassingColour(foreground, background, target, { + canvas, + }); + return { + report, + suggestion, + difference: deltaE(foreground, background, method), + foregroundPreview: toSrgbPreview(report.flattenedForeground), + backgroundPreview: toSrgbPreview(report.flattenedBackground), + gamut: gamutReport(foreground), + simulations: DEFICIENCIES.map((item) => ({ + ...item, + foreground: toSrgbPreview( + simulateColourVision(foreground, item.id, { severity }), + ).css, + background: toSrgbPreview( + simulateColourVision(background, item.id, { severity }), + ).css, + })), + }; + } catch { + return null; + } + }, [background, canvas, foreground, method, severity, target]); + + return ( +
+
+
+

Readable, distinguishable, in gamut

+

Contrast & compare

+

+ Flatten transparency over a real canvas, test WCAG 2.x ratios and + compare perceptual distance. Simulations are useful approximations, + not a diagnosis or substitute for user testing. +

+
+ +
+ +
+
+ + + + + {analysis?.suggestion && ( +
+ +
+ Nearest passing foreground + {analysis.suggestion.hex} + + {analysis.suggestion.ratio.toFixed(2)}:1 · ΔE OK{" "} + {analysis.suggestion.deltaEOK.toFixed(3)} + +
+ + +
+ )} + {analysis && !analysis.suggestion && ( +

+ No foreground at the current alpha can reach this target. Increase + its opacity or change the background. +

+ )} +
+ +
+ {analysis ? ( + <> +
+ Aa + Colour should carry meaning beautifully—not alone. + +
+
+
+ {analysis.report.ratio.toFixed(2)} + : 1 contrast +
+ + {analysis.report.passes.aaNormal + ? "AA normal passes" + : "AA normal fails"} + +
+
+ + + + + +
+

+ WCAG 2.x defines large text as at least 18pt, or 14pt bold. + Check states, focus, disabled controls and non-colour cues + separately. +

+ + ) : ( +

+ Enter three valid colours to run the checks. +

+ )} +
+
+ + {analysis && ( +
+
+
+
+

Perceptual difference

+

Different formulas answer different historical needs.

+
+ +
+
+ {analysis.difference.toFixed(3)} + ΔE {method} +
+

+ There is no universal “just noticeable” number across formulas, + media, surrounds and observers. Use this as a comparison, not a + pass/fail guarantee. +

+
+

Foreground gamut

+
+ {analysis.gamut.spaces.map((space) => ( + + {space.label} {space.inGamut ? "✓" : "↘"} + + ))} +
+
+
+ +
+
+
+

Colour-vision preview

+

Full-severity matrix approximation in linear sRGB.

+
+ +
+
+ {analysis.simulations.map((simulation) => ( +
+
+ Aa +
+ {simulation.label} +
+ ))} +
+
+
+ )} + + {analysis && ( +
+
+
+

Quick contrast matrix

+

Current colours against each other, black and white.

+
+
+
+ {[foreground, background, "#000000", "#ffffff"].map((fg, row) => ( +
+ {[foreground, background, "#000000", "#ffffff"].map( + (bg, column) => { + const valid = fg !== bg; + let ratio = 1; + if (valid) ratio = contrastReport(fg, bg, { canvas }).ratio; + return ( +
+ {ratio.toFixed(1)} + + {valid + ? ratio >= 4.5 + ? "AA" + : ratio >= 3 + ? "Large" + : "Fail" + : "Same"} + +
+ ); + }, + )} +
+ ))} +
+
+ + +
+
+ )} +
+ ); +} + +function Pass({ + label, + threshold, + pass, +}: { + label: string; + threshold: string; + pass: boolean; +}) { + return ( +
+ +
+ {label} + {threshold} +
+
+ ); +} diff --git a/src/components/ConverterLab.tsx b/src/components/ConverterLab.tsx new file mode 100644 index 0000000..3cd736b --- /dev/null +++ b/src/components/ConverterLab.tsx @@ -0,0 +1,327 @@ +import { useMemo, useState } from "react"; +import { + conversionRows, + gamutReport, + toSrgbPreview, + tryParseColour, + type ColourFormat, + type ColourValue, +} from "../colour"; +import { ColourField } from "./ColourField"; +import { CopyButton } from "./CopyButton"; + +const EXAMPLES = [ + "#f8f5ee", + "rgb(21 96 189 / 72%)", + "oklch(68% 0.18 264 / .9)", + "color(display-p3 0.91 0.24 0.42)", +]; + +const CONVERSION_FORMATS: readonly ColourFormat[] = [ + "hex", + "hex8", + "rgb", + "rgba", + "hsl", + "hsv", + "hwb", + "cmyk", + "lab", + "lch", + "oklab", + "oklch", + "p3", + "rec2020", + "a98rgb", + "prophoto", + "xyz-d50", + "xyz-d65", +]; + +export function ConverterLab({ + onAddColour, +}: { + onAddColour: (colour: string, name?: string) => void; +}) { + const [input, setInput] = useState("#f8f5ee"); + const [precision, setPrecision] = useState(4); + const parsed = useMemo(() => tryParseColour(input), [input]); + const [lastValid, setLastValid] = useState(() => + parsed.ok ? parsed.value : null, + ); + + function updateInput(value: string) { + setInput(value); + const result = tryParseColour(value); + if (result.ok) setLastValid(result.value); + } + + const rows = useMemo( + () => + lastValid + ? conversionRows(lastValid, CONVERSION_FORMATS).map((row) => ({ + ...row, + })) + : [], + [lastValid], + ); + const preview = lastValid ? toSrgbPreview(lastValid) : null; + const gamut = lastValid ? gamutReport(lastValid) : null; + + return ( +
+
+
+

One colour, many representations

+

Convert colours

+

+ Parse CSS colour syntax without discarding wide-gamut coordinates. + The large preview is mapped only for this display. +

+
+ +
+ +
+
+ +
+ {EXAMPLES.map((example) => ( + + ))} +
+ + {preview && ( +
+
+ + {preview.wasMapped + ? "Mapped to sRGB for preview" + : "In sRGB gamut"} + +
+ )} + + {preview && ( +
+ + +
+ )} + + {gamut && ( +
+

Gamut

+
+ {gamut.spaces.map((space) => ( + + {space.label} {space.inGamut ? "✓" : "↘"} + + ))} +
+

+ ↘ means the source is outside that gamut. Values below remain + unmodified; each mapped value is labelled and copyable. +

+ {gamut.spaces.some((space) => !space.inGamut) && ( +
+ {gamut.spaces + .filter((space) => !space.inGamut) + .map((space) => ( +
+ {space.label} mapped + {space.mappedCss} + +
+ ))} +
+ )} +
+ )} +
+ +
+
+
+

Representations

+

Copy-ready CSS and developer formats.

+
+
+
+ {rows.map((row) => { + const rendered = row.value.replace(/(-?\d+\.\d{2,})/g, (number) => + Number(number) + .toFixed(precision) + .replace(/0+$/, "") + .replace(/\.$/, ""), + ); + return ( +
+ {row.label} + {rendered} + +
+ ); + })} +
+ {!parsed.ok && lastValid && ( +

+ Keeping the last valid conversion while you finish editing. +

+ )} +
+
+ + +
+ ); +} + +function BatchConverter({ + onAddColour, +}: { + onAddColour: (colour: string, name?: string) => void; +}) { + const [source, setSource] = useState( + ["#264653", "rgb(42 157 143)", "oklch(78% 0.15 85)", "rebeccapurple"].join( + "\n", + ), + ); + const results = useMemo( + () => + source + .split(/\r?\n/) + .map((line, index) => ({ line: line.trim(), index })) + .filter(({ line }) => line.length > 0) + .map(({ line, index }) => ({ + line, + index, + result: tryParseColour(line), + })), + [source], + ); + const valid = results.filter((item) => item.result.ok); + const exportText = valid + .map((item) => { + if (!item.result.ok) return ""; + const preview = toSrgbPreview(item.result.value); + return `${item.line}\t${preview.hex}\t${preview.css}`; + }) + .join("\n"); + + return ( +
+
+
+

Batch

+

Convert a colour list

+

+ One CSS colour per line. Invalid lines are isolated, not discarded + silently. +

+
+
+ + +
+
+
+