@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,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
|
+
}
|