@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.
- package/dist/{chunk-YKOLOZB2.js → chunk-DRCPDOW3.js} +14 -0
- package/dist/{chunk-MBJUWOM2.js → chunk-EO7K45GV.js} +1 -1
- package/dist/{chunk-2CVP2AGK.js → chunk-H2FENAQH.js} +6 -6
- package/dist/{chunk-DKXISNJ2.js → chunk-NEDOJHHR.js} +2 -2
- package/dist/{chunk-DO3ZENLN.js → chunk-OEL57NYV.js} +1 -1
- package/dist/{chunk-FDBZZ4AT.js → chunk-PTVY3Q7P.js} +1 -1
- package/dist/{chunk-665DW3HR.js → chunk-SK6GGTTO.js} +220 -207
- package/dist/{chunk-EP3LVOXZ.js → chunk-WA3HFN54.js} +1 -1
- package/dist/{chunk-YAPG44NS.js → chunk-XDG2K6UT.js} +1 -1
- package/dist/{chunk-E3MTIMOG.js → chunk-XPUBUR64.js} +10 -0
- package/dist/components/brandguide/Colors.d.ts.map +1 -1
- package/dist/components/brandguide/ColorsEditable.d.ts +20 -0
- package/dist/components/brandguide/ColorsEditable.d.ts.map +1 -0
- package/dist/components/brandguide/ColorsView.d.ts +9 -0
- package/dist/components/brandguide/ColorsView.d.ts.map +1 -0
- package/dist/components/brandguide/colors-shared.d.ts +4 -0
- package/dist/components/brandguide/colors-shared.d.ts.map +1 -0
- package/dist/components/editor/index.js +4 -4
- package/dist/components/primitives/index.js +5 -5
- package/dist/components/sections/Colors/index.d.ts.map +1 -1
- package/dist/components/sections/SectionIsland.d.ts +8 -0
- package/dist/components/sections/SectionIsland.d.ts.map +1 -0
- package/dist/components/sections/brand-guide.js +5 -5
- package/dist/components/sections/register-schemas.js +8 -8
- package/dist/components/sections/register.js +8 -8
- package/dist/components/shared/Navigation.d.ts +6 -1
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
- package/dist/hooks/useCollabSession.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts +2 -1
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/lib/image-refs.d.ts +9 -0
- package/dist/lib/image-refs.d.ts.map +1 -1
- package/dist/lib/index.js +2 -2
- package/dist/lib/registry.d.ts +7 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/media/index.js +4 -2
- package/dist/media/resolve.d.ts +25 -0
- package/dist/media/resolve.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -2
- package/dist/schemas/site-config.d.ts +10 -0
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/brandguide/Colors.tsx +2 -275
- package/src/components/brandguide/ColorsEditable.tsx +155 -0
- package/src/components/brandguide/ColorsView.tsx +100 -0
- package/src/components/brandguide/colors-shared.ts +17 -0
- package/src/components/sections/Colors/index.tsx +1 -0
- package/src/components/sections/SectionIsland.tsx +21 -0
- package/src/components/shared/Navigation.tsx +20 -2
- package/src/components/shell/EditorShell.tsx +4 -0
- package/src/components/shell/SiteSettingsDisplay.tsx +106 -0
- package/src/hooks/useCollabSession.ts +27 -0
- package/src/hooks/useMediaPipeline.ts +16 -3
- package/src/lib/image-refs.ts +19 -0
- package/src/lib/registry.ts +7 -0
- package/src/media/resolve.ts +34 -0
- 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
|
|
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
|
+
});
|
|
@@ -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
|
-
<
|
|
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();
|