@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.
Files changed (86) hide show
  1. package/dist/Editor.d.ts +3 -0
  2. package/dist/Editor.d.ts.map +1 -0
  3. package/dist/components/CodeBlock.d.ts +7 -0
  4. package/dist/components/CodeBlock.d.ts.map +1 -0
  5. package/dist/components/EditorHeader.d.ts +16 -0
  6. package/dist/components/EditorHeader.d.ts.map +1 -0
  7. package/dist/components/EditorShell.d.ts +10 -0
  8. package/dist/components/EditorShell.d.ts.map +1 -0
  9. package/dist/components/FontPicker.d.ts +11 -0
  10. package/dist/components/FontPicker.d.ts.map +1 -0
  11. package/dist/components/PresetSelector.d.ts +14 -0
  12. package/dist/components/PresetSelector.d.ts.map +1 -0
  13. package/dist/components/PreviewWindow.d.ts +11 -0
  14. package/dist/components/PreviewWindow.d.ts.map +1 -0
  15. package/dist/components/RightSidebar.d.ts +12 -0
  16. package/dist/components/RightSidebar.d.ts.map +1 -0
  17. package/dist/components/Sidebar.d.ts +25 -0
  18. package/dist/components/Sidebar.d.ts.map +1 -0
  19. package/dist/components/TableOfContents.d.ts +9 -0
  20. package/dist/components/TableOfContents.d.ts.map +1 -0
  21. package/dist/components/ThemeBar.d.ts +8 -0
  22. package/dist/components/ThemeBar.d.ts.map +1 -0
  23. package/dist/components/sections/ColorsSection.d.ts +14 -0
  24. package/dist/components/sections/ColorsSection.d.ts.map +1 -0
  25. package/dist/components/sections/DynamicRangeSection.d.ts +9 -0
  26. package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -0
  27. package/dist/components/sections/FontsSection.d.ts +9 -0
  28. package/dist/components/sections/FontsSection.d.ts.map +1 -0
  29. package/dist/components/sections/IconsSection.d.ts +9 -0
  30. package/dist/components/sections/IconsSection.d.ts.map +1 -0
  31. package/dist/components/sections/OthersSection.d.ts +9 -0
  32. package/dist/components/sections/OthersSection.d.ts.map +1 -0
  33. package/dist/components/sections/index.d.ts +6 -0
  34. package/dist/components/sections/index.d.ts.map +1 -0
  35. package/dist/hooks/useEditorState.d.ts +53 -0
  36. package/dist/hooks/useEditorState.d.ts.map +1 -0
  37. package/dist/hooks/useHover.d.ts +8 -0
  38. package/dist/hooks/useHover.d.ts.map +1 -0
  39. package/dist/hooks/usePresets.d.ts +33 -0
  40. package/dist/hooks/usePresets.d.ts.map +1 -0
  41. package/dist/index.cjs +3846 -0
  42. package/dist/index.cjs.map +1 -0
  43. package/dist/index.d.ts +22 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3819 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/preview/CategoryView.d.ts +7 -0
  48. package/dist/preview/CategoryView.d.ts.map +1 -0
  49. package/dist/preview/ComponentDetailView.d.ts +9 -0
  50. package/dist/preview/ComponentDetailView.d.ts.map +1 -0
  51. package/dist/preview/ComponentRenderer.d.ts +7 -0
  52. package/dist/preview/ComponentRenderer.d.ts.map +1 -0
  53. package/dist/preview/OverviewView.d.ts +7 -0
  54. package/dist/preview/OverviewView.d.ts.map +1 -0
  55. package/dist/types.d.ts +69 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/utils/presets.d.ts +5 -0
  58. package/dist/utils/presets.d.ts.map +1 -0
  59. package/package.json +51 -0
  60. package/src/Editor.tsx +128 -0
  61. package/src/components/CodeBlock.tsx +58 -0
  62. package/src/components/EditorHeader.tsx +86 -0
  63. package/src/components/EditorShell.tsx +67 -0
  64. package/src/components/FontPicker.tsx +351 -0
  65. package/src/components/PresetSelector.tsx +455 -0
  66. package/src/components/PreviewWindow.tsx +69 -0
  67. package/src/components/RightSidebar.tsx +374 -0
  68. package/src/components/Sidebar.tsx +332 -0
  69. package/src/components/TableOfContents.tsx +152 -0
  70. package/src/components/ThemeBar.tsx +76 -0
  71. package/src/components/sections/ColorsSection.tsx +485 -0
  72. package/src/components/sections/DynamicRangeSection.tsx +399 -0
  73. package/src/components/sections/FontsSection.tsx +132 -0
  74. package/src/components/sections/IconsSection.tsx +66 -0
  75. package/src/components/sections/OthersSection.tsx +70 -0
  76. package/src/components/sections/index.ts +5 -0
  77. package/src/hooks/useEditorState.ts +381 -0
  78. package/src/hooks/useHover.ts +8 -0
  79. package/src/hooks/usePresets.ts +254 -0
  80. package/src/index.ts +52 -0
  81. package/src/preview/CategoryView.tsx +134 -0
  82. package/src/preview/ComponentDetailView.tsx +126 -0
  83. package/src/preview/ComponentRenderer.tsx +107 -0
  84. package/src/preview/OverviewView.tsx +177 -0
  85. package/src/types.ts +77 -0
  86. 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
+ }