-
**Img2Num** converts photos into printable, browser-colourable **colour-by-number** templates using a fast WebAssembly (C++) image pipeline.
> A fast, offline, serverless application that runs at near-native speeds, enabling in-browser colouring or printing of the image.
@@ -69,12 +65,12 @@
### What are you waiting for?
+
Try it out now by [clicking here](https://ryan-millard.github.io/Img2Num/)!
## What this repository contains (short)
-


@@ -84,10 +80,8 @@ Try it out now by [clicking here](https://ryan-millard.github.io/Img2Num/)!


-
-


@@ -97,12 +91,10 @@ Try it out now by [clicking here](https://ryan-millard.github.io/Img2Num/)!


-
-
-* A React frontend that handles image input, preview and in-browser colouring.
-* A WebAssembly module (C++ β Emscripten) that performs image processing and colour quantisation.
+- A React frontend that handles image input, preview and in-browser colouring.
+- A WebAssembly module (C++ β Emscripten) that performs image processing and colour quantisation.
This README is intentionally short β full installation steps, guides and references live in the docs site (see **Essential links** below).
@@ -110,11 +102,11 @@ This README is intentionally short β full installation steps, guides and refer
Visit the docs site for full guides, API references and troubleshooting:
-* Quick start - [https://ryan-millard.github.io/Img2Num/info/docs/getting-started/](https://ryan-millard.github.io/Img2Num/info/docs/introduction/getting-started)
-* Guidelines - [https://ryan-millard.github.io/Img2Num/info/docs/category/-guidelines/](https://ryan-millard.github.io/Img2Num/info/docs/category/-guidelines)
-* Documentation - [https://ryan-millard.github.io/Img2Num/info/docs/](https://ryan-millard.github.io/Img2Num/info/docs/)
-* Reference & Advanced Guides - [https://ryan-millard.github.io/Img2Num/info/docs/reference/](https://ryan-millard.github.io/Img2Num/info/docs/reference/)
-* Changelog - [https://ryan-millard.github.io/Img2Num/info/docs/changelog/](https://ryan-millard.github.io/Img2Num/info/changelog)
+- Quick start - [https://ryan-millard.github.io/Img2Num/info/docs/getting-started/](https://ryan-millard.github.io/Img2Num/info/docs/introduction/getting-started)
+- Guidelines - [https://ryan-millard.github.io/Img2Num/info/docs/category/-guidelines/](https://ryan-millard.github.io/Img2Num/info/docs/category/-guidelines)
+- Documentation - [https://ryan-millard.github.io/Img2Num/info/docs/](https://ryan-millard.github.io/Img2Num/info/docs/)
+- Reference & Advanced Guides - [https://ryan-millard.github.io/Img2Num/info/docs/reference/](https://ryan-millard.github.io/Img2Num/info/docs/reference/)
+- Changelog - [https://ryan-millard.github.io/Img2Num/info/docs/changelog/](https://ryan-millard.github.io/Img2Num/info/changelog)
(These replace long, duplicate instructions in this README to keep maintenance easier.)
@@ -124,9 +116,9 @@ We welcome contributions. Please read [CONTRIBUTING.md](https://ryan-millard.git
**A few important points:**
-* **Add tests** with your PR β new features and bug fixes **must** include tests where appropriate. PRs without tests are unlikely to be approved.
-* Follow the repository's [coding style rules](https://ryan-millard.github.io/Img2Num/info/docs/guidelines/coding-style) and [commit message rules](https://ryan-millard.github.io/Img2Num/info/docs/guidelines/commits).
-* Use the issue and PR templates when filing issues or submitting code. Your PR will be rejected if you don't.
+- **Add tests** with your PR β new features and bug fixes **must** include tests where appropriate. PRs without tests are unlikely to be approved.
+- Follow the repository's [coding style rules](https://ryan-millard.github.io/Img2Num/info/docs/guidelines/coding-style) and [commit message rules](https://ryan-millard.github.io/Img2Num/info/docs/guidelines/commits).
+- Use the issue and PR templates when filing issues or submitting code. Your PR will be rejected if you don't.
If you're unsure what to change, open an issue first and we can discuss scope.
@@ -136,18 +128,19 @@ If you're unsure what to change, open an issue first and we can discuss scope.
## What we intentionally keep out of this README
-* Long, stepβbyβstep build instructions (moved to the [docs site](https://ryan-millard.github.io/Img2Num/info/docs/))
-* Full API reference (moved to the [docs site](https://ryan-millard.github.io/Img2Num/info/docs/reference))
-* Very large images or heavy explanations β use the [docs site](https://ryan-millard.github.io/Img2Num/info/docs/) for in-depth content
+- Long, stepβbyβstep build instructions (moved to the [docs site](https://ryan-millard.github.io/Img2Num/info/docs/))
+- Full API reference (moved to the [docs site](https://ryan-millard.github.io/Img2Num/info/docs/reference))
+- Very large images or heavy explanations β use the [docs site](https://ryan-millard.github.io/Img2Num/info/docs/) for in-depth content
## Can't find something?
+
Hopefully you understand by now that if you need something, it should be on the [docs site](https://ryan-millard.github.io/Img2Num/info/docs/).
If it isn't, please open a ["New Feature" issue](https://github.com/Ryan-Millard/Img2Num/issues/new?template=feature_request.yml) to request its addition to the [docs site](https://ryan-millard.github.io/Img2Num/info/docs/).
## Maintainers
-* [Ryan](https://github.com/Ryan-Millard/)
-* [Hayden](https://github.com/hjmillard/) (temporarily unavailable)
+- [Ryan](https://github.com/Ryan-Millard/)
+- [Hayden](https://github.com/hjmillard/) (temporarily unavailable)
> β οΈ **Disclaimer:** Pull request reviews may take some time as we try to keep up with contributions.
> We highly encourage everyone to review each other's pull requests where possible β this helps the project move faster and benefits all contributors in the long run. Thank you for your support!
diff --git a/docker-compose.yml b/docker-compose.yml
index da81d0e6c..1dd6e0c40 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,12 +11,12 @@ services:
- node_modules:/usr/src/app/node_modules
- docs_node_modules:/usr/src/app/docs/node_modules
environment:
- CHOKIDAR_USEPOLLING: "true"
- CHOKIDAR_INTERVAL: "100"
+ CHOKIDAR_USEPOLLING: 'true'
+ CHOKIDAR_INTERVAL: '100'
ports:
- - "5173:5173" # Vite dev
- - "4173:4173" # Vite preview
- - "3000:3000" # Docusaurus dev
+ - '5173:5173' # Vite dev
+ - '4173:4173' # Vite preview
+ - '3000:3000' # Docusaurus dev
stdin_open: true
tty: true
diff --git a/docs/changelogSidebarGenerator.js b/docs/changelogSidebarGenerator.js
index fec8c5ef6..652b75d2b 100644
--- a/docs/changelogSidebarGenerator.js
+++ b/docs/changelogSidebarGenerator.js
@@ -4,10 +4,7 @@
* 1. By date (descending)
* 2. By semver version (descending)
*/
-export async function changelogSidebarGenerator({
- defaultSidebarItemsGenerator,
- ...args
-}) {
+export async function changelogSidebarGenerator({ defaultSidebarItemsGenerator, ...args }) {
const items = await defaultSidebarItemsGenerator(args);
const isReleaseDoc = (item) =>
@@ -28,9 +25,7 @@ export async function changelogSidebarGenerator({
const [dateStr, versionStr] = filename.replace(/\.md$/, '').split('_');
const date = new Date(dateStr).getTime() || 0;
- const [major = 0, minor = 0, patch = 0] = (versionStr || '0.0.0')
- .split('.')
- .map((n) => Number(n));
+ const [major = 0, minor = 0, patch = 0] = (versionStr || '0.0.0').split('.').map((n) => Number(n));
return { date, major, minor, patch };
};
@@ -45,9 +40,5 @@ export async function changelogSidebarGenerator({
return B.patch - A.patch;
});
- return [
- ...(indexItem ? [indexItem] : []),
- ...sortedReleaseItems,
- ...(mainChangelogItem ? [mainChangelogItem] : []),
- ];
-};
+ return [...(indexItem ? [indexItem] : []), ...sortedReleaseItems, ...(mainChangelogItem ? [mainChangelogItem] : [])];
+}
diff --git a/docs/docs/guidelines/CONTRIBUTING.md b/docs/docs/guidelines/CONTRIBUTING.md
index b768ae4ae..986f719c1 100644
--- a/docs/docs/guidelines/CONTRIBUTING.md
+++ b/docs/docs/guidelines/CONTRIBUTING.md
@@ -23,6 +23,12 @@ _When reporting issues, please:_
- Attach screenshots or logs if applicable.
- Specify your environment (OS, Node.js version, browser).
+## Claiming Issues
+
+- To claim an issue, comment: `/take`. This will assign the issue to you and add the `taken` label.
+- To release an issue, comment: `/untake`. This will unassign the issue from you and remove the `taken` label.
+- Issues labeled `taken` are currently owned and being worked on.
+
## Development Setup
The [Getting Started](../introduction/getting-started.md) section shows how to clone and run the application for the first time.
diff --git a/docs/docs/guidelines/_category_.json b/docs/docs/guidelines/_category_.json
index c7b516dd8..fcc255f35 100644
--- a/docs/docs/guidelines/_category_.json
+++ b/docs/docs/guidelines/_category_.json
@@ -2,6 +2,6 @@
"label": "Guidelines",
"position": 4,
"link": {
- "type": "generated-index",
+ "type": "generated-index"
}
}
diff --git a/docs/docs/guidelines/coding-style.md b/docs/docs/guidelines/coding-style.md
index 6cee45e86..f790ccdb4 100644
--- a/docs/docs/guidelines/coding-style.md
+++ b/docs/docs/guidelines/coding-style.md
@@ -5,6 +5,7 @@ sidebar_position: 3
---
## πGeneral Rules
+
- **Follow `.editorconfig`** exactly:
- Indent: **2 spaces**
- Charset: **UTF-8**
@@ -18,6 +19,7 @@ sidebar_position: 3
- **Do not manually override formatting** outside Prettier/clang-format unless necessary.
## β JavaScript / React
+
- Indent: 2 spaces
- Max line length: 120
- Single quotes `'...'`
@@ -30,6 +32,7 @@ sidebar_position: 3
- Globals: browser
## π» C / C++
+
- Indent: 2 spaces
- Max line length: 120
- **Brace style: Allman** (opening brace on a new line)
@@ -55,6 +58,7 @@ namespace exampleNamespace
#endif // EXAMPLE_FUNCTION_H
```
+
@@ -67,18 +71,20 @@ namespace exampleNamespace
namespace exampleNamespace
{
- void exampleFunction(uint8_t x)
- {
- if (internalLibrary::isPrime(x))
- {
- std::cout << "Prime" << std::endl;
- return;
- }
+void exampleFunction(uint8_t x)
+{
+if (internalLibrary::isPrime(x))
+{
+std::cout << "Prime" << std::endl;
+return;
+}
std::cout << "Non-prime" << std::endl;
- }
+
}
-```
+}
+
+````
## π HTML / CSS / Markdown / YAML
@@ -98,3 +104,71 @@ namespace exampleNamespace
- Do not trim trailing whitespace
- No final newline
- Max line length: off
+
+## π Linting and Style Checks
+
+### Running Lints Locally
+
+Before submitting a pull request, run these commands to ensure your code meets our quality standards:
+
+```bash
+# Install dependencies
+npm ci
+
+# Run ESLint (JavaScript/React)
+npm run lint
+
+# Auto-fix ESLint issues
+npm run lint:fix
+
+# Check code style (indentation, line endings, etc.)
+npm run lint:style
+````
+
+### What the Linters Check
+
+**ESLint** validates:
+
+- JavaScript/React syntax and best practices
+- Potential bugs and code smells
+- Consistent code style
+- React Hooks rules
+
+**editorconfig-checker** validates:
+
+- Indentation style (spaces vs tabs)
+- Line ending consistency (LF)
+- Trailing whitespace
+- Final newline in files
+- Line length limits (120 characters)
+
+### Fixing Lint Issues
+
+1. **Auto-fixable**: Run `npm run lint:fix` to automatically fix most ESLint issues
+
+2. **Manual fixes** required for:
+ - Complex indentation errors
+ - Line length violations (refactor long lines)
+ - Missing final newlines
+
+3. **Editor setup**: Install EditorConfig plugin for your editor:
+ - **VS Code**: "EditorConfig for VS Code" extension
+ - **Other editors**: See [EditorConfig.org](https://editorconfig.org/)
+
+### Special Cases
+
+- **Makefiles**: Must use tabs (not spaces) for indentation
+- **Binary files**: Linting does not apply to images, compiled files, etc.
+
+### CI Integration
+
+All pull requests automatically run linting checks. If the lint job fails:
+
+1. Review the CI logs
+2. Fix issues locally using the commands above
+3. Commit and push your fixes
+4. The CI will automatically re-run
+
+:::tip Current Status
+The project has some existing lint violations being addressed incrementally. Focus on keeping your changes lint-clean. Don't feel obligated to fix unrelated issues!
+:::
diff --git a/docs/docs/guidelines/commits.md b/docs/docs/guidelines/commits.md
index fc7eeb945..cfeffc956 100644
--- a/docs/docs/guidelines/commits.md
+++ b/docs/docs/guidelines/commits.md
@@ -5,21 +5,23 @@ sidebar_position: 4
---
## π Commits
+
:::info
Format: `(): `
Optional body below. Reference issues: `Fixes #123`
:::
## Types
-| Type | Description |
-|----------|--------------------------------------------------|
-| **feat** | A new feature |
-| **fix** | A bug fix |
-| **docs** | Documentation only |
-| **style**| Formatting, linting, whitespace changes only |
-| **refactor** | Code changes without affecting functionality |
-| **test** | Adding or updating tests |
-| **chore**| Maintenance tasks (dependencies, build tools) |
+
+| Type | Description |
+| ------------ | --------------------------------------------- |
+| **feat** | A new feature |
+| **fix** | A bug fix |
+| **docs** | Documentation only |
+| **style** | Formatting, linting, whitespace changes only |
+| **refactor** | Code changes without affecting functionality |
+| **test** | Adding or updating tests |
+| **chore** | Maintenance tasks (dependencies, build tools) |
:::important
@@ -27,12 +29,13 @@ Commits should be **atomic**, addressing one logical change per commit. Always c
:::
-
## Examples
+
- Commits must be **atomic** (one logical change per commit)
- Always check `.editorconfig` before committing
### [Version bump](https://github.com/Ryan-Millard/Img2Num/commit/426ac4f655343b06429b5f976e794b448f1afa0f)
+
```bash
chore(deps-dev): Bump prettier from 3.7.1 to 3.7.3 in the all-npm group
@@ -57,11 +60,13 @@ Signed-off-by: dependabot[bot]
```
### [Additional WASM image processing function](https://github.com/Ryan-Millard/Img2Num/commit/1b85d2d1fc358f10d1a122d988c6a94b275bac9a)
+
```bash
feat(Merge Small Regions): Detect & merge regions in processed images that are difficult to click
```
### [React Helmet & index.html wrapper + SEO images](https://github.com/Ryan-Millard/Img2Num/commit/3d090919dae749d884a0413b07a8897d0478e8eb)
+
```bash
feat(HTML head tags): Add proper head tags & favicon.svg
@@ -71,21 +76,25 @@ feat(HTML head tags): Add proper head tags & favicon.svg
```
### [Basic SEO - sitemap & robots.txt](https://github.com/Ryan-Millard/Img2Num/commit/de420c0bd2a323341b425776c7e5096f8f6a726d)
+
```bash
create(robots.txt, automatic sitemap): Basic robots.txt & vite-plugin-sitemap
```
### [Change React's routing system & add redirect on route not found](https://github.com/Ryan-Millard/Img2Num/commit/02f70f268c9605b50882368958df9b097e173be0)
+
```bash
update(main.jsx, 404.html): Switch to BrowserRouter and redirect on 404
```
### [Fallback for users who can't run JavaScript](https://github.com/Ryan-Millard/Img2Num/commit/f07c6c60e67f36be955ad850e96f4d234c4a5264)
+
```bash
update(index.html): Add noscript fallback for users with js disabled
```
### [Remove dynamic code from Credits page to allow it to be statically generated](https://github.com/Ryan-Millard/Img2Num/commit/645aea4375d33f6c3223da78efb55ecda21c9f4f)
+
```bash
refactor(Credits Page): Contributors card now fully static
```
diff --git a/docs/docs/guidelines/issues.md b/docs/docs/guidelines/issues.md
index 590653638..fbb772b97 100644
--- a/docs/docs/guidelines/issues.md
+++ b/docs/docs/guidelines/issues.md
@@ -7,13 +7,14 @@ sidebar_position: 5
To keep issues clear, actionable, and easy to triage, we provide templates for contributors. Always use the appropriate template when creating a new issue.
### Templates
-| Template | Description | Labels |
-|------------------------------------------|---------------------------------------------------------------------------------------------------------|--------|
-| **Bug Report** | Report a bug in the code or functionality. Fill out all fields to help maintainers reproduce the issue. | bug |
-| **New Feature** | Request a new feature or enhancement. Provide clear motivation and expected behavior. | feature |
-| **Good First Issue** | Labelled issues ideal for new contributors. Great starting point to get familiar with the project. | good first issue |
-| **Refactor or Code Quality Improvement** | Request or suggest refactoring of existing code for maintainability or performance. | refactor |
-| **Blank Issue** | Use when the other templates don't fit your issue's description (try not to use this one). | misc |
+
+| Template | Description | Labels |
+| ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------- |
+| **Bug Report** | Report a bug in the code or functionality. Fill out all fields to help maintainers reproduce the issue. | bug |
+| **New Feature** | Request a new feature or enhancement. Provide clear motivation and expected behavior. | feature |
+| **Good First Issue** | Labelled issues ideal for new contributors. Great starting point to get familiar with the project. | good first issue |
+| **Refactor or Code Quality Improvement** | Request or suggest refactoring of existing code for maintainability or performance. | refactor |
+| **Blank Issue** | Use when the other templates don't fit your issue's description (try not to use this one). | misc |
### Guidelines
diff --git a/docs/docs/guidelines/pull-requests.md b/docs/docs/guidelines/pull-requests.md
index dc0f84e52..94f58e4e1 100644
--- a/docs/docs/guidelines/pull-requests.md
+++ b/docs/docs/guidelines/pull-requests.md
@@ -5,13 +5,17 @@ sidebar_position: 6
---
## Fork the repo & create a feature branch:
+
```bash title="Switch to a new branch"
git checkout -b feat/your-feature
```
+
## Make commits following guidelines
+
These can be found in the [previous section](../commits).
## Push to your fork:
+
:::danger Danger: **Merge Conflicts**
Keep your branch **up-to-date with [main](https://github.com/Ryan-Millard/Img2Num/tree/main)** to avoid conflicts
@@ -27,7 +31,9 @@ git push origin feat/your-feature
```
## Open PR against `main`:
+
Make sure it has:
+
- A clear title
- A summary of changes & motivation
- References to related issues: `Fixes #123`
diff --git a/docs/docs/index.md b/docs/docs/index.md
index 06134baec..4cc39815f 100644
--- a/docs/docs/index.md
+++ b/docs/docs/index.md
@@ -36,7 +36,8 @@ For issues or contributions, visit our
If you spot something wrong in the documentation or elsewhere, please help the community by opening an issue for it!
Issue links:
+
- [Bug Report](https://github.com/Ryan-Millard/Img2Num/issues/new?template=bug_report.yml)
- [Refactor / Code Quality Improvement](https://github.com/Ryan-Millard/Img2Num/issues/new?template=refactor.yml)
- [Blank Issue](https://github.com/Ryan-Millard/Img2Num/issues/new)
-:::
+ :::
diff --git a/docs/docs/introduction/getting-started.md b/docs/docs/introduction/getting-started.md
index e33f1224f..d441f7dc5 100644
--- a/docs/docs/introduction/getting-started.md
+++ b/docs/docs/introduction/getting-started.md
@@ -7,9 +7,9 @@ sidebar_position: 2
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
-import DockerHomepage from './img/docker-desktop-homepage.jpg';
-import DockerSettings from './img/docker-desktop-settings-button-location.jpg';
-import DockerWslSetup from './img/docker-desktop-wsl-integration-setup.jpg';
+import DockerHomepage from './img/docker-desktop-homepage.jpg';
+import DockerSettings from './img/docker-desktop-settings-button-location.jpg';
+import DockerWslSetup from './img/docker-desktop-wsl-integration-setup.jpg';
import DockerResources from './img/docker-desktop-resources-button-location.jpg';
import DockerWslButton from './img/docker-desktop-wsl-integration-button-location.jpg';
@@ -33,7 +33,7 @@ Before you start installing anything, make sure you have the below installed.
-#### Installing Docker
+### Installing Docker
The section below will guide you through installing Docker on your operating system.
@@ -53,7 +53,7 @@ The section below will guide you through installing Docker on your operating sys
wsl --install
```
This will install Ubuntu by default. You can use other distributions if you prefer.
- See [Microsoft's documentation](https://learn.microsoft.com/en-us/windows/wsl/install) to
+ See [Microsoft's documentation](https://learn.microsoft.com/en-us/windows/wsl/install) to
find out more about installing WSL.
@@ -121,42 +121,45 @@ The section below will guide you through installing Docker on your operating sys
:::danger Docker not working?
Make sure to keep Docker Desktop open while you're using Docker because it needs to be open to run containers.
:::
+
- ### Installing Docker on macOS
+### Installing Docker on macOS
- 1. Download and install **Docker Desktop** from [https://www.docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop).
+1. Download and install **Docker Desktop** from [https://www.docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop).
- 2. Open Docker Desktop and ensure it is running.
+2. Open Docker Desktop and ensure it is running.
- 3. Verify installation in Terminal:
- ```bash
- docker --version
- docker compose version
- ```
+3. Verify installation in Terminal:
+ ```bash
+ docker --version
+ docker compose version
+ ```
- ### Installing Docker on Linux
+### Installing Docker on Linux
+
+1. Install Docker and Docker Compose via your package manager. For Ubuntu/Debian:
+
+ ```bash
+ sudo apt update
+ sudo apt install -y docker.io docker-compose
+ sudo systemctl enable --now docker
+ sudo usermod -aG docker $USER
+ ```
- 1. Install Docker and Docker Compose via your package manager. For Ubuntu/Debian:
- ```bash
- sudo apt update
- sudo apt install -y docker.io docker-compose
- sudo systemctl enable --now docker
- sudo usermod -aG docker $USER
- ```
- > You may need to log out and back in for the group change to take effect.
+ > You may need to log out and back in for the group change to take effect.
- 2. Verify installation:
- ```bash
- docker --version
- docker compose version
- ```
+2. Verify installation:
+ ```bash
+ docker --version
+ docker compose version
+ ```
@@ -358,6 +361,7 @@ You can choose to only install the dependencies for one portion of the app, but
```
+
@@ -556,5 +560,6 @@ This section will help you run both the main application and the documentation s
### Further Information
+
You may want to have a look at the [Project Scripts section](../../project-scripts/overview) now, but make sure that you understand and
agree with Img2Num's [License](../../license) and [guidelines](../../category/guidelines) first.
diff --git a/docs/docs/project-scripts/help-scripts/index.md b/docs/docs/project-scripts/help-scripts/index.md
index 0ae61e86a..bedc94d49 100644
--- a/docs/docs/project-scripts/help-scripts/index.md
+++ b/docs/docs/project-scripts/help-scripts/index.md
@@ -26,6 +26,7 @@ npm run help
This launches an interactive CLI that shows all scripts in `package.json` grouped by category and allows fuzzy search.
**Features:**
+
- Lists scripts in groups: Development, Build, Cleaning, Formatting, Linting, Other
- Allows fuzzy search for script names
- `a + enter` lists all scripts
@@ -38,9 +39,10 @@ This launches an interactive CLI that shows all scripts in `package.json` groupe
:::info
+
- These scripts are meant to manage the documentation site.
- Use `start` to run a local dev server for docs.
- Use `build` and `deploy` to publish to GitHub Pages.
- `swizzle` allows customizing theme components safely.
- `write-translations` and `write-heading-ids` are useful for internationalization and stable MDX anchors.
-:::
+ :::
diff --git a/docs/docs/project-scripts/help-scripts/scripts-guide.md b/docs/docs/project-scripts/help-scripts/scripts-guide.md
index 8809acccf..9107ce4a3 100644
--- a/docs/docs/project-scripts/help-scripts/scripts-guide.md
+++ b/docs/docs/project-scripts/help-scripts/scripts-guide.md
@@ -14,12 +14,12 @@ The `help` CLIs were refactored to read this metadata and a GitHub Actions workf
## What's in this page
-* `scriptsInfo` schema and examples
-* How to add or update scripts
-* `validate-scripts` usage and CI integration
-* Refactored `help` CLI and fuzzy search usage
-* Location of shared libraries and utilities
-* Troubleshooting & tips
+- `scriptsInfo` schema and examples
+- How to add or update scripts
+- `validate-scripts` usage and CI integration
+- Refactored `help` CLI and fuzzy search usage
+- Location of shared libraries and utilities
+- Troubleshooting & tips
## `scriptsInfo` β schema & examples
@@ -37,9 +37,7 @@ A minimal example:
},
"help": {
"group": "dev",
- "desc": [
- "Interactive help for available npm scripts.",
- ],
+ "desc": ["Interactive help for available npm scripts."],
"args": []
}
},
@@ -51,56 +49,58 @@ A minimal example:
```
:::note Notes on the schema used in this PR:
-* `scriptsInfo` is an object whose keys exactly match the `scripts` keys in `package.json`.
-* Each script entry may include:
-
- * `group` β logical grouping used in the help CLI (e.g. `dev`, `build`, `docs`).
- * `desc` β a string or an array of strings (multi-line descriptions supported).
- * `args` β an array describing positional or named args the script accepts.
-* The validator enforces a strict 1:1 mapping; every `scripts` entry must have a corresponding `scriptsInfo` entry and vice versa.
- * This ensures the scripts are properly documented.
-:::
+
+- `scriptsInfo` is an object whose keys exactly match the `scripts` keys in `package.json`.
+- Each script entry may include:
+ - `group` β logical grouping used in the help CLI (e.g. `dev`, `build`, `docs`).
+ - `desc` β a string or an array of strings (multi-line descriptions supported).
+ - `args` β an array describing positional or named args the script accepts.
+
+- The validator enforces a strict 1:1 mapping; every `scripts` entry must have a corresponding `scriptsInfo` entry and vice versa.
+ - This ensures the scripts are properly documented.
+ :::
## Adding or updating scripts
1. Add your script command under `scripts` in `package.json`.
2. Add a matching entry in `scriptsInfo` using the schema above.
- ```json
- "": {
- "group": "",
- "desc": "
diff --git a/package-lock.json b/package-lock.json
index 3b4b91a64..751b83efa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"imagetracerjs": "^1.2.6",
"lucide-react": "^0.562.0",
+ "prop-types": "^15.8.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-helmet": "^6.1.0",
@@ -18,7 +19,7 @@
},
"devDependencies": {
"@eslint/js": "^9.39.2",
- "@tanstack/react-query": "^5.90.14",
+ "@tanstack/react-query": "^5.90.12",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
diff --git a/package.json b/package.json
index 66ca30a54..b831b3171 100644
--- a/package.json
+++ b/package.json
@@ -66,13 +66,22 @@
},
"Formatting": {
"format": {
- "desc": "Format all files with Prettier and clang-format"
+ "desc": "Format all files with Prettier and clang-format (modifies files)"
+ },
+ "format:check": {
+ "desc": "Check all files with Prettier and clang-format without modifying them (files untouched)"
},
"format-js": {
- "desc": "Format all non-C++ files with Prettier"
+ "desc": "Format all non-C++ files with Prettier (modifies files)"
+ },
+ "format-js:check": {
+ "desc": "Check all non-C++ files with Prettier without modifying them (files untouched)"
},
"format-wasm": {
- "desc": "Format all C++ files with clang-format"
+ "desc": "Format all C++ files with clang-format (modifies files)"
+ },
+ "format-wasm:check": {
+ "desc": "Check all C++ files with clang-format without modifying them (files untouched)"
}
},
"Linting": {
@@ -137,8 +146,11 @@
"clean-js": "rimraf dist",
"clean-wasm": "node scripts/build-wasm.js --clean",
"format": "npm run format-js && npm run format-wasm",
+ "format:check": "npm run format-js:check && npm run format-wasm:check",
"format-js": "prettier --write .",
+ "format-js:check": "prettier --check .",
"format-wasm": "node scripts/format-wasm.js",
+ "format-wasm:check": "node scripts/format-wasm.js --check",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint:style": "editorconfig-checker",
@@ -151,6 +163,7 @@
"dependencies": {
"imagetracerjs": "^1.2.6",
"lucide-react": "^0.562.0",
+ "prop-types": "^15.8.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-helmet": "^6.1.0",
@@ -159,7 +172,7 @@
},
"devDependencies": {
"@eslint/js": "^9.39.2",
- "@tanstack/react-query": "^5.90.14",
+ "@tanstack/react-query": "^5.90.12",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
diff --git a/scripts/build-wasm.js b/scripts/build-wasm.js
index e871c243b..e3e66c2b0 100644
--- a/scripts/build-wasm.js
+++ b/scripts/build-wasm.js
@@ -107,7 +107,7 @@ function safeRemoveDir(dir) {
if (error.message) {
console.error(` Message: ${error.message}`);
}
- console.log("You may need to forcefully remove it.");
+ console.log('You may need to forcefully remove it.');
return false;
}
}
@@ -206,12 +206,7 @@ function build() {
const buildType = isDebug ? 'Debug' : 'Release';
const emcmake = isWindows ? 'emcmake.bat' : 'emcmake';
- run(emcmake, [
- 'cmake',
- '-S', WASM_DIR,
- '-B', BUILD_DIR,
- `-DCMAKE_BUILD_TYPE=${buildType}`,
- ]);
+ run(emcmake, ['cmake', '-S', WASM_DIR, '-B', BUILD_DIR, `-DCMAKE_BUILD_TYPE=${buildType}`]);
// Build
run('cmake', ['--build', BUILD_DIR, '--parallel', '--config', buildType]);
diff --git a/scripts/format-wasm.js b/scripts/format-wasm.js
index c625e3e65..d53d331ff 100644
--- a/scripts/format-wasm.js
+++ b/scripts/format-wasm.js
@@ -9,13 +9,21 @@ if (!files.length) {
process.exit(0);
}
+const checkOnly = process.argv.includes('--check');
+
files.forEach((file) => {
try {
- execSync(`npx clang-format -i "${file}"`, { stdio: 'inherit' });
+ if (checkOnly) {
+ execSync(`clang-format --dry-run --Werror "${file}"`, { stdio: 'inherit' });
+ return;
+ }
+
+ execSync(`clang-format -i "${file}"`, { stdio: 'inherit' });
console.log(`Formatted: ${file}`);
} catch (err) {
console.error(`Error formatting ${file}:`, err.message);
+ process.exit(1);
}
});
-console.log('C++ formatting complete.');
+console.log(`C++ ${checkOnly ? 'format check' : 'formatting'} complete.`);
diff --git a/scripts/handle-changelog.js b/scripts/handle-changelog.js
index 6c2282705..2412c7e3a 100644
--- a/scripts/handle-changelog.js
+++ b/scripts/handle-changelog.js
@@ -1,37 +1,37 @@
#!/usr/bin/env node
-import fs from "fs";
-import path from "path";
-import { execSync } from "node:child_process";
+import fs from 'fs';
+import path from 'path';
+import { execSync } from 'node:child_process';
// Run standard-version
try {
// Stage the changelog folder
- execSync("npx standard-version", { stdio: "inherit" });
+ execSync('npx standard-version', { stdio: 'inherit' });
- console.log("[release] release created successfully");
+ console.log('[release] release created successfully');
} catch (err) {
- console.error("[release] Error:", err.message);
+ console.error('[release] Error:', err.message);
process.exit(1);
}
-const changelogPath = "CHANGELOG.md";
-const outputDir = "docs/changelog";
+const changelogPath = 'CHANGELOG.md';
+const outputDir = 'docs/changelog';
if (!fs.existsSync(changelogPath)) {
- console.log("[changelog] No CHANGELOG.md found. Skipping.");
+ console.log('[changelog] No CHANGELOG.md found. Skipping.');
process.exit(0);
}
-const content = fs.readFileSync(changelogPath, "utf8");
+const content = fs.readFileSync(changelogPath, 'utf8');
// Write to docs/changelog/complete-changelog.md
-const completeChangelogPath = path.join(outputDir, "complete-changelog.md");
+const completeChangelogPath = path.join(outputDir, 'complete-changelog.md');
let completeChangelogMdHeader = `---
title: Complete Changelog
---
`;
-fs.writeFileSync(completeChangelogPath, completeChangelogMdHeader + content, "utf8");
+fs.writeFileSync(completeChangelogPath, completeChangelogMdHeader + content, 'utf8');
const lines = content.split(/\r?\n/);
@@ -46,13 +46,13 @@ for (const line of lines) {
if (capture) break; // stop at next release
capture = true;
version = releaseMatch[1];
- date = releaseMatch[2]
+ date = releaseMatch[2];
}
if (capture) releaseLines.push(line);
}
if (!version) {
- console.log("[changelog] No release section detected. Skipping.");
+ console.log('[changelog] No release section detected. Skipping.');
process.exit(0);
}
@@ -69,20 +69,19 @@ id: ${fileName}
# Release ${version}
`;
-const fileLines = frontmatter + releaseLines.join("\n");
-fs.writeFileSync(outPath, fileLines, "utf8");
+const fileLines = frontmatter + releaseLines.join('\n');
+fs.writeFileSync(outPath, fileLines, 'utf8');
console.log(`[changelog] Extracted release ${version} -> ${outPath}`);
// Stage the file so it also gets committed
try {
// Stage the changelog folder
- execSync(`git add ${outPath} ${completeChangelogPath}`, { stdio: "inherit" });
- execSync(`git commit -m "chore(changelog): add ${version} release notes"`, { stdio: "inherit" });
+ execSync(`git add ${outPath} ${completeChangelogPath}`, { stdio: 'inherit' });
+ execSync(`git commit -m "chore(changelog): add ${version} release notes"`, { stdio: 'inherit' });
- console.log("[git] docs/changelog added and commit amended successfully.");
+ console.log('[git] docs/changelog added and commit amended successfully.');
} catch (err) {
- console.error("[git] Error:", err.message);
+ console.error('[git] Error:', err.message);
process.exit(1);
}
-
diff --git a/scripts/help.js b/scripts/help.js
index c1879a526..a1e372a74 100644
--- a/scripts/help.js
+++ b/scripts/help.js
@@ -1,13 +1,12 @@
-import { runFuzzyCli } from "./lib/cli-fuzzy.js";
-import { readPackageJsonScripts } from "./lib/read-packageJson-scripts.js";
+import { runFuzzyCli } from './lib/cli-fuzzy.js';
+import { readPackageJsonScripts } from './lib/read-packageJson-scripts.js';
-const title =
-`Img2Num CLI Scripts
+const title = `Img2Num CLI Scripts
Also see: https://ryan-millard.github.io/Img2Num/info/docs/category/-project-scripts
`;
try {
- const { flat: items, basicItems } = readPackageJsonScripts(new URL("../package.json", import.meta.url));
+ const { flat: items, basicItems } = readPackageJsonScripts(new URL('../package.json', import.meta.url));
// Grab all CLI args after `npm run help --`
const initialSearch = process.argv.slice(2);
@@ -19,6 +18,6 @@ try {
initialSearch,
});
} catch (error) {
- console.error("Failed to read root package.json scripts:", error.message);
+ console.error('Failed to read root package.json scripts:', error.message);
process.exit(1);
}
diff --git a/scripts/lib/cli-fuzzy.js b/scripts/lib/cli-fuzzy.js
index c2345576c..8aba52c32 100644
--- a/scripts/lib/cli-fuzzy.js
+++ b/scripts/lib/cli-fuzzy.js
@@ -1,6 +1,6 @@
-import readline from "readline";
-import fuzzy from "fuzzy";
-import { Colors, colorText } from "./colors.js";
+import readline from 'readline';
+import fuzzy from 'fuzzy';
+import { Colors, colorText } from './colors.js';
/**
* Start an interactive fuzzy-search CLI for the provided script items.
@@ -36,7 +36,7 @@ export function runFuzzyCli({ items, basicItems, title, initialSearch = [] }) {
// Run initial search terms if provided
if (initialSearch.length > 0) {
- initialSearch.forEach(term => runSearch(term, items));
+ initialSearch.forEach((term) => runSearch(term, items));
}
startInteractive(items, initialSearch.length > 0);
@@ -44,7 +44,7 @@ export function runFuzzyCli({ items, basicItems, title, initialSearch = [] }) {
const HEADER_LINE_WIDTH = 80;
const HEADER_INSTRUCTIONS = "Type 'a' to list all, 'q' to quit.";
-const HEADER_LINE = colorText("β".repeat(HEADER_LINE_WIDTH), Colors.BLUE);
+const HEADER_LINE = colorText('β'.repeat(HEADER_LINE_WIDTH), Colors.BLUE);
/**
* Prints a styled header block containing the provided title and header instructions.
* @param {string} title - The header title displayed between decorative horizontal lines.
@@ -63,11 +63,11 @@ function printHeader(title) {
* @param {string[]} basicItems - Ordered list of script names to include in the basic section.
*/
function printBasics(items, basicItems) {
- console.log("\nBasic scripts:");
+ console.log('\nBasic scripts:');
for (const name of basicItems) {
if (items[name]) printItem(name, items[name]);
}
- console.log("");
+ console.log('');
}
/**
@@ -88,12 +88,12 @@ function startInteractive(items, skipIfInitialSearch = false) {
output: process.stdout,
completer(line) {
const names = Object.keys(items);
- const hits = fuzzy.filter(line, names).map(x => x.original);
+ const hits = fuzzy.filter(line, names).map((x) => x.original);
return [hits, line];
},
});
- rl.setPrompt(colorText("> ", Colors.CYAN));
+ rl.setPrompt(colorText('> ', Colors.CYAN));
// If initialSearch was provided, and we just want one-shot results, skip the interactive prompt
if (skipIfInitialSearch) {
@@ -102,18 +102,18 @@ function startInteractive(items, skipIfInitialSearch = false) {
rl.prompt();
- rl.on("line", line => {
+ rl.on('line', (line) => {
const input = line.trim();
- if (input === "q") return rl.close();
- if (input === "a") return printAll(items, rl);
+ if (input === 'q') return rl.close();
+ if (input === 'a') return printAll(items, rl);
runSearch(input, items);
rl.prompt();
});
- rl.on("close", () => {
- console.log(colorText("Exiting.", Colors.MAGENTA));
+ rl.on('close', () => {
+ console.log(colorText('Exiting.', Colors.MAGENTA));
process.exit(0);
});
}
@@ -127,9 +127,9 @@ function startInteractive(items, skipIfInitialSearch = false) {
* @param {Object.} items - Mapping of item names to metadata used when printing matches.
*/
function runSearch(input, items) {
- const matches = fuzzy.filter(input, Object.keys(items)).map(x => x.original);
+ const matches = fuzzy.filter(input, Object.keys(items)).map((x) => x.original);
if (!matches.length) {
- console.log(colorText("No matches.", Colors.RED));
+ console.log(colorText('No matches.', Colors.RED));
return;
}
@@ -141,7 +141,9 @@ function runSearch(input, items) {
/**
* Print all scripts grouped by their `info.group` and re-prompt the given readline interface.
*
- * Groups items by the `group` property on each info object (uses "Other" when absent), prints a blue header for each group, lists each script using `printItem`, and then calls `rl.prompt()` to resume the interactive prompt.
+ * Groups items by the `group` property on each info object (uses "Other" when absent),
+ * prints a blue header for each group, lists each script using `printItem`, and then
+ * calls `rl.prompt()` to resume the interactive prompt.
*
* @param {Object} items - Mapping of script names to their info objects.
* @param {import('readline').Interface} rl - Readline interface used to re-prompt after listing.
@@ -150,7 +152,7 @@ function printAll(items, rl) {
const groups = {};
for (const [name, info] of Object.entries(items)) {
- const group = info.group || "Other";
+ const group = info.group || 'Other';
if (!groups[group]) groups[group] = [];
groups[group].push([name, info]);
}
@@ -175,8 +177,8 @@ function printAll(items, rl) {
* @param {string} [info.command] - Optional command string displayed as a cyan-prefixed line.
*/
function printItem(name, info) {
- console.log(`\n\t${colorText(name, Colors.YELLOW)}${info.group ? ` (${info.group})` : ""}`);
- const description = Array.isArray(info.desc) ? info.desc.join(" ") : info.desc;
+ console.log(`\n\t${colorText(name, Colors.YELLOW)}${info.group ? ` (${info.group})` : ''}`);
+ const description = Array.isArray(info.desc) ? info.desc.join(' ') : info.desc;
if (description) {
console.log(`\t\t- ${colorText(description, Colors.YELLOW)}`);
}
@@ -190,4 +192,4 @@ function printItem(name, info) {
if (info.command) {
console.log(`\t\t\t\t> ${colorText(info.command, Colors.CYAN)}`);
}
-}
\ No newline at end of file
+}
diff --git a/scripts/lib/colors.js b/scripts/lib/colors.js
index a0f3cf11f..71694d189 100644
--- a/scripts/lib/colors.js
+++ b/scripts/lib/colors.js
@@ -4,44 +4,44 @@ const supportsColor = process.stdout.isTTY;
// Define allowed color names as an enum
export const Colors = Object.freeze({
- RESET: "reset",
- BOLD: "bold",
- DIM: "dim",
- RED: "red",
- GREEN: "green",
- YELLOW: "yellow",
- BLUE: "blue",
- MAGENTA: "magenta",
- CYAN: "cyan",
- WHITE: "white",
- BG_RED: "bgRed",
- BG_GREEN: "bgGreen",
- BG_YELLOW: "bgYellow",
- BG_BLUE: "bgBlue",
- BG_MAGENTA: "bgMagenta",
- BG_CYAN: "bgCyan",
- BG_WHITE: "bgWhite",
+ RESET: 'reset',
+ BOLD: 'bold',
+ DIM: 'dim',
+ RED: 'red',
+ GREEN: 'green',
+ YELLOW: 'yellow',
+ BLUE: 'blue',
+ MAGENTA: 'magenta',
+ CYAN: 'cyan',
+ WHITE: 'white',
+ BG_RED: 'bgRed',
+ BG_GREEN: 'bgGreen',
+ BG_YELLOW: 'bgYellow',
+ BG_BLUE: 'bgBlue',
+ BG_MAGENTA: 'bgMagenta',
+ BG_CYAN: 'bgCyan',
+ BG_WHITE: 'bgWhite',
});
// Mapping from enum to ANSI codes
const codes = Object.freeze({
- reset: "\x1b[0m",
- bold: "\x1b[1m",
- dim: "\x1b[2m",
- red: "\x1b[31m",
- green: "\x1b[32m",
- yellow: "\x1b[33m",
- blue: "\x1b[34m",
- magenta: "\x1b[35m",
- cyan: "\x1b[36m",
- white: "\x1b[37m",
- bgRed: "\x1b[41m",
- bgGreen: "\x1b[42m",
- bgYellow: "\x1b[43m",
- bgBlue: "\x1b[44m",
- bgMagenta: "\x1b[45m",
- bgCyan: "\x1b[46m",
- bgWhite: "\x1b[47m",
+ reset: '\x1b[0m',
+ bold: '\x1b[1m',
+ dim: '\x1b[2m',
+ red: '\x1b[31m',
+ green: '\x1b[32m',
+ yellow: '\x1b[33m',
+ blue: '\x1b[34m',
+ magenta: '\x1b[35m',
+ cyan: '\x1b[36m',
+ white: '\x1b[37m',
+ bgRed: '\x1b[41m',
+ bgGreen: '\x1b[42m',
+ bgYellow: '\x1b[43m',
+ bgBlue: '\x1b[44m',
+ bgMagenta: '\x1b[45m',
+ bgCyan: '\x1b[46m',
+ bgWhite: '\x1b[47m',
});
/**
@@ -64,4 +64,4 @@ export function colorText(text, colorEnum) {
*/
export function logColor(text, colorEnum) {
console.log(colorText(text, colorEnum));
-}
\ No newline at end of file
+}
diff --git a/scripts/lib/read-packageJson-scripts.js b/scripts/lib/read-packageJson-scripts.js
index c4e51f29d..16ebfbe2f 100644
--- a/scripts/lib/read-packageJson-scripts.js
+++ b/scripts/lib/read-packageJson-scripts.js
@@ -1,11 +1,13 @@
-import fs from "fs";
+import fs from 'fs';
/**
* Load and normalize script metadata from a package-style JSON file.
*
* @param {string} fileUrl - Path to a JSON file that contains `scriptsInfo` and `scripts` top-level properties.
* @returns {{flat: Record, basicItems: any[]}} An object with:
- * - `flat`: a mapping of script name to its CLI metadata (description defaults to `""`, args defaults to `[]`, command falls back to `"No command defined"`, and `group` is the originating group key).
+ * - `flat`: a mapping of script name to its CLI metadata (description defaults to `""`,
+ * args defaults to `[]`, command falls back to `"No command defined"`, and `group`
+ * is the originating group key).
* - `basicItems`: the array from `scriptsInfo._meta.basic` or an empty array when not present.
*/
export function readPackageJsonScripts(fileUrl) {
@@ -20,13 +22,13 @@ export function readPackageJsonScripts(fileUrl) {
for (const [group, entries] of Object.entries(groups)) {
for (const [name, desc] of Object.entries(entries)) {
flat[name] = {
- desc: desc.desc || "", // take the actual string description
+ desc: desc.desc || '', // take the actual string description
args: desc.args || [], // optional, if you want to show CLI args
- command: scripts[name] || "No command defined",
+ command: scripts[name] || 'No command defined',
group,
};
}
}
return { flat, basicItems };
-}
\ No newline at end of file
+}
diff --git a/scripts/validate-scripts.js b/scripts/validate-scripts.js
index 04c9f4e10..43866799a 100644
--- a/scripts/validate-scripts.js
+++ b/scripts/validate-scripts.js
@@ -1,6 +1,6 @@
#!/usr/bin/env node
-import fs from "fs";
-import path from "path";
+import fs from 'fs';
+import path from 'path';
/**
* Load and parse a package.json (or other JSON) file from disk.
@@ -11,7 +11,7 @@ import path from "path";
*/
function loadPackageJson(filePath) {
try {
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
} catch (error) {
console.error(`β Failed to load ${filePath}: ${error.message}`);
process.exit(1);
@@ -27,7 +27,7 @@ function loadPackageJson(filePath) {
function flattenScriptsInfo(scriptsInfo) {
const flat = {};
for (const [group, entries] of Object.entries(scriptsInfo)) {
- if (group === "_meta") continue;
+ if (group === '_meta') continue;
for (const [name] of Object.entries(entries)) {
flat[name] = true;
}
@@ -38,7 +38,9 @@ function flattenScriptsInfo(scriptsInfo) {
/**
* Validates that the "scripts" keys in a package.json match the flattened entries in "scriptsInfo".
*
- * If "scripts" or "scriptsInfo" is missing, or any script is undocumented or any description refers to a non-existent script, logs errors and exits the process with code 1. On success, logs a confirmation message.
+ * If "scripts" or "scriptsInfo" is missing, or any script is undocumented or any
+ * description refers to a non-existent script, logs errors and exits the process with
+ * code 1. On success, logs a confirmation message.
*
* @param {string} pkgPath - Path to the package.json file to validate.
*/
@@ -78,7 +80,7 @@ function validateScripts(pkgPath) {
}
// Validate main project
-validateScripts(path.resolve("./package.json"));
+validateScripts(path.resolve('./package.json'));
// Validate docs project
-validateScripts(path.resolve("./docs/package.json"));
\ No newline at end of file
+validateScripts(path.resolve('./docs/package.json'));
diff --git a/src/components/GlassSwitch.jsx b/src/components/GlassSwitch.jsx
new file mode 100644
index 000000000..8ce342d5a
--- /dev/null
+++ b/src/components/GlassSwitch.jsx
@@ -0,0 +1,31 @@
+import styles from './GlassSwitch.module.css';
+import Tooltip from '@components/Tooltip';
+import PropTypes from 'prop-types';
+
+const GlassSwitch = ({ onChange, isOn, ariaLabel, thumbContent, disabled = false }) => {
+ const fallbackContent = isOn ? styles.fallbackThumbContentOn : styles.fallbackThumbContentOff;
+ return (
+
+
+
+ );
+};
+
+GlassSwitch.propTypes = {
+ isOn: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired,
+ ariaLabel: PropTypes.string.isRequired,
+ thumbContent: PropTypes.node,
+ disabled: PropTypes.bool,
+};
+
+export default GlassSwitch;
diff --git a/src/components/GlassSwitch.module.css b/src/components/GlassSwitch.module.css
new file mode 100644
index 000000000..91b035763
--- /dev/null
+++ b/src/components/GlassSwitch.module.css
@@ -0,0 +1,54 @@
+.switch {
+ --size: 30px;
+ display: flex;
+ align-items: center;
+ width: calc(var(--size) * 2);
+ height: var(--size);
+ padding: 0;
+ border-radius: 9999px;
+ position: relative;
+ cursor: pointer;
+ margin-right: 15px;
+}
+
+/* WCAG 2.4.7 Compliant Focus Indicator */
+.switch:focus-visible {
+ outline: 2px solid var(--color-primary, #4f46e5);
+ outline-offset: 2px;
+ box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.1);
+}
+
+/* Fallback for browsers that don't support :focus-visible */
+.switch:focus {
+ outline: 2px solid var(--color-primary, #4f46e5);
+ outline-offset: 2px;
+}
+
+/* Remove default outline from focus-visible for cleaner look */
+.switch:focus:not(:focus-visible) {
+ outline: none;
+ box-shadow: none;
+}
+
+.thumb {
+ position: absolute;
+ left: 0;
+ bottom: 1;
+ width: calc(var(--size) - 4px);
+ height: calc(var(--size) - 4px);
+ border-radius: 50%;
+ background: var(--color-surface);
+ color: var(--color-text);
+ transition: transform 0.3s ease;
+}
+
+.checked .thumb {
+ transform: translateX(var(--size));
+}
+.fallbackThumbContentOff {
+ background-color: rgb(110, 110, 110);
+}
+
+.fallbackThumbContentOn {
+ background-color: var(--color-success);
+}
diff --git a/src/components/GlassSwitch.test.jsx b/src/components/GlassSwitch.test.jsx
new file mode 100644
index 000000000..e0cc84ed7
--- /dev/null
+++ b/src/components/GlassSwitch.test.jsx
@@ -0,0 +1,116 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import GlassSwitch from './GlassSwitch';
+
+// Mock the Tooltip component
+vi.mock('@components/Tooltip', () => ({
+ __esModule: true,
+ default: ({ children }) =>