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