@newtonedev/configurator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/Configurator.d.ts +13 -0
  2. package/dist/Configurator.d.ts.map +1 -0
  3. package/dist/Configurator.types.d.ts +13 -0
  4. package/dist/Configurator.types.d.ts.map +1 -0
  5. package/dist/bridge/toCSS.d.ts +7 -0
  6. package/dist/bridge/toCSS.d.ts.map +1 -0
  7. package/dist/bridge/toJSON.d.ts +15 -0
  8. package/dist/bridge/toJSON.d.ts.map +1 -0
  9. package/dist/bridge/toThemeConfig.d.ts +8 -0
  10. package/dist/bridge/toThemeConfig.d.ts.map +1 -0
  11. package/dist/constants.d.ts +16 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/hooks/useConfigurator.d.ts +11 -0
  14. package/dist/hooks/useConfigurator.d.ts.map +1 -0
  15. package/dist/hooks/usePreviewColors.d.ts +8 -0
  16. package/dist/hooks/usePreviewColors.d.ts.map +1 -0
  17. package/dist/hue-conversion.d.ts +10 -0
  18. package/dist/hue-conversion.d.ts.map +1 -0
  19. package/dist/index.cjs +827 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +814 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/panels/CanvasPanel.d.ts +8 -0
  26. package/dist/panels/CanvasPanel.d.ts.map +1 -0
  27. package/dist/panels/ExportPanel.d.ts +8 -0
  28. package/dist/panels/ExportPanel.d.ts.map +1 -0
  29. package/dist/panels/GlobalPanel.d.ts +10 -0
  30. package/dist/panels/GlobalPanel.d.ts.map +1 -0
  31. package/dist/panels/PalettePanel.d.ts +13 -0
  32. package/dist/panels/PalettePanel.d.ts.map +1 -0
  33. package/dist/panels/PreviewPanel.d.ts +12 -0
  34. package/dist/panels/PreviewPanel.d.ts.map +1 -0
  35. package/dist/state/actions.d.ts +62 -0
  36. package/dist/state/actions.d.ts.map +1 -0
  37. package/dist/state/defaults.d.ts +9 -0
  38. package/dist/state/defaults.d.ts.map +1 -0
  39. package/dist/state/reducer.d.ts +4 -0
  40. package/dist/state/reducer.d.ts.map +1 -0
  41. package/dist/types.d.ts +35 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/package.json +56 -0
  44. package/src/Configurator.tsx +177 -0
  45. package/src/Configurator.types.ts +13 -0
  46. package/src/bridge/toCSS.ts +43 -0
  47. package/src/bridge/toJSON.ts +24 -0
  48. package/src/bridge/toThemeConfig.ts +58 -0
  49. package/src/constants.ts +16 -0
  50. package/src/hooks/useConfigurator.ts +30 -0
  51. package/src/hooks/usePreviewColors.ts +40 -0
  52. package/src/hue-conversion.ts +25 -0
  53. package/src/index.ts +24 -0
  54. package/src/panels/CanvasPanel.tsx +90 -0
  55. package/src/panels/ExportPanel.tsx +95 -0
  56. package/src/panels/GlobalPanel.tsx +126 -0
  57. package/src/panels/PalettePanel.tsx +145 -0
  58. package/src/panels/PreviewPanel.tsx +124 -0
  59. package/src/state/actions.ts +27 -0
  60. package/src/state/defaults.ts +74 -0
  61. package/src/state/reducer.ts +163 -0
  62. package/src/types.ts +37 -0
@@ -0,0 +1,43 @@
1
+ import type { ConfiguratorState } from '../types';
2
+ import type { ColorMode } from '@newtonedev/components';
3
+ import { computeTokens } from '@newtonedev/components';
4
+ import { srgbToHex } from 'newtone';
5
+ import { toThemeConfig } from './toThemeConfig';
6
+
7
+ /**
8
+ * Generate CSS custom properties for both light and dark modes.
9
+ * Computes all 12 design tokens for the neutral theme at default elevation.
10
+ */
11
+ export function toCSS(state: ConfiguratorState): string {
12
+ const config = toThemeConfig(state);
13
+ const modes: readonly ColorMode[] = ['light', 'dark'];
14
+
15
+ let css = '';
16
+ for (const mode of modes) {
17
+ const tokens = computeTokens(
18
+ config.colorSystem,
19
+ mode,
20
+ config.themes.neutral,
21
+ 1,
22
+ config.elevation.offsets,
23
+ );
24
+
25
+ const selector = mode === 'light' ? ':root' : '[data-theme="dark"]';
26
+ css += `${selector} {\n`;
27
+ css += ` --newtone-background: ${srgbToHex(tokens.background.srgb)};\n`;
28
+ css += ` --newtone-background-elevated: ${srgbToHex(tokens.backgroundElevated.srgb)};\n`;
29
+ css += ` --newtone-background-sunken: ${srgbToHex(tokens.backgroundSunken.srgb)};\n`;
30
+ css += ` --newtone-text-primary: ${srgbToHex(tokens.textPrimary.srgb)};\n`;
31
+ css += ` --newtone-text-secondary: ${srgbToHex(tokens.textSecondary.srgb)};\n`;
32
+ css += ` --newtone-interactive: ${srgbToHex(tokens.interactive.srgb)};\n`;
33
+ css += ` --newtone-interactive-hover: ${srgbToHex(tokens.interactiveHover.srgb)};\n`;
34
+ css += ` --newtone-interactive-active: ${srgbToHex(tokens.interactiveActive.srgb)};\n`;
35
+ css += ` --newtone-border: ${srgbToHex(tokens.border.srgb)};\n`;
36
+ css += ` --newtone-success: ${srgbToHex(tokens.success.srgb)};\n`;
37
+ css += ` --newtone-warning: ${srgbToHex(tokens.warning.srgb)};\n`;
38
+ css += ` --newtone-error: ${srgbToHex(tokens.error.srgb)};\n`;
39
+ css += `}\n\n`;
40
+ }
41
+
42
+ return css;
43
+ }
@@ -0,0 +1,24 @@
1
+ import type { ConfiguratorState } from '../types';
2
+ import type { NewtoneThemeConfig } from '@newtonedev/components';
3
+ import { toThemeConfig } from './toThemeConfig';
4
+
5
+ /** Serializable configurator export format */
6
+ export interface ConfiguratorExport {
7
+ readonly version: '1.0';
8
+ readonly configuratorState: ConfiguratorState;
9
+ readonly themeConfig: NewtoneThemeConfig;
10
+ }
11
+
12
+ /**
13
+ * Export the configurator state as a JSON string.
14
+ * Includes both the human-readable state (for round-tripping)
15
+ * and the engine-ready theme config.
16
+ */
17
+ export function toJSON(state: ConfiguratorState): string {
18
+ const output: ConfiguratorExport = {
19
+ version: '1.0',
20
+ configuratorState: state,
21
+ themeConfig: toThemeConfig(state),
22
+ };
23
+ return JSON.stringify(output, null, 2);
24
+ }
@@ -0,0 +1,58 @@
1
+ import type { ConfiguratorState } from '../types';
2
+ import type { NewtoneThemeConfig } from '@newtonedev/components';
3
+ import type { PaletteConfig, DynamicRange, HueGrading, HueGradingEndpoint } from 'newtone';
4
+ import { traditionalHueToOklch } from '../hue-conversion';
5
+
6
+ /**
7
+ * Convert configurator state (traditional hues, human-readable controls)
8
+ * to a NewtoneThemeConfig (OKLCH hues, engine-ready format).
9
+ */
10
+ export function toThemeConfig(state: ConfiguratorState): NewtoneThemeConfig {
11
+ const palettes: readonly PaletteConfig[] = state.palettes.map(p => {
12
+ const oklchHue = traditionalHueToOklch(p.hue);
13
+
14
+ const desaturation = p.desaturationStrength !== 'none'
15
+ ? { direction: p.desaturationDirection, strength: p.desaturationStrength } as const
16
+ : undefined;
17
+
18
+ const paletteHueGrading = p.hueGradeStrength !== 'none'
19
+ ? {
20
+ hue: traditionalHueToOklch(p.hueGradeHue),
21
+ strength: p.hueGradeStrength,
22
+ direction: p.hueGradeDirection,
23
+ } as const
24
+ : undefined;
25
+
26
+ return { hue: oklchHue, saturation: p.saturation, desaturation, paletteHueGrading };
27
+ });
28
+
29
+ // Build global hue grading
30
+ const light: HueGradingEndpoint | undefined = state.globalHueGrading.light.strength !== 'none'
31
+ ? { hue: traditionalHueToOklch(state.globalHueGrading.light.hue), strength: state.globalHueGrading.light.strength }
32
+ : undefined;
33
+
34
+ const dark: HueGradingEndpoint | undefined = state.globalHueGrading.dark.strength !== 'none'
35
+ ? { hue: traditionalHueToOklch(state.globalHueGrading.dark.hue), strength: state.globalHueGrading.dark.strength }
36
+ : undefined;
37
+
38
+ const hueGrading: HueGrading | undefined = (light || dark) ? { light, dark } : undefined;
39
+
40
+ const dynamicRange: DynamicRange = {
41
+ lightest: state.dynamicRange.lightest,
42
+ darkest: state.dynamicRange.darkest,
43
+ ...(hueGrading ? { hueGrading } : {}),
44
+ };
45
+
46
+ return {
47
+ colorSystem: { dynamicRange, palettes },
48
+ themes: {
49
+ neutral: { paletteIndex: 0, lightModeNv: 0.95, darkModeNv: 0.1 },
50
+ primary: { paletteIndex: 1, lightModeNv: 0.95, darkModeNv: 0.1 },
51
+ secondary: { paletteIndex: 1, lightModeNv: 0.85, darkModeNv: 0.15 },
52
+ strong: { paletteIndex: 0, lightModeNv: 0.1, darkModeNv: 0.95 },
53
+ },
54
+ elevation: {
55
+ offsets: [-0.02, 0, 0.04] as const,
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Hue ranges for semantic palettes (traditional HSL hues).
3
+ * Indices 0 (Neutral) and 1 (Accent) are unconstrained.
4
+ *
5
+ * For wrapping ranges (e.g. Error crosses 0°), max exceeds 359.
6
+ * The HueSlider component normalizes values to [0, 359] via % 360.
7
+ *
8
+ * Gaps between ranges prevent adjacent semantics from overlapping:
9
+ * - 15–25: gap between Error (reds) and Warning (yellows)
10
+ * - 55–80: gap between Warning and Success (greens)
11
+ */
12
+ export const SEMANTIC_HUE_RANGES: Readonly<Record<number, { readonly min: number; readonly max: number }>> = {
13
+ 2: { min: 80, max: 160 }, // Success (greens)
14
+ 3: { min: 25, max: 55 }, // Warning (yellow/orange)
15
+ 4: { min: 345, max: 375 }, // Error (reds, wraps 0°)
16
+ };
@@ -0,0 +1,30 @@
1
+ import { useReducer, useCallback, useMemo } from 'react';
2
+ import type { ConfiguratorState } from '../types';
3
+ import type { ConfiguratorAction } from '../state/actions';
4
+ import type { NewtoneThemeConfig } from '@newtonedev/components';
5
+ import { configuratorReducer } from '../state/reducer';
6
+ import { DEFAULT_CONFIGURATOR_STATE } from '../state/defaults';
7
+ import { toThemeConfig } from '../bridge/toThemeConfig';
8
+
9
+ export interface UseConfiguratorResult {
10
+ readonly state: ConfiguratorState;
11
+ readonly dispatch: (action: ConfiguratorAction) => void;
12
+ readonly themeConfig: NewtoneThemeConfig;
13
+ readonly reset: () => void;
14
+ }
15
+
16
+ export function useConfigurator(
17
+ initialState?: Partial<ConfiguratorState>,
18
+ ): UseConfiguratorResult {
19
+ const mergedInitial: ConfiguratorState = initialState
20
+ ? { ...DEFAULT_CONFIGURATOR_STATE, ...initialState }
21
+ : DEFAULT_CONFIGURATOR_STATE;
22
+
23
+ const [state, dispatch] = useReducer(configuratorReducer, mergedInitial);
24
+
25
+ const themeConfig = useMemo(() => toThemeConfig(state), [state]);
26
+
27
+ const reset = useCallback(() => dispatch({ type: 'RESET' }), []);
28
+
29
+ return { state, dispatch, themeConfig, reset };
30
+ }
@@ -0,0 +1,40 @@
1
+ import { useMemo } from 'react';
2
+ import { generatePreview } from 'newtone';
3
+ import type { ColorResult, DynamicRange } from 'newtone';
4
+ import type { ConfiguratorState } from '../types';
5
+ import { traditionalHueToOklch } from '../hue-conversion';
6
+
7
+ const PREVIEW_STEPS = 26;
8
+
9
+ /**
10
+ * Compute preview swatches for all palettes.
11
+ * Returns an array of arrays: one preview row per palette.
12
+ */
13
+ export function usePreviewColors(state: ConfiguratorState): readonly (readonly ColorResult[])[] {
14
+ return useMemo(() => {
15
+ const light = state.globalHueGrading.light.strength !== 'none'
16
+ ? { hue: traditionalHueToOklch(state.globalHueGrading.light.hue), strength: state.globalHueGrading.light.strength }
17
+ : undefined;
18
+ const dark = state.globalHueGrading.dark.strength !== 'none'
19
+ ? { hue: traditionalHueToOklch(state.globalHueGrading.dark.hue), strength: state.globalHueGrading.dark.strength }
20
+ : undefined;
21
+ const hueGrading = (light || dark) ? { light, dark } : undefined;
22
+
23
+ const dynamicRange: DynamicRange = {
24
+ lightest: state.dynamicRange.lightest,
25
+ darkest: state.dynamicRange.darkest,
26
+ ...(hueGrading ? { hueGrading } : {}),
27
+ };
28
+
29
+ return state.palettes.map(p => {
30
+ const oklchHue = traditionalHueToOklch(p.hue);
31
+ const desat = p.desaturationStrength !== 'none'
32
+ ? { direction: p.desaturationDirection, strength: p.desaturationStrength } as const
33
+ : undefined;
34
+ const phg = p.hueGradeStrength !== 'none'
35
+ ? { hue: traditionalHueToOklch(p.hueGradeHue), strength: p.hueGradeStrength, direction: p.hueGradeDirection } as const
36
+ : undefined;
37
+ return generatePreview(oklchHue, p.saturation, dynamicRange, PREVIEW_STEPS, desat, phg);
38
+ });
39
+ }, [state]);
40
+ }
@@ -0,0 +1,25 @@
1
+ import { srgbToOklch } from 'newtone';
2
+ import type { Srgb } from 'newtone';
3
+
4
+ /**
5
+ * Convert a traditional HSL color wheel hue (0-359) to its equivalent OKLCH hue.
6
+ *
7
+ * Uses the fully saturated HSL color (S=1, L=0.5) at the given hue,
8
+ * converts to sRGB, then extracts the OKLCH hue component.
9
+ *
10
+ * Traditional hues: 0=red, 60=yellow, 120=green, 180=cyan, 240=blue, 300=magenta.
11
+ */
12
+ export function traditionalHueToOklch(hue: number): number {
13
+ const h = ((hue % 360) + 360) % 360;
14
+ const x = 1 - Math.abs((h / 60) % 2 - 1);
15
+
16
+ let r: number, g: number, b: number;
17
+ if (h < 60) { r = 1; g = x; b = 0; }
18
+ else if (h < 120) { r = x; g = 1; b = 0; }
19
+ else if (h < 180) { r = 0; g = 1; b = x; }
20
+ else if (h < 240) { r = 0; g = x; b = 1; }
21
+ else if (h < 300) { r = x; g = 0; b = 1; }
22
+ else { r = 1; g = 0; b = x; }
23
+
24
+ return srgbToOklch({ r, g, b } as Srgb).h;
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ // Main component
2
+ export { Configurator } from './Configurator';
3
+ export type { ConfiguratorProps } from './Configurator.types';
4
+
5
+ // State types
6
+ export type { ConfiguratorState, PaletteState, HueGradingEndpointState } from './types';
7
+ export type { ConfiguratorAction } from './state/actions';
8
+
9
+ // Bridge functions (for headless usage)
10
+ export { toThemeConfig } from './bridge/toThemeConfig';
11
+ export { toCSS } from './bridge/toCSS';
12
+ export { toJSON } from './bridge/toJSON';
13
+ export type { ConfiguratorExport } from './bridge/toJSON';
14
+
15
+ // Hue conversion utility
16
+ export { traditionalHueToOklch } from './hue-conversion';
17
+
18
+ // Defaults
19
+ export { DEFAULT_CONFIGURATOR_STATE } from './state/defaults';
20
+
21
+ // Hooks (for custom configurator UIs)
22
+ export { useConfigurator } from './hooks/useConfigurator';
23
+ export type { UseConfiguratorResult } from './hooks/useConfigurator';
24
+ export { usePreviewColors } from './hooks/usePreviewColors';
@@ -0,0 +1,90 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+ import { Card, useTokens } from '@newtonedev/components';
4
+ import { srgbToHex } from 'newtone';
5
+ import type { ColorResult } from 'newtone';
6
+ import type { ConfiguratorState } from '../types';
7
+ import { usePreviewColors } from '../hooks/usePreviewColors';
8
+
9
+ interface CanvasPanelProps {
10
+ readonly state: ConfiguratorState;
11
+ }
12
+
13
+ function SwatchRow({ name, colors }: { readonly name: string; readonly colors: readonly ColorResult[] }) {
14
+ const tokens = useTokens(1);
15
+
16
+ return (
17
+ <View style={styles.paletteRow}>
18
+ <Text style={[styles.label, { color: srgbToHex(tokens.textSecondary.srgb) }]}>
19
+ {name}
20
+ </Text>
21
+ <View style={styles.swatches}>
22
+ {colors.map((color, i) => {
23
+ const hex = srgbToHex(color.srgb);
24
+ const textColor = color.oklch.L > 0.6 ? '#000000' : '#ffffff';
25
+ return (
26
+ <View
27
+ key={i}
28
+ style={[styles.swatch, { backgroundColor: hex }]}
29
+ >
30
+ <Text style={[styles.swatchText, { color: textColor }]}>
31
+ {hex}
32
+ </Text>
33
+ </View>
34
+ );
35
+ })}
36
+ </View>
37
+ </View>
38
+ );
39
+ }
40
+
41
+ export function CanvasPanel({ state }: CanvasPanelProps) {
42
+ const previews = usePreviewColors(state);
43
+ const tokens = useTokens(1);
44
+
45
+ return (
46
+ <Card elevation={0} style={styles.container}>
47
+ <Text style={[styles.title, { color: srgbToHex(tokens.textPrimary.srgb) }]}>
48
+ Color Swatches
49
+ </Text>
50
+ {state.palettes.map((palette, i) => (
51
+ <SwatchRow key={i} name={palette.name} colors={previews[i]} />
52
+ ))}
53
+ </Card>
54
+ );
55
+ }
56
+
57
+ const styles = StyleSheet.create({
58
+ container: {
59
+ gap: 12,
60
+ marginBottom: 12,
61
+ },
62
+ title: {
63
+ fontSize: 16,
64
+ fontWeight: '700',
65
+ },
66
+ paletteRow: {
67
+ gap: 4,
68
+ },
69
+ label: {
70
+ fontSize: 12,
71
+ fontWeight: '600',
72
+ },
73
+ swatches: {
74
+ flexDirection: 'row',
75
+ flexWrap: 'wrap',
76
+ gap: 2,
77
+ },
78
+ swatch: {
79
+ width: 56,
80
+ height: 32,
81
+ borderRadius: 4,
82
+ alignItems: 'center',
83
+ justifyContent: 'center',
84
+ },
85
+ swatchText: {
86
+ fontSize: 7,
87
+ fontWeight: '500',
88
+ fontFamily: 'monospace',
89
+ },
90
+ });
@@ -0,0 +1,95 @@
1
+ import React, { useMemo, useState, useCallback } from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+ import { Card, Button, useTokens } from '@newtonedev/components';
4
+ import { srgbToHex } from 'newtone';
5
+ import type { ConfiguratorState } from '../types';
6
+ import { toCSS } from '../bridge/toCSS';
7
+ import { toJSON } from '../bridge/toJSON';
8
+
9
+ interface ExportPanelProps {
10
+ readonly state: ConfiguratorState;
11
+ }
12
+
13
+ type ExportFormat = 'css' | 'json';
14
+
15
+ export function ExportPanel({ state }: ExportPanelProps) {
16
+ const tokens = useTokens(1);
17
+ const [format, setFormat] = useState<ExportFormat>('css');
18
+ const [copied, setCopied] = useState(false);
19
+
20
+ const output = useMemo(() => {
21
+ return format === 'css' ? toCSS(state) : toJSON(state);
22
+ }, [state, format]);
23
+
24
+ const handleCopy = useCallback(() => {
25
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
26
+ navigator.clipboard.writeText(output).then(() => {
27
+ setCopied(true);
28
+ setTimeout(() => setCopied(false), 2000);
29
+ });
30
+ }
31
+ }, [output]);
32
+
33
+ return (
34
+ <Card elevation={1} style={styles.container}>
35
+ <Text style={[styles.title, { color: srgbToHex(tokens.textPrimary.srgb) }]}>
36
+ Export
37
+ </Text>
38
+
39
+ <View style={styles.tabs}>
40
+ <Button
41
+ variant={format === 'css' ? 'primary' : 'ghost'}
42
+ size="sm"
43
+ onPress={() => setFormat('css')}
44
+ >
45
+ CSS Variables
46
+ </Button>
47
+ <Button
48
+ variant={format === 'json' ? 'primary' : 'ghost'}
49
+ size="sm"
50
+ onPress={() => setFormat('json')}
51
+ >
52
+ JSON
53
+ </Button>
54
+ <Button variant="outline" size="sm" onPress={handleCopy}>
55
+ {copied ? 'Copied!' : 'Copy'}
56
+ </Button>
57
+ </View>
58
+
59
+ <View style={[styles.codeBlock, { backgroundColor: srgbToHex(tokens.backgroundSunken.srgb) }]}>
60
+ <Text
61
+ style={[styles.code, { color: srgbToHex(tokens.textPrimary.srgb) }]}
62
+ selectable
63
+ >
64
+ {output}
65
+ </Text>
66
+ </View>
67
+ </Card>
68
+ );
69
+ }
70
+
71
+ const styles = StyleSheet.create({
72
+ container: {
73
+ gap: 12,
74
+ marginBottom: 12,
75
+ },
76
+ title: {
77
+ fontSize: 16,
78
+ fontWeight: '700',
79
+ },
80
+ tabs: {
81
+ flexDirection: 'row',
82
+ gap: 8,
83
+ },
84
+ codeBlock: {
85
+ borderRadius: 6,
86
+ padding: 12,
87
+ maxHeight: 300,
88
+ overflow: 'scroll' as unknown as 'visible',
89
+ },
90
+ code: {
91
+ fontSize: 11,
92
+ fontFamily: 'monospace',
93
+ lineHeight: 16,
94
+ },
95
+ });
@@ -0,0 +1,126 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+ import { Card, Slider, Select, HueSlider, useTokens } from '@newtonedev/components';
4
+ import { srgbToHex } from 'newtone';
5
+ import type { ConfiguratorState } from '../types';
6
+ import type { ConfiguratorAction } from '../state/actions';
7
+ import type { HueGradingStrength } from 'newtone';
8
+
9
+ const STRENGTH_OPTIONS = [
10
+ { label: 'None', value: 'none' },
11
+ { label: 'Low', value: 'low' },
12
+ { label: 'Medium', value: 'medium' },
13
+ { label: 'Hard', value: 'hard' },
14
+ ];
15
+
16
+ interface GlobalPanelProps {
17
+ readonly state: ConfiguratorState;
18
+ readonly dispatch: (action: ConfiguratorAction) => void;
19
+ }
20
+
21
+ export function GlobalPanel({ state, dispatch }: GlobalPanelProps) {
22
+ const tokens = useTokens(1);
23
+
24
+ return (
25
+ <Card elevation={1} style={styles.container}>
26
+ <Text style={[styles.title, { color: srgbToHex(tokens.textPrimary.srgb) }]}>
27
+ Dynamic Range
28
+ </Text>
29
+
30
+ <View style={styles.row}>
31
+ <View style={styles.flex}>
32
+ <Slider
33
+ value={Math.round(state.dynamicRange.lightest * 100)}
34
+ onValueChange={(v) => dispatch({ type: 'SET_LIGHTEST', value: v / 100 })}
35
+ min={0}
36
+ max={100}
37
+ label="Lightest"
38
+ showValue
39
+ />
40
+ </View>
41
+ <View style={styles.flex}>
42
+ <Slider
43
+ value={Math.round(state.dynamicRange.darkest * 100)}
44
+ onValueChange={(v) => dispatch({ type: 'SET_DARKEST', value: v / 100 })}
45
+ min={0}
46
+ max={100}
47
+ label="Darkest"
48
+ showValue
49
+ />
50
+ </View>
51
+ </View>
52
+
53
+ <Text style={[styles.subtitle, { color: srgbToHex(tokens.textSecondary.srgb) }]}>
54
+ Global Hue Grading — Light End
55
+ </Text>
56
+
57
+ <View style={styles.row}>
58
+ <View style={styles.flex}>
59
+ <Select
60
+ options={STRENGTH_OPTIONS}
61
+ value={state.globalHueGrading.light.strength}
62
+ onValueChange={(s) => dispatch({ type: 'SET_GLOBAL_GRADE_LIGHT_STRENGTH', strength: s as HueGradingStrength })}
63
+ label="Strength"
64
+ />
65
+ </View>
66
+ {state.globalHueGrading.light.strength !== 'none' && (
67
+ <View style={styles.flex}>
68
+ <HueSlider
69
+ value={state.globalHueGrading.light.hue}
70
+ onValueChange={(hue) => dispatch({ type: 'SET_GLOBAL_GRADE_LIGHT_HUE', hue })}
71
+ label="Target Hue"
72
+ showValue
73
+ />
74
+ </View>
75
+ )}
76
+ </View>
77
+
78
+ <Text style={[styles.subtitle, { color: srgbToHex(tokens.textSecondary.srgb) }]}>
79
+ Global Hue Grading — Dark End
80
+ </Text>
81
+
82
+ <View style={styles.row}>
83
+ <View style={styles.flex}>
84
+ <Select
85
+ options={STRENGTH_OPTIONS}
86
+ value={state.globalHueGrading.dark.strength}
87
+ onValueChange={(s) => dispatch({ type: 'SET_GLOBAL_GRADE_DARK_STRENGTH', strength: s as HueGradingStrength })}
88
+ label="Strength"
89
+ />
90
+ </View>
91
+ {state.globalHueGrading.dark.strength !== 'none' && (
92
+ <View style={styles.flex}>
93
+ <HueSlider
94
+ value={state.globalHueGrading.dark.hue}
95
+ onValueChange={(hue) => dispatch({ type: 'SET_GLOBAL_GRADE_DARK_HUE', hue })}
96
+ label="Target Hue"
97
+ showValue
98
+ />
99
+ </View>
100
+ )}
101
+ </View>
102
+ </Card>
103
+ );
104
+ }
105
+
106
+ const styles = StyleSheet.create({
107
+ container: {
108
+ gap: 12,
109
+ },
110
+ title: {
111
+ fontSize: 16,
112
+ fontWeight: '700',
113
+ },
114
+ subtitle: {
115
+ fontSize: 13,
116
+ fontWeight: '600',
117
+ marginTop: 4,
118
+ },
119
+ row: {
120
+ flexDirection: 'row',
121
+ gap: 12,
122
+ },
123
+ flex: {
124
+ flex: 1,
125
+ },
126
+ });