@newtonedev/configurator 0.1.0 → 0.1.1
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.map +1 -1
- package/dist/bridge/toCSS.d.ts.map +1 -1
- package/dist/bridge/toThemeConfig.d.ts.map +1 -1
- package/dist/hex-conversion.d.ts +25 -0
- package/dist/hex-conversion.d.ts.map +1 -0
- package/dist/hooks/useWcagValidation.d.ts +20 -0
- package/dist/hooks/useWcagValidation.d.ts.map +1 -0
- package/dist/index.cjs +668 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +654 -39
- package/dist/index.js.map +1 -1
- package/dist/panels/DesignPanel.d.ts +10 -0
- package/dist/panels/DesignPanel.d.ts.map +1 -0
- package/dist/panels/PalettePanel.d.ts +3 -2
- package/dist/panels/PalettePanel.d.ts.map +1 -1
- package/dist/state/actions.d.ts +57 -1
- package/dist/state/actions.d.ts.map +1 -1
- package/dist/state/defaults.d.ts.map +1 -1
- package/dist/state/reducer.d.ts.map +1 -1
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Configurator.tsx +10 -0
- package/src/bridge/toCSS.ts +4 -0
- package/src/bridge/toThemeConfig.ts +101 -2
- package/src/hex-conversion.ts +99 -0
- package/src/hooks/useWcagValidation.ts +111 -0
- package/src/index.ts +8 -1
- package/src/panels/DesignPanel.tsx +145 -0
- package/src/panels/PalettePanel.tsx +182 -6
- package/src/state/actions.ts +21 -1
- package/src/state/defaults.ts +31 -0
- package/src/state/reducer.ts +181 -0
- package/src/types.ts +32 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
-
import { Card, HueSlider, Slider, Select, Toggle, useTokens } from '@newtonedev/components';
|
|
2
|
+
import { View, Text, Pressable, StyleSheet } from 'react-native';
|
|
3
|
+
import { Card, HueSlider, Slider, Select, Toggle, ColorScaleSlider, TextInput, useTokens } from '@newtonedev/components';
|
|
4
4
|
import { srgbToHex } from 'newtone';
|
|
5
|
-
import type { ColorResult } from 'newtone';
|
|
6
|
-
import type { PaletteState } from '../types';
|
|
5
|
+
import type { ColorResult, DynamicRange } from 'newtone';
|
|
6
|
+
import type { ConfiguratorState, PaletteState } from '../types';
|
|
7
7
|
import type { ConfiguratorAction } from '../state/actions';
|
|
8
8
|
import type { DesaturationStrength, HueGradingStrength } from 'newtone';
|
|
9
9
|
import { SEMANTIC_HUE_RANGES } from '../constants';
|
|
10
|
+
import { useWcagValidation } from '../hooks/useWcagValidation';
|
|
11
|
+
import { hexToPaletteParams } from '../hex-conversion';
|
|
12
|
+
import { traditionalHueToOklch } from '../hue-conversion';
|
|
10
13
|
|
|
11
14
|
const STRENGTH_OPTIONS = [
|
|
12
15
|
{ label: 'None', value: 'none' },
|
|
@@ -20,11 +23,120 @@ interface PalettePanelProps {
|
|
|
20
23
|
readonly index: number;
|
|
21
24
|
readonly dispatch: (action: ConfiguratorAction) => void;
|
|
22
25
|
readonly previewColors?: readonly ColorResult[];
|
|
26
|
+
readonly state: ConfiguratorState;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
/** Get the hex color at a normalizedValue from the preview colors array */
|
|
30
|
+
function getHexAtNv(previewColors: readonly ColorResult[], nv: number): string {
|
|
31
|
+
const idx = Math.round((1 - nv) * (previewColors.length - 1));
|
|
32
|
+
const clamped = Math.max(0, Math.min(previewColors.length - 1, idx));
|
|
33
|
+
return srgbToHex(previewColors[clamped].srgb);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function PalettePanel({ palette, index, dispatch, previewColors, state }: PalettePanelProps) {
|
|
26
37
|
const tokens = useTokens(1);
|
|
27
38
|
const hueRange = SEMANTIC_HUE_RANGES[index];
|
|
39
|
+
const isNeutral = index === 0;
|
|
40
|
+
const mode = state.preview.mode;
|
|
41
|
+
|
|
42
|
+
// Resolve per-mode key color
|
|
43
|
+
const effectiveKeyColor = mode === 'dark' ? palette.keyColorDark : palette.keyColor;
|
|
44
|
+
|
|
45
|
+
// Mode-aware action types
|
|
46
|
+
const setKeyColorAction = mode === 'dark' ? 'SET_PALETTE_KEY_COLOR_DARK' as const : 'SET_PALETTE_KEY_COLOR' as const;
|
|
47
|
+
const clearKeyColorAction = mode === 'dark' ? 'CLEAR_PALETTE_KEY_COLOR_DARK' as const : 'CLEAR_PALETTE_KEY_COLOR' as const;
|
|
48
|
+
const hexActionType = mode === 'dark' ? 'SET_PALETTE_FROM_HEX_DARK' as const : 'SET_PALETTE_FROM_HEX' as const;
|
|
49
|
+
|
|
50
|
+
// WCAG validation for non-neutral palettes (already mode-aware)
|
|
51
|
+
const wcag = useWcagValidation(state, index);
|
|
52
|
+
|
|
53
|
+
// Hex input state
|
|
54
|
+
const [hexText, setHexText] = React.useState('');
|
|
55
|
+
const [hexError, setHexError] = React.useState('');
|
|
56
|
+
const [isEditingHex, setIsEditingHex] = React.useState(false);
|
|
57
|
+
const [isHexUserSet, setIsHexUserSet] = React.useState(false);
|
|
58
|
+
|
|
59
|
+
// Reset hex input state when mode changes
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
setHexText('');
|
|
62
|
+
setHexError('');
|
|
63
|
+
setIsEditingHex(false);
|
|
64
|
+
setIsHexUserSet(false);
|
|
65
|
+
}, [mode]);
|
|
66
|
+
|
|
67
|
+
// Compute displayed hex from current key color position
|
|
68
|
+
const displayedHex = React.useMemo(() => {
|
|
69
|
+
if (!previewColors || previewColors.length === 0) return '';
|
|
70
|
+
const nv = effectiveKeyColor ?? wcag.autoNormalizedValue;
|
|
71
|
+
return getHexAtNv(previewColors, nv);
|
|
72
|
+
}, [previewColors, effectiveKeyColor, wcag.autoNormalizedValue]);
|
|
73
|
+
|
|
74
|
+
// Sync hex text when not actively editing and not user-submitted
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (!isEditingHex && !isHexUserSet) {
|
|
77
|
+
setHexText(displayedHex);
|
|
78
|
+
}
|
|
79
|
+
}, [displayedHex, isEditingHex, isHexUserSet]);
|
|
80
|
+
|
|
81
|
+
// Build dynamic range for hex conversion
|
|
82
|
+
const dynamicRange = React.useMemo((): DynamicRange => {
|
|
83
|
+
const light = state.globalHueGrading.light.strength !== 'none'
|
|
84
|
+
? { hue: traditionalHueToOklch(state.globalHueGrading.light.hue), strength: state.globalHueGrading.light.strength }
|
|
85
|
+
: undefined;
|
|
86
|
+
const dark = state.globalHueGrading.dark.strength !== 'none'
|
|
87
|
+
? { hue: traditionalHueToOklch(state.globalHueGrading.dark.hue), strength: state.globalHueGrading.dark.strength }
|
|
88
|
+
: undefined;
|
|
89
|
+
const hueGrading = (light || dark) ? { light, dark } : undefined;
|
|
90
|
+
return {
|
|
91
|
+
lightest: state.dynamicRange.lightest,
|
|
92
|
+
darkest: state.dynamicRange.darkest,
|
|
93
|
+
...(hueGrading ? { hueGrading } : {}),
|
|
94
|
+
};
|
|
95
|
+
}, [state.dynamicRange, state.globalHueGrading]);
|
|
96
|
+
|
|
97
|
+
const handleHexSubmit = React.useCallback(() => {
|
|
98
|
+
setIsEditingHex(false);
|
|
99
|
+
const trimmed = hexText.trim();
|
|
100
|
+
if (!trimmed) {
|
|
101
|
+
setHexError('');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const hex = trimmed.startsWith('#') ? trimmed : `#${trimmed}`;
|
|
106
|
+
const params = hexToPaletteParams(hex, dynamicRange);
|
|
107
|
+
|
|
108
|
+
if (!params) {
|
|
109
|
+
setHexError('Invalid hex color');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setHexError('');
|
|
114
|
+
setIsHexUserSet(true);
|
|
115
|
+
dispatch({
|
|
116
|
+
type: hexActionType,
|
|
117
|
+
index,
|
|
118
|
+
hue: params.hue,
|
|
119
|
+
saturation: params.saturation,
|
|
120
|
+
keyColor: params.normalizedValue,
|
|
121
|
+
});
|
|
122
|
+
}, [hexText, dynamicRange, dispatch, index, hexActionType]);
|
|
123
|
+
|
|
124
|
+
const handleClearKeyColor = React.useCallback(() => {
|
|
125
|
+
dispatch({ type: clearKeyColorAction, index });
|
|
126
|
+
setHexError('');
|
|
127
|
+
setIsHexUserSet(false);
|
|
128
|
+
}, [dispatch, index, clearKeyColorAction]);
|
|
129
|
+
|
|
130
|
+
// Build WCAG warning message
|
|
131
|
+
const wcagWarning = React.useMemo(() => {
|
|
132
|
+
if (effectiveKeyColor === undefined || wcag.keyColorContrast === null) return undefined;
|
|
133
|
+
if (wcag.passesAA) return undefined;
|
|
134
|
+
const ratio = wcag.keyColorContrast.toFixed(1);
|
|
135
|
+
if (wcag.passesAALargeText) {
|
|
136
|
+
return `Contrast ${ratio}:1 — passes large text (AA) but fails normal text (requires 4.5:1)`;
|
|
137
|
+
}
|
|
138
|
+
return `Contrast ${ratio}:1 — fails WCAG AA (requires 4.5:1 for normal text, 3:1 for large text)`;
|
|
139
|
+
}, [effectiveKeyColor, wcag]);
|
|
28
140
|
|
|
29
141
|
return (
|
|
30
142
|
<Card elevation={1} style={styles.container}>
|
|
@@ -32,7 +144,52 @@ export function PalettePanel({ palette, index, dispatch, previewColors }: Palett
|
|
|
32
144
|
{palette.name}
|
|
33
145
|
</Text>
|
|
34
146
|
|
|
35
|
-
{
|
|
147
|
+
{/* Non-neutral: interactive luminosity slider + hex input */}
|
|
148
|
+
{!isNeutral && previewColors && (
|
|
149
|
+
<>
|
|
150
|
+
<ColorScaleSlider
|
|
151
|
+
colors={previewColors}
|
|
152
|
+
value={effectiveKeyColor ?? wcag.autoNormalizedValue}
|
|
153
|
+
onValueChange={(nv) => { setIsHexUserSet(false); dispatch({ type: setKeyColorAction, index, normalizedValue: nv }); }}
|
|
154
|
+
label="Key Color"
|
|
155
|
+
warning={wcagWarning}
|
|
156
|
+
trimEnds
|
|
157
|
+
snap
|
|
158
|
+
animateValue
|
|
159
|
+
/>
|
|
160
|
+
<View style={styles.hexRow}>
|
|
161
|
+
<View style={styles.hexInputContainer}>
|
|
162
|
+
<TextInput
|
|
163
|
+
label="Hex"
|
|
164
|
+
value={hexText}
|
|
165
|
+
onChangeText={(text) => {
|
|
166
|
+
setIsEditingHex(true);
|
|
167
|
+
setHexText(text);
|
|
168
|
+
setHexError('');
|
|
169
|
+
}}
|
|
170
|
+
onBlur={handleHexSubmit}
|
|
171
|
+
onSubmitEditing={handleHexSubmit}
|
|
172
|
+
placeholder="#000000"
|
|
173
|
+
/>
|
|
174
|
+
</View>
|
|
175
|
+
{effectiveKeyColor !== undefined && (
|
|
176
|
+
<Pressable onPress={handleClearKeyColor} style={styles.autoButton}>
|
|
177
|
+
<Text style={[styles.autoText, { color: srgbToHex(tokens.interactive.srgb) }]}>
|
|
178
|
+
Auto
|
|
179
|
+
</Text>
|
|
180
|
+
</Pressable>
|
|
181
|
+
)}
|
|
182
|
+
</View>
|
|
183
|
+
{hexError !== '' && (
|
|
184
|
+
<Text style={[styles.errorText, { color: srgbToHex(tokens.error.srgb) }]}>
|
|
185
|
+
{hexError}
|
|
186
|
+
</Text>
|
|
187
|
+
)}
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{/* Neutral: static swatches (unchanged) */}
|
|
192
|
+
{isNeutral && previewColors && (
|
|
36
193
|
<View style={styles.inlineSwatches}>
|
|
37
194
|
{previewColors.map((color, i) => (
|
|
38
195
|
<View
|
|
@@ -142,4 +299,23 @@ const styles = StyleSheet.create({
|
|
|
142
299
|
toggleContainer: {
|
|
143
300
|
paddingBottom: 2,
|
|
144
301
|
},
|
|
302
|
+
hexRow: {
|
|
303
|
+
flexDirection: 'row',
|
|
304
|
+
alignItems: 'flex-end',
|
|
305
|
+
gap: 8,
|
|
306
|
+
},
|
|
307
|
+
hexInputContainer: {
|
|
308
|
+
flex: 1,
|
|
309
|
+
},
|
|
310
|
+
autoButton: {
|
|
311
|
+
paddingBottom: 6,
|
|
312
|
+
},
|
|
313
|
+
autoText: {
|
|
314
|
+
fontSize: 13,
|
|
315
|
+
fontWeight: '600',
|
|
316
|
+
},
|
|
317
|
+
errorText: {
|
|
318
|
+
fontSize: 12,
|
|
319
|
+
fontWeight: '500',
|
|
320
|
+
},
|
|
145
321
|
});
|
package/src/state/actions.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DesaturationStrength, HueGradingStrength } from 'newtone';
|
|
2
2
|
import type { ColorMode, ThemeName } from '@newtonedev/components';
|
|
3
|
-
import type { ConfiguratorState } from '../types';
|
|
3
|
+
import type { ConfiguratorState, FontConfig } from '../types';
|
|
4
4
|
|
|
5
5
|
export type ConfiguratorAction =
|
|
6
6
|
// Palette actions
|
|
@@ -11,6 +11,12 @@ export type ConfiguratorAction =
|
|
|
11
11
|
| { readonly type: 'SET_PALETTE_HUE_GRADE_STRENGTH'; readonly index: number; readonly strength: HueGradingStrength }
|
|
12
12
|
| { readonly type: 'SET_PALETTE_HUE_GRADE_HUE'; readonly index: number; readonly hue: number }
|
|
13
13
|
| { readonly type: 'SET_PALETTE_HUE_GRADE_DIRECTION'; readonly index: number; readonly direction: 'light' | 'dark' }
|
|
14
|
+
| { readonly type: 'SET_PALETTE_KEY_COLOR'; readonly index: number; readonly normalizedValue: number }
|
|
15
|
+
| { readonly type: 'CLEAR_PALETTE_KEY_COLOR'; readonly index: number }
|
|
16
|
+
| { readonly type: 'SET_PALETTE_FROM_HEX'; readonly index: number; readonly hue: number; readonly saturation: number; readonly keyColor: number }
|
|
17
|
+
| { readonly type: 'SET_PALETTE_KEY_COLOR_DARK'; readonly index: number; readonly normalizedValue: number }
|
|
18
|
+
| { readonly type: 'CLEAR_PALETTE_KEY_COLOR_DARK'; readonly index: number }
|
|
19
|
+
| { readonly type: 'SET_PALETTE_FROM_HEX_DARK'; readonly index: number; readonly hue: number; readonly saturation: number; readonly keyColor: number }
|
|
14
20
|
// Dynamic range actions
|
|
15
21
|
| { readonly type: 'SET_LIGHTEST'; readonly value: number }
|
|
16
22
|
| { readonly type: 'SET_DARKEST'; readonly value: number }
|
|
@@ -19,6 +25,20 @@ export type ConfiguratorAction =
|
|
|
19
25
|
| { readonly type: 'SET_GLOBAL_GRADE_LIGHT_HUE'; readonly hue: number }
|
|
20
26
|
| { readonly type: 'SET_GLOBAL_GRADE_DARK_STRENGTH'; readonly strength: HueGradingStrength }
|
|
21
27
|
| { readonly type: 'SET_GLOBAL_GRADE_DARK_HUE'; readonly hue: number }
|
|
28
|
+
// Spacing actions
|
|
29
|
+
| { readonly type: 'SET_SPACING_DENSITY'; readonly density: number }
|
|
30
|
+
// Roundness actions
|
|
31
|
+
| { readonly type: 'SET_ROUNDNESS_INTENSITY'; readonly intensity: number }
|
|
32
|
+
// Typography actions
|
|
33
|
+
| { readonly type: 'SET_TYPOGRAPHY_BASE_SIZE'; readonly baseSize: number }
|
|
34
|
+
| { readonly type: 'SET_TYPOGRAPHY_RATIO'; readonly ratio: number }
|
|
35
|
+
| { readonly type: 'SET_FONT_MONO'; readonly font: FontConfig }
|
|
36
|
+
| { readonly type: 'SET_FONT_DISPLAY'; readonly font: FontConfig }
|
|
37
|
+
| { readonly type: 'SET_FONT_DEFAULT'; readonly font: FontConfig }
|
|
38
|
+
// Icons actions
|
|
39
|
+
| { readonly type: 'SET_ICON_VARIANT'; readonly variant: 'outlined' | 'rounded' | 'sharp' }
|
|
40
|
+
| { readonly type: 'SET_ICON_WEIGHT'; readonly weight: 100 | 200 | 300 | 400 | 500 | 600 | 700 }
|
|
41
|
+
| { readonly type: 'SET_ICON_AUTO_GRADE'; readonly autoGrade: boolean }
|
|
22
42
|
// Preview actions
|
|
23
43
|
| { readonly type: 'SET_PREVIEW_MODE'; readonly mode: ColorMode }
|
|
24
44
|
| { readonly type: 'SET_PREVIEW_THEME'; readonly theme: ThemeName }
|
package/src/state/defaults.ts
CHANGED
|
@@ -71,4 +71,35 @@ export const DEFAULT_CONFIGURATOR_STATE: ConfiguratorState = {
|
|
|
71
71
|
mode: 'light',
|
|
72
72
|
theme: 'neutral',
|
|
73
73
|
},
|
|
74
|
+
spacing: {
|
|
75
|
+
density: 0.5, // 1.0x multiplier = preserves current hardcoded values
|
|
76
|
+
},
|
|
77
|
+
roundness: {
|
|
78
|
+
intensity: 0.5, // 1.0x multiplier = preserves current hardcoded values
|
|
79
|
+
},
|
|
80
|
+
typography: {
|
|
81
|
+
fonts: {
|
|
82
|
+
mono: {
|
|
83
|
+
type: 'system',
|
|
84
|
+
family: 'ui-monospace',
|
|
85
|
+
fallback: 'SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
86
|
+
},
|
|
87
|
+
display: {
|
|
88
|
+
type: 'system',
|
|
89
|
+
family: 'system-ui',
|
|
90
|
+
fallback: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
91
|
+
},
|
|
92
|
+
default: {
|
|
93
|
+
type: 'system',
|
|
94
|
+
family: 'system-ui',
|
|
95
|
+
fallback: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
scale: { baseSize: 16, ratio: 1.25 },
|
|
99
|
+
},
|
|
100
|
+
icons: {
|
|
101
|
+
variant: 'rounded', // Material Design 3 aesthetic
|
|
102
|
+
weight: 400, // Normal weight
|
|
103
|
+
autoGrade: true, // Enable mode-aware grade
|
|
104
|
+
},
|
|
74
105
|
};
|
package/src/state/reducer.ts
CHANGED
|
@@ -81,6 +81,58 @@ export function configuratorReducer(
|
|
|
81
81
|
}),
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
case 'SET_PALETTE_KEY_COLOR':
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
88
|
+
keyColor: clamp(action.normalizedValue, 0, 1),
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
case 'CLEAR_PALETTE_KEY_COLOR':
|
|
93
|
+
return {
|
|
94
|
+
...state,
|
|
95
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
96
|
+
keyColor: undefined,
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
case 'SET_PALETTE_FROM_HEX':
|
|
101
|
+
return {
|
|
102
|
+
...state,
|
|
103
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
104
|
+
hue: wrapHue(action.hue),
|
|
105
|
+
saturation: clamp(action.saturation, 0, 100),
|
|
106
|
+
keyColor: clamp(action.keyColor, 0, 1),
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
case 'SET_PALETTE_KEY_COLOR_DARK':
|
|
111
|
+
return {
|
|
112
|
+
...state,
|
|
113
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
114
|
+
keyColorDark: clamp(action.normalizedValue, 0, 1),
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
case 'CLEAR_PALETTE_KEY_COLOR_DARK':
|
|
119
|
+
return {
|
|
120
|
+
...state,
|
|
121
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
122
|
+
keyColorDark: undefined,
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
case 'SET_PALETTE_FROM_HEX_DARK':
|
|
127
|
+
return {
|
|
128
|
+
...state,
|
|
129
|
+
palettes: updatePalette(state.palettes, action.index, {
|
|
130
|
+
hue: wrapHue(action.hue),
|
|
131
|
+
saturation: clamp(action.saturation, 0, 100),
|
|
132
|
+
keyColorDark: clamp(action.keyColor, 0, 1),
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
|
|
84
136
|
// Dynamic range actions
|
|
85
137
|
case 'SET_LIGHTEST':
|
|
86
138
|
return {
|
|
@@ -137,6 +189,135 @@ export function configuratorReducer(
|
|
|
137
189
|
},
|
|
138
190
|
};
|
|
139
191
|
|
|
192
|
+
// Spacing actions
|
|
193
|
+
case 'SET_SPACING_DENSITY':
|
|
194
|
+
return {
|
|
195
|
+
...state,
|
|
196
|
+
spacing: {
|
|
197
|
+
density: clamp(action.density, 0, 1),
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Roundness actions
|
|
202
|
+
case 'SET_ROUNDNESS_INTENSITY':
|
|
203
|
+
return {
|
|
204
|
+
...state,
|
|
205
|
+
roundness: {
|
|
206
|
+
intensity: clamp(action.intensity, 0, 1),
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Typography actions
|
|
211
|
+
case 'SET_TYPOGRAPHY_BASE_SIZE': {
|
|
212
|
+
const defaultTypography = DEFAULT_CONFIGURATOR_STATE.typography!;
|
|
213
|
+
return {
|
|
214
|
+
...state,
|
|
215
|
+
typography: {
|
|
216
|
+
fonts: state.typography?.fonts ?? defaultTypography.fonts,
|
|
217
|
+
scale: {
|
|
218
|
+
baseSize: clamp(action.baseSize, 12, 24),
|
|
219
|
+
ratio: state.typography?.scale.ratio ?? defaultTypography.scale.ratio,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case 'SET_TYPOGRAPHY_RATIO': {
|
|
226
|
+
const defaultTypography = DEFAULT_CONFIGURATOR_STATE.typography!;
|
|
227
|
+
return {
|
|
228
|
+
...state,
|
|
229
|
+
typography: {
|
|
230
|
+
fonts: state.typography?.fonts ?? defaultTypography.fonts,
|
|
231
|
+
scale: {
|
|
232
|
+
baseSize: state.typography?.scale.baseSize ?? defaultTypography.scale.baseSize,
|
|
233
|
+
ratio: clamp(action.ratio, 1.1, 1.5),
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case 'SET_FONT_MONO': {
|
|
240
|
+
const defaultTypography = DEFAULT_CONFIGURATOR_STATE.typography!;
|
|
241
|
+
return {
|
|
242
|
+
...state,
|
|
243
|
+
typography: {
|
|
244
|
+
fonts: {
|
|
245
|
+
mono: action.font,
|
|
246
|
+
display: state.typography?.fonts.display ?? defaultTypography.fonts.display,
|
|
247
|
+
default: state.typography?.fonts.default ?? defaultTypography.fonts.default,
|
|
248
|
+
},
|
|
249
|
+
scale: state.typography?.scale ?? defaultTypography.scale,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
case 'SET_FONT_DISPLAY': {
|
|
255
|
+
const defaultTypography = DEFAULT_CONFIGURATOR_STATE.typography!;
|
|
256
|
+
return {
|
|
257
|
+
...state,
|
|
258
|
+
typography: {
|
|
259
|
+
fonts: {
|
|
260
|
+
mono: state.typography?.fonts.mono ?? defaultTypography.fonts.mono,
|
|
261
|
+
display: action.font,
|
|
262
|
+
default: state.typography?.fonts.default ?? defaultTypography.fonts.default,
|
|
263
|
+
},
|
|
264
|
+
scale: state.typography?.scale ?? defaultTypography.scale,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case 'SET_FONT_DEFAULT': {
|
|
270
|
+
const defaultTypography = DEFAULT_CONFIGURATOR_STATE.typography!;
|
|
271
|
+
return {
|
|
272
|
+
...state,
|
|
273
|
+
typography: {
|
|
274
|
+
fonts: {
|
|
275
|
+
mono: state.typography?.fonts.mono ?? defaultTypography.fonts.mono,
|
|
276
|
+
display: state.typography?.fonts.display ?? defaultTypography.fonts.display,
|
|
277
|
+
default: action.font,
|
|
278
|
+
},
|
|
279
|
+
scale: state.typography?.scale ?? defaultTypography.scale,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Icons actions
|
|
285
|
+
case 'SET_ICON_VARIANT': {
|
|
286
|
+
const defaultIcons = DEFAULT_CONFIGURATOR_STATE.icons!;
|
|
287
|
+
return {
|
|
288
|
+
...state,
|
|
289
|
+
icons: {
|
|
290
|
+
variant: action.variant,
|
|
291
|
+
weight: state.icons?.weight ?? defaultIcons.weight,
|
|
292
|
+
autoGrade: state.icons?.autoGrade ?? defaultIcons.autoGrade,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
case 'SET_ICON_WEIGHT': {
|
|
298
|
+
const defaultIcons = DEFAULT_CONFIGURATOR_STATE.icons!;
|
|
299
|
+
return {
|
|
300
|
+
...state,
|
|
301
|
+
icons: {
|
|
302
|
+
variant: state.icons?.variant ?? defaultIcons.variant,
|
|
303
|
+
weight: action.weight,
|
|
304
|
+
autoGrade: state.icons?.autoGrade ?? defaultIcons.autoGrade,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case 'SET_ICON_AUTO_GRADE': {
|
|
310
|
+
const defaultIcons = DEFAULT_CONFIGURATOR_STATE.icons!;
|
|
311
|
+
return {
|
|
312
|
+
...state,
|
|
313
|
+
icons: {
|
|
314
|
+
variant: state.icons?.variant ?? defaultIcons.variant,
|
|
315
|
+
weight: state.icons?.weight ?? defaultIcons.weight,
|
|
316
|
+
autoGrade: action.autoGrade,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
140
321
|
// Preview actions
|
|
141
322
|
case 'SET_PREVIEW_MODE':
|
|
142
323
|
return {
|
package/src/types.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface PaletteState {
|
|
|
11
11
|
readonly hueGradeStrength: HueGradingStrength; // 'none' | 'low' | 'medium' | 'hard'
|
|
12
12
|
readonly hueGradeHue: number; // Traditional HSL hue [0, 359]
|
|
13
13
|
readonly hueGradeDirection: 'light' | 'dark';
|
|
14
|
+
readonly keyColor?: number; // Light mode normalizedValue [0, 1] — undefined = auto
|
|
15
|
+
readonly keyColorDark?: number; // Dark mode normalizedValue [0, 1] — undefined = auto
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/** Global hue grading endpoint state */
|
|
@@ -19,6 +21,14 @@ export interface HueGradingEndpointState {
|
|
|
19
21
|
readonly hue: number; // Traditional HSL hue [0, 359]
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/** Font configuration for a single font slot */
|
|
25
|
+
export interface FontConfig {
|
|
26
|
+
readonly type: 'system' | 'google' | 'custom';
|
|
27
|
+
readonly family: string; // 'ui-monospace' | 'Roboto' | custom name
|
|
28
|
+
readonly customUrl?: string; // Supabase Storage URL for custom fonts
|
|
29
|
+
readonly fallback: string; // CSS fallback stack
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
/** Complete configurator state */
|
|
23
33
|
export interface ConfiguratorState {
|
|
24
34
|
readonly palettes: readonly PaletteState[];
|
|
@@ -34,4 +44,26 @@ export interface ConfiguratorState {
|
|
|
34
44
|
readonly mode: ColorMode;
|
|
35
45
|
readonly theme: ThemeName;
|
|
36
46
|
};
|
|
47
|
+
readonly spacing?: {
|
|
48
|
+
readonly density: number; // [0, 1] where 0=tight, 1=relaxed
|
|
49
|
+
};
|
|
50
|
+
readonly roundness?: {
|
|
51
|
+
readonly intensity: number; // [0, 1] where 0=rectangle, 1=pill
|
|
52
|
+
};
|
|
53
|
+
readonly typography?: {
|
|
54
|
+
readonly fonts: {
|
|
55
|
+
readonly mono: FontConfig; // currencies, code, amounts
|
|
56
|
+
readonly display: FontConfig; // headlines
|
|
57
|
+
readonly default: FontConfig; // fallback
|
|
58
|
+
};
|
|
59
|
+
readonly scale: {
|
|
60
|
+
readonly baseSize: number; // px (default: 16)
|
|
61
|
+
readonly ratio: number; // scale ratio (default: 1.25)
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
readonly icons?: {
|
|
65
|
+
readonly variant: 'outlined' | 'rounded' | 'sharp';
|
|
66
|
+
readonly weight: 100 | 200 | 300 | 400 | 500 | 600 | 700;
|
|
67
|
+
readonly autoGrade: boolean; // true = mode-aware grade (light=-25, dark=200)
|
|
68
|
+
};
|
|
37
69
|
}
|