@elevasis/ui 2.72.0 → 2.74.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 (100) hide show
  1. package/dist/api/index.js +3 -3
  2. package/dist/app/index.d.ts +172 -20
  3. package/dist/app/index.js +9 -10
  4. package/dist/auth/index.js +5 -6
  5. package/dist/charts/index.js +5 -6
  6. package/dist/chunk-2GIJBHZT.js +60 -0
  7. package/dist/{chunk-2454MK7P.js → chunk-2MRRDF32.js} +115 -8
  8. package/dist/chunk-3LOKYZJC.js +231 -0
  9. package/dist/chunk-3QHX4ZKV.js +224 -0
  10. package/dist/chunk-43LC6XAX.js +69 -0
  11. package/dist/chunk-4M2BSHVT.js +116 -0
  12. package/dist/{chunk-SV4M2BUI.js → chunk-4MXNHJTA.js} +111 -46
  13. package/dist/{chunk-LEQGGX2U.js → chunk-5A6KCFS5.js} +36 -43
  14. package/dist/{chunk-KIDZXZM5.js → chunk-6JN2TLV2.js} +1480 -4200
  15. package/dist/{chunk-UZ5JO5TK.js → chunk-7ARBBELS.js} +1 -1
  16. package/dist/{chunk-DJWIIPKW.js → chunk-7RJ7P6S6.js} +6 -3
  17. package/dist/chunk-ARAFQBNI.js +515 -0
  18. package/dist/{chunk-DKVGO6RJ.js → chunk-BA3AZJUR.js} +1 -1
  19. package/dist/chunk-BRTGCFBH.js +305 -0
  20. package/dist/{chunk-7ESZKTQG.js → chunk-CQPULKKL.js} +5 -3
  21. package/dist/chunk-CRA7WQZH.js +49 -0
  22. package/dist/{chunk-XZIOCB5D.js → chunk-DCQKTDIG.js} +2 -52
  23. package/dist/{chunk-A3JHFZDP.js → chunk-E6YGA5WH.js} +1 -1
  24. package/dist/chunk-FA7LMNQO.js +439 -0
  25. package/dist/chunk-FZSG6M6W.js +45 -0
  26. package/dist/chunk-GDOR57D7.js +165 -0
  27. package/dist/{chunk-RRS2QR3W.js → chunk-GO4XJ5D4.js} +1 -1
  28. package/dist/chunk-GPQQGQGT.js +645 -0
  29. package/dist/chunk-HYQOGRWG.js +54 -0
  30. package/dist/{chunk-LCHL5SM2.js → chunk-LN4UW6S4.js} +1 -1
  31. package/dist/chunk-N6K2Q5BM.js +393 -0
  32. package/dist/{chunk-Z2W5VNN3.js → chunk-O32UQ26A.js} +3 -2
  33. package/dist/{chunk-M7WWRZ5Z.js → chunk-QFGNRKIE.js} +1 -78
  34. package/dist/{chunk-Q3O3WY7Q.js → chunk-R5UGIFYU.js} +74 -18
  35. package/dist/{chunk-753MKWFV.js → chunk-RFNJ2PTO.js} +1 -1
  36. package/dist/chunk-VM2OWOXL.js +24 -0
  37. package/dist/{chunk-ZBXCEWKF.js → chunk-W2QRBQQA.js} +5 -3
  38. package/dist/chunk-WDBNW44Z.js +49 -0
  39. package/dist/{chunk-IXBRUIYI.js → chunk-WZU3MCKI.js} +1 -1
  40. package/dist/chunk-YQQ5GRUR.js +21 -0
  41. package/dist/{chunk-IMZTRDY2.js → chunk-ZFA56VSR.js} +2 -2
  42. package/dist/{chunk-AFBFSXP3.js → chunk-ZWI62BQ6.js} +7 -4
  43. package/dist/chunk-ZYNVCWDZ.js +286 -0
  44. package/dist/components/index.d.ts +490 -22
  45. package/dist/components/index.js +335 -35
  46. package/dist/components/navigation/index.js +6 -7
  47. package/dist/features/auth/index.d.ts +154 -2
  48. package/dist/features/auth/index.js +18 -9
  49. package/dist/features/clients/index.js +7 -7
  50. package/dist/features/content/index.d.ts +718 -34
  51. package/dist/features/content/index.js +1345 -51
  52. package/dist/features/crm/index.d.ts +172 -20
  53. package/dist/features/crm/index.js +10 -8
  54. package/dist/features/dashboard/index.d.ts +19 -10
  55. package/dist/features/dashboard/index.js +9 -8
  56. package/dist/features/delivery/index.d.ts +134 -0
  57. package/dist/features/delivery/index.js +23 -7
  58. package/dist/features/knowledge/index.js +5 -6
  59. package/dist/features/lead-gen/index.d.ts +47 -35
  60. package/dist/features/lead-gen/index.js +12 -8
  61. package/dist/features/monitoring/index.js +11 -9
  62. package/dist/features/monitoring/requests/index.js +8 -7
  63. package/dist/features/operations/index.d.ts +38 -20
  64. package/dist/features/operations/index.js +15 -11
  65. package/dist/features/public-agent-chat/index.js +3 -1
  66. package/dist/features/settings/index.d.ts +134 -0
  67. package/dist/features/settings/index.js +9 -8
  68. package/dist/hooks/access/index.js +5 -6
  69. package/dist/hooks/delivery/index.d.ts +134 -0
  70. package/dist/hooks/delivery/index.js +3 -2
  71. package/dist/hooks/operations/command-view/utils/transformCommandViewData.d.ts +20 -11
  72. package/dist/hooks/published.d.ts +1421 -29
  73. package/dist/hooks/published.js +22 -6
  74. package/dist/initialization/index.d.ts +134 -0
  75. package/dist/knowledge/index.d.ts +38 -20
  76. package/dist/knowledge/index.js +167 -11
  77. package/dist/{knowledge-search-index-RZLIVWPJ.js → knowledge-search-index-6GQK55IQ.js} +1 -1
  78. package/dist/layout/index.js +8 -9
  79. package/dist/profile/index.d.ts +134 -0
  80. package/dist/provider/index.d.ts +191 -21
  81. package/dist/provider/index.js +5 -6
  82. package/dist/provider/published.d.ts +219 -21
  83. package/dist/provider/published.js +5 -6
  84. package/dist/test-utils/index.d.ts +16 -5
  85. package/dist/test-utils/index.js +17 -18
  86. package/dist/types/index.d.ts +184 -14
  87. package/dist/utils/index.d.ts +102 -13
  88. package/dist/utils/index.js +1 -1
  89. package/package.json +7 -6
  90. package/dist/chunk-6M6OLGQY.js +0 -36
  91. package/dist/chunk-MKH2KOAO.js +0 -39
  92. package/dist/hooks/index.css +0 -659
  93. package/dist/hooks/index.d.ts +0 -12853
  94. package/dist/hooks/index.js +0 -27
  95. package/dist/supabase/index.d.ts +0 -6969
  96. package/dist/supabase/index.js +0 -3
  97. package/dist/typeform/index.d.ts +0 -462
  98. package/dist/typeform/index.js +0 -2051
  99. package/dist/typeform/schemas.d.ts +0 -67
  100. package/dist/typeform/schemas.js +0 -2
@@ -1,18 +1,18 @@
1
+ import { useCloudStorageTemporaryLinks, useStorageSignedUrls, storageObjectRefKey } from '../../chunk-3QHX4ZKV.js';
1
2
  import { PageContainer } from '../../chunk-BZZCNLT6.js';
2
3
  import '../../chunk-TUMSNGTX.js';
3
4
  import { FilterBar } from '../../chunk-PDHTXPSF.js';
4
- import { CustomModal, useContentItems, useContentDistributions, useContentConfig, SubshellContentContainer, PageTitleCaption, CenteredErrorState, EmptyState, StatCard, getContentItemIdentity, usePaginationState, useContentItem, useContentItemAttempts, useUpdateContentItem, useReviewContentItem, getOpenContentReviewGates, resolveContentWorkspaceRoute, useContentDistribution, useContentPipelineSummary, deriveContentBoard, useExecutions } from '../../chunk-KIDZXZM5.js';
5
- export { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle, DEFAULT_CONTENT_VOCABULARY, MyReviewQueuePanel, contentManifest, deriveContentBoard, getContentItemIdentity, resolveContentStepResource, resolveContentVocabulary, useContentConfig } from '../../chunk-KIDZXZM5.js';
5
+ import { CustomModal, useContentItemSourceAssetDetails, useContentSourceAssets, useAddContentItemSourceAsset, useRemoveContentItemSourceAsset, useReorderContentItemSourceAssets, useUpdateContentItemSourceAsset, useContentItemAttempts, useExecuteAsync, useUpdateContentItem, useExecution, contentKeys, useContentItems, useContentDistributions, useContentConfig, isAwaitingReview, SubshellContentContainer, PageTitleCaption, CenteredErrorState, EmptyState, StatCard, getContentItemIdentity, usePaginationState, resolveCurrentContentStep, useContentItem, useReviewContentItem, resolveReviewTarget, useContentDistribution, useContentDistributionMetrics, useContentPipelineSummary, deriveContentBoard, resolveContentWorkspaceRoute, useExecutions } from '../../chunk-6JN2TLV2.js';
6
+ export { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle, DEFAULT_CONTENT_VOCABULARY, MyReviewQueuePanel, contentManifest, deriveContentBoard, getContentItemIdentity, resolveContentStepResource, resolveContentVocabulary, useContentConfig } from '../../chunk-6JN2TLV2.js';
6
7
  import '../../chunk-R6NMRXNO.js';
7
8
  import '../../chunk-RBGVNPA6.js';
8
- import '../../chunk-MKH2KOAO.js';
9
- import '../../chunk-Z2W5VNN3.js';
9
+ import '../../chunk-O32UQ26A.js';
10
10
  import '../../chunk-L7BZZ4SI.js';
11
- import { showSuccessNotification } from '../../chunk-XZIOCB5D.js';
11
+ import { showSuccessNotification } from '../../chunk-HYQOGRWG.js';
12
12
  import '../../chunk-MISHPEJX.js';
13
13
  import { CardHeader } from '../../chunk-62GVNH5U.js';
14
14
  import '../../chunk-P45GQ3ZW.js';
15
- import '../../chunk-M7WWRZ5Z.js';
15
+ import '../../chunk-2GIJBHZT.js';
16
16
  import '../../chunk-3KMDHCAR.js';
17
17
  import '../../chunk-2IFYDILW.js';
18
18
  import '../../chunk-BLXJEIQS.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-Q3O3WY7Q.js';
23
+ import { formatTimeAgo, formatRelativeTime } from '../../chunk-R5UGIFYU.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, Image, ActionIcon, UnstyledButton, ScrollArea } from '@mantine/core';
33
- import { IconAlertTriangle, IconShieldCheck, IconShieldX, IconArticle, IconArrowRight, IconClock, IconSend, IconSearch, IconFileText, IconExternalLink, IconListDetails, IconAlertCircle, IconLayoutGrid, IconShare, IconPlayerPlay, IconCircleCheck, IconCircleDashed, IconMaximize, IconCopy, IconDownload, 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, Stepper, Grid, 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 === "") {
@@ -536,6 +537,1007 @@ function ContentReviewCard({
536
537
  /* @__PURE__ */ jsx(ReviewActionBar, { actions, onAction, pending: actionsPending })
537
538
  ] }) });
538
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
+ }
539
1541
 
540
1542
  // src/lib/content/stage-colors.ts
541
1543
  var FALLBACK_CONTENT_STAGE_COLORS = {
@@ -555,6 +1557,10 @@ function getContentStageColor(entry) {
555
1557
  }
556
1558
  var OVERVIEW_ITEMS_LIMIT = 100;
557
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
+ }
558
1564
  function ContentOverviewPage({ headerActions, footer } = {}) {
559
1565
  const navigate = useNavigate();
560
1566
  const itemsQuery = useContentItems({ limit: OVERVIEW_ITEMS_LIMIT });
@@ -569,7 +1575,9 @@ function ContentOverviewPage({ headerActions, footer } = {}) {
569
1575
  }
570
1576
  return counts;
571
1577
  }, [items]);
572
- const pendingReviewCount = items.filter((item) => item.reviewedAt === null).length;
1578
+ const pendingReviewCount = items.filter(
1579
+ (item) => isAwaitingReview(item, resolveItemCurrentStep(item, contentConfig.pipelines))
1580
+ ).length;
573
1581
  const totalItems = itemsQuery.data?.total ?? items.length;
574
1582
  const totalDistributions = distributionsQuery.data?.data.length ?? 0;
575
1583
  const recentItems = useMemo(
@@ -711,10 +1719,9 @@ function ContentItemsPage({ headerActions } = {}) {
711
1719
  const pagination = usePaginationState(PAGE_SIZE, [searchQuery, statusFilter, pillarFilter, reviewFilterActive]);
712
1720
  const openItem = (item) => {
713
1721
  const pipeline = contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId);
714
- const openGate = getOpenContentReviewGates(item.processingState ?? {}, pipeline?.steps ?? [])[0];
715
- const workspaceRoute = openGate ? resolveContentWorkspaceRoute(pipeline, openGate.step) : void 0;
716
- if (workspaceRoute) {
717
- void navigate({ to: workspaceRoute });
1722
+ const target = resolveReviewTarget(item, pipeline, pipeline?.steps ?? []);
1723
+ if (target.kind === "workspace") {
1724
+ void navigate({ to: target.route });
718
1725
  return;
719
1726
  }
720
1727
  void navigate({ to: "/content/items/$itemId", params: { itemId: item.id } });
@@ -727,7 +1734,11 @@ function ContentItemsPage({ headerActions } = {}) {
727
1734
  offset: pagination.offset
728
1735
  });
729
1736
  const fetchedItems = itemsQuery.data?.data ?? [];
730
- 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;
731
1742
  const total = itemsQuery.data?.total ?? 0;
732
1743
  const statusOptions = [
733
1744
  { value: "", label: "All statuses" },
@@ -945,18 +1956,18 @@ function ContentItemReviewPage({ itemId, headerActions }) {
945
1956
  const pipeline = item ? contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId) : void 0;
946
1957
  const pipelineSteps = useMemo(() => pipeline?.steps ?? [], [pipeline]);
947
1958
  const isTerminalPipeline = pipelineSteps.length <= 1;
948
- const openGates = useMemo(
949
- () => getOpenContentReviewGates(item?.processingState ?? {}, pipelineSteps),
950
- [item?.processingState, pipelineSteps]
1959
+ const reviewTarget = useMemo(
1960
+ () => item ? resolveReviewTarget(item, pipeline, pipelineSteps) : { kind: "none" },
1961
+ [item, pipeline, pipelineSteps]
951
1962
  );
952
- const earliestOpenGate = openGates[0];
1963
+ const earliestOpenGate = reviewTarget.kind === "queued" ? reviewTarget.gate : void 0;
1964
+ const workspaceRoute = reviewTarget.kind === "workspace" ? reviewTarget.route : void 0;
953
1965
  const reviewDisabledReason = useMemo(() => {
954
- if (earliestOpenGate) return void 0;
1966
+ if (reviewTarget.kind !== "none") return void 0;
955
1967
  if (!item?.pipelineId) return "This item has no pipeline, so there is no review gate to clear.";
956
1968
  if (!pipeline) return `Unknown pipeline "${item.pipelineId}" -- there is no review gate to clear.`;
957
1969
  return "No open review gate on this item. Every queued step has already been reviewed, or the current step is still running.";
958
- }, [earliestOpenGate, item?.pipelineId, pipeline]);
959
- const workspaceRoute = resolveContentWorkspaceRoute(pipeline, earliestOpenGate?.step);
1970
+ }, [reviewTarget, item?.pipelineId, pipeline]);
960
1971
  const reviewActions = useMemo(
961
1972
  () => workspaceRoute ? [] : buildReviewActions(isTerminalPipeline, earliestOpenGate, reviewDisabledReason),
962
1973
  [workspaceRoute, isTerminalPipeline, earliestOpenGate, reviewDisabledReason]
@@ -983,8 +1994,8 @@ function ContentItemReviewPage({ itemId, headerActions }) {
983
1994
  });
984
1995
  const reviewQueue = useMemo(() => {
985
1996
  if (!item?.pipelineId) return [];
986
- return (pipelineItemsQuery.data?.data ?? []).filter((candidate) => candidate.reviewedAt === null).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
987
- }, [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]);
988
1999
  const queueIndex = item ? reviewQueue.findIndex((candidate) => candidate.id === item.id) : -1;
989
2000
  const queuePosition = queueIndex === -1 ? void 0 : queueIndex + 1;
990
2001
  const nextQueueItemId = queueIndex === -1 ? void 0 : reviewQueue[queueIndex + 1]?.id;
@@ -1128,7 +2139,7 @@ function ContentItemReviewPage({ itemId, headerActions }) {
1128
2139
  children: /* @__PURE__ */ jsx(Text, { size: "sm", children: reviewDisabledReason })
1129
2140
  }
1130
2141
  ),
1131
- workspaceRoute && earliestOpenGate && /* @__PURE__ */ jsx(
2142
+ workspaceRoute && /* @__PURE__ */ jsx(
1132
2143
  Alert,
1133
2144
  {
1134
2145
  icon: /* @__PURE__ */ jsx(IconExternalLink, { size: 16 }),
@@ -1174,6 +2185,9 @@ function ContentItemReviewPage({ itemId, headerActions }) {
1174
2185
  /* @__PURE__ */ jsx(Text, { size: "xs", c: "var(--color-error)", fw: 600, children: "Reject reason" }),
1175
2186
  /* @__PURE__ */ jsx(Text, { size: "sm", children: item.rejectReason })
1176
2187
  ] }) }),
2188
+ /* @__PURE__ */ jsx(SourceAssetsPanel, { itemId }),
2189
+ /* @__PURE__ */ jsx(SlideAnalysisPanel, { itemId }),
2190
+ /* @__PURE__ */ jsx(CaptionPanel, { itemId, body: item.body }),
1177
2191
  /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
1178
2192
  /* @__PURE__ */ jsx(Title, { order: 4, children: "Attempt History" }),
1179
2193
  attempts.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No recorded attempts." }) : /* @__PURE__ */ jsxs(Table, { children: [
@@ -1300,17 +2314,18 @@ function ContentPipelinesPage({ headerActions } = {}) {
1300
2314
  ] }) }) });
1301
2315
  }
1302
2316
  var ACTOR_LEAD = {
1303
- vendor: "An outside service",
1304
- workflow: "An automated workflow",
1305
- agent: "An AI agent"
2317
+ vendor: "An outside service handles",
2318
+ workflow: "An automated workflow handles",
2319
+ agent: "An AI agent handles",
2320
+ human: "You do"
1306
2321
  };
1307
- var REVIEW_MODE_TAIL = {
1308
- live: "and you review it live, in the moment",
1309
- queued: "then it waits for your review",
1310
- none: "and moves on without review"
2322
+ var REVIEW_VENUE_TAIL = {
2323
+ workspace: "and you review it on your own screen, in the moment",
2324
+ queue: "then it waits for your review",
2325
+ none: "with no review after it"
1311
2326
  };
1312
2327
  function describeStep(step) {
1313
- return `${ACTOR_LEAD[step.actor]} handles "${step.label}," ${REVIEW_MODE_TAIL[step.reviewMode]}.`;
2328
+ return `${ACTOR_LEAD[step.actor]} "${step.label}," ${REVIEW_VENUE_TAIL[step.reviewVenue]}.`;
1314
2329
  }
1315
2330
  function StepRow({ step, resourceId }) {
1316
2331
  return /* @__PURE__ */ jsxs(Group, { gap: "sm", align: "flex-start", wrap: "nowrap", children: [
@@ -1726,6 +2741,13 @@ var QUERY_LIMIT = 100;
1726
2741
  function formatDate(value) {
1727
2742
  return value ? new Date(value).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "\u2014";
1728
2743
  }
2744
+ function summarizeMetrics2(metrics) {
2745
+ if (!metrics || typeof metrics !== "object" || Array.isArray(metrics)) return "\u2014";
2746
+ const entries = Object.entries(metrics);
2747
+ if (entries.length === 0) return "\u2014";
2748
+ const shown = entries.slice(0, 2).map(([key, value]) => `${key} ${String(value)}`).join(" \xB7 ");
2749
+ return entries.length > 2 ? `${shown} +${entries.length - 2}` : shown;
2750
+ }
1729
2751
  function ContentDistributionsPage({ headerActions } = {}) {
1730
2752
  const navigate = useNavigate();
1731
2753
  const contentConfig = useContentConfig();
@@ -1821,7 +2843,10 @@ function ContentDistributionsPage({ headerActions } = {}) {
1821
2843
  /* @__PURE__ */ jsx(Table.Th, { children: "Platform" }),
1822
2844
  /* @__PURE__ */ jsx(Table.Th, { children: "Format" }),
1823
2845
  /* @__PURE__ */ jsx(Table.Th, { children: "Status" }),
1824
- /* @__PURE__ */ jsx(Table.Th, { children: "Publish Method" }),
2846
+ /* @__PURE__ */ jsx(Table.Th, { style: { whiteSpace: "nowrap" }, children: "Publish Method" }),
2847
+ /* @__PURE__ */ jsx(Table.Th, { style: { whiteSpace: "nowrap" }, children: "Platform post ID" }),
2848
+ /* @__PURE__ */ jsx(Table.Th, { children: "Metrics" }),
2849
+ /* @__PURE__ */ jsx(Table.Th, { style: { whiteSpace: "nowrap" }, children: "Metrics updated" }),
1825
2850
  /* @__PURE__ */ jsx(Table.Th, { children: "Published" }),
1826
2851
  /* @__PURE__ */ jsx(Table.Th, {})
1827
2852
  ] }) }),
@@ -1852,6 +2877,9 @@ function ContentDistributionsPage({ headerActions } = {}) {
1852
2877
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", children: distribution.format }) }),
1853
2878
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: distribution.status }) }),
1854
2879
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: distribution.publishMethod ?? "\u2014" }) }),
2880
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "xs", ff: "monospace", c: "dimmed", children: distribution.platformPostId ?? "\u2014" }) }),
2881
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: summarizeMetrics2(distribution.metrics) }) }),
2882
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDate(distribution.metricsUpdatedAt) }) }),
1855
2883
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDate(distribution.publishedAt) }) }),
1856
2884
  /* @__PURE__ */ jsx(Table.Td, { children: distribution.platformUrl && /* @__PURE__ */ jsx(
1857
2885
  Anchor,
@@ -1948,6 +2976,22 @@ function MetricsDisplay({ metrics }) {
1948
2976
  /* @__PURE__ */ jsx(Text, { size: "sm", children: String(value) })
1949
2977
  ] }, key)) });
1950
2978
  }
2979
+ var METRIC_HISTORY_COLUMNS = [
2980
+ { key: "views", label: "Views" },
2981
+ { key: "reach", label: "Reach" },
2982
+ { key: "likes", label: "Likes" },
2983
+ { key: "comments", label: "Comments" },
2984
+ { key: "shares", label: "Shares" },
2985
+ { key: "saves", label: "Saves" },
2986
+ { key: "engagements", label: "Engagements" },
2987
+ { key: "profile_visits", label: "Profile Visits" },
2988
+ { key: "profile_activity", label: "Profile Activity" },
2989
+ { key: "follows", label: "Follows" },
2990
+ { key: "clicks", label: "Clicks" }
2991
+ ];
2992
+ function formatMetricValue(value) {
2993
+ return value === void 0 ? "\u2014" : value.toLocaleString();
2994
+ }
1951
2995
  function ContentDistributionDetailPage({
1952
2996
  distributionId,
1953
2997
  headerActions
@@ -1957,8 +3001,16 @@ function ContentDistributionDetailPage({
1957
3001
  const distributionQuery = useContentDistribution(distributionId);
1958
3002
  const dist = distributionQuery.data;
1959
3003
  const itemQuery = useContentItem(dist?.contentItemId);
3004
+ const metricsHistoryQuery = useContentDistributionMetrics(distributionId);
3005
+ const metricsHistory = metricsHistoryQuery.data?.data ?? [];
1960
3006
  const [mediaOpened, { open: openMedia, close: closeMedia }] = useDisclosure(false);
1961
3007
  const [previewUrl, setPreviewUrl] = useState(null);
3008
+ const mediaEntries = useMemo(() => dist?.mediaUrls ?? [], [dist]);
3009
+ const mediaRefs = useMemo(
3010
+ () => mediaEntries.filter((entry) => entry.kind === "storage").map(({ bucket, path }) => ({ bucket, path })),
3011
+ [mediaEntries]
3012
+ );
3013
+ const { urls: signedMediaUrls } = useStorageSignedUrls(mediaRefs);
1962
3014
  const backButton = /* @__PURE__ */ jsx(
1963
3015
  Button,
1964
3016
  {
@@ -1985,7 +3037,9 @@ function ContentDistributionDetailPage({
1985
3037
  const contentTitle = contentItem?.title ?? dist.contentItemId;
1986
3038
  const checklist = toChecklist(dist.checklist);
1987
3039
  const checklistProgress = getChecklistProgress(checklist);
1988
- const mediaUrls = dist.mediaUrls ?? [];
3040
+ const mediaItems = mediaEntries.map(
3041
+ (entry) => entry.kind === "storage" ? { key: storageObjectRefKey(entry), url: signedMediaUrls[storageObjectRefKey(entry)] ?? null } : { key: entry.url, url: entry.url }
3042
+ );
1989
3043
  const platformEntry = contentConfig.stageCatalog[dist.platform];
1990
3044
  const platformColors = getContentStageColor(platformEntry);
1991
3045
  const pillarEntry = contentItem?.pillar ? contentConfig.stageCatalog[contentItem.pillar] : void 0;
@@ -2103,6 +3157,23 @@ function ContentDistributionDetailPage({
2103
3157
  /* @__PURE__ */ jsx(Divider, {}),
2104
3158
  /* @__PURE__ */ jsx(MetricsDisplay, { metrics: dist.metrics })
2105
3159
  ] }) }),
3160
+ /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
3161
+ /* @__PURE__ */ jsx(Title, { order: 3, children: "Metrics History" }),
3162
+ /* @__PURE__ */ jsx(Divider, {}),
3163
+ metricsHistoryQuery.isLoading ? /* @__PURE__ */ jsx(Center, { p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : metricsHistoryQuery.isError ? /* @__PURE__ */ jsx(CenteredErrorState, { error: metricsHistoryQuery.error, title: "Failed to load metrics history" }) : metricsHistory.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No metrics readings captured yet." }) : /* @__PURE__ */ jsx(Table.ScrollContainer, { minWidth: 900, children: /* @__PURE__ */ jsxs(Table, { children: [
3164
+ /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
3165
+ /* @__PURE__ */ jsx(Table.Th, { children: "Captured" }),
3166
+ /* @__PURE__ */ jsx(Table.Th, { children: "Source" }),
3167
+ METRIC_HISTORY_COLUMNS.map((column) => /* @__PURE__ */ jsx(Table.Th, { children: column.label }, column.key))
3168
+ ] }) }),
3169
+ /* @__PURE__ */ jsx(Table.Tbody, { children: metricsHistory.map((reading) => /* @__PURE__ */ jsxs(Table.Tr, { children: [
3170
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDateTime3(reading.capturedAt) }) }),
3171
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", children: reading.source }) }),
3172
+ METRIC_HISTORY_COLUMNS.map((column) => /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", children: formatMetricValue(reading.metrics[column.key]) }) }, column.key))
3173
+ ] }, reading.id)) })
3174
+ ] }) }),
3175
+ /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "An em dash means this platform does not report that metric -- it is never the same as zero." })
3176
+ ] }) }),
2106
3177
  checklist.length > 0 ? /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
2107
3178
  /* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
2108
3179
  /* @__PURE__ */ jsx(Title, { order: 3, children: "Checklist" }),
@@ -2161,24 +3232,24 @@ function ContentDistributionDetailPage({
2161
3232
  /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
2162
3233
  /* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
2163
3234
  /* @__PURE__ */ jsx(Title, { order: 3, children: "Media" }),
2164
- mediaUrls.length > 0 ? /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
2165
- mediaUrls.length,
3235
+ mediaItems.length > 0 ? /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3236
+ mediaItems.length,
2166
3237
  " image",
2167
- mediaUrls.length !== 1 ? "s" : ""
3238
+ mediaItems.length !== 1 ? "s" : ""
2168
3239
  ] }) : null
2169
3240
  ] }),
2170
3241
  /* @__PURE__ */ jsx(Divider, {}),
2171
- 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: [
2172
- /* @__PURE__ */ jsx(
2173
- Image,
3242
+ 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: [
3243
+ item.url === null ? /* @__PURE__ */ jsx(Center, { h: 120, children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : /* @__PURE__ */ jsx(
3244
+ Image$1,
2174
3245
  {
2175
- src: url,
3246
+ src: item.url,
2176
3247
  alt: `Media variant ${index + 1}`,
2177
3248
  w: "100%",
2178
3249
  radius: "sm",
2179
3250
  style: { cursor: "pointer" },
2180
3251
  onClick: () => {
2181
- setPreviewUrl(url);
3252
+ setPreviewUrl(item.url);
2182
3253
  openMedia();
2183
3254
  }
2184
3255
  }
@@ -2189,21 +3260,244 @@ function ContentDistributionDetailPage({
2189
3260
  {
2190
3261
  variant: "subtle",
2191
3262
  size: "sm",
3263
+ disabled: item.url === null,
2192
3264
  onClick: () => {
2193
- setPreviewUrl(url);
3265
+ setPreviewUrl(item.url);
2194
3266
  openMedia();
2195
3267
  },
2196
3268
  children: /* @__PURE__ */ jsx(IconMaximize, { size: 16 })
2197
3269
  }
2198
3270
  ) }),
2199
- /* @__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 }) }) }),
2200
- /* @__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 }) }) })
3271
+ /* @__PURE__ */ jsx(Tooltip, { label: "Copy URL", children: /* @__PURE__ */ jsx(
3272
+ ActionIcon,
3273
+ {
3274
+ variant: "subtle",
3275
+ size: "sm",
3276
+ disabled: item.url === null,
3277
+ onClick: () => {
3278
+ if (item.url !== null) void navigator.clipboard.writeText(item.url);
3279
+ },
3280
+ children: /* @__PURE__ */ jsx(IconCopy, { size: 16 })
3281
+ }
3282
+ ) }),
3283
+ /* @__PURE__ */ jsx(Tooltip, { label: "Download", children: /* @__PURE__ */ jsx(
3284
+ ActionIcon,
3285
+ {
3286
+ variant: "subtle",
3287
+ size: "sm",
3288
+ disabled: item.url === null,
3289
+ component: "a",
3290
+ href: item.url ?? void 0,
3291
+ download: true,
3292
+ target: "_blank",
3293
+ children: /* @__PURE__ */ jsx(IconDownload, { size: 16 })
3294
+ }
3295
+ ) })
2201
3296
  ] })
2202
- ] }, url)) })
3297
+ ] }, item.key)) })
2203
3298
  ] }) })
2204
3299
  ] }),
2205
- /* @__PURE__ */ jsx(CustomModal, { opened: mediaOpened, onClose: closeMedia, size: "xl", children: previewUrl ? /* @__PURE__ */ jsx(Image, { src: previewUrl, alt: "Media preview", fit: "contain" }) : null })
3300
+ /* @__PURE__ */ jsx(CustomModal, { opened: mediaOpened, onClose: closeMedia, size: "xl", children: previewUrl ? /* @__PURE__ */ jsx(Image$1, { src: previewUrl, alt: "Media preview", fit: "contain" }) : null })
3301
+ ] });
3302
+ }
3303
+ function ContentWorkspaceShell({
3304
+ pipelineId,
3305
+ panels,
3306
+ initialDraft,
3307
+ title,
3308
+ caption,
3309
+ headerRight,
3310
+ renderStart,
3311
+ renderPersistent,
3312
+ renderAside,
3313
+ canOpenStep,
3314
+ testId
3315
+ }) {
3316
+ const { pipelines, defaultPipelineId } = useContentConfig();
3317
+ const resolvedPipelineId = pipelineId ?? defaultPipelineId;
3318
+ const pipeline = useMemo(
3319
+ () => pipelines.find((entry) => entry.id === resolvedPipelineId),
3320
+ [pipelines, resolvedPipelineId]
3321
+ );
3322
+ const steps = useMemo(() => pipeline?.steps ?? [], [pipeline]);
3323
+ const [isStarted, setIsStarted] = useState(renderStart === void 0);
3324
+ const [activeStepKey, setActiveStepKey] = useState(null);
3325
+ const [furthestIndex, setFurthestIndex] = useState(0);
3326
+ const [adoptedItem, setAdoptedItem] = useState(null);
3327
+ const [bodyDraft, setBodyDraft] = useState(null);
3328
+ const [draft, setDraftState] = useState(initialDraft);
3329
+ const leaveGuardRef = useRef(null);
3330
+ const itemId = adoptedItem?.id;
3331
+ const liveItem = useContentItem(itemId);
3332
+ const distributions = useContentDistributions({ contentItemId: itemId });
3333
+ const item = useMemo(() => {
3334
+ const live = liveItem.data;
3335
+ return live !== void 0 && live.id === itemId ? live : adoptedItem;
3336
+ }, [liveItem.data, itemId, adoptedItem]);
3337
+ const distribution = useMemo(() => {
3338
+ if (itemId === void 0) return null;
3339
+ return distributions.data?.data.find((row) => row.contentItemId === itemId) ?? null;
3340
+ }, [distributions.data, itemId]);
3341
+ const stepIndex = useMemo(() => {
3342
+ const index = steps.findIndex((step2) => step2.key === activeStepKey);
3343
+ return index === -1 ? 0 : index;
3344
+ }, [steps, activeStepKey]);
3345
+ const step = steps[stepIndex];
3346
+ const setDraft = useCallback((update) => {
3347
+ setDraftState(
3348
+ (current) => typeof update === "function" ? update(current) : { ...current, ...update }
3349
+ );
3350
+ }, []);
3351
+ const setItem = useCallback((next) => {
3352
+ setAdoptedItem(next);
3353
+ setBodyDraft(null);
3354
+ }, []);
3355
+ const refetchDistributionsQuery = distributions.refetch;
3356
+ const refetchDistributions = useCallback(() => void refetchDistributionsQuery(), [refetchDistributionsQuery]);
3357
+ const registerLeaveGuard = useCallback((guard) => {
3358
+ leaveGuardRef.current = guard;
3359
+ }, []);
3360
+ const goToStep = useCallback(
3361
+ (stepKey) => {
3362
+ const nextIndex = steps.findIndex((entry) => entry.key === stepKey);
3363
+ const next = steps[nextIndex];
3364
+ if (next === void 0) return;
3365
+ const guard = leaveGuardRef.current;
3366
+ void (async () => {
3367
+ if (guard !== null && nextIndex > stepIndex) await guard(next);
3368
+ setActiveStepKey(next.key);
3369
+ setFurthestIndex((current) => Math.max(current, nextIndex));
3370
+ })();
3371
+ },
3372
+ [steps, stepIndex]
3373
+ );
3374
+ const goToNextStep = useCallback(() => {
3375
+ const next = steps[stepIndex + 1];
3376
+ if (next !== void 0) goToStep(next.key);
3377
+ }, [steps, stepIndex, goToStep]);
3378
+ const goToPreviousStep = useCallback(() => {
3379
+ const previous = steps[stepIndex - 1];
3380
+ if (previous !== void 0) goToStep(previous.key);
3381
+ }, [steps, stepIndex, goToStep]);
3382
+ const restart = useCallback(() => {
3383
+ setAdoptedItem(null);
3384
+ setBodyDraft(null);
3385
+ setDraftState(initialDraft);
3386
+ setActiveStepKey(null);
3387
+ setFurthestIndex(0);
3388
+ leaveGuardRef.current = null;
3389
+ setIsStarted(renderStart === void 0);
3390
+ }, [initialDraft, renderStart]);
3391
+ const state = useMemo(() => {
3392
+ if (pipeline === void 0 || step === void 0) return null;
3393
+ const base = {
3394
+ pipeline,
3395
+ steps,
3396
+ step,
3397
+ stepIndex,
3398
+ item,
3399
+ setItem,
3400
+ body: bodyDraft ?? item?.body ?? "",
3401
+ bodyDraft,
3402
+ setBodyDraft,
3403
+ distribution,
3404
+ refetchDistributions,
3405
+ draft,
3406
+ setDraft,
3407
+ goToStep,
3408
+ goToNextStep,
3409
+ goToPreviousStep,
3410
+ registerLeaveGuard,
3411
+ restart
3412
+ };
3413
+ const resolved = {
3414
+ ...base,
3415
+ canOpenStep: (candidate) => canOpenStep === void 0 ? steps.indexOf(candidate) <= furthestIndex : canOpenStep(candidate, resolved)
3416
+ };
3417
+ return resolved;
3418
+ }, [
3419
+ pipeline,
3420
+ steps,
3421
+ step,
3422
+ stepIndex,
3423
+ item,
3424
+ setItem,
3425
+ bodyDraft,
3426
+ distribution,
3427
+ refetchDistributions,
3428
+ draft,
3429
+ setDraft,
3430
+ goToStep,
3431
+ goToNextStep,
3432
+ goToPreviousStep,
3433
+ registerLeaveGuard,
3434
+ restart,
3435
+ canOpenStep,
3436
+ furthestIndex
3437
+ ]);
3438
+ const startControls = useMemo(
3439
+ () => ({
3440
+ onStartNew: () => setIsStarted(true),
3441
+ onResume: (resumed, resumeAtStepKey) => {
3442
+ setItem(resumed);
3443
+ setDraftState(initialDraft);
3444
+ setFurthestIndex(steps.length === 0 ? 0 : steps.length - 1);
3445
+ setActiveStepKey(resumeAtStepKey ?? steps[0]?.key ?? null);
3446
+ setIsStarted(true);
3447
+ }
3448
+ }),
3449
+ [setItem, initialDraft, steps]
3450
+ );
3451
+ const body = (() => {
3452
+ if (pipeline === void 0) {
3453
+ return /* @__PURE__ */ jsx(Alert, { color: "yellow", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), children: resolvedPipelineId === void 0 ? "The deployed Organization Model declares no content pipelines, so there is nothing to walk through here." : `The deployed Organization Model does not declare a "${resolvedPipelineId}" pipeline.` });
3454
+ }
3455
+ if (steps.length === 0 || state === null || step === void 0) {
3456
+ return /* @__PURE__ */ jsxs(Alert, { color: "yellow", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), children: [
3457
+ "The ",
3458
+ /* @__PURE__ */ jsx("strong", { children: pipeline.label }),
3459
+ " pipeline declares no steps, so there is nothing to render."
3460
+ ] });
3461
+ }
3462
+ const Panel = panels[step.key];
3463
+ const panelNode = Panel === void 0 ? /* @__PURE__ */ jsxs(Alert, { color: "yellow", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), children: [
3464
+ "No panel is registered for the ",
3465
+ /* @__PURE__ */ jsx("strong", { children: step.label }),
3466
+ " step (",
3467
+ /* @__PURE__ */ jsx("code", { children: step.key }),
3468
+ "). Add one to the workspace's panel map."
3469
+ ] }) : /* @__PURE__ */ jsx(Panel, { ...state });
3470
+ const aside = renderAside?.(state) ?? null;
3471
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3472
+ /* @__PURE__ */ jsx(Stepper, { active: stepIndex, onStepClick: (next) => goToStep(steps[next]?.key ?? step.key), size: "sm", children: steps.map((entry) => /* @__PURE__ */ jsx(Stepper.Step, { label: entry.label, allowStepSelect: state.canOpenStep(entry) }, entry.key)) }),
3473
+ step.description === void 0 || step.description === "" ? null : /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: step.description }),
3474
+ renderPersistent?.(state),
3475
+ aside === null ? panelNode : /* @__PURE__ */ jsxs(Grid, { gutter: "md", align: "flex-start", children: [
3476
+ /* @__PURE__ */ jsx(Grid.Col, { span: { base: 12, lg: 7 }, children: panelNode }),
3477
+ /* @__PURE__ */ jsx(Grid.Col, { span: { base: 12, lg: 5 }, children: /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: aside }) })
3478
+ ] })
3479
+ ] });
3480
+ })();
3481
+ return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { "data-testid": testId, children: [
3482
+ /* @__PURE__ */ jsx(PageTitleCaption, { title, caption, rightSection: headerRight }),
3483
+ !isStarted && renderStart !== void 0 ? renderStart(startControls) : body
3484
+ ] }) }) });
3485
+ }
3486
+ function ContentAnalysisStepPanel(state) {
3487
+ const { item, stepIndex, steps, goToNextStep, goToPreviousStep } = state;
3488
+ if (item === null) {
3489
+ return /* @__PURE__ */ jsx(Alert, { color: "yellow", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), children: "Nothing has been created yet, so there is nothing to analyse. Go back and finish the earlier steps first." });
3490
+ }
3491
+ return /* @__PURE__ */ jsxs(Stack, { children: [
3492
+ /* @__PURE__ */ jsx(SlideAnalysisPanel, { itemId: item.id }),
3493
+ /* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
3494
+ stepIndex > 0 ? /* @__PURE__ */ jsxs(Button, { variant: "default", leftSection: /* @__PURE__ */ jsx(IconArrowLeft, { size: 16 }), onClick: goToPreviousStep, children: [
3495
+ "Back to ",
3496
+ steps[stepIndex - 1]?.label
3497
+ ] }) : /* @__PURE__ */ jsx("span", {}),
3498
+ stepIndex < steps.length - 1 && /* @__PURE__ */ jsx(Button, { rightSection: /* @__PURE__ */ jsx(IconArrowRight, { size: 16 }), onClick: goToNextStep, children: steps[stepIndex + 1]?.label })
3499
+ ] })
2206
3500
  ] });
2207
3501
  }
2208
3502
 
2209
- export { AlternatesPanel, ContentDistributionDetailPage, ContentDistributionsPage, ContentItemReviewPage, ContentItemsPage, ContentOverviewPage, ContentPipelineBoardPage, ContentPipelineBoardPage as ContentPipelineWorkspacePage, ContentPipelinesPage, ContentReviewCard, PayloadBody, ProcessingStateStrip, ReviewActionBar, firstLine, formatElapsed, lastLine, swapFirstLine, swapLastLine };
3503
+ export { AlternatesPanel, AssetCropEditor, CAPTION_STEP_KEY, CAPTION_WORKFLOW_ID, CONTENT_DERIVATIVES_BUCKET, CROP_ASPECT_OPTIONS, CaptionPanel, ContentAnalysisStepPanel, ContentDistributionDetailPage, ContentDistributionsPage, ContentItemReviewPage, ContentItemsPage, ContentOverviewPage, ContentPipelineBoardPage, ContentPipelineBoardPage as ContentPipelineWorkspacePage, ContentPipelinesPage, ContentReviewCard, ContentWorkspaceShell, 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 };