@drawnagency/primitives 0.1.87 → 0.1.89
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-PTVY3Q7P.js → chunk-2QXJ2KGB.js} +1 -1
- package/dist/{chunk-DRCPDOW3.js → chunk-CZ3YXGK5.js} +59 -0
- package/dist/{chunk-NEDOJHHR.js → chunk-J75XGXYD.js} +2 -2
- package/dist/{chunk-6VOIZP2O.js → chunk-L2MAJKKC.js} +220 -207
- package/dist/{chunk-VY5V4HBV.js → chunk-MVX4ULYE.js} +6 -6
- package/dist/{chunk-EO7K45GV.js → chunk-NV4R6VHN.js} +1 -1
- package/dist/{chunk-XDG2K6UT.js → chunk-QFV5TSPO.js} +1 -1
- package/dist/{chunk-OEL57NYV.js → chunk-VK5KNH73.js} +25 -1
- package/dist/{chunk-XPUBUR64.js → chunk-WCBYXNAG.js} +27 -2
- package/dist/{chunk-WA3HFN54.js → chunk-Y24T75HT.js} +1 -1
- 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/chat/ChatPanel.d.ts +20 -3
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/chat/ChatWidget.d.ts +7 -1
- package/dist/components/chat/ChatWidget.d.ts.map +1 -1
- package/dist/components/chat/chat-history.d.ts +24 -0
- package/dist/components/chat/chat-history.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/MediaLibraryContext.d.ts +3 -1
- package/dist/components/shell/MediaLibraryContext.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsChatbot.d.ts.map +1 -1
- package/dist/hooks/useCollabSession.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts +5 -1
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/index.js +14 -4
- package/dist/lib/chatbot.d.ts +10 -0
- package/dist/lib/chatbot.d.ts.map +1 -0
- package/dist/lib/contrast.d.ts +5 -0
- package/dist/lib/contrast.d.ts.map +1 -1
- package/dist/lib/index.d.ts +2 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +10 -2
- package/dist/lib/registry.d.ts +7 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/media/compose-preview.d.ts +23 -0
- package/dist/media/compose-preview.d.ts.map +1 -0
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +4 -2
- package/dist/media/preview-inputs.d.ts +31 -0
- package/dist/media/preview-inputs.d.ts.map +1 -0
- package/dist/schemas/index.js +2 -2
- package/dist/schemas/media.d.ts +2 -0
- package/dist/schemas/media.d.ts.map +1 -1
- package/dist/schemas/site-config.d.ts +12 -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/chat/ChatPanel.tsx +437 -65
- package/src/components/chat/ChatWidget.tsx +90 -32
- package/src/components/chat/chat-history.ts +105 -0
- package/src/components/sections/Colors/index.tsx +1 -0
- package/src/components/sections/SectionIsland.tsx +21 -0
- package/src/components/shared/Navigation.tsx +18 -5
- package/src/components/shell/EditorShell.tsx +71 -1
- package/src/components/shell/MediaLibraryContext.tsx +1 -1
- package/src/components/shell/SiteSettingsChatbot.tsx +47 -0
- package/src/hooks/useCollabSession.ts +27 -0
- package/src/hooks/useMediaPipeline.ts +7 -3
- package/src/lib/chatbot.ts +30 -0
- package/src/lib/contrast.ts +12 -0
- package/src/lib/index.ts +2 -1
- package/src/lib/registry.ts +7 -0
- package/src/media/compose-preview.ts +90 -0
- package/src/media/index.ts +1 -0
- package/src/media/preview-inputs.ts +77 -0
- package/src/schemas/media.ts +3 -0
- package/src/schemas/site-config.ts +26 -1
|
@@ -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
|
+
});
|