Add Niteshift Dials SDK for runtime design prototyping

Introduces a complete design dials system that allows designers and PMs
to adjust UI parameters at runtime without code changes.

**Dials SDK (`packages/dials/`):**
- useDynamicColor: Color values with design system integration
- useDynamicSpacing: Spacing/padding/margin controls
- useDynamicVariant: Discrete choice selections
- useDynamicBoolean: Toggle/feature flag controls
- useDynamicNumber: Numeric values with min/max/step
- DialsOverlay: Compact Leva-inspired UI (Ctrl+D to toggle)
- DialsProvider: React context for dial state management
- Design manifest integration for design system tokens

**App Integration:**
- Added DialsProvider to app Providers
- Example dials on WebsitePage (metrics bar, panels, navigation)
- MetricCard component with adjustable typography dials
- TypeScript manifest at src/config/niteshift-manifest.ts

**Documentation:**
- Comprehensive CLAUDE.md section on dials usage
- Best practices for preserving original appearance
- Examples for all dial types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sajid Mehmood 2025-11-25 13:13:28 -05:00
parent f4d0a65b16
commit 2727fd6dff
39 changed files with 4623 additions and 19 deletions

View file

@ -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<T>(id: string, type: DialType, config: DialConfig): T {
const registry = getDialRegistry();
// Register the dial and get initial value
const [value, setValue] = useState<T>(() => registry.register<T>(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;
}

View file

@ -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 && <MetricsPanel />}
* ```
*/
export function useDynamicBoolean(id: string, config: Omit<BooleanDialConfig, 'type'>): boolean {
return useDial<boolean>(id, 'boolean', { ...config, type: 'boolean' });
}

View file

@ -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<ColorDialConfig, 'type'>): 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<string>(id, 'color', finalConfig);
}

View file

@ -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'
* });
*
* <Chart height={chartHeight} />
* ```
*/
export function useDynamicNumber(id: string, config: Omit<NumberDialConfig, 'type'>): number {
return useDial<number>(id, 'number', { ...config, type: 'number' });
}

View file

@ -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<SpacingDialConfig, 'type'>): 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<string>(id, 'spacing', finalConfig);
}

View file

@ -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' && <GridView />}
* {layout === 'list' && <ListView />}
* ```
*/
export function useDynamicVariant<T extends string>(
id: string,
config: Omit<VariantDialConfig<T>, 'type'>,
): T {
return useDial<T>(id, 'variant', { ...config, type: 'variant' });
}