@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.
Files changed (87) hide show
  1. package/dist/{chunk-PTVY3Q7P.js → chunk-2QXJ2KGB.js} +1 -1
  2. package/dist/{chunk-DRCPDOW3.js → chunk-CZ3YXGK5.js} +59 -0
  3. package/dist/{chunk-NEDOJHHR.js → chunk-J75XGXYD.js} +2 -2
  4. package/dist/{chunk-6VOIZP2O.js → chunk-L2MAJKKC.js} +220 -207
  5. package/dist/{chunk-VY5V4HBV.js → chunk-MVX4ULYE.js} +6 -6
  6. package/dist/{chunk-EO7K45GV.js → chunk-NV4R6VHN.js} +1 -1
  7. package/dist/{chunk-XDG2K6UT.js → chunk-QFV5TSPO.js} +1 -1
  8. package/dist/{chunk-OEL57NYV.js → chunk-VK5KNH73.js} +25 -1
  9. package/dist/{chunk-XPUBUR64.js → chunk-WCBYXNAG.js} +27 -2
  10. package/dist/{chunk-WA3HFN54.js → chunk-Y24T75HT.js} +1 -1
  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/chat/ChatPanel.d.ts +20 -3
  19. package/dist/components/chat/ChatPanel.d.ts.map +1 -1
  20. package/dist/components/chat/ChatWidget.d.ts +7 -1
  21. package/dist/components/chat/ChatWidget.d.ts.map +1 -1
  22. package/dist/components/chat/chat-history.d.ts +24 -0
  23. package/dist/components/chat/chat-history.d.ts.map +1 -0
  24. package/dist/components/editor/index.js +4 -4
  25. package/dist/components/primitives/index.js +5 -5
  26. package/dist/components/sections/Colors/index.d.ts.map +1 -1
  27. package/dist/components/sections/SectionIsland.d.ts +8 -0
  28. package/dist/components/sections/SectionIsland.d.ts.map +1 -0
  29. package/dist/components/sections/brand-guide.js +5 -5
  30. package/dist/components/sections/register-schemas.js +8 -8
  31. package/dist/components/sections/register.js +8 -8
  32. package/dist/components/shared/Navigation.d.ts +6 -1
  33. package/dist/components/shared/Navigation.d.ts.map +1 -1
  34. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  35. package/dist/components/shell/MediaLibraryContext.d.ts +3 -1
  36. package/dist/components/shell/MediaLibraryContext.d.ts.map +1 -1
  37. package/dist/components/shell/SiteSettingsChatbot.d.ts.map +1 -1
  38. package/dist/hooks/useCollabSession.d.ts.map +1 -1
  39. package/dist/hooks/useMediaPipeline.d.ts +5 -1
  40. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  41. package/dist/index.js +14 -4
  42. package/dist/lib/chatbot.d.ts +10 -0
  43. package/dist/lib/chatbot.d.ts.map +1 -0
  44. package/dist/lib/contrast.d.ts +5 -0
  45. package/dist/lib/contrast.d.ts.map +1 -1
  46. package/dist/lib/index.d.ts +2 -1
  47. package/dist/lib/index.d.ts.map +1 -1
  48. package/dist/lib/index.js +10 -2
  49. package/dist/lib/registry.d.ts +7 -0
  50. package/dist/lib/registry.d.ts.map +1 -1
  51. package/dist/media/compose-preview.d.ts +23 -0
  52. package/dist/media/compose-preview.d.ts.map +1 -0
  53. package/dist/media/index.d.ts +1 -0
  54. package/dist/media/index.d.ts.map +1 -1
  55. package/dist/media/index.js +4 -2
  56. package/dist/media/preview-inputs.d.ts +31 -0
  57. package/dist/media/preview-inputs.d.ts.map +1 -0
  58. package/dist/schemas/index.js +2 -2
  59. package/dist/schemas/media.d.ts +2 -0
  60. package/dist/schemas/media.d.ts.map +1 -1
  61. package/dist/schemas/site-config.d.ts +12 -0
  62. package/dist/schemas/site-config.d.ts.map +1 -1
  63. package/package.json +1 -1
  64. package/src/components/brandguide/Colors.tsx +2 -275
  65. package/src/components/brandguide/ColorsEditable.tsx +155 -0
  66. package/src/components/brandguide/ColorsView.tsx +100 -0
  67. package/src/components/brandguide/colors-shared.ts +17 -0
  68. package/src/components/chat/ChatPanel.tsx +437 -65
  69. package/src/components/chat/ChatWidget.tsx +90 -32
  70. package/src/components/chat/chat-history.ts +105 -0
  71. package/src/components/sections/Colors/index.tsx +1 -0
  72. package/src/components/sections/SectionIsland.tsx +21 -0
  73. package/src/components/shared/Navigation.tsx +18 -5
  74. package/src/components/shell/EditorShell.tsx +71 -1
  75. package/src/components/shell/MediaLibraryContext.tsx +1 -1
  76. package/src/components/shell/SiteSettingsChatbot.tsx +47 -0
  77. package/src/hooks/useCollabSession.ts +27 -0
  78. package/src/hooks/useMediaPipeline.ts +7 -3
  79. package/src/lib/chatbot.ts +30 -0
  80. package/src/lib/contrast.ts +12 -0
  81. package/src/lib/index.ts +2 -1
  82. package/src/lib/registry.ts +7 -0
  83. package/src/media/compose-preview.ts +90 -0
  84. package/src/media/index.ts +1 -0
  85. package/src/media/preview-inputs.ts +77 -0
  86. package/src/schemas/media.ts +3 -0
  87. 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 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
+ });