@elevasis/ui 2.71.0 → 2.73.0

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 (71) hide show
  1. package/dist/api/index.js +3 -3
  2. package/dist/app/index.d.ts +116 -22
  3. package/dist/app/index.js +8 -8
  4. package/dist/auth/index.js +4 -4
  5. package/dist/charts/index.js +4 -4
  6. package/dist/{chunk-DCMFNSJ6.js → chunk-3TS5U5BL.js} +1 -1
  7. package/dist/{chunk-VRRIHA3S.js → chunk-4CW2C5BD.js} +1 -1
  8. package/dist/{chunk-26BTE3Z4.js → chunk-5B2ZFO44.js} +1 -1
  9. package/dist/{chunk-BM2WKRAQ.js → chunk-AJOVWB47.js} +3 -3
  10. package/dist/{chunk-DH2MO5DX.js → chunk-CJ2232PQ.js} +1 -1
  11. package/dist/{chunk-TVFSVTRG.js → chunk-ETN3TTRT.js} +7 -7
  12. package/dist/{chunk-NM4A63KJ.js → chunk-IBCEAVB2.js} +56 -20
  13. package/dist/{chunk-3BLYJT5L.js → chunk-IGBPWD6S.js} +3 -3
  14. package/dist/{chunk-TB2PM2OU.js → chunk-JYBJMVNY.js} +1 -1
  15. package/dist/{chunk-TCQZ4AJI.js → chunk-LEPYXLJ2.js} +1 -1
  16. package/dist/{chunk-H3SDGC5F.js → chunk-LZT2H2MA.js} +4 -4
  17. package/dist/{chunk-VDX6DL7Z.js → chunk-NHNTX6ZL.js} +1 -1
  18. package/dist/{chunk-VHPHWTX7.js → chunk-NI37YNRP.js} +4 -4
  19. package/dist/{chunk-UZ7MSORW.js → chunk-OCVSPAKM.js} +540 -29
  20. package/dist/{chunk-LL5H7J4N.js → chunk-PQ4B62HH.js} +2 -2
  21. package/dist/{chunk-CPBT6Z2G.js → chunk-SWKAH2YM.js} +52 -20
  22. package/dist/{chunk-C75XWBYZ.js → chunk-U2H3TIHD.js} +1 -1
  23. package/dist/{chunk-AIOV5NM7.js → chunk-Z4ICFQXG.js} +1 -1
  24. package/dist/{chunk-JTSNU6KA.js → chunk-ZDKPHFTH.js} +3 -3
  25. package/dist/components/index.d.ts +307 -24
  26. package/dist/components/index.js +316 -26
  27. package/dist/components/navigation/index.js +5 -5
  28. package/dist/features/auth/index.d.ts +76 -0
  29. package/dist/features/auth/index.js +6 -6
  30. package/dist/features/clients/index.js +5 -5
  31. package/dist/features/content/index.d.ts +586 -66
  32. package/dist/features/content/index.js +1213 -312
  33. package/dist/features/crm/index.d.ts +117 -22
  34. package/dist/features/crm/index.js +6 -6
  35. package/dist/features/dashboard/index.d.ts +20 -11
  36. package/dist/features/dashboard/index.js +6 -6
  37. package/dist/features/delivery/index.d.ts +76 -0
  38. package/dist/features/delivery/index.js +5 -5
  39. package/dist/features/knowledge/index.js +4 -4
  40. package/dist/features/lead-gen/index.d.ts +41 -22
  41. package/dist/features/lead-gen/index.js +6 -6
  42. package/dist/features/monitoring/index.js +7 -7
  43. package/dist/features/monitoring/requests/index.js +5 -5
  44. package/dist/features/operations/index.d.ts +40 -22
  45. package/dist/features/operations/index.js +9 -9
  46. package/dist/features/settings/index.d.ts +76 -0
  47. package/dist/features/settings/index.js +6 -6
  48. package/dist/hooks/access/index.js +4 -4
  49. package/dist/hooks/delivery/index.d.ts +76 -0
  50. package/dist/hooks/delivery/index.js +2 -2
  51. package/dist/hooks/index.d.ts +1259 -32
  52. package/dist/hooks/index.js +4 -4
  53. package/dist/hooks/operations/command-view/utils/transformCommandViewData.d.ts +20 -11
  54. package/dist/hooks/published.d.ts +1259 -32
  55. package/dist/hooks/published.js +4 -4
  56. package/dist/initialization/index.d.ts +76 -0
  57. package/dist/knowledge/index.d.ts +40 -22
  58. package/dist/knowledge/index.js +18 -18
  59. package/dist/{knowledge-search-index-LP2WVSQX.js → knowledge-search-index-RZLIVWPJ.js} +5 -5
  60. package/dist/layout/index.js +7 -7
  61. package/dist/profile/index.d.ts +76 -0
  62. package/dist/provider/index.d.ts +116 -22
  63. package/dist/provider/index.js +4 -4
  64. package/dist/provider/published.d.ts +116 -22
  65. package/dist/provider/published.js +4 -4
  66. package/dist/supabase/index.d.ts +149 -0
  67. package/dist/test-utils/index.js +3 -3
  68. package/dist/types/index.d.ts +126 -14
  69. package/dist/utils/index.d.ts +20 -11
  70. package/dist/utils/index.js +1 -1
  71. package/package.json +5 -5
@@ -1,14 +1,14 @@
1
1
  import { PageContainer } from '../../chunk-BZZCNLT6.js';
2
2
  import '../../chunk-TUMSNGTX.js';
3
3
  import { FilterBar } from '../../chunk-PDHTXPSF.js';
4
- import { CustomModal, useContentItems, useContentDistributions, useContentConfig, SubshellContentContainer, PageTitleCaption, CenteredErrorState, EmptyState, StatCard, getContentItemIdentity, usePaginationState, useContentItem, useContentItemAttempts, useUpdateContentItem, useReviewContentItem, getOpenContentReviewGates, useContentDistribution, useUpdateContentDistribution, useContentPipelineSummary, deriveContentBoard, useExecutions } from '../../chunk-UZ7MSORW.js';
5
- export { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle, DEFAULT_CONTENT_VOCABULARY, MyReviewQueuePanel, contentManifest, deriveContentBoard, getContentItemIdentity, resolveContentStepResource, resolveContentVocabulary, useContentConfig } from '../../chunk-UZ7MSORW.js';
4
+ import { CustomModal, useContentItemSourceAssetDetails, useContentSourceAssets, useAddContentItemSourceAsset, useRemoveContentItemSourceAsset, useReorderContentItemSourceAssets, useUpdateContentItemSourceAsset, useCloudStorageTemporaryLinks, useContentItemAttempts, useExecuteAsync, useUpdateContentItem, useExecution, contentKeys, useContentItems, useContentDistributions, useContentConfig, isAwaitingReview, SubshellContentContainer, PageTitleCaption, CenteredErrorState, EmptyState, StatCard, getContentItemIdentity, usePaginationState, resolveCurrentContentStep, useContentItem, useReviewContentItem, resolveReviewTarget, useContentDistribution, useStorageSignedUrls, storageObjectRefKey, useContentPipelineSummary, deriveContentBoard, resolveContentWorkspaceRoute, useExecutions } from '../../chunk-OCVSPAKM.js';
5
+ export { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle, DEFAULT_CONTENT_VOCABULARY, MyReviewQueuePanel, contentManifest, deriveContentBoard, getContentItemIdentity, resolveContentStepResource, resolveContentVocabulary, useContentConfig } from '../../chunk-OCVSPAKM.js';
6
6
  import '../../chunk-R6NMRXNO.js';
7
7
  import '../../chunk-RBGVNPA6.js';
8
8
  import '../../chunk-MKH2KOAO.js';
9
- import '../../chunk-VDX6DL7Z.js';
9
+ import '../../chunk-NHNTX6ZL.js';
10
10
  import '../../chunk-L7BZZ4SI.js';
11
- import { showSuccessNotification } from '../../chunk-C75XWBYZ.js';
11
+ import { showSuccessNotification } from '../../chunk-U2H3TIHD.js';
12
12
  import '../../chunk-MISHPEJX.js';
13
13
  import { CardHeader } from '../../chunk-62GVNH5U.js';
14
14
  import '../../chunk-P45GQ3ZW.js';
@@ -20,7 +20,7 @@ import '../../chunk-UD657ICS.js';
20
20
  import '../../chunk-DD3CCMCZ.js';
21
21
  import '../../chunk-RNP5R5I3.js';
22
22
  import '../../chunk-RT4KRGZT.js';
23
- import { formatTimeAgo, formatRelativeTime } from '../../chunk-NM4A63KJ.js';
23
+ import { formatTimeAgo, formatRelativeTime } from '../../chunk-IBCEAVB2.js';
24
24
  import '../../chunk-Y6I3IC45.js';
25
25
  import '../../chunk-6SSQGWK7.js';
26
26
  import '../../chunk-BI5VDO2K.js';
@@ -29,12 +29,13 @@ import '../../chunk-TVTSASST.js';
29
29
  import '../../chunk-BRJ3QZ4E.js';
30
30
  import '../../chunk-Q7DJKLEN.js';
31
31
  import '../../chunk-I2KLQ2HA.js';
32
- import { Stack, Group, Text, Badge, Button, Textarea, Box, Loader, Accordion, Tooltip, Card, Alert, List, Paper, Center, SimpleGrid, Table, TextInput, Select, Pagination, Title, Tabs, Anchor, Divider, Progress, Checkbox, TagsInput, Image, ActionIcon, UnstyledButton, ScrollArea } from '@mantine/core';
33
- import { IconAlertTriangle, IconShieldCheck, IconShieldX, IconArticle, IconArrowRight, IconClock, IconSend, IconSearch, IconFileText, IconListDetails, IconAlertCircle, IconLayoutGrid, IconShare, IconPlayerPlay, IconExternalLink, IconMaximize, IconCopy, IconDownload, IconTrash, IconArrowLeft, IconArrowsExchange, IconCheck, IconX, IconChecklist, IconRoute, IconInfoCircle, IconClockHour4 } from '@tabler/icons-react';
32
+ import { Stack, Group, Text, Badge, Button, Textarea, Box, Loader, Accordion, Tooltip, Card, Alert, List, Paper, SegmentedControl, Slider, Title, Table, TextInput, Select, Modal, ActionIcon, Center, SimpleGrid, Pagination, Tabs, Anchor, Divider, Progress, Image as Image$1, UnstyledButton, ScrollArea } from '@mantine/core';
33
+ import { IconAlertTriangle, IconShieldCheck, IconShieldX, IconPhoto, IconGripVertical, IconCrop, IconArrowUp, IconArrowDown, IconUnlink, IconLink, IconPhotoOff, IconChevronLeft, IconChevronRight, IconCheck, IconSparkles, IconEye, IconArticle, IconArrowRight, IconClock, IconSend, IconSearch, IconFileText, IconExternalLink, IconListDetails, IconAlertCircle, IconLayoutGrid, IconShare, IconPlayerPlay, IconCircleCheck, IconCircleDashed, IconMaximize, IconCopy, IconDownload, IconArrowLeft, IconArrowsExchange, IconX, IconChecklist, IconRoute, IconInfoCircle, IconClockHour4 } from '@tabler/icons-react';
34
34
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
35
- import { useState, useMemo, Fragment as Fragment$1, useCallback, useEffect } from 'react';
35
+ import { useState, useRef, useCallback, useMemo, useEffect, Fragment as Fragment$1 } from 'react';
36
+ import { useQueryClient } from '@tanstack/react-query';
37
+ import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
36
38
  import { useNavigate, useSearch } from '@tanstack/react-router';
37
- import { useDisclosure } from '@mantine/hooks';
38
39
 
39
40
  function renderFieldValue(value, kind) {
40
41
  if (value === void 0 || value === null || value === "") {
@@ -425,7 +426,8 @@ function ContentReviewCard({
425
426
  processingSteps = [],
426
427
  actions,
427
428
  onAction,
428
- actionsPending = false
429
+ actionsPending = false,
430
+ readOnly = false
429
431
  }) {
430
432
  return /* @__PURE__ */ jsx(Card, { withBorder: true, radius: "md", padding: "xl", "data-testid": "content-review-card", children: /* @__PURE__ */ jsxs(Stack, { gap: "lg", children: [
431
433
  title || processingSteps.length > 0 ? /* @__PURE__ */ jsxs(Group, { justify: "space-between", align: "flex-start", wrap: "nowrap", children: [
@@ -466,10 +468,10 @@ function ContentReviewCard({
466
468
  onReset,
467
469
  payload,
468
470
  payloadFields,
469
- disabled: actionsPending
471
+ disabled: actionsPending || readOnly
470
472
  }
471
473
  ),
472
- /* @__PURE__ */ jsx(
474
+ readOnly ? null : /* @__PURE__ */ jsx(
473
475
  AlternatesPanel,
474
476
  {
475
477
  body,
@@ -535,6 +537,1007 @@ function ContentReviewCard({
535
537
  /* @__PURE__ */ jsx(ReviewActionBar, { actions, onAction, pending: actionsPending })
536
538
  ] }) });
537
539
  }
540
+ function moveItem(items, fromIndex, toIndex) {
541
+ const next = items.slice();
542
+ const [moved] = next.splice(fromIndex, 1);
543
+ next.splice(toIndex, 0, moved);
544
+ return next;
545
+ }
546
+ function SelectedAssetStrip({ assets, onReorder }) {
547
+ const [dragOverIndex, setDragOverIndex] = useState(null);
548
+ const draggingIndexRef = useRef(null);
549
+ const handleDragStart = useCallback((e, index) => {
550
+ draggingIndexRef.current = index;
551
+ e.dataTransfer.effectAllowed = "move";
552
+ e.dataTransfer.setData("text/plain", String(index));
553
+ }, []);
554
+ const handleDragOver = useCallback((e, index) => {
555
+ e.preventDefault();
556
+ e.dataTransfer.dropEffect = "move";
557
+ setDragOverIndex(index);
558
+ }, []);
559
+ const handleDragLeave = useCallback(() => {
560
+ setDragOverIndex(null);
561
+ }, []);
562
+ const handleDrop = useCallback(
563
+ (e, targetIndex) => {
564
+ e.preventDefault();
565
+ setDragOverIndex(null);
566
+ const sourceIndex = draggingIndexRef.current;
567
+ draggingIndexRef.current = null;
568
+ if (sourceIndex === null || sourceIndex === targetIndex) return;
569
+ onReorder(moveItem(assets, sourceIndex, targetIndex));
570
+ },
571
+ [assets, onReorder]
572
+ );
573
+ const handleDragEnd = useCallback(() => {
574
+ draggingIndexRef.current = null;
575
+ setDragOverIndex(null);
576
+ }, []);
577
+ if (assets.length === 0) {
578
+ return /* @__PURE__ */ jsx(
579
+ Paper,
580
+ {
581
+ withBorder: true,
582
+ p: "md",
583
+ "data-testid": "selected-asset-strip-empty",
584
+ style: { borderStyle: "dashed", borderColor: "var(--color-border)" },
585
+ children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", ta: "center", children: "No assets selected yet." })
586
+ }
587
+ );
588
+ }
589
+ return /* @__PURE__ */ jsx(Group, { gap: "sm", wrap: "wrap", "data-testid": "selected-asset-strip", role: "list", children: assets.map((asset, index) => {
590
+ const isCover = index === 0;
591
+ const isDragTarget = dragOverIndex === index;
592
+ return /* @__PURE__ */ jsx(
593
+ Paper,
594
+ {
595
+ withBorder: true,
596
+ role: "listitem",
597
+ "data-testid": `selected-asset-strip-item-${asset.id}`,
598
+ draggable: assets.length > 1,
599
+ onDragStart: (e) => handleDragStart(e, index),
600
+ onDragOver: (e) => handleDragOver(e, index),
601
+ onDragLeave: handleDragLeave,
602
+ onDrop: (e) => handleDrop(e, index),
603
+ onDragEnd: handleDragEnd,
604
+ style: {
605
+ width: 120,
606
+ overflow: "hidden",
607
+ cursor: assets.length > 1 ? "grab" : "default",
608
+ userSelect: "none",
609
+ borderColor: isDragTarget ? "var(--color-primary)" : "var(--color-border)",
610
+ transition: `border-color var(--duration-fast) var(--easing)`
611
+ },
612
+ children: /* @__PURE__ */ jsxs(Stack, { gap: 4, p: 6, children: [
613
+ /* @__PURE__ */ jsxs(
614
+ Box,
615
+ {
616
+ style: {
617
+ position: "relative",
618
+ width: "100%",
619
+ aspectRatio: "1 / 1",
620
+ borderRadius: 4,
621
+ overflow: "hidden",
622
+ backgroundColor: "var(--color-surface)"
623
+ },
624
+ children: [
625
+ asset.thumbnailUrl ? /* @__PURE__ */ jsx(
626
+ "img",
627
+ {
628
+ src: asset.thumbnailUrl,
629
+ alt: asset.name,
630
+ style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
631
+ }
632
+ ) : /* @__PURE__ */ jsx(Group, { justify: "center", align: "center", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx(IconPhoto, { size: 24, color: "var(--color-textDimmed)" }) }),
633
+ isCover && /* @__PURE__ */ jsx(
634
+ Badge,
635
+ {
636
+ size: "xs",
637
+ variant: "filled",
638
+ "data-testid": `selected-asset-strip-cover-${asset.id}`,
639
+ style: { position: "absolute", top: 4, left: 4 },
640
+ children: "Cover"
641
+ }
642
+ ),
643
+ assets.length > 1 && /* @__PURE__ */ jsx(Box, { style: { position: "absolute", top: 4, right: 4, color: "var(--color-textDimmed)" }, children: /* @__PURE__ */ jsx(IconGripVertical, { size: 16 }) })
644
+ ]
645
+ }
646
+ ),
647
+ /* @__PURE__ */ jsx(Text, { size: "xs", lineClamp: 1, title: asset.name, children: asset.name })
648
+ ] })
649
+ },
650
+ asset.id
651
+ );
652
+ }) });
653
+ }
654
+ var CROP_ASPECT_OPTIONS = [
655
+ { value: "original", label: "Original", ratio: null },
656
+ { value: "portrait", label: "4:5", ratio: 4 / 5 },
657
+ { value: "square", label: "1:1", ratio: 1 },
658
+ { value: "landscape", label: "1.91:1", ratio: 1.91 }
659
+ ];
660
+ var MIN_CROP_SCALE = 0.2;
661
+ function fitBox(pixelRatio, naturalWidth, naturalHeight) {
662
+ if (pixelRatio === null || naturalWidth === 0 || naturalHeight === 0) return { width: 1, height: 1 };
663
+ const normalizedRatio = pixelRatio * naturalHeight / naturalWidth;
664
+ return normalizedRatio >= 1 ? { width: 1, height: 1 / normalizedRatio } : { width: normalizedRatio, height: 1 };
665
+ }
666
+ function clamp(value, min, max) {
667
+ return Math.min(Math.max(value, min), max);
668
+ }
669
+ function round(value) {
670
+ return Math.round(value * 1e4) / 1e4;
671
+ }
672
+ function AssetCropEditor({
673
+ imageUrl,
674
+ value,
675
+ onChange,
676
+ defaultAspect = "original",
677
+ aspect,
678
+ disabled = false,
679
+ label
680
+ }) {
681
+ const [internalAspect, setInternalAspect] = useState(defaultAspect);
682
+ const activeAspect = aspect ?? internalAspect;
683
+ const [natural, setNatural] = useState(null);
684
+ const frameRef = useRef(null);
685
+ const dragOffsetRef = useRef(null);
686
+ const ratio = useMemo(
687
+ () => CROP_ASPECT_OPTIONS.find((option) => option.value === activeAspect)?.ratio ?? null,
688
+ [activeAspect]
689
+ );
690
+ const maxBox = useMemo(
691
+ () => fitBox(ratio, natural?.width ?? 0, natural?.height ?? 0),
692
+ [ratio, natural?.width, natural?.height]
693
+ );
694
+ const crop = value ?? { x: 0, y: 0, width: 1, height: 1 };
695
+ const scale = maxBox.width === 0 ? 1 : clamp(crop.width / maxBox.width, MIN_CROP_SCALE, 1);
696
+ const emit = useCallback(
697
+ (next) => {
698
+ const width = clamp(next.width, 0.01, 1);
699
+ const height = clamp(next.height, 0.01, 1);
700
+ onChange({
701
+ x: round(clamp(next.x, 0, 1 - width)),
702
+ y: round(clamp(next.y, 0, 1 - height)),
703
+ width: round(width),
704
+ height: round(height)
705
+ });
706
+ },
707
+ [onChange]
708
+ );
709
+ const refit = useCallback(
710
+ (nextScale) => {
711
+ const width = maxBox.width * nextScale;
712
+ const height = maxBox.height * nextScale;
713
+ emit({ x: (1 - width) / 2, y: (1 - height) / 2, width, height });
714
+ },
715
+ [emit, maxBox.height, maxBox.width]
716
+ );
717
+ const fittedRatioRef = useRef(void 0);
718
+ useEffect(() => {
719
+ if (natural === null || disabled) return;
720
+ const isFirstMeasurement = fittedRatioRef.current === void 0;
721
+ fittedRatioRef.current = ratio;
722
+ if (isFirstMeasurement && value !== null) return;
723
+ if (ratio === null && value === null) return;
724
+ refit(scale);
725
+ }, [ratio, natural?.width, natural?.height]);
726
+ const handlePointerDown = (event) => {
727
+ if (disabled || frameRef.current === null) return;
728
+ const frame = frameRef.current.getBoundingClientRect();
729
+ dragOffsetRef.current = {
730
+ x: (event.clientX - frame.left) / frame.width - crop.x,
731
+ y: (event.clientY - frame.top) / frame.height - crop.y
732
+ };
733
+ event.currentTarget.setPointerCapture(event.pointerId);
734
+ };
735
+ const handlePointerMove = (event) => {
736
+ const offset = dragOffsetRef.current;
737
+ if (offset === null || frameRef.current === null) return;
738
+ const frame = frameRef.current.getBoundingClientRect();
739
+ emit({
740
+ x: (event.clientX - frame.left) / frame.width - offset.x,
741
+ y: (event.clientY - frame.top) / frame.height - offset.y,
742
+ width: crop.width,
743
+ height: crop.height
744
+ });
745
+ };
746
+ const endDrag = (event) => {
747
+ dragOffsetRef.current = null;
748
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
749
+ event.currentTarget.releasePointerCapture(event.pointerId);
750
+ }
751
+ };
752
+ const handleKeyDown = (event) => {
753
+ if (disabled) return;
754
+ const step = 0.01;
755
+ const nudge = {
756
+ ArrowLeft: [-step, 0],
757
+ ArrowRight: [step, 0],
758
+ ArrowUp: [0, -step],
759
+ ArrowDown: [0, step]
760
+ };
761
+ const delta = nudge[event.key];
762
+ if (delta === void 0) return;
763
+ event.preventDefault();
764
+ emit({ x: crop.x + delta[0], y: crop.y + delta[1], width: crop.width, height: crop.height });
765
+ };
766
+ const croppedPixels = natural === null ? null : `${Math.round(crop.width * natural.width)}x${Math.round(crop.height * natural.height)}`;
767
+ return /* @__PURE__ */ jsxs(Stack, { gap: "xs", "data-testid": "asset-crop-editor", children: [
768
+ /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", children: [
769
+ label !== void 0 && /* @__PURE__ */ jsx(Text, { fw: 500, children: label }),
770
+ croppedPixels !== null && /* @__PURE__ */ jsx(Badge, { variant: "light", size: "sm", children: croppedPixels })
771
+ ] }),
772
+ /* @__PURE__ */ jsx(Paper, { withBorder: true, p: 0, style: { overflow: "hidden", position: "relative" }, children: /* @__PURE__ */ jsxs("div", { ref: frameRef, style: { position: "relative", lineHeight: 0 }, children: [
773
+ imageUrl === null ? /* @__PURE__ */ jsx(Group, { justify: "center", p: "xl", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : /* @__PURE__ */ jsx(
774
+ "img",
775
+ {
776
+ src: imageUrl,
777
+ alt: "",
778
+ style: { width: "100%", display: "block" },
779
+ onLoad: (event) => setNatural({
780
+ width: event.currentTarget.naturalWidth,
781
+ height: event.currentTarget.naturalHeight
782
+ })
783
+ }
784
+ ),
785
+ imageUrl !== null && /* @__PURE__ */ jsx(
786
+ "div",
787
+ {
788
+ role: "slider",
789
+ tabIndex: disabled ? -1 : 0,
790
+ "aria-label": `Crop area${label === void 0 ? "" : ` for ${label}`}`,
791
+ "aria-valuetext": `x ${Math.round(crop.x * 100)}%, y ${Math.round(crop.y * 100)}%, ${Math.round(
792
+ crop.width * 100
793
+ )}% wide`,
794
+ "data-testid": "asset-crop-box",
795
+ onPointerDown: handlePointerDown,
796
+ onPointerMove: handlePointerMove,
797
+ onPointerUp: endDrag,
798
+ onPointerCancel: endDrag,
799
+ onKeyDown: handleKeyDown,
800
+ style: {
801
+ position: "absolute",
802
+ left: `${crop.x * 100}%`,
803
+ top: `${crop.y * 100}%`,
804
+ width: `${crop.width * 100}%`,
805
+ height: `${crop.height * 100}%`,
806
+ border: "2px solid var(--mantine-color-blue-5)",
807
+ boxShadow: "0 0 0 9999px rgba(0, 0, 0, 0.45)",
808
+ cursor: disabled ? "default" : "move",
809
+ touchAction: "none"
810
+ }
811
+ }
812
+ )
813
+ ] }) }),
814
+ /* @__PURE__ */ jsxs(Group, { gap: "sm", align: "center", wrap: "wrap", children: [
815
+ aspect === void 0 && /* @__PURE__ */ jsx(
816
+ SegmentedControl,
817
+ {
818
+ size: "xs",
819
+ disabled,
820
+ value: activeAspect,
821
+ onChange: setInternalAspect,
822
+ data: CROP_ASPECT_OPTIONS.map((option) => ({ value: option.value, label: option.label }))
823
+ }
824
+ ),
825
+ /* @__PURE__ */ jsx(
826
+ Slider,
827
+ {
828
+ "aria-label": "Crop size",
829
+ style: { flex: 1, minWidth: 140 },
830
+ disabled: disabled || imageUrl === null,
831
+ min: MIN_CROP_SCALE,
832
+ max: 1,
833
+ step: 0.01,
834
+ value: scale,
835
+ label: (current) => `${Math.round(current * 100)}%`,
836
+ onChange: refit
837
+ }
838
+ )
839
+ ] })
840
+ ] });
841
+ }
842
+ var ASSET_PICKER_FETCH_LIMIT = 100;
843
+ function describeAsset(membership, asset) {
844
+ return asset?.title ?? membership.sourceAssetId;
845
+ }
846
+ function readStorageReference(asset) {
847
+ const metadata = asset?.metadata;
848
+ if (typeof metadata !== "object" || metadata === null) return null;
849
+ const { credentialName, cloudStorageId } = metadata;
850
+ if (typeof credentialName !== "string" || typeof cloudStorageId !== "string") return null;
851
+ return { credentialName, cloudStorageId };
852
+ }
853
+ function SourceAssetsPanel({ itemId }) {
854
+ const { memberships, isMembershipLoaded } = useContentItemSourceAssetDetails(itemId);
855
+ const assetsQuery = useContentSourceAssets({ limit: ASSET_PICKER_FETCH_LIMIT });
856
+ const addMembership = useAddContentItemSourceAsset();
857
+ const removeMembership = useRemoveContentItemSourceAsset();
858
+ const reorderMemberships = useReorderContentItemSourceAssets();
859
+ const updateMembership = useUpdateContentItemSourceAsset();
860
+ const [assetToLink, setAssetToLink] = useState(null);
861
+ const [altDrafts, setAltDrafts] = useState({});
862
+ const [croppingAssetId, setCroppingAssetId] = useState(null);
863
+ const [cropDraft, setCropDraft] = useState(null);
864
+ const croppingDetail = memberships.find((detail) => detail.membership.sourceAssetId === croppingAssetId);
865
+ const croppingReference = readStorageReference(croppingDetail?.asset);
866
+ const { links } = useCloudStorageTemporaryLinks(
867
+ croppingReference?.credentialName ?? "",
868
+ croppingReference === null ? [] : [croppingReference.cloudStorageId]
869
+ );
870
+ const linkedIds = useMemo(() => new Set(memberships.map((detail) => detail.membership.sourceAssetId)), [memberships]);
871
+ const linkableOptions = useMemo(
872
+ () => (assetsQuery.data?.data ?? []).filter((asset) => !linkedIds.has(asset.id)).map((asset) => ({ value: asset.id, label: `${asset.title} (${asset.kind})` })),
873
+ [assetsQuery.data, linkedIds]
874
+ );
875
+ const orderedIds = memberships.map((detail) => detail.membership.sourceAssetId);
876
+ const handleLink = () => {
877
+ if (!assetToLink) return;
878
+ const highestPosition = memberships.reduce((highest, detail) => Math.max(highest, detail.membership.position), -1);
879
+ addMembership.mutate(
880
+ { itemId, membership: { sourceAssetId: assetToLink, position: highestPosition + 1 } },
881
+ { onSuccess: () => setAssetToLink(null) }
882
+ );
883
+ };
884
+ const stripAssets = useMemo(
885
+ () => memberships.map((detail) => ({
886
+ id: detail.membership.sourceAssetId,
887
+ name: describeAsset(detail.membership, detail.asset),
888
+ kind: "file"
889
+ })),
890
+ [memberships]
891
+ );
892
+ const handleMove = (fromIndex, toIndex) => {
893
+ if (toIndex < 0 || toIndex >= orderedIds.length) return;
894
+ const next = orderedIds.slice();
895
+ const [moved] = next.splice(fromIndex, 1);
896
+ next.splice(toIndex, 0, moved);
897
+ reorderMemberships.mutate({ itemId, orderedAssetIds: next });
898
+ };
899
+ const handleSaveAltText = (sourceAssetId) => {
900
+ const draft = altDrafts[sourceAssetId];
901
+ if (draft === void 0) return;
902
+ const trimmed = draft.trim();
903
+ updateMembership.mutate(
904
+ // `altText` is `.min(1)` when present, so clearing the field is `null` rather
905
+ // than an empty string -- an empty string is a validation error, not a clear.
906
+ { itemId, sourceAssetId, updates: { altText: trimmed === "" ? null : trimmed } },
907
+ {
908
+ onSuccess: () => setAltDrafts((drafts) => {
909
+ const { [sourceAssetId]: _cleared, ...rest } = drafts;
910
+ return rest;
911
+ })
912
+ }
913
+ );
914
+ };
915
+ const isMutating = addMembership.isPending || removeMembership.isPending || reorderMemberships.isPending || updateMembership.isPending;
916
+ return /* @__PURE__ */ jsxs(Paper, { withBorder: true, p: "md", "data-testid": "source-assets-panel", children: [
917
+ /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
918
+ /* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
919
+ /* @__PURE__ */ jsx(Title, { order: 4, children: "Source Assets" }),
920
+ isMembershipLoaded && /* @__PURE__ */ jsxs(Badge, { variant: "light", size: "sm", children: [
921
+ memberships.length,
922
+ " linked"
923
+ ] })
924
+ ] }),
925
+ !isMembershipLoaded ? /* @__PURE__ */ jsx(Group, { justify: "center", py: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : memberships.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No source assets linked to this item." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
926
+ /* @__PURE__ */ jsx(
927
+ SelectedAssetStrip,
928
+ {
929
+ assets: stripAssets,
930
+ onReorder: (reordered) => reorderMemberships.mutate({ itemId, orderedAssetIds: reordered.map((asset) => asset.id) })
931
+ }
932
+ ),
933
+ /* @__PURE__ */ jsxs(Table, { children: [
934
+ /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
935
+ /* @__PURE__ */ jsx(Table.Th, { children: "Position" }),
936
+ /* @__PURE__ */ jsx(Table.Th, { children: "Asset" }),
937
+ /* @__PURE__ */ jsx(Table.Th, { children: "Kind" }),
938
+ /* @__PURE__ */ jsx(Table.Th, { children: "Alt text" }),
939
+ /* @__PURE__ */ jsx(Table.Th, { children: "Crop" }),
940
+ /* @__PURE__ */ jsx(Table.Th, {})
941
+ ] }) }),
942
+ /* @__PURE__ */ jsx(Table.Tbody, { children: memberships.map((detail, index) => {
943
+ const { membership, asset } = detail;
944
+ const draft = altDrafts[membership.sourceAssetId];
945
+ const storedAltText = membership.altText ?? "";
946
+ return /* @__PURE__ */ jsxs(
947
+ Table.Tr,
948
+ {
949
+ "data-testid": `source-asset-row-${membership.sourceAssetId}`,
950
+ children: [
951
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
952
+ /* @__PURE__ */ jsx(Text, { size: "sm", children: membership.position }),
953
+ index === 0 && /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "filled", children: "Cover" })
954
+ ] }) }),
955
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", children: describeAsset(membership, asset) }) }),
956
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: asset?.kind ?? "\u2014" }) }),
957
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
958
+ /* @__PURE__ */ jsx(
959
+ TextInput,
960
+ {
961
+ size: "xs",
962
+ placeholder: "Describe this slide",
963
+ value: draft ?? storedAltText,
964
+ onChange: (event) => setAltDrafts((drafts) => ({
965
+ ...drafts,
966
+ [membership.sourceAssetId]: event.currentTarget.value
967
+ }))
968
+ }
969
+ ),
970
+ draft !== void 0 && draft !== storedAltText && /* @__PURE__ */ jsx(
971
+ Button,
972
+ {
973
+ size: "compact-xs",
974
+ variant: "light",
975
+ disabled: isMutating,
976
+ onClick: () => handleSaveAltText(membership.sourceAssetId),
977
+ children: "Save"
978
+ }
979
+ )
980
+ ] }) }),
981
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
982
+ /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: membership.crop ? "set" : "\u2014" }),
983
+ /* @__PURE__ */ jsx(
984
+ Button,
985
+ {
986
+ size: "compact-xs",
987
+ variant: "subtle",
988
+ leftSection: /* @__PURE__ */ jsx(IconCrop, { size: 14 }),
989
+ disabled: isMutating || readStorageReference(asset) === null,
990
+ onClick: () => {
991
+ setCropDraft(membership.crop);
992
+ setCroppingAssetId(membership.sourceAssetId);
993
+ },
994
+ children: "Crop"
995
+ }
996
+ )
997
+ ] }) }),
998
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: 4, justify: "flex-end", wrap: "nowrap", children: [
999
+ /* @__PURE__ */ jsx(
1000
+ Button,
1001
+ {
1002
+ size: "compact-xs",
1003
+ variant: "subtle",
1004
+ "aria-label": "Move up",
1005
+ disabled: index === 0 || isMutating,
1006
+ onClick: () => handleMove(index, index - 1),
1007
+ children: /* @__PURE__ */ jsx(IconArrowUp, { size: 14 })
1008
+ }
1009
+ ),
1010
+ /* @__PURE__ */ jsx(
1011
+ Button,
1012
+ {
1013
+ size: "compact-xs",
1014
+ variant: "subtle",
1015
+ "aria-label": "Move down",
1016
+ disabled: index === memberships.length - 1 || isMutating,
1017
+ onClick: () => handleMove(index, index + 1),
1018
+ children: /* @__PURE__ */ jsx(IconArrowDown, { size: 14 })
1019
+ }
1020
+ ),
1021
+ /* @__PURE__ */ jsx(
1022
+ Button,
1023
+ {
1024
+ size: "compact-xs",
1025
+ variant: "subtle",
1026
+ color: "red",
1027
+ leftSection: /* @__PURE__ */ jsx(IconUnlink, { size: 14 }),
1028
+ disabled: isMutating,
1029
+ onClick: () => removeMembership.mutate({ itemId, sourceAssetId: membership.sourceAssetId }),
1030
+ children: "Unlink"
1031
+ }
1032
+ )
1033
+ ] }) })
1034
+ ]
1035
+ },
1036
+ membership.sourceAssetId
1037
+ );
1038
+ }) })
1039
+ ] })
1040
+ ] }),
1041
+ /* @__PURE__ */ jsxs(Group, { gap: "sm", align: "flex-end", children: [
1042
+ /* @__PURE__ */ jsx(
1043
+ Select,
1044
+ {
1045
+ size: "xs",
1046
+ searchable: true,
1047
+ clearable: true,
1048
+ label: "Link an existing source asset",
1049
+ placeholder: linkableOptions.length === 0 ? "Nothing left to link" : "Choose an asset",
1050
+ data: linkableOptions,
1051
+ value: assetToLink,
1052
+ onChange: setAssetToLink,
1053
+ disabled: linkableOptions.length === 0 || isMutating,
1054
+ style: { minWidth: 320 }
1055
+ }
1056
+ ),
1057
+ /* @__PURE__ */ jsx(
1058
+ Button,
1059
+ {
1060
+ size: "compact-sm",
1061
+ variant: "light",
1062
+ leftSection: /* @__PURE__ */ jsx(IconLink, { size: 14 }),
1063
+ disabled: !assetToLink || isMutating,
1064
+ onClick: handleLink,
1065
+ children: "Link"
1066
+ }
1067
+ )
1068
+ ] }),
1069
+ /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Position 0 is the cover. Unlinking leaves the remaining positions untouched, so a gap is normal \u2014 use move up / move down to renumber the whole set. Unlinking never deletes the source asset itself." })
1070
+ ] }),
1071
+ /* @__PURE__ */ jsx(
1072
+ Modal,
1073
+ {
1074
+ opened: croppingAssetId !== null,
1075
+ onClose: () => setCroppingAssetId(null),
1076
+ title: croppingDetail === void 0 ? "Crop" : `Crop ${describeAsset(croppingDetail.membership, croppingDetail.asset)}`,
1077
+ size: "lg",
1078
+ children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
1079
+ /* @__PURE__ */ jsx(
1080
+ AssetCropEditor,
1081
+ {
1082
+ imageUrl: croppingReference === null ? null : links[croppingReference.cloudStorageId] ?? null,
1083
+ value: cropDraft,
1084
+ onChange: setCropDraft,
1085
+ disabled: isMutating
1086
+ }
1087
+ ),
1088
+ /* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
1089
+ /* @__PURE__ */ jsx(
1090
+ Button,
1091
+ {
1092
+ variant: "subtle",
1093
+ color: "red",
1094
+ disabled: isMutating || cropDraft === null,
1095
+ onClick: () => setCropDraft(null),
1096
+ children: "Clear crop"
1097
+ }
1098
+ ),
1099
+ /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
1100
+ /* @__PURE__ */ jsx(Button, { variant: "default", onClick: () => setCroppingAssetId(null), children: "Cancel" }),
1101
+ /* @__PURE__ */ jsx(
1102
+ Button,
1103
+ {
1104
+ disabled: isMutating || croppingAssetId === null,
1105
+ onClick: () => {
1106
+ if (croppingAssetId === null) return;
1107
+ updateMembership.mutate(
1108
+ { itemId, sourceAssetId: croppingAssetId, updates: { crop: cropDraft } },
1109
+ { onSuccess: () => setCroppingAssetId(null) }
1110
+ );
1111
+ },
1112
+ children: "Save crop"
1113
+ }
1114
+ )
1115
+ ] })
1116
+ ] })
1117
+ ] })
1118
+ }
1119
+ )
1120
+ ] });
1121
+ }
1122
+ var PREVIEW_MAX_WIDTH = 380;
1123
+ function PostPreview({ slides, caption, hashtags = [], aspectRatio = 4 / 5, title }) {
1124
+ const [index, setIndex] = useState(0);
1125
+ useEffect(() => {
1126
+ if (index > slides.length - 1) setIndex(Math.max(0, slides.length - 1));
1127
+ }, [slides.length, index]);
1128
+ const slide = slides[index];
1129
+ return /* @__PURE__ */ jsxs(Stack, { gap: "xs", style: { maxWidth: PREVIEW_MAX_WIDTH }, "data-testid": "post-preview", children: [
1130
+ title !== void 0 && /* @__PURE__ */ jsx(Text, { fw: 500, lineClamp: 1, children: title }),
1131
+ /* @__PURE__ */ jsx(Paper, { withBorder: true, p: 0, style: { overflow: "hidden" }, children: /* @__PURE__ */ jsxs(
1132
+ Box,
1133
+ {
1134
+ style: {
1135
+ position: "relative",
1136
+ width: "100%",
1137
+ aspectRatio: `${aspectRatio}`,
1138
+ overflow: "hidden",
1139
+ background: "var(--mantine-color-gray-1)"
1140
+ },
1141
+ "data-testid": "post-preview-frame",
1142
+ children: [
1143
+ slide === void 0 || slide.imageUrl === null ? /* @__PURE__ */ jsx(Group, { justify: "center", align: "center", h: "100%", children: /* @__PURE__ */ jsx(IconPhotoOff, { size: 20, color: "var(--mantine-color-dimmed)" }) }) : /* @__PURE__ */ jsx(
1144
+ "img",
1145
+ {
1146
+ src: slide.imageUrl,
1147
+ alt: slide.altText ?? "",
1148
+ "data-testid": "post-preview-image",
1149
+ style: cropStyle(slide.crop ?? null)
1150
+ }
1151
+ ),
1152
+ index > 0 && /* @__PURE__ */ jsx(
1153
+ ActionIcon,
1154
+ {
1155
+ variant: "default",
1156
+ radius: "xl",
1157
+ size: "lg",
1158
+ "aria-label": "Previous slide",
1159
+ onClick: () => setIndex((current) => Math.max(0, current - 1)),
1160
+ style: { position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)" },
1161
+ children: /* @__PURE__ */ jsx(IconChevronLeft, { size: 18 })
1162
+ }
1163
+ ),
1164
+ index < slides.length - 1 && /* @__PURE__ */ jsx(
1165
+ ActionIcon,
1166
+ {
1167
+ variant: "default",
1168
+ radius: "xl",
1169
+ size: "lg",
1170
+ "aria-label": "Next slide",
1171
+ onClick: () => setIndex((current) => Math.min(slides.length - 1, current + 1)),
1172
+ style: { position: "absolute", right: 8, top: "50%", transform: "translateY(-50%)" },
1173
+ children: /* @__PURE__ */ jsx(IconChevronRight, { size: 18 })
1174
+ }
1175
+ )
1176
+ ]
1177
+ }
1178
+ ) }),
1179
+ slides.length > 1 && /* @__PURE__ */ jsx(Group, { justify: "center", gap: 6, "data-testid": "post-preview-dots", children: slides.map((each, dotIndex) => /* @__PURE__ */ jsx(
1180
+ Box,
1181
+ {
1182
+ style: {
1183
+ width: 6,
1184
+ height: 6,
1185
+ borderRadius: "50%",
1186
+ background: dotIndex === index ? "var(--mantine-color-blue-5)" : "var(--mantine-color-gray-4)"
1187
+ }
1188
+ },
1189
+ each.id
1190
+ )) }),
1191
+ /* @__PURE__ */ jsx(Text, { size: "sm", style: { whiteSpace: "pre-wrap" }, "data-testid": "post-preview-caption", children: caption === null || caption === void 0 || caption.length === 0 ? /* @__PURE__ */ jsx(Text, { component: "span", size: "sm", c: "dimmed", children: "No caption yet" }) : caption }),
1192
+ hashtags.length > 0 && /* @__PURE__ */ jsx(Text, { size: "sm", c: "blue", "data-testid": "post-preview-hashtags", children: hashtags.map((tag) => `#${tag.replace(/^#/, "")}`).join(" ") })
1193
+ ] });
1194
+ }
1195
+ function cropStyle(crop) {
1196
+ if (crop === null) {
1197
+ return { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" };
1198
+ }
1199
+ return {
1200
+ position: "absolute",
1201
+ left: `${-crop.x / crop.width * 100}%`,
1202
+ top: `${-crop.y / crop.height * 100}%`,
1203
+ width: `${1 / crop.width * 100}%`,
1204
+ height: `${1 / crop.height * 100}%`,
1205
+ objectFit: "cover"
1206
+ };
1207
+ }
1208
+
1209
+ // src/features/content/renderCrop.ts
1210
+ var CONTENT_DERIVATIVES_BUCKET = "content-derivatives";
1211
+ var DERIVATIVE_MAX_WIDTH = 1080;
1212
+ var DERIVATIVE_JPEG_QUALITY = 0.9;
1213
+ function derivativeObjectKey(contentItemId, sourceAssetId) {
1214
+ return `${contentItemId}/${sourceAssetId}.jpg`;
1215
+ }
1216
+ function isDerivativeStale(crop, derivativeCrop, derivativePath) {
1217
+ if (crop === null || derivativePath === null) return false;
1218
+ if (derivativeCrop === null) return true;
1219
+ return crop.x !== derivativeCrop.x || crop.y !== derivativeCrop.y || crop.width !== derivativeCrop.width || crop.height !== derivativeCrop.height;
1220
+ }
1221
+ function loadImage(url) {
1222
+ return new Promise((resolve, reject) => {
1223
+ const image = new Image();
1224
+ image.crossOrigin = "anonymous";
1225
+ image.onload = () => resolve(image);
1226
+ image.onerror = () => reject(new Error(`Could not load the image to crop: ${url}`));
1227
+ image.src = url;
1228
+ });
1229
+ }
1230
+ function canvasToJpegBlob(canvas, quality) {
1231
+ return new Promise((resolve, reject) => {
1232
+ canvas.toBlob(
1233
+ (blob) => {
1234
+ if (blob === null) {
1235
+ reject(new Error("The browser produced no image data for the crop"));
1236
+ return;
1237
+ }
1238
+ resolve(blob);
1239
+ },
1240
+ "image/jpeg",
1241
+ quality
1242
+ );
1243
+ });
1244
+ }
1245
+ async function renderCropToJpeg(imageUrl, crop, options = {}) {
1246
+ const maxWidth = options.maxWidth ?? DERIVATIVE_MAX_WIDTH;
1247
+ const quality = options.quality ?? DERIVATIVE_JPEG_QUALITY;
1248
+ const image = await loadImage(imageUrl);
1249
+ const box = crop ?? { x: 0, y: 0, width: 1, height: 1 };
1250
+ const sourceX = box.x * image.naturalWidth;
1251
+ const sourceY = box.y * image.naturalHeight;
1252
+ const sourceWidth = box.width * image.naturalWidth;
1253
+ const sourceHeight = box.height * image.naturalHeight;
1254
+ if (sourceWidth < 1 || sourceHeight < 1) {
1255
+ throw new Error("The crop resolves to less than one pixel of the source");
1256
+ }
1257
+ const width = Math.max(1, Math.round(Math.min(maxWidth, sourceWidth)));
1258
+ const height = Math.max(1, Math.round(width * sourceHeight / sourceWidth));
1259
+ const canvas = document.createElement("canvas");
1260
+ canvas.width = width;
1261
+ canvas.height = height;
1262
+ const context = canvas.getContext("2d");
1263
+ if (context === null) throw new Error("The browser would not provide a 2D canvas context");
1264
+ context.fillStyle = "#ffffff";
1265
+ context.fillRect(0, 0, width, height);
1266
+ context.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, width, height);
1267
+ return canvasToJpegBlob(canvas, quality);
1268
+ }
1269
+ var CAPTION_STEP_KEY = "caption-generation";
1270
+ var CAPTION_WORKFLOW_ID = "cnt-caption-generation-workflow";
1271
+ var AUTOSAVE_DELAY_MS = 800;
1272
+ function readCaptionAttempt(attempt) {
1273
+ if (attempt.stepKey !== CAPTION_STEP_KEY) return null;
1274
+ const payload = attempt.payload;
1275
+ const caption = payload?.caption;
1276
+ if (typeof caption !== "string" || caption.length === 0) return null;
1277
+ return {
1278
+ attempt,
1279
+ caption,
1280
+ hashtags: Array.isArray(payload?.hashtags) ? payload.hashtags.filter((tag) => typeof tag === "string") : [],
1281
+ instructions: typeof payload?.instructions === "string" ? payload.instructions : ""
1282
+ };
1283
+ }
1284
+ function CaptionPanel({ itemId, body, onDraftChange }) {
1285
+ const queryClient = useQueryClient();
1286
+ const savedBody = body ?? "";
1287
+ const [instructions, setInstructions] = useState("");
1288
+ const [executionId, setExecutionId] = useState(null);
1289
+ const [failure, setFailure] = useState(null);
1290
+ const [draft, setDraft] = useState(savedBody);
1291
+ const attemptsQuery = useContentItemAttempts(itemId);
1292
+ const executeAsync = useExecuteAsync();
1293
+ const updateItem = useUpdateContentItem();
1294
+ const execution = useExecution(CAPTION_WORKFLOW_ID, executionId ?? "");
1295
+ const captions = useMemo(() => {
1296
+ const rows = (attemptsQuery.data?.data ?? []).map(readCaptionAttempt).filter((row) => row !== null);
1297
+ rows.sort((left, right) => right.attempt.attemptNumber - left.attempt.attemptNumber);
1298
+ return rows.map((row, index) => ({ ...row, displayNumber: rows.length - index }));
1299
+ }, [attemptsQuery.data]);
1300
+ const status = execution.data?.status;
1301
+ const isRunning = executeAsync.isPending || executionId !== null && (status === void 0 || status === "running");
1302
+ const lastSyncedRef = useRef(savedBody);
1303
+ const [debouncedDraft] = useDebouncedValue(draft, AUTOSAVE_DELAY_MS);
1304
+ useEffect(() => {
1305
+ if (savedBody === lastSyncedRef.current) return;
1306
+ lastSyncedRef.current = savedBody;
1307
+ setDraft(savedBody);
1308
+ }, [savedBody]);
1309
+ useEffect(() => {
1310
+ if (debouncedDraft === lastSyncedRef.current) return;
1311
+ lastSyncedRef.current = debouncedDraft;
1312
+ updateItem.mutate({ itemId, updates: { body: debouncedDraft } });
1313
+ }, [debouncedDraft, itemId, updateItem]);
1314
+ useEffect(() => {
1315
+ onDraftChange?.(draft);
1316
+ }, [draft, onDraftChange]);
1317
+ const isDirty = draft !== lastSyncedRef.current;
1318
+ useEffect(() => {
1319
+ if (executionId === null || status === void 0 || status === "running") return;
1320
+ if (status === "failed") {
1321
+ setFailure(execution.data?.error ?? "The caption workflow failed. Check the execution log for details.");
1322
+ }
1323
+ void queryClient.invalidateQueries({ queryKey: contentKeys.items() });
1324
+ setExecutionId(null);
1325
+ }, [status, executionId, execution.data?.error, queryClient]);
1326
+ const handleGenerate = async () => {
1327
+ setFailure(null);
1328
+ try {
1329
+ const result = await executeAsync.mutateAsync({
1330
+ resourceId: CAPTION_WORKFLOW_ID,
1331
+ resourceType: "workflow",
1332
+ input: {
1333
+ contentItemId: itemId,
1334
+ ...instructions.trim().length > 0 ? { instructions: instructions.trim() } : {}
1335
+ }
1336
+ });
1337
+ setExecutionId(result.executionId);
1338
+ } catch (error) {
1339
+ setFailure(error instanceof Error ? error.message : "Could not start the caption workflow.");
1340
+ }
1341
+ };
1342
+ const handleUse = (candidate) => {
1343
+ setDraft(
1344
+ candidate.hashtags.length === 0 ? candidate.caption : `${candidate.caption}
1345
+
1346
+ ${candidate.hashtags.map((tag) => `#${tag.replace(/^#/, "")}`).join(" ")}`
1347
+ );
1348
+ };
1349
+ return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", "data-testid": "caption-panel", children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
1350
+ /* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
1351
+ /* @__PURE__ */ jsx(Title, { order: 5, children: "Caption" }),
1352
+ /* @__PURE__ */ jsxs(Badge, { variant: "light", size: "sm", children: [
1353
+ captions.length,
1354
+ " generated"
1355
+ ] })
1356
+ ] }),
1357
+ failure !== null && /* @__PURE__ */ jsx(Alert, { color: "red", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), "data-testid": "caption-panel-error", children: failure }),
1358
+ /* @__PURE__ */ jsx(
1359
+ Textarea,
1360
+ {
1361
+ label: "The caption",
1362
+ description: "Edit it freely. Changes save on their own.",
1363
+ placeholder: "Write it yourself, or generate one below and edit from there.",
1364
+ autosize: true,
1365
+ minRows: 4,
1366
+ maxRows: 16,
1367
+ value: draft,
1368
+ onChange: (event) => setDraft(event.currentTarget.value),
1369
+ "data-testid": "caption-panel-body"
1370
+ }
1371
+ ),
1372
+ /* @__PURE__ */ jsx(Group, { justify: "flex-end", gap: "xs", mt: -6, children: updateItem.isPending ? /* @__PURE__ */ jsxs(Group, { gap: 6, children: [
1373
+ /* @__PURE__ */ jsx(Loader, { size: 12 }),
1374
+ /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Saving" })
1375
+ ] }) : isDirty ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", "data-testid": "caption-panel-save-state", children: "Unsaved" }) : /* @__PURE__ */ jsxs(Group, { gap: 4, children: [
1376
+ /* @__PURE__ */ jsx(IconCheck, { size: 12, color: "var(--mantine-color-dimmed)" }),
1377
+ /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", "data-testid": "caption-panel-save-state", children: "Saved" })
1378
+ ] }) }),
1379
+ /* @__PURE__ */ jsx(
1380
+ Textarea,
1381
+ {
1382
+ label: "Direction (optional)",
1383
+ description: "The angle, the occasion, who is in frame -- anything the photos cannot show on their own.",
1384
+ placeholder: "e.g. this is the second shop opening, keep it short and warm",
1385
+ autosize: true,
1386
+ minRows: 2,
1387
+ maxRows: 5,
1388
+ value: instructions,
1389
+ disabled: isRunning,
1390
+ onChange: (event) => setInstructions(event.currentTarget.value)
1391
+ }
1392
+ ),
1393
+ /* @__PURE__ */ jsxs(Group, { children: [
1394
+ /* @__PURE__ */ jsx(
1395
+ Button,
1396
+ {
1397
+ leftSection: isRunning ? /* @__PURE__ */ jsx(Loader, { size: 14 }) : /* @__PURE__ */ jsx(IconSparkles, { size: 16 }),
1398
+ disabled: isRunning,
1399
+ onClick: () => void handleGenerate(),
1400
+ "data-testid": "caption-panel-generate",
1401
+ children: captions.length === 0 ? "Generate caption" : "Regenerate"
1402
+ }
1403
+ ),
1404
+ isRunning && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Reading the slides and writing a caption. This does not overwrite the previous one." })
1405
+ ] }),
1406
+ attemptsQuery.isLoading ? /* @__PURE__ */ jsx(Group, { justify: "center", p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : captions.map((candidate) => {
1407
+ const isLive = draft.startsWith(candidate.caption);
1408
+ return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "sm", "data-testid": "caption-panel-attempt", children: /* @__PURE__ */ jsxs(Stack, { gap: 6, children: [
1409
+ /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", children: [
1410
+ /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
1411
+ /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
1412
+ "Attempt ",
1413
+ candidate.displayNumber
1414
+ ] }),
1415
+ isLive && /* @__PURE__ */ jsx(Badge, { size: "xs", color: "green", variant: "light", leftSection: /* @__PURE__ */ jsx(IconCheck, { size: 10 }), children: "In use" })
1416
+ ] }),
1417
+ /* @__PURE__ */ jsx(
1418
+ Button,
1419
+ {
1420
+ size: "compact-xs",
1421
+ variant: "light",
1422
+ disabled: isLive || updateItem.isPending,
1423
+ onClick: () => handleUse(candidate),
1424
+ children: "Use this caption"
1425
+ }
1426
+ )
1427
+ ] }),
1428
+ /* @__PURE__ */ jsx(Text, { size: "sm", style: { whiteSpace: "pre-wrap" }, children: candidate.caption }),
1429
+ candidate.hashtags.length > 0 && /* @__PURE__ */ jsx(Text, { size: "sm", c: "blue", children: candidate.hashtags.map((tag) => `#${tag.replace(/^#/, "")}`).join(" ") }),
1430
+ candidate.instructions.length > 0 && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
1431
+ "Asked for: ",
1432
+ candidate.instructions
1433
+ ] })
1434
+ ] }) }, candidate.attempt.id);
1435
+ }),
1436
+ !attemptsQuery.isLoading && captions.length === 0 && /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "Nothing generated yet. Analysed slides make for a caption that describes what is actually in them -- or just write the caption above yourself." })
1437
+ ] }) });
1438
+ }
1439
+ var IMAGE_ANALYSIS_WORKFLOW_ID = "cnt-image-analysis-workflow";
1440
+ function readStoredAnalysis(asset) {
1441
+ const metadata = asset?.metadata;
1442
+ if (typeof metadata !== "object" || metadata === null) return null;
1443
+ const analysis = metadata.analysis;
1444
+ if (typeof analysis !== "object" || analysis === null) return null;
1445
+ const raw = analysis;
1446
+ const asString = (key) => typeof raw[key] === "string" ? raw[key] : void 0;
1447
+ return {
1448
+ description: asString("description"),
1449
+ subjects: Array.isArray(raw.subjects) ? raw.subjects.filter((one) => typeof one === "string") : void 0,
1450
+ setting: asString("setting"),
1451
+ mood: asString("mood"),
1452
+ textInImage: asString("textInImage"),
1453
+ altText: asString("altText"),
1454
+ model: asString("model"),
1455
+ generatedAt: asString("generatedAt")
1456
+ };
1457
+ }
1458
+ function SlideAnalysisPanel({ itemId }) {
1459
+ const queryClient = useQueryClient();
1460
+ const [executionId, setExecutionId] = useState(null);
1461
+ const [failure, setFailure] = useState(null);
1462
+ const { memberships, isMembershipLoaded } = useContentItemSourceAssetDetails(itemId);
1463
+ const executeAsync = useExecuteAsync();
1464
+ const execution = useExecution(IMAGE_ANALYSIS_WORKFLOW_ID, executionId ?? "");
1465
+ const status = execution.data?.status;
1466
+ const isRunning = executeAsync.isPending || executionId !== null && (status === void 0 || status === "running");
1467
+ const analyzedCount = useMemo(
1468
+ () => memberships.filter((detail) => readStoredAnalysis(detail.asset) !== null).length,
1469
+ [memberships]
1470
+ );
1471
+ useEffect(() => {
1472
+ if (executionId === null || status === void 0 || status === "running") return;
1473
+ if (status === "failed") {
1474
+ setFailure(execution.data?.error ?? "The analysis workflow failed. Check the execution log for details.");
1475
+ }
1476
+ void queryClient.invalidateQueries({ queryKey: contentKeys.sourceAssets() });
1477
+ setExecutionId(null);
1478
+ }, [status, executionId, execution.data?.error, queryClient]);
1479
+ const handleAnalyze = async () => {
1480
+ setFailure(null);
1481
+ try {
1482
+ const result = await executeAsync.mutateAsync({
1483
+ resourceId: IMAGE_ANALYSIS_WORKFLOW_ID,
1484
+ resourceType: "workflow",
1485
+ input: { contentItemId: itemId }
1486
+ });
1487
+ setExecutionId(result.executionId);
1488
+ } catch (error) {
1489
+ setFailure(error instanceof Error ? error.message : "Could not start the analysis workflow.");
1490
+ }
1491
+ };
1492
+ return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", "data-testid": "slide-analysis-panel", children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
1493
+ /* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
1494
+ /* @__PURE__ */ jsx(Title, { order: 5, children: "Slide analysis" }),
1495
+ /* @__PURE__ */ jsxs(Badge, { variant: "light", size: "sm", children: [
1496
+ analyzedCount,
1497
+ " of ",
1498
+ memberships.length,
1499
+ " analysed"
1500
+ ] })
1501
+ ] }),
1502
+ failure !== null && /* @__PURE__ */ jsx(Alert, { color: "red", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), "data-testid": "slide-analysis-error", children: failure }),
1503
+ /* @__PURE__ */ jsxs(Group, { children: [
1504
+ /* @__PURE__ */ jsx(
1505
+ Button,
1506
+ {
1507
+ leftSection: isRunning ? /* @__PURE__ */ jsx(Loader, { size: 14 }) : /* @__PURE__ */ jsx(IconEye, { size: 16 }),
1508
+ disabled: isRunning || memberships.length === 0,
1509
+ onClick: () => void handleAnalyze(),
1510
+ "data-testid": "slide-analysis-run",
1511
+ children: analyzedCount === 0 ? "Analyse slides" : "Re-analyse slides"
1512
+ }
1513
+ ),
1514
+ isRunning && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Reading each slide with a vision model." })
1515
+ ] }),
1516
+ !isMembershipLoaded ? /* @__PURE__ */ jsx(Group, { justify: "center", p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : memberships.map((detail) => {
1517
+ const analysis = readStoredAnalysis(detail.asset);
1518
+ const label = detail.asset?.title ?? detail.membership.sourceAssetId;
1519
+ return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "sm", "data-testid": "slide-analysis-row", children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
1520
+ /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
1521
+ /* @__PURE__ */ jsxs(Badge, { size: "xs", variant: "light", children: [
1522
+ "Slide ",
1523
+ detail.membership.position + 1
1524
+ ] }),
1525
+ /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, lineClamp: 1, children: label })
1526
+ ] }),
1527
+ analysis === null ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Not analysed yet." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1528
+ analysis.description !== void 0 && /* @__PURE__ */ jsx(Text, { size: "sm", children: analysis.description }),
1529
+ analysis.subjects !== void 0 && analysis.subjects.length > 0 && /* @__PURE__ */ jsx(Group, { gap: 4, children: analysis.subjects.map((subject) => /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "outline", children: subject }, subject)) }),
1530
+ (analysis.setting !== void 0 || analysis.mood !== void 0) && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: [analysis.setting, analysis.mood].filter(Boolean).join(" - ") }),
1531
+ analysis.textInImage !== void 0 && analysis.textInImage.length > 0 && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
1532
+ "Text in image: ",
1533
+ analysis.textInImage
1534
+ ] })
1535
+ ] })
1536
+ ] }) }, detail.membership.id);
1537
+ }),
1538
+ /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Analysis is stored on the photo itself, so every post that reuses it reads the same description and a re-crop does not invalidate it." })
1539
+ ] }) });
1540
+ }
538
1541
 
539
1542
  // src/lib/content/stage-colors.ts
540
1543
  var FALLBACK_CONTENT_STAGE_COLORS = {
@@ -554,6 +1557,10 @@ function getContentStageColor(entry) {
554
1557
  }
555
1558
  var OVERVIEW_ITEMS_LIMIT = 100;
556
1559
  var RECENT_ITEMS_LIMIT = 8;
1560
+ function resolveItemCurrentStep(item, pipelines) {
1561
+ const pipeline = pipelines.find((candidate) => candidate.id === item.pipelineId);
1562
+ return resolveCurrentContentStep(item, pipeline?.steps ?? []);
1563
+ }
557
1564
  function ContentOverviewPage({ headerActions, footer } = {}) {
558
1565
  const navigate = useNavigate();
559
1566
  const itemsQuery = useContentItems({ limit: OVERVIEW_ITEMS_LIMIT });
@@ -568,7 +1575,9 @@ function ContentOverviewPage({ headerActions, footer } = {}) {
568
1575
  }
569
1576
  return counts;
570
1577
  }, [items]);
571
- const pendingReviewCount = items.filter((item) => item.reviewedAt === null).length;
1578
+ const pendingReviewCount = items.filter(
1579
+ (item) => isAwaitingReview(item, resolveItemCurrentStep(item, contentConfig.pipelines))
1580
+ ).length;
572
1581
  const totalItems = itemsQuery.data?.total ?? items.length;
573
1582
  const totalDistributions = distributionsQuery.data?.data.length ?? 0;
574
1583
  const recentItems = useMemo(
@@ -708,6 +1717,15 @@ function ContentItemsPage({ headerActions } = {}) {
708
1717
  const [statusFilter, setStatusFilter] = useState("");
709
1718
  const [pillarFilter, setPillarFilter] = useState("");
710
1719
  const pagination = usePaginationState(PAGE_SIZE, [searchQuery, statusFilter, pillarFilter, reviewFilterActive]);
1720
+ const openItem = (item) => {
1721
+ const pipeline = contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId);
1722
+ const target = resolveReviewTarget(item, pipeline, pipeline?.steps ?? []);
1723
+ if (target.kind === "workspace") {
1724
+ void navigate({ to: target.route });
1725
+ return;
1726
+ }
1727
+ void navigate({ to: "/content/items/$itemId", params: { itemId: item.id } });
1728
+ };
711
1729
  const itemsQuery = useContentItems({
712
1730
  search: searchQuery.trim() || void 0,
713
1731
  status: statusFilter || void 0,
@@ -716,7 +1734,11 @@ function ContentItemsPage({ headerActions } = {}) {
716
1734
  offset: pagination.offset
717
1735
  });
718
1736
  const fetchedItems = itemsQuery.data?.data ?? [];
719
- const items = reviewFilterActive ? fetchedItems.filter((item) => item.reviewedAt === null) : fetchedItems;
1737
+ const items = reviewFilterActive ? fetchedItems.filter((item) => {
1738
+ const pipeline = contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId);
1739
+ const step = resolveCurrentContentStep(item, pipeline?.steps ?? []);
1740
+ return isAwaitingReview(item, step);
1741
+ }) : fetchedItems;
720
1742
  const total = itemsQuery.data?.total ?? 0;
721
1743
  const statusOptions = [
722
1744
  { value: "", label: "All statuses" },
@@ -789,31 +1811,23 @@ function ContentItemsPage({ headerActions } = {}) {
789
1811
  const statusEntry = contentConfig.stageCatalog[item.status];
790
1812
  const pillarEntry = item.pillar ? contentConfig.stageCatalog[item.pillar] : void 0;
791
1813
  const statusColors = getContentStageColor(statusEntry);
792
- return /* @__PURE__ */ jsxs(
793
- Table.Tr,
794
- {
795
- style: { cursor: "pointer" },
796
- onClick: () => navigate({ to: "/content/items/$itemId", params: { itemId: item.id } }),
797
- children: [
798
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, children: getContentItemIdentity(item) }) }),
799
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(
800
- Badge,
801
- {
802
- size: "sm",
803
- variant: "light",
804
- styles: {
805
- root: { color: statusColors.accent, backgroundColor: statusColors.background }
806
- },
807
- children: statusEntry?.label ?? item.status
808
- }
809
- ) }),
810
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: pillarEntry?.label ?? item.pillar ?? "\u2014" }) }),
811
- /* @__PURE__ */ jsx(Table.Td, { children: item.reviewedAt ? /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-success)", children: "Reviewed" }) : /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-warning)", children: "Pending" }) }),
812
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatRelativeTime(item.updatedAt) }) })
813
- ]
814
- },
815
- item.id
816
- );
1814
+ return /* @__PURE__ */ jsxs(Table.Tr, { style: { cursor: "pointer" }, onClick: () => openItem(item), children: [
1815
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, children: getContentItemIdentity(item) }) }),
1816
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(
1817
+ Badge,
1818
+ {
1819
+ size: "sm",
1820
+ variant: "light",
1821
+ styles: {
1822
+ root: { color: statusColors.accent, backgroundColor: statusColors.background }
1823
+ },
1824
+ children: statusEntry?.label ?? item.status
1825
+ }
1826
+ ) }),
1827
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: pillarEntry?.label ?? item.pillar ?? "\u2014" }) }),
1828
+ /* @__PURE__ */ jsx(Table.Td, { children: item.reviewedAt ? /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-success)", children: "Reviewed" }) : /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-warning)", children: "Pending" }) }),
1829
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatRelativeTime(item.updatedAt) }) })
1830
+ ] }, item.id);
817
1831
  }) })
818
1832
  ] }),
819
1833
  total > PAGE_SIZE && /* @__PURE__ */ jsx(Group, { justify: "center", children: /* @__PURE__ */ jsx(
@@ -942,20 +1956,21 @@ function ContentItemReviewPage({ itemId, headerActions }) {
942
1956
  const pipeline = item ? contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId) : void 0;
943
1957
  const pipelineSteps = useMemo(() => pipeline?.steps ?? [], [pipeline]);
944
1958
  const isTerminalPipeline = pipelineSteps.length <= 1;
945
- const openGates = useMemo(
946
- () => getOpenContentReviewGates(item?.processingState ?? {}, pipelineSteps),
947
- [item?.processingState, pipelineSteps]
1959
+ const reviewTarget = useMemo(
1960
+ () => item ? resolveReviewTarget(item, pipeline, pipelineSteps) : { kind: "none" },
1961
+ [item, pipeline, pipelineSteps]
948
1962
  );
949
- const earliestOpenGate = openGates[0];
1963
+ const earliestOpenGate = reviewTarget.kind === "queued" ? reviewTarget.gate : void 0;
1964
+ const workspaceRoute = reviewTarget.kind === "workspace" ? reviewTarget.route : void 0;
950
1965
  const reviewDisabledReason = useMemo(() => {
951
- if (earliestOpenGate) return void 0;
1966
+ if (reviewTarget.kind !== "none") return void 0;
952
1967
  if (!item?.pipelineId) return "This item has no pipeline, so there is no review gate to clear.";
953
1968
  if (!pipeline) return `Unknown pipeline "${item.pipelineId}" -- there is no review gate to clear.`;
954
1969
  return "No open review gate on this item. Every queued step has already been reviewed, or the current step is still running.";
955
- }, [earliestOpenGate, item?.pipelineId, pipeline]);
1970
+ }, [reviewTarget, item?.pipelineId, pipeline]);
956
1971
  const reviewActions = useMemo(
957
- () => buildReviewActions(isTerminalPipeline, earliestOpenGate, reviewDisabledReason),
958
- [isTerminalPipeline, earliestOpenGate, reviewDisabledReason]
1972
+ () => workspaceRoute ? [] : buildReviewActions(isTerminalPipeline, earliestOpenGate, reviewDisabledReason),
1973
+ [workspaceRoute, isTerminalPipeline, earliestOpenGate, reviewDisabledReason]
959
1974
  );
960
1975
  const pipelinePayloadFields = useMemo(() => getPipelinePayloadFields(pipelineSteps), [pipelineSteps]);
961
1976
  const primaryTextField = useMemo(() => getPrimaryPayloadTextField(pipelinePayloadFields), [pipelinePayloadFields]);
@@ -979,8 +1994,8 @@ function ContentItemReviewPage({ itemId, headerActions }) {
979
1994
  });
980
1995
  const reviewQueue = useMemo(() => {
981
1996
  if (!item?.pipelineId) return [];
982
- return (pipelineItemsQuery.data?.data ?? []).filter((candidate) => candidate.reviewedAt === null).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
983
- }, [pipelineItemsQuery.data, item?.pipelineId]);
1997
+ return (pipelineItemsQuery.data?.data ?? []).filter((candidate) => isAwaitingReview(candidate, resolveCurrentContentStep(candidate, pipelineSteps))).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
1998
+ }, [pipelineItemsQuery.data, item?.pipelineId, pipelineSteps]);
984
1999
  const queueIndex = item ? reviewQueue.findIndex((candidate) => candidate.id === item.id) : -1;
985
2000
  const queuePosition = queueIndex === -1 ? void 0 : queueIndex + 1;
986
2001
  const nextQueueItemId = queueIndex === -1 ? void 0 : reviewQueue[queueIndex + 1]?.id;
@@ -1099,7 +2114,7 @@ function ContentItemReviewPage({ itemId, headerActions }) {
1099
2114
  ] })
1100
2115
  ] }),
1101
2116
  /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
1102
- /* @__PURE__ */ jsx(
2117
+ workspaceRoute ? null : /* @__PURE__ */ jsx(
1103
2118
  Button,
1104
2119
  {
1105
2120
  size: "xs",
@@ -1124,6 +2139,27 @@ function ContentItemReviewPage({ itemId, headerActions }) {
1124
2139
  children: /* @__PURE__ */ jsx(Text, { size: "sm", children: reviewDisabledReason })
1125
2140
  }
1126
2141
  ),
2142
+ workspaceRoute && /* @__PURE__ */ jsx(
2143
+ Alert,
2144
+ {
2145
+ icon: /* @__PURE__ */ jsx(IconExternalLink, { size: 16 }),
2146
+ variant: "light",
2147
+ title: "Reviewed in your workspace",
2148
+ "data-testid": "content-review-workspace-pointer",
2149
+ children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", align: "flex-start", children: [
2150
+ /* @__PURE__ */ jsx(Text, { size: "sm", children: "This pipeline clears its gates on your own screen. Everything below is the piece as it stands." }),
2151
+ /* @__PURE__ */ jsx(
2152
+ Button,
2153
+ {
2154
+ size: "xs",
2155
+ rightSection: /* @__PURE__ */ jsx(IconExternalLink, { size: 14 }),
2156
+ onClick: () => void navigate({ to: workspaceRoute }),
2157
+ children: "Open workspace"
2158
+ }
2159
+ )
2160
+ ] })
2161
+ }
2162
+ ),
1127
2163
  /* @__PURE__ */ jsx(
1128
2164
  ContentReviewCard,
1129
2165
  {
@@ -1137,7 +2173,8 @@ function ContentItemReviewPage({ itemId, headerActions }) {
1137
2173
  processingSteps: visibleProcessingSteps,
1138
2174
  actions: reviewActions,
1139
2175
  onAction: handleAction,
1140
- actionsPending: isSaving
2176
+ actionsPending: isSaving,
2177
+ readOnly: workspaceRoute !== void 0
1141
2178
  }
1142
2179
  ),
1143
2180
  item.reviewerFeedback && /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
@@ -1148,6 +2185,9 @@ function ContentItemReviewPage({ itemId, headerActions }) {
1148
2185
  /* @__PURE__ */ jsx(Text, { size: "xs", c: "var(--color-error)", fw: 600, children: "Reject reason" }),
1149
2186
  /* @__PURE__ */ jsx(Text, { size: "sm", children: item.rejectReason })
1150
2187
  ] }) }),
2188
+ /* @__PURE__ */ jsx(SourceAssetsPanel, { itemId }),
2189
+ /* @__PURE__ */ jsx(SlideAnalysisPanel, { itemId }),
2190
+ /* @__PURE__ */ jsx(CaptionPanel, { itemId, body: item.body }),
1151
2191
  /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
1152
2192
  /* @__PURE__ */ jsx(Title, { order: 4, children: "Attempt History" }),
1153
2193
  attempts.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No recorded attempts." }) : /* @__PURE__ */ jsxs(Table, { children: [
@@ -1278,13 +2318,13 @@ var ACTOR_LEAD = {
1278
2318
  workflow: "An automated workflow",
1279
2319
  agent: "An AI agent"
1280
2320
  };
1281
- var REVIEW_MODE_TAIL = {
1282
- live: "and you review it live, in the moment",
1283
- queued: "then it waits for your review",
2321
+ var REVIEW_VENUE_TAIL = {
2322
+ workspace: "and you review it on your own screen, in the moment",
2323
+ queue: "then it waits for your review",
1284
2324
  none: "and moves on without review"
1285
2325
  };
1286
2326
  function describeStep(step) {
1287
- return `${ACTOR_LEAD[step.actor]} handles "${step.label}," ${REVIEW_MODE_TAIL[step.reviewMode]}.`;
2327
+ return `${ACTOR_LEAD[step.actor]} handles "${step.label}," ${REVIEW_VENUE_TAIL[step.reviewVenue]}.`;
1288
2328
  }
1289
2329
  function StepRow({ step, resourceId }) {
1290
2330
  return /* @__PURE__ */ jsxs(Group, { gap: "sm", align: "flex-start", wrap: "nowrap", children: [
@@ -1318,7 +2358,7 @@ function formatAge(ms) {
1318
2358
  return `${Math.floor(hours / 24)}d`;
1319
2359
  }
1320
2360
  function BoardItemCard({ card, onOpen }) {
1321
- return /* @__PURE__ */ jsx(Card, { withBorder: true, padding: "xs", onClick: () => onOpen(card.item.id), style: { cursor: "pointer" }, children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
2361
+ return /* @__PURE__ */ jsx(Card, { withBorder: true, padding: "xs", onClick: () => onOpen(card), style: { cursor: "pointer" }, children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
1322
2362
  /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, lineClamp: 2, children: getContentItemIdentity(card.item) }),
1323
2363
  /* @__PURE__ */ jsxs(Group, { gap: 6, justify: "space-between", wrap: "nowrap", children: [
1324
2364
  /* @__PURE__ */ jsxs(Group, { gap: 4, wrap: "nowrap", children: [
@@ -1398,7 +2438,15 @@ function BoardTab({ pipeline }) {
1398
2438
  const itemsQuery = useContentItems({ pipelineId: pipeline.id, limit: BOARD_FETCH_LIMIT });
1399
2439
  const items = useMemo(() => itemsQuery.data?.data ?? [], [itemsQuery.data]);
1400
2440
  const board = useMemo(() => deriveContentBoard(items, pipeline, Date.now()), [items, pipeline]);
1401
- const openItem = (itemId) => navigate({ to: "/content/items/$itemId", params: { itemId } });
2441
+ const openItem = (card) => {
2442
+ const heldAtStep = pipeline.steps.find((step) => step.key === card.stepKey);
2443
+ const workspaceRoute = card.awaitingReview ? resolveContentWorkspaceRoute(pipeline, heldAtStep) : void 0;
2444
+ if (workspaceRoute) {
2445
+ void navigate({ to: workspaceRoute });
2446
+ return;
2447
+ }
2448
+ void navigate({ to: "/content/items/$itemId", params: { itemId: card.item.id } });
2449
+ };
1402
2450
  if (itemsQuery.isLoading) {
1403
2451
  return /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) });
1404
2452
  }
@@ -1613,21 +2661,21 @@ function RunsTab({ pipeline }) {
1613
2661
  const contentConfig = useContentConfig();
1614
2662
  return /* @__PURE__ */ jsx(Stack, { gap: "md", children: pipeline.steps.map((step) => /* @__PURE__ */ jsx(StepRunsSection, { step, resourceId: contentConfig.resolveStepResource(step)?.resourceId }, step.key)) });
1615
2663
  }
1616
- var PIPELINE_WORKSPACE_TABS = ["board", "distributions", "runs"];
1617
- function isPipelineWorkspaceTab(value) {
1618
- return typeof value === "string" && PIPELINE_WORKSPACE_TABS.includes(value);
2664
+ var PIPELINE_BOARD_TABS = ["board", "distributions", "runs"];
2665
+ function isPipelineBoardTab(value) {
2666
+ return typeof value === "string" && PIPELINE_BOARD_TABS.includes(value);
1619
2667
  }
1620
- function ContentPipelineWorkspacePage({
2668
+ function ContentPipelineBoardPage({
1621
2669
  pipelineId,
1622
2670
  headerActions
1623
2671
  }) {
1624
2672
  const navigate = useNavigate();
1625
2673
  const contentConfig = useContentConfig();
1626
2674
  const rawSearch = useSearch({ strict: false });
1627
- const activeTab = isPipelineWorkspaceTab(rawSearch.tab) ? rawSearch.tab : "board";
2675
+ const activeTab = isPipelineBoardTab(rawSearch.tab) ? rawSearch.tab : "board";
1628
2676
  const handleTabChange = useCallback(
1629
2677
  (value) => {
1630
- if (!isPipelineWorkspaceTab(value) || value === activeTab) return;
2678
+ if (!isPipelineBoardTab(value) || value === activeTab) return;
1631
2679
  void navigate({
1632
2680
  to: ".",
1633
2681
  search: ((current) => ({ ...current, tab: value })),
@@ -1848,6 +2896,33 @@ function ContentDistributionsPage({ headerActions } = {}) {
1848
2896
  ] }) })
1849
2897
  ] }) }) });
1850
2898
  }
2899
+ var COMMON_PLATFORM_FIELDS = [
2900
+ { key: "title", label: "Title" },
2901
+ { key: "description", label: "Description" },
2902
+ { key: "tags", label: "Tags" }
2903
+ ];
2904
+ var PLATFORM_SPECIFIC_FIELDS = {
2905
+ youtube: [
2906
+ { key: "timestamps", label: "Timestamps" },
2907
+ { key: "endScreenVideoId", label: "End Screen Video ID" },
2908
+ { key: "pinnedComment", label: "Pinned Comment" }
2909
+ ],
2910
+ linkedin: [
2911
+ { key: "hook", label: "Hook" },
2912
+ { key: "hashtags", label: "Hashtags" },
2913
+ { key: "firstComment", label: "First Comment" }
2914
+ ],
2915
+ instagram: [
2916
+ { key: "altText", label: "Alt Text" },
2917
+ { key: "hashtags", label: "Hashtags" },
2918
+ { key: "storyLink", label: "Story Link" }
2919
+ ],
2920
+ x: [
2921
+ { key: "hookTweet", label: "Hook Tweet" },
2922
+ { key: "threadTweets", label: "Thread Tweets" },
2923
+ { key: "replyThread", label: "Reply Thread" }
2924
+ ]
2925
+ };
1851
2926
  function formatDateTime3(value) {
1852
2927
  return value ? new Date(value).toLocaleString() : "\u2014";
1853
2928
  }
@@ -1862,6 +2937,17 @@ function getChecklistProgress(checklist) {
1862
2937
  const done = checklist.filter((item) => item.done).length;
1863
2938
  return { done, total: checklist.length, pct: Math.round(done / checklist.length * 100) };
1864
2939
  }
2940
+ function hasPlatformFieldValue(value) {
2941
+ if (Array.isArray(value)) return value.length > 0;
2942
+ return typeof value === "string" && value.trim() !== "";
2943
+ }
2944
+ function PlatformContentField({ label, value }) {
2945
+ if (!hasPlatformFieldValue(value)) return null;
2946
+ return /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
2947
+ /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: label }),
2948
+ Array.isArray(value) ? /* @__PURE__ */ jsx(Group, { gap: 4, children: value.map((entry, index) => /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: String(entry) }, `${String(entry)}-${index}`)) }) : /* @__PURE__ */ jsx(Text, { size: "sm", style: { whiteSpace: "pre-wrap" }, children: String(value) })
2949
+ ] });
2950
+ }
1865
2951
  function MetricsDisplay({ metrics }) {
1866
2952
  if (!metrics || typeof metrics !== "object" || Object.keys(metrics).length === 0) {
1867
2953
  return /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No metrics recorded yet." });
@@ -1885,17 +2971,14 @@ function ContentDistributionDetailPage({
1885
2971
  const distributionQuery = useContentDistribution(distributionId);
1886
2972
  const dist = distributionQuery.data;
1887
2973
  const itemQuery = useContentItem(dist?.contentItemId);
1888
- const updateDistribution = useUpdateContentDistribution();
1889
- const [platformContent, setPlatformContent] = useState({});
1890
- const [platformUrl, setPlatformUrl] = useState("");
1891
- const [status, setStatus] = useState("");
1892
2974
  const [mediaOpened, { open: openMedia, close: closeMedia }] = useDisclosure(false);
1893
2975
  const [previewUrl, setPreviewUrl] = useState(null);
1894
- useEffect(() => {
1895
- setPlatformUrl(dist?.platformUrl ?? "");
1896
- setStatus(dist?.status ?? "");
1897
- setPlatformContent(dist?.platformContent ?? {});
1898
- }, [dist?.id]);
2976
+ const mediaEntries = useMemo(() => dist?.mediaUrls ?? [], [dist]);
2977
+ const mediaRefs = useMemo(
2978
+ () => mediaEntries.filter((entry) => entry.kind === "storage").map(({ bucket, path }) => ({ bucket, path })),
2979
+ [mediaEntries]
2980
+ );
2981
+ const { urls: signedMediaUrls } = useStorageSignedUrls(mediaRefs);
1899
2982
  const backButton = /* @__PURE__ */ jsx(
1900
2983
  Button,
1901
2984
  {
@@ -1922,24 +3005,15 @@ function ContentDistributionDetailPage({
1922
3005
  const contentTitle = contentItem?.title ?? dist.contentItemId;
1923
3006
  const checklist = toChecklist(dist.checklist);
1924
3007
  const checklistProgress = getChecklistProgress(checklist);
1925
- const mediaUrls = dist.mediaUrls ?? [];
3008
+ const mediaItems = mediaEntries.map(
3009
+ (entry) => entry.kind === "storage" ? { key: storageObjectRefKey(entry), url: signedMediaUrls[storageObjectRefKey(entry)] ?? null } : { key: entry.url, url: entry.url }
3010
+ );
1926
3011
  const platformEntry = contentConfig.stageCatalog[dist.platform];
1927
3012
  const platformColors = getContentStageColor(platformEntry);
1928
3013
  const pillarEntry = contentItem?.pillar ? contentConfig.stageCatalog[contentItem.pillar] : void 0;
1929
- const isPlatformContentDirty = JSON.stringify(platformContent) !== JSON.stringify(dist.platformContent ?? {});
1930
- const patch = (updates) => {
1931
- updateDistribution.mutate({ distributionId, updates });
1932
- };
1933
- const updatePlatformField = (field, value) => {
1934
- setPlatformContent((prev) => ({ ...prev, [field]: value }));
1935
- };
1936
- const toggleChecklistItem = (key) => {
1937
- const nextChecklist = checklist.map((item) => item.key === key ? { ...item, done: !item.done } : item);
1938
- patch({ checklist: nextChecklist });
1939
- };
1940
- const deleteMedia = (url) => {
1941
- patch({ mediaUrls: mediaUrls.filter((existing) => existing !== url) });
1942
- };
3014
+ const platformContent = dist.platformContent ?? {};
3015
+ const platformFields = [...COMMON_PLATFORM_FIELDS, ...PLATFORM_SPECIFIC_FIELDS[dist.platform] ?? []];
3016
+ const hasPlatformContent = platformFields.some((field) => hasPlatformFieldValue(platformContent[field.key]));
1943
3017
  return /* @__PURE__ */ jsxs(SubshellContentContainer, { children: [
1944
3018
  /* @__PURE__ */ jsxs(PageContainer, { children: [
1945
3019
  /* @__PURE__ */ jsxs(Stack, { children: [
@@ -1997,30 +3071,9 @@ function ContentDistributionDetailPage({
1997
3071
  /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", w: 140, children: "Format" }),
1998
3072
  /* @__PURE__ */ jsx(Text, { size: "sm", children: dist.format })
1999
3073
  ] }),
2000
- /* @__PURE__ */ jsxs(Group, { wrap: "nowrap", children: [
3074
+ /* @__PURE__ */ jsxs(Group, { children: [
2001
3075
  /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", w: 140, children: "Status" }),
2002
- /* @__PURE__ */ jsx(
2003
- TextInput,
2004
- {
2005
- size: "xs",
2006
- value: status,
2007
- onChange: (event) => setStatus(event.currentTarget.value),
2008
- style: { flex: 1 },
2009
- onBlur: () => {
2010
- const value = status.trim();
2011
- if (value && value !== dist.status) {
2012
- const updates = { status: value };
2013
- if (value === "published" && !dist.publishedAt) {
2014
- updates.publishedAt = (/* @__PURE__ */ new Date()).toISOString();
2015
- }
2016
- patch(updates);
2017
- }
2018
- },
2019
- onKeyDown: (event) => {
2020
- if (event.key === "Enter") event.currentTarget.blur();
2021
- }
2022
- }
2023
- )
3076
+ /* @__PURE__ */ jsx(Text, { size: "sm", children: dist.status })
2024
3077
  ] }),
2025
3078
  /* @__PURE__ */ jsxs(Group, { children: [
2026
3079
  /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", w: 140, children: "Publish Method" }),
@@ -2044,26 +3097,20 @@ function ContentDistributionDetailPage({
2044
3097
  ] }),
2045
3098
  /* @__PURE__ */ jsxs(Group, { wrap: "nowrap", children: [
2046
3099
  /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", w: 140, children: "URL" }),
2047
- /* @__PURE__ */ jsx(
2048
- TextInput,
3100
+ dist.platformUrl ? /* @__PURE__ */ jsx(
3101
+ Anchor,
2049
3102
  {
2050
- size: "xs",
2051
- placeholder: "https://...",
2052
- value: platformUrl,
2053
- onChange: (event) => setPlatformUrl(event.currentTarget.value),
2054
- style: { flex: 1 },
2055
- onBlur: () => {
2056
- const value = platformUrl.trim();
2057
- if (value !== (dist.platformUrl ?? "")) {
2058
- patch({ platformUrl: value || null });
2059
- }
2060
- },
2061
- onKeyDown: (event) => {
2062
- if (event.key === "Enter") event.currentTarget.blur();
2063
- },
2064
- rightSection: dist.platformUrl ? /* @__PURE__ */ jsx(Anchor, { href: dist.platformUrl, target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ jsx(IconExternalLink, { size: 14 }) }) : void 0
3103
+ href: dist.platformUrl,
3104
+ target: "_blank",
3105
+ rel: "noopener noreferrer",
3106
+ size: "sm",
3107
+ style: { overflowWrap: "anywhere" },
3108
+ children: /* @__PURE__ */ jsxs(Group, { gap: 4, wrap: "nowrap", children: [
3109
+ dist.platformUrl,
3110
+ /* @__PURE__ */ jsx(IconExternalLink, { size: 14 })
3111
+ ] })
2065
3112
  }
2066
- )
3113
+ ) : /* @__PURE__ */ jsx(Text, { size: "sm", children: "\u2014" })
2067
3114
  ] })
2068
3115
  ] })
2069
3116
  ] }) }),
@@ -2094,174 +3141,15 @@ function ContentDistributionDetailPage({
2094
3141
  ] })
2095
3142
  ] }),
2096
3143
  /* @__PURE__ */ jsx(Divider, {}),
2097
- /* @__PURE__ */ jsx(SimpleGrid, { cols: { base: 1, sm: 2 }, spacing: "sm", verticalSpacing: "sm", children: checklist.map((item) => /* @__PURE__ */ jsx(
2098
- Checkbox,
2099
- {
2100
- label: item.label,
2101
- checked: item.done,
2102
- onChange: () => toggleChecklistItem(item.key),
2103
- disabled: updateDistribution.isPending,
2104
- size: "sm"
2105
- },
2106
- item.key
2107
- )) })
3144
+ /* @__PURE__ */ jsx(SimpleGrid, { cols: { base: 1, sm: 2 }, spacing: "sm", verticalSpacing: "sm", children: checklist.map((item) => /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", align: "flex-start", children: [
3145
+ item.done ? /* @__PURE__ */ jsx(IconCircleCheck, { size: 16, color: "var(--color-success)" }) : /* @__PURE__ */ jsx(IconCircleDashed, { size: 16, color: "var(--mantine-color-dimmed)" }),
3146
+ /* @__PURE__ */ jsx(Text, { size: "sm", c: item.done ? "dimmed" : void 0, children: item.label })
3147
+ ] }, item.key)) })
2108
3148
  ] }) }) : null,
2109
3149
  /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
2110
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
2111
- /* @__PURE__ */ jsx(Title, { order: 3, children: "Platform Content" }),
2112
- /* @__PURE__ */ jsx(
2113
- Button,
2114
- {
2115
- size: "xs",
2116
- disabled: !isPlatformContentDirty,
2117
- loading: updateDistribution.isPending,
2118
- onClick: () => patch({ platformContent }),
2119
- children: "Save"
2120
- }
2121
- )
2122
- ] }),
3150
+ /* @__PURE__ */ jsx(Title, { order: 3, children: "Platform Content" }),
2123
3151
  /* @__PURE__ */ jsx(Divider, {}),
2124
- /* @__PURE__ */ jsx(
2125
- TextInput,
2126
- {
2127
- label: "Title",
2128
- value: platformContent.title ?? "",
2129
- onChange: (event) => updatePlatformField("title", event.currentTarget.value)
2130
- }
2131
- ),
2132
- /* @__PURE__ */ jsx(
2133
- Textarea,
2134
- {
2135
- label: "Description",
2136
- autosize: true,
2137
- minRows: 2,
2138
- value: platformContent.description ?? "",
2139
- onChange: (event) => updatePlatformField("description", event.currentTarget.value)
2140
- }
2141
- ),
2142
- /* @__PURE__ */ jsx(
2143
- TagsInput,
2144
- {
2145
- label: "Tags",
2146
- value: platformContent.tags ?? [],
2147
- onChange: (value) => updatePlatformField("tags", value)
2148
- }
2149
- ),
2150
- dist.platform === "youtube" && /* @__PURE__ */ jsxs(Fragment, { children: [
2151
- /* @__PURE__ */ jsx(
2152
- TagsInput,
2153
- {
2154
- label: "Timestamps",
2155
- placeholder: "0:00 Intro",
2156
- value: platformContent.timestamps ?? [],
2157
- onChange: (value) => updatePlatformField("timestamps", value)
2158
- }
2159
- ),
2160
- /* @__PURE__ */ jsx(
2161
- TextInput,
2162
- {
2163
- label: "End Screen Video ID",
2164
- value: platformContent.endScreenVideoId ?? "",
2165
- onChange: (event) => updatePlatformField("endScreenVideoId", event.currentTarget.value)
2166
- }
2167
- ),
2168
- /* @__PURE__ */ jsx(
2169
- Textarea,
2170
- {
2171
- label: "Pinned Comment",
2172
- autosize: true,
2173
- minRows: 2,
2174
- value: platformContent.pinnedComment ?? "",
2175
- onChange: (event) => updatePlatformField("pinnedComment", event.currentTarget.value)
2176
- }
2177
- )
2178
- ] }),
2179
- dist.platform === "linkedin" && /* @__PURE__ */ jsxs(Fragment, { children: [
2180
- /* @__PURE__ */ jsx(
2181
- Textarea,
2182
- {
2183
- label: "Hook",
2184
- autosize: true,
2185
- minRows: 2,
2186
- value: platformContent.hook ?? "",
2187
- onChange: (event) => updatePlatformField("hook", event.currentTarget.value)
2188
- }
2189
- ),
2190
- /* @__PURE__ */ jsx(
2191
- TagsInput,
2192
- {
2193
- label: "Hashtags",
2194
- value: platformContent.hashtags ?? [],
2195
- onChange: (value) => updatePlatformField("hashtags", value)
2196
- }
2197
- ),
2198
- /* @__PURE__ */ jsx(
2199
- Textarea,
2200
- {
2201
- label: "First Comment",
2202
- autosize: true,
2203
- minRows: 2,
2204
- value: platformContent.firstComment ?? "",
2205
- onChange: (event) => updatePlatformField("firstComment", event.currentTarget.value)
2206
- }
2207
- )
2208
- ] }),
2209
- dist.platform === "instagram" && /* @__PURE__ */ jsxs(Fragment, { children: [
2210
- /* @__PURE__ */ jsx(
2211
- Textarea,
2212
- {
2213
- label: "Alt Text",
2214
- autosize: true,
2215
- minRows: 2,
2216
- value: platformContent.altText ?? "",
2217
- onChange: (event) => updatePlatformField("altText", event.currentTarget.value)
2218
- }
2219
- ),
2220
- /* @__PURE__ */ jsx(
2221
- TagsInput,
2222
- {
2223
- label: "Hashtags",
2224
- value: platformContent.hashtags ?? [],
2225
- onChange: (value) => updatePlatformField("hashtags", value)
2226
- }
2227
- ),
2228
- /* @__PURE__ */ jsx(
2229
- TextInput,
2230
- {
2231
- label: "Story Link",
2232
- value: platformContent.storyLink ?? "",
2233
- onChange: (event) => updatePlatformField("storyLink", event.currentTarget.value)
2234
- }
2235
- )
2236
- ] }),
2237
- dist.platform === "x" && /* @__PURE__ */ jsxs(Fragment, { children: [
2238
- /* @__PURE__ */ jsx(
2239
- Textarea,
2240
- {
2241
- label: "Hook Tweet",
2242
- autosize: true,
2243
- minRows: 2,
2244
- value: platformContent.hookTweet ?? "",
2245
- onChange: (event) => updatePlatformField("hookTweet", event.currentTarget.value)
2246
- }
2247
- ),
2248
- /* @__PURE__ */ jsx(
2249
- TagsInput,
2250
- {
2251
- label: "Thread Tweets",
2252
- value: platformContent.threadTweets ?? [],
2253
- onChange: (value) => updatePlatformField("threadTweets", value)
2254
- }
2255
- ),
2256
- /* @__PURE__ */ jsx(
2257
- TextInput,
2258
- {
2259
- label: "Reply Thread",
2260
- value: platformContent.replyThread ?? "",
2261
- onChange: (event) => updatePlatformField("replyThread", event.currentTarget.value)
2262
- }
2263
- )
2264
- ] })
3152
+ hasPlatformContent ? platformFields.map((field) => /* @__PURE__ */ jsx(PlatformContentField, { label: field.label, value: platformContent[field.key] }, field.key)) : /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No platform content recorded yet." })
2265
3153
  ] }) }),
2266
3154
  dist.adaptedBody ? /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
2267
3155
  /* @__PURE__ */ jsx(Title, { order: 3, children: "Adapted Content" }),
@@ -2295,24 +3183,24 @@ function ContentDistributionDetailPage({
2295
3183
  /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
2296
3184
  /* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
2297
3185
  /* @__PURE__ */ jsx(Title, { order: 3, children: "Media" }),
2298
- mediaUrls.length > 0 ? /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
2299
- mediaUrls.length,
3186
+ mediaItems.length > 0 ? /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3187
+ mediaItems.length,
2300
3188
  " image",
2301
- mediaUrls.length !== 1 ? "s" : ""
3189
+ mediaItems.length !== 1 ? "s" : ""
2302
3190
  ] }) : null
2303
3191
  ] }),
2304
3192
  /* @__PURE__ */ jsx(Divider, {}),
2305
- mediaUrls.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No media yet." }) : /* @__PURE__ */ jsx(SimpleGrid, { cols: { base: 1, xs: 2, sm: 3 }, spacing: "sm", children: mediaUrls.map((url, index) => /* @__PURE__ */ jsxs(Card, { withBorder: true, radius: "md", style: { overflow: "hidden" }, children: [
2306
- /* @__PURE__ */ jsx(
2307
- Image,
3193
+ mediaItems.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No media yet." }) : /* @__PURE__ */ jsx(SimpleGrid, { cols: { base: 1, xs: 2, sm: 3 }, spacing: "sm", children: mediaItems.map((item, index) => /* @__PURE__ */ jsxs(Card, { withBorder: true, radius: "md", style: { overflow: "hidden" }, children: [
3194
+ item.url === null ? /* @__PURE__ */ jsx(Center, { h: 120, children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : /* @__PURE__ */ jsx(
3195
+ Image$1,
2308
3196
  {
2309
- src: url,
3197
+ src: item.url,
2310
3198
  alt: `Media variant ${index + 1}`,
2311
3199
  w: "100%",
2312
3200
  radius: "sm",
2313
3201
  style: { cursor: "pointer" },
2314
3202
  onClick: () => {
2315
- setPreviewUrl(url);
3203
+ setPreviewUrl(item.url);
2316
3204
  openMedia();
2317
3205
  }
2318
3206
  }
@@ -2323,32 +3211,45 @@ function ContentDistributionDetailPage({
2323
3211
  {
2324
3212
  variant: "subtle",
2325
3213
  size: "sm",
3214
+ disabled: item.url === null,
2326
3215
  onClick: () => {
2327
- setPreviewUrl(url);
3216
+ setPreviewUrl(item.url);
2328
3217
  openMedia();
2329
3218
  },
2330
3219
  children: /* @__PURE__ */ jsx(IconMaximize, { size: 16 })
2331
3220
  }
2332
3221
  ) }),
2333
- /* @__PURE__ */ jsx(Tooltip, { label: "Copy URL", children: /* @__PURE__ */ jsx(ActionIcon, { variant: "subtle", size: "sm", onClick: () => navigator.clipboard.writeText(url), children: /* @__PURE__ */ jsx(IconCopy, { size: 16 }) }) }),
2334
- /* @__PURE__ */ jsx(Tooltip, { label: "Download", children: /* @__PURE__ */ jsx(ActionIcon, { variant: "subtle", size: "sm", component: "a", href: url, download: true, target: "_blank", children: /* @__PURE__ */ jsx(IconDownload, { size: 16 }) }) }),
2335
- /* @__PURE__ */ jsx(Tooltip, { label: "Delete", children: /* @__PURE__ */ jsx(
3222
+ /* @__PURE__ */ jsx(Tooltip, { label: "Copy URL", children: /* @__PURE__ */ jsx(
3223
+ ActionIcon,
3224
+ {
3225
+ variant: "subtle",
3226
+ size: "sm",
3227
+ disabled: item.url === null,
3228
+ onClick: () => {
3229
+ if (item.url !== null) void navigator.clipboard.writeText(item.url);
3230
+ },
3231
+ children: /* @__PURE__ */ jsx(IconCopy, { size: 16 })
3232
+ }
3233
+ ) }),
3234
+ /* @__PURE__ */ jsx(Tooltip, { label: "Download", children: /* @__PURE__ */ jsx(
2336
3235
  ActionIcon,
2337
3236
  {
2338
3237
  variant: "subtle",
2339
- color: "var(--color-error)",
2340
3238
  size: "sm",
2341
- loading: updateDistribution.isPending,
2342
- onClick: () => deleteMedia(url),
2343
- children: /* @__PURE__ */ jsx(IconTrash, { size: 16 })
3239
+ disabled: item.url === null,
3240
+ component: "a",
3241
+ href: item.url ?? void 0,
3242
+ download: true,
3243
+ target: "_blank",
3244
+ children: /* @__PURE__ */ jsx(IconDownload, { size: 16 })
2344
3245
  }
2345
3246
  ) })
2346
3247
  ] })
2347
- ] }, url)) })
3248
+ ] }, item.key)) })
2348
3249
  ] }) })
2349
3250
  ] }),
2350
- /* @__PURE__ */ jsx(CustomModal, { opened: mediaOpened, onClose: closeMedia, size: "xl", children: previewUrl ? /* @__PURE__ */ jsx(Image, { src: previewUrl, alt: "Media preview", fit: "contain" }) : null })
3251
+ /* @__PURE__ */ jsx(CustomModal, { opened: mediaOpened, onClose: closeMedia, size: "xl", children: previewUrl ? /* @__PURE__ */ jsx(Image$1, { src: previewUrl, alt: "Media preview", fit: "contain" }) : null })
2351
3252
  ] });
2352
3253
  }
2353
3254
 
2354
- export { AlternatesPanel, ContentDistributionDetailPage, ContentDistributionsPage, ContentItemReviewPage, ContentItemsPage, ContentOverviewPage, ContentPipelineWorkspacePage, ContentPipelinesPage, ContentReviewCard, PayloadBody, ProcessingStateStrip, ReviewActionBar, firstLine, formatElapsed, lastLine, swapFirstLine, swapLastLine };
3255
+ export { AlternatesPanel, AssetCropEditor, CAPTION_STEP_KEY, CAPTION_WORKFLOW_ID, CONTENT_DERIVATIVES_BUCKET, CROP_ASPECT_OPTIONS, CaptionPanel, ContentDistributionDetailPage, ContentDistributionsPage, ContentItemReviewPage, ContentItemsPage, ContentOverviewPage, ContentPipelineBoardPage, ContentPipelineBoardPage as ContentPipelineWorkspacePage, ContentPipelinesPage, ContentReviewCard, DERIVATIVE_JPEG_QUALITY, DERIVATIVE_MAX_WIDTH, IMAGE_ANALYSIS_WORKFLOW_ID, PayloadBody, PostPreview, ProcessingStateStrip, ReviewActionBar, SelectedAssetStrip, SlideAnalysisPanel, SourceAssetsPanel, cropStyle, derivativeObjectKey, firstLine, formatElapsed, isDerivativeStale, lastLine, readStoredAnalysis, renderCropToJpeg, swapFirstLine, swapLastLine };