@newtonedev/editor 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/Editor.d.ts +3 -0
- package/dist/Editor.d.ts.map +1 -0
- package/dist/components/CodeBlock.d.ts +7 -0
- package/dist/components/CodeBlock.d.ts.map +1 -0
- package/dist/components/EditorHeader.d.ts +16 -0
- package/dist/components/EditorHeader.d.ts.map +1 -0
- package/dist/components/EditorShell.d.ts +10 -0
- package/dist/components/EditorShell.d.ts.map +1 -0
- package/dist/components/FontPicker.d.ts +11 -0
- package/dist/components/FontPicker.d.ts.map +1 -0
- package/dist/components/PresetSelector.d.ts +14 -0
- package/dist/components/PresetSelector.d.ts.map +1 -0
- package/dist/components/PreviewWindow.d.ts +11 -0
- package/dist/components/PreviewWindow.d.ts.map +1 -0
- package/dist/components/RightSidebar.d.ts +12 -0
- package/dist/components/RightSidebar.d.ts.map +1 -0
- package/dist/components/Sidebar.d.ts +25 -0
- package/dist/components/Sidebar.d.ts.map +1 -0
- package/dist/components/TableOfContents.d.ts +9 -0
- package/dist/components/TableOfContents.d.ts.map +1 -0
- package/dist/components/ThemeBar.d.ts +8 -0
- package/dist/components/ThemeBar.d.ts.map +1 -0
- package/dist/components/sections/ColorsSection.d.ts +14 -0
- package/dist/components/sections/ColorsSection.d.ts.map +1 -0
- package/dist/components/sections/DynamicRangeSection.d.ts +9 -0
- package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -0
- package/dist/components/sections/FontsSection.d.ts +9 -0
- package/dist/components/sections/FontsSection.d.ts.map +1 -0
- package/dist/components/sections/IconsSection.d.ts +9 -0
- package/dist/components/sections/IconsSection.d.ts.map +1 -0
- package/dist/components/sections/OthersSection.d.ts +9 -0
- package/dist/components/sections/OthersSection.d.ts.map +1 -0
- package/dist/components/sections/index.d.ts +6 -0
- package/dist/components/sections/index.d.ts.map +1 -0
- package/dist/hooks/useEditorState.d.ts +53 -0
- package/dist/hooks/useEditorState.d.ts.map +1 -0
- package/dist/hooks/useHover.d.ts +8 -0
- package/dist/hooks/useHover.d.ts.map +1 -0
- package/dist/hooks/usePresets.d.ts +33 -0
- package/dist/hooks/usePresets.d.ts.map +1 -0
- package/dist/index.cjs +3846 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3819 -0
- package/dist/index.js.map +1 -0
- package/dist/preview/CategoryView.d.ts +7 -0
- package/dist/preview/CategoryView.d.ts.map +1 -0
- package/dist/preview/ComponentDetailView.d.ts +9 -0
- package/dist/preview/ComponentDetailView.d.ts.map +1 -0
- package/dist/preview/ComponentRenderer.d.ts +7 -0
- package/dist/preview/ComponentRenderer.d.ts.map +1 -0
- package/dist/preview/OverviewView.d.ts +7 -0
- package/dist/preview/OverviewView.d.ts.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/presets.d.ts +5 -0
- package/dist/utils/presets.d.ts.map +1 -0
- package/package.json +51 -0
- package/src/Editor.tsx +128 -0
- package/src/components/CodeBlock.tsx +58 -0
- package/src/components/EditorHeader.tsx +86 -0
- package/src/components/EditorShell.tsx +67 -0
- package/src/components/FontPicker.tsx +351 -0
- package/src/components/PresetSelector.tsx +455 -0
- package/src/components/PreviewWindow.tsx +69 -0
- package/src/components/RightSidebar.tsx +374 -0
- package/src/components/Sidebar.tsx +332 -0
- package/src/components/TableOfContents.tsx +152 -0
- package/src/components/ThemeBar.tsx +76 -0
- package/src/components/sections/ColorsSection.tsx +485 -0
- package/src/components/sections/DynamicRangeSection.tsx +399 -0
- package/src/components/sections/FontsSection.tsx +132 -0
- package/src/components/sections/IconsSection.tsx +66 -0
- package/src/components/sections/OthersSection.tsx +70 -0
- package/src/components/sections/index.ts +5 -0
- package/src/hooks/useEditorState.ts +381 -0
- package/src/hooks/useHover.ts +8 -0
- package/src/hooks/usePresets.ts +254 -0
- package/src/index.ts +52 -0
- package/src/preview/CategoryView.tsx +134 -0
- package/src/preview/ComponentDetailView.tsx +126 -0
- package/src/preview/ComponentRenderer.tsx +107 -0
- package/src/preview/OverviewView.tsx +177 -0
- package/src/types.ts +77 -0
- package/src/utils/presets.ts +24 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
HueSlider,
|
|
4
|
+
Slider,
|
|
5
|
+
Select,
|
|
6
|
+
Toggle,
|
|
7
|
+
ColorScaleSlider,
|
|
8
|
+
TextInput,
|
|
9
|
+
useTokens,
|
|
10
|
+
} from "@newtonedev/components";
|
|
11
|
+
import type { ColorMode } from "@newtonedev/components";
|
|
12
|
+
import { srgbToHex } from "newtone";
|
|
13
|
+
import type { ColorResult, DynamicRange } from "newtone";
|
|
14
|
+
import type { DesaturationStrength, HueGradingStrength } from "newtone";
|
|
15
|
+
import type { ConfiguratorState } from "@newtonedev/configurator";
|
|
16
|
+
import type { ConfiguratorAction } from "@newtonedev/configurator";
|
|
17
|
+
import {
|
|
18
|
+
SEMANTIC_HUE_RANGES,
|
|
19
|
+
useWcagValidation,
|
|
20
|
+
hexToPaletteParams,
|
|
21
|
+
traditionalHueToOklch,
|
|
22
|
+
} from "@newtonedev/configurator";
|
|
23
|
+
|
|
24
|
+
const STRENGTH_OPTIONS = [
|
|
25
|
+
{ label: "None", value: "none" },
|
|
26
|
+
{ label: "Low", value: "low" },
|
|
27
|
+
{ label: "Medium", value: "medium" },
|
|
28
|
+
{ label: "Hard", value: "hard" },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
interface ColorsSectionProps {
|
|
32
|
+
readonly state: ConfiguratorState;
|
|
33
|
+
readonly dispatch: (action: ConfiguratorAction) => void;
|
|
34
|
+
readonly previewColors: readonly (readonly ColorResult[])[];
|
|
35
|
+
readonly colorMode: ColorMode;
|
|
36
|
+
readonly onColorModeChange: (mode: ColorMode) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Get the hex color at a normalizedValue from the preview colors array */
|
|
40
|
+
function getHexAtNv(
|
|
41
|
+
previewColors: readonly ColorResult[],
|
|
42
|
+
nv: number,
|
|
43
|
+
): string {
|
|
44
|
+
const idx = Math.round((1 - nv) * (previewColors.length - 1));
|
|
45
|
+
const clamped = Math.max(0, Math.min(previewColors.length - 1, idx));
|
|
46
|
+
return srgbToHex(previewColors[clamped].srgb);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function ColorsSection({
|
|
50
|
+
state,
|
|
51
|
+
dispatch,
|
|
52
|
+
previewColors,
|
|
53
|
+
colorMode,
|
|
54
|
+
onColorModeChange,
|
|
55
|
+
}: ColorsSectionProps) {
|
|
56
|
+
const tokens = useTokens();
|
|
57
|
+
const [activePaletteIndex, setActivePaletteIndex] = useState(0);
|
|
58
|
+
const [modeToggleHovered, setModeToggleHovered] = useState(false);
|
|
59
|
+
|
|
60
|
+
const palette = state.palettes[activePaletteIndex];
|
|
61
|
+
const hueRange = SEMANTIC_HUE_RANGES[activePaletteIndex];
|
|
62
|
+
const isNeutral = activePaletteIndex === 0;
|
|
63
|
+
|
|
64
|
+
const activeColor = srgbToHex(tokens.interactive.srgb);
|
|
65
|
+
const borderColor = srgbToHex(tokens.border.srgb);
|
|
66
|
+
|
|
67
|
+
// Resolve effective key color for current mode
|
|
68
|
+
const effectiveKeyColor =
|
|
69
|
+
colorMode === "dark" ? palette.keyColorDark : palette.keyColor;
|
|
70
|
+
|
|
71
|
+
// Mode-aware action types
|
|
72
|
+
const setKeyColorAction =
|
|
73
|
+
colorMode === "dark"
|
|
74
|
+
? ("SET_PALETTE_KEY_COLOR_DARK" as const)
|
|
75
|
+
: ("SET_PALETTE_KEY_COLOR" as const);
|
|
76
|
+
const clearKeyColorAction =
|
|
77
|
+
colorMode === "dark"
|
|
78
|
+
? ("CLEAR_PALETTE_KEY_COLOR_DARK" as const)
|
|
79
|
+
: ("CLEAR_PALETTE_KEY_COLOR" as const);
|
|
80
|
+
const hexAction =
|
|
81
|
+
colorMode === "dark"
|
|
82
|
+
? ("SET_PALETTE_FROM_HEX_DARK" as const)
|
|
83
|
+
: ("SET_PALETTE_FROM_HEX" as const);
|
|
84
|
+
|
|
85
|
+
// WCAG validation for key color (already mode-aware)
|
|
86
|
+
const wcag = useWcagValidation(state, activePaletteIndex);
|
|
87
|
+
|
|
88
|
+
// Hex input state
|
|
89
|
+
const [hexText, setHexText] = useState("");
|
|
90
|
+
const [hexError, setHexError] = useState("");
|
|
91
|
+
const [isEditingHex, setIsEditingHex] = useState(false);
|
|
92
|
+
const [isHexUserSet, setIsHexUserSet] = useState(false);
|
|
93
|
+
|
|
94
|
+
// Reset hex input state when mode changes
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
setHexText("");
|
|
97
|
+
setHexError("");
|
|
98
|
+
setIsEditingHex(false);
|
|
99
|
+
setIsHexUserSet(false);
|
|
100
|
+
}, [colorMode]);
|
|
101
|
+
|
|
102
|
+
// Compute displayed hex from current key color position
|
|
103
|
+
const currentPreview = previewColors[activePaletteIndex];
|
|
104
|
+
const displayedHex = useMemo(() => {
|
|
105
|
+
if (!currentPreview || currentPreview.length === 0) return "";
|
|
106
|
+
const nv = effectiveKeyColor ?? wcag.autoNormalizedValue;
|
|
107
|
+
return getHexAtNv(currentPreview, nv);
|
|
108
|
+
}, [currentPreview, effectiveKeyColor, wcag.autoNormalizedValue]);
|
|
109
|
+
|
|
110
|
+
// Sync hex text when not actively editing and not user-submitted
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!isEditingHex && !isHexUserSet) {
|
|
113
|
+
setHexText(displayedHex);
|
|
114
|
+
}
|
|
115
|
+
}, [displayedHex, isEditingHex, isHexUserSet]);
|
|
116
|
+
|
|
117
|
+
// Build dynamic range for hex conversion
|
|
118
|
+
const dynamicRange = useMemo((): DynamicRange => {
|
|
119
|
+
const light =
|
|
120
|
+
state.globalHueGrading.light.strength !== "none"
|
|
121
|
+
? {
|
|
122
|
+
hue: traditionalHueToOklch(state.globalHueGrading.light.hue),
|
|
123
|
+
strength: state.globalHueGrading.light.strength,
|
|
124
|
+
}
|
|
125
|
+
: undefined;
|
|
126
|
+
const dark =
|
|
127
|
+
state.globalHueGrading.dark.strength !== "none"
|
|
128
|
+
? {
|
|
129
|
+
hue: traditionalHueToOklch(state.globalHueGrading.dark.hue),
|
|
130
|
+
strength: state.globalHueGrading.dark.strength,
|
|
131
|
+
}
|
|
132
|
+
: undefined;
|
|
133
|
+
const hueGrading = light || dark ? { light, dark } : undefined;
|
|
134
|
+
return {
|
|
135
|
+
lightest: state.dynamicRange.lightest,
|
|
136
|
+
darkest: state.dynamicRange.darkest,
|
|
137
|
+
...(hueGrading ? { hueGrading } : {}),
|
|
138
|
+
};
|
|
139
|
+
}, [state.dynamicRange, state.globalHueGrading]);
|
|
140
|
+
|
|
141
|
+
const handleHexSubmit = useCallback(() => {
|
|
142
|
+
setIsEditingHex(false);
|
|
143
|
+
const trimmed = hexText.trim();
|
|
144
|
+
if (!trimmed) {
|
|
145
|
+
setHexError("");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hex = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
|
150
|
+
const params = hexToPaletteParams(hex, dynamicRange);
|
|
151
|
+
|
|
152
|
+
if (!params) {
|
|
153
|
+
setHexError("Invalid hex color");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setHexError("");
|
|
158
|
+
setIsHexUserSet(true);
|
|
159
|
+
dispatch({
|
|
160
|
+
type: hexAction,
|
|
161
|
+
index: activePaletteIndex,
|
|
162
|
+
hue: params.hue,
|
|
163
|
+
saturation: params.saturation,
|
|
164
|
+
keyColor: params.normalizedValue,
|
|
165
|
+
});
|
|
166
|
+
}, [hexText, dynamicRange, dispatch, activePaletteIndex, hexAction]);
|
|
167
|
+
|
|
168
|
+
const handleClearKeyColor = useCallback(() => {
|
|
169
|
+
dispatch({ type: clearKeyColorAction, index: activePaletteIndex });
|
|
170
|
+
setHexError("");
|
|
171
|
+
setIsHexUserSet(false);
|
|
172
|
+
}, [dispatch, activePaletteIndex, clearKeyColorAction]);
|
|
173
|
+
|
|
174
|
+
// Build WCAG warning message
|
|
175
|
+
const wcagWarning = useMemo(() => {
|
|
176
|
+
if (effectiveKeyColor === undefined || wcag.keyColorContrast === null)
|
|
177
|
+
return undefined;
|
|
178
|
+
if (wcag.passesAA) return undefined;
|
|
179
|
+
const ratio = wcag.keyColorContrast.toFixed(1);
|
|
180
|
+
if (wcag.passesAALargeText) {
|
|
181
|
+
return `Contrast ${ratio}:1 — passes large text (AA) but fails normal text (requires 4.5:1)`;
|
|
182
|
+
}
|
|
183
|
+
return `Contrast ${ratio}:1 — fails WCAG AA (requires 4.5:1 for normal text, 3:1 for large text)`;
|
|
184
|
+
}, [effectiveKeyColor, wcag]);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
188
|
+
{/* ─── Palette Circles + Mode Toggle ─── */}
|
|
189
|
+
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
|
190
|
+
{state.palettes.map((_p, index) => {
|
|
191
|
+
const isActive = index === activePaletteIndex;
|
|
192
|
+
const colors = previewColors[index];
|
|
193
|
+
const isNeutralCircle = index === 0;
|
|
194
|
+
|
|
195
|
+
// Per-mode key color for this palette's circle
|
|
196
|
+
const paletteKeyColor =
|
|
197
|
+
colorMode === "dark" ? _p.keyColorDark : _p.keyColor;
|
|
198
|
+
const circleColor =
|
|
199
|
+
!isNeutralCircle && colors
|
|
200
|
+
? getHexAtNv(
|
|
201
|
+
colors,
|
|
202
|
+
paletteKeyColor ?? wcag.autoNormalizedValue,
|
|
203
|
+
)
|
|
204
|
+
: undefined;
|
|
205
|
+
|
|
206
|
+
const ringStyle = isActive
|
|
207
|
+
? `0 0 0 2px ${srgbToHex(tokens.background.srgb)}, 0 0 0 4px ${activeColor}`
|
|
208
|
+
: "none";
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<button
|
|
212
|
+
key={index}
|
|
213
|
+
onClick={() => setActivePaletteIndex(index)}
|
|
214
|
+
aria-label={_p.name}
|
|
215
|
+
aria-pressed={isActive}
|
|
216
|
+
style={{
|
|
217
|
+
width: 32,
|
|
218
|
+
height: 32,
|
|
219
|
+
borderRadius: "50%",
|
|
220
|
+
border: "none",
|
|
221
|
+
cursor: "pointer",
|
|
222
|
+
flexShrink: 0,
|
|
223
|
+
boxShadow: ringStyle,
|
|
224
|
+
transition: "box-shadow 150ms ease",
|
|
225
|
+
padding: 0,
|
|
226
|
+
overflow: "hidden",
|
|
227
|
+
...(isNeutralCircle
|
|
228
|
+
? {
|
|
229
|
+
background: colors
|
|
230
|
+
? `linear-gradient(to right, ${srgbToHex(colors[0].srgb)} 50%, ${srgbToHex(colors[colors.length - 1].srgb)} 50%)`
|
|
231
|
+
: `linear-gradient(to right, #ffffff 50%, #000000 50%)`,
|
|
232
|
+
}
|
|
233
|
+
: { backgroundColor: circleColor ?? borderColor }),
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
);
|
|
237
|
+
})}
|
|
238
|
+
|
|
239
|
+
{/* Spacer */}
|
|
240
|
+
<div style={{ flex: 1 }} />
|
|
241
|
+
|
|
242
|
+
{/* Light/Dark Mode Toggle */}
|
|
243
|
+
<button
|
|
244
|
+
onClick={() =>
|
|
245
|
+
onColorModeChange(colorMode === "light" ? "dark" : "light")
|
|
246
|
+
}
|
|
247
|
+
onMouseEnter={() => setModeToggleHovered(true)}
|
|
248
|
+
onMouseLeave={() => setModeToggleHovered(false)}
|
|
249
|
+
aria-label={
|
|
250
|
+
colorMode === "light"
|
|
251
|
+
? "Switch to dark mode"
|
|
252
|
+
: "Switch to light mode"
|
|
253
|
+
}
|
|
254
|
+
style={{
|
|
255
|
+
display: "flex",
|
|
256
|
+
alignItems: "center",
|
|
257
|
+
gap: 6,
|
|
258
|
+
padding: "4px 10px",
|
|
259
|
+
borderRadius: 6,
|
|
260
|
+
border: `1px solid ${borderColor}`,
|
|
261
|
+
background: modeToggleHovered ? `${borderColor}20` : "none",
|
|
262
|
+
cursor: "pointer",
|
|
263
|
+
fontSize: 12,
|
|
264
|
+
color: srgbToHex(tokens.textPrimary.srgb),
|
|
265
|
+
transition: "background-color 150ms ease",
|
|
266
|
+
}}
|
|
267
|
+
>
|
|
268
|
+
{colorMode === "light" ? "\u2600" : "\u263E"}
|
|
269
|
+
<span>{colorMode === "light" ? "Light" : "Dark"}</span>
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* ─── Key Color (mode-specific) ─── */}
|
|
274
|
+
{currentPreview &&
|
|
275
|
+
(isNeutral ? (
|
|
276
|
+
<div style={{ display: "flex", gap: 1 }}>
|
|
277
|
+
{currentPreview.map((color, i) => (
|
|
278
|
+
<div
|
|
279
|
+
key={i}
|
|
280
|
+
style={{
|
|
281
|
+
flex: 1,
|
|
282
|
+
height: 64,
|
|
283
|
+
borderRadius: 2,
|
|
284
|
+
backgroundColor: srgbToHex(color.srgb),
|
|
285
|
+
}}
|
|
286
|
+
/>
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
) : (
|
|
290
|
+
<div
|
|
291
|
+
style={{ display: "flex", flexDirection: "column", gap: 8 }}
|
|
292
|
+
>
|
|
293
|
+
<ColorScaleSlider
|
|
294
|
+
colors={currentPreview}
|
|
295
|
+
value={effectiveKeyColor ?? wcag.autoNormalizedValue}
|
|
296
|
+
onValueChange={(nv) => {
|
|
297
|
+
setIsHexUserSet(false);
|
|
298
|
+
dispatch({
|
|
299
|
+
type: setKeyColorAction,
|
|
300
|
+
index: activePaletteIndex,
|
|
301
|
+
normalizedValue: nv,
|
|
302
|
+
});
|
|
303
|
+
}}
|
|
304
|
+
trimEnds
|
|
305
|
+
snap
|
|
306
|
+
label="Key Color"
|
|
307
|
+
warning={wcagWarning}
|
|
308
|
+
animateValue
|
|
309
|
+
/>
|
|
310
|
+
<div
|
|
311
|
+
style={{
|
|
312
|
+
display: "flex",
|
|
313
|
+
gap: 8,
|
|
314
|
+
alignItems: "flex-end",
|
|
315
|
+
}}
|
|
316
|
+
>
|
|
317
|
+
<div style={{ flex: 1 }}>
|
|
318
|
+
<TextInput
|
|
319
|
+
label="Hex"
|
|
320
|
+
value={hexText}
|
|
321
|
+
onChangeText={(text) => {
|
|
322
|
+
setIsEditingHex(true);
|
|
323
|
+
setHexText(text);
|
|
324
|
+
setHexError("");
|
|
325
|
+
}}
|
|
326
|
+
onBlur={handleHexSubmit}
|
|
327
|
+
onSubmitEditing={handleHexSubmit}
|
|
328
|
+
placeholder="#000000"
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
331
|
+
{effectiveKeyColor !== undefined && (
|
|
332
|
+
<button
|
|
333
|
+
onClick={handleClearKeyColor}
|
|
334
|
+
style={{
|
|
335
|
+
background: "none",
|
|
336
|
+
border: "none",
|
|
337
|
+
cursor: "pointer",
|
|
338
|
+
padding: "0 0 6px",
|
|
339
|
+
fontSize: 13,
|
|
340
|
+
fontWeight: 600,
|
|
341
|
+
color: activeColor,
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
Auto
|
|
345
|
+
</button>
|
|
346
|
+
)}
|
|
347
|
+
</div>
|
|
348
|
+
{hexError && (
|
|
349
|
+
<div
|
|
350
|
+
style={{
|
|
351
|
+
fontSize: 12,
|
|
352
|
+
fontWeight: 500,
|
|
353
|
+
color: srgbToHex(tokens.error.srgb),
|
|
354
|
+
}}
|
|
355
|
+
>
|
|
356
|
+
{hexError}
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
))}
|
|
361
|
+
|
|
362
|
+
{/* ─── Divider ─── */}
|
|
363
|
+
<div
|
|
364
|
+
style={{
|
|
365
|
+
height: 1,
|
|
366
|
+
backgroundColor: borderColor,
|
|
367
|
+
margin: "4px 0",
|
|
368
|
+
}}
|
|
369
|
+
/>
|
|
370
|
+
|
|
371
|
+
{/* ─── Shared Per-Palette Controls ─── */}
|
|
372
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
373
|
+
<HueSlider
|
|
374
|
+
value={palette.hue}
|
|
375
|
+
onValueChange={(hue) =>
|
|
376
|
+
dispatch({
|
|
377
|
+
type: "SET_PALETTE_HUE",
|
|
378
|
+
index: activePaletteIndex,
|
|
379
|
+
hue,
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
label="Hue"
|
|
383
|
+
editableValue
|
|
384
|
+
{...(hueRange ? { min: hueRange.min, max: hueRange.max } : {})}
|
|
385
|
+
/>
|
|
386
|
+
|
|
387
|
+
<Slider
|
|
388
|
+
value={palette.saturation}
|
|
389
|
+
onValueChange={(saturation) =>
|
|
390
|
+
dispatch({
|
|
391
|
+
type: "SET_PALETTE_SATURATION",
|
|
392
|
+
index: activePaletteIndex,
|
|
393
|
+
saturation,
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
min={0}
|
|
397
|
+
max={100}
|
|
398
|
+
label="Saturation"
|
|
399
|
+
editableValue
|
|
400
|
+
/>
|
|
401
|
+
|
|
402
|
+
{/* Desaturation */}
|
|
403
|
+
<div style={{ display: "flex", gap: 12, alignItems: "flex-end" }}>
|
|
404
|
+
<div style={{ flex: 1 }}>
|
|
405
|
+
<Select
|
|
406
|
+
options={STRENGTH_OPTIONS}
|
|
407
|
+
value={palette.desaturationStrength}
|
|
408
|
+
onValueChange={(strength) =>
|
|
409
|
+
dispatch({
|
|
410
|
+
type: "SET_PALETTE_DESAT_STRENGTH",
|
|
411
|
+
index: activePaletteIndex,
|
|
412
|
+
strength: strength as DesaturationStrength,
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
label="Desaturation"
|
|
416
|
+
/>
|
|
417
|
+
</div>
|
|
418
|
+
{palette.desaturationStrength !== "none" && (
|
|
419
|
+
<div style={{ paddingBottom: 2 }}>
|
|
420
|
+
<Toggle
|
|
421
|
+
value={palette.desaturationDirection === "dark"}
|
|
422
|
+
onValueChange={(v) =>
|
|
423
|
+
dispatch({
|
|
424
|
+
type: "SET_PALETTE_DESAT_DIRECTION",
|
|
425
|
+
index: activePaletteIndex,
|
|
426
|
+
direction: v ? "dark" : "light",
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
label="Invert"
|
|
430
|
+
/>
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
{/* Hue Grading */}
|
|
436
|
+
<div style={{ display: "flex", gap: 12, alignItems: "flex-end" }}>
|
|
437
|
+
<div style={{ flex: 1 }}>
|
|
438
|
+
<Select
|
|
439
|
+
options={STRENGTH_OPTIONS}
|
|
440
|
+
value={palette.hueGradeStrength}
|
|
441
|
+
onValueChange={(strength) =>
|
|
442
|
+
dispatch({
|
|
443
|
+
type: "SET_PALETTE_HUE_GRADE_STRENGTH",
|
|
444
|
+
index: activePaletteIndex,
|
|
445
|
+
strength: strength as HueGradingStrength,
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
label="Hue Grading"
|
|
449
|
+
/>
|
|
450
|
+
</div>
|
|
451
|
+
{palette.hueGradeStrength !== "none" && (
|
|
452
|
+
<div style={{ paddingBottom: 2 }}>
|
|
453
|
+
<Toggle
|
|
454
|
+
value={palette.hueGradeDirection === "dark"}
|
|
455
|
+
onValueChange={(v) =>
|
|
456
|
+
dispatch({
|
|
457
|
+
type: "SET_PALETTE_HUE_GRADE_DIRECTION",
|
|
458
|
+
index: activePaletteIndex,
|
|
459
|
+
direction: v ? "dark" : "light",
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
label="Invert"
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
{palette.hueGradeStrength !== "none" && (
|
|
469
|
+
<HueSlider
|
|
470
|
+
value={palette.hueGradeHue}
|
|
471
|
+
onValueChange={(hue) =>
|
|
472
|
+
dispatch({
|
|
473
|
+
type: "SET_PALETTE_HUE_GRADE_HUE",
|
|
474
|
+
index: activePaletteIndex,
|
|
475
|
+
hue,
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
label="Grade Target"
|
|
479
|
+
editableValue
|
|
480
|
+
/>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
);
|
|
485
|
+
}
|