diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..8c52ff9
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8d73975
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,47 @@
+name: CI
+
+on:
+ push:
+ branches: [master, main]
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ validate:
+ name: Lint, typecheck, test, build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Typecheck
+ run: npm run typecheck
+
+ - name: Lint
+ run: npm run lint
+
+ - name: Format check
+ run: npm run format:check
+
+ - name: Test
+ run: npm run test -- --reporter=default
+
+ - name: Build
+ run: npm run build
+
+ - name: Upload extension artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: clay-slip-extension
+ path: dist
+ retention-days: 14
diff --git a/.gitignore b/.gitignore
index ad46b30..13cab46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,61 +1,9 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
+node_modules
+dist
coverage
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# TypeScript v1 declaration files
-typings/
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
+.DS_Store
+*.log
+.vite
.env
-
-# next.js build output
-.next
+.env.local
+.eslintcache
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..a45fd52
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+24
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..31af4f8
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,7 @@
+dist
+coverage
+node_modules
+*.png
+*.jpg
+*.svg
+package-lock.json
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..1d98457
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,10 @@
+{
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "printWidth": 100,
+ "tabWidth": 2,
+ "useTabs": false,
+ "arrowParens": "always",
+ "endOfLine": "lf"
+}
diff --git a/LICENSE b/LICENSE
index 013f13d..5102606 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2018 Clay
+Copyright (c) 2026 Clay
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 19f3bbd..c0e03ce 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,173 @@
-Slip
-====
+# Clay Slip
-*slip (n): A slip is a liquid mixture or slurry of clay and/or other materials suspended in water used in making pottery*
+> A modern Chrome extension for exploring [Clay](https://github.com/clay) CMS pages.
-Slip is a Chrome extension for exploring [Clay](https://github.com/clay) pages
+Clay annotates rendered HTML with `data-uri` attributes on every component, page, and layout. Clay Slip reads those attributes and gives you a powerful developer overlay — visualize component boundaries, inspect data, jump between published and draft versions, and copy URIs without ever opening DevTools.
-- adds an info window to the bottom right corner of the page for easy page/layout/component discovery
-- adds debugging borders to components
-- shift+click opens the (unresolved) instance data in a new tab
-- alt+click opens the resolved component instance data in a new tab
-- clicking a component highlights it
+
-## In Progress: Vim-like keystroke functionality
-Currently...
-- keystroke 'y + c' copies uri of selected component to keyboard
-- keystroke 'y + p' copies page uri to keyboard
-- keystroke 'o + p' opens the page uri in a new tab
-- keystroke 'o + c' opens selected component uri into a new tab (or page uri if no component selected)
+## Highlights
+
+- **Manifest V3** Chrome extension built with TypeScript, React, Vite, and `@crxjs/vite-plugin`
+- **Shadow-DOM panel** that never collides with host page styles
+- **Component tree + find-on-page** — live filter dims non-matches on the page, Enter cycles through them, Esc clears
+- **Inline JSON preview** so you don't need to open a new tab to read component data
+- **Diff view** comparing the published version against the unpublished draft, **or** the same URI across two environments
+- **Environment switcher** that rewrites every link and JSON fetch through your configured local / dev / staging / prod host
+- **Site host mappings** — per-brand hostname config that powers a one-click _View on prod / staging / qa_ pill row and lets the Share button hand out cross-env links without leaving the page
+- **Open in Clay editor** — jump from the page or any component straight into Clay edit mode (always opens the unpublished version)
+- **Sticky-note annotations** pinned to component URIs, surfaced as a dot on the page and a dedicated Notes tab — leave async review notes for teammates
+- **Page audit export** to clipboard as JSON, CSV, or Markdown — every component on the page, ready to paste into a ticket or QA checklist
+- **Shareable selection links** — copy a `?clay-slip-select=…` URL that auto-opens the panel and selects the same component on someone else's machine
+- **Component screenshot to clipboard** — one-click PNG of any selected component, panel auto-hides during capture
+- **SEO tab** — title / meta / og / twitter / JSON-LD with a Twitter + Facebook card preview and lints (length, missing image, duplicate `
`, etc.)
+- **Recently viewed components** persisted across sessions, with one-click jump back
+- **Resizable + dockable panel** — drag the inner edges (or the inner-corner grabber) to resize width _and_ height; choose any of four corners or a full-height left/right side dock
+- **Toggleable component outlines** (button in the header, h shortcut) with a configurable opacity
+- **Auto / light / dark themes** that respond to OS theme changes live
+- **Keyboard shortcuts** with a ? overlay listing every binding
+- **Options page** for theme, dock side + width, environment hosts, highlight intensity, shortcut toggle, and recents history size
+- **Floating Clay button (FAB)** on every Clay page — collapsed/idle state of the panel is a small circular button anchored to the user's preferred corner, with a live component-count badge. Click it to expand the full panel. The standard browser-extension chrome pattern (Sentry / Hotjar / Crisp / Intercom).
+- **Smart popup**: friendly "Not a Clay page" popup on non-Clay pages; on Clay pages the toolbar icon mounts/unmounts the entire extension as an escape hatch
+- **Toolbar badge** shows the count of Clay components on the current page (cleared on navigation)
+- **Click-through-aware selection**: the panel selects the component you clicked but lets real interactive elements (links, buttons, inputs) keep working
+- **Copy-as menu**: URI / cURL / `fetch()` snippet / CSS selector — all env-host aware
+- **Vitest** unit tests and **GitHub Actions** CI on every PR
+
+## Install (development)
+
+```bash
+npm install
+npm run build
+```
+
+Then in Chrome:
+
+1. Visit `chrome://extensions`
+2. Enable **Developer mode** (top right)
+3. Click **Load unpacked** and select the `dist/` directory
+
+For live development with HMR:
+
+```bash
+npm run dev
+```
+
+Reload the extension in `chrome://extensions` after switching between `dev` and `build` outputs.
+
+## Usage
+
+| Action | Shortcut / Click |
+| ------------------------ | ------------------------------------------------------------------------------------------------- |
+| Open the panel | Click the floating **Clay** button (FAB) anchored at your preferred corner |
+| Collapse to FAB | Click the collapse button in the panel header (or press [ ) |
+| Hide the extension | Click the toolbar icon (toggles mount on/off for the current tab) |
+| Select a component | Click any outlined element on the page |
+| Open in Clay editor | **Edit** button on a page or component — opens the page with `?edit=true` |
+| Open component JSON | Use the **Data** / **.json** / **.html** buttons in the panel |
+| Cross-env diff | **Diff** tab → `Compare:` select → pick another configured env |
+| View page on another env | **View on:** pill row in PageInfo (one pill per env configured for this site) |
+| Annotate a component | **Inspect** tab, scroll to **Note**, type and Save — orange dot appears on the page |
+| Share a selection | **Share** button copies for the current env; click **▾** to share for prod / staging / qa instead |
+| Screenshot a component | **Screenshot** button — PNG copied to clipboard |
+| Export page manifest | **Export ▾** button on the Inspect tab — copies JSON / CSV / Markdown to your clipboard |
+| Find on page | **Tree** tab search box → matches dim non-matches; Enter cycles, Esc clears |
+| Resize the panel | Drag the inner vertical / horizontal edge — or the inner-corner grabber for both at once |
+| Copy URI | Press y then c (component) or p (page) |
+| Open URI in new tab | Press o then c or p |
+| Toggle outlines on page | Press h or click the eye icon in the header |
+| Cycle environment | Click the `env: …` pill at the bottom of the Inspect tab |
+| Show shortcut overlay | Press ? |
+| Toggle FAB ↔ panel | Press [ or click the collapse button / the FAB |
+| Switch tabs | Press i (Inspect) or t (Tree) |
+| Open settings | Click the gear icon in the panel header |
+
+## Screenshots
+
+| Inspect | Tree | Options |
+| ---------------------------------------- | ---------------------------------- | ---------------------------------------- |
+|  |  |  |
+
+## Configuration
+
+### Site host mappings
+
+The **Site host mappings** section in the options page is a per-instance lookup table mapping each brand to its hostnames per environment. With it configured, the panel renders a **View on:** pill row on every Clay page so you can jump to the equivalent URL on a different env, and the **Share** button gains a **▾** picker for cross-env share links. Empty by default — every fork populates its own.
+
+Example for a Vox-Media-style multi-brand setup:
+
+| Label | Production | Staging | QA |
+| ------- | --------------- | --------------- | ------------- |
+| The Cut | www.thecut.com | stg.thecut.com | qa.thecut.com |
+| Vulture | www.vulture.com | stg.vulture.com | |
+| Curbed | www.curbed.com | stg.curbed.com | qa.curbed.com |
+
+Hostnames are matched **exactly** (case-insensitive) — no prefix stripping or wildcards — so the mapping does what you wrote and nothing more. The `dev` and `local` envs are out of scope for site mappings; use the existing **Environments** section for Clay-API hosts.
+
+## Architecture
+
+```
+src/
+├── manifest.ts # MV3 manifest defined in TypeScript
+├── background/
+│ └── service-worker.ts # MV3 service worker (open tabs, badge counts)
+├── content/
+│ ├── index.ts # Content script entry
+│ ├── highlighter.ts # Component outline styles (host DOM)
+│ ├── shadow-host.ts # Mounts React app inside a Shadow DOM
+│ ├── page-info.ts # Reads Clay metadata from the page
+│ └── panel/ # The React panel UI
+│ ├── App.tsx
+│ ├── store.ts # Zustand store
+│ ├── theme.ts # Light / dark tokens
+│ ├── styles.css # Shadow-scoped styles
+│ ├── components/ # Tabs, tree, JSON viewer, diff, breadcrumb…
+│ └── hooks/ # Drag, theme, shortcuts, selection
+├── popup/ # "Not a Clay page" popup (active until a page sends CLAY_DETECTED)
+├── options/ # Full options page (env hosts, dock + width, intensity, recents, shortcuts)
+└── lib/ # Pure utilities
+ ├── clay-uri.ts # URI parsing + buildUrl/buildEditorUrl/buildShareLink + copy-as helpers
+ ├── clipboard.ts # Modern + legacy clipboard
+ ├── storage.ts # User preferences in chrome.storage.sync
+ ├── annotations.ts # Sticky notes per component URI
+ ├── recents.ts # Recently viewed components history
+ ├── exporter.ts # Page manifest → JSON / CSV / Markdown
+ ├── seo.ts # Document head extractor + linter
+ ├── screenshot.ts # captureVisibleTab + canvas crop → clipboard PNG
+ ├── site-host.ts # Per-brand hostname mapping → cross-env URL rewrites
+ └── types.ts # Shared types + DEFAULT_PREFERENCES
+
+```
+
+## Scripts
+
+| Command | What it does |
+| ----------------------- | --------------------------------------- |
+| `npm run dev` | Vite dev server with HMR |
+| `npm run build` | Typecheck + production build → `dist/` |
+| `npm run lint` | ESLint with zero-warning policy |
+| `npm run lint:fix` | ESLint auto-fix |
+| `npm run format` | Prettier write |
+| `npm run format:check` | Prettier check (used in CI) |
+| `npm run test` | Run Vitest |
+| `npm run test:watch` | Vitest in watch mode |
+| `npm run test:coverage` | Vitest with coverage |
+| `npm run typecheck` | `tsc --noEmit` |
+| `npm run validate` | Typecheck + lint + format check + tests |
+
+## Migration notes (1.0 → 2.0)
+
+This release is a full rewrite. There are no breaking _features_ — every capability of 1.0 is still present, plus a much larger set of new ones — but every implementation file changed:
+
+- **Node 24 LTS** (`.nvmrc` pinned, `engines.node = ">=24"`).
+- **Manifest V2 → V3**: replaces `browserAction` and the persistent background page with `action` and a service worker.
+- **Vanilla JS → TypeScript 6 + React 19**: the panel UI is React inside a Shadow DOM, with strict typing.
+- **Build system**: `npm` + **Vite 8** + `@crxjs/vite-plugin` for HMR-friendly extension development.
+- **State**: **Zustand 5** for the panel store.
+- **Testing**: **Vitest 4** + happy-dom 20; 85 tests.
+- **Lint / format**: ESLint 9 flat config + `typescript-eslint@8` + Prettier 3.
+- **CI**: GitHub Actions runs typecheck, lint, format check, tests, and a production build on every push and PR.
+
+## License
+
+MIT — see [LICENSE](LICENSE).
diff --git a/background.js b/background.js
deleted file mode 100644
index c347245..0000000
--- a/background.js
+++ /dev/null
@@ -1,21 +0,0 @@
-chrome.runtime.onMessage.addListener(
- function (message) {
- if (message.url) {
- const cmptUrl = new URL(message.url);
-
- chrome.tabs.create({ url: cmptUrl.href });
- }
- }
-);
-
-chrome.browserAction.onClicked.addListener(function(tab) {
- chrome.tabs.executeScript(tab.id, {
- file: 'isClayPage.js',
- }, (result) => {
- if (result.pop()) {
- chrome.tabs.executeScript(tab.id, { file: 'highlight.js' });
- } else {
- chrome.browserAction.setPopup({ tabId: tab.id, popup: 'no-slip.html' });
- }
- });
-});
diff --git a/docs/screenshots/inspect.png b/docs/screenshots/inspect.png
new file mode 100644
index 0000000..30d6869
Binary files /dev/null and b/docs/screenshots/inspect.png differ
diff --git a/docs/screenshots/options.png b/docs/screenshots/options.png
new file mode 100644
index 0000000..ff5ad14
Binary files /dev/null and b/docs/screenshots/options.png differ
diff --git a/docs/screenshots/tree.png b/docs/screenshots/tree.png
new file mode 100644
index 0000000..ca072eb
Binary files /dev/null and b/docs/screenshots/tree.png differ
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..f86fa6e
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,43 @@
+import js from '@eslint/js';
+import tseslint from 'typescript-eslint';
+import react from 'eslint-plugin-react';
+import reactHooks from 'eslint-plugin-react-hooks';
+import prettier from 'eslint-config-prettier';
+import globals from 'globals';
+
+export default tseslint.config(
+ { ignores: ['dist', 'coverage', 'node_modules', '*.config.js', '*.config.ts', 'scripts'] },
+ js.configs.recommended,
+ ...tseslint.configs.recommended,
+ {
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2022,
+ globals: { ...globals.browser, ...globals.webextensions },
+ parserOptions: { ecmaFeatures: { jsx: true } },
+ },
+ plugins: {
+ react,
+ 'react-hooks': reactHooks,
+ },
+ settings: { react: { version: '18.3' } },
+ rules: {
+ ...react.configs.recommended.rules,
+ ...reactHooks.configs.recommended.rules,
+ 'react/react-in-jsx-scope': 'off',
+ 'react/prop-types': 'off',
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
+ ],
+ '@typescript-eslint/consistent-type-imports': 'error',
+ },
+ },
+ {
+ files: ['tests/**/*.{ts,tsx}'],
+ languageOptions: {
+ globals: { ...globals.browser, ...globals.node },
+ },
+ },
+ prettier
+);
diff --git a/highlight.css b/highlight.css
deleted file mode 100644
index e272252..0000000
--- a/highlight.css
+++ /dev/null
@@ -1,91 +0,0 @@
-.slip-buttons {
- background: #fff;
- border: 1px solid #000;
- bottom: 40px;
- padding: 20px;
- position: fixed;
- right: 40px;
- text-align: left;
- z-index: 1000000000000;
-}
-
-.slip-buttons .info-label {
- color: #999;
- display: block;
- font: bold 12px/20px Helvetica, sans-serif;
- text-transform: uppercase;
-}
-
-.slip-buttons a {
- border: 1px solid;
- color: #000 !important;
- display: inline-block;
- font: 14px/20px Helvetica, sans-serif;
- margin: 10px 5px 0 0;
- padding: 5px;
- text-decoration: none;
-}
-
-.slip-buttons a:hover,
-.slip-buttons a:focus {
- color: blue !important;
-}
-
-.slip-buttons .page-name {
- display: block;
- font: bold 18px/20px Helvetica, sans-serif;
-}
-
-.slip-buttons .component-name {
- display: block;
- font: bold 16px/20px Helvetica, sans-serif;
-}
-
-.slip-buttons .page-actions {
- margin: 0 0 10px;
-}
-
-.slip-buttons .component-actions.no-selection .component-name {
- color: #999;
-}
-
-.slip-buttons .component-actions.no-selection a {
- display: none;
-}
-
-.slip-buttons:not(.published) .component-unpublished {
- display: none;
-}
-
-[data-uri].color-0 {
- outline: 2px solid #dda1a1 !important;
-}
-
-[data-uri].color-1 {
- outline: 3px dashed #dddda1 !important;
-}
-
-[data-uri].color-2 {
- outline: 4px dotted #b0dda1 !important;
-}
-
-[data-uri].color-3 {
- outline: 5px solid #a1dddd !important;
-}
-
-[data-uri].color-4 {
- outline: 4px dashed #a1a1dd !important;
-}
-
-[data-uri].color-5 {
- outline: 2px double #dda0dd !important;
-}
-
-[data-uri].slip-selected {
- outline: 5px solid #e22c2c !important;
-}
-
-.slip-hidden-input {
- position: fixed;
- top: -100px;
-}
diff --git a/highlight.js b/highlight.js
deleted file mode 100644
index 6064886..0000000
--- a/highlight.js
+++ /dev/null
@@ -1,238 +0,0 @@
-var components = document.querySelectorAll('[data-uri]'),
- pageUri = document.querySelector('html').getAttribute('data-uri'),
- layoutUri = document.querySelector('html').getAttribute('data-layout-uri'),
- hiddenInput = document.createElement('input'),
- keystroke = '',
- selectedComponent,
- hiddenInput,
- infoContainer;
-
-hiddenInput.classList.add('slip-hidden-input');
-document.querySelector('body').appendChild(hiddenInput);
-addInfoContainer();
-
-components.forEach(addEvent);
-addHierarchyClass();
-
-document.addEventListener('keydown', getKeyInput);
-document.addEventListener('keyup', evaluateKeystroke);
-
-function addButtons() {
- const buttonContainer = document.createElement('div'),
- dataButton = document.createElement('a'),
- htmlButton = document.createElement('a'),
- unpublishedButton = document.createElement('a');
-
- buttonContainer.classList.add('component-actions');
- buttonContainer.classList.add('no-selection');
- buttonContainer.innerHTML = 'No Component Selected '
- dataButton.setAttribute('target', '_blank');
- dataButton.classList.add('component-data');
- dataButton.innerHTML = 'Data';
- buttonContainer.appendChild(dataButton);
- htmlButton.setAttribute('target', '_blank');
- htmlButton.innerHTML = 'HTML';
- htmlButton.classList.add('component-html');
- buttonContainer.appendChild(htmlButton);
- unpublishedButton.setAttribute('target', '_blank');
- unpublishedButton.innerHTML = 'Unpublished Data';
- unpublishedButton.classList.add('component-unpublished');
- buttonContainer.appendChild(unpublishedButton);
-
- addButtonEvents(buttonContainer);
-
- return buttonContainer;
-}
-
-function addButtonEvents(container) {
- Array.from(container.childNodes).forEach((child) => {
- child.addEventListener('click', (e) => { e.stopPropagation(); return true });
- });
-}
-
-function toTitleCase(string) {
- if (!string) { return; }
-
- let str = string.replace(/\-/g, ' ');
-
- str = str.toLowerCase().split(' ');
-
- for (var i = 0; i < str.length; i++) {
- str[i] = str[i].charAt(0).toUpperCase() + str[i].slice(1);
- }
-
- return str.join(' ');
-}
-
-function getComponentName(uri) {
- const result = /_components\/(.+?)[\/\.]/.exec(uri) || /_components\/(.*)/.exec(uri);
-
- return result && result[1];
-}
-
-function getInstance(uri) {
- const result = /\/_components\/.+?\/instances\/([^\.@]+)/.exec(uri);
-
- return result && result[1];
-}
-
-function getPageInstance(uri) {
- const result = /\/_pages\/([^\.\/]+)/.exec(uri);
-
- return result && result[1];
-}
-
-function addInfoContainer() {
- infoContainer = document.createElement('div');
-
- var pageInfoContainer = document.createElement('div'),
- pageButton = document.createElement('a'),
- layoutButton = document.createElement('a'),
- metaButton = document.createElement('a'),
- isPublished = pageUri.indexOf('@published') > -1,
- pageInstance = getPageInstance(pageUri).replace('@published', ''),
- uPageButton = document.createElement('a'),
- uLayoutButton = document.createElement('a');
-
- if (isPublished) {
- infoContainer.classList.add('published');
- }
-
- pageInfoContainer.innerHTML = `${isPublished ? 'Published' : 'Unpublished'} Page${pageInstance ? ` (${pageInstance})` : ''} `;
- pageButton.setAttribute('href', `//${pageUri}`);
- pageButton.setAttribute('target', '_blank');
- pageButton.innerHTML = 'Page';
- pageInfoContainer.appendChild(pageButton);
- metaButton.setAttribute('href', `//${pageUri}/meta`);
- metaButton.setAttribute('target', '_blank');
- metaButton.innerHTML = 'Metadata';
- pageInfoContainer.appendChild(metaButton);
- layoutButton.setAttribute('href', `//${layoutUri}`);
- layoutButton.setAttribute('target', '_blank');
- layoutButton.innerHTML = 'Layout';
- pageInfoContainer.appendChild(layoutButton);
- pageInfoContainer.classList.add('page-actions');
- uPageButton.setAttribute('href', `//${pageUri.replace('@published', '')}`);
- uPageButton.setAttribute('target', '_blank');
- uPageButton.innerHTML = 'Unpublished Page';
- uPageButton.classList.add('component-unpublished');
- pageInfoContainer.appendChild(uPageButton);
- uLayoutButton.setAttribute('href', `//${layoutUri.replace('@published', '')}`);
- uLayoutButton.setAttribute('target', '_blank');
- uLayoutButton.innerHTML = 'Unpublished Layout';
- uLayoutButton.classList.add('component-unpublished');
- pageInfoContainer.appendChild(uLayoutButton);
-
- infoContainer.appendChild(pageInfoContainer);
- infoContainer.appendChild(addButtons());
- infoContainer.classList.add('slip-buttons');
-
- addButtonEvents(infoContainer);
-
- document.querySelector('body').appendChild(infoContainer);
-}
-
-function updateButtonValues(uri) {
- const label = infoContainer.querySelector('span.component-name'),
- container = infoContainer.querySelector('.component-actions'),
- dataButton = container.querySelector('.component-data'),
- htmlButton = container.querySelector('.component-html'),
- unpubButton = container.querySelector('.component-unpublished');
-
- let name, instance;
-
- if (uri.indexOf('/_pages/') > -1) {
- label.innerHTML = 'No Component Selected';
- container.classList.add('no-selection');
-
- return;
- }
-
- name = toTitleCase(getComponentName(uri));
- instance = getInstance(uri);
-
- label.innerHTML = `${name} (${instance})`;
- dataButton.setAttribute('href', `//${uri}`);
- htmlButton.setAttribute('href', `//${uri}.html`);
- unpubButton.setAttribute('href', `//${uri.replace('@published', '')}`);
-
- container.classList.remove('no-selection');
-}
-
-function evaluateKeystroke(e) {
- if (keystroke === e.key) {
- return;
- }
-
- if (e.key === 'p') {
- hiddenInput.value = pageUri;
- } else if (e.key === 'c' && selectedComponent) {
- hiddenInput.value = selectedComponent.getAttribute('data-uri');
- }
-
- if (keystroke === 'y') {
- copyInput();
- keystroke = '';
- } else if (keystroke === 'o') {
- uri = hiddenInput.value;
- opts = { url: `http://${uri}` };
- chrome.runtime.sendMessage(opts);
- keystroke = '';
- }
-}
-
-function getKeyInput(e) {
- if (e.key === 'y') {
- keystroke = 'y';
- } else if (e.key === 'o') {
- keystroke = 'o';
- }
-}
-
-function copyInput() {
- hiddenInput.select();
- document.execCommand('copy');
- hiddenInput.blur();
-}
-
-function addEvent(el) {
- el.addEventListener('click', clickEvent);
-}
-
-function addHierarchyClass() {
- let lastElem,
- i = 0;
-
- components.forEach(function(el) {
- if (lastElem && el.parentNode !== lastElem.parentNode) {
- i += 1;
- }
- el.classList.add(`color-${i % 5}`);
-
- lastElem = el;
- });
-}
-
-
-function clickEvent(e) {
- e.stopPropagation();
- e.preventDefault();
- var uri = this.getAttribute('data-uri');
-
- if (selectedComponent) {
- selectedComponent.classList.toggle('slip-selected');
- }
-
- selectedComponent = this;
- selectedComponent.classList.add('slip-selected');
- hiddenInput.value = uri;
-
- updateButtonValues(uri);
-
- if (e.altKey || e.shiftKey) {
- var opts;
-
- opts = { url: `http://${uri}${e.altKey ? '.json' : ''}` };
- chrome.runtime.sendMessage(opts);
- }
-}
diff --git a/isClayPage.js b/isClayPage.js
deleted file mode 100644
index 1bebb8c..0000000
--- a/isClayPage.js
+++ /dev/null
@@ -1,5 +0,0 @@
-function isClayPage() {
- return !!document.querySelector('html').getAttribute('data-uri');
-}
-
-isClayPage();
diff --git a/manifest.json b/manifest.json
deleted file mode 100644
index 9b620b4..0000000
--- a/manifest.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "name": "Clay Slip",
- "version": "1.0",
- "manifest_version": 2,
- "content_scripts": [
- {
- "matches": [
- ""
- ],
- "css": ["highlight.css"]
- }
- ],
- "background": {
- "scripts": ["background.js"],
- "persistent": false
- },
- "browser_action": {
- "name": "Activate Clay Slip",
- "default_icon": "clay.png"
- },
- "permissions": [
- "tabs",
- ""
- ]
-}
diff --git a/no-slip.html b/no-slip.html
deleted file mode 100644
index a4ff72e..0000000
--- a/no-slip.html
+++ /dev/null
@@ -1 +0,0 @@
-No Clay components found on this page :(
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..f1f6ed9
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,6240 @@
+{
+ "name": "clay-slip",
+ "version": "2.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "clay-slip",
+ "version": "2.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "zustand": "^5.0.13"
+ },
+ "devDependencies": {
+ "@crxjs/vite-plugin": "^2.4.0",
+ "@eslint/js": "^9.39.4",
+ "@types/chrome": "^0.1.42",
+ "@types/node": "^24.10.0",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "@vitest/coverage-v8": "^4.1.6",
+ "eslint": "^9.39.4",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "globals": "^17.6.0",
+ "happy-dom": "^20.9.0",
+ "prettier": "^3.8.3",
+ "typescript": "^6.0.3",
+ "typescript-eslint": "^8.59.3",
+ "vite": "^8.0.12",
+ "vitest": "^4.1.6"
+ },
+ "engines": {
+ "node": ">=24"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/code-frame/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/@babel/compat-data": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@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/core/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/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@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.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "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.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@crxjs/vite-plugin": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.4.0.tgz",
+ "integrity": "sha512-bDLdq0W2V1SkMQDJjrcYyjK9/uKtdl4joT7GRImcootCjZdKRiRYt+cv9z8tJoU/tK3o1lX48LTqN7JMsk5AQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^4.1.2",
+ "@webcomponents/custom-elements": "^1.5.0",
+ "acorn-walk": "^8.2.0",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.3.3",
+ "es-module-lexer": "^0.10.0",
+ "fast-glob": "^3.2.11",
+ "fs-extra": "^10.0.1",
+ "jsesc": "^3.0.2",
+ "magic-string": "^0.30.12",
+ "node-html-parser": "^7.0.2",
+ "pathe": "^2.0.1",
+ "picocolors": "^1.1.1",
+ "react-refresh": "^0.13.0",
+ "rollup": "2.79.2",
+ "rxjs": "7.5.7"
+ },
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "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.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.129.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz",
+ "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz",
+ "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz",
+ "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz",
+ "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz",
+ "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz",
+ "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz",
+ "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz",
+ "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz",
+ "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz",
+ "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz",
+ "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz",
+ "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz",
+ "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz",
+ "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz",
+ "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==",
+ "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.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz",
+ "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==",
+ "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.0-rc.7",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
+ "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
+ "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "estree-walker": "^2.0.1",
+ "picomatch": "^2.2.2"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "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/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "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/chrome": {
+ "version": "0.1.42",
+ "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.42.tgz",
+ "integrity": "sha512-tdT2roFqGecZZDjA9fUEAINb2STxSPifHMDvY6EfRjNRCjdrs/0FwKt5RCIA9MKMd1arAYZZL3nwEkp6ZLZu2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/filesystem": "*",
+ "@types/har-format": "*"
+ }
+ },
+ "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/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/filesystem": {
+ "version": "0.0.36",
+ "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz",
+ "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/filewriter": "*"
+ }
+ },
+ "node_modules/@types/filewriter": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz",
+ "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/har-format": {
+ "version": "1.2.16",
+ "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz",
+ "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==",
+ "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": "24.12.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz",
+ "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@types/whatwg-mimetype": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
+ "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.59.3",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
+ "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.59.3",
+ "@typescript-eslint/type-utils": "8.59.3",
+ "@typescript-eslint/utils": "8.59.3",
+ "@typescript-eslint/visitor-keys": "8.59.3",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.59.3",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.59.3",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
+ "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.59.3",
+ "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/typescript-estree": "8.59.3",
+ "@typescript-eslint/visitor-keys": "8.59.3",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.59.3",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
+ "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.59.3",
+ "@typescript-eslint/types": "^8.59.3",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.59.3",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
+ "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/visitor-keys": "8.59.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.59.3",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
+ "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.59.3",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
+ "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/typescript-estree": "8.59.3",
+ "@typescript-eslint/utils": "8.59.3",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.59.3",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
+ "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.59.3",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
+ "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.59.3",
+ "@typescript-eslint/tsconfig-utils": "8.59.3",
+ "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/visitor-keys": "8.59.3",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/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/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.59.3",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
+ "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.59.3",
+ "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/typescript-estree": "8.59.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.59.3",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
+ "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.3",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/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/@vitejs/plugin-react": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
+ "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-rc.7"
+ },
+ "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/coverage-v8": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.6.tgz",
+ "integrity": "sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/utils": "4.1.6",
+ "ast-v8-to-istanbul": "^1.0.0",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.2.0",
+ "magicast": "^0.5.2",
+ "obug": "^2.1.1",
+ "std-env": "^4.0.0-rc.1",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "4.1.6",
+ "vitest": "4.1.6"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz",
+ "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.6",
+ "@vitest/utils": "4.1.6",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz",
+ "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.6",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/mocker/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/@vitest/pretty-format": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz",
+ "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz",
+ "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.6",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz",
+ "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.6",
+ "@vitest/utils": "4.1.6",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz",
+ "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz",
+ "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.6",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils/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/@webcomponents/custom-elements": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz",
+ "integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "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/acorn-walk": {
+ "version": "8.3.5",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
+ "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.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-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/ast-v8-to-istanbul": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz",
+ "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul/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/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.29",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
+ "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001792",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
+ "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
+ "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/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "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-select": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "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==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/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/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/dom-serializer/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.354",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.354.tgz",
+ "integrity": "sha512-JaBHwWcfIdmSAfWM5l3uwjGd431j8YEMikZ+K/2nXVuBqJKyZ0f+2h4n4JY5AyNiZmnY9qQr2RU3v9DxDmHMNg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.24.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
+ "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz",
+ "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.2",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.1.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.3.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.5",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.10.5.tgz",
+ "integrity": "sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.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",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "10.1.8",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
+ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-config-prettier"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
+ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.9",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ }
+ },
+ "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-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "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": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.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-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "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/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "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/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.6.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
+ "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/happy-dom": {
+ "version": "20.9.0",
+ "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.9.0.tgz",
+ "integrity": "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": ">=20.0.0",
+ "@types/whatwg-mimetype": "^3.0.2",
+ "@types/ws": "^8.18.1",
+ "entities": "^7.0.1",
+ "whatwg-mimetype": "^3.0.0",
+ "ws": "^8.18.3"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "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-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "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/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/iterator.prototype": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+ "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "get-proto": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "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/jsonfile": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "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.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "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.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "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/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/loose-envify/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/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/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/magicast": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz",
+ "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "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.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "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-exports-info": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
+ "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array.prototype.flatmap": "^1.3.3",
+ "es-errors": "^1.3.0",
+ "object.entries": "^1.1.9",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/node-html-parser": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz",
+ "integrity": "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-select": "^5.1.0",
+ "he": "1.2.0"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.44",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
+ "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
+ "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
+ "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/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "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.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
+ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "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/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
+ "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
+ "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.6"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react-refresh": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz",
+ "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "2.0.0-next.6",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz",
+ "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "node-exports-info": "^1.6.0",
+ "object-keys": "^1.1.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz",
+ "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.129.0",
+ "@rolldown/pluginutils": "1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0",
+ "@rolldown/binding-darwin-arm64": "1.0.0",
+ "@rolldown/binding-darwin-x64": "1.0.0",
+ "@rolldown/binding-freebsd-x64": "1.0.0",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0",
+ "@rolldown/binding-linux-x64-musl": "1.0.0",
+ "@rolldown/binding-openharmony-arm64": "1.0.0",
+ "@rolldown/binding-wasm32-wasi": "1.0.0",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0"
+ }
+ },
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz",
+ "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "2.79.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
+ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.5.7",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz",
+ "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
+ "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "get-intrinsic": "^1.3.0",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+ "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "regexp.prototype.flags": "^1.5.3",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-object-atoms": "^1.0.0",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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.1.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
+ "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "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/tinyglobby/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/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "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/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0",
+ "reflect.getprototypeof": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.59.3",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
+ "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.59.3",
+ "@typescript-eslint/parser": "8.59.3",
+ "@typescript-eslint/typescript-estree": "8.59.3",
+ "@typescript-eslint/utils": "8.59.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.0.12",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz",
+ "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.14",
+ "rolldown": "1.0.0",
+ "tinyglobby": "^0.2.16"
+ },
+ "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.1.18",
+ "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/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz",
+ "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.6",
+ "@vitest/mocker": "4.1.6",
+ "@vitest/pretty-format": "4.1.6",
+ "@vitest/runner": "4.1.6",
+ "@vitest/snapshot": "4.1.6",
+ "@vitest/spy": "4.1.6",
+ "@vitest/utils": "4.1.6",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.6",
+ "@vitest/browser-preview": "4.1.6",
+ "@vitest/browser-webdriverio": "4.1.6",
+ "@vitest/coverage-istanbul": "4.1.6",
+ "@vitest/coverage-v8": "4.1.6",
+ "@vitest/ui": "4.1.6",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.20",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
+ "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/ws": {
+ "version": "8.20.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
+ "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.13",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz",
+ "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ee77224
--- /dev/null
+++ b/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "clay-slip",
+ "version": "2.0.0",
+ "description": "A modern Chrome extension for exploring Clay CMS pages — visualize component boundaries, inspect data, and navigate the page/layout hierarchy.",
+ "private": true,
+ "type": "module",
+ "license": "MIT",
+ "homepage": "https://github.com/clay/clay-devtools",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/clay/clay-devtools.git"
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc --noEmit && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --max-warnings=0",
+ "lint:fix": "eslint . --fix",
+ "format": "prettier --write \"**/*.{ts,tsx,js,json,css,md}\"",
+ "format:check": "prettier --check \"**/*.{ts,tsx,js,json,css,md}\"",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:coverage": "vitest run --coverage",
+ "typecheck": "tsc --noEmit",
+ "validate": "npm run typecheck && npm run lint && npm run format:check && npm run test"
+ },
+ "dependencies": {
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "zustand": "^5.0.13"
+ },
+ "devDependencies": {
+ "@crxjs/vite-plugin": "^2.4.0",
+ "@eslint/js": "^9.39.4",
+ "@types/chrome": "^0.1.42",
+ "@types/node": "^24.10.0",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "@vitest/coverage-v8": "^4.1.6",
+ "eslint": "^9.39.4",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "globals": "^17.6.0",
+ "happy-dom": "^20.9.0",
+ "prettier": "^3.8.3",
+ "typescript": "^6.0.3",
+ "typescript-eslint": "^8.59.3",
+ "vite": "^8.0.12",
+ "vitest": "^4.1.6"
+ },
+ "engines": {
+ "node": ">=24"
+ }
+}
diff --git a/public/icons/icon-128.png b/public/icons/icon-128.png
new file mode 100644
index 0000000..75e85fc
Binary files /dev/null and b/public/icons/icon-128.png differ
diff --git a/public/icons/icon-16.png b/public/icons/icon-16.png
new file mode 100644
index 0000000..8bc0a7b
Binary files /dev/null and b/public/icons/icon-16.png differ
diff --git a/public/icons/icon-32.png b/public/icons/icon-32.png
new file mode 100644
index 0000000..4833fab
Binary files /dev/null and b/public/icons/icon-32.png differ
diff --git a/public/icons/icon-48.png b/public/icons/icon-48.png
new file mode 100644
index 0000000..dfdd5f1
Binary files /dev/null and b/public/icons/icon-48.png differ
diff --git a/scripts/build-icons.mjs b/scripts/build-icons.mjs
new file mode 100644
index 0000000..e8d0176
--- /dev/null
+++ b/scripts/build-icons.mjs
@@ -0,0 +1,24 @@
+// One-shot helper to render the SVG icon at extension sizes.
+// Run with: npx --yes -p sharp@0.34 node scripts/build-icons.mjs
+import sharp from 'sharp';
+import { mkdir } from 'node:fs/promises';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const root = dirname(dirname(fileURLToPath(import.meta.url)));
+const svgPath = join(root, 'scripts', 'icon-source.svg');
+const outDir = join(root, 'public', 'icons');
+
+await mkdir(outDir, { recursive: true });
+
+const sizes = [16, 32, 48, 128];
+
+await Promise.all(
+ sizes.map((size) =>
+ sharp(svgPath)
+ .resize(size, size)
+ .png({ compressionLevel: 9 })
+ .toFile(join(outDir, `icon-${size}.png`))
+ .then(() => console.log(`wrote icon-${size}.png`))
+ )
+);
diff --git a/scripts/icon-source.svg b/scripts/icon-source.svg
new file mode 100644
index 0000000..7b4bafd
--- /dev/null
+++ b/scripts/icon-source.svg
@@ -0,0 +1,14 @@
+
+
+ S
+
diff --git a/clay.png b/src/assets/clay-icon.png
similarity index 100%
rename from clay.png
rename to src/assets/clay-icon.png
diff --git a/src/background/service-worker.ts b/src/background/service-worker.ts
new file mode 100644
index 0000000..6ee9fec
--- /dev/null
+++ b/src/background/service-worker.ts
@@ -0,0 +1,84 @@
+import type { CaptureResponse, RuntimeMessage } from '@/lib/types';
+
+const BADGE_BG = '#e22c2c';
+const POPUP_PATH = 'src/popup/index.html';
+
+chrome.runtime.onInstalled.addListener(() => {
+ chrome.action.setBadgeBackgroundColor({ color: BADGE_BG });
+});
+
+/**
+ * Reset the popup whenever a tab starts navigating. The new page's content
+ * script will re-disable the popup with CLAY_DETECTED if the destination is a
+ * Clay page; otherwise the popup stays active and the icon click shows the
+ * "Not a Clay page" message.
+ */
+chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
+ if (changeInfo.status === 'loading') {
+ chrome.action.setPopup({ tabId, popup: POPUP_PATH }).catch(() => undefined);
+ chrome.action.setBadgeText({ tabId, text: '' }).catch(() => undefined);
+ }
+});
+
+/**
+ * On Clay pages the popup is disabled (so this handler fires); on non-Clay
+ * pages the default popup shows automatically and this never runs.
+ */
+chrome.action.onClicked.addListener((tab) => {
+ if (!tab.id) return;
+ chrome.tabs.sendMessage(tab.id, { type: 'PANEL_TOGGLE' } satisfies RuntimeMessage).catch(() => {
+ // Content script not loaded (e.g. chrome:// pages). Ignore.
+ });
+});
+
+chrome.runtime.onMessage.addListener((message: RuntimeMessage, sender, sendResponse) => {
+ switch (message.type) {
+ case 'OPEN_TAB': {
+ chrome.tabs.create({ url: message.url, active: true });
+ sendResponse({ ok: true });
+ break;
+ }
+ case 'OPEN_OPTIONS': {
+ chrome.runtime.openOptionsPage().catch(() => undefined);
+ sendResponse({ ok: true });
+ break;
+ }
+ case 'UPDATE_BADGE': {
+ const tabId = message.tabId ?? sender.tab?.id;
+ if (typeof tabId === 'number') {
+ const text = message.count > 0 ? String(message.count) : '';
+ chrome.action.setBadgeText({ text, tabId }).catch(() => undefined);
+ }
+ sendResponse({ ok: true });
+ break;
+ }
+ case 'CLAY_DETECTED': {
+ const tabId = sender.tab?.id;
+ if (typeof tabId === 'number') {
+ chrome.action.setPopup({ tabId, popup: '' }).catch(() => undefined);
+ }
+ sendResponse({ ok: true });
+ break;
+ }
+ case 'CAPTURE_TAB': {
+ const windowId = sender.tab?.windowId;
+ if (typeof windowId !== 'number') {
+ sendResponse({ ok: false, error: 'No window id' } satisfies CaptureResponse);
+ break;
+ }
+ chrome.tabs
+ .captureVisibleTab(windowId, { format: 'png' })
+ .then((dataUrl) => sendResponse({ ok: true, dataUrl } satisfies CaptureResponse))
+ .catch((err: unknown) =>
+ sendResponse({
+ ok: false,
+ error: err instanceof Error ? err.message : String(err),
+ } satisfies CaptureResponse)
+ );
+ return true;
+ }
+ default:
+ break;
+ }
+ return true;
+});
diff --git a/src/content/highlighter.ts b/src/content/highlighter.ts
new file mode 100644
index 0000000..ec37aea
--- /dev/null
+++ b/src/content/highlighter.ts
@@ -0,0 +1,129 @@
+/**
+ * Manages outline highlighting for Clay components in the host page.
+ * Uses a single style element scoped via data-attribute selectors so we never
+ * mutate inline styles on host elements.
+ */
+const STYLE_ID = 'clay-slip-highlight-styles';
+const HIGHLIGHT_ATTR = 'data-clay-slip-color';
+const SELECTED_ATTR = 'data-clay-slip-selected';
+const HOVER_ATTR = 'data-clay-slip-hover';
+const ANNOTATED_ATTR = 'data-clay-slip-annotated';
+const MATCH_ATTR = 'data-clay-slip-match';
+const FILTER_MODE_ATTR = 'data-clay-slip-filtering';
+const OPACITY_VAR = '--clay-slip-outline-opacity';
+const DEFAULT_OPACITY = 0.85;
+
+interface PaletteEntry {
+ readonly r: number;
+ readonly g: number;
+ readonly b: number;
+ readonly style: string;
+ readonly width: number;
+}
+
+const PALETTE: ReadonlyArray = [
+ { r: 221, g: 161, b: 161, style: 'solid', width: 2 },
+ { r: 221, g: 221, b: 161, style: 'dashed', width: 3 },
+ { r: 176, g: 221, b: 161, style: 'dotted', width: 4 },
+ { r: 161, g: 221, b: 221, style: 'solid', width: 5 },
+ { r: 161, g: 161, b: 221, style: 'dashed', width: 4 },
+ { r: 221, g: 160, b: 221, style: 'double', width: 4 },
+];
+
+export function installHighlightStyles(): void {
+ if (document.getElementById(STYLE_ID)) return;
+ const style = document.createElement('style');
+ style.id = STYLE_ID;
+ style.textContent = buildStyleSheet();
+ document.head.appendChild(style);
+ setHighlightOpacity(DEFAULT_OPACITY);
+}
+
+function buildStyleSheet(): string {
+ const colorRules = PALETTE.map(
+ (p, i) =>
+ `[${HIGHLIGHT_ATTR}="${i}"]{outline:${p.width}px ${p.style} rgba(${p.r},${p.g},${p.b},var(${OPACITY_VAR},${DEFAULT_OPACITY})) !important;outline-offset:-${p.width}px !important;}`
+ ).join('\n');
+
+ return `
+ ${colorRules}
+ [${HOVER_ATTR}]{outline:3px solid rgba(255,175,58,var(${OPACITY_VAR},${DEFAULT_OPACITY})) !important;outline-offset:-3px !important;}
+ [${SELECTED_ATTR}]{outline:5px solid rgba(226,44,44,var(${OPACITY_VAR},${DEFAULT_OPACITY})) !important;outline-offset:-5px !important;}
+ [${ANNOTATED_ATTR}]{position:relative;}
+ [${ANNOTATED_ATTR}]::before{
+ content:"";position:absolute;top:4px;right:4px;width:10px;height:10px;border-radius:50%;
+ background:rgba(245,158,11,0.95);box-shadow:0 0 0 2px rgba(255,255,255,0.85);
+ pointer-events:none;z-index:2147483646;
+ }
+ html[${FILTER_MODE_ATTR}] [data-uri]:not([${MATCH_ATTR}]){opacity:0.25 !important;transition:opacity 0.12s;}
+ [${MATCH_ATTR}]{outline:3px solid rgba(34,197,94,var(${OPACITY_VAR},${DEFAULT_OPACITY})) !important;outline-offset:-3px !important;}
+ `;
+}
+
+export function applyHighlights(elements: HTMLElement[]): void {
+ let lastParent: ParentNode | null = null;
+ let colorIdx = 0;
+ for (const el of elements) {
+ if (lastParent && el.parentNode !== lastParent) {
+ colorIdx = (colorIdx + 1) % PALETTE.length;
+ }
+ el.setAttribute(HIGHLIGHT_ATTR, String(colorIdx));
+ lastParent = el.parentNode;
+ }
+}
+
+export function clearHighlights(elements: HTMLElement[]): void {
+ for (const el of elements) {
+ el.removeAttribute(HIGHLIGHT_ATTR);
+ el.removeAttribute(SELECTED_ATTR);
+ el.removeAttribute(HOVER_ATTR);
+ el.removeAttribute(ANNOTATED_ATTR);
+ el.removeAttribute(MATCH_ATTR);
+ }
+ document.documentElement.removeAttribute(FILTER_MODE_ATTR);
+}
+
+export function setSelected(prev: HTMLElement | null, next: HTMLElement | null): void {
+ if (prev) prev.removeAttribute(SELECTED_ATTR);
+ if (next) next.setAttribute(SELECTED_ATTR, '');
+}
+
+export function setHovered(prev: HTMLElement | null, next: HTMLElement | null): void {
+ if (prev) prev.removeAttribute(HOVER_ATTR);
+ if (next) next.setAttribute(HOVER_ATTR, '');
+}
+
+export function setHighlightingEnabled(enabled: boolean): void {
+ const style = document.getElementById(STYLE_ID) as HTMLStyleElement | null;
+ if (!style) return;
+ style.disabled = !enabled;
+}
+
+export function setHighlightOpacity(opacity: number): void {
+ const clamped = Math.max(0, Math.min(1, opacity));
+ document.documentElement.style.setProperty(OPACITY_VAR, String(clamped));
+}
+
+export function setAnnotatedUris(allElements: HTMLElement[], annotatedUris: Set): void {
+ for (const el of allElements) {
+ const uri = el.getAttribute('data-uri');
+ if (uri && annotatedUris.has(uri)) el.setAttribute(ANNOTATED_ATTR, '');
+ else el.removeAttribute(ANNOTATED_ATTR);
+ }
+}
+
+export function setFindMatches(
+ allElements: HTMLElement[],
+ matchSet: Set | null
+): void {
+ if (!matchSet || matchSet.size === 0) {
+ document.documentElement.removeAttribute(FILTER_MODE_ATTR);
+ for (const el of allElements) el.removeAttribute(MATCH_ATTR);
+ return;
+ }
+ document.documentElement.setAttribute(FILTER_MODE_ATTR, '');
+ for (const el of allElements) {
+ if (matchSet.has(el)) el.setAttribute(MATCH_ATTR, '');
+ else el.removeAttribute(MATCH_ATTR);
+ }
+}
diff --git a/src/content/index.ts b/src/content/index.ts
new file mode 100644
index 0000000..2f13ae5
--- /dev/null
+++ b/src/content/index.ts
@@ -0,0 +1,80 @@
+import { isClayDocument, parseShareTarget } from '@/lib/clay-uri';
+import type { RuntimeMessage } from '@/lib/types';
+import {
+ applyHighlights,
+ clearHighlights,
+ installHighlightStyles,
+ setSelected,
+} from './highlighter';
+import { readComponents } from './page-info';
+import { isPanelMounted, mountPanel, unmountPanel } from './shadow-host';
+import { useStore } from './panel/store';
+
+function send(message: RuntimeMessage): void {
+ chrome.runtime.sendMessage(message).catch(() => undefined);
+}
+
+function paintAndSync(): number {
+ installHighlightStyles();
+ const components = readComponents();
+ applyHighlights(components.map((c) => c.element));
+ useStore.getState().setComponents(components);
+ return components.length;
+}
+
+function handleDeepLink(): void {
+ const target = parseShareTarget(location.href);
+ if (!target) return;
+ const components = useStore.getState().components;
+ const match = components.find((c) => c.uri === target);
+ if (!match) return;
+ setSelected(null, match.element);
+ useStore.getState().setSelected(match);
+ match.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+}
+
+function bootstrap(): void {
+ if (!isClayDocument()) {
+ send({ type: 'UPDATE_BADGE', count: 0 });
+ return;
+ }
+ send({ type: 'CLAY_DETECTED' });
+ const count = paintAndSync();
+ send({ type: 'UPDATE_BADGE', count });
+
+ // Auto-mount on every Clay page. The panel boots into its collapsed state
+ // (the floating Clay button); the user clicks the FAB to expand. This is
+ // the standard pattern for in-page extension chrome (Sentry/Hotjar/Crisp).
+ if (!isPanelMounted()) mountPanel();
+
+ // If the user landed via a Slip share link, also auto-expand + select.
+ if (parseShareTarget(location.href)) {
+ useStore.getState().toggleCollapsed();
+ setTimeout(handleDeepLink, 50);
+ }
+}
+
+chrome.runtime.onMessage.addListener((message: RuntimeMessage, _sender, sendResponse) => {
+ if (message.type === 'PANEL_TOGGLE') {
+ if (!isClayDocument()) {
+ sendResponse({ ok: false, reason: 'not-clay' });
+ return true;
+ }
+ if (isPanelMounted()) {
+ const components = useStore.getState().components.map((c) => c.element);
+ clearHighlights(components);
+ unmountPanel();
+ } else {
+ paintAndSync();
+ mountPanel();
+ }
+ sendResponse({ ok: true });
+ }
+ return true;
+});
+
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', bootstrap);
+} else {
+ bootstrap();
+}
diff --git a/src/content/page-info.ts b/src/content/page-info.ts
new file mode 100644
index 0000000..d16d711
--- /dev/null
+++ b/src/content/page-info.ts
@@ -0,0 +1,60 @@
+import { getDisplayName, getInstance, getPageInstance, isPublished } from '@/lib/clay-uri';
+import type { ClayComponentInfo, ClayPageInfo } from '@/lib/types';
+
+export function readPageInfo(): ClayPageInfo | null {
+ const html = document.documentElement;
+ const pageUri = html.getAttribute('data-uri');
+ if (!pageUri) return null;
+
+ return {
+ pageUri,
+ layoutUri: html.getAttribute('data-layout-uri'),
+ isPublished: isPublished(pageUri),
+ pageInstance: getPageInstance(pageUri),
+ };
+}
+
+export function readComponents(): ClayComponentInfo[] {
+ const elements = Array.from(document.querySelectorAll('[data-uri]')).filter(
+ (el) => el !== document.documentElement
+ );
+
+ return elements.map((element) => {
+ const uri = element.getAttribute('data-uri') ?? '';
+ return {
+ uri,
+ name: getComponentNameFromUri(uri),
+ displayName: getDisplayName(uri),
+ instance: getInstance(uri),
+ element,
+ depth: computeDepth(element),
+ };
+ });
+}
+
+function getComponentNameFromUri(uri: string): string {
+ const match = /_components\/([^/.]+)/.exec(uri);
+ return match?.[1] ?? 'unknown';
+}
+
+function computeDepth(element: HTMLElement): number {
+ let depth = 0;
+ let parent = element.parentElement;
+ while (parent && parent !== document.documentElement) {
+ if (parent.hasAttribute('data-uri')) depth += 1;
+ parent = parent.parentElement;
+ }
+ return depth;
+}
+
+export function getNestingPath(target: HTMLElement): HTMLElement[] {
+ const path: HTMLElement[] = [];
+ let current: HTMLElement | null = target;
+ while (current && current !== document.documentElement) {
+ if (current.hasAttribute('data-uri') && current !== target) {
+ path.unshift(current);
+ }
+ current = current.parentElement;
+ }
+ return path;
+}
diff --git a/src/content/panel/App.tsx b/src/content/panel/App.tsx
new file mode 100644
index 0000000..bd9cc17
--- /dev/null
+++ b/src/content/panel/App.tsx
@@ -0,0 +1,135 @@
+import { useEffect, useRef } from 'react';
+import { loadPreferences, onPreferencesChanged } from '@/lib/storage';
+import { listAnnotations, onAnnotationsChanged } from '@/lib/annotations';
+import { loadRecents, onRecentsChanged, pushRecent } from '@/lib/recents';
+import { setAnnotatedUris, setHighlightingEnabled, setHighlightOpacity } from '../highlighter';
+import { useStore } from './store';
+import { useDraggable } from './hooks/useDraggable';
+import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
+import { useThemedRoot } from './hooks/useThemedRoot';
+import { useElementSelection } from './hooks/useElementSelection';
+import { Fab } from './components/Fab';
+import { Header } from './components/Header';
+import { Tabs } from './components/Tabs';
+import { PageInfo } from './components/PageInfo';
+import { ComponentDetails } from './components/ComponentDetails';
+import { ComponentTree } from './components/ComponentTree';
+import { JsonPreview } from './components/JsonPreview';
+import { DiffView } from './components/DiffView';
+import { EnvironmentSwitcher } from './components/EnvironmentSwitcher';
+import { ShortcutOverlay } from './components/ShortcutOverlay';
+import { Toasts } from './components/Toasts';
+import { ResizeHandle } from './components/ResizeHandle';
+import { RecentList } from './components/RecentList';
+import { NotesTab } from './components/NotesTab';
+import { SeoTab } from './components/SeoTab';
+
+export function App() {
+ const headerRef = useRef(null);
+ const collapsed = useStore((s) => s.collapsed);
+ const activeTab = useStore((s) => s.activeTab);
+ const corner = useStore((s) => s.preferences.panelPosition);
+ const panelWidth = useStore((s) => s.preferences.panelWidth);
+ const panelHeight = useStore((s) => s.preferences.panelHeight);
+ const highlightOpacity = useStore((s) => s.preferences.highlightOpacity);
+ const highlightEnabled = useStore((s) => s.highlightEnabled);
+ const setPrefs = useStore((s) => s.setPreferences);
+ const setRecents = useStore((s) => s.setRecents);
+ const setAnnotations = useStore((s) => s.setAnnotations);
+ const selected = useStore((s) => s.selected);
+ const annotatedUris = useStore((s) => s.annotatedUris);
+ const components = useStore((s) => s.components);
+ const maxRecent = useStore((s) => s.preferences.maxRecentComponents);
+
+ const { style: themeStyle } = useThemedRoot();
+ const { style: positionStyle } = useDraggable(headerRef, corner, panelWidth, panelHeight);
+ const isSideDock = corner === 'left-side' || corner === 'right-side';
+
+ useKeyboardShortcuts();
+ useElementSelection();
+
+ useEffect(() => {
+ loadPreferences().then((prefs) => setPrefs(prefs));
+ return onPreferencesChanged((prefs) => setPrefs(prefs));
+ }, [setPrefs]);
+
+ useEffect(() => {
+ loadRecents().then(setRecents);
+ return onRecentsChanged(setRecents);
+ }, [setRecents]);
+
+ useEffect(() => {
+ listAnnotations().then(setAnnotations);
+ return onAnnotationsChanged(setAnnotations);
+ }, [setAnnotations]);
+
+ useEffect(() => {
+ setHighlightOpacity(highlightOpacity);
+ }, [highlightOpacity]);
+
+ useEffect(() => {
+ setHighlightingEnabled(highlightEnabled);
+ }, [highlightEnabled]);
+
+ // Sync the annotation dot indicators on the page whenever either set changes.
+ useEffect(() => {
+ setAnnotatedUris(
+ components.map((c) => c.element),
+ annotatedUris
+ );
+ }, [components, annotatedUris]);
+
+ // Push every selection into the recents list (deduped + capped).
+ useEffect(() => {
+ if (!selected) return;
+ void pushRecent(
+ {
+ uri: selected.uri,
+ displayName: selected.displayName,
+ instance: selected.instance,
+ pageUrl: location.href,
+ pageTitle: document.title,
+ visitedAt: Date.now(),
+ },
+ maxRecent
+ );
+ }, [selected, maxRecent]);
+
+ if (collapsed) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {activeTab === 'inspect' && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+ {activeTab === 'tree' &&
}
+ {activeTab === 'json' &&
}
+ {activeTab === 'diff' &&
}
+ {activeTab === 'seo' &&
}
+ {activeTab === 'notes' &&
}
+
+
+ {!isSideDock &&
}
+ {!isSideDock &&
}
+
+
+
+ );
+}
diff --git a/src/content/panel/components/AnnotationEditor.tsx b/src/content/panel/components/AnnotationEditor.tsx
new file mode 100644
index 0000000..c7b6bf0
--- /dev/null
+++ b/src/content/panel/components/AnnotationEditor.tsx
@@ -0,0 +1,96 @@
+import { useEffect, useState } from 'react';
+import { deleteAnnotation, getAnnotation, upsertAnnotation } from '@/lib/annotations';
+import { useStore } from '../store';
+
+interface Props {
+ uri: string;
+ displayName: string;
+}
+
+export function AnnotationEditor({ uri, displayName }: Props) {
+ const [draft, setDraft] = useState('');
+ const [savedNote, setSavedNote] = useState('');
+ const [savedAt, setSavedAt] = useState(null);
+ const [prevUri, setPrevUri] = useState(uri);
+ const pushToast = useStore((s) => s.pushToast);
+
+ if (prevUri !== uri) {
+ setPrevUri(uri);
+ setDraft('');
+ setSavedNote('');
+ setSavedAt(null);
+ }
+
+ useEffect(() => {
+ let cancelled = false;
+ getAnnotation(uri).then((a) => {
+ if (cancelled) return;
+ setSavedNote(a?.note ?? '');
+ setDraft(a?.note ?? '');
+ setSavedAt(a?.updatedAt ?? null);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [uri]);
+
+ const dirty = draft !== savedNote;
+
+ const save = async () => {
+ const trimmed = draft.trim();
+ if (!trimmed) {
+ await deleteAnnotation(uri);
+ setSavedNote('');
+ setSavedAt(null);
+ pushToast('Note deleted', 'info');
+ return;
+ }
+ const a = await upsertAnnotation({
+ uri,
+ note: trimmed,
+ displayName,
+ pageUrl: location.href,
+ pageTitle: document.title,
+ });
+ setSavedNote(a.note);
+ setSavedAt(a.updatedAt);
+ pushToast('Note saved', 'success');
+ };
+
+ return (
+
+
+ Note
+ {savedAt && (
+ saved {new Date(savedAt).toLocaleString()}
+ )}
+
+
+ );
+}
diff --git a/src/content/panel/components/Breadcrumb.tsx b/src/content/panel/components/Breadcrumb.tsx
new file mode 100644
index 0000000..8e4544b
--- /dev/null
+++ b/src/content/panel/components/Breadcrumb.tsx
@@ -0,0 +1,44 @@
+import { useMemo } from 'react';
+import { getDisplayName } from '@/lib/clay-uri';
+import { getNestingPath } from '../../page-info';
+import { setSelected } from '../../highlighter';
+import { useStore } from '../store';
+
+export function Breadcrumb() {
+ const selected = useStore((s) => s.selected);
+ const components = useStore((s) => s.components);
+ const setSelectedStore = useStore((s) => s.setSelected);
+
+ const trail = useMemo(() => {
+ if (!selected) return [];
+ return getNestingPath(selected.element);
+ }, [selected]);
+
+ if (!selected || trail.length === 0) return null;
+
+ return (
+
+ {trail.map((el, i) => {
+ const uri = el.getAttribute('data-uri') ?? '';
+ const info = components.find((c) => c.element === el);
+ return (
+
+ {
+ if (!info) return;
+ setSelected(selected.element, info.element);
+ setSelectedStore(info);
+ info.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }}
+ >
+ {getDisplayName(uri)}
+
+ /
+
+ );
+ })}
+ {selected.displayName}
+
+ );
+}
diff --git a/src/content/panel/components/ComponentDetails.tsx b/src/content/panel/components/ComponentDetails.tsx
new file mode 100644
index 0000000..4945665
--- /dev/null
+++ b/src/content/panel/components/ComponentDetails.tsx
@@ -0,0 +1,124 @@
+import {
+ buildCurlCommand,
+ buildSchemaUrl,
+ buildUrl,
+ copyAsCssSelector,
+ copyAsFetchSnippet,
+ unpublishedUri,
+} from '@/lib/clay-uri';
+import { copyToClipboard } from '@/lib/clipboard';
+import { captureElementToClipboard } from '@/lib/screenshot';
+import type { RuntimeMessage } from '@/lib/types';
+import { getPanelHost } from '../../shadow-host';
+import { useEnvHost, useStore } from '../store';
+import { Icon } from './Icon';
+import { Breadcrumb } from './Breadcrumb';
+import { AnnotationEditor } from './AnnotationEditor';
+import { ShareMenu } from './ShareMenu';
+
+export function ComponentDetails() {
+ const selected = useStore((s) => s.selected);
+ const pushToast = useStore((s) => s.pushToast);
+ const envHost = useEnvHost();
+
+ if (!selected) {
+ return (
+
+ Click any component on the page to inspect it.
+
+ );
+ }
+
+ const open = (url: string) => {
+ chrome.runtime.sendMessage({ type: 'OPEN_TAB', url } satisfies RuntimeMessage);
+ };
+
+ const copy = async (text: string, label: string) => {
+ const ok = await copyToClipboard(text);
+ pushToast(ok ? `${label} copied` : 'Copy failed', ok ? 'success' : 'error');
+ };
+
+ const screenshot = async () => {
+ try {
+ const ok = await captureElementToClipboard(selected.element, getPanelHost());
+ pushToast(
+ ok ? 'Screenshot copied to clipboard' : 'Screenshot failed',
+ ok ? 'success' : 'error'
+ );
+ } catch (err) {
+ pushToast(err instanceof Error ? err.message : 'Screenshot failed', 'error');
+ }
+ };
+
+ const isPublished = selected.uri.includes('@published');
+ const schemaUrl = buildSchemaUrl(selected.uri, envHost);
+
+ return (
+
+ Component
+
+ {selected.displayName}
+ {selected.instance && {selected.instance}
}
+
+ open(buildUrl(selected.uri, '', envHost))}
+ >
+ Data
+
+ open(buildUrl(selected.uri, '.json', envHost))}>
+ .json
+
+ open(buildUrl(selected.uri, '.html', envHost))}>
+ .html
+
+ {schemaUrl && (
+ open(schemaUrl)}>
+ Schema
+
+ )}
+ {isPublished && (
+ open(buildUrl(unpublishedUri(selected.uri), '', envHost))}
+ >
+ Unpublished
+
+ )}
+
+
+ Screenshot
+
+
+
+
+ Copy as…
+
+ copy(selected.uri, 'URI')}>
+ URI
+
+ copy(buildCurlCommand(selected.uri, '.json', envHost), 'cURL command')}
+ >
+ cURL
+
+ copy(copyAsFetchSnippet(selected.uri, envHost), 'fetch() snippet')}
+ >
+ fetch()
+
+ copy(copyAsCssSelector(selected.uri), 'CSS selector')}
+ >
+ CSS
+
+
+
+
+
+
+ );
+}
diff --git a/src/content/panel/components/ComponentTree.tsx b/src/content/panel/components/ComponentTree.tsx
new file mode 100644
index 0000000..fcafcad
--- /dev/null
+++ b/src/content/panel/components/ComponentTree.tsx
@@ -0,0 +1,114 @@
+import { useEffect, useMemo } from 'react';
+import { setFindMatches, setHovered, setSelected } from '../../highlighter';
+import { useStore } from '../store';
+import { SearchBar } from './SearchBar';
+
+export function ComponentTree() {
+ const components = useStore((s) => s.components);
+ const selected = useStore((s) => s.selected);
+ const search = useStore((s) => s.search);
+ const setSelectedStore = useStore((s) => s.setSelected);
+ const find = useStore((s) => s.find);
+ const setFind = useStore((s) => s.setFind);
+
+ const filtered = useMemo(() => {
+ if (!search.trim()) return components;
+ const needle = search.toLowerCase();
+ return components.filter(
+ (c) =>
+ c.name.toLowerCase().includes(needle) ||
+ c.displayName.toLowerCase().includes(needle) ||
+ c.uri.toLowerCase().includes(needle) ||
+ (c.instance?.toLowerCase().includes(needle) ?? false)
+ );
+ }, [components, search]);
+
+ // Sync find query with the search box and apply page-wide dim/match styling.
+ useEffect(() => {
+ if (find.query !== search) {
+ setFind({ query: search, index: 0 });
+ }
+ const all = components.map((c) => c.element);
+ setFindMatches(all, search.trim() ? new Set(filtered.map((c) => c.element)) : null);
+ return () => setFindMatches(all, null);
+ }, [search, find.query, components, filtered, setFind]);
+
+ const jumpTo = (delta: 1 | -1) => {
+ if (filtered.length === 0) return;
+ const next = (find.index + delta + filtered.length) % filtered.length;
+ setFind({ query: search, index: next });
+ const target = filtered[next];
+ if (!target) return;
+ setSelected(selected?.element ?? null, target.element);
+ setSelectedStore(target);
+ target.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ };
+
+ return (
+
+ jumpTo(1)}
+ onShiftEnter={() => jumpTo(-1)}
+ onEscape={() => useStore.getState().setSearch('')}
+ />
+ {search.trim() && (
+
+ {filtered.length === 0 ? (
+
0 matches
+ ) : (
+ <>
+
+ {find.index + 1} / {filtered.length} matches
+
+
+ jumpTo(-1)}
+ title="Previous (Shift+Enter)"
+ >
+ ↑
+
+ jumpTo(1)} title="Next (Enter)">
+ ↓
+
+ useStore.getState().setSearch('')}
+ title="Clear (Esc)"
+ >
+ ×
+
+
+ >
+ )}
+
+ )}
+
+ {filtered.length === 0 && No matching components }
+ {filtered.map((info, idx) => {
+ const isSelected = selected?.element === info.element;
+ const isCurrentMatch = search.trim() && idx === find.index;
+ return (
+ {
+ setSelected(selected?.element ?? null, info.element);
+ setSelectedStore(info);
+ info.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }}
+ onMouseEnter={() => setHovered(null, info.element)}
+ onMouseLeave={() => setHovered(info.element, null)}
+ >
+ {info.displayName}
+ {info.instance && (
+ · {info.instance.slice(0, 8)}
+ )}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/content/panel/components/DiffView.tsx b/src/content/panel/components/DiffView.tsx
new file mode 100644
index 0000000..6904387
--- /dev/null
+++ b/src/content/panel/components/DiffView.tsx
@@ -0,0 +1,193 @@
+import { useEffect, useMemo, useState } from 'react';
+import { buildUrl, isPublished, unpublishedUri } from '@/lib/clay-uri';
+import { ENVIRONMENT_LABELS, ENVIRONMENT_ORDER, type Environment } from '@/lib/types';
+import { useEnvHost, useStore } from '../store';
+
+type DiffMode = 'published-vs-draft' | `env:${Environment}`;
+
+interface DualState {
+ readonly status: 'idle' | 'loading' | 'success' | 'error';
+ readonly left?: unknown;
+ readonly right?: unknown;
+ readonly error?: string;
+}
+
+interface DiffOption {
+ readonly id: DiffMode;
+ readonly label: string;
+ readonly leftLabel: string;
+ readonly rightLabel: string;
+ readonly available: boolean;
+ readonly hostOverride?: string;
+}
+
+function diffLines(
+ left: unknown,
+ right: unknown
+): Array<{ text: string; tone: 'added' | 'removed' | 'context' }> {
+ const l = JSON.stringify(left, null, 2).split('\n');
+ const r = JSON.stringify(right, null, 2).split('\n');
+ const result: Array<{ text: string; tone: 'added' | 'removed' | 'context' }> = [];
+ const max = Math.max(l.length, r.length);
+ for (let i = 0; i < max; i += 1) {
+ const a = l[i];
+ const b = r[i];
+ if (a === b) {
+ if (a !== undefined) result.push({ text: a, tone: 'context' });
+ } else {
+ if (a !== undefined) result.push({ text: a, tone: 'removed' });
+ if (b !== undefined) result.push({ text: b, tone: 'added' });
+ }
+ }
+ return result;
+}
+
+export function DiffView() {
+ const selected = useStore((s) => s.selected);
+ const page = useStore((s) => s.page);
+ const envHost = useEnvHost();
+ const env = useStore((s) => s.preferences.defaultEnvironment);
+ const envHosts = useStore((s) => s.preferences.environments);
+ const targetUri = selected?.uri ?? page?.pageUri ?? null;
+
+ const [mode, setMode] = useState('published-vs-draft');
+
+ const options: DiffOption[] = useMemo(() => {
+ const list: DiffOption[] = [
+ {
+ id: 'published-vs-draft',
+ label: 'Published vs. Draft',
+ leftLabel: 'Published',
+ rightLabel: 'Draft',
+ available: !!targetUri && isPublished(targetUri ?? ''),
+ },
+ ];
+ for (const e of ENVIRONMENT_ORDER) {
+ if (e === env) continue;
+ const host = envHosts[e];
+ list.push({
+ id: `env:${e}`,
+ label: `${ENVIRONMENT_LABELS[env]} vs. ${ENVIRONMENT_LABELS[e]}`,
+ leftLabel: ENVIRONMENT_LABELS[env],
+ rightLabel: ENVIRONMENT_LABELS[e],
+ available: !!targetUri && Boolean(host?.trim()),
+ hostOverride: host,
+ });
+ }
+ return list;
+ }, [targetUri, env, envHosts]);
+
+ const activeOption =
+ options.find((o) => o.id === mode && o.available) ??
+ options.find((o) => o.available) ??
+ options[0]!;
+
+ const currentKey = `${activeOption.id}::${envHost}::${targetUri ?? ''}`;
+ const initial: DualState =
+ targetUri && activeOption.available ? { status: 'loading' } : { status: 'idle' };
+ const [state, setState] = useState(initial);
+ const [prevKey, setPrevKey] = useState(currentKey);
+
+ if (prevKey !== currentKey) {
+ setPrevKey(currentKey);
+ setState(initial);
+ }
+
+ useEffect(() => {
+ if (!targetUri || !activeOption.available) return;
+ let cancelled = false;
+
+ const leftUrl = buildUrl(targetUri, '.json', envHost);
+ const rightUrl =
+ activeOption.id === 'published-vs-draft'
+ ? buildUrl(unpublishedUri(targetUri), '.json', envHost)
+ : buildUrl(targetUri, '.json', activeOption.hostOverride ?? '');
+
+ Promise.all([
+ fetch(leftUrl, { credentials: 'include' }).then((r) => r.json()),
+ fetch(rightUrl, { credentials: 'include' }).then((r) => r.json()),
+ ])
+ .then(([left, right]) => {
+ if (cancelled) return;
+ setState({ status: 'success', left, right });
+ })
+ .catch((err: unknown) => {
+ if (cancelled) return;
+ setState({
+ status: 'error',
+ error: err instanceof Error ? err.message : String(err),
+ });
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [targetUri, envHost, activeOption.id, activeOption.available, activeOption.hostOverride]);
+
+ if (!targetUri) {
+ return Select a component to compare its versions.
;
+ }
+
+ return (
+
+
+
+ Compare:
+ setMode(e.target.value as DiffMode)}>
+ {options.map((o) => (
+
+ {o.label}
+ {!o.available ? ' (unavailable)' : ''}
+
+ ))}
+
+
+
+
+ {!activeOption.available && (
+
+ {activeOption.id === 'published-vs-draft'
+ ? 'Diff is only available for published items. The current selection is a draft.'
+ : `No host configured for ${activeOption.rightLabel}. Set one in Settings → Environments.`}
+
+ )}
+
+ {activeOption.available && state.status === 'loading' && (
+
+ Loading both versions…
+
+ )}
+
+ {activeOption.available && state.status === 'error' && (
+
+ Failed to load diff: {state.error}
+
+ )}
+
+ {activeOption.available && state.status === 'success' && (
+ <>
+
+ {activeOption.leftLabel} → {activeOption.rightLabel}
+
+
+ {diffLines(state.left, state.right).map((l, i) => {
+ const cls =
+ l.tone === 'added'
+ ? 'cs-diff-added'
+ : l.tone === 'removed'
+ ? 'cs-diff-removed'
+ : '';
+ const prefix = l.tone === 'added' ? '+ ' : l.tone === 'removed' ? '- ' : ' ';
+ return (
+
+ {prefix}
+ {l.text}
+
+ );
+ })}
+
+ >
+ )}
+
+ );
+}
diff --git a/src/content/panel/components/EnvironmentSwitcher.tsx b/src/content/panel/components/EnvironmentSwitcher.tsx
new file mode 100644
index 0000000..3728c9e
--- /dev/null
+++ b/src/content/panel/components/EnvironmentSwitcher.tsx
@@ -0,0 +1,33 @@
+import { useStore } from '../store';
+import { savePreferences } from '@/lib/storage';
+import { ENVIRONMENT_LABELS, ENVIRONMENT_ORDER } from '@/lib/types';
+
+export function EnvironmentSwitcher() {
+ const env = useStore((s) => s.preferences.defaultEnvironment);
+ const host = useStore((s) => s.preferences.environments[env]);
+ const setPrefs = useStore((s) => s.setPreferences);
+
+ const cycle = () => {
+ const idx = ENVIRONMENT_ORDER.indexOf(env);
+ const next = ENVIRONMENT_ORDER[(idx + 1) % ENVIRONMENT_ORDER.length]!;
+ setPrefs({ defaultEnvironment: next });
+ void savePreferences({ defaultEnvironment: next });
+ };
+
+ const isConfigured = Boolean(host?.trim());
+
+ return (
+
+ env: {env}
+ {isConfigured ? '' : ' (unset)'}
+
+ );
+}
diff --git a/src/content/panel/components/ExportMenu.tsx b/src/content/panel/components/ExportMenu.tsx
new file mode 100644
index 0000000..00c02b1
--- /dev/null
+++ b/src/content/panel/components/ExportMenu.tsx
@@ -0,0 +1,120 @@
+import { useEffect, useRef, useState } from 'react';
+import { buildManifest, formatManifest } from '@/lib/exporter';
+import { copyToClipboard } from '@/lib/clipboard';
+import type { ExportFormat } from '@/lib/types';
+import { useStore } from '../store';
+
+const OPTIONS: Array<{ format: ExportFormat; label: string; help: string }> = [
+ { format: 'json', label: 'JSON', help: 'Full structured data' },
+ { format: 'csv', label: 'CSV', help: 'Spreadsheet-friendly' },
+ { format: 'markdown', label: 'Markdown', help: 'Pasteable into a ticket' },
+];
+
+const ESTIMATED_MENU_HEIGHT = 140;
+
+interface MenuCoords {
+ readonly top: number;
+ readonly right: number;
+}
+
+export function ExportMenu() {
+ const [open, setOpen] = useState(false);
+ const [coords, setCoords] = useState(null);
+ const wrapperRef = useRef(null);
+ const triggerRef = useRef(null);
+
+ const page = useStore((s) => s.page);
+ const components = useStore((s) => s.components);
+ const pushToast = useStore((s) => s.pushToast);
+
+ useEffect(() => {
+ if (!open) return;
+
+ // Use composedPath() so we correctly see clicks inside the shadow tree
+ // (default e.target gets retargeted to the shadow host at document level).
+ const onPointerDown = (e: Event) => {
+ const path = e.composedPath();
+ if (wrapperRef.current && !path.includes(wrapperRef.current)) {
+ setOpen(false);
+ }
+ };
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') setOpen(false);
+ };
+ const onResize = () => setOpen(false);
+
+ document.addEventListener('pointerdown', onPointerDown, true);
+ document.addEventListener('keydown', onKey);
+ window.addEventListener('resize', onResize);
+ window.addEventListener('scroll', onResize, true);
+
+ return () => {
+ document.removeEventListener('pointerdown', onPointerDown, true);
+ document.removeEventListener('keydown', onKey);
+ window.removeEventListener('resize', onResize);
+ window.removeEventListener('scroll', onResize, true);
+ };
+ }, [open]);
+
+ const toggle = () => {
+ if (open) {
+ setOpen(false);
+ return;
+ }
+ const rect = triggerRef.current?.getBoundingClientRect();
+ if (!rect) return;
+ const flipUp = rect.bottom + ESTIMATED_MENU_HEIGHT + 8 > window.innerHeight;
+ setCoords({
+ top: flipUp ? rect.top - ESTIMATED_MENU_HEIGHT - 4 : rect.bottom + 4,
+ right: Math.max(8, window.innerWidth - rect.right),
+ });
+ setOpen(true);
+ };
+
+ const exportAs = async (format: ExportFormat) => {
+ setOpen(false);
+ try {
+ const text = formatManifest(buildManifest(page, components), format);
+ const ok = await copyToClipboard(text);
+ pushToast(
+ ok ? `${format.toUpperCase()} manifest copied to clipboard` : 'Copy failed',
+ ok ? 'success' : 'error'
+ );
+ } catch (err) {
+ pushToast(err instanceof Error ? err.message : 'Export failed', 'error');
+ }
+ };
+
+ return (
+
+
+ Export ▾
+
+ {open && coords && (
+
+ {OPTIONS.map((opt) => (
+ void exportAs(opt.format)}
+ className="cs-export-item"
+ >
+ Copy as {opt.label}
+ {opt.help}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/content/panel/components/Fab.tsx b/src/content/panel/components/Fab.tsx
new file mode 100644
index 0000000..0ca0cbd
--- /dev/null
+++ b/src/content/panel/components/Fab.tsx
@@ -0,0 +1,59 @@
+import type { CSSProperties } from 'react';
+// Inline as a data URL so we don't depend on chrome.runtime.getURL() and
+// avoid a network fetch from the host page's origin (which would 404 — the
+// asset lives under the extension's origin, not the page's).
+import clayIconUrl from '@/assets/clay-icon.png?inline';
+import type { PanelPosition } from '@/lib/types';
+import { useStore } from '../store';
+
+const MARGIN = 20;
+
+/**
+ * Maps the user's panel-position preference to a FAB anchor. Side-dock modes
+ * (`left-side` / `right-side`) collapse to the bottom corner on the matching
+ * side — full-height side panels can't meaningfully shrink to a horizontal
+ * pill, so the bottom corner is the natural resting place.
+ */
+function fabAnchorFor(corner: PanelPosition): CSSProperties {
+ const isRight = corner === 'bottom-right' || corner === 'top-right' || corner === 'right-side';
+ const isTop = corner === 'top-right' || corner === 'top-left';
+ return {
+ position: 'fixed',
+ [isRight ? 'right' : 'left']: MARGIN,
+ [isTop ? 'top' : 'bottom']: MARGIN,
+ };
+}
+
+/**
+ * Floating action button that lives at the user's preferred corner whenever
+ * the panel is collapsed. Click expands the panel; the panel's collapse
+ * button returns to this state. Standard browser-extension chrome pattern.
+ */
+export function Fab() {
+ const corner = useStore((s) => s.preferences.panelPosition);
+ const componentCount = useStore((s) => s.components.length);
+ const toggleCollapsed = useStore((s) => s.toggleCollapsed);
+ const page = useStore((s) => s.page);
+
+ const tooltip = page
+ ? `Open Clay Slip — ${componentCount} component${componentCount === 1 ? '' : 's'} on this page`
+ : 'Open Clay Slip';
+
+ return (
+
+
+ {componentCount > 0 && (
+
+ {componentCount > 99 ? '99+' : componentCount}
+
+ )}
+
+ );
+}
diff --git a/src/content/panel/components/Header.tsx b/src/content/panel/components/Header.tsx
new file mode 100644
index 0000000..4462566
--- /dev/null
+++ b/src/content/panel/components/Header.tsx
@@ -0,0 +1,69 @@
+import type { Ref } from 'react';
+import clayIconUrl from '@/assets/clay-icon.png?inline';
+import type { RuntimeMessage } from '@/lib/types';
+import { Icon } from './Icon';
+import { useStore } from '../store';
+
+interface HeaderProps {
+ ref?: Ref;
+}
+
+export function Header({ ref }: HeaderProps) {
+ const page = useStore((s) => s.page);
+ const toggleCollapsed = useStore((s) => s.toggleCollapsed);
+ const toggleShortcuts = useStore((s) => s.toggleShortcuts);
+ const toggleHighlights = useStore((s) => s.toggleHighlights);
+ const highlightEnabled = useStore((s) => s.highlightEnabled);
+ const componentCount = useStore((s) => s.components.length);
+
+ const openOptions = () => {
+ chrome.runtime
+ .sendMessage({ type: 'OPEN_OPTIONS' } satisfies RuntimeMessage)
+ .catch(() => undefined);
+ };
+
+ return (
+
+
+
+ Clay Slip
+
+ {componentCount}
+
+
+ {page && (
+
+ {page.isPublished ? 'Published' : 'Draft'}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/content/panel/components/Icon.tsx b/src/content/panel/components/Icon.tsx
new file mode 100644
index 0000000..8352009
--- /dev/null
+++ b/src/content/panel/components/Icon.tsx
@@ -0,0 +1,172 @@
+import type { JSX } from 'react';
+
+const ICONS = {
+ collapse: (
+
+ ),
+ expand: (
+
+ ),
+ close: (
+
+ ),
+ copy: (
+ <>
+
+
+ >
+ ),
+ external: (
+ <>
+
+ >
+ ),
+ search: (
+ <>
+
+
+ >
+ ),
+ question: (
+ <>
+
+
+ >
+ ),
+ settings: (
+ <>
+
+
+ >
+ ),
+ eye: (
+ <>
+
+
+ >
+ ),
+ eyeOff: (
+ <>
+
+
+ >
+ ),
+ edit: (
+ <>
+
+ >
+ ),
+ share: (
+ <>
+
+
+
+
+ >
+ ),
+ camera: (
+ <>
+
+
+ >
+ ),
+ note: (
+ <>
+
+
+ >
+ ),
+} as const;
+
+export type IconName = keyof typeof ICONS;
+
+export function Icon({ name, size = 16 }: { name: IconName; size?: number }): JSX.Element {
+ return (
+
+ {ICONS[name]}
+
+ );
+}
diff --git a/src/content/panel/components/JsonPreview.tsx b/src/content/panel/components/JsonPreview.tsx
new file mode 100644
index 0000000..cde95a2
--- /dev/null
+++ b/src/content/panel/components/JsonPreview.tsx
@@ -0,0 +1,139 @@
+import { useEffect, useState } from 'react';
+import { buildUrl } from '@/lib/clay-uri';
+import { copyToClipboard } from '@/lib/clipboard';
+import { useEnvHost, useStore } from '../store';
+import { Icon } from './Icon';
+
+interface FetchState {
+ readonly status: 'idle' | 'loading' | 'success' | 'error';
+ readonly data?: unknown;
+ readonly error?: string;
+}
+
+const cache = new Map();
+
+function highlightJson(value: unknown): string {
+ const json = JSON.stringify(value, null, 2);
+ return json
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(
+ /("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(?=\s*:))|("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")|(\b(?:true|false)\b)|(\bnull\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
+ (match: string) => {
+ if (/^"[^"]+"\s*:?$/.test(match) && match.endsWith(':')) {
+ return `${match} `;
+ }
+ if (match.startsWith('"')) {
+ return `${match} `;
+ }
+ if (match === 'true' || match === 'false') {
+ return `${match} `;
+ }
+ if (match === 'null') {
+ return `${match} `;
+ }
+ return `${match} `;
+ }
+ );
+}
+
+function cacheKey(uri: string | null, host: string): string {
+ return `${host || '_'}::${uri ?? ''}`;
+}
+
+function initialStateFor(uri: string | null, host: string): FetchState {
+ if (!uri) return { status: 'idle' };
+ return cache.get(cacheKey(uri, host)) ?? { status: 'loading' };
+}
+
+export function JsonPreview() {
+ const selected = useStore((s) => s.selected);
+ const page = useStore((s) => s.page);
+ const pushToast = useStore((s) => s.pushToast);
+ const envHost = useEnvHost();
+
+ const targetUri = selected?.uri ?? page?.pageUri ?? null;
+ const fetchUrl = targetUri ? buildUrl(targetUri, '.json', envHost) : null;
+
+ const [state, setState] = useState(() => initialStateFor(targetUri, envHost));
+ const [prevKey, setPrevKey] = useState(cacheKey(targetUri, envHost));
+
+ const currentKey = cacheKey(targetUri, envHost);
+ if (prevKey !== currentKey) {
+ setPrevKey(currentKey);
+ setState(initialStateFor(targetUri, envHost));
+ }
+
+ useEffect(() => {
+ if (!fetchUrl || !targetUri) return;
+ const key = cacheKey(targetUri, envHost);
+ const cached = cache.get(key);
+ if (cached && cached.status !== 'loading') return;
+
+ let cancelled = false;
+ fetch(fetchUrl, { credentials: 'include' })
+ .then((r) => {
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
+ return r.json();
+ })
+ .then((data) => {
+ if (cancelled) return;
+ const next: FetchState = { status: 'success', data };
+ cache.set(key, next);
+ setState(next);
+ })
+ .catch((err: unknown) => {
+ if (cancelled) return;
+ const message = err instanceof Error ? err.message : String(err);
+ const next: FetchState = { status: 'error', error: message };
+ cache.set(key, next);
+ setState(next);
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [fetchUrl, targetUri, envHost]);
+
+ if (!targetUri) {
+ return Select a component to preview its data.
;
+ }
+
+ if (state.status === 'loading') {
+ return (
+
+ Fetching {fetchUrl}…
+
+ );
+ }
+
+ if (state.status === 'error') {
+ return (
+
+ Failed to fetch JSON: {state.error}
+
+ );
+ }
+
+ if (state.status !== 'success' || state.data === undefined) {
+ return null;
+ }
+
+ const onCopy = async () => {
+ const ok = await copyToClipboard(JSON.stringify(state.data, null, 2));
+ pushToast(ok ? 'JSON copied' : 'Copy failed', ok ? 'success' : 'error');
+ };
+
+ return (
+
+ );
+}
diff --git a/src/content/panel/components/NotesTab.tsx b/src/content/panel/components/NotesTab.tsx
new file mode 100644
index 0000000..88743fa
--- /dev/null
+++ b/src/content/panel/components/NotesTab.tsx
@@ -0,0 +1,71 @@
+import type { RuntimeMessage } from '@/lib/types';
+import { deleteAnnotation } from '@/lib/annotations';
+import { useStore } from '../store';
+import { setSelected } from '../../highlighter';
+
+export function NotesTab() {
+ const annotations = useStore((s) => s.annotations);
+ const components = useStore((s) => s.components);
+ const setSelectedStore = useStore((s) => s.setSelected);
+ const selected = useStore((s) => s.selected);
+ const pushToast = useStore((s) => s.pushToast);
+
+ if (!annotations.length) {
+ return (
+
+ No notes yet. Select a component on the Inspect tab and add a sticky note — it will appear
+ here, with an orange dot on the component itself when Slip is open.
+
+ );
+ }
+
+ const open = (url: string) => {
+ chrome.runtime.sendMessage({ type: 'OPEN_TAB', url } satisfies RuntimeMessage);
+ };
+
+ return (
+
+ Notes ({annotations.length})
+
+ {annotations.map((a) => {
+ const onPage = components.find((c) => c.uri === a.uri);
+ return (
+
+
+ {
+ if (onPage) {
+ setSelected(selected?.element ?? null, onPage.element);
+ setSelectedStore(onPage);
+ onPage.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ } else {
+ open(a.pageUrl);
+ }
+ }}
+ title={onPage ? 'Select on this page' : 'Open the page where it lives'}
+ >
+ {a.displayName}
+ {onPage && · here }
+
+ {
+ await deleteAnnotation(a.uri);
+ pushToast('Note deleted', 'info');
+ }}
+ >
+ Delete
+
+
+ {a.note}
+
+ {a.pageTitle || a.pageUrl} · {new Date(a.updatedAt).toLocaleString()}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/content/panel/components/PageInfo.tsx b/src/content/panel/components/PageInfo.tsx
new file mode 100644
index 0000000..bfd607b
--- /dev/null
+++ b/src/content/panel/components/PageInfo.tsx
@@ -0,0 +1,95 @@
+import { buildEditorUrl, buildUrl, unpublishedUri } from '@/lib/clay-uri';
+import { findMappingForHost, rewriteUrlToEnv } from '@/lib/site-host';
+import { SITE_ENV_LABELS, SITE_ENV_ORDER, type RuntimeMessage } from '@/lib/types';
+import { useEnvHost, useStore } from '../store';
+import { Icon } from './Icon';
+import { ExportMenu } from './ExportMenu';
+
+export function PageInfo() {
+ const page = useStore((s) => s.page);
+ const envHost = useEnvHost();
+ const siteHosts = useStore((s) => s.preferences.siteHosts);
+ if (!page) return null;
+
+ const open = (url: string) => {
+ chrome.runtime.sendMessage({ type: 'OPEN_TAB', url } satisfies RuntimeMessage);
+ };
+
+ const currentHost = location.hostname;
+ const match = findMappingForHost(currentHost, siteHosts);
+ const viewOnTargets = match
+ ? SITE_ENV_ORDER.filter((env) => env !== match.env && Boolean(match.mapping.hosts[env])).map(
+ (env) => ({
+ env,
+ label: SITE_ENV_LABELS[env],
+ url: rewriteUrlToEnv(location.href, env, siteHosts),
+ })
+ )
+ : [];
+
+ return (
+
+ Page
+ {page.pageInstance ?? 'Unknown page'}
+ {page.pageUri}
+
+ open(buildUrl(page.pageUri, '', envHost))}
+ >
+ Page
+
+ open(buildEditorUrl(page.pageUri, envHost))}
+ title="Open this page in Clay edit mode"
+ >
+ Edit
+
+ open(buildUrl(page.pageUri, '/meta', envHost))}>
+ Metadata
+
+ {page.layoutUri !== null && (
+ open(buildUrl(page.layoutUri!, '', envHost))}>
+ Layout
+
+ )}
+ {page.isPublished && (
+ <>
+ open(buildUrl(unpublishedUri(page.pageUri), '', envHost))}
+ >
+ Unpublished Page
+
+ {page.layoutUri !== null && (
+ open(buildUrl(unpublishedUri(page.layoutUri!), '', envHost))}
+ >
+ Unpublished Layout
+
+ )}
+ >
+ )}
+
+
+ {match && viewOnTargets.length > 0 && (
+
+ View on:
+ {viewOnTargets.map(({ env, label, url }) => (
+ url && open(url)}
+ title={url ?? `No host configured for ${label}`}
+ >
+ {label}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/content/panel/components/RecentList.tsx b/src/content/panel/components/RecentList.tsx
new file mode 100644
index 0000000..3afb52e
--- /dev/null
+++ b/src/content/panel/components/RecentList.tsx
@@ -0,0 +1,63 @@
+import type { RuntimeMessage } from '@/lib/types';
+import { useStore } from '../store';
+import { setSelected } from '../../highlighter';
+
+const MAX_VISIBLE = 6;
+
+function timeAgo(ts: number): string {
+ const seconds = Math.floor((Date.now() - ts) / 1000);
+ if (seconds < 60) return 'just now';
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
+ return `${Math.floor(seconds / 86400)}d ago`;
+}
+
+export function RecentList() {
+ const recents = useStore((s) => s.recents);
+ const components = useStore((s) => s.components);
+ const setSelectedStore = useStore((s) => s.setSelected);
+ const selected = useStore((s) => s.selected);
+
+ if (recents.length === 0) return null;
+
+ const open = (url: string) => {
+ chrome.runtime.sendMessage({ type: 'OPEN_TAB', url } satisfies RuntimeMessage);
+ };
+
+ const visible = recents.slice(0, MAX_VISIBLE);
+
+ return (
+
+ Recently viewed
+
+ {visible.map((r) => {
+ const onPage = components.find((c) => c.uri === r.uri);
+ return (
+
+ {
+ if (onPage) {
+ setSelected(selected?.element ?? null, onPage.element);
+ setSelectedStore(onPage);
+ onPage.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ } else {
+ open(r.pageUrl);
+ }
+ }}
+ >
+ {r.displayName}
+ {onPage && · here }
+
+
+ {timeAgo(r.visitedAt)}
+ {!onPage && · {new URL(r.pageUrl).hostname} }
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/content/panel/components/ResizeHandle.tsx b/src/content/panel/components/ResizeHandle.tsx
new file mode 100644
index 0000000..8f7a301
--- /dev/null
+++ b/src/content/panel/components/ResizeHandle.tsx
@@ -0,0 +1,105 @@
+import { savePreferences } from '@/lib/storage';
+import type { PanelPosition } from '@/lib/types';
+import { useStore } from '../store';
+
+const MIN_W = 280;
+const MAX_W = 720;
+const MIN_H = 240;
+
+type Mode = 'width' | 'height' | 'corner';
+
+interface Props {
+ mode: Mode;
+}
+
+const isRightAnchor = (p: PanelPosition) =>
+ p === 'bottom-right' || p === 'top-right' || p === 'right-side';
+const isBottomAnchor = (p: PanelPosition) => p === 'bottom-right' || p === 'bottom-left';
+
+function clamp(min: number, max: number, n: number): number {
+ return Math.max(min, Math.min(max, n));
+}
+
+/**
+ * One handle, three flavors:
+ * - width: vertical strip on the inner vertical edge
+ * - height: horizontal strip on the inner horizontal edge
+ * - corner: small grabber in the inner corner that does both at once
+ *
+ * "Inner" = the edge of the panel that faces the viewport interior, opposite
+ * to the dock anchor. For a bottom-right panel that's the top + left edges
+ * (and the top-left grabber), so the panel always grows toward the inside.
+ */
+export function ResizeHandle({ mode }: Props) {
+ const corner = useStore((s) => s.preferences.panelPosition);
+ const setPrefs = useStore((s) => s.setPreferences);
+
+ const right = isRightAnchor(corner);
+ const bottom = isBottomAnchor(corner);
+ const widthDir = right ? -1 : 1;
+ const heightDir = bottom ? -1 : 1;
+
+ const onPointerDown = (e: React.PointerEvent) => {
+ const startW = useStore.getState().preferences.panelWidth;
+ const startH = useStore.getState().preferences.panelHeight;
+ const startX = e.clientX;
+ const startY = e.clientY;
+ const maxH = window.innerHeight - 40;
+
+ const onMove = (ev: PointerEvent) => {
+ const next: { panelWidth?: number; panelHeight?: number } = {};
+ if (mode === 'width' || mode === 'corner') {
+ const dx = ev.clientX - startX;
+ next.panelWidth = clamp(MIN_W, MAX_W, Math.round(startW + dx * widthDir));
+ }
+ if (mode === 'height' || mode === 'corner') {
+ const dy = ev.clientY - startY;
+ next.panelHeight = clamp(MIN_H, maxH, Math.round(startH + dy * heightDir));
+ }
+ setPrefs(next);
+ };
+
+ const onUp = () => {
+ document.removeEventListener('pointermove', onMove);
+ document.removeEventListener('pointerup', onUp);
+ const { panelWidth, panelHeight } = useStore.getState().preferences;
+ void savePreferences({ panelWidth, panelHeight });
+ };
+
+ document.addEventListener('pointermove', onMove);
+ document.addEventListener('pointerup', onUp);
+ e.preventDefault();
+ e.stopPropagation();
+ };
+
+ // Position class — handle lives on the panel's inner edge(s).
+ const xPos = right ? 'left' : 'right'; // inner vertical edge
+ const yPos = bottom ? 'top' : 'bottom'; // inner horizontal edge
+
+ let className = 'cs-resize';
+ if (mode === 'width') className += ` cs-resize-width cs-resize-${xPos}`;
+ else if (mode === 'height') className += ` cs-resize-height cs-resize-${yPos}`;
+ else className += ` cs-resize-corner cs-resize-corner-${yPos}-${xPos}`;
+
+ // Corner cursor: nwse when inner-corner is top-left or bottom-right (panel
+ // anchored bottom-right or top-left). nesw otherwise.
+ const cornerCursor = right === bottom ? 'nwse-resize' : 'nesw-resize';
+ const cursor = mode === 'width' ? 'ew-resize' : mode === 'height' ? 'ns-resize' : cornerCursor;
+
+ return (
+
+ );
+}
diff --git a/src/content/panel/components/SearchBar.tsx b/src/content/panel/components/SearchBar.tsx
new file mode 100644
index 0000000..630fbcc
--- /dev/null
+++ b/src/content/panel/components/SearchBar.tsx
@@ -0,0 +1,35 @@
+import type { KeyboardEvent } from 'react';
+import { useStore } from '../store';
+
+interface Props {
+ onEnter?: () => void;
+ onShiftEnter?: () => void;
+ onEscape?: () => void;
+}
+
+export function SearchBar({ onEnter, onShiftEnter, onEscape }: Props = {}) {
+ const search = useStore((s) => s.search);
+ const setSearch = useStore((s) => s.setSearch);
+
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ if (e.shiftKey) onShiftEnter?.();
+ else onEnter?.();
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ onEscape?.();
+ }
+ };
+
+ return (
+ setSearch(e.target.value)}
+ onKeyDown={onKeyDown}
+ />
+ );
+}
diff --git a/src/content/panel/components/SeoTab.tsx b/src/content/panel/components/SeoTab.tsx
new file mode 100644
index 0000000..f61307d
--- /dev/null
+++ b/src/content/panel/components/SeoTab.tsx
@@ -0,0 +1,114 @@
+import { useEffect, useState } from 'react';
+import { extractSeoMeta, lintSeo, type SeoIssue, type SeoMeta } from '@/lib/seo';
+
+const TONE: Record = {
+ error: 'cs-seo-issue-error',
+ warn: 'cs-seo-issue-warn',
+ info: 'cs-seo-issue-info',
+};
+
+function CardPreview({ meta, kind }: { meta: SeoMeta; kind: 'twitter' | 'facebook' }) {
+ const title =
+ (kind === 'twitter' && meta.twitter['twitter:title']) || meta.og['og:title'] || meta.title;
+ const description =
+ (kind === 'twitter' && meta.twitter['twitter:description']) ||
+ meta.og['og:description'] ||
+ meta.description;
+ const image = (kind === 'twitter' && meta.twitter['twitter:image']) || meta.og['og:image'] || '';
+ const host = (() => {
+ try {
+ return new URL(meta.canonical || location.href).hostname;
+ } catch {
+ return '';
+ }
+ })();
+
+ return (
+
+
+ {image ? (
+
+ ) : (
+
No og:image
+ )}
+
+
+
{host}
+
{title || 'Untitled'}
+
{description || 'No description set.'}
+
+
+ );
+}
+
+export function SeoTab() {
+ const [meta, setMeta] = useState(() => extractSeoMeta());
+
+ useEffect(() => {
+ const observer = new MutationObserver(() => setMeta(extractSeoMeta()));
+ observer.observe(document.head, { childList: true, subtree: true, characterData: true });
+ return () => observer.disconnect();
+ }, []);
+
+ const issues = lintSeo(meta);
+
+ return (
+
+
+ Page basics
+
+
+
Title
+
+ {meta.title || — }
+ ({meta.title.length})
+
+
+
+
Description
+
+ {meta.description || — }
+ ({meta.description.length})
+
+
+
+
Canonical
+ {meta.canonical || — }
+
+
+
Robots
+ {meta.robots || default }
+
+
+
JSON-LD
+ {meta.jsonLd.length} block(s)
+
+
+
+
+
+ Twitter card preview
+
+
+
+
+ Facebook / Slack preview
+
+
+
+ {issues.length > 0 && (
+
+ Issues ({issues.length})
+
+ {issues.map((i) => (
+
+ {i.severity}
+ {i.message}
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/content/panel/components/ShareMenu.tsx b/src/content/panel/components/ShareMenu.tsx
new file mode 100644
index 0000000..d18d1b8
--- /dev/null
+++ b/src/content/panel/components/ShareMenu.tsx
@@ -0,0 +1,189 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { buildShareLink } from '@/lib/clay-uri';
+import { copyToClipboard } from '@/lib/clipboard';
+import { availableEnvsFor, findMappingForHost, rewriteUrlToEnv } from '@/lib/site-host';
+import { SITE_ENV_LABELS, SITE_ENV_ORDER, type SiteEnv } from '@/lib/types';
+import { useStore } from '../store';
+import { Icon } from './Icon';
+
+interface Props {
+ readonly uri: string;
+}
+
+interface MenuCoords {
+ readonly top: number;
+ readonly right: number;
+}
+
+const ESTIMATED_MENU_HEIGHT = 160;
+
+interface MenuTarget {
+ readonly key: string;
+ readonly label: string;
+ readonly env: SiteEnv;
+}
+
+/**
+ * Split share button. The main face copies a share link for the *current*
+ * page (always reading {@link Window.location.href} at click time so it can
+ * never go stale). The trailing ▾ — only rendered when site-host mappings
+ * are configured — opens a menu that adds one row per cross-env target,
+ * using {@link rewriteUrlToEnv} to swap the host before generating the
+ * share link.
+ */
+export function ShareMenu({ uri }: Props) {
+ const [open, setOpen] = useState(false);
+ const [coords, setCoords] = useState(null);
+ const wrapperRef = useRef(null);
+ const triggerRef = useRef(null);
+
+ const siteHosts = useStore((s) => s.preferences.siteHosts);
+ const pushToast = useStore((s) => s.pushToast);
+
+ // Cross-env targets are derived from configuration only — the URL itself
+ // is computed at click time so SPA navigation can never produce a stale
+ // share link.
+ const menuTargets = useMemo(() => {
+ if (siteHosts.length === 0) return [];
+ const currentMatch = findMappingForHost(location.hostname, siteHosts);
+ if (!currentMatch) return [];
+ const envs = availableEnvsFor(location.hostname, siteHosts);
+ const list: MenuTarget[] = [];
+ for (const env of SITE_ENV_ORDER) {
+ if (env === currentMatch.env || !envs.includes(env)) continue;
+ list.push({ key: env, label: SITE_ENV_LABELS[env], env });
+ }
+ return list;
+ }, [siteHosts]);
+
+ const hasMenu = menuTargets.length > 0;
+
+ useEffect(() => {
+ if (!open) return;
+
+ const onPointerDown = (e: Event) => {
+ const path = e.composedPath();
+ if (wrapperRef.current && !path.includes(wrapperRef.current)) {
+ setOpen(false);
+ }
+ };
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') setOpen(false);
+ };
+ const close = () => setOpen(false);
+
+ document.addEventListener('pointerdown', onPointerDown, true);
+ document.addEventListener('keydown', onKey);
+ window.addEventListener('resize', close);
+ window.addEventListener('scroll', close, true);
+
+ return () => {
+ document.removeEventListener('pointerdown', onPointerDown, true);
+ document.removeEventListener('keydown', onKey);
+ window.removeEventListener('resize', close);
+ window.removeEventListener('scroll', close, true);
+ };
+ }, [open]);
+
+ const copyShare = useCallback(
+ async (targetUrl: string, label: string) => {
+ const ok = await copyToClipboard(targetUrl);
+ pushToast(ok ? `Share link copied (${label})` : 'Copy failed', ok ? 'success' : 'error');
+ },
+ [pushToast]
+ );
+
+ const onShareClick = useCallback(() => {
+ setOpen(false);
+ const currentMatch = findMappingForHost(location.hostname, siteHosts);
+ const label = currentMatch ? `Current — ${SITE_ENV_LABELS[currentMatch.env]}` : 'Current page';
+ void copyShare(buildShareLink(location.href, uri), label);
+ }, [copyShare, siteHosts, uri]);
+
+ const onMenuClick = useCallback(
+ (target: MenuTarget) => {
+ setOpen(false);
+ const rewritten = rewriteUrlToEnv(location.href, target.env, siteHosts);
+ if (!rewritten) {
+ pushToast(`No ${target.label} host configured for this site`, 'error');
+ return;
+ }
+ void copyShare(buildShareLink(rewritten, uri), target.label);
+ },
+ [copyShare, pushToast, siteHosts, uri]
+ );
+
+ const toggle = useCallback(() => {
+ if (open) {
+ setOpen(false);
+ return;
+ }
+ const rect = triggerRef.current?.getBoundingClientRect();
+ if (!rect) return;
+ const flipUp = rect.bottom + ESTIMATED_MENU_HEIGHT + 8 > window.innerHeight;
+ setCoords({
+ top: flipUp ? rect.top - ESTIMATED_MENU_HEIGHT - 4 : rect.bottom + 4,
+ right: Math.max(8, window.innerWidth - rect.right),
+ });
+ setOpen(true);
+ }, [open]);
+
+ // Without a menu, render a normal pill button (full radius, full border)
+ // so it doesn't look visually broken / half-cut next to nothing.
+ if (!hasMenu) {
+ return (
+
+ Share
+
+ );
+ }
+
+ return (
+
+
+ Share
+
+
+ ▾
+
+ {open && coords && (
+
+ {menuTargets.map((t) => (
+ onMenuClick(t)}
+ >
+ Open on {t.label}
+ Rewrites the host for {t.label}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/content/panel/components/ShortcutOverlay.tsx b/src/content/panel/components/ShortcutOverlay.tsx
new file mode 100644
index 0000000..5a9d5fd
--- /dev/null
+++ b/src/content/panel/components/ShortcutOverlay.tsx
@@ -0,0 +1,40 @@
+import { Icon } from './Icon';
+import { SHORTCUTS } from '../hooks/useKeyboardShortcuts';
+import { useStore } from '../store';
+
+export function ShortcutOverlay() {
+ const visible = useStore((s) => s.showShortcuts);
+ const close = useStore((s) => s.toggleShortcuts);
+
+ if (!visible) return null;
+
+ return (
+ {
+ if (e.target === e.currentTarget) close();
+ }}
+ >
+
+
+
Keyboard Shortcuts
+
+
+
+
+ {SHORTCUTS.map((s) => (
+
+ {s.description}
+
+ {s.keys.map((k, i) => (
+
+ {k}
+
+ ))}
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/content/panel/components/Tabs.tsx b/src/content/panel/components/Tabs.tsx
new file mode 100644
index 0000000..1c93508
--- /dev/null
+++ b/src/content/panel/components/Tabs.tsx
@@ -0,0 +1,36 @@
+import type { PanelTab } from '../store';
+import { useStore } from '../store';
+
+const TABS: ReadonlyArray<{ id: PanelTab; label: string }> = [
+ { id: 'inspect', label: 'Inspect' },
+ { id: 'tree', label: 'Tree' },
+ { id: 'json', label: 'JSON' },
+ { id: 'diff', label: 'Diff' },
+ { id: 'seo', label: 'SEO' },
+ { id: 'notes', label: 'Notes' },
+];
+
+export function Tabs() {
+ const activeTab = useStore((s) => s.activeTab);
+ const setActiveTab = useStore((s) => s.setActiveTab);
+ const annotationCount = useStore((s) => s.annotations.length);
+
+ return (
+
+ {TABS.map((tab) => (
+ setActiveTab(tab.id)}
+ >
+ {tab.label}
+ {tab.id === 'notes' && annotationCount > 0 && (
+ {annotationCount}
+ )}
+
+ ))}
+
+ );
+}
diff --git a/src/content/panel/components/Toasts.tsx b/src/content/panel/components/Toasts.tsx
new file mode 100644
index 0000000..d0aedbe
--- /dev/null
+++ b/src/content/panel/components/Toasts.tsx
@@ -0,0 +1,27 @@
+import { useEffect } from 'react';
+import { useStore } from '../store';
+
+const DISMISS_AFTER_MS = 2200;
+
+export function Toasts() {
+ const toasts = useStore((s) => s.toasts);
+ const dismiss = useStore((s) => s.dismissToast);
+
+ useEffect(() => {
+ if (toasts.length === 0) return;
+ const timers = toasts.map((t) => setTimeout(() => dismiss(t.id), DISMISS_AFTER_MS));
+ return () => timers.forEach(clearTimeout);
+ }, [toasts, dismiss]);
+
+ if (toasts.length === 0) return null;
+
+ return (
+
+ {toasts.map((t) => (
+
+ {t.text}
+
+ ))}
+
+ );
+}
diff --git a/src/content/panel/hooks/useDraggable.ts b/src/content/panel/hooks/useDraggable.ts
new file mode 100644
index 0000000..0e775b5
--- /dev/null
+++ b/src/content/panel/hooks/useDraggable.ts
@@ -0,0 +1,120 @@
+import { useEffect, useState } from 'react';
+import type { CSSProperties } from 'react';
+import type { PanelPosition } from '@/lib/types';
+
+interface Anchor {
+ readonly x: number;
+ readonly y: number;
+}
+
+const DEFAULT_MARGIN = 24;
+
+function defaultAnchorFor(_corner: PanelPosition): Anchor {
+ return { x: DEFAULT_MARGIN, y: DEFAULT_MARGIN };
+}
+
+function isSideDock(p: PanelPosition): boolean {
+ return p === 'left-side' || p === 'right-side';
+}
+
+function isRightAnchored(p: PanelPosition): boolean {
+ return p === 'bottom-right' || p === 'top-right' || p === 'right-side';
+}
+
+function isBottomAnchored(p: PanelPosition): boolean {
+ return p === 'bottom-right' || p === 'bottom-left';
+}
+
+/**
+ * Positions the panel using anchor-edge CSS (right/bottom for right/bottom-
+ * anchored panels, left/top for the others). The anchor is the distance from
+ * the panel's anchored corner to the corresponding screen edge — so resizing
+ * width/height naturally grows the panel toward the screen interior, never
+ * off-screen.
+ *
+ * For side docks the panel is full-height; only width matters.
+ */
+export function useDraggable(
+ handleRef: React.RefObject,
+ corner: PanelPosition,
+ panelWidth: number,
+ panelHeight: number
+): { style: CSSProperties } {
+ const [anchor, setAnchor] = useState(() => defaultAnchorFor(corner));
+ const [prevCorner, setPrevCorner] = useState(corner);
+
+ if (prevCorner !== corner) {
+ setPrevCorner(corner);
+ setAnchor(defaultAnchorFor(corner));
+ }
+
+ const sideDock = isSideDock(corner);
+ const rightAnchor = isRightAnchored(corner);
+ const bottomAnchor = isBottomAnchored(corner);
+
+ useEffect(() => {
+ if (sideDock) return;
+ const handle = handleRef.current;
+ if (!handle) return;
+
+ const onMouseDown = (e: MouseEvent) => {
+ if ((e.target as HTMLElement).closest('button')) return;
+ const startAnchor = anchor;
+ const startX = e.clientX;
+ const startY = e.clientY;
+
+ const onMove = (ev: MouseEvent) => {
+ const dx = ev.clientX - startX;
+ const dy = ev.clientY - startY;
+ // Panel anchored to right? Then dragging right (+dx) DECREASES the
+ // distance from the right edge. Same for bottom.
+ const nextX = rightAnchor ? startAnchor.x - dx : startAnchor.x + dx;
+ const nextY = bottomAnchor ? startAnchor.y - dy : startAnchor.y + dy;
+ const maxX = window.innerWidth - 200;
+ const maxY = window.innerHeight - 80;
+ setAnchor({
+ x: Math.max(0, Math.min(maxX, nextX)),
+ y: Math.max(0, Math.min(maxY, nextY)),
+ });
+ };
+
+ const onUp = () => {
+ document.removeEventListener('mousemove', onMove);
+ document.removeEventListener('mouseup', onUp);
+ };
+
+ document.addEventListener('mousemove', onMove);
+ document.addEventListener('mouseup', onUp);
+ e.preventDefault();
+ };
+
+ handle.addEventListener('mousedown', onMouseDown);
+ return () => handle.removeEventListener('mousedown', onMouseDown);
+ }, [handleRef, anchor, sideDock, rightAnchor, bottomAnchor]);
+
+ if (sideDock) {
+ return {
+ style: {
+ top: 0,
+ bottom: 0,
+ left: corner === 'left-side' ? 0 : 'auto',
+ right: corner === 'right-side' ? 0 : 'auto',
+ width: panelWidth,
+ height: 'auto',
+ maxHeight: '100vh',
+ },
+ };
+ }
+
+ return {
+ style: {
+ left: rightAnchor ? 'auto' : anchor.x,
+ right: rightAnchor ? anchor.x : 'auto',
+ top: bottomAnchor ? 'auto' : anchor.y,
+ bottom: bottomAnchor ? anchor.y : 'auto',
+ width: panelWidth,
+ height: panelHeight,
+ maxHeight: 'calc(100vh - 40px)',
+ },
+ };
+}
diff --git a/src/content/panel/hooks/useElementSelection.ts b/src/content/panel/hooks/useElementSelection.ts
new file mode 100644
index 0000000..c1c0bb6
--- /dev/null
+++ b/src/content/panel/hooks/useElementSelection.ts
@@ -0,0 +1,74 @@
+import { useEffect } from 'react';
+import { setSelected, setHovered as setHoveredOutline } from '../../highlighter';
+import { useStore } from '../store';
+import type { ClayComponentInfo } from '@/lib/types';
+
+const INTERACTIVE_TAGS = new Set([
+ 'A',
+ 'BUTTON',
+ 'INPUT',
+ 'SELECT',
+ 'TEXTAREA',
+ 'LABEL',
+ 'AUDIO',
+ 'VIDEO',
+ 'DETAILS',
+ 'SUMMARY',
+]);
+
+function clickIsOnInteractive(target: EventTarget | null): boolean {
+ if (!(target instanceof HTMLElement)) return false;
+ let node: HTMLElement | null = target;
+ while (node && !node.hasAttribute('data-uri')) {
+ if (INTERACTIVE_TAGS.has(node.tagName)) return true;
+ if (node.isContentEditable) return true;
+ node = node.parentElement;
+ }
+ return false;
+}
+
+export function useElementSelection(): void {
+ const components = useStore((s) => s.components);
+ const setSelectedStore = useStore((s) => s.setSelected);
+
+ useEffect(() => {
+ const byElement = new Map(
+ components.map((c) => [c.element, c])
+ );
+
+ let hovered: HTMLElement | null = null;
+
+ const onClick = (e: MouseEvent) => {
+ const target = (e.target as HTMLElement)?.closest?.('[data-uri]') as HTMLElement | null;
+ if (!target) return;
+ const info = byElement.get(target);
+ if (!info) return;
+
+ // Always update the selection, but only swallow the event if the user
+ // didn't actually click a real interactive element (link, button, etc.).
+ const onInteractive = clickIsOnInteractive(e.target);
+ if (!onInteractive) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ const prev = useStore.getState().selected;
+ setSelected(prev?.element ?? null, target);
+ setSelectedStore(info);
+ };
+
+ const onMouseOver = (e: MouseEvent) => {
+ const target = (e.target as HTMLElement)?.closest?.('[data-uri]') as HTMLElement | null;
+ if (target === hovered) return;
+ setHoveredOutline(hovered, target);
+ hovered = target;
+ };
+
+ document.addEventListener('click', onClick, true);
+ document.addEventListener('mouseover', onMouseOver, true);
+ return () => {
+ document.removeEventListener('click', onClick, true);
+ document.removeEventListener('mouseover', onMouseOver, true);
+ setHoveredOutline(hovered, null);
+ };
+ }, [components, setSelectedStore]);
+}
diff --git a/src/content/panel/hooks/useKeyboardShortcuts.ts b/src/content/panel/hooks/useKeyboardShortcuts.ts
new file mode 100644
index 0000000..8e16906
--- /dev/null
+++ b/src/content/panel/hooks/useKeyboardShortcuts.ts
@@ -0,0 +1,130 @@
+import { useEffect } from 'react';
+import { copyToClipboard } from '@/lib/clipboard';
+import { buildUrl } from '@/lib/clay-uri';
+import type { RuntimeMessage } from '@/lib/types';
+import { useStore } from '../store';
+
+const SEQUENCE_TIMEOUT_MS = 600;
+
+export function useKeyboardShortcuts(): void {
+ useEffect(() => {
+ let pending: string | null = null;
+ let pendingTimer: ReturnType | null = null;
+
+ const clearPending = () => {
+ pending = null;
+ if (pendingTimer) clearTimeout(pendingTimer);
+ pendingTimer = null;
+ };
+
+ const handle = async (e: KeyboardEvent) => {
+ if (!useStore.getState().preferences.enableShortcuts) return;
+
+ // Bail when typing into ANY editable element — including those inside
+ // our Shadow DOM panel. Default `e.target` gets retargeted to the shadow
+ // host at document level, hiding the real focused element from us, so
+ // we walk `composedPath()` to find the actual focus surface.
+ const path = e.composedPath();
+ for (const node of path) {
+ if (!(node instanceof HTMLElement)) continue;
+ if (/^(input|textarea|select)$/i.test(node.tagName)) return;
+ if (node.isContentEditable) return;
+ }
+
+ const state = useStore.getState();
+ const {
+ page,
+ selected,
+ toggleShortcuts,
+ toggleCollapsed,
+ toggleHighlights,
+ pushToast,
+ setActiveTab,
+ } = state;
+ const envHost = state.preferences.environments[state.preferences.defaultEnvironment] ?? '';
+
+ if (e.key === '?' && (e.shiftKey || e.key === '?')) {
+ e.preventDefault();
+ toggleShortcuts();
+ return;
+ }
+ if (e.key === 'Escape') {
+ if (useStore.getState().showShortcuts) {
+ toggleShortcuts();
+ }
+ return;
+ }
+ if (e.key === '[') {
+ e.preventDefault();
+ toggleCollapsed();
+ return;
+ }
+ if ((e.key === 'h' || e.key === 'H') && !e.metaKey && !e.ctrlKey && !e.altKey) {
+ e.preventDefault();
+ toggleHighlights();
+ return;
+ }
+ if ((e.key === 't' || e.key === 'T') && !e.metaKey && !e.ctrlKey) {
+ e.preventDefault();
+ setActiveTab('tree');
+ return;
+ }
+ if ((e.key === 'i' || e.key === 'I') && !e.metaKey && !e.ctrlKey) {
+ e.preventDefault();
+ setActiveTab('inspect');
+ return;
+ }
+
+ if (pending) {
+ const combo = `${pending}${e.key}`;
+ clearPending();
+
+ if (combo === 'yp' && page) {
+ const ok = await copyToClipboard(page.pageUri);
+ pushToast(ok ? 'Page URI copied' : 'Copy failed', ok ? 'success' : 'error');
+ } else if (combo === 'yc' && selected) {
+ const ok = await copyToClipboard(selected.uri);
+ pushToast(ok ? 'Component URI copied' : 'Copy failed', ok ? 'success' : 'error');
+ } else if (combo === 'op' && page) {
+ chrome.runtime.sendMessage({
+ type: 'OPEN_TAB',
+ url: buildUrl(page.pageUri, '', envHost),
+ } satisfies RuntimeMessage);
+ } else if (combo === 'oc' && (selected || page)) {
+ const uri = selected?.uri ?? page?.pageUri;
+ if (uri) {
+ chrome.runtime.sendMessage({
+ type: 'OPEN_TAB',
+ url: buildUrl(uri, '', envHost),
+ } satisfies RuntimeMessage);
+ }
+ }
+ return;
+ }
+
+ if (e.key === 'y' || e.key === 'o') {
+ pending = e.key;
+ pendingTimer = setTimeout(clearPending, SEQUENCE_TIMEOUT_MS);
+ }
+ };
+
+ document.addEventListener('keydown', handle);
+ return () => {
+ document.removeEventListener('keydown', handle);
+ clearPending();
+ };
+ }, []);
+}
+
+export const SHORTCUTS = [
+ { keys: ['?'], description: 'Show this shortcut overlay' },
+ { keys: ['['], description: 'Collapse / expand the panel' },
+ { keys: ['h'], description: 'Toggle component outlines' },
+ { keys: ['i'], description: 'Switch to Inspect tab' },
+ { keys: ['t'], description: 'Switch to Tree tab' },
+ { keys: ['y', 'p'], description: 'Copy current page URI' },
+ { keys: ['y', 'c'], description: 'Copy selected component URI' },
+ { keys: ['o', 'p'], description: 'Open current page in new tab' },
+ { keys: ['o', 'c'], description: 'Open selected component in new tab' },
+ { keys: ['Esc'], description: 'Close overlays' },
+] as const;
diff --git a/src/content/panel/hooks/useThemedRoot.ts b/src/content/panel/hooks/useThemedRoot.ts
new file mode 100644
index 0000000..87adac8
--- /dev/null
+++ b/src/content/panel/hooks/useThemedRoot.ts
@@ -0,0 +1,30 @@
+import { useEffect, useMemo, useState } from 'react';
+import type { CSSProperties } from 'react';
+import { darkTheme, lightTheme, tokensToCssVars, type ThemeMode } from '../theme';
+import { useStore } from '../store';
+
+export function useThemedRoot(): { style: CSSProperties; mode: ThemeMode } {
+ const themeMode = useStore((s) => s.preferences.theme);
+
+ const [systemDark, setSystemDark] = useState(
+ () => window.matchMedia('(prefers-color-scheme: dark)').matches
+ );
+
+ useEffect(() => {
+ if (themeMode !== 'auto') return;
+ const mql = window.matchMedia('(prefers-color-scheme: dark)');
+ const handler = (e: MediaQueryListEvent) => setSystemDark(e.matches);
+ mql.addEventListener('change', handler);
+ return () => mql.removeEventListener('change', handler);
+ }, [themeMode]);
+
+ const resolved = useMemo(() => {
+ const mode: ThemeMode = themeMode === 'auto' ? (systemDark ? 'dark' : 'light') : themeMode;
+ return { mode, tokens: mode === 'dark' ? darkTheme : lightTheme };
+ }, [themeMode, systemDark]);
+
+ return {
+ style: tokensToCssVars(resolved.tokens) as unknown as CSSProperties,
+ mode: resolved.mode,
+ };
+}
diff --git a/src/content/panel/store.ts b/src/content/panel/store.ts
new file mode 100644
index 0000000..01b035c
--- /dev/null
+++ b/src/content/panel/store.ts
@@ -0,0 +1,105 @@
+import { create } from 'zustand';
+import type {
+ Annotation,
+ ClayComponentInfo,
+ ClayPageInfo,
+ RecentComponent,
+ UserPreferences,
+} from '@/lib/types';
+import { DEFAULT_PREFERENCES } from '@/lib/types';
+import { readPageInfo } from '../page-info';
+
+export type PanelTab = 'inspect' | 'tree' | 'json' | 'diff' | 'seo' | 'notes';
+
+interface ToastMessage {
+ readonly id: number;
+ readonly text: string;
+ readonly tone: 'info' | 'success' | 'error';
+}
+
+interface FindState {
+ readonly query: string;
+ readonly index: number;
+}
+
+interface StoreState {
+ page: ClayPageInfo | null;
+ components: ClayComponentInfo[];
+ selected: ClayComponentInfo | null;
+ hovered: ClayComponentInfo | null;
+ collapsed: boolean;
+ search: string;
+ find: FindState;
+ activeTab: PanelTab;
+ showShortcuts: boolean;
+ highlightEnabled: boolean;
+ preferences: UserPreferences;
+ toasts: ToastMessage[];
+ recents: RecentComponent[];
+ annotations: Annotation[];
+ annotatedUris: Set;
+
+ setComponents: (components: ClayComponentInfo[]) => void;
+ setSelected: (next: ClayComponentInfo | null) => void;
+ setHovered: (next: ClayComponentInfo | null) => void;
+ toggleCollapsed: () => void;
+ setSearch: (q: string) => void;
+ setFind: (next: FindState) => void;
+ setActiveTab: (tab: PanelTab) => void;
+ toggleShortcuts: () => void;
+ toggleHighlights: () => void;
+ setPreferences: (next: Partial) => void;
+ setRecents: (next: RecentComponent[]) => void;
+ setAnnotations: (next: Annotation[]) => void;
+ pushToast: (text: string, tone?: ToastMessage['tone']) => void;
+ dismissToast: (id: number) => void;
+}
+
+let toastSeq = 0;
+
+export const useStore = create()((set) => ({
+ page: readPageInfo(),
+ components: [],
+ selected: null,
+ hovered: null,
+ // Mount in collapsed state — the panel renders as a small floating "Clay"
+ // button (FAB) until the user clicks it. Matches the standard pattern used
+ // by Sentry, Hotjar, Crisp, Intercom etc. See {@link Fab}.
+ collapsed: true,
+ search: '',
+ find: { query: '', index: 0 },
+ activeTab: 'inspect',
+ showShortcuts: false,
+ highlightEnabled: true,
+ preferences: DEFAULT_PREFERENCES,
+ toasts: [],
+ recents: [],
+ annotations: [],
+ annotatedUris: new Set(),
+
+ setComponents: (components) => set({ components, page: readPageInfo() }),
+ setSelected: (selected) => set({ selected }),
+ setHovered: (hovered) => set({ hovered }),
+ toggleCollapsed: () => set((s) => ({ collapsed: !s.collapsed })),
+ setSearch: (search) => set({ search }),
+ setFind: (find) => set({ find }),
+ setActiveTab: (activeTab) => set({ activeTab }),
+ toggleShortcuts: () => set((s) => ({ showShortcuts: !s.showShortcuts })),
+ toggleHighlights: () => set((s) => ({ highlightEnabled: !s.highlightEnabled })),
+ setPreferences: (prefs) => set((s) => ({ preferences: { ...s.preferences, ...prefs } })),
+ setRecents: (recents) => set({ recents }),
+ setAnnotations: (annotations) =>
+ set({
+ annotations,
+ annotatedUris: new Set(annotations.map((a) => a.uri)),
+ }),
+ pushToast: (text, tone = 'info') =>
+ set((s) => ({
+ toasts: [...s.toasts, { id: ++toastSeq, text, tone }],
+ })),
+ dismissToast: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })),
+}));
+
+export function useEnvHost(): string {
+ return useStore((s) => s.preferences.environments[s.preferences.defaultEnvironment] ?? '');
+}
diff --git a/src/content/panel/styles.css b/src/content/panel/styles.css
new file mode 100644
index 0000000..d029b8a
--- /dev/null
+++ b/src/content/panel/styles.css
@@ -0,0 +1,1198 @@
+:host,
+:root {
+ all: initial;
+}
+
+* {
+ box-sizing: border-box;
+ font-family:
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+}
+
+.cs-panel {
+ position: fixed;
+ width: 380px;
+ max-height: calc(100vh - 80px);
+ background: var(--cs-bg);
+ color: var(--cs-text);
+ border: 1px solid var(--cs-border);
+ border-radius: 12px;
+ font-size: 13px;
+ line-height: 1.45;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ user-select: none;
+}
+
+/* Floating Clay button — the panel's collapsed/idle state. Standard
+ "extension chrome" pattern (Sentry, Hotjar, Crisp, Intercom, etc.):
+ small circular FAB anchored to the user's preferred corner. Click
+ expands it to the full panel; the panel's collapse button shrinks
+ it back here. */
+.cs-fab {
+ width: 52px;
+ height: 52px;
+ border-radius: 50%;
+ background: var(--cs-bg-elevated);
+ border: 1px solid var(--cs-border);
+ cursor: pointer;
+ font-family: inherit;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow:
+ 0 2px 6px rgba(0, 0, 0, 0.18),
+ 0 10px 28px rgba(0, 0, 0, 0.22);
+ transition:
+ transform 0.12s ease,
+ box-shadow 0.12s ease,
+ border-color 0.12s ease;
+}
+
+.cs-fab:hover {
+ transform: scale(1.06);
+ border-color: var(--cs-accent);
+ box-shadow:
+ 0 4px 10px rgba(0, 0, 0, 0.22),
+ 0 12px 32px rgba(0, 0, 0, 0.26);
+}
+
+.cs-fab:active {
+ transform: scale(0.97);
+}
+
+.cs-fab:focus-visible {
+ outline: 2px solid var(--cs-accent);
+ outline-offset: 3px;
+}
+
+.cs-fab-logo {
+ width: 36px;
+ height: 36px;
+ display: block;
+ pointer-events: none;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+.cs-fab-badge {
+ position: absolute;
+ top: -4px;
+ right: -4px;
+ min-width: 18px;
+ height: 18px;
+ padding: 0 5px;
+ border-radius: 9px;
+ background: var(--cs-accent);
+ color: #fff;
+ border: 2px solid var(--cs-bg);
+ font-size: 10px;
+ font-weight: 700;
+ line-height: 14px;
+ text-align: center;
+}
+
+.cs-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 12px;
+ background: var(--cs-bg-elevated);
+ border-bottom: 1px solid var(--cs-border);
+ cursor: grab;
+}
+
+.cs-header:active {
+ cursor: grabbing;
+}
+
+.cs-logo {
+ width: 22px;
+ height: 22px;
+ display: block;
+ flex: 0 0 auto;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+.cs-title {
+ font-weight: 600;
+ font-size: 13px;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+
+.cs-title-text {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.cs-count {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 18px;
+ height: 18px;
+ padding: 0 5px;
+ border-radius: 9px;
+ background: var(--cs-accent-bg);
+ color: var(--cs-accent);
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1;
+}
+
+.cs-status {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-weight: 600;
+}
+
+.cs-status.cs-published {
+ background: var(--cs-success);
+ color: #fff;
+}
+
+.cs-status.cs-draft {
+ background: var(--cs-warning);
+ color: #fff;
+}
+
+.cs-icon-btn {
+ background: transparent;
+ border: none;
+ color: var(--cs-text-muted);
+ cursor: pointer;
+ padding: 4px;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.cs-icon-btn:hover {
+ background: var(--cs-bg-hover);
+ color: var(--cs-text);
+}
+
+.cs-icon-btn.cs-icon-btn-off {
+ color: var(--cs-text-subtle);
+}
+
+.cs-tabs {
+ display: flex;
+ gap: 0;
+ border-bottom: 1px solid var(--cs-border);
+ background: var(--cs-bg);
+}
+
+.cs-tab {
+ flex: 1;
+ background: transparent;
+ border: none;
+ color: var(--cs-text-muted);
+ padding: 8px 4px;
+ font-size: 12px;
+ font-weight: 500;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition:
+ color 0.12s,
+ border-color 0.12s;
+}
+
+.cs-tab:hover {
+ color: var(--cs-text);
+}
+
+.cs-tab.cs-active {
+ color: var(--cs-accent);
+ border-bottom-color: var(--cs-accent);
+}
+
+.cs-body {
+ overflow-y: auto;
+ padding: 12px;
+ flex: 1;
+}
+
+.cs-section {
+ margin-bottom: 14px;
+}
+
+.cs-section:last-child {
+ margin-bottom: 0;
+}
+
+.cs-section-title {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--cs-text-subtle);
+ margin: 0 0 6px;
+}
+
+.cs-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--cs-text);
+ margin: 0;
+ word-break: break-word;
+}
+
+.cs-instance {
+ font-family: var(--cs-mono);
+ font-size: 11px;
+ color: var(--cs-text-muted);
+ word-break: break-all;
+}
+
+.cs-link-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-top: 8px;
+}
+
+/* "View on prod / staging / qa" cross-environment pill row */
+.cs-view-on {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-items: center;
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px dashed var(--cs-border);
+}
+
+.cs-view-on-label {
+ font-size: 11px;
+ color: var(--cs-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ font-weight: 600;
+}
+
+.cs-view-on-pill {
+ background: var(--cs-bg-elevated);
+ color: var(--cs-text);
+ border: 1px solid var(--cs-border);
+ border-radius: 999px;
+ padding: 3px 10px;
+ font-size: 11px;
+ font-family: inherit;
+ cursor: pointer;
+ font-weight: 500;
+}
+
+.cs-view-on-pill:hover:not(:disabled) {
+ border-color: var(--cs-accent);
+ color: var(--cs-accent);
+}
+
+.cs-view-on-pill:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+}
+
+.cs-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ background: var(--cs-bg-elevated);
+ color: var(--cs-text) !important;
+ border: 1px solid var(--cs-border);
+ border-radius: 6px;
+ padding: 4px 8px;
+ font-size: 11px;
+ font-weight: 500;
+ text-decoration: none;
+ cursor: pointer;
+ transition:
+ background 0.12s,
+ border-color 0.12s;
+}
+
+.cs-link:hover {
+ background: var(--cs-bg-hover);
+ border-color: var(--cs-borderStrong);
+ color: var(--cs-accent) !important;
+}
+
+.cs-link.cs-link-primary {
+ background: var(--cs-accent);
+ color: #fff !important;
+ border-color: var(--cs-accent);
+}
+
+.cs-link.cs-link-primary:hover {
+ filter: brightness(1.05);
+}
+
+.cs-empty {
+ text-align: center;
+ padding: 32px 16px;
+ color: var(--cs-text-subtle);
+ font-size: 12px;
+}
+
+.cs-search {
+ width: 100%;
+ background: var(--cs-bg-elevated);
+ color: var(--cs-text);
+ border: 1px solid var(--cs-border);
+ border-radius: 6px;
+ padding: 6px 8px;
+ font-size: 12px;
+ outline: none;
+}
+
+.cs-search:focus {
+ border-color: var(--cs-accent);
+}
+
+.cs-tree {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ font-family: var(--cs-mono);
+ font-size: 11px;
+}
+
+.cs-tree-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 6px;
+ border-radius: 4px;
+ cursor: pointer;
+ color: var(--cs-text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.cs-tree-item:hover {
+ background: var(--cs-bg-hover);
+}
+
+.cs-tree-item.cs-selected {
+ background: var(--cs-accent-bg);
+ color: var(--cs-accent);
+}
+
+.cs-tree-instance {
+ color: var(--cs-text-subtle);
+ font-size: 10px;
+}
+
+.cs-breadcrumb {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px;
+ font-size: 11px;
+ color: var(--cs-text-muted);
+ margin-bottom: 8px;
+}
+
+.cs-breadcrumb-item {
+ background: transparent;
+ border: none;
+ color: var(--cs-text-muted);
+ cursor: pointer;
+ padding: 2px 4px;
+ border-radius: 3px;
+ font-size: 11px;
+}
+
+.cs-breadcrumb-item:hover {
+ background: var(--cs-bg-hover);
+ color: var(--cs-text);
+}
+
+.cs-breadcrumb-sep {
+ color: var(--cs-text-subtle);
+}
+
+.cs-json {
+ background: var(--cs-bg-elevated);
+ color: var(--cs-text);
+ border: 1px solid var(--cs-border);
+ border-radius: 6px;
+ padding: 8px;
+ font-family: var(--cs-mono);
+ font-size: 11px;
+ white-space: pre-wrap;
+ word-break: break-word;
+ max-height: 320px;
+ overflow: auto;
+}
+
+.cs-json-key {
+ color: var(--cs-accent);
+}
+
+.cs-json-string {
+ color: var(--cs-success);
+}
+
+.cs-json-number {
+ color: var(--cs-warning);
+}
+
+.cs-json-bool {
+ color: var(--cs-warning);
+ font-weight: 600;
+}
+
+.cs-json-null {
+ color: var(--cs-text-subtle);
+ font-style: italic;
+}
+
+.cs-loading {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--cs-text-muted);
+ font-size: 12px;
+ padding: 16px;
+ justify-content: center;
+}
+
+.cs-spinner {
+ width: 12px;
+ height: 12px;
+ border: 2px solid var(--cs-border);
+ border-top-color: var(--cs-accent);
+ border-radius: 50%;
+ animation: cs-spin 0.8s linear infinite;
+}
+
+@keyframes cs-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.cs-shortcut-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1;
+}
+
+.cs-shortcut-card {
+ background: var(--cs-bg);
+ border: 1px solid var(--cs-border);
+ border-radius: 12px;
+ padding: 20px 24px;
+ min-width: 360px;
+ max-width: 90vw;
+ color: var(--cs-text);
+}
+
+.cs-shortcut-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 6px 0;
+ font-size: 13px;
+}
+
+.cs-kbd {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ background: var(--cs-bg-elevated);
+ border: 1px solid var(--cs-border);
+ border-radius: 4px;
+ padding: 2px 6px;
+ font-family: var(--cs-mono);
+ font-size: 11px;
+ color: var(--cs-text);
+}
+
+.cs-toast-stack {
+ position: fixed;
+ bottom: 16px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ pointer-events: none;
+}
+
+.cs-toast {
+ background: var(--cs-bg);
+ color: var(--cs-text);
+ border: 1px solid var(--cs-border);
+ border-radius: 8px;
+ padding: 8px 14px;
+ font-size: 12px;
+ font-weight: 500;
+ pointer-events: auto;
+}
+
+.cs-toast.cs-toast-success {
+ border-color: var(--cs-success);
+}
+
+.cs-toast.cs-toast-error {
+ border-color: var(--cs-danger);
+}
+
+.cs-env-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ background: var(--cs-bg-elevated);
+ border: 1px solid var(--cs-border);
+ border-radius: 999px;
+ padding: 2px 8px;
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--cs-text-muted);
+ cursor: pointer;
+}
+
+.cs-env-pill:hover {
+ border-color: var(--cs-accent);
+ color: var(--cs-accent);
+}
+
+.cs-env-pill.cs-env-pill-active {
+ background: var(--cs-accent-bg);
+ border-color: var(--cs-accent);
+ color: var(--cs-accent);
+}
+
+.cs-diff-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+}
+
+.cs-diff-col-header {
+ font-size: 10px;
+ text-transform: uppercase;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ color: var(--cs-text-subtle);
+ margin-bottom: 4px;
+}
+
+.cs-diff-removed {
+ background: rgba(220, 38, 38, 0.12);
+ color: var(--cs-danger);
+}
+
+.cs-diff-added {
+ background: rgba(22, 163, 74, 0.12);
+ color: var(--cs-success);
+}
+
+/* ============================================================
+ Side-dock variants (left-side / right-side)
+ ============================================================ */
+
+.cs-panel.cs-pos-left-side,
+.cs-panel.cs-pos-right-side {
+ border-radius: 0;
+ max-height: 100vh;
+}
+
+.cs-panel.cs-pos-left-side {
+ border-right: 1px solid var(--cs-border);
+ border-left: none;
+ border-top: none;
+ border-bottom: none;
+}
+
+.cs-panel.cs-pos-right-side {
+ border-left: 1px solid var(--cs-border);
+ border-right: none;
+ border-top: none;
+ border-bottom: none;
+}
+
+/* ============================================================
+ Resize handles — width (vertical strip), height (horizontal
+ strip), and corner (small grabber). All sit just inside the
+ panel edge so they're not clipped by `overflow: hidden`.
+ ============================================================ */
+
+.cs-resize {
+ position: absolute;
+ background: transparent;
+ z-index: 10;
+}
+
+.cs-resize:hover {
+ background: var(--cs-accent-bg);
+}
+
+.cs-resize-width {
+ top: 0;
+ bottom: 0;
+ width: 6px;
+}
+
+.cs-resize-height {
+ left: 0;
+ right: 0;
+ height: 6px;
+}
+
+.cs-resize-left {
+ left: 0;
+}
+
+.cs-resize-right {
+ right: 0;
+}
+
+.cs-resize-top {
+ top: 0;
+}
+
+.cs-resize-bottom {
+ bottom: 0;
+}
+
+.cs-resize-corner {
+ width: 14px;
+ height: 14px;
+ z-index: 11;
+}
+
+.cs-resize-corner:hover {
+ background: var(--cs-accent);
+ opacity: 0.4;
+ border-radius: 2px;
+}
+
+.cs-resize-corner-top-left {
+ top: 0;
+ left: 0;
+}
+
+.cs-resize-corner-top-right {
+ top: 0;
+ right: 0;
+}
+
+.cs-resize-corner-bottom-left {
+ bottom: 0;
+ left: 0;
+}
+
+.cs-resize-corner-bottom-right {
+ bottom: 0;
+ right: 0;
+}
+
+/* ============================================================
+ Tab badge (Notes count, etc.)
+ ============================================================ */
+
+.cs-tab-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 16px;
+ height: 16px;
+ margin-left: 6px;
+ padding: 0 5px;
+ border-radius: 8px;
+ background: var(--cs-accent);
+ color: #fff;
+ font-size: 10px;
+ font-weight: 700;
+}
+
+/* ============================================================
+ Find-on-page status row
+ ============================================================ */
+
+.cs-find-status {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 6px;
+ font-size: 11px;
+ color: var(--cs-text-muted);
+}
+
+.cs-find-actions {
+ display: flex;
+ gap: 4px;
+}
+
+.cs-tree-item.cs-current-match {
+ outline: 1px solid var(--cs-accent);
+ background: var(--cs-accent-bg);
+}
+
+/* ============================================================
+ Edit / Share / Screenshot link variants
+ ============================================================ */
+
+.cs-link-edit {
+ border-color: var(--cs-warning);
+ color: var(--cs-warning);
+}
+
+.cs-link-edit:hover {
+ background: rgba(217, 119, 6, 0.1);
+}
+
+/* ============================================================
+ Copy-as details flyout
+ ============================================================ */
+
+.cs-copy-as {
+ margin-top: 10px;
+ font-size: 12px;
+ color: var(--cs-text-muted);
+}
+
+.cs-copy-as summary {
+ cursor: pointer;
+ user-select: none;
+ padding: 4px 0;
+}
+
+.cs-copy-as[open] summary {
+ margin-bottom: 4px;
+}
+
+/* ============================================================
+ Annotation editor
+ ============================================================ */
+
+.cs-annotation {
+ margin-top: 16px;
+ padding-top: 12px;
+ border-top: 1px dashed var(--cs-border);
+}
+
+.cs-annotation-meta {
+ margin-left: 8px;
+ font-size: 10px;
+ font-weight: 400;
+ color: var(--cs-text-subtle);
+}
+
+.cs-annotation-input {
+ width: 100%;
+ min-height: 60px;
+ resize: vertical;
+ font-family: inherit;
+ font-size: 12px;
+ padding: 6px 8px;
+ border: 1px solid var(--cs-border);
+ border-radius: 6px;
+ background: var(--cs-bg);
+ color: var(--cs-text);
+}
+
+.cs-annotation-input:focus {
+ outline: none;
+ border-color: var(--cs-accent);
+}
+
+.cs-annotation-actions {
+ display: flex;
+ gap: 6px;
+ margin-top: 6px;
+}
+
+.cs-annotation-actions .cs-link[disabled] {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+/* ============================================================
+ Notes tab list
+ ============================================================ */
+
+.cs-notes {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.cs-notes-item {
+ border: 1px solid var(--cs-border);
+ border-radius: 8px;
+ padding: 10px 12px;
+ background: var(--cs-bg-elevated);
+}
+
+.cs-notes-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.cs-notes-title {
+ background: transparent;
+ border: none;
+ color: var(--cs-text);
+ font-weight: 600;
+ font-size: 13px;
+ cursor: pointer;
+ padding: 0;
+ text-align: left;
+}
+
+.cs-notes-title:hover {
+ color: var(--cs-accent);
+}
+
+.cs-notes-body {
+ margin: 6px 0 4px;
+ font-size: 12px;
+ color: var(--cs-text);
+ white-space: pre-wrap;
+ line-height: 1.5;
+}
+
+.cs-notes-meta {
+ margin: 0;
+ font-size: 10px;
+ color: var(--cs-text-subtle);
+}
+
+/* ============================================================
+ Recents section
+ ============================================================ */
+
+.cs-recents {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.cs-recents-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 4px 6px;
+ border-radius: 4px;
+}
+
+.cs-recents-item:hover {
+ background: var(--cs-bg-hover);
+}
+
+.cs-recents-name {
+ background: transparent;
+ border: none;
+ color: var(--cs-text);
+ cursor: pointer;
+ padding: 0;
+ font-size: 12px;
+ text-align: left;
+}
+
+.cs-recents-name:hover {
+ color: var(--cs-accent);
+}
+
+.cs-recents-here {
+ color: var(--cs-success);
+ font-size: 10px;
+ font-weight: 600;
+}
+
+.cs-recents-meta {
+ font-size: 10px;
+ color: var(--cs-text-subtle);
+}
+
+/* ============================================================
+ Export menu
+ ============================================================ */
+
+.cs-export {
+ position: relative;
+ display: inline-block;
+}
+
+/* Split share button — main face copies the current page link; the trailing
+ ▾ chip opens a tiny menu of cross-environment share targets. */
+.cs-share-split {
+ display: inline-flex;
+ align-items: stretch;
+ position: relative;
+}
+
+.cs-share-split .cs-share-main {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-right: none;
+}
+
+.cs-share-split .cs-share-toggle {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ padding-left: 6px;
+ padding-right: 6px;
+ font-weight: 700;
+}
+
+.cs-export-menu {
+ position: fixed;
+ min-width: 200px;
+ background: var(--cs-bg-elevated);
+ border: 1px solid var(--cs-border);
+ border-radius: 6px;
+ padding: 4px;
+ z-index: 2147483646;
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.cs-export-item {
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 6px 8px;
+ border-radius: 4px;
+ text-align: left;
+ display: flex;
+ flex-direction: column;
+ color: var(--cs-text);
+}
+
+.cs-export-item:hover {
+ background: var(--cs-bg-hover);
+}
+
+.cs-export-label {
+ font-weight: 600;
+ font-size: 12px;
+}
+
+.cs-export-help {
+ font-size: 10px;
+ color: var(--cs-text-subtle);
+}
+
+/* ============================================================
+ Diff controls
+ ============================================================ */
+
+.cs-diff-controls {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+ font-size: 12px;
+ color: var(--cs-text-muted);
+}
+
+.cs-diff-controls label {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.cs-diff-controls select {
+ background: var(--cs-bg);
+ color: var(--cs-text);
+ border: 1px solid var(--cs-border);
+ border-radius: 4px;
+ padding: 3px 6px;
+ font-size: 12px;
+ font-family: inherit;
+}
+
+/* ============================================================
+ SEO tab
+ ============================================================ */
+
+.cs-seo-list {
+ margin: 0;
+ display: grid;
+ grid-template-columns: 90px 1fr;
+ row-gap: 6px;
+ column-gap: 12px;
+ font-size: 12px;
+}
+
+.cs-seo-list > div {
+ display: contents;
+}
+
+.cs-seo-list dt {
+ color: var(--cs-text-subtle);
+ font-weight: 600;
+ text-transform: uppercase;
+ font-size: 10px;
+ letter-spacing: 0.05em;
+ align-self: start;
+ padding-top: 2px;
+}
+
+.cs-seo-list dd {
+ margin: 0;
+ word-break: break-word;
+ color: var(--cs-text);
+}
+
+.cs-seo-len {
+ margin-left: 6px;
+ font-size: 10px;
+ color: var(--cs-text-subtle);
+}
+
+.cs-seo-card {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--cs-border);
+ border-radius: 8px;
+ overflow: hidden;
+ background: var(--cs-bg-elevated);
+}
+
+.cs-seo-card-image {
+ width: 100%;
+ aspect-ratio: 1.91 / 1;
+ background: var(--cs-bg-hover);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.cs-seo-card-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+.cs-seo-card-placeholder {
+ font-size: 11px;
+ color: var(--cs-text-subtle);
+}
+
+.cs-seo-card-body {
+ padding: 8px 10px;
+}
+
+.cs-seo-card-host {
+ margin: 0 0 2px;
+ font-size: 10px;
+ color: var(--cs-text-subtle);
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+
+.cs-seo-card-title {
+ margin: 0 0 4px;
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--cs-text);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.cs-seo-card-desc {
+ margin: 0;
+ font-size: 11px;
+ color: var(--cs-text-muted);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.cs-seo-issues {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.cs-seo-issue {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 8px;
+ border-radius: 6px;
+ font-size: 12px;
+ border-left: 3px solid var(--cs-border);
+ background: var(--cs-bg-elevated);
+}
+
+.cs-seo-issue-tag {
+ font-size: 9px;
+ text-transform: uppercase;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ padding: 2px 6px;
+ border-radius: 3px;
+ background: var(--cs-bg-hover);
+ color: var(--cs-text-muted);
+}
+
+.cs-seo-issue-error {
+ border-left-color: var(--cs-danger);
+}
+
+.cs-seo-issue-error .cs-seo-issue-tag {
+ background: rgba(220, 38, 38, 0.15);
+ color: var(--cs-danger);
+}
+
+.cs-seo-issue-warn {
+ border-left-color: var(--cs-warning);
+}
+
+.cs-seo-issue-warn .cs-seo-issue-tag {
+ background: rgba(217, 119, 6, 0.15);
+ color: var(--cs-warning);
+}
+
+.cs-seo-issue-info {
+ border-left-color: var(--cs-accent);
+}
+
+.cs-seo-issue-info .cs-seo-issue-tag {
+ background: var(--cs-accent-bg);
+ color: var(--cs-accent);
+}
diff --git a/src/content/panel/theme.ts b/src/content/panel/theme.ts
new file mode 100644
index 0000000..88f9808
--- /dev/null
+++ b/src/content/panel/theme.ts
@@ -0,0 +1,71 @@
+export type ThemeMode = 'light' | 'dark';
+
+export interface ThemeTokens {
+ readonly bg: string;
+ readonly bgElevated: string;
+ readonly bgHover: string;
+ readonly border: string;
+ readonly borderStrong: string;
+ readonly text: string;
+ readonly textMuted: string;
+ readonly textSubtle: string;
+ readonly accent: string;
+ readonly accentBg: string;
+ readonly success: string;
+ readonly warning: string;
+ readonly danger: string;
+ readonly mono: string;
+}
+
+export const lightTheme: ThemeTokens = {
+ bg: '#ffffff',
+ bgElevated: '#f6f7f9',
+ bgHover: '#eef0f3',
+ border: '#e5e7eb',
+ borderStrong: '#d1d5db',
+ text: '#0f172a',
+ textMuted: '#475569',
+ textSubtle: '#94a3b8',
+ accent: '#e22c2c',
+ accentBg: 'rgba(226, 44, 44, 0.08)',
+ success: '#16a34a',
+ warning: '#d97706',
+ danger: '#dc2626',
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
+};
+
+export const darkTheme: ThemeTokens = {
+ bg: '#0f1115',
+ bgElevated: '#161922',
+ bgHover: '#1f2330',
+ border: '#252a36',
+ borderStrong: '#363c4d',
+ text: '#e6e8ee',
+ textMuted: '#9aa3b2',
+ textSubtle: '#5f6776',
+ accent: '#ff5c5c',
+ accentBg: 'rgba(255, 92, 92, 0.12)',
+ success: '#34d399',
+ warning: '#fbbf24',
+ danger: '#f87171',
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
+};
+
+export function tokensToCssVars(tokens: ThemeTokens): Record {
+ return {
+ '--cs-bg': tokens.bg,
+ '--cs-bg-elevated': tokens.bgElevated,
+ '--cs-bg-hover': tokens.bgHover,
+ '--cs-border': tokens.border,
+ '--cs-border-strong': tokens.borderStrong,
+ '--cs-text': tokens.text,
+ '--cs-text-muted': tokens.textMuted,
+ '--cs-text-subtle': tokens.textSubtle,
+ '--cs-accent': tokens.accent,
+ '--cs-accent-bg': tokens.accentBg,
+ '--cs-success': tokens.success,
+ '--cs-warning': tokens.warning,
+ '--cs-danger': tokens.danger,
+ '--cs-mono': tokens.mono,
+ };
+}
diff --git a/src/content/shadow-host.ts b/src/content/shadow-host.ts
new file mode 100644
index 0000000..10afaed
--- /dev/null
+++ b/src/content/shadow-host.ts
@@ -0,0 +1,50 @@
+import { createRoot, type Root } from 'react-dom/client';
+import { createElement } from 'react';
+import panelCss from './panel/styles.css?inline';
+import { App } from './panel/App';
+
+const HOST_ID = 'clay-slip-shadow-host';
+
+let root: Root | null = null;
+let host: HTMLDivElement | null = null;
+
+export function mountPanel(): void {
+ if (host) return;
+
+ host = document.createElement('div');
+ host.id = HOST_ID;
+ host.style.position = 'fixed';
+ host.style.zIndex = '2147483647';
+ host.style.inset = 'auto 0 0 auto';
+ host.style.pointerEvents = 'none';
+ document.documentElement.appendChild(host);
+
+ const shadow = host.attachShadow({ mode: 'open' });
+
+ const style = document.createElement('style');
+ style.textContent = panelCss;
+ shadow.appendChild(style);
+
+ const reactMount = document.createElement('div');
+ reactMount.id = 'clay-slip-root';
+ reactMount.style.pointerEvents = 'auto';
+ shadow.appendChild(reactMount);
+
+ root = createRoot(reactMount);
+ root.render(createElement(App));
+}
+
+export function unmountPanel(): void {
+ root?.unmount();
+ host?.remove();
+ root = null;
+ host = null;
+}
+
+export function isPanelMounted(): boolean {
+ return host !== null;
+}
+
+export function getPanelHost(): HTMLElement | null {
+ return host;
+}
diff --git a/src/lib/annotations.ts b/src/lib/annotations.ts
new file mode 100644
index 0000000..866595f
--- /dev/null
+++ b/src/lib/annotations.ts
@@ -0,0 +1,62 @@
+import type { Annotation } from './types';
+
+const STORAGE_KEY = 'annotations';
+
+type AnnotationMap = Record;
+
+async function loadMap(): Promise {
+ const stored = await chrome.storage.local.get(STORAGE_KEY);
+ return (stored[STORAGE_KEY] as AnnotationMap | undefined) ?? {};
+}
+
+async function saveMap(map: AnnotationMap): Promise {
+ await chrome.storage.local.set({ [STORAGE_KEY]: map });
+}
+
+export async function listAnnotations(): Promise {
+ const map = await loadMap();
+ return Object.values(map).sort((a, b) => b.updatedAt - a.updatedAt);
+}
+
+export async function getAnnotation(uri: string): Promise {
+ const map = await loadMap();
+ return map[uri] ?? null;
+}
+
+export async function getAnnotationUris(): Promise> {
+ const map = await loadMap();
+ return new Set(Object.keys(map));
+}
+
+export async function upsertAnnotation(meta: Omit): Promise {
+ const map = await loadMap();
+ const next: Annotation = { ...meta, updatedAt: Date.now() };
+ map[meta.uri] = next;
+ await saveMap(map);
+ return next;
+}
+
+export async function deleteAnnotation(uri: string): Promise {
+ const map = await loadMap();
+ if (!(uri in map)) return;
+ delete map[uri];
+ await saveMap(map);
+}
+
+/**
+ * Subscribe to cross-context annotation changes (other tabs, options page,
+ * etc.). Listener fires with the current full list.
+ */
+export function onAnnotationsChanged(listener: (next: Annotation[]) => void): () => void {
+ const handler = (
+ changes: { [key: string]: chrome.storage.StorageChange },
+ areaName: chrome.storage.AreaName
+ ) => {
+ if (areaName !== 'local') return;
+ if (!(STORAGE_KEY in changes)) return;
+ const next = (changes[STORAGE_KEY]?.newValue as AnnotationMap | undefined) ?? {};
+ listener(Object.values(next).sort((a, b) => b.updatedAt - a.updatedAt));
+ };
+ chrome.storage.onChanged.addListener(handler);
+ return () => chrome.storage.onChanged.removeListener(handler);
+}
diff --git a/src/lib/clay-uri.ts b/src/lib/clay-uri.ts
new file mode 100644
index 0000000..e37167a
--- /dev/null
+++ b/src/lib/clay-uri.ts
@@ -0,0 +1,184 @@
+/**
+ * Pure functions for parsing and constructing Clay component URIs.
+ *
+ * A Clay URI looks like:
+ * {host}/_components/{component-name}/instances/{instance-id}@published
+ * {host}/_pages/{page-id}@published
+ * {host}/_layouts/{layout-name}/instances/{instance-id}@published
+ */
+
+const COMPONENT_RE = /_components\/([^/.]+?)(?:[/.@]|$)/;
+const COMPONENT_INSTANCE_RE = /\/_components\/[^/]+?\/instances\/([^.@/]+)/;
+const PAGE_INSTANCE_RE = /\/_pages\/([^./@]+)/;
+const PUBLISHED_SUFFIX = '@published';
+/** Path segments Clay uses as the boundary between host and resource path. */
+const PATH_PREFIXES = ['/_pages/', '/_components/', '/_layouts/', '/_lists/', '/_users/'] as const;
+
+export type UriSuffix = '' | '.json' | '.html' | '/meta';
+
+export function isClayDocument(doc: Document = document): boolean {
+ const html = doc.documentElement;
+ return Boolean(html?.getAttribute('data-uri'));
+}
+
+export function getComponentName(uri: string | null | undefined): string | null {
+ if (!uri) return null;
+ const match = COMPONENT_RE.exec(uri);
+ return match?.[1] ?? null;
+}
+
+export function getInstance(uri: string | null | undefined): string | null {
+ if (!uri) return null;
+ const match = COMPONENT_INSTANCE_RE.exec(uri);
+ return match?.[1] ?? null;
+}
+
+export function getPageInstance(uri: string | null | undefined): string | null {
+ if (!uri) return null;
+ const match = PAGE_INSTANCE_RE.exec(uri);
+ return match?.[1] ?? null;
+}
+
+export function isPublished(uri: string | null | undefined): boolean {
+ return Boolean(uri?.includes(PUBLISHED_SUFFIX));
+}
+
+export function unpublishedUri(uri: string): string {
+ return uri.replace(PUBLISHED_SUFFIX, '');
+}
+
+export function toTitleCase(input: string | null | undefined): string {
+ if (!input) return '';
+ return input
+ .replace(/[-_]/g, ' ')
+ .toLowerCase()
+ .split(' ')
+ .filter(Boolean)
+ .map((word) => word[0]!.toUpperCase() + word.slice(1))
+ .join(' ');
+}
+
+export function getDisplayName(uri: string | null | undefined): string {
+ return toTitleCase(getComponentName(uri)) || 'Unknown';
+}
+
+/**
+ * Splits a Clay URI into its host portion and Clay path portion.
+ * Returns a path that always starts with one of the known Clay path prefixes
+ * (e.g. `/_components/...`). Falls back to the input if no prefix is found.
+ */
+export function splitHostAndPath(uri: string): { host: string; path: string } {
+ const cleaned = uri.replace(/^https?:\/\//, '');
+ for (const prefix of PATH_PREFIXES) {
+ const idx = cleaned.indexOf(prefix);
+ if (idx > -1) {
+ return { host: cleaned.slice(0, idx), path: cleaned.slice(idx) };
+ }
+ }
+ return { host: '', path: '/' + cleaned };
+}
+
+/**
+ * Normalizes a user-provided host string into a `protocol://hostname` form
+ * with no trailing slash. Returns an empty string when no host is set.
+ */
+export function normalizeHost(host: string | null | undefined): string {
+ if (!host) return '';
+ const trimmed = host.trim().replace(/\/+$/, '');
+ if (!trimmed) return '';
+ if (/^https?:\/\//i.test(trimmed)) return trimmed;
+ return `https://${trimmed}`;
+}
+
+/**
+ * Build the full URL for a Clay URI.
+ * - When `hostOverride` is provided, the URI's host is replaced with it.
+ * - When omitted (or empty), the URI's existing host is used with `https://`.
+ */
+export function buildUrl(uri: string, suffix: UriSuffix = '', hostOverride = ''): string {
+ const normalizedOverride = normalizeHost(hostOverride);
+ if (!normalizedOverride) {
+ const cleaned = uri.replace(/^https?:\/\//, '');
+ return `https://${cleaned}${suffix}`;
+ }
+ const { path } = splitHostAndPath(uri);
+ return `${normalizedOverride}${path}${suffix}`;
+}
+
+/**
+ * Builds the schema URL from a component URI.
+ * Clay component schemas live at:
+ * {host}/_components/{name}/schema
+ */
+export function buildSchemaUrl(uri: string, hostOverride = ''): string | null {
+ const name = getComponentName(uri);
+ if (!name) return null;
+ const normalizedOverride = normalizeHost(hostOverride);
+ if (normalizedOverride) {
+ return `${normalizedOverride}/_components/${name}/schema`;
+ }
+ const { host } = splitHostAndPath(uri);
+ if (!host) return null;
+ return `https://${host}/_components/${name}/schema`;
+}
+
+export function buildCurlCommand(
+ uri: string,
+ suffix: UriSuffix = '.json',
+ hostOverride = ''
+): string {
+ const url = buildUrl(uri, suffix, hostOverride);
+ return `curl -X GET "${url}" -H "Accept: application/json"`;
+}
+
+/**
+ * Open the Clay page editor for a given page URI. Standard Amphora Clay
+ * accepts `?edit=true` on the page URL to enter edit mode. When a component
+ * instance is provided, it's appended as a hash anchor for editor focus.
+ *
+ * Note: the editor only operates on the *unpublished* version of a page, so
+ * we always strip `@published` from the URI before building the URL.
+ */
+export function buildEditorUrl(
+ pageUri: string,
+ hostOverride = '',
+ componentInstance: string | null = null
+): string {
+ const base = buildUrl(unpublishedUri(pageUri), '.html', hostOverride);
+ const hash = componentInstance ? `#${componentInstance}` : '';
+ return `${base}?edit=true${hash}`;
+}
+
+/**
+ * Build a Clay-Slip deep link that, when opened, auto-selects the given
+ * component URI on page load.
+ */
+export function buildShareLink(currentUrl: string, uri: string): string {
+ try {
+ const url = new URL(currentUrl);
+ url.searchParams.set('clay-slip-select', uri);
+ return url.toString();
+ } catch {
+ const sep = currentUrl.includes('?') ? '&' : '?';
+ return `${currentUrl}${sep}clay-slip-select=${encodeURIComponent(uri)}`;
+ }
+}
+
+export function parseShareTarget(currentUrl: string): string | null {
+ try {
+ const url = new URL(currentUrl);
+ return url.searchParams.get('clay-slip-select');
+ } catch {
+ return null;
+ }
+}
+
+export function copyAsFetchSnippet(uri: string, hostOverride = ''): string {
+ const url = buildUrl(uri, '.json', hostOverride);
+ return `await fetch(${JSON.stringify(url)}, { credentials: 'include' }).then((r) => r.json());`;
+}
+
+export function copyAsCssSelector(uri: string): string {
+ const safe = uri.replace(/"/g, '\\"');
+ return `[data-uri="${safe}"]`;
+}
diff --git a/src/lib/clipboard.ts b/src/lib/clipboard.ts
new file mode 100644
index 0000000..e3c6630
--- /dev/null
+++ b/src/lib/clipboard.ts
@@ -0,0 +1,35 @@
+/**
+ * Modern clipboard API with a graceful fallback for legacy contexts.
+ * Replaces the deprecated `document.execCommand('copy')` flow.
+ */
+export async function copyToClipboard(text: string): Promise {
+ if (navigator.clipboard?.writeText) {
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch {
+ // fall through to legacy path
+ }
+ }
+ return legacyCopy(text);
+}
+
+function legacyCopy(text: string): boolean {
+ const textarea = document.createElement('textarea');
+ textarea.value = text;
+ textarea.setAttribute('readonly', '');
+ textarea.style.position = 'fixed';
+ textarea.style.top = '-9999px';
+ document.body.appendChild(textarea);
+ textarea.select();
+
+ let ok = false;
+ try {
+ ok = document.execCommand('copy');
+ } catch {
+ ok = false;
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ return ok;
+}
diff --git a/src/lib/exporter.ts b/src/lib/exporter.ts
new file mode 100644
index 0000000..312d937
--- /dev/null
+++ b/src/lib/exporter.ts
@@ -0,0 +1,71 @@
+import type { ClayComponentInfo, ClayPageInfo, ExportFormat } from './types';
+
+interface ManifestRow {
+ readonly name: string;
+ readonly displayName: string;
+ readonly uri: string;
+ readonly instance: string | null;
+ readonly depth: number;
+}
+
+export interface PageManifest {
+ readonly exportedAt: string;
+ readonly page: ClayPageInfo | null;
+ readonly url: string;
+ readonly title: string;
+ readonly components: ManifestRow[];
+}
+
+export function buildManifest(
+ page: ClayPageInfo | null,
+ components: ClayComponentInfo[]
+): PageManifest {
+ return {
+ exportedAt: new Date().toISOString(),
+ page,
+ url: location.href,
+ title: document.title,
+ components: components.map(({ name, displayName, uri, instance, depth }) => ({
+ name,
+ displayName,
+ uri,
+ instance,
+ depth,
+ })),
+ };
+}
+
+function escapeCsv(value: string | number | null): string {
+ if (value === null || value === undefined) return '';
+ const text = String(value);
+ if (/[",\n]/.test(text)) {
+ return `"${text.replace(/"/g, '""')}"`;
+ }
+ return text;
+}
+
+export function formatManifest(manifest: PageManifest, format: ExportFormat): string {
+ switch (format) {
+ case 'json':
+ return JSON.stringify(manifest, null, 2);
+ case 'csv': {
+ const header = ['name', 'displayName', 'uri', 'instance', 'depth'];
+ const rows = manifest.components.map((c) =>
+ [c.name, c.displayName, c.uri, c.instance ?? '', c.depth].map(escapeCsv).join(',')
+ );
+ return [header.join(','), ...rows].join('\n');
+ }
+ case 'markdown': {
+ const head = `# ${manifest.title}\n\n- **URL**: ${manifest.url}\n- **Page URI**: ${manifest.page?.pageUri ?? '—'}\n- **Layout URI**: ${manifest.page?.layoutUri ?? '—'}\n- **Status**: ${manifest.page?.isPublished ? 'Published' : 'Draft'}\n- **Exported**: ${manifest.exportedAt}\n\n## Components (${manifest.components.length})\n`;
+ const table = [
+ '| # | Component | Instance | Depth | URI |',
+ '| -- | -- | -- | -- | -- |',
+ ...manifest.components.map(
+ (c, i) =>
+ `| ${i + 1} | ${c.displayName} | ${c.instance ?? '—'} | ${c.depth} | \`${c.uri}\` |`
+ ),
+ ].join('\n');
+ return `${head}\n${table}\n`;
+ }
+ }
+}
diff --git a/src/lib/recents.ts b/src/lib/recents.ts
new file mode 100644
index 0000000..bc6c94d
--- /dev/null
+++ b/src/lib/recents.ts
@@ -0,0 +1,40 @@
+import type { RecentComponent } from './types';
+
+const STORAGE_KEY = 'recentComponents';
+const HARD_CAP = 100;
+
+export async function loadRecents(): Promise {
+ const stored = await chrome.storage.local.get(STORAGE_KEY);
+ const list = stored[STORAGE_KEY] as RecentComponent[] | undefined;
+ return list ?? [];
+}
+
+/**
+ * Push a new entry to the front of the recents list, dedup'd by URI, and
+ * clamp the list to `cap` (defaulting to a hard cap so storage stays small).
+ */
+export async function pushRecent(entry: RecentComponent, cap = 20): Promise {
+ const list = await loadRecents();
+ const filtered = list.filter((r) => r.uri !== entry.uri);
+ const next = [entry, ...filtered].slice(0, Math.min(cap, HARD_CAP));
+ await chrome.storage.local.set({ [STORAGE_KEY]: next });
+ return next;
+}
+
+export async function clearRecents(): Promise {
+ await chrome.storage.local.remove(STORAGE_KEY);
+}
+
+export function onRecentsChanged(listener: (next: RecentComponent[]) => void): () => void {
+ const handler = (
+ changes: { [key: string]: chrome.storage.StorageChange },
+ areaName: chrome.storage.AreaName
+ ) => {
+ if (areaName !== 'local') return;
+ if (!(STORAGE_KEY in changes)) return;
+ const next = (changes[STORAGE_KEY]?.newValue as RecentComponent[] | undefined) ?? [];
+ listener(next);
+ };
+ chrome.storage.onChanged.addListener(handler);
+ return () => chrome.storage.onChanged.removeListener(handler);
+}
diff --git a/src/lib/screenshot.ts b/src/lib/screenshot.ts
new file mode 100644
index 0000000..070780d
--- /dev/null
+++ b/src/lib/screenshot.ts
@@ -0,0 +1,70 @@
+import type { CaptureResponse, RuntimeMessage } from './types';
+
+/**
+ * Capture a PNG of the given element by:
+ * 1. Scrolling it into view + briefly hiding the panel host so it doesn't
+ * appear in the screenshot
+ * 2. Asking the service worker to call chrome.tabs.captureVisibleTab
+ * 3. Cropping the returned full-tab PNG to the element's viewport rect
+ * using a canvas, accounting for devicePixelRatio
+ * 4. Writing the result to the system clipboard as image/png
+ *
+ * Returns true on success.
+ */
+export async function captureElementToClipboard(
+ el: HTMLElement,
+ panelHost: HTMLElement | null
+): Promise {
+ el.scrollIntoView({ behavior: 'instant', block: 'center' });
+ await new Promise((r) => requestAnimationFrame(r));
+
+ const previousVisibility = panelHost?.style.visibility ?? '';
+ if (panelHost) panelHost.style.visibility = 'hidden';
+ await new Promise((r) => requestAnimationFrame(r));
+
+ try {
+ const response = (await chrome.runtime.sendMessage({
+ type: 'CAPTURE_TAB',
+ } satisfies RuntimeMessage)) as CaptureResponse;
+
+ if (!response?.ok || !response.dataUrl) {
+ throw new Error(response?.error ?? 'Capture failed');
+ }
+
+ const rect = el.getBoundingClientRect();
+ const dpr = window.devicePixelRatio || 1;
+
+ const img = await loadImage(response.dataUrl);
+ const sx = rect.left * dpr;
+ const sy = rect.top * dpr;
+ const sw = rect.width * dpr;
+ const sh = rect.height * dpr;
+
+ const canvas = document.createElement('canvas');
+ canvas.width = Math.max(1, Math.round(rect.width));
+ canvas.height = Math.max(1, Math.round(rect.height));
+ const ctx = canvas.getContext('2d');
+ if (!ctx) throw new Error('Canvas 2D context unavailable');
+ ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
+
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
+ if (!blob) throw new Error('PNG encoding failed');
+
+ if (!navigator.clipboard?.write) {
+ throw new Error('Clipboard image write not supported');
+ }
+ await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
+ return true;
+ } finally {
+ if (panelHost) panelHost.style.visibility = previousVisibility;
+ }
+}
+
+function loadImage(dataUrl: string): Promise {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => resolve(img);
+ img.onerror = () => reject(new Error('Image decode failed'));
+ img.src = dataUrl;
+ });
+}
diff --git a/src/lib/seo.ts b/src/lib/seo.ts
new file mode 100644
index 0000000..902c8bb
--- /dev/null
+++ b/src/lib/seo.ts
@@ -0,0 +1,158 @@
+/**
+ * Extracts SEO/social metadata from the host document and produces a list of
+ * lint warnings about common issues.
+ */
+
+export interface SeoMeta {
+ readonly title: string;
+ readonly description: string;
+ readonly canonical: string;
+ readonly robots: string;
+ readonly og: Record;
+ readonly twitter: Record;
+ readonly jsonLd: unknown[];
+ readonly h1Count: number;
+}
+
+export interface SeoIssue {
+ readonly id: string;
+ readonly severity: 'info' | 'warn' | 'error';
+ readonly message: string;
+}
+
+const TITLE_MAX = 60;
+const DESC_MIN = 50;
+const DESC_MAX = 160;
+
+function readMetaByName(doc: Document, names: string[]): Record {
+ const out: Record = {};
+ for (const m of doc.querySelectorAll('meta[property], meta[name]')) {
+ const key = (m.getAttribute('property') ?? m.getAttribute('name') ?? '').toLowerCase();
+ if (!key) continue;
+ for (const prefix of names) {
+ if (key === prefix || key.startsWith(`${prefix}:`)) {
+ out[key] = m.getAttribute('content') ?? '';
+ }
+ }
+ }
+ return out;
+}
+
+function readSimpleMeta(doc: Document, name: string): string {
+ const el = doc.querySelector(
+ `meta[name="${name}" i], meta[property="${name}" i]`
+ );
+ return el?.getAttribute('content') ?? '';
+}
+
+function readJsonLd(doc: Document): unknown[] {
+ const blocks: unknown[] = [];
+ for (const s of doc.querySelectorAll('script[type="application/ld+json"]')) {
+ try {
+ blocks.push(JSON.parse(s.textContent ?? 'null'));
+ } catch {
+ blocks.push({ __invalid: true, raw: s.textContent });
+ }
+ }
+ return blocks;
+}
+
+export function extractSeoMeta(doc: Document = document): SeoMeta {
+ return {
+ title: doc.title ?? '',
+ description: readSimpleMeta(doc, 'description'),
+ canonical: doc.querySelector('link[rel="canonical"]')?.href ?? '',
+ robots: readSimpleMeta(doc, 'robots'),
+ og: readMetaByName(doc, ['og']),
+ twitter: readMetaByName(doc, ['twitter']),
+ jsonLd: readJsonLd(doc),
+ h1Count: doc.querySelectorAll('h1').length,
+ };
+}
+
+export function lintSeo(meta: SeoMeta): SeoIssue[] {
+ const issues: SeoIssue[] = [];
+
+ if (!meta.title) {
+ issues.push({ id: 'title-missing', severity: 'error', message: 'Page is missing a .' });
+ } else if (meta.title.length > TITLE_MAX) {
+ issues.push({
+ id: 'title-long',
+ severity: 'warn',
+ message: `Title is ${meta.title.length} chars (recommended ≤ ${TITLE_MAX}).`,
+ });
+ }
+
+ if (!meta.description) {
+ issues.push({
+ id: 'desc-missing',
+ severity: 'warn',
+ message: 'No meta description set.',
+ });
+ } else if (meta.description.length < DESC_MIN) {
+ issues.push({
+ id: 'desc-short',
+ severity: 'info',
+ message: `Description is ${meta.description.length} chars (try ${DESC_MIN}–${DESC_MAX}).`,
+ });
+ } else if (meta.description.length > DESC_MAX) {
+ issues.push({
+ id: 'desc-long',
+ severity: 'warn',
+ message: `Description is ${meta.description.length} chars (try ${DESC_MIN}–${DESC_MAX}).`,
+ });
+ }
+
+ if (!meta.canonical) {
+ issues.push({
+ id: 'canonical-missing',
+ severity: 'info',
+ message: 'No canonical URL declared.',
+ });
+ }
+
+ if (!meta.og['og:image']) {
+ issues.push({
+ id: 'og-image-missing',
+ severity: 'warn',
+ message: 'Missing og:image — link previews will fall back to the platform default.',
+ });
+ }
+ if (!meta.og['og:title']) {
+ issues.push({ id: 'og-title-missing', severity: 'info', message: 'No og:title set.' });
+ }
+ if (!meta.og['og:description']) {
+ issues.push({
+ id: 'og-desc-missing',
+ severity: 'info',
+ message: 'No og:description set.',
+ });
+ }
+ if (!meta.twitter['twitter:card']) {
+ issues.push({
+ id: 'tw-card-missing',
+ severity: 'info',
+ message: 'No twitter:card type — Twitter will pick a default.',
+ });
+ }
+
+ if (meta.h1Count === 0) {
+ issues.push({ id: 'h1-missing', severity: 'warn', message: 'Page has no .' });
+ } else if (meta.h1Count > 1) {
+ issues.push({
+ id: 'h1-multiple',
+ severity: 'info',
+ message: `Page has ${meta.h1Count} tags (one is usually enough).`,
+ });
+ }
+
+ if (!meta.jsonLd.length) {
+ issues.push({
+ id: 'jsonld-missing',
+ severity: 'info',
+ message: 'No JSON-LD structured data found.',
+ });
+ }
+
+ return issues;
+}
diff --git a/src/lib/site-host.ts b/src/lib/site-host.ts
new file mode 100644
index 0000000..5f6a546
--- /dev/null
+++ b/src/lib/site-host.ts
@@ -0,0 +1,94 @@
+/**
+ * Pure helpers for the per-instance site-host mapping feature.
+ *
+ * A {@link SiteHostMapping} declares one brand/site and the bare hostnames
+ * it serves on per environment. These helpers answer three questions:
+ *
+ * 1. Which mapping/env owns the host I'm currently on?
+ * 2. What's the equivalent URL on a different env?
+ * 3. Which envs are available to switch to for this host?
+ *
+ * Matching is exact hostname comparison — no prefix stripping, no wildcards.
+ * That keeps behaviour predictable and avoids accidental false matches when
+ * a customer's mapping doesn't follow the "stg./qa." convention.
+ */
+
+import { SITE_ENV_ORDER, type SiteEnv, type SiteHostMapping } from './types';
+
+export interface MappingMatch {
+ readonly mapping: SiteHostMapping;
+ readonly env: SiteEnv;
+}
+
+/**
+ * Find the mapping + env that own a given hostname (case-insensitive).
+ * Returns `null` when the hostname isn't configured anywhere.
+ */
+export function findMappingForHost(
+ host: string,
+ mappings: readonly SiteHostMapping[]
+): MappingMatch | null {
+ const needle = host.toLowerCase();
+ for (const mapping of mappings) {
+ for (const env of SITE_ENV_ORDER) {
+ const candidate = mapping.hosts[env];
+ if (candidate && candidate.toLowerCase() === needle) {
+ return { mapping, env };
+ }
+ }
+ }
+ return null;
+}
+
+/**
+ * Rewrite the hostname of `url` to the matching site-mapping's hostname
+ * for `toEnv`. Returns `null` when:
+ * - `url` isn't a valid URL,
+ * - the URL's host isn't in any mapping, or
+ * - the matched mapping has no host configured for `toEnv`.
+ *
+ * The path, query, and hash are preserved verbatim.
+ */
+export function rewriteUrlToEnv(
+ url: string,
+ toEnv: SiteEnv,
+ mappings: readonly SiteHostMapping[]
+): string | null {
+ let parsed: URL;
+ try {
+ parsed = new URL(url);
+ } catch {
+ return null;
+ }
+ const match = findMappingForHost(parsed.hostname, mappings);
+ if (!match) return null;
+ const target = match.mapping.hosts[toEnv];
+ if (!target) return null;
+ parsed.hostname = target;
+ return parsed.toString();
+}
+
+/**
+ * Which envs have a host configured for the mapping that owns `host`?
+ * Returns an empty array when the host isn't in any mapping.
+ */
+export function availableEnvsFor(
+ host: string,
+ mappings: readonly SiteHostMapping[]
+): readonly SiteEnv[] {
+ const match = findMappingForHost(host, mappings);
+ if (!match) return [];
+ return SITE_ENV_ORDER.filter((env) => Boolean(match.mapping.hosts[env]));
+}
+
+/**
+ * Generate a small unique id for a new mapping row. Not cryptographic — just
+ * unique within a small array of user-managed rows.
+ */
+export function newMappingId(): string {
+ return `m-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e6).toString(36)}`;
+}
+
+export function emptyMapping(): SiteHostMapping {
+ return { id: newMappingId(), label: '', hosts: {} };
+}
diff --git a/src/lib/storage.ts b/src/lib/storage.ts
new file mode 100644
index 0000000..54ddb02
--- /dev/null
+++ b/src/lib/storage.ts
@@ -0,0 +1,29 @@
+import { DEFAULT_PREFERENCES, type UserPreferences } from './types';
+
+const PREFS_KEY = 'preferences';
+
+export async function loadPreferences(): Promise {
+ if (!chrome?.storage?.sync) return DEFAULT_PREFERENCES;
+ const stored = await chrome.storage.sync.get(PREFS_KEY);
+ return { ...DEFAULT_PREFERENCES, ...(stored[PREFS_KEY] ?? {}) };
+}
+
+export async function savePreferences(prefs: Partial): Promise {
+ if (!chrome?.storage?.sync) return;
+ const current = await loadPreferences();
+ const merged = { ...current, ...prefs };
+ await chrome.storage.sync.set({ [PREFS_KEY]: merged });
+}
+
+export function onPreferencesChanged(cb: (prefs: UserPreferences) => void): () => void {
+ const listener = (
+ changes: { [key: string]: chrome.storage.StorageChange },
+ area: chrome.storage.AreaName
+ ) => {
+ if (area === 'sync' && changes[PREFS_KEY]) {
+ cb({ ...DEFAULT_PREFERENCES, ...(changes[PREFS_KEY].newValue ?? {}) });
+ }
+ };
+ chrome.storage?.onChanged.addListener(listener);
+ return () => chrome.storage?.onChanged.removeListener(listener);
+}
diff --git a/src/lib/types.ts b/src/lib/types.ts
new file mode 100644
index 0000000..842bcf7
--- /dev/null
+++ b/src/lib/types.ts
@@ -0,0 +1,137 @@
+export type Environment = 'local' | 'dev' | 'staging' | 'prod';
+
+export interface EnvironmentConfig {
+ readonly id: Environment;
+ readonly label: string;
+ readonly host: string;
+}
+
+export interface ClayPageInfo {
+ readonly pageUri: string;
+ readonly layoutUri: string | null;
+ readonly isPublished: boolean;
+ readonly pageInstance: string | null;
+}
+
+export interface ClayComponentInfo {
+ readonly uri: string;
+ readonly name: string;
+ readonly displayName: string;
+ readonly instance: string | null;
+ readonly element: HTMLElement;
+ readonly depth: number;
+}
+
+export type EnvironmentHosts = Readonly>;
+
+export type PanelPosition =
+ | 'bottom-right'
+ | 'bottom-left'
+ | 'top-right'
+ | 'top-left'
+ | 'left-side'
+ | 'right-side';
+
+export interface UserPreferences {
+ readonly theme: 'auto' | 'light' | 'dark';
+ readonly panelPosition: PanelPosition;
+ readonly panelWidth: number;
+ readonly panelHeight: number;
+ readonly defaultEnvironment: Environment;
+ readonly environments: EnvironmentHosts;
+ readonly highlightOpacity: number;
+ readonly enableShortcuts: boolean;
+ readonly maxRecentComponents: number;
+ readonly siteHosts: readonly SiteHostMapping[];
+}
+
+/**
+ * Environments supported by the site-host mapping feature. Intentionally
+ * narrower than {@link Environment} (no `local`/`dev`) — site-host mappings
+ * are a per-brand lookup that only makes sense for stable shared envs.
+ */
+export type SiteEnv = 'prod' | 'staging' | 'qa';
+
+export const SITE_ENV_ORDER: readonly SiteEnv[] = ['prod', 'staging', 'qa'];
+
+export const SITE_ENV_LABELS: Readonly> = {
+ prod: 'Production',
+ staging: 'Staging',
+ qa: 'QA',
+};
+
+/**
+ * One brand/site, with the hostname it serves on under each supported env.
+ * Hostnames are bare (`www.thecut.com`, not a full URL). Missing entries
+ * mean "this site isn't deployed in that env" and the corresponding
+ * "View on…" pill will not appear.
+ */
+export interface SiteHostMapping {
+ readonly id: string;
+ readonly label: string;
+ readonly hosts: Partial>;
+}
+
+export const DEFAULT_ENVIRONMENT_HOSTS: EnvironmentHosts = {
+ local: 'http://localhost:3001',
+ dev: '',
+ staging: '',
+ prod: '',
+};
+
+export const DEFAULT_PREFERENCES: UserPreferences = {
+ theme: 'auto',
+ panelPosition: 'bottom-right',
+ panelWidth: 380,
+ panelHeight: 540,
+ defaultEnvironment: 'prod',
+ environments: DEFAULT_ENVIRONMENT_HOSTS,
+ highlightOpacity: 0.85,
+ enableShortcuts: true,
+ maxRecentComponents: 20,
+ siteHosts: [],
+};
+
+export const ENVIRONMENT_ORDER: readonly Environment[] = ['local', 'dev', 'staging', 'prod'];
+
+export const ENVIRONMENT_LABELS: Readonly> = {
+ local: 'Local',
+ dev: 'Dev',
+ staging: 'Staging',
+ prod: 'Production',
+};
+
+/** Minimal serializable info we keep about a component for recents/annotations. */
+export interface RecentComponent {
+ readonly uri: string;
+ readonly displayName: string;
+ readonly instance: string | null;
+ readonly pageUrl: string;
+ readonly pageTitle: string;
+ readonly visitedAt: number;
+}
+
+export interface Annotation {
+ readonly uri: string;
+ readonly note: string;
+ readonly displayName: string;
+ readonly pageUrl: string;
+ readonly pageTitle: string;
+ readonly updatedAt: number;
+}
+
+export type ExportFormat = 'json' | 'csv' | 'markdown';
+
+export type RuntimeMessage =
+ | { type: 'OPEN_TAB'; url: string }
+ | { type: 'OPEN_OPTIONS' }
+ | { type: 'UPDATE_BADGE'; count: number; tabId?: number }
+ | { type: 'CLAY_DETECTED' }
+ | { type: 'PANEL_TOGGLE' }
+ | { type: 'CAPTURE_TAB' };
+
+export interface CaptureResponse {
+ readonly ok: boolean;
+ readonly dataUrl?: string;
+ readonly error?: string;
+}
diff --git a/src/manifest.ts b/src/manifest.ts
new file mode 100644
index 0000000..6796617
--- /dev/null
+++ b/src/manifest.ts
@@ -0,0 +1,50 @@
+import { defineManifest } from '@crxjs/vite-plugin';
+import pkg from '../package.json' with { type: 'json' };
+
+export default defineManifest({
+ manifest_version: 3,
+ name: 'Clay Slip',
+ short_name: 'Slip',
+ version: pkg.version,
+ description: pkg.description,
+ minimum_chrome_version: '116',
+
+ icons: {
+ 16: 'icons/icon-16.png',
+ 32: 'icons/icon-32.png',
+ 48: 'icons/icon-48.png',
+ 128: 'icons/icon-128.png',
+ },
+
+ action: {
+ default_title: 'Toggle Clay Slip',
+ default_popup: 'src/popup/index.html',
+ default_icon: {
+ 16: 'icons/icon-16.png',
+ 32: 'icons/icon-32.png',
+ 48: 'icons/icon-48.png',
+ 128: 'icons/icon-128.png',
+ },
+ },
+
+ options_ui: {
+ page: 'src/options/index.html',
+ open_in_tab: true,
+ },
+
+ background: {
+ service_worker: 'src/background/service-worker.ts',
+ type: 'module',
+ },
+
+ content_scripts: [
+ {
+ matches: [''],
+ js: ['src/content/index.ts'],
+ run_at: 'document_idle',
+ },
+ ],
+
+ permissions: ['activeTab', 'scripting', 'storage', 'clipboardWrite'],
+ host_permissions: [''],
+});
diff --git a/src/options/Options.tsx b/src/options/Options.tsx
new file mode 100644
index 0000000..b636d9e
--- /dev/null
+++ b/src/options/Options.tsx
@@ -0,0 +1,356 @@
+import { useEffect, useRef, useState } from 'react';
+import { loadPreferences, savePreferences } from '@/lib/storage';
+import { clearRecents } from '@/lib/recents';
+import { emptyMapping } from '@/lib/site-host';
+import {
+ DEFAULT_PREFERENCES,
+ ENVIRONMENT_LABELS,
+ ENVIRONMENT_ORDER,
+ SITE_ENV_LABELS,
+ SITE_ENV_ORDER,
+ type Environment,
+ type EnvironmentHosts,
+ type PanelPosition,
+ type SiteEnv,
+ type SiteHostMapping,
+ type UserPreferences,
+} from '@/lib/types';
+
+const PANEL_POSITIONS: Array<{ value: PanelPosition; label: string }> = [
+ { value: 'bottom-right', label: 'Bottom right (corner)' },
+ { value: 'bottom-left', label: 'Bottom left (corner)' },
+ { value: 'top-right', label: 'Top right (corner)' },
+ { value: 'top-left', label: 'Top left (corner)' },
+ { value: 'left-side', label: 'Left side (full height)' },
+ { value: 'right-side', label: 'Right side (full height)' },
+];
+
+export function Options() {
+ const [prefs, setPrefs] = useState(DEFAULT_PREFERENCES);
+ const [saved, setSaved] = useState(false);
+ const savedTimer = useRef | null>(null);
+
+ useEffect(() => {
+ loadPreferences().then(setPrefs);
+ return () => {
+ if (savedTimer.current) clearTimeout(savedTimer.current);
+ };
+ }, []);
+
+ const flashSaved = () => {
+ setSaved(true);
+ if (savedTimer.current) clearTimeout(savedTimer.current);
+ savedTimer.current = setTimeout(() => setSaved(false), 1200);
+ };
+
+ const update = (key: K, value: UserPreferences[K]) => {
+ const next = { ...prefs, [key]: value };
+ setPrefs(next);
+ void savePreferences({ [key]: value });
+ flashSaved();
+ };
+
+ const updateEnvHost = (env: Environment, host: string) => {
+ const nextHosts: EnvironmentHosts = { ...prefs.environments, [env]: host };
+ update('environments', nextHosts);
+ };
+
+ const updateSiteHosts = (next: readonly SiteHostMapping[]) => update('siteHosts', next);
+
+ const addSiteMapping = () => updateSiteHosts([...prefs.siteHosts, emptyMapping()]);
+
+ const removeSiteMapping = (id: string) =>
+ updateSiteHosts(prefs.siteHosts.filter((m) => m.id !== id));
+
+ const editSiteLabel = (id: string, label: string) =>
+ updateSiteHosts(prefs.siteHosts.map((m) => (m.id === id ? { ...m, label } : m)));
+
+ const editSiteHost = (id: string, env: SiteEnv, host: string) =>
+ updateSiteHosts(
+ prefs.siteHosts.map((m) => {
+ if (m.id !== id) return m;
+ const trimmed = host.trim();
+ const nextHosts = { ...m.hosts };
+ if (trimmed) {
+ nextHosts[env] = trimmed;
+ } else {
+ delete nextHosts[env];
+ }
+ return { ...m, hosts: nextHosts };
+ })
+ );
+
+ return (
+
+
+ S
+ Clay Slip Settings
+ {saved && Saved }
+
+
+
+
+
+ Environments
+
+ Configure each environment’s host (e.g. https://prod.example.com).
+ Leave blank to keep using the page’s existing host. The env switcher pill in the
+ panel cycles through these, and the Diff tab can compare any two configured envs.
+
+
+
+
+ Active environment
+
+ Which configured host to route links + fetches through.
+
+
+
+ update('defaultEnvironment', e.target.value as UserPreferences['defaultEnvironment'])
+ }
+ >
+ {ENVIRONMENT_ORDER.map((env) => (
+
+ {ENVIRONMENT_LABELS[env]}
+
+ ))}
+
+
+
+ {ENVIRONMENT_ORDER.map((env) => (
+
+
+ {ENVIRONMENT_LABELS[env]} host
+
+ Used when env: {env} is selected.
+
+
+ updateEnvHost(env, e.target.value)}
+ />
+
+ ))}
+
+
+
+ Site host mappings
+
+ Per-brand hostnames for each environment. When configured, the panel shows a{' '}
+ View on… pill row on every Clay page so you can jump to the equivalent
+ URL on a different env in one click. Enter bare hostnames (e.g.{' '}
+ www.thecut.com, not https://www.thecut.com). Leave a cell blank
+ if the brand isn’t deployed in that env.
+
+
+ {prefs.siteHosts.length === 0 && (
+
+ No mappings configured yet. Add one to enable cross-env navigation.
+
+ )}
+
+ {prefs.siteHosts.length > 0 && (
+
+ )}
+
+
+
+ Add mapping
+ Create a new brand row.
+
+
+ + Add
+
+
+
+
+
+ Workflow
+
+
+
+ Keyboard shortcuts
+
+ Press ? while inspecting to see the full list.
+
+
+ update('enableShortcuts', e.target.checked)}
+ />
+
+
+
+
+ Recent components history
+
+ How many of your most recently inspected components Slip remembers across sessions.
+
+
+ update('maxRecentComponents', Number(e.target.value))}
+ />
+
+
+
+
+ Clear recents
+ Wipe the recent components list.
+
+
{
+ await clearRecents();
+ flashSaved();
+ }}
+ >
+ Clear
+
+
+
+
+
+
+ Clay Slip · open source · MIT ·{' '}
+
+ GitHub
+
+
+
+
+ );
+}
diff --git a/src/options/index.html b/src/options/index.html
new file mode 100644
index 0000000..bb26204
--- /dev/null
+++ b/src/options/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Clay Slip Settings
+
+
+
+
+
+
diff --git a/src/options/main.tsx b/src/options/main.tsx
new file mode 100644
index 0000000..5f241d9
--- /dev/null
+++ b/src/options/main.tsx
@@ -0,0 +1,6 @@
+import { createRoot } from 'react-dom/client';
+import { Options } from './Options';
+import './options.css';
+
+const root = document.getElementById('root');
+if (root) createRoot(root).render( );
diff --git a/src/options/options.css b/src/options/options.css
new file mode 100644
index 0000000..8db476f
--- /dev/null
+++ b/src/options/options.css
@@ -0,0 +1,262 @@
+:root {
+ color-scheme: light dark;
+ --bg: #ffffff;
+ --bg-elevated: #f6f7f9;
+ --text: #0f172a;
+ --text-muted: #475569;
+ --border: #e5e7eb;
+ --accent: #e22c2c;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #0f1115;
+ --bg-elevated: #161922;
+ --text: #e6e8ee;
+ --text-muted: #9aa3b2;
+ --border: #252a36;
+ --accent: #ff5c5c;
+ }
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ background: var(--bg);
+ color: var(--text);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 14px;
+}
+
+.options {
+ max-width: 640px;
+ margin: 0 auto;
+ padding: 32px 24px 64px;
+}
+
+.options-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 32px;
+}
+
+.options-header h1 {
+ font-size: 18px;
+ margin: 0;
+ flex: 1;
+}
+
+.options-logo {
+ width: 28px;
+ height: 28px;
+ border-radius: 6px;
+ background: var(--accent);
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+}
+
+.options-saved {
+ font-size: 12px;
+ color: var(--accent);
+ font-weight: 500;
+}
+
+.options-section {
+ background: var(--bg-elevated);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 8px 16px;
+ margin-bottom: 18px;
+}
+
+.options-section h2 {
+ font-size: 11px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--text-muted);
+ margin: 14px 0 8px;
+}
+
+.options-row {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 0;
+ border-top: 1px solid var(--border);
+}
+
+.options-row:first-of-type {
+ border-top: none;
+}
+
+.options-label {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.options-help {
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.options-row select,
+.options-row input[type='range'],
+.options-row input[type='text'],
+.options-row input[type='number'] {
+ background: var(--bg);
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 6px 8px;
+ font-size: 13px;
+ min-width: 220px;
+ font-family: inherit;
+}
+
+.options-row input[type='number'] {
+ min-width: 80px;
+}
+
+.options-secondary {
+ background: var(--bg);
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 6px 12px;
+ font-size: 13px;
+ cursor: pointer;
+ font-family: inherit;
+}
+
+.options-secondary:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.options-row input[type='text']:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.options-section-help {
+ margin: 12px 0 4px;
+ padding: 0;
+ font-size: 12px;
+ color: var(--text-muted);
+ line-height: 1.5;
+}
+
+.options-section-help code {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 3px;
+ padding: 1px 4px;
+ font-size: 11px;
+}
+
+.options-row input[type='checkbox'] {
+ width: 18px;
+ height: 18px;
+ accent-color: var(--accent);
+}
+
+.options-footer {
+ margin-top: 24px;
+ font-size: 12px;
+ color: var(--text-muted);
+ text-align: center;
+}
+
+.options-footer a {
+ color: var(--accent);
+ text-decoration: none;
+}
+
+.options-footer a:hover {
+ text-decoration: underline;
+}
+
+kbd {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 3px;
+ padding: 1px 4px;
+ font-size: 11px;
+ font-family: ui-monospace, monospace;
+}
+
+.options-empty {
+ margin: 12px 0;
+ padding: 12px;
+ background: var(--bg);
+ border: 1px dashed var(--border);
+ border-radius: 6px;
+ font-size: 12px;
+ color: var(--text-muted);
+ text-align: center;
+}
+
+.options-mappings {
+ margin: 12px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.options-mappings-header,
+.options-mappings-row {
+ display: grid;
+ grid-template-columns: 1.2fr 1.4fr 1.4fr 1.4fr 28px;
+ gap: 6px;
+ align-items: center;
+}
+
+.options-mappings-header {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ padding: 0 2px;
+}
+
+.options-mappings-row input[type='text'] {
+ background: var(--bg);
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 6px 8px;
+ font-size: 13px;
+ min-width: 0;
+ width: 100%;
+ font-family: inherit;
+}
+
+.options-mappings-row input[type='text']:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.options-remove {
+ background: transparent;
+ color: var(--text-muted);
+ border: 1px solid transparent;
+ border-radius: 6px;
+ padding: 4px 6px;
+ font-size: 14px;
+ cursor: pointer;
+ font-family: inherit;
+ line-height: 1;
+}
+
+.options-remove:hover {
+ color: var(--accent);
+ border-color: var(--border);
+}
diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx
new file mode 100644
index 0000000..2d154f1
--- /dev/null
+++ b/src/popup/Popup.tsx
@@ -0,0 +1,20 @@
+export function Popup() {
+ return (
+
+
S
+
No Clay components found
+
+ This page does not appear to be powered by Clay. Open a Clay page and click the toolbar icon
+ to inspect components.
+
+
+ Learn about Clay →
+
+
+ );
+}
diff --git a/src/popup/index.html b/src/popup/index.html
new file mode 100644
index 0000000..0181408
--- /dev/null
+++ b/src/popup/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Clay Slip
+
+
+
+
+
+
diff --git a/src/popup/main.tsx b/src/popup/main.tsx
new file mode 100644
index 0000000..0e9c27d
--- /dev/null
+++ b/src/popup/main.tsx
@@ -0,0 +1,6 @@
+import { createRoot } from 'react-dom/client';
+import { Popup } from './Popup';
+import './popup.css';
+
+const root = document.getElementById('root');
+if (root) createRoot(root).render( );
diff --git a/src/popup/popup.css b/src/popup/popup.css
new file mode 100644
index 0000000..a02b956
--- /dev/null
+++ b/src/popup/popup.css
@@ -0,0 +1,63 @@
+:root {
+ color-scheme: light dark;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: #ffffff;
+ color: #0f172a;
+ width: 280px;
+}
+
+@media (prefers-color-scheme: dark) {
+ body {
+ background: #0f1115;
+ color: #e6e8ee;
+ }
+}
+
+.popup {
+ padding: 18px 20px;
+ text-align: center;
+}
+
+.popup-logo {
+ width: 36px;
+ height: 36px;
+ margin: 0 auto 12px;
+ border-radius: 8px;
+ background: #e22c2c;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ font-size: 18px;
+}
+
+.popup-title {
+ font-size: 14px;
+ margin: 0 0 6px;
+ font-weight: 600;
+}
+
+.popup-body {
+ margin: 0 0 12px;
+ font-size: 12px;
+ line-height: 1.5;
+ opacity: 0.75;
+}
+
+.popup-link {
+ display: inline-block;
+ font-size: 12px;
+ color: #e22c2c;
+ text-decoration: none;
+ font-weight: 500;
+}
+
+.popup-link:hover {
+ text-decoration: underline;
+}
diff --git a/tests/lib/annotations.test.ts b/tests/lib/annotations.test.ts
new file mode 100644
index 0000000..05b17fc
--- /dev/null
+++ b/tests/lib/annotations.test.ts
@@ -0,0 +1,88 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import {
+ deleteAnnotation,
+ getAnnotation,
+ getAnnotationUris,
+ listAnnotations,
+ upsertAnnotation,
+} from '@/lib/annotations';
+
+interface MockLocal {
+ data: Record;
+ get: (key: string) => Promise>;
+ set: (entries: Record) => Promise;
+ remove: (key: string) => Promise;
+}
+
+interface MockChrome {
+ storage: {
+ local: MockLocal;
+ onChanged: {
+ addListener: ReturnType;
+ removeListener: ReturnType;
+ };
+ };
+}
+
+beforeEach(() => {
+ const local: MockLocal = {
+ data: {},
+ get: vi.fn(async (key: string) => ({ [key]: local.data[key] })),
+ set: vi.fn(async (entries: Record) => {
+ Object.assign(local.data, entries);
+ }),
+ remove: vi.fn(async (key: string) => {
+ delete local.data[key];
+ }),
+ };
+ const mock: MockChrome = {
+ storage: {
+ local,
+ onChanged: { addListener: vi.fn(), removeListener: vi.fn() },
+ },
+ };
+ (globalThis as { chrome?: unknown }).chrome = mock;
+});
+
+const meta = {
+ uri: 'site/_components/byline/instances/x',
+ displayName: 'Byline',
+ pageUrl: 'https://example.com/page',
+ pageTitle: 'Test',
+ note: 'Hello',
+};
+
+describe('annotations', () => {
+ it('round-trips an annotation', async () => {
+ const saved = await upsertAnnotation(meta);
+ expect(saved.note).toBe('Hello');
+ expect(saved.updatedAt).toBeGreaterThan(0);
+
+ const fetched = await getAnnotation(meta.uri);
+ expect(fetched?.note).toBe('Hello');
+ });
+
+ it('exposes the set of annotated URIs', async () => {
+ await upsertAnnotation(meta);
+ await upsertAnnotation({ ...meta, uri: 'other', note: 'Other note' });
+ const uris = await getAnnotationUris();
+ expect(uris.has(meta.uri)).toBe(true);
+ expect(uris.has('other')).toBe(true);
+ expect(uris.size).toBe(2);
+ });
+
+ it('lists annotations newest-first', async () => {
+ await upsertAnnotation(meta);
+ await new Promise((r) => setTimeout(r, 5));
+ await upsertAnnotation({ ...meta, uri: 'newer', note: 'newer one' });
+
+ const list = await listAnnotations();
+ expect(list[0]!.uri).toBe('newer');
+ });
+
+ it('deletes annotations', async () => {
+ await upsertAnnotation(meta);
+ await deleteAnnotation(meta.uri);
+ expect(await getAnnotation(meta.uri)).toBeNull();
+ });
+});
diff --git a/tests/lib/clay-uri.test.ts b/tests/lib/clay-uri.test.ts
new file mode 100644
index 0000000..675c4b8
--- /dev/null
+++ b/tests/lib/clay-uri.test.ts
@@ -0,0 +1,318 @@
+import { describe, expect, it } from 'vitest';
+import {
+ buildCurlCommand,
+ buildEditorUrl,
+ buildSchemaUrl,
+ buildShareLink,
+ buildUrl,
+ copyAsCssSelector,
+ copyAsFetchSnippet,
+ getComponentName,
+ getDisplayName,
+ getInstance,
+ getPageInstance,
+ isClayDocument,
+ isPublished,
+ normalizeHost,
+ parseShareTarget,
+ splitHostAndPath,
+ toTitleCase,
+ unpublishedUri,
+} from '@/lib/clay-uri';
+
+describe('getComponentName', () => {
+ it('extracts the component name from a standard component URI', () => {
+ const uri = 'example.com/_components/article-header/instances/abc123';
+ expect(getComponentName(uri)).toBe('article-header');
+ });
+
+ it('extracts the component name when followed by extension', () => {
+ expect(getComponentName('site/_components/byline.json')).toBe('byline');
+ });
+
+ it('returns null for non-component URIs', () => {
+ expect(getComponentName('example.com/_pages/abc')).toBeNull();
+ });
+
+ it('handles null and undefined inputs', () => {
+ expect(getComponentName(null)).toBeNull();
+ expect(getComponentName(undefined)).toBeNull();
+ expect(getComponentName('')).toBeNull();
+ });
+});
+
+describe('getInstance', () => {
+ it('extracts the instance ID', () => {
+ expect(getInstance('site/_components/foo/instances/xyz789')).toBe('xyz789');
+ });
+
+ it('handles published variants', () => {
+ expect(getInstance('site/_components/foo/instances/xyz789@published')).toBe('xyz789');
+ });
+
+ it('returns null when there is no instance segment', () => {
+ expect(getInstance('site/_components/foo')).toBeNull();
+ });
+});
+
+describe('getPageInstance', () => {
+ it('returns the page instance', () => {
+ expect(getPageInstance('site/_pages/myPage')).toBe('myPage');
+ });
+
+ it('returns null for component URIs', () => {
+ expect(getPageInstance('site/_components/foo/instances/xyz')).toBeNull();
+ });
+
+ it('does not throw on null or undefined', () => {
+ expect(() => getPageInstance(null)).not.toThrow();
+ expect(getPageInstance(null)).toBeNull();
+ });
+});
+
+describe('isPublished', () => {
+ it('detects @published suffix', () => {
+ expect(isPublished('site/_pages/abc@published')).toBe(true);
+ expect(isPublished('site/_pages/abc')).toBe(false);
+ });
+});
+
+describe('unpublishedUri', () => {
+ it('strips the @published suffix', () => {
+ expect(unpublishedUri('site/_pages/abc@published')).toBe('site/_pages/abc');
+ });
+
+ it('returns the same string when not published', () => {
+ expect(unpublishedUri('site/_pages/abc')).toBe('site/_pages/abc');
+ });
+});
+
+describe('toTitleCase', () => {
+ it('converts kebab-case to title case', () => {
+ expect(toTitleCase('article-header')).toBe('Article Header');
+ });
+
+ it('converts snake_case to title case', () => {
+ expect(toTitleCase('breaking_news')).toBe('Breaking News');
+ });
+
+ it('handles single words', () => {
+ expect(toTitleCase('byline')).toBe('Byline');
+ });
+
+ it('returns empty string for null/undefined', () => {
+ expect(toTitleCase(null)).toBe('');
+ expect(toTitleCase(undefined)).toBe('');
+ expect(toTitleCase('')).toBe('');
+ });
+});
+
+describe('getDisplayName', () => {
+ it('returns titlecase from a component URI', () => {
+ expect(getDisplayName('site/_components/article-header/instances/x')).toBe('Article Header');
+ });
+
+ it('returns "Unknown" when no component name found', () => {
+ expect(getDisplayName('site/_pages/abc')).toBe('Unknown');
+ });
+});
+
+describe('buildUrl', () => {
+ it('prepends https:// and optional suffix', () => {
+ expect(buildUrl('site/_components/foo/instances/x')).toBe(
+ 'https://site/_components/foo/instances/x'
+ );
+ expect(buildUrl('site/_components/foo/instances/x', '.json')).toBe(
+ 'https://site/_components/foo/instances/x.json'
+ );
+ });
+
+ it('strips an existing protocol', () => {
+ expect(buildUrl('http://site/_components/foo')).toBe('https://site/_components/foo');
+ });
+
+ it('appends meta path correctly', () => {
+ expect(buildUrl('site/_pages/abc', '/meta')).toBe('https://site/_pages/abc/meta');
+ });
+
+ it('rewrites the host when override is provided (bare hostname)', () => {
+ expect(
+ buildUrl('prod.example.com/_components/foo/instances/x', '.json', 'staging.example.com')
+ ).toBe('https://staging.example.com/_components/foo/instances/x.json');
+ });
+
+ it('rewrites the host when override includes protocol', () => {
+ expect(buildUrl('prod.example.com/_pages/x', '', 'http://localhost:3001')).toBe(
+ 'http://localhost:3001/_pages/x'
+ );
+ });
+
+ it('strips trailing slash from host override', () => {
+ expect(buildUrl('prod.example.com/_pages/x', '', 'https://staging.example.com/')).toBe(
+ 'https://staging.example.com/_pages/x'
+ );
+ });
+
+ it('falls back to original host when override is empty', () => {
+ expect(buildUrl('prod.example.com/_pages/x', '', '')).toBe('https://prod.example.com/_pages/x');
+ });
+});
+
+describe('buildSchemaUrl', () => {
+ it('builds the schema URL for a component', () => {
+ expect(buildSchemaUrl('site/_components/byline/instances/x')).toBe(
+ 'https://site/_components/byline/schema'
+ );
+ });
+
+ it('returns null for non-component URIs', () => {
+ expect(buildSchemaUrl('site/_pages/abc')).toBeNull();
+ });
+
+ it('respects host override', () => {
+ expect(buildSchemaUrl('prod.example.com/_components/byline/x', 'staging.example.com')).toBe(
+ 'https://staging.example.com/_components/byline/schema'
+ );
+ });
+});
+
+describe('buildCurlCommand', () => {
+ it('emits a curl command targeting JSON', () => {
+ const cmd = buildCurlCommand('site/_components/foo/instances/x');
+ expect(cmd).toContain('curl -X GET');
+ expect(cmd).toContain('https://site/_components/foo/instances/x.json');
+ expect(cmd).toContain('Accept: application/json');
+ });
+
+ it('uses host override in URL', () => {
+ const cmd = buildCurlCommand(
+ 'prod.example.com/_components/foo/instances/x',
+ '.json',
+ 'staging.example.com'
+ );
+ expect(cmd).toContain('https://staging.example.com/_components/foo/instances/x.json');
+ });
+});
+
+describe('splitHostAndPath', () => {
+ it('separates host and path on a clean URI', () => {
+ expect(splitHostAndPath('site.example.com/_components/byline/instances/x')).toEqual({
+ host: 'site.example.com',
+ path: '/_components/byline/instances/x',
+ });
+ });
+
+ it('strips the protocol if present', () => {
+ expect(splitHostAndPath('https://site.example.com/_pages/foo')).toEqual({
+ host: 'site.example.com',
+ path: '/_pages/foo',
+ });
+ });
+
+ it('handles all known Clay path prefixes', () => {
+ expect(splitHostAndPath('site/_layouts/main/instances/x').path).toBe(
+ '/_layouts/main/instances/x'
+ );
+ expect(splitHostAndPath('site/_lists/x').path).toBe('/_lists/x');
+ expect(splitHostAndPath('site/_users/x').path).toBe('/_users/x');
+ });
+
+ it('returns empty host when there is no Clay prefix', () => {
+ expect(splitHostAndPath('not-a-clay-uri/whatever').host).toBe('');
+ });
+});
+
+describe('normalizeHost', () => {
+ it('returns empty string for empty input', () => {
+ expect(normalizeHost('')).toBe('');
+ expect(normalizeHost(null)).toBe('');
+ expect(normalizeHost(undefined)).toBe('');
+ });
+
+ it('prepends https:// to bare hostnames', () => {
+ expect(normalizeHost('staging.example.com')).toBe('https://staging.example.com');
+ });
+
+ it('preserves http:// for local dev', () => {
+ expect(normalizeHost('http://localhost:3001')).toBe('http://localhost:3001');
+ });
+
+ it('strips trailing slashes', () => {
+ expect(normalizeHost('https://example.com/')).toBe('https://example.com');
+ expect(normalizeHost('https://example.com///')).toBe('https://example.com');
+ });
+});
+
+describe('buildEditorUrl', () => {
+ it('appends ?edit=true to the page URL', () => {
+ expect(buildEditorUrl('site/_pages/abc')).toBe('https://site/_pages/abc.html?edit=true');
+ });
+
+ it('respects host override', () => {
+ expect(buildEditorUrl('prod.example.com/_pages/abc', 'staging.example.com')).toBe(
+ 'https://staging.example.com/_pages/abc.html?edit=true'
+ );
+ });
+
+ it('appends instance hash when given', () => {
+ expect(buildEditorUrl('site/_pages/abc', '', 'inst-123')).toBe(
+ 'https://site/_pages/abc.html?edit=true#inst-123'
+ );
+ });
+
+ it('strips @published from the page URI (editor only operates on the unpublished version)', () => {
+ expect(buildEditorUrl('site/_pages/abc@published')).toBe(
+ 'https://site/_pages/abc.html?edit=true'
+ );
+ expect(buildEditorUrl('site/_pages/abc@published', 'staging.example.com', 'inst-1')).toBe(
+ 'https://staging.example.com/_pages/abc.html?edit=true#inst-1'
+ );
+ });
+});
+
+describe('buildShareLink + parseShareTarget', () => {
+ it('appends the clay-slip-select param to a URL', () => {
+ const link = buildShareLink('https://example.com/page', 'site/_components/byline/instances/x');
+ expect(link).toContain('clay-slip-select=site');
+ expect(parseShareTarget(link)).toBe('site/_components/byline/instances/x');
+ });
+
+ it('replaces an existing clay-slip-select param rather than duplicating', () => {
+ const link = buildShareLink('https://example.com/page?clay-slip-select=old', 'new');
+ const url = new URL(link);
+ expect(url.searchParams.getAll('clay-slip-select')).toEqual(['new']);
+ });
+
+ it('returns null when no param is present', () => {
+ expect(parseShareTarget('https://example.com')).toBeNull();
+ });
+});
+
+describe('copy helpers', () => {
+ it('builds a fetch snippet against the env host', () => {
+ const snip = copyAsFetchSnippet(
+ 'prod.example.com/_components/byline/instances/x',
+ 'staging.example.com'
+ );
+ expect(snip).toContain('await fetch(');
+ expect(snip).toContain('staging.example.com');
+ expect(snip).toContain('.json');
+ });
+
+ it('builds a CSS selector with quote escaping', () => {
+ expect(copyAsCssSelector('site/_components/x"y')).toBe('[data-uri="site/_components/x\\"y"]');
+ });
+});
+
+describe('isClayDocument', () => {
+ it('returns true when html has data-uri', () => {
+ document.documentElement.setAttribute('data-uri', 'site/_pages/x');
+ expect(isClayDocument()).toBe(true);
+ document.documentElement.removeAttribute('data-uri');
+ });
+
+ it('returns false when html lacks data-uri', () => {
+ document.documentElement.removeAttribute('data-uri');
+ expect(isClayDocument()).toBe(false);
+ });
+});
diff --git a/tests/lib/clipboard.test.ts b/tests/lib/clipboard.test.ts
new file mode 100644
index 0000000..c5e33bc
--- /dev/null
+++ b/tests/lib/clipboard.test.ts
@@ -0,0 +1,62 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { copyToClipboard } from '@/lib/clipboard';
+
+const originalClipboard = navigator.clipboard;
+
+afterEach(() => {
+ Object.defineProperty(navigator, 'clipboard', {
+ value: originalClipboard,
+ configurable: true,
+ writable: true,
+ });
+});
+
+describe('copyToClipboard', () => {
+ it('uses navigator.clipboard when available', async () => {
+ const writeText = vi.fn().mockResolvedValue(undefined);
+ Object.defineProperty(navigator, 'clipboard', {
+ value: { writeText },
+ configurable: true,
+ writable: true,
+ });
+
+ const ok = await copyToClipboard('hello');
+ expect(ok).toBe(true);
+ expect(writeText).toHaveBeenCalledWith('hello');
+ });
+
+ it('falls back to legacy textarea when clipboard API is missing', async () => {
+ Object.defineProperty(navigator, 'clipboard', {
+ value: undefined,
+ configurable: true,
+ writable: true,
+ });
+
+ const exec = vi.fn().mockReturnValue(true);
+ Object.defineProperty(document, 'execCommand', {
+ value: exec,
+ configurable: true,
+ writable: true,
+ });
+ const ok = await copyToClipboard('legacy');
+ expect(ok).toBe(true);
+ expect(exec).toHaveBeenCalledWith('copy');
+ });
+
+ it('returns false when both paths fail', async () => {
+ const writeText = vi.fn().mockRejectedValue(new Error('blocked'));
+ Object.defineProperty(navigator, 'clipboard', {
+ value: { writeText },
+ configurable: true,
+ writable: true,
+ });
+ Object.defineProperty(document, 'execCommand', {
+ value: vi.fn().mockReturnValue(false),
+ configurable: true,
+ writable: true,
+ });
+
+ const ok = await copyToClipboard('test');
+ expect(ok).toBe(false);
+ });
+});
diff --git a/tests/lib/exporter.test.ts b/tests/lib/exporter.test.ts
new file mode 100644
index 0000000..ba17137
--- /dev/null
+++ b/tests/lib/exporter.test.ts
@@ -0,0 +1,67 @@
+import { describe, expect, it } from 'vitest';
+import { buildManifest, formatManifest } from '@/lib/exporter';
+import type { ClayComponentInfo, ClayPageInfo } from '@/lib/types';
+
+const samplePage: ClayPageInfo = {
+ pageUri: 'site.example.com/_pages/abc',
+ layoutUri: 'site.example.com/_layouts/main/instances/x',
+ isPublished: true,
+ pageInstance: 'abc',
+};
+
+function comp(overrides: Partial = {}): ClayComponentInfo {
+ const el = document.createElement('div');
+ return {
+ uri: 'site.example.com/_components/byline/instances/x@published',
+ name: 'byline',
+ displayName: 'Byline',
+ instance: 'x',
+ element: el,
+ depth: 1,
+ ...overrides,
+ };
+}
+
+describe('buildManifest', () => {
+ it('captures page metadata + components without DOM refs', () => {
+ const m = buildManifest(samplePage, [comp()]);
+ expect(m.page).toBe(samplePage);
+ expect(m.components).toHaveLength(1);
+ expect(m.components[0]).not.toHaveProperty('element');
+ expect(m.components[0]).toMatchObject({
+ name: 'byline',
+ displayName: 'Byline',
+ depth: 1,
+ instance: 'x',
+ });
+ expect(m.exportedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
+ });
+});
+
+describe('formatManifest', () => {
+ const manifest = buildManifest(samplePage, [
+ comp({ name: 'a', displayName: 'A' }),
+ comp({ name: 'b', displayName: 'B', instance: 'with"quote', uri: 'site/_components/b' }),
+ ]);
+
+ it('produces parseable JSON', () => {
+ const out = formatManifest(manifest, 'json');
+ expect(JSON.parse(out)).toMatchObject({ components: expect.any(Array) });
+ });
+
+ it('produces a CSV with header + rows + escapes quotes', () => {
+ const out = formatManifest(manifest, 'csv');
+ const lines = out.split('\n');
+ expect(lines[0]).toBe('name,displayName,uri,instance,depth');
+ expect(lines).toHaveLength(3);
+ expect(lines[2]).toContain('"with""quote"');
+ });
+
+ it('produces markdown with a heading + table', () => {
+ const out = formatManifest(manifest, 'markdown');
+ expect(out).toContain('# ');
+ expect(out).toContain('| # | Component |');
+ expect(out).toContain('| 1 | A |');
+ expect(out).toContain('| 2 | B |');
+ });
+});
diff --git a/tests/lib/recents.test.ts b/tests/lib/recents.test.ts
new file mode 100644
index 0000000..a0e6150
--- /dev/null
+++ b/tests/lib/recents.test.ts
@@ -0,0 +1,79 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { loadRecents, pushRecent } from '@/lib/recents';
+import type { RecentComponent } from '@/lib/types';
+
+interface MockLocal {
+ data: Record;
+ get: (key: string) => Promise>;
+ set: (entries: Record) => Promise;
+ remove: (key: string) => Promise;
+}
+
+interface MockChrome {
+ storage: {
+ local: MockLocal;
+ onChanged: {
+ addListener: ReturnType;
+ removeListener: ReturnType;
+ };
+ };
+}
+
+beforeEach(() => {
+ const local: MockLocal = {
+ data: {},
+ get: vi.fn(async (key: string) => ({ [key]: local.data[key] })),
+ set: vi.fn(async (entries: Record) => {
+ Object.assign(local.data, entries);
+ }),
+ remove: vi.fn(async (key: string) => {
+ delete local.data[key];
+ }),
+ };
+ const mock: MockChrome = {
+ storage: {
+ local,
+ onChanged: { addListener: vi.fn(), removeListener: vi.fn() },
+ },
+ };
+ (globalThis as { chrome?: unknown }).chrome = mock;
+});
+
+function entry(uri: string): RecentComponent {
+ return {
+ uri,
+ displayName: uri,
+ instance: null,
+ pageUrl: 'https://example.com',
+ pageTitle: 'Example',
+ visitedAt: Date.now(),
+ };
+}
+
+describe('pushRecent', () => {
+ it('starts empty', async () => {
+ expect(await loadRecents()).toEqual([]);
+ });
+
+ it('pushes new entries to the front', async () => {
+ await pushRecent(entry('a'));
+ await pushRecent(entry('b'));
+ const list = await loadRecents();
+ expect(list.map((r) => r.uri)).toEqual(['b', 'a']);
+ });
+
+ it('dedupes by URI and re-promotes to the front', async () => {
+ await pushRecent(entry('a'));
+ await pushRecent(entry('b'));
+ await pushRecent(entry('a'));
+ const list = await loadRecents();
+ expect(list.map((r) => r.uri)).toEqual(['a', 'b']);
+ });
+
+ it('caps the list to the requested cap', async () => {
+ for (let i = 0; i < 10; i += 1) await pushRecent(entry(`u${i}`));
+ const list = await pushRecent(entry('u10'), 5);
+ expect(list).toHaveLength(5);
+ expect(list[0]!.uri).toBe('u10');
+ });
+});
diff --git a/tests/lib/seo.test.ts b/tests/lib/seo.test.ts
new file mode 100644
index 0000000..c0e9c7b
--- /dev/null
+++ b/tests/lib/seo.test.ts
@@ -0,0 +1,88 @@
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import { extractSeoMeta, lintSeo } from '@/lib/seo';
+
+function setMeta(name: string, content: string, attr: 'name' | 'property' = 'name') {
+ const el = document.createElement('meta');
+ el.setAttribute(attr, name);
+ el.setAttribute('content', content);
+ document.head.appendChild(el);
+ return el;
+}
+
+describe('extractSeoMeta', () => {
+ let added: HTMLElement[] = [];
+
+ beforeEach(() => {
+ document.title = 'Test Title';
+ added = [
+ setMeta('description', 'A description here'),
+ setMeta('og:title', 'OG Title', 'property'),
+ setMeta('og:image', 'https://example.com/img.jpg', 'property'),
+ setMeta('twitter:card', 'summary_large_image'),
+ ];
+ const link = document.createElement('link');
+ link.rel = 'canonical';
+ link.href = 'https://example.com/test';
+ document.head.appendChild(link);
+ added.push(link);
+ const h1 = document.createElement('h1');
+ h1.textContent = 'Heading';
+ document.body.appendChild(h1);
+ added.push(h1);
+ });
+
+ afterEach(() => {
+ added.forEach((el) => el.remove());
+ document.title = '';
+ });
+
+ it('reads title, description, canonical, og + twitter', () => {
+ const meta = extractSeoMeta();
+ expect(meta.title).toBe('Test Title');
+ expect(meta.description).toBe('A description here');
+ expect(meta.canonical).toBe('https://example.com/test');
+ expect(meta.og['og:title']).toBe('OG Title');
+ expect(meta.og['og:image']).toBe('https://example.com/img.jpg');
+ expect(meta.twitter['twitter:card']).toBe('summary_large_image');
+ expect(meta.h1Count).toBe(1);
+ });
+});
+
+describe('lintSeo', () => {
+ it('flags long titles + long descriptions', () => {
+ const issues = lintSeo({
+ title: 'a'.repeat(80),
+ description: 'b'.repeat(200),
+ canonical: '',
+ robots: '',
+ og: {},
+ twitter: {},
+ jsonLd: [],
+ h1Count: 0,
+ });
+ const ids = issues.map((i) => i.id);
+ expect(ids).toContain('title-long');
+ expect(ids).toContain('desc-long');
+ expect(ids).toContain('og-image-missing');
+ expect(ids).toContain('h1-missing');
+ });
+
+ it('reports nothing critical for a healthy page', () => {
+ const issues = lintSeo({
+ title: 'A perfectly normal title length',
+ description: 'A '.repeat(40).trim(),
+ canonical: 'https://example.com/x',
+ robots: 'index, follow',
+ og: {
+ 'og:title': 'x',
+ 'og:image': 'https://example.com/x.jpg',
+ 'og:description': 'x',
+ },
+ twitter: { 'twitter:card': 'summary' },
+ jsonLd: [{ '@type': 'NewsArticle' }],
+ h1Count: 1,
+ });
+ expect(issues.filter((i) => i.severity === 'error')).toHaveLength(0);
+ expect(issues.filter((i) => i.severity === 'warn')).toHaveLength(0);
+ });
+});
diff --git a/tests/lib/site-host.test.ts b/tests/lib/site-host.test.ts
new file mode 100644
index 0000000..6c81f17
--- /dev/null
+++ b/tests/lib/site-host.test.ts
@@ -0,0 +1,95 @@
+import { describe, expect, it } from 'vitest';
+import { availableEnvsFor, findMappingForHost, rewriteUrlToEnv } from '@/lib/site-host';
+import type { SiteHostMapping } from '@/lib/types';
+
+const mappings: readonly SiteHostMapping[] = [
+ {
+ id: 'thecut',
+ label: 'The Cut',
+ hosts: {
+ prod: 'www.thecut.com',
+ staging: 'stg.thecut.com',
+ qa: 'qa.thecut.com',
+ },
+ },
+ {
+ id: 'vulture',
+ label: 'Vulture',
+ hosts: {
+ prod: 'www.vulture.com',
+ staging: 'stg.vulture.com',
+ // qa intentionally missing
+ },
+ },
+];
+
+describe('findMappingForHost', () => {
+ it('matches the prod hostname of a mapping', () => {
+ expect(findMappingForHost('www.thecut.com', mappings)).toEqual({
+ mapping: mappings[0],
+ env: 'prod',
+ });
+ });
+
+ it('matches a staging hostname', () => {
+ expect(findMappingForHost('stg.vulture.com', mappings)).toEqual({
+ mapping: mappings[1],
+ env: 'staging',
+ });
+ });
+
+ it('is case insensitive', () => {
+ expect(findMappingForHost('STG.THECUT.COM', mappings)).toEqual({
+ mapping: mappings[0],
+ env: 'staging',
+ });
+ });
+
+ it('returns null for an unmapped hostname', () => {
+ expect(findMappingForHost('example.com', mappings)).toBeNull();
+ });
+
+ it('returns null for an empty mappings list', () => {
+ expect(findMappingForHost('www.thecut.com', [])).toBeNull();
+ });
+});
+
+describe('rewriteUrlToEnv', () => {
+ it('swaps the hostname while preserving path/query/hash', () => {
+ expect(
+ rewriteUrlToEnv('https://stg.thecut.com/article/123?ref=newsletter#share', 'prod', mappings)
+ ).toBe('https://www.thecut.com/article/123?ref=newsletter#share');
+ });
+
+ it('rewrites between two non-prod envs', () => {
+ expect(rewriteUrlToEnv('https://qa.thecut.com/foo', 'staging', mappings)).toBe(
+ 'https://stg.thecut.com/foo'
+ );
+ });
+
+ it('returns null when the host is not in any mapping', () => {
+ expect(rewriteUrlToEnv('https://other.example.com/foo', 'prod', mappings)).toBeNull();
+ });
+
+ it('returns null when the target env has no host configured', () => {
+ expect(rewriteUrlToEnv('https://www.vulture.com/foo', 'qa', mappings)).toBeNull();
+ });
+
+ it('returns null for invalid URLs', () => {
+ expect(rewriteUrlToEnv('::not a url::', 'prod', mappings)).toBeNull();
+ });
+});
+
+describe('availableEnvsFor', () => {
+ it('lists every env that has a host for the matched mapping', () => {
+ expect(availableEnvsFor('www.thecut.com', mappings)).toEqual(['prod', 'staging', 'qa']);
+ });
+
+ it('omits envs that are not configured for the matched mapping', () => {
+ expect(availableEnvsFor('www.vulture.com', mappings)).toEqual(['prod', 'staging']);
+ });
+
+ it('returns an empty array for an unmapped host', () => {
+ expect(availableEnvsFor('example.com', mappings)).toEqual([]);
+ });
+});
diff --git a/tests/lib/storage.test.ts b/tests/lib/storage.test.ts
new file mode 100644
index 0000000..b50b171
--- /dev/null
+++ b/tests/lib/storage.test.ts
@@ -0,0 +1,68 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { loadPreferences, savePreferences } from '@/lib/storage';
+import { DEFAULT_PREFERENCES } from '@/lib/types';
+
+interface MockSync {
+ data: Record;
+ get: (key: string) => Promise>;
+ set: (entries: Record) => Promise;
+}
+
+interface MockChrome {
+ storage: {
+ sync: MockSync;
+ onChanged: {
+ addListener: ReturnType;
+ removeListener: ReturnType;
+ };
+ };
+}
+
+function getMockChrome(): MockChrome {
+ return globalThis.chrome as unknown as MockChrome;
+}
+
+beforeEach(() => {
+ const sync: MockSync = {
+ data: {},
+ get: vi.fn(async (key: string) => ({ [key]: sync.data[key] })),
+ set: vi.fn(async (entries: Record) => {
+ Object.assign(sync.data, entries);
+ }),
+ };
+ const mock: MockChrome = {
+ storage: {
+ sync,
+ onChanged: { addListener: vi.fn(), removeListener: vi.fn() },
+ },
+ };
+ (globalThis as { chrome?: unknown }).chrome = mock;
+});
+
+describe('loadPreferences', () => {
+ it('returns defaults when nothing is stored', async () => {
+ const prefs = await loadPreferences();
+ expect(prefs).toEqual(DEFAULT_PREFERENCES);
+ });
+
+ it('merges stored values over defaults', async () => {
+ getMockChrome().storage.sync.data['preferences'] = { theme: 'dark' };
+ const prefs = await loadPreferences();
+ expect(prefs.theme).toBe('dark');
+ expect(prefs.panelPosition).toBe(DEFAULT_PREFERENCES.panelPosition);
+ });
+});
+
+describe('savePreferences', () => {
+ it('merges partial updates into existing preferences', async () => {
+ await savePreferences({ theme: 'light' });
+ expect(getMockChrome().storage.sync.data['preferences']).toMatchObject({
+ theme: 'light',
+ });
+ await savePreferences({ panelPosition: 'top-left' });
+ expect(getMockChrome().storage.sync.data['preferences']).toMatchObject({
+ theme: 'light',
+ panelPosition: 'top-left',
+ });
+ });
+});
diff --git a/tests/setup.ts b/tests/setup.ts
new file mode 100644
index 0000000..7c95589
--- /dev/null
+++ b/tests/setup.ts
@@ -0,0 +1,7 @@
+import { afterEach, vi } from 'vitest';
+
+afterEach(() => {
+ vi.restoreAllMocks();
+ document.documentElement.removeAttribute('data-uri');
+ document.documentElement.removeAttribute('data-layout-uri');
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..6234b7b
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "useUnknownInCatchVariables": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "types": ["chrome", "node", "vite/client", "vitest/globals"],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src", "tests", "vite.config.ts", "vitest.config.ts"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..f6dcd30
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,29 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { crx } from '@crxjs/vite-plugin';
+import path from 'node:path';
+import manifest from './src/manifest';
+
+export default defineConfig({
+ plugins: [react(), crx({ manifest })],
+ resolve: {
+ alias: {
+ '@': path.resolve(import.meta.dirname, 'src'),
+ },
+ },
+ build: {
+ target: 'esnext',
+ sourcemap: true,
+ rollupOptions: {
+ input: {
+ popup: 'src/popup/index.html',
+ options: 'src/options/index.html',
+ },
+ },
+ },
+ server: {
+ port: 5173,
+ strictPort: true,
+ hmr: { port: 5173 },
+ },
+});
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..a8a4755
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vitest/config';
+import path from 'node:path';
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '@': path.resolve(import.meta.dirname, 'src'),
+ },
+ },
+ test: {
+ environment: 'happy-dom',
+ globals: true,
+ setupFiles: ['./tests/setup.ts'],
+ coverage: {
+ reporter: ['text', 'html'],
+ include: ['src/**/*.{ts,tsx}'],
+ exclude: ['src/**/*.d.ts', 'src/manifest.ts', 'src/**/index.ts'],
+ },
+ },
+});