From 2727fd6dff3b2356c20088cbce882982e0c2a20f Mon Sep 17 00:00:00 2001 From: Sajid Mehmood Date: Tue, 25 Nov 2025 13:13:28 -0500 Subject: [PATCH] Add Niteshift Dials SDK for runtime design prototyping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a complete design dials system that allows designers and PMs to adjust UI parameters at runtime without code changes. **Dials SDK (`packages/dials/`):** - useDynamicColor: Color values with design system integration - useDynamicSpacing: Spacing/padding/margin controls - useDynamicVariant: Discrete choice selections - useDynamicBoolean: Toggle/feature flag controls - useDynamicNumber: Numeric values with min/max/step - DialsOverlay: Compact Leva-inspired UI (Ctrl+D to toggle) - DialsProvider: React context for dial state management - Design manifest integration for design system tokens **App Integration:** - Added DialsProvider to app Providers - Example dials on WebsitePage (metrics bar, panels, navigation) - MetricCard component with adjustable typography dials - TypeScript manifest at src/config/niteshift-manifest.ts **Documentation:** - Comprehensive CLAUDE.md section on dials usage - Best practices for preserving original appearance - Examples for all dial types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 356 +++++ package.json | 2 + packages/dials/.eslintignore | 4 + packages/dials/.gitignore | 4 + packages/dials/package.json | 49 + packages/dials/src/__tests__/.eslintrc.json | 7 + packages/dials/src/__tests__/registry.test.ts | 224 ++++ packages/dials/src/__tests__/setup.ts | 10 + .../src/__tests__/useDynamicColor.test.tsx | 153 +++ .../dials/src/components/DialsOverlay.tsx | 384 ++++++ .../dials/src/components/DialsProvider.tsx | 58 + .../dials/src/controls/BooleanControl.tsx | 48 + packages/dials/src/controls/ColorControl.tsx | 152 +++ packages/dials/src/controls/NumberControl.tsx | 85 ++ .../dials/src/controls/SpacingControl.tsx | 57 + .../dials/src/controls/VariantControl.tsx | 65 + packages/dials/src/hooks/useDial.ts | 36 + packages/dials/src/hooks/useDynamicBoolean.ts | 26 + packages/dials/src/hooks/useDynamicColor.ts | 52 + packages/dials/src/hooks/useDynamicNumber.ts | 28 + packages/dials/src/hooks/useDynamicSpacing.ts | 44 + packages/dials/src/hooks/useDynamicVariant.ts | 29 + packages/dials/src/index.ts | 42 + packages/dials/src/registry.ts | 314 +++++ packages/dials/src/styles.css | 457 +++++++ packages/dials/src/types.ts | 178 +++ packages/dials/src/utils/manifest.ts | 149 +++ packages/dials/tsconfig.json | 25 + packages/dials/tsup.config.ts | 11 + packages/dials/vitest.config.ts | 11 + pnpm-lock.yaml | 1172 ++++++++++++++++- .../[websiteId]/WebsiteMetricsBar.tsx | 9 + .../websites/[websiteId]/WebsiteNav.tsx | 7 + .../websites/[websiteId]/WebsitePage.tsx | 123 +- .../websites/[websiteId]/WebsitePanels.tsx | 39 +- src/app/Providers.tsx | 7 +- src/app/layout.tsx | 1 + src/components/metrics/MetricCard.tsx | 26 +- src/config/niteshift-manifest.ts | 198 +++ 39 files changed, 4623 insertions(+), 19 deletions(-) create mode 100644 packages/dials/.eslintignore create mode 100644 packages/dials/.gitignore create mode 100644 packages/dials/package.json create mode 100644 packages/dials/src/__tests__/.eslintrc.json create mode 100644 packages/dials/src/__tests__/registry.test.ts create mode 100644 packages/dials/src/__tests__/setup.ts create mode 100644 packages/dials/src/__tests__/useDynamicColor.test.tsx create mode 100644 packages/dials/src/components/DialsOverlay.tsx create mode 100644 packages/dials/src/components/DialsProvider.tsx create mode 100644 packages/dials/src/controls/BooleanControl.tsx create mode 100644 packages/dials/src/controls/ColorControl.tsx create mode 100644 packages/dials/src/controls/NumberControl.tsx create mode 100644 packages/dials/src/controls/SpacingControl.tsx create mode 100644 packages/dials/src/controls/VariantControl.tsx create mode 100644 packages/dials/src/hooks/useDial.ts create mode 100644 packages/dials/src/hooks/useDynamicBoolean.ts create mode 100644 packages/dials/src/hooks/useDynamicColor.ts create mode 100644 packages/dials/src/hooks/useDynamicNumber.ts create mode 100644 packages/dials/src/hooks/useDynamicSpacing.ts create mode 100644 packages/dials/src/hooks/useDynamicVariant.ts create mode 100644 packages/dials/src/index.ts create mode 100644 packages/dials/src/registry.ts create mode 100644 packages/dials/src/styles.css create mode 100644 packages/dials/src/types.ts create mode 100644 packages/dials/src/utils/manifest.ts create mode 100644 packages/dials/tsconfig.json create mode 100644 packages/dials/tsup.config.ts create mode 100644 packages/dials/vitest.config.ts create mode 100644 src/config/niteshift-manifest.ts diff --git a/CLAUDE.md b/CLAUDE.md index b7851b75..32c64c64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -462,6 +462,362 @@ export default function MyPage() { 7. **TypeScript**: Leverage type inference - the codebase has strong typing 8. **Component library first**: Always check if `@umami/react-zen` has what you need before writing custom UI +## Niteshift Dials SDK + +**New capability for design prototyping!** The Dials SDK allows you to expose design parameters as runtime-adjustable "dials" that users can tweak via an overlay UI. This gives designers and PMs fine-grained control without requiring code changes. + +### Setup and Installation + +The Dials SDK lives in `packages/dials/` and is automatically built when you run `pnpm install` via the repo’s postinstall script. **Do not edit the SDK source or build pipeline unless the user explicitly instructs you to do so.** Your default interaction with dials should be importing the provided hooks inside app components. + +**Key files:** +- `packages/dials/src/` – SDK source **(hands-off unless asked)** +- `packages/dials/dist/` – Built package (gitignored, auto-generated) +- `src/config/niteshift-manifest.ts` – Umami’s design system manifest for reference in app code + +If—*and only if*—a user asks you to modify the SDK internals, rebuild with: +```bash +pnpm --filter @niteshift/dials build +``` + +### When to Use Dials + +Only add dials when the user explicitly says they want “dials” (or otherwise asks for adjustable controls). When they do, prioritize these scenarios: +- Subjective color/spacing/typography tweaks tied to design-system tokens +- Layout variants or experimental sections the user wants to tune live +- Feature toggles the user specifically calls out for dial-based control + +**Key principle**: Follow the user's direction—never invent dials on your own; create them only when requested, and let the user decide which values need adjustment. + +### Critical: Preserving Original Appearance + +**⚠️ EXTREMELY IMPORTANT: When adding dials to existing code, the defaults MUST ALWAYS preserve the exact original appearance.** + +Before adding dials to any component: + +1. **Document the original values** - Record what props/styles existed before dials +2. **Match defaults exactly** - Dial defaults must produce identical output to pre-dial code +3. **Use empty string for "no prop"** - If original had no prop, use `default: ''` not `'inherit'` or a value +4. **Conditionally spread props** - Only pass props when they have truthy values + +**Example - WRONG approach:** +```typescript +// Original code (before dials): +{label} // No size prop! + +// ❌ WRONG - adds size prop that wasn't there: +const labelSize = useDynamicVariant('label-size', { + default: '1', // ❌ Original had NO size, this changes appearance! + options: ['0', '1', '2', '3'] as const, +}); +{label} +``` + +**Example - CORRECT approach:** +```typescript +// Original code (before dials): +{label} // No size prop! + +// ✅ CORRECT - empty string means "no change": +const labelSize = useDynamicVariant('label-size', { + default: '', // ✅ Empty string = no size prop = matches original + options: ['', '0', '1', '2', '3'] as const, // First option is "default/none" +}); + +// ✅ CORRECT - only pass size if truthy: + + {label} + +``` + +**Why this matters:** +- Users expect dials at default = original appearance +- Dials should enable exploration, not force changes +- Breaking the original look confuses users and defeats the purpose + +**Testing your defaults:** +1. Add dials with defaults +2. View the page - should look IDENTICAL to before dials +3. Reset All in dials overlay - should look IDENTICAL to before dials +4. Only when adjusting dials should appearance change + +### Design System Manifest + +The design system manifest is defined in `src/config/niteshift-manifest.ts` as a TypeScript module: +- Colors (primary, base, accent, semantic) +- Spacing scale (4px to 128px) +- Typography (fonts, sizes, weights) +- Border radius, shadows + +**Benefits of TypeScript manifest:** +- Type-safe with full IDE autocomplete +- Bundled with app (not publicly accessible) +- No runtime HTTP fetch (faster) +- Hot reload compatible + +Reference these tokens in dial configs to provide users with design system-aligned options. + +### Manifest-Powered Defaults + +**Smart defaults from the design system!** When you omit the `options` parameter in color and spacing dials, the SDK automatically pulls values from the design manifest. This reduces boilerplate and ensures consistency with your design system. + +**Color dials (design system defaults)** +```typescript +import { useDynamicColor } from '@niteshift/dials'; + +const badgeColor = useDynamicColor('hero-badge-color', { + label: 'Hero Badge Color', + group: 'Hero Section', + default: 'var(--primary-color)', + manifestCategory: 'primary', // pulls tokens from designManifest.colors.primary + allowCustom: true, +}); + +return Top Performer; +``` + +**Spacing dials (design system defaults)** +```typescript +import { useDynamicSpacing } from '@niteshift/dials'; + +const cardPadding = useDynamicSpacing('hero-card-padding', { + label: 'Hero Card Padding', + group: 'Hero Section', + default: 'var(--spacing-5)', + manifestCategory: 'spacing', +}); + +return {children}; +``` + +// Manifest defaults (automatic - uses full spacing scale): +const margin2 = useDynamicSpacing('margin-2', { + label: 'Margin', + default: '24px', + // options omitted - uses designManifest.spacing.values (4px to 128px) +}); +``` + +**When to use manifest defaults:** +- ✅ You want design system consistency +- ✅ You're prototyping and want quick setup +- ✅ The default color category (accent) or spacing scale fits your needs +- ❌ You need a specific subset of values +- ❌ You're using custom values outside the design system + +### Available Dial Types + +#### Color Dials +For any color value (backgrounds, text, borders, etc.): +```typescript +import { useDynamicColor } from '@niteshift/dials'; + +const bgColor = useDynamicColor('hero-background', { + label: 'Hero Background Color', + description: 'Background color for the hero section', + group: 'Hero Section', + default: '#1a1a1a', + options: ['#1a1a1a', '#2d2d2d', '#404040', '#525252'], // From design system + allowCustom: true // Allows custom hex input +}); + +
...
+``` + +#### Spacing Dials +For padding, margin, gap, dimensions: +```typescript +import { useDynamicSpacing } from '@niteshift/dials'; + +const padding = useDynamicSpacing('card-padding', { + label: 'Card Padding', + group: 'Card Component', + default: '1.5rem', + options: ['0.5rem', '1rem', '1.5rem', '2rem', '3rem'], + allowCustom: true +}); + +
...
+``` + +#### Variant Dials +For discrete choices (layouts, styles, chart types): +```typescript +import { useDynamicVariant } from '@niteshift/dials'; + +const layout = useDynamicVariant('dashboard-layout', { + label: 'Dashboard Layout', + group: 'Dashboard', + default: 'grid', + options: ['grid', 'list', 'compact'] as const +}); + +{layout === 'grid' && } +{layout === 'list' && } +{layout === 'compact' && } +``` + +#### Boolean Dials +For toggles, feature flags, show/hide: +```typescript +import { useDynamicBoolean } from '@niteshift/dials'; + +const showDelta = useDynamicBoolean('show-metrics-delta', { + label: 'Show Change Indicators', + description: 'Display +/- changes in metrics', + default: true, + trueLabel: 'Visible', + falseLabel: 'Hidden', + group: 'Metrics Bar' +}); + +{showDelta && } +``` + +#### Number Dials +For numeric values with constraints: +```typescript +import { useDynamicNumber } from '@niteshift/dials'; + +const chartHeight = useDynamicNumber('chart-height', { + label: 'Chart Height', + default: 400, + min: 200, + max: 800, + step: 50, + unit: 'px', + options: [300, 400, 500, 600], // Preset options + group: 'Chart' +}); + + +``` + +### Advanced Use Cases + +**Layout Controls:** +```typescript +const columns = useDynamicVariant('metrics-columns', { + label: 'Metrics Layout', + default: '4', + options: ['2', '3', '4', '6'] as const, + group: 'Dashboard' +}); + + + {metrics.map(m => )} + +``` + +**Chart Type Selection:** +```typescript +const chartType = useDynamicVariant('analytics-chart', { + label: 'Visualization Type', + default: 'line', + options: ['line', 'bar', 'area'] as const, + group: 'Analytics' +}); + +{chartType === 'line' && } +{chartType === 'bar' && } +{chartType === 'area' && } +``` + +**Feature Flags:** +```typescript +const showSparklines = useDynamicBoolean('show-sparklines', { + label: 'Show Sparklines', + default: false, + group: 'Metrics Display' +}); + + +``` + +**Icon Selection:** +```typescript +const emptyIcon = useDynamicVariant('empty-state-icon', { + label: 'Empty State Icon', + default: 'inbox', + options: ['inbox', 'folder', 'archive', 'alert'] as const, + group: 'Empty States' +}); + +const icons = { inbox: , folder: , archive: , alert: }; + +``` + +### Best Practices + +1. **Use semantic IDs**: e.g., `'hero-background'` not `'color-1'` +2. **Provide design system options first**: Always include tokens from `.niteshift-manifest` +3. **Group related dials**: Use the `group` property to organize by component/section +4. **Add helpful labels**: Make labels clear for non-technical users +5. **Set sensible defaults**: Choose the best option; users can refine later +6. **Reference manifest colors**: Pull from design system categories: + ```typescript + // Colors from .niteshift-manifest + options: ['#147af3', '#2680eb', '#0090ff', '#3e63dd'] // Primary colors + ``` + +### Communicating with Users + +After creating dials, tell the user: +> "I've made [X, Y, Z] adjustable via design dials. Press **Ctrl+D** on macOS (use the Control key, not Command) or **Ctrl+Alt+D** on Windows/Linux to open the panel and fine-tune these values. You can select from design system options or enter custom values." + +### Accessing the Overlay + +- **Keyboard shortcut**: `Ctrl+D` on macOS, `Ctrl+Alt+D` on Windows/Linux toggles the dials overlay +- **Location**: Bottom-left floating panel +- **Persistence**: Visibility state and dial values persist across reloads (localStorage) +- **Features**: + - Search/filter dials + - Grouped by component/section + - Reset individual dials or all at once + - Keyboard shortcut hint shown in overlay header + +### Examples from Umami + +**WebsitePage with dynamic chart:** +```typescript +const chartHeight = useDynamicNumber('website-chart-height', { + label: 'Chart Height', + default: 520, + options: [400, 520, 640, 760], + allowCustom: true, + unit: 'px', + group: 'Website Analytics' +}); + + + + +``` + +**Dashboard with layout options:** +```typescript +const layout = useDynamicVariant('dashboard-layout', { + label: 'Board Layout', + default: 'grid', + options: ['grid', 'list', 'masonry'] as const, + group: 'Dashboard' +}); + +{layout === 'grid' && } +{layout === 'list' && } +{layout === 'masonry' && } +``` + +### Implementation Notes + +- Dials are already integrated into the app via `DialsProvider` in `src/app/Providers.tsx` +- The overlay (`DialsOverlay`) is automatically rendered +- Values are persisted to localStorage and survive hot reloads +- No additional setup required - just import and use the hooks! + ## Project Architecture ### Directory Structure diff --git a/package.json b/package.json index c6ae9002..48deaa17 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "change-password": "node scripts/change-password.js", "lint": "next lint --quiet", "prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky install", + "postinstall": "pnpm --filter @niteshift/dials build", "postbuild": "node scripts/postbuild.js", "test": "jest", "cypress-open": "cypress open cypress run", @@ -73,6 +74,7 @@ "@dicebear/core": "^9.2.3", "@fontsource/inter": "^5.2.8", "@hello-pangea/dnd": "^17.0.0", + "@niteshift/dials": "workspace:*", "@prisma/adapter-pg": "^6.18.0", "@prisma/client": "^6.18.0", "@prisma/extension-read-replicas": "^0.4.1", diff --git a/packages/dials/.eslintignore b/packages/dials/.eslintignore new file mode 100644 index 00000000..0afeda09 --- /dev/null +++ b/packages/dials/.eslintignore @@ -0,0 +1,4 @@ +*.test.ts +*.test.tsx +*.config.ts +__tests__/ diff --git a/packages/dials/.gitignore b/packages/dials/.gitignore new file mode 100644 index 00000000..e0fc267f --- /dev/null +++ b/packages/dials/.gitignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +*.log +.DS_Store diff --git a/packages/dials/package.json b/packages/dials/package.json new file mode 100644 index 00000000..619ee335 --- /dev/null +++ b/packages/dials/package.json @@ -0,0 +1,49 @@ +{ + "name": "@niteshift/dials", + "version": "0.1.0", + "description": "Runtime design parameter controls for rapid prototyping", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./styles.css": "./dist/styles.css" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "devDependencies": { + "@testing-library/react": "^14.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.2.0", + "jsdom": "^23.0.0", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^1.0.0" + }, + "keywords": [ + "design", + "prototyping", + "ai", + "controls", + "runtime-config" + ], + "author": "Niteshift", + "license": "MIT" +} diff --git a/packages/dials/src/__tests__/.eslintrc.json b/packages/dials/src/__tests__/.eslintrc.json new file mode 100644 index 00000000..4633e9c4 --- /dev/null +++ b/packages/dials/src/__tests__/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "rules": { + "import/no-unresolved": "off", + "@typescript-eslint/no-unused-vars": "off", + "no-unused-vars": "off" + } +} diff --git a/packages/dials/src/__tests__/registry.test.ts b/packages/dials/src/__tests__/registry.test.ts new file mode 100644 index 00000000..5531e60b --- /dev/null +++ b/packages/dials/src/__tests__/registry.test.ts @@ -0,0 +1,224 @@ +/** + * Tests for the dial registry + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { getDialRegistry } from '../registry'; +import type { DialConfig } from '../types'; + +describe('DialRegistry', () => { + let registry: ReturnType; + + beforeEach(() => { + // Get fresh registry instance + registry = getDialRegistry(); + // Clear all registered dials + registry.resetAll(); + localStorage.clear(); + }); + + describe('register', () => { + it('should register a new dial with default value', () => { + const config: DialConfig = { + type: 'color', + label: 'Test Color', + default: '#ff0000', + options: ['#ff0000', '#00ff00', '#0000ff'], + }; + + const value = registry.register('test-dial', 'color', config); + + expect(value).toBe('#ff0000'); + }); + + it('should return existing value for already registered dial', () => { + const config: DialConfig = { + type: 'color', + label: 'Test Color', + default: '#ff0000', + options: ['#ff0000', '#00ff00'], + }; + + const value1 = registry.register('test-dial', 'color', config); + const value2 = registry.register('test-dial', 'color', config); + + expect(value1).toBe(value2); + }); + }); + + describe('setValue', () => { + it('should set a new value and notify subscribers', () => { + const config: DialConfig = { + type: 'number', + label: 'Test Number', + default: 10, + min: 0, + max: 100, + }; + + registry.register('test-number', 'number', config); + + const callback = vi.fn(); + registry.subscribe('test-number', callback); + + registry.setValue('test-number', 50); + + expect(callback).toHaveBeenCalledWith('test-number', 50); + expect(registry.getValue('test-number')).toBe(50); + }); + + it('should persist value to localStorage', () => { + const config: DialConfig = { + type: 'variant', + label: 'Test Variant', + default: 'option1', + options: ['option1', 'option2', 'option3'], + }; + + registry.register('test-variant', 'variant', config); + registry.setValue('test-variant', 'option2'); + + const stored = localStorage.getItem('niteshift-dial-test-variant'); + expect(stored).toBe('"option2"'); + }); + }); + + describe('reset', () => { + it('should reset dial to default value', () => { + const config: DialConfig = { + type: 'boolean', + label: 'Test Boolean', + default: false, + }; + + registry.register('test-bool', 'boolean', config); + registry.setValue('test-bool', true); + + expect(registry.getValue('test-bool')).toBe(true); + + registry.reset('test-bool'); + + expect(registry.getValue('test-bool')).toBe(false); + }); + + it('should remove value from localStorage', () => { + const config: DialConfig = { + type: 'spacing', + label: 'Test Spacing', + default: '16px', + options: ['8px', '16px', '24px'], + }; + + registry.register('test-spacing', 'spacing', config); + registry.setValue('test-spacing', '24px'); + + expect(localStorage.getItem('niteshift-dial-test-spacing')).toBeTruthy(); + + registry.reset('test-spacing'); + + expect(localStorage.getItem('niteshift-dial-test-spacing')).toBeNull(); + }); + }); + + describe('resetAll', () => { + it('should reset all dials to defaults', () => { + registry.register('dial-1', 'color', { + type: 'color', + label: 'Color 1', + default: '#ff0000', + }); + registry.register('dial-2', 'number', { + type: 'number', + label: 'Number 1', + default: 10, + }); + + registry.setValue('dial-1', '#00ff00'); + registry.setValue('dial-2', 50); + + registry.resetAll(); + + expect(registry.getValue('dial-1')).toBe('#ff0000'); + expect(registry.getValue('dial-2')).toBe(10); + }); + }); + + describe('persistence', () => { + it('should load value from localStorage on registration', () => { + // Simulate existing localStorage value + localStorage.setItem('niteshift-dial-persisted', '"custom-value"'); + + const config: DialConfig = { + type: 'variant', + label: 'Persisted Dial', + default: 'default-value', + options: ['default-value', 'custom-value'], + }; + + const value = registry.register('persisted', 'variant', config); + + expect(value).toBe('custom-value'); + }); + + it('should handle corrupted localStorage gracefully', () => { + // Set invalid JSON in localStorage + localStorage.setItem('niteshift-dial-corrupted', 'invalid-json{'); + + const config: DialConfig = { + type: 'color', + label: 'Corrupted Dial', + default: '#ff0000', + }; + + const value = registry.register('corrupted', 'color', config); + + // Should fall back to default + expect(value).toBe('#ff0000'); + }); + }); + + describe('subscriptions', () => { + it('should notify multiple subscribers', () => { + const config: DialConfig = { + type: 'number', + label: 'Test Number', + default: 0, + }; + + registry.register('test-multi', 'number', config); + + const callback1 = vi.fn(); + const callback2 = vi.fn(); + + registry.subscribe('test-multi', callback1); + registry.subscribe('test-multi', callback2); + + registry.setValue('test-multi', 100); + + expect(callback1).toHaveBeenCalledWith('test-multi', 100); + expect(callback2).toHaveBeenCalledWith('test-multi', 100); + }); + + it('should unsubscribe correctly', () => { + const config: DialConfig = { + type: 'boolean', + label: 'Test Boolean', + default: false, + }; + + registry.register('test-unsub', 'boolean', config); + + const callback = vi.fn(); + const unsubscribe = registry.subscribe('test-unsub', callback); + + registry.setValue('test-unsub', true); + expect(callback).toHaveBeenCalledTimes(1); + + unsubscribe(); + + registry.setValue('test-unsub', false); + // Should still be 1 (not called again) + expect(callback).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/dials/src/__tests__/setup.ts b/packages/dials/src/__tests__/setup.ts new file mode 100644 index 00000000..e885e33a --- /dev/null +++ b/packages/dials/src/__tests__/setup.ts @@ -0,0 +1,10 @@ +/** + * Test setup file for Vitest + */ + +import { afterEach } from 'vitest'; + +// Clean up localStorage after each test +afterEach(() => { + localStorage.clear(); +}); diff --git a/packages/dials/src/__tests__/useDynamicColor.test.tsx b/packages/dials/src/__tests__/useDynamicColor.test.tsx new file mode 100644 index 00000000..88b174bc --- /dev/null +++ b/packages/dials/src/__tests__/useDynamicColor.test.tsx @@ -0,0 +1,153 @@ +/** + * Tests for useDynamicColor hook + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useDynamicColor } from '../hooks/useDynamicColor'; +import { getDialRegistry } from '../registry'; +import { DialsProvider } from '../components/DialsProvider'; +import type { DesignManifest } from '../types'; + +// Wrapper with DialsProvider +function createWrapper(manifest?: DesignManifest | null) { + return ({ children }: { children: React.ReactNode }) => ( + {children} + ); +} + +describe('useDynamicColor', () => { + let registry: ReturnType; + + beforeEach(() => { + registry = getDialRegistry(); + registry.resetAll(); + localStorage.clear(); + }); + + it('should return default color value', () => { + const { result } = renderHook( + () => + useDynamicColor('test-color', { + label: 'Test Color', + default: '#ff0000', + options: ['#ff0000', '#00ff00', '#0000ff'], + }), + { wrapper: createWrapper() }, + ); + + expect(result.current).toBe('#ff0000'); + }); + + it('should update when registry value changes', () => { + const { result } = renderHook( + () => + useDynamicColor('reactive-color', { + label: 'Reactive Color', + default: '#ff0000', + options: ['#ff0000', '#00ff00', '#0000ff'], + }), + { wrapper: createWrapper() }, + ); + + expect(result.current).toBe('#ff0000'); + + // Update via registry + act(() => { + registry.setValue('reactive-color', '#00ff00'); + }); + + expect(result.current).toBe('#00ff00'); + }); + + it('should use manifest defaults when options not provided', () => { + const manifest: DesignManifest = { + name: 'Test Manifest', + version: '1.0.0', + colors: { + accent: { + label: 'Accent Colors', + values: { + blue: '#0090ff', + green: '#30a46c', + red: '#e5484d', + }, + }, + }, + }; + + const { result } = renderHook( + () => + useDynamicColor('manifest-color', { + label: 'Manifest Color', + default: '#0090ff', + // No options - should use manifest + }), + { wrapper: createWrapper(manifest) }, + ); + + expect(result.current).toBe('#0090ff'); + + // Verify the dial was registered with manifest options + const dial = registry.getAllDials().find(d => d.id === 'manifest-color'); + expect(dial?.config.options).toEqual(['#0090ff', '#30a46c', '#e5484d']); + }); + + it('should prefer explicit options over manifest', () => { + const manifest: DesignManifest = { + name: 'Test Manifest', + version: '1.0.0', + colors: { + accent: { + label: 'Accent Colors', + values: ['#0090ff', '#30a46c'], + }, + }, + }; + + const explicitOptions = ['#ff0000', '#00ff00']; + + const { result } = renderHook( + () => + useDynamicColor('explicit-color', { + label: 'Explicit Color', + default: '#ff0000', + options: explicitOptions, + }), + { wrapper: createWrapper(manifest) }, + ); + + const dial = registry.getAllDials().find(d => d.id === 'explicit-color'); + expect(dial?.config.options).toEqual(explicitOptions); + }); + + it('should handle manifest category selection', () => { + const manifest: DesignManifest = { + name: 'Test Manifest', + version: '1.0.0', + colors: { + primary: { + label: 'Primary Colors', + values: ['#147af3', '#2680eb'], + }, + accent: { + label: 'Accent Colors', + values: ['#0090ff', '#30a46c'], + }, + }, + }; + + const { result } = renderHook( + () => + useDynamicColor('primary-color', { + label: 'Primary Color', + default: '#147af3', + manifestCategory: 'primary', + } as any), // manifestCategory is not in type yet, but handled in implementation + { wrapper: createWrapper(manifest) }, + ); + + const dial = registry.getAllDials().find(d => d.id === 'primary-color'); + expect(dial?.config.options).toEqual(['#147af3', '#2680eb']); + }); +}); diff --git a/packages/dials/src/components/DialsOverlay.tsx b/packages/dials/src/components/DialsOverlay.tsx new file mode 100644 index 00000000..a93a3da7 --- /dev/null +++ b/packages/dials/src/components/DialsOverlay.tsx @@ -0,0 +1,384 @@ +/** + * Main overlay UI component for displaying and controlling dials + */ + +import React, { useState, useEffect, useMemo } from 'react'; +import { Gauge } from 'lucide-react'; +import { getDialRegistry } from '../registry'; +import { ColorControl } from '../controls/ColorControl'; +import { SpacingControl } from '../controls/SpacingControl'; +import { VariantControl } from '../controls/VariantControl'; +import { BooleanControl } from '../controls/BooleanControl'; +import { NumberControl } from '../controls/NumberControl'; +import type { DialRegistration } from '../types'; + +export interface DialsOverlayProps { + /** Initial visibility state */ + defaultVisible?: boolean; + /** Position of the overlay */ + position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; +} + +/** + * Overlay UI for controlling dials + * Should be rendered at the root level of your app + * + * @example + * ```typescript + * + * + * + * + * ``` + */ +export function DialsOverlay({ + defaultVisible = true, + position = 'bottom-left', +}: DialsOverlayProps) { + // Load visibility state from localStorage (avoiding hydration mismatch) + const [isVisible, setIsVisible] = useState(defaultVisible); + + // Load from localStorage after mount to avoid hydration issues + useEffect(() => { + const stored = localStorage.getItem('niteshift-dials-visible'); + if (stored !== null) { + setIsVisible(stored === 'true'); + } + }, []); + const [searchTerm, setSearchTerm] = useState(''); + const [dials, setDials] = useState([]); + const [hasNextOverlay, setHasNextOverlay] = useState(false); + const [isMacLike, setIsMacLike] = useState(false); + const [shortcutLabel, setShortcutLabel] = useState('Ctrl+D (macOS) / Ctrl+Alt+D (Win/Linux)'); + const registry = getDialRegistry(); + + // Persist visibility state to localStorage + useEffect(() => { + localStorage.setItem('niteshift-dials-visible', String(isVisible)); + }, [isVisible]); + + // Detect Next.js error overlay + useEffect(() => { + const checkNextOverlay = () => { + // Next.js error overlay has specific identifiers + const nextjsOverlay = + document.querySelector('nextjs-portal') || + document.querySelector('[data-nextjs-dialog-overlay]') || + document.querySelector('[data-nextjs-toast]'); + setHasNextOverlay(!!nextjsOverlay); + }; + + // Check on mount and set up observer + checkNextOverlay(); + + const observer = new MutationObserver(checkNextOverlay); + observer.observe(document.body, { childList: true, subtree: true }); + + return () => observer.disconnect(); + }, []); + + // Subscribe to registry changes + useEffect(() => { + const updateDials = () => { + setDials(registry.getAllDials()); + }; + + // Initial load + updateDials(); + + // Subscribe to changes + const unsubscribe = registry.subscribeToRegistry(updateDials); + + return unsubscribe; + }, [registry]); + + // Detect platform to configure shortcut labels + useEffect(() => { + if (typeof navigator === 'undefined') return; + const isMac = /Mac|iPhone|iPod|iPad/i.test(navigator.platform); + setIsMacLike(isMac); + }, []); + + useEffect(() => { + setShortcutLabel(isMacLike ? 'Ctrl+D (macOS)' : 'Ctrl+Alt+D (Windows/Linux)'); + }, [isMacLike]); + + // Keyboard shortcut to toggle visibility + useEffect(() => { + const handleKeyPress = (e: KeyboardEvent) => { + const key = e.key.toLowerCase(); + if (key !== 'd') return; + + const macCombo = isMacLike && e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey; + const otherCombo = !isMacLike && e.ctrlKey && e.altKey && !e.metaKey && !e.shiftKey; + + if (macCombo || otherCombo) { + e.preventDefault(); + setIsVisible(prev => !prev); + } + }; + + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, [isMacLike]); + + // Filter and group dials + const filteredDials = useMemo(() => { + if (!searchTerm) return dials; + + const term = searchTerm.toLowerCase(); + return dials.filter(dial => { + const label = dial.config.label.toLowerCase(); + const group = dial.config.group?.toLowerCase() || ''; + const id = dial.id.toLowerCase(); + return label.includes(term) || group.includes(term) || id.includes(term); + }); + }, [dials, searchTerm]); + + const groupedDials = useMemo(() => { + const groups = new Map(); + + for (const dial of filteredDials) { + const group = dial.config.group || 'Ungrouped'; + if (!groups.has(group)) { + groups.set(group, []); + } + groups.get(group)!.push(dial); + } + + return groups; + }, [filteredDials]); + + const handleChange = (id: string, value: any) => { + registry.setValue(id, value); + }; + + const handleReset = (id: string) => { + registry.reset(id); + }; + + const handleResetAll = () => { + if (confirm('Reset all dials to their default values?')) { + registry.resetAll(); + } + }; + + // Calculate bottom position based on Next.js overlay presence + const bottomPosition = hasNextOverlay ? '140px' : '20px'; + + if (!isVisible) { + return ( + + ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

+ Design Dials +

+
+ {shortcutLabel} +
+
+
+ +
+ + {/* Search */} +
+ setSearchTerm(e.target.value)} + style={{ + width: '100%', + padding: '6px 8px', + border: '1px solid transparent', + borderRadius: '2px', + fontSize: '11px', + background: '#373c4b', + color: '#fefefe', + outline: 'none', + }} + /> +
+ + {/* Dials list */} +
+ {filteredDials.length === 0 ? ( +
+ {searchTerm ? 'No dials match your search' : 'No dials registered yet'} +
+ ) : ( + Array.from(groupedDials.entries()).map(([groupName, groupDials]) => ( +
+

+ {groupName} +

+
+ {groupDials.map(dial => ( +
{renderControl(dial, handleChange, handleReset)}
+ ))} +
+
+ )) + )} +
+ + {/* Footer */} +
+ +
+ {dials.length} dial{dials.length !== 1 ? 's' : ''} +
+
+
+ ); +} + +/** + * Render the appropriate control component based on dial type + */ +function renderControl( + dial: DialRegistration, + onChange: (id: string, value: any) => void, + onReset: (id: string) => void, +) { + const commonProps = { + id: dial.id, + value: dial.currentValue, + onChange: (value: any) => onChange(dial.id, value), + onReset: () => onReset(dial.id), + }; + + switch (dial.type) { + case 'color': + return ; + case 'spacing': + return ; + case 'variant': + return ; + case 'boolean': + return ; + case 'number': + return ; + default: + return null; + } +} diff --git a/packages/dials/src/components/DialsProvider.tsx b/packages/dials/src/components/DialsProvider.tsx new file mode 100644 index 00000000..6e31b3a2 --- /dev/null +++ b/packages/dials/src/components/DialsProvider.tsx @@ -0,0 +1,58 @@ +/** + * React context provider for dials + * Provides access to the design manifest and configuration + */ + +import React, { createContext, useContext, useEffect, type ReactNode } from 'react'; +import { getDialRegistry } from '../registry'; +import type { DesignManifest } from '../types'; + +interface DialsContextValue { + manifest: DesignManifest | null; +} + +const DialsContext = createContext({ + manifest: null, +}); + +export interface DialsProviderProps { + children: ReactNode; + /** Design system manifest (imported from config) */ + manifest?: DesignManifest | null; + /** Optional project ID for scoping localStorage */ + projectId?: string; +} + +/** + * Provider component for dials + * Should wrap your app at the root level + * + * @example + * ```typescript + * import { designManifest } from '@/config/niteshift-manifest'; + * + * + * + * + * + * ``` + */ +export function DialsProvider({ children, manifest = null, projectId }: DialsProviderProps) { + useEffect(() => { + // Set project ID if provided + if (projectId) { + const registry = getDialRegistry(); + registry.setProjectId(projectId); + } + }, [projectId]); + + return {children}; +} + +/** + * Hook to access the dials context + * @returns The dials context value (manifest) + */ +export function useDialsContext(): DialsContextValue { + return useContext(DialsContext); +} diff --git a/packages/dials/src/controls/BooleanControl.tsx b/packages/dials/src/controls/BooleanControl.tsx new file mode 100644 index 00000000..c08e65fa --- /dev/null +++ b/packages/dials/src/controls/BooleanControl.tsx @@ -0,0 +1,48 @@ +/** + * Boolean control component for the overlay UI + */ + +import React from 'react'; +import type { BooleanDialConfig } from '../types'; + +export interface BooleanControlProps { + id: string; + value: boolean; + config: BooleanDialConfig; + onChange: (value: boolean) => void; + onReset: () => void; +} + +export function BooleanControl({ id, value, config, onChange, onReset }: BooleanControlProps) { + const trueLabel = config.trueLabel || 'On'; + const falseLabel = config.falseLabel || 'Off'; + + return ( +
+
+ + {config.description && {config.description}} + +
+ +
+
+ + +
+
+
+ ); +} diff --git a/packages/dials/src/controls/ColorControl.tsx b/packages/dials/src/controls/ColorControl.tsx new file mode 100644 index 00000000..ef5c063e --- /dev/null +++ b/packages/dials/src/controls/ColorControl.tsx @@ -0,0 +1,152 @@ +/** + * Color control component for the overlay UI + */ + +import React, { useState, useRef, useEffect } from 'react'; +import type { ColorDialConfig } from '../types'; +import { designManifest } from '@/config/niteshift-manifest'; + +export interface ColorControlProps { + id: string; + value: string; + config: ColorDialConfig; + onChange: (value: string) => void; + onReset: () => void; +} + +// Helper to get color name from design system +function getColorName(hex: string): string | null { + const normalizedHex = hex.toLowerCase(); + + // Check accent colors + if (designManifest.colors.accent.values) { + for (const [name, color] of Object.entries(designManifest.colors.accent.values)) { + if (color.toLowerCase() === normalizedHex) { + return name.charAt(0).toUpperCase() + name.slice(1); + } + } + } + + // Check semantic colors + if (designManifest.colors.semantic.values) { + for (const [name, color] of Object.entries(designManifest.colors.semantic.values)) { + if (color.toLowerCase() === normalizedHex) { + return name.charAt(0).toUpperCase() + name.slice(1); + } + } + } + + return null; +} + +export function ColorControl({ id, value, config, onChange, onReset }: ColorControlProps) { + const [showPicker, setShowPicker] = useState(false); + const [pickerPosition, setPickerPosition] = useState({ top: 0, left: 0 }); + const swatchRef = useRef(null); + const pickerRef = useRef(null); + const colorName = getColorName(value); + + const handleSwatchClick = () => { + if (swatchRef.current) { + const rect = swatchRef.current.getBoundingClientRect(); + setPickerPosition({ + top: rect.bottom + 4, + left: rect.left, + }); + setShowPicker(true); + } + }; + + const handlePresetClick = (color: string) => { + onChange(color); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + onChange(e.target.value); + }; + + // Close picker when clicking outside + useEffect(() => { + if (!showPicker) return; + + const handleClickOutside = (e: MouseEvent) => { + if ( + pickerRef.current && + !pickerRef.current.contains(e.target as Node) && + swatchRef.current && + !swatchRef.current.contains(e.target as Node) + ) { + setShowPicker(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [showPicker]); + + return ( +
+
+ + +
+ +
+ {/* Swatch */} +
+ + {/* Input */} + + + {/* Popover picker */} + {showPicker && ( + <> +
setShowPicker(false)} /> +
+ {/* Preset colors */} + {config.options && config.options.length > 0 && ( +
+ {config.options.map(color => { + const name = getColorName(color); + return ( +
handlePresetClick(color)} + title={name || color} + /> + ); + })} +
+ )} +
+ + )} +
+
+ ); +} diff --git a/packages/dials/src/controls/NumberControl.tsx b/packages/dials/src/controls/NumberControl.tsx new file mode 100644 index 00000000..08a277c3 --- /dev/null +++ b/packages/dials/src/controls/NumberControl.tsx @@ -0,0 +1,85 @@ +/** + * Number control component for the overlay UI + */ + +import React from 'react'; +import type { NumberDialConfig } from '../types'; + +export interface NumberControlProps { + id: string; + value: number; + config: NumberDialConfig; + onChange: (value: number) => void; + onReset: () => void; +} + +export function NumberControl({ id, value, config, onChange, onReset }: NumberControlProps) { + const handleSliderChange = (e: React.ChangeEvent) => { + onChange(Number(e.target.value)); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const num = Number(e.target.value); + if (!isNaN(num)) { + onChange(num); + } + }; + + const hasRange = config.min !== undefined && config.max !== undefined; + + return ( +
+
+ + {config.description && {config.description}} + +
+ +
+ {hasRange && ( + <> + {/* Slider */} +
+ +
+ {/* Input */} +
+ + {config.unit && {config.unit}} +
+ + )} + {!hasRange && ( +
+ + {config.unit && {config.unit}} +
+ )} +
+
+ ); +} diff --git a/packages/dials/src/controls/SpacingControl.tsx b/packages/dials/src/controls/SpacingControl.tsx new file mode 100644 index 00000000..fdfcd6b3 --- /dev/null +++ b/packages/dials/src/controls/SpacingControl.tsx @@ -0,0 +1,57 @@ +/** + * Spacing control component for the overlay UI + */ + +import React from 'react'; +import type { SpacingDialConfig } from '../types'; + +export interface SpacingControlProps { + id: string; + value: string; + config: SpacingDialConfig; + onChange: (value: string) => void; + onReset: () => void; +} + +export function SpacingControl({ id, value, config, onChange, onReset }: SpacingControlProps) { + return ( +
+
+ + {config.description && {config.description}} + +
+ +
+ {/* Preset spacing values */} + {config.options && config.options.length > 0 && ( +
+ {config.options.map(spacing => ( + + ))} +
+ )} + + {/* Custom spacing input */} + {config.allowCustom && ( +
+ onChange(e.target.value)} + placeholder={`e.g., 16${config.unit || 'px'}`} + /> +
+ )} +
+
+ ); +} diff --git a/packages/dials/src/controls/VariantControl.tsx b/packages/dials/src/controls/VariantControl.tsx new file mode 100644 index 00000000..63ca4dac --- /dev/null +++ b/packages/dials/src/controls/VariantControl.tsx @@ -0,0 +1,65 @@ +/** + * Variant control component for the overlay UI + */ + +import React from 'react'; +import type { VariantDialConfig } from '../types'; + +export interface VariantControlProps { + id: string; + value: string; + config: VariantDialConfig; + onChange: (value: string) => void; + onReset: () => void; +} + +export function VariantControl({ id, value, config, onChange, onReset }: VariantControlProps) { + // Check if all options are numeric strings + const allNumeric = config.options.every(opt => !isNaN(Number(opt))); + + const handleSliderChange = (e: React.ChangeEvent) => { + const index = Number(e.target.value); + onChange(config.options[index]); + }; + + const currentIndex = config.options.indexOf(value); + + return ( +
+
+ + {config.description && {config.description}} + +
+ +
+ {allNumeric ? ( +
+ +
+ ) : ( + + )} +
+
+ ); +} diff --git a/packages/dials/src/hooks/useDial.ts b/packages/dials/src/hooks/useDial.ts new file mode 100644 index 00000000..816a5022 --- /dev/null +++ b/packages/dials/src/hooks/useDial.ts @@ -0,0 +1,36 @@ +/** + * Core React hook for creating a dynamic dial + * This is the base hook that all specific dial hooks use internally + */ + +import { useState, useEffect } from 'react'; +import { getDialRegistry } from '../registry'; +import type { DialType, DialConfig } from '../types'; + +/** + * Core hook for creating a dial + * Registers the dial in the global registry and subscribes to changes + * + * @param id - Unique identifier for this dial + * @param type - Type of dial + * @param config - Configuration for the dial + * @returns Current value of the dial + */ +export function useDial(id: string, type: DialType, config: DialConfig): T { + const registry = getDialRegistry(); + + // Register the dial and get initial value + const [value, setValue] = useState(() => registry.register(id, type, config)); + + useEffect(() => { + // Subscribe to changes for this specific dial + const unsubscribe = registry.subscribe(id, (dialId, newValue) => { + setValue(newValue); + }); + + // Cleanup subscription on unmount + return unsubscribe; + }, [id, registry]); + + return value; +} diff --git a/packages/dials/src/hooks/useDynamicBoolean.ts b/packages/dials/src/hooks/useDynamicBoolean.ts new file mode 100644 index 00000000..5cb2de36 --- /dev/null +++ b/packages/dials/src/hooks/useDynamicBoolean.ts @@ -0,0 +1,26 @@ +/** + * React hook for creating a boolean dial (toggle) + */ + +import { useDial } from './useDial'; +import type { BooleanDialConfig } from '../types'; + +/** + * Create a dynamic boolean dial for toggles and feature flags + * + * @example + * ```typescript + * const showMetrics = useDynamicBoolean('show-metrics', { + * label: 'Show Metrics', + * default: true, + * trueLabel: 'Visible', + * falseLabel: 'Hidden', + * group: 'Dashboard' + * }); + * + * {showMetrics && } + * ``` + */ +export function useDynamicBoolean(id: string, config: Omit): boolean { + return useDial(id, 'boolean', { ...config, type: 'boolean' }); +} diff --git a/packages/dials/src/hooks/useDynamicColor.ts b/packages/dials/src/hooks/useDynamicColor.ts new file mode 100644 index 00000000..52a751ba --- /dev/null +++ b/packages/dials/src/hooks/useDynamicColor.ts @@ -0,0 +1,52 @@ +/** + * React hook for creating a color dial + */ + +import { useDial } from './useDial'; +import { useDialsContext } from '../components/DialsProvider'; +import type { ColorDialConfig } from '../types'; + +/** + * Create a dynamic color dial + * + * When options are not provided, automatically pulls color values from the + * design manifest (if available). Supports manifest categories like 'primary', + * 'accent', 'semantic', etc. + * + * @example + * ```typescript + * // With explicit options: + * const bgColor = useDynamicColor('hero-bg', { + * label: 'Background Color', + * default: '#1a1a1a', + * options: ['#1a1a1a', '#2d2d2d', '#404040'], + * group: 'Hero Section' + * }); + * + * // With manifest defaults (auto-populated from designManifest.colors.accent): + * const accentColor = useDynamicColor('accent', { + * label: 'Accent Color', + * default: '#3e63dd', + * manifestCategory: 'accent', // pulls from manifest + * }); + * ``` + */ +export function useDynamicColor(id: string, config: Omit): string { + const { manifest } = useDialsContext(); + + // Build config with optional manifest defaults + const finalConfig: ColorDialConfig = { ...config, type: 'color' as const }; + + // If options not provided and we have a manifest, try to load from manifest + if (!config.options && manifest?.colors) { + const category = (config as any).manifestCategory || 'accent'; + const colorCategory = manifest.colors[category]; + + if (colorCategory?.values) { + const values = colorCategory.values; + finalConfig.options = Array.isArray(values) ? values : Object.values(values); + } + } + + return useDial(id, 'color', finalConfig); +} diff --git a/packages/dials/src/hooks/useDynamicNumber.ts b/packages/dials/src/hooks/useDynamicNumber.ts new file mode 100644 index 00000000..0229d9a0 --- /dev/null +++ b/packages/dials/src/hooks/useDynamicNumber.ts @@ -0,0 +1,28 @@ +/** + * React hook for creating a number dial + */ + +import { useDial } from './useDial'; +import type { NumberDialConfig } from '../types'; + +/** + * Create a dynamic number dial + * + * @example + * ```typescript + * const chartHeight = useDynamicNumber('chart-height', { + * label: 'Chart Height', + * default: 400, + * min: 200, + * max: 800, + * step: 50, + * unit: 'px', + * group: 'Chart' + * }); + * + * + * ``` + */ +export function useDynamicNumber(id: string, config: Omit): number { + return useDial(id, 'number', { ...config, type: 'number' }); +} diff --git a/packages/dials/src/hooks/useDynamicSpacing.ts b/packages/dials/src/hooks/useDynamicSpacing.ts new file mode 100644 index 00000000..bf437474 --- /dev/null +++ b/packages/dials/src/hooks/useDynamicSpacing.ts @@ -0,0 +1,44 @@ +/** + * React hook for creating a spacing dial + */ + +import { useDial } from './useDial'; +import { useDialsContext } from '../components/DialsProvider'; +import type { SpacingDialConfig } from '../types'; + +/** + * Create a dynamic spacing dial + * + * When options are not provided, automatically pulls spacing values from the + * design manifest's spacing scale (if available). + * + * @example + * ```typescript + * // With explicit options: + * const padding = useDynamicSpacing('card-padding', { + * label: 'Card Padding', + * default: '1rem', + * options: ['0.5rem', '1rem', '1.5rem', '2rem'], + * group: 'Card' + * }); + * + * // With manifest defaults (auto-populated from designManifest.spacing): + * const margin = useDynamicSpacing('section-margin', { + * label: 'Section Margin', + * default: '24px', // pulls options from manifest.spacing.values + * }); + * ``` + */ +export function useDynamicSpacing(id: string, config: Omit): string { + const { manifest } = useDialsContext(); + + // Build config with optional manifest defaults + const finalConfig: SpacingDialConfig = { ...config, type: 'spacing' as const }; + + // If options not provided and we have a manifest, load from manifest spacing + if (!config.options && manifest?.spacing?.values) { + finalConfig.options = manifest.spacing.values; + } + + return useDial(id, 'spacing', finalConfig); +} diff --git a/packages/dials/src/hooks/useDynamicVariant.ts b/packages/dials/src/hooks/useDynamicVariant.ts new file mode 100644 index 00000000..84d38fdd --- /dev/null +++ b/packages/dials/src/hooks/useDynamicVariant.ts @@ -0,0 +1,29 @@ +/** + * React hook for creating a variant dial + */ + +import { useDial } from './useDial'; +import type { VariantDialConfig } from '../types'; + +/** + * Create a dynamic variant dial for discrete choices + * + * @example + * ```typescript + * const layout = useDynamicVariant('dashboard-layout', { + * label: 'Layout Style', + * default: 'grid', + * options: ['grid', 'list', 'compact'] as const, + * group: 'Dashboard' + * }); + * + * {layout === 'grid' && } + * {layout === 'list' && } + * ``` + */ +export function useDynamicVariant( + id: string, + config: Omit, 'type'>, +): T { + return useDial(id, 'variant', { ...config, type: 'variant' }); +} diff --git a/packages/dials/src/index.ts b/packages/dials/src/index.ts new file mode 100644 index 00000000..936cd050 --- /dev/null +++ b/packages/dials/src/index.ts @@ -0,0 +1,42 @@ +/** + * Dials SDK - Runtime design parameter controls for rapid prototyping + * @packageDocumentation + */ + +// Core types +export type { + DialType, + DialConfig, + ColorDialConfig, + SpacingDialConfig, + VariantDialConfig, + BooleanDialConfig, + NumberDialConfig, + DialRegistration, + DesignManifest, +} from './types'; + +// React hooks +export { useDynamicColor } from './hooks/useDynamicColor'; +export { useDynamicSpacing } from './hooks/useDynamicSpacing'; +export { useDynamicVariant } from './hooks/useDynamicVariant'; +export { useDynamicBoolean } from './hooks/useDynamicBoolean'; +export { useDynamicNumber } from './hooks/useDynamicNumber'; + +// Components +export { DialsProvider, useDialsContext } from './components/DialsProvider'; +export { DialsOverlay } from './components/DialsOverlay'; + +// Registry (for advanced usage) +export { getDialRegistry } from './registry'; + +// Manifest utilities +export { + loadManifest, + getManifestColors, + getManifestSpacing, + getManifestTypography, + getManifestBorderRadius, + getManifestShadows, + buildColorOptions, +} from './utils/manifest'; diff --git a/packages/dials/src/registry.ts b/packages/dials/src/registry.ts new file mode 100644 index 00000000..e24129dc --- /dev/null +++ b/packages/dials/src/registry.ts @@ -0,0 +1,314 @@ +/** + * Global dial registry - singleton that manages all dials + * Survives hot reloads by using a singleton pattern + * Persists values to localStorage + */ + +import type { + DialType, + DialConfig, + DialRegistration, + DialChangeListener, + DialRegistryListener, +} from './types'; + +const STORAGE_KEY_PREFIX = 'niteshift-dials'; +const STORAGE_VERSION = '1'; + +/** + * Get the storage key for persisting dial values + * Can be scoped by project ID in the future + */ +function getStorageKey(projectId?: string): string { + return projectId + ? `${STORAGE_KEY_PREFIX}-${projectId}-v${STORAGE_VERSION}` + : `${STORAGE_KEY_PREFIX}-v${STORAGE_VERSION}`; +} + +/** + * Singleton registry for all dials + */ +class DialRegistry { + private static instance: DialRegistry | null = null; + + /** All registered dials */ + private dials = new Map(); + + /** Listeners for specific dial changes */ + private changeListeners = new Map>(); + + /** Listeners for any registry change (for overlay UI) */ + private registryListeners = new Set(); + + /** Project ID for storage scoping */ + private projectId?: string; + + private constructor() { + // Load persisted values on initialization + this.loadFromStorage(); + } + + /** + * Get the singleton instance + */ + static getInstance(): DialRegistry { + if (!DialRegistry.instance) { + DialRegistry.instance = new DialRegistry(); + } + return DialRegistry.instance; + } + + /** + * Set the project ID for storage scoping + */ + setProjectId(projectId: string): void { + this.projectId = projectId; + this.loadFromStorage(); + } + + /** + * Register a new dial or get existing value + * Returns the current value (persisted or default) + */ + register(id: string, type: DialType, config: DialConfig): T { + // If already registered, return current value + if (this.dials.has(id)) { + return this.dials.get(id)!.currentValue as T; + } + + // Check for persisted value + const persistedValue = this.getPersistedValue(id); + const currentValue = persistedValue !== null ? persistedValue : config.default; + + const registration: DialRegistration = { + id, + type, + config, + currentValue, + updatedAt: Date.now(), + }; + + this.dials.set(id, registration); + this.notifyRegistryListeners(); + + return currentValue as T; + } + + /** + * Update a dial's value + */ + setValue(id: string, value: any): void { + const dial = this.dials.get(id); + if (!dial) { + // eslint-disable-next-line no-console + console.warn(`[Dials] Attempted to set value for unregistered dial: ${id}`); + return; + } + + dial.currentValue = value; + dial.updatedAt = Date.now(); + + this.persistValue(id, value); + this.notifyChangeListeners(id, value); + this.notifyRegistryListeners(); + } + + /** + * Get a dial's current value + */ + getValue(id: string): any { + return this.dials.get(id)?.currentValue; + } + + /** + * Get a dial's registration + */ + getDial(id: string): DialRegistration | undefined { + return this.dials.get(id); + } + + /** + * Get all registered dials + */ + getAllDials(): DialRegistration[] { + return Array.from(this.dials.values()); + } + + /** + * Get dials by group + */ + getDialsByGroup(): Map { + const groups = new Map(); + + for (const dial of this.dials.values()) { + const group = dial.config.group || 'Ungrouped'; + if (!groups.has(group)) { + groups.set(group, []); + } + groups.get(group)!.push(dial); + } + + return groups; + } + + /** + * Reset a dial to its default value + */ + reset(id: string): void { + const dial = this.dials.get(id); + if (!dial) return; + + this.setValue(id, dial.config.default); + } + + /** + * Reset all dials to their default values + */ + resetAll(): void { + for (const [id] of this.dials) { + this.reset(id); + } + } + + /** + * Subscribe to changes for a specific dial + */ + subscribe(id: string, listener: DialChangeListener): () => void { + if (!this.changeListeners.has(id)) { + this.changeListeners.set(id, new Set()); + } + this.changeListeners.get(id)!.add(listener); + + // Return unsubscribe function + return () => { + this.changeListeners.get(id)?.delete(listener); + }; + } + + /** + * Subscribe to any registry change (for overlay UI) + */ + subscribeToRegistry(listener: DialRegistryListener): () => void { + this.registryListeners.add(listener); + + // Return unsubscribe function + return () => { + this.registryListeners.delete(listener); + }; + } + + /** + * Notify listeners of a dial value change + */ + private notifyChangeListeners(id: string, value: any): void { + const listeners = this.changeListeners.get(id); + if (listeners) { + listeners.forEach(listener => listener(id, value)); + } + } + + /** + * Notify registry listeners (for overlay UI updates) + * Deferred to avoid React "setState during render" errors + */ + private notifyRegistryListeners(): void { + queueMicrotask(() => { + this.registryListeners.forEach(listener => listener()); + }); + } + + /** + * Load persisted values from localStorage + */ + private loadFromStorage(): void { + if (typeof window === 'undefined') return; + + try { + const key = getStorageKey(this.projectId); + const stored = localStorage.getItem(key); + if (stored) { + JSON.parse(stored); // Validate JSON, values will be applied when dials are registered + } + } catch (error) { + // eslint-disable-next-line no-console + console.warn('[Dials] Failed to load from localStorage:', error); + } + } + + /** + * Get a persisted value for a dial + */ + private getPersistedValue(id: string): any | null { + if (typeof window === 'undefined') return null; + + try { + const key = getStorageKey(this.projectId); + const stored = localStorage.getItem(key); + if (stored) { + const data = JSON.parse(stored); + return data[id] !== undefined ? data[id] : null; + } + } catch (error) { + // eslint-disable-next-line no-console + console.warn('[Dials] Failed to get persisted value:', error); + } + + return null; + } + + /** + * Persist a dial value to localStorage + */ + private persistValue(id: string, value: any): void { + if (typeof window === 'undefined') return; + + try { + const key = getStorageKey(this.projectId); + const stored = localStorage.getItem(key); + const data = stored ? JSON.parse(stored) : {}; + + data[id] = value; + + localStorage.setItem(key, JSON.stringify(data)); + } catch (error) { + // eslint-disable-next-line no-console + console.warn('[Dials] Failed to persist value:', error); + } + } + + /** + * Export all current values as an object + */ + exportValues(): Record { + const values: Record = {}; + for (const [id, dial] of this.dials) { + values[id] = dial.currentValue; + } + return values; + } + + /** + * Export all dials with their configurations + */ + exportDials(): DialRegistration[] { + return this.getAllDials(); + } + + /** + * Clear all persisted values + */ + clearStorage(): void { + if (typeof window === 'undefined') return; + + try { + const key = getStorageKey(this.projectId); + localStorage.removeItem(key); + } catch (error) { + // eslint-disable-next-line no-console + console.warn('[Dials] Failed to clear storage:', error); + } + } +} + +// Export singleton instance getter +export const getDialRegistry = () => DialRegistry.getInstance(); diff --git a/packages/dials/src/styles.css b/packages/dials/src/styles.css new file mode 100644 index 00000000..8ed90596 --- /dev/null +++ b/packages/dials/src/styles.css @@ -0,0 +1,457 @@ +/** + * Styles for dials overlay and controls - Compact Leva-inspired design + */ + +/* Control base styles - Horizontal layout */ +.dial-control { + display: grid; + grid-template-columns: 1fr 140px; + column-gap: 8px; + min-height: 24px; + padding: 6px 12px; + background: #181c20; + border-bottom: 1px solid #292d39; + transition: background 0.15s; + overflow: hidden; +} + +.dial-control:hover { + background: #1f2329; +} + +.control-header { + display: flex; + align-items: center; + gap: 6px; + min-height: 24px; +} + +.control-header label { + font-weight: 400; + font-size: 11px; + color: #d4d4d4; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: help; +} + +.control-header label:hover { + color: #fefefe; +} + +.control-description { + display: none; /* Moved to tooltip */ +} + +.reset-button { + background: none; + border: none; + cursor: pointer; + font-size: 14px; + padding: 0; + color: #535760; + transition: color 0.15s; + opacity: 0; + width: 16px; + text-align: center; +} + +.dial-control:hover .reset-button { + opacity: 1; +} + +.reset-button:hover { + color: #8c92a4; +} + +.control-body { + display: flex; + align-items: center; + min-height: 24px; + overflow: hidden; + max-width: 100%; +} + +/* Color control - Inline layout */ +.color-control .control-body { + display: grid; + grid-template-columns: 20px 1fr; + gap: 6px; + width: 100%; +} + +.color-swatch { + width: 20px; + height: 20px; + border: 1px solid #535760; + border-radius: 2px; + cursor: pointer; + transition: border-color 0.15s; +} + +.color-swatch:hover { + border-color: #8c92a4; +} + +.color-value-input { + flex: 1; + padding: 2px 6px; + background: #373c4b; + border: 1px solid transparent; + border-radius: 2px; + font-family: ui-monospace, 'SF Mono', monospace; + font-size: 11px; + color: #fefefe; + outline: none; + transition: border-color 0.15s; +} + +.color-value-input:hover { + border-color: #535760; +} + +.color-value-input:focus { + border-color: #0066dc; +} + +/* Color picker popover */ +.color-picker-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + background: rgba(0, 0, 0, 0.3); +} + +.color-picker-wrapper { + position: fixed; + z-index: 10001; + padding: 8px; + background: #181c20; + border: 1px solid #292d39; + border-radius: 4px; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4); +} + +.color-presets { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 4px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid #292d39; +} + +.color-preset { + width: 20px; + height: 20px; + border: 1px solid #535760; + border-radius: 2px; + cursor: pointer; + transition: all 0.15s; +} + +.color-preset:hover { + transform: scale(1.15); + border-color: #8c92a4; +} + +.color-preset.active { + border-color: #0066dc; + box-shadow: 0 0 0 2px #0066dc; +} + +.color-custom { + margin-top: 4px; +} + +.custom-toggle { + width: 100%; + padding: 6px 12px; + background: #fff; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: background 0.2s; +} + +.custom-toggle:hover { + background: #f5f5f5; +} + +.custom-input { + display: flex; + gap: 4px; +} + +.custom-input input { + flex: 1; + padding: 6px 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 12px; +} + +.custom-input button { + padding: 6px 12px; + background: #1a1a1a; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; +} + +.custom-input button:last-child { + background: #fff; + color: #333; + border: 1px solid #ddd; +} + +/* Spacing control */ +.spacing-options { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); + gap: 6px; +} + +.spacing-option { + padding: 8px 12px; + background: #fff; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.2s; + text-align: center; +} + +.spacing-option:hover { + border-color: #999; +} + +.spacing-option.active { + background: #1a1a1a; + color: #fff; + border-color: #1a1a1a; +} + +.spacing-custom input { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 12px; +} + +/* Variant control */ +.variant-select { + width: 100%; + padding: 2px 20px 2px 6px; + background: #373c4b; + border: 1px solid transparent; + border-radius: 2px; + font-size: 11px; + color: #fefefe; + cursor: pointer; + outline: none; + transition: border-color 0.15s; + appearance: none; + background-image: url('data:image/svg+xml;charset=UTF-8,%3csvg width="8" height="5" viewBox="0 0 8 5" fill="none" xmlns="http://www.w3.org/2000/svg"%3e%3cpath d="M0 0L4 4L8 0" fill="%238c92a4"/%3e%3c/svg%3e'); + background-repeat: no-repeat; + background-position: right 6px center; + background-size: 8px 5px; +} + +.variant-select:hover { + border-color: #535760; +} + +.variant-select:focus { + border-color: #0066dc; +} + +.variant-select option { + background: #373c4b; + color: #fefefe; +} + +.variant-slider { + display: grid; + grid-template-columns: 1fr; + gap: 4px; + width: 100%; +} + +.variant-slider input[type='range'] { + width: 100%; + height: 3px; + background: #373c4b; + border-radius: 2px; + outline: none; + appearance: none; +} + +.variant-slider input[type='range']::-webkit-slider-thumb { + appearance: none; + width: 8px; + height: 16px; + background: #8c92a4; + border-radius: 2px; + cursor: pointer; +} + +.variant-slider input[type='range']::-webkit-slider-thumb:hover { + background: #fefefe; +} + +.variant-slider input[type='range']::-moz-range-thumb { + width: 8px; + height: 16px; + background: #8c92a4; + border: none; + border-radius: 2px; + cursor: pointer; +} + +.variant-slider input[type='range']::-moz-range-thumb:hover { + background: #fefefe; +} + +/* Boolean control */ +.boolean-toggle { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3px; + width: 100%; +} + +.toggle-option { + padding: 2px 8px; + background: #373c4b; + border: 1px solid transparent; + border-radius: 2px; + cursor: pointer; + font-size: 11px; + transition: all 0.15s; + text-align: center; + color: #8c92a4; +} + +.toggle-option:hover { + border-color: #535760; + color: #fefefe; +} + +.toggle-option.active { + background: #0066dc; + color: #fefefe; + border-color: #0066dc; +} + +/* Number control - Inline slider + input */ +.number-control .control-body { + display: grid; + grid-template-columns: 1fr 42px; + gap: 6px; + width: 100%; + max-width: 100%; +} + +.number-slider { + display: flex; + align-items: center; + width: 100%; +} + +.number-slider input[type='range'] { + width: 100%; + height: 3px; + background: #373c4b; + border-radius: 2px; + outline: none; + appearance: none; +} + +.number-slider input[type='range']::-webkit-slider-thumb { + appearance: none; + width: 8px; + height: 16px; + background: #8c92a4; + border-radius: 2px; + cursor: pointer; +} + +.number-slider input[type='range']::-webkit-slider-thumb:hover { + background: #fefefe; +} + +.number-slider input[type='range']::-moz-range-thumb { + width: 8px; + height: 16px; + background: #8c92a4; + border: none; + border-radius: 2px; + cursor: pointer; +} + +.number-slider input[type='range']::-moz-range-thumb:hover { + background: #fefefe; +} + +.number-input { + display: flex; + align-items: center; + gap: 2px; +} + +.number-input input { + width: 100%; + padding: 2px 4px; + background: #373c4b; + border: 1px solid transparent; + border-radius: 2px; + font-family: ui-monospace, 'SF Mono', monospace; + font-size: 11px; + color: #fefefe; + text-align: right; + outline: none; + transition: border-color 0.15s; +} + +.number-input input:hover { + border-color: #535760; +} + +.number-input input:focus { + border-color: #0066dc; + text-align: left; +} + +.number-unit { + font-size: 10px; + color: #8c92a4; +} + +.slider-labels { + display: none; /* Removed for compactness */ +} + +.slider-value { + display: none; /* Removed for compactness */ +} + +/* Search input placeholder */ +input[type='search']::placeholder { + color: #8c92a4; + opacity: 1; +} + +input[type='search']:focus::placeholder { + color: #6e6e6e; +} diff --git a/packages/dials/src/types.ts b/packages/dials/src/types.ts new file mode 100644 index 00000000..5fba26f6 --- /dev/null +++ b/packages/dials/src/types.ts @@ -0,0 +1,178 @@ +/** + * Core type definitions for the Dials SDK + */ + +export type DialType = 'color' | 'spacing' | 'variant' | 'boolean' | 'number'; + +/** + * Base configuration shared by all dial types + */ +export interface BaseDialConfig { + /** Human-readable label for the dial */ + label: string; + /** Optional description/help text */ + description?: string; + /** Group/category for organization in overlay UI */ + group?: string; + /** Default value */ + default: T; +} + +/** + * Color dial configuration + * For any color value (backgrounds, text, borders, etc.) + */ +export interface ColorDialConfig extends BaseDialConfig { + type?: 'color'; + /** Predefined color options (from design system) */ + options?: string[]; + /** Allow custom hex input */ + allowCustom?: boolean; + /** Color format hint */ + format?: 'hex' | 'rgb' | 'hsl' | 'var'; +} + +/** + * Spacing dial configuration + * For padding, margin, gap, width, height, etc. + */ +export interface SpacingDialConfig extends BaseDialConfig { + type?: 'spacing'; + /** Predefined spacing options (e.g., '4px', '8px', 'var(--spacing-3)') */ + options?: string[]; + /** Allow custom values */ + allowCustom?: boolean; + /** Unit for custom values */ + unit?: 'px' | 'rem' | 'em' | '%'; + /** Min value for custom input */ + min?: number; + /** Max value for custom input */ + max?: number; +} + +/** + * Variant dial configuration + * For discrete choices (layouts, styles, chart types, etc.) + */ +export interface VariantDialConfig extends BaseDialConfig { + type?: 'variant'; + /** Array of allowed values (enum-like) */ + options: readonly T[]; + /** Optional labels for each option (if different from value) */ + optionLabels?: Record; +} + +/** + * Boolean dial configuration + * For toggles, feature flags, show/hide, etc. + */ +export interface BooleanDialConfig extends BaseDialConfig { + type?: 'boolean'; + /** Label for "true" state */ + trueLabel?: string; + /** Label for "false" state */ + falseLabel?: string; +} + +/** + * Number dial configuration + * For numeric values with constraints + */ +export interface NumberDialConfig extends BaseDialConfig { + type?: 'number'; + /** Minimum value */ + min?: number; + /** Maximum value */ + max?: number; + /** Step increment */ + step?: number; + /** Unit to display (e.g., 'px', '%', 'ms') */ + unit?: string; + /** Predefined number options */ + options?: number[]; +} + +/** + * Union type for all dial configurations + */ +export type DialConfig = + | ColorDialConfig + | SpacingDialConfig + | VariantDialConfig + | BooleanDialConfig + | NumberDialConfig; + +/** + * Internal dial registration stored in registry + */ +export interface DialRegistration { + /** Unique identifier for the dial */ + id: string; + /** Type of dial */ + type: DialType; + /** Configuration */ + config: DialConfig; + /** Current value (user override or default) */ + currentValue: any; + /** Timestamp of last update */ + updatedAt?: number; +} + +/** + * Design system manifest structure + */ +export interface DesignManifest { + name?: string; + version?: string; + colors?: { + [category: string]: { + label?: string; + values: string[] | Record; + }; + }; + spacing?: { + label?: string; + values: string[]; + variables?: string[]; + }; + typography?: { + fontFamilies?: { + label?: string; + values: string[]; + }; + fontSizes?: { + label?: string; + values: string[]; + variables?: string[]; + }; + fontWeights?: { + label?: string; + values: string[]; + labels?: string[]; + }; + headingSizes?: { + label?: string; + values: string[]; + variables?: string[]; + }; + }; + borderRadius?: { + label?: string; + values: string[]; + variables?: string[]; + labels?: string[]; + }; + shadows?: { + label?: string; + values: string[]; + variables?: string[]; + labels?: string[]; + }; + [key: string]: any; +} + +/** + * Event types for dial changes + */ +export type DialChangeListener = (id: string, value: any) => void; +export type DialRegistryListener = () => void; diff --git a/packages/dials/src/utils/manifest.ts b/packages/dials/src/utils/manifest.ts new file mode 100644 index 00000000..102c8d8a --- /dev/null +++ b/packages/dials/src/utils/manifest.ts @@ -0,0 +1,149 @@ +/** + * Utilities for loading and working with the design system manifest + */ + +import type { DesignManifest } from '../types'; + +let cachedManifest: DesignManifest | null = null; + +/** + * Load the design system manifest from .niteshift-manifest + * Caches the result for subsequent calls + */ +export async function loadManifest( + manifestPath = '/.niteshift-manifest', +): Promise { + // Return cached manifest if available + if (cachedManifest) { + return cachedManifest; + } + + try { + const response = await fetch(manifestPath); + if (!response.ok) { + // eslint-disable-next-line no-console + console.warn('[Dials] Design manifest not found at', manifestPath); + return null; + } + + const manifest = await response.json(); + cachedManifest = manifest; + return manifest; + } catch (error) { + // eslint-disable-next-line no-console + console.warn('[Dials] Failed to load design manifest:', error); + return null; + } +} + +/** + * Get color options from the manifest + * @param category - Optional category (e.g., 'primary', 'accent', 'semantic') + * @returns Array of color values + */ +export function getManifestColors(manifest: DesignManifest, category?: string): string[] { + if (!manifest.colors) return []; + + if (category && manifest.colors[category]) { + const cat = manifest.colors[category]; + if (Array.isArray(cat.values)) { + return cat.values; + } else if (typeof cat.values === 'object') { + return Object.values(cat.values); + } + } + + // Return all colors if no category specified + const allColors: string[] = []; + for (const cat of Object.values(manifest.colors)) { + if (Array.isArray(cat.values)) { + allColors.push(...cat.values); + } else if (typeof cat.values === 'object') { + allColors.push(...Object.values(cat.values)); + } + } + + return allColors; +} + +/** + * Get spacing options from the manifest + * @param useVariables - If true, returns CSS variable names instead of pixel values + * @returns Array of spacing values + */ +export function getManifestSpacing(manifest: DesignManifest, useVariables = false): string[] { + if (!manifest.spacing) return []; + + if (useVariables && manifest.spacing.variables) { + return manifest.spacing.variables; + } + + return manifest.spacing.values || []; +} + +/** + * Get typography options from the manifest + */ +export function getManifestTypography( + manifest: DesignManifest, + type: 'fontFamilies' | 'fontSizes' | 'fontWeights' | 'headingSizes', + useVariables = false, +): string[] { + if (!manifest.typography || !manifest.typography[type]) return []; + + const config = manifest.typography[type]; + + if (useVariables && 'variables' in config && config.variables) { + return config.variables; + } + + return config.values || []; +} + +/** + * Get border radius options from the manifest + */ +export function getManifestBorderRadius(manifest: DesignManifest, useVariables = false): string[] { + if (!manifest.borderRadius) return []; + + if (useVariables && manifest.borderRadius.variables) { + return manifest.borderRadius.variables; + } + + return manifest.borderRadius.values || []; +} + +/** + * Get shadow options from the manifest + */ +export function getManifestShadows(manifest: DesignManifest, useVariables = false): string[] { + if (!manifest.shadows) return []; + + if (useVariables && manifest.shadows.variables) { + return manifest.shadows.variables; + } + + return manifest.shadows.values || []; +} + +/** + * Helper to build color options with categories + * Returns a flat array with all colors from specified categories + */ +export function buildColorOptions(manifest: DesignManifest, categories: string[]): string[] { + const colors: string[] = []; + + for (const category of categories) { + const categoryColors = getManifestColors(manifest, category); + colors.push(...categoryColors); + } + + return colors; +} + +/** + * Invalidate the cached manifest (useful for hot reload scenarios) + */ +export function invalidateManifestCache(): void { + cachedManifest = null; +} diff --git a/packages/dials/tsconfig.json b/packages/dials/tsconfig.json new file mode 100644 index 00000000..adb0e812 --- /dev/null +++ b/packages/dials/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowJs": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/dials/tsup.config.ts b/packages/dials/tsup.config.ts new file mode 100644 index 00000000..1de62012 --- /dev/null +++ b/packages/dials/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + sourcemap: true, + clean: true, + external: ['react', 'react-dom', '@/config/niteshift-manifest'], + onSuccess: 'cp src/styles.css dist/styles.css', +}); diff --git a/packages/dials/vitest.config.ts b/packages/dials/vitest.config.ts new file mode 100644 index 00000000..3b922664 --- /dev/null +++ b/packages/dials/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/__tests__/setup.ts'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ca04cd9..e03332be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@hello-pangea/dnd': specifier: ^17.0.0 version: 17.0.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@niteshift/dials': + specifier: workspace:* + version: link:packages/dials '@prisma/adapter-pg': specifier: ^6.18.0 version: 6.18.0 @@ -364,7 +367,39 @@ importers: specifier: ^5.9.3 version: 5.9.3 - dist: {} + packages/dials: + dependencies: + react: + specifier: ^18.0.0 || ^19.0.0 + version: 19.2.0 + react-dom: + specifier: ^18.0.0 || ^19.0.0 + version: 19.2.0(react@19.2.0) + devDependencies: + '@testing-library/react': + specifier: ^14.0.0 + version: 14.3.1(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@types/react': + specifier: ^19.0.0 + version: 19.2.2 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.2.2(@types/react@19.2.2) + '@vitejs/plugin-react': + specifier: ^4.2.0 + version: 4.7.0(vite@5.4.21(@types/node@24.9.2)(terser@5.43.1)) + jsdom: + specifier: ^23.0.0 + version: 23.2.0 + tsup: + specifier: ^8.0.0 + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.0 + version: 5.9.3 + vitest: + specifier: ^1.0.0 + version: 1.6.1(@types/node@24.9.2)(jsdom@23.2.0)(terser@5.43.1) packages: @@ -372,6 +407,12 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@asamuzakjp/dom-selector@2.0.2': + resolution: {integrity: sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -522,6 +563,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.3': resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} engines: {node: '>=6.9.0'} @@ -556,16 +609,44 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms@2.7.1': resolution: {integrity: sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: '@csstools/css-tokenizer': ^2.4.1 + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-tokenizer@2.4.1': resolution: {integrity: sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==} engines: {node: ^14 || ^16 || >=18} + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@csstools/media-query-list-parser@2.1.13': resolution: {integrity: sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==} engines: {node: ^14 || ^16 || >=18} @@ -884,102 +965,204 @@ packages: '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.11': resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.11': resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.11': resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.11': resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.11': resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.11': resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.11': resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.11': resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.11': resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.11': resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.11': resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.11': resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.11': resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.11': resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.11': resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.11': resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.11': resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} engines: {node: '>=18'} @@ -992,6 +1175,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.11': resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} engines: {node: '>=18'} @@ -1004,6 +1193,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.11': resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} engines: {node: '>=18'} @@ -1016,24 +1211,48 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.11': resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.11': resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.11': resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.11': resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} engines: {node: '>=18'} @@ -2391,6 +2610,9 @@ packages: peerDependencies: '@redis/client': ^1.0.0 + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} engines: {node: '>=14.0.0'} @@ -2701,6 +2923,17 @@ packages: peerDependencies: react: ^18 || ^19 + '@testing-library/dom@9.3.4': + resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} + engines: {node: '>=14'} + + '@testing-library/react@14.3.1': + resolution: {integrity: sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==} + engines: {node: '>=14'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -2720,6 +2953,9 @@ packages: '@tybys/wasm-util@0.10.0': resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2793,6 +3029,11 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + '@types/react-dom@19.2.2': resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} peerDependencies: @@ -3025,6 +3266,27 @@ packages: cpu: [x64] os: [win32] + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/expect@1.6.1': + resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} + + '@vitest/runner@1.6.1': + resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} + + '@vitest/snapshot@1.6.1': + resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} + + '@vitest/spy@1.6.1': + resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} + + '@vitest/utils@1.6.1': + resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} + '@vue/compiler-core@3.5.18': resolution: {integrity: sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==} @@ -3068,6 +3330,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -3138,6 +3404,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -3193,6 +3462,9 @@ packages: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -3287,6 +3559,9 @@ packages: resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==} hasBin: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + blob-util@2.0.2: resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} @@ -3399,6 +3674,10 @@ packages: caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -3425,6 +3704,9 @@ packages: chart.js: '>=2.8.0' date-fns: '>=2.0.0' + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + check-more-types@2.24.0: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} engines: {node: '>= 0.8.0'} @@ -3720,6 +4002,10 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -3782,6 +4068,10 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -3846,6 +4136,14 @@ packages: babel-plugin-macros: optional: true + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -3925,6 +4223,9 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-serializer@1.4.1: resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} @@ -4009,6 +4310,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -4028,6 +4333,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-iterator-helpers@1.2.1: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} @@ -4048,6 +4356,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.25.11: resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} engines: {node: '>=18'} @@ -4252,6 +4565,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -4273,6 +4589,10 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + executable@4.1.1: resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} engines: {node: '>=4'} @@ -4475,6 +4795,9 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -4495,6 +4818,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -4642,6 +4969,10 @@ packages: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -4649,10 +4980,18 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http-signature@1.4.0: resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} engines: {node: '>=0.10'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} @@ -4661,11 +5000,19 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} hasBin: true + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + icss-replace-symbols@1.1.0: resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} @@ -4761,6 +5108,10 @@ packages: resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -4898,6 +5249,9 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -4920,6 +5274,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -5166,6 +5524,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -5177,6 +5538,15 @@ packages: jsbn@0.1.1: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + jsdom@23.2.0: + resolution: {integrity: sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -5337,6 +5707,10 @@ packages: resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} engines: {node: '>= 12.13.0'} + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -5414,6 +5788,9 @@ packages: resolution: {integrity: sha512-S0FayMXku80toa5sZ6Ro4C+s+EtFDCsyJNG/AzFMfX3AxD5Si4dZsgzm/kKnbOxHl5Cv8jBlno8+3XYIh2pNjQ==} engines: {node: '>=8'} + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -5437,6 +5814,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -5532,6 +5913,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -5710,6 +6095,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -5726,6 +6115,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -5760,6 +6153,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -5787,6 +6184,10 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -5836,6 +6237,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -5852,6 +6256,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -5871,9 +6279,15 @@ packages: resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} engines: {node: '>=18'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -6442,6 +6856,10 @@ packages: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6481,6 +6899,9 @@ packages: proxy-from-env@1.0.0: resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -6498,6 +6919,9 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -6563,6 +6987,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -6578,6 +7005,10 @@ packages: redux: optional: true + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + react-simple-maps@2.3.0: resolution: {integrity: sha512-IZVeiPSRZKwD6I/2NvXpQ2uENYGDGZp8DvZjkapcxuJ/LQHTfl+Byb+KNgY7s+iatRA2ad8LnZ3AgqcjziCCsw==} peerDependencies: @@ -6680,6 +7111,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -6770,6 +7204,12 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -6800,6 +7240,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -6886,6 +7330,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -6983,6 +7430,12 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -7064,6 +7517,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -7076,6 +7533,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + style-inject@0.3.0: resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==} @@ -7171,6 +7631,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -7218,6 +7681,9 @@ packages: resolution: {integrity: sha512-UxWEfRKpFCabAf6fkTNdlfSw/RDUJ/4C6i1aLZaDnGF82PERHyYhz5CMCVYXtLt34LbqgfpJ2bjmgGKgxuF/6A==} engines: {node: '>=12'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -7228,6 +7694,14 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + engines: {node: '>=14.0.0'} + + tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} @@ -7250,6 +7724,10 @@ packages: resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==} hasBin: true + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -7257,6 +7735,10 @@ packages: tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -7367,6 +7849,10 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + type-fest@0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} @@ -7454,6 +7940,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -7474,6 +7964,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-memo-one@1.1.3: resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} peerDependencies: @@ -7518,6 +8011,67 @@ packages: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} + vite-node@1.6.1: + resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@1.6.1: + resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.1 + '@vitest/ui': 1.6.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vue@3.5.18: resolution: {integrity: sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==} peerDependencies: @@ -7526,6 +8080,10 @@ packages: typescript: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -7536,6 +8094,22 @@ packages: webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -7564,6 +8138,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -7605,6 +8184,25 @@ packages: resolution: {integrity: sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==} engines: {node: '>=8.3'} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -7655,6 +8253,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + zod@4.1.12: resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} @@ -7683,6 +8285,20 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.30 + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@asamuzakjp/dom-selector@2.0.2': + dependencies: + bidi-js: 1.0.3 + css-tree: 2.3.1 + is-potential-custom-element-name: 1.0.1 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -7847,6 +8463,16 @@ snapshots: '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/runtime@7.28.3': {} '@babel/template@7.27.2': @@ -7887,12 +8513,32 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1)': dependencies: '@csstools/css-tokenizer': 2.4.1 + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-tokenizer@2.4.1': {} + '@csstools/css-tokenizer@3.0.4': {} + '@csstools/media-query-list-parser@2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1)': dependencies: '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) @@ -8191,81 +8837,150 @@ snapshots: '@epic-web/invariant@1.0.0': {} + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.25.11': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.25.11': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.25.11': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.25.11': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.25.11': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.25.11': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.25.11': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.25.11': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.25.11': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.25.11': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.25.11': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.25.11': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.25.11': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.25.11': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.25.11': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.25.11': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.25.11': optional: true '@esbuild/netbsd-arm64@0.25.11': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.25.11': optional: true '@esbuild/openbsd-arm64@0.25.11': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.25.11': optional: true '@esbuild/openharmony-arm64@0.25.11': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.25.11': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.25.11': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.25.11': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.25.11': optional: true @@ -10133,6 +10848,8 @@ snapshots: dependencies: '@redis/client': 1.6.1 + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/plugin-alias@5.1.1(rollup@4.52.5)': optionalDependencies: rollup: 4.52.5 @@ -10398,6 +11115,27 @@ snapshots: '@tanstack/query-core': 5.90.5 react: 19.2.0 + '@testing-library/dom@9.3.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.3 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/react@14.3.1(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.3 + '@testing-library/dom': 9.3.4 + '@types/react-dom': 18.3.7(@types/react@19.2.2) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + '@trysound/sax@0.2.0': {} '@tsconfig/node10@1.0.11': {} @@ -10413,6 +11151,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.3 @@ -10497,6 +11237,10 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/react-dom@18.3.7(@types/react@19.2.2)': + dependencies: + '@types/react': 19.2.2 + '@types/react-dom@19.2.2(@types/react@19.2.2)': dependencies: '@types/react': 19.2.2 @@ -10767,6 +11511,47 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@24.9.2)(terser@5.43.1))': + dependencies: + '@babel/core': 7.28.3 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.3) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@24.9.2)(terser@5.43.1) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@1.6.1': + dependencies: + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + chai: 4.5.0 + + '@vitest/runner@1.6.1': + dependencies: + '@vitest/utils': 1.6.1 + p-limit: 5.0.0 + pathe: 1.1.2 + + '@vitest/snapshot@1.6.1': + dependencies: + magic-string: 0.30.18 + pathe: 1.1.2 + pretty-format: 29.7.0 + + '@vitest/spy@1.6.1': + dependencies: + tinyspy: 2.2.1 + + '@vitest/utils@1.6.1': + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + '@vue/compiler-core@3.5.18': dependencies: '@babel/parser': 7.28.3 @@ -10831,6 +11616,8 @@ snapshots: acorn@8.15.0: {} + agent-base@7.1.4: {} + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -10897,6 +11684,10 @@ snapshots: argparse@2.0.1: {} + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -10978,6 +11769,8 @@ snapshots: assert-plus@1.0.0: {} + assertion-error@1.1.0: {} + ast-types-flow@0.0.8: {} astral-regex@2.0.0: {} @@ -11099,6 +11892,10 @@ snapshots: bcryptjs@3.0.2: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + blob-util@2.0.2: {} bluebird@3.7.2: {} @@ -11219,6 +12016,16 @@ snapshots: caseless@0.12.0: {} + chai@4.5.0: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -11243,6 +12050,10 @@ snapshots: chart.js: 4.5.1 date-fns: 2.30.0 + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + check-more-types@2.24.0: {} chokidar@4.0.3: @@ -11544,6 +12355,11 @@ snapshots: dependencies: css-tree: 2.2.1 + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + csstype@3.1.3: {} currently-unhandled@0.4.1: @@ -11650,6 +12466,11 @@ snapshots: data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -11703,6 +12524,31 @@ snapshots: dedent@1.6.0: {} + deep-eql@4.1.4: + dependencies: + type-detect: 4.1.0 + + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + deep-is@0.1.4: {} deepmerge-ts@7.1.5: {} @@ -11777,6 +12623,8 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + dom-serializer@1.4.1: dependencies: domelementtype: 2.3.0 @@ -11867,6 +12715,8 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: {} + environment@1.1.0: {} error-ex@1.3.2: @@ -11934,6 +12784,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-iterator-helpers@1.2.1: dependencies: call-bind: 1.0.8 @@ -11974,6 +12836,32 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.11: optionalDependencies: '@esbuild/aix-ppc64': 0.25.11 @@ -12263,6 +13151,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} eventemitter2@6.4.7: {} @@ -12295,6 +13187,18 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + executable@4.1.1: dependencies: pify: 2.3.0 @@ -12526,6 +13430,8 @@ snapshots: get-east-asian-width@1.4.0: {} + get-func-name@2.0.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -12552,6 +13458,8 @@ snapshots: get-stream@6.0.1: {} + get-stream@8.0.1: {} + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -12732,22 +13640,46 @@ snapshots: dependencies: lru-cache: 6.0.0 + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} html-tags@3.3.1: {} + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + http-signature@1.4.0: dependencies: assert-plus: 1.0.0 jsprim: 2.0.2 sshpk: 1.18.0 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + human-signals@1.1.1: {} human-signals@2.1.0: {} + human-signals@5.0.0: {} + husky@9.1.7: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + icss-replace-symbols@1.1.0: {} icss-utils@5.1.0(postcss@8.5.6): @@ -12827,6 +13759,11 @@ snapshots: ipaddr.js@2.2.0: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -12943,6 +13880,8 @@ snapshots: is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -12964,6 +13903,8 @@ snapshots: is-stream@2.0.1: {} + is-stream@3.0.0: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -13424,6 +14365,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -13435,6 +14378,34 @@ snapshots: jsbn@0.1.1: {} + jsdom@23.2.0: + dependencies: + '@asamuzakjp/dom-selector': 2.0.2 + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.4 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 7.3.0 + rrweb-cssom: 0.6.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -13614,6 +14585,11 @@ snapshots: loader-utils@3.3.1: {} + local-pkg@0.5.1: + dependencies: + mlly: 1.8.0 + pkg-types: 1.3.1 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -13683,6 +14659,10 @@ snapshots: currently-unhandled: 0.4.1 signal-exit: 3.0.7 + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -13705,6 +14685,8 @@ snapshots: dependencies: react: 19.2.0 + lz-string@1.5.0: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -13804,6 +14786,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-fn@4.0.0: {} + mimic-function@5.0.1: {} min-indent@1.0.1: {} @@ -13979,6 +14963,10 @@ snapshots: dependencies: path-key: 3.1.1 + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -13995,6 +14983,11 @@ snapshots: object-inspect@1.13.4: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + object-keys@1.1.1: {} object.assign@4.1.7: @@ -14043,6 +15036,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -14074,6 +15071,10 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@5.0.0: + dependencies: + yocto-queue: 1.2.2 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -14121,6 +15122,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -14129,6 +15134,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -14144,8 +15151,12 @@ snapshots: path-type@6.0.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@1.1.1: {} + pend@1.2.0: {} perfect-debounce@1.0.0: {} @@ -14676,6 +15687,12 @@ snapshots: pretty-bytes@5.6.0: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -14716,6 +15733,10 @@ snapshots: proxy-from-env@1.0.0: {} + psl@1.15.0: + dependencies: + punycode: 2.3.1 + pump@3.0.3: dependencies: end-of-stream: 1.4.5 @@ -14731,6 +15752,8 @@ snapshots: dependencies: side-channel: 1.1.0 + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} quick-lru@4.0.1: {} @@ -14863,6 +15886,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@18.3.1: {} react-redux@9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1): @@ -14874,6 +15899,8 @@ snapshots: '@types/react': 19.2.2 redux: 5.0.1 + react-refresh@0.17.0: {} + react-simple-maps@2.3.0(prop-types@15.8.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: d3-geo: 2.0.2 @@ -15033,6 +16060,8 @@ snapshots: require-from-string@2.0.2: {} + requires-port@1.0.0: {} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -15155,6 +16184,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 + rrweb-cssom@0.6.0: {} + + rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -15190,6 +16223,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} schema-utils@2.7.1: @@ -15338,6 +16375,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -15437,6 +16476,10 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -15549,6 +16592,8 @@ snapshots: strip-final-newline@2.0.0: {} + strip-final-newline@3.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -15559,6 +16604,10 @@ snapshots: strip-json-comments@3.1.1: {} + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + style-inject@0.3.0: {} style-search@0.1.0: {} @@ -15702,6 +16751,8 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 + symbol-tree@3.2.4: {} + synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 @@ -15756,6 +16807,8 @@ snapshots: tiny-lru@11.3.4: {} + tinybench@2.9.0: {} + tinyexec@0.3.2: {} tinyexec@1.0.1: {} @@ -15765,6 +16818,10 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@0.8.4: {} + + tinyspy@2.2.1: {} + tldts-core@6.1.86: {} tldts@6.1.86: @@ -15783,6 +16840,13 @@ snapshots: dependencies: commander: 2.20.3 + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -15791,6 +16855,10 @@ snapshots: dependencies: punycode: 2.3.1 + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} trim-newlines@3.0.1: {} @@ -15898,6 +16966,8 @@ snapshots: type-detect@4.0.8: {} + type-detect@4.1.0: {} + type-fest@0.13.1: {} type-fest@0.20.2: {} @@ -15979,6 +17049,8 @@ snapshots: universalify@0.1.2: {} + universalify@0.2.0: {} + universalify@2.0.1: {} unrs-resolver@1.11.1: @@ -16017,6 +17089,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + use-memo-one@1.1.3(react@19.2.0): dependencies: react: 19.2.0 @@ -16056,6 +17133,69 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 + vite-node@1.6.1(@types/node@24.9.2)(terser@5.43.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@8.1.1) + pathe: 1.1.2 + picocolors: 1.1.1 + vite: 5.4.21(@types/node@24.9.2)(terser@5.43.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@24.9.2)(terser@5.43.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.5 + optionalDependencies: + '@types/node': 24.9.2 + fsevents: 2.3.3 + terser: 5.43.1 + + vitest@1.6.1(@types/node@24.9.2)(jsdom@23.2.0)(terser@5.43.1): + dependencies: + '@vitest/expect': 1.6.1 + '@vitest/runner': 1.6.1 + '@vitest/snapshot': 1.6.1 + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + acorn-walk: 8.3.4 + chai: 4.5.0 + debug: 4.4.3(supports-color@8.1.1) + execa: 8.0.1 + local-pkg: 0.5.1 + magic-string: 0.30.18 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + tinybench: 2.9.0 + tinypool: 0.8.4 + vite: 5.4.21(@types/node@24.9.2)(terser@5.43.1) + vite-node: 1.6.1(@types/node@24.9.2)(terser@5.43.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.9.2 + jsdom: 23.2.0 + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vue@3.5.18(typescript@4.9.5): dependencies: '@vue/compiler-dom': 3.5.18 @@ -16066,6 +17206,10 @@ snapshots: optionalDependencies: typescript: 4.9.5 + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -16074,6 +17218,19 @@ snapshots: webidl-conversions@4.0.2: {} + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 @@ -16129,6 +17286,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wordwrap@1.0.0: {} @@ -16185,6 +17347,12 @@ snapshots: sort-keys: 4.2.0 write-file-atomic: 3.0.3 + ws@8.18.3: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} @@ -16225,6 +17393,8 @@ snapshots: yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} + zod@4.1.12: {} zustand@5.0.8(@types/react@19.2.2)(immer@10.2.0)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)): diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index 7522c8ec..b77a44b8 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -4,6 +4,8 @@ import { MetricsBar } from '@/components/metrics/MetricsBar'; import { formatShortTime, formatLongNumber } from '@/lib/format'; import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery'; import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useContext } from 'react'; +import { TypographyContext } from './WebsitePage'; export function WebsiteMetricsBar({ websiteId, @@ -12,6 +14,7 @@ export function WebsiteMetricsBar({ showChange?: boolean; compareMode?: boolean; }) { + const typography = useContext(TypographyContext); const { isAllTime } = useDateRange(); const { formatMessage, labels, getErrorMessage } = useMessages(); const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId); @@ -79,6 +82,12 @@ export function WebsiteMetricsBar({ formatValue={formatValue} reverseColors={reverseColors} showChange={!isAllTime} + labelSize={typography.metricLabelSize as any} + valueSize={typography.metricValueSize as any} + labelWeight={typography.metricLabelWeight as any} + valueWeight={typography.metricValueWeight as any} + labelColor={typography.metricLabelColor} + valueColor={typography.metricValueColor} /> ); })} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx index 0fb6d565..8e4daf2d 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx @@ -8,6 +8,7 @@ import { ChartPie, UserPlus, AlignEndHorizontal, + Sparkles, } from '@/components/icons'; import { Lightning, Path, Money, Target, Funnel, Magnet, Network } from '@/components/svg'; import { useMessages, useNavigation } from '@/components/hooks'; @@ -41,6 +42,12 @@ export function WebsiteNav({ icon: , path: renderPath(''), }, + { + id: 'overview-alt', + label: 'Overview Alt', + icon: , + path: renderPath('/overview-alt'), + }, { id: 'events', label: formatMessage(labels.events), diff --git a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx index bf8afe98..e733c756 100644 --- a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx @@ -6,17 +6,122 @@ import { WebsiteMetricsBar } from './WebsiteMetricsBar'; import { WebsitePanels } from './WebsitePanels'; import { WebsiteControls } from './WebsiteControls'; import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal'; +import { useDynamicVariant, useDynamicColor } from '@niteshift/dials'; +import { createContext } from 'react'; + +export const TypographyContext = createContext<{ + metricLabelSize?: string; + metricValueSize?: string; + metricLabelWeight?: string; + metricValueWeight?: string; + metricLabelColor?: string; + metricValueColor?: string; + sectionHeadingSize?: string; + sectionHeadingWeight?: string; + sectionHeadingColor?: string; +}>({}); export function WebsitePage({ websiteId }: { websiteId: string }) { + // Metric Typography Controls + const metricLabelSize = useDynamicVariant('metric-label-size', { + label: 'Metric Label Size', + description: 'Font size for metric labels (Visitors, Views, etc.)', + default: '', + options: ['', '0', '1', '2', '3', '4'] as const, + group: 'Typography - Metrics', + }); + + const metricValueSize = useDynamicVariant('metric-value-size', { + label: 'Metric Value Size', + description: 'Font size for metric values (numbers)', + default: '8', + options: ['4', '5', '6', '7', '8', '9'] as const, + group: 'Typography - Metrics', + }); + + const metricLabelWeight = useDynamicVariant('metric-label-weight', { + label: 'Metric Label Weight', + description: 'Font weight for metric labels', + default: 'bold', + options: ['normal', 'medium', 'semibold', 'bold'] as const, + group: 'Typography - Metrics', + }); + + const metricValueWeight = useDynamicVariant('metric-value-weight', { + label: 'Metric Value Weight', + description: 'Font weight for metric values', + default: 'bold', + options: ['normal', 'medium', 'semibold', 'bold'] as const, + group: 'Typography - Metrics', + }); + + const metricLabelColor = useDynamicColor('metric-label-color', { + label: 'Metric Label Color', + description: 'Text color for metric labels', + default: '', + options: ['', '#000000', '#333333', '#666666', '#999999', '#3e63dd', '#30a46c', '#e5484d'], + allowCustom: true, + group: 'Typography - Metrics', + }); + + const metricValueColor = useDynamicColor('metric-value-color', { + label: 'Metric Value Color', + description: 'Text color for metric values', + default: '', + options: ['', '#000000', '#333333', '#666666', '#999999', '#3e63dd', '#30a46c', '#e5484d'], + allowCustom: true, + group: 'Typography - Metrics', + }); + + // Section Heading Controls + const sectionHeadingSize = useDynamicVariant('section-heading-size', { + label: 'Section Heading Size', + description: 'Font size for section headings (Pages, Sources, etc.)', + default: '2', + options: ['1', '2', '3', '4', '5'] as const, + group: 'Typography - Headings', + }); + + const sectionHeadingWeight = useDynamicVariant('section-heading-weight', { + label: 'Section Heading Weight', + description: 'Font weight for section headings', + default: 'bold', + options: ['normal', 'medium', 'semibold', 'bold'] as const, + group: 'Typography - Headings', + }); + + const sectionHeadingColor = useDynamicColor('section-heading-color', { + label: 'Section Heading Color', + description: 'Text color for section headings', + default: '', + options: ['', '#000000', '#333333', '#666666', '#999999', '#3e63dd', '#30a46c', '#e5484d'], + allowCustom: true, + group: 'Typography - Headings', + }); + + const typographyConfig = { + metricLabelSize, + metricValueSize, + metricLabelWeight, + metricValueWeight, + metricLabelColor, + metricValueColor, + sectionHeadingSize, + sectionHeadingWeight, + sectionHeadingColor, + }; + return ( - - - - - - - - - + + + + + + + + + + + ); } diff --git a/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx b/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx index db52573d..399ece50 100644 --- a/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx @@ -6,10 +6,13 @@ import { MetricsTable } from '@/components/metrics/MetricsTable'; import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic'; import { WorldMap } from '@/components/metrics/WorldMap'; import { Grid, Heading, Row, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen'; +import { useContext } from 'react'; +import { TypographyContext } from './WebsitePage'; export function WebsitePanels({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); const { pathname } = useNavigation(); + const typography = useContext(TypographyContext); const tableProps = { websiteId, limit: 10, @@ -20,11 +23,25 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) { const rowProps = { minHeight: '570px' }; const isSharePage = pathname.includes('/share/'); + const headingStyle = { + fontWeight: + typography.sectionHeadingWeight === 'normal' + ? 400 + : typography.sectionHeadingWeight === 'medium' + ? 500 + : typography.sectionHeadingWeight === 'semibold' + ? 600 + : 700, + color: typography.sectionHeadingColor, + }; + return ( - {formatMessage(labels.pages)} + + {formatMessage(labels.pages)} + {formatMessage(labels.path)} @@ -43,7 +60,9 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) { - {formatMessage(labels.sources)} + + {formatMessage(labels.sources)} + {formatMessage(labels.referrers)} @@ -65,7 +84,9 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) { - {formatMessage(labels.environment)} + + {formatMessage(labels.environment)} + {formatMessage(labels.browsers)} @@ -85,7 +106,9 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) { - {formatMessage(labels.location)} + + {formatMessage(labels.location)} + {formatMessage(labels.countries)} @@ -111,7 +134,9 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) { - {formatMessage(labels.traffic)} + + {formatMessage(labels.traffic)} + @@ -119,7 +144,9 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) { {isSharePage && ( - {formatMessage(labels.events)} + + {formatMessage(labels.events)} + - {children} + + {children} + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 745f6461..89c3c5be 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ import '@fontsource/inter/400.css'; import '@fontsource/inter/500.css'; import '@fontsource/inter/700.css'; import '@umami/react-zen/styles.css'; +import '@niteshift/dials/styles.css'; import '@/styles/global.css'; import '@/styles/variables.css'; diff --git a/src/components/metrics/MetricCard.tsx b/src/components/metrics/MetricCard.tsx index 4b3577e8..c8a20ce2 100644 --- a/src/components/metrics/MetricCard.tsx +++ b/src/components/metrics/MetricCard.tsx @@ -13,6 +13,12 @@ export interface MetricCardProps { formatValue?: (n: any) => string; showLabel?: boolean; showChange?: boolean; + labelSize?: '0' | '1' | '2' | '3' | '4'; + valueSize?: '4' | '5' | '6' | '7' | '8' | '9'; + labelWeight?: 'normal' | 'medium' | 'semibold' | 'bold'; + valueWeight?: 'normal' | 'medium' | 'semibold' | 'bold'; + labelColor?: string; + valueColor?: string; } export const MetricCard = ({ @@ -23,6 +29,12 @@ export const MetricCard = ({ formatValue = formatNumber, showLabel = true, showChange = false, + labelSize, + valueSize, + labelWeight, + valueWeight, + labelColor, + valueColor, }: MetricCardProps) => { const diff = value - change; const pct = ((value - diff) / diff) * 100; @@ -39,11 +51,21 @@ export const MetricCard = ({ border > {showLabel && ( - + {label} )} - + {props?.x?.to(x => formatValue(x))} {showChange && ( diff --git a/src/config/niteshift-manifest.ts b/src/config/niteshift-manifest.ts new file mode 100644 index 00000000..366b3abc --- /dev/null +++ b/src/config/niteshift-manifest.ts @@ -0,0 +1,198 @@ +/** + * Niteshift Dials Design System Manifest + * + * This file defines the available design tokens for the Umami design system. + * These tokens are used by the Dials SDK to provide preset options for + * color, spacing, typography, and other design parameters. + */ + +import type { DesignManifest } from '@niteshift/dials'; + +export const designManifest: DesignManifest = { + name: 'Umami Design System', + version: '1.0.0', + colors: { + primary: { + label: 'Primary Colors', + values: ['#147af3', '#2680eb', '#0090ff', '#3e63dd', '#5b5bd6'], + }, + base: { + label: 'Base Colors (Light Theme)', + values: [ + '#fcfcfc', + '#f9f9f9', + '#f0f0f0', + '#e8e8e8', + '#e0e0e0', + '#d9d9d9', + '#cecece', + '#bbbbbb', + '#8d8d8d', + '#838383', + '#646464', + '#202020', + ], + }, + baseDark: { + label: 'Base Colors (Dark Theme)', + values: [ + '#111111', + '#191919', + '#222222', + '#2a2a2a', + '#313131', + '#3a3a3a', + '#484848', + '#606060', + '#6e6e6e', + '#7b7b7b', + '#b4b4b4', + '#eeeeee', + ], + }, + accent: { + label: 'Accent Colors', + values: { + gray: '#8d8d8d', + blue: '#0090ff', + indigo: '#3e63dd', + purple: '#8e4ec6', + violet: '#6e56cf', + pink: '#d6409f', + red: '#e5484d', + orange: '#f76b15', + amber: '#ffc53d', + yellow: '#ffe629', + green: '#30a46c', + teal: '#12a594', + cyan: '#00a2c7', + }, + }, + semantic: { + label: 'Semantic Colors', + values: { + success: '#30a46c', + danger: '#e5484d', + warning: '#f76b15', + info: '#0090ff', + }, + }, + }, + spacing: { + label: 'Spacing Scale', + values: [ + '4px', + '8px', + '12px', + '16px', + '24px', + '32px', + '40px', + '48px', + '64px', + '80px', + '96px', + '128px', + ], + variables: [ + 'var(--spacing-1)', + 'var(--spacing-2)', + 'var(--spacing-3)', + 'var(--spacing-4)', + 'var(--spacing-5)', + 'var(--spacing-6)', + 'var(--spacing-7)', + 'var(--spacing-8)', + 'var(--spacing-9)', + 'var(--spacing-10)', + 'var(--spacing-11)', + 'var(--spacing-12)', + ], + }, + typography: { + fontFamilies: { + label: 'Font Families', + values: ['Inter', 'system-ui', '-apple-system', 'JetBrains Mono'], + }, + fontSizes: { + label: 'Font Sizes', + values: [ + '11px', + '12px', + '14px', + '16px', + '18px', + '24px', + '30px', + '36px', + '48px', + '60px', + '72px', + '96px', + ], + variables: [ + 'var(--font-size-1)', + 'var(--font-size-2)', + 'var(--font-size-3)', + 'var(--font-size-4)', + 'var(--font-size-5)', + 'var(--font-size-6)', + 'var(--font-size-7)', + 'var(--font-size-8)', + 'var(--font-size-9)', + 'var(--font-size-10)', + 'var(--font-size-11)', + 'var(--font-size-12)', + ], + }, + fontWeights: { + label: 'Font Weights', + values: ['300', '400', '500', '600', '700', '800', '900'], + labels: ['Light', 'Regular', 'Medium', 'Semi Bold', 'Bold', 'Extra Bold', 'Black'], + }, + headingSizes: { + label: 'Heading Sizes', + values: ['16px', '20px', '24px', '32px', '42px', '60px'], + variables: [ + 'var(--heading-size-1)', + 'var(--heading-size-2)', + 'var(--heading-size-3)', + 'var(--heading-size-4)', + 'var(--heading-size-5)', + 'var(--heading-size-6)', + ], + }, + }, + borderRadius: { + label: 'Border Radius', + values: ['2px', '4px', '8px', '16px', '9999px'], + variables: [ + 'var(--border-radius-1)', + 'var(--border-radius-2)', + 'var(--border-radius-3)', + 'var(--border-radius-4)', + 'var(--border-radius-full)', + ], + labels: ['Small', 'Default', 'Medium', 'Large', 'Full'], + }, + shadows: { + label: 'Box Shadows', + values: [ + '0 1px 2px 0 rgb(0 0 0 / 0.05)', + '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', + '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', + '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', + '0 25px 50px -12px rgb(0 0 0 / 0.25)', + ], + variables: [ + 'var(--box-shadow-1)', + 'var(--box-shadow-2)', + 'var(--box-shadow-3)', + 'var(--box-shadow-4)', + 'var(--box-shadow-5)', + 'var(--box-shadow-6)', + ], + labels: ['Extra Small', 'Small', 'Medium', 'Large', 'Extra Large', '2XL'], + }, +};