@drawnagency/primitives 0.1.86 → 0.1.88

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 (60) hide show
  1. package/dist/{chunk-YKOLOZB2.js → chunk-DRCPDOW3.js} +14 -0
  2. package/dist/{chunk-MBJUWOM2.js → chunk-EO7K45GV.js} +1 -1
  3. package/dist/{chunk-2CVP2AGK.js → chunk-H2FENAQH.js} +6 -6
  4. package/dist/{chunk-DKXISNJ2.js → chunk-NEDOJHHR.js} +2 -2
  5. package/dist/{chunk-DO3ZENLN.js → chunk-OEL57NYV.js} +1 -1
  6. package/dist/{chunk-FDBZZ4AT.js → chunk-PTVY3Q7P.js} +1 -1
  7. package/dist/{chunk-665DW3HR.js → chunk-SK6GGTTO.js} +220 -207
  8. package/dist/{chunk-EP3LVOXZ.js → chunk-WA3HFN54.js} +1 -1
  9. package/dist/{chunk-YAPG44NS.js → chunk-XDG2K6UT.js} +1 -1
  10. package/dist/{chunk-E3MTIMOG.js → chunk-XPUBUR64.js} +10 -0
  11. package/dist/components/brandguide/Colors.d.ts.map +1 -1
  12. package/dist/components/brandguide/ColorsEditable.d.ts +20 -0
  13. package/dist/components/brandguide/ColorsEditable.d.ts.map +1 -0
  14. package/dist/components/brandguide/ColorsView.d.ts +9 -0
  15. package/dist/components/brandguide/ColorsView.d.ts.map +1 -0
  16. package/dist/components/brandguide/colors-shared.d.ts +4 -0
  17. package/dist/components/brandguide/colors-shared.d.ts.map +1 -0
  18. package/dist/components/editor/index.js +4 -4
  19. package/dist/components/primitives/index.js +5 -5
  20. package/dist/components/sections/Colors/index.d.ts.map +1 -1
  21. package/dist/components/sections/SectionIsland.d.ts +8 -0
  22. package/dist/components/sections/SectionIsland.d.ts.map +1 -0
  23. package/dist/components/sections/brand-guide.js +5 -5
  24. package/dist/components/sections/register-schemas.js +8 -8
  25. package/dist/components/sections/register.js +8 -8
  26. package/dist/components/shared/Navigation.d.ts +6 -1
  27. package/dist/components/shared/Navigation.d.ts.map +1 -1
  28. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  29. package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
  30. package/dist/hooks/useCollabSession.d.ts.map +1 -1
  31. package/dist/hooks/useMediaPipeline.d.ts +2 -1
  32. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  33. package/dist/index.js +6 -4
  34. package/dist/lib/image-refs.d.ts +9 -0
  35. package/dist/lib/image-refs.d.ts.map +1 -1
  36. package/dist/lib/index.js +2 -2
  37. package/dist/lib/registry.d.ts +7 -0
  38. package/dist/lib/registry.d.ts.map +1 -1
  39. package/dist/media/index.js +4 -2
  40. package/dist/media/resolve.d.ts +25 -0
  41. package/dist/media/resolve.d.ts.map +1 -1
  42. package/dist/schemas/index.js +2 -2
  43. package/dist/schemas/site-config.d.ts +10 -0
  44. package/dist/schemas/site-config.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/components/brandguide/Colors.tsx +2 -275
  47. package/src/components/brandguide/ColorsEditable.tsx +155 -0
  48. package/src/components/brandguide/ColorsView.tsx +100 -0
  49. package/src/components/brandguide/colors-shared.ts +17 -0
  50. package/src/components/sections/Colors/index.tsx +1 -0
  51. package/src/components/sections/SectionIsland.tsx +21 -0
  52. package/src/components/shared/Navigation.tsx +20 -2
  53. package/src/components/shell/EditorShell.tsx +4 -0
  54. package/src/components/shell/SiteSettingsDisplay.tsx +106 -0
  55. package/src/hooks/useCollabSession.ts +27 -0
  56. package/src/hooks/useMediaPipeline.ts +16 -3
  57. package/src/lib/image-refs.ts +19 -0
  58. package/src/lib/registry.ts +7 -0
  59. package/src/media/resolve.ts +34 -0
  60. package/src/schemas/site-config.ts +13 -0
@@ -1,11 +1,6 @@
1
- import { useState, useEffect } from "react";
2
- import { Eye, EyeOff } from "lucide-react";
3
- import { cn } from "../../lib/cn";
4
- import { containerGridClass } from "../../lib/container-grid";
5
- import { EditableGrid } from "../primitives/EditableGrid";
6
- import { EditablePlainText } from "../primitives/EditablePlainText";
7
1
  import type { ReactNode } from "react";
8
- import ColorSwatchSettings from "./ColorSwatchSettings";
2
+ import ColorsView from "./ColorsView";
3
+ import ColorsEditable from "./ColorsEditable";
9
4
  import type { ColorItem } from "../sections/Colors/schema";
10
5
  import type { SectionContent } from "../../schemas/sections";
11
6
 
@@ -20,36 +15,6 @@ interface Props {
20
15
  onDelete?: () => void;
21
16
  }
22
17
 
23
- function getContrastClass(hex: string | undefined): string {
24
- if (!hex) return "text-white";
25
- const h = hex.replace("#", "");
26
- if (h.length < 6) return "text-white";
27
- const r = parseInt(h.substring(0, 2), 16);
28
- const g = parseInt(h.substring(2, 4), 16);
29
- const b = parseInt(h.substring(4, 6), 16);
30
- const yiq = (r * 299 + g * 587 + b * 114) / 1000;
31
- return yiq >= 128 ? "text-black" : "text-white";
32
- }
33
-
34
- const createColorItem = (): ColorItem => ({
35
- name: "",
36
- spaces: [{ hex: "#cccccc" }],
37
- });
38
-
39
- function buildContent(
40
- colors: ColorItem[],
41
- options: { label?: string; columns?: number; collapsing?: boolean; showLabel?: boolean },
42
- ): SectionContent {
43
- const { label, columns, collapsing, showLabel } = options;
44
- return {
45
- type: "colors",
46
- content: { colors },
47
- ...(label || columns || collapsing !== undefined || showLabel !== undefined
48
- ? { options: { label, columns, collapsing, showLabel } }
49
- : {}),
50
- };
51
- }
52
-
53
18
  export default function Colors({ colors, columns = 3, label, collapsing, showLabel = true, onChange, openModal, onDelete }: Props) {
54
19
  if (onChange && openModal) {
55
20
  return (
@@ -68,241 +33,3 @@ export default function Colors({ colors, columns = 3, label, collapsing, showLab
68
33
 
69
34
  return <ColorsView colors={colors} columns={columns} label={label} collapsing={collapsing} showLabel={showLabel} />;
70
35
  }
71
-
72
- function ColorsView({
73
- colors,
74
- columns,
75
- label,
76
- collapsing,
77
- showLabel,
78
- }: {
79
- colors: ColorItem[];
80
- columns: number;
81
- label?: string;
82
- collapsing?: boolean;
83
- showLabel?: boolean;
84
- }) {
85
- const [expanded, setExpanded] = useState(!collapsing);
86
- const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
87
-
88
- const handleCopy = (value: string, colorIndex: number) => {
89
- if (navigator.clipboard?.writeText) {
90
- navigator.clipboard.writeText(value);
91
- } else {
92
- const ta = document.createElement("textarea");
93
- ta.value = value;
94
- ta.style.position = "fixed";
95
- ta.style.opacity = "0";
96
- document.body.appendChild(ta);
97
- ta.select();
98
- document.execCommand("copy");
99
- document.body.removeChild(ta);
100
- }
101
- setCopiedIndex(colorIndex);
102
- setTimeout(() => setCopiedIndex(null), 1500);
103
- };
104
-
105
- return (
106
- <div>
107
- {(showLabel && !!label || collapsing) && (
108
- <div className="mb-4 flex items-center gap-2">
109
- {showLabel && !!label && (
110
- <span className="text-lg font-bold">{label}</span>
111
- )}
112
- {collapsing && (
113
- <button
114
- onClick={() => setExpanded(!expanded)}
115
- className="cursor-pointer ml-auto flex items-center gap-1.5 text-sm text-base-contrast-light hover:text-base-contrast"
116
- >
117
- <span>{expanded ? "Hide Details" : "Show Details"}</span>
118
- {expanded ? <EyeOff size={16} /> : <Eye size={16} />}
119
- </button>
120
- )}
121
- </div>
122
- )}
123
-
124
- <div className="@container">
125
- <div className={cn("grid gap-4", containerGridClass[columns] || containerGridClass[3])}>
126
- {colors.map((color, i) => {
127
- const hex = color.spaces[0]?.hex;
128
- const contrast = getContrastClass(hex);
129
- return (
130
- <div key={i} className="flex h-full flex-col overflow-hidden rounded-outer border border-base-200">
131
- <div className={cn("relative flex min-h-[80px] items-end p-3", contrast)} style={{ backgroundColor: hex || "#ccc" }}>
132
- {color.name && <span className="text-sm font-bold">{color.name}</span>}
133
- {copiedIndex === i && (
134
- <span className={cn(
135
- "absolute top-2 right-2 rounded-full px-2.5 py-0.5 text-xs font-medium",
136
- getContrastClass(hex) === "text-black" ? "bg-black/15 text-black" : "bg-white/25 text-white",
137
- )}>
138
- Copied!
139
- </span>
140
- )}
141
- </div>
142
- {expanded && (
143
- // flex-1 so every swatch's label block grows to the tallest in its row.
144
- <div className="flex-1 space-y-1 bg-base-accent p-3 text-sm">
145
- {color.spaces.map((space, j) =>
146
- Object.entries(space).map(([key, value]) =>
147
- value ? (
148
- <button key={`${j}-${key}`} onClick={() => handleCopy(value, i)} className="cursor-pointer flex w-full justify-between hover:text-primary">
149
- <span className="font-medium uppercase">{key}</span>
150
- <span>{value}</span>
151
- </button>
152
- ) : null
153
- )
154
- )}
155
- </div>
156
- )}
157
- </div>
158
- );
159
- })}
160
- </div>
161
- </div>
162
- </div>
163
- );
164
- }
165
-
166
- function ColorsEditable({
167
- colors,
168
- columns,
169
- label,
170
- collapsing,
171
- showLabel,
172
- onChange,
173
- openModal,
174
- onDelete,
175
- }: {
176
- colors: ColorItem[];
177
- columns: number;
178
- label?: string;
179
- collapsing?: boolean;
180
- showLabel?: boolean;
181
- onChange: (content: SectionContent) => void;
182
- openModal: (title: string, content: ReactNode) => void;
183
- onDelete?: () => void;
184
- }) {
185
- const [expanded, setExpanded] = useState(!collapsing);
186
- const options = { label, columns, collapsing, showLabel };
187
-
188
- // Sync expanded state when collapsing option changes via settings
189
- useEffect(() => {
190
- setExpanded(!collapsing);
191
- }, [collapsing]);
192
-
193
- const handleItemSettings = (index: number) => {
194
- const color = colors[index];
195
- openModal(
196
- "Color Settings",
197
- <ColorSwatchSettings color={color} onChange={(updated) => {
198
- const newColors = colors.map((c, i) => (i === index ? updated : c));
199
- onChange(buildContent(newColors, options));
200
- }} />,
201
- );
202
- };
203
-
204
- return (
205
- <div>
206
- {(showLabel && !!label || collapsing) && (
207
- <div className="mb-4 flex items-center gap-2">
208
- {showLabel && !!label && (
209
- <EditablePlainText
210
- tag="span"
211
- value={label}
212
- onChange={(newLabel) => onChange(buildContent(colors, { ...options, label: newLabel }))}
213
- isEditMode={true}
214
- className="text-lg font-bold"
215
- />
216
- )}
217
- {collapsing && (
218
- <button
219
- onClick={() => setExpanded(!expanded)}
220
- className="cursor-pointer ml-auto flex items-center gap-1.5 text-sm text-base-contrast-light hover:text-base-contrast"
221
- >
222
- <span>{expanded ? "Hide Details" : "Show Details"}</span>
223
- {expanded ? <EyeOff size={16} /> : <Eye size={16} />}
224
- </button>
225
- )}
226
- </div>
227
- )}
228
-
229
- <EditableGrid
230
- items={colors}
231
- columns={columns}
232
- // Shared group lets a swatch be dragged from one colors section into another.
233
- group="colors"
234
- // When the last swatch is dragged out to another section, delete this now-empty one.
235
- onEmptied={onDelete}
236
- onChange={(newColors) => onChange(buildContent(newColors, options))}
237
- createItem={createColorItem}
238
- isEditMode={true}
239
- onItemSettings={handleItemSettings}
240
- renderItem={(color, { index }) => (
241
- <ColorSwatchEditable
242
- color={color}
243
- index={index}
244
- colors={colors}
245
- options={options}
246
- onChange={onChange}
247
- expanded={expanded}
248
- />
249
- )}
250
- />
251
- </div>
252
- );
253
- }
254
-
255
- function ColorSwatchEditable({
256
- color,
257
- index,
258
- colors,
259
- options,
260
- onChange,
261
- expanded,
262
- }: {
263
- color: ColorItem;
264
- index: number;
265
- colors: ColorItem[];
266
- options: { label?: string; columns?: number; collapsing?: boolean; showLabel?: boolean };
267
- onChange: (content: SectionContent) => void;
268
- expanded: boolean;
269
- }) {
270
- const hex = color.spaces[0]?.hex;
271
- const contrast = getContrastClass(hex);
272
-
273
- const updateColor = (updated: ColorItem) => {
274
- const newColors = colors.map((c, i) => (i === index ? updated : c));
275
- onChange(buildContent(newColors, options));
276
- };
277
-
278
- return (
279
- <div className="flex h-full flex-col overflow-hidden rounded-outer border border-base-200">
280
- <div className={cn("flex min-h-[80px] items-end p-3", contrast)} style={{ backgroundColor: hex || "#ccc" }}>
281
- <EditablePlainText
282
- tag="span"
283
- value={color.name || ""}
284
- onChange={(name) => updateColor({ ...color, name })}
285
- isEditMode={true}
286
- placeholder="Color name"
287
- className="text-sm font-bold"
288
- />
289
- </div>
290
- {expanded && (
291
- // flex-1 so every swatch's label block grows to the tallest in its row.
292
- <div className="flex-1 space-y-1 bg-base-accent p-3 text-sm">
293
- {color.spaces.map((space, j) =>
294
- Object.entries(space).map(([key, value]) =>
295
- value ? (
296
- <div key={`${j}-${key}`} className="flex w-full justify-between">
297
- <span className="font-medium uppercase">{key}</span>
298
- <span>{value}</span>
299
- </div>
300
- ) : null
301
- )
302
- )}
303
- </div>
304
- )}
305
- </div>
306
- );
307
- }
308
-
@@ -0,0 +1,155 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Eye, EyeOff } from "lucide-react";
3
+ import { cn } from "../../lib/cn";
4
+ import { EditableGrid } from "../primitives/EditableGrid";
5
+ import { EditablePlainText } from "../primitives/EditablePlainText";
6
+ import type { ReactNode } from "react";
7
+ import ColorSwatchSettings from "./ColorSwatchSettings";
8
+ import { getContrastClass, createColorItem } from "./colors-shared";
9
+ import type { ColorItem } from "../sections/Colors/schema";
10
+ import type { SectionContent } from "../../schemas/sections";
11
+
12
+ export function buildContent(
13
+ colors: ColorItem[],
14
+ options: { label?: string; columns?: number; collapsing?: boolean; showLabel?: boolean },
15
+ ): SectionContent {
16
+ const { label, columns, collapsing, showLabel } = options;
17
+ return {
18
+ type: "colors",
19
+ content: { colors },
20
+ ...(label || columns || collapsing !== undefined || showLabel !== undefined
21
+ ? { options: { label, columns, collapsing, showLabel } }
22
+ : {}),
23
+ };
24
+ }
25
+
26
+ export default function ColorsEditable({
27
+ colors, columns, label, collapsing, showLabel, onChange, openModal, onDelete,
28
+ }: {
29
+ colors: ColorItem[];
30
+ columns: number;
31
+ label?: string;
32
+ collapsing?: boolean;
33
+ showLabel?: boolean;
34
+ onChange: (content: SectionContent) => void;
35
+ openModal: (title: string, content: ReactNode) => void;
36
+ onDelete?: () => void;
37
+ }) {
38
+ const [expanded, setExpanded] = useState(!collapsing);
39
+ const options = { label, columns, collapsing, showLabel };
40
+
41
+ // Sync expanded state when collapsing option changes via settings
42
+ useEffect(() => {
43
+ setExpanded(!collapsing);
44
+ }, [collapsing]);
45
+
46
+ const handleItemSettings = (index: number) => {
47
+ const color = colors[index];
48
+ openModal(
49
+ "Color Settings",
50
+ <ColorSwatchSettings color={color} onChange={(updated) => {
51
+ const newColors = colors.map((c, i) => (i === index ? updated : c));
52
+ onChange(buildContent(newColors, options));
53
+ }} />,
54
+ );
55
+ };
56
+
57
+ return (
58
+ <div>
59
+ {(showLabel && !!label || collapsing) && (
60
+ <div className="mb-4 flex items-center gap-2">
61
+ {showLabel && !!label && (
62
+ <EditablePlainText
63
+ tag="span"
64
+ value={label}
65
+ onChange={(newLabel) => onChange(buildContent(colors, { ...options, label: newLabel }))}
66
+ isEditMode={true}
67
+ className="text-lg font-bold"
68
+ />
69
+ )}
70
+ {collapsing && (
71
+ <button
72
+ onClick={() => setExpanded(!expanded)}
73
+ className="cursor-pointer ml-auto flex items-center gap-1.5 text-sm text-base-contrast-light hover:text-base-contrast"
74
+ >
75
+ <span>{expanded ? "Hide Details" : "Show Details"}</span>
76
+ {expanded ? <EyeOff size={16} /> : <Eye size={16} />}
77
+ </button>
78
+ )}
79
+ </div>
80
+ )}
81
+
82
+ <EditableGrid
83
+ items={colors}
84
+ columns={columns}
85
+ // Shared group lets a swatch be dragged from one colors section into another.
86
+ group="colors"
87
+ // When the last swatch is dragged out to another section, delete this now-empty one.
88
+ onEmptied={onDelete}
89
+ onChange={(newColors) => onChange(buildContent(newColors, options))}
90
+ createItem={createColorItem}
91
+ isEditMode={true}
92
+ onItemSettings={handleItemSettings}
93
+ renderItem={(color, { index }) => (
94
+ <ColorSwatchEditable
95
+ color={color}
96
+ index={index}
97
+ colors={colors}
98
+ options={options}
99
+ onChange={onChange}
100
+ expanded={expanded}
101
+ />
102
+ )}
103
+ />
104
+ </div>
105
+ );
106
+ }
107
+
108
+ function ColorSwatchEditable({
109
+ color, index, colors, options, onChange, expanded,
110
+ }: {
111
+ color: ColorItem;
112
+ index: number;
113
+ colors: ColorItem[];
114
+ options: { label?: string; columns?: number; collapsing?: boolean; showLabel?: boolean };
115
+ onChange: (content: SectionContent) => void;
116
+ expanded: boolean;
117
+ }) {
118
+ const hex = color.spaces[0]?.hex;
119
+ const contrast = getContrastClass(hex);
120
+
121
+ const updateColor = (updated: ColorItem) => {
122
+ const newColors = colors.map((c, i) => (i === index ? updated : c));
123
+ onChange(buildContent(newColors, options));
124
+ };
125
+
126
+ return (
127
+ <div className="flex h-full flex-col overflow-hidden rounded-outer border border-base-200">
128
+ <div className={cn("flex min-h-[80px] items-end p-3", contrast)} style={{ backgroundColor: hex || "#ccc" }}>
129
+ <EditablePlainText
130
+ tag="span"
131
+ value={color.name || ""}
132
+ onChange={(name) => updateColor({ ...color, name })}
133
+ isEditMode={true}
134
+ placeholder="Color name"
135
+ className="text-sm font-bold"
136
+ />
137
+ </div>
138
+ {expanded && (
139
+ // flex-1 so every swatch's label block grows to the tallest in its row.
140
+ <div className="flex-1 space-y-1 bg-base-accent p-3 text-sm">
141
+ {color.spaces.map((space, j) =>
142
+ Object.entries(space).map(([key, value]) =>
143
+ value ? (
144
+ <div key={`${j}-${key}`} className="flex w-full justify-between">
145
+ <span className="font-medium uppercase">{key}</span>
146
+ <span>{value}</span>
147
+ </div>
148
+ ) : null
149
+ )
150
+ )}
151
+ </div>
152
+ )}
153
+ </div>
154
+ );
155
+ }
@@ -0,0 +1,100 @@
1
+ import { useState } from "react";
2
+ import { Eye, EyeOff } from "lucide-react";
3
+ import { cn } from "../../lib/cn";
4
+ import { containerGridClass } from "../../lib/container-grid";
5
+ import { getContrastClass } from "./colors-shared";
6
+ import type { ColorItem } from "../sections/Colors/schema";
7
+
8
+ export default function ColorsView({
9
+ colors,
10
+ columns,
11
+ label,
12
+ collapsing,
13
+ showLabel,
14
+ }: {
15
+ colors: ColorItem[];
16
+ columns: number;
17
+ label?: string;
18
+ collapsing?: boolean;
19
+ showLabel?: boolean;
20
+ }) {
21
+ const [expanded, setExpanded] = useState(!collapsing);
22
+ const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
23
+
24
+ const handleCopy = (value: string, colorIndex: number) => {
25
+ if (navigator.clipboard?.writeText) {
26
+ navigator.clipboard.writeText(value);
27
+ } else {
28
+ const ta = document.createElement("textarea");
29
+ ta.value = value;
30
+ ta.style.position = "fixed";
31
+ ta.style.opacity = "0";
32
+ document.body.appendChild(ta);
33
+ ta.select();
34
+ document.execCommand("copy");
35
+ document.body.removeChild(ta);
36
+ }
37
+ setCopiedIndex(colorIndex);
38
+ setTimeout(() => setCopiedIndex(null), 1500);
39
+ };
40
+
41
+ return (
42
+ <div>
43
+ {(showLabel && !!label || collapsing) && (
44
+ <div className="mb-4 flex items-center gap-2">
45
+ {showLabel && !!label && (
46
+ <span className="text-lg font-bold">{label}</span>
47
+ )}
48
+ {collapsing && (
49
+ <button
50
+ onClick={() => setExpanded(!expanded)}
51
+ className="cursor-pointer ml-auto flex items-center gap-1.5 text-sm text-base-contrast-light hover:text-base-contrast"
52
+ >
53
+ <span>{expanded ? "Hide Details" : "Show Details"}</span>
54
+ {expanded ? <EyeOff size={16} /> : <Eye size={16} />}
55
+ </button>
56
+ )}
57
+ </div>
58
+ )}
59
+
60
+ <div className="@container">
61
+ <div className={cn("grid gap-4", containerGridClass[columns] || containerGridClass[3])}>
62
+ {colors.map((color, i) => {
63
+ const hex = color.spaces[0]?.hex;
64
+ const contrast = getContrastClass(hex);
65
+ return (
66
+ <div key={i} className="flex h-full flex-col overflow-hidden rounded-outer border border-base-200">
67
+ <div className={cn("relative flex min-h-[80px] items-end p-3", contrast)} style={{ backgroundColor: hex || "#ccc" }}>
68
+ {color.name && <span className="text-sm font-bold">{color.name}</span>}
69
+ {copiedIndex === i && (
70
+ <span className={cn(
71
+ "absolute top-2 right-2 rounded-full px-2.5 py-0.5 text-xs font-medium",
72
+ getContrastClass(hex) === "text-black" ? "bg-black/15 text-black" : "bg-white/25 text-white",
73
+ )}>
74
+ Copied!
75
+ </span>
76
+ )}
77
+ </div>
78
+ {expanded && (
79
+ // flex-1 so every swatch's label block grows to the tallest in its row.
80
+ <div className="flex-1 space-y-1 bg-base-accent p-3 text-sm">
81
+ {color.spaces.map((space, j) =>
82
+ Object.entries(space).map(([key, value]) =>
83
+ value ? (
84
+ <button key={`${j}-${key}`} onClick={() => handleCopy(value, i)} className="cursor-pointer flex w-full justify-between hover:text-primary">
85
+ <span className="font-medium uppercase">{key}</span>
86
+ <span>{value}</span>
87
+ </button>
88
+ ) : null
89
+ )
90
+ )}
91
+ </div>
92
+ )}
93
+ </div>
94
+ );
95
+ })}
96
+ </div>
97
+ </div>
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,17 @@
1
+ import type { ColorItem } from "../sections/Colors/schema";
2
+
3
+ export function getContrastClass(hex: string | undefined): string {
4
+ if (!hex) return "text-white";
5
+ const h = hex.replace("#", "");
6
+ if (h.length < 6) return "text-white";
7
+ const r = parseInt(h.substring(0, 2), 16);
8
+ const g = parseInt(h.substring(2, 4), 16);
9
+ const b = parseInt(h.substring(4, 6), 16);
10
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000;
11
+ return yiq >= 128 ? "text-black" : "text-white";
12
+ }
13
+
14
+ export const createColorItem = (): ColorItem => ({
15
+ name: "",
16
+ spaces: [{ hex: "#cccccc" }],
17
+ });
@@ -24,6 +24,7 @@ const schema = z.object({
24
24
 
25
25
  export default defineSection({
26
26
  type: "colors",
27
+ interactive: true,
27
28
  category: "brand-guide",
28
29
  label: "Colors",
29
30
  icon: <Palette size={18} />,
@@ -0,0 +1,21 @@
1
+ import { ensureSectionsRegistered } from "./register";
2
+ import { getSection } from "../../lib/registry";
3
+
4
+ // Registering on import means the registry is populated both during SSR of the
5
+ // island and after it hydrates client-side (register runs again in the browser
6
+ // bundle; the Symbol.for-keyed singleton dedupes).
7
+ ensureSectionsRegistered();
8
+
9
+ interface Props {
10
+ type: string;
11
+ content: unknown;
12
+ options?: Record<string, unknown>;
13
+ }
14
+
15
+ export default function SectionIsland({ type, content, options }: Props) {
16
+ const def = getSection(type);
17
+ if (!def) return null;
18
+ const Component = def.component;
19
+ // Viewer mode: no onChange / openModal callbacks -> the view (not editable) path.
20
+ return <Component content={content} options={options} isEditMode={false} />;
21
+ }
@@ -14,12 +14,13 @@ interface Props {
14
14
  siteName: string;
15
15
  darkMode: "light" | "dark" | "optional";
16
16
  lastUpdated: string | null;
17
+ logo?: { lightUrl: string; darkUrl: string | null; invertInDark: boolean } | null;
17
18
  // Optional same-island handler (tests). In production the editor nav is a
18
19
  // separate island from EditorShell, so a click dispatches pageSelectEvent.
19
20
  onPageSelect?: (pageId: string) => void;
20
21
  }
21
22
 
22
- export default function Navigation({ siteNav: initialNav, siteName, darkMode, lastUpdated, onPageSelect }: Props) {
23
+ export default function Navigation({ siteNav: initialNav, siteName, darkMode, lastUpdated, logo, onPageSelect }: Props) {
23
24
  const [isOpen, setIsOpen] = useState(false);
24
25
  const [isEditMode, setIsEditMode] = useState(false);
25
26
  const [currentDarkMode, setCurrentDarkMode] = useState(darkMode);
@@ -135,6 +136,19 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
135
136
  </ul>
136
137
  );
137
138
 
139
+ const logoMark = logo ? (
140
+ <>
141
+ <img
142
+ src={logo.lightUrl}
143
+ alt={currentSiteName}
144
+ className={logo.darkUrl ? "block h-8 w-auto object-contain dark:hidden" : logo.invertInDark ? "h-8 w-auto object-contain dark:invert" : "h-8 w-auto object-contain"}
145
+ />
146
+ {logo.darkUrl && (
147
+ <img src={logo.darkUrl} alt={currentSiteName} className="hidden h-8 w-auto object-contain dark:block" />
148
+ )}
149
+ </>
150
+ ) : null;
151
+
138
152
  const handlePageClick = useCallback((id: string) => {
139
153
  if (onPageSelect) onPageSelect(id);
140
154
  else pageSelectEvent.dispatch(id);
@@ -193,7 +207,10 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
193
207
  <>
194
208
  <header className="fixed top-0 left-0 right-0 z-50 bg-base lg:hidden">
195
209
  <div className="mx-auto max-w-screen-xl flex h-16 items-center justify-between px-4">
196
- <span className="text-lg font-bold text-primary">{currentSiteName}</span>
210
+ <div className="flex flex-col items-start gap-1">
211
+ {logoMark}
212
+ <span className="text-lg font-bold text-primary">{currentSiteName}</span>
213
+ </div>
197
214
  <button onClick={() => setIsOpen(!isOpen)} className="cursor-pointer p-2 text-base-contrast" aria-label="Toggle navigation">
198
215
  <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
199
216
  {isOpen
@@ -209,6 +226,7 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
209
226
  <nav className={cn("fixed top-0 left-0 lg:left-auto z-40 h-full w-64 flex flex-col overflow-y-auto border-r border-base-200 bg-base pt-16 transition-transform lg:translate-x-0 nav-sidebar",
210
227
  isOpen ? "translate-x-0" : "-translate-x-full")}>
211
228
  <div className="hidden px-4 py-4 lg:block">
229
+ {logoMark}
212
230
  <span className="text-lg font-bold text-primary">{currentSiteName}</span>
213
231
  </div>
214
232
 
@@ -363,6 +363,10 @@ export default function EditorShell({
363
363
  markSectionDirty: persistence.markSectionDirty,
364
364
  onUploadError: (message) => setUploadErrorMessage(message),
365
365
  onUploadNotice: setUploadNotice,
366
+ onSiteConfigChange: (updater) => {
367
+ if (!siteConfig) return;
368
+ handleSiteConfigChange(updater(siteConfig));
369
+ },
366
370
  });
367
371
 
368
372
  const buildStatus = useBuildStatus();