@tangle-network/sandbox-ui 0.22.1 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/assets.js ADDED
@@ -0,0 +1,946 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-EI44GEQ5.js";
4
+
5
+ // src/assets/approval-queue.tsx
6
+ import * as React3 from "react";
7
+ import { Button as Button3, Input as Input2 } from "@tangle-network/ui/primitives";
8
+ import { Search, CheckCheck } from "lucide-react";
9
+
10
+ // src/assets/asset-card.tsx
11
+ import { Badge } from "@tangle-network/ui/primitives";
12
+ import { Check, X, Pencil, GitBranch } from "lucide-react";
13
+
14
+ // src/assets/preview/image-preview.tsx
15
+ import * as React from "react";
16
+ import { ChevronLeft, ChevronRight } from "lucide-react";
17
+ import { jsx, jsxs } from "react/jsx-runtime";
18
+ var ASPECT = {
19
+ feed: "aspect-square",
20
+ story: "aspect-[9/16]",
21
+ carousel: "aspect-square"
22
+ };
23
+ function bgStyle(bg, brand) {
24
+ if (bg.type === "color") return { background: bg.value };
25
+ if (bg.type === "gradient") return { background: `linear-gradient(${bg.direction ?? "to bottom right"}, ${bg.from}, ${bg.to})` };
26
+ if (bg.type === "image") {
27
+ return {
28
+ backgroundImage: `url(${bg.url})`,
29
+ backgroundSize: "cover",
30
+ backgroundPosition: "center"
31
+ };
32
+ }
33
+ return { background: brand.primaryColor };
34
+ }
35
+ function Layer({ layer, brand }) {
36
+ const base = {
37
+ position: "absolute",
38
+ left: `${layer.x}%`,
39
+ top: `${layer.y}%`
40
+ };
41
+ if (layer.type === "text") {
42
+ return /* @__PURE__ */ jsx(
43
+ "div",
44
+ {
45
+ style: {
46
+ ...base,
47
+ fontSize: layer.fontSize ?? 16,
48
+ fontWeight: layer.fontWeight ?? "normal",
49
+ color: layer.color ?? brand.textColor,
50
+ width: layer.width ? `${layer.width}%` : void 0,
51
+ textAlign: layer.align ?? "left"
52
+ },
53
+ children: layer.text
54
+ }
55
+ );
56
+ }
57
+ if (layer.type === "image") {
58
+ return /* @__PURE__ */ jsx(
59
+ "img",
60
+ {
61
+ src: layer.url,
62
+ alt: "",
63
+ style: {
64
+ ...base,
65
+ width: `${layer.width}%`,
66
+ height: `${layer.height}%`,
67
+ opacity: layer.opacity ?? 1,
68
+ objectFit: "cover"
69
+ }
70
+ }
71
+ );
72
+ }
73
+ if (layer.type === "shape") {
74
+ return /* @__PURE__ */ jsx(
75
+ "div",
76
+ {
77
+ style: {
78
+ ...base,
79
+ width: `${layer.width}%`,
80
+ height: `${layer.height}%`,
81
+ background: layer.fill ?? brand.accentColor,
82
+ opacity: layer.opacity ?? 1,
83
+ borderRadius: layer.shape === "circle" ? "50%" : layer.shape === "rounded-rect" ? "8px" : 0
84
+ }
85
+ }
86
+ );
87
+ }
88
+ if (layer.type === "logo" && brand.logoUrl) {
89
+ return /* @__PURE__ */ jsx(
90
+ "img",
91
+ {
92
+ src: brand.logoUrl,
93
+ alt: brand.businessName,
94
+ style: {
95
+ ...base,
96
+ width: layer.width ? `${layer.width}%` : "12%"
97
+ }
98
+ }
99
+ );
100
+ }
101
+ return null;
102
+ }
103
+ function Slide({ slide, brand }) {
104
+ return /* @__PURE__ */ jsx("div", { className: "relative w-full h-full overflow-hidden", style: bgStyle(slide.background, brand), children: slide.layers.map((layer, i) => /* @__PURE__ */ jsx(Layer, { layer, brand }, i)) });
105
+ }
106
+ function ImagePreview({ content, brand, format = "feed", className }) {
107
+ const [activeIndex, setActiveIndex] = React.useState(0);
108
+ const slides = content.slides;
109
+ const isMulti = slides.length > 1;
110
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-2", className), children: [
111
+ /* @__PURE__ */ jsxs("div", { className: cn("relative w-full rounded overflow-hidden", ASPECT[format]), children: [
112
+ /* @__PURE__ */ jsx(Slide, { slide: slides[activeIndex], brand }),
113
+ isMulti && activeIndex > 0 && /* @__PURE__ */ jsx(
114
+ "button",
115
+ {
116
+ onClick: () => setActiveIndex((i) => i - 1),
117
+ className: "absolute left-1 top-1/2 -translate-y-1/2 rounded-full p-1 bg-black/40 text-white hover:bg-black/60",
118
+ children: /* @__PURE__ */ jsx(ChevronLeft, { size: 16 })
119
+ }
120
+ ),
121
+ isMulti && activeIndex < slides.length - 1 && /* @__PURE__ */ jsx(
122
+ "button",
123
+ {
124
+ onClick: () => setActiveIndex((i) => i + 1),
125
+ className: "absolute right-1 top-1/2 -translate-y-1/2 rounded-full p-1 bg-black/40 text-white hover:bg-black/60",
126
+ children: /* @__PURE__ */ jsx(ChevronRight, { size: 16 })
127
+ }
128
+ ),
129
+ isMulti && /* @__PURE__ */ jsx("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 flex gap-1", children: slides.map((_, i) => /* @__PURE__ */ jsx(
130
+ "button",
131
+ {
132
+ onClick: () => setActiveIndex(i),
133
+ className: cn(
134
+ "w-1.5 h-1.5 rounded-full transition-colors",
135
+ i === activeIndex ? "bg-white" : "bg-white/40"
136
+ )
137
+ },
138
+ i
139
+ )) })
140
+ ] }),
141
+ isMulti && /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground text-center", children: [
142
+ activeIndex + 1,
143
+ " / ",
144
+ slides.length
145
+ ] })
146
+ ] });
147
+ }
148
+
149
+ // src/assets/asset-card.tsx
150
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
151
+ var STATUS_VARIANT = {
152
+ draft: "secondary",
153
+ pending_review: "outline",
154
+ approved: "default",
155
+ rejected: "destructive",
156
+ scheduled: "secondary",
157
+ published: "default"
158
+ };
159
+ var STATUS_LABEL = {
160
+ draft: "Draft",
161
+ pending_review: "Review",
162
+ approved: "Approved",
163
+ rejected: "Rejected",
164
+ scheduled: "Scheduled",
165
+ published: "Published"
166
+ };
167
+ function Thumbnail({ spec }) {
168
+ const { format, brand, content } = spec;
169
+ if (format === "email") {
170
+ const ec = content;
171
+ return /* @__PURE__ */ jsxs2("div", { className: "p-2 text-xs space-y-0.5", children: [
172
+ /* @__PURE__ */ jsx2("div", { className: "font-medium truncate", children: ec.subject }),
173
+ /* @__PURE__ */ jsxs2("div", { className: "text-muted-foreground truncate text-[10px]", children: [
174
+ ec.sections.length,
175
+ " sections"
176
+ ] })
177
+ ] });
178
+ }
179
+ if (format.startsWith("image:")) {
180
+ const imgFormat = format === "image:story" ? "story" : "feed";
181
+ return /* @__PURE__ */ jsx2("div", { className: "overflow-hidden rounded", style: { maxHeight: 120 }, children: /* @__PURE__ */ jsx2(
182
+ ImagePreview,
183
+ {
184
+ content,
185
+ brand,
186
+ format: imgFormat
187
+ }
188
+ ) });
189
+ }
190
+ if (format.startsWith("video:")) {
191
+ const vc = content;
192
+ return /* @__PURE__ */ jsxs2("div", { className: "p-2 text-xs space-y-0.5", children: [
193
+ /* @__PURE__ */ jsxs2("div", { className: "font-medium", children: [
194
+ vc.scenes.length,
195
+ " scenes"
196
+ ] }),
197
+ /* @__PURE__ */ jsxs2("div", { className: "text-muted-foreground", children: [
198
+ vc.durationSeconds,
199
+ "s video"
200
+ ] })
201
+ ] });
202
+ }
203
+ if (format.startsWith("copy:")) {
204
+ const cc = content;
205
+ return /* @__PURE__ */ jsxs2("div", { className: "p-2 text-xs truncate space-y-0.5", children: [
206
+ /* @__PURE__ */ jsx2("div", { className: "font-medium truncate", children: cc.headline }),
207
+ /* @__PURE__ */ jsx2("div", { className: "text-muted-foreground line-clamp-2 text-[10px]", children: cc.body })
208
+ ] });
209
+ }
210
+ return null;
211
+ }
212
+ function AssetCard({ spec, variantCount, onApprove, onReject, onEdit, className }) {
213
+ return /* @__PURE__ */ jsxs2("div", { className: cn("rounded-lg border border-border bg-card flex flex-col overflow-hidden hover:border-border/80 transition-colors", className), children: [
214
+ /* @__PURE__ */ jsx2("div", { className: "min-h-[80px] bg-muted/30", children: /* @__PURE__ */ jsx2(Thumbnail, { spec }) }),
215
+ /* @__PURE__ */ jsxs2("div", { className: "px-2 py-1.5 flex items-center gap-1.5", children: [
216
+ /* @__PURE__ */ jsx2("span", { className: "text-[10px] text-muted-foreground flex-1 truncate", children: spec.format }),
217
+ /* @__PURE__ */ jsx2(Badge, { variant: STATUS_VARIANT[spec.status], className: "text-[10px] h-4 px-1.5", children: STATUS_LABEL[spec.status] }),
218
+ variantCount !== void 0 && variantCount > 0 && /* @__PURE__ */ jsxs2("span", { className: "flex items-center gap-0.5 text-[10px] text-muted-foreground", children: [
219
+ /* @__PURE__ */ jsx2(GitBranch, { size: 10 }),
220
+ variantCount
221
+ ] })
222
+ ] }),
223
+ /* @__PURE__ */ jsxs2("div", { className: "flex border-t border-border", children: [
224
+ onEdit && /* @__PURE__ */ jsxs2(
225
+ "button",
226
+ {
227
+ type: "button",
228
+ onClick: () => onEdit(spec.id),
229
+ className: "flex-1 flex items-center justify-center gap-1 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors",
230
+ children: [
231
+ /* @__PURE__ */ jsx2(Pencil, { size: 12 }),
232
+ "Edit"
233
+ ]
234
+ }
235
+ ),
236
+ onReject && spec.status === "pending_review" && /* @__PURE__ */ jsxs2(
237
+ "button",
238
+ {
239
+ type: "button",
240
+ onClick: () => onReject(spec.id),
241
+ className: "flex-1 flex items-center justify-center gap-1 py-1.5 text-xs text-muted-foreground hover:text-destructive hover:bg-destructive/5 transition-colors border-l border-border",
242
+ children: [
243
+ /* @__PURE__ */ jsx2(X, { size: 12 }),
244
+ "Reject"
245
+ ]
246
+ }
247
+ ),
248
+ onApprove && spec.status === "pending_review" && /* @__PURE__ */ jsxs2(
249
+ "button",
250
+ {
251
+ type: "button",
252
+ onClick: () => onApprove(spec.id),
253
+ className: "flex-1 flex items-center justify-center gap-1 py-1.5 text-xs text-muted-foreground hover:text-green-600 hover:bg-green-600/5 transition-colors border-l border-border",
254
+ children: [
255
+ /* @__PURE__ */ jsx2(Check, { size: 12 }),
256
+ "Approve"
257
+ ]
258
+ }
259
+ )
260
+ ] })
261
+ ] });
262
+ }
263
+
264
+ // src/assets/asset-editor.tsx
265
+ import * as React2 from "react";
266
+ import { Button as Button2, Input, Textarea, Label } from "@tangle-network/ui/primitives";
267
+ import { MessageSquare, Save, ChevronDown, ChevronRight as ChevronRight2 } from "lucide-react";
268
+
269
+ // src/assets/preview/email-preview.tsx
270
+ import { jsxs as jsxs3, jsx as jsx3 } from "react/jsx-runtime";
271
+ function EmailPreview({ content, brand, previewUrl, className }) {
272
+ if (previewUrl) {
273
+ return jsxs3("div", { className: cn("flex flex-col gap-1", className), children: [
274
+ jsx3("div", { className: "text-xs text-muted-foreground font-medium truncate", children: content.subject }),
275
+ content.preheader && jsx3("div", { className: "text-xs text-muted-foreground/60 truncate", children: content.preheader }),
276
+ jsx3(
277
+ "iframe",
278
+ {
279
+ src: previewUrl,
280
+ className: "w-full rounded border border-border",
281
+ style: { height: 480, background: "#fff" },
282
+ title: "Email preview",
283
+ sandbox: "allow-same-origin"
284
+ }
285
+ )
286
+ ] });
287
+ }
288
+ return jsxs3("div", { className: cn("flex flex-col gap-2", className), children: [
289
+ jsx3("div", { className: "text-sm font-semibold truncate", children: content.subject }),
290
+ content.preheader && jsx3("div", { className: "text-xs text-muted-foreground truncate", children: content.preheader }),
291
+ jsx3(
292
+ "div",
293
+ {
294
+ className: "rounded border border-border p-4 space-y-3 overflow-y-auto",
295
+ style: { maxHeight: 480, fontFamily: brand.fontFamily, color: brand.textColor },
296
+ children: content.sections.map((section, i) => {
297
+ if (section.type === "hero") {
298
+ return jsxs3("div", { className: "text-center py-4 space-y-2", children: [
299
+ section.imageUrl && jsx3("img", { src: section.imageUrl, alt: "", className: "mx-auto max-h-40 object-cover rounded" }),
300
+ jsx3("div", { className: "text-xl font-bold", style: { color: brand.primaryColor }, children: section.headline }),
301
+ section.subheadline && jsx3("div", { className: "text-sm text-muted-foreground", children: section.subheadline }),
302
+ section.ctaLabel && jsx3(
303
+ "a",
304
+ {
305
+ href: section.ctaUrl ?? "#",
306
+ className: "inline-block px-4 py-2 rounded text-sm font-medium text-white",
307
+ style: { background: brand.primaryColor },
308
+ children: section.ctaLabel
309
+ }
310
+ )
311
+ ] }, i);
312
+ }
313
+ if (section.type === "body") {
314
+ return jsx3("p", { className: "text-sm leading-relaxed whitespace-pre-wrap", children: section.text }, i);
315
+ }
316
+ if (section.type === "feature") {
317
+ return jsxs3("div", { className: "flex gap-3 items-start", children: [
318
+ section.imageUrl && jsx3("img", { src: section.imageUrl, alt: "", className: "w-16 h-16 object-cover rounded shrink-0" }),
319
+ jsxs3("div", { children: [
320
+ jsx3("div", { className: "text-sm font-semibold", children: section.headline }),
321
+ jsx3("div", { className: "text-xs text-muted-foreground mt-0.5", children: section.description })
322
+ ] })
323
+ ] }, i);
324
+ }
325
+ if (section.type === "testimonial") {
326
+ return jsxs3("blockquote", { className: "border-l-2 pl-3 italic text-sm text-muted-foreground", style: { borderColor: brand.accentColor }, children: [
327
+ jsxs3("p", { children: [
328
+ '"',
329
+ section.quote,
330
+ '"'
331
+ ] }),
332
+ jsxs3("footer", { className: "mt-1 text-xs not-italic font-medium", children: [
333
+ section.author,
334
+ section.role ? `, ${section.role}` : ""
335
+ ] })
336
+ ] }, i);
337
+ }
338
+ if (section.type === "cta") {
339
+ return jsxs3("div", { className: "text-center py-3 space-y-1", children: [
340
+ jsx3(
341
+ "a",
342
+ {
343
+ href: section.url,
344
+ className: "inline-block px-5 py-2.5 rounded font-medium text-white text-sm",
345
+ style: { background: brand.primaryColor },
346
+ children: section.label
347
+ }
348
+ ),
349
+ section.subtext && jsx3("p", { className: "text-xs text-muted-foreground", children: section.subtext })
350
+ ] }, i);
351
+ }
352
+ if (section.type === "divider") {
353
+ return jsx3("hr", { className: "border-border" }, i);
354
+ }
355
+ return null;
356
+ })
357
+ }
358
+ )
359
+ ] });
360
+ }
361
+
362
+ // src/assets/preview/video-preview.tsx
363
+ import { Fragment } from "react";
364
+ import { Play, Film, Loader2 } from "lucide-react";
365
+ import { Button } from "@tangle-network/ui/primitives";
366
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
367
+ function VideoPreview({ content, brand, onRenderRequest, isRendering, className }) {
368
+ if (content.renderedUrl) {
369
+ return /* @__PURE__ */ jsx4("div", { className: cn("rounded overflow-hidden aspect-[9/16] bg-black", className), children: /* @__PURE__ */ jsx4(
370
+ "video",
371
+ {
372
+ src: content.renderedUrl,
373
+ controls: true,
374
+ className: "w-full h-full object-contain",
375
+ playsInline: true
376
+ }
377
+ ) });
378
+ }
379
+ const sceneSummary = content.scenes.map((s) => {
380
+ if (s.type === "text-animation") return s.headline;
381
+ if (s.type === "image-reveal") return s.caption ?? "Image";
382
+ if (s.type === "slide") return "Slide";
383
+ if (s.type === "countdown") return `${s.from}\u2026`;
384
+ return "Scene";
385
+ });
386
+ return /* @__PURE__ */ jsxs4("div", { className: cn("flex flex-col gap-3", className), children: [
387
+ /* @__PURE__ */ jsxs4(
388
+ "div",
389
+ {
390
+ className: "relative rounded overflow-hidden aspect-[9/16] flex flex-col items-center justify-center gap-3",
391
+ style: { background: brand.primaryColor, color: brand.textColor },
392
+ children: [
393
+ /* @__PURE__ */ jsx4(Film, { size: 32, className: "opacity-60" }),
394
+ /* @__PURE__ */ jsx4("div", { className: "text-sm font-medium text-center px-4", children: brand.businessName }),
395
+ /* @__PURE__ */ jsxs4("div", { className: "text-xs opacity-60", children: [
396
+ content.scenes.length,
397
+ " scenes ",
398
+ "\xB7",
399
+ " ",
400
+ content.durationSeconds,
401
+ "s"
402
+ ] })
403
+ ]
404
+ }
405
+ ),
406
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-1", children: [
407
+ /* @__PURE__ */ jsx4("div", { className: "text-xs font-medium text-muted-foreground", children: "Scenes" }),
408
+ sceneSummary.map((label, i) => /* @__PURE__ */ jsxs4("div", { className: "text-xs text-muted-foreground flex gap-1.5 items-center", children: [
409
+ /* @__PURE__ */ jsx4("span", { className: "font-mono text-[10px] w-4 text-right opacity-50", children: i + 1 }),
410
+ /* @__PURE__ */ jsx4("span", { children: label }),
411
+ /* @__PURE__ */ jsxs4("span", { className: "opacity-40", children: [
412
+ "\xB7",
413
+ " ",
414
+ content.scenes[i].durationSeconds,
415
+ "s"
416
+ ] })
417
+ ] }, i))
418
+ ] }),
419
+ onRenderRequest && /* @__PURE__ */ jsx4(
420
+ Button,
421
+ {
422
+ size: "sm",
423
+ variant: "outline",
424
+ onClick: onRenderRequest,
425
+ disabled: isRendering,
426
+ className: "w-full gap-2",
427
+ children: isRendering ? /* @__PURE__ */ jsxs4(Fragment, { children: [
428
+ /* @__PURE__ */ jsx4(Loader2, { size: 14, className: "animate-spin" }),
429
+ "Rendering\u2026"
430
+ ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
431
+ /* @__PURE__ */ jsx4(Play, { size: 14 }),
432
+ "Render in sandbox"
433
+ ] })
434
+ }
435
+ )
436
+ ] });
437
+ }
438
+
439
+ // src/assets/preview/copy-preview.tsx
440
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
441
+ var PLATFORM_LIMITS = {
442
+ instagram: 2200,
443
+ tiktok: 2200,
444
+ x: 280,
445
+ linkedin: 3e3,
446
+ sms: 160,
447
+ "email-subject": 60
448
+ };
449
+ var PLATFORM_LABELS = {
450
+ instagram: "Instagram",
451
+ tiktok: "TikTok",
452
+ x: "X / Twitter",
453
+ linkedin: "LinkedIn",
454
+ sms: "SMS",
455
+ "email-subject": "Email Subject"
456
+ };
457
+ function CopyPreview({ content, className }) {
458
+ const limit = PLATFORM_LIMITS[content.platform];
459
+ const bodyLen = content.body.length;
460
+ const isOverLimit = limit !== null && bodyLen > limit;
461
+ const warningThreshold = limit !== null ? limit * 0.9 : null;
462
+ const isNearLimit = !isOverLimit && warningThreshold !== null && bodyLen >= warningThreshold;
463
+ return /* @__PURE__ */ jsxs5("div", { className: cn("flex flex-col gap-2", className), children: [
464
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between", children: [
465
+ /* @__PURE__ */ jsx5("span", { className: "text-xs font-medium text-muted-foreground", children: PLATFORM_LABELS[content.platform] }),
466
+ limit !== null && /* @__PURE__ */ jsxs5("span", { className: cn("text-xs tabular-nums", isOverLimit ? "text-destructive font-medium" : isNearLimit ? "text-warning" : "text-muted-foreground"), children: [
467
+ bodyLen,
468
+ " / ",
469
+ limit
470
+ ] })
471
+ ] }),
472
+ /* @__PURE__ */ jsxs5("div", { className: "rounded border border-border p-3 space-y-2 bg-card", children: [
473
+ content.headline && /* @__PURE__ */ jsx5("div", { className: "text-sm font-semibold", children: content.headline }),
474
+ /* @__PURE__ */ jsx5("p", { className: "text-sm whitespace-pre-wrap leading-relaxed", children: content.body }),
475
+ content.hashtags && content.hashtags.length > 0 && /* @__PURE__ */ jsx5("div", { className: "text-sm text-blue-500 flex flex-wrap gap-1", children: content.hashtags.map((tag) => /* @__PURE__ */ jsxs5("span", { children: [
476
+ "#",
477
+ tag
478
+ ] }, tag)) })
479
+ ] })
480
+ ] });
481
+ }
482
+
483
+ // src/assets/asset-editor.tsx
484
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
485
+ function BrandPanel({ brand, onChange }) {
486
+ const [open, setOpen] = React2.useState(false);
487
+ return /* @__PURE__ */ jsxs6("div", { className: "border border-border rounded", children: [
488
+ /* @__PURE__ */ jsxs6(
489
+ "button",
490
+ {
491
+ type: "button",
492
+ className: "flex w-full items-center justify-between px-3 py-2 text-xs font-medium text-muted-foreground hover:text-foreground",
493
+ onClick: () => setOpen((o) => !o),
494
+ children: [
495
+ "Brand tokens",
496
+ open ? /* @__PURE__ */ jsx6(ChevronDown, { size: 14 }) : /* @__PURE__ */ jsx6(ChevronRight2, { size: 14 })
497
+ ]
498
+ }
499
+ ),
500
+ open && /* @__PURE__ */ jsxs6("div", { className: "px-3 pb-3 space-y-2 border-t border-border pt-2", children: [
501
+ ["primaryColor", "accentColor", "textColor"].map((key) => /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
502
+ /* @__PURE__ */ jsx6(
503
+ "input",
504
+ {
505
+ type: "color",
506
+ value: brand[key],
507
+ onChange: (e) => onChange({ ...brand, [key]: e.target.value }),
508
+ className: "w-6 h-6 rounded cursor-pointer border-0"
509
+ }
510
+ ),
511
+ /* @__PURE__ */ jsx6(Label, { className: "text-xs capitalize", children: key.replace(/Color/, " color") })
512
+ ] }, key)),
513
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
514
+ /* @__PURE__ */ jsx6(Label, { className: "text-xs", children: "Font family" }),
515
+ /* @__PURE__ */ jsx6(
516
+ Input,
517
+ {
518
+ value: brand.fontFamily,
519
+ onChange: (e) => onChange({ ...brand, fontFamily: e.target.value }),
520
+ className: "h-7 text-xs"
521
+ }
522
+ )
523
+ ] }),
524
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
525
+ /* @__PURE__ */ jsx6(Label, { className: "text-xs", children: "Logo URL" }),
526
+ /* @__PURE__ */ jsx6(
527
+ Input,
528
+ {
529
+ value: brand.logoUrl ?? "",
530
+ onChange: (e) => onChange({ ...brand, logoUrl: e.target.value || void 0 }),
531
+ className: "h-7 text-xs",
532
+ placeholder: "https://\u2026"
533
+ }
534
+ )
535
+ ] })
536
+ ] })
537
+ ] });
538
+ }
539
+ function EmailEditor({ spec, onChange }) {
540
+ const content = spec.content;
541
+ return /* @__PURE__ */ jsxs6("div", { className: "space-y-3", children: [
542
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
543
+ /* @__PURE__ */ jsx6(Label, { className: "text-xs", children: "Subject" }),
544
+ /* @__PURE__ */ jsx6(
545
+ Input,
546
+ {
547
+ value: content.subject,
548
+ onChange: (e) => onChange({ ...spec, content: { ...content, subject: e.target.value } }),
549
+ className: "h-8 text-sm"
550
+ }
551
+ )
552
+ ] }),
553
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
554
+ /* @__PURE__ */ jsx6(Label, { className: "text-xs", children: "Preheader" }),
555
+ /* @__PURE__ */ jsx6(
556
+ Input,
557
+ {
558
+ value: content.preheader ?? "",
559
+ onChange: (e) => onChange({ ...spec, content: { ...content, preheader: e.target.value || void 0 } }),
560
+ className: "h-8 text-sm",
561
+ placeholder: "Preview text\u2026"
562
+ }
563
+ )
564
+ ] }),
565
+ content.sections.map((section, i) => {
566
+ if (section.type === "hero" || section.type === "body" || section.type === "cta") {
567
+ const textField = section.type === "body" ? "text" : section.type === "cta" ? "label" : "headline";
568
+ const val = section[textField] ?? "";
569
+ return /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
570
+ /* @__PURE__ */ jsxs6(Label, { className: "text-xs capitalize", children: [
571
+ section.type,
572
+ " \u2014 ",
573
+ textField
574
+ ] }),
575
+ /* @__PURE__ */ jsx6(
576
+ Textarea,
577
+ {
578
+ value: val,
579
+ onChange: (e) => {
580
+ const updated = [...content.sections];
581
+ updated[i] = { ...updated[i], [textField]: e.target.value };
582
+ onChange({ ...spec, content: { ...content, sections: updated } });
583
+ },
584
+ className: "text-sm min-h-[60px]"
585
+ }
586
+ )
587
+ ] }, i);
588
+ }
589
+ return null;
590
+ })
591
+ ] });
592
+ }
593
+ function CopyEditor({ spec, onChange }) {
594
+ const content = spec.content;
595
+ return /* @__PURE__ */ jsxs6("div", { className: "space-y-3", children: [
596
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
597
+ /* @__PURE__ */ jsx6(Label, { className: "text-xs", children: "Headline" }),
598
+ /* @__PURE__ */ jsx6(
599
+ Input,
600
+ {
601
+ value: content.headline,
602
+ onChange: (e) => onChange({ ...spec, content: { ...content, headline: e.target.value } }),
603
+ className: "h-8 text-sm"
604
+ }
605
+ )
606
+ ] }),
607
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
608
+ /* @__PURE__ */ jsx6(Label, { className: "text-xs", children: "Body" }),
609
+ /* @__PURE__ */ jsx6(
610
+ Textarea,
611
+ {
612
+ value: content.body,
613
+ onChange: (e) => onChange({ ...spec, content: { ...content, body: e.target.value } }),
614
+ className: "text-sm min-h-[80px]"
615
+ }
616
+ )
617
+ ] }),
618
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
619
+ /* @__PURE__ */ jsx6(Label, { className: "text-xs", children: "Hashtags (comma-separated)" }),
620
+ /* @__PURE__ */ jsx6(
621
+ Input,
622
+ {
623
+ value: (content.hashtags ?? []).join(", "),
624
+ onChange: (e) => onChange({ ...spec, content: { ...content, hashtags: e.target.value.split(",").map((t) => t.trim()).filter(Boolean) } }),
625
+ className: "h-8 text-sm",
626
+ placeholder: "marketing, growth, saas"
627
+ }
628
+ )
629
+ ] })
630
+ ] });
631
+ }
632
+ function Preview({ spec, previewUrl, isRendering, onRenderRequest }) {
633
+ const { format, brand, content } = spec;
634
+ if (format === "email") return /* @__PURE__ */ jsx6(EmailPreview, { content, brand, previewUrl });
635
+ if (format.startsWith("image:")) {
636
+ const imgFormat = format === "image:story" ? "story" : format === "image:carousel" ? "carousel" : "feed";
637
+ return /* @__PURE__ */ jsx6(ImagePreview, { content, brand, format: imgFormat });
638
+ }
639
+ if (format.startsWith("video:")) {
640
+ return /* @__PURE__ */ jsx6(VideoPreview, { content, brand, onRenderRequest, isRendering });
641
+ }
642
+ if (format.startsWith("copy:")) {
643
+ return /* @__PURE__ */ jsx6(CopyPreview, { content });
644
+ }
645
+ return null;
646
+ }
647
+ function AssetEditor({ spec, previewUrl, isRendering, onSave, onRenderRequest, onRevisionRequest, className }) {
648
+ const [draft, setDraft] = React2.useState(spec);
649
+ const [revisionInput, setRevisionInput] = React2.useState("");
650
+ const isDirty = JSON.stringify(draft) !== JSON.stringify(spec);
651
+ React2.useEffect(() => {
652
+ setDraft(spec);
653
+ }, [spec]);
654
+ const handleRevision = () => {
655
+ if (!revisionInput.trim()) return;
656
+ onRevisionRequest?.(revisionInput.trim());
657
+ setRevisionInput("");
658
+ };
659
+ return /* @__PURE__ */ jsxs6("div", { className: cn("grid grid-cols-2 gap-4 h-full min-h-0", className), children: [
660
+ /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-3 overflow-y-auto pr-1", children: [
661
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between", children: [
662
+ /* @__PURE__ */ jsx6("span", { className: "text-xs font-semibold text-muted-foreground uppercase tracking-wider", children: draft.format }),
663
+ isDirty && onSave && /* @__PURE__ */ jsxs6(Button2, { size: "sm", variant: "default", onClick: () => onSave(draft), className: "h-7 gap-1.5 text-xs", children: [
664
+ /* @__PURE__ */ jsx6(Save, { size: 12 }),
665
+ "Save"
666
+ ] })
667
+ ] }),
668
+ draft.format === "email" && /* @__PURE__ */ jsx6(EmailEditor, { spec: draft, onChange: (s) => setDraft(s) }),
669
+ (draft.format === "copy:caption" || draft.format === "copy:headline" || draft.format === "copy:sms") && /* @__PURE__ */ jsx6(CopyEditor, { spec: draft, onChange: (s) => setDraft(s) }),
670
+ /* @__PURE__ */ jsx6(BrandPanel, { brand: draft.brand, onChange: (b) => setDraft((d) => ({ ...d, brand: b })) }),
671
+ onRevisionRequest && /* @__PURE__ */ jsxs6("div", { className: "space-y-1.5", children: [
672
+ /* @__PURE__ */ jsx6(Label, { className: "text-xs", children: "Ask agent to revise" }),
673
+ /* @__PURE__ */ jsxs6("div", { className: "flex gap-1.5", children: [
674
+ /* @__PURE__ */ jsx6(
675
+ Input,
676
+ {
677
+ value: revisionInput,
678
+ onChange: (e) => setRevisionInput(e.target.value),
679
+ placeholder: "Make the CTA more urgent\u2026",
680
+ className: "h-8 text-sm",
681
+ onKeyDown: (e) => {
682
+ if (e.key === "Enter" && !e.shiftKey) {
683
+ e.preventDefault();
684
+ handleRevision();
685
+ }
686
+ }
687
+ }
688
+ ),
689
+ /* @__PURE__ */ jsx6(
690
+ Button2,
691
+ {
692
+ size: "sm",
693
+ variant: "outline",
694
+ onClick: handleRevision,
695
+ disabled: !revisionInput.trim(),
696
+ className: "h-8 px-2",
697
+ children: /* @__PURE__ */ jsx6(MessageSquare, { size: 14 })
698
+ }
699
+ )
700
+ ] })
701
+ ] })
702
+ ] }),
703
+ /* @__PURE__ */ jsx6("div", { className: "overflow-y-auto", children: /* @__PURE__ */ jsx6(
704
+ Preview,
705
+ {
706
+ spec: draft,
707
+ previewUrl,
708
+ isRendering,
709
+ onRenderRequest
710
+ }
711
+ ) })
712
+ ] });
713
+ }
714
+
715
+ // src/assets/approval-queue.tsx
716
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
717
+ var FORMAT_OPTIONS = [
718
+ { value: "all", label: "All formats" },
719
+ { value: "email", label: "Email" },
720
+ { value: "image:feed", label: "Feed image" },
721
+ { value: "image:story", label: "Story" },
722
+ { value: "image:carousel", label: "Carousel" },
723
+ { value: "video:reel", label: "Reel" },
724
+ { value: "video:feed", label: "Video" },
725
+ { value: "copy:caption", label: "Caption" },
726
+ { value: "copy:headline", label: "Headline" },
727
+ { value: "copy:sms", label: "SMS" }
728
+ ];
729
+ var STATUS_OPTIONS = [
730
+ { value: "all", label: "All statuses" },
731
+ { value: "pending_review", label: "Needs review" },
732
+ { value: "approved", label: "Approved" },
733
+ { value: "rejected", label: "Rejected" },
734
+ { value: "scheduled", label: "Scheduled" },
735
+ { value: "published", label: "Published" }
736
+ ];
737
+ function ApprovalQueue({
738
+ assets,
739
+ variantCounts = {},
740
+ onApprove,
741
+ onReject,
742
+ onEdit,
743
+ onSave,
744
+ onRevisionRequest,
745
+ onRenderRequest,
746
+ renderingIds = /* @__PURE__ */ new Set(),
747
+ previewUrls = {},
748
+ className
749
+ }) {
750
+ const [search, setSearch] = React3.useState("");
751
+ const [formatFilter, setFormatFilter] = React3.useState("all");
752
+ const [statusFilter, setStatusFilter] = React3.useState("pending_review");
753
+ const [editingId, setEditingId] = React3.useState(null);
754
+ const [scheduleDate, setScheduleDate] = React3.useState("");
755
+ const filtered = assets.filter((a) => {
756
+ if (formatFilter !== "all" && a.format !== formatFilter) return false;
757
+ if (statusFilter !== "all" && a.status !== statusFilter) return false;
758
+ if (search) {
759
+ const q = search.toLowerCase();
760
+ if (!a.id.includes(q) && !a.format.includes(q) && !a.brand.businessName.toLowerCase().includes(q)) return false;
761
+ }
762
+ return true;
763
+ });
764
+ const pendingCount = assets.filter((a) => a.status === "pending_review").length;
765
+ const handleBulkApprove = () => {
766
+ filtered.filter((a) => a.status === "pending_review").forEach((a) => onApprove?.(a.id, scheduleDate || void 0));
767
+ };
768
+ const editingSpec = editingId ? assets.find((a) => a.id === editingId) : null;
769
+ if (editingSpec) {
770
+ return /* @__PURE__ */ jsxs7("div", { className: cn("flex flex-col gap-3 h-full", className), children: [
771
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [
772
+ /* @__PURE__ */ jsx7(
773
+ "button",
774
+ {
775
+ type: "button",
776
+ onClick: () => setEditingId(null),
777
+ className: "text-xs text-muted-foreground hover:text-foreground",
778
+ children: "\u2190 Back to queue"
779
+ }
780
+ ),
781
+ /* @__PURE__ */ jsxs7("div", { className: "flex gap-2", children: [
782
+ onReject && editingSpec.status === "pending_review" && /* @__PURE__ */ jsx7(Button3, { size: "sm", variant: "outline", onClick: () => {
783
+ onReject(editingSpec.id);
784
+ setEditingId(null);
785
+ }, children: "Reject" }),
786
+ onApprove && editingSpec.status === "pending_review" && /* @__PURE__ */ jsx7(Button3, { size: "sm", onClick: () => {
787
+ onApprove(editingSpec.id, scheduleDate || void 0);
788
+ setEditingId(null);
789
+ }, children: "Approve" })
790
+ ] })
791
+ ] }),
792
+ /* @__PURE__ */ jsx7(
793
+ AssetEditor,
794
+ {
795
+ spec: editingSpec,
796
+ previewUrl: previewUrls[editingSpec.id],
797
+ isRendering: renderingIds.has(editingSpec.id),
798
+ onSave: (s) => {
799
+ onSave?.(s);
800
+ setEditingId(null);
801
+ },
802
+ onRenderRequest: () => onRenderRequest?.(editingSpec.id),
803
+ onRevisionRequest: (instr) => onRevisionRequest?.(editingSpec.id, instr),
804
+ className: "flex-1"
805
+ }
806
+ )
807
+ ] });
808
+ }
809
+ return /* @__PURE__ */ jsxs7("div", { className: cn("flex flex-col gap-3", className), children: [
810
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-wrap gap-2 items-center", children: [
811
+ /* @__PURE__ */ jsxs7("div", { className: "relative flex-1 min-w-40", children: [
812
+ /* @__PURE__ */ jsx7(Search, { size: 12, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" }),
813
+ /* @__PURE__ */ jsx7(
814
+ Input2,
815
+ {
816
+ value: search,
817
+ onChange: (e) => setSearch(e.target.value),
818
+ placeholder: "Filter assets\u2026",
819
+ className: "h-8 pl-7 text-sm"
820
+ }
821
+ )
822
+ ] }),
823
+ /* @__PURE__ */ jsx7(
824
+ "select",
825
+ {
826
+ value: formatFilter,
827
+ onChange: (e) => setFormatFilter(e.target.value),
828
+ className: "h-8 px-2 text-xs rounded border border-input bg-background",
829
+ children: FORMAT_OPTIONS.map((o) => /* @__PURE__ */ jsx7("option", { value: o.value, children: o.label }, o.value))
830
+ }
831
+ ),
832
+ /* @__PURE__ */ jsx7(
833
+ "select",
834
+ {
835
+ value: statusFilter,
836
+ onChange: (e) => setStatusFilter(e.target.value),
837
+ className: "h-8 px-2 text-xs rounded border border-input bg-background",
838
+ children: STATUS_OPTIONS.map((o) => /* @__PURE__ */ jsx7("option", { value: o.value, children: o.label }, o.value))
839
+ }
840
+ ),
841
+ pendingCount > 0 && onApprove && /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1.5", children: [
842
+ /* @__PURE__ */ jsx7(
843
+ "input",
844
+ {
845
+ type: "date",
846
+ value: scheduleDate,
847
+ onChange: (e) => setScheduleDate(e.target.value),
848
+ className: "h-8 px-2 text-xs rounded border border-input bg-background w-32"
849
+ }
850
+ ),
851
+ /* @__PURE__ */ jsxs7(Button3, { size: "sm", variant: "outline", onClick: handleBulkApprove, className: "h-8 gap-1.5 text-xs", children: [
852
+ /* @__PURE__ */ jsx7(CheckCheck, { size: 13 }),
853
+ "Approve all (",
854
+ pendingCount,
855
+ ")"
856
+ ] })
857
+ ] })
858
+ ] }),
859
+ filtered.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "py-12 text-center text-sm text-muted-foreground", children: "No assets match your filters." }) : /* @__PURE__ */ jsx7("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3", children: filtered.map((a) => /* @__PURE__ */ jsx7(
860
+ AssetCard,
861
+ {
862
+ spec: a,
863
+ variantCount: variantCounts[a.id],
864
+ onApprove,
865
+ onReject,
866
+ onEdit: () => setEditingId(a.id)
867
+ },
868
+ a.id
869
+ )) })
870
+ ] });
871
+ }
872
+
873
+ // src/assets/variant-compare.tsx
874
+ import { Button as Button4, Badge as Badge2 } from "@tangle-network/ui/primitives";
875
+ import { Trophy } from "lucide-react";
876
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
877
+ function VariantCompare({ variants, onPromote, onSave, className }) {
878
+ const cols = Math.min(variants.length, 3);
879
+ if (variants.length === 0) {
880
+ return /* @__PURE__ */ jsx8("div", { className: cn("py-12 text-center text-sm text-muted-foreground", className), children: "No variants to compare." });
881
+ }
882
+ return /* @__PURE__ */ jsxs8("div", { className: cn("flex flex-col gap-3", className), children: [
883
+ /* @__PURE__ */ jsxs8("div", { className: "text-xs text-muted-foreground", children: [
884
+ "Comparing ",
885
+ variants.length,
886
+ " variant",
887
+ variants.length !== 1 ? "s" : "",
888
+ ".",
889
+ onPromote && " Promote one to mark it approved and archive the rest."
890
+ ] }),
891
+ /* @__PURE__ */ jsx8(
892
+ "div",
893
+ {
894
+ className: "grid gap-3",
895
+ style: { gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` },
896
+ children: variants.slice(0, 3).map((variant) => /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-2 rounded-lg border border-border p-3", children: [
897
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between", children: [
898
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-1.5", children: [
899
+ /* @__PURE__ */ jsx8("span", { className: "text-xs font-medium", children: variant.label }),
900
+ variant.approvedAt && /* @__PURE__ */ jsx8(Badge2, { variant: "default", className: "text-[10px] h-4 px-1.5", children: "Approved" }),
901
+ variant.rejectedAt && /* @__PURE__ */ jsx8(Badge2, { variant: "destructive", className: "text-[10px] h-4 px-1.5", children: "Rejected" })
902
+ ] }),
903
+ onPromote && !variant.approvedAt && /* @__PURE__ */ jsxs8(
904
+ Button4,
905
+ {
906
+ size: "sm",
907
+ variant: "outline",
908
+ onClick: () => onPromote(variant.id),
909
+ className: "h-6 gap-1 text-[11px] px-2",
910
+ children: [
911
+ /* @__PURE__ */ jsx8(Trophy, { size: 11 }),
912
+ "Pick this"
913
+ ]
914
+ }
915
+ )
916
+ ] }),
917
+ /* @__PURE__ */ jsx8(
918
+ AssetEditor,
919
+ {
920
+ spec: variant.spec,
921
+ onSave: (spec) => onSave?.({ ...variant, spec }),
922
+ className: "min-h-[300px]"
923
+ }
924
+ ),
925
+ variant.editLog.length > 0 && /* @__PURE__ */ jsxs8("div", { className: "text-[10px] text-muted-foreground space-y-0.5", children: [
926
+ /* @__PURE__ */ jsx8("span", { className: "font-medium", children: "Edit history" }),
927
+ variant.editLog.slice(-3).map((ev, i) => /* @__PURE__ */ jsxs8("div", { children: [
928
+ ev.action,
929
+ ev.editedFields?.length ? ` (${ev.editedFields.join(", ")})` : ""
930
+ ] }, i))
931
+ ] })
932
+ ] }, variant.id))
933
+ }
934
+ )
935
+ ] });
936
+ }
937
+ export {
938
+ ApprovalQueue,
939
+ AssetCard,
940
+ AssetEditor,
941
+ CopyPreview,
942
+ EmailPreview,
943
+ ImagePreview,
944
+ VariantCompare,
945
+ VideoPreview
946
+ };