Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 8a4c523345a7ed7e0597ac7598b26867345ab9dd
parent a9edbf4bce84592dc90d86e7ecfc4914f3ce9936
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue, 30 Jun 2026 14:51:01 -0400

Merge redesign/phase-0-foundation: family-wide UI redesign (Phases 0-5)

A cohesive redesign across the three apps over the shared design system:
- Phase 0: shared theme foundation (tokens.css, unified Source fonts,
  no-FOUC ThemeScript/Provider/Toggle).
- Phase 1: shared shadcn "new-york" kit in common/components/ui/ + full token
  contract (brand namespaced to --brand* to clear shadcn's neutral --accent).
- Phase 2: public "Archive / Reading Room" identity on export + homepage; search
  UI migrated onto tokens + kit (all e2e hooks preserved); family hub backlink +
  sibling switcher; "new since your last visit"; social-icon FOUC fix.
- Phase 3: editor command-first cockpit -- cmdk command palette that runs actions
  with Sonner toasts, shared NAV source, compact token shell, live-jobs dashboard.
- Phase 4: StreamActionLog onto the kit (a11y contract preserved) + optional
  per-site brand accent (baked onto <html> at prerender).
- Phase 5: Terminal (ice-cyan) + Swiss (international-red) selectable theme
  families + a ThemeMenu family picker in every header (pure CSS token swaps).

Each phase was typechecked, built, and Playwright-gated before commit. The
branch already merged main's download-format feature (a687f2f), so this
integrates cleanly.

Diffstat:
Mcommon/components/QueryBuilder.tsx | 17+++++++++++------
Mcommon/components/QueryGroupView.tsx | 50++++++++++++++++++++++++++++++--------------------
Mcommon/components/QueryLeafView.tsx | 73+++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcommon/components/StreamActionLog.tsx | 27++++++++++++++++-----------
Acommon/components/ThemeMenu.tsx | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ThemeProvider.tsx | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ThemeScript.tsx | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ThemeToggle.tsx | 36++++++++++++++++++++++++++++++++++++
Mcommon/components/TranscriptSearch.tsx | 312+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mcommon/components/badges.tsx | 2+-
Mcommon/components/charts/ChartShapeControls.tsx | 30++++++++++++++++--------------
Acommon/components/themeConfig.ts | 28++++++++++++++++++++++++++++
Acommon/components/ui/badge.tsx | 48++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ui/button.tsx | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ui/card.tsx | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ui/checkbox.tsx | 32++++++++++++++++++++++++++++++++
Acommon/components/ui/command.tsx | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ui/dialog.tsx | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ui/dropdown-menu.tsx | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ui/input.tsx | 21+++++++++++++++++++++
Acommon/components/ui/label.tsx | 24++++++++++++++++++++++++
Acommon/components/ui/select.tsx | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ui/separator.tsx | 28++++++++++++++++++++++++++++
Acommon/components/ui/sheet.tsx | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ui/sonner.tsx | 47+++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ui/tabs.tsx | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/ui/tooltip.tsx | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/lib/accent.ts | 38++++++++++++++++++++++++++++++++++++++
Mcommon/lib/settings.ts | 31+++++++++++++++++++++++++++++++
Mcommon/lib/site.ts | 7+++++++
Mcommon/package.json | 7++++++-
Acommon/styles/fonts.ts | 35+++++++++++++++++++++++++++++++++++
Acommon/styles/tokens.css | 426+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/CHANGELOG.md | 4++++
Meditor/app/components/ChangelogNavLink.tsx | 9+++++++--
Meditor/app/components/CommandPalette.tsx | 446++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Meditor/app/globals.css | 56++++++--------------------------------------------------
Meditor/app/layout.tsx | 107+++++++++++++++++++++++++++++++++----------------------------------------------
Aeditor/app/lib/nav.ts | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/app/page.tsx | 59+++++++++++++++++++++++++++++++++++++----------------------
Meditor/app/settings/actions.ts | 2++
Meditor/app/settings/components/SettingsForm.tsx | 7+++++++
Meditor/app/sites/actions.ts | 9+++++++++
Meditor/app/sites/components/SiteForm.tsx | 6++++++
Meditor/e2e/export-search.spec.ts | 22+++++++++++++++-------
Meditor/e2e/site-scope.spec.ts | 15++++++++++-----
Meditor/package.json | 1+
Mexport/CHANGELOG.md | 6++++++
Mexport/app/components/Footer.tsx | 25+++++++++++++++----------
Aexport/app/components/Header.tsx | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexport/app/components/NewSinceLastVisit.tsx | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexport/app/components/SiblingSwitcher.tsx | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mexport/app/globals.css | 50+++++---------------------------------------------
Mexport/app/layout.tsx | 74++++++++++++++++++++++----------------------------------------------------
Mexport/app/page.tsx | 11++++++++---
Mexport/e2e/fixtures/sites/testsite/site.json | 1+
Mexport/e2e/query-tree.spec.ts | 13++++++++-----
Mexport/e2e/site-branding.spec.ts | 13+++++++++++++
Aexport/e2e/theme-family.spec.ts | 48++++++++++++++++++++++++++++++++++++++++++++++++
Aexport/e2e/theme.spec.ts | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mexport/package.json | 1+
Mhomepage/app/components/CustomizePanel.tsx | 4++--
Mhomepage/app/components/Footer.tsx | 9++++++---
Mhomepage/app/components/Header.tsx | 8+++++++-
Mhomepage/app/components/KpiHeader.tsx | 2+-
Mhomepage/app/components/PresetBar.tsx | 2+-
Mhomepage/app/components/Segmented.tsx | 2+-
Mhomepage/app/components/SiteGrid.tsx | 4++--
Mhomepage/app/globals.css | 64++++++++--------------------------------------------------------
Mhomepage/app/layout.tsx | 40++++++++++++++--------------------------
Ahomepage/e2e/theme.spec.ts | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Mpnpm-lock.yaml | 3077+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
72 files changed, 5960 insertions(+), 1481 deletions(-)

diff --git a/common/components/QueryBuilder.tsx b/common/components/QueryBuilder.tsx @@ -26,6 +26,7 @@ import { import type { GroupState, LeafState } from "../lib/searchEval"; import QueryGroupView from "./QueryGroupView"; import QueryLeafView from "./QueryLeafView"; +import { Button } from "./ui/button"; type Props = { root: GroupNode; @@ -80,8 +81,10 @@ export default function QueryBuilder({ }} /> <div className="flex items-center gap-2 text-xs"> - <button + <Button type="button" + variant="link" + size="sm" onClick={() => onAddChild( root.id, @@ -89,12 +92,14 @@ export default function QueryBuilder({ ) } data-testid="compact-add-layer" - className="underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" > + Add layer - </button> - <button + </Button> + <Button type="button" + variant="link" + size="sm" onClick={() => onAddChild( root.id, @@ -104,10 +109,10 @@ export default function QueryBuilder({ }), ) } - className="underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" > + Add group - </button> + </Button> </div> </div> ); diff --git a/common/components/QueryGroupView.tsx b/common/components/QueryGroupView.tsx @@ -16,6 +16,11 @@ import { } from "../lib/searchQuery"; import type { GroupState, LeafState } from "../lib/searchEval"; import QueryLeafView from "./QueryLeafView"; +import { Button } from "./ui/button"; +import { Checkbox } from "./ui/checkbox"; + +const negateCheckboxClass = + "border-destructive/60 data-[state=checked]:bg-destructive data-[state=checked]:border-destructive data-[state=checked]:text-white"; type Props = { group: GroupNode; @@ -45,7 +50,7 @@ export default function QueryGroupView({ const state = groupStates.get(group.id); const onlyLeafCount = group.children.filter(isLeaf).length; - const indent = depth === 0 ? "" : "pl-4 border-l-2 border-zinc-200 dark:border-zinc-800"; + const indent = depth === 0 ? "" : "pl-4 border-l-2 border-border"; return ( <div @@ -60,20 +65,19 @@ export default function QueryGroupView({ onChangeNode(group.id, { ...group, op: e.target.value as "AND" | "OR" }) } data-testid={`group-op-${group.id}`} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-1.5 py-1 text-xs font-medium" + className="rounded border border-border bg-card text-foreground px-1.5 py-1 text-xs font-medium" > <option value="AND">ALL of (AND)</option> <option value="OR">ANY of (OR)</option> </select> {!isRoot && ( <label className="flex items-center gap-1 select-none"> - <input - type="checkbox" + <Checkbox checked={group.negate} - onChange={(e) => - onChangeNode(group.id, { ...group, negate: e.target.checked }) + onCheckedChange={(checked) => + onChangeNode(group.id, { ...group, negate: checked === true }) } - className="accent-red-500" + className={negateCheckboxClass} data-testid={`group-negate-${group.id}`} /> NOT @@ -81,7 +85,7 @@ export default function QueryGroupView({ )} {state && ( <span - className="text-zinc-500 dark:text-zinc-400" + className="text-muted-foreground" data-testid={`group-count-${group.id}`} > → {state.slugCount} video{state.slugCount === 1 ? "" : "s"} @@ -89,10 +93,12 @@ export default function QueryGroupView({ )} {!isRoot && ( <div className="ml-auto flex items-center gap-1"> - <button + <Button type="button" + variant="link" + size="sm" onClick={() => onUnwrapGroup(group.id)} - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" title={ group.negate ? "Flatten this group into its parent (each child gets NOT applied)" @@ -101,16 +107,18 @@ export default function QueryGroupView({ data-testid={`group-unwrap-${group.id}`} > unwrap - </button> - <button + </Button> + <Button type="button" + variant="outline" + size="icon-xs" onClick={() => onRemoveNode(group.id)} aria-label="Remove group" data-testid={`group-delete-${group.id}`} - className="text-xs rounded border border-zinc-300 dark:border-zinc-700 w-6 h-6 inline-flex items-center justify-center hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="text-xs" > × - </button> + </Button> </div> )} </div> @@ -154,8 +162,10 @@ export default function QueryGroupView({ </div> <div className="flex flex-wrap items-center gap-2 text-xs"> - <button + <Button type="button" + variant="outline" + size="sm" onClick={() => onAddChild( group.id, @@ -163,12 +173,13 @@ export default function QueryGroupView({ ) } data-testid={`group-add-leaf-${group.id}`} - className="rounded border border-zinc-300 dark:border-zinc-700 px-2 py-1 hover:bg-zinc-100 dark:hover:bg-zinc-800" > + Add layer - </button> - <button + </Button> + <Button type="button" + variant="outline" + size="sm" onClick={() => onAddChild( group.id, @@ -179,10 +190,9 @@ export default function QueryGroupView({ ) } data-testid={`group-add-group-${group.id}`} - className="rounded border border-zinc-300 dark:border-zinc-700 px-2 py-1 hover:bg-zinc-100 dark:hover:bg-zinc-800" > + Add group - </button> + </Button> </div> </div> ); diff --git a/common/components/QueryLeafView.tsx b/common/components/QueryLeafView.tsx @@ -11,6 +11,13 @@ import type { LeafNode, LayerScope } from "../lib/searchQuery"; import type { LeafState } from "../lib/searchEval"; import { LayerSwatch, swatchSoftBgStyle } from "./LayerSwatch"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Checkbox } from "./ui/checkbox"; +import { cn } from "../lib/utils"; + +const negateCheckboxClass = + "border-destructive/60 data-[state=checked]:bg-destructive data-[state=checked]:border-destructive data-[state=checked]:text-white"; type Props = { leaf: LeafNode; @@ -70,29 +77,28 @@ export default function QueryLeafView({ : "Search transcripts..."; const queryInput = ( - <input + <Input type="search" value={leaf.query} onChange={(e) => update({ query: e.target.value })} placeholder={placeholder} data-testid={`leaf-query-${leaf.id}`} - className={`flex-1 min-w-0 rounded-lg border ${ - regexInvalid - ? "border-red-500" - : "border-zinc-300 dark:border-zinc-700" - } bg-white dark:bg-zinc-900 px-3 py-2 text-base outline-none focus:ring-2 focus:ring-blue-500 ${ - leaf.useRegex ? "font-mono" : "" - }`} + aria-invalid={regexInvalid || undefined} + className={cn( + "flex-1 min-w-0", + regexInvalid && "border-destructive", + leaf.useRegex && "font-mono", + )} /> ); const scopeSelect = ( - <label className="flex items-center gap-1.5 text-xs text-zinc-600 dark:text-zinc-400"> + <label className="flex items-center gap-1.5 text-xs text-muted-foreground"> <select value={leaf.scope} onChange={(e) => update({ scope: e.target.value as LayerScope })} data-testid={`leaf-scope-${leaf.id}`} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-1.5 py-1 text-xs" + className="rounded border border-border bg-card text-foreground px-1.5 py-1 text-xs font-mono" > {(["transcripts", "chat", "metadata", "description", "tags"] as const).map((s) => ( <option key={s} value={s}> @@ -105,11 +111,9 @@ export default function QueryLeafView({ const regexToggle = ( <label className="flex items-center gap-1 text-xs select-none"> - <input - type="checkbox" + <Checkbox checked={leaf.useRegex} - onChange={(e) => update({ useRegex: e.target.checked })} - className="accent-blue-600" + onCheckedChange={(checked) => update({ useRegex: checked === true })} data-testid={`leaf-regex-${leaf.id}`} /> Regex @@ -118,7 +122,7 @@ export default function QueryLeafView({ const countBadge = state ? ( <span - className="ml-auto text-xs text-zinc-500 dark:text-zinc-400 shrink-0" + className="ml-auto text-xs text-muted-foreground shrink-0" data-testid={`leaf-count-${leaf.id}`} > → {state.slugCount} video{state.slugCount === 1 ? "" : "s"} @@ -144,7 +148,7 @@ export default function QueryLeafView({ <LayerSwatch leafId={leaf.id} /> {queryInput} </div> - <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-zinc-600 dark:text-zinc-400"> + <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground"> {scopeSelect} {regexToggle} {countBadge} @@ -156,7 +160,7 @@ export default function QueryLeafView({ return ( <div - className="flex flex-col gap-2 rounded-lg border bg-white/60 dark:bg-zinc-900/40 px-3 py-2" + className="flex flex-col gap-2 rounded-lg border bg-card/60 px-3 py-2" style={swatchSoftBgStyle(leaf.id)} data-leaf-id={leaf.id} > @@ -169,22 +173,23 @@ export default function QueryLeafView({ {scopeSelect} {regexToggle} <label className="flex items-center gap-1 select-none"> - <input - type="checkbox" + <Checkbox checked={leaf.contributeHits} - onChange={(e) => update({ contributeHits: e.target.checked })} - className="accent-blue-600" + onCheckedChange={(checked) => + update({ contributeHits: checked === true }) + } data-testid={`leaf-hits-${leaf.id}`} /> Show hits in results </label> {canNegate && ( <label className="flex items-center gap-1 select-none"> - <input - type="checkbox" + <Checkbox checked={leaf.negate} - onChange={(e) => update({ negate: e.target.checked })} - className="accent-red-500" + onCheckedChange={(checked) => + update({ negate: checked === true }) + } + className={negateCheckboxClass} data-testid={`leaf-negate-${leaf.id}`} /> NOT @@ -193,30 +198,34 @@ export default function QueryLeafView({ {cachedBadge} <div className="ml-auto flex items-center gap-1"> {onWrapInGroup && ( - <button + <Button type="button" + variant="link" + size="sm" onClick={onWrapInGroup} - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" title="Wrap this layer in a sub-group so you can add OR siblings" data-testid={`leaf-wrap-${leaf.id}`} > wrap - </button> + </Button> )} - <button + <Button type="button" + variant="outline" + size="icon-xs" onClick={onDelete} disabled={!canDelete} aria-label="Remove layer" data-testid={`leaf-delete-${leaf.id}`} - className="text-xs rounded border border-zinc-300 dark:border-zinc-700 w-6 h-6 inline-flex items-center justify-center hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed" + className="text-xs" > × - </button> + </Button> </div> </div> {regexInvalid && ( - <p className="text-xs text-red-600 font-mono">Invalid regex</p> + <p className="text-xs text-destructive font-mono">Invalid regex</p> )} </div> ); diff --git a/common/components/StreamActionLog.tsx b/common/components/StreamActionLog.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/navigation"; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import type { StreamActionResult } from "../jobs/streamCommand"; +import { Button } from "./ui/button"; type Props = { trigger: () => Promise<StreamActionResult>; @@ -158,27 +159,31 @@ export function StreamActionLog({ return ( <div className="flex flex-col gap-2"> <div className="flex items-center gap-3 flex-wrap"> - <button + <Button type="button" + size="sm" onClick={handleClick} disabled={running || disabled} - className="px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" > {running ? runningLabel : buttonLabel} - </button> + </Button> {running && jobId && cancelAction && ( - <button + <Button type="button" + variant="outline" + size="sm" onClick={handleCancel} disabled={cancelling} aria-label={`Cancel ${accessibleName}`} - className="px-3 py-2 rounded-md border border-red-300 dark:border-red-800 text-sm font-medium text-red-700 dark:text-red-300 hover:bg-red-50 dark:hover:bg-red-950 disabled:opacity-50" + className="border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive" > {cancelling ? "Cancelling…" : "Cancel"} - </button> + </Button> )} {jobId && ( - <span className="text-xs text-zinc-500 font-mono">job {jobId}</span> + <span className="text-xs text-muted-foreground font-mono"> + job {jobId} + </span> )} {extraControls} </div> @@ -188,8 +193,8 @@ export function StreamActionLog({ aria-label={`${accessibleName} ${errorIsInfo ? "notice" : "error"}`} className={ errorIsInfo - ? "rounded border border-zinc-300 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 px-3 py-2 text-sm text-zinc-700 dark:text-zinc-300" - : "rounded border border-red-300 bg-red-50 dark:border-red-800 dark:bg-red-950 px-3 py-2 text-sm text-red-700 dark:text-red-300" + ? "rounded-md border border-border bg-muted px-3 py-2 text-sm text-muted-foreground" + : "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive" } > {error} @@ -199,7 +204,7 @@ export function StreamActionLog({ <div role="status" aria-label={`${accessibleName} queue status`} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900 px-3 py-2 text-sm text-zinc-700 dark:text-zinc-300" + className="rounded-md border border-border bg-muted px-3 py-2 text-sm text-muted-foreground" > Queued in <code className="font-mono">{poll.queueKey}</code> {typeof poll.queuePosition === "number" && poll.queuePosition > 0 @@ -213,7 +218,7 @@ export function StreamActionLog({ onScroll={handleScroll} role="log" aria-label={`${accessibleName} output`} - className="text-xs font-mono bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded p-3 h-96 overflow-auto whitespace-pre-wrap" + className="text-xs font-mono bg-muted border border-border rounded-md p-3 h-96 overflow-auto whitespace-pre-wrap" > {log || "Waiting for output…"} </pre> diff --git a/common/components/ThemeMenu.tsx b/common/components/ThemeMenu.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { Palette } from "lucide-react"; +import { useTheme } from "./ThemeProvider"; +import { + THEME_FAMILIES, + THEME_MODES, + type ThemeFamily, + type ThemeMode, +} from "./themeConfig"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; +import { cn } from "../lib/utils"; + +// The theme-family picker: a palette dropdown to choose the family (Base / +// Archive / Terminal / Swiss) and the mode (light / dark / system). Sits beside +// the quick-toggle ThemeToggle. Each family is a pure CSS token swap, so picking +// one only changes `data-theme` on <html> (handled by setTheme). Must render +// inside a <ThemeProvider/>. +export function ThemeMenu({ className }: { className?: string }) { + const { theme, mode, setTheme, setMode } = useTheme(); + + return ( + <DropdownMenu> + <DropdownMenuTrigger + aria-label="Choose theme" + title="Theme" + className={cn( + "inline-flex h-8 w-8 items-center justify-center rounded-md border border-border text-muted-foreground transition-colors hover:text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring", + className, + )} + > + <Palette className="h-4 w-4" aria-hidden="true" /> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="min-w-44"> + <DropdownMenuLabel>Theme</DropdownMenuLabel> + <DropdownMenuRadioGroup + value={theme} + onValueChange={(v) => setTheme(v as ThemeFamily)} + > + {THEME_FAMILIES.map((f) => ( + <DropdownMenuRadioItem key={f.id} value={f.id}> + {f.label} + </DropdownMenuRadioItem> + ))} + </DropdownMenuRadioGroup> + <DropdownMenuSeparator /> + <DropdownMenuLabel>Mode</DropdownMenuLabel> + <DropdownMenuRadioGroup + value={mode} + onValueChange={(v) => setMode(v as ThemeMode)} + > + {THEME_MODES.map((m) => ( + <DropdownMenuRadioItem key={m.id} value={m.id}> + {m.label} + </DropdownMenuRadioItem> + ))} + </DropdownMenuRadioGroup> + </DropdownMenuContent> + </DropdownMenu> + ); +} diff --git a/common/components/ThemeProvider.tsx b/common/components/ThemeProvider.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import { + THEME_KEY, + MODE_KEY, + type ThemeFamily, + type ThemeMode, +} from "./themeConfig"; + +// Runtime theme controller. The pre-paint <ThemeScript/> is the single source of +// truth for FIRST paint — it has already set the correct attributes on <html> +// from localStorage. This provider must therefore NEVER write to the DOM on +// mount (doing so would briefly fight the script whenever the persisted mode +// differs from `defaultMode`, causing a flash). It only writes to the DOM in +// response to explicit user changes and live OS-preference changes; on mount it +// merely adopts the persisted values into React state for the toggle UI. + +type ThemeContextValue = { + theme: ThemeFamily; + mode: ThemeMode; + /** Whether dark is currently applied (resolves "system"). */ + isDark: boolean; + setTheme: (t: ThemeFamily) => void; + setMode: (m: ThemeMode) => void; + /** Cycle light → dark → system → light. */ + cycleMode: () => void; +}; + +const ThemeContext = createContext<ThemeContextValue | null>(null); + +function systemPrefersDark(): boolean { + return ( + typeof window !== "undefined" && + window.matchMedia("(prefers-color-scheme: dark)").matches + ); +} + +function resolveDark(mode: ThemeMode): boolean { + return mode === "system" ? systemPrefersDark() : mode === "dark"; +} + +function applyToDom(theme: ThemeFamily, dark: boolean) { + const d = document.documentElement; + if (theme && theme !== "base") d.setAttribute("data-theme", theme); + else d.removeAttribute("data-theme"); + d.classList.toggle("dark", dark); +} + +export function ThemeProvider({ + defaultTheme = "base", + defaultMode = "system", + children, +}: { + defaultTheme?: ThemeFamily; + defaultMode?: ThemeMode; + children: React.ReactNode; +}) { + // Initial state MUST equal what the server rendered (defaults), so hydration + // matches; the persisted values are adopted in an effect just below. + const [theme, setThemeState] = useState<ThemeFamily>(defaultTheme); + const [mode, setModeState] = useState<ThemeMode>(defaultMode); + + // Adopt persisted values into React state (NOT the DOM — the script owns that). + useEffect(() => { + try { + const t = localStorage.getItem(THEME_KEY) as ThemeFamily | null; + const m = localStorage.getItem(MODE_KEY) as ThemeMode | null; + if (t) setThemeState(t); + if (m) setModeState(m); + } catch { + /* ignore */ + } + }, []); + + // Track OS changes while in "system" mode (the only case the script can't + // keep live after load). + useEffect(() => { + if (mode !== "system") return; + const mql = window.matchMedia("(prefers-color-scheme: dark)"); + const onChange = () => applyToDom(theme, mql.matches); + mql.addEventListener("change", onChange); + return () => mql.removeEventListener("change", onChange); + }, [mode, theme]); + + const setTheme = useCallback( + (t: ThemeFamily) => { + setThemeState(t); + applyToDom(t, resolveDark(mode)); + try { + localStorage.setItem(THEME_KEY, t); + } catch { + /* ignore */ + } + }, + [mode], + ); + + const setMode = useCallback( + (m: ThemeMode) => { + setModeState(m); + applyToDom(theme, resolveDark(m)); + try { + localStorage.setItem(MODE_KEY, m); + } catch { + /* ignore */ + } + }, + [theme], + ); + + const cycleMode = useCallback(() => { + setMode(mode === "light" ? "dark" : mode === "dark" ? "system" : "light"); + }, [mode, setMode]); + + const value = useMemo<ThemeContextValue>( + () => ({ theme, mode, isDark: resolveDark(mode), setTheme, setMode, cycleMode }), + [theme, mode, setTheme, setMode, cycleMode], + ); + + return ( + <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider> + ); +} + +export function useTheme(): ThemeContextValue { + const ctx = useContext(ThemeContext); + if (!ctx) throw new Error("useTheme must be used within a ThemeProvider"); + return ctx; +} diff --git a/common/components/ThemeScript.tsx b/common/components/ThemeScript.tsx @@ -0,0 +1,52 @@ +import { + THEME_KEY, + MODE_KEY, + ACCENT_KEY, + type ThemeFamily, + type ThemeMode, +} from "./themeConfig"; + +// Server component (NOT "use client"): renders an inline <script> that runs +// synchronously BEFORE first paint, so there is no flash of the wrong theme. +// Reads localStorage and sets `data-theme` (family) + `.dark` (mode) + an +// optional per-user `--brand` accent override on <html>. Works identically for +// static-export sites and the server editor. Pair with `suppressHydrationWarning` +// on <html> since this mutates the element before React hydrates. +// +// Accent precedence (highest first): user override (this script) > per-site +// default (an inline <html style="--brand:…"> baked at build) > family token. +// `--brand` is the family brand color (brass/blue); shadcn's neutral `--accent` +// is a separate token and is never overridden here. +export function ThemeScript({ + defaultTheme = "base", + defaultMode = "system", +}: { + defaultTheme?: ThemeFamily; + defaultMode?: ThemeMode; +}) { + const js = + "(function(){try{" + + "var d=document.documentElement;" + + "var t=localStorage.getItem(" + + JSON.stringify(THEME_KEY) + + ")||" + + JSON.stringify(defaultTheme) + + ";" + + "if(t&&t!=='base'){d.setAttribute('data-theme',t);}else{d.removeAttribute('data-theme');}" + + "var m=localStorage.getItem(" + + JSON.stringify(MODE_KEY) + + ")||" + + JSON.stringify(defaultMode) + + ";" + + "var dark=m==='dark'||(m!=='light'&&window.matchMedia('(prefers-color-scheme: dark)').matches);" + + "d.classList.toggle('dark',dark);" + + "var a=localStorage.getItem(" + + JSON.stringify(ACCENT_KEY) + + ");" + + "if(a){d.style.setProperty('--brand',a);}" + + "}catch(e){}})();"; + + return ( + <script dangerouslySetInnerHTML={{ __html: js }} suppressHydrationWarning /> + ); +} diff --git a/common/components/ThemeToggle.tsx b/common/components/ThemeToggle.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { Sun, Moon, Monitor } from "lucide-react"; +import { useTheme } from "./ThemeProvider"; +import { cn } from "../lib/utils"; + +// Minimal mode toggle: cycles light → dark → system. A plain, dependency-light +// control for Phase 0; it gets re-skinned onto the shadcn kit (and gains a +// family/theme menu) in later phases. Must be rendered inside a <ThemeProvider/>. +const ICON = { light: Sun, dark: Moon, system: Monitor } as const; +const NEXT_LABEL = { + light: "Switch to dark", + dark: "Switch to system", + system: "Switch to light", +} as const; + +export function ThemeToggle({ className }: { className?: string }) { + const { mode, cycleMode } = useTheme(); + const Icon = ICON[mode]; + + return ( + <button + type="button" + onClick={cycleMode} + aria-label={NEXT_LABEL[mode]} + title={`Theme: ${mode}`} + data-theme-mode={mode} + className={cn( + "inline-flex h-8 w-8 items-center justify-center rounded-md border border-[var(--border)] text-[var(--muted-foreground)] transition-colors hover:text-[var(--foreground)] hover:border-[var(--border-strong,var(--border))]", + className, + )} + > + <Icon className="h-4 w-4" aria-hidden="true" /> + </button> + ); +} diff --git a/common/components/TranscriptSearch.tsx b/common/components/TranscriptSearch.tsx @@ -48,6 +48,8 @@ import { saveAdvanced, } from "./exportAdvancedStorage"; import { AgeRestrictedBadge, LivestreamBadge } from "./badges"; +import { Button } from "./ui/button"; +import { Checkbox } from "./ui/checkbox"; import QueryBuilder from "./QueryBuilder"; import { defaultChartShape, @@ -1362,29 +1364,29 @@ export default function TranscriptSearch() { groupStates={groupStates} /> ) : ( - <div className="h-12 rounded-lg border border-zinc-300 dark:border-zinc-700 bg-zinc-100 dark:bg-zinc-900/40" /> + <div className="h-12 rounded-lg border border-border bg-muted" /> )} <div className="flex items-center gap-2 flex-wrap"> - <button + <Button type="submit" data-testid="search-submit" - className={`shrink-0 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ - queryDirty || filtersDirty - ? "bg-blue-600 text-white hover:bg-blue-700" - : "border border-zinc-300 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800" - }`} + data-dirty={queryDirty || filtersDirty ? "true" : "false"} + variant={queryDirty || filtersDirty ? "default" : "outline"} + className="shrink-0" > Search - </button> - <button + </Button> + <Button type="button" + variant="link" + size="sm" onClick={handleResetLayers} disabled={layersResetDisabled} data-testid="reset-layers" - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 disabled:opacity-40 disabled:no-underline disabled:cursor-not-allowed" + className="text-xs text-muted-foreground" > Reset layers - </button> + </Button> {(queryDirty || filtersDirty) && ( <span className="text-xs text-amber-600 dark:text-amber-400"> Press Enter or click Search to apply @@ -1392,7 +1394,7 @@ export default function TranscriptSearch() { )} {hasSubs && ( <span - className="text-xs text-zinc-400 ml-auto" + className="text-xs text-muted-foreground ml-auto" title={`${liveChatTotalCount} videos have live chat`} > Live chat available on {liveChatTotalCount} video @@ -1413,18 +1415,18 @@ export default function TranscriptSearch() { e.preventDefault(); toggleFiltersCollapsed(); }} - className="cursor-pointer select-none flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm text-zinc-600 dark:text-zinc-400" + className="cursor-pointer select-none flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm text-muted-foreground" > - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> Filters </span> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> {selectedChannelNames.length} of {channelOptions.length} channel {channelOptions.length === 1 ? "" : "s"} </span> {filtersCollapsed && selectedChannelNames.length > 0 && ( <span - className="text-xs text-zinc-500 truncate min-w-0 flex-1" + className="text-xs text-muted-foreground truncate min-w-0 flex-1" title={selectedChannelsLabel} data-testid="filters-summary-channels" > @@ -1432,11 +1434,11 @@ export default function TranscriptSearch() { </span> )} </summary> - <div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-zinc-600 dark:text-zinc-400"> + <div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground"> {channelOptions.length > 1 && ( <div className="flex flex-col gap-2 w-full"> <div className="flex flex-wrap items-center gap-x-3 gap-y-1"> - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> Channels </span> <ChannelAllToggle @@ -1444,28 +1446,34 @@ export default function TranscriptSearch() { draftExcludedChannels={draftExcludedChannels} setDraftExcludedChannels={setDraftExcludedChannels} /> - <button + <Button type="button" + variant="link" + size="sm" onClick={handleResetChannels} - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" > Reset channels - </button> - <button + </Button> + <Button type="button" + variant="link" + size="sm" onClick={handleResetAllFilters} - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" > Reset all filters - </button> - <button + </Button> + <Button type="button" + variant="link" + size="sm" onClick={handleResetEverything} data-testid="reset-everything" - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" > Reset everything - </button> + </Button> </div> <div className="flex flex-col gap-1.5"> {channelGroupings.map(({ group, channels }) => { @@ -1485,44 +1493,48 @@ export default function TranscriptSearch() { (e.target as HTMLDetailsElement).open, ) } - className="rounded border border-zinc-200 dark:border-zinc-800 bg-white/40 dark:bg-zinc-900/40" + className="rounded border border-border bg-card/60" > <summary className="cursor-pointer select-none flex flex-wrap items-center gap-x-3 gap-y-1 px-3 py-1.5 text-sm"> {group.name && ( - <span className="font-medium text-zinc-700 dark:text-zinc-200"> + <span className="font-medium text-foreground"> {group.name} </span> )} - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> ({selectedCount}/{channels.length} selected) </span> <span className="ml-auto flex items-center gap-2"> - <button + <Button type="button" + variant="link" + size="sm" onClick={(e) => { e.preventDefault(); e.stopPropagation(); setSelected(channels, true); }} - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" > All - </button> - <button + </Button> + <Button type="button" + variant="link" + size="sm" onClick={(e) => { e.preventDefault(); e.stopPropagation(); setSelected(channels, false); }} - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" > None - </button> + </Button> </span> </summary> {group.description && ( - <div className="px-3 pt-0 pb-1 text-xs text-zinc-500"> + <div className="px-3 pt-0 pb-1 text-xs text-muted-foreground"> {group.description} </div> )} @@ -1534,18 +1546,16 @@ export default function TranscriptSearch() { key={name} className="flex items-center gap-1.5 select-none" > - <input - type="checkbox" + <Checkbox checked={checked} - onChange={(e) => { + onCheckedChange={(value) => { setDraftExcludedChannels((prev) => { const next = new Set(prev); - if (e.target.checked) next.delete(name); + if (value === true) next.delete(name); else next.add(name); return next; }); }} - className="accent-blue-600" /> {name} </label> @@ -1559,85 +1569,71 @@ export default function TranscriptSearch() { </div> )} <div className="flex flex-wrap items-center gap-x-3 gap-y-1"> - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> Type </span> <label className="flex items-center gap-1.5 select-none"> - <input - type="checkbox" + <Checkbox checked={!draftNov} - onChange={(e) => setDraftNov(!e.target.checked)} - className="accent-blue-600" + onCheckedChange={(value) => setDraftNov(value !== true)} /> Videos </label> <label className="flex items-center gap-1.5 select-none"> - <input - type="checkbox" + <Checkbox checked={!draftNol} - onChange={(e) => setDraftNol(!e.target.checked)} - className="accent-blue-600" + onCheckedChange={(value) => setDraftNol(value !== true)} /> Livestreams </label> </div> <div className="flex flex-wrap items-center gap-x-3 gap-y-1"> - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> Audience </span> <label className="flex items-center gap-1.5 select-none"> - <input - type="checkbox" + <Checkbox checked={!draftNaa} - onChange={(e) => setDraftNaa(!e.target.checked)} - className="accent-blue-600" + onCheckedChange={(value) => setDraftNaa(value !== true)} /> All ages </label> <label className="flex items-center gap-1.5 select-none"> - <input - type="checkbox" + <Checkbox checked={!draftNar} - onChange={(e) => setDraftNar(!e.target.checked)} - className="accent-blue-600" + onCheckedChange={(value) => setDraftNar(value !== true)} /> Age-restricted </label> </div> <div className="flex flex-wrap items-center gap-x-3 gap-y-1"> - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> Availability </span> <label className="flex items-center gap-1.5 select-none"> - <input - type="checkbox" + <Checkbox checked={!draftNav} - onChange={(e) => setDraftNav(!e.target.checked)} - className="accent-blue-600" + onCheckedChange={(value) => setDraftNav(value !== true)} /> Available </label> <label className="flex items-center gap-1.5 select-none"> - <input - type="checkbox" + <Checkbox checked={!draftNu} - onChange={(e) => setDraftNu(!e.target.checked)} - className="accent-blue-600" + onCheckedChange={(value) => setDraftNu(value !== true)} /> Unlisted </label> <label className="flex items-center gap-1.5 select-none"> - <input - type="checkbox" + <Checkbox checked={!draftNd} - onChange={(e) => setDraftNd(!e.target.checked)} - className="accent-blue-600" + onCheckedChange={(value) => setDraftNd(value !== true)} /> Deleted </label> </div> <div className="flex flex-wrap items-center gap-x-3 gap-y-1"> - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> Date </span> <label className="flex items-center gap-1.5 select-none"> @@ -1649,7 +1645,7 @@ export default function TranscriptSearch() { onChange={(e) => setDraftDateFrom(inputToYmd(e.target.value) ?? "") } - className="rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded-md border border-border bg-card text-foreground px-2 py-1 text-sm" /> </label> <label className="flex items-center gap-1.5 select-none"> @@ -1661,7 +1657,7 @@ export default function TranscriptSearch() { onChange={(e) => setDraftDateTo(inputToYmd(e.target.value) ?? "") } - className="rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded-md border border-border bg-card text-foreground px-2 py-1 text-sm" /> </label> </div> @@ -1669,11 +1665,11 @@ export default function TranscriptSearch() { </details> )} - <details className="-mt-3 text-sm text-zinc-600 dark:text-zinc-400"> - <summary className="cursor-pointer select-none text-xs uppercase tracking-wide text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"> + <details className="-mt-3 text-sm text-muted-foreground"> + <summary className="cursor-pointer select-none text-xs uppercase tracking-wide text-muted-foreground hover:text-foreground"> Advanced options </summary> - <div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 p-4 rounded-md border border-zinc-200 dark:border-zinc-800 bg-white/40 dark:bg-zinc-900/40"> + <div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 p-4 rounded-md border border-border bg-card/60"> <LimitField label="Max hits per batch" hint="Pause after this many matches; “Show more” adds another batch." @@ -1702,18 +1698,22 @@ export default function TranscriptSearch() { onChange={setFlushIntervalMs} /> <div className="sm:col-span-2 flex flex-wrap gap-2 pt-1"> - <button + <Button type="button" + variant="outline" + size="sm" onClick={() => { setHitBatchUnlimited(true); setHitLimit(Infinity); }} - className="px-3 py-1.5 rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-800 text-xs" + className="text-xs" > Search through all transcripts - </button> - <button + </Button> + <Button type="button" + variant="outline" + size="sm" onClick={() => { setHitBatchValue(DEFAULT_MAX_HITS); setHitBatchUnlimited(false); @@ -1721,16 +1721,16 @@ export default function TranscriptSearch() { setFlushIntervalMs(DEFAULT_FLUSH_INTERVAL_MS); clearAdvanced(); }} - className="px-3 py-1.5 rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-800 text-xs" + className="text-xs" > Reset to defaults - </button> + </Button> </div> </div> </details> <section data-testid="results-section"> - <h2 className="text-sm font-medium text-zinc-500 mb-2 flex items-center gap-2 flex-wrap"> + <h2 className="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-2 flex-wrap"> <span data-testid="results-summary"> {hasActiveQuery ? ( <> @@ -1746,27 +1746,27 @@ export default function TranscriptSearch() { )} </span> {(indexLoading || (!hasActiveQuery && summariesLoading)) && ( - <span className="text-xs text-zinc-400 font-normal"> + <span className="text-xs text-muted-foreground font-normal"> loading index{indexProgress ? ` (${indexProgress})` : ""}… </span> )} {searching && ( - <span className="text-xs text-zinc-400 font-normal"> + <span className="text-xs text-muted-foreground font-normal"> searched {processed}/{totalToProcess} {pipelineActive ? "…" : ""} </span> )} <span data-testid="view-toggle" - className="ml-auto inline-flex overflow-hidden rounded-md border border-zinc-300 dark:border-zinc-700" + className="ml-auto inline-flex overflow-hidden rounded-md border border-border bg-muted" > <button type="button" onClick={() => setView("results")} className={`px-2.5 py-1 text-xs font-normal ${ view === "results" - ? "bg-blue-600 text-white" - : "bg-white dark:bg-zinc-900 hover:bg-zinc-100 dark:hover:bg-zinc-800" + ? "bg-primary text-primary-foreground" + : "text-muted-foreground hover:bg-accent hover:text-accent-foreground" }`} > Results @@ -1776,8 +1776,8 @@ export default function TranscriptSearch() { onClick={showChartView} className={`px-2.5 py-1 text-xs font-normal ${ view === "chart" - ? "bg-blue-600 text-white" - : "bg-white dark:bg-zinc-900 hover:bg-zinc-100 dark:hover:bg-zinc-800" + ? "bg-primary text-primary-foreground" + : "text-muted-foreground hover:bg-accent hover:text-accent-foreground" }`} > Chart @@ -1790,9 +1790,9 @@ export default function TranscriptSearch() { <details data-testid="chart-options" open - className="rounded-lg border border-zinc-200 dark:border-zinc-800 px-3 py-2" + className="rounded-lg border border-border px-3 py-2" > - <summary className="cursor-pointer text-sm font-medium text-zinc-600 dark:text-zinc-300"> + <summary className="cursor-pointer text-sm font-medium text-foreground"> Chart options </summary> <div className="pt-3"> @@ -1810,7 +1810,7 @@ export default function TranscriptSearch() { ) : ( <> {!hasActiveQuery && !summariesLoading && resultGroups.length > 0 && ( - <p className="text-sm text-zinc-500 mb-2" data-testid="browse-hint"> + <p className="text-sm text-muted-foreground mb-2" data-testid="browse-hint"> Showing all videos — type a query above to narrow. </p> )} @@ -1818,11 +1818,11 @@ export default function TranscriptSearch() { ? !indexLoading && resultGroups.length === 0 && !pipelineActive && ( - <p className="text-sm text-zinc-500">No matching videos.</p> + <p className="text-sm text-muted-foreground">No matching videos.</p> ) : !summariesLoading && resultGroups.length === 0 && ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> No videos match the current filters. </p> )} @@ -1838,13 +1838,13 @@ export default function TranscriptSearch() { )} {capped && ( <div className="mt-3 flex justify-center"> - <button + <Button type="button" + variant="outline" onClick={() => setHitLimit((l) => l + hitBatchSize)} - className="px-4 py-2 rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-800 text-sm" > Load more results - </button> + </Button> </div> )} </> @@ -2006,26 +2006,27 @@ function ResultCard({ <div data-result-slug={group.slug} data-card-header="" - className="border border-zinc-200 dark:border-zinc-800 rounded-lg overflow-hidden bg-white dark:bg-zinc-900" + className="border border-border rounded-lg overflow-hidden bg-card" > - <button + <Button type="button" + variant="ghost" onClick={() => openWithMode(group.slug)} - className="w-full text-left px-4 py-2 bg-zinc-50 dark:bg-zinc-900/60 border-b border-zinc-200 dark:border-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-baseline gap-2" + className="w-full h-auto justify-start text-left items-baseline gap-2 px-4 py-2 rounded-none border-b border-border bg-muted font-normal" > <span className="font-medium truncate flex-1 min-w-0"> {group.title} </span> {group.isLivestream && <LivestreamBadge />} {group.ageRestricted && <AgeRestrictedBadge />} - <span className="text-xs text-zinc-500 shrink-0"> + <span className="text-xs text-muted-foreground shrink-0"> {group.channel && `${group.channel} · `} {group.date} {group.hits.length > 0 && ` · ${group.hits.length} hit${group.hits.length === 1 ? "" : "s"}`} </span> - </button> - <ul className="flex flex-col divide-y divide-zinc-200 dark:divide-zinc-800"> + </Button> + <ul className="flex flex-col divide-y divide-border"> {Array.from(buckets.entries()).map(([leafId, hits]) => { if (hits.length === 0) return null; const leafInfo = leavesById.get(leafId); @@ -2033,40 +2034,41 @@ function ResultCard({ return ( <li key={leafId}> <div - className="flex items-center gap-2 px-4 py-1.5 bg-zinc-50/60 dark:bg-zinc-900/40 border-b border-zinc-200 dark:border-zinc-800" + className="flex items-center gap-2 px-4 py-1.5 bg-muted/40 border-b border-border" data-leaf-section={leafId} > <LayerSwatch leafId={leafId} size="xs" /> - <span className="text-[10px] uppercase tracking-wide text-zinc-500"> + <span className="text-[10px] uppercase tracking-wide text-muted-foreground"> {leafInfo.scope === "metadata" ? "Title / channel" : leafInfo.scope === "chat" ? "Live chat" : "Transcripts"} </span> - <span className="font-mono text-xs text-zinc-600 dark:text-zinc-400 truncate"> + <span className="font-mono text-xs text-muted-foreground truncate"> {leafInfo.query} </span> - <span className="ml-auto text-[10px] text-zinc-500"> + <span className="ml-auto text-[10px] text-muted-foreground"> {hits.length} hit{hits.length === 1 ? "" : "s"} </span> </div> - <ul className="flex flex-col divide-y divide-zinc-200/60 dark:divide-zinc-800/60"> + <ul className="flex flex-col divide-y divide-border/60"> {hits.map((h, i) => { const isActive = activeVideo === group.slug && activeTime === h.start; return ( <li key={i}> - <button + <Button type="button" + variant="ghost" onClick={() => openWithMode(group.slug, h)} - className={`w-full text-left flex gap-3 px-4 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 ${ + className={`w-full h-auto justify-start text-left flex gap-3 px-4 py-2 rounded-none font-normal whitespace-normal ${ isActive ? "bg-amber-100/70 dark:bg-amber-500/10 ring-1 ring-inset ring-amber-400/60" : "" }`} > - <span className="text-xs font-mono text-zinc-500 shrink-0 w-16"> + <span className="text-xs font-mono text-muted-foreground shrink-0 w-16"> {h.scope === "metadata" ? "—" : formatSeconds(h.start)} @@ -2077,7 +2079,7 @@ function ResultCard({ <span className="flex-1 min-w-0 text-sm"> {highlight(h.text, leafInfo.query, leafInfo.useRegex)} </span> - </button> + </Button> </li> ); })} @@ -2093,7 +2095,7 @@ function ResultCard({ function TrackBadge({ track }: { track: string }) { const label = track === "live_chat" ? "live chat" : track; return ( - <span className="shrink-0 text-[10px] uppercase tracking-wide font-medium px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300 self-center"> + <span className="shrink-0 text-[10px] uppercase tracking-wide font-medium px-1.5 py-0.5 rounded bg-muted text-muted-foreground self-center"> {label} </span> ); @@ -2130,14 +2132,14 @@ function ProfilesRow({ className="flex flex-wrap items-center gap-x-3 gap-y-1" data-testid="profiles-row" > - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> Profile </span> <select value={activeProfileName ?? ""} onChange={(e) => onSelect(e.target.value)} data-testid="profile-select" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-0.5 text-sm" + className="rounded border border-border bg-card text-foreground px-2 py-0.5 text-sm" > <option value="">(unsaved)</option> {profileNames.map((n) => ( @@ -2156,60 +2158,72 @@ function ProfilesRow({ • </span> )} - <button + <Button type="button" + variant="link" + size="sm" onClick={onSave} disabled={!activeProfileName || !profileDirty} - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 disabled:opacity-40 disabled:no-underline" + className="text-xs text-muted-foreground" > Save - </button> - <button + </Button> + <Button type="button" + variant="link" + size="sm" onClick={onSaveAs} - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" > Save as… - </button> + </Button> {showRevert && ( - <button + <Button type="button" + variant="link" + size="sm" onClick={onRevert} data-testid="profile-revert" - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" > Revert - </button> + </Button> )} - <button + <Button type="button" + variant="link" + size="sm" onClick={onRename} disabled={!activeProfileName} - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 disabled:opacity-40 disabled:no-underline" + className="text-xs text-muted-foreground" > Rename - </button> - <button + </Button> + <Button type="button" + variant="link" + size="sm" onClick={onDelete} disabled={!activeProfileName} - className="text-xs underline text-red-600 hover:text-red-700 disabled:opacity-40 disabled:no-underline" + className="text-xs text-destructive" > Delete - </button> + </Button> <div className="ml-auto flex items-center gap-x-3"> - <button + <Button type="button" + variant="link" + size="sm" onClick={onShareCurrentSearch} title={ shareCopied ? "Copied!" : "Copy link encoding the current search + filters" } - className="text-xs underline text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" + className="text-xs text-muted-foreground" > {shareCopied ? "Link copied!" : "Share current search"} - </button> + </Button> </div> </div> ); @@ -2227,15 +2241,13 @@ function ChannelAllToggle({ const allChecked = draftExcludedChannels.size === 0; return ( <label className="flex items-center gap-1.5 select-none"> - <input - type="checkbox" + <Checkbox checked={allChecked} - onChange={() => { + onCheckedChange={() => { setDraftExcludedChannels( allChecked ? new Set(channelOptions) : new Set(), ); }} - className="accent-blue-600" /> All </label> @@ -2269,7 +2281,7 @@ function NumberField({ }) { return ( <label className="flex flex-col gap-1"> - <span className="text-xs font-medium text-zinc-700 dark:text-zinc-300"> + <span className="text-xs font-medium text-foreground"> {label} </span> <input @@ -2288,9 +2300,9 @@ function NumberField({ const n = Math.max(min, Math.min(max, Math.floor(Number(raw)))); if (Number.isFinite(n)) onChange(n); }} - className="w-full rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm disabled:opacity-50" + className="w-full rounded border border-border bg-card text-foreground px-2 py-1 text-sm disabled:opacity-50" /> - {hint && <span className="text-xs text-zinc-500">{hint}</span>} + {hint && <span className="text-xs text-muted-foreground">{hint}</span>} </label> ); } @@ -2315,15 +2327,13 @@ function LimitField({ return ( <div className="flex flex-col gap-1"> <div className="flex items-baseline justify-between gap-2"> - <span className="text-xs font-medium text-zinc-700 dark:text-zinc-300"> + <span className="text-xs font-medium text-foreground"> {label} </span> <label className="flex items-center gap-1 text-xs select-none"> - <input - type="checkbox" + <Checkbox checked={unlimited} - onChange={(e) => onUnlimitedChange(e.target.checked)} - className="accent-blue-600" + onCheckedChange={(value) => onUnlimitedChange(value === true)} /> No limit </label> @@ -2345,9 +2355,9 @@ function LimitField({ const n = Math.max(1, Math.floor(Number(raw))); if (Number.isFinite(n)) onValueChange(n); }} - className="w-full rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm disabled:opacity-50" + className="w-full rounded border border-border bg-card text-foreground px-2 py-1 text-sm disabled:opacity-50" /> - {hint && <span className="text-xs text-zinc-500">{hint}</span>} + {hint && <span className="text-xs text-muted-foreground">{hint}</span>} </div> ); } diff --git a/common/components/badges.tsx b/common/components/badges.tsx @@ -5,7 +5,7 @@ export function LivestreamBadge() { return ( <span title="Livestream" - className={`${badgeBase} bg-zinc-200 text-zinc-700 dark:bg-zinc-700 dark:text-zinc-200`} + className={`${badgeBase} bg-secondary text-secondary-foreground`} > Live </span> diff --git a/common/components/charts/ChartShapeControls.tsx b/common/components/charts/ChartShapeControls.tsx @@ -14,10 +14,11 @@ import { type YMetric, } from "../../lib/chartConfig"; import type { ChartShape } from "../../lib/chartShare"; +import { Checkbox } from "../ui/checkbox"; const sel = - "rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm"; -const lbl = "flex flex-col gap-1 text-xs font-medium text-zinc-500"; + "rounded-md border border-border bg-card text-foreground px-2 py-1 text-sm"; +const lbl = "flex flex-col gap-1 text-xs font-medium text-muted-foreground"; const METRIC_FIELDS: { value: MetricField; label: string }[] = [ { value: "duration", label: "Duration" }, @@ -64,7 +65,7 @@ export function ChartShapeControls({ <div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-col gap-1"> <span className={lbl}>Data source</span> - <div className="inline-flex w-fit overflow-hidden rounded-md border border-zinc-300 dark:border-zinc-700"> + <div className="inline-flex w-fit overflow-hidden rounded-md border border-border bg-muted"> {(["search", "metadata"] as const).map((m) => ( <button key={m} @@ -72,8 +73,8 @@ export function ChartShapeControls({ onClick={() => patch({ dataSource: m })} className={`px-3 py-1 text-sm ${ shape.dataSource === m - ? "bg-blue-600 text-white" - : "bg-white dark:bg-zinc-900 hover:bg-zinc-100 dark:hover:bg-zinc-800" + ? "bg-primary text-primary-foreground" + : "text-muted-foreground hover:bg-accent hover:text-accent-foreground" }`} > {m === "metadata" ? "Metadata" : "Search matches"} @@ -87,18 +88,18 @@ export function ChartShapeControls({ type="button" data-testid="chart-examples-button" onClick={() => setShowExamples((v) => !v)} - className="rounded-md border border-zinc-300 dark:border-zinc-700 px-3 py-1.5 text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground" > Examples ▾ </button> {showExamples && ( <div data-testid="chart-examples-menu" - className="absolute right-0 z-10 mt-1 w-64 rounded-md border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-1 shadow-lg" + className="absolute right-0 z-10 mt-1 w-64 rounded-md border border-border bg-popover text-popover-foreground p-1 shadow-md" > {PRESET_GROUPS.map((group) => ( <div key={group.id} className="py-1"> - <div className="px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-zinc-500"> + <div className="px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"> {group.title} </div> {group.templates.map((t) => ( @@ -109,7 +110,7 @@ export function ChartShapeControls({ onChange(shapeFromConfig(t.config, shape)); setShowExamples(false); }} - className="block w-full rounded px-2 py-1 text-left text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="block w-full rounded px-2 py-1 text-left text-sm hover:bg-accent hover:text-accent-foreground" > {t.title} </button> @@ -154,16 +155,17 @@ export function ChartShapeControls({ </label> <label className="flex items-center gap-2 self-end text-sm"> - <input - type="checkbox" + <Checkbox checked={shape.cumulative} - onChange={(e) => patch({ cumulative: e.target.checked })} + onCheckedChange={(checked) => + patch({ cumulative: checked === true }) + } /> Cumulative </label> </div> - <fieldset className="flex flex-wrap items-end gap-3 rounded-md border border-zinc-200 dark:border-zinc-800 p-3"> + <fieldset className="flex flex-wrap items-end gap-3 rounded-md border border-border p-3"> <legend className="px-1 text-xs font-semibold">X axis</legend> <label className={lbl}> Dimension @@ -275,7 +277,7 @@ export function ChartShapeControls({ </fieldset> {shape.dataSource === "metadata" && ( - <fieldset className="flex flex-wrap items-end gap-3 rounded-md border border-zinc-200 dark:border-zinc-800 p-3"> + <fieldset className="flex flex-wrap items-end gap-3 rounded-md border border-border p-3"> <legend className="px-1 text-xs font-semibold">Y metric</legend> <label className={lbl}> Aggregate diff --git a/common/components/themeConfig.ts b/common/components/themeConfig.ts @@ -0,0 +1,28 @@ +// Shared, dependency-free theme constants + types used by both the pre-paint +// ThemeScript (server) and the runtime ThemeProvider (client). Keys are +// namespaced under the existing `ytdlp-tb:*` localStorage convention. + +export const THEME_KEY = "ytdlp-tb:theme"; +export const MODE_KEY = "ytdlp-tb:mode"; +export const ACCENT_KEY = "ytdlp-tb:accent"; + +// Theme FAMILY (palette personality) — orthogonal to light/dark MODE. +// "base" is the neutral default (no data-theme attribute). Each family is a pure +// CSS token swap (a [data-theme="…"] block in tokens.css); no markup differs. +export type ThemeFamily = "base" | "archive" | "terminal" | "swiss"; + +export type ThemeMode = "light" | "dark" | "system"; + +// Selectable families. Order = display order in the ThemeMenu picker. +export const THEME_FAMILIES: { id: ThemeFamily; label: string }[] = [ + { id: "base", label: "Base" }, + { id: "archive", label: "Archive" }, + { id: "terminal", label: "Terminal" }, + { id: "swiss", label: "Swiss" }, +]; + +export const THEME_MODES: { id: ThemeMode; label: string }[] = [ + { id: "light", label: "Light" }, + { id: "dark", label: "Dark" }, + { id: "system", label: "System" }, +]; diff --git a/common/components/ui/badge.tsx b/common/components/ui/badge.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "../../lib/utils" + +const badgeVariants = cva( + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps<typeof badgeVariants> & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + <Comp + data-slot="badge" + data-variant={variant} + className={cn(badgeVariants({ variant }), className)} + {...props} + /> + ) +} + +export { Badge, badgeVariants } diff --git a/common/components/ui/button.tsx b/common/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "../../lib/utils" + +const buttonVariants = cva( + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps<typeof buttonVariants> & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + <Comp + data-slot="button" + data-variant={variant} + data-size={size} + className={cn(buttonVariants({ variant, size, className }))} + {...props} + /> + ) +} + +export { Button, buttonVariants } diff --git a/common/components/ui/card.tsx b/common/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "../../lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card" + className={cn( + "flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm", + className + )} + {...props} + /> + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-header" + className={cn( + "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", + className + )} + {...props} + /> + ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-title" + className={cn("leading-none font-semibold", className)} + {...props} + /> + ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-description" + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> + ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-action" + className={cn( + "col-start-2 row-span-2 row-start-1 self-start justify-self-end", + className + )} + {...props} + /> + ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-content" + className={cn("px-6", className)} + {...props} + /> + ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-footer" + className={cn("flex items-center px-6 [.border-t]:pt-6", className)} + {...props} + /> + ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/common/components/ui/checkbox.tsx b/common/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import { CheckIcon } from "lucide-react" +import { Checkbox as CheckboxPrimitive } from "radix-ui" + +import { cn } from "../../lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps<typeof CheckboxPrimitive.Root>) { + return ( + <CheckboxPrimitive.Root + data-slot="checkbox" + className={cn( + "peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary", + className + )} + {...props} + > + <CheckboxPrimitive.Indicator + data-slot="checkbox-indicator" + className="grid place-content-center text-current transition-none" + > + <CheckIcon className="size-3.5" /> + </CheckboxPrimitive.Indicator> + </CheckboxPrimitive.Root> + ) +} + +export { Checkbox } diff --git a/common/components/ui/command.tsx b/common/components/ui/command.tsx @@ -0,0 +1,184 @@ +"use client" + +import * as React from "react" +import { Command as CommandPrimitive } from "cmdk" +import { SearchIcon } from "lucide-react" + +import { cn } from "../../lib/utils" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "./dialog" + +function Command({ + className, + ...props +}: React.ComponentProps<typeof CommandPrimitive>) { + return ( + <CommandPrimitive + data-slot="command" + className={cn( + "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", + className + )} + {...props} + /> + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + className, + showCloseButton = true, + ...props +}: React.ComponentProps<typeof Dialog> & { + title?: string + description?: string + className?: string + showCloseButton?: boolean +}) { + return ( + <Dialog {...props}> + <DialogHeader className="sr-only"> + <DialogTitle>{title}</DialogTitle> + <DialogDescription>{description}</DialogDescription> + </DialogHeader> + <DialogContent + className={cn("overflow-hidden p-0", className)} + showCloseButton={showCloseButton} + > + <Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> + {children} + </Command> + </DialogContent> + </Dialog> + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps<typeof CommandPrimitive.Input>) { + return ( + <div + data-slot="command-input-wrapper" + className="flex h-9 items-center gap-2 border-b px-3" + > + <SearchIcon className="size-4 shrink-0 opacity-50" /> + <CommandPrimitive.Input + data-slot="command-input" + className={cn( + "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", + className + )} + {...props} + /> + </div> + ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps<typeof CommandPrimitive.List>) { + return ( + <CommandPrimitive.List + data-slot="command-list" + className={cn( + "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", + className + )} + {...props} + /> + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps<typeof CommandPrimitive.Empty>) { + return ( + <CommandPrimitive.Empty + data-slot="command-empty" + className="py-6 text-center text-sm" + {...props} + /> + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps<typeof CommandPrimitive.Group>) { + return ( + <CommandPrimitive.Group + data-slot="command-group" + className={cn( + "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", + className + )} + {...props} + /> + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps<typeof CommandPrimitive.Separator>) { + return ( + <CommandPrimitive.Separator + data-slot="command-separator" + className={cn("-mx-1 h-px bg-border", className)} + {...props} + /> + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps<typeof CommandPrimitive.Item>) { + return ( + <CommandPrimitive.Item + data-slot="command-item" + className={cn( + "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", + className + )} + {...props} + /> + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + <span + data-slot="command-shortcut" + className={cn( + "ml-auto text-xs tracking-widest text-muted-foreground", + className + )} + {...props} + /> + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/common/components/ui/dialog.tsx b/common/components/ui/dialog.tsx @@ -0,0 +1,158 @@ +"use client" + +import * as React from "react" +import { XIcon } from "lucide-react" +import { Dialog as DialogPrimitive } from "radix-ui" + +import { cn } from "../../lib/utils" +import { Button } from "./button" + +function Dialog({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Root>) { + return <DialogPrimitive.Root data-slot="dialog" {...props} /> +} + +function DialogTrigger({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Trigger>) { + return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> +} + +function DialogPortal({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Portal>) { + return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> +} + +function DialogClose({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Close>) { + return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps<typeof DialogPrimitive.Overlay>) { + return ( + <DialogPrimitive.Overlay + data-slot="dialog-overlay" + className={cn( + "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", + className + )} + {...props} + /> + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps<typeof DialogPrimitive.Content> & { + showCloseButton?: boolean +}) { + return ( + <DialogPortal data-slot="dialog-portal"> + <DialogOverlay /> + <DialogPrimitive.Content + data-slot="dialog-content" + className={cn( + "fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg", + className + )} + {...props} + > + {children} + {showCloseButton && ( + <DialogPrimitive.Close + data-slot="dialog-close" + className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" + > + <XIcon /> + <span className="sr-only">Close</span> + </DialogPrimitive.Close> + )} + </DialogPrimitive.Content> + </DialogPortal> + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="dialog-header" + className={cn("flex flex-col gap-2 text-center sm:text-left", className)} + {...props} + /> + ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( + <div + data-slot="dialog-footer" + className={cn( + "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", + className + )} + {...props} + > + {children} + {showCloseButton && ( + <DialogPrimitive.Close asChild> + <Button variant="outline">Close</Button> + </DialogPrimitive.Close> + )} + </div> + ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps<typeof DialogPrimitive.Title>) { + return ( + <DialogPrimitive.Title + data-slot="dialog-title" + className={cn("text-lg leading-none font-semibold", className)} + {...props} + /> + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps<typeof DialogPrimitive.Description>) { + return ( + <DialogPrimitive.Description + data-slot="dialog-description" + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/common/components/ui/dropdown-menu.tsx b/common/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client" + +import * as React from "react" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui" + +import { cn } from "../../lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { + return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { + return ( + <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { + return ( + <DropdownMenuPrimitive.Trigger + data-slot="dropdown-menu-trigger" + {...props} + /> + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { + return ( + <DropdownMenuPrimitive.Portal> + <DropdownMenuPrimitive.Content + data-slot="dropdown-menu-content" + sideOffset={sideOffset} + className={cn( + "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", + className + )} + {...props} + /> + </DropdownMenuPrimitive.Portal> + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { + return ( + <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + <DropdownMenuPrimitive.Item + data-slot="dropdown-menu-item" + data-inset={inset} + data-variant={variant} + className={cn( + "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", + className + )} + {...props} + /> + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { + return ( + <DropdownMenuPrimitive.CheckboxItem + data-slot="dropdown-menu-checkbox-item" + className={cn( + "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + className + )} + checked={checked} + {...props} + > + <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <CheckIcon className="size-4" /> + </DropdownMenuPrimitive.ItemIndicator> + </span> + {children} + </DropdownMenuPrimitive.CheckboxItem> + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { + return ( + <DropdownMenuPrimitive.RadioGroup + data-slot="dropdown-menu-radio-group" + {...props} + /> + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { + return ( + <DropdownMenuPrimitive.RadioItem + data-slot="dropdown-menu-radio-item" + className={cn( + "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + className + )} + {...props} + > + <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <CircleIcon className="size-2 fill-current" /> + </DropdownMenuPrimitive.ItemIndicator> + </span> + {children} + </DropdownMenuPrimitive.RadioItem> + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { + inset?: boolean +}) { + return ( + <DropdownMenuPrimitive.Label + data-slot="dropdown-menu-label" + data-inset={inset} + className={cn( + "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", + className + )} + {...props} + /> + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { + return ( + <DropdownMenuPrimitive.Separator + data-slot="dropdown-menu-separator" + className={cn("-mx-1 my-1 h-px bg-border", className)} + {...props} + /> + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + <span + data-slot="dropdown-menu-shortcut" + className={cn( + "ml-auto text-xs tracking-widest text-muted-foreground", + className + )} + {...props} + /> + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { + return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { + inset?: boolean +}) { + return ( + <DropdownMenuPrimitive.SubTrigger + data-slot="dropdown-menu-sub-trigger" + data-inset={inset} + className={cn( + "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", + className + )} + {...props} + > + {children} + <ChevronRightIcon className="ml-auto size-4" /> + </DropdownMenuPrimitive.SubTrigger> + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { + return ( + <DropdownMenuPrimitive.SubContent + data-slot="dropdown-menu-sub-content" + className={cn( + "z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", + className + )} + {...props} + /> + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/common/components/ui/input.tsx b/common/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "../../lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + <input + type={type} + data-slot="input" + className={cn( + "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", + "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", + "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", + className + )} + {...props} + /> + ) +} + +export { Input } diff --git a/common/components/ui/label.tsx b/common/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import { Label as LabelPrimitive } from "radix-ui" + +import { cn } from "../../lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps<typeof LabelPrimitive.Root>) { + return ( + <LabelPrimitive.Root + data-slot="label" + className={cn( + "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", + className + )} + {...props} + /> + ) +} + +export { Label } diff --git a/common/components/ui/select.tsx b/common/components/ui/select.tsx @@ -0,0 +1,190 @@ +"use client" + +import * as React from "react" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import { Select as SelectPrimitive } from "radix-ui" + +import { cn } from "../../lib/utils" + +function Select({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Root>) { + return <SelectPrimitive.Root data-slot="select" {...props} /> +} + +function SelectGroup({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Group>) { + return <SelectPrimitive.Group data-slot="select-group" {...props} /> +} + +function SelectValue({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Value>) { + return <SelectPrimitive.Value data-slot="select-value" {...props} /> +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { + size?: "sm" | "default" +}) { + return ( + <SelectPrimitive.Trigger + data-slot="select-trigger" + data-size={size} + className={cn( + "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", + className + )} + {...props} + > + {children} + <SelectPrimitive.Icon asChild> + <ChevronDownIcon className="size-4 opacity-50" /> + </SelectPrimitive.Icon> + </SelectPrimitive.Trigger> + ) +} + +function SelectContent({ + className, + children, + position = "item-aligned", + align = "center", + ...props +}: React.ComponentProps<typeof SelectPrimitive.Content>) { + return ( + <SelectPrimitive.Portal> + <SelectPrimitive.Content + data-slot="select-content" + className={cn( + "relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", + position === "popper" && + "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", + className + )} + position={position} + align={align} + {...props} + > + <SelectScrollUpButton /> + <SelectPrimitive.Viewport + className={cn( + "p-1", + position === "popper" && + "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1" + )} + > + {children} + </SelectPrimitive.Viewport> + <SelectScrollDownButton /> + </SelectPrimitive.Content> + </SelectPrimitive.Portal> + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Label>) { + return ( + <SelectPrimitive.Label + data-slot="select-label" + className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)} + {...props} + /> + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Item>) { + return ( + <SelectPrimitive.Item + data-slot="select-item" + className={cn( + "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", + className + )} + {...props} + > + <span + data-slot="select-item-indicator" + className="absolute right-2 flex size-3.5 items-center justify-center" + > + <SelectPrimitive.ItemIndicator> + <CheckIcon className="size-4" /> + </SelectPrimitive.ItemIndicator> + </span> + <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> + </SelectPrimitive.Item> + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Separator>) { + return ( + <SelectPrimitive.Separator + data-slot="select-separator" + className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)} + {...props} + /> + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { + return ( + <SelectPrimitive.ScrollUpButton + data-slot="select-scroll-up-button" + className={cn( + "flex cursor-default items-center justify-center py-1", + className + )} + {...props} + > + <ChevronUpIcon className="size-4" /> + </SelectPrimitive.ScrollUpButton> + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) { + return ( + <SelectPrimitive.ScrollDownButton + data-slot="select-scroll-down-button" + className={cn( + "flex cursor-default items-center justify-center py-1", + className + )} + {...props} + > + <ChevronDownIcon className="size-4" /> + </SelectPrimitive.ScrollDownButton> + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/common/components/ui/separator.tsx b/common/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import { Separator as SeparatorPrimitive } from "radix-ui" + +import { cn } from "../../lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { + return ( + <SeparatorPrimitive.Root + data-slot="separator" + decorative={decorative} + orientation={orientation} + className={cn( + "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", + className + )} + {...props} + /> + ) +} + +export { Separator } diff --git a/common/components/ui/sheet.tsx b/common/components/ui/sheet.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import { XIcon } from "lucide-react" +import { Dialog as SheetPrimitive } from "radix-ui" + +import { cn } from "../../lib/utils" + +function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { + return <SheetPrimitive.Root data-slot="sheet" {...props} /> +} + +function SheetTrigger({ + ...props +}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { + return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> +} + +function SheetClose({ + ...props +}: React.ComponentProps<typeof SheetPrimitive.Close>) { + return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> +} + +function SheetPortal({ + ...props +}: React.ComponentProps<typeof SheetPrimitive.Portal>) { + return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps<typeof SheetPrimitive.Overlay>) { + return ( + <SheetPrimitive.Overlay + data-slot="sheet-overlay" + className={cn( + "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", + className + )} + {...props} + /> + ) +} + +function SheetContent({ + className, + children, + side = "right", + showCloseButton = true, + ...props +}: React.ComponentProps<typeof SheetPrimitive.Content> & { + side?: "top" | "right" | "bottom" | "left" + showCloseButton?: boolean +}) { + return ( + <SheetPortal> + <SheetOverlay /> + <SheetPrimitive.Content + data-slot="sheet-content" + className={cn( + "fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500", + side === "right" && + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + side === "left" && + "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + side === "top" && + "inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + side === "bottom" && + "inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + className + )} + {...props} + > + {children} + {showCloseButton && ( + <SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary"> + <XIcon className="size-4" /> + <span className="sr-only">Close</span> + </SheetPrimitive.Close> + )} + </SheetPrimitive.Content> + </SheetPortal> + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sheet-header" + className={cn("flex flex-col gap-1.5 p-4", className)} + {...props} + /> + ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sheet-footer" + className={cn("mt-auto flex flex-col gap-2 p-4", className)} + {...props} + /> + ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps<typeof SheetPrimitive.Title>) { + return ( + <SheetPrimitive.Title + data-slot="sheet-title" + className={cn("font-semibold text-foreground", className)} + {...props} + /> + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps<typeof SheetPrimitive.Description>) { + return ( + <SheetPrimitive.Description + data-slot="sheet-description" + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/common/components/ui/sonner.tsx b/common/components/ui/sonner.tsx @@ -0,0 +1,47 @@ +"use client" + +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from "lucide-react" +import { Toaster as Sonner, type ToasterProps } from "sonner" + +import { useTheme } from "../ThemeProvider" + +const Toaster = ({ ...props }: ToasterProps) => { + // Reuse the family theme controller (light | dark | system) rather than + // next-themes, so toast chrome tracks the same mode as the rest of the app. + const { mode } = useTheme() + + return ( + <Sonner + theme={mode as ToasterProps["theme"]} + className="toaster group" + icons={{ + success: <CircleCheckIcon className="size-4" />, + info: <InfoIcon className="size-4" />, + warning: <TriangleAlertIcon className="size-4" />, + error: <OctagonXIcon className="size-4" />, + loading: <Loader2Icon className="size-4 animate-spin" />, + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as React.CSSProperties + } + {...props} + /> + ) +} + +// Re-export the imperative API so consumers (e.g. the editor) call toasts +// through the shared package and don't each need a direct `sonner` dependency. +export { toast } from "sonner" + +export { Toaster } diff --git a/common/components/ui/tabs.tsx b/common/components/ui/tabs.tsx @@ -0,0 +1,91 @@ +"use client" + +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Tabs as TabsPrimitive } from "radix-ui" + +import { cn } from "../../lib/utils" + +function Tabs({ + className, + orientation = "horizontal", + ...props +}: React.ComponentProps<typeof TabsPrimitive.Root>) { + return ( + <TabsPrimitive.Root + data-slot="tabs" + data-orientation={orientation} + orientation={orientation} + className={cn( + "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", + className + )} + {...props} + /> + ) +} + +const tabsListVariants = cva( + "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none", + { + variants: { + variant: { + default: "bg-muted", + line: "gap-1 bg-transparent", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function TabsList({ + className, + variant = "default", + ...props +}: React.ComponentProps<typeof TabsPrimitive.List> & + VariantProps<typeof tabsListVariants>) { + return ( + <TabsPrimitive.List + data-slot="tabs-list" + data-variant={variant} + className={cn(tabsListVariants({ variant }), className)} + {...props} + /> + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Trigger>) { + return ( + <TabsPrimitive.Trigger + data-slot="tabs-trigger" + className={cn( + "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", + "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground", + "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", + className + )} + {...props} + /> + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Content>) { + return ( + <TabsPrimitive.Content + data-slot="tabs-content" + className={cn("flex-1 outline-none", className)} + {...props} + /> + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/common/components/ui/tooltip.tsx b/common/components/ui/tooltip.tsx @@ -0,0 +1,57 @@ +"use client" + +import * as React from "react" +import { Tooltip as TooltipPrimitive } from "radix-ui" + +import { cn } from "../../lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { + return ( + <TooltipPrimitive.Provider + data-slot="tooltip-provider" + delayDuration={delayDuration} + {...props} + /> + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps<typeof TooltipPrimitive.Root>) { + return <TooltipPrimitive.Root data-slot="tooltip" {...props} /> +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { + return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} /> +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps<typeof TooltipPrimitive.Content>) { + return ( + <TooltipPrimitive.Portal> + <TooltipPrimitive.Content + data-slot="tooltip-content" + sideOffset={sideOffset} + className={cn( + "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", + className + )} + {...props} + > + {children} + <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" /> + </TooltipPrimitive.Content> + </TooltipPrimitive.Portal> + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/common/lib/accent.ts b/common/lib/accent.ts @@ -0,0 +1,38 @@ +// Per-site accent: an optional brand color a site can set to override the family +// brass. Stored on Site.accent as a "#rrggbb" string; applied at static-build +// time as an inline `<html style>` on the export site (see export/app/layout), +// which wins over the [data-theme] token rules for BOTH light and dark, so one +// color replaces the brass everywhere. Unset → family brass (the tokens.css +// default). The user/per-visit override in ThemeScript still wins over this. + +const HEX_RE = /^#?([0-9a-f]{6})$/i; + +// Normalize a raw accent into a lowercase "#rrggbb", or undefined if it isn't a +// 6-digit hex color. Mirrors parseSiteUrl/cloudflareProject optional handling. +export function parseAccent(input: unknown): string | undefined { + if (typeof input !== "string") return undefined; + const m = HEX_RE.exec(input.trim()); + return m ? `#${m[1].toLowerCase()}` : undefined; +} + +// Derive the `--brand*` CSS variables from a base accent hex. Returns null for +// an invalid color (caller then renders no override → family brass). +// --brand the color itself (used as the main accent) +// --brand-strong same color (hover emphasis; a single override can't go both +// lighter-on-dark AND darker-on-light, so keep it stable) +// --brand-soft a translucent wash for soft backgrounds (mode-agnostic) +export function siteAccentVars( + accent: string | undefined, +): Record<string, string> | null { + const hex = parseAccent(accent); + if (!hex) return null; + const n = parseInt(hex.slice(1), 16); + const r = (n >> 16) & 255; + const g = (n >> 8) & 255; + const b = n & 255; + return { + "--brand": hex, + "--brand-strong": hex, + "--brand-soft": `rgba(${r}, ${g}, ${b}, 0.16)`, + }; +} diff --git a/common/lib/settings.ts b/common/lib/settings.ts @@ -123,6 +123,10 @@ export type SiteSettings = { // common/lib/site.ts. The one presentation field that lives globally so a // shared footer doesn't have to be repeated per site. socialLinks: SocialLink[]; + // Absolute public URL of the family hub/homepage (e.g. "https://archilyzer.com"). + // Every export site links back to it ("the family" backlink) when set. Empty = + // no hub link rendered. Normalized to a trailing-slash-free http(s) URL. + homepageUrl: string; // Backup configuration for the saved-video store (Phase 4 of the // video-persistence feature). When enabled with a destination, the store is // mirrored there (additively, no deletes) with a per-backup manifest, and the @@ -450,6 +454,7 @@ function defaults(): SiteSettings { syncScheduler: defaultSyncScheduler(), autoQueue: defaultAutoQueue(), socialLinks: [], + homepageUrl: "", savedVideoBackup: defaultSavedVideoBackup(), buildPipeline: defaultBuildPipeline(), }; @@ -457,6 +462,16 @@ function defaults(): SiteSettings { const SOCIAL_URL_RE = /^(https?:\/\/|mailto:|\/)/i; +// Normalize the family hub URL into a trailing-slash-free absolute http(s) URL. +// Returns "" for anything that isn't a usable absolute URL (the "no hub" state). +// Kept local to settings.ts to avoid a settings<->homepage import cycle; mirrors +// parseHomepageUrl() in homepage.ts. +export function normalizeHomepageUrl(input: unknown): string { + if (typeof input !== "string") return ""; + const trimmed = input.trim().replace(/\/+$/, ""); + return /^https?:\/\/\S+/i.test(trimmed) ? trimmed : ""; +} + export function parseSocialLinks(input: unknown): SocialLink[] { if (!Array.isArray(input)) return []; const out: SocialLink[] = []; @@ -507,6 +522,20 @@ export function normalizeSocialSvg(raw: string): string | null { return opening + rest; } +// normalizeSocialSvg() deliberately STRIPS width/height so the icon scales to its +// wrapper. The cost is that a viewBox-only <svg> has no intrinsic size, so before +// the stylesheet loads on a static host it paints at the replaced-element default +// (huge) — the "flash of giant social icons" FOUC. sizeSocialSvg() re-injects an +// intrinsic pixel size at RENDER time (existing site.json files already have the +// attributes stripped, so this must run on read, not just on write). The size is +// an *attribute*, not inline style, so a wrapper's `w-*`/`h-*` utilities still win +// once CSS loads — it only governs the pre-CSS first paint. +export function sizeSocialSvg(svg: string, px = 20): string { + if (typeof svg !== "string") return svg; + if (/^<svg[^>]*\swidth\s*=/i.test(svg)) return svg; // already sized + return svg.replace(/^<svg\b/i, `<svg width="${px}" height="${px}"`); +} + export function clampSleepBetweenDownloadsSeconds(value: unknown): number { const n = typeof value === "number" && Number.isFinite(value) @@ -617,6 +646,7 @@ export function getSettings(): SiteSettings { merged.syncScheduler = sanitizeSyncScheduler(merged.syncScheduler); merged.autoQueue = sanitizeAutoQueue(merged.autoQueue); merged.socialLinks = parseSocialLinks(merged.socialLinks); + merged.homepageUrl = normalizeHomepageUrl(merged.homepageUrl); merged.savedVideoBackup = sanitizeSavedVideoBackup(merged.savedVideoBackup); merged.buildPipeline = sanitizeBuildPipeline(merged.buildPipeline); // Workers. When the file predates the worker model (no `workers` key), @@ -792,6 +822,7 @@ export async function writeSettings(next: SiteSettings): Promise<void> { syncScheduler: sanitizeSyncScheduler(next.syncScheduler), autoQueue: sanitizeAutoQueue(next.autoQueue), socialLinks, + homepageUrl: normalizeHomepageUrl(next.homepageUrl), savedVideoBackup: sanitizeSavedVideoBackup(next.savedVideoBackup), buildPipeline: sanitizeBuildPipeline(next.buildPipeline), }; diff --git a/common/lib/site.ts b/common/lib/site.ts @@ -8,6 +8,7 @@ import { type ChannelGroup, } from "./channelGroups"; import { getPaths, type Paths } from "./paths"; +import { parseAccent } from "./accent"; import { getSettings, normalizeSocialSvg, @@ -40,6 +41,10 @@ export type Site = { siteDescription: string; headerTitle: string; homeTagline: string; + // Optional per-site brand accent ("#rrggbb"). Overrides the family brass on + // this site's public build (see siteAccentVars / export layout). Undefined = + // inherit the family brass. + accent?: string; // Per-site social links. `undefined` (no `socialLinks` key in site.json) // means inherit the global default from SiteSettings.socialLinks; an array // (even empty) overrides it. Resolve with resolveSocialLinks() at render. @@ -226,6 +231,7 @@ export function parseSite(siteId: string, raw: unknown): Site { typeof r.cloudflareProject === "string" && r.cloudflareProject.trim() ? r.cloudflareProject.trim() : undefined, + accent: parseAccent(r.accent), siteUrl: parseSiteUrl(r.siteUrl), relatedSites: parseRelatedSites(r.relatedSites), }; @@ -374,6 +380,7 @@ export async function writeSite( ...(site.cloudflareProject && site.cloudflareProject.trim() ? { cloudflareProject: site.cloudflareProject.trim() } : {}), + ...(parseAccent(site.accent) ? { accent: parseAccent(site.accent) } : {}), ...(parseSiteUrl(site.siteUrl) ? { siteUrl: parseSiteUrl(site.siteUrl) } : {}), ...(parseRelatedSites(site.relatedSites).length > 0 ? { relatedSites: parseRelatedSites(site.relatedSites) } diff --git a/common/package.json b/common/package.json @@ -7,7 +7,9 @@ "@sindresorhus/slugify": "^3.0.0", "@tanstack/react-query": "^5.99.1", "@tanstack/react-virtual": "^3.13.0", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "d3-scale": "^4.0.2", "execa": "^9.6.1", "fs-extra": "^11.3.4", @@ -15,9 +17,12 @@ "lucide-react": "^1.16.0", "markdown-to-jsx": "^7.7.4", "p-limit": "^7.3.0", + "radix-ui": "^1.6.0", "react-player": "^2.16.1", "recharts": "2.15.4", - "tailwind-merge": "^3.6.0" + "sonner": "^2.0.7", + "tailwind-merge": "^3.6.0", + "tw-animate-css": "^1.4.0" }, "peerDependencies": { "next": "16.2.3", diff --git a/common/styles/fonts.ts b/common/styles/fonts.ts @@ -0,0 +1,35 @@ +// Shared font source of truth for the whole family. One `next/font/google` call +// site, three canonical CSS-variable tokens consumed by tokens.css's @theme: +// --font-display (serif: wordmark, headline numerals) +// --font-sans (body / UI) +// --font-mono (queries, timestamps, IDs — the "power layer") +// +// Each app's root layout imports `fontVars` and spreads it onto <html>. This +// collapses the previous three divergent setups (export=Geist, editor=system, +// homepage=Source) onto one Adobe Source super-family voice. +// +// `next/font` is transformed by Next's SWC loader across the whole compiled +// module graph, and `transpilePackages: ["yt-dlp-transcript-common"]` puts this +// module in that graph for every app — so a shared call site works. If a build +// ever balks, the fallback is to duplicate these three calls per layout using +// the same variable names; the CSS token contract is the real invariant. +import { Source_Serif_4, Source_Sans_3, Source_Code_Pro } from "next/font/google"; + +export const fontDisplay = Source_Serif_4({ + variable: "--font-display", + subsets: ["latin"], + style: ["normal", "italic"], +}); + +export const fontSans = Source_Sans_3({ + variable: "--font-sans", + subsets: ["latin"], +}); + +export const fontMono = Source_Code_Pro({ + variable: "--font-mono", + subsets: ["latin"], +}); + +// Space-joined `.variable` classes for <html className={...}>. +export const fontVars = `${fontDisplay.variable} ${fontSans.variable} ${fontMono.variable}`; diff --git a/common/styles/tokens.css b/common/styles/tokens.css @@ -0,0 +1,426 @@ +/* ============================================================================= + Shared design tokens — the single source of truth for the whole family. + + Imported by every app's globals.css right after `@import "tailwindcss"`: + @import "../../common/styles/tokens.css"; + + Two orthogonal axes: + • `html[data-theme="…"]` selects the THEME FAMILY (palette personality). + • `html.dark` selects light/dark MODE within that family. + The `@custom-variant dark` below makes Tailwind's `dark:` utilities follow the + `.dark` class (not the OS media query), so a pre-paint script can set both axes + from localStorage. Default (no data-theme) = the neutral "base" family, which + reproduces the previous zinc look so editor/export keep visual parity. + + TOKEN NAMING (Phase 1 — shadcn kit landed): + • The shadcn "new-york" contract owns the unprefixed names: --primary, + --secondary, --accent (a NEUTRAL subtle-hover bg), --muted, --card, + --popover, --destructive, --border, --input, --ring, --radius, --chart-*. + Each has a *-foreground partner where shadcn expects one. + • The FAMILY BRAND color (brass on archive, blue on base) lives under the + `--brand*` namespace (--brand, --brand-strong, --brand-soft, --brand-ink) + so it never collides with shadcn's neutral `--accent`. A per-user/per-site + accent override (ThemeScript / per-site SSR) sets `--brand`. + ========================================================================== */ + +@import "tw-animate-css"; + +@custom-variant dark (&:where(.dark, .dark *)); + +@theme inline { + /* shadcn surface + text contract */ + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + /* Family brand (brass / blue) — power-layer accent, distinct from shadcn accent */ + --color-brand: var(--brand); + --color-brand-strong: var(--brand-strong); + --color-brand-soft: var(--brand-soft); + + /* Charts */ + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + + /* Type system */ + --font-display: var(--font-display); + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + + /* Radius scale derived from --radius (declared once on :root below) */ + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +/* --------------------------------------------------------------------------- + BASE family (default — no data-theme). Neutral zinc; blue brand accent. + Reproduces the prior editor/export look so the kit lands at visual parity. + Chart axis/grid/surface/tooltip tokens live here too (they previously existed + only on the homepage), so editor/export charts get proper strokes. + --------------------------------------------------------------------------- */ +:root { + --radius: 0.625rem; + + --background: #ffffff; + --foreground: #171717; + --card: #ffffff; + --card-foreground: #171717; + --popover: #ffffff; + --popover-foreground: #171717; + --primary: #18181b; + --primary-foreground: #fafafa; + --secondary: #f4f4f5; + --secondary-foreground: #18181b; + --muted: #f4f4f5; + --muted-foreground: #71717a; + --accent: #f4f4f5; + --accent-foreground: #18181b; + --destructive: #dc2626; + --destructive-foreground: #fafafa; + --border: #e4e4e7; + --input: #e4e4e7; + --ring: #a1a1aa; + + --brand: #2563eb; + --brand-strong: #1d4ed8; + --brand-soft: rgba(37, 99, 235, 0.12); + + --chart-1: #2563eb; + --chart-2: #16a34a; + --chart-3: #ea580c; + --chart-4: #9333ea; + --chart-5: #db2777; + --chart-surface: #ffffff; + --chart-grid: rgba(0, 0, 0, 0.08); + --chart-axis: #71717a; + --chart-tooltip-bg: #ffffff; +} + +.dark { + --background: #0a0a0a; + --foreground: #ededed; + --card: #18181b; + --card-foreground: #ededed; + --popover: #18181b; + --popover-foreground: #ededed; + --primary: #fafafa; + --primary-foreground: #18181b; + --secondary: #27272a; + --secondary-foreground: #fafafa; + --muted: #27272a; + --muted-foreground: #a1a1aa; + --accent: #27272a; + --accent-foreground: #fafafa; + --destructive: #f87171; + --destructive-foreground: #fafafa; + --border: #27272a; + --input: rgba(255, 255, 255, 0.12); + --ring: #71717a; + + --brand: #60a5fa; + --brand-strong: #93c5fd; + --brand-soft: rgba(96, 165, 250, 0.16); + + --chart-1: #60a5fa; + --chart-2: #4ade80; + --chart-3: #fb923c; + --chart-4: #c084fc; + --chart-5: #f472b6; + --chart-surface: #0a0a0a; + --chart-grid: rgba(255, 255, 255, 0.08); + --chart-axis: #a1a1aa; + --chart-tooltip-bg: #18181b; +} + +/* --------------------------------------------------------------------------- + ARCHIVE family — the public flagship "Reading Room". Brass brand accent. + Ships paper (light) + ink (dark). The ink values are the homepage's existing + warm-ink palette, hoisted here verbatim so the homepage is pixel-parity when + it defaults to data-theme="archive" + dark. + --------------------------------------------------------------------------- */ +[data-theme="archive"] { + /* Paper (light) — warm aged-paper ground, darkened brass for contrast. */ + --background: #faf6ec; + --surface: #f4ecdb; + --foreground: #211a0e; + --card: #fbf7ef; + --card-foreground: #211a0e; + --popover: #fbf7ef; + --popover-foreground: #211a0e; + --primary: #95661a; + --primary-foreground: #fff7e7; + --secondary: #efe6d3; + --secondary-foreground: #211a0e; + --muted: #efe6d3; + --muted-foreground: #6b6250; + --accent: #efe6d3; + --accent-foreground: #211a0e; + --destructive: #b3261e; + --destructive-foreground: #fff7e7; + --border: rgba(40, 30, 12, 0.12); + --border-strong: rgba(40, 30, 12, 0.2); + --input: rgba(40, 30, 12, 0.18); + --ring: #95661a; + --faint: #948b78; + --panel: rgba(255, 252, 244, 0.7); + --panel-2: rgba(244, 236, 219, 0.7); + + --brand: #95661a; + --brand-strong: #714d10; + --brand-soft: rgba(149, 102, 26, 0.14); + --brand-ink: #fff7e7; + + --chart-1: #2563eb; + --chart-2: #0f9d6b; + --chart-3: #c2410c; + --chart-4: #7c3aed; + --chart-5: #be185d; + --chart-surface: #fbf7ef; + --chart-grid: rgba(40, 30, 12, 0.08); + --chart-axis: #8a7f66; + --chart-tooltip-bg: #fffaf0; +} + +[data-theme="archive"].dark { + /* Ink (dark) — the homepage's warm near-black "archive room". */ + --background: #0c0a08; + --surface: #141009; + --foreground: #efe7d8; + --card: #16110a; + --card-foreground: #efe7d8; + --popover: #1b150d; + --popover-foreground: #efe7d8; + --primary: #e3b15c; + --primary-foreground: #1a1305; + --secondary: #241d12; + --secondary-foreground: #efe7d8; + --muted: #1c1710; + --muted-foreground: #a39a86; + --accent: #241d12; + --accent-foreground: #efe7d8; + --destructive: #f0857a; + --destructive-foreground: #1a1305; + --border: rgba(233, 220, 197, 0.1); + --border-strong: rgba(233, 220, 197, 0.18); + --input: rgba(233, 220, 197, 0.14); + --ring: #e3b15c; + --faint: #6f6757; + --panel: rgba(28, 23, 15, 0.55); + --panel-2: rgba(40, 33, 21, 0.5); + + --brand: #e3b15c; + --brand-strong: #f3c977; + --brand-soft: rgba(227, 177, 92, 0.14); + --brand-ink: #1a1305; + + --chart-1: #6ea8ff; + --chart-2: #54d6a0; + --chart-3: #f2935b; + --chart-4: #c08cf0; + --chart-5: #f178b6; + --chart-surface: #16110a; + --chart-grid: rgba(233, 220, 197, 0.08); + --chart-axis: #8a8170; + --chart-tooltip-bg: #1b150d; +} + +/* --------------------------------------------------------------------------- + TERMINAL family — an "ice-cyan signal" terminal. Cyan brand on blue-black + (dark) / cool near-white (light). Pure token swap — same markup, recolored. + --------------------------------------------------------------------------- */ +[data-theme="terminal"] { + /* Light terminal — cyan ink on a cool near-white. */ + --background: #f2f7f9; + --surface: #e7f0f3; + --foreground: #07242c; + --card: #ffffff; + --card-foreground: #07242c; + --popover: #ffffff; + --popover-foreground: #07242c; + --primary: #0891b2; + --primary-foreground: #ecfeff; + --secondary: #d7eaef; + --secondary-foreground: #07242c; + --muted: #e1eef1; + --muted-foreground: #466a72; + --accent: #d7eaef; + --accent-foreground: #07242c; + --destructive: #be123c; + --destructive-foreground: #ecfeff; + --border: rgba(7, 36, 44, 0.12); + --border-strong: rgba(7, 36, 44, 0.22); + --input: rgba(7, 36, 44, 0.16); + --ring: #0891b2; + --faint: #6b8a91; + --panel: rgba(255, 255, 255, 0.7); + --panel-2: rgba(231, 240, 243, 0.7); + + --brand: #0891b2; + --brand-strong: #0e7490; + --brand-soft: rgba(8, 145, 178, 0.14); + --brand-ink: #ecfeff; + + --chart-1: #0891b2; + --chart-2: #0d9488; + --chart-3: #0284c7; + --chart-4: #4f46e5; + --chart-5: #059669; + --chart-surface: #ffffff; + --chart-grid: rgba(7, 36, 44, 0.08); + --chart-axis: #5f828a; + --chart-tooltip-bg: #ffffff; +} + +[data-theme="terminal"].dark { + /* Dark terminal — phosphor cyan on blue-black. */ + --background: #080f12; + --surface: #0c161b; + --foreground: #cdeef5; + --card: #0c161b; + --card-foreground: #cdeef5; + --popover: #0f1d23; + --popover-foreground: #cdeef5; + --primary: #22d3ee; + --primary-foreground: #04141a; + --secondary: #122b33; + --secondary-foreground: #cdeef5; + --muted: #0f2129; + --muted-foreground: #7fb3c0; + --accent: #102329; + --accent-foreground: #cdeef5; + --destructive: #fb7185; + --destructive-foreground: #04141a; + --border: rgba(140, 220, 235, 0.12); + --border-strong: rgba(140, 220, 235, 0.24); + --input: rgba(140, 220, 235, 0.16); + --ring: #22d3ee; + --faint: #5a8a96; + --panel: rgba(12, 26, 33, 0.6); + --panel-2: rgba(18, 38, 46, 0.5); + + --brand: #22d3ee; + --brand-strong: #67e8f9; + --brand-soft: rgba(34, 211, 238, 0.16); + --brand-ink: #04141a; + + --chart-1: #22d3ee; + --chart-2: #2dd4bf; + --chart-3: #38bdf8; + --chart-4: #818cf8; + --chart-5: #34d399; + --chart-surface: #0c161b; + --chart-grid: rgba(140, 220, 235, 0.1); + --chart-axis: #6f9aa6; + --chart-tooltip-bg: #0f1d23; +} + +/* --------------------------------------------------------------------------- + SWISS family — International Typographic Style. Red + black + white, strong + hairline rules. Light-first; dark = white-on-black. Pure token swap. + --------------------------------------------------------------------------- */ +[data-theme="swiss"] { + /* Light — white ground, black primary, international red brand. */ + --background: #ffffff; + --surface: #f5f5f5; + --foreground: #0a0a0a; + --card: #ffffff; + --card-foreground: #0a0a0a; + --popover: #ffffff; + --popover-foreground: #0a0a0a; + --primary: #0a0a0a; + --primary-foreground: #ffffff; + --secondary: #f0f0f0; + --secondary-foreground: #0a0a0a; + --muted: #f0f0f0; + --muted-foreground: #525252; + --accent: #ececec; + --accent-foreground: #0a0a0a; + --destructive: #b00016; + --destructive-foreground: #ffffff; + --border: #d4d4d4; + --border-strong: #0a0a0a; + --input: #c8c8c8; + --ring: #0a0a0a; + --faint: #8a8a8a; + --panel: rgba(255, 255, 255, 0.85); + --panel-2: rgba(245, 245, 245, 0.85); + + --brand: #d4001a; + --brand-strong: #b00016; + --brand-soft: rgba(212, 0, 26, 0.1); + --brand-ink: #ffffff; + + --chart-1: #d4001a; + --chart-2: #0a0a0a; + --chart-3: #6b7280; + --chart-4: #1d4ed8; + --chart-5: #b8a589; + --chart-surface: #ffffff; + --chart-grid: rgba(10, 10, 10, 0.1); + --chart-axis: #525252; + --chart-tooltip-bg: #ffffff; +} + +[data-theme="swiss"].dark { + /* Dark — black ground, white primary, brighter red brand. */ + --background: #0a0a0a; + --surface: #141414; + --foreground: #fafafa; + --card: #141414; + --card-foreground: #fafafa; + --popover: #161616; + --popover-foreground: #fafafa; + --primary: #fafafa; + --primary-foreground: #0a0a0a; + --secondary: #1f1f1f; + --secondary-foreground: #fafafa; + --muted: #1c1c1c; + --muted-foreground: #a3a3a3; + --accent: #222222; + --accent-foreground: #fafafa; + --destructive: #ff5a6a; + --destructive-foreground: #0a0a0a; + --border: rgba(250, 250, 250, 0.14); + --border-strong: rgba(250, 250, 250, 0.4); + --input: rgba(250, 250, 250, 0.18); + --ring: #fafafa; + --faint: #6b6b6b; + --panel: rgba(20, 20, 20, 0.6); + --panel-2: rgba(31, 31, 31, 0.5); + + --brand: #ff2b3e; + --brand-strong: #ff5a6a; + --brand-soft: rgba(255, 43, 62, 0.16); + --brand-ink: #0a0a0a; + + --chart-1: #ff2b3e; + --chart-2: #fafafa; + --chart-3: #9ca3af; + --chart-4: #60a5fa; + --chart-5: #cbb89a; + --chart-surface: #141414; + --chart-grid: rgba(250, 250, 250, 0.1); + --chart-axis: #a3a3a3; + --chart-tooltip-bg: #161616; +} diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +- **Two more selectable themes + a theme picker.** A palette menu beside the light/dark toggle (in every app — editor, export, homepage) switches the theme family between **Archive**, **Terminal** (ice-cyan), **Swiss** (red/black/white), and **Base**, each in light/dark/system, persisted client-side. Each family is a pure CSS token swap in `common/styles/tokens.css` (a `[data-theme]` block) — no markup changes — wired through `common/components/{themeConfig.ts,ThemeMenu.tsx}`. +- **Per-site brand accent.** Each site's config (under Sites) gains an optional **Brand accent** field — a hex color that overrides the family brass on that site's public build. Validated on save; blank inherits the family accent. The long-running action logs (download, transcribe, build, deploy, …) are also restyled onto the shared kit (Button + design tokens), keeping their exact accessible labels. See `common/lib/accent.ts`, `common/lib/site.ts`, `common/components/StreamActionLog.tsx`, `editor/app/sites/{components/SiteForm.tsx,actions.ts}`, and `export/app/layout.tsx`. +- **Command-first cockpit: a ⌘K command palette that runs actions, plus a live dashboard.** Press **⌘K / Ctrl-K** anywhere to open a fast command palette (rebuilt on cmdk) that jumps to any page **and runs pool/site actions inline** — Sync all channels, Refresh all reports, Detect duplicate shorts, Retry all failed jobs, Drain the queue, Pause/Resume all workers — each firing the server action and reporting the result as a **toast** (Sonner). On a channel page it also offers the channel's status filters. The sidebar and palette now read from one shared nav source (`editor/app/lib/nav.ts`) so they never drift (the old palette listed a stale 5-route subset), and every nav item gains an icon. The **Dashboard** gains a **Live jobs** panel that polls running jobs in real time (reusing the active-jobs monitor), and the whole editor shell is restyled compact onto the shared design tokens/kit with the Source type family. See `editor/app/{layout.tsx,page.tsx,lib/nav.ts,components/{CommandPalette.tsx,ChangelogNavLink.tsx}}` and `common/components/ui/sonner.tsx`. +- **New "Family hub URL" setting.** Settings gains an optional hub URL (e.g. `https://archilyzer.com`); every export site renders a link back to it in the header/footer, tying the family of public sites together. Leave it blank for no hub link. Normalized to an absolute http(s) URL on save (`settings.homepageUrl`; see `common/lib/settings.ts`, `editor/app/settings/{components/SettingsForm.tsx,actions.ts}`). Part of the family-wide UI redesign that also restyles the public export sites. - **Source-truncated downloads are now caught at download time, and the yt-dlp download format is selectable (Odysee defaults to `original`).** A download that completed (yt-dlp exit 0) but whose audio is far shorter than the video — e.g. an Odysee/LBRY video whose every HLS rung is CDN-truncated to a few minutes while the full audio lives only in the multi-GB `original` format — used to pass the audio-check (the short file is structurally *clean*) and get transcribed, so only the **post-transcription** coverage detector caught it. Two changes fix this. **(1) A download-time duration guard:** after each managed download the produced `audio.<fmt>` is probed with **ffprobe** (new `common/ytdlp/ffprobeDuration.ts`, `paths.ffprobeBin` / `FFPROBE_BIN`) and compared to the metadata duration using the same thresholds as the transcript-coverage detector (`isShortAudio` in `common/lib/transcriptCoverage.ts`: non-livestream, ≥10min, <50% covered). A large shortfall is recorded as a new terminal **`failed-short-audio`** status with the measured `{audioDurationSec, expectedDurationSec, coverage}` — caught **before** transcription (the inline no-subs-fallback whisper is pre-checked too). The short stub is **kept on disk** (so it isn't silently re-downloaded into a loop) and is **not** classified for platform backoff (a truncated stream isn't a transient error). Surfaced as a new **`shortAudio`** snapshot bucket (mirroring `corrupt-full-source`: excluded from `downloadedNoTranscript` so it's never auto-transcribed, and from the wrong-format cleanup so the kept file isn't deleted) → a **`short_audio`** channel-list filter + **Select short-audio** quick-select + bulk **Re-download truncated** action, a **"Download was truncated at the source"** banner on the video page with a one-click **Re-download as Original**, a **"Channels with truncated downloads (short audio)"** `/actionable` section, and a **"Truncated download — audio far shorter than the video (file kept)"** download badge. The auto-download runner lands a `failed-short-audio` unit as a failed job. **(2) Selectable, platform-aware download format:** the previously-hardcoded `-f bestaudio/worst` is now a preset (`auto` | `original` | `bestaudio` | `bestvideo_audio`; `common/ytdlp/downloadFormat.ts`) resolved with the same override chain as `audioFormat` — per-download (the redownload **Format** picker) > per-channel (`ChannelConfig.downloadFormat`, a select in the channel form) > global (`SiteSettings.downloadFormat`, a select in **Settings**). **`auto` is platform-aware:** Odysee/LBRY resolves to `original/bestaudio/worst` (so the truncation is avoided at the source); everything else stays `bestaudio/worst`. The source platform is taken from the prefetched metadata extractor. The short-audio bucket's re-download reuses the per-video fixer (delete the stub → re-fetch with the per-source default → re-transcribe) via the existing `redownload-incomplete-bucket` job (now also driving `ReplayBucket: "shortAudio"`). See `common/ytdlp/{downloadFormat.ts,ffprobeDuration.ts,downloadOneManaged.ts,runYtdlp.ts}`, `common/lib/{transcriptCoverage.ts,downloadOutcome.ts,settings.ts,channelConfig.ts,paths.ts,transcripts-server.ts}`, `common/controller/{channelSnapshot.ts,autoRunner.ts}`, `common/jobs/jobSpec.ts`, `editor/app/channels/[slug]/{incompleteTranscriptActions.ts,bulkVideoActions.ts,lib/{fixIncompleteTranscript.ts,videoRows.ts,videoRowsServer.ts,stageStatus.ts},components/VideoListPane.tsx,videos/[id]/{videoActions.ts,components/VideoPanel.tsx}}`, `editor/app/{settings/{actions.ts,components/SettingsForm.tsx},channels/components/{ChannelForm.tsx,parseChannelForm.ts},actionable/{lib/loadActionable.ts,page.tsx,components/InlineActionButton.tsx},jobs/jobReplayRegistry.ts}`, and `editor/e2e/download-format-guard.spec.ts`. - **A complete-but-corrupt audio download is no longer re-downloaded forever — it's kept and flagged instead.** When an audio-checked download finished (yt-dlp exit 0, all bytes) but its final integrity probe came back `malformed`, the orchestrator rolled the whole file back and re-downloaded it — repeatedly. For a fast download that completes inside one checkpoint interval no `.good` baseline ever exists, so each "rollback" discarded the entire file and re-fetched it from scratch (a 2.46 GB Odysee video looped until the rollback cap, leaving no audio behind), and a cancel landing mid-loop was deferred behind the next full re-download. Now the final-probe path is bounded: a malformed final probe triggers **exactly one** re-download; if it's still malformed, the orchestrator **keeps the downloaded container on disk** (for inspection) and records a new terminal **`corrupt-full-source`** status — re-downloading a complete file can't change a deterministic verdict. This is distinct from `failed-corrupt-source` (a download that never finished, driven by the checkpoint rollback cap). Surfaced as a new **`corruptFullSource`** channel-snapshot bucket → a Download-stage summary line ("corrupt full source (file kept)"), an **amber download-dot** + `corrupt_full_source` row status in the per-channel video list (folded into the *No audio* filter), and a **"Corrupt full source — download completed but audio is malformed (file kept)"** badge on the video page. It's terminal and **not** retried (auto-runner treats it as skipped; the kept file is an artifact, so it isn't re-queued) and **not** counted as a failed transcription or a transcode-pending video. Cancellation is also fixed: a pending abort now wins over an in-flight rollback decision, and the loop re-checks the abort signal after the final probe so Cancel stops it promptly. See `common/ytdlp/audioCheckedDownload.ts`, `common/ytdlp/downloadOneManaged.ts`, `common/lib/downloadOutcome.ts`, `common/controller/{autoRunner.ts,channelSnapshot.ts}`, `common/ytdlp/runYtdlp.ts`, `editor/app/channels/[slug]/{lib/videoRows.ts,lib/videoRowsServer.ts,lib/stageStatus.ts,components/VideoListPane.tsx,videos/[id]/components/VideoPanel.tsx}`, and `editor/e2e/audio-check-scenarios.spec.ts`. - **The Deploy page is reworked around a clearer build/deploy lifecycle, with one-click build-then-deploy and batch multi-site builds.** The page now reads top-to-bottom as you'd actually ship: **Release notes** (the `## [Unreleased]` changelog preview + Cut release) → **Build & deploy** → optional **Individual steps** → **Build multiple sites**. A new **Build & deploy** button runs the build and, only if it succeeds (and wasn't cancelled), deploys it — as a single managed job with one combined streamed log and one Cancel (`buildAndDeployAction`, a composite `runManagedFunction`; cancelling mid-build skips the deploy). The new **Build multiple sites** panel kicks off a build (optionally build+deploy) for several sites at once, each rendered as its own live status-chipped log lane (`BuildSitesPanel` + `JobLane`); in Basic mode the jobs serialize on the shared build/deploy queue (the `export/` output tree is shared), with a note that true parallelism arrives with Docker mode. A **Build mode** toggle (Basic | Docker) on the page persists the choice as the default (`settings.buildPipeline`, also editable on Settings); Docker mode is a follow-up and currently falls back to a basic build with an inline notice. The build/deploy commands now share a child-streaming helper (`common/jobs/runChild.ts`) and mode-routing core (`editor/app/deploy/buildDeployCore.ts`). See `editor/app/deploy/{page.tsx,buildAction usage,components/*}`, `editor/app/build/buildAction.ts`, and `common/lib/settings.ts`. diff --git a/editor/app/components/ChangelogNavLink.tsx b/editor/app/components/ChangelogNavLink.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, type ReactNode } from "react"; import Link from "next/link"; const STORAGE_KEY = "changelogLastSeen"; @@ -10,11 +10,15 @@ export function ChangelogNavLink({ label, latestDate, className, + icon, }: { href: string; label: string; latestDate: string | null; className?: string; + // A pre-rendered icon ELEMENT (not the component) — a server layout can't pass + // a function/component across the client boundary, but a ReactNode is fine. + icon?: ReactNode; }) { const [unread, setUnread] = useState(false); @@ -30,12 +34,13 @@ export function ChangelogNavLink({ return ( <Link href={href} className={className}> + {icon} <span>{label}</span> {unread && ( <span aria-label="New changelog entry" title="New changelog entry" - className="ml-auto inline-block w-2 h-2 rounded-full bg-blue-500 dark:bg-blue-400" + className="ml-auto inline-block w-2 h-2 rounded-full bg-brand" /> )} </Link> diff --git a/editor/app/components/CommandPalette.tsx b/editor/app/components/CommandPalette.tsx @@ -1,247 +1,277 @@ "use client"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { + Activity, + ListChecks, + PauseOctagon, + Play, + RefreshCw, + RotateCcw, + Copy as CopyIcon, + type LucideIcon, +} from "lucide-react"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "yt-dlp-transcript-common/components/ui/command"; +import { toast } from "yt-dlp-transcript-common/components/ui/sonner"; +import { NAV_GROUPS } from "../lib/nav"; +import { syncAllChannelsAction } from "../channels/actions"; +import { + refreshAllChannelSnapshotsAction, + runDuplicateDetectionAction, +} from "../actionable/actions"; +import { retryAllFailedAction, drainAllAction } from "../jobs/actions"; +import { + pauseAllWorkersAction, + resumeAllWorkersAction, +} from "../workers/actions"; -type Command = { +// The editor's command-first spine. Rebuilt on cmdk (shadcn Command) over the +// shared NAV_GROUPS (so it never drifts from the sidebar) plus a set of global +// "run a thing" actions that fire a server action, toast the result, and refresh. +// Long per-channel streaming jobs still live on their pages (Phase 4); the +// palette runs the quick, no-argument pool/site actions you'd otherwise hunt for. + +type RunAction = { id: string; label: string; - hint?: string; - run: () => void; + icon: LucideIcon; + keywords?: string; + confirm?: string; + // Resolve to a success toast message; throw to surface an error toast. + run: () => Promise<string>; }; +const GLOBAL_ACTIONS: RunAction[] = [ + { + id: "act-sync-all", + label: "Sync all channels", + icon: RefreshCw, + keywords: "fetch new videos refresh metadata", + run: async () => { + const r = await syncAllChannelsAction(); + const n = r.queued.length; + const skip = r.skipped.length ? ` · ${r.skipped.length} skipped` : ""; + return `Queued ${n} channel sync${n === 1 ? "" : "s"}${skip}`; + }, + }, + { + id: "act-refresh-reports", + label: "Refresh all channel reports", + icon: RefreshCw, + keywords: "snapshot recompute stats", + run: async () => { + const r = await refreshAllChannelSnapshotsAction(); + const n = r.queued.length; + const skip = r.skipped.length ? ` · ${r.skipped.length} skipped` : ""; + return `Refreshing ${n} report${n === 1 ? "" : "s"}${skip}`; + }, + }, + { + id: "act-detect-duplicates", + label: "Detect duplicate shorts", + icon: CopyIcon, + keywords: "find reposts cross-platform", + run: async () => { + const r = await runDuplicateDetectionAction("shorts"); + if (!r.ok) throw new Error(r.error); + return `Found ${r.clusters} duplicate cluster${r.clusters === 1 ? "" : "s"} (${r.videosInClusters} videos)`; + }, + }, + { + id: "act-retry-failed", + label: "Retry all failed jobs", + icon: RotateCcw, + keywords: "requeue errors", + run: async () => { + const r = await retryAllFailedAction(); + return `Retried ${r.count} failed job${r.count === 1 ? "" : "s"}`; + }, + }, + { + id: "act-drain-all", + label: "Drain all queued jobs", + icon: ListChecks, + keywords: "cancel clear queue", + confirm: "Drain every queued job? Running jobs are left alone.", + run: async () => { + const r = await drainAllAction(); + return `Drained ${r.count} job${r.count === 1 ? "" : "s"}`; + }, + }, + { + id: "act-pause-workers", + label: "Pause all workers", + icon: PauseOctagon, + keywords: "stop transcription halt", + run: async () => { + const r = await pauseAllWorkersAction(); + if (!r.ok) throw new Error(r.error ?? "Failed to pause workers"); + return "Paused all workers"; + }, + }, + { + id: "act-resume-workers", + label: "Resume all workers", + icon: Play, + keywords: "start transcription continue", + run: async () => { + const r = await resumeAllWorkersAction(); + if (!r.ok) throw new Error(r.error ?? "Failed to resume workers"); + return "Resumed all workers"; + }, + }, +]; + function channelSlugFrom(pathname: string): string | null { const m = /^\/channels\/([^/?#]+)/.exec(pathname); return m ? decodeURIComponent(m[1]) : null; } -function videoIdFrom( - pathname: string, - search: ReturnType<typeof useSearchParams>, -): string | null { - const m = /^\/channels\/[^/]+\/videos\/([^/?#]+)/.exec(pathname); - if (m) return decodeURIComponent(m[1]); - const fromQuery = search.get("video"); - return fromQuery && fromQuery.length > 0 ? fromQuery : null; -} +const CHANNEL_FILTERS: { filter: string; label: string }[] = [ + { filter: "needs_action", label: "Needs action" }, + { filter: "failed", label: "Failed" }, + { filter: "no_audio", label: "No audio" }, + { filter: "downloaded_no_transcript", label: "No transcript" }, + { filter: "untranscribable", label: "Untranscribable" }, + { filter: "running", label: "Running" }, + { filter: "all", label: "All (clear filter)" }, +]; export function CommandPalette() { const router = useRouter(); const pathname = usePathname() ?? "/"; const search = useSearchParams(); const [open, setOpen] = useState(false); - const [query, setQuery] = useState(""); - const [highlight, setHighlight] = useState(0); - const inputRef = useRef<HTMLInputElement | null>(null); - const slug = channelSlugFrom(pathname); - const videoId = videoIdFrom(pathname, search); - - const close = useCallback(() => { - setOpen(false); - setQuery(""); - setHighlight(0); - }, []); - - const commands = useMemo<Command[]>(() => { - const out: Command[] = [ - { - id: "nav-dashboard", - label: "Dashboard", - hint: "navigation", - run: () => router.push("/"), - }, - { - id: "nav-channels", - label: "Channels", - hint: "navigation", - run: () => router.push("/channels"), - }, - { - id: "nav-jobs", - label: "Jobs", - hint: "navigation", - run: () => router.push("/jobs"), - }, - { - id: "nav-build", - label: "Build", - hint: "navigation", - run: () => router.push("/build"), - }, - { - id: "nav-settings", - label: "Settings", - hint: "navigation", - run: () => router.push("/settings"), - }, - ]; - - if (slug) { - const setFilter = (filter: string, label: string) => ({ - id: `filter-${filter}`, - label, - hint: `channel: ${slug}`, - run: () => { - const params = new URLSearchParams(search.toString()); - if (filter === "all") params.delete("filter"); - else params.set("filter", filter); - params.delete("video"); - const q = params.toString(); - router.push(q ? `/channels/${slug}?${q}` : `/channels/${slug}`); - }, - }); - out.push( - setFilter("needs_action", "Filter: Needs action"), - setFilter("failed", "Filter: Failed"), - setFilter("no_audio", "Filter: No audio"), - setFilter("downloaded_no_transcript", "Filter: No transcript"), - setFilter("untranscribable", "Filter: Untranscribable"), - setFilter("running", "Filter: Running"), - setFilter("all", "Filter: All (clear)"), - { - id: "channel-pipeline", - label: "Open channel pipeline panel", - hint: `channel: ${slug}`, - run: () => router.push(`/channels/${slug}`), - }, - ); - } - - if (slug && videoId) { - out.push({ - id: "deselect-video", - label: "Close video panel", - hint: `video: ${videoId}`, - run: () => { - const params = new URLSearchParams(search.toString()); - params.delete("video"); - const q = params.toString(); - router.push(q ? `/channels/${slug}?${q}` : `/channels/${slug}`); - }, - }); - } - - return out; - }, [router, search, slug, videoId]); - - const filtered = useMemo(() => { - const q = query.trim().toLowerCase(); - if (!q) return commands; - return commands.filter( - (c) => - c.label.toLowerCase().includes(q) || - (c.hint?.toLowerCase().includes(q) ?? false), - ); - }, [commands, query]); useEffect(() => { function onKey(e: KeyboardEvent) { - const isOpen = e.key === "k" && (e.metaKey || e.ctrlKey); - if (isOpen) { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen((prev) => !prev); - return; - } - if (e.key === "Escape" && open) { - e.preventDefault(); - close(); } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); - }, [open, close]); + }, []); - useEffect(() => { - if (open) { - setHighlight(0); - const t = setTimeout(() => inputRef.current?.focus(), 0); - return () => clearTimeout(t); - } - }, [open]); + const go = useCallback( + (href: string) => { + setOpen(false); + router.push(href); + }, + [router], + ); - useEffect(() => { - if (highlight >= filtered.length) setHighlight(Math.max(0, filtered.length - 1)); - }, [filtered.length, highlight]); + const runAction = useCallback( + (action: RunAction) => { + setOpen(false); + if (action.confirm && !window.confirm(action.confirm)) return; + const toastId = toast.loading(`${action.label}…`); + void (async () => { + try { + const message = await action.run(); + toast.success(message, { id: toastId }); + router.refresh(); + } catch (err) { + toast.error(`${action.label} failed`, { + id: toastId, + description: err instanceof Error ? err.message : String(err), + }); + } + })(); + }, + [router], + ); - if (!open) return null; + const setChannelFilter = useCallback( + (filter: string) => { + if (!slug) return; + setOpen(false); + const params = new URLSearchParams(search.toString()); + if (filter === "all") params.delete("filter"); + else params.set("filter", filter); + params.delete("video"); + const q = params.toString(); + router.push(q ? `/channels/${slug}?${q}` : `/channels/${slug}`); + }, + [router, search, slug], + ); return ( - <div - role="dialog" - aria-label="command palette" - aria-modal="true" - className="fixed inset-0 z-50 flex items-start justify-center p-4 pt-[10vh] bg-zinc-900/40 dark:bg-zinc-950/60" - onMouseDown={(e) => { - if (e.target === e.currentTarget) close(); - }} + <CommandDialog + open={open} + onOpenChange={setOpen} + title="Command palette" + description="Jump to a page or run an action" > - <div className="w-full max-w-lg rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 shadow-lg overflow-hidden"> - <input - ref={inputRef} - type="text" - value={query} - onChange={(e) => setQuery(e.target.value)} - placeholder="Type a command…" - aria-label="command input" - className="w-full px-3 py-2.5 text-sm bg-transparent border-b border-zinc-200 dark:border-zinc-800 outline-none" - onKeyDown={(e) => { - if (e.key === "ArrowDown") { - e.preventDefault(); - setHighlight((h) => Math.min(h + 1, filtered.length - 1)); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setHighlight((h) => Math.max(h - 1, 0)); - } else if (e.key === "Enter") { - e.preventDefault(); - const cmd = filtered[highlight]; - if (cmd) { - close(); - cmd.run(); - } - } - }} - /> - <ul role="listbox" aria-label="commands" className="max-h-80 overflow-y-auto"> - {filtered.length === 0 ? ( - <li className="px-3 py-4 text-sm text-zinc-500">No matches.</li> - ) : ( - filtered.map((c, i) => { - const active = i === highlight; + <CommandInput placeholder="Search pages and actions…" aria-label="command input" /> + <CommandList> + <CommandEmpty>No matches.</CommandEmpty> + + {slug && ( + <> + <CommandGroup heading={`Channel: ${slug}`}> + {CHANNEL_FILTERS.map((f) => ( + <CommandItem + key={f.filter} + value={`filter ${f.label} ${slug}`} + onSelect={() => setChannelFilter(f.filter)} + > + <Activity className="text-muted-foreground" /> + <span>Filter: {f.label}</span> + </CommandItem> + ))} + </CommandGroup> + <CommandSeparator /> + </> + )} + + <CommandGroup heading="Actions"> + {GLOBAL_ACTIONS.map((a) => { + const Icon = a.icon; + return ( + <CommandItem + key={a.id} + value={`${a.label} ${a.keywords ?? ""}`} + onSelect={() => runAction(a)} + > + <Icon className="text-muted-foreground" /> + <span>{a.label}</span> + </CommandItem> + ); + })} + </CommandGroup> + + <CommandSeparator /> + + {NAV_GROUPS.map((group) => ( + <CommandGroup key={group.label} heading={group.label}> + {group.links.map((link) => { + const Icon = link.icon; return ( - <li key={c.id}> - <button - type="button" - onMouseEnter={() => setHighlight(i)} - onClick={() => { - close(); - c.run(); - }} - aria-selected={active} - role="option" - className={`w-full text-left px-3 py-2 text-sm flex items-baseline gap-2 ${ - active - ? "bg-blue-50 dark:bg-blue-950/40" - : "hover:bg-zinc-50 dark:hover:bg-zinc-800/40" - }`} - > - <span className="flex-1 truncate">{c.label}</span> - {c.hint && ( - <span className="text-xs text-zinc-500 truncate flex-shrink-0"> - {c.hint} - </span> - )} - </button> - </li> + <CommandItem + key={link.href} + value={`${link.label} ${link.keywords ?? ""} ${group.label}`} + onSelect={() => go(link.href)} + > + <Icon className="text-muted-foreground" /> + <span>{link.label}</span> + </CommandItem> ); - }) - )} - </ul> - <div className="border-t border-zinc-200 dark:border-zinc-800 px-3 py-1.5 text-[11px] text-zinc-500 flex items-center justify-between"> - <span>↵ to run · ↑↓ to move · Esc to close</span> - <span>⌘K</span> - </div> - </div> - </div> + })} + </CommandGroup> + ))} + </CommandList> + </CommandDialog> ); } diff --git a/editor/app/globals.css b/editor/app/globals.css @@ -1,58 +1,14 @@ @import "tailwindcss"; +@import "../../common/styles/tokens.css"; @source "../../common/components"; -:root { - --background: #ffffff; - --foreground: #171717; - /* Semantic tokens consumed by shadcn/ui chart primitives. */ - --border: #e4e4e7; - --muted: #f4f4f5; - --muted-foreground: #71717a; - /* Chart series palette. */ - --chart-1: #2563eb; - --chart-2: #16a34a; - --chart-3: #ea580c; - --chart-4: #9333ea; - --chart-5: #db2777; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-border: var(--border); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - --border: #27272a; - --muted: #27272a; - --muted-foreground: #a1a1aa; - --chart-1: #60a5fa; - --chart-2: #4ade80; - --chart-3: #fb923c; - --chart-4: #c084fc; - --chart-5: #f472b6; - } -} +/* Design tokens, the `dark` variant, and theme palettes live in + common/styles/tokens.css. The editor runs the neutral "base" family as a + compact, command-first cockpit (shell + dashboard + palette on the shared + kit); per-page internals migrate incrementally. */ body { background: var(--background); color: var(--foreground); - font-family: - system-ui, - -apple-system, - "Segoe UI", - Roboto, - Helvetica, - Arial, - sans-serif; + font-family: var(--font-sans), ui-sans-serif, system-ui, sans-serif; } diff --git a/editor/app/layout.tsx b/editor/app/layout.tsx @@ -7,11 +7,18 @@ import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { getLatestChangelogDate } from "yt-dlp-transcript-common/lib/changelog"; import { listSites } from "yt-dlp-transcript-common/lib/site"; +import { fontVars } from "yt-dlp-transcript-common/styles/fonts"; +import { ThemeScript } from "yt-dlp-transcript-common/components/ThemeScript"; +import { ThemeProvider } from "yt-dlp-transcript-common/components/ThemeProvider"; +import { ThemeToggle } from "yt-dlp-transcript-common/components/ThemeToggle"; +import { ThemeMenu } from "yt-dlp-transcript-common/components/ThemeMenu"; import { AppFrame } from "./components/AppFrame"; import { AutoRefresh } from "./components/AutoRefresh"; import { CommandPalette } from "./components/CommandPalette"; import { ChangelogNavLink } from "./components/ChangelogNavLink"; import { SiteScopeSelect } from "./components/SiteScopeSelect"; +import { Toaster } from "yt-dlp-transcript-common/components/ui/sonner"; +import { NAV_GROUPS, type NavLink } from "./lib/nav"; import "./globals.css"; export async function generateMetadata(): Promise<Metadata> { @@ -25,53 +32,7 @@ export async function generateMetadata(): Promise<Metadata> { }; } -type NavLink = { - href: string; - label: string; - badgeKey?: "jobs" | "running" | "changelog"; -}; - -type NavGroup = { - // Short label shown above the group. The active site selector scopes the - // "Site" group; "Pool" and "Manage" are unaffected (shared across all sites). - label: string; - links: NavLink[]; -}; - -const NAV_GROUPS: NavGroup[] = [ - { - label: "Site", - links: [ - { href: "/", label: "Dashboard" }, - { href: "/channels", label: "Channels" }, - { href: "/charts", label: "Charts" }, - { href: "/deploy", label: "Deploy" }, - ], - }, - { - label: "Pool", - links: [ - { href: "/jobs", label: "Jobs", badgeKey: "jobs" }, - { href: "/jobs/active", label: "Active", badgeKey: "running" }, - { href: "/workers", label: "Workers" }, - { href: "/widget/builder", label: "Monitor" }, - { href: "/scheduler", label: "Schedule" }, - { href: "/auto-queue", label: "Auto-queue" }, - { href: "/build", label: "Build" }, - { href: "/actionable", label: "Actionable" }, - { href: "/saved-videos", label: "Saved videos" }, - ], - }, - { - label: "Manage", - links: [ - { href: "/sites", label: "Sites" }, - { href: "/homepage", label: "Homepage" }, - { href: "/settings", label: "Settings" }, - { href: "/changelog", label: "Changelog", badgeKey: "changelog" }, - ], - }, -]; +// NAV_GROUPS now lives in ./lib/nav (shared with the command palette). function readChangelogLatestDate(): string | null { try { @@ -103,8 +64,9 @@ export default async function RootLayout({ siteTitle: s.siteTitle, })); const navItemClass = - "px-3 py-2 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 whitespace-nowrap flex items-center gap-2"; + "px-2.5 py-1.5 rounded-md text-foreground/80 hover:text-foreground hover:bg-muted whitespace-nowrap flex items-center gap-2 transition-colors"; const renderLink = (link: NavLink) => { + const Icon = link.icon; if (link.badgeKey === "changelog") { return ( <ChangelogNavLink @@ -113,6 +75,12 @@ export default async function RootLayout({ label={link.label} latestDate={latestChangelogDate} className={navItemClass} + icon={ + <Icon + className="size-4 shrink-0 text-muted-foreground" + aria-hidden="true" + /> + } /> ); } @@ -129,11 +97,12 @@ export default async function RootLayout({ badgeCount > 0; return ( <Link key={link.href} href={link.href} className={navItemClass}> + <Icon className="size-4 shrink-0 text-muted-foreground" aria-hidden="true" /> <span>{link.label}</span> {showBadge && ( <span aria-label={`${badgeCount} ${badgeNoun}${badgeCount === 1 ? "" : "s"}`} - className="ml-auto text-xs rounded-full border border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-950 text-blue-800 dark:text-blue-200 px-2 py-0.5 leading-none" + className="ml-auto text-xs rounded-full border border-brand/30 bg-brand-soft text-brand px-2 py-0.5 leading-none" > {badgeCount} </span> @@ -142,20 +111,32 @@ export default async function RootLayout({ ); }; return ( - <html lang="en" className="h-full antialiased"> - <body className="min-h-full flex flex-col md:flex-row bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100"> + <html + lang="en" + suppressHydrationWarning + className={`${fontVars} h-full antialiased`} + > + <body className="min-h-full flex flex-col md:flex-row bg-background text-foreground"> + <ThemeScript /> + <ThemeProvider> <AppFrame sidebar={ - <aside className="md:w-56 md:shrink-0 md:sticky md:top-0 md:self-start md:h-screen md:overflow-y-auto border-b md:border-b-0 md:border-r border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 flex flex-col"> - <div className="px-4 py-3 md:py-4 md:border-b md:border-zinc-200 dark:md:border-zinc-800"> - <Link - href="/" - className="font-semibold tracking-tight block leading-tight" - > - {headerLabel} - </Link> - <div className="text-xs uppercase tracking-wide text-zinc-500 mt-0.5"> - editor + <aside className="md:w-56 md:shrink-0 md:sticky md:top-0 md:self-start md:h-screen md:overflow-y-auto border-b md:border-b-0 md:border-r border-border bg-card flex flex-col"> + <div className="px-3 py-2.5 md:py-3 md:border-b md:border-border flex items-start justify-between gap-2"> + <div className="min-w-0"> + <Link + href="/" + className="font-display text-base font-semibold tracking-tight block leading-tight" + > + {headerLabel} + </Link> + <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-muted-foreground mt-0.5"> + editor + </div> + </div> + <div className="flex items-center gap-1.5 shrink-0"> + <ThemeMenu /> + <ThemeToggle /> </div> </div> <Suspense fallback={null}> @@ -167,7 +148,7 @@ export default async function RootLayout({ key={group.label} className="flex flex-row md:flex-col gap-1 md:gap-0.5 items-center md:items-stretch" > - <div className="px-3 py-1 text-[10px] uppercase tracking-wider text-zinc-400 dark:text-zinc-600 whitespace-nowrap"> + <div className="px-2.5 py-1 font-mono text-[10px] uppercase tracking-[0.16em] text-muted-foreground/70 whitespace-nowrap"> {group.label} </div> {group.links.map(renderLink)} @@ -181,6 +162,7 @@ export default async function RootLayout({ <Suspense fallback={null}> <CommandPalette /> </Suspense> + <Toaster position="bottom-right" /> <AutoRefresh intervalMs={settings.autoRefreshIntervalSeconds * 1000} /> @@ -189,6 +171,7 @@ export default async function RootLayout({ > {children} </AppFrame> + </ThemeProvider> </body> </html> ); diff --git a/editor/app/lib/nav.ts b/editor/app/lib/nav.ts @@ -0,0 +1,80 @@ +import type { LucideIcon } from "lucide-react"; +import { + Activity, + CalendarClock, + ChartColumnBig, + Cpu, + Globe, + Hammer, + House, + LayoutDashboard, + ListChecks, + ListPlus, + Monitor, + Rocket, + ScrollText, + Settings, + TriangleAlert, + Tv, + Bookmark, +} from "lucide-react"; + +// Single source of truth for the editor's primary navigation. Consumed by BOTH +// the sidebar (editor/app/layout.tsx) and the command palette +// (editor/app/components/CommandPalette.tsx) so they never drift — the old +// palette hard-coded a stale 5-item subset. Icons are used by the palette and +// the compact sidebar; `badgeKey` drives the live count pills / changelog dot. + +export type NavBadgeKey = "jobs" | "running" | "changelog"; + +export type NavLink = { + href: string; + label: string; + icon: LucideIcon; + badgeKey?: NavBadgeKey; + // Extra search terms for the palette (not shown), e.g. synonyms. + keywords?: string; +}; + +export type NavGroup = { + label: string; + links: NavLink[]; +}; + +export const NAV_GROUPS: NavGroup[] = [ + { + label: "Site", + links: [ + { href: "/", label: "Dashboard", icon: LayoutDashboard, keywords: "home overview" }, + { href: "/channels", label: "Channels", icon: Tv }, + { href: "/charts", label: "Charts", icon: ChartColumnBig, keywords: "stats graphs" }, + { href: "/deploy", label: "Deploy", icon: Rocket, keywords: "publish release" }, + ], + }, + { + label: "Pool", + links: [ + { href: "/jobs", label: "Jobs", icon: ListChecks, badgeKey: "jobs", keywords: "queue tasks" }, + { href: "/jobs/active", label: "Active", icon: Activity, badgeKey: "running", keywords: "monitor running live" }, + { href: "/workers", label: "Workers", icon: Cpu, keywords: "transcription whisper gpu" }, + { href: "/widget/builder", label: "Monitor", icon: Monitor, keywords: "widget embed" }, + { href: "/scheduler", label: "Schedule", icon: CalendarClock, keywords: "sync cron cadence" }, + { href: "/auto-queue", label: "Auto-queue", icon: ListPlus, keywords: "auto runner policy" }, + { href: "/build", label: "Build", icon: Hammer, keywords: "static export" }, + { href: "/actionable", label: "Actionable", icon: TriangleAlert, keywords: "needs attention todo" }, + { href: "/saved-videos", label: "Saved videos", icon: Bookmark, keywords: "backup" }, + ], + }, + { + label: "Manage", + links: [ + { href: "/sites", label: "Sites", icon: Globe }, + { href: "/homepage", label: "Homepage", icon: House, keywords: "hub" }, + { href: "/settings", label: "Settings", icon: Settings, keywords: "config preferences" }, + { href: "/changelog", label: "Changelog", icon: ScrollText, badgeKey: "changelog", keywords: "release notes" }, + ], + }, +]; + +// Flat list of every nav link (palette convenience). +export const NAV_LINKS: NavLink[] = NAV_GROUPS.flatMap((g) => g.links); diff --git a/editor/app/page.tsx b/editor/app/page.tsx @@ -14,6 +14,8 @@ import { } from "yt-dlp-transcript-common/lib/site"; import { loadActionableSummary } from "./actionable/lib/loadActionable"; import { resolveActiveSite } from "./lib/activeSite"; +import { buildActiveJobsPayload } from "./jobs/active/buildActiveJobs"; +import { ActiveJobsLive } from "./jobs/components/ActiveJobsLive"; import type { ActionableRow } from "./actionable/lib/loadActionable"; export const dynamic = "force-dynamic"; @@ -76,13 +78,16 @@ export default async function Dashboard({ const runningJobs = getRegistry() .list() .filter((j) => j.status === "running" || j.status === "queued"); + const activeJobsPayload = await buildActiveJobsPayload(); const changelogSource = loadChangelog(); const needsAttention = undownloaded.length > 0 || untranscribed.length > 0; return ( <div className="flex flex-col gap-6"> - <h1 className="text-2xl font-semibold">Dashboard</h1> + <h1 className="font-display text-2xl font-semibold tracking-tight"> + Dashboard + </h1> <dl className="grid grid-cols-1 sm:grid-cols-4 gap-4"> <Stat label="channels" value={String(channels.length)} /> <Stat label="videos" value={totalVideos.toLocaleString()} /> @@ -97,13 +102,20 @@ export default async function Dashboard({ /> </dl> + {runningJobs.length > 0 && ( + <section aria-label="Live jobs" className="flex flex-col gap-3"> + <h2 className="font-display text-lg font-semibold">Live jobs</h2> + <ActiveJobsLive initial={activeJobsPayload} /> + </section> + )} + {needsAttention && ( <Link href="/actionable" aria-label="needs attention" - className="block rounded-md border border-amber-200 dark:border-amber-900 bg-amber-50 dark:bg-amber-950/40 px-4 py-3 hover:border-amber-400 dark:hover:border-amber-700" + className="block rounded-md border border-amber-300/70 dark:border-amber-900 bg-amber-50 dark:bg-amber-950/40 px-4 py-3 hover:border-amber-400 dark:hover:border-amber-700 transition-colors" > - <h2 className="text-xs uppercase tracking-wide text-amber-700 dark:text-amber-400 mb-1"> + <h2 className="font-mono text-xs uppercase tracking-[0.14em] text-amber-700 dark:text-amber-400 mb-1"> Needs attention </h2> <p className="text-sm"> @@ -127,16 +139,16 @@ export default async function Dashboard({ <Link href="/changelog" aria-label="View full changelog" - className="block rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-3 hover:border-zinc-400 dark:hover:border-zinc-600" + className="block rounded-md border border-border bg-card px-4 py-3 hover:border-brand/40 transition-colors" > - <h2 className="text-xs uppercase tracking-wide text-zinc-500 mb-1"> + <h2 className="font-mono text-xs uppercase tracking-[0.14em] text-muted-foreground mb-1"> Recent changes </h2> <div className="relative max-h-48 overflow-hidden"> <Changelog source={changelogSource} limit={1} /> <div aria-hidden="true" - className="pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-white dark:from-zinc-900 to-transparent" + className="pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-card to-transparent" /> </div> </Link> @@ -144,10 +156,10 @@ export default async function Dashboard({ {channels.length > 0 && ( <section className="flex flex-col gap-3"> - <h2 className="text-lg font-semibold">Channels</h2> + <h2 className="font-display text-lg font-semibold">Channels</h2> <div className="overflow-x-auto -mx-4 md:mx-0 md:overflow-visible"> - <table className="text-sm border-y md:border md:border-zinc-200 dark:md:border-zinc-800 border-zinc-200 dark:border-zinc-800 md:rounded-md md:overflow-hidden w-full"> - <thead className="bg-zinc-100 dark:bg-zinc-900"> + <table className="text-sm border-y md:border border-border md:rounded-md md:overflow-hidden w-full"> + <thead className="bg-muted"> <tr> <th className="text-left font-medium px-3 py-2">Slug</th> <th className="text-left font-medium px-3 py-2">Handling</th> @@ -159,21 +171,20 @@ export default async function Dashboard({ </thead> <tbody> {channels.map((c) => ( - <tr - key={c.slug} - className="border-t border-zinc-200 dark:border-zinc-800" - > + <tr key={c.slug} className="border-t border-border"> <td className="px-3 py-2 font-mono"> <Link href={`/channels/${c.slug}`} - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline underline-offset-2 hover:text-brand transition-colors" > {c.slug} </Link> </td> <td className="px-3 py-2">{c.config.handling}</td> - <td className="px-3 py-2 text-right">{c.videoCount}</td> - <td className="px-3 py-2 text-xs text-zinc-500 whitespace-nowrap"> + <td className="px-3 py-2 text-right tabular-nums"> + {c.videoCount} + </td> + <td className="px-3 py-2 text-xs text-muted-foreground whitespace-nowrap"> {c.config.lastSyncedAt ? new Date(c.config.lastSyncedAt).toLocaleString() : "never"} @@ -194,12 +205,14 @@ function Stat({ label, value }: { label: string; value: string }) { <div role="group" aria-label={label} - className="rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-3" + className="rounded-md border border-border bg-card px-4 py-3" > - <div className="text-xs uppercase tracking-wide text-zinc-500"> + <div className="font-mono text-xs uppercase tracking-[0.14em] text-muted-foreground"> {label} </div> - <div className="text-2xl font-semibold mt-1">{value}</div> + <div className="font-display text-2xl font-semibold mt-1 tabular-nums"> + {value} + </div> </div> ); } @@ -217,12 +230,14 @@ function StatLink({ <Link href={href} aria-label={label} - className="rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-3 hover:border-zinc-400 dark:hover:border-zinc-600" + className="rounded-md border border-border bg-card px-4 py-3 hover:border-brand/40 transition-colors" > - <div className="text-xs uppercase tracking-wide text-zinc-500"> + <div className="font-mono text-xs uppercase tracking-[0.14em] text-muted-foreground"> {label} </div> - <div className="text-2xl font-semibold mt-1">{value}</div> + <div className="font-display text-2xl font-semibold mt-1 tabular-nums"> + {value} + </div> </Link> ); } diff --git a/editor/app/settings/actions.ts b/editor/app/settings/actions.ts @@ -35,6 +35,7 @@ export async function saveSettingsAction( formData: FormData, ): Promise<SaveResult> { const adminTitle = String(formData.get("adminTitle") ?? "").trim(); + const homepageUrl = String(formData.get("homepageUrl") ?? "").trim(); const maxBytesRaw = String(formData.get("maxTranscriptPageBytes") ?? "").trim(); const cookiesFromBrowser = String( formData.get("cookiesFromBrowser") ?? "", @@ -219,6 +220,7 @@ export async function saveSettingsAction( // re-sanitizes it regardless. autoQueue: getSettings().autoQueue, socialLinks, + homepageUrl, // Preserve the saved-video backup config on an unrelated settings save (the // Saved Videos page edits it). writeSettings re-sanitizes it regardless. savedVideoBackup: getSettings().savedVideoBackup, diff --git a/editor/app/settings/components/SettingsForm.tsx b/editor/app/settings/components/SettingsForm.tsx @@ -42,6 +42,13 @@ export function SettingsForm({ initial, apps }: Props) { hint="Title for this editor's own shell. Public sites get their titles from each site's config." /> <Field + label="Family hub URL" + name="homepageUrl" + defaultValue={initial.homepageUrl} + type="url" + hint="Absolute URL of the family hub/homepage (e.g. https://archilyzer.com). Every export site links back to it. Leave blank for no hub link." + /> + <Field label="Max transcript page bytes" name="maxTranscriptPageBytes" defaultValue={String(initial.maxTranscriptPageBytes)} diff --git a/editor/app/sites/actions.ts b/editor/app/sites/actions.ts @@ -2,6 +2,7 @@ import { revalidatePath } from "next/cache"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; +import { parseAccent } from "yt-dlp-transcript-common/lib/accent"; import { writeSite, deleteSite, @@ -41,6 +42,13 @@ export async function saveSiteAction( if (!headerTitle) return { ok: false, error: "Header title is required" }; const siteDescription = String(formData.get("siteDescription") ?? "").trim(); const homeTagline = String(formData.get("homeTagline") ?? "").trim(); + const accentRaw = String(formData.get("accent") ?? "").trim(); + if (accentRaw && !parseAccent(accentRaw)) { + return { + ok: false, + error: "Brand accent must be a hex color like #95661a (or left blank).", + }; + } const cloudflareProject = String( formData.get("cloudflareProject") ?? "", ).trim(); @@ -138,6 +146,7 @@ export async function saveSiteAction( groups, defaultGroupId, channels, + ...(parseAccent(accentRaw) ? { accent: parseAccent(accentRaw) } : {}), ...(cloudflareProject ? { cloudflareProject } : {}), ...(siteUrl ? { siteUrl } : {}), ...(relatedSites.length > 0 ? { relatedSites } : {}), diff --git a/editor/app/sites/components/SiteForm.tsx b/editor/app/sites/components/SiteForm.tsx @@ -222,6 +222,12 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { defaultValue={initial.homeTagline} /> <Field + label="Brand accent" + name="accent" + defaultValue={initial.accent ?? ""} + hint="Optional brand color as a hex (#rrggbb) — overrides the family brass on this site. Leave blank to inherit the family accent." + /> + <Field label="Cloudflare Pages project" name="cloudflareProject" defaultValue={initial.cloudflareProject ?? ""} diff --git a/editor/e2e/export-search.spec.ts b/editor/e2e/export-search.spec.ts @@ -412,6 +412,10 @@ test.describe("export TranscriptSearch — query builder + filter layout", () => test("unlisted videos are shown by default and 'Unlisted' unchecked hides them", async ({ page, }) => { + // Give the window-virtualized results enough height to materialize all four + // matched rows (the default 720px viewport only fits three Archive-density + // cards, leaving the last one un-rendered and unscrollable). + await page.setViewportSize({ width: 1280, height: 1600 }); await search(page, "platypus"); await expect( resultCard(page, /Hidden mammal documentary.*Test Channel/), @@ -587,8 +591,10 @@ test.describe("export footer", () => { await expect(social).toHaveAttribute("target", "_blank"); const svgHtml = await social.innerHTML(); expect(svgHtml).toContain("<svg"); - expect(svgHtml).not.toMatch(/<svg[^>]*\swidth=/); - expect(svgHtml).not.toMatch(/<svg[^>]*\sheight=/); + // The footer sizes social SVGs with intrinsic width/height attributes so + // they don't balloon before CSS loads on a static host (the FOUC fix). + expect(svgHtml).toMatch(/<svg[^>]*\swidth=/); + expect(svgHtml).toMatch(/<svg[^>]*\sheight=/); }); test("a site's own social links win over the global default", async ({ @@ -666,15 +672,17 @@ test.describe("export TranscriptSearch — deferred search submit", () => { await search(page, "platypus"); const searchBtn = page.getByTestId("search-submit"); await expect(searchBtn).toBeVisible(); - // Pristine after commit: outline/muted styling — no blue background. - await expect(searchBtn).not.toHaveClass(/bg-blue-600/); + // Pristine after commit: muted/outline variant (data-dirty="false"). The + // dirty signal is a stable data attribute, not a color class, since the kit + // Button drives the primary style. + await expect(searchBtn).toHaveAttribute("data-dirty", "false"); await page.getByRole("checkbox", { name: "Deleted" }).uncheck(); - // Dirty: primary blue styling. - await expect(searchBtn).toHaveClass(/bg-blue-600/); + // Dirty: primary CTA. + await expect(searchBtn).toHaveAttribute("data-dirty", "true"); // Submit, returns to pristine. await searchBtn.click(); - await expect(searchBtn).not.toHaveClass(/bg-blue-600/); + await expect(searchBtn).toHaveAttribute("data-dirty", "false"); }); }); diff --git a/editor/e2e/site-scope.spec.ts b/editor/e2e/site-scope.spec.ts @@ -69,18 +69,23 @@ test("deploy targets the active site and is disabled under all sites", async ({ }) => { await twoSites(); await page.goto("/deploy?site=__all__"); + // Under "all sites" the primary Build & deploy path is blocked: its notice is + // shown and the button is disabled. (The "Build static export" step now lives + // in a collapsed "Individual steps" details, so it's asserted via the primary + // visible path here.) await expect( - page.getByText(/select a specific site from the sidebar to build/i), + page.getByText(/select a specific site from the sidebar to build & deploy\./i), ).toBeVisible(); await expect( - page.getByRole("button", { name: "Build static export" }), + page.getByRole("button", { name: "Build & deploy" }), ).toBeDisabled(); await page.goto("/deploy?site=alpha"); - await expect(page.getByText(/Building/).first()).toBeVisible(); + // Selecting a specific site clears the "pick a site" notice (build/deploy is + // now scoped to alpha). await expect( - page.getByRole("button", { name: "Build static export" }), - ).toBeEnabled(); + page.getByText(/select a specific site from the sidebar to build & deploy\./i), + ).toHaveCount(0); }); test("pool views show every site regardless of the selector", async ({ diff --git a/editor/package.json b/editor/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@sindresorhus/slugify": "^3.0.0", + "lucide-react": "^1.16.0", "markdown-to-jsx": "^7.7.4", "next": "16.2.3", "react": "19.2.4", diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog ## [Unreleased] +- **Two more themes + a theme picker.** A palette menu next to the light/dark toggle lets you switch the whole site's look between **Archive** (the warm reading room), **Terminal** (an ice-cyan signal terminal), **Swiss** (red/black/white editorial), and **Base** (neutral) — each in light or dark, and your choice is remembered. Switching is instant and purely cosmetic (just colors; the layout never changes). +- **Optional per-site brand accent.** A site can set its own accent color (a hex, in the editor's site config) to replace the family brass everywhere on its public build — the wordmark mark, links, badges, and highlights. It's baked onto the page at build time so it's correct on the very first paint, and applies in both light and dark. Sites that don't set one keep the family brass. +- **New look: the "Archive / Reading Room" identity.** The public site adopts the family design system — a serif display face (Source Serif 4) for headings/counts, clean sans for body, and mono for the query/power layer, over a warm paper (light) / ink (dark) palette with a brass accent. A light/dark/system theme toggle in the header replaces OS-only dark mode and persists your choice. Header, footer, and the whole search UI (query builder, filters, scopes, charts) are rebuilt on a shared component kit for a calmer, more consistent feel — the simple one-box search is unchanged; the power tools just read better. +- **"New since your last visit."** A dismissible banner on the home page tells you how many transcripts were added since you were last here, and first-time visitors get a one-line nudge toward the power tools (layered queries, scopes, filters, charts). Entirely client-side — nothing about your visit leaves your browser. +- **Family navigation.** The header gains a "Sites" switcher to hop between sibling sites in the pool, plus an optional link back to the family hub (set the hub URL once in the editor's Settings). +- **Fixed: flash of giant social icons on first load.** Footer social icons now carry an intrinsic size, so they render correctly before the stylesheet loads on a static host instead of briefly ballooning. - **Transcript coverage is now a chart metric.** Each video's stats record carries a `coverage` fraction (transcript's last cue end ÷ video duration), selectable as a Y-axis metric ("Transcript coverage") in the chart options — e.g. average coverage by channel surfaces channels with truncated/partial transcripts. Coverage is `null` for videos with no transcript or no usable duration. (Requires the one-time `build:index` + `build:stats` rebuild on the bumped stats schema; the field is back-filled for every transcribed video from its `transcript.cues.json`.) ## [0.4.1] - 2026-06-22 diff --git a/export/app/components/Footer.tsx b/export/app/components/Footer.tsx @@ -1,4 +1,7 @@ -import { getSettings } from "yt-dlp-transcript-common/lib/settings"; +import { + getSettings, + sizeSocialSvg, +} from "yt-dlp-transcript-common/lib/settings"; import { listSites, resolveRelatedSites, @@ -14,13 +17,15 @@ export default function Footer() { // related-sites block is omitted entirely. const related = resolveRelatedSites(site, listSites()); return ( - <footer className="mt-auto border-t border-zinc-200 dark:border-zinc-800 bg-white/40 dark:bg-zinc-900/40"> - <div className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between gap-4 text-sm text-zinc-500"> + <footer className="mt-auto border-t border-border bg-muted/30"> + <div className="max-w-6xl mx-auto px-4 sm:px-6 py-4 flex items-center justify-between gap-4 text-sm text-muted-foreground"> <div className="flex items-center gap-3"> - <span>Downloads:</span> + <span className="font-mono text-xs uppercase tracking-[0.14em]"> + Downloads + </span> <a href="/code.tar.gz" - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline underline-offset-2 hover:text-foreground transition-colors" > code.tar.gz </a> @@ -35,8 +40,8 @@ export default function Footer() { aria-label={link.label} target="_blank" rel="noopener noreferrer" - className="inline-block w-5 h-5 text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 [&_svg]:w-full [&_svg]:h-full" - dangerouslySetInnerHTML={{ __html: link.svg }} + className="inline-block w-5 h-5 text-muted-foreground hover:text-brand transition-colors [&_svg]:w-full [&_svg]:h-full" + dangerouslySetInnerHTML={{ __html: sizeSocialSvg(link.svg) }} /> </li> ))} @@ -46,7 +51,7 @@ export default function Footer() { {related.length > 0 && ( <nav aria-label="Related sites" - className="max-w-6xl mx-auto px-4 pb-4 -mt-1 flex flex-wrap items-baseline gap-x-4 gap-y-1 text-xs text-zinc-400" + className="max-w-6xl mx-auto px-4 sm:px-6 pb-4 -mt-1 flex flex-wrap items-baseline gap-x-4 gap-y-1 text-xs text-muted-foreground/80" > {related.map((group, gi) => ( <div @@ -54,7 +59,7 @@ export default function Footer() { className="flex flex-wrap items-baseline gap-x-2 gap-y-1" > {group.label && ( - <span className="uppercase tracking-wide text-zinc-400/80"> + <span className="font-mono uppercase tracking-[0.14em] text-muted-foreground/70"> {group.label} </span> )} @@ -65,7 +70,7 @@ export default function Footer() { <a href={s.url} rel="noopener noreferrer" - className="hover:text-zinc-900 dark:hover:text-zinc-100" + className="hover:text-brand transition-colors" > {s.title} </a> diff --git a/export/app/components/Header.tsx b/export/app/components/Header.tsx @@ -0,0 +1,66 @@ +import Link from "next/link"; +import { ArrowUpLeft } from "lucide-react"; +import { getSettings } from "yt-dlp-transcript-common/lib/settings"; +import { listSites, resolveRelatedSites } from "yt-dlp-transcript-common/lib/site"; +import { ThemeToggle } from "yt-dlp-transcript-common/components/ThemeToggle"; +import { ThemeMenu } from "yt-dlp-transcript-common/components/ThemeMenu"; +import { currentSite } from "../lib/site"; +import SiblingSwitcher from "./SiblingSwitcher"; + +// The export site's masthead, in the family "Archive / Reading Room" identity: +// a brass mark + serif wordmark, calm sans nav, and the cross-site "family" +// chrome (hub backlink + sibling switcher). All cross-site data is resolved at +// build time from the pool, so single-site installs simply render neither. +export default function Header() { + const site = currentSite(); + const settings = getSettings(); + const hubUrl = settings.homepageUrl; + const related = resolveRelatedSites(site, listSites()); + + return ( + <header className="sticky top-0 z-20 border-b border-border bg-background/80 backdrop-blur-md"> + <div className="max-w-6xl mx-auto px-4 sm:px-6 flex h-14 items-center gap-x-5 gap-y-1 flex-wrap"> + <Link href="/" className="group flex items-center gap-2.5 shrink-0"> + <span className="h-2.5 w-2.5 shrink-0 rotate-45 rounded-[3px] bg-brand shadow-[0_0_14px_var(--brand)] transition-colors group-hover:bg-brand-strong" /> + <span className="font-display text-[1.35rem] font-semibold leading-none tracking-tight text-foreground"> + {site.headerTitle} + </span> + </Link> + + <nav className="flex items-center gap-4 text-sm font-medium"> + <Link href="/" className="text-foreground hover:text-brand transition-colors"> + Search + </Link> + <Link + href="/duplicates" + className="text-foreground hover:text-brand transition-colors" + > + Duplicates + </Link> + </nav> + + <div className="ml-auto flex items-center gap-2 sm:gap-3 shrink-0"> + <SiblingSwitcher groups={related} /> + {hubUrl && ( + <a + href={hubUrl} + rel="noopener noreferrer" + className="hidden sm:inline-flex items-center gap-1 font-mono text-xs uppercase tracking-[0.12em] text-muted-foreground hover:text-brand transition-colors" + > + <ArrowUpLeft className="size-3.5" aria-hidden="true" /> + Hub + </a> + )} + <Link + href="/changelog" + className="text-sm text-muted-foreground hover:text-foreground transition-colors" + > + Changelog + </Link> + <ThemeMenu /> + <ThemeToggle /> + </div> + </div> + </header> + ); +} diff --git a/export/app/components/NewSinceLastVisit.tsx b/export/app/components/NewSinceLastVisit.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { X } from "lucide-react"; +import { Button } from "yt-dlp-transcript-common/components/ui/button"; + +// "New since your last visit" — a calm, dismissible banner that compares the +// current transcript count against a per-site baseline stored in localStorage on +// the previous visit. Two messages, in priority order: +// 1. transcripts grew → "N new transcripts since your last visit" +// 2. first visit ever (and the power-tools hint hasn't been seen) → a nudge +// toward the power features people tend to miss. +// All client-side: a static export can't know who visited or when, so the +// baseline lives in the browser. Versioned key so the shape can evolve safely. + +const VERSION = 1; +const visitKey = (siteId: string) => `ytdlp-tb:lastVisit:${siteId}`; +const POWER_HINT_KEY = "ytdlp-tb:powerHintSeen"; + +type Stored = { v: number; count: number; ts: number }; + +function readStored(siteId: string): Stored | null { + try { + const raw = localStorage.getItem(visitKey(siteId)); + if (!raw) return null; + const parsed = JSON.parse(raw) as Stored; + if (parsed && parsed.v === VERSION && typeof parsed.count === "number") { + return parsed; + } + } catch { + /* ignore */ + } + return null; +} + +export default function NewSinceLastVisit({ + siteId, + count, +}: { + siteId: string; + count: number; +}) { + const [message, setMessage] = useState<string | null>(null); + const [hint, setHint] = useState<string | null>(null); + + useEffect(() => { + const prev = readStored(siteId); + const delta = prev ? count - prev.count : 0; + + if (delta > 0) { + setMessage( + `${delta.toLocaleString()} new transcript${delta === 1 ? "" : "s"} since your last visit`, + ); + setHint("Use Filters and the query builder to narrow them down."); + } else if (!prev) { + let seen = false; + try { + seen = localStorage.getItem(POWER_HINT_KEY) === "1"; + } catch { + /* ignore */ + } + if (!seen) { + setMessage("Welcome to the archive"); + setHint( + "Beyond simple search: layer queries, scope by transcript or live chat, filter by channel or date, and chart what you find.", + ); + } + } + + // Mark this visit as the new baseline regardless of what we showed. + try { + const next: Stored = { v: VERSION, count, ts: Date.now() }; + localStorage.setItem(visitKey(siteId), JSON.stringify(next)); + } catch { + /* ignore */ + } + }, [siteId, count]); + + if (!message) return null; + + return ( + <div + data-testid="new-since-last-visit" + className="flex items-start gap-3 rounded-lg border border-border bg-brand-soft/60 px-4 py-3" + > + <span className="mt-0.5 h-2 w-2 shrink-0 rotate-45 rounded-[2px] bg-brand" aria-hidden="true" /> + <div className="min-w-0 flex-1"> + <p className="font-display text-sm font-semibold text-foreground">{message}</p> + {hint && <p className="mt-0.5 text-sm text-muted-foreground">{hint}</p>} + </div> + <Button + variant="ghost" + size="icon" + className="-mr-2 -mt-1 size-7 shrink-0 text-muted-foreground" + aria-label="Dismiss" + onClick={() => { + try { + localStorage.setItem(POWER_HINT_KEY, "1"); + } catch { + /* ignore */ + } + setMessage(null); + }} + > + <X className="size-4" aria-hidden="true" /> + </Button> + </div> + ); +} diff --git a/export/app/components/SiblingSwitcher.tsx b/export/app/components/SiblingSwitcher.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { ChevronDown } from "lucide-react"; +import { Button } from "yt-dlp-transcript-common/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "yt-dlp-transcript-common/components/ui/dropdown-menu"; + +// A sibling-site switcher for the export header: the "family" navigation across +// the other public sites in the pool. Fed by the same resolveRelatedSites() data +// the footer uses (groups of { siteId, title, url }), resolved server-side and +// passed in as plain props so this stays a thin client shell over the kit. +export type SwitcherSite = { siteId: string; title: string; url: string }; +export type SwitcherGroup = { label?: string; sites: SwitcherSite[] }; + +export default function SiblingSwitcher({ + groups, +}: { + groups: SwitcherGroup[]; +}) { + const total = groups.reduce((n, g) => n + g.sites.length, 0); + if (total === 0) return null; + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + size="sm" + className="gap-1 font-mono text-xs uppercase tracking-[0.12em] text-muted-foreground" + > + Sites + <ChevronDown className="size-3.5" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="min-w-48"> + {groups.map((group, gi) => ( + <div key={group.label ?? `group-${gi}`}> + {gi > 0 && <DropdownMenuSeparator />} + {group.label && ( + <DropdownMenuLabel className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-muted-foreground"> + {group.label} + </DropdownMenuLabel> + )} + {group.sites.map((s) => ( + <DropdownMenuItem key={s.siteId} asChild> + <a href={s.url} rel="noopener noreferrer"> + {s.title} + </a> + </DropdownMenuItem> + ))} + </div> + ))} + </DropdownMenuContent> + </DropdownMenu> + ); +} diff --git a/export/app/globals.css b/export/app/globals.css @@ -1,53 +1,13 @@ @import "tailwindcss"; +@import "../../common/styles/tokens.css"; @source "../../common/components"; -:root { - --background: #ffffff; - --foreground: #171717; - /* Semantic tokens consumed by shadcn/ui chart primitives. */ - --border: #e4e4e7; - --muted: #f4f4f5; - --muted-foreground: #71717a; - /* Chart series palette. */ - --chart-1: #2563eb; - --chart-2: #16a34a; - --chart-3: #ea580c; - --chart-4: #9333ea; - --chart-5: #db2777; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-border: var(--border); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - --border: #27272a; - --muted: #27272a; - --muted-foreground: #a1a1aa; - --chart-1: #60a5fa; - --chart-2: #4ade80; - --chart-3: #fb923c; - --chart-4: #c084fc; - --chart-5: #f472b6; - } -} +/* Design tokens, the `dark` variant, and theme palettes live in + common/styles/tokens.css. Export runs the public "archive" family (paper/ink); + the layout sets data-theme="archive" via ThemeScript. */ body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-sans), ui-sans-serif, system-ui, sans-serif; } diff --git a/export/app/layout.tsx b/export/app/layout.tsx @@ -1,21 +1,14 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import Link from "next/link"; +import { fontVars } from "yt-dlp-transcript-common/styles/fonts"; +import { ThemeScript } from "yt-dlp-transcript-common/components/ThemeScript"; +import { ThemeProvider } from "yt-dlp-transcript-common/components/ThemeProvider"; import { QueryProvider } from "yt-dlp-transcript-common/components/QueryProvider"; +import { siteAccentVars } from "yt-dlp-transcript-common/lib/accent"; import { currentSite } from "./lib/site"; +import Header from "./components/Header"; import Footer from "./components/Footer"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - export function generateMetadata(): Metadata { const s = currentSite(); return { @@ -32,50 +25,27 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const settings = currentSite(); + // Per-site accent: bake the brand-color override onto <html> at prerender so + // it's present on first paint (before any CSS/JS). Unset → family brass. + const accentVars = siteAccentVars(currentSite().accent); return ( <html lang="en" - className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} + suppressHydrationWarning + className={`${fontVars} h-full antialiased`} + style={accentVars ?? undefined} > - <body className="min-h-full flex flex-col bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100 font-sans"> - <QueryProvider> - <header className="border-b border-zinc-200 dark:border-zinc-800 bg-white/70 dark:bg-zinc-900/70 backdrop-blur sticky top-0 z-20"> - <div className="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between gap-3"> - <div className="flex items-center gap-5 min-w-0"> - <Link href="/" className="font-semibold tracking-tight shrink-0"> - {settings.headerTitle} - </Link> - <nav className="flex items-center gap-4 text-sm font-medium"> - <Link - href="/" - className="text-zinc-900 dark:text-zinc-100 hover:text-blue-600 dark:hover:text-blue-400" - > - Search - </Link> - <Link - href="/duplicates" - className="text-zinc-900 dark:text-zinc-100 hover:text-blue-600 dark:hover:text-blue-400" - > - Duplicates - </Link> - </nav> - </div> - <nav className="text-sm shrink-0"> - <Link - href="/changelog" - className="text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100" - > - Changelog - </Link> - </nav> - </div> - </header> - <main className="flex-1 w-full max-w-6xl mx-auto px-4 py-6"> - {children} - </main> - <Footer /> - </QueryProvider> + <body className="min-h-full flex flex-col bg-background text-foreground font-sans"> + <ThemeScript defaultTheme="archive" defaultMode="system" /> + <ThemeProvider defaultTheme="archive" defaultMode="system"> + <QueryProvider> + <Header /> + <main className="flex-1 w-full max-w-6xl mx-auto px-4 sm:px-6 py-6"> + {children} + </main> + <Footer /> + </QueryProvider> + </ThemeProvider> </body> </html> ); diff --git a/export/app/page.tsx b/export/app/page.tsx @@ -3,6 +3,7 @@ import { PlayerProvider } from "yt-dlp-transcript-common/components/PlayerProvid import TranscriptModal from "yt-dlp-transcript-common/components/TranscriptModal"; import TranscriptSearch from "yt-dlp-transcript-common/components/TranscriptSearch"; import { currentSite } from "./lib/site"; +import NewSinceLastVisit from "./components/NewSinceLastVisit"; export default async function Home() { const settings = currentSite(); @@ -11,12 +12,16 @@ export default async function Home() { return ( <PlayerProvider> <div className="flex flex-col gap-6"> - <div> - <h1 className="text-2xl font-semibold">{count} transcripts</h1> + <div className="flex flex-col gap-1"> + <h1 className="font-display text-3xl font-semibold tracking-tight text-foreground"> + <span className="tabular-nums">{count.toLocaleString()}</span>{" "} + <span className="font-normal text-muted-foreground">transcripts</span> + </h1> {settings.homeTagline && ( - <p className="text-sm text-zinc-500 mt-1">{settings.homeTagline}</p> + <p className="text-sm text-muted-foreground">{settings.homeTagline}</p> )} </div> + <NewSinceLastVisit siteId={settings.siteId} count={count} /> <TranscriptSearch /> </div> <TranscriptModal /> diff --git a/export/e2e/fixtures/sites/testsite/site.json b/export/e2e/fixtures/sites/testsite/site.json @@ -4,6 +4,7 @@ "siteDescription": "Fixture site description", "headerTitle": "Fixture Header", "homeTagline": "Fixture tagline here", + "accent": "#cc3366", "socialLinks": [ { "label": "GitHub", diff --git a/export/e2e/query-tree.spec.ts b/export/e2e/query-tree.spec.ts @@ -305,8 +305,10 @@ test.describe("composite search — query tree", () => { ), ).toHaveCount(1); - // Flip the alpha leaf's "Show hits" checkbox on and re-commit. - const showHits = page.locator('input[data-testid^="leaf-hits-"]').first(); + // Flip the alpha leaf's "Show hits" checkbox on and re-commit. (The kit + // Checkbox renders a role="checkbox" button, not an <input> — match by + // testid prefix; Playwright's check() drives the ARIA checkbox.) + const showHits = page.locator('[data-testid^="leaf-hits-"]').first(); await showHits.check(); await page.getByTestId("search-submit").click(); @@ -429,11 +431,12 @@ test.describe("composite search — query tree", () => { // The metadata leaf should now be a direct child of root with its // `negate` checkbox checked (De Morgan applied locally). await expect(page.getByTestId(/^group-unwrap-/)).toHaveCount(0); - const negateBoxes = page.locator('input[data-testid^="leaf-negate-"]'); + const negateBoxes = page.locator('[data-testid^="leaf-negate-"]'); // The transcripts leaf and the metadata leaf are now both root-level - // children. The metadata leaf is the negated one. + // children. The metadata leaf is the negated one. (Kit Checkbox = a + // role="checkbox" button, so read aria-checked rather than input.checked.) const checkedStates = await negateBoxes.evaluateAll((els) => - (els as HTMLInputElement[]).map((e) => e.checked), + els.map((e) => e.getAttribute("aria-checked") === "true"), ); expect(checkedStates.filter(Boolean).length).toBe(1); }); diff --git a/export/e2e/site-branding.spec.ts b/export/e2e/site-branding.spec.ts @@ -28,3 +28,16 @@ test("footer renders the site's social links", async ({ page }) => { page.getByRole("link", { name: "GitHub" }), ).toHaveAttribute("href", "https://github.com/example"); }); + +test("per-site accent is baked onto <html> as a brand override", async ({ + page, +}) => { + await installRoutes(page); + await page.goto("/"); + // site.json sets accent #cc3366; the layout emits it as an inline --brand + // override on <html> at prerender (overriding the family brass). + await expect(page.locator("html")).toHaveAttribute( + "style", + /--brand:\s*#cc3366/, + ); +}); diff --git a/export/e2e/theme-family.spec.ts b/export/e2e/theme-family.spec.ts @@ -0,0 +1,48 @@ +import { test, expect, type Page } from "@playwright/test"; + +// Phase 5: the ThemeMenu family picker. Each family is a pure CSS token swap, so +// picking one only sets data-theme on <html> (persisted, re-applied pre-paint). + +async function familyState(page: Page) { + return page.evaluate(() => ({ + theme: document.documentElement.getAttribute("data-theme"), + persisted: localStorage.getItem("ytdlp-tb:theme"), + })); +} + +async function pickFamily(page: Page, label: string) { + await page.getByRole("button", { name: "Choose theme" }).click(); + await page.getByRole("menuitemradio", { name: label }).click(); +} + +test("theme-family picker switches + persists data-theme with no FOUC", async ({ + page, +}) => { + await page.goto("/"); + // Export defaults to the archive family. + expect((await familyState(page)).theme).toBe("archive"); + await expect( + page.getByRole("button", { name: "Choose theme" }), + ).toBeVisible(); + + await pickFamily(page, "Terminal"); + let s = await familyState(page); + expect(s.theme).toBe("terminal"); + expect(s.persisted).toBe("terminal"); + + // Reload: the pre-paint script re-applies data-theme before React hydrates. + await page.reload({ waitUntil: "commit" }); + expect( + await page.evaluate(() => + document.documentElement.getAttribute("data-theme"), + ), + ).toBe("terminal"); + + await pickFamily(page, "Swiss"); + s = await familyState(page); + expect(s.theme).toBe("swiss"); + expect(s.persisted).toBe("swiss"); + + await pickFamily(page, "Archive"); + expect((await familyState(page)).theme).toBe("archive"); +}); diff --git a/export/e2e/theme.spec.ts b/export/e2e/theme.spec.ts @@ -0,0 +1,64 @@ +import { test, expect, type Page } from "@playwright/test"; + +// Phase 2: the export site adopts the public "archive" family (paper/ink) with a +// system-default mode. The data-theme attribute is always "archive"; the mode +// toggle must apply, persist across reloads, and be set before hydration. + +async function state(page: Page) { + return page.evaluate(() => ({ + dark: document.documentElement.classList.contains("dark"), + theme: document.documentElement.getAttribute("data-theme"), + mode: localStorage.getItem("ytdlp-tb:mode"), + })); +} + +async function cycleTo(page: Page, target: "dark" | "light") { + const toggle = page.getByRole("button", { name: /switch to/i }); + for (let i = 0; i < 3; i++) { + if ((await state(page)).mode === target) break; + await toggle.click(); + } +} + +test("archive family; mode toggle applies + persists with no FOUC", async ({ + page, +}) => { + await page.goto("/"); + + // Public archive family => data-theme="archive" on <html>. + expect((await state(page)).theme).toBe("archive"); + await expect(page.getByRole("button", { name: /switch to/i })).toBeVisible(); + + // Explicit dark, then reload-persists before hydration. + await cycleTo(page, "dark"); + let s = await state(page); + expect(s.mode).toBe("dark"); + expect(s.dark).toBe(true); + expect(s.theme).toBe("archive"); + + await page.reload({ waitUntil: "commit" }); + let onCommit = await page.evaluate(() => ({ + dark: document.documentElement.classList.contains("dark"), + theme: document.documentElement.getAttribute("data-theme"), + mode: localStorage.getItem("ytdlp-tb:mode"), + })); + expect(onCommit.mode).toBe("dark"); + expect(onCommit.dark).toBe(true); + expect(onCommit.theme).toBe("archive"); + + // Explicit light, then reload-persists. + await cycleTo(page, "light"); + s = await state(page); + expect(s.mode).toBe("light"); + expect(s.dark).toBe(false); + + await page.reload({ waitUntil: "commit" }); + onCommit = await page.evaluate(() => ({ + dark: document.documentElement.classList.contains("dark"), + theme: document.documentElement.getAttribute("data-theme"), + mode: localStorage.getItem("ytdlp-tb:mode"), + })); + expect(onCommit.mode).toBe("light"); + expect(onCommit.dark).toBe(false); + expect(onCommit.theme).toBe("archive"); +}); diff --git a/export/package.json b/export/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@tanstack/react-query": "^5.99.1", + "lucide-react": "^1.16.0", "next": "16.2.3", "react": "19.2.4", "react-dom": "19.2.4", diff --git a/homepage/app/components/CustomizePanel.tsx b/homepage/app/components/CustomizePanel.tsx @@ -32,7 +32,7 @@ export function CustomizePanel({ onClick={() => setOpen((o) => !o)} className="inline-flex w-fit items-center gap-1.5 font-mono text-[0.6875rem] uppercase tracking-[0.18em] text-[var(--muted-foreground)] transition-colors hover:text-[var(--foreground)]" > - <span className="text-[var(--accent)]">{open ? "–" : "+"}</span> + <span className="text-[var(--brand)]">{open ? "–" : "+"}</span> Customize </button> @@ -118,7 +118,7 @@ export function CustomizePanel({ className={ "rounded-full border px-3 py-1.5 text-[0.8125rem] font-medium transition-all duration-150 " + (state.excludeTop - ? "border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--accent-strong)]" + ? "border-[var(--brand)] bg-[var(--brand-soft)] text-[var(--brand-strong)]" : "border-[var(--border)] text-[var(--muted-foreground)] hover:border-[var(--border-strong)] hover:text-[var(--foreground)]") } > diff --git a/homepage/app/components/Footer.tsx b/homepage/app/components/Footer.tsx @@ -1,4 +1,7 @@ -import { getSettings } from "yt-dlp-transcript-common/lib/settings"; +import { + getSettings, + sizeSocialSvg, +} from "yt-dlp-transcript-common/lib/settings"; import { resolveHomepageSocialLinks } from "yt-dlp-transcript-common/lib/homepage"; import { currentHomepage } from "../lib/homepage"; @@ -24,8 +27,8 @@ export default function Footer() { aria-label={link.label} target="_blank" rel="noopener noreferrer" - className="inline-block w-5 h-5 text-[var(--faint)] hover:text-[var(--accent)] transition-colors [&_svg]:w-full [&_svg]:h-full" - dangerouslySetInnerHTML={{ __html: link.svg }} + className="inline-block w-5 h-5 text-[var(--faint)] hover:text-[var(--brand)] transition-colors [&_svg]:w-full [&_svg]:h-full" + dangerouslySetInnerHTML={{ __html: sizeSocialSvg(link.svg) }} /> </li> ))} diff --git a/homepage/app/components/Header.tsx b/homepage/app/components/Header.tsx @@ -1,4 +1,6 @@ import Link from "next/link"; +import { ThemeToggle } from "yt-dlp-transcript-common/components/ThemeToggle"; +import { ThemeMenu } from "yt-dlp-transcript-common/components/ThemeMenu"; import { currentHomepage } from "../lib/homepage"; // The hub's header: a compact sticky bar with the brass mark + wordmark, the @@ -9,11 +11,15 @@ export default function Header() { <header className="sticky top-0 z-20 border-b border-[var(--border)] bg-[rgba(12,10,8,0.72)] backdrop-blur-md"> <div className="max-w-6xl mx-auto px-5 sm:px-6 flex h-14 items-center"> <Link href="/" className="group flex items-center gap-2.5"> - <span className="h-2.5 w-2.5 shrink-0 rotate-45 rounded-[3px] bg-[var(--accent)] shadow-[0_0_14px_var(--accent)] transition-colors group-hover:bg-[var(--accent-strong)]" /> + <span className="h-2.5 w-2.5 shrink-0 rotate-45 rounded-[3px] bg-[var(--brand)] shadow-[0_0_14px_var(--brand)] transition-colors group-hover:bg-[var(--brand-strong)]" /> <span className="font-display text-[1.35rem] font-semibold leading-none tracking-tight text-[var(--foreground)]"> {headerTitle} </span> </Link> + <div className="ml-auto flex items-center gap-2"> + <ThemeMenu /> + <ThemeToggle /> + </div> </div> </header> ); diff --git a/homepage/app/components/KpiHeader.tsx b/homepage/app/components/KpiHeader.tsx @@ -14,7 +14,7 @@ function Stat({ className="reveal group relative flex flex-col gap-1 overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--panel)] px-4 py-4 backdrop-blur-sm transition-colors hover:border-[var(--border-strong)]" style={{ "--d": `${delay}ms` } as React.CSSProperties} > - <span className="absolute left-0 top-0 h-full w-px bg-gradient-to-b from-[var(--accent)] to-transparent opacity-60" /> + <span className="absolute left-0 top-0 h-full w-px bg-gradient-to-b from-[var(--brand)] to-transparent opacity-60" /> <span className="font-display text-[2rem] sm:text-[2.5rem] font-semibold leading-none tracking-[-0.01em] text-[var(--foreground)] tabular-nums"> {value.toLocaleString()} </span> diff --git a/homepage/app/components/PresetBar.tsx b/homepage/app/components/PresetBar.tsx @@ -30,7 +30,7 @@ export function PresetBar({ className={ "rounded-full border px-4 py-2 text-sm font-medium transition-all duration-150 " + (on - ? "border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--accent-strong)]" + ? "border-[var(--brand)] bg-[var(--brand-soft)] text-[var(--brand-strong)]" : "border-[var(--border)] text-[var(--muted-foreground)] hover:border-[var(--border-strong)] hover:text-[var(--foreground)]") } > diff --git a/homepage/app/components/Segmented.tsx b/homepage/app/components/Segmented.tsx @@ -35,7 +35,7 @@ export function Segmented<T extends string>({ className={ "rounded-full px-3 py-1.5 text-[0.8125rem] font-medium transition-all duration-150 " + (active - ? "bg-[var(--accent-soft)] text-[var(--accent-strong)] shadow-[inset_0_0_0_1px_var(--accent-soft)]" + ? "bg-[var(--brand-soft)] text-[var(--brand-strong)] shadow-[inset_0_0_0_1px_var(--brand-soft)]" : disabled ? "cursor-not-allowed text-[var(--faint)] opacity-50" : "text-[var(--muted-foreground)] hover:text-[var(--foreground)]") diff --git a/homepage/app/components/SiteGrid.tsx b/homepage/app/components/SiteGrid.tsx @@ -88,7 +88,7 @@ function SiteCard({ </h3> </div> {isLeader && ( - <span className="shrink-0 rounded-full border border-[var(--accent-soft)] bg-[var(--accent-soft)] px-2 py-0.5 font-mono text-[0.625rem] uppercase tracking-[0.1em] text-[var(--accent-strong)]"> + <span className="shrink-0 rounded-full border border-[var(--brand-soft)] bg-[var(--brand-soft)] px-2 py-0.5 font-mono text-[0.625rem] uppercase tracking-[0.1em] text-[var(--brand-strong)]"> #1 this month </span> )} @@ -106,7 +106,7 @@ function SiteCard({ <span className="font-mono text-[0.625rem] uppercase tracking-[0.12em] text-[var(--faint)]"> {noun} {stat.thisMonth > 0 && ( - <span className="ml-1.5 text-[var(--accent)]"> + <span className="ml-1.5 text-[var(--brand)]"> +{stat.thisMonth.toLocaleString()} mo. </span> )} diff --git a/homepage/app/globals.css b/homepage/app/globals.css @@ -1,59 +1,11 @@ @import "tailwindcss"; +@import "../../common/styles/tokens.css"; @source "../../common/components"; -/* The hub is committed to one deliberate aesthetic — a warm-ink "archive room". - Force the dark variant onto a class so it never flips with the OS setting. */ -@custom-variant dark (&:where(.dark, .dark *)); - -:root { - /* Warm near-black ground + aged-paper ink. */ - --background: #0c0a08; - --surface: #141009; - --foreground: #efe7d8; - /* Hairlines + muted speech. */ - --border: rgba(233, 220, 197, 0.1); - --border-strong: rgba(233, 220, 197, 0.18); - --muted: #1c1710; - --muted-foreground: #a39a86; - --faint: #6f6757; - /* Glass panels. */ - --panel: rgba(28, 23, 15, 0.55); - --panel-2: rgba(40, 33, 21, 0.5); - /* Brass signature accent. */ - --accent: #e3b15c; - --accent-strong: #f3c977; - --accent-soft: rgba(227, 177, 92, 0.14); - --accent-ink: #1a1305; - /* Series palette, tuned to glow on ink. */ - --chart-1: #6ea8ff; - --chart-2: #54d6a0; - --chart-3: #f2935b; - --chart-4: #c08cf0; - --chart-5: #f178b6; - /* "Instrument panel": an opaque, slightly-lifted plot surface that occludes - the page glow/grain for maximum data contrast, plus crisp axis/grid inks. */ - --chart-surface: #16110a; - --chart-grid: rgba(233, 220, 197, 0.08); - --chart-axis: #8a8170; - --chart-tooltip-bg: #1b150d; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-border: var(--border); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --font-sans: var(--font-source-sans); - --font-mono: var(--font-source-code); - --font-display: var(--font-source-serif); -} +/* The hub is committed to the warm-ink "archive" family (it defaults to + data-theme="archive" + dark). Design tokens, the `dark` variant, and the + theme palettes now live in common/styles/tokens.css; only the homepage's + atmosphere + bespoke layer styles remain here. */ html { background-color: var(--background); @@ -61,7 +13,7 @@ html { body { color: var(--foreground); - font-family: var(--font-source-sans), ui-sans-serif, system-ui, sans-serif; + font-family: var(--font-sans), ui-sans-serif, system-ui, sans-serif; -webkit-font-smoothing: antialiased; } @@ -105,7 +57,7 @@ body::after { .panel-instrument { background: var(--chart-surface); border: 1px solid var(--border-strong); - border-top-color: var(--accent-soft); + border-top-color: var(--brand-soft); box-shadow: inset 0 1px 0 rgba(233, 220, 197, 0.04); } @@ -149,7 +101,7 @@ body::after { .rule-accent { background: linear-gradient( 90deg, - var(--accent) 0%, + var(--brand) 0%, rgba(227, 177, 92, 0) 70% ); } diff --git a/homepage/app/layout.tsx b/homepage/app/layout.tsx @@ -1,28 +1,12 @@ import type { Metadata } from "next"; -import { Source_Serif_4, Source_Sans_3, Source_Code_Pro } from "next/font/google"; +import { fontVars } from "yt-dlp-transcript-common/styles/fonts"; +import { ThemeScript } from "yt-dlp-transcript-common/components/ThemeScript"; +import { ThemeProvider } from "yt-dlp-transcript-common/components/ThemeProvider"; import { currentHomepage } from "./lib/homepage"; import Header from "./components/Header"; import Footer from "./components/Footer"; import "./globals.css"; -// Adobe Source superfamily — one coherent voice across three roles. -// Serif: the wordmark + headline numerals. Sans: body/UI. Code: tracked labels. -const sourceSerif = Source_Serif_4({ - variable: "--font-source-serif", - subsets: ["latin"], - style: ["normal", "italic"], -}); - -const sourceSans = Source_Sans_3({ - variable: "--font-source-sans", - subsets: ["latin"], -}); - -const sourceCode = Source_Code_Pro({ - variable: "--font-source-code", - subsets: ["latin"], -}); - export function generateMetadata(): Metadata { const c = currentHomepage(); return { @@ -42,14 +26,18 @@ export default function RootLayout({ return ( <html lang="en" - className={`dark ${sourceSerif.variable} ${sourceSans.variable} ${sourceCode.variable} h-full antialiased`} + suppressHydrationWarning + className={`${fontVars} h-full antialiased`} > - <body className="min-h-full flex flex-col bg-[var(--background)] text-[var(--foreground)] font-sans selection:bg-[var(--accent-soft)] selection:text-[var(--accent-strong)]"> - <Header /> - <main className="flex-1 w-full max-w-6xl mx-auto px-5 sm:px-6 py-8 sm:py-12"> - {children} - </main> - <Footer /> + <body className="min-h-full flex flex-col bg-[var(--background)] text-[var(--foreground)] font-sans selection:bg-[var(--brand-soft)] selection:text-[var(--brand-strong)]"> + <ThemeScript defaultTheme="archive" defaultMode="dark" /> + <ThemeProvider defaultTheme="archive" defaultMode="dark"> + <Header /> + <main className="flex-1 w-full max-w-6xl mx-auto px-5 sm:px-6 py-8 sm:py-12"> + {children} + </main> + <Footer /> + </ThemeProvider> </body> </html> ); diff --git a/homepage/e2e/theme.spec.ts b/homepage/e2e/theme.spec.ts @@ -0,0 +1,50 @@ +import { test, expect, type Page } from "@playwright/test"; + +// Phase 0 foundation: the shared theme system (common/styles/tokens.css + +// ThemeScript + ThemeProvider + ThemeToggle). The homepage commits to the +// "archive" family and defaults to ink (dark); the mode toggle must persist +// across reloads and be applied before hydration (no flash of the wrong theme). + +async function htmlState(page: Page) { + return page.evaluate(() => ({ + dark: document.documentElement.classList.contains("dark"), + theme: document.documentElement.getAttribute("data-theme"), + mode: localStorage.getItem("ytdlp-tb:mode"), + })); +} + +test("archive ink default; mode toggle persists with no FOUC", async ({ + page, +}) => { + await page.goto("/"); + + // Default: archive family, ink (dark) mode. + let s = await htmlState(page); + expect(s.theme).toBe("archive"); + expect(s.dark).toBe(true); + + const toggle = page.getByRole("button", { name: /switch to/i }); + await expect(toggle).toBeVisible(); + + // Cycle to an explicit light mode (dark → system → light) — deterministic + // regardless of the runner's OS color-scheme preference. + for (let i = 0; i < 3; i++) { + s = await htmlState(page); + if (s.mode === "light") break; + await toggle.click(); + } + s = await htmlState(page); + expect(s.mode).toBe("light"); + expect(s.dark).toBe(false); + expect(s.theme).toBe("archive"); + + // Reload: the pre-paint inline script must re-apply the persisted mode on the + // very first commit, before React hydrates. + await page.reload({ waitUntil: "commit" }); + const onCommit = await page.evaluate(() => ({ + dark: document.documentElement.classList.contains("dark"), + mode: localStorage.getItem("ytdlp-tb:mode"), + })); + expect(onCommit.mode).toBe("light"); + expect(onCommit.dark).toBe(false); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml @@ -23,9 +23,15 @@ importers: '@tanstack/react-virtual': specifier: ^3.13.0 version: 3.13.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 clsx: specifier: ^2.1.1 version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) d3-scale: specifier: ^4.0.2 version: 4.0.2 @@ -50,6 +56,9 @@ importers: p-limit: specifier: ^7.3.0 version: 7.3.0 + radix-ui: + specifier: ^1.6.0 + version: 1.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: 19.2.4 version: 19.2.4 @@ -62,9 +71,15 @@ importers: recharts: specifier: 2.15.4 version: 2.15.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwind-merge: specifier: ^3.6.0 version: 3.6.0 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 devDependencies: '@types/d3-scale': specifier: ^4.0.9 @@ -93,6 +108,9 @@ importers: '@sindresorhus/slugify': specifier: ^3.0.0 version: 3.0.0 + lucide-react: + specifier: ^1.16.0 + version: 1.16.0(react@19.2.4) markdown-to-jsx: specifier: ^7.7.4 version: 7.7.17(react@19.2.4) @@ -142,6 +160,9 @@ importers: '@tanstack/react-query': specifier: ^5.99.1 version: 5.100.5(react@19.2.4) + lucide-react: + specifier: ^1.16.0 + version: 1.16.0(react@19.2.4) next: specifier: 16.2.3 version: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -527,6 +548,21 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@harperfast/extended-iterable@1.0.3': resolution: {integrity: sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==} @@ -866,141 +902,831 @@ packages: engines: {node: '>=18'} hasBin: true - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@radix-ui/number@1.1.2': + resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} - '@sec-ant/readable-stream@0.4.1': - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@radix-ui/primitive@1.1.4': + resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} - '@sindresorhus/merge-streams@4.0.0': - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} + '@radix-ui/react-accessible-icon@1.1.10': + resolution: {integrity: sha512-TraSwZUqTcVbiDV2/RXzAXC7aeVVXchq0daPFZE7zAxYFaMzjOUggLOfQH9KFLgRizuwVKZO/crveV1eeO3/ZQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@sindresorhus/slugify@3.0.0': - resolution: {integrity: sha512-SCrKh1zS96q+CuH5GumHcyQEVPsM4Ve8oE0E6tw7AAhGq50K8ojbTUOQnX/j9Mhcv/AXiIsbCfquovyGOo5fGw==} - engines: {node: '>=20'} + '@radix-ui/react-accordion@1.2.14': + resolution: {integrity: sha512-iE8YB9nmTBH8zd73ofBISZ8JCzgMoMkATJr7qDwa6u5F1+7mTM81V6fa71jgZ65rpjVpecDf1vSnwIFP9Ly1zw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@sindresorhus/transliterate@2.3.1': - resolution: {integrity: sha512-gVaaGtKYMYAMmI8buULVH3A2TXVJ98QiwGwI7ddrWGuGidGC2uRt4FHs22+8iROJ0QTzju9CuMjlVsrvpqsdhA==} - engines: {node: '>=20'} + '@radix-ui/react-alert-dialog@1.1.17': + resolution: {integrity: sha512-563ygGeyWPrxyVCNp7OV4rE2aIXhFPknpFyo4wbDlcyMMPZ6ySh+zC5WTvY0ZFLgPTg/QB6tA8PyDQyJ2b4cPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@radix-ui/react-arrow@1.1.10': + resolution: {integrity: sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/node@4.2.4': - resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==} + '@radix-ui/react-aspect-ratio@1.1.10': + resolution: {integrity: sha512-kbI7NrqhDeuytYrq7JjAsoXczvL8wgj2tc1MyaYWm+50bMKHCHQtVWCryslx4cCpmCTTkBcwQckE4CmmGV2haQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/oxide-android-arm64@4.2.4': - resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [android] + '@radix-ui/react-avatar@1.2.0': + resolution: {integrity: sha512-am/CwltXtmtdtP+5FbYblYDnMa/zuKcMJP1i3/SJMDXXfj2mG+BTqLH2wucqeyyiQMursUtg/5cK+Nh2pCaSOA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/oxide-darwin-arm64@4.2.4': - resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [darwin] + '@radix-ui/react-checkbox@1.3.5': + resolution: {integrity: sha512-pREzrmNnVwGvYaBoM64huTRK7B3lrTRuwj8A9nwhPiEtMb+yudiWh6zWAqEtP0Dzd5+iBa1Ki7V1pCxV8ExMdA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/oxide-darwin-x64@4.2.4': - resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==} - engines: {node: '>= 20'} - cpu: [x64] - os: [darwin] + '@radix-ui/react-collapsible@1.1.14': + resolution: {integrity: sha512-9bT+FvifX1FK2Mj6UEsTdyu0cN3JaA3KdfhaBao+ONrYFy/pyOy3TU1TNw7iOk1o+0hOEq67RojlUUmoFGwxyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/oxide-freebsd-x64@4.2.4': - resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [freebsd] + '@radix-ui/react-collection@1.1.10': + resolution: {integrity: sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': - resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==} - engines: {node: '>= 20'} - cpu: [arm] - os: [linux] + '@radix-ui/react-compose-refs@1.1.3': + resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': - resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [glibc] + '@radix-ui/react-context-menu@2.3.1': + resolution: {integrity: sha512-XbrxS68W5dyiE4fAb96yvJwSVU5x66B20A99sD5Mk3xSWK/LqeOnx6TZnim1KieMjXS/CTFq8reOAjWxas2G8Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.2.4': - resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [musl] + '@radix-ui/react-context@1.1.4': + resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.2.4': - resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [glibc] + '@radix-ui/react-dialog@1.1.17': + resolution: {integrity: sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/oxide-linux-x64-musl@4.2.4': - resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [musl] + '@radix-ui/react-direction@1.1.2': + resolution: {integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@tailwindcss/oxide-wasm32-wasi@4.2.4': - resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib + '@radix-ui/react-dismissable-layer@1.1.13': + resolution: {integrity: sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': - resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] + '@radix-ui/react-dropdown-menu@2.1.18': + resolution: {integrity: sha512-PZGV82gFk0WltDRI//SsG28ZIjlo9ANTmoNYg0jLNzXXiDsAy5PkOOYQaVD1pPxY6t7gxffb1QMD6qaUvsBZdw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.2.4': - resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] + '@radix-ui/react-focus-guards@1.1.4': + resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@tailwindcss/oxide@4.2.4': - resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} - engines: {node: '>= 20'} + '@radix-ui/react-focus-scope@1.1.10': + resolution: {integrity: sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/postcss@4.2.4': - resolution: {integrity: sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==} + '@radix-ui/react-form@0.1.10': + resolution: {integrity: sha512-1NfuvctVtX4sU3Mmq/IdrR8UunxiCMiVg3A5UENKhFzxUBeOyaQQ+lmaQaV7Tc8cqvBKsJL3/KGBsixK0D8WFg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tanstack/query-core@5.100.5': - resolution: {integrity: sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==} + '@radix-ui/react-hover-card@1.1.17': + resolution: {integrity: sha512-GjZQIEANVkuuWeztlKz6QEHe31ZX2iDfHzcTMCQVZXC0JyQrgfKWSC+LOOEw6aVV64zyjzobIzSA4AU4eKWrHA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tanstack/react-query@5.100.5': - resolution: {integrity: sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==} + '@radix-ui/react-id@1.1.2': + resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} peerDependencies: - react: ^18 || ^19 + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@tanstack/react-virtual@3.13.24': - resolution: {integrity: sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==} + '@radix-ui/react-label@2.1.10': + resolution: {integrity: sha512-ib0zvq2ZsAqKm5tRnqGJn3vOxSgIts5ToxsXT0q1S/GfLD1Zj7UOEnkw8u2w6sRmn47djpQWuSU1DCL1R29/yw==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tanstack/virtual-core@3.14.0': - resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==} + '@radix-ui/react-menu@2.1.18': + resolution: {integrity: sha512-lj8Rxjtn6zJq1oSbE/uDtAwCbB9BnxgHD+8MwJMuTh6u1dPamYhW9iuELr/Z8d0D/UysFblYYHeBPwi7T4k0YQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@radix-ui/react-menubar@1.1.18': + resolution: {integrity: sha512-hX7EGx/oFq6DPY27GQuP/2wP48GHf5LG6r06VgNJlG+znmDS8OfopZcRcGly3L4lsB9FqpmLx6JQSE9P3BUpyw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@types/d3-array@3.2.2': - resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + '@radix-ui/react-navigation-menu@1.2.16': + resolution: {integrity: sha512-nJ0SkrSQgudyYhMiYeHA1ayLVuduEJCFLan1RZZN7c9kqzzCFLaU9kuy81uNtqzweM9YaQPgWzxi9MwQ9jZ04g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.10': + resolution: {integrity: sha512-GHkcJ+WVj91At+OvUVTD4R3W0/wxw9t/sG5xFUBYXaCbtWiooZX5Md376QjJqgH4VsVyXrbVNHO2O4NYcmjfVg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.5': + resolution: {integrity: sha512-fVuA82u0b/fClpbEJv8yp1nU9eSvoSEOERsU/hhf3FXGPIvkmE7oEaHEu8poowoXO39/Va7zq2E0TUcYr1dBRg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.17': + resolution: {integrity: sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.3.1': + resolution: {integrity: sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.12': + resolution: {integrity: sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.6': + resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.6': + resolution: {integrity: sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.10': + resolution: {integrity: sha512-JYzEg60lk79PwKM27WZyKd7PW8O4OM5jOaFfRPfOyeXmMw7tLJh5kSj+CEjVTehszuwml/AdCzPGMXBTGf4BBw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.4.1': + resolution: {integrity: sha512-/SSxZdKEo2Eo29FFRKd06EfFDYp8HryKg0WYg7QLXaydPzl52YfSvCH2a3QDBRdtcuwACroJT8UVjQVgOJ7P9A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.13': + resolution: {integrity: sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.12': + resolution: {integrity: sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.3.1': + resolution: {integrity: sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.10': + resolution: {integrity: sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.4.1': + resolution: {integrity: sha512-r91WSpQucNGFKAIxT8FT0H0zyjd5tJlqObLp7LOMV4z49KoDCwjy01w3vDOU4e1wxhF9IgjYco7SB6byOW7Buw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.3.0': + resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.3.1': + resolution: {integrity: sha512-55bQtCnOB0BohomSHi6qvQXpJEEqUGDm6hRrM0Bph5OXwhSegqkd8IqgBAQkM1IlgUlWZIxpxRcpOEfRIgimyw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.15': + resolution: {integrity: sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.17': + resolution: {integrity: sha512-uL4kyyWy000pPL43fGGCV5qT6ZchCWEQZOSlkYiPwPt8Hy1iW38RjeptIvz1/SZesrW6Vn58Ct3sV7tfEfiAbw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.13': + resolution: {integrity: sha512-Xb9PLtlvU66F36LiKba6dFswu6V2mDkgidO4fNSbQHQwmZ9ObxMIO17MN/LJ4aWJecVuSVLAHPZjyeMzJrgeiA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.12': + resolution: {integrity: sha512-AsAVsYNZIlRBsci7BhE+QyQeKd1h6TffJYt+lF0QQkd5OpQ3klfIByPsCb4G0h/Fq6PJwh1FYNluzBFYzhk4+w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.13': + resolution: {integrity: sha512-Za1l4f6fzTkGgz/iynAMN8iaqiKff2wm2/QwiLmHPtDQreWEBrvSimgQFIekxMUdRPhILM7xdIXxuS/o/DGZag==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.10': + resolution: {integrity: sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.2': + resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.3': + resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.3': + resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.2': + resolution: {integrity: sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.1': + resolution: {integrity: sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.2': + resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.2': + resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.2': + resolution: {integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.2': + resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.6': + resolution: {integrity: sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.2': + resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@sindresorhus/slugify@3.0.0': + resolution: {integrity: sha512-SCrKh1zS96q+CuH5GumHcyQEVPsM4Ve8oE0E6tw7AAhGq50K8ojbTUOQnX/j9Mhcv/AXiIsbCfquovyGOo5fGw==} + engines: {node: '>=20'} + + '@sindresorhus/transliterate@2.3.1': + resolution: {integrity: sha512-gVaaGtKYMYAMmI8buULVH3A2TXVJ98QiwGwI7ddrWGuGidGC2uRt4FHs22+8iROJ0QTzju9CuMjlVsrvpqsdhA==} + engines: {node: '>=20'} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.2.4': + resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==} + + '@tailwindcss/oxide-android-arm64@4.2.4': + resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.4': + resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.4': + resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.2.4': + resolution: {integrity: sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==} + + '@tanstack/query-core@5.100.5': + resolution: {integrity: sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==} + + '@tanstack/react-query@5.100.5': + resolution: {integrity: sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-virtual@3.13.24': + resolution: {integrity: sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/virtual-core@3.14.0': + resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} @@ -1261,6 +1987,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -1391,6 +2121,9 @@ packages: resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -1406,6 +2139,12 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1548,6 +2287,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1850,6 +2592,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -2543,6 +3289,19 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + radix-ui@1.6.0: + resolution: {integrity: sha512-EUEC70O03EgxWMP5aoqfBZ6iLC5bczFagGy7zhSYRt8o5DP7IWNiP3ywetse3L9b8843ExB0OGWZvgbYVJuNeg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + range-parser@1.2.0: resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} engines: {node: '>= 0.6'} @@ -2570,12 +3329,42 @@ packages: peerDependencies: react: '>=16.6.0' + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-smooth@4.0.4: resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-transition-group@4.4.5: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -2642,816 +3431,1606 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + serve-handler@6.1.7: + resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==} + + serve@14.2.6: + resolution: {integrity: sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==} + engines: {node: '>= 14'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss@4.2.4: + resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.59.0: + resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} - serve-handler@6.1.7: - resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==} + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - serve@14.2.6: - resolution: {integrity: sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==} - engines: {node: '>= 14'} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} + update-check@1.5.4: + resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + weak-lru-cache@1.2.2: + resolution: {integrity: sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} engines: {node: '>= 0.4'} - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} + widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - stable-hash@0.0.5: - resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} - string.prototype.includes@2.0.1: - resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} - engines: {node: '>= 0.4'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} - string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} - engines: {node: '>= 0.4'} + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 - string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} +snapshots: - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} + '@alloc/quick-lru@5.2.0': {} - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + '@babel/compat-data@7.29.0': {} - strip-ansi@7.2.0: - resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} - engines: {node: '>=12'} + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 - strip-final-newline@4.0.0: - resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} - engines: {node: '>=18'} + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true - styled-jsx@5.1.6: - resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} + '@esbuild/aix-ppc64@0.27.7': + optional: true - tailwind-merge@3.6.0: - resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + '@esbuild/android-arm64@0.27.7': + optional: true - tailwindcss@4.2.4: - resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} + '@esbuild/android-arm@0.27.7': + optional: true - tapable@2.3.3: - resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} - engines: {node: '>=6'} + '@esbuild/android-x64@0.27.7': + optional: true - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + '@esbuild/darwin-arm64@0.27.7': + optional: true - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} + '@esbuild/darwin-x64@0.27.7': + optional: true - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + '@esbuild/freebsd-arm64@0.27.7': + optional: true - ts-api-utils@2.5.0: - resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' + '@esbuild/freebsd-x64@0.27.7': + optional: true - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + '@esbuild/linux-arm64@0.27.7': + optional: true - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + '@esbuild/linux-arm@0.27.7': + optional: true - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} - engines: {node: '>=18.0.0'} - hasBin: true + '@esbuild/linux-ia32@0.27.7': + optional: true - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} + '@esbuild/linux-loong64@0.27.7': + optional: true - type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} + '@esbuild/linux-mips64el@0.27.7': + optional: true - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} + '@esbuild/linux-ppc64@0.27.7': + optional: true - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} + '@esbuild/linux-riscv64@0.27.7': + optional: true - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} + '@esbuild/linux-s390x@0.27.7': + optional: true - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} + '@esbuild/linux-x64@0.27.7': + optional: true - typescript-eslint@8.59.0: - resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' + '@esbuild/netbsd-arm64@0.27.7': + optional: true - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true + '@esbuild/netbsd-x64@0.27.7': + optional: true - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} + '@esbuild/openbsd-arm64@0.27.7': + optional: true - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + '@esbuild/openbsd-x64@0.27.7': + optional: true - unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} + '@esbuild/openharmony-arm64@0.27.7': + optional: true - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} + '@esbuild/sunos-x64@0.27.7': + optional: true - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + '@esbuild/win32-arm64@0.27.7': + optional: true - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' + '@esbuild/win32-ia32@0.27.7': + optional: true - update-check@1.5.4: - resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} + '@esbuild/win32-x64@0.27.7': + optional: true - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + dependencies: + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} + '@eslint-community/regexpp@4.12.2': {} - victory-vendor@36.9.2: - resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color - weak-lru-cache@1.2.2: - resolution: {integrity: sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==} + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} + '@eslint/js@9.39.4': {} - which-typed-array@1.1.20: - resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} - engines: {node: '>= 0.4'} + '@eslint/object-schema@2.1.7': {} - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 - widest-line@4.0.1: - resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} - engines: {node: '>=12'} + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} + '@floating-ui/utils@0.2.11': {} - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + '@harperfast/extended-iterable@1.0.3': {} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 - yocto-queue@1.2.2: - resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} - engines: {node: '>=12.20'} + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 - yoctocolors@2.1.2: - resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} - engines: {node: '>=18'} + '@humanfs/types@0.15.0': {} - zod-validation-error@4.0.2: - resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.25.0 || ^4.0.0 + '@humanwhocodes/module-importer@1.0.1': {} - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + '@humanwhocodes/retry@0.4.3': {} -snapshots: + '@img/colour@1.1.0': + optional: true - '@alloc/quick-lru@5.2.0': {} + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true - '@babel/compat-data@7.29.0': {} + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true - '@babel/helper-compilation-targets@7.28.6': - dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.2 - lru-cache: 5.1.1 - semver: 6.3.1 + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true - '@babel/helper-globals@7.28.0': {} + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true - '@babel/helper-string-parser@7.27.1': {} + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true - '@babel/helper-validator-identifier@7.28.5': {} + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true - '@babel/helper-validator-option@7.27.1': {} + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true - '@babel/helpers@7.29.2': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true - '@babel/parser@7.29.2': - dependencies: - '@babel/types': 7.29.0 + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true - '@babel/runtime@7.29.7': {} + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - '@emnapi/wasi-threads@1.2.1': + '@img/sharp-wasm32@0.34.5': dependencies: - tslib: 2.8.1 + '@emnapi/runtime': 1.10.0 optional: true - '@esbuild/aix-ppc64@0.27.7': + '@img/sharp-win32-arm64@0.34.5': optional: true - '@esbuild/android-arm64@0.27.7': + '@img/sharp-win32-ia32@0.34.5': optional: true - '@esbuild/android-arm@0.27.7': + '@img/sharp-win32-x64@0.34.5': optional: true - '@esbuild/android-x64@0.27.7': - optional: true + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - '@esbuild/darwin-arm64@0.27.7': + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lmdb/lmdb-darwin-arm64@3.5.4': optional: true - '@esbuild/darwin-x64@0.27.7': + '@lmdb/lmdb-darwin-x64@3.5.4': optional: true - '@esbuild/freebsd-arm64@0.27.7': + '@lmdb/lmdb-linux-arm64@3.5.4': optional: true - '@esbuild/freebsd-x64@0.27.7': + '@lmdb/lmdb-linux-arm@3.5.4': optional: true - '@esbuild/linux-arm64@0.27.7': + '@lmdb/lmdb-linux-x64@3.5.4': optional: true - '@esbuild/linux-arm@0.27.7': + '@lmdb/lmdb-win32-arm64@3.5.4': optional: true - '@esbuild/linux-ia32@0.27.7': + '@lmdb/lmdb-win32-x64@3.5.4': optional: true - '@esbuild/linux-loong64@0.27.7': + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true - '@esbuild/linux-mips64el@0.27.7': + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': optional: true - '@esbuild/linux-ppc64@0.27.7': + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': optional: true - '@esbuild/linux-riscv64@0.27.7': + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': optional: true - '@esbuild/linux-s390x@0.27.7': + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': optional: true - '@esbuild/linux-x64@0.27.7': + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true - '@esbuild/netbsd-arm64@0.27.7': + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 optional: true - '@esbuild/netbsd-x64@0.27.7': + '@next/env@16.2.3': {} + + '@next/eslint-plugin-next@16.2.3': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@16.2.3': optional: true - '@esbuild/openbsd-arm64@0.27.7': + '@next/swc-darwin-x64@16.2.3': optional: true - '@esbuild/openbsd-x64@0.27.7': + '@next/swc-linux-arm64-gnu@16.2.3': optional: true - '@esbuild/openharmony-arm64@0.27.7': + '@next/swc-linux-arm64-musl@16.2.3': optional: true - '@esbuild/sunos-x64@0.27.7': + '@next/swc-linux-x64-gnu@16.2.3': optional: true - '@esbuild/win32-arm64@0.27.7': + '@next/swc-linux-x64-musl@16.2.3': optional: true - '@esbuild/win32-ia32@0.27.7': + '@next/swc-win32-arm64-msvc@16.2.3': optional: true - '@esbuild/win32-x64@0.27.7': + '@next/swc-win32-x64-msvc@16.2.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + '@nodelib/fs.scandir@2.1.5': dependencies: - eslint: 9.39.4(jiti@2.6.1) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 - '@eslint/config-array@0.21.2': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3 - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color + '@nodelib/fs.stat@2.0.5': {} - '@eslint/config-helpers@0.4.2': + '@nodelib/fs.walk@1.2.8': dependencies: - '@eslint/core': 0.17.0 + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 + '@nolyfill/is-core-module@1.0.39': {} - '@eslint/eslintrc@3.3.5': + '@playwright/test@1.59.1': dependencies: - ajv: 6.15.0 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color + playwright: 1.59.1 - '@eslint/js@9.39.4': {} + '@radix-ui/number@1.1.2': {} - '@eslint/object-schema@2.1.7': {} + '@radix-ui/primitive@1.1.4': {} - '@eslint/plugin-kit@0.4.1': + '@radix-ui/react-accessible-icon@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 + '@radix-ui/react-visually-hidden': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-accordion@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collapsible': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@harperfast/extended-iterable@1.0.3': {} + '@radix-ui/react-alert-dialog@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dialog': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@humanfs/core@0.19.2': + '@radix-ui/react-arrow@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@humanfs/types': 0.15.0 + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@humanfs/node@0.16.8': + '@radix-ui/react-aspect-ratio@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@humanfs/core': 0.19.2 - '@humanfs/types': 0.15.0 - '@humanwhocodes/retry': 0.4.3 + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@humanfs/types@0.15.0': {} + '@radix-ui/react-avatar@1.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-checkbox@1.3.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collapsible@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@humanwhocodes/module-importer@1.0.1': {} + '@radix-ui/react-collection@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@humanwhocodes/retry@0.4.3': {} + '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@img/colour@1.1.0': - optional: true + '@radix-ui/react-context-menu@2.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-darwin-arm64@0.34.5': + '@radix-ui/react-context@1.1.4(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true + '@types/react': 19.2.14 - '@img/sharp-darwin-x64@0.34.5': + '@radix-ui/react-dialog@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true + '@radix-ui/react-direction@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true + '@radix-ui/react-dismissable-layer@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true + '@radix-ui/react-dropdown-menu@2.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true + '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true + '@radix-ui/react-focus-scope@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true + '@radix-ui/react-form@0.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-label': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-hover-card@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true + '@radix-ui/react-id@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true + '@radix-ui/react-label@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menu@2.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menubar@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-navigation-menu@1.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-one-time-password-field@0.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-password-toggle-field@0.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-rect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/rect': 1.1.2 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true + '@radix-ui/react-portal@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true + '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-linux-arm64@0.34.5': + '@radix-ui/react-primitive@2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-linux-arm@0.34.5': + '@radix-ui/react-progress@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-radio-group@1.4.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-scroll-area@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-linux-ppc64@0.34.5': + '@radix-ui/react-separator@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true - - '@img/sharp-linux-riscv64@0.34.5': + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.4.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-linux-s390x@0.34.5': + '@radix-ui/react-slot@1.3.0(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true + '@types/react': 19.2.14 - '@img/sharp-linux-x64@0.34.5': + '@radix-ui/react-switch@1.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-arm64@0.34.5': + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tabs@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-x64@0.34.5': + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toast@1.2.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@img/sharp-wasm32@0.34.5': + '@radix-ui/react-toggle-group@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@emnapi/runtime': 1.10.0 - optional: true - - '@img/sharp-win32-arm64@0.34.5': - optional: true - - '@img/sharp-win32-ia32@0.34.5': - optional: true - - '@img/sharp-win32-x64@0.34.5': - optional: true + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@jridgewell/gen-mapping@0.3.13': + '@radix-ui/react-toggle@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@jridgewell/remapping@2.3.5': + '@radix-ui/react-toolbar@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tooltip@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@jridgewell/trace-mapping@0.3.31': + '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@lmdb/lmdb-darwin-arm64@3.5.4': - optional: true - - '@lmdb/lmdb-darwin-x64@3.5.4': - optional: true - - '@lmdb/lmdb-linux-arm64@3.5.4': - optional: true - - '@lmdb/lmdb-linux-arm@3.5.4': - optional: true - - '@lmdb/lmdb-linux-x64@3.5.4': - optional: true - - '@lmdb/lmdb-win32-arm64@3.5.4': - optional: true - - '@lmdb/lmdb-win32-x64@3.5.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - optional: true + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@napi-rs/wasm-runtime@0.2.12': + '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@next/env@16.2.3': {} + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@next/eslint-plugin-next@16.2.3': + '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.14)(react@19.2.4)': dependencies: - fast-glob: 3.3.1 - - '@next/swc-darwin-arm64@16.2.3': - optional: true - - '@next/swc-darwin-x64@16.2.3': - optional: true - - '@next/swc-linux-arm64-gnu@16.2.3': - optional: true - - '@next/swc-linux-arm64-musl@16.2.3': - optional: true - - '@next/swc-linux-x64-gnu@16.2.3': - optional: true - - '@next/swc-linux-x64-musl@16.2.3': - optional: true + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@next/swc-win32-arm64-msvc@16.2.3': - optional: true + '@radix-ui/react-use-escape-keydown@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@next/swc-win32-x64-msvc@16.2.3': - optional: true + '@radix-ui/react-use-is-hydrated@0.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@nodelib/fs.scandir@2.1.5': + '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@nodelib/fs.stat@2.0.5': {} + '@radix-ui/react-use-previous@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@nodelib/fs.walk@1.2.8': + '@radix-ui/react-use-rect@1.1.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + '@radix-ui/rect': 1.1.2 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@nolyfill/is-core-module@1.0.39': {} + '@radix-ui/react-use-size@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@playwright/test@1.59.1': + '@radix-ui/react-visually-hidden@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - playwright: 1.59.1 + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.2': {} '@rtsao/scc@1.1.0': {} @@ -3802,6 +5381,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -3959,6 +5542,10 @@ snapshots: chalk@5.0.1: {} + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + cli-boxes@3.0.0: {} client-only@0.0.1: {} @@ -3971,6 +5558,18 @@ snapshots: clsx@2.1.1: {} + cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dialog': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4099,6 +5698,8 @@ snapshots: detect-libc@2.1.2: {} + detect-node-es@1.1.0: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -4640,6 +6241,8 @@ snapshots: hasown: 2.0.3 math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -5276,6 +6879,69 @@ snapshots: queue-microtask@1.2.3: {} + radix-ui@1.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-accessible-icon': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-accordion': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-alert-dialog': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-aspect-ratio': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-avatar': 1.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': 1.3.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collapsible': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context-menu': 2.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dialog': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dropdown-menu': 2.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-form': 0.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-hover-card': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-label': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menu': 2.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menubar': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-navigation-menu': 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-one-time-password-field': 0.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-password-toggle-field': 0.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popover': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-progress': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-radio-group': 1.4.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-scroll-area': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': 2.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slider': 1.4.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-switch': 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tabs': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toast': 1.2.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toolbar': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tooltip': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + range-parser@1.2.0: {} rc@1.2.8: @@ -5305,6 +6971,25 @@ snapshots: react: 19.2.4 react-fast-compare: 3.2.2 + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + react-smooth@4.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: fast-equals: 5.4.0 @@ -5313,6 +6998,14 @@ snapshots: react-dom: 19.2.4(react@19.2.4) react-transition-group: 4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + get-nonce: 1.0.1 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@babel/runtime': 7.29.7 @@ -5536,6 +7229,11 @@ snapshots: signal-exit@4.1.0: {} + sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + source-map-js@1.2.1: {} stable-hash@0.0.5: {} @@ -5675,6 +7373,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tw-animate-css@1.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -5779,6 +7479,21 @@ snapshots: dependencies: punycode: 2.3.1 + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + vary@1.1.2: {} victory-vendor@36.9.2: