diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 0102ecb..3ed33fb 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -11,7 +11,7 @@ module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
plugins: ['jsx-a11y'],
- settings: { react: { version: '18.3' } },
+ settings: { react: { version: 'detect' } },
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 47084cf..720f432 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -79,6 +79,28 @@ jobs:
*) echo "::warning::CLIENT_SECRET does not start with GOCSPX- — is this an OAuth client secret?" ;;
esac
+ # A well-formed secret can still be the wrong one. Exchange the
+ # refresh token for an access token now — the call the upload makes
+ # first — so a rejected credential fails here, before a version is
+ # bumped and pushed. Only Google's error is printed, never a token.
+ trim() { printf '%s' "$1" | tr -d '[:space:]'; }
+ response="$(curl -sS -X POST https://oauth2.googleapis.com/token \
+ --data-urlencode "client_id=$(trim "$CLIENT_ID")" \
+ --data-urlencode "client_secret=$(trim "$CLIENT_SECRET")" \
+ --data-urlencode "refresh_token=$(trim "$REFRESH_TOKEN")" \
+ --data-urlencode "grant_type=refresh_token" || true)"
+ if ! printf '%s' "$response" | jq -e '.access_token' >/dev/null 2>&1; then
+ error="$(printf '%s' "$response" | jq -r '.error // empty' 2>/dev/null || true)"
+ description="$(printf '%s' "$response" | jq -r '.error_description // empty' 2>/dev/null || true)"
+ echo "::error::Google rejected the store credentials: ${error:-no response} ${description:+— $description}"
+ case "$error" in
+ invalid_client) echo "CLIENT_ID or CLIENT_SECRET is wrong — copy a current secret of this OAuth client from Google Cloud Console." ;;
+ invalid_grant) echo "REFRESH_TOKEN is expired, revoked, or was issued for another OAuth client — generate a new one." ;;
+ esac
+ exit 1
+ fi
+ echo "Store credentials accepted by Google"
+
- name: Setup Node.js
uses: actions/setup-node@v4
with:
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..8513427
--- /dev/null
+++ b/components.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "base-nova",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/styles/global.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "rtl": false,
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "menuColor": "default",
+ "menuAccent": "subtle",
+ "registries": {
+ "@coss": "https://coss.com/ui/r/{name}.json"
+ }
+}
diff --git a/docs/redesign/README.md b/docs/redesign/README.md
new file mode 100644
index 0000000..6111605
--- /dev/null
+++ b/docs/redesign/README.md
@@ -0,0 +1,566 @@
+# QueryCraft × coss ui — redesign handoff
+
+Implementation brief for rebuilding the QueryCraft popup UI on [coss ui](https://coss.com/ui) (Base UI + Tailwind v4).
+Written for Claude Code. Read this whole file before touching code, then work **one phase at a time** (§10).
+
+- **Design canvas (source of truth for visuals):** https://claude.ai/code/artifact/6f586460-f2ba-49dd-a935-a47a810fbb3b — private; ask the owner for screenshots if you need to see it.
+- **Mockup sources:** `docs/redesign/mockups/*.dc.html` — the canvas artboards as authored. They are **not runnable** (they need the canvas runtime) but every size, colour, copy string and state is written inline. Read them when this doc is ambiguous. `Main` = param list, `Overflow` = max-height states, `Settings`, `Saved`.
+- **Branch:** work on `redesign/coss-ui` off `main`. The `DSL-beta` branch is out of scope.
+
+---
+
+## 1. Scope
+
+| Area | Status | What to build |
+|---|---|---|
+| Param list (main view) | Designed | Full rebuild (§6.1) |
+| Max-height behaviour, internal scroll, key Autocomplete | Designed | Full rebuild (§5, §6.2) |
+| Settings sheet: accent + **theme preference (new)** | Designed | Full rebuild (§6.3) |
+| Saved URLs sheet: list, row menu, save/edit form | Designed | Full rebuild (§6.4) |
+| Command palette (⌘K) | Spec only, no mockup | Phase 6, §6.5 |
+| Loading / unsupported / error states | Not designed | Interim: coss `Empty` with existing copy (§6.6) |
+| JSON drill-down (`JsonStack/*`) | Not designed | Restyle with coss primitives only, keep behaviour (§6.6) |
+| DSL filter builder, token/chip primitive | Not designed | **Out of scope** |
+
+**Do not** add features beyond this doc. Where the doc proposes something new it is marked **(new)**; build only those.
+
+## 2. Hard constraints
+
+1. **Manifest V3 popup.** Width **380px**. Height = content height, **max 600px** (Chrome's cap). Height is set from JS by a `ResizeObserver` on the app container (§5.1).
+2. **No remote resources.** No font CDNs, no inline ``, before the module script.
+ - Wrap it in `try/catch`.
+
+## 5. Layout, sizing, scrolling
+
+### 5.1 Popup height
+
+- `html` and `body`: width 380px, height auto.
+- The app container is `flex flex-col max-h-[600px]` with natural height.
+- In `main.tsx`, observe the app container. On each entry set `document.documentElement.style.height = document.body.style.height = Math.min(600, Math.ceil(entry.borderBoxSize[0].blockSize)) + 'px'`.
+- **While a Sheet is open**, set the app container's `min-height` to the sheet content's natural height (capped at 600). Otherwise a sheet opened over a short view, e.g. an empty list, gets clipped. Measure the sheet popup with its own `ResizeObserver`.
+
+### 5.2 Scroll model
+
+```
+header (flex-none)
+main (flex-1 min-h-0, flex-col gap-3, px-4 py-3)
+ URL editor (flex-none, max 4 lines, own internal scroll)
+ filter (flex-none)
+ section "Parameters" (flex-1 min-h-0, flex-col gap-2)
+ section header (flex-none)
+ ScrollArea scrollFade (flex-1 min-h-0) ← the ONLY scrolling list
+ Separator
+ add row (flex-none, pinned)
+footer action bar (flex-none)
+```
+
+- At 600px max the list viewport is ~241px (~7 rows).
+- **Focus never hidden.** On the ScrollArea viewport set `scroll-padding-block: 1.5rem`, the same as coss's `--fade-size`. A row focused by Tab then always lands fully above or below the scroll fade.
+- **Full-bleed viewport.** Give the ScrollArea `-mx-4` and its content `px-4`. Then `overflow` does not clip the 3px focus rings of the edge inputs.
+- The URL editor shows a bottom fade (`mask-b-from-[calc(100%-1.5rem)]`) **only** when it overflows. Set a `data-overflowing` attribute from `scrollHeight > clientHeight`.
+
+### 5.3 Portaled popups
+
+Default positioner props everywhere: `collisionPadding={8}`, `sideOffset={4}`. Cap heights with `--available-height`.
+
+- **Key Autocomplete (add row):**
+ - Anchor to the whole add-row Group (`anchor={groupRef}`), so the popup is the row's width (320px), not the 128px key cell's.
+ - `side="bottom"` with flip. In practice it opens **up**, because the add row sits near the bottom.
+ - `[--popup-max-h:12rem]`; the list scrolls inside with the coss scroll fade.
+- **Saved URLs row Menu:** `align="end"`, below its ⋯ trigger.
+- Toasts render bottom-centre, 8px above the action bar, max width 348px.
+
+## 6. Screens
+
+All copy below is final. Sizes are px with the Tailwind class in brackets; at our density, `sm:` classes are the active ones.
+
+### 6.1 Param list (`Main` artboard)
+
+Content height ~557px with 6 params.
+
+**Header** `[flex items-center justify-between border-b py-3 ps-4 pe-3]`
+- **Left:**
+ - Logo tile 24px `[size-6 rounded-sm bg-primary text-primary-foreground]` with lucide `Search` 14px, `aria-hidden`.
+ - `
` "QueryCraft" `[text-sm font-semibold]`.
+- **Right** `[flex gap-0.5]`:
+ - Command palette trigger: `Button variant="ghost"` `[h-7 px-1 gap-1]` containing `KbdGroup` `⌘` `K`, `aria-label="Command palette"`, `aria-keyshortcuts="Meta+K"`.
+ - Saved URLs: `Button variant="ghost" size="icon-sm"` with `Bookmark`, `aria-keyshortcuts="Meta+S"`.
+ - Settings: `Button variant="ghost" size="icon-sm"` with `Settings`.
+ - Icon-only buttons get a `Tooltip`: label + Kbd.
+
+**URL editor** — InputGroup shell + `InputGroupTextarea`.
+- Text: `font-mono sm:text-code leading-5`, 5px vertical padding, 11px horizontal.
+- Max 4 lines (80px), then internal scroll.
+- Keep the existing highlight-overlay technique: an `aria-hidden` mirror div under a transparent-text textarea. Sync scroll.
+- Colours:
+ - Base URL and the `? & =` separators: `text-muted-foreground`.
+ - Keys: `text-key-foreground font-medium`.
+ - Values: `text-foreground`.
+- `aria-label="Current URL"`. Enter commits, as today.
+
+**Filter** — `InputGroup` (32px):
+- `InputGroupAddon` with `Search` icon (muted).
+- `InputGroupInput type="search" placeholder="Filter parameters"`, `aria-label="Filter parameters by key"`, `aria-keyshortcuts="/"`.
+- `InputGroupAddon align="inline-end"` containing `/ `.
+
+**Section header** `[flex h-6 items-center justify-between]`
+- **Left** `[flex items-center gap-1.5]`:
+ - `` "Parameters" `[text-xs font-medium text-muted-foreground]`.
+ - `Badge variant="secondary"` with the visible count, `aria-label="{n} parameters"`.
+- **Right:** `Button variant="ghost" size="icon-xs"` with `Ellipsis`, `aria-label="Parameter actions"`. It opens a `Menu` **(new)** with:
+ - Copy as query string
+ - Copy as JSON
+ - Sort keys A→Z
+ - separator
+ - Remove all parameters (destructive variant; undoable, see §7)
+
+**Row** — ` ` containing:
+
+```tsx
+
+
+
+ {valueControl}
+
+
+```
+
+| Value type | Control |
+|---|---|
+| string / number | `Input size="sm"` `[flex-1 min-w-0 font-mono sm:text-code]`, `aria-label="Value"`. Inputs truncate with `text-ellipsis` at rest. |
+| boolean | InputGroup shell (28px): `Switch` (`aria-label="Value"`) + `InputGroupText` with the literal value in mono, casing preserved. State is never carried by the switch alone. |
+| structured JSON | `Button variant="outline" size="sm"` `[flex-1 min-w-0 justify-between font-mono font-normal sm:text-code text-muted-foreground]`: truncated preview + `ChevronRight`. `aria-label="Edit {key}, JSON array, {n} items"`. Opens the existing JSON stack. |
+| DSL (later, DSL-beta) | Same button, sans text: `ListFilter` icon + "{n} conditions" + `ChevronRight`. Accessible name includes the full humanised expression. Document only. |
+
+Wrap each row in a `ContextMenu` **(new)**: Copy key, Copy value, Copy `key=value`, Duplicate, separator, Remove. It opens on right-click and on Shift+F10 / the Menu key. This replaces the old hover-only copy buttons, which go away.
+
+**Add row** — `role="group" aria-label="Add parameter"`:
+
+```tsx
+
+ } placeholder="key" aria-label="New parameter key" /> {/* basis-2/5 */}
+
+
+
+
+```
+
+- Suggestions:
+ - "From saved URLs on {host}": keys from saved URLs on the same host, shown first, only when non-empty.
+ - "Common keys": `utm_source utm_medium utm_campaign utm_term utm_content utm_id utm_source_platform page limit sort q debug`.
+- Item rendering: the typed prefix in `text-muted-foreground`, the completion in `font-medium`.
+
+**Action bar** `[flex items-center gap-2 border-t px-4 py-3]`:
+- `Button` "Apply" containing `⌘↵ `. Kbd on primary: `bg-primary-foreground/12 text-primary-foreground`; this is a small gap in coss.
+- `Button variant="ghost"` "Reset".
+- `flex-1` spacer.
+- `Button variant="outline"` "Copy". The label swaps to "Copied" for 1.6s.
+- `Button variant="outline"` "Save". Opens the save form directly.
+
+All footer buttons are the default height (32px), text-only.
+
+### 6.2 Max-height states (`Overflow` artboard)
+
+16 params, popup at 600px.
+
+- **(a) Tab focus at the list edge.** The focused row (`env`) sits fully above the bottom fade because of `scroll-padding-block`. The ScrollArea thumb is visible while scrolling.
+- **(b) Add-row key focused, `utm_` typed.**
+ - The Autocomplete flips above the row, 320px wide, capped at 192px, with a fade at the bottom.
+ - The highlighted item has `bg-accent` + inset `--ring`.
+ - `AutocompleteStatus` announces "{n} suggestions".
+
+### 6.3 Settings sheet (`Settings` artboard)
+
+`Sheet side="right" density="compact"`: coss default width `calc(100% - 3rem)` = 332px. The left 48px shows the editor under `bg-black/32 backdrop-blur-sm`. Clicking it or pressing Esc closes the sheet.
+
+- **Header** `[flex items-center gap-2 py-3 ps-4 pe-2]`:
+ - `SheetTitle` "Settings" `[text-base font-semibold]`.
+ - `SheetClose` as `Button variant="ghost" size="icon"` with `X`, `aria-label="Close settings"`. Use `showCloseButton={false}` and render the close button in the header row.
+- **Panel** `[flex flex-col gap-4]`:
+ 1. **Accent color.**
+ - Label row: "Accent color" `[text-sm font-medium]`, and the selected swatch's *name* right-aligned in muted. Colour is never the only cue.
+ - `RadioGroup aria-label="Accent color"`, `grid grid-cols-6 gap-2`.
+ - 11 options: **Mono** (fill `--foreground`) then Blue, Cyan, Teal, Green, Lime, Amber, Orange, Red, Pink, Violet. Hex values live in `src/lib/accents.ts`.
+ - Each swatch: 32px high, `rounded-md`, inset 1px `black/10` (several swatches are < 3:1 on white).
+ - Selected swatch: 2px offset outline in `--foreground` + `Check` icon in `readableOn(hex)`.
+ - **Mono replaces the old "Reset to monochrome" link.**
+ - `FieldDescription`: "Tints the primary button, switches and URL keys. Text on the accent is picked to keep 4.5:1."
+ - The swatch is a gap in coss: compose `RadioGroup` + `Radio` with a custom render. Do not create a new package-level component yet.
+ 2. **Preview.**
+ - Label "Preview" `[text-xs font-medium text-muted-foreground]`.
+ - `Frame` > `FramePanel p-3`, `inert` + `aria-hidden`.
+ - Contents: `?view=table&live=true` with keys in `text-key-foreground`, a checked Switch + mono "true", and a small primary "Apply ⌘↵". It shows exactly what the accent changes.
+ 3. `Separator`.
+ 4. **Theme (new).**
+ - Label "Theme".
+ - Segmented control with radio semantics, `aria-label="Theme"`, full width, three equal items, each a 16px icon plus label: `Monitor` System · `Sun` Light · `Moon` Dark.
+ - Description text:
+ - System: "Follows your OS appearance — {light|dark} right now."
+ - Light or Dark: "Overrides your OS appearance for QueryCraft only."
+- Changes apply instantly; there is no footer. Announce "Accent: Violet" / "Theme: Dark" through the hidden LiveRegion.
+- On open, focus goes to the checked accent radio.
+
+### 6.4 Saved URLs sheet (`Saved` artboard)
+
+Same sheet chrome (compact, 332px).
+
+**List mode**
+- **Header:** "Saved URLs" + `Badge variant="secondary"` total count + close button (`aria-label="Close saved URLs"`).
+- **Panel:**
+ - **Filter (new):** `InputGroup` with search icon, placeholder "Filter saved URLs", trailing `/ `. It matches label, URL and group name.
+ - **Groups:** one `Collapsible` per group. The trigger is a full-width ghost row (28px) `[text-xs font-medium text-muted-foreground]`: `ChevronDown` (rotates −90° when closed) + group name + `Badge variant="secondary"` count, with `aria-expanded`. Groups start expanded; empty groups are hidden.
+ - **Rows:** inside ``.
+ - Main `` `[flex-1 min-w-0 flex-col items-stretch py-2 ps-3 pe-1 text-start]`.
+ - With a label: label `[truncate text-sm font-medium]` over the URL `[truncate font-mono text-xs text-muted-foreground]`. The URL is shown **without** its protocol.
+ - Without a label: the URL alone `[truncate font-mono sm:text-code text-foreground]`.
+ - Click / Enter on the main button = **Load into editor** (current behaviour).
+ - Trailing `Button variant="ghost" size="icon-xs"` ⋯, `aria-label="Actions for {name}"`, opens a `Menu`:
+ - Load into editor ↵
+ - Copy URL
+ - Edit…
+ - separator
+ - Delete ⌫ (destructive variant)
+ - This replaces the three always-visible icon buttons.
+ - Backspace/Delete on a focused row deletes it, with Undo (§7).
+- **Footer** (`SheetFooter`, muted, right-aligned): primary `Button` "Save current URL", disabled when there is no current URL.
+
+**Form mode** (save or edit; same sheet, content swaps)
+- **Header:** `Button variant="ghost" size="icon"` `ChevronLeft` (`aria-label="Back to saved URLs"`), title "Save current URL" / "Edit saved URL", close button.
+- **Fields** (coss `Field` / `FieldLabel` / `FieldDescription`):
+ - **Name (optional).**
+ - `Input`, placeholder "e.g. Staging with debug flag", maxLength 80, autofocus on open.
+ - Description: "Shown instead of the URL in the list."
+ - "(optional)" is in muted, normal weight.
+ - **Group.**
+ - **Creatable `Combobox`** (replaces `GroupSelector`'s native select + "Create new group…" swap). Typing filters groups.
+ - When nothing matches exactly, the last item is "Create “{query}”". Enter on it creates the group, reusing the existing de-duplication in `handleCreateNewGroup`.
+ - Description: "Type to find a group, or create a new one."
+ - **URL.**
+ - Read-only `Textarea`, `aria-readonly`, `bg-muted`. coss has no read-only style, so this is a gap. Same mono highlighting as the main URL editor, max 4 lines.
+ - Description: "Read-only — edit parameters in the main view before saving."
+- **Footer:** `Button variant="ghost"` "Cancel" + primary "Save" / "Update" with `⌘↵ `. Rule: **⌘↵ is the primary action in every view.**
+
+### 6.5 Command palette (spec only, Phase 6)
+
+Built as `Dialog` + `Command`, positioned 48px from the top, 16px side margins, max height `min(var(--available-height), 24rem)`.
+
+- Input placeholder "Search parameters and actions…".
+- **Groups:**
+ - **Parameters:** key in mono + truncated value in muted. Enter closes the palette and focuses that row's value input, scrolling it into view.
+ - **Actions:** Apply ⌘↵, Copy URL, Save URL ⌘S, Reset, Add parameter (focuses the add-row key).
+ - **Go to:** Saved URLs, Settings.
+- Shortcuts appear as right-aligned `Kbd`.
+- Keep it visually identical to the Autocomplete popup (item height, highlight, inset ring).
+
+### 6.6 Interim (not designed)
+
+- **Loading / unsupported / error:** coss `Empty` with the existing titles and messages. Loading uses `Spinner`.
+- **JSON stack:** keep the behaviour.
+ - Frames → coss `Breadcrumb`.
+ - Structured/Raw switch → segmented control.
+ - Raw editor → `Textarea` (`font-mono sm:text-code`).
+ - Leaf inputs → `Input size="sm"`.
+ - Nothing new visually beyond tokens.
+
+## 7. Behaviour
+
+### 7.1 Keyboard map
+
+| Where | Key | Action |
+|---|---|---|
+| Global | ⌘/Ctrl+↵ | Apply (main), Save/Update (form) |
+| Global | ⌘/Ctrl+K | Command palette |
+| Global (not typing) | `/` | Focus the filter of the current view |
+| Global | ⌘/Ctrl+S | Saved URLs sheet |
+| Global (not typing) | ⌘/Ctrl+Z **(new)** | Undo last removal / reset / saved-URL delete while its toast is alive |
+| Global | Esc | Close the topmost popup → sheet (Base UI); then the existing JSON-stack pop |
+| Row key input | ↵ | Move to the value input |
+| Row value input | ↵ | Move to the next row's key; after the last row, the add-row key |
+| Row | Shift+F10 / Menu key | Row context menu |
+| Add row | ↵ | Commit, clear, focus returns to the add-row key (as today) |
+| Autocomplete | ↑ ↓ / ↵ / Esc / Tab | Move / accept / close / close without accepting and move to value |
+| Saved row | ↵ / Backspace, Delete | Load / delete with undo |
+
+**Tab order (main):**
+1. Header: ⌘K, Saved URLs, Settings
+2. URL
+3. Filter
+4. Parameter actions
+5. Each row: key → value → remove
+6. Add row: key → value → add
+7. Action bar: Apply → Reset → Copy → Save
+
+### 7.2 Focus management
+
+| Event | Focus goes to |
+|---|---|
+| Row removed | The **remove button of the row that took its place**. If none, the previous row's remove button. If the list is empty, the add-row key. Moves immediately, before the exit animation. |
+| Row added from the add row | Stays on the add-row key (rapid entry). The new row is announced. |
+| Undo restores a row | The restored row's key input |
+| Sheet opens | Settings: checked accent radio. Saved list: filter input. Form: Name. |
+| Sheet closes | The trigger that opened it (Base UI restores it). Delete `useFocusTrap` for sheets. |
+| Saved URL loaded | Sheet closes; focus goes to the URL editor |
+| Palette selects a parameter | That row's value input |
+
+### 7.3 Announcements: exactly once each
+
+Base UI's `Toast` viewport is already a polite live region. Visible feedback goes **only** through Toast. Events without a toast go through a single visually-hidden `LiveRegion` (port `LiveRegion` from DSL-beta). Never both.
+
+| Event | Visible | Announced via |
+|---|---|---|
+| URL applied | Toast "Applied to the tab" | Toast |
+| URL copied | Toast "URL copied" + button label "Copied" | Toast |
+| Param removed | Toast "Removed {key}" with Undo action, hint "⌘Z" | Toast |
+| Reset | Toast "Reset to the original URL" with Undo | Toast |
+| Remove all | Toast "Removed {n} parameters" with Undo | Toast |
+| Saved / updated / deleted URL | Toast; delete has Undo | Toast |
+| Param added | Row appears | LiveRegion "Added {key}" |
+| Filter typed | Count badge updates | LiveRegion "{n} of {m} parameters" (debounce 500ms) |
+| Accent / theme changed | Instant restyle | LiveRegion "Accent: {name}" / "Theme: {name}" |
+| Autocomplete results | — | `AutocompleteStatus` |
+
+- Undo toasts last 6s; others 3s.
+- Hover or focus pauses the toast.
+- Undo keeps the removed item in memory, including saved-URL deletes, which are written to storage only when the toast expires or is replaced.
+- **Alert Dialog** is reserved for irreversible actions. None exist in this scope; deleting a group would be the first, if group management is ever exposed.
+
+## 8. Motion
+
+| What | Enter | Exit | Notes |
+|---|---|---|---|
+| Param row added | 150ms `--ease-out`, opacity 0→1 + translateY(−4px→0) | — | CSS `@starting-style` (Chrome-only target), no JS animation lib |
+| Param row removed | — | 150ms `--ease-in`, opacity →0 + height collapse (`grid-template-rows: 1fr→0fr`) | Row is `inert` while exiting; focus already moved |
+| Portaled popups (Autocomplete, Menu, Combobox, Tooltip, Command) | 150ms `--ease-out`, opacity + scale .98→1 from `--transform-origin` | 100ms `--ease-in`, opacity | Base UI `data-starting-style` / `data-ending-style`; Tooltip open delay 600ms |
+| Sheet | 200ms ease-in-out, translateX(2rem→0) + opacity (coss default) | same | Backdrop opacity 200ms |
+| Toast | 200ms `--ease-out`, translateY(8px→0) + opacity | 150ms `--ease-in`, opacity | |
+| Collapsible group | 150ms `--ease-out`, height via `--collapsible-panel-height`, chevron rotation | same | |
+| Theme or accent change | none | none | Keep `data-theme-switching` suppression |
+
+Anything not in this table does not animate. Under `prefers-reduced-motion: reduce`, drop all transforms and height animation, and keep only opacity at ≤ 100ms. The existing global reduced-motion rule (0.01ms) is acceptable.
+
+## 9. Component inventory (current → new)
+
+| Current | New |
+|---|---|
+| `Header` + `IconButton` | header markup + `Button` ghost `icon-sm` + `Tooltip`; ghost `Button` + `KbdGroup` for ⌘K |
+| `Button` | `Button` (default / ghost / outline) |
+| `IconButton` | `Button size="icon*"` |
+| `UrlPreview` | InputGroup shell + `InputGroupTextarea` + highlight overlay |
+| `SearchInput` | `InputGroup` + addons + `Kbd` |
+| `ParamList` | section + `ScrollArea scrollFade` + `Menu` (bulk) |
+| `ParamRow` + `ParamTextInput` | `Group` + `Input sm` + `GroupSeparator` + `Button` ghost icon-xs + `ContextMenu` |
+| `CopyButton` (hover-only) | **removed**: ContextMenu, Command palette |
+| `BooleanToggle` | `Switch` in an InputGroup shell + literal text |
+| structured value | `Button` outline sm → JSON stack |
+| `AddParamRow` | `Group` + `Autocomplete` + `Input` + `Button` outline icon-xs |
+| `ActionBar` | footer + `Button` ×4 + `Kbd` |
+| `Toast` | coss `Toast` (provider at root) + hidden `LiveRegion` |
+| `EmptyState` | `Empty` (+ `Spinner`) |
+| `SlidePanel` + `useFocusTrap` | `Sheet` (compact density); focus handled by Base UI |
+| `SettingsDrawer` | `Sheet` + `RadioGroup` swatches + `Frame` preview + segmented control (theme) |
+| `SavedLinksDrawer` / `GroupedLinksList` | `Sheet` + `InputGroup` filter + `Collapsible` + list + `Menu` + `SheetFooter` |
+| `SaveForm` | `Field` + `Input` + `Combobox` (creatable) + `Textarea` read-only |
+| `GroupSelector` | creatable `Combobox` |
+| `JsonStack/*` | restyle only (§6.6) |
+| `src/styles/tokens.css` | deleted; coss tokens + `theme.css` (§4.2) |
+
+## 10. Phases
+
+Each phase ends with `npm run build && npm run lint && npm test` green and a short summary of what changed.
+
+0. **Foundation.**
+ - Branch, Tailwind v4, coss init and components, fonts, `theme.css`, density re-key, `.dark` strategy, `isolation: isolate`.
+ - Apply the §4.3 component edits.
+ - No visual migration yet; the app must still look like today.
+1. **Shell and sizing.**
+ - `ResizeObserver` height, app layout (§5.2), header, action bar, Toast provider + LiveRegion.
+ - Remove the fixed 600px html/body height.
+2. **Param list.**
+ - Rows by type, filter, section header + bulk Menu, ContextMenu.
+ - ScrollArea with `scroll-padding`.
+ - Focus management (§7.2), row motion (§8).
+3. **Add row + Autocomplete.** Anchor, flip, 12rem cap, suggestions sources, Enter flow.
+4. **Undo and announcements.** §7.3 table, ⌘Z.
+5. **Sheets.**
+ - Settings: accent pipeline rewrite §4.4, theme preference §4.5 + `theme-boot.js`.
+ - Saved URLs list + form (creatable Combobox). Delete `SlidePanel`, `useFocusTrap` usage, `GroupSelector`.
+6. **Command palette** (§6.5).
+7. **Cleanup and docs.**
+ - Delete unused CSS Modules, `tokens.css`, dead components.
+ - Update the README "Design system", "Keyboard shortcuts" and "Accessibility notes" sections to match this doc.
+
+### Tests to add or update
+
+- `storage`: theme preference get/set and default.
+- `useTheme`: `system` follows `matchMedia`; explicit preference ignores it; `.dark` and `color-scheme` applied.
+- `useAccent`: writes the three variables; mono removes them; dark uses `#161616` for `--key-foreground`.
+- `contrast.test.ts`: update backgrounds. Add a token-pair test asserting the table in §11.
+- ParamList: removing a row moves focus per §7.2; Undo restores the row at the same index and focuses its key.
+- Add row: Enter commits and refocuses the key; the add button is disabled while the key is blank.
+- Saved form: creatable Combobox reuses an existing group case-insensitively.
+- Announcements: each §7.3 event produces exactly one announcement.
+
+## 11. Contrast acceptance (computed from final tokens)
+
+| Pair | Light | Dark | Needs |
+|---|---|---|---|
+| foreground / background | 15.1 | 16.6 | 4.5 |
+| muted-foreground / background | 5.6 | 5.6 | 4.5 |
+| placeholder (muted-fg) / input | 5.6 | 5.3 | 4.5 |
+| Kbd text / Kbd bg | 5.1 | 4.7 | 4.5 |
+| muted-fg / popover (group labels) | 5.6 | 5.3 | 4.5 |
+| ring / background (focus) | 4.7 | 7.0 | 3 |
+| focused border vs resting border | 3.8 | 5.7 | 3 |
+| highlighted-item inset ring / its bg | 4.3 | 6.0 | 3 |
+| primary-foreground / primary (mono) | 14.5 | 13.9 | 4.5 |
+| ⌘↵ Kbd on primary (mono) | 10.0 | 11.1 | 4.5 |
+| primary-foreground / primary (Violet `#9575e0`) | 5.6 | 5.6 | 4.5 |
+| ⌘↵ Kbd on primary (Violet) | 4.5 | 4.5 | 4.5 |
+| key-foreground (Violet) / input | 5.9 (`#7250b8`) | 6.1 (`#a58ce8`) | 4.5 |
+| destructive-foreground / highlighted menu item | 5.9 | 5.4 | 4.5 |
+
+**Deliberately unchanged:** input borders are ~1.25:1 (coss default). WCAG 1.4.11 does not require a field boundary when the field is identified by its visible label or placeholder, which now meet 4.5:1.
+
+## 12. Don'ts
+
+- No `window.confirm` / `prompt` (suppressed in MV3 popups).
+- No hover-only controls. No `outline: none` without a replacement.
+- No sticky header or footer. No second scrolling region besides the list (and the URL editor's own 4-line scroll).
+- No remote fonts or scripts. No inline scripts.
+- Don't let the user's accent drive `--ring` or `--accent`.
+- Don't invent UI for the "not designed" items. Use the interim rules in §6.6 and leave a `TODO(design)` comment.
diff --git a/docs/redesign/mockups/Main.dc.html b/docs/redesign/mockups/Main.dc.html
new file mode 100644
index 0000000..6ab0f54
--- /dev/null
+++ b/docs/redesign/mockups/Main.dc.html
@@ -0,0 +1,180 @@
+
+
+
+
+QueryCraft · param list
+
+
+
+
+
+
+
+
+
+
+
+
{{c.caption}}
+
+
+
+
+
+
{{p.t}} {{p.t}} {{p.t}}
+
+
+
+
+
+
+
+
Parameters
+{{c.count}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{r.value}}
+
{{r.value}}
+
{{r.value}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+Apply⌘↵
+Reset
+
+Copy
+Save
+
+
+
+
+
+
Common keys
+
+{{o.pre}} {{o.rest}}
+{{o.pre}} {{o.rest}}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/redesign/mockups/Overflow.dc.html b/docs/redesign/mockups/Overflow.dc.html
new file mode 100644
index 0000000..b0eebd1
--- /dev/null
+++ b/docs/redesign/mockups/Overflow.dc.html
@@ -0,0 +1,197 @@
+
+
+
+
+QueryCraft · param list, max height
+
+
+
+
+
+
+
+
+
+
+
+
{{c.caption}}
+
+
+
+
+
+
{{p.t}} {{p.t}} {{p.t}}
+
+
+
+
+
+
+
+
Parameters
+{{c.count}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{r.value}}
+
{{r.value}}
+
{{r.value}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+Apply⌘↵
+Reset
+
+Copy
+Save
+
+
+
+
+
+
Common keys
+
+{{o.pre}} {{o.rest}}
+{{o.pre}} {{o.rest}}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/redesign/mockups/Saved.dc.html b/docs/redesign/mockups/Saved.dc.html
new file mode 100644
index 0000000..61a5eb7
--- /dev/null
+++ b/docs/redesign/mockups/Saved.dc.html
@@ -0,0 +1,287 @@
+
+
+
+
+QueryCraft · saved URLs
+
+
+
+
+
+
+
+
+
+
+
+
{{c.caption}}
+
+
+
+
+
+
+
{{p.t}} {{p.t}} {{p.t}}
+
+
+
+
+
+
+
+
Parameters
+{{c.count}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{r.value}}
+
{{r.value}}
+
{{r.value}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+Apply⌘↵
+Reset
+
+Copy
+Save
+
+
+
+
+
+
Saved URLs
+6
+
+%saved URLsvg:x:16%%
+
+
+
+
+
+
+
+{{g.name}}
+{{g.count}}
+
+
+
+
+
+
+{{l.label}} {{l.url}}
+{{l.url}}
+
+
+
+
+
+
+
+
+
+
+
+Save current URL
+
+
+
+
+
+
+
Save current URL
+
%saved URLsvg:x:16%%
+
+
+
+
+Name (optional)
+
+
+
Shown instead of the URL in the list.
+
+
+
+Group
+
+
+
Type to find a group, or create a new one.
+
+
+
+URL
+
+
+{{p.t}} {{p.t}} {{p.t}}
+
+
+
+
+
Read-only — edit parameters in the main view before saving.
+
+
+
+Cancel
+Save⌘↵
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/redesign/mockups/Settings.dc.html b/docs/redesign/mockups/Settings.dc.html
new file mode 100644
index 0000000..f2f668b
--- /dev/null
+++ b/docs/redesign/mockups/Settings.dc.html
@@ -0,0 +1,239 @@
+
+
+
+
+QueryCraft · settings
+
+
+
+
+
+
+
+
+
+
+
+
{{c.caption}}
+
+
+
+
+
+
+
{{p.t}} {{p.t}} {{p.t}}
+
+
+
+
+
+
+
+
Parameters
+{{c.count}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{r.value}}
+
{{r.value}}
+
{{r.value}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+Apply⌘↵
+Reset
+
+Copy
+Save
+
+
+
+
+
+
Settings
+%settingsvg:x:16%%
+
+
+
+
+
Accent color
+
{{c.accentName}}
+
+
+
Tints the primary button, switches and URL keys. Text on the accent is picked to keep 4.5:1.
+
+
+
Preview
+
+
+
{{q.t}} {{q.t}} {{q.t}}
+
+
+
+
+
+
+
Theme
+
+
+ {{m.label}}
+ {{m.label}}
+
+
+
{{c.themeDesc}}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/manifest.json b/manifest.json
index 1b91ac3..c233f09 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "QueryCraft",
- "version": "1.0.9",
+ "version": "1.0.10",
"description": "Inspect, edit, save, and replay URL query parameters on the active tab.",
"action": {
"default_popup": "src/popup/index.html",
diff --git a/package-lock.json b/package-lock.json
index 3de4f4f..a0c8de4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,25 +1,34 @@
{
"name": "querycraft",
- "version": "1.0.8",
+ "version": "1.0.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "querycraft",
- "version": "1.0.8",
+ "version": "1.0.10",
"dependencies": {
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
+ "@base-ui/react": "^1.8.0",
+ "@fontsource-variable/inter": "^5.3.0",
+ "@fontsource/geist-mono": "^5.3.0",
+ "class-variance-authority": "^0.7.1",
+ "cn": "^0.4.0",
+ "lucide-react": "^1.52.0",
+ "react": "^19.3.0",
+ "react-dom": "^19.3.0",
+ "shadcn": "^4.21.3",
+ "tw-animate-css": "^1.4.0",
"zustand": "^4.5.5"
},
"devDependencies": {
"@crxjs/vite-plugin": "2.0.0-beta.26",
+ "@tailwindcss/vite": "^4.3.3",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/chrome": "^0.0.270",
- "@types/react": "^18.3.11",
- "@types/react-dom": "^18.3.0",
+ "@types/react": "^19.3.0",
+ "@types/react-dom": "^19.3.0",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"@vitejs/plugin-react": "^4.3.2",
@@ -29,6 +38,7 @@
"eslint-plugin-react-hooks": "^4.6.2",
"jsdom": "^25.0.1",
"prettier": "^3.3.3",
+ "tailwindcss": "^4.3.3",
"typescript": "^5.6.2",
"vite": "^5.4.8",
"vitest": "^2.1.2"
@@ -63,13 +73,12 @@
"license": "ISC"
},
"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,
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/helper-validator-identifier": "^7.29.7",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -81,7 +90,6 @@
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -91,7 +99,6 @@
"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",
@@ -122,28 +129,25 @@
"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/core/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/@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,
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.29.0",
- "@babel/types": "^7.29.0",
+ "@babel/parser": "^7.29.8",
+ "@babel/types": "^7.29.8",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -152,11 +156,22 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz",
+ "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "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",
@@ -173,46 +188,85 @@
"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/@babel/helper-create-class-features-plugin": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz",
+ "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-member-expression-to-functions": "^7.29.7",
+ "@babel/helper-optimise-call-expression": "^7.29.7",
+ "@babel/helper-replace-supers": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"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,
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz",
+ "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==",
"license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
"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,
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"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,
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.28.6",
- "@babel/helper-validator-identifier": "^7.28.5",
- "@babel/traverse": "^7.28.6"
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -221,41 +275,79 @@
"@babel/core": "^7.0.0"
}
},
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz",
+ "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
- "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
- "dev": true,
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz",
+ "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.29.7",
+ "@babel/helper-optimise-call-expression": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz",
+ "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==",
"license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
"engines": {
"node": ">=6.9.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,
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"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,
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"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,
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -265,7 +357,6 @@
"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",
@@ -276,13 +367,12 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.29.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
- "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
- "dev": true,
+ "version": "7.29.9",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
+ "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.0"
+ "@babel/types": "^7.29.8"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -291,6 +381,52 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz",
+ "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz",
+ "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz",
+ "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-transform-react-jsx-self": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
@@ -323,44 +459,79 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.29.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.9.tgz",
+ "integrity": "sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
+ "@babel/plugin-syntax-typescript": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz",
+ "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "@babel/plugin-syntax-jsx": "^7.29.7",
+ "@babel/plugin-transform-modules-commonjs": "^7.29.7",
+ "@babel/plugin-transform-typescript": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/runtime": {
- "version": "7.29.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
- "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
- "dev": true,
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.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,
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/parser": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
- "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
- "dev": true,
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+ "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
"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",
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.8",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.8",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.8",
"debug": "^4.3.1"
},
"engines": {
@@ -368,19 +539,78 @@
}
},
"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,
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.28.5"
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
+ "node_modules/@base-ui/react": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.8.0.tgz",
+ "integrity": "sha512-P0/1sxo6SBVZOklKMIedvTWqw2s2IQzi9x5bIVsXu980cuSOD4NeuRSs+/L7LZQfDkZP/uRZyGPyfFl/B1oH+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.7",
+ "@base-ui/utils": "0.4.0",
+ "@floating-ui/react-dom": "^2.1.9",
+ "@floating-ui/utils": "^0.2.12",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@date-fns/tz": "^1.2.0",
+ "@types/react": "^17 || ^18 || ^19",
+ "date-fns": "^4.0.0",
+ "react": "^17 || ^18 || ^19",
+ "react-dom": "^17 || ^18 || ^19"
+ },
+ "peerDependenciesMeta": {
+ "@date-fns/tz": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ },
+ "date-fns": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@base-ui/utils": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.4.0.tgz",
+ "integrity": "sha512-bO9fz25kKtPf+aZVyfQrC0PDmJdmVni31W2hCS5/Owb+inwdIL3XU26pCPRPlt4LSxZrBgLwubXQXQlKaFEZzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.7",
+ "@floating-ui/utils": "^0.2.12",
+ "reselect": "^5.2.0",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^17 || ^18 || ^19",
+ "react": "^17 || ^18 || ^19",
+ "react-dom": "^17 || ^18 || ^19"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@crxjs/vite-plugin": {
"version": "2.0.0-beta.26",
"resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.0.0-beta.26.tgz",
@@ -522,78 +752,321 @@
"node": ">=18"
}
},
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=12"
+ "node_modules/@dotenvx/dotenvx": {
+ "version": "1.75.1",
+ "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.75.1.tgz",
+ "integrity": "sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@dotenvx/primitives": "^0.8.0",
+ "commander": "^11.1.0",
+ "conf": "^10.2.0",
+ "dotenv": "^17.2.1",
+ "enquirer": "^2.4.1",
+ "env-paths": "^2.2.1",
+ "execa": "^5.1.1",
+ "fdir": "^6.2.0",
+ "ignore": "^5.3.0",
+ "object-treeify": "1.1.33",
+ "open": "^8.4.2",
+ "picomatch": "^4.0.4",
+ "systeminformation": "^5.22.11",
+ "undici": "^7.11.0",
+ "which": "^4.0.0",
+ "yocto-spinner": "^1.1.0"
+ },
+ "bin": {
+ "dotenvx": "src/cli/dotenvx.js"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
}
},
- "node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
+ "node_modules/@dotenvx/dotenvx/node_modules/commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
"engines": {
- "node": ">=12"
+ "node": ">=16"
}
},
- "node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/@dotenvx/dotenvx/node_modules/define-lazy-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
"engines": {
- "node": ">=12"
+ "node": ">=8"
}
},
- "node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
+ "node_modules/@dotenvx/dotenvx/node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
"engines": {
- "node": ">=12"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "node_modules/@dotenvx/dotenvx/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/isexe": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
+ "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/open": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@dotenvx/dotenvx/node_modules/which": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+ "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@dotenvx/primitives": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@dotenvx/primitives/-/primitives-0.8.0.tgz",
+ "integrity": "sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
@@ -1017,6 +1490,74 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
+ "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
+ "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.8.0",
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz",
+ "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.8.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.12",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
+ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
+ "license": "MIT"
+ },
+ "node_modules/@fontsource-variable/inter": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz",
+ "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@fontsource/geist-mono": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/geist-mono/-/geist-mono-5.3.0.tgz",
+ "integrity": "sha512-UtJ1BBBCVpMYdIcW7nEB45UAoAw5M53ZXs2t0ciPW+IokuAAIc56M8+kW5tXbRJCTpDw4XTtU7proT6NdQAHTg==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@hono/node-server": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.3.tgz",
+ "integrity": "sha512-TA//nWMqPhbfdfneACk6t5a9eqbS9lABEPyKn0/xZTah3H3U2XaVg85rJFl0/Fyit0I552YDHgXGVSf3GwqbUw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -1090,7 +1631,6 @@
"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",
@@ -1101,7 +1641,6 @@
"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",
@@ -1112,7 +1651,6 @@
"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"
@@ -1122,25 +1660,84 @@
"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/@modelcontextprotocol/sdk": {
+ "version": "1.32.1",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz",
+ "integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9 || ^2.0.5",
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.2.1",
+ "express-rate-limit": "^8.2.1",
+ "hono": "^4.11.4",
+ "jose": "^6.1.3",
+ "json-schema-typed": "^8.0.2",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.25 || ^4.0",
+ "zod-to-json-schema": "^3.25.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "@cfworker/json-schema": {
+ "optional": true
+ },
+ "zod": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
"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",
@@ -1154,7 +1751,6 @@
"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"
@@ -1164,7 +1760,6 @@
"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",
@@ -1545,7 +2140,379 @@
"win32"
]
},
- "node_modules/@testing-library/dom": {
+ "node_modules/@sec-ant/readable-stream": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
+ "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
+ "license": "MIT"
+ },
+ "node_modules/@shadcn/registry": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@shadcn/registry/-/registry-0.1.2.tgz",
+ "integrity": "sha512-yN+z+I9lBPI0b5DOr5IwrC4E1kkoSMkdO3M6pNALz07c0if9zFE/cH+Ekfp7AT93FBN0d49rSiVyKwqIAsrEsg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/parser": "^7.28.0",
+ "@babel/plugin-transform-typescript": "^7.28.0",
+ "@babel/preset-typescript": "^7.27.1",
+ "@dotenvx/dotenvx": "^1.48.4",
+ "cn": "^0.2.4",
+ "deepmerge": "^4.3.1",
+ "execa": "^9.6.0",
+ "fast-glob": "^3.3.3",
+ "fs-extra": "^11.3.1",
+ "fuzzysort": "^3.1.0",
+ "kleur": "^4.1.5",
+ "ora": "^8.2.0",
+ "postcss": "^8.5.23",
+ "prompts": "^2.4.2",
+ "recast": "^0.23.11",
+ "socks": "^2.8.8",
+ "stringify-object": "^5.0.0",
+ "ts-morph": "^26.0.0",
+ "tsconfig-paths": "^4.2.0",
+ "type-fest": "^4.41.0",
+ "undici": "^7.29.0",
+ "zod": "^3.24.1"
+ },
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/@shadcn/registry/node_modules/cn": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/cn/-/cn-0.2.6.tgz",
+ "integrity": "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ==",
+ "license": "MIT",
+ "bin": {
+ "cn": "bin/cn.mjs"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shadcn/registry/node_modules/fs-extra": {
+ "version": "11.4.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz",
+ "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/@shadcn/registry/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sindresorhus/merge-streams": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
+ "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
+ "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.24.1",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/node/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/@tailwindcss/oxide": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
+ "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-x64": "4.3.3",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.3",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.3",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.3",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz",
+ "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
+ "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz",
+ "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz",
+ "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz",
+ "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz",
+ "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz",
+ "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz",
+ "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz",
+ "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz",
+ "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
+ "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz",
+ "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz",
+ "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.3",
+ "@tailwindcss/oxide": "4.3.3",
+ "tailwindcss": "4.3.3"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
@@ -1635,6 +2602,17 @@
"@testing-library/dom": ">=7.21.4"
}
},
+ "node_modules/@ts-morph/common": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz",
+ "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-glob": "^3.3.3",
+ "minimatch": "^10.0.1",
+ "path-browserify": "^1.0.1"
+ }
+ },
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
@@ -1730,34 +2708,32 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/prop-types": {
- "version": "15.7.15",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
- "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
- "devOptional": true,
- "license": "MIT"
- },
"node_modules/@types/react": {
- "version": "18.3.28",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
- "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
+ "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
- "version": "18.3.7",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
- "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
+ "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "@types/react": "^18.0.0"
+ "@types/react": "^19.3.0"
}
},
+ "node_modules/@types/validate-npm-package-name": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz",
+ "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz",
@@ -2179,6 +3155,44 @@
"dev": true,
"license": "BSD-3-Clause"
},
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -2242,13 +3256,60 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"license": "MIT",
- "engines": {
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
"node": ">=8"
}
},
@@ -2433,6 +3494,18 @@
"node": ">=12"
}
},
+ "node_modules/ast-types": {
+ "version": "0.16.3",
+ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.3.tgz",
+ "integrity": "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/ast-types-flow": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
@@ -2457,6 +3530,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/atomically": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
+ "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -2497,7 +3579,6 @@
"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"
@@ -2507,7 +3588,6 @@
"version": "2.10.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",
"integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==",
- "dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -2516,6 +3596,59 @@
"node": ">=6.0.0"
}
},
+ "node_modules/body-parser": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
+ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^2.0.0",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.1",
+ "iconv-lite": "^0.7.2",
+ "on-finished": "^2.4.1",
+ "qs": "^6.15.2",
+ "raw-body": "^3.0.2",
+ "type-is": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/body-parser/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -2527,7 +3660,6 @@
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -2540,7 +3672,6 @@
"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"
@@ -2553,7 +3684,6 @@
"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",
@@ -2583,6 +3713,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/bundle-name": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz",
+ "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==",
+ "license": "MIT",
+ "dependencies": {
+ "run-applescript": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -2616,7 +3770,6 @@
"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",
@@ -2630,7 +3783,6 @@
"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",
@@ -2657,7 +3809,6 @@
"version": "1.0.30001788",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
"integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -2762,6 +3913,72 @@
"url": "https://github.com/sponsors/fb55"
}
},
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cn": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cn/-/cn-0.4.0.tgz",
+ "integrity": "sha512-qOAeUhPwPzBxx6jaeQdo646XIKliKHc2j1ZF7Ngb1o0pJZKH5gq8FkA0aVdUdPFUDXNXyUCXpCtoJISo6dhizg==",
+ "license": "MIT",
+ "bin": {
+ "cn": "bin/cn.mjs"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/code-block-writer": {
+ "version": "13.0.3",
+ "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
+ "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",
+ "license": "MIT"
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2795,6 +4012,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2802,6 +4028,75 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/conf": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz",
+ "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.6.3",
+ "ajv-formats": "^2.1.1",
+ "atomically": "^1.7.0",
+ "debounce-fn": "^4.0.0",
+ "dot-prop": "^6.0.1",
+ "env-paths": "^2.2.1",
+ "json-schema-typed": "^7.0.3",
+ "onetime": "^5.1.2",
+ "pkg-up": "^3.1.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/conf/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/conf/node_modules/ajv-formats": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/conf/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/conf/node_modules/json-schema-typed": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz",
+ "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/connect-injector": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/connect-injector/-/connect-injector-0.4.4.tgz",
@@ -2834,6 +4129,28 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
@@ -2841,11 +4158,45 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"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",
@@ -2893,6 +4244,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
@@ -2996,11 +4359,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/debounce-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz",
+ "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"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"
@@ -3021,6 +4398,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/dedent": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
+ "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
@@ -3038,24 +4429,73 @@
"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,
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
- "dependencies": {
- "es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "gopd": "^1.0.1"
- },
"engines": {
- "node": ">= 0.4"
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/default-browser": {
+ "version": "5.5.1",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz",
+ "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==",
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
+ "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
@@ -3084,6 +4524,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -3094,6 +4543,25 @@
"node": ">=6"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz",
+ "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -3174,11 +4642,37 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
+ "node_modules/dot-prop": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
+ "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-obj": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"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",
@@ -3189,11 +4683,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.339",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.339.tgz",
"integrity": "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==",
- "dev": true,
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -3203,6 +4702,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/encoding-sniffer": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
@@ -3217,6 +4725,33 @@
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
+ "node_modules/enhanced-resolve": {
+ "version": "5.26.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz",
+ "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/enquirer": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
+ "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^4.1.1",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -3230,6 +4765,15 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/es-abstract": {
"version": "1.24.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
@@ -3303,7 +4847,6 @@
"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"
@@ -3313,7 +4856,6 @@
"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"
@@ -3358,7 +4900,6 @@
"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"
@@ -3457,12 +4998,17 @@
"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-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -3793,6 +5339,19 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/esquery": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
@@ -3846,6 +5405,62 @@
"node": ">=0.10.0"
}
},
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
+ "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/execa": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
+ "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/merge-streams": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "figures": "^6.1.0",
+ "get-stream": "^9.0.0",
+ "human-signals": "^8.0.1",
+ "is-plain-obj": "^4.1.0",
+ "is-stream": "^4.0.1",
+ "npm-run-path": "^6.0.0",
+ "pretty-ms": "^9.2.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^4.0.0",
+ "yoctocolors": "^2.1.1"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
@@ -3856,18 +5471,103 @@
"node": ">=12.0.0"
}
},
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "8.7.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.1.tgz",
+ "integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "ip-address": "^10.2.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
+ "node_modules/express/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"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",
@@ -3884,7 +5584,6 @@
"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"
@@ -3907,16 +5606,46 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-uri": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
+ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"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/figures": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
+ "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-unicode-supported": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -3934,7 +5663,6 @@
"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"
@@ -3943,6 +5671,27 @@
"node": ">=8"
}
},
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -4015,6 +5764,24 @@
"node": ">= 6"
}
},
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -4056,7 +5823,6 @@
"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"
@@ -4093,6 +5859,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/fuzzysort": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz",
+ "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==",
+ "license": "MIT"
+ },
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
@@ -4107,17 +5879,27 @@
"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-east-asian-width": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz",
+ "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"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",
@@ -4138,11 +5920,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-own-enumerable-keys": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz",
+ "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"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",
@@ -4152,9 +5945,25 @@
"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",
+ "node_modules/get-stream": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
+ "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sec-ant/readable-stream": "^0.4.1",
+ "is-stream": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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",
@@ -4273,7 +6082,6 @@
"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"
@@ -4286,7 +6094,6 @@
"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/graphemer": {
@@ -4352,7 +6159,6 @@
"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"
@@ -4381,7 +6187,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -4390,6 +6195,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/hono": {
+ "version": "4.13.13",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz",
+ "integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
@@ -4436,6 +6250,26 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -4464,6 +6298,15 @@
"node": ">= 14"
}
},
+ "node_modules/human-signals": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
+ "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -4540,7 +6383,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true,
"license": "ISC"
},
"node_modules/internal-slot": {
@@ -4558,6 +6400,24 @@
"node": ">= 0.4"
}
},
+ "node_modules/ip-address": {
+ "version": "10.7.3",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz",
+ "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -4693,11 +6553,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"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"
@@ -4743,7 +6617,6 @@
"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"
@@ -4752,6 +6625,48 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-in-ssh": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
+ "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+ "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
@@ -4782,7 +6697,6 @@
"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"
@@ -4805,6 +6719,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
@@ -4815,6 +6738,18 @@
"node": ">=8"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -4822,6 +6757,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -4841,6 +6782,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-regexp": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz",
+ "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-set": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
@@ -4870,6 +6823,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-stream": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
+ "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-string": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
@@ -4921,6 +6886,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-unicode-supported": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+ "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-weakmap": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@@ -4967,6 +6944,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-wsl": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
+ "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -4978,7 +6970,6 @@
"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/iterator.prototype": {
@@ -4999,6 +6990,25 @@
"node": ">= 0.4"
}
},
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/jose": {
+ "version": "6.2.12",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
+ "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -5063,7 +7073,6 @@
"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"
@@ -5086,6 +7095,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-schema-typed": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
+ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
+ "license": "BSD-2-Clause"
+ },
"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",
@@ -5097,7 +7112,6 @@
"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"
@@ -5110,7 +7124,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
@@ -5145,6 +7158,15 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -5179,6 +7201,267 @@
"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,
+ "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,
+ "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,
+ "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,
+ "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",
@@ -5202,10 +7485,51 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/log-symbols": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
+ "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.3.0",
+ "is-unicode-supported": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/log-symbols/node_modules/is-unicode-supported": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+ "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"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"
@@ -5225,12 +7549,26 @@
"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/lucide-react": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.52.0.tgz",
+ "integrity": "sha512-TUgPtl5ZI9WgxOcbu4ckaC5B3l1qxPQrCMgb6OfUB9owx/OvSJfcf93SMgUWAxRw5/1XDfh9uI5F5iNGXCcGDQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -5259,17 +7597,46 @@
"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/media-typer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
+ "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "license": "MIT"
+ },
"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"
@@ -5279,7 +7646,6 @@
"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",
@@ -5312,6 +7678,27 @@
"node": ">= 0.6"
}
},
+ "node_modules/mimic-fn": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
+ "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -5326,7 +7713,6 @@
"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"
@@ -5338,18 +7724,25 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"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.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "dev": true,
+ "version": "3.3.20",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
+ "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
"funding": [
{
"type": "github",
@@ -5371,6 +7764,35 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/negotiator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
+ "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/negotiator/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/node-exports-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
@@ -5404,9 +7826,36 @@
"version": "2.0.37",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/npm-run-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
+ "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^4.0.0",
+ "unicorn-magic": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -5431,7 +7880,6 @@
"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"
@@ -5441,7 +7889,6 @@
"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"
@@ -5460,6 +7907,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/object-treeify": {
+ "version": "1.1.33",
+ "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz",
+ "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/object.assign": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
@@ -5535,16 +7991,71 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/onetime/node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/open": {
+ "version": "11.0.4",
+ "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz",
+ "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==",
+ "license": "MIT",
+ "dependencies": {
+ "default-browser": "^5.5.1",
+ "define-lazy-prop": "^3.0.0",
+ "is-in-ssh": "^1.0.0",
+ "is-inside-container": "^1.0.0",
+ "powershell-utils": "^0.2.1",
+ "wsl-utils": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -5563,6 +8074,68 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/ora": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
+ "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.3.0",
+ "cli-cursor": "^5.0.0",
+ "cli-spinners": "^2.9.2",
+ "is-interactive": "^2.0.0",
+ "is-unicode-supported": "^2.0.0",
+ "log-symbols": "^6.0.0",
+ "stdin-discarder": "^0.2.2",
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/ansi-regex": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz",
+ "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ora/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/ora/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
"node_modules/own-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -5613,6 +8186,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -5626,6 +8208,18 @@
"node": ">=6"
}
},
+ "node_modules/parse-ms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
+ "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -5679,6 +8273,21 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "license": "MIT"
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5703,7 +8312,6 @@
"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"
@@ -5716,6 +8324,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
@@ -5737,14 +8355,12 @@
"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"
@@ -5753,6 +8369,88 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pkce-challenge": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/pkg-up": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
+ "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-up/node_modules/find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pkg-up/node_modules/locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pkg-up/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-up/node_modules/p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pkg-up/node_modules/path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"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",
@@ -5764,10 +8462,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.10",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
- "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
- "dev": true,
+ "version": "8.5.29",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz",
+ "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==",
"funding": [
{
"type": "opencollective",
@@ -5784,14 +8481,39 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.19",
"picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
+ "source-map-js": "^1.2.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postcss-selector-parser": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz",
+ "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/powershell-utils": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz",
+ "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5848,6 +8570,43 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/pretty-ms": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz",
+ "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==",
+ "license": "MIT",
+ "dependencies": {
+ "parse-ms": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prompts/node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -5867,6 +8626,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/proxy-addr": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
+ "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -5889,11 +8665,26 @@
"teleport": ">=0.2.0"
}
},
+ "node_modules/qs": {
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
+ "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"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",
@@ -5910,29 +8701,69 @@
],
"license": "MIT"
},
- "node_modules/react": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "node_modules/range-parser": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
+ "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
- "loose-envify": "^1.1.0"
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
},
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
+ "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
+ "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
"license": "MIT",
"dependencies": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
+ "scheduler": "^0.28.0"
},
"peerDependencies": {
- "react": "^18.3.1"
+ "react": "^19.3.0"
}
},
"node_modules/react-is": {
@@ -5953,6 +8784,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/recast": {
+ "version": "0.23.21",
+ "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz",
+ "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==",
+ "license": "MIT",
+ "dependencies": {
+ "ast-types": "^0.16.1",
+ "esprima": "~4.0.0",
+ "source-map": "~0.6.1",
+ "tiny-invariant": "^1.3.3",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@@ -6011,6 +8858,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/reselect": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz",
+ "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "2.0.0-next.6",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz",
@@ -6045,11 +8907,41 @@
"node": ">=4"
}
},
+ "node_modules/restore-cursor": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/onetime": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-function": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"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",
@@ -6089,6 +8981,22 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/rrweb-cssom": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
@@ -6096,11 +9004,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/run-applescript": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+ "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"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",
@@ -6189,7 +9108,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true,
"license": "MIT"
},
"node_modules/saxes": {
@@ -6206,19 +9124,15 @@
}
},
"node_modules/scheduler": {
- "version": "0.23.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
- "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.1.0"
- }
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
+ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
+ "license": "MIT"
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
- "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -6227,6 +9141,76 @@
"node": ">=10"
}
},
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/send/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/send/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -6276,11 +9260,89 @@
"node": ">= 0.4"
}
},
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shadcn": {
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.21.3.tgz",
+ "integrity": "sha512-r88yKaFvIwz2DInrE/DZn0XZVSuKWahouQVJBjYxVAcZ3GRkfh+3VDhIeCc18eYiIOuMgZoGaPYjhzhaON2mzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/parser": "^7.28.0",
+ "@babel/plugin-transform-typescript": "^7.28.0",
+ "@babel/preset-typescript": "^7.27.1",
+ "@dotenvx/dotenvx": "^1.48.4",
+ "@modelcontextprotocol/sdk": "^1.26.0",
+ "@shadcn/registry": "0.1.2",
+ "@types/validate-npm-package-name": "^4.0.2",
+ "browserslist": "^4.26.2",
+ "cn": "^0.2.4",
+ "commander": "^14.0.0",
+ "dedent": "^1.6.0",
+ "deepmerge": "^4.3.1",
+ "diff": "^8.0.2",
+ "execa": "^9.6.0",
+ "fast-glob": "^3.3.3",
+ "fs-extra": "^11.3.1",
+ "fuzzysort": "^3.1.0",
+ "kleur": "^4.1.5",
+ "open": "^11.0.0",
+ "ora": "^8.2.0",
+ "postcss": "^8.5.23",
+ "postcss-selector-parser": "^7.1.3",
+ "prompts": "^2.4.2",
+ "recast": "^0.23.11",
+ "socks": "^2.8.8",
+ "stringify-object": "^5.0.0",
+ "ts-morph": "^26.0.0",
+ "tsconfig-paths": "^4.2.0",
+ "undici": "^7.29.0",
+ "validate-npm-package-name": "^7.0.1",
+ "zod": "^3.24.1",
+ "zod-to-json-schema": "^3.24.6"
+ },
+ "bin": {
+ "shadcn": "dist/index.js"
+ },
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/shadcn/node_modules/cn": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/cn/-/cn-0.2.6.tgz",
+ "integrity": "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ==",
+ "license": "MIT",
+ "bin": {
+ "cn": "bin/cn.mjs"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/shadcn/node_modules/fs-extra": {
+ "version": "11.4.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz",
+ "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
"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"
@@ -6293,22 +9355,20 @@
"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,
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
@@ -6323,7 +9383,6 @@
"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",
@@ -6340,7 +9399,6 @@
"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",
@@ -6359,7 +9417,6 @@
"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",
@@ -6369,24 +9426,74 @@
"side-channel-map": "^1.0.1"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "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/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "license": "MIT"
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.10",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz",
+ "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^10.1.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
}
},
- "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": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
},
"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,
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -6407,6 +9514,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/std-env": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
@@ -6414,6 +9530,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/stdin-discarder": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
+ "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -6437,6 +9565,56 @@
"node": ">= 0.3.0"
}
},
+ "node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz",
+ "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/string-width/node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "license": "MIT"
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -6550,11 +9728,39 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/stringify-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz",
+ "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "get-own-enumerable-keys": "^1.0.0",
+ "is-obj": "^3.0.0",
+ "is-regexp": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/yeoman/stringify-object?sponsor=1"
+ }
+ },
+ "node_modules/stringify-object/node_modules/is-obj": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz",
+ "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@@ -6563,6 +9769,27 @@
"node": ">=8"
}
},
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
+ "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -6622,6 +9849,53 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/systeminformation": {
+ "version": "5.33.15",
+ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.15.tgz",
+ "integrity": "sha512-s5RloL0F7xhN0ZjfHtLGnbf7EW74EI0N8NFL+1xR7Oxz9QA33obNZsl7lyfnJOhN04E/kGFPoJPlvY7TZ5fzmw==",
+ "license": "MIT",
+ "os": [
+ "darwin",
+ "linux",
+ "win32",
+ "freebsd",
+ "openbsd",
+ "netbsd",
+ "sunos",
+ "android"
+ ],
+ "bin": {
+ "systeminformation": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "funding": {
+ "type": "Buy me a coffee",
+ "url": "https://www.buymeacoffee.com/systeminfo"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
+ "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -6629,6 +9903,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -6745,7 +10025,6 @@
"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"
@@ -6754,6 +10033,15 @@
"node": ">=8.0"
}
},
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/tough-cookie": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
@@ -6793,13 +10081,45 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/ts-morph": {
+ "version": "26.0.0",
+ "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz",
+ "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@ts-morph/common": "~0.27.0",
+ "code-block-writer": "^13.0.3"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
+ "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
+ "license": "MIT",
+ "dependencies": {
+ "json5": "^2.2.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"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/tw-animate-css": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
+ "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Wombosvideo"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -6826,6 +10146,62 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/type-is/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -6947,30 +10323,48 @@
}
},
"node_modules/undici": {
- "version": "7.25.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
- "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
- "dev": true,
+ "version": "7.30.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
+ "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
+ "node_modules/unicorn-magic": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"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/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"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",
@@ -7016,6 +10410,30 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/validate-npm-package-name": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz",
+ "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
@@ -7292,7 +10710,6 @@
"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"
@@ -7424,7 +10841,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true,
"license": "ISC"
},
"node_modules/ws": {
@@ -7449,6 +10865,22 @@
}
}
},
+ "node_modules/wsl-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz",
+ "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-wsl": "^3.1.0",
+ "powershell-utils": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
@@ -7470,7 +10902,6 @@
"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": {
@@ -7486,6 +10917,51 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/yocto-spinner": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.2.tgz",
+ "integrity": "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng==",
+ "license": "MIT",
+ "dependencies": {
+ "yoctocolors": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=18.19"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yoctocolors": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz",
+ "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.25.2",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
+ "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.25.28 || ^4"
+ }
+ },
"node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
diff --git a/package.json b/package.json
index 7e6af30..1bec4c5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "querycraft",
- "version": "1.0.8",
+ "version": "1.0.10",
"description": "Inspect, edit, save, and replay URL query parameters on the active tab.",
"private": true,
"type": "module",
@@ -14,18 +14,27 @@
"test:ui": "vitest --ui"
},
"dependencies": {
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
+ "@base-ui/react": "^1.8.0",
+ "@fontsource-variable/inter": "^5.3.0",
+ "@fontsource/geist-mono": "^5.3.0",
+ "class-variance-authority": "^0.7.1",
+ "cn": "^0.4.0",
+ "lucide-react": "^1.52.0",
+ "react": "^19.3.0",
+ "react-dom": "^19.3.0",
+ "shadcn": "^4.21.3",
+ "tw-animate-css": "^1.4.0",
"zustand": "^4.5.5"
},
"devDependencies": {
"@crxjs/vite-plugin": "2.0.0-beta.26",
+ "@tailwindcss/vite": "^4.3.3",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/chrome": "^0.0.270",
- "@types/react": "^18.3.11",
- "@types/react-dom": "^18.3.0",
+ "@types/react": "^19.3.0",
+ "@types/react-dom": "^19.3.0",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"@vitejs/plugin-react": "^4.3.2",
@@ -35,6 +44,7 @@
"eslint-plugin-react-hooks": "^4.6.2",
"jsdom": "^25.0.1",
"prettier": "^3.3.3",
+ "tailwindcss": "^4.3.3",
"typescript": "^5.6.2",
"vite": "^5.4.8",
"vitest": "^2.1.2"
diff --git a/public/theme-boot.js b/public/theme-boot.js
new file mode 100644
index 0000000..b536273
--- /dev/null
+++ b/public/theme-boot.js
@@ -0,0 +1,26 @@
+/*
+ * Applies the saved theme and accent before first paint (docs/redesign §4.5).
+ * chrome.storage is async, so the popup would flash the wrong scheme; this
+ * reads the synchronous localStorage mirror that useTheme / useAccent keep.
+ * Loaded as a file, not inline: the MV3 content security policy blocks inline scripts.
+ */
+try {
+ var root = document.documentElement;
+ var preference = localStorage.getItem('qc-theme');
+ var dark =
+ preference === 'dark' ||
+ (preference !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
+ var scheme = dark ? 'dark' : 'light';
+ root.classList.toggle('dark', dark);
+ root.setAttribute('data-theme', scheme);
+ root.style.colorScheme = scheme;
+
+ var accent = JSON.parse(localStorage.getItem('qc-accent') || 'null');
+ if (accent) {
+ root.style.setProperty('--primary', accent.primary);
+ root.style.setProperty('--primary-foreground', accent.primaryForeground);
+ root.style.setProperty('--key-foreground', accent.keyForeground[scheme]);
+ }
+} catch (e) {
+ // Storage blocked or malformed: the React hooks apply the theme a moment later.
+}
diff --git a/src/__tests__/ActionBar.test.tsx b/src/__tests__/ActionBar.test.tsx
new file mode 100644
index 0000000..452022c
--- /dev/null
+++ b/src/__tests__/ActionBar.test.tsx
@@ -0,0 +1,28 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { ActionBar } from '@/components/ActionBar';
+
+function renderBar(props: Partial[0]> = {}) {
+ render(
+ ,
+ );
+}
+
+describe('ActionBar', () => {
+ it('names Apply without its shortcut glyph, which aria-keyshortcuts carries', () => {
+ renderBar();
+ const apply = screen.getByRole('button', { name: 'Apply' });
+ expect(apply.getAttribute('aria-keyshortcuts')).toMatch(/^(Meta|Control)\+Enter$/);
+ });
+
+ it('swaps the Copy label to Copied while the copy is confirmed', () => {
+ renderBar({ copied: true });
+ expect(screen.getByRole('button', { name: 'Copied' })).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Copy' })).not.toBeInTheDocument();
+ });
+
+ it('disables Apply when there is nothing to apply', () => {
+ renderBar({ applyDisabled: true });
+ expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
+ });
+});
diff --git a/src/__tests__/GroupCombobox.test.tsx b/src/__tests__/GroupCombobox.test.tsx
new file mode 100644
index 0000000..06fe2e1
--- /dev/null
+++ b/src/__tests__/GroupCombobox.test.tsx
@@ -0,0 +1,40 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { GroupCombobox } from '@/components/SavedLinksDrawer/GroupCombobox';
+import type { Group } from '@/types';
+
+const groups: Group[] = [
+ { id: 'default', name: 'Unsorted', createdAt: 0 },
+ { id: 'g2', name: 'Staging', createdAt: 1 },
+];
+
+function renderCombobox() {
+ const onChange = vi.fn();
+ const onCreate = vi.fn();
+ render( );
+ return { input: screen.getByRole('combobox'), onChange, onCreate };
+}
+
+describe('GroupCombobox', () => {
+ it('offers to create a group when nothing matches exactly', async () => {
+ const { input, onCreate } = renderCombobox();
+
+ await userEvent.clear(input);
+ await userEvent.type(input, 'Bug repros');
+ await userEvent.click(await screen.findByRole('option', { name: 'Create “Bug repros”' }));
+
+ expect(onCreate).toHaveBeenCalledWith('Bug repros');
+ });
+
+ it('reuses an existing group case-insensitively instead of offering a duplicate', async () => {
+ const { input, onChange } = renderCombobox();
+
+ await userEvent.clear(input);
+ await userEvent.type(input, 'staging');
+
+ expect(screen.queryByRole('option', { name: /Create/ })).not.toBeInTheDocument();
+ await userEvent.click(screen.getByRole('option', { name: 'Staging' }));
+ expect(onChange).toHaveBeenCalledWith('g2');
+ });
+});
diff --git a/src/__tests__/GroupedLinksList.test.tsx b/src/__tests__/GroupedLinksList.test.tsx
index 7d71b8c..c110c2d 100644
--- a/src/__tests__/GroupedLinksList.test.tsx
+++ b/src/__tests__/GroupedLinksList.test.tsx
@@ -4,42 +4,106 @@ import userEvent from '@testing-library/user-event';
import { GroupedLinksList } from '@/components/SavedLinksDrawer/GroupedLinksList';
import type { Group, SavedLink } from '@/types';
-const group: Group = { id: 'g1', name: 'Unsorted', createdAt: 0 };
-const link: SavedLink = {
+const groups: Group[] = [
+ { id: 'g1', name: 'Unsorted', createdAt: 0 },
+ { id: 'g2', name: 'Staging', createdAt: 1 },
+];
+const labelled: SavedLink = {
id: 'l1',
url: 'https://example.com/?foo=bar',
label: 'Example',
createdAt: 0,
groupId: 'g1',
};
+const bare: SavedLink = {
+ id: 'l2',
+ url: 'https://staging.example.com/app?debug=true',
+ createdAt: 1,
+ groupId: 'g2',
+};
-function renderList(onLoadLink = vi.fn()) {
+function renderList() {
+ const handlers = {
+ onLoadLink: vi.fn(),
+ onCopyLink: vi.fn(),
+ onDeleteLink: vi.fn(),
+ onStartEdit: vi.fn(),
+ };
render(
,
);
- return onLoadLink;
+ return handlers;
}
describe('GroupedLinksList', () => {
- it('loads the saved URL when its name is clicked', async () => {
- const onLoadLink = renderList();
+ it('loads the saved URL when its row is clicked', async () => {
+ const { onLoadLink } = renderList();
await userEvent.click(screen.getByText('Example'));
expect(onLoadLink).toHaveBeenCalledWith('https://example.com/?foo=bar');
});
- it('exposes the name as a button so it can be reached by keyboard', () => {
+ it('shows URLs without their protocol', () => {
+ renderList();
+ expect(screen.getByText('example.com/?foo=bar')).toBeInTheDocument();
+ expect(screen.getByText('staging.example.com/app?debug=true')).toBeInTheDocument();
+ });
+
+ it('deletes a focused row with Backspace', async () => {
+ const { onDeleteLink } = renderList();
+ screen.getByText('Example').closest('button')!.focus();
+
+ await userEvent.keyboard('{Backspace}');
+
+ expect(onDeleteLink).toHaveBeenCalledWith(labelled);
+ });
+
+ it('filters by label, URL and group name', async () => {
+ renderList();
+ const filter = screen.getByRole('searchbox', { name: 'Filter saved URLs' });
+
+ await userEvent.type(filter, 'staging');
+ expect(screen.queryByText('Example')).not.toBeInTheDocument();
+ expect(screen.getByText('staging.example.com/app?debug=true')).toBeInTheDocument();
+
+ await userEvent.clear(filter);
+ await userEvent.type(filter, 'unsorted');
+ expect(screen.getByText('Example')).toBeInTheDocument();
+ });
+
+ it('says so when nothing matches the filter', async () => {
renderList();
+ await userEvent.type(screen.getByRole('searchbox', { name: 'Filter saved URLs' }), 'zzz');
+ expect(screen.getByText('No saved URLs match “zzz”')).toBeInTheDocument();
+ });
+
+ it('moves focus to the next row after Delete from the row menu', async () => {
+ const { onDeleteLink } = renderList();
+
+ await userEvent.click(screen.getByRole('button', { name: 'Actions for Example' }));
+ await userEvent.click(await screen.findByRole('menuitem', { name: /Delete/ }));
+ await new Promise((r) => setTimeout(r, 10));
+
+ expect(onDeleteLink).toHaveBeenCalledWith(labelled);
+ expect(document.activeElement).toHaveTextContent('staging.example.com/app?debug=true');
+ });
+
+ it('offers the row actions in a menu named after the link', async () => {
+ const { onCopyLink } = renderList();
+
+ await userEvent.click(screen.getByRole('button', { name: 'Actions for Example' }));
+ await userEvent.click(await screen.findByRole('menuitem', { name: 'Copy URL' }));
- expect(screen.getByRole('button', { name: /^Example/ })).toBeInTheDocument();
+ expect(onCopyLink).toHaveBeenCalledWith('https://example.com/?foo=bar');
});
});
diff --git a/src/__tests__/LiveRegion.test.tsx b/src/__tests__/LiveRegion.test.tsx
new file mode 100644
index 0000000..904bb1c
--- /dev/null
+++ b/src/__tests__/LiveRegion.test.tsx
@@ -0,0 +1,35 @@
+import { describe, it, expect, vi, afterEach } from 'vitest';
+import { act, render, screen } from '@testing-library/react';
+import { LiveRegion, announce } from '@/components/LiveRegion';
+
+afterEach(() => {
+ vi.useRealTimers();
+});
+
+describe('LiveRegion', () => {
+ it('announces a message in a polite, visually hidden status region', () => {
+ vi.useFakeTimers();
+ render( );
+
+ act(() => announce('Added utm_source'));
+ act(() => vi.advanceTimersByTime(50));
+
+ const region = screen.getByRole('status');
+ expect(region).toHaveAttribute('aria-live', 'polite');
+ expect(region).toHaveClass('sr-only');
+ expect(region).toHaveTextContent('Added utm_source');
+ });
+
+ it('re-announces the same message: cleared first, then set again', () => {
+ vi.useFakeTimers();
+ render( );
+ act(() => announce('3 of 7 parameters'));
+ act(() => vi.advanceTimersByTime(50));
+
+ act(() => announce('3 of 7 parameters'));
+ expect(screen.getByRole('status')).toHaveTextContent('');
+
+ act(() => vi.advanceTimersByTime(50));
+ expect(screen.getByRole('status')).toHaveTextContent('3 of 7 parameters');
+ });
+});
diff --git a/src/__tests__/contrast.test.ts b/src/__tests__/contrast.test.ts
index 4c9c884..c982d31 100644
--- a/src/__tests__/contrast.test.ts
+++ b/src/__tests__/contrast.test.ts
@@ -95,8 +95,8 @@ describe('withAlpha', () => {
});
describe('ACCENTS', () => {
- it('has the ten swatches from the handoff', () => {
- expect(ACCENTS).toHaveLength(10);
+ it('has the ten handoff swatches plus Fuchsia', () => {
+ expect(ACCENTS).toHaveLength(11);
});
it('uses lowercase 6-digit hex so storage comparisons are stable', () => {
diff --git a/src/__tests__/popupHeight.test.ts b/src/__tests__/popupHeight.test.ts
new file mode 100644
index 0000000..20a5a43
--- /dev/null
+++ b/src/__tests__/popupHeight.test.ts
@@ -0,0 +1,43 @@
+import { describe, it, expect, vi, afterEach } from 'vitest';
+import { syncPopupHeight, POPUP_MAX_HEIGHT } from '@/popup/popupHeight';
+
+// jsdom has no ResizeObserver: capture the callback and feed it entries by hand.
+let resize: (blockSize: number) => void = () => {};
+class FakeResizeObserver {
+ constructor(cb: ResizeObserverCallback) {
+ resize = (blockSize) =>
+ cb(
+ [{ borderBoxSize: [{ blockSize, inlineSize: 380 }] } as unknown as ResizeObserverEntry],
+ this as never,
+ );
+ }
+ observe() {}
+ disconnect() {}
+}
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+ document.documentElement.style.height = '';
+ document.body.style.height = '';
+});
+
+describe('syncPopupHeight', () => {
+ it('sizes html and body to the content, rounded up', () => {
+ vi.stubGlobal('ResizeObserver', FakeResizeObserver);
+ syncPopupHeight(document.createElement('div'));
+
+ resize(357.2);
+
+ expect(document.documentElement.style.height).toBe('358px');
+ expect(document.body.style.height).toBe('358px');
+ });
+
+ it("caps the height at Chrome's popup maximum", () => {
+ vi.stubGlobal('ResizeObserver', FakeResizeObserver);
+ syncPopupHeight(document.createElement('div'));
+
+ resize(912);
+
+ expect(document.body.style.height).toBe(`${POPUP_MAX_HEIGHT}px`);
+ });
+});
diff --git a/src/__tests__/storage.test.ts b/src/__tests__/storage.test.ts
index 816e2d8..3fb695c 100644
--- a/src/__tests__/storage.test.ts
+++ b/src/__tests__/storage.test.ts
@@ -110,3 +110,19 @@ describe('storage.setAccent', () => {
expect(await storage.getAccent()).toBeNull();
});
});
+
+describe('storage theme preference', () => {
+ it("defaults to 'system' when nothing is stored", async () => {
+ expect(await storage.getThemePreference()).toBe('system');
+ });
+
+ it('persists and retrieves a preference', async () => {
+ await storage.setThemePreference('dark');
+ expect(await storage.getThemePreference()).toBe('dark');
+ });
+
+ it("falls back to 'system' for an unknown stored value", async () => {
+ await chrome.storage.local.set({ themePreference: 'sepia' });
+ expect(await storage.getThemePreference()).toBe('system');
+ });
+});
diff --git a/src/__tests__/useAccent.test.ts b/src/__tests__/useAccent.test.ts
new file mode 100644
index 0000000..bcfb135
--- /dev/null
+++ b/src/__tests__/useAccent.test.ts
@@ -0,0 +1,74 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { act, renderHook } from '@testing-library/react';
+import { useAccent, ACCENT_MIRROR_KEY } from '@/hooks/useAccent';
+import { adjustForContrast, contrastRatio } from '@/lib/contrast';
+
+const VIOLET = '#9575e0';
+const root = document.documentElement;
+
+afterEach(() => {
+ root.removeAttribute('style');
+ localStorage.clear();
+});
+
+describe('useAccent', () => {
+ it('writes --primary, --primary-foreground and --key-foreground', () => {
+ const { result } = renderHook(() => useAccent('light'));
+
+ act(() => result.current.setAccent(VIOLET));
+
+ expect(root.style.getPropertyValue('--primary')).toBe(VIOLET);
+ expect(root.style.getPropertyValue('--primary-foreground')).toBe('#0a0a0a');
+ const key = root.style.getPropertyValue('--key-foreground');
+ expect(contrastRatio(key, '#ffffff')).toBeGreaterThanOrEqual(4.5);
+ });
+
+ it('never drives the focus ring', () => {
+ const { result } = renderHook(() => useAccent('light'));
+ act(() => result.current.setAccent(VIOLET));
+ expect(root.style.getPropertyValue('--ring')).toBe('');
+ });
+
+ it('computes --key-foreground against the dark panel #161616', () => {
+ const { result } = renderHook(() => useAccent('dark'));
+
+ act(() => result.current.setAccent(VIOLET));
+
+ expect(root.style.getPropertyValue('--key-foreground')).toBe(
+ adjustForContrast(VIOLET, '#161616'),
+ );
+ });
+
+ it('removes all three for Mono and clears the mirror', () => {
+ const { result } = renderHook(() => useAccent('light'));
+ act(() => result.current.setAccent(VIOLET));
+ expect(localStorage.getItem(ACCENT_MIRROR_KEY)).not.toBeNull();
+
+ act(() => result.current.setAccent(null));
+
+ for (const v of ['--primary', '--primary-foreground', '--key-foreground']) {
+ expect(root.style.getPropertyValue(v)).toBe('');
+ }
+ expect(localStorage.getItem(ACCENT_MIRROR_KEY)).toBeNull();
+ });
+
+ it('starts from the mirror, keeping what the boot script applied before first paint', () => {
+ localStorage.setItem(ACCENT_MIRROR_KEY, JSON.stringify({ hex: VIOLET }));
+ root.style.setProperty('--primary', VIOLET);
+
+ const { result } = renderHook(() => useAccent('light'));
+
+ expect(result.current.accent).toBe(VIOLET);
+ expect(root.style.getPropertyValue('--primary')).toBe(VIOLET);
+ });
+
+ it('mirrors both themes’ key colour for the boot script', () => {
+ const { result } = renderHook(() => useAccent('light'));
+ act(() => result.current.setAccent(VIOLET));
+
+ const mirror = JSON.parse(localStorage.getItem(ACCENT_MIRROR_KEY)!);
+ expect(mirror.primary).toBe(VIOLET);
+ expect(mirror.keyForeground.dark).toBe(adjustForContrast(VIOLET, '#161616'));
+ expect(mirror.keyForeground.light).toBe(adjustForContrast(VIOLET, '#ffffff'));
+ });
+});
diff --git a/src/__tests__/useAppStore.test.ts b/src/__tests__/useAppStore.test.ts
index 2fd4c30..1d364a4 100644
--- a/src/__tests__/useAppStore.test.ts
+++ b/src/__tests__/useAppStore.test.ts
@@ -206,10 +206,10 @@ describe('addParam', () => {
expect(p.type).toBe('number');
});
- it('sets an announcement', () => {
+ it('leaves the announcement alone — no toast for an added row', () => {
useAppStore.getState().loadUrl('https://example.com/', 1);
useAppStore.getState().addParam('q', 'search');
- expect(useAppStore.getState().announcement).toContain('q');
+ expect(useAppStore.getState().announcement).toBe('');
});
it('does nothing when currentParsed is null', () => {
diff --git a/src/__tests__/useTheme.test.ts b/src/__tests__/useTheme.test.ts
new file mode 100644
index 0000000..cfcb29e
--- /dev/null
+++ b/src/__tests__/useTheme.test.ts
@@ -0,0 +1,79 @@
+import { describe, it, expect, vi, afterEach } from 'vitest';
+import { act, renderHook, waitFor } from '@testing-library/react';
+import { useTheme, THEME_MIRROR_KEY } from '@/hooks/useTheme';
+
+function mockSystemDark(matches: boolean) {
+ const addEventListener = vi.fn();
+ vi.mocked(window.matchMedia).mockImplementation((query: string) => ({
+ matches,
+ media: query,
+ onchange: null,
+ addEventListener,
+ removeEventListener: vi.fn(),
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ }));
+ return addEventListener;
+}
+
+afterEach(() => {
+ const root = document.documentElement;
+ root.removeAttribute('data-theme');
+ root.classList.remove('dark');
+ root.style.colorScheme = '';
+ localStorage.clear();
+});
+
+describe('useTheme', () => {
+ it("follows the OS while the preference is 'system'", () => {
+ const listen = mockSystemDark(true);
+ const { result } = renderHook(() => useTheme());
+
+ expect(result.current.preference).toBe('system');
+ expect(result.current.resolved).toBe('dark');
+ expect(listen).toHaveBeenCalledWith('change', expect.any(Function));
+ });
+
+ it('applies the coss .dark class, color-scheme and the legacy data-theme', () => {
+ mockSystemDark(true);
+ renderHook(() => useTheme());
+
+ const root = document.documentElement;
+ expect(root).toHaveClass('dark');
+ expect(root.style.colorScheme).toBe('dark');
+ expect(root).toHaveAttribute('data-theme', 'dark');
+ });
+
+ it('ignores the OS once a scheme is chosen, and stops listening to it', async () => {
+ mockSystemDark(true);
+ const { result } = renderHook(() => useTheme());
+
+ const listen = mockSystemDark(true);
+ act(() => result.current.setPreference('light'));
+
+ expect(result.current.resolved).toBe('light');
+ expect(document.documentElement).not.toHaveClass('dark');
+ expect(listen).not.toHaveBeenCalled();
+ });
+
+ it('persists the preference and mirrors it for the boot script', async () => {
+ mockSystemDark(false);
+ const { result } = renderHook(() => useTheme());
+
+ act(() => result.current.setPreference('dark'));
+
+ expect(localStorage.getItem(THEME_MIRROR_KEY)).toBe('dark');
+ await waitFor(() =>
+ expect(chrome.storage.local.set).toHaveBeenCalledWith({ themePreference: 'dark' }),
+ );
+ });
+
+ it('starts from the mirror, so the first render already has the right scheme', () => {
+ mockSystemDark(false);
+ localStorage.setItem(THEME_MIRROR_KEY, 'dark');
+ const { result } = renderHook(() => useTheme());
+
+ expect(result.current.resolved).toBe('dark');
+ });
+});
diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts
new file mode 100644
index 0000000..4640ff2
--- /dev/null
+++ b/src/__tests__/utils.test.ts
@@ -0,0 +1,18 @@
+import { describe, it, expect } from 'vitest';
+import { cn } from '@/lib/utils';
+
+// `text-code` is QueryCraft's 13px mono size (src/styles/theme.css). coss sets
+// sizes under `sm:`, so overrides are written `sm:text-code` (docs/redesign §4.1).
+describe('cn', () => {
+ it('treats text-code as a font size that replaces another size', () => {
+ expect(cn('sm:text-sm', 'sm:text-code')).toBe('sm:text-code');
+ });
+
+ it('keeps the text colour next to text-code', () => {
+ expect(cn('text-muted-foreground', 'text-code')).toBe('text-muted-foreground text-code');
+ });
+
+ it('lets text-key-foreground replace another text colour', () => {
+ expect(cn('text-foreground', 'text-key-foreground')).toBe('text-key-foreground');
+ });
+});
diff --git a/src/components/ActionBar/ActionBar.module.css b/src/components/ActionBar/ActionBar.module.css
deleted file mode 100644
index 78f9515..0000000
--- a/src/components/ActionBar/ActionBar.module.css
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Four labelled buttons in a 380px panel leave only a couple of pixels of
- * slack at the reference's paddings — and only when Inter is installed. With
- * a wider fallback face the row overflowed and ate the 18px right padding.
- * The gap is tightened to 6px and the labels are allowed to shrink so the
- * padding is never the thing that gives.
- */
-.root {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- padding: var(--space-7) var(--pad-inline);
- border-top: 1px solid var(--color-border);
- flex-shrink: 0;
- min-width: 0;
-}
-
-.root > button {
- min-width: 0;
-}
-
-/* Pushes Copy/Save to the trailing edge. */
-.spacer {
- flex: 1;
- min-width: 0;
-}
diff --git a/src/components/ActionBar/ActionBar.tsx b/src/components/ActionBar/ActionBar.tsx
index 5bf3c41..9e030df 100644
--- a/src/components/ActionBar/ActionBar.tsx
+++ b/src/components/ActionBar/ActionBar.tsx
@@ -1,6 +1,7 @@
-import { Button } from '../Button';
-import { IconBookmark, IconCheck, IconCopy, IconReset } from '../icons';
-import styles from './ActionBar.module.css';
+import { useEffect, useRef } from 'react';
+import { Button } from '@/components/ui/button';
+import { Kbd } from '@/components/ui/kbd';
+import { ariaShortcut, shortcutLabel } from '@/lib/platform';
interface ActionBarProps {
onApply: () => void;
@@ -12,10 +13,11 @@ interface ActionBarProps {
}
/**
- * Bottom action bar:
- * [Apply] [Reset] ...................................... [Copy] [Save]
+ * Footer action bar (docs/redesign §6.1):
+ * [Apply ⌘↵] [Reset] ................................. [Copy] [Save]
*
- * Apply is the primary (filled) action; the rest are outlined.
+ * It also publishes its height as --action-bar-height, so toasts can sit 8px
+ * above it (§5.3).
*/
export function ActionBar({
onApply,
@@ -25,34 +27,51 @@ export function ActionBar({
applyDisabled = false,
copied = false,
}: ActionBarProps) {
+ const ref = useActionBarHeight();
+
return (
-
-
}
- >
+
+
Apply
+ {/* Hidden from assistive tech: aria-keyshortcuts already names the shortcut. */}
+
+ {shortcutLabel('↵')}
+
- }>
+
Reset
-
+
- : }
- aria-label={copied ? 'URL copied to clipboard' : 'Copy URL to clipboard'}
- >
+
{copied ? 'Copied' : 'Copy'}
- }>
+
Save
-
+
);
}
+
+function useActionBarHeight() {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el || typeof ResizeObserver === 'undefined') return;
+ const root = document.documentElement;
+ const observer = new ResizeObserver(([entry]) => {
+ if (!entry) return;
+ const height = entry.borderBoxSize[0]?.blockSize ?? entry.contentRect.height;
+ root.style.setProperty('--action-bar-height', `${Math.ceil(height)}px`);
+ });
+ observer.observe(el);
+ return () => {
+ observer.disconnect();
+ root.style.removeProperty('--action-bar-height');
+ };
+ }, []);
+
+ return ref;
+}
diff --git a/src/components/GroupSelector/GroupSelector.module.css b/src/components/GroupSelector/GroupSelector.module.css
deleted file mode 100644
index ff25d00..0000000
--- a/src/components/GroupSelector/GroupSelector.module.css
+++ /dev/null
@@ -1,62 +0,0 @@
-.wrapper {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
-}
-
-.label {
- font-size: var(--font-size-xs);
- font-weight: 600;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- color: var(--color-text-muted);
-}
-
-.select {
- width: 100%;
- background: var(--color-surface-inset);
- color: var(--color-text-body);
- border: 1px solid var(--color-fill-strong);
- border-radius: var(--radius-lg);
- padding: 9px 11px;
- font-family: var(--font-sans);
- font-size: var(--font-size-md);
- cursor: pointer;
- transition: border-color var(--transition-fast);
-}
-
-.select:hover {
- border-color: var(--color-border-strong);
-}
-
-.select:focus {
- border-color: var(--color-border-focus);
-}
-
-/* Inline "create new group" form shown in place of the select */
-.createRow {
- display: flex;
- align-items: center;
- gap: var(--space-4);
-}
-
-.createInput {
- flex: 1;
- min-width: 0;
- background: var(--color-surface-inset);
- color: var(--color-text-body);
- border: 1px solid var(--color-fill-strong);
- border-radius: var(--radius-lg);
- padding: 9px 11px;
- font-family: var(--font-sans);
- font-size: var(--font-size-md);
- transition: border-color var(--transition-fast);
-}
-
-.createInput:hover {
- border-color: var(--color-border-strong);
-}
-
-.createInput:focus {
- border-color: var(--color-border-focus);
-}
diff --git a/src/components/GroupSelector/GroupSelector.tsx b/src/components/GroupSelector/GroupSelector.tsx
deleted file mode 100644
index 45f5c56..0000000
--- a/src/components/GroupSelector/GroupSelector.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import { useEffect, useId, useRef, useState } from 'react';
-import type { Group } from '@/types';
-import { Button } from '../Button';
-import styles from './GroupSelector.module.css';
-
-interface GroupSelectorProps {
- groups: Group[];
- value: string;
- onChange: (groupId: string) => void;
- /** Called with the trimmed name when the user confirms the inline "new group" form. */
- onCreateNew: (name: string) => void;
- label?: string;
-}
-
-const CREATE_NEW_VALUE = '__create_new__';
-
-/**
- * A native for picking a group, with a "+ Create new group" option
- * at the top. Native select is deliberate: it's fully keyboard-accessible by
- * default and screen readers handle it well, no custom ARIA combobox needed.
- *
- * Choosing "+ Create new group…" swaps in an inline name input. This must NOT
- * use window.prompt: modal JS dialogs are suppressed inside MV3 extension
- * popups, so a prompt would silently return null.
- */
-export function GroupSelector({
- groups,
- value,
- onChange,
- onCreateNew,
- label = 'Group',
-}: GroupSelectorProps) {
- const id = useId();
- const [creating, setCreating] = useState(false);
- const [draftName, setDraftName] = useState('');
- const inputRef = useRef(null);
- const selectRef = useRef(null);
-
- useEffect(() => {
- if (creating) inputRef.current?.focus();
- }, [creating]);
-
- function onSelectChange(e: React.ChangeEvent) {
- const next = e.target.value;
- if (next === CREATE_NEW_VALUE) {
- setCreating(true);
- return;
- }
- onChange(next);
- }
-
- function confirmCreate() {
- const name = draftName.trim();
- if (!name) return;
- onCreateNew(name);
- setDraftName('');
- setCreating(false);
- }
-
- function cancelCreate() {
- setDraftName('');
- setCreating(false);
- // Hand focus back to the select so keyboard users aren't dropped.
- selectRef.current?.focus();
- }
-
- return (
-
-
- {creating ? 'New group name' : label}
-
-
- {creating ? (
-
- setDraftName(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- confirmCreate();
- }
- if (e.key === 'Escape') {
- // Cancel only the inline form — stop the event so the drawer
- // (focus trap listens on document) doesn't close too.
- e.preventDefault();
- e.stopPropagation();
- cancelCreate();
- }
- }}
- />
-
- Add
-
-
- Cancel
-
-
- ) : (
-
- + Create new group…
- {groups.map((g) => (
-
- {g.name}
-
- ))}
-
- )}
-
- );
-}
diff --git a/src/components/GroupSelector/index.ts b/src/components/GroupSelector/index.ts
deleted file mode 100644
index 48ca7eb..0000000
--- a/src/components/GroupSelector/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { GroupSelector } from './GroupSelector';
diff --git a/src/components/Header/Header.module.css b/src/components/Header/Header.module.css
deleted file mode 100644
index 74af387..0000000
--- a/src/components/Header/Header.module.css
+++ /dev/null
@@ -1,43 +0,0 @@
-.root {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: var(--pad-block) var(--pad-inline);
- border-bottom: 1px solid var(--color-border);
- flex-shrink: 0;
-}
-
-.brand {
- display: flex;
- align-items: center;
- gap: var(--space-5);
- min-width: 0;
-}
-
-/* Bordered square mark holding the magnifier — inspect-the-URL, in one glyph. */
-.logo {
- width: 26px;
- height: 26px;
- border-radius: var(--radius-md);
- border: 1.5px solid var(--color-accent-text);
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- color: var(--color-accent-text);
-}
-
-.title {
- font-size: var(--font-size-lg);
- font-weight: 600;
- letter-spacing: -0.01em;
- color: var(--color-text-primary);
- margin: 0;
-}
-
-.actions {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- flex-shrink: 0;
-}
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
index 732bed7..fbcba90 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Header/Header.tsx
@@ -1,22 +1,76 @@
-import type { ReactNode } from 'react';
-import { IconSearch } from '../icons';
-import styles from './Header.module.css';
+import type { ReactElement } from 'react';
+import { BookmarkIcon, SearchIcon, SettingsIcon } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Kbd } from '@/components/ui/kbd';
+import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { ariaShortcut, shortcutLabel } from '@/lib/platform';
interface HeaderProps {
- /** Right-side slot for action buttons (theme toggle, expand, saved links). */
- actions?: ReactNode;
+ onOpenSaved: () => void;
+ onOpenSettings: () => void;
}
-export function Header({ actions }: HeaderProps) {
+/** Popup header (docs/redesign §6.1). Phase 6 adds the ⌘K palette trigger first in the row. */
+export function Header({ onOpenSaved, onOpenSettings }: HeaderProps) {
return (
-
-
-
-
+
);
}
+
+interface HeaderButtonProps {
+ label: string;
+ /** Key pressed with Cmd/Ctrl, shown in the tooltip and exposed to assistive tech. */
+ shortcut?: string;
+ icon: ReactElement;
+ onClick: () => void;
+}
+
+/** Icon-only, so it carries an aria-label and a tooltip with the label and its shortcut. */
+function HeaderButton({ label, shortcut, icon, onClick }: HeaderButtonProps) {
+ return (
+
+
+ }
+ >
+ {icon}
+
+
+
+ {label}
+ {shortcut && {shortcutLabel(shortcut)} }
+
+
+
+ );
+}
diff --git a/src/components/LiveRegion/LiveRegion.tsx b/src/components/LiveRegion/LiveRegion.tsx
new file mode 100644
index 0000000..dbc5505
--- /dev/null
+++ b/src/components/LiveRegion/LiveRegion.tsx
@@ -0,0 +1,43 @@
+import { useEffect, useState } from 'react';
+import { create } from 'zustand';
+
+interface LiveMessage {
+ text: string;
+ /** Bumped on every announce, so the same text twice is announced twice. */
+ id: number;
+}
+
+const useLiveMessage = create(() => ({ text: '', id: 0 }));
+
+/**
+ * Announce through the hidden region. For events with no visible toast only —
+ * the toast viewport is a live region of its own (docs/redesign §7.3).
+ */
+export function announce(text: string): void {
+ useLiveMessage.setState((s) => ({ text, id: s.id + 1 }));
+}
+
+/**
+ * The single visually-hidden polite live region, ported from DSL-beta. Mount
+ * it once, at the root.
+ *
+ * The text is cleared before each announcement so that announcing the same
+ * message twice in a row still triggers screen reader output.
+ */
+export function LiveRegion() {
+ const { text: message, id } = useLiveMessage();
+ const [text, setText] = useState('');
+
+ useEffect(() => {
+ if (!message) return;
+ setText('');
+ const timer = window.setTimeout(() => setText(message), 50);
+ return () => window.clearTimeout(timer);
+ }, [message, id]);
+
+ return (
+
+ {text}
+
+ );
+}
diff --git a/src/components/LiveRegion/index.ts b/src/components/LiveRegion/index.ts
new file mode 100644
index 0000000..b44350d
--- /dev/null
+++ b/src/components/LiveRegion/index.ts
@@ -0,0 +1 @@
+export { LiveRegion, announce } from './LiveRegion';
diff --git a/src/components/ParamList/ParamList.module.css b/src/components/ParamList/ParamList.module.css
index 4a387c0..5dbb99f 100644
--- a/src/components/ParamList/ParamList.module.css
+++ b/src/components/ParamList/ParamList.module.css
@@ -3,16 +3,10 @@
flex-direction: column;
}
-/* Separates the filter from the URL preview above it. */
-.searchWrap {
- margin-top: var(--space-8);
-}
-
.header {
display: flex;
align-items: center;
justify-content: space-between;
- margin-top: var(--space-9);
margin-bottom: var(--space-5);
}
diff --git a/src/components/ParamList/ParamList.tsx b/src/components/ParamList/ParamList.tsx
index 707364c..8b19ba4 100644
--- a/src/components/ParamList/ParamList.tsx
+++ b/src/components/ParamList/ParamList.tsx
@@ -88,7 +88,8 @@ export function ParamList({
return (
<>
-
+ {/* Spacing above is the shell's gap (App
, gap-3). */}
+
void;
+ /** Called with the typed name when "Create “…”" is chosen. */
+ onCreate: (name: string) => void;
+ 'aria-describedby'?: string;
+}
+
+interface GroupOption {
+ id: string;
+ name: string;
+ /** The trailing "Create “{query}”" item. */
+ create?: boolean;
+}
+
+const CREATE_ID = '__create__';
+
+/**
+ * Creatable group picker (docs/redesign §6.4): typing filters the groups, and
+ * when nothing matches exactly the last item offers to create one. The match
+ * is case-insensitive, so "staging" never offers a duplicate of "Staging".
+ */
+export function GroupCombobox({
+ id,
+ groups,
+ value,
+ onChange,
+ onCreate,
+ 'aria-describedby': describedBy,
+}: GroupComboboxProps) {
+ const selected = groups.find((g) => g.id === value) ?? null;
+ const [query, setQuery] = useState(selected?.name ?? '');
+
+ // Show the selection's name whenever the selection itself changes.
+ useEffect(() => {
+ setQuery(selected?.name ?? '');
+ }, [selected?.id, selected?.name]);
+
+ const options = useMemo(() => {
+ const q = query.trim();
+ // An untouched input lists every group.
+ if (!q || q === selected?.name) return groups;
+ const needle = q.toLowerCase();
+ const matches = groups.filter((g) => g.name.toLowerCase().includes(needle));
+ const exact = groups.some((g) => g.name.toLowerCase() === needle);
+ return exact ? matches : [...matches, { id: CREATE_ID, name: q, create: true }];
+ }, [groups, query, selected?.name]);
+
+ return (
+
+ items={groups}
+ filteredItems={options}
+ value={selected}
+ onValueChange={(option) => {
+ if (!option) return;
+ if (option.create) onCreate(option.name);
+ else onChange(option.id);
+ }}
+ inputValue={query}
+ onInputValueChange={setQuery}
+ itemToStringLabel={(option) => option.name}
+ isItemEqualToValue={(a, b) => a.id === b.id}
+ openOnInputClick
+ // Closed without a choice: show the selected group again, so the text
+ // never disagrees with what will be saved.
+ onOpenChange={(open) => {
+ if (!open) setQuery(selected?.name ?? '');
+ }}
+ >
+ {/*
+ * No trigger button: coss's is a second role="combobox" in the tab order,
+ * sharing the input's id. The chevron is decoration (as in the design);
+ * a click on the field or ↓ opens the list.
+ */}
+
+
+
+
+ {/* Focus stays in the input while the list is open; the popup never takes it. */}
+
+
+ {(option: GroupOption) => (
+
+ {option.create ? `Create “${option.name}”` : option.name}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/SavedLinksDrawer/GroupedLinksList.tsx b/src/components/SavedLinksDrawer/GroupedLinksList.tsx
index 38b066f..d697962 100644
--- a/src/components/SavedLinksDrawer/GroupedLinksList.tsx
+++ b/src/components/SavedLinksDrawer/GroupedLinksList.tsx
@@ -1,130 +1,239 @@
-import { useState } from 'react';
+import { useMemo, useState, type KeyboardEvent } from 'react';
+import {
+ ChevronDownIcon,
+ CopyIcon,
+ CornerDownLeftIcon,
+ EllipsisIcon,
+ PencilIcon,
+ SearchIcon,
+ Trash2Icon,
+} from 'lucide-react';
import type { Group, SavedLink } from '@/types';
-import { Button } from '../Button';
-import { IconButton } from '../IconButton';
-import { IconChevronDown, IconClose, IconEdit, IconLoad } from '../icons';
-import styles from './SavedLinksDrawer.module.css';
+import { cn } from '@/lib/utils';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@/components/ui/collapsible';
+import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group';
+import { Kbd } from '@/components/ui/kbd';
+import {
+ Menu,
+ MenuItem,
+ MenuPopup,
+ MenuSeparator,
+ MenuShortcut,
+ MenuTrigger,
+} from '@/components/ui/menu';
+
+/** Read by the sheet to focus the filter (on open, and on `/`). */
+export const SAVED_FILTER_ID = 'saved-urls-filter';
interface GroupedLinksListProps {
groups: Group[];
linksByGroup: Map;
onLoadLink: (url: string) => void;
- onDeleteLink: (id: string) => void;
+ onCopyLink: (url: string) => void;
+ onDeleteLink: (link: SavedLink) => void;
onStartEdit: (link: SavedLink) => void;
- onStartSave: () => void;
- canSave: boolean;
}
-/** The default drawer view: saved links grouped into collapsible sections. */
+/** "https://x.com/a" → "x.com/a": the list shows URLs without their protocol (§6.4). */
+function withoutProtocol(url: string): string {
+ return url.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
+}
+
+/** Saved URLs, filtered, in one collapsible section per group (docs/redesign §6.4). */
export function GroupedLinksList({
groups,
linksByGroup,
onLoadLink,
+ onCopyLink,
onDeleteLink,
onStartEdit,
- onStartSave,
- canSave,
}: GroupedLinksListProps) {
- const totalCount = Array.from(linksByGroup.values()).reduce((sum, l) => sum + l.length, 0);
+ const [query, setQuery] = useState('');
+ // Groups start expanded; only the ones the user closed are remembered.
+ const [collapsed, setCollapsed] = useState>(() => new Set());
- // All groups expanded by default.
- const [collapsedGroups, setCollapsedGroups] = useState>(() => new Set());
+ const needle = query.trim().toLowerCase();
+ const visibleGroups = useMemo(
+ () =>
+ groups
+ .map((group) => {
+ const links = linksByGroup.get(group.id) ?? [];
+ const groupMatches = group.name.toLowerCase().includes(needle);
+ const matching =
+ !needle || groupMatches
+ ? links
+ : links.filter(
+ (l) =>
+ l.url.toLowerCase().includes(needle) ||
+ (l.label ?? '').toLowerCase().includes(needle),
+ );
+ return { group, links: matching };
+ })
+ // Empty groups are hidden, and so are groups with nothing matching.
+ .filter(({ links }) => links.length > 0),
+ [groups, linksByGroup, needle],
+ );
+ const total = Array.from(linksByGroup.values()).reduce((sum, l) => sum + l.length, 0);
- function toggleGroup(id: string) {
- setCollapsedGroups((prev) => {
+ function setGroupOpen(id: string, open: boolean) {
+ setCollapsed((prev) => {
const next = new Set(prev);
- if (next.has(id)) next.delete(id);
+ if (open) next.delete(id);
else next.add(id);
return next;
});
}
+ /**
+ * Delete, then focus the next row (else the previous, else the filter) so
+ * focus is never left on a row that is gone. Deferred a tick: from the row
+ * menu, the closing menu first restores focus to its own (removed) trigger.
+ */
+ function deleteAndRefocus(link: SavedLink) {
+ const rows = Array.from(document.querySelectorAll('[data-qc-saved-row]'));
+ const at = rows.findIndex((row) => row.dataset.qcSavedRow === link.id);
+ const neighbour = rows[at + 1] ?? rows[at - 1];
+ onDeleteLink(link);
+ window.setTimeout(() => (neighbour ?? document.getElementById(SAVED_FILTER_ID))?.focus(), 0);
+ }
+
+ /** Backspace/Delete on a focused row deletes it (§7.1). */
+ function handleRowKeyDown(e: KeyboardEvent, link: SavedLink) {
+ if (e.key !== 'Backspace' && e.key !== 'Delete') return;
+ e.preventDefault();
+ deleteAndRefocus(link);
+ }
+
return (
-
-
-
- Save current URL
-
-
+
+
+
+
+
+ setQuery(e.target.value)}
+ placeholder="Filter saved URLs"
+ aria-label="Filter saved URLs"
+ aria-keyshortcuts="/"
+ />
+
+ /
+
+
- {totalCount === 0 ? (
-
No saved URLs yet
- ) : (
- groups.map((g) => {
- const groupLinks = linksByGroup.get(g.id) ?? [];
- if (groupLinks.length === 0) return null;
- const isExpanded = !collapsedGroups.has(g.id);
- return (
-
- toggleGroup(g.id)}
- aria-expanded={isExpanded}
- >
-
- {g.name}
-
- {groupLinks.length}
-
-
-
-
-
-
+ {total === 0 && (
+ No saved URLs yet
+ )}
+ {/* TODO(design): the empty filter result is not designed; copy follows the param filter's. */}
+ {total > 0 && visibleGroups.length === 0 && (
+
+ No saved URLs match “{query.trim()}”
+
+ )}
- {isExpanded && (
-
- {groupLinks.map((link) => {
- const name = link.label || link.url;
- return (
-
- onLoadLink(link.url)}
+ {visibleGroups.map(({ group, links }) => {
+ // A filter shows every match, whatever was collapsed.
+ const open = needle !== '' || !collapsed.has(group.id);
+ return (
+ setGroupOpen(group.id, next)}
+ render={}
+ >
+
+ }
+ >
+
+ {group.name}
+
+ {links.length}
+
+
+
+
+ {links.map((link) => {
+ const name = link.label || withoutProtocol(link.url);
+ return (
+
+ {/* Click / Enter loads the link into the editor. */}
+ onLoadLink(link.url)}
+ onKeyDown={(e) => handleRowKeyDown(e, link)}
+ className="flex min-w-0 flex-1 flex-col items-stretch rounded-lg py-2 ps-3 pe-1 text-start outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
+ >
+ {link.label ? (
+ <>
+ {link.label}
+
+ {withoutProtocol(link.url)}
+
+ >
+ ) : (
+
+ {withoutProtocol(link.url)}
+
+ )}
+
+
+
+ }
>
- {name}
- {link.label && {link.url} }
-
-
- }
- size="md"
- onClick={() => onLoadLink(link.url)}
- />
- }
- size="md"
- onClick={() => onStartEdit(link)}
- />
- }
- size="md"
- tone="danger"
- onClick={() => onDeleteLink(link.id)}
- />
-
-
- );
- })}
-
- )}
-
- );
- })
- )}
+
+
+
+ onLoadLink(link.url)}>
+
+ Load into editor
+ ↵
+
+ onCopyLink(link.url)}>
+
+ Copy URL
+
+ onStartEdit(link)}>
+
+ Edit…
+
+
+ deleteAndRefocus(link)}>
+
+ Delete
+ ⌫
+
+
+
+
+ );
+ })}
+
+
+
+ );
+ })}
);
}
diff --git a/src/components/SavedLinksDrawer/SaveForm.tsx b/src/components/SavedLinksDrawer/SaveForm.tsx
index cae4504..90b4b99 100644
--- a/src/components/SavedLinksDrawer/SaveForm.tsx
+++ b/src/components/SavedLinksDrawer/SaveForm.tsx
@@ -1,8 +1,11 @@
-import { useId } from 'react';
+import { useEffect, useId, useLayoutEffect, useRef, useState, type ReactNode } from 'react';
import type { Group } from '@/types';
-import { Button } from '../Button';
-import { GroupSelector } from '../GroupSelector';
-import styles from './SavedLinksDrawer.module.css';
+import { Field, FieldDescription, FieldLabel } from '@/components/ui/field';
+import { Input } from '@/components/ui/input';
+import { GroupCombobox } from './GroupCombobox';
+
+/** Read by the sheet: Name takes focus when the form opens (§7.2). */
+export const SAVE_NAME_ID = 'saved-url-name';
interface SaveFormProps {
label: string;
@@ -11,13 +14,13 @@ interface SaveFormProps {
onGroupChange: (id: string) => void;
groups: Group[];
onCreateNewGroup: (name: string) => void;
- onSubmit: () => void;
- onCancel: () => void;
previewUrl: string;
- submitLabel?: string;
}
-/** Shared form body for both "save current URL" and "edit saved URL" modes. */
+/**
+ * Body of the save and edit forms (docs/redesign §6.4). The sheet around it
+ * owns the header, the Cancel / Save footer and ⌘↵.
+ */
export function SaveForm({
label,
onLabelChange,
@@ -25,61 +28,155 @@ export function SaveForm({
onGroupChange,
groups,
onCreateNewGroup,
- onSubmit,
- onCancel,
previewUrl,
- submitLabel = 'Save',
}: SaveFormProps) {
- const labelId = useId();
+ const nameId = SAVE_NAME_ID;
+ const groupFieldId = useId();
+ const groupDescriptionId = useId();
const urlId = useId();
+ const urlDescriptionId = useId();
+
+ // Focus lands on Name whenever the form is shown (§7.2) — also when it
+ // replaces the list inside an open sheet. Deferred a tick so a closing row
+ // menu has already restored focus to its trigger.
+ useEffect(() => {
+ const timer = window.setTimeout(() => document.getElementById(nameId)?.focus(), 0);
+ return () => window.clearTimeout(timer);
+ }, [nameId]);
return (
-
-
-
- Label (optional)
-
-
+
+
+ Name (optional)
+
+ onLabelChange(e.target.value)}
placeholder="e.g. Staging with debug flag"
maxLength={80}
/>
-
+
Shown instead of the URL in the list.
+
+
+
+ Group
+
+
+ Type to find a group, or create a new one.
+
+
-
+
+ );
+}
-
-
- URL
-
- {/* Read-only: the URL is edited in the main view, shown here to confirm. */}
-
-
+/**
+ * The URL being saved, read-only, with the same mono highlighting as the main
+ * URL editor and at most four lines before it scrolls (§6.4). coss has no
+ * read-only field style, so it is a muted shell around a transparent textarea
+ * laid over the highlighted text.
+ */
+function ReadOnlyUrl({
+ id,
+ descriptionId,
+ url,
+ label,
+ description,
+}: {
+ id: string;
+ descriptionId: string;
+ url: string;
+ label: string;
+ description: string;
+}) {
+ const scrollRef = useRef
(null);
+ const [overflowing, setOverflowing] = useState(false);
+
+ // The bottom fade shows only when there is more to scroll to.
+ useLayoutEffect(() => {
+ const el = scrollRef.current;
+ if (el) setOverflowing(el.scrollHeight > el.clientHeight);
+ }, [url]);
-
-
- {submitLabel}
-
-
- Cancel
-
+ return (
+
+ {label}
+
+
+
+
+ {highlightUrl(url)}
+
+
+
+
-
+ {description}
+
);
}
+
+/**
+ * Base and the `? & =` separators muted, keys medium, values in the foreground.
+ * Keys are not in the accent here: on the read-only field's bg-muted the key
+ * colour, tuned against the plain background (§4.4), falls under 4.5:1 for 9 of
+ * the 11 accents in light — so they keep the foreground, as Mono does.
+ */
+function highlightUrl(url: string): ReactNode {
+ const q = url.indexOf('?');
+ if (q === -1) return url;
+ const nodes: ReactNode[] = [url.slice(0, q + 1)];
+ url
+ .slice(q + 1)
+ .split('&')
+ .forEach((pair, i) => {
+ if (i > 0) nodes.push('&');
+ const eq = pair.indexOf('=');
+ const key = eq === -1 ? pair : pair.slice(0, eq);
+ nodes.push(
+
+ {key}
+ ,
+ );
+ if (eq !== -1) {
+ nodes.push(
+ '=',
+
+ {pair.slice(eq + 1)}
+ ,
+ );
+ }
+ });
+ return nodes;
+}
diff --git a/src/components/SavedLinksDrawer/SavedLinksDrawer.module.css b/src/components/SavedLinksDrawer/SavedLinksDrawer.module.css
deleted file mode 100644
index 9097540..0000000
--- a/src/components/SavedLinksDrawer/SavedLinksDrawer.module.css
+++ /dev/null
@@ -1,205 +0,0 @@
-/* ── Save / edit form ── */
-
-.form {
- display: flex;
- flex-direction: column;
- gap: var(--space-8);
-}
-
-.field {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
-}
-
-.fieldLabel {
- font-size: var(--font-size-xs);
- font-weight: 600;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- color: var(--color-text-muted);
-}
-
-.textInput {
- width: 100%;
- background: var(--color-surface-inset);
- border: 1px solid var(--color-fill-strong);
- border-radius: var(--radius-lg);
- padding: 9px 11px;
- font-size: var(--font-size-md);
- color: var(--color-text-body);
- transition: border-color var(--transition-fast);
-}
-
-.textInput:hover {
- border-color: var(--color-border-strong);
-}
-
-.textInput:focus {
- border-color: var(--color-border-focus);
-}
-
-.urlArea {
- width: 100%;
- height: 88px;
- resize: none;
- background: var(--color-surface-inset);
- border: 1px solid var(--color-fill-strong);
- border-radius: var(--radius-lg);
- padding: 9px 11px;
- font-family: var(--font-mono);
- font-size: var(--font-size-base);
- line-height: var(--line-height-mono);
- color: var(--color-text-secondary);
- word-break: break-all;
-}
-
-.formActions {
- display: flex;
- gap: var(--space-4);
-}
-
-/* ── List ── */
-
-.list {
- display: flex;
- flex-direction: column;
-}
-
-.saveCurrent {
- margin-bottom: var(--space-10);
-}
-
-.empty {
- text-align: center;
- color: var(--color-text-faint);
- font-size: var(--font-size-mono);
- padding: 40px var(--space-5);
- margin: 0;
-}
-
-.groupSection {
- margin-bottom: var(--space-7);
-}
-
-.groupToggle {
- display: flex;
- align-items: center;
- justify-content: space-between;
- width: 100%;
- padding: var(--space-2) 0;
- background: none;
- border: none;
- cursor: pointer;
- color: var(--color-text-muted);
- border-radius: var(--radius-sm);
-}
-
-.groupToggle:hover {
- color: var(--color-text-primary);
-}
-
-.groupName {
- display: flex;
- align-items: center;
- gap: var(--space-4);
- font-size: var(--font-size-xs);
- font-weight: 600;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-.groupCount {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- background: var(--color-fill);
- color: var(--color-text-secondary);
- font-size: var(--font-size-xs);
- font-weight: 600;
- padding: 1px 7px;
- border-radius: var(--radius-full);
- text-transform: none;
- letter-spacing: 0;
-}
-
-.chevron {
- display: flex;
- align-items: center;
- flex-shrink: 0;
- transition: transform var(--transition-fast);
-}
-
-.chevronCollapsed {
- transform: rotate(-90deg);
-}
-
-.linkList {
- list-style: none;
- padding: 0;
- margin: var(--space-4) 0 0;
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
-}
-
-.linkRow {
- display: flex;
- align-items: center;
- gap: var(--space-5);
- background: var(--color-surface-inset);
- border: 1px solid var(--color-border-subtle);
- border-radius: var(--radius-lg);
- padding: 10px 12px;
-}
-
-.linkRow:hover {
- border-color: var(--color-border-strong);
-}
-
-/* Clicking the name loads the link into the editor. */
-.linkText {
- flex: 1;
- min-width: 0;
- text-align: left;
- line-height: inherit;
- border-radius: var(--radius-sm);
-}
-
-.linkLabel {
- display: block;
- font-size: var(--font-size-md);
- font-weight: 600;
- color: var(--color-text-body);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- transition: color var(--transition-fast);
-}
-
-.linkText:hover .linkLabel {
- color: var(--color-text-primary);
-}
-
-.linkText:hover .linkUrl {
- color: var(--color-text-secondary);
-}
-
-.linkUrl {
- display: block;
- margin-top: 2px;
- font-family: var(--font-mono);
- font-size: var(--font-size-sm);
- color: var(--color-text-muted);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- transition: color var(--transition-fast);
-}
-
-.linkActions {
- display: flex;
- align-items: center;
- gap: var(--space-1);
- flex-shrink: 0;
-}
diff --git a/src/components/SavedLinksDrawer/SavedLinksDrawer.tsx b/src/components/SavedLinksDrawer/SavedLinksDrawer.tsx
index c80f029..bb629ba 100644
--- a/src/components/SavedLinksDrawer/SavedLinksDrawer.tsx
+++ b/src/components/SavedLinksDrawer/SavedLinksDrawer.tsx
@@ -1,12 +1,29 @@
-import { useEffect, useMemo, useRef, useState } from 'react';
+import { useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react';
+import { ChevronLeftIcon, XIcon } from 'lucide-react';
import type { Group, SavedLink } from '@/types';
import { DEFAULT_GROUP_ID } from '@/lib/storage';
-import { SlidePanel } from '../SlidePanel';
-import { SaveForm } from './SaveForm';
-import { GroupedLinksList } from './GroupedLinksList';
+import { isEditableTarget } from '@/lib/dom';
+import { ariaShortcut, shortcutLabel } from '@/lib/platform';
+import { useReportSheetHeight } from '@/popup/popupHeight';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Kbd } from '@/components/ui/kbd';
+import {
+ Sheet,
+ SheetClose,
+ SheetFooter,
+ SheetHeader,
+ SheetPanel,
+ SheetPopup,
+ SheetTitle,
+} from '@/components/ui/sheet';
+import { SaveForm, SAVE_NAME_ID } from './SaveForm';
+import { GroupedLinksList, SAVED_FILTER_ID } from './GroupedLinksList';
type DrawerMode = 'list' | 'save' | 'edit';
+const SHEET_ID = 'saved';
+
interface SavedLinksDrawerProps {
open: boolean;
/** Mode to show when the drawer opens (e.g. jump straight to the save form). */
@@ -17,24 +34,16 @@ interface SavedLinksDrawerProps {
groups: Group[];
onSave: (input: { url: string; label?: string; groupId: string }) => void;
onUpdateLink: (id: string, label: string | undefined, groupId: string) => void;
- onDeleteLink: (id: string) => void;
+ onDeleteLink: (link: SavedLink) => void;
onCreateGroup: (name: string) => Promise;
onLoadLink: (url: string) => void;
+ onCopyLink: (url: string) => void;
}
-const MODE_TITLES: Record = {
- list: 'Saved URLs',
- save: 'Save current URL',
- edit: 'Edit saved URL',
-};
-
/**
- * Slide-in panel covering the editor. Role dialog with a focus trap.
- *
- * Three modes:
- * - "list" (default): browse existing saved links, grouped.
- * - "save": form to save the current URL with a label + group.
- * - "edit": same form, editing an existing saved link.
+ * Saved URLs sheet (docs/redesign §6.4). Two modes in one sheet:
+ * - list: filter, groups, rows with an actions menu, "Save current URL";
+ * - form: save the current URL, or edit a saved one (Cancel / Save ⌘↵).
*/
export function SavedLinksDrawer({
open,
@@ -48,20 +57,27 @@ export function SavedLinksDrawer({
onDeleteLink,
onCreateGroup,
onLoadLink,
+ onCopyLink,
}: SavedLinksDrawerProps) {
const [mode, setMode] = useState('list');
const [label, setLabel] = useState('');
const [groupId, setGroupId] = useState(DEFAULT_GROUP_ID);
const [editingLink, setEditingLink] = useState(null);
+ // Set when a link is loaded, so closing focuses the URL editor (§7.2).
+ const loadedRef = useRef(false);
+
+ useReportSheetHeight(open, SHEET_ID, mode);
// Apply initialMode only on the closed → open transition, so re-renders
// (or pressing Cmd+S while already open) don't yank the user out of a form.
+ // A layout effect, so a sheet opened on the form never paints the list first.
const wasOpen = useRef(false);
- useEffect(() => {
+ useLayoutEffect(() => {
if (open && !wasOpen.current) {
setMode(initialMode);
setEditingLink(null);
setLabel('');
+ setGroupId(DEFAULT_GROUP_ID);
}
wasOpen.current = open;
}, [open, initialMode]);
@@ -91,6 +107,8 @@ export function SavedLinksDrawer({
setEditingLink(null);
setLabel('');
setMode('list');
+ // Focus would otherwise be left on the form that just unmounted.
+ window.setTimeout(() => document.getElementById(SAVED_FILTER_ID)?.focus(), 0);
}
function handleSave() {
@@ -111,41 +129,120 @@ export function SavedLinksDrawer({
backToList();
}
+ function handleLoad(url: string) {
+ loadedRef.current = true;
+ onLoadLink(url);
+ }
+
const showingForm = mode === 'save' || (mode === 'edit' && editingLink !== null);
+ const submit = mode === 'edit' ? handleUpdate : handleSave;
+
+ function handleKeyDown(e: KeyboardEvent) {
+ // ⌘↵ is the primary action in every view: here, Save / Update (§7.1).
+ if (showingForm && e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ submit();
+ return;
+ }
+ if (!showingForm && e.key === '/' && !isEditableTarget(e.target)) {
+ e.preventDefault();
+ document.getElementById(SAVED_FILTER_ID)?.focus();
+ }
+ }
return (
-
- {showingForm ? (
-
- ) : (
- setMode('save')}
- canSave={!!currentUrl}
- />
- )}
-
+ !next && onClose()}>
+ document.getElementById(showingForm ? SAVE_NAME_ID : SAVED_FILTER_ID)}
+ finalFocus={() => {
+ if (!loadedRef.current) return true;
+ loadedRef.current = false;
+ return document.querySelector('textarea[aria-label="Current URL"]') ?? true;
+ }}
+ >
+ {showingForm ? (
+ <>
+
+
+
+
+
+ {mode === 'edit' ? 'Edit saved URL' : 'Save current URL'}
+
+ }
+ >
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ {mode === 'edit' ? 'Update' : 'Save'}
+
+ {shortcutLabel('↵')}
+
+
+
+ >
+ ) : (
+ <>
+
+ Saved URLs
+
+ {links.length}
+
+
+ }
+ >
+
+
+
+
+
+
+
+ setMode('save')} disabled={!currentUrl}>
+ Save current URL
+
+
+ >
+ )}
+
+
);
}
diff --git a/src/components/SettingsDrawer/SettingsDrawer.module.css b/src/components/SettingsDrawer/SettingsDrawer.module.css
deleted file mode 100644
index 9ed0ae4..0000000
--- a/src/components/SettingsDrawer/SettingsDrawer.module.css
+++ /dev/null
@@ -1,74 +0,0 @@
-.sectionLabel {
- display: block;
- margin-bottom: var(--space-5);
- font-size: var(--font-size-xs);
- font-weight: 600;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- color: var(--color-text-muted);
-}
-
-.grid {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- gap: var(--space-5);
-}
-
-.swatch {
- width: 100%;
- aspect-ratio: 1;
- padding: 0;
- border: 2px solid transparent;
- border-radius: var(--radius-card);
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition:
- border-color var(--transition-fast),
- transform var(--transition-fast);
-}
-
-.swatch:hover {
- transform: scale(1.06);
-}
-
-.selected {
- border-color: var(--color-text-primary);
-}
-
-/* The check is the non-color signal that a swatch is active. */
-.check {
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--swatch-check-color);
-}
-
-.reset {
- margin-top: var(--space-8);
- padding: 0;
- background: transparent;
- border: none;
- color: var(--color-text-muted);
- font-size: var(--font-size-mono);
- text-decoration: underline;
- cursor: pointer;
- transition: color var(--transition-fast);
-}
-
-.reset:hover:not(:disabled) {
- color: var(--color-text-primary);
-}
-
-.reset:disabled {
- opacity: 0.45;
- cursor: not-allowed;
-}
-
-.hint {
- margin-top: var(--space-8);
- font-size: var(--font-size-base);
- line-height: var(--line-height-base);
- color: var(--color-text-muted);
-}
diff --git a/src/components/SettingsDrawer/SettingsDrawer.tsx b/src/components/SettingsDrawer/SettingsDrawer.tsx
index 5926b4f..14110c8 100644
--- a/src/components/SettingsDrawer/SettingsDrawer.tsx
+++ b/src/components/SettingsDrawer/SettingsDrawer.tsx
@@ -1,72 +1,212 @@
-import type { CSSProperties } from 'react';
-import type { AccentColor } from '@/types';
+import { useRef, type CSSProperties } from 'react';
+import { CheckIcon, MonitorIcon, MoonIcon, SunIcon, XIcon } from 'lucide-react';
+import type { AccentColor, ThemePreference } from '@/types';
+import type { ResolvedTheme } from '@/hooks/useTheme';
import { ACCENTS } from '@/lib/accents';
import { readableOn } from '@/lib/contrast';
-import { SlidePanel } from '../SlidePanel';
-import { IconCheck } from '../icons';
-import styles from './SettingsDrawer.module.css';
+import { shortcutLabel } from '@/lib/platform';
+import {
+ segmentedControlItemVariants,
+ segmentedControlRootClassName,
+} from '@/lib/segmented-control';
+import { cn } from '@/lib/utils';
+import { useReportSheetHeight } from '@/popup/popupHeight';
+import { announce } from '@/components/LiveRegion';
+import { Button } from '@/components/ui/button';
+import { Field, FieldDescription } from '@/components/ui/field';
+import { Frame, FramePanel } from '@/components/ui/frame';
+import { Kbd } from '@/components/ui/kbd';
+import { RadioGroupPrimitive, RadioPrimitive } from '@/components/ui/radio-group';
+import { Separator } from '@/components/ui/separator';
+import {
+ Sheet,
+ SheetClose,
+ SheetHeader,
+ SheetPanel,
+ SheetPopup,
+ SheetTitle,
+} from '@/components/ui/sheet';
+import { Switch } from '@/components/ui/switch';
interface SettingsDrawerProps {
open: boolean;
onClose: () => void;
accent: AccentColor;
onAccentChange: (accent: AccentColor) => void;
+ preference: ThemePreference;
+ /** The scheme in use: the OS's while the preference is 'system'. */
+ resolved: ResolvedTheme;
+ onPreferenceChange: (preference: ThemePreference) => void;
}
-/** Settings panel — currently just the accent color picker. */
-export function SettingsDrawer({ open, onClose, accent, onAccentChange }: SettingsDrawerProps) {
+const SHEET_ID = 'settings';
+const MONO = 'mono';
+
+/** Mono first, then the palette; Mono replaces the old "Reset to monochrome" link. */
+const SWATCHES = [
+ { name: 'Mono', value: MONO },
+ ...ACCENTS.map((a) => ({ name: a.name, value: a.hex })),
+];
+
+const THEMES: { value: ThemePreference; label: string; Icon: typeof MonitorIcon }[] = [
+ { value: 'system', label: 'System', Icon: MonitorIcon },
+ { value: 'light', label: 'Light', Icon: SunIcon },
+ { value: 'dark', label: 'Dark', Icon: MoonIcon },
+];
+
+/** Settings sheet (docs/redesign §6.3). Changes apply instantly; there is no footer. */
+export function SettingsDrawer({
+ open,
+ onClose,
+ accent,
+ onAccentChange,
+ preference,
+ resolved,
+ onPreferenceChange,
+}: SettingsDrawerProps) {
+ const checkedSwatchRef = useRef(null);
+ useReportSheetHeight(open, SHEET_ID);
+
+ const selected = accent ?? MONO;
+ const selectedName = SWATCHES.find((s) => s.value === selected)?.name ?? 'Mono';
+
+ function handleAccentChange(value: unknown) {
+ const swatch = SWATCHES.find((s) => s.value === value);
+ if (!swatch) return;
+ onAccentChange(swatch.value === MONO ? null : swatch.value);
+ announce(`Accent: ${swatch.name}`);
+ }
+
+ function handleThemeChange(value: unknown) {
+ const theme = THEMES.find((t) => t.value === value);
+ if (!theme) return;
+ onPreferenceChange(theme.value);
+ announce(`Theme: ${theme.label}`);
+ }
+
return (
-
-
- Accent color
-
+ !next && onClose()}>
+
+
+ Settings
+ }>
+
+
+
- {/*
- radiogroup rather than a list of buttons: exactly one accent is active,
- and this gives arrow-key navigation and "3 of 10" announcements free.
- */}
-
- {ACCENTS.map((c) => {
- const selected = accent === c.hex;
- return (
-
onAccentChange(c.hex)}
+
+
+
+
Accent color
+ {/* The name, so colour is never the only cue. */}
+
{selectedName}
+
+
- {selected && (
-
-
-
- )}
-
- );
- })}
-
+ {SWATCHES.map((swatch) => {
+ const isMono = swatch.value === MONO;
+ const style: CSSProperties = {
+ background: isMono ? 'var(--foreground)' : swatch.value,
+ color: isMono ? 'var(--background)' : readableOn(swatch.value),
+ };
+ return (
+
+
+
+
+
+ );
+ })}
+
+
+ Tints the primary button, switches and URL keys. Text on the accent is picked to keep
+ 4.5:1.
+
+
- onAccentChange(null)}
- disabled={accent === null}
- >
- Reset to monochrome
-
+
+ Preview
+ {/* Shows exactly what the accent changes; not interactive. */}
+
+
+
+ ?view =
+ table &
+ live =
+ true
+
+
+
+
+ true
+
+
+ Apply
+
+ {shortcutLabel('↵')}
+
+
+
+
+
+
-
- Light and dark follow your system appearance automatically.
-
-
+
+
+
+ Theme
+
+ {THEMES.map(({ value, label, Icon }) => (
+
+
+ {label}
+
+ ))}
+
+
+ {preference === 'system'
+ ? `Follows your OS appearance — ${resolved} right now.`
+ : 'Overrides your OS appearance for QueryCraft only.'}
+
+
+
+
+
);
}
diff --git a/src/components/SlidePanel/SlidePanel.module.css b/src/components/SlidePanel/SlidePanel.module.css
deleted file mode 100644
index 8fcfeb4..0000000
--- a/src/components/SlidePanel/SlidePanel.module.css
+++ /dev/null
@@ -1,47 +0,0 @@
-/* Full-width overlay that slides in over the editor. */
-.panel {
- position: absolute;
- inset: 0;
- z-index: 25;
- display: flex;
- flex-direction: column;
- background: var(--color-bg);
- transform: translateX(100%);
- /*
- * `visibility` is what actually takes the closed panel out of the tab order
- * and the accessibility tree. It flips to visible at the start of the
- * transition and to hidden only at its end, so both directions animate.
- */
- visibility: hidden;
- transition:
- transform var(--transition-panel),
- visibility var(--transition-panel);
-}
-
-.open {
- transform: translateX(0);
- visibility: visible;
-}
-
-.header {
- display: flex;
- align-items: center;
- gap: var(--space-5);
- padding: var(--pad-block) var(--pad-inline);
- border-bottom: 1px solid var(--color-border);
- flex-shrink: 0;
-}
-
-.title {
- flex: 1;
- min-width: 0;
- font-size: var(--font-size-xl);
- font-weight: 600;
- color: var(--color-text-primary);
-}
-
-.body {
- flex: 1;
- overflow-y: auto;
- padding: var(--pad-block) var(--pad-inline);
-}
diff --git a/src/components/SlidePanel/SlidePanel.tsx b/src/components/SlidePanel/SlidePanel.tsx
deleted file mode 100644
index c74cf44..0000000
--- a/src/components/SlidePanel/SlidePanel.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import type { ReactNode } from 'react';
-import { IconButton } from '../IconButton';
-import { IconChevronLeft, IconClose } from '../icons';
-import { useFocusTrap } from '@/hooks/useFocusTrap';
-import styles from './SlidePanel.module.css';
-
-interface SlidePanelProps {
- open: boolean;
- title: string;
- /** Stable id for the heading, referenced by aria-labelledby. */
- titleId: string;
- onClose: () => void;
- /** When given, a back chevron is shown to the left of the title. */
- onBack?: () => void;
- backLabel?: string;
- children: ReactNode;
-}
-
-/**
- * The shared chrome for every side panel: slides in from the right, traps
- * focus, and renders a header of [optional back] · title · close.
- *
- * Kept mounted while closed so it can animate both ways; the stylesheet's
- * `visibility: hidden` is what removes it from the tab order meanwhile.
- */
-export function SlidePanel({
- open,
- title,
- titleId,
- onClose,
- onBack,
- backLabel = 'Back',
- children,
-}: SlidePanelProps) {
- const trapRef = useFocusTrap(open, onClose);
-
- return (
-
-
- {onBack && (
- } onClick={onBack} />
- )}
-
- {title}
-
- }
- onClick={onClose}
- />
-
-
-
{children}
-
- );
-}
diff --git a/src/components/SlidePanel/index.ts b/src/components/SlidePanel/index.ts
deleted file mode 100644
index e195955..0000000
--- a/src/components/SlidePanel/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { SlidePanel } from './SlidePanel';
diff --git a/src/components/Toast/Toast.module.css b/src/components/Toast/Toast.module.css
deleted file mode 100644
index 2645451..0000000
--- a/src/components/Toast/Toast.module.css
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * The live region is always in the DOM so screen readers keep observing it;
- * only the visible pill mounts and unmounts.
- */
-.region {
- position: absolute;
- bottom: 70px;
- left: 50%;
- transform: translateX(-50%);
- z-index: 30;
- pointer-events: none;
- max-width: calc(100% - 36px);
-}
-
-.toast {
- display: block;
- background: var(--color-toast-bg);
- border: 1px solid var(--color-toast-border);
- color: var(--color-toast-text);
- font-size: var(--font-size-base);
- font-weight: 500;
- padding: 8px 14px;
- border-radius: var(--radius-full);
- box-shadow: var(--shadow-md);
- text-align: center;
- animation: toast-in var(--transition-base) ease-out;
-}
-
-@keyframes toast-in {
- from {
- opacity: 0;
- transform: translateY(6px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx
deleted file mode 100644
index bc7174c..0000000
--- a/src/components/Toast/Toast.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { useEffect, useState } from 'react';
-import styles from './Toast.module.css';
-
-interface ToastProps {
- /** Changing this shows the pill and re-announces. Cleared after `duration`. */
- message: string;
- duration?: number;
- politeness?: 'polite' | 'assertive';
-}
-
-/**
- * Floating status pill, bottom-centered, auto-dismissing.
- *
- * This doubles as the app's single aria-live region: the visible toast and the
- * announcement are the same node, so screen-reader users hear each status once
- * rather than twice (which is what a separate hidden region would cause).
- */
-export function Toast({ message, duration = 1600, politeness = 'polite' }: ToastProps) {
- const [text, setText] = useState('');
-
- useEffect(() => {
- if (!message) return;
- // Clear first, then set — ensures repeated identical messages re-announce.
- setText('');
- const show = window.setTimeout(() => setText(message), 50);
- const hide = window.setTimeout(() => setText(''), duration + 50);
- return () => {
- window.clearTimeout(show);
- window.clearTimeout(hide);
- };
- }, [message, duration]);
-
- return (
-
- {text && {text} }
-
- );
-}
diff --git a/src/components/Toast/index.ts b/src/components/Toast/index.ts
deleted file mode 100644
index 03e94bf..0000000
--- a/src/components/Toast/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { Toast } from './Toast';
diff --git a/src/components/ui/autocomplete.tsx b/src/components/ui/autocomplete.tsx
new file mode 100644
index 0000000..5eb64c9
--- /dev/null
+++ b/src/components/ui/autocomplete.tsx
@@ -0,0 +1,305 @@
+"use client";
+
+import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
+import { ChevronsUpDownIcon, XIcon } from "lucide-react";
+import type React from "react";
+import { cn } from "@/lib/utils";
+import { Input } from "@/components/ui/input";
+import { ScrollArea } from "@/components/ui/scroll-area";
+
+export const Autocomplete: typeof AutocompletePrimitive.Root =
+ AutocompletePrimitive.Root;
+
+export function AutocompleteInput({
+ className,
+ showTrigger = false,
+ showClear = false,
+ startAddon,
+ size,
+ triggerProps,
+ clearProps,
+ ...props
+}: Omit & {
+ showTrigger?: boolean;
+ showClear?: boolean;
+ startAddon?: React.ReactNode;
+ size?: "sm" | "default" | "lg" | number;
+ ref?: React.Ref;
+ triggerProps?: AutocompletePrimitive.Trigger.Props;
+ clearProps?: AutocompletePrimitive.Clear.Props;
+}): React.ReactElement {
+ const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
+
+ return (
+
+ {startAddon && (
+
+ {startAddon}
+
+ )}
+ }
+ {...props}
+ />
+ {showTrigger && (
+
+
+
+
+
+ )}
+ {showClear && (
+
+
+
+ )}
+
+ );
+}
+
+export function AutocompletePopup({
+ className,
+ children,
+ side = "bottom",
+ sideOffset = 4,
+ alignOffset,
+ align = "start",
+ anchor,
+ portalProps,
+ ...props
+}: AutocompletePrimitive.Popup.Props & {
+ align?: AutocompletePrimitive.Positioner.Props["align"];
+ sideOffset?: AutocompletePrimitive.Positioner.Props["sideOffset"];
+ alignOffset?: AutocompletePrimitive.Positioner.Props["alignOffset"];
+ side?: AutocompletePrimitive.Positioner.Props["side"];
+ anchor?: AutocompletePrimitive.Positioner.Props["anchor"];
+ portalProps?: AutocompletePrimitive.Portal.Props;
+}): React.ReactElement {
+ return (
+
+
+
+ {/* QueryCraft density: --popup-max-h lets a popup cap itself (e.g. 12rem) without forking */}
+
+ {children}
+
+
+
+
+ );
+}
+
+export function AutocompleteItem({
+ className,
+ children,
+ ...props
+}: AutocompletePrimitive.Item.Props): React.ReactElement {
+ return (
+
+ {children}
+
+ );
+}
+
+export function AutocompleteSeparator({
+ className,
+ ...props
+}: AutocompletePrimitive.Separator.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function AutocompleteGroup({
+ className,
+ ...props
+}: AutocompletePrimitive.Group.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function AutocompleteGroupLabel({
+ className,
+ ...props
+}: AutocompletePrimitive.GroupLabel.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function AutocompleteEmpty({
+ className,
+ ...props
+}: AutocompletePrimitive.Empty.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function AutocompleteRow({
+ className,
+ ...props
+}: AutocompletePrimitive.Row.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export const AutocompleteValue: typeof AutocompletePrimitive.Value =
+ AutocompletePrimitive.Value;
+
+export function AutocompleteList({
+ className,
+ ...props
+}: AutocompletePrimitive.List.Props): React.ReactElement {
+ return (
+
+
+
+ );
+}
+
+export function AutocompleteClear({
+ className,
+ ...props
+}: AutocompletePrimitive.Clear.Props): React.ReactElement {
+ return (
+
+
+
+ );
+}
+
+export function AutocompleteStatus({
+ className,
+ ...props
+}: AutocompletePrimitive.Status.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export const AutocompleteCollection: typeof AutocompletePrimitive.Collection =
+ AutocompletePrimitive.Collection;
+
+export function AutocompleteTrigger({
+ className,
+ children,
+ ...props
+}: AutocompletePrimitive.Trigger.Props): React.ReactElement {
+ return (
+
+ {children}
+
+ );
+}
+
+export const useAutocompleteFilter: typeof AutocompletePrimitive.useFilter =
+ AutocompletePrimitive.useFilter;
+
+export { AutocompletePrimitive };
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
new file mode 100644
index 0000000..e01d08f
--- /dev/null
+++ b/src/components/ui/badge.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { mergeProps } from "@base-ui/react/merge-props";
+import { useRender } from "@base-ui/react/use-render";
+import { cva, type VariantProps } from "class-variance-authority";
+import type React from "react";
+import { cn } from "@/lib/utils";
+
+export const badgeVariants = cva(
+ "relative inline-flex shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-sm border border-transparent font-medium outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-3.5 sm:[&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0 [button&,a&]:cursor-pointer [button&,a&]:pointer-coarse:after:absolute [button&,a&]:pointer-coarse:after:size-full [button&,a&]:pointer-coarse:after:min-h-11 [button&,a&]:pointer-coarse:after:min-w-11",
+ {
+ defaultVariants: {
+ size: "default",
+ variant: "default",
+ },
+ variants: {
+ size: {
+ default:
+ "h-5.5 min-w-5.5 px-[calc(--spacing(1)-1px)] text-sm sm:h-4.5 sm:min-w-4.5 sm:text-xs",
+ lg: "h-6.5 min-w-6.5 px-[calc(--spacing(1.5)-1px)] text-base sm:h-5.5 sm:min-w-5.5 sm:text-sm",
+ sm: "h-5 min-w-5 rounded-[.25rem] px-[calc(--spacing(1)-1px)] text-xs sm:h-4 sm:min-w-4 sm:text-[.625rem]",
+ },
+ variant: {
+ default:
+ "bg-primary text-primary-foreground [button&,a&]:hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white [button&,a&]:hover:bg-destructive/90",
+ error:
+ "bg-destructive/8 text-destructive-foreground dark:bg-destructive/16",
+ info: "bg-info/8 text-info-foreground dark:bg-info/16",
+ outline:
+ "border-input bg-background text-foreground dark:bg-input/32 [button&,a&]:hover:bg-accent/50 dark:[button&,a&]:hover:bg-input/48",
+ secondary:
+ "bg-secondary text-secondary-foreground [button&,a&]:hover:bg-secondary/90",
+ success: "bg-success/8 text-success-foreground dark:bg-success/16",
+ warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16",
+ },
+ },
+ },
+);
+
+export interface BadgeProps extends useRender.ComponentProps<"span"> {
+ variant?: VariantProps["variant"];
+ size?: VariantProps["size"];
+}
+
+export function Badge({
+ className,
+ variant,
+ size,
+ render,
+ ...props
+}: BadgeProps): React.ReactElement {
+ const defaultProps = {
+ className: cn(badgeVariants({ className, size, variant })),
+ "data-slot": "badge",
+ };
+
+ return useRender({
+ defaultTagName: "span",
+ props: mergeProps<"span">(defaultProps, props),
+ render,
+ });
+}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..66bbb89
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+import { mergeProps } from "@base-ui/react/merge-props";
+import { useRender } from "@base-ui/react/use-render";
+import { cva, type VariantProps } from "class-variance-authority";
+import type * as React from "react";
+import { cn } from "@/lib/utils";
+import { Spinner } from "@/components/ui/spinner";
+
+export const buttonVariants = cva(
+ "relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 data-loading:select-none data-loading:text-transparent sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0",
+ {
+ defaultVariants: {
+ size: "default",
+ variant: "default",
+ },
+ variants: {
+ size: {
+ default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8",
+ icon: "size-9 sm:size-8",
+ "icon-lg": "size-10 sm:size-9",
+ "icon-sm": "size-8 sm:size-7",
+ "icon-xl":
+ "size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
+ "icon-xs":
+ "size-7 rounded-md before:rounded-[calc(var(--radius-md)-1px)] sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5",
+ lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",
+ sm: "h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7",
+ xl: "h-11 px-[calc(--spacing(4)-1px)] text-lg sm:h-10 sm:text-base [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
+ xs: "h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-sm before:rounded-[calc(var(--radius-md)-1px)] sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5",
+ },
+ variant: {
+ default:
+ "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90 data-pressed:bg-primary/90 *:data-[slot=button-loading-indicator]:text-primary-foreground [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none",
+ destructive:
+ "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs hover:bg-destructive/90 data-pressed:bg-destructive/90 *:data-[slot=button-loading-indicator]:text-white [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none",
+ "destructive-outline":
+ "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:border-destructive/32 hover:bg-destructive/4 data-pressed:border-destructive/32 data-pressed:bg-destructive/4 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none",
+ ghost:
+ "border-transparent text-foreground hover:bg-accent data-pressed:bg-accent *:data-[slot=button-loading-indicator]:text-foreground",
+ link: "border-transparent text-foreground underline-offset-4 hover:underline data-pressed:underline *:data-[slot=button-loading-indicator]:text-foreground",
+ outline:
+ "border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:bg-accent/50 data-pressed:bg-accent/50 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:data-pressed:bg-input/64 dark:hover:bg-input/64 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90 *:data-[slot=button-loading-indicator]:text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80",
+ },
+ },
+ },
+);
+
+export interface ButtonProps extends useRender.ComponentProps<"button"> {
+ variant?: VariantProps["variant"];
+ size?: VariantProps["size"];
+ loading?: boolean;
+}
+
+export function Button({
+ className,
+ variant,
+ size,
+ render,
+ children,
+ loading = false,
+ disabled: disabledProp,
+ ...props
+}: ButtonProps): React.ReactElement {
+ const isDisabled: boolean = Boolean(loading || disabledProp);
+ const typeValue: React.ButtonHTMLAttributes["type"] =
+ render ? undefined : "button";
+
+ const defaultProps = {
+ children: (
+ <>
+ {children}
+ {loading && (
+
+ )}
+ >
+ ),
+ className: cn(buttonVariants({ className, size, variant })),
+ "aria-disabled": loading || undefined,
+ "data-loading": loading ? "" : undefined,
+ "data-slot": "button",
+ disabled: isDisabled,
+ type: typeValue,
+ };
+
+ return useRender({
+ defaultTagName: "button",
+ props: mergeProps<"button">(defaultProps, props),
+ render,
+ });
+}
diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx
new file mode 100644
index 0000000..606e94b
--- /dev/null
+++ b/src/components/ui/collapsible.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
+import type React from "react";
+import { cn } from "@/lib/utils";
+
+export function Collapsible({
+ ...props
+}: CollapsiblePrimitive.Root.Props): React.ReactElement {
+ return ;
+}
+
+export function CollapsibleTrigger({
+ className,
+ ...props
+}: CollapsiblePrimitive.Trigger.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CollapsiblePanel({
+ className,
+ ...props
+}: CollapsiblePrimitive.Panel.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export { CollapsiblePrimitive, CollapsiblePanel as CollapsibleContent };
diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx
new file mode 100644
index 0000000..1e4b7de
--- /dev/null
+++ b/src/components/ui/combobox.tsx
@@ -0,0 +1,447 @@
+"use client";
+
+import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox";
+import { ChevronsUpDownIcon, XIcon } from "lucide-react";
+import * as React from "react";
+import { cn } from "@/lib/utils";
+import { Input } from "@/components/ui/input";
+import { ScrollArea } from "@/components/ui/scroll-area";
+
+export const ComboboxContext: React.Context<{
+ chipsRef: React.RefObject | null;
+ multiple: boolean;
+}> = React.createContext<{
+ chipsRef: React.RefObject | null;
+ multiple: boolean;
+}>({
+ chipsRef: null,
+ multiple: false,
+});
+
+export function Combobox<
+ Value,
+ Multiple extends boolean | undefined = false,
+ Item = Value,
+>(
+ props: ComboboxPrimitive.Root.Props,
+): React.ReactElement {
+ const chipsRef = React.useRef(null);
+ return (
+
+
+
+ );
+}
+
+export function ComboboxChipsInput({
+ className,
+ size,
+ ...props
+}: Omit & {
+ size?: "sm" | "default" | "lg" | number;
+ ref?: React.Ref;
+}): React.ReactElement {
+ const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
+
+ return (
+
+ );
+}
+
+export function ComboboxInput({
+ className,
+ showTrigger = true,
+ showClear = false,
+ startAddon,
+ size,
+ triggerProps,
+ clearProps,
+ ...props
+}: Omit & {
+ showTrigger?: boolean;
+ showClear?: boolean;
+ startAddon?: React.ReactNode;
+ size?: "sm" | "default" | "lg" | number;
+ ref?: React.Ref;
+ triggerProps?: ComboboxPrimitive.Trigger.Props;
+ clearProps?: ComboboxPrimitive.Clear.Props;
+}): React.ReactElement {
+ const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
+
+ return (
+
+ {startAddon && (
+
+ {startAddon}
+
+ )}
+
+ }
+ {...props}
+ />
+ {showTrigger && (
+
+
+
+
+
+ )}
+ {showClear && (
+
+
+
+ )}
+
+ );
+}
+
+export function ComboboxTrigger({
+ className,
+ children,
+ ...props
+}: ComboboxPrimitive.Trigger.Props): React.ReactElement {
+ return (
+
+ {children}
+
+ );
+}
+
+export function ComboboxPopup({
+ className,
+ children,
+ side = "bottom",
+ sideOffset = 4,
+ // QueryCraft density: keep popups 8px inside the 380px popup (docs/redesign §5.3)
+ collisionPadding = 8,
+ alignOffset,
+ align = "start",
+ anchor: anchorProp,
+ portalProps,
+ ...props
+}: ComboboxPrimitive.Popup.Props & {
+ align?: ComboboxPrimitive.Positioner.Props["align"];
+ sideOffset?: ComboboxPrimitive.Positioner.Props["sideOffset"];
+ collisionPadding?: ComboboxPrimitive.Positioner.Props["collisionPadding"];
+ alignOffset?: ComboboxPrimitive.Positioner.Props["alignOffset"];
+ side?: ComboboxPrimitive.Positioner.Props["side"];
+ anchor?: ComboboxPrimitive.Positioner.Props["anchor"];
+ portalProps?: ComboboxPrimitive.Portal.Props;
+}): React.ReactElement {
+ const { chipsRef } = React.useContext(ComboboxContext);
+ const anchor = anchorProp ?? chipsRef;
+
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export function ComboboxItem({
+ className,
+ children,
+ ...props
+}: ComboboxPrimitive.Item.Props): React.ReactElement {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
+
+export function ComboboxSeparator({
+ className,
+ ...props
+}: ComboboxPrimitive.Separator.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function ComboboxGroup({
+ className,
+ ...props
+}: ComboboxPrimitive.Group.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function ComboboxGroupLabel({
+ className,
+ ...props
+}: ComboboxPrimitive.GroupLabel.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function ComboboxEmpty({
+ className,
+ ...props
+}: ComboboxPrimitive.Empty.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function ComboboxRow({
+ className,
+ ...props
+}: ComboboxPrimitive.Row.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export const ComboboxValue: typeof ComboboxPrimitive.Value =
+ ComboboxPrimitive.Value;
+
+export function ComboboxList({
+ className,
+ ...props
+}: ComboboxPrimitive.List.Props): React.ReactElement {
+ return (
+
+
+
+ );
+}
+
+export function ComboboxClear({
+ className,
+ ...props
+}: ComboboxPrimitive.Clear.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function ComboboxStatus({
+ className,
+ ...props
+}: ComboboxPrimitive.Status.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export const ComboboxCollection: typeof ComboboxPrimitive.Collection =
+ ComboboxPrimitive.Collection;
+
+export function ComboboxChips({
+ className,
+ children,
+ startAddon,
+ ...props
+}: ComboboxPrimitive.Chips.Props & {
+ startAddon?: React.ReactNode;
+}): React.ReactElement {
+ const { chipsRef } = React.useContext(ComboboxContext);
+
+ return (
+ | null}
+ {...props}
+ >
+ {startAddon && (
+
+ {startAddon}
+
+ )}
+ {children}
+
+ );
+}
+
+export function ComboboxChip({
+ children,
+ removeProps,
+ ...props
+}: ComboboxPrimitive.Chip.Props & {
+ removeProps?: ComboboxPrimitive.ChipRemove.Props;
+}): React.ReactElement {
+ return (
+
+ {children}
+
+
+ );
+}
+
+export function ComboboxChipRemove(
+ props: ComboboxPrimitive.ChipRemove.Props,
+): React.ReactElement {
+ return (
+
+
+
+ );
+}
+
+export const createComboboxItems: typeof ComboboxPrimitive.createItems =
+ ComboboxPrimitive.createItems;
+
+export const useComboboxFilter: typeof ComboboxPrimitive.useFilter =
+ ComboboxPrimitive.useFilter;
+
+export { ComboboxPrimitive };
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
new file mode 100644
index 0000000..c1842c2
--- /dev/null
+++ b/src/components/ui/command.tsx
@@ -0,0 +1,265 @@
+"use client";
+
+import { Dialog as CommandDialogPrimitive } from "@base-ui/react/dialog";
+import { SearchIcon } from "lucide-react";
+import type * as React from "react";
+import { cn } from "@/lib/utils";
+import {
+ Autocomplete,
+ AutocompleteCollection,
+ AutocompleteEmpty,
+ AutocompleteGroup,
+ AutocompleteGroupLabel,
+ AutocompleteInput,
+ AutocompleteItem,
+ AutocompleteList,
+ AutocompleteSeparator,
+} from "@/components/ui/autocomplete";
+
+export const CommandDialog: typeof CommandDialogPrimitive.Root =
+ CommandDialogPrimitive.Root;
+
+export const CommandDialogPortal: typeof CommandDialogPrimitive.Portal =
+ CommandDialogPrimitive.Portal;
+
+export const CommandCreateHandle: typeof CommandDialogPrimitive.createHandle =
+ CommandDialogPrimitive.createHandle;
+
+export function CommandDialogTrigger(
+ props: CommandDialogPrimitive.Trigger.Props,
+): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandDialogBackdrop({
+ className,
+ ...props
+}: CommandDialogPrimitive.Backdrop.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandDialogViewport({
+ className,
+ ...props
+}: CommandDialogPrimitive.Viewport.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandDialogPopup({
+ className,
+ children,
+ portalProps,
+ ...props
+}: CommandDialogPrimitive.Popup.Props & {
+ portalProps?: CommandDialogPrimitive.Portal.Props;
+}): React.ReactElement {
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+}
+
+export function Command({
+ autoHighlight = "always",
+ keepHighlight = true,
+ ...props
+}: React.ComponentProps): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandInput({
+ className,
+ placeholder = undefined,
+ ...props
+}: React.ComponentProps): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandList({
+ className,
+ ...props
+}: React.ComponentProps): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandEmpty({
+ className,
+ ...props
+}: React.ComponentProps): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandPanel({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandGroup({
+ className,
+ ...props
+}: React.ComponentProps): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandGroupLabel({
+ className,
+ ...props
+}: React.ComponentProps): React.ReactElement {
+ return (
+
+ );
+}
+
+export const CommandCollection = AutocompleteCollection;
+
+export function CommandItem({
+ className,
+ ...props
+}: React.ComponentProps): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandSeparator({
+ className,
+ ...props
+}: React.ComponentProps): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"kbd">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function CommandFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export { CommandDialogPrimitive };
diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx
new file mode 100644
index 0000000..21ddae1
--- /dev/null
+++ b/src/components/ui/context-menu.tsx
@@ -0,0 +1,348 @@
+"use client";
+
+import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu";
+import { ChevronRightIcon } from "lucide-react";
+import type * as React from "react";
+import { cn } from "@/lib/utils";
+
+export const ContextMenu: typeof ContextMenuPrimitive.Root =
+ ContextMenuPrimitive.Root;
+
+export const ContextMenuPortal: typeof ContextMenuPrimitive.Portal =
+ ContextMenuPrimitive.Portal;
+
+export function ContextMenuTrigger({
+ className,
+ children,
+ ...props
+}: ContextMenuPrimitive.Trigger.Props): React.ReactElement {
+ return (
+
+ {children}
+
+ );
+}
+
+export function ContextMenuPopup({
+ children,
+ className,
+ sideOffset = 4,
+ align = "center",
+ alignOffset,
+ side = "bottom",
+ anchor,
+ portalProps,
+ ...props
+}: ContextMenuPrimitive.Popup.Props & {
+ align?: ContextMenuPrimitive.Positioner.Props["align"];
+ sideOffset?: ContextMenuPrimitive.Positioner.Props["sideOffset"];
+ alignOffset?: ContextMenuPrimitive.Positioner.Props["alignOffset"];
+ side?: ContextMenuPrimitive.Positioner.Props["side"];
+ anchor?: ContextMenuPrimitive.Positioner.Props["anchor"];
+ portalProps?: ContextMenuPrimitive.Portal.Props;
+}): React.ReactElement {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export function ContextMenuGroup(
+ props: ContextMenuPrimitive.Group.Props,
+): React.ReactElement {
+ return (
+
+ );
+}
+
+export function ContextMenuItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}: ContextMenuPrimitive.Item.Props & {
+ inset?: boolean;
+ variant?: "default" | "destructive";
+}): React.ReactElement {
+ return (
+ svg:not([class*='opacity-'])]:opacity-80 [&>svg:not([class*='size-'])]:size-4.5 sm:[&>svg:not([class*='size-'])]:size-4 [&>svg]:pointer-events-none [&>svg]:-mx-0.5 [&>svg]:shrink-0",
+ className,
+ )}
+ data-inset={inset}
+ data-slot="context-menu-item"
+ data-variant={variant}
+ {...props}
+ />
+ );
+}
+
+export function ContextMenuLinkItem({
+ className,
+ inset,
+ variant = "default",
+ closeOnClick = true,
+ ...props
+}: ContextMenuPrimitive.LinkItem.Props & {
+ inset?: boolean;
+ variant?: "default" | "destructive";
+}): React.ReactElement {
+ return (
+ svg:not([class*='opacity-'])]:opacity-80 [&>svg:not([class*='size-'])]:size-4.5 sm:[&>svg:not([class*='size-'])]:size-4 [&>svg]:pointer-events-none [&>svg]:-mx-0.5 [&>svg]:shrink-0",
+ className,
+ )}
+ closeOnClick={closeOnClick}
+ data-inset={inset}
+ data-slot="context-menu-link-item"
+ data-variant={variant}
+ {...props}
+ />
+ );
+}
+
+export function ContextMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ variant = "default",
+ ...props
+}: ContextMenuPrimitive.CheckboxItem.Props & {
+ variant?: "default" | "switch";
+}): React.ReactElement {
+ return (
+
+ {variant === "switch" ? (
+ <>
+ {children}
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+
+ {children}
+ >
+ )}
+
+ );
+}
+
+export function ContextMenuRadioGroup(
+ props: ContextMenuPrimitive.RadioGroup.Props,
+): React.ReactElement {
+ return (
+
+ );
+}
+
+export function ContextMenuRadioItem({
+ className,
+ children,
+ ...props
+}: ContextMenuPrimitive.RadioItem.Props): React.ReactElement {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
+
+export function ContextMenuGroupLabel({
+ className,
+ inset,
+ ...props
+}: ContextMenuPrimitive.GroupLabel.Props & {
+ inset?: boolean;
+}): React.ReactElement {
+ return (
+
+ );
+}
+
+export function ContextMenuSeparator({
+ className,
+ ...props
+}: ContextMenuPrimitive.Separator.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function ContextMenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"kbd">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function ContextMenuSub(
+ props: ContextMenuPrimitive.SubmenuRoot.Props,
+): React.ReactElement {
+ return (
+
+ );
+}
+
+export function ContextMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: ContextMenuPrimitive.SubmenuTrigger.Props & {
+ inset?: boolean;
+}): React.ReactElement {
+ return (
+ svg:not(:last-child)]:-mx-0.5 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
+ className,
+ )}
+ data-inset={inset}
+ data-slot="context-menu-sub-trigger"
+ {...props}
+ >
+ {children}
+
+
+ );
+}
+
+export function ContextMenuSubPopup({
+ className,
+ sideOffset = 0,
+ alignOffset,
+ align = "start",
+ ...props
+}: ContextMenuPrimitive.Popup.Props & {
+ align?: ContextMenuPrimitive.Positioner.Props["align"];
+ sideOffset?: ContextMenuPrimitive.Positioner.Props["sideOffset"];
+ alignOffset?: ContextMenuPrimitive.Positioner.Props["alignOffset"];
+}): React.ReactElement {
+ const defaultAlignOffset = align !== "center" ? -5 : undefined;
+
+ return (
+
+ );
+}
+
+export { ContextMenuPrimitive };
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..f562953
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -0,0 +1,220 @@
+"use client";
+
+import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
+import { mergeProps } from "@base-ui/react/merge-props";
+import { useRender } from "@base-ui/react/use-render";
+import { XIcon } from "lucide-react";
+import type React from "react";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { ScrollArea } from "@/components/ui/scroll-area";
+
+export const DialogCreateHandle: typeof DialogPrimitive.createHandle =
+ DialogPrimitive.createHandle;
+
+export const Dialog: typeof DialogPrimitive.Root = DialogPrimitive.Root;
+
+export const DialogPortal: typeof DialogPrimitive.Portal =
+ DialogPrimitive.Portal;
+
+export function DialogTrigger(
+ props: DialogPrimitive.Trigger.Props,
+): React.ReactElement {
+ return ;
+}
+
+export function DialogClose(
+ props: DialogPrimitive.Close.Props,
+): React.ReactElement {
+ return ;
+}
+
+export function DialogBackdrop({
+ className,
+ ...props
+}: DialogPrimitive.Backdrop.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function DialogViewport({
+ className,
+ ...props
+}: DialogPrimitive.Viewport.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function DialogPopup({
+ className,
+ children,
+ showCloseButton = true,
+ bottomStickOnMobile = true,
+ closeProps,
+ portalProps,
+ ...props
+}: DialogPrimitive.Popup.Props & {
+ showCloseButton?: boolean;
+ bottomStickOnMobile?: boolean;
+ closeProps?: DialogPrimitive.Close.Props;
+ portalProps?: DialogPrimitive.Portal.Props;
+}): React.ReactElement {
+ return (
+
+
+
+
+ {children}
+ {showCloseButton && (
+ }
+ {...closeProps}
+ >
+
+
+ )}
+
+
+
+ );
+}
+
+export function DialogHeader({
+ className,
+ render,
+ ...props
+}: useRender.ComponentProps<"div">): React.ReactElement {
+ const defaultProps = {
+ className: cn(
+ "flex flex-col gap-2 p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pb-3 max-sm:pb-4",
+ className,
+ ),
+ "data-slot": "dialog-header",
+ };
+
+ return useRender({
+ defaultTagName: "div",
+ props: mergeProps<"div">(defaultProps, props),
+ render,
+ });
+}
+
+export function DialogFooter({
+ className,
+ variant = "default",
+ render,
+ ...props
+}: useRender.ComponentProps<"div"> & {
+ variant?: "default" | "bare";
+}): React.ReactElement {
+ const defaultProps = {
+ className: cn(
+ "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]",
+ variant === "default" && "border-t bg-muted/72 py-4",
+ variant === "bare" &&
+ "in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pt-3 pt-4 pb-6",
+ className,
+ ),
+ "data-slot": "dialog-footer",
+ };
+
+ return useRender({
+ defaultTagName: "div",
+ props: mergeProps<"div">(defaultProps, props),
+ render,
+ });
+}
+
+export function DialogTitle({
+ className,
+ ...props
+}: DialogPrimitive.Title.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function DialogDescription({
+ className,
+ ...props
+}: DialogPrimitive.Description.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function DialogPanel({
+ className,
+ scrollFade = true,
+ render,
+ ...props
+}: useRender.ComponentProps<"div"> & {
+ scrollFade?: boolean;
+}): React.ReactElement {
+ const defaultProps = {
+ className: cn(
+ "p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-header])]:pt-1 in-[[data-slot=dialog-popup]:has([data-slot=dialog-footer]:not(.border-t))]:pb-1",
+ className,
+ ),
+ "data-slot": "dialog-panel",
+ };
+
+ return (
+
+ {useRender({
+ defaultTagName: "div",
+ props: mergeProps<"div">(defaultProps, props),
+ render,
+ })}
+
+ );
+}
+
+export {
+ DialogPrimitive,
+ DialogBackdrop as DialogOverlay,
+ DialogPopup as DialogContent,
+};
diff --git a/src/components/ui/empty.tsx b/src/components/ui/empty.tsx
new file mode 100644
index 0000000..48eb441
--- /dev/null
+++ b/src/components/ui/empty.tsx
@@ -0,0 +1,134 @@
+import { cva, type VariantProps } from "class-variance-authority";
+import type React from "react";
+import { cn } from "@/lib/utils";
+
+const emptyMediaVariants = cva(
+ "flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
+ {
+ defaultVariants: {
+ variant: "default",
+ },
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ icon: "relative flex size-9 shrink-0 items-center justify-center rounded-md border bg-card not-dark:bg-clip-padding text-foreground shadow-sm/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5",
+ },
+ },
+ },
+);
+
+export function Empty({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function EmptyHeader({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function EmptyMedia({
+ className,
+ variant = "default",
+ ...props
+}: React.ComponentProps<"div"> &
+ VariantProps): React.ReactElement {
+ return (
+
+ {variant === "icon" && (
+ <>
+
+
+ >
+ )}
+
+
+ );
+}
+
+export function EmptyTitle({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function EmptyDescription({
+ className,
+ ...props
+}: React.ComponentProps<"p">): React.ReactElement {
+ return (
+ a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4 [[data-slot=empty-title]+&]:mt-1",
+ className,
+ )}
+ data-slot="empty-description"
+ {...props}
+ />
+ );
+}
+
+export function EmptyContent({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx
new file mode 100644
index 0000000..ca97f9c
--- /dev/null
+++ b/src/components/ui/field.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import { Field as FieldPrimitive } from "@base-ui/react/field";
+import type React from "react";
+import { cn } from "@/lib/utils";
+
+export function Field({
+ className,
+ ...props
+}: FieldPrimitive.Root.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FieldLabel({
+ className,
+ ...props
+}: FieldPrimitive.Label.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FieldItem({
+ className,
+ ...props
+}: FieldPrimitive.Item.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FieldDescription({
+ className,
+ ...props
+}: FieldPrimitive.Description.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FieldError({
+ className,
+ ...props
+}: FieldPrimitive.Error.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export const FieldControl: typeof FieldPrimitive.Control =
+ FieldPrimitive.Control;
+export const FieldValidity: typeof FieldPrimitive.Validity =
+ FieldPrimitive.Validity;
+
+export { FieldPrimitive };
diff --git a/src/components/ui/frame.tsx b/src/components/ui/frame.tsx
new file mode 100644
index 0000000..8b96910
--- /dev/null
+++ b/src/components/ui/frame.tsx
@@ -0,0 +1,87 @@
+import type * as React from "react";
+import { cn } from "@/lib/utils";
+
+export function Frame({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FramePanel({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FrameHeader({
+ className,
+ ...props
+}: React.ComponentProps<"header">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FrameTitle({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FrameDescription({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function FrameFooter({
+ className,
+ ...props
+}: React.ComponentProps<"footer">): React.ReactElement {
+ return (
+
+ );
+}
diff --git a/src/components/ui/group.tsx b/src/components/ui/group.tsx
new file mode 100644
index 0000000..0a07154
--- /dev/null
+++ b/src/components/ui/group.tsx
@@ -0,0 +1,92 @@
+"use client";
+
+import { mergeProps } from "@base-ui/react/merge-props";
+import { useRender } from "@base-ui/react/use-render";
+import { cva, type VariantProps } from "class-variance-authority";
+import type * as React from "react";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
+
+export const groupVariants = cva(
+ "flex w-fit *:focus-visible:z-1 has-[>[data-slot=group]]:gap-2 *:has-focus-visible:z-1 dark:*:[[data-slot=separator]:has(~button:hover):not(:has(~[data-slot=separator]~[data-slot]:hover)),[data-slot=separator]:has(~[data-slot][data-pressed]):not(:has(~[data-slot=separator]~[data-slot][data-pressed]))]:before:bg-input/64 dark:*:[button:hover~[data-slot=separator]:not([data-slot]:hover~[data-slot=separator]~[data-slot=separator]),[data-slot][data-pressed]~[data-slot=separator]:not([data-slot][data-pressed]~[data-slot=separator]~[data-slot=separator])]:before:bg-input/64",
+ {
+ defaultVariants: {
+ orientation: "horizontal",
+ },
+ variants: {
+ orientation: {
+ horizontal:
+ "*:pointer-coarse:after:min-w-auto *:data-slot:has-[~[data-slot]]:rounded-e-none *:data-slot:has-[~[data-slot]]:border-e-0 *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:-end-[0.5px] *:data-slot:has-[~[data-slot]]:before:rounded-e-none *:[[data-slot]~[data-slot]:not([data-slot=separator])]:before:-start-[0.5px] *:[[data-slot]~[data-slot]]:rounded-s-none *:[[data-slot]~[data-slot]]:border-s-0 *:[[data-slot]~[data-slot]]:before:rounded-s-none",
+ vertical:
+ "flex-col *:pointer-coarse:after:min-h-auto *:data-slot:has-[~[data-slot]]:rounded-b-none *:data-slot:has-[~[data-slot]]:border-b-0 *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:-bottom-[0.5px] *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:hidden *:data-slot:has-[~[data-slot]]:before:rounded-b-none dark:*:last:before:hidden dark:*:first:before:block *:[[data-slot]~[data-slot]:not([data-slot=separator])]:before:-top-[0.5px] *:[[data-slot]~[data-slot]]:rounded-t-none *:[[data-slot]~[data-slot]]:border-t-0 *:[[data-slot]~[data-slot]]:before:rounded-t-none",
+ },
+ },
+ },
+);
+
+export function Group({
+ className,
+ orientation,
+ children,
+ ...props
+}: {
+ className?: string;
+ orientation?: VariantProps
["orientation"];
+ children: React.ReactNode;
+} & React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ {children}
+
+ );
+}
+
+export function GroupText({
+ className,
+ render,
+ ...props
+}: useRender.ComponentProps<"div">): React.ReactElement {
+ const defaultProps = {
+ className: cn(
+ "relative inline-flex items-center gap-2 whitespace-nowrap rounded-lg border border-input bg-muted not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-base text-muted-foreground shadow-xs/5 outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] sm:text-sm dark:bg-input/64 dark:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:-mx-0.5 [&_svg]:shrink-0",
+ className,
+ ),
+ "data-slot": "group-text",
+ };
+ return useRender({
+ defaultTagName: "div",
+ props: mergeProps(defaultProps, props),
+ render,
+ });
+}
+
+export function GroupSeparator({
+ className,
+ orientation = "vertical",
+ ...props
+}: {
+ className?: string;
+} & React.ComponentProps): React.ReactElement {
+ return (
+
+ );
+}
+
+export {
+ Group as ButtonGroup,
+ GroupText as ButtonGroupText,
+ GroupSeparator as ButtonGroupSeparator,
+};
diff --git a/src/components/ui/input-group.tsx b/src/components/ui/input-group.tsx
new file mode 100644
index 0000000..238d973
--- /dev/null
+++ b/src/components/ui/input-group.tsx
@@ -0,0 +1,108 @@
+"use client";
+
+import { cva, type VariantProps } from "class-variance-authority";
+import type * as React from "react";
+import { cn } from "@/lib/utils";
+import { Input, type InputProps } from "@/components/ui/input";
+import { Textarea, type TextareaProps } from "@/components/ui/textarea";
+
+const inputGroupAddonVariants = cva(
+ "flex h-auto cursor-text select-none items-center justify-center gap-2 [&>kbd]:rounded-[calc(var(--radius)-5px)] in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:-mx-0.5 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80",
+ {
+ defaultVariants: {
+ align: "inline-start",
+ },
+ variants: {
+ align: {
+ "block-end":
+ "order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
+ "block-start":
+ "order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
+ "inline-end":
+ "order-last pe-[calc(--spacing(3)-1px)] has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]",
+ "inline-start":
+ "order-first ps-[calc(--spacing(3)-1px)] has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]",
+ },
+ },
+ },
+);
+
+export function InputGroup({
+ className,
+ ...props
+}: React.ComponentProps<"div">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function InputGroupAddon({
+ className,
+ align = "inline-start",
+ ...props
+}: React.ComponentProps<"div"> &
+ VariantProps): React.ReactElement {
+ return (
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- pointer convenience only: a click on the addon focuses the input; keyboard users reach the input directly
+ ) => {
+ const target = e.target as Element;
+ if (!e.currentTarget.contains(target)) return;
+
+ const isInteractive = target.closest(
+ "button, a, input, select, textarea, [role='button'], [role='combobox'], [role='listbox'], [data-slot='select-trigger']",
+ );
+ if (isInteractive) return;
+ e.preventDefault();
+ const parent = e.currentTarget.parentElement;
+ const input = parent?.querySelector<
+ HTMLInputElement | HTMLTextAreaElement
+ >("input, textarea");
+ if (input && !parent?.querySelector("input:focus, textarea:focus")) {
+ input.focus();
+ }
+ }}
+ {...props}
+ />
+ );
+}
+
+export function InputGroupText({
+ className,
+ ...props
+}: React.ComponentProps<"span">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function InputGroupInput({
+ className,
+ ...props
+}: InputProps): React.ReactElement {
+ return
;
+}
+
+export function InputGroupTextarea({
+ className,
+ ...props
+}: TextareaProps): React.ReactElement {
+ return
;
+}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..93c9c2d
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import { Input as InputPrimitive } from "@base-ui/react/input";
+import type * as React from "react";
+import { cn } from "@/lib/utils";
+
+export type InputProps = Omit<
+ InputPrimitive.Props & React.RefAttributes
,
+ "size"
+> & {
+ size?: "sm" | "default" | "lg" | number;
+ unstyled?: boolean;
+ nativeInput?: boolean;
+};
+
+export function Input({
+ className,
+ size = "default",
+ unstyled = false,
+ nativeInput = false,
+ style,
+ ...props
+}: InputProps): React.ReactElement {
+ const inputClassName = cn(
+ // QueryCraft AA: full muted-foreground — /72 is 3.1:1 light, 2.9:1 dark
+ "h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] text-foreground leading-8.5 outline-none [transition:background-color_5000000s_ease-in-out_0s] placeholder:text-muted-foreground sm:h-7.5 sm:leading-7.5 autofill:[-webkit-text-fill-color:var(--foreground)]",
+ size === "sm" &&
+ "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
+ size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",
+ props.type === "search" &&
+ "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
+ props.type === "file" &&
+ "text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm",
+ );
+
+ return (
+
+ {nativeInput ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export { InputPrimitive };
diff --git a/src/components/ui/kbd.tsx b/src/components/ui/kbd.tsx
new file mode 100644
index 0000000..59c15c2
--- /dev/null
+++ b/src/components/ui/kbd.tsx
@@ -0,0 +1,31 @@
+import type * as React from "react";
+import { cn } from "@/lib/utils";
+
+export function Kbd({
+ className,
+ ...props
+}: React.ComponentProps<"kbd">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function KbdGroup({
+ className,
+ ...props
+}: React.ComponentProps<"kbd">): React.ReactElement {
+ return (
+
+ );
+}
diff --git a/src/components/ui/menu.tsx b/src/components/ui/menu.tsx
new file mode 100644
index 0000000..76cfc68
--- /dev/null
+++ b/src/components/ui/menu.tsx
@@ -0,0 +1,362 @@
+"use client";
+
+import { Menu as MenuPrimitive } from "@base-ui/react/menu";
+import { ChevronRightIcon } from "lucide-react";
+import type * as React from "react";
+import { cn } from "@/lib/utils";
+
+export const MenuCreateHandle: typeof MenuPrimitive.createHandle =
+ MenuPrimitive.createHandle;
+
+export const Menu: typeof MenuPrimitive.Root = MenuPrimitive.Root;
+
+export const MenuPortal: typeof MenuPrimitive.Portal = MenuPrimitive.Portal;
+
+export function MenuTrigger({
+ className,
+ children,
+ ...props
+}: MenuPrimitive.Trigger.Props): React.ReactElement {
+ return (
+
+ {children}
+
+ );
+}
+
+export function MenuPopup({
+ children,
+ className,
+ sideOffset = 4,
+ // QueryCraft density: keep popups 8px inside the 380px popup (docs/redesign §5.3)
+ collisionPadding = 8,
+ align = "center",
+ alignOffset,
+ side = "bottom",
+ anchor,
+ portalProps,
+ ...props
+}: MenuPrimitive.Popup.Props & {
+ align?: MenuPrimitive.Positioner.Props["align"];
+ sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
+ collisionPadding?: MenuPrimitive.Positioner.Props["collisionPadding"];
+ alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
+ side?: MenuPrimitive.Positioner.Props["side"];
+ anchor?: MenuPrimitive.Positioner.Props["anchor"];
+ portalProps?: MenuPrimitive.Portal.Props;
+}): React.ReactElement {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export function MenuGroup(
+ props: MenuPrimitive.Group.Props,
+): React.ReactElement {
+ return ;
+}
+
+export function MenuItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}: MenuPrimitive.Item.Props & {
+ inset?: boolean;
+ variant?: "default" | "destructive";
+}): React.ReactElement {
+ return (
+ svg:not([class*='opacity-'])]:opacity-80 [&>svg:not([class*='size-'])]:size-4.5 sm:[&>svg:not([class*='size-'])]:size-4 [&>svg]:pointer-events-none [&>svg]:-mx-0.5 [&>svg]:shrink-0",
+ className,
+ )}
+ data-inset={inset}
+ data-slot="menu-item"
+ data-variant={variant}
+ {...props}
+ />
+ );
+}
+
+export function MenuLinkItem({
+ className,
+ inset,
+ variant = "default",
+ closeOnClick = true,
+ ...props
+}: MenuPrimitive.LinkItem.Props & {
+ inset?: boolean;
+ variant?: "default" | "destructive";
+}): React.ReactElement {
+ return (
+ svg:not([class*='opacity-'])]:opacity-80 [&>svg:not([class*='size-'])]:size-4.5 sm:[&>svg:not([class*='size-'])]:size-4 [&>svg]:pointer-events-none [&>svg]:-mx-0.5 [&>svg]:shrink-0",
+ className,
+ )}
+ closeOnClick={closeOnClick}
+ data-inset={inset}
+ data-slot="menu-link-item"
+ data-variant={variant}
+ {...props}
+ />
+ );
+}
+
+export function MenuCheckboxItem({
+ className,
+ children,
+ checked,
+ variant = "default",
+ ...props
+}: MenuPrimitive.CheckboxItem.Props & {
+ variant?: "default" | "switch";
+}): React.ReactElement {
+ return (
+
+ {variant === "switch" ? (
+ <>
+ {children}
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+
+ {children}
+ >
+ )}
+
+ );
+}
+
+export function MenuRadioGroup(
+ props: MenuPrimitive.RadioGroup.Props,
+): React.ReactElement {
+ return ;
+}
+
+export function MenuRadioItem({
+ className,
+ children,
+ ...props
+}: MenuPrimitive.RadioItem.Props): React.ReactElement {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
+
+export function MenuGroupLabel({
+ className,
+ inset,
+ ...props
+}: MenuPrimitive.GroupLabel.Props & {
+ inset?: boolean;
+}): React.ReactElement {
+ return (
+
+ );
+}
+
+export function MenuSeparator({
+ className,
+ ...props
+}: MenuPrimitive.Separator.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function MenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"kbd">): React.ReactElement {
+ return (
+
+ );
+}
+
+export function MenuSub(
+ props: MenuPrimitive.SubmenuRoot.Props,
+): React.ReactElement {
+ return ;
+}
+
+export function MenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: MenuPrimitive.SubmenuTrigger.Props & {
+ inset?: boolean;
+}): React.ReactElement {
+ return (
+ svg:not(:last-child)]:-mx-0.5 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
+ className,
+ )}
+ data-inset={inset}
+ data-slot="menu-sub-trigger"
+ {...props}
+ >
+ {children}
+
+
+ );
+}
+
+export function MenuSubPopup({
+ className,
+ sideOffset = 0,
+ alignOffset,
+ align = "start",
+ ...props
+}: MenuPrimitive.Popup.Props & {
+ align?: MenuPrimitive.Positioner.Props["align"];
+ sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
+ alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
+}): React.ReactElement {
+ const defaultAlignOffset = align !== "center" ? -5 : undefined;
+
+ return (
+
+ );
+}
+
+export {
+ MenuPrimitive,
+ MenuCreateHandle as DropdownMenuCreateHandle,
+ Menu as DropdownMenu,
+ MenuPortal as DropdownMenuPortal,
+ MenuTrigger as DropdownMenuTrigger,
+ MenuPopup as DropdownMenuContent,
+ MenuGroup as DropdownMenuGroup,
+ MenuItem as DropdownMenuItem,
+ MenuCheckboxItem as DropdownMenuCheckboxItem,
+ MenuRadioGroup as DropdownMenuRadioGroup,
+ MenuRadioItem as DropdownMenuRadioItem,
+ MenuGroupLabel as DropdownMenuLabel,
+ MenuSeparator as DropdownMenuSeparator,
+ MenuShortcut as DropdownMenuShortcut,
+ MenuSub as DropdownMenuSub,
+ MenuSubTrigger as DropdownMenuSubTrigger,
+ MenuSubPopup as DropdownMenuSubContent,
+};
diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx
new file mode 100644
index 0000000..4de4f53
--- /dev/null
+++ b/src/components/ui/radio-group.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { Radio as RadioPrimitive } from "@base-ui/react/radio";
+import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
+import type React from "react";
+import { cn } from "@/lib/utils";
+
+export function RadioGroup({
+ className,
+ ...props
+}: RadioGroupPrimitive.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function Radio({
+ className,
+ ...props
+}: RadioPrimitive.Root.Props): React.ReactElement {
+ return (
+
+
+
+ );
+}
+
+export { RadioGroupPrimitive, RadioPrimitive, Radio as RadioGroupItem };
diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..ddb649b
--- /dev/null
+++ b/src/components/ui/scroll-area.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
+import type React from "react";
+import { cn } from "@/lib/utils";
+
+export function ScrollArea({
+ className,
+ children,
+ scrollFade = false,
+ scrollbarGutter = false,
+ fill = false,
+ clampContentMinWidth = true,
+ overscrollContain = false,
+ ...props
+}: ScrollAreaPrimitive.Root.Props & {
+ scrollFade?: boolean;
+ scrollbarGutter?: boolean;
+ fill?: boolean;
+ clampContentMinWidth?: boolean;
+ overscrollContain?: boolean;
+}): React.ReactElement {
+ return (
+
+
+
+ {children}
+
+
+
+
+
+
+ );
+}
+
+export function ScrollBar({
+ className,
+ orientation = "vertical",
+ ...props
+}: ScrollAreaPrimitive.Scrollbar.Props): React.ReactElement {
+ return (
+
+
+
+ );
+}
+
+export { ScrollAreaPrimitive };
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 0000000..f3db7fb
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,23 @@
+import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
+import type React from "react";
+import { cn } from "@/lib/utils";
+
+export function Separator({
+ className,
+ orientation = "horizontal",
+ ...props
+}: SeparatorPrimitive.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export { SeparatorPrimitive };
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
new file mode 100644
index 0000000..253000b
--- /dev/null
+++ b/src/components/ui/sheet.tsx
@@ -0,0 +1,254 @@
+"use client";
+
+import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
+import { mergeProps } from "@base-ui/react/merge-props";
+import { useRender } from "@base-ui/react/use-render";
+import { XIcon } from "lucide-react";
+import type React from "react";
+import { createContext, useContext } from "react";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { ScrollArea } from "@/components/ui/scroll-area";
+
+// QueryCraft density: coss sheets are sized for pages (p-6, text-xl); "compact" fits the 380px popup.
+export type SheetDensity = "default" | "compact";
+const SheetDensityContext = createContext("default");
+
+export const Sheet: typeof SheetPrimitive.Root = SheetPrimitive.Root;
+
+export const SheetPortal: typeof SheetPrimitive.Portal = SheetPrimitive.Portal;
+
+export function SheetTrigger(
+ props: SheetPrimitive.Trigger.Props,
+): React.ReactElement {
+ return ;
+}
+
+export function SheetClose(
+ props: SheetPrimitive.Close.Props,
+): React.ReactElement {
+ return ;
+}
+
+export function SheetBackdrop({
+ className,
+ ...props
+}: SheetPrimitive.Backdrop.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function SheetViewport({
+ className,
+ side,
+ variant = "default",
+ ...props
+}: SheetPrimitive.Viewport.Props & {
+ side?: "right" | "left" | "top" | "bottom";
+ variant?: "default" | "inset";
+}): React.ReactElement {
+ return (
+
+ );
+}
+
+export function SheetPopup({
+ className,
+ children,
+ showCloseButton = true,
+ side = "right",
+ variant = "default",
+ closeProps,
+ portalProps,
+ density = "default",
+ ...props
+}: SheetPrimitive.Popup.Props & {
+ showCloseButton?: boolean;
+ side?: "right" | "left" | "top" | "bottom";
+ variant?: "default" | "inset";
+ // QueryCraft density: read by the header, title, panel and footer below.
+ density?: SheetDensity;
+ closeProps?: SheetPrimitive.Close.Props;
+ portalProps?: SheetPrimitive.Portal.Props;
+}): React.ReactElement {
+ return (
+
+
+
+
+
+ {children}
+
+ {showCloseButton && (
+ }
+ {...closeProps}
+ >
+
+
+ )}
+
+
+
+ );
+}
+
+export function SheetHeader({
+ className,
+ render,
+ ...props
+}: useRender.ComponentProps<"div">): React.ReactElement {
+ const density = useContext(SheetDensityContext);
+ const defaultProps = {
+ className: cn(
+ "flex flex-col gap-2 p-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pb-3 max-sm:pb-4",
+ // QueryCraft density: compact header
+ density === "compact" && "px-4 py-3",
+ className,
+ ),
+ "data-slot": "sheet-header",
+ };
+
+ return useRender({
+ defaultTagName: "div",
+ props: mergeProps<"div">(defaultProps, props),
+ render,
+ });
+}
+
+export function SheetFooter({
+ className,
+ variant = "default",
+ render,
+ ...props
+}: useRender.ComponentProps<"div"> & {
+ variant?: "default" | "bare";
+}): React.ReactElement {
+ const density = useContext(SheetDensityContext);
+ const defaultProps = {
+ className: cn(
+ "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end",
+ variant === "default" && "border-t bg-muted/72 py-4",
+ variant === "bare" &&
+ "in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pt-3 pt-4 pb-6",
+ // QueryCraft density: compact footer
+ density === "compact" && "px-4 py-3",
+ className,
+ ),
+ "data-slot": "sheet-footer",
+ };
+
+ return useRender({
+ defaultTagName: "div",
+ props: mergeProps<"div">(defaultProps, props),
+ render,
+ });
+}
+
+export function SheetTitle({
+ className,
+ ...props
+}: SheetPrimitive.Title.Props): React.ReactElement {
+ const density = useContext(SheetDensityContext);
+ return (
+
+ );
+}
+
+export function SheetDescription({
+ className,
+ ...props
+}: SheetPrimitive.Description.Props): React.ReactElement {
+ return (
+
+ );
+}
+
+export function SheetPanel({
+ className,
+ scrollFade = true,
+ render,
+ ...props
+}: useRender.ComponentProps<"div"> & {
+ scrollFade?: boolean;
+}): React.ReactElement {
+ const density = useContext(SheetDensityContext);
+ const defaultProps = {
+ className: cn(
+ "p-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-header])]:pt-1 in-[[data-slot=sheet-popup]:has([data-slot=sheet-footer]:not(.border-t))]:pb-1",
+ // QueryCraft density: compact panel
+ density === "compact" && "px-4 pt-1 pb-4",
+ className,
+ ),
+ "data-slot": "sheet-panel",
+ };
+
+ return (
+
+ {useRender({
+ defaultTagName: "div",
+ props: mergeProps<"div">(defaultProps, props),
+ render,
+ })}
+
+ );
+}
+
+export {
+ SheetPrimitive,
+ SheetBackdrop as SheetOverlay,
+ SheetPopup as SheetContent,
+};
diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx
new file mode 100644
index 0000000..fcc8c7b
--- /dev/null
+++ b/src/components/ui/spinner.tsx
@@ -0,0 +1,17 @@
+import { Loader2Icon } from "lucide-react";
+import type React from "react";
+import { cn } from "@/lib/utils";
+
+export function Spinner({
+ className,
+ ...props
+}: React.ComponentProps): React.ReactElement {
+ return (
+
+ );
+}
diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx
new file mode 100644
index 0000000..1f9123d
--- /dev/null
+++ b/src/components/ui/switch.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
+import type React from "react";
+import { cn } from "@/lib/utils";
+
+export function Switch({
+ className,
+ ...props
+}: SwitchPrimitive.Root.Props): React.ReactElement {
+ return (
+
+
+
+ );
+}
+
+export { SwitchPrimitive };
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..fdd8e0d
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+import { Field as FieldPrimitive } from "@base-ui/react/field";
+import { mergeProps } from "@base-ui/react/merge-props";
+import type * as React from "react";
+import { cn } from "@/lib/utils";
+
+export type TextareaProps = React.ComponentPropsWithoutRef<"textarea"> &
+ React.RefAttributes & {
+ size?: "sm" | "default" | "lg" | number;
+ unstyled?: boolean;
+ };
+
+export function Textarea({
+ className,
+ size = "default",
+ unstyled = false,
+ ref,
+ ...props
+}: TextareaProps): React.ReactElement {
+ return (
+
+ ) => (
+
+ )}
+ />
+
+ );
+}
+
+export { FieldPrimitive };
diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx
new file mode 100644
index 0000000..2061524
--- /dev/null
+++ b/src/components/ui/toast.tsx
@@ -0,0 +1,336 @@
+"use client";
+
+import { Toast } from "@base-ui/react/toast";
+import {
+ CircleAlertIcon,
+ CircleCheckIcon,
+ InfoIcon,
+ LoaderCircleIcon,
+ TriangleAlertIcon,
+} from "lucide-react";
+import type React from "react";
+import { cn } from "@/lib/utils";
+import { buttonVariants } from "@/components/ui/button";
+
+const TOAST_ICONS = {
+ error: CircleAlertIcon,
+ info: InfoIcon,
+ loading: LoaderCircleIcon,
+ success: CircleCheckIcon,
+ warning: TriangleAlertIcon,
+} as const;
+
+type SwipeDirection = "up" | "down" | "left" | "right";
+
+type ToastData = {
+ rootProps?: Omit<
+ React.ComponentProps,
+ "children" | "className" | "swipeDirection" | "toast"
+ >;
+ tooltipStyle?: boolean;
+};
+
+function getSwipeDirection(position: ToastPosition): SwipeDirection[] {
+ const verticalDirection: SwipeDirection = position.startsWith("top")
+ ? "up"
+ : "down";
+
+ if (position.includes("center")) {
+ return [verticalDirection];
+ }
+
+ if (position.includes("left")) {
+ return ["left", verticalDirection];
+ }
+
+ return ["right", verticalDirection];
+}
+
+function upsertReplayClassName(toast: {
+ type?: string;
+ updateKey?: number;
+}): string | undefined {
+ const k = toast.updateKey ?? 0;
+ if (k <= 0) return undefined;
+ const isEven = k % 2 === 0;
+ if (toast.type === "error") {
+ return isEven ? "animate-toast-error-even" : "animate-toast-error-odd";
+ }
+ return isEven ? "animate-toast-success-even" : "animate-toast-success-odd";
+}
+
+function Toasts({
+ position,
+ portalProps,
+ viewportClassName,
+}: {
+ position: ToastPosition;
+ portalProps?: React.ComponentProps;
+ viewportClassName?: string;
+}): React.ReactElement {
+ const { toasts } = Toast.useToastManager();
+ const swipeDirection = getSwipeDirection(position);
+
+ return (
+
+
+ {toasts.map((toast) => {
+ const Icon = toast.type
+ ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
+ : null;
+ const toastData = toast.data as ToastData | undefined;
+
+ return (
+
+
+
+ {Icon && (
+
+
+
+ )}
+
+
+
+
+
+
+ {toast.actionProps && (
+
+ {toast.actionProps.children}
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+}
+
+function AnchoredToasts({
+ portalProps,
+}: {
+ portalProps?: React.ComponentProps;
+}): React.ReactElement {
+ const { toasts } = Toast.useToastManager();
+
+ return (
+
+
+ {toasts.map((toast) => {
+ const Icon = toast.type
+ ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
+ : null;
+ const toastData = toast.data as ToastData | undefined;
+ const tooltipStyle = toastData?.tooltipStyle ?? false;
+ const positionerProps = toast.positionerProps;
+
+ if (!positionerProps?.anchor) {
+ return null;
+ }
+
+ return (
+
+
+ {tooltipStyle ? (
+
+
+
+ ) : (
+
+
+ {Icon && (
+
+
+
+ )}
+
+
+
+
+
+
+ {toast.actionProps && (
+
+ {toast.actionProps.children}
+
+ )}
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+}
+
+export const toastManager: ReturnType =
+ Toast.createToastManager();
+
+export const anchoredToastManager: ReturnType =
+ Toast.createToastManager();
+
+export type ToastPosition =
+ | "top-left"
+ | "top-center"
+ | "top-right"
+ | "bottom-left"
+ | "bottom-center"
+ | "bottom-right";
+
+export interface ToastProviderProps extends Toast.Provider.Props {
+ position?: ToastPosition;
+ portalProps?: React.ComponentProps;
+ // QueryCraft density: classes merged into the viewport after its defaults
+ viewportClassName?: string;
+}
+
+export function ToastProvider({
+ children,
+ position = "bottom-right",
+ portalProps,
+ viewportClassName,
+ ...props
+}: ToastProviderProps): React.ReactElement {
+ return (
+
+ {children}
+
+
+ );
+}
+
+export interface AnchoredToastProviderProps extends Toast.Provider.Props {
+ portalProps?: React.ComponentProps;
+}
+
+export function AnchoredToastProvider({
+ children,
+ portalProps,
+ ...props
+}: AnchoredToastProviderProps): React.ReactElement {
+ return (
+
+ {children}
+
+
+ );
+}
+
+export { Toast as ToastPrimitive };
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..7a890f5
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
+import type React from "react";
+import { cn } from "@/lib/utils";
+
+export const TooltipCreateHandle: typeof TooltipPrimitive.createHandle =
+ TooltipPrimitive.createHandle;
+
+export const TooltipProvider: typeof TooltipPrimitive.Provider =
+ TooltipPrimitive.Provider;
+
+export const Tooltip: typeof TooltipPrimitive.Root = TooltipPrimitive.Root;
+
+export function TooltipTrigger(
+ props: TooltipPrimitive.Trigger.Props,
+): React.ReactElement {
+ return ;
+}
+
+export function TooltipPopup({
+ className,
+ align = "center",
+ sideOffset = 4,
+ // QueryCraft density: keep tooltips 8px inside the 380px popup (docs/redesign §5.3)
+ collisionPadding = 8,
+ side = "top",
+ anchor,
+ children,
+ portalProps,
+ ...props
+}: TooltipPrimitive.Popup.Props & {
+ align?: TooltipPrimitive.Positioner.Props["align"];
+ side?: TooltipPrimitive.Positioner.Props["side"];
+ sideOffset?: TooltipPrimitive.Positioner.Props["sideOffset"];
+ collisionPadding?: TooltipPrimitive.Positioner.Props["collisionPadding"];
+ anchor?: TooltipPrimitive.Positioner.Props["anchor"];
+ portalProps?: TooltipPrimitive.Portal.Props;
+}): React.ReactElement {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export { TooltipPrimitive, TooltipPopup as TooltipContent };
diff --git a/src/hooks/useAccent.ts b/src/hooks/useAccent.ts
index df3d295..cba896e 100644
--- a/src/hooks/useAccent.ts
+++ b/src/hooks/useAccent.ts
@@ -5,40 +5,98 @@ import { adjustForContrast, readableOn, withAlpha } from '@/lib/contrast';
import type { AccentColor } from '@/types';
import type { ResolvedTheme } from './useTheme';
-/** Panel background per theme — the surface accent-colored text sits on. */
+/** The surface accent-coloured text sits on, per theme (coss background). */
const PANEL_BG: Record = {
light: '#ffffff',
- dark: '#262626',
+ dark: '#161616',
};
+/** Exactly what the accent setting overrides (docs/redesign §4.4). --ring never follows it. */
+const ACCENT_VARS = ['--primary', '--primary-foreground', '--key-foreground'] as const;
+
/**
- * Custom properties the accent overrides. Listed so clearing an accent can
- * remove exactly what it set and let the stylesheet defaults win again.
+ * Pre-redesign accent variables, still read by the CSS Modules components
+ * until they move to coss; removed with tokens.css in Phase 7. Their text
+ * variant keeps the old, lighter dark panel the old surfaces were tuned for.
*/
-const VARS = [
+const LEGACY_VARS = [
'--color-accent',
'--color-accent-hover',
'--color-on-accent',
'--color-accent-text',
'--color-accent-soft',
] as const;
+const LEGACY_PANEL_BG: Record = { light: '#ffffff', dark: '#262626' };
+
+/** Synchronous mirror read by public/theme-boot.js before first paint (§4.5). */
+export const ACCENT_MIRROR_KEY = 'qc-accent';
+
+/** The three variables for an accent swatch in a theme. */
+export function accentVars(
+ hex: string,
+ theme: ResolvedTheme,
+): Record<(typeof ACCENT_VARS)[number], string> {
+ return {
+ // The exact swatch, so the button and switch match what the user picked.
+ '--primary': hex,
+ '--primary-foreground': readableOn(hex),
+ // Text on the panel is darkened or lightened just enough to keep 4.5:1.
+ '--key-foreground': adjustForContrast(hex, PANEL_BG[theme]),
+ };
+}
function applyAccent(accent: AccentColor, theme: ResolvedTheme): void {
const root = document.documentElement;
if (!accent) {
- for (const v of VARS) root.style.removeProperty(v);
+ for (const v of [...ACCENT_VARS, ...LEGACY_VARS]) root.style.removeProperty(v);
return;
}
- // Solid fills keep the exact swatch the user picked, so the button matches
- // the swatch. Only the *label* and text-on-panel uses are corrected, since
- // every swatch in this palette fails AA otherwise.
+ for (const [name, value] of Object.entries(accentVars(accent, theme))) {
+ root.style.setProperty(name, value);
+ }
root.style.setProperty('--color-accent', accent);
- root.style.setProperty('--color-accent-hover', adjustForContrast(accent, PANEL_BG[theme], 3));
+ root.style.setProperty(
+ '--color-accent-hover',
+ adjustForContrast(accent, LEGACY_PANEL_BG[theme], 3),
+ );
root.style.setProperty('--color-on-accent', readableOn(accent));
- root.style.setProperty('--color-accent-text', adjustForContrast(accent, PANEL_BG[theme]));
+ root.style.setProperty('--color-accent-text', adjustForContrast(accent, LEGACY_PANEL_BG[theme]));
root.style.setProperty('--color-accent-soft', withAlpha(accent, 0.15));
}
+function readMirror(): AccentColor {
+ try {
+ const hex = JSON.parse(localStorage.getItem(ACCENT_MIRROR_KEY) ?? 'null')?.hex;
+ return typeof hex === 'string' && isKnownAccent(hex) ? hex : null;
+ } catch {
+ return null;
+ }
+}
+
+function writeMirror(accent: AccentColor): void {
+ try {
+ if (!accent) {
+ localStorage.removeItem(ACCENT_MIRROR_KEY);
+ return;
+ }
+ const light = accentVars(accent, 'light');
+ localStorage.setItem(
+ ACCENT_MIRROR_KEY,
+ JSON.stringify({
+ hex: accent,
+ primary: light['--primary'],
+ primaryForeground: light['--primary-foreground'],
+ keyForeground: {
+ light: light['--key-foreground'],
+ dark: accentVars(accent, 'dark')['--key-foreground'],
+ },
+ }),
+ );
+ } catch {
+ // Storage blocked: the boot script shows mono until the hook applies the accent.
+ }
+}
+
/**
* Accent color state, persisted in chrome.storage and projected onto CSS
* custom properties so components stay declarative — no component branches on
@@ -48,7 +106,9 @@ export function useAccent(theme: ResolvedTheme): {
accent: AccentColor;
setAccent: (next: AccentColor) => void;
} {
- const [accent, setAccentState] = useState(null);
+ // Start from the mirror, so the first render keeps what theme-boot.js already
+ // applied instead of stripping it until chrome.storage answers (§4.5).
+ const [accent, setAccentState] = useState(readMirror);
// Load once. On storage failure keep monochrome rather than surfacing an
// unhandled rejection.
@@ -58,7 +118,9 @@ export function useAccent(theme: ResolvedTheme): {
(stored) => {
if (cancelled) return;
// Guard against a value left by an older build or hand-edited storage.
- if (typeof stored === 'string' && isKnownAccent(stored)) setAccentState(stored);
+ const valid = typeof stored === 'string' && isKnownAccent(stored) ? stored : null;
+ setAccentState(valid);
+ writeMirror(valid);
},
() => undefined,
);
@@ -75,6 +137,7 @@ export function useAccent(theme: ResolvedTheme): {
const setAccent = useCallback((next: AccentColor) => {
setAccentState(next);
+ writeMirror(next);
void storage.setAccent(next);
}, []);
diff --git a/src/hooks/useFocusTrap.ts b/src/hooks/useFocusTrap.ts
deleted file mode 100644
index fe2ec7a..0000000
--- a/src/hooks/useFocusTrap.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { useEffect, useRef } from 'react';
-
-/**
- * Trap focus inside a container while it's active. Used for modal/drawer
- * components. Returns a ref to attach to the container's root element.
- *
- * - Remembers the element that had focus before activation and restores it on deactivate.
- * - Cycles Tab / Shift+Tab within the container's focusable descendants.
- * - Closes on Escape via the `onClose` callback.
- */
-export function useFocusTrap(
- active: boolean,
- onClose?: () => void,
-): React.RefObject {
- const ref = useRef(null);
- const previouslyFocused = useRef(null);
-
- // Read onClose through a ref so the effect below depends only on `active`.
- // Depending on the callback identity would re-run the effect on every parent
- // render, yanking focus back to the first focusable element mid-interaction.
- const onCloseRef = useRef(onClose);
- onCloseRef.current = onClose;
-
- useEffect(() => {
- if (!active) return;
-
- const container = ref.current;
- if (!container) return;
-
- previouslyFocused.current = document.activeElement as HTMLElement | null;
-
- // Focus first focusable child on activation.
- const focusables = getFocusable(container);
- (focusables[0] ?? container).focus();
-
- function onKeyDown(e: KeyboardEvent) {
- if (e.key === 'Escape' && onCloseRef.current) {
- e.preventDefault();
- onCloseRef.current();
- return;
- }
- if (e.key !== 'Tab') return;
- const nodes = getFocusable(container!);
- if (nodes.length === 0) return;
- const first = nodes[0];
- const last = nodes[nodes.length - 1];
- if (e.shiftKey && document.activeElement === first) {
- e.preventDefault();
- last.focus();
- } else if (!e.shiftKey && document.activeElement === last) {
- e.preventDefault();
- first.focus();
- }
- }
-
- document.addEventListener('keydown', onKeyDown);
-
- return () => {
- document.removeEventListener('keydown', onKeyDown);
- // Restore focus on deactivation — but only if the element still exists.
- previouslyFocused.current?.focus?.();
- };
- }, [active]);
-
- return ref;
-}
-
-/** Selector list adapted from WAI-ARIA authoring practices for focusable elements. */
-const FOCUSABLE_SELECTOR = [
- 'a[href]',
- 'button:not([disabled])',
- 'input:not([disabled])',
- 'select:not([disabled])',
- 'textarea:not([disabled])',
- '[tabindex]:not([tabindex="-1"])',
-].join(',');
-
-function getFocusable(root: HTMLElement): HTMLElement[] {
- return Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
- (el) => !el.hasAttribute('aria-hidden'),
- );
-}
diff --git a/src/hooks/useSavedLinks.ts b/src/hooks/useSavedLinks.ts
index 5ae6c8b..9861b64 100644
--- a/src/hooks/useSavedLinks.ts
+++ b/src/hooks/useSavedLinks.ts
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import { DEFAULT_GROUP_ID, storage } from '@/lib/storage';
import { generateId } from '@/lib/id';
import type { Group, SavedLink } from '@/types';
@@ -10,6 +10,8 @@ export function useSavedLinks(): {
saveLink: (input: { url: string; label?: string; groupId?: string }) => Promise;
updateLink: (id: string, label: string | undefined, groupId: string) => Promise;
deleteLink: (id: string) => Promise;
+ /** Put a deleted link back at its old position (Undo). */
+ restoreLink: (link: SavedLink, index: number) => Promise;
createGroup: (name: string) => Promise;
renameGroup: (id: string, name: string) => Promise;
deleteGroup: (id: string) => Promise;
@@ -17,6 +19,10 @@ export function useSavedLinks(): {
const [links, setLinks] = useState([]);
const [groups, setGroups] = useState([]);
const [isLoading, setIsLoading] = useState(true);
+ // Undo runs from a toast created before the delete re-rendered: it must read
+ // the current list, not the one its callback closed over.
+ const linksRef = useRef(links);
+ linksRef.current = links;
useEffect(() => {
let cancelled = false;
@@ -63,6 +69,15 @@ export function useSavedLinks(): {
[links],
);
+ const restoreLink = useCallback(async (link: SavedLink, index: number) => {
+ const current = linksRef.current;
+ if (current.some((l) => l.id === link.id)) return;
+ const next = [...current];
+ next.splice(Math.min(index, next.length), 0, link);
+ setLinks(next);
+ await storage.setSavedLinks(next);
+ }, []);
+
const createGroup = useCallback(
async (name: string) => {
const group: Group = { id: generateId(), name, createdAt: Date.now() };
@@ -117,6 +132,7 @@ export function useSavedLinks(): {
saveLink,
updateLink,
deleteLink,
+ restoreLink,
createGroup,
renameGroup,
deleteGroup,
diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts
index d9af77a..4a003c3 100644
--- a/src/hooks/useTheme.ts
+++ b/src/hooks/useTheme.ts
@@ -1,42 +1,96 @@
-import { useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import type { ThemePreference } from '@/types';
+import { storage } from '@/lib/storage';
export type ResolvedTheme = 'light' | 'dark';
-function resolveSystemTheme(): ResolvedTheme {
+/**
+ * Synchronous mirror of the preference, read by public/theme-boot.js before
+ * first paint. chrome.storage stays the source of truth (docs/redesign §4.5).
+ */
+export const THEME_MIRROR_KEY = 'qc-theme';
+
+const DARK_QUERY = '(prefers-color-scheme: dark)';
+
+function systemTheme(): ResolvedTheme {
if (typeof window === 'undefined' || !window.matchMedia) return 'light';
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ return window.matchMedia(DARK_QUERY).matches ? 'dark' : 'light';
+}
+
+function readMirror(): ThemePreference {
+ try {
+ const value = localStorage.getItem(THEME_MIRROR_KEY);
+ return value === 'light' || value === 'dark' ? value : 'system';
+ } catch {
+ return 'system';
+ }
+}
+
+function writeMirror(preference: ThemePreference): void {
+ try {
+ localStorage.setItem(THEME_MIRROR_KEY, preference);
+ } catch {
+ // Storage blocked: the boot script falls back to the OS, nothing else breaks.
+ }
}
/**
- * Mirrors the OS color scheme onto `data-theme` on .
- *
- * There is deliberately no user-facing theme setting: the popup is a companion
- * to whatever the browser is already doing, so it follows the system and
- * updates live when that changes.
+ * Theme preference — follow the OS, or force light or dark for QueryCraft only —
+ * and the scheme it resolves to, applied to as coss's `.dark` class,
+ * `color-scheme`, and the `data-theme` the pre-redesign tokens still read.
*/
-export function useTheme(): ResolvedTheme {
- const [resolved, setResolved] = useState(resolveSystemTheme);
+export function useTheme(): {
+ preference: ThemePreference;
+ resolved: ResolvedTheme;
+ setPreference: (next: ThemePreference) => void;
+} {
+ const [preference, setPreferenceState] = useState(readMirror);
+ const [system, setSystem] = useState(systemTheme);
useEffect(() => {
- const mql = window.matchMedia('(prefers-color-scheme: dark)');
- const handler = (e: MediaQueryListEvent) => setResolved(e.matches ? 'dark' : 'light');
- setResolved(mql.matches ? 'dark' : 'light');
+ let cancelled = false;
+ storage.getThemePreference().then(
+ (stored) => {
+ if (cancelled) return;
+ setPreferenceState(stored);
+ writeMirror(stored);
+ },
+ () => undefined,
+ );
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ // Listen to the OS only while it decides the theme.
+ useEffect(() => {
+ if (preference !== 'system') return;
+ const mql = window.matchMedia(DARK_QUERY);
+ setSystem(mql.matches ? 'dark' : 'light');
+ const handler = (e: MediaQueryListEvent) => setSystem(e.matches ? 'dark' : 'light');
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
- }, []);
+ }, [preference]);
+
+ const resolved: ResolvedTheme = preference === 'system' ? system : preference;
// Apply to document root, suppressing transitions across the swap so the
// two palettes don't cross-fade into muddy intermediate colors.
const isFirstApply = useRef(true);
useEffect(() => {
const root = document.documentElement;
+ const apply = () => {
+ root.setAttribute('data-theme', resolved);
+ root.classList.toggle('dark', resolved === 'dark');
+ root.style.colorScheme = resolved;
+ };
if (isFirstApply.current) {
isFirstApply.current = false;
- root.setAttribute('data-theme', resolved);
+ apply();
return;
}
root.setAttribute('data-theme-switching', '');
- root.setAttribute('data-theme', resolved);
+ apply();
// Two frames: one for the attribute to take effect, one for the repaint.
const raf = requestAnimationFrame(() => {
requestAnimationFrame(() => root.removeAttribute('data-theme-switching'));
@@ -44,5 +98,11 @@ export function useTheme(): ResolvedTheme {
return () => cancelAnimationFrame(raf);
}, [resolved]);
- return resolved;
+ const setPreference = useCallback((next: ThemePreference) => {
+ setPreferenceState(next);
+ writeMirror(next);
+ void storage.setThemePreference(next);
+ }, []);
+
+ return { preference, resolved, setPreference };
}
diff --git a/src/lib/accents.ts b/src/lib/accents.ts
index 4563e7c..dc349c6 100644
--- a/src/lib/accents.ts
+++ b/src/lib/accents.ts
@@ -17,6 +17,9 @@ export const ACCENTS: readonly Accent[] = [
{ name: 'Orange', hex: '#d9824f' },
{ name: 'Red', hex: '#d9636b' },
{ name: 'Pink', hex: '#d9709c' },
+ // Between Pink and Violet on the hue wheel, at their lightness and chroma.
+ // With Mono first, 12 swatches fill the Settings grid's two rows of six.
+ { name: 'Fuchsia', hex: '#bf6fc3' },
{ name: 'Violet', hex: '#9575e0' },
] as const;
diff --git a/src/lib/contrast.ts b/src/lib/contrast.ts
index 3fa117f..e5f66b8 100644
--- a/src/lib/contrast.ts
+++ b/src/lib/contrast.ts
@@ -8,7 +8,7 @@
*/
const NEAR_WHITE = '#fafafa';
-const NEAR_BLACK = '#141414';
+const NEAR_BLACK = '#0a0a0a'; // docs/redesign §4.4: every swatch's label resolves to this
type Rgb = [number, number, number];
diff --git a/src/lib/platform.ts b/src/lib/platform.ts
new file mode 100644
index 0000000..9638bf4
--- /dev/null
+++ b/src/lib/platform.ts
@@ -0,0 +1,15 @@
+/**
+ * Shortcut labels follow the platform. useKeyboardShortcuts accepts Cmd or
+ * Ctrl, so the label only has to name the key the user actually has.
+ */
+const isApple = typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
+
+/** Visible label for a Cmd/Ctrl shortcut: "⌘↵" on Apple, "Ctrl+↵" elsewhere. */
+export function shortcutLabel(key: string): string {
+ return isApple ? `⌘${key}` : `Ctrl+${key}`;
+}
+
+/** Value for aria-keyshortcuts, which names the modifier "Meta" or "Control". */
+export function ariaShortcut(key: string): string {
+ return `${isApple ? 'Meta' : 'Control'}+${key}`;
+}
diff --git a/src/lib/segmented-control.ts b/src/lib/segmented-control.ts
new file mode 100644
index 0000000..a719bbb
--- /dev/null
+++ b/src/lib/segmented-control.ts
@@ -0,0 +1,43 @@
+import { cva } from "class-variance-authority";
+
+export type SegmentedControlSize = "default" | "lg" | "sm";
+
+export const segmentedControlItemSizeClassNames: Record<
+ SegmentedControlSize,
+ string
+> = {
+ default: "h-8.5 px-[calc(--spacing(2.5)-1px)] sm:h-7.5",
+ lg: "h-9.5 px-[calc(--spacing(3)-1px)] sm:h-8.5",
+ sm: "h-7.5 px-[calc(--spacing(2)-1px)] sm:h-6.5",
+};
+
+export const segmentedControlRootClassName =
+ "relative z-0 flex w-fit items-center justify-center gap-0.5 rounded-lg bg-muted p-0.5";
+
+export const segmentedControlItemLayoutClassName =
+ "gap-1.5 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0";
+
+export const segmentedControlItemVariants = cva(
+ [
+ // QueryCraft AA: full muted-foreground for unchecked items — /72 fails 4.5:1
+ "relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap rounded-md border border-transparent font-medium text-base text-muted-foreground outline-2 outline-transparent transition-[outline-color] hover:bg-transparent hover:text-muted-foreground focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-64 data-disabled:pointer-events-none data-disabled:opacity-64 sm:text-sm",
+ segmentedControlItemLayoutClassName,
+ ],
+ {
+ defaultVariants: {
+ size: "default",
+ },
+ variants: {
+ size: segmentedControlItemSizeClassNames,
+ state: {
+ checked:
+ // QueryCraft AA: selected = foreground-tone outline — the checked fill alone is 1.1:1
+ "data-checked:bg-background data-checked:text-foreground data-checked:inset-ring data-checked:inset-ring-foreground/60 data-checked:shadow-sm/5 dark:data-checked:bg-input",
+ current:
+ "aria-[current=page]:bg-background aria-[current=page]:text-foreground aria-[current=page]:shadow-sm/5 dark:aria-[current=page]:bg-input",
+ pressed:
+ "data-pressed:bg-background data-pressed:text-foreground data-pressed:shadow-sm/5 dark:data-pressed:bg-input",
+ },
+ },
+ },
+);
diff --git a/src/lib/storage.ts b/src/lib/storage.ts
index 06ab3d5..c8838d5 100644
--- a/src/lib/storage.ts
+++ b/src/lib/storage.ts
@@ -1,4 +1,4 @@
-import type { SavedLink, Group, AccentColor } from '@/types';
+import type { SavedLink, Group, AccentColor, ThemePreference } from '@/types';
/**
* Adapter over chrome.storage.local.
@@ -12,8 +12,11 @@ const KEYS = {
savedLinks: 'qc.savedLinks',
groups: 'qc.groups',
accent: 'qc.accent',
+ themePreference: 'themePreference',
} as const;
+const THEME_PREFERENCES: readonly ThemePreference[] = ['system', 'light', 'dark'];
+
export const DEFAULT_GROUP_ID = 'default';
async function get(key: string, fallback: T): Promise {
@@ -59,4 +62,16 @@ export const storage = {
async setAccent(accent: AccentColor): Promise {
await set(KEYS.accent, accent);
},
+
+ /** 'system' unless a valid preference was stored. */
+ async getThemePreference(): Promise {
+ const stored = await get(KEYS.themePreference, 'system');
+ return THEME_PREFERENCES.includes(stored as ThemePreference)
+ ? (stored as ThemePreference)
+ : 'system';
+ },
+
+ async setThemePreference(preference: ThemePreference): Promise {
+ await set(KEYS.themePreference, preference);
+ },
};
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..c5cf235
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,10 @@
+import { createCn } from 'cn/config';
+
+/**
+ * Class merging aware of QueryCraft's theme additions (src/styles/theme.css).
+ * Without this, `text-code` reads as a text colour: it would drop
+ * `text-muted-foreground`, and never replace `sm:text-sm`.
+ */
+export const cn = createCn({
+ extend: { classGroups: { 'font-size': [{ text: ['code'] }] } },
+});
diff --git a/src/popup/App.module.css b/src/popup/App.module.css
deleted file mode 100644
index b184e9e..0000000
--- a/src/popup/App.module.css
+++ /dev/null
@@ -1,22 +0,0 @@
-.app {
- position: relative;
- display: flex;
- flex-direction: column;
- width: 100%;
- min-width: 0;
- height: 100%;
- min-height: 100%;
- max-height: 100%;
- background: var(--color-bg);
- color: var(--color-text-primary);
- overflow: hidden;
-}
-
-/* Header + footer are fixed; only this middle band scrolls. */
-.scrollArea {
- flex: 1;
- min-width: 0;
- overflow-y: auto;
- overflow-x: hidden;
- padding: var(--pad-block) var(--pad-inline) var(--space-4);
-}
diff --git a/src/popup/App.tsx b/src/popup/App.tsx
index 52ba629..8bc918a 100644
--- a/src/popup/App.tsx
+++ b/src/popup/App.tsx
@@ -1,4 +1,5 @@
-import { useCallback, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import type { SavedLink } from '@/types';
import { useAppStore, selectCurrentUrl, selectNavUrl } from '@/store/useAppStore';
import { useActiveTabUrl } from '@/hooks/useActiveTabUrl';
import { useClipboard } from '@/hooks/useClipboard';
@@ -12,19 +13,26 @@ import { Header } from '@/components/Header';
import { UrlPreview } from '@/components/UrlPreview';
import { ParamList } from '@/components/ParamList';
import { ActionBar } from '@/components/ActionBar';
-import { IconButton } from '@/components/IconButton';
-import { IconBookmark, IconSettings } from '@/components/icons';
-import { Toast } from '@/components/Toast';
+import { LiveRegion, announce as announceLive } from '@/components/LiveRegion';
+import { ToastProvider, toastManager } from '@/components/ui/toast';
import { SavedLinksDrawer } from '@/components/SavedLinksDrawer';
import { SettingsDrawer } from '@/components/SettingsDrawer';
import { EmptyState } from '@/components/EmptyState';
-import styles from './App.module.css';
+import { PanelMinHeightContext } from './popupHeight';
+
+/*
+ * Toasts sit bottom-centre, 8px above the action bar, at most 348px wide
+ * (docs/redesign §5.3): a 16px inset gives 380 - 2×16. `sm:` because coss sets
+ * its inset under `sm:`, which is always on in the popup (§4.1).
+ */
+const TOAST_VIEWPORT_CLASS =
+ 'sm:[--toast-inset:--spacing(4)] data-[position*=bottom]:bottom-[calc(var(--action-bar-height,0px)+--spacing(2))]';
export function App() {
// Load URL on mount, hydrate theme, bind storage-backed saved links.
useActiveTabUrl();
- const theme = useTheme();
- const { accent, setAccent } = useAccent(theme);
+ const { preference, resolved, setPreference } = useTheme();
+ const { accent, setAccent } = useAccent(resolved);
// Zustand store — individual selectors keep re-renders narrow.
const tabState = useAppStore((s) => s.tabState);
@@ -42,12 +50,25 @@ export function App() {
const currentUrl = useAppStore(selectCurrentUrl); // human-readable, for display
const navUrl = useAppStore(selectNavUrl); // encoded, for Apply / Copy
- const { copied, copy } = useClipboard();
- const { links, groups, saveLink, updateLink, deleteLink, createGroup } = useSavedLinks();
+ const { copied, copy } = useClipboard(1600);
+ const { links, groups, saveLink, updateLink, deleteLink, restoreLink, createGroup } =
+ useSavedLinks();
const [drawerOpen, setDrawerOpen] = useState(false);
const [drawerMode, setDrawerMode] = useState<'list' | 'save'>('list');
const [settingsOpen, setSettingsOpen] = useState(false);
+ const [panelMinHeight, setPanelMinHeight] = useState(null);
+
+ // Every announcement is shown as a toast, as the old status pill did. The
+ // toast viewport is the live region for it; Phase 4 moves the events with no
+ // visible feedback to (docs/redesign §7.3).
+ // Cleared once shown: the effect fires only on a changed value, so the same
+ // message twice in a row (remove "a", add "a", remove "a") still toasts twice.
+ useEffect(() => {
+ if (!announcement) return;
+ toastManager.add({ title: announcement });
+ useAppStore.setState({ announcement: '' });
+ }, [announcement]);
/* ---------- Action handlers ---------- */
@@ -67,6 +88,16 @@ export function App() {
}
}, [announce]);
+ // The new row is the visible feedback; screen readers hear it through the
+ // hidden live region, never a toast (docs/redesign §7.3).
+ const handleAddParam = useCallback(
+ (key: string, value: string) => {
+ addParam(key, value);
+ announceLive(`Added ${key}`);
+ },
+ [addParam],
+ );
+
const handleReset = useCallback(() => {
resetStore();
}, [resetStore]);
@@ -100,12 +131,6 @@ export function App() {
setSettingsOpen(false);
}, []);
- /** Escape closes whichever panel is on top. */
- const handleEscape = useCallback(() => {
- setSettingsOpen(false);
- setDrawerOpen(false);
- }, []);
-
const handleUpdateLink = useCallback(
(id: string, label: string | undefined, groupId: string) => {
void updateLink(id, label, groupId).then(
@@ -126,14 +151,43 @@ export function App() {
[saveLink, announce],
);
+ // Delete at once, with Undo in the toast for 6s (docs/redesign §7.3). Undo
+ // puts the link back where it was. ⌘Z for it arrives with Phase 4.
const handleDeleteLink = useCallback(
- (id: string) => {
- void deleteLink(id).then(
- () => announce('Saved URL deleted'),
+ (link: SavedLink) => {
+ const index = links.findIndex((l) => l.id === link.id);
+ void deleteLink(link.id).then(
+ () => {
+ const toastId = toastManager.add({
+ title: 'Saved URL deleted',
+ timeout: 6000,
+ actionProps: {
+ children: 'Undo',
+ onClick: () => {
+ toastManager.close(toastId);
+ void restoreLink(link, index).then(
+ () => announceLive('Saved URL restored'),
+ () => announce('Failed to restore saved URL'),
+ );
+ },
+ },
+ });
+ },
() => announce('Failed to delete saved URL'),
);
},
- [deleteLink, announce],
+ [links, deleteLink, restoreLink, announce],
+ );
+
+ // Copies a saved URL — not the editor's, so the footer's "Copied" stays put.
+ const handleCopySavedLink = useCallback(
+ (url: string) => {
+ navigator.clipboard.writeText(url).then(
+ () => announce('URL copied to clipboard'),
+ () => announce('Failed to copy URL'),
+ );
+ },
+ [announce],
);
const handleLoadSavedLink = useCallback(
@@ -170,100 +224,109 @@ export function App() {
() => [
{ key: 'Enter', mod: true, preventDefault: true, handler: () => void handleApply() },
{ key: 's', mod: true, preventDefault: true, handler: handleOpenDrawer },
- { key: 'Escape', handler: handleEscape },
+ // No Escape here: the sheets close on Esc themselves (Base UI), after any
+ // menu or list open inside them — a global handler closed both at once.
],
- [handleApply, handleOpenDrawer, handleEscape],
+ [handleApply, handleOpenDrawer],
);
useKeyboardShortcuts(shortcuts);
/* ---------- Render ---------- */
- const headerActions = (
- <>
- }
- variant="bordered"
- size="lg"
- onClick={handleOpenSettings}
- />
- }
- variant="bordered"
- size="lg"
- onClick={handleOpenDrawer}
- />
- >
- );
-
return (
-
-
-
- {tabState.status === 'loading' && (
-
- )}
-
- {tabState.status === 'unsupported' && (
-
- )}
-
- {tabState.status === 'error' && (
-
- )}
-
- {tabState.status === 'ready' && currentParsed && (
- <>
-
-
void handleApply()}
- onReset={handleReset}
- onCopy={() => void handleCopy()}
- onSave={handleOpenSaveDrawer}
- copied={copied}
- applyDisabled={!navUrl}
+
+
+ {/*
+ * Natural height, capped at Chrome's 600px popup limit; main.tsx sizes
+ * the popup from it (docs/redesign §5.1). An open panel raises the
+ * minimum so it is not clipped over a short view. `overflow-clip`, not
+ * hidden: the closed panel waits off to the right, and a clip is not a
+ * scroll container, so nothing can scroll the holder sideways to it.
+ */}
+
+
+
+ {tabState.status === 'loading' && (
+
+
+
+ )}
+
+ {tabState.status === 'unsupported' && (
+
+
+
+ )}
+
+ {tabState.status === 'error' && (
+
+
+
+ )}
+
+ {tabState.status === 'ready' && currentParsed && (
+ <>
+ {/* Interim: main scrolls as a whole until Phase 2 moves scrolling into the list (§5.2). */}
+
+
+
+
+
void handleApply()}
+ onReset={handleReset}
+ onCopy={() => void handleCopy()}
+ onSave={handleOpenSaveDrawer}
+ copied={copied}
+ applyDisabled={!navUrl}
+ />
+ >
+ )}
+
+
- >
- )}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/src/popup/index.html b/src/popup/index.html
index 27ef8f2..2301010 100644
--- a/src/popup/index.html
+++ b/src/popup/index.html
@@ -4,6 +4,8 @@
QueryCraft
+
+
diff --git a/src/popup/main.tsx b/src/popup/main.tsx
index d33bf20..249945f 100644
--- a/src/popup/main.tsx
+++ b/src/popup/main.tsx
@@ -1,23 +1,30 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
+import { syncPopupHeight } from './popupHeight';
+// Bundled fonts: MV3's content security policy blocks font CDNs.
+import '@fontsource-variable/inter';
+import '@fontsource/geist-mono/400.css';
+import '@fontsource/geist-mono/500.css';
+import '@fontsource/geist-mono/600.css';
import '@/styles/global.css';
-// Chrome sizes the popup from the document box and can ignore CSS height in
-// some cases, so the dimensions are pinned at runtime as well.
+// Chrome sizes the popup from the document box and can ignore CSS width in
+// some cases, so the width is pinned at runtime as well. The height follows
+// the content (see syncPopupHeight below).
const POPUP_WIDTH = '380px';
-const POPUP_HEIGHT = '600px';
document.documentElement.style.width = POPUP_WIDTH;
-document.documentElement.style.height = POPUP_HEIGHT;
document.body.style.width = POPUP_WIDTH;
-document.body.style.height = POPUP_HEIGHT;
-document.body.style.minHeight = POPUP_HEIGHT;
const rootEl = document.getElementById('root');
if (!rootEl) {
throw new Error('Root element #root not found in popup.html');
}
+// #root holds only the app container, so its box is the container's natural
+// height — including the min-height an open panel asks for.
+syncPopupHeight(rootEl);
+
createRoot(rootEl).render(
diff --git a/src/popup/popupHeight.ts b/src/popup/popupHeight.ts
new file mode 100644
index 0000000..52bbaac
--- /dev/null
+++ b/src/popup/popupHeight.ts
@@ -0,0 +1,74 @@
+import { createContext, useContext, useEffect } from 'react';
+
+/** Chrome caps extension popups at 600px tall (docs/redesign §2). */
+export const POPUP_MAX_HEIGHT = 600;
+
+/**
+ * Chrome sizes the popup from the document box, so html and body are pinned
+ * from JS to the app's natural height, capped at the popup maximum (§5.1).
+ */
+export function syncPopupHeight(target: Element): () => void {
+ const observer = new ResizeObserver(([entry]) => {
+ if (!entry) return;
+ const blockSize = entry.borderBoxSize[0]?.blockSize ?? entry.contentRect.height;
+ const height = `${Math.min(POPUP_MAX_HEIGHT, Math.ceil(blockSize))}px`;
+ document.documentElement.style.height = height;
+ document.body.style.height = height;
+ });
+ observer.observe(target);
+ return () => observer.disconnect();
+}
+
+/**
+ * An open panel reports its natural height (capped at the popup maximum) so
+ * the app holder grows to fit it, and reports null when it closes. Otherwise a
+ * panel opened over a short view would be clipped (§5.1).
+ */
+export const PanelMinHeightContext = createContext<(height: number | null) => void>(() => {});
+
+/**
+ * While a sheet is open, report its natural height — every row of the sheet
+ * at its own height, with the scrolling panel at its content's full height —
+ * so the app holder grows to fit it (§5.1).
+ *
+ * The sheet is portaled into , so it is found by the `data-qc-sheet`
+ * attribute passed to SheetPopup rather than threaded through a ref.
+ * `contentKey` re-measures when the sheet swaps its content (list ↔ form).
+ */
+export function useReportSheetHeight(open: boolean, sheetId: string, contentKey?: unknown): void {
+ const setPanelMinHeight = useContext(PanelMinHeightContext);
+
+ // Released only when the sheet closes: dropping it between a list ↔ form
+ // swap would let the popup shrink for a frame and grow back.
+ useEffect(() => {
+ if (!open) return;
+ return () => setPanelMinHeight(null);
+ }, [open, setPanelMinHeight]);
+
+ useEffect(() => {
+ if (!open || typeof ResizeObserver === 'undefined') return;
+ let observer: ResizeObserver | null = null;
+ // The popup (or its new content) mounts after this render: wait a frame.
+ const frame = requestAnimationFrame(() => {
+ const popup = document.querySelector(`[data-qc-sheet="${sheetId}"]`);
+ if (!popup) return;
+ const rows = Array.from(popup.children) as HTMLElement[];
+ const naturalHeight = (row: HTMLElement) =>
+ row.querySelector('[data-slot=scroll-area-content]')?.offsetHeight ??
+ row.offsetHeight;
+ observer = new ResizeObserver(() => {
+ const total = rows.reduce((sum, row) => sum + naturalHeight(row), 0);
+ setPanelMinHeight(Math.min(POPUP_MAX_HEIGHT, Math.ceil(total)));
+ });
+ for (const row of rows) {
+ observer.observe(row);
+ const content = row.querySelector('[data-slot=scroll-area-content]');
+ if (content) observer.observe(content);
+ }
+ });
+ return () => {
+ cancelAnimationFrame(frame);
+ observer?.disconnect();
+ };
+ }, [open, sheetId, contentKey, setPanelMinHeight]);
+}
diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts
index c9e88a5..4887889 100644
--- a/src/store/useAppStore.ts
+++ b/src/store/useAppStore.ts
@@ -13,7 +13,7 @@ interface AppState {
/** Current editable parsed URL — what the user sees & edits. */
currentParsed: ParsedUrl | null;
- /** Last announcement for the aria-live region. Monotonically replaced on each event. */
+ /** Latest status message; the popup shows it as a toast, then clears it. */
announcement: string;
/** Actions */
@@ -141,7 +141,8 @@ export const useAppStore = create((set, get) => ({
...state.currentParsed,
params: [...state.currentParsed.params, newParam],
},
- announcement: `Added parameter ${key}.`,
+ // No announcement: the new row is the visible feedback, and the UI
+ // announces "Added {key}" through its hidden live region (docs/redesign §7.3).
});
},
diff --git a/src/styles/global.css b/src/styles/global.css
index 38e5ce5..f7195f5 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -1,165 +1,188 @@
-@import './tokens.css';
-
-/* ------------ Reset ------------- */
-
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
-
-html,
-body {
- margin: 0;
- padding: 0;
-}
-
-/*
- * Headings and paragraphs keep their UA margins otherwise — at these small
- * type sizes a stray `margin: 1em 0` silently doubles a card's header height.
- * Spacing is owned by the layout, never by the text element.
- */
-h1,
-h2,
-h3,
-h4,
-p,
-figure {
- margin: 0;
-}
-
-html {
- font-family: var(--font-sans);
- font-size: var(--font-size-md);
- line-height: var(--line-height-base);
- color: var(--color-text-primary);
- background: var(--color-bg-outer);
- width: var(--popup-width);
- height: var(--popup-height);
- overflow: hidden;
- -webkit-text-size-adjust: 100%;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-body {
- width: 100%;
- height: 100%;
- min-height: 100%;
- max-height: 100%;
- background: var(--color-bg);
- color: var(--color-text-primary);
- overflow: hidden;
- display: flex;
-}
-
-#root {
- flex: 1;
- height: 100%;
- min-height: 100%;
- /*
- * Flex items default to min-width:auto, which refuses to shrink below the
- * content's min-content width — a long unbroken URL would then push the
- * whole panel wider than the popup. Opt out so the panel owns its width.
- */
- min-width: 0;
- max-width: 100%;
-}
-
-button {
- font-family: inherit;
- font-size: inherit;
- color: inherit;
- background: none;
- border: none;
- cursor: pointer;
- padding: 0;
-}
-
-input,
-select,
-textarea {
- font-family: inherit;
- font-size: inherit;
- color: inherit;
-}
-
-input::placeholder,
-textarea::placeholder {
- color: var(--color-text-faint);
-}
-
-/* ------------ Global focus ------------- */
-
-/*
- * The reference design signals focus only by lightening an input's border.
- * That is not enough on its own for keyboard users, so the border treatment
- * is kept per-component and a :focus-visible ring is layered on top —
- * pointer interaction stays clean because :focus-visible excludes clicks.
- */
-*:focus {
- outline: none;
-}
-
-*:focus-visible {
- outline: var(--focus-ring-width) solid var(--color-focus);
- outline-offset: var(--focus-ring-offset);
-}
-
-/* ------------ Utilities ------------- */
-
-/* Visually hide but keep accessible to assistive tech (WCAG compliant pattern). */
-.visually-hidden {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
-}
-
-/* Scrollbar — subtle, theme-aware */
-::-webkit-scrollbar {
- width: 8px;
- height: 8px;
-}
-
-::-webkit-scrollbar-track {
- background: transparent;
-}
-
-::-webkit-scrollbar-thumb {
- background: var(--color-fill-strong);
- border-radius: var(--radius-full);
+/* Scan src/ only: docs/redesign mockups would otherwise add every class they mention. */
+@import 'tailwindcss' source('../');
+@import 'tw-animate-css';
+@import 'shadcn/tailwind.css';
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --font-heading: var(--font-sans);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --color-foreground: var(--foreground);
+ --color-background: var(--background);
+ --radius-sm: calc(var(--radius) * 0.6);
+ --radius-md: calc(var(--radius) * 0.8);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) * 1.4);
+ --radius-2xl: calc(var(--radius) * 1.8);
+ --radius-3xl: calc(var(--radius) * 2.2);
+ --radius-4xl: calc(var(--radius) * 2.6);
+ --color-warning-foreground: var(--warning-foreground);
+ --color-warning: var(--warning);
+ --color-success-foreground: var(--success-foreground);
+ --color-success: var(--success);
+ --color-info-foreground: var(--info-foreground);
+ --color-info: var(--info);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --animate-toast-success-odd: toast-success-odd 0.32s cubic-bezier(0.5, 1, 0.89, 1);
+ --animate-toast-success-even: toast-success-even 0.32s cubic-bezier(0.5, 1, 0.89, 1);
+ --animate-toast-error-odd: toast-error-odd 0.28s cubic-bezier(0.5, 1, 0.89, 1);
+ --animate-toast-error-even: toast-error-even 0.28s cubic-bezier(0.5, 1, 0.89, 1);
+ @keyframes toast-success-odd {
+ 0% {
+ scale: 1;
+ }
+ 30% {
+ scale: 1.025;
+ }
+ 60% {
+ scale: 0.99;
+ }
+ 100% {
+ scale: 1;
+ }
+ }
+ @keyframes toast-error-odd {
+ 0% {
+ translate: 0 0;
+ }
+ 25% {
+ translate: -3px 0;
+ }
+ 50% {
+ translate: 3px 0;
+ }
+ 75% {
+ translate: -3px 0;
+ }
+ 100% {
+ translate: 0 0;
+ }
+ }
+ @keyframes toast-success-even {
+ 0% {
+ scale: 1;
+ }
+ 30% {
+ scale: 1.025;
+ }
+ 60% {
+ scale: 0.99;
+ }
+ 100% {
+ scale: 1;
+ }
+ }
+ @keyframes toast-error-even {
+ 0% {
+ translate: 0 0;
+ }
+ 25% {
+ translate: -3px 0;
+ }
+ 50% {
+ translate: 3px 0;
+ }
+ 75% {
+ translate: -3px 0;
+ }
+ 100% {
+ translate: 0 0;
+ }
+ }
}
-::-webkit-scrollbar-thumb:hover {
- background: var(--color-text-faint);
+:root {
+ --background: var(--color-white);
+ --foreground: var(--color-neutral-800);
+ --card: var(--color-white);
+ --card-foreground: var(--color-neutral-800);
+ --popover: var(--color-white);
+ --popover-foreground: var(--color-neutral-800);
+ --primary: var(--color-neutral-800);
+ --primary-foreground: var(--color-neutral-50);
+ --secondary: --alpha(var(--color-black) / 4%);
+ --secondary-foreground: var(--color-neutral-800);
+ --muted: --alpha(var(--color-black) / 4%);
+ --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-black));
+ --accent: --alpha(var(--color-black) / 4%);
+ --accent-foreground: var(--color-neutral-800);
+ --destructive: var(--color-red-500);
+ --border: --alpha(var(--color-black) / 8%);
+ --input: --alpha(var(--color-black) / 10%);
+ --ring: var(--color-neutral-400);
+ --radius: 0.625rem;
+ --destructive-foreground: var(--color-red-700);
+ --info: var(--color-blue-500);
+ --info-foreground: var(--color-blue-700);
+ --success: var(--color-emerald-500);
+ --success-foreground: var(--color-emerald-700);
+ --warning: var(--color-amber-500);
+ --warning-foreground: var(--color-amber-700);
+}
+
+.dark {
+ --background: color-mix(in srgb, var(--color-neutral-950) 95%, var(--color-white));
+ --foreground: var(--color-neutral-100);
+ --card: color-mix(in srgb, var(--background) 98%, var(--color-white));
+ --card-foreground: var(--color-neutral-100);
+ --popover: color-mix(in srgb, var(--background) 98%, var(--color-white));
+ --popover-foreground: var(--color-neutral-100);
+ --primary: var(--color-neutral-100);
+ --primary-foreground: var(--color-neutral-800);
+ --secondary: --alpha(var(--color-white) / 4%);
+ --secondary-foreground: var(--color-neutral-100);
+ --muted: --alpha(var(--color-white) / 4%);
+ --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-white));
+ --accent: --alpha(var(--color-white) / 4%);
+ --accent-foreground: var(--color-neutral-100);
+ --destructive: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));
+ --border: --alpha(var(--color-white) / 6%);
+ --input: --alpha(var(--color-white) / 8%);
+ --ring: var(--color-neutral-500);
+ --destructive-foreground: var(--color-red-400);
+ --info: var(--color-blue-500);
+ --info-foreground: var(--color-blue-400);
+ --success: var(--color-emerald-500);
+ --success-foreground: var(--color-emerald-400);
+ --warning: var(--color-amber-500);
+ --warning-foreground: var(--color-amber-400);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+ html {
+ @apply font-sans;
+ }
}
/*
- * Applied for one frame while the theme flips. Without it, every element that
- * transitions `background` (inputs, toggles, buttons) animates between the two
- * palettes, so the switch reads as a smear rather than an instant change.
+ * QueryCraft layers. They come after the coss tokens above so they win; Tailwind
+ * inlines each @import where it stands.
+ * theme.css — AA patches, density re-key, fonts (docs/redesign §4.2)
+ * tokens.css + legacy.css — pre-redesign styles for the CSS Modules
+ * components, deleted in Phase 7
*/
-html[data-theme-switching] *,
-html[data-theme-switching] *::before,
-html[data-theme-switching] *::after {
- transition: none !important;
-}
-
-/* Respect reduced motion preference */
-@media (prefers-reduced-motion: reduce) {
- *,
- *::before,
- *::after {
- animation-duration: 0.01ms !important;
- animation-iteration-count: 1 !important;
- transition-duration: 0.01ms !important;
- scroll-behavior: auto !important;
- }
-}
+@import './theme.css';
+@import './tokens.css';
+@import './legacy.css';
diff --git a/src/styles/legacy.css b/src/styles/legacy.css
new file mode 100644
index 0000000..91e1397
--- /dev/null
+++ b/src/styles/legacy.css
@@ -0,0 +1,171 @@
+/*
+ * Pre-redesign base styles, kept so the CSS Modules components look exactly as
+ * before while they are migrated to coss ui one by one (docs/redesign, §10).
+ *
+ * They sit in Tailwind's `base` layer on purpose: unlayered element rules such
+ * as `button { background: none }` would beat every coss utility. Inside the
+ * layer they still override Tailwind's preflight (same layer, later in order),
+ * and the unlayered CSS Modules still override them, as they did before.
+ *
+ * Deleted in Phase 7 together with tokens.css.
+ */
+@layer base {
+ /* ------------ Reset ------------- */
+
+ *,
+ *::before,
+ *::after {
+ box-sizing: border-box;
+ }
+
+ html,
+ body {
+ margin: 0;
+ padding: 0;
+ }
+
+ /*
+ * Headings and paragraphs keep their UA margins otherwise — at these small
+ * type sizes a stray `margin: 1em 0` silently doubles a card's header height.
+ * Spacing is owned by the layout, never by the text element.
+ */
+ h1,
+ h2,
+ h3,
+ h4,
+ p,
+ figure {
+ margin: 0;
+ }
+
+ html {
+ font-family: var(--font-sans);
+ line-height: var(--line-height-base);
+ color: var(--color-text-primary);
+ background: var(--color-bg-outer);
+ width: var(--qc-popup-width);
+ /* No height here: src/popup/popupHeight.ts sets it from the content (§5.1). */
+ overflow: hidden;
+ -webkit-text-size-adjust: 100%;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+
+ body {
+ /* On body, not html: the root size stays 16px, so coss's rem-based sizes
+ come out as designed. Nothing below uses rem, only em letter-spacing. */
+ font-size: var(--font-size-md);
+ width: 100%;
+ background: var(--color-bg);
+ color: var(--color-text-primary);
+ overflow: hidden;
+ }
+
+ button {
+ font-family: inherit;
+ font-size: inherit;
+ color: inherit;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ }
+
+ input,
+ select,
+ textarea {
+ font-family: inherit;
+ font-size: inherit;
+ color: inherit;
+ }
+
+ /*
+ * Tailwind's preflight gives form controls `font: inherit`, which also
+ * inherits line-height (1.5 here) where the UA default was `normal` — the
+ * filter input and the saved-group toggles grew 3–4px. Restored for the
+ * pre-redesign markup only: every coss element carries a data-slot.
+ */
+ :where(button, input, select, textarea):not([data-slot]) {
+ line-height: normal;
+ }
+
+ input::placeholder,
+ textarea::placeholder {
+ color: var(--color-text-faint);
+ }
+
+ /* ------------ Global focus ------------- */
+
+ /*
+ * The reference design signals focus only by lightening an input's border.
+ * That is not enough on its own for keyboard users, so the border treatment
+ * is kept per-component and a :focus-visible ring is layered on top —
+ * pointer interaction stays clean because :focus-visible excludes clicks.
+ */
+ *:focus {
+ outline: none;
+ }
+
+ *:focus-visible {
+ outline: var(--focus-ring-width) solid var(--color-focus);
+ outline-offset: var(--focus-ring-offset);
+ }
+
+ /* ------------ Utilities ------------- */
+
+ /* Visually hide but keep accessible to assistive tech (WCAG compliant pattern). */
+ .visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
+
+ /* Scrollbar — subtle, theme-aware */
+ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: var(--color-fill-strong);
+ border-radius: var(--radius-full);
+ }
+
+ ::-webkit-scrollbar-thumb:hover {
+ background: var(--color-text-faint);
+ }
+
+ /*
+ * Applied for one frame while the theme flips. Without it, every element that
+ * transitions `background` (inputs, toggles, buttons) animates between the two
+ * palettes, so the switch reads as a smear rather than an instant change.
+ */
+ html[data-theme-switching] *,
+ html[data-theme-switching] *::before,
+ html[data-theme-switching] *::after {
+ transition: none !important;
+ }
+
+ /* Respect reduced motion preference */
+ @media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+ }
+}
diff --git a/src/styles/theme.css b/src/styles/theme.css
new file mode 100644
index 0000000..147b0bb
--- /dev/null
+++ b/src/styles/theme.css
@@ -0,0 +1,46 @@
+/*
+ * QueryCraft overrides on top of the coss tokens (docs/redesign/README.md §4.2).
+ * Imported after the coss :root / .dark blocks in global.css, so it wins.
+ */
+
+@theme {
+ /* Always desktop density: coss switches to compact sizes at `sm`, and the
+ 380px popup would otherwise get the touch sizes (§4.1). */
+ --breakpoint-sm: 0rem;
+
+ --text-code: 0.8125rem; /* 13px — every monospace value/key/URL */
+ --text-code--line-height: 1.25rem; /* 20px */
+ --ease-out: cubic-bezier(0.2, 0, 0, 1);
+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
+
+ /* Bundled via @fontsource in src/popup/main.tsx — MV3 blocks font CDNs (§3.3). */
+ --font-sans: 'Inter Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
+}
+
+@theme inline {
+ --color-key-foreground: var(--key-foreground);
+ --color-segment-checked: var(--segment-checked);
+}
+
+/* AA patch layer — every value below exists because the coss default fails WCAG 2.2 AA */
+:root {
+ --ring: var(--color-neutral-500); /* coss neutral-400 = 2.6:1 on white → 4.7:1 */
+ --key-foreground: var(--foreground); /* URL keys; overridden by the accent pipeline */
+ --segment-checked: var(--background);
+}
+
+.dark {
+ /* coss neutral-500: focused vs resting border only 3.1:1 → 5.7:1 */
+ --ring: var(--color-neutral-400);
+ --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 80%, var(--color-white));
+ /* coss 90% mix (#818181) = 4.2–4.4:1 on muted/popover → #8f8f8f ≥ 4.8:1 */
+ --segment-checked: --alpha(var(--color-white) / 8%);
+}
+
+@layer base {
+ /* coss requirement: portaled popups stack inside their own context. */
+ #root {
+ isolation: isolate;
+ }
+}
diff --git a/src/styles/tokens.css b/src/styles/tokens.css
index 1aee8f7..b98917b 100644
--- a/src/styles/tokens.css
+++ b/src/styles/tokens.css
@@ -150,14 +150,8 @@
/* ── Theme-agnostic scales ────────────────────────────────────────── */
:root {
- /* Typography. Inter / JetBrains Mono are used when present on the system;
- the extension ships no webfonts (remote font CDNs are blocked by the
- MV3 content security policy), so the fallbacks matter. */
- --font-sans:
- 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
- --font-mono:
- 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
-
+ /* Typography. --font-sans / --font-mono live in theme.css's @theme now:
+ an unlayered copy here would override Tailwind's for every element. */
--font-size-2xs: 10.5px;
--font-size-xs: 11px;
--font-size-sm: 11.5px;
@@ -207,11 +201,9 @@
--min-target: 24px;
/*
- * Popup dimensions. The handoff specifies 380x640, but Chrome hard-caps
- * extension popups at 800x600 — a 640px panel would simply be clipped.
- * The layout is a flex column with a scrolling middle, so 600 is the
- * faithful rendering of the design within the platform limit.
+ * Popup width. The height follows the content up to Chrome's 600px cap
+ * (src/popup/popupHeight.ts). Prefixed: Base UI sizes its popups with
+ * --popup-width / --popup-height, and an inherited :root value would win.
*/
- --popup-width: 380px;
- --popup-height: 600px;
+ --qc-popup-width: 380px;
}
diff --git a/src/types/index.ts b/src/types/index.ts
index 03d6dee..732e662 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -49,10 +49,12 @@ export interface Group {
/**
* Chosen accent color as a hex string, or `null` for the monochrome default.
- * The theme itself is not a preference — it always follows the OS.
*/
export type AccentColor = string | null;
+/** Theme setting: follow the OS, or force one scheme for QueryCraft only. */
+export type ThemePreference = 'system' | 'light' | 'dark';
+
/** Special state when the active tab's URL is a browser-internal page we can't edit. */
export type TabLoadState =
| { status: 'loading' }
diff --git a/store-assets/redesign/01-edit-params.png b/store-assets/redesign/01-edit-params.png
new file mode 100644
index 0000000..d9ddff1
Binary files /dev/null and b/store-assets/redesign/01-edit-params.png differ
diff --git a/store-assets/redesign/02-add-keys.png b/store-assets/redesign/02-add-keys.png
new file mode 100644
index 0000000..d6b84b6
Binary files /dev/null and b/store-assets/redesign/02-add-keys.png differ
diff --git a/store-assets/redesign/03-saved-urls.png b/store-assets/redesign/03-saved-urls.png
new file mode 100644
index 0000000..441dafd
Binary files /dev/null and b/store-assets/redesign/03-saved-urls.png differ
diff --git a/store-assets/redesign/04-keyboard.png b/store-assets/redesign/04-keyboard.png
new file mode 100644
index 0000000..33f766b
Binary files /dev/null and b/store-assets/redesign/04-keyboard.png differ
diff --git a/store-assets/redesign/05-theme-accent.png b/store-assets/redesign/05-theme-accent.png
new file mode 100644
index 0000000..472de8d
Binary files /dev/null and b/store-assets/redesign/05-theme-accent.png differ
diff --git a/store-assets/redesign/marquee-promo-1400x560.png b/store-assets/redesign/marquee-promo-1400x560.png
new file mode 100644
index 0000000..a9017e8
Binary files /dev/null and b/store-assets/redesign/marquee-promo-1400x560.png differ
diff --git a/store-assets/redesign/small-promo-440x280.png b/store-assets/redesign/small-promo-440x280.png
new file mode 100644
index 0000000..22d593c
Binary files /dev/null and b/store-assets/redesign/small-promo-440x280.png differ
diff --git a/vite.config.ts b/vite.config.ts
index a289048..6d04a22 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,13 +1,14 @@
// vitest/config's defineConfig is a superset of vite's — it types the `test` key.
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
+import tailwindcss from '@tailwindcss/vite';
import { crx } from '@crxjs/vite-plugin';
import manifest from './manifest.json' with { type: 'json' };
import path from 'node:path';
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react(), crx({ manifest })],
+ plugins: [react(), tailwindcss(), crx({ manifest })],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),