@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,145 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { Card, HueSlider, Slider, Select, Toggle, useTokens } from '@newtonedev/components';
|
|
4
|
+
import { srgbToHex } from 'newtone';
|
|
5
|
+
import type { ColorResult } from 'newtone';
|
|
6
|
+
import type { PaletteState } from '../types';
|
|
7
|
+
import type { ConfiguratorAction } from '../state/actions';
|
|
8
|
+
import type { DesaturationStrength, HueGradingStrength } from 'newtone';
|
|
9
|
+
import { SEMANTIC_HUE_RANGES } from '../constants';
|
|
10
|
+
|
|
11
|
+
const STRENGTH_OPTIONS = [
|
|
12
|
+
{ label: 'None', value: 'none' },
|
|
13
|
+
{ label: 'Low', value: 'low' },
|
|
14
|
+
{ label: 'Medium', value: 'medium' },
|
|
15
|
+
{ label: 'Hard', value: 'hard' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
interface PalettePanelProps {
|
|
19
|
+
readonly palette: PaletteState;
|
|
20
|
+
readonly index: number;
|
|
21
|
+
readonly dispatch: (action: ConfiguratorAction) => void;
|
|
22
|
+
readonly previewColors?: readonly ColorResult[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function PalettePanel({ palette, index, dispatch, previewColors }: PalettePanelProps) {
|
|
26
|
+
const tokens = useTokens(1);
|
|
27
|
+
const hueRange = SEMANTIC_HUE_RANGES[index];
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Card elevation={1} style={styles.container}>
|
|
31
|
+
<Text style={[styles.title, { color: srgbToHex(tokens.textPrimary.srgb) }]}>
|
|
32
|
+
{palette.name}
|
|
33
|
+
</Text>
|
|
34
|
+
|
|
35
|
+
{previewColors && (
|
|
36
|
+
<View style={styles.inlineSwatches}>
|
|
37
|
+
{previewColors.map((color, i) => (
|
|
38
|
+
<View
|
|
39
|
+
key={i}
|
|
40
|
+
style={[styles.inlineSwatch, { backgroundColor: srgbToHex(color.srgb) }]}
|
|
41
|
+
/>
|
|
42
|
+
))}
|
|
43
|
+
</View>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
<HueSlider
|
|
47
|
+
value={palette.hue}
|
|
48
|
+
onValueChange={(hue) => dispatch({ type: 'SET_PALETTE_HUE', index, hue })}
|
|
49
|
+
label="Hue"
|
|
50
|
+
showValue
|
|
51
|
+
{...(hueRange ? { min: hueRange.min, max: hueRange.max } : {})}
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
<Slider
|
|
55
|
+
value={palette.saturation}
|
|
56
|
+
onValueChange={(saturation) => dispatch({ type: 'SET_PALETTE_SATURATION', index, saturation })}
|
|
57
|
+
min={0}
|
|
58
|
+
max={100}
|
|
59
|
+
label="Saturation"
|
|
60
|
+
showValue
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<View style={styles.row}>
|
|
64
|
+
<View style={styles.flex}>
|
|
65
|
+
<Select
|
|
66
|
+
options={STRENGTH_OPTIONS}
|
|
67
|
+
value={palette.desaturationStrength}
|
|
68
|
+
onValueChange={(strength) => dispatch({ type: 'SET_PALETTE_DESAT_STRENGTH', index, strength: strength as DesaturationStrength })}
|
|
69
|
+
label="Desaturation"
|
|
70
|
+
/>
|
|
71
|
+
</View>
|
|
72
|
+
{palette.desaturationStrength !== 'none' && (
|
|
73
|
+
<View style={styles.toggleContainer}>
|
|
74
|
+
<Toggle
|
|
75
|
+
value={palette.desaturationDirection === 'dark'}
|
|
76
|
+
onValueChange={(v) => dispatch({ type: 'SET_PALETTE_DESAT_DIRECTION', index, direction: v ? 'dark' : 'light' })}
|
|
77
|
+
label="Invert"
|
|
78
|
+
/>
|
|
79
|
+
</View>
|
|
80
|
+
)}
|
|
81
|
+
</View>
|
|
82
|
+
|
|
83
|
+
<View style={styles.row}>
|
|
84
|
+
<View style={styles.flex}>
|
|
85
|
+
<Select
|
|
86
|
+
options={STRENGTH_OPTIONS}
|
|
87
|
+
value={palette.hueGradeStrength}
|
|
88
|
+
onValueChange={(strength) => dispatch({ type: 'SET_PALETTE_HUE_GRADE_STRENGTH', index, strength: strength as HueGradingStrength })}
|
|
89
|
+
label="Hue Grading"
|
|
90
|
+
/>
|
|
91
|
+
</View>
|
|
92
|
+
{palette.hueGradeStrength !== 'none' && (
|
|
93
|
+
<View style={styles.toggleContainer}>
|
|
94
|
+
<Toggle
|
|
95
|
+
value={palette.hueGradeDirection === 'dark'}
|
|
96
|
+
onValueChange={(v) => dispatch({ type: 'SET_PALETTE_HUE_GRADE_DIRECTION', index, direction: v ? 'dark' : 'light' })}
|
|
97
|
+
label="Invert"
|
|
98
|
+
/>
|
|
99
|
+
</View>
|
|
100
|
+
)}
|
|
101
|
+
</View>
|
|
102
|
+
|
|
103
|
+
{palette.hueGradeStrength !== 'none' && (
|
|
104
|
+
<HueSlider
|
|
105
|
+
value={palette.hueGradeHue}
|
|
106
|
+
onValueChange={(hue) => dispatch({ type: 'SET_PALETTE_HUE_GRADE_HUE', index, hue })}
|
|
107
|
+
label="Grade Target"
|
|
108
|
+
showValue
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
</Card>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const styles = StyleSheet.create({
|
|
116
|
+
container: {
|
|
117
|
+
gap: 12,
|
|
118
|
+
marginBottom: 12,
|
|
119
|
+
},
|
|
120
|
+
title: {
|
|
121
|
+
fontSize: 16,
|
|
122
|
+
fontWeight: '700',
|
|
123
|
+
},
|
|
124
|
+
inlineSwatches: {
|
|
125
|
+
flexDirection: 'row',
|
|
126
|
+
flexWrap: 'wrap',
|
|
127
|
+
gap: 1,
|
|
128
|
+
},
|
|
129
|
+
inlineSwatch: {
|
|
130
|
+
width: 20,
|
|
131
|
+
height: 16,
|
|
132
|
+
borderRadius: 2,
|
|
133
|
+
},
|
|
134
|
+
row: {
|
|
135
|
+
flexDirection: 'row',
|
|
136
|
+
alignItems: 'flex-end',
|
|
137
|
+
gap: 12,
|
|
138
|
+
},
|
|
139
|
+
flex: {
|
|
140
|
+
flex: 1,
|
|
141
|
+
},
|
|
142
|
+
toggleContainer: {
|
|
143
|
+
paddingBottom: 2,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
Button,
|
|
6
|
+
TextInput,
|
|
7
|
+
Select,
|
|
8
|
+
NewtoneProvider,
|
|
9
|
+
useTokens,
|
|
10
|
+
} from '@newtonedev/components';
|
|
11
|
+
import { srgbToHex } from 'newtone';
|
|
12
|
+
import type { ConfiguratorState } from '../types';
|
|
13
|
+
import type { ConfiguratorAction } from '../state/actions';
|
|
14
|
+
import type { ThemeName, NewtoneThemeConfig } from '@newtonedev/components';
|
|
15
|
+
|
|
16
|
+
const THEME_OPTIONS = [
|
|
17
|
+
{ label: 'Neutral', value: 'neutral' },
|
|
18
|
+
{ label: 'Primary', value: 'primary' },
|
|
19
|
+
{ label: 'Secondary', value: 'secondary' },
|
|
20
|
+
{ label: 'Strong', value: 'strong' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
interface PreviewPanelProps {
|
|
24
|
+
readonly state: ConfiguratorState;
|
|
25
|
+
readonly dispatch: (action: ConfiguratorAction) => void;
|
|
26
|
+
readonly themeConfig: NewtoneThemeConfig;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Inner card that reads tokens from the nested provider, so its background reflects the selected theme. */
|
|
30
|
+
function PreviewCard({
|
|
31
|
+
state,
|
|
32
|
+
dispatch,
|
|
33
|
+
}: {
|
|
34
|
+
readonly state: ConfiguratorState;
|
|
35
|
+
readonly dispatch: (action: ConfiguratorAction) => void;
|
|
36
|
+
}) {
|
|
37
|
+
const tokens = useTokens(1);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Card elevation={1} style={styles.container}>
|
|
41
|
+
<Text style={[styles.title, { color: srgbToHex(tokens.textPrimary.srgb) }]}>
|
|
42
|
+
Component Preview
|
|
43
|
+
</Text>
|
|
44
|
+
|
|
45
|
+
<View style={styles.controls}>
|
|
46
|
+
<Select
|
|
47
|
+
options={THEME_OPTIONS}
|
|
48
|
+
value={state.preview.theme}
|
|
49
|
+
onValueChange={(t) => dispatch({ type: 'SET_PREVIEW_THEME', theme: t as ThemeName })}
|
|
50
|
+
label="Theme"
|
|
51
|
+
/>
|
|
52
|
+
</View>
|
|
53
|
+
|
|
54
|
+
<View style={previewStyles.wrapper}>
|
|
55
|
+
<View style={previewStyles.row}>
|
|
56
|
+
<Button variant="primary" size="sm">Primary</Button>
|
|
57
|
+
<Button variant="secondary" size="sm">Secondary</Button>
|
|
58
|
+
<Button variant="ghost" size="sm">Ghost</Button>
|
|
59
|
+
<Button variant="outline" size="sm">Outline</Button>
|
|
60
|
+
</View>
|
|
61
|
+
|
|
62
|
+
<TextInput label="Sample Input" value="Hello, Newtone" onChangeText={() => {}} />
|
|
63
|
+
|
|
64
|
+
<View style={previewStyles.row}>
|
|
65
|
+
<View style={[previewStyles.statusDot, { backgroundColor: srgbToHex(tokens.success.srgb) }]} />
|
|
66
|
+
<Text style={[previewStyles.statusText, { color: srgbToHex(tokens.textPrimary.srgb) }]}>Success</Text>
|
|
67
|
+
<View style={[previewStyles.statusDot, { backgroundColor: srgbToHex(tokens.warning.srgb) }]} />
|
|
68
|
+
<Text style={[previewStyles.statusText, { color: srgbToHex(tokens.textPrimary.srgb) }]}>Warning</Text>
|
|
69
|
+
<View style={[previewStyles.statusDot, { backgroundColor: srgbToHex(tokens.error.srgb) }]} />
|
|
70
|
+
<Text style={[previewStyles.statusText, { color: srgbToHex(tokens.textPrimary.srgb) }]}>Error</Text>
|
|
71
|
+
</View>
|
|
72
|
+
</View>
|
|
73
|
+
</Card>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function PreviewPanel({ state, dispatch, themeConfig }: PreviewPanelProps) {
|
|
78
|
+
return (
|
|
79
|
+
<NewtoneProvider
|
|
80
|
+
key={`${state.preview.mode}-${state.preview.theme}`}
|
|
81
|
+
config={themeConfig}
|
|
82
|
+
initialMode={state.preview.mode}
|
|
83
|
+
initialTheme={state.preview.theme}
|
|
84
|
+
>
|
|
85
|
+
<PreviewCard state={state} dispatch={dispatch} />
|
|
86
|
+
</NewtoneProvider>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const styles = StyleSheet.create({
|
|
91
|
+
container: {
|
|
92
|
+
gap: 12,
|
|
93
|
+
},
|
|
94
|
+
title: {
|
|
95
|
+
fontSize: 16,
|
|
96
|
+
fontWeight: '700',
|
|
97
|
+
},
|
|
98
|
+
controls: {
|
|
99
|
+
flexDirection: 'row',
|
|
100
|
+
alignItems: 'flex-end',
|
|
101
|
+
gap: 16,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const previewStyles = StyleSheet.create({
|
|
106
|
+
wrapper: {
|
|
107
|
+
gap: 12,
|
|
108
|
+
},
|
|
109
|
+
row: {
|
|
110
|
+
flexDirection: 'row',
|
|
111
|
+
alignItems: 'center',
|
|
112
|
+
gap: 8,
|
|
113
|
+
flexWrap: 'wrap',
|
|
114
|
+
},
|
|
115
|
+
statusDot: {
|
|
116
|
+
width: 12,
|
|
117
|
+
height: 12,
|
|
118
|
+
borderRadius: 6,
|
|
119
|
+
},
|
|
120
|
+
statusText: {
|
|
121
|
+
fontSize: 13,
|
|
122
|
+
marginRight: 8,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DesaturationStrength, HueGradingStrength } from 'newtone';
|
|
2
|
+
import type { ColorMode, ThemeName } from '@newtonedev/components';
|
|
3
|
+
import type { ConfiguratorState } from '../types';
|
|
4
|
+
|
|
5
|
+
export type ConfiguratorAction =
|
|
6
|
+
// Palette actions
|
|
7
|
+
| { readonly type: 'SET_PALETTE_HUE'; readonly index: number; readonly hue: number }
|
|
8
|
+
| { readonly type: 'SET_PALETTE_SATURATION'; readonly index: number; readonly saturation: number }
|
|
9
|
+
| { readonly type: 'SET_PALETTE_DESAT_STRENGTH'; readonly index: number; readonly strength: DesaturationStrength }
|
|
10
|
+
| { readonly type: 'SET_PALETTE_DESAT_DIRECTION'; readonly index: number; readonly direction: 'light' | 'dark' }
|
|
11
|
+
| { readonly type: 'SET_PALETTE_HUE_GRADE_STRENGTH'; readonly index: number; readonly strength: HueGradingStrength }
|
|
12
|
+
| { readonly type: 'SET_PALETTE_HUE_GRADE_HUE'; readonly index: number; readonly hue: number }
|
|
13
|
+
| { readonly type: 'SET_PALETTE_HUE_GRADE_DIRECTION'; readonly index: number; readonly direction: 'light' | 'dark' }
|
|
14
|
+
// Dynamic range actions
|
|
15
|
+
| { readonly type: 'SET_LIGHTEST'; readonly value: number }
|
|
16
|
+
| { readonly type: 'SET_DARKEST'; readonly value: number }
|
|
17
|
+
// Global hue grading actions
|
|
18
|
+
| { readonly type: 'SET_GLOBAL_GRADE_LIGHT_STRENGTH'; readonly strength: HueGradingStrength }
|
|
19
|
+
| { readonly type: 'SET_GLOBAL_GRADE_LIGHT_HUE'; readonly hue: number }
|
|
20
|
+
| { readonly type: 'SET_GLOBAL_GRADE_DARK_STRENGTH'; readonly strength: HueGradingStrength }
|
|
21
|
+
| { readonly type: 'SET_GLOBAL_GRADE_DARK_HUE'; readonly hue: number }
|
|
22
|
+
// Preview actions
|
|
23
|
+
| { readonly type: 'SET_PREVIEW_MODE'; readonly mode: ColorMode }
|
|
24
|
+
| { readonly type: 'SET_PREVIEW_THEME'; readonly theme: ThemeName }
|
|
25
|
+
// Control actions
|
|
26
|
+
| { readonly type: 'RESET' }
|
|
27
|
+
| { readonly type: 'LOAD_STATE'; readonly state: ConfiguratorState };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ConfiguratorState } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default configurator state.
|
|
5
|
+
* Traditional hues match the playground defaults.
|
|
6
|
+
* These are HSL hues (0=red, 60=yellow, 120=green, etc.),
|
|
7
|
+
* NOT OKLCH hues — the bridge converts them.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_CONFIGURATOR_STATE: ConfiguratorState = {
|
|
10
|
+
palettes: [
|
|
11
|
+
{
|
|
12
|
+
name: 'Neutral',
|
|
13
|
+
hue: 220,
|
|
14
|
+
saturation: 24,
|
|
15
|
+
desaturationStrength: 'none',
|
|
16
|
+
desaturationDirection: 'light',
|
|
17
|
+
hueGradeStrength: 'none',
|
|
18
|
+
hueGradeHue: 0,
|
|
19
|
+
hueGradeDirection: 'light',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'Accent',
|
|
23
|
+
hue: 24,
|
|
24
|
+
saturation: 80,
|
|
25
|
+
desaturationStrength: 'none',
|
|
26
|
+
desaturationDirection: 'light',
|
|
27
|
+
hueGradeStrength: 'none',
|
|
28
|
+
hueGradeHue: 0,
|
|
29
|
+
hueGradeDirection: 'light',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Success',
|
|
33
|
+
hue: 145,
|
|
34
|
+
saturation: 64,
|
|
35
|
+
desaturationStrength: 'none',
|
|
36
|
+
desaturationDirection: 'light',
|
|
37
|
+
hueGradeStrength: 'none',
|
|
38
|
+
hueGradeHue: 0,
|
|
39
|
+
hueGradeDirection: 'light',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Warning',
|
|
43
|
+
hue: 40,
|
|
44
|
+
saturation: 80,
|
|
45
|
+
desaturationStrength: 'none',
|
|
46
|
+
desaturationDirection: 'light',
|
|
47
|
+
hueGradeStrength: 'none',
|
|
48
|
+
hueGradeHue: 0,
|
|
49
|
+
hueGradeDirection: 'light',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Error',
|
|
53
|
+
hue: 0,
|
|
54
|
+
saturation: 80,
|
|
55
|
+
desaturationStrength: 'none',
|
|
56
|
+
desaturationDirection: 'light',
|
|
57
|
+
hueGradeStrength: 'none',
|
|
58
|
+
hueGradeHue: 0,
|
|
59
|
+
hueGradeDirection: 'light',
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
dynamicRange: {
|
|
63
|
+
lightest: 1,
|
|
64
|
+
darkest: 1,
|
|
65
|
+
},
|
|
66
|
+
globalHueGrading: {
|
|
67
|
+
light: { strength: 'none', hue: 30 },
|
|
68
|
+
dark: { strength: 'none', hue: 220 },
|
|
69
|
+
},
|
|
70
|
+
preview: {
|
|
71
|
+
mode: 'light',
|
|
72
|
+
theme: 'neutral',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { ConfiguratorState, PaletteState } from '../types';
|
|
2
|
+
import type { ConfiguratorAction } from './actions';
|
|
3
|
+
import { DEFAULT_CONFIGURATOR_STATE } from './defaults';
|
|
4
|
+
|
|
5
|
+
function updatePalette(
|
|
6
|
+
palettes: readonly PaletteState[],
|
|
7
|
+
index: number,
|
|
8
|
+
update: Partial<PaletteState>,
|
|
9
|
+
): readonly PaletteState[] {
|
|
10
|
+
if (index < 0 || index >= palettes.length) return palettes;
|
|
11
|
+
return palettes.map((p, i) => (i === index ? { ...p, ...update } : p));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function clamp(value: number, min: number, max: number): number {
|
|
15
|
+
return Math.max(min, Math.min(max, value));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function wrapHue(hue: number): number {
|
|
19
|
+
return ((hue % 360) + 360) % 360;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function configuratorReducer(
|
|
23
|
+
state: ConfiguratorState,
|
|
24
|
+
action: ConfiguratorAction,
|
|
25
|
+
): ConfiguratorState {
|
|
26
|
+
switch (action.type) {
|
|
27
|
+
// Palette actions
|
|
28
|
+
case 'SET_PALETTE_HUE':
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
32
|
+
hue: wrapHue(action.hue),
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
case 'SET_PALETTE_SATURATION':
|
|
37
|
+
return {
|
|
38
|
+
...state,
|
|
39
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
40
|
+
saturation: clamp(action.saturation, 0, 100),
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
case 'SET_PALETTE_DESAT_STRENGTH':
|
|
45
|
+
return {
|
|
46
|
+
...state,
|
|
47
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
48
|
+
desaturationStrength: action.strength,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
case 'SET_PALETTE_DESAT_DIRECTION':
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
56
|
+
desaturationDirection: action.direction,
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
case 'SET_PALETTE_HUE_GRADE_STRENGTH':
|
|
61
|
+
return {
|
|
62
|
+
...state,
|
|
63
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
64
|
+
hueGradeStrength: action.strength,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
case 'SET_PALETTE_HUE_GRADE_HUE':
|
|
69
|
+
return {
|
|
70
|
+
...state,
|
|
71
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
72
|
+
hueGradeHue: wrapHue(action.hue),
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
case 'SET_PALETTE_HUE_GRADE_DIRECTION':
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
80
|
+
hueGradeDirection: action.direction,
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Dynamic range actions
|
|
85
|
+
case 'SET_LIGHTEST':
|
|
86
|
+
return {
|
|
87
|
+
...state,
|
|
88
|
+
dynamicRange: {
|
|
89
|
+
...state.dynamicRange,
|
|
90
|
+
lightest: clamp(action.value, 0, 1),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
case 'SET_DARKEST':
|
|
95
|
+
return {
|
|
96
|
+
...state,
|
|
97
|
+
dynamicRange: {
|
|
98
|
+
...state.dynamicRange,
|
|
99
|
+
darkest: clamp(action.value, 0, 1),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Global hue grading actions
|
|
104
|
+
case 'SET_GLOBAL_GRADE_LIGHT_STRENGTH':
|
|
105
|
+
return {
|
|
106
|
+
...state,
|
|
107
|
+
globalHueGrading: {
|
|
108
|
+
...state.globalHueGrading,
|
|
109
|
+
light: { ...state.globalHueGrading.light, strength: action.strength },
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
case 'SET_GLOBAL_GRADE_LIGHT_HUE':
|
|
114
|
+
return {
|
|
115
|
+
...state,
|
|
116
|
+
globalHueGrading: {
|
|
117
|
+
...state.globalHueGrading,
|
|
118
|
+
light: { ...state.globalHueGrading.light, hue: wrapHue(action.hue) },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
case 'SET_GLOBAL_GRADE_DARK_STRENGTH':
|
|
123
|
+
return {
|
|
124
|
+
...state,
|
|
125
|
+
globalHueGrading: {
|
|
126
|
+
...state.globalHueGrading,
|
|
127
|
+
dark: { ...state.globalHueGrading.dark, strength: action.strength },
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
case 'SET_GLOBAL_GRADE_DARK_HUE':
|
|
132
|
+
return {
|
|
133
|
+
...state,
|
|
134
|
+
globalHueGrading: {
|
|
135
|
+
...state.globalHueGrading,
|
|
136
|
+
dark: { ...state.globalHueGrading.dark, hue: wrapHue(action.hue) },
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Preview actions
|
|
141
|
+
case 'SET_PREVIEW_MODE':
|
|
142
|
+
return {
|
|
143
|
+
...state,
|
|
144
|
+
preview: { ...state.preview, mode: action.mode },
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
case 'SET_PREVIEW_THEME':
|
|
148
|
+
return {
|
|
149
|
+
...state,
|
|
150
|
+
preview: { ...state.preview, theme: action.theme },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Control actions
|
|
154
|
+
case 'RESET':
|
|
155
|
+
return DEFAULT_CONFIGURATOR_STATE;
|
|
156
|
+
|
|
157
|
+
case 'LOAD_STATE':
|
|
158
|
+
return action.state;
|
|
159
|
+
|
|
160
|
+
default:
|
|
161
|
+
return state;
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DesaturationStrength, HueGradingStrength } from 'newtone';
|
|
2
|
+
import type { ColorMode, ThemeName } from '@newtonedev/components';
|
|
3
|
+
|
|
4
|
+
/** Per-palette state in the configurator (human-readable, traditional hues) */
|
|
5
|
+
export interface PaletteState {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly hue: number; // Traditional HSL hue [0, 359]
|
|
8
|
+
readonly saturation: number; // [0, 100]
|
|
9
|
+
readonly desaturationStrength: DesaturationStrength; // 'none' | 'low' | 'medium' | 'hard'
|
|
10
|
+
readonly desaturationDirection: 'light' | 'dark';
|
|
11
|
+
readonly hueGradeStrength: HueGradingStrength; // 'none' | 'low' | 'medium' | 'hard'
|
|
12
|
+
readonly hueGradeHue: number; // Traditional HSL hue [0, 359]
|
|
13
|
+
readonly hueGradeDirection: 'light' | 'dark';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Global hue grading endpoint state */
|
|
17
|
+
export interface HueGradingEndpointState {
|
|
18
|
+
readonly strength: HueGradingStrength;
|
|
19
|
+
readonly hue: number; // Traditional HSL hue [0, 359]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Complete configurator state */
|
|
23
|
+
export interface ConfiguratorState {
|
|
24
|
+
readonly palettes: readonly PaletteState[];
|
|
25
|
+
readonly dynamicRange: {
|
|
26
|
+
readonly lightest: number; // [0, 1]
|
|
27
|
+
readonly darkest: number; // [0, 1]
|
|
28
|
+
};
|
|
29
|
+
readonly globalHueGrading: {
|
|
30
|
+
readonly light: HueGradingEndpointState;
|
|
31
|
+
readonly dark: HueGradingEndpointState;
|
|
32
|
+
};
|
|
33
|
+
readonly preview: {
|
|
34
|
+
readonly mode: ColorMode;
|
|
35
|
+
readonly theme: ThemeName;
|
|
36
|
+
};
|
|
37
|
+
}
|