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 (
+ setIsVisible(true)}
+ title={`Show Dials (${shortcutLabel})`}
+ style={{
+ position: 'fixed',
+ [position.includes('bottom') ? 'bottom' : 'top']: position.includes('bottom')
+ ? bottomPosition
+ : '20px',
+ [position.includes('right') ? 'right' : 'left']: '20px',
+ width: '48px',
+ height: '48px',
+ borderRadius: '50%',
+ border: '2px solid #666',
+ background: '#1a1a1a',
+ color: '#fff',
+ fontSize: '20px',
+ cursor: 'pointer',
+ zIndex: 9999999, // Very high to be above everything
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ }}
+ >
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ Design Dials
+
+
+ {shortcutLabel}
+
+
+
+
setIsVisible(false)}
+ style={{
+ background: 'none',
+ border: 'none',
+ fontSize: '20px',
+ cursor: 'pointer',
+ padding: 0,
+ lineHeight: 1,
+ color: '#8c92a4',
+ }}
+ title="Close (Shift+Cmd/Ctrl+D)"
+ >
+ ×
+
+
+
+ {/* 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 */}
+
+
0 ? 'pointer' : 'not-allowed',
+ fontSize: '11px',
+ color: dials.length > 0 ? '#fefefe' : '#535760',
+ transition: 'border-color 0.15s',
+ }}
+ >
+ Reset All
+
+
+ {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.label}
+ {config.description && {config.description} }
+
+ ↺
+
+
+
+
+
+ onChange(true)}
+ >
+ {trueLabel}
+
+ onChange(false)}
+ >
+ {falseLabel}
+
+
+
+
+ );
+}
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 (
+
+
+
+ {config.label}
+
+
+ ↺
+
+
+
+
+ {/* 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.label}
+ {config.description && {config.description} }
+
+ ↺
+
+
+
+
+
+ );
+}
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.label}
+ {config.description && {config.description} }
+
+ ↺
+
+
+
+
+ {/* Preset spacing values */}
+ {config.options && config.options.length > 0 && (
+
+ {config.options.map(spacing => (
+ onChange(spacing)}
+ >
+ {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.label}
+ {config.description && {config.description} }
+
+ ↺
+
+
+
+
+ {allNumeric ? (
+
+
+
+ ) : (
+
onChange(e.target.value)}>
+ {config.options.map(option => {
+ const label = config.optionLabels?.[option] || option;
+ return (
+
+ {label}
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
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'],
+ },
+};