@elevasis/ui 2.72.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.
- package/dist/api/index.js +3 -3
- package/dist/app/index.d.ts +114 -20
- package/dist/app/index.js +8 -8
- package/dist/auth/index.js +4 -4
- package/dist/charts/index.js +4 -4
- package/dist/{chunk-A3JHFZDP.js → chunk-3TS5U5BL.js} +1 -1
- package/dist/{chunk-IXBRUIYI.js → chunk-4CW2C5BD.js} +1 -1
- package/dist/{chunk-753MKWFV.js → chunk-5B2ZFO44.js} +1 -1
- package/dist/{chunk-DJWIIPKW.js → chunk-AJOVWB47.js} +3 -3
- package/dist/{chunk-RRS2QR3W.js → chunk-CJ2232PQ.js} +1 -1
- package/dist/{chunk-2454MK7P.js → chunk-ETN3TTRT.js} +7 -7
- package/dist/{chunk-Q3O3WY7Q.js → chunk-IBCEAVB2.js} +33 -13
- package/dist/{chunk-7ESZKTQG.js → chunk-IGBPWD6S.js} +3 -3
- package/dist/{chunk-UZ5JO5TK.js → chunk-JYBJMVNY.js} +1 -1
- package/dist/{chunk-LCHL5SM2.js → chunk-LEPYXLJ2.js} +1 -1
- package/dist/{chunk-LEQGGX2U.js → chunk-LZT2H2MA.js} +4 -4
- package/dist/{chunk-Z2W5VNN3.js → chunk-NHNTX6ZL.js} +1 -1
- package/dist/{chunk-AFBFSXP3.js → chunk-NI37YNRP.js} +4 -4
- package/dist/{chunk-KIDZXZM5.js → chunk-OCVSPAKM.js} +501 -25
- package/dist/{chunk-IMZTRDY2.js → chunk-PQ4B62HH.js} +2 -2
- package/dist/{chunk-SV4M2BUI.js → chunk-SWKAH2YM.js} +52 -20
- package/dist/{chunk-XZIOCB5D.js → chunk-U2H3TIHD.js} +1 -1
- package/dist/{chunk-DKVGO6RJ.js → chunk-Z4ICFQXG.js} +1 -1
- package/dist/{chunk-ZBXCEWKF.js → chunk-ZDKPHFTH.js} +3 -3
- package/dist/components/index.d.ts +305 -22
- package/dist/components/index.js +316 -26
- package/dist/components/navigation/index.js +5 -5
- package/dist/features/auth/index.d.ts +76 -0
- package/dist/features/auth/index.js +6 -6
- package/dist/features/clients/index.js +5 -5
- package/dist/features/content/index.d.ts +494 -31
- package/dist/features/content/index.js +1090 -44
- package/dist/features/crm/index.d.ts +114 -20
- package/dist/features/crm/index.js +6 -6
- package/dist/features/dashboard/index.d.ts +19 -10
- package/dist/features/dashboard/index.js +6 -6
- package/dist/features/delivery/index.d.ts +76 -0
- package/dist/features/delivery/index.js +5 -5
- package/dist/features/knowledge/index.js +4 -4
- package/dist/features/lead-gen/index.d.ts +38 -20
- package/dist/features/lead-gen/index.js +6 -6
- package/dist/features/monitoring/index.js +7 -7
- package/dist/features/monitoring/requests/index.js +5 -5
- package/dist/features/operations/index.d.ts +38 -20
- package/dist/features/operations/index.js +9 -9
- package/dist/features/settings/index.d.ts +76 -0
- package/dist/features/settings/index.js +6 -6
- package/dist/hooks/access/index.js +4 -4
- package/dist/hooks/delivery/index.d.ts +76 -0
- package/dist/hooks/delivery/index.js +2 -2
- package/dist/hooks/index.d.ts +1248 -28
- package/dist/hooks/index.js +4 -4
- package/dist/hooks/operations/command-view/utils/transformCommandViewData.d.ts +19 -10
- package/dist/hooks/published.d.ts +1248 -28
- package/dist/hooks/published.js +4 -4
- package/dist/initialization/index.d.ts +76 -0
- package/dist/knowledge/index.d.ts +38 -20
- package/dist/knowledge/index.js +8 -8
- package/dist/layout/index.js +7 -7
- package/dist/profile/index.d.ts +76 -0
- package/dist/provider/index.d.ts +114 -20
- package/dist/provider/index.js +4 -4
- package/dist/provider/published.d.ts +114 -20
- package/dist/provider/published.js +4 -4
- package/dist/supabase/index.d.ts +149 -0
- package/dist/test-utils/index.js +3 -3
- package/dist/types/index.d.ts +125 -13
- package/dist/utils/index.d.ts +19 -10
- package/dist/utils/index.js +1 -1
- 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,
|
|
5
|
-
export { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle, DEFAULT_CONTENT_VOCABULARY, MyReviewQueuePanel, contentManifest, deriveContentBoard, getContentItemIdentity, resolveContentStepResource, resolveContentVocabulary, useContentConfig } from '../../chunk-
|
|
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-
|
|
9
|
+
import '../../chunk-NHNTX6ZL.js';
|
|
10
10
|
import '../../chunk-L7BZZ4SI.js';
|
|
11
|
-
import { showSuccessNotification } from '../../chunk-
|
|
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-
|
|
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,
|
|
33
|
-
import { IconAlertTriangle, IconShieldCheck, IconShieldX, IconArticle, IconArrowRight, IconClock, IconSend, IconSearch, IconFileText, IconExternalLink, IconListDetails, IconAlertCircle, IconLayoutGrid, IconShare, IconPlayerPlay, IconCircleCheck, IconCircleDashed, IconMaximize, IconCopy, IconDownload, IconArrowLeft, IconArrowsExchange,
|
|
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
|
|
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(
|
|
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
|
|
715
|
-
|
|
716
|
-
|
|
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) =>
|
|
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
|
|
949
|
-
() =>
|
|
950
|
-
[item
|
|
1959
|
+
const reviewTarget = useMemo(
|
|
1960
|
+
() => item ? resolveReviewTarget(item, pipeline, pipelineSteps) : { kind: "none" },
|
|
1961
|
+
[item, pipeline, pipelineSteps]
|
|
951
1962
|
);
|
|
952
|
-
const earliestOpenGate =
|
|
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 (
|
|
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
|
-
}, [
|
|
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
|
|
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 &&
|
|
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: [
|
|
@@ -1304,13 +2318,13 @@ var ACTOR_LEAD = {
|
|
|
1304
2318
|
workflow: "An automated workflow",
|
|
1305
2319
|
agent: "An AI agent"
|
|
1306
2320
|
};
|
|
1307
|
-
var
|
|
1308
|
-
|
|
1309
|
-
|
|
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",
|
|
1310
2324
|
none: "and moves on without review"
|
|
1311
2325
|
};
|
|
1312
2326
|
function describeStep(step) {
|
|
1313
|
-
return `${ACTOR_LEAD[step.actor]} handles "${step.label}," ${
|
|
2327
|
+
return `${ACTOR_LEAD[step.actor]} handles "${step.label}," ${REVIEW_VENUE_TAIL[step.reviewVenue]}.`;
|
|
1314
2328
|
}
|
|
1315
2329
|
function StepRow({ step, resourceId }) {
|
|
1316
2330
|
return /* @__PURE__ */ jsxs(Group, { gap: "sm", align: "flex-start", wrap: "nowrap", children: [
|
|
@@ -1959,6 +2973,12 @@ function ContentDistributionDetailPage({
|
|
|
1959
2973
|
const itemQuery = useContentItem(dist?.contentItemId);
|
|
1960
2974
|
const [mediaOpened, { open: openMedia, close: closeMedia }] = useDisclosure(false);
|
|
1961
2975
|
const [previewUrl, setPreviewUrl] = useState(null);
|
|
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);
|
|
1962
2982
|
const backButton = /* @__PURE__ */ jsx(
|
|
1963
2983
|
Button,
|
|
1964
2984
|
{
|
|
@@ -1985,7 +3005,9 @@ function ContentDistributionDetailPage({
|
|
|
1985
3005
|
const contentTitle = contentItem?.title ?? dist.contentItemId;
|
|
1986
3006
|
const checklist = toChecklist(dist.checklist);
|
|
1987
3007
|
const checklistProgress = getChecklistProgress(checklist);
|
|
1988
|
-
const
|
|
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
|
+
);
|
|
1989
3011
|
const platformEntry = contentConfig.stageCatalog[dist.platform];
|
|
1990
3012
|
const platformColors = getContentStageColor(platformEntry);
|
|
1991
3013
|
const pillarEntry = contentItem?.pillar ? contentConfig.stageCatalog[contentItem.pillar] : void 0;
|
|
@@ -2161,24 +3183,24 @@ function ContentDistributionDetailPage({
|
|
|
2161
3183
|
/* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
2162
3184
|
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
2163
3185
|
/* @__PURE__ */ jsx(Title, { order: 3, children: "Media" }),
|
|
2164
|
-
|
|
2165
|
-
|
|
3186
|
+
mediaItems.length > 0 ? /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
|
|
3187
|
+
mediaItems.length,
|
|
2166
3188
|
" image",
|
|
2167
|
-
|
|
3189
|
+
mediaItems.length !== 1 ? "s" : ""
|
|
2168
3190
|
] }) : null
|
|
2169
3191
|
] }),
|
|
2170
3192
|
/* @__PURE__ */ jsx(Divider, {}),
|
|
2171
|
-
|
|
2172
|
-
/* @__PURE__ */ jsx(
|
|
2173
|
-
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,
|
|
2174
3196
|
{
|
|
2175
|
-
src: url,
|
|
3197
|
+
src: item.url,
|
|
2176
3198
|
alt: `Media variant ${index + 1}`,
|
|
2177
3199
|
w: "100%",
|
|
2178
3200
|
radius: "sm",
|
|
2179
3201
|
style: { cursor: "pointer" },
|
|
2180
3202
|
onClick: () => {
|
|
2181
|
-
setPreviewUrl(url);
|
|
3203
|
+
setPreviewUrl(item.url);
|
|
2182
3204
|
openMedia();
|
|
2183
3205
|
}
|
|
2184
3206
|
}
|
|
@@ -2189,21 +3211,45 @@ function ContentDistributionDetailPage({
|
|
|
2189
3211
|
{
|
|
2190
3212
|
variant: "subtle",
|
|
2191
3213
|
size: "sm",
|
|
3214
|
+
disabled: item.url === null,
|
|
2192
3215
|
onClick: () => {
|
|
2193
|
-
setPreviewUrl(url);
|
|
3216
|
+
setPreviewUrl(item.url);
|
|
2194
3217
|
openMedia();
|
|
2195
3218
|
},
|
|
2196
3219
|
children: /* @__PURE__ */ jsx(IconMaximize, { size: 16 })
|
|
2197
3220
|
}
|
|
2198
3221
|
) }),
|
|
2199
|
-
/* @__PURE__ */ jsx(Tooltip, { label: "Copy URL", children: /* @__PURE__ */ jsx(
|
|
2200
|
-
|
|
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(
|
|
3235
|
+
ActionIcon,
|
|
3236
|
+
{
|
|
3237
|
+
variant: "subtle",
|
|
3238
|
+
size: "sm",
|
|
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 })
|
|
3245
|
+
}
|
|
3246
|
+
) })
|
|
2201
3247
|
] })
|
|
2202
|
-
] },
|
|
3248
|
+
] }, item.key)) })
|
|
2203
3249
|
] }) })
|
|
2204
3250
|
] }),
|
|
2205
|
-
/* @__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 })
|
|
2206
3252
|
] });
|
|
2207
3253
|
}
|
|
2208
3254
|
|
|
2209
|
-
export { AlternatesPanel, ContentDistributionDetailPage, ContentDistributionsPage, ContentItemReviewPage, ContentItemsPage, ContentOverviewPage, ContentPipelineBoardPage, ContentPipelineBoardPage as 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 };
|