@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,134 @@
1
+ import { useState } from "react";
2
+ import { useTokens } from "@newtonedev/components";
3
+ import { srgbToHex } from "newtone";
4
+ import { getCategory, getComponentsByCategory } from "@newtonedev/components";
5
+ import { ComponentRenderer } from "./ComponentRenderer";
6
+
7
+ interface CategoryViewProps {
8
+ readonly categoryId: string;
9
+ readonly onNavigateToComponent: (componentId: string) => void;
10
+ }
11
+
12
+ export function CategoryView({
13
+ categoryId,
14
+ onNavigateToComponent,
15
+ }: CategoryViewProps) {
16
+ const tokens = useTokens();
17
+ const category = getCategory(categoryId);
18
+ const components = getComponentsByCategory(categoryId);
19
+ const [hoveredId, setHoveredId] = useState<string | null>(null);
20
+
21
+ if (!category) return null;
22
+
23
+ return (
24
+ <div style={{ padding: 32 }}>
25
+ <h2
26
+ style={{
27
+ fontSize: 22,
28
+ fontWeight: 700,
29
+ color: srgbToHex(tokens.textPrimary.srgb),
30
+ margin: 0,
31
+ marginBottom: 4,
32
+ }}
33
+ >
34
+ {category.name}
35
+ </h2>
36
+ <p
37
+ style={{
38
+ fontSize: 14,
39
+ color: srgbToHex(tokens.textSecondary.srgb),
40
+ margin: 0,
41
+ marginBottom: 32,
42
+ }}
43
+ >
44
+ {category.description}
45
+ </p>
46
+
47
+ <div
48
+ style={{
49
+ display: "grid",
50
+ gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
51
+ gap: 20,
52
+ }}
53
+ >
54
+ {components.map((component) => {
55
+ const isHovered = hoveredId === component.id;
56
+
57
+ return (
58
+ <button
59
+ key={component.id}
60
+ onClick={() => onNavigateToComponent(component.id)}
61
+ onMouseEnter={() => setHoveredId(component.id)}
62
+ onMouseLeave={() => setHoveredId(null)}
63
+ style={{
64
+ display: "flex",
65
+ flexDirection: "column",
66
+ padding: 24,
67
+ borderRadius: 12,
68
+ border: `1px solid ${srgbToHex(
69
+ isHovered ? tokens.interactive.srgb : tokens.border.srgb,
70
+ )}`,
71
+ backgroundColor: srgbToHex(tokens.backgroundElevated.srgb),
72
+ cursor: "pointer",
73
+ textAlign: "left",
74
+ transform: isHovered ? "translateY(-1px)" : "none",
75
+ boxShadow: isHovered
76
+ ? "0 2px 8px rgba(0,0,0,0.08)"
77
+ : "none",
78
+ transition:
79
+ "border-color 150ms ease, transform 150ms ease, box-shadow 150ms ease",
80
+ }}
81
+ >
82
+ <div
83
+ style={{
84
+ display: "flex",
85
+ alignItems: "center",
86
+ justifyContent: "center",
87
+ padding: 20,
88
+ marginBottom: 16,
89
+ borderRadius: 8,
90
+ backgroundColor: srgbToHex(tokens.background.srgb),
91
+ minHeight: 60,
92
+ }}
93
+ >
94
+ <ComponentRenderer
95
+ componentId={component.id}
96
+ props={component.variants[0]?.props ?? {}}
97
+ />
98
+ </div>
99
+ <span
100
+ style={{
101
+ fontSize: 15,
102
+ fontWeight: 600,
103
+ color: srgbToHex(tokens.textPrimary.srgb),
104
+ marginBottom: 4,
105
+ }}
106
+ >
107
+ {component.name}
108
+ </span>
109
+ <span
110
+ style={{
111
+ fontSize: 13,
112
+ color: srgbToHex(tokens.textSecondary.srgb),
113
+ lineHeight: 1.4,
114
+ }}
115
+ >
116
+ {component.description}
117
+ </span>
118
+ <span
119
+ style={{
120
+ fontSize: 12,
121
+ color: srgbToHex(tokens.textSecondary.srgb),
122
+ marginTop: 8,
123
+ }}
124
+ >
125
+ {component.variants.length} variant
126
+ {component.variants.length !== 1 ? "s" : ""}
127
+ </span>
128
+ </button>
129
+ );
130
+ })}
131
+ </div>
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,126 @@
1
+ import { useState } from "react";
2
+ import { useTokens } from "@newtonedev/components";
3
+ import { srgbToHex } from "newtone";
4
+ import { getComponent } from "@newtonedev/components";
5
+ import { ComponentRenderer } from "./ComponentRenderer";
6
+
7
+ interface ComponentDetailViewProps {
8
+ readonly componentId: string;
9
+ readonly selectedVariantId: string | null;
10
+ readonly propOverrides?: Record<string, unknown>;
11
+ readonly onSelectVariant: (variantId: string) => void;
12
+ }
13
+
14
+ export function ComponentDetailView({
15
+ componentId,
16
+ selectedVariantId,
17
+ propOverrides,
18
+ onSelectVariant,
19
+ }: ComponentDetailViewProps) {
20
+ const tokens = useTokens();
21
+ const component = getComponent(componentId);
22
+ const [hoveredId, setHoveredId] = useState<string | null>(null);
23
+
24
+ if (!component) return null;
25
+
26
+ const interactiveColor = srgbToHex(tokens.interactive.srgb);
27
+
28
+ return (
29
+ <div style={{ padding: 32 }}>
30
+ <h2
31
+ style={{
32
+ fontSize: 22,
33
+ fontWeight: 700,
34
+ color: srgbToHex(tokens.textPrimary.srgb),
35
+ margin: 0,
36
+ marginBottom: 4,
37
+ }}
38
+ >
39
+ {component.name}
40
+ </h2>
41
+ <p
42
+ style={{
43
+ fontSize: 14,
44
+ color: srgbToHex(tokens.textSecondary.srgb),
45
+ margin: 0,
46
+ marginBottom: 32,
47
+ }}
48
+ >
49
+ {component.description}
50
+ </p>
51
+
52
+ <div
53
+ style={{
54
+ display: "grid",
55
+ gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
56
+ gap: 16,
57
+ }}
58
+ >
59
+ {component.variants.map((variant) => {
60
+ const isSelected = selectedVariantId === variant.id;
61
+ const isHovered = hoveredId === variant.id;
62
+
63
+ const borderColor = isSelected
64
+ ? interactiveColor
65
+ : isHovered
66
+ ? `${interactiveColor}66`
67
+ : srgbToHex(tokens.border.srgb);
68
+
69
+ return (
70
+ <button
71
+ key={variant.id}
72
+ onClick={() => onSelectVariant(variant.id)}
73
+ onMouseEnter={() => setHoveredId(variant.id)}
74
+ onMouseLeave={() => setHoveredId(null)}
75
+ style={{
76
+ display: "flex",
77
+ flexDirection: "column",
78
+ alignItems: "stretch",
79
+ padding: 16,
80
+ borderRadius: 12,
81
+ border: `2px solid ${borderColor}`,
82
+ backgroundColor: srgbToHex(tokens.backgroundElevated.srgb),
83
+ cursor: "pointer",
84
+ textAlign: "left",
85
+ transition: "border-color 150ms ease",
86
+ }}
87
+ >
88
+ <div
89
+ style={{
90
+ display: "flex",
91
+ alignItems: "center",
92
+ justifyContent: "center",
93
+ padding: 20,
94
+ marginBottom: 12,
95
+ borderRadius: 8,
96
+ backgroundColor: srgbToHex(tokens.background.srgb),
97
+ minHeight: 56,
98
+ }}
99
+ >
100
+ <ComponentRenderer
101
+ componentId={componentId}
102
+ props={
103
+ isSelected && propOverrides
104
+ ? { ...variant.props, ...propOverrides }
105
+ : variant.props
106
+ }
107
+ />
108
+ </div>
109
+ <span
110
+ style={{
111
+ fontSize: 13,
112
+ fontWeight: isSelected ? 600 : 500,
113
+ color: isSelected
114
+ ? interactiveColor
115
+ : srgbToHex(tokens.textPrimary.srgb),
116
+ }}
117
+ >
118
+ {variant.label}
119
+ </span>
120
+ </button>
121
+ );
122
+ })}
123
+ </div>
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,107 @@
1
+ import { useState, useCallback } from "react";
2
+ import {
3
+ Button,
4
+ Card,
5
+ TextInput,
6
+ Select,
7
+ Toggle,
8
+ Slider,
9
+ HueSlider,
10
+ useTokens,
11
+ } from "@newtonedev/components";
12
+ import { srgbToHex } from "newtone";
13
+
14
+ interface ComponentRendererProps {
15
+ readonly componentId: string;
16
+ readonly props: Record<string, unknown>;
17
+ }
18
+
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ type AnyProps = any;
21
+
22
+ function StatefulToggle(props: AnyProps) {
23
+ const [value, setValue] = useState(props.value as boolean);
24
+ return <Toggle {...props} value={value} onValueChange={setValue} />;
25
+ }
26
+
27
+ function StatefulSlider(props: AnyProps) {
28
+ const [value, setValue] = useState(props.value as number);
29
+ return <Slider {...props} value={value} onValueChange={setValue} />;
30
+ }
31
+
32
+ function StatefulHueSlider(props: AnyProps) {
33
+ const [value, setValue] = useState(props.value as number);
34
+ return <HueSlider {...props} value={value} onValueChange={setValue} />;
35
+ }
36
+
37
+ function StatefulTextInput(props: AnyProps) {
38
+ const [value, setValue] = useState((props.value as string) ?? "");
39
+ return <TextInput {...props} value={value} onChangeText={setValue} />;
40
+ }
41
+
42
+ function StatefulSelect(props: AnyProps) {
43
+ const [value, setValue] = useState(props.value as string);
44
+ return <Select {...props} value={value} onValueChange={setValue} />;
45
+ }
46
+
47
+ function CardPreview(props: AnyProps) {
48
+ const tokens = useTokens();
49
+ return (
50
+ <Card {...props} style={{ padding: 20, minWidth: 200 }}>
51
+ <div
52
+ style={{
53
+ fontSize: 14,
54
+ fontWeight: 600,
55
+ color: srgbToHex(tokens.textPrimary.srgb),
56
+ marginBottom: 8,
57
+ }}
58
+ >
59
+ Card Title
60
+ </div>
61
+ <div
62
+ style={{
63
+ fontSize: 13,
64
+ color: srgbToHex(tokens.textSecondary.srgb),
65
+ lineHeight: 1.4,
66
+ }}
67
+ >
68
+ Sample card content at elevation {String(props.elevation ?? 0)}.
69
+ </div>
70
+ </Card>
71
+ );
72
+ }
73
+
74
+ export function ComponentRenderer({ componentId, props }: ComponentRendererProps) {
75
+ const noop = useCallback(() => {}, []);
76
+
77
+ switch (componentId) {
78
+ case "button": {
79
+ const icon = (props.icon as string) || undefined;
80
+ return (
81
+ <Button
82
+ variant={props.variant as AnyProps}
83
+ size={props.size as AnyProps}
84
+ icon={icon}
85
+ iconPosition={props.iconPosition as AnyProps}
86
+ onPress={noop}
87
+ >
88
+ Button
89
+ </Button>
90
+ );
91
+ }
92
+ case "text-input":
93
+ return <StatefulTextInput {...props} />;
94
+ case "select":
95
+ return <StatefulSelect {...props} />;
96
+ case "toggle":
97
+ return <StatefulToggle {...props} />;
98
+ case "slider":
99
+ return <StatefulSlider {...props} />;
100
+ case "hue-slider":
101
+ return <StatefulHueSlider {...props} />;
102
+ case "card":
103
+ return <CardPreview {...props} />;
104
+ default:
105
+ return null;
106
+ }
107
+ }
@@ -0,0 +1,177 @@
1
+ import { useTokens } from "@newtonedev/components";
2
+ import { srgbToHex } from "newtone";
3
+ import { CATEGORIES, getComponentsByCategory } from "@newtonedev/components";
4
+ import { ComponentRenderer } from "./ComponentRenderer";
5
+ import { useHover } from "../hooks/useHover";
6
+
7
+ interface OverviewViewProps {
8
+ readonly onNavigateToCategory: (categoryId: string) => void;
9
+ readonly onNavigateToComponent: (componentId: string) => void;
10
+ }
11
+
12
+ export function OverviewView({
13
+ onNavigateToCategory,
14
+ onNavigateToComponent,
15
+ }: OverviewViewProps) {
16
+ const tokens = useTokens();
17
+
18
+ return (
19
+ <div style={{ padding: 32 }}>
20
+ <h2
21
+ style={{
22
+ fontSize: 22,
23
+ fontWeight: 700,
24
+ color: srgbToHex(tokens.textPrimary.srgb),
25
+ margin: 0,
26
+ marginBottom: 4,
27
+ }}
28
+ >
29
+ Component Library
30
+ </h2>
31
+ <p
32
+ style={{
33
+ fontSize: 14,
34
+ color: srgbToHex(tokens.textSecondary.srgb),
35
+ margin: 0,
36
+ marginBottom: 32,
37
+ }}
38
+ >
39
+ Browse components to see how your color system affects them.
40
+ </p>
41
+
42
+ {CATEGORIES.map((category) => {
43
+ const components = getComponentsByCategory(category.id);
44
+ if (components.length === 0) return null;
45
+
46
+ return (
47
+ <div key={category.id} style={{ marginBottom: 32 }}>
48
+ <button
49
+ onClick={() => onNavigateToCategory(category.id)}
50
+ style={{
51
+ background: "none",
52
+ border: "none",
53
+ cursor: "pointer",
54
+ padding: 0,
55
+ margin: 0,
56
+ marginBottom: 16,
57
+ display: "block",
58
+ }}
59
+ >
60
+ <span
61
+ style={{
62
+ fontSize: 16,
63
+ fontWeight: 600,
64
+ color: srgbToHex(tokens.textPrimary.srgb),
65
+ }}
66
+ >
67
+ {category.name}
68
+ </span>
69
+ <span
70
+ style={{
71
+ fontSize: 13,
72
+ color: srgbToHex(tokens.textSecondary.srgb),
73
+ marginLeft: 8,
74
+ }}
75
+ >
76
+ {category.description}
77
+ </span>
78
+ </button>
79
+
80
+ <div
81
+ style={{
82
+ display: "grid",
83
+ gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))",
84
+ gap: 16,
85
+ }}
86
+ >
87
+ {components.map((component) => (
88
+ <ComponentCard
89
+ key={component.id}
90
+ componentId={component.id}
91
+ name={component.name}
92
+ description={component.description}
93
+ defaultVariantProps={component.variants[0]?.props ?? {}}
94
+ onClick={() => onNavigateToComponent(component.id)}
95
+ />
96
+ ))}
97
+ </div>
98
+ </div>
99
+ );
100
+ })}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ function ComponentCard({
106
+ componentId,
107
+ name,
108
+ description,
109
+ defaultVariantProps,
110
+ onClick,
111
+ }: {
112
+ readonly componentId: string;
113
+ readonly name: string;
114
+ readonly description: string;
115
+ readonly defaultVariantProps: Record<string, unknown>;
116
+ readonly onClick: () => void;
117
+ }) {
118
+ const tokens = useTokens();
119
+ const { isHovered, hoverProps } = useHover();
120
+
121
+ return (
122
+ <button
123
+ onClick={onClick}
124
+ {...hoverProps}
125
+ style={{
126
+ display: "flex",
127
+ flexDirection: "column",
128
+ padding: 20,
129
+ borderRadius: 12,
130
+ border: `1px solid ${srgbToHex(
131
+ isHovered ? tokens.interactive.srgb : tokens.border.srgb,
132
+ )}`,
133
+ backgroundColor: srgbToHex(tokens.backgroundElevated.srgb),
134
+ cursor: "pointer",
135
+ textAlign: "left",
136
+ transform: isHovered ? "translateY(-1px)" : "none",
137
+ boxShadow: isHovered ? "0 2px 8px rgba(0,0,0,0.08)" : "none",
138
+ transition:
139
+ "border-color 150ms ease, transform 150ms ease, box-shadow 150ms ease",
140
+ }}
141
+ >
142
+ <div
143
+ style={{
144
+ display: "flex",
145
+ alignItems: "center",
146
+ justifyContent: "center",
147
+ padding: 16,
148
+ marginBottom: 16,
149
+ borderRadius: 8,
150
+ backgroundColor: srgbToHex(tokens.background.srgb),
151
+ minHeight: 60,
152
+ }}
153
+ >
154
+ <ComponentRenderer componentId={componentId} props={defaultVariantProps} />
155
+ </div>
156
+ <span
157
+ style={{
158
+ fontSize: 14,
159
+ fontWeight: 600,
160
+ color: srgbToHex(tokens.textPrimary.srgb),
161
+ marginBottom: 4,
162
+ }}
163
+ >
164
+ {name}
165
+ </span>
166
+ <span
167
+ style={{
168
+ fontSize: 12,
169
+ color: srgbToHex(tokens.textSecondary.srgb),
170
+ lineHeight: 1.4,
171
+ }}
172
+ >
173
+ {description}
174
+ </span>
175
+ </button>
176
+ );
177
+ }
package/src/types.ts ADDED
@@ -0,0 +1,77 @@
1
+ import type { ConfiguratorState } from "@newtonedev/configurator";
2
+ import type { NewtoneThemeConfig } from "@newtonedev/components";
3
+ import type { ReactNode } from "react";
4
+
5
+ // --- Data types ---
6
+
7
+ export interface Preset {
8
+ readonly id: string;
9
+ readonly name: string;
10
+ readonly draft_state: ConfiguratorState;
11
+ readonly published_state: ConfiguratorState | null;
12
+ }
13
+
14
+ // --- UI state types ---
15
+
16
+ export type SaveStatus = "saved" | "saving" | "unsaved" | "error";
17
+
18
+ export type ThemeName = "neutral" | "primary" | "secondary" | "strong";
19
+
20
+ export type PreviewView =
21
+ | { readonly kind: "overview" }
22
+ | { readonly kind: "category"; readonly categoryId: string }
23
+ | { readonly kind: "component"; readonly componentId: string };
24
+
25
+ export type SidebarSelection =
26
+ | null
27
+ | { readonly scope: "component"; readonly componentId: string }
28
+ | {
29
+ readonly scope: "variant";
30
+ readonly componentId: string;
31
+ readonly variantId: string;
32
+ };
33
+
34
+ // --- Persistence interface ---
35
+
36
+ export interface EditorPersistence {
37
+ /** Save draft state. Called on 2s debounce. */
38
+ readonly onSaveDraft: (params: {
39
+ readonly state: ConfiguratorState;
40
+ readonly presets: readonly Preset[];
41
+ }) => Promise<{ error?: unknown }>;
42
+
43
+ /** Publish the active preset. */
44
+ readonly onPublish: (params: {
45
+ readonly state: ConfiguratorState;
46
+ readonly presets: readonly Preset[];
47
+ readonly activePresetId: string;
48
+ }) => Promise<{ error?: unknown }>;
49
+
50
+ /** Persist preset metadata (used by preset CRUD operations). */
51
+ readonly persistPresets: (params: {
52
+ readonly presets: readonly Preset[];
53
+ readonly activePresetId: string;
54
+ readonly publishedPresetId: string | null;
55
+ }) => Promise<void>;
56
+ }
57
+
58
+ // --- Editor component props ---
59
+
60
+ export interface EditorHeaderSlots {
61
+ readonly left?: ReactNode;
62
+ readonly right?: ReactNode;
63
+ }
64
+
65
+ export interface EditorProps {
66
+ readonly initialState: ConfiguratorState;
67
+ readonly initialIsPublished: boolean;
68
+ readonly initialPresets: readonly Preset[];
69
+ readonly initialActivePresetId: string;
70
+ readonly initialPublishedPresetId: string | null;
71
+ readonly defaultState: ConfiguratorState;
72
+ readonly chromeThemeConfig: NewtoneThemeConfig;
73
+ readonly persistence: EditorPersistence;
74
+ readonly headerSlots?: EditorHeaderSlots;
75
+ readonly onNavigate?: (view: PreviewView) => void;
76
+ readonly initialPreviewView?: PreviewView;
77
+ }
@@ -0,0 +1,24 @@
1
+ import type { Preset } from "../types";
2
+
3
+ export function findPreset(
4
+ presets: readonly Preset[],
5
+ presetId: string,
6
+ ): Preset | undefined {
7
+ return presets.find((p) => p.id === presetId);
8
+ }
9
+
10
+ export function updatePresetInArray(
11
+ presets: readonly Preset[],
12
+ presetId: string,
13
+ updater: (preset: Preset) => Preset,
14
+ ): readonly Preset[] {
15
+ return presets.map((p) => (p.id === presetId ? updater(p) : p));
16
+ }
17
+
18
+ export function presetHasUnpublishedChanges(preset: Preset): boolean {
19
+ if (preset.published_state === null) return true;
20
+ return (
21
+ JSON.stringify(preset.draft_state) !==
22
+ JSON.stringify(preset.published_state)
23
+ );
24
+ }