@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.
- package/dist/Configurator.d.ts +13 -0
- package/dist/Configurator.d.ts.map +1 -0
- package/dist/Configurator.types.d.ts +13 -0
- package/dist/Configurator.types.d.ts.map +1 -0
- package/dist/bridge/toCSS.d.ts +7 -0
- package/dist/bridge/toCSS.d.ts.map +1 -0
- package/dist/bridge/toJSON.d.ts +15 -0
- package/dist/bridge/toJSON.d.ts.map +1 -0
- package/dist/bridge/toThemeConfig.d.ts +8 -0
- package/dist/bridge/toThemeConfig.d.ts.map +1 -0
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/hooks/useConfigurator.d.ts +11 -0
- package/dist/hooks/useConfigurator.d.ts.map +1 -0
- package/dist/hooks/usePreviewColors.d.ts +8 -0
- package/dist/hooks/usePreviewColors.d.ts.map +1 -0
- package/dist/hue-conversion.d.ts +10 -0
- package/dist/hue-conversion.d.ts.map +1 -0
- package/dist/index.cjs +827 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +814 -0
- package/dist/index.js.map +1 -0
- package/dist/panels/CanvasPanel.d.ts +8 -0
- package/dist/panels/CanvasPanel.d.ts.map +1 -0
- package/dist/panels/ExportPanel.d.ts +8 -0
- package/dist/panels/ExportPanel.d.ts.map +1 -0
- package/dist/panels/GlobalPanel.d.ts +10 -0
- package/dist/panels/GlobalPanel.d.ts.map +1 -0
- package/dist/panels/PalettePanel.d.ts +13 -0
- package/dist/panels/PalettePanel.d.ts.map +1 -0
- package/dist/panels/PreviewPanel.d.ts +12 -0
- package/dist/panels/PreviewPanel.d.ts.map +1 -0
- package/dist/state/actions.d.ts +62 -0
- package/dist/state/actions.d.ts.map +1 -0
- package/dist/state/defaults.d.ts +9 -0
- package/dist/state/defaults.d.ts.map +1 -0
- package/dist/state/reducer.d.ts +4 -0
- package/dist/state/reducer.d.ts.map +1 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +56 -0
- package/src/Configurator.tsx +177 -0
- package/src/Configurator.types.ts +13 -0
- package/src/bridge/toCSS.ts +43 -0
- package/src/bridge/toJSON.ts +24 -0
- package/src/bridge/toThemeConfig.ts +58 -0
- package/src/constants.ts +16 -0
- package/src/hooks/useConfigurator.ts +30 -0
- package/src/hooks/usePreviewColors.ts +40 -0
- package/src/hue-conversion.ts +25 -0
- package/src/index.ts +24 -0
- package/src/panels/CanvasPanel.tsx +90 -0
- package/src/panels/ExportPanel.tsx +95 -0
- package/src/panels/GlobalPanel.tsx +126 -0
- package/src/panels/PalettePanel.tsx +145 -0
- package/src/panels/PreviewPanel.tsx +124 -0
- package/src/state/actions.ts +27 -0
- package/src/state/defaults.ts +74 -0
- package/src/state/reducer.ts +163 -0
- 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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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
|
+
});
|