@elevasis/ui 2.69.0 → 2.70.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 (64) hide show
  1. package/dist/app/index.js +7 -7
  2. package/dist/auth/index.css +149 -149
  3. package/dist/auth/index.js +3 -3
  4. package/dist/charts/index.css +208 -208
  5. package/dist/charts/index.js +3 -3
  6. package/dist/{chunk-ZD6LM53O.js → chunk-5C62IRKS.js} +2 -2
  7. package/dist/{chunk-WXHAHKKI.js → chunk-6TT3FQEZ.js} +16204 -16039
  8. package/dist/{chunk-MFLM7BVC.js → chunk-AGX2CY2O.js} +1 -1
  9. package/dist/{chunk-IRV6KJPV.js → chunk-CCZJ4OWY.js} +1 -1
  10. package/dist/{chunk-XAZR3T6Y.js → chunk-DUEGHF7A.js} +3 -3
  11. package/dist/{chunk-NU3NBXHP.js → chunk-GJQF3S2Q.js} +1 -1
  12. package/dist/{chunk-YNRYIXUT.js → chunk-GZADST64.js} +6 -6
  13. package/dist/{chunk-YCRINA2D.js → chunk-JA2XDMAE.js} +1 -1
  14. package/dist/{chunk-6PZFOHJK.js → chunk-LWYLZUUV.js} +2 -2
  15. package/dist/{chunk-7WJEPTE7.js → chunk-LY3IKYTZ.js} +1 -1
  16. package/dist/{chunk-RW64UJWT.js → chunk-MFCV4JMQ.js} +2 -2
  17. package/dist/{chunk-IT7GTJCB.js → chunk-QDMIFD6F.js} +1 -1
  18. package/dist/{chunk-FLECOYUS.js → chunk-R6NMRXNO.js} +1 -1
  19. package/dist/{chunk-XQ3ILKYN.js → chunk-SMH6SQQE.js} +1 -1
  20. package/dist/{chunk-MRVGQLWN.js → chunk-TJMZUL6E.js} +1 -1
  21. package/dist/{chunk-YGCMMOY4.js → chunk-XKUEO4HG.js} +695 -73
  22. package/dist/{chunk-4JHLS234.js → chunk-ZFISGKYW.js} +1 -1
  23. package/dist/components/index.css +208 -208
  24. package/dist/components/index.js +24 -25
  25. package/dist/components/navigation/index.css +149 -149
  26. package/dist/components/navigation/index.js +4 -4
  27. package/dist/features/auth/index.css +149 -149
  28. package/dist/features/auth/index.js +5 -5
  29. package/dist/features/clients/index.css +208 -208
  30. package/dist/features/clients/index.js +4 -4
  31. package/dist/features/content/index.d.ts +238 -48
  32. package/dist/features/content/index.js +542 -410
  33. package/dist/features/crm/index.js +5 -5
  34. package/dist/features/dashboard/index.js +6 -7
  35. package/dist/features/delivery/index.css +149 -149
  36. package/dist/features/delivery/index.js +4 -4
  37. package/dist/features/knowledge/index.css +149 -149
  38. package/dist/features/knowledge/index.js +3 -3
  39. package/dist/features/lead-gen/index.js +5 -5
  40. package/dist/features/monitoring/index.css +149 -149
  41. package/dist/features/monitoring/index.js +6 -6
  42. package/dist/features/monitoring/requests/index.js +4 -4
  43. package/dist/features/operations/index.js +9 -10
  44. package/dist/features/settings/index.js +6 -6
  45. package/dist/hooks/access/index.css +149 -149
  46. package/dist/hooks/access/index.js +3 -3
  47. package/dist/hooks/index.css +149 -149
  48. package/dist/hooks/index.d.ts +2 -60
  49. package/dist/hooks/index.js +3 -3
  50. package/dist/hooks/published.css +149 -149
  51. package/dist/hooks/published.d.ts +2 -60
  52. package/dist/hooks/published.js +3 -3
  53. package/dist/knowledge/index.css +149 -149
  54. package/dist/knowledge/index.js +7 -7
  55. package/dist/layout/index.css +149 -149
  56. package/dist/layout/index.js +6 -6
  57. package/dist/provider/index.js +3 -3
  58. package/dist/provider/published.css +149 -149
  59. package/dist/provider/published.js +3 -3
  60. package/dist/theme/index.js +3 -3
  61. package/dist/theme/presets/index.js +1 -1
  62. package/package.json +3 -3
  63. package/dist/chunk-ZPTEGGKU.js +0 -631
  64. /package/dist/{chunk-XQBURFKM.js → chunk-RBGVNPA6.js} +0 -0
@@ -1,13 +1,10 @@
1
- import { RunResourceButton } from '../../chunk-YGCMMOY4.js';
2
- import '../../chunk-3MEXPLWT.js';
3
- import '../../chunk-HENXLGVD.js';
4
1
  import { PageContainer } from '../../chunk-BZZCNLT6.js';
5
2
  import '../../chunk-TUMSNGTX.js';
6
3
  import { FilterBar } from '../../chunk-PDHTXPSF.js';
7
- import { CustomModal, useOptionalElevasisSystems, getContentBuildTemplates, getContentStageCatalog, useContentItems, useContentDistributions, SubshellContentContainer, PageTitleCaption, CenteredErrorState, StatCard, usePaginationState, EmptyState, useContentItem, useContentItemAttempts, useUpdateContentItem, useReviewContentItem, useContentArtifacts, useContentDistribution, useUpdateContentDistribution, useContentPipelineSummary, useAccess, AccessKeys, compileContentPayloadValidator, useExecutions } from '../../chunk-WXHAHKKI.js';
8
- export { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle, ContentSidebarTop, MyReviewQueuePanel, contentManifest } from '../../chunk-WXHAHKKI.js';
9
- import '../../chunk-FLECOYUS.js';
10
- import '../../chunk-XQBURFKM.js';
4
+ import { CustomModal, useContentItems, useContentDistributions, useContentConfig, SubshellContentContainer, PageTitleCaption, CenteredErrorState, EmptyState, StatCard, getContentItemIdentity, usePaginationState, useContentItem, useContentItemAttempts, useUpdateContentItem, useReviewContentItem, useContentDistribution, useUpdateContentDistribution, useContentPipelineSummary, deriveContentBoard, useExecutions } from '../../chunk-6TT3FQEZ.js';
5
+ export { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle, DEFAULT_CONTENT_VOCABULARY, MyReviewQueuePanel, contentManifest, deriveContentBoard, getContentItemIdentity, resolveContentStepResource, resolveContentVocabulary, useContentConfig } from '../../chunk-6TT3FQEZ.js';
6
+ import '../../chunk-R6NMRXNO.js';
7
+ import '../../chunk-RBGVNPA6.js';
11
8
  import '../../chunk-MKH2KOAO.js';
12
9
  import '../../chunk-VDX6DL7Z.js';
13
10
  import '../../chunk-L7BZZ4SI.js';
@@ -23,7 +20,7 @@ import '../../chunk-UD657ICS.js';
23
20
  import '../../chunk-DD3CCMCZ.js';
24
21
  import '../../chunk-RNP5R5I3.js';
25
22
  import '../../chunk-RT4KRGZT.js';
26
- import { getSystem } from '../../chunk-NM4A63KJ.js';
23
+ import { formatTimeAgo, formatRelativeTime } from '../../chunk-NM4A63KJ.js';
27
24
  import '../../chunk-Y6I3IC45.js';
28
25
  import '../../chunk-6SSQGWK7.js';
29
26
  import '../../chunk-BI5VDO2K.js';
@@ -32,10 +29,10 @@ import '../../chunk-TVTSASST.js';
32
29
  import '../../chunk-BRJ3QZ4E.js';
33
30
  import '../../chunk-Q7DJKLEN.js';
34
31
  import '../../chunk-I2KLQ2HA.js';
35
- import { Stack, Group, Text, Badge, Button, Textarea, Box, Loader, Accordion, Card, Alert, List, Paper, Center, SimpleGrid, Table, TextInput, Select, Pagination, Title, Tabs, Anchor, Divider, Progress, Checkbox, TagsInput, Image, Tooltip, ActionIcon, UnstyledButton } from '@mantine/core';
36
- import { IconAlertTriangle, IconShieldCheck, IconShieldX, IconAlertCircle, IconArticle, IconClock, IconSend, IconSearch, IconFileText, IconCheck, IconX, IconListDetails, IconLayoutGrid, IconFolder, IconBolt, IconPlayerPlay, IconShare, IconExternalLink, IconMaximize, IconCopy, IconDownload, IconTrash, IconArrowLeft, IconArrowsExchange, IconFolderOff } from '@tabler/icons-react';
32
+ import { Stack, Group, Text, Badge, Button, Textarea, Box, Loader, Accordion, Card, Alert, List, Paper, Center, SimpleGrid, Table, TextInput, Select, Pagination, Title, Tabs, Anchor, Divider, Progress, Checkbox, TagsInput, Image, Tooltip, ActionIcon, UnstyledButton, ScrollArea } from '@mantine/core';
33
+ import { IconAlertTriangle, IconShieldCheck, IconShieldX, IconArticle, IconArrowRight, IconClock, IconSend, IconSearch, IconFileText, IconListDetails, IconAlertCircle, IconLayoutGrid, IconShare, IconPlayerPlay, IconExternalLink, IconMaximize, IconCopy, IconDownload, IconTrash, IconArrowLeft, IconArrowsExchange, IconCheck, IconX, IconChecklist, IconRoute, IconInfoCircle, IconClockHour4 } from '@tabler/icons-react';
37
34
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
38
- import { useState, useMemo, useEffect, useCallback } from 'react';
35
+ import { useState, useMemo, Fragment as Fragment$1, useCallback, useEffect } from 'react';
39
36
  import { useNavigate, useSearch } from '@tanstack/react-router';
40
37
  import { useDisclosure } from '@mantine/hooks';
41
38
 
@@ -75,7 +72,7 @@ function PayloadBody({
75
72
  value: body,
76
73
  onChange: (event) => onBodyChange(event.currentTarget.value),
77
74
  autosize: true,
78
- minRows: 6,
75
+ minRows: 3,
79
76
  disabled,
80
77
  styles: {
81
78
  input: {
@@ -303,6 +300,17 @@ function AlternatesPanel({
303
300
  ] }) })
304
301
  ] }) });
305
302
  }
303
+ var IRREVERSIBLE_WARNING = "This action is irreversible and visible outside the platform.";
304
+ function isIrreversible(action) {
305
+ return action?.consequence === "irreversible";
306
+ }
307
+ function resolveActionColor(action) {
308
+ if (!action) return void 0;
309
+ if (action.color) return action.color;
310
+ if (action.tone === "danger") return "var(--color-error)";
311
+ if (isIrreversible(action)) return "var(--color-warning)";
312
+ return void 0;
313
+ }
306
314
  function ReviewActionBar({ actions, onAction, pending = false }) {
307
315
  const [confirmAction, setConfirmAction] = useState(null);
308
316
  const [feedbackAction, setFeedbackAction] = useState(null);
@@ -311,7 +319,7 @@ function ReviewActionBar({ actions, onAction, pending = false }) {
311
319
  if (action.requiresFeedback) {
312
320
  setFeedback("");
313
321
  setFeedbackAction(action);
314
- } else if (action.requiresConfirmation) {
322
+ } else if (action.requiresConfirmation || isIrreversible(action)) {
315
323
  setConfirmAction(action);
316
324
  } else {
317
325
  onAction(action.id);
@@ -326,11 +334,13 @@ function ReviewActionBar({ actions, onAction, pending = false }) {
326
334
  return /* @__PURE__ */ jsxs(Fragment, { children: [
327
335
  /* @__PURE__ */ jsx(Group, { gap: "xs", children: actions.map((action) => {
328
336
  const Icon = action.icon;
337
+ const irreversible = isIrreversible(action);
329
338
  return /* @__PURE__ */ jsx(
330
339
  Button,
331
340
  {
332
- variant: "light",
333
- color: action.color || (action.tone === "danger" ? "var(--color-error)" : void 0),
341
+ variant: irreversible ? "filled" : "light",
342
+ fw: irreversible ? 700 : void 0,
343
+ color: resolveActionColor(action),
334
344
  leftSection: Icon ? /* @__PURE__ */ jsx(Icon, { size: 16 }) : void 0,
335
345
  loading: pending,
336
346
  disabled: pending,
@@ -345,13 +355,15 @@ function ReviewActionBar({ actions, onAction, pending = false }) {
345
355
  }) }),
346
356
  /* @__PURE__ */ jsx(CustomModal, { opened: !!confirmAction, onClose: () => setConfirmAction(null), size: "sm", children: /* @__PURE__ */ jsxs(Stack, { children: [
347
357
  /* @__PURE__ */ jsx(Text, { fw: 600, children: confirmAction?.label }),
358
+ isIrreversible(confirmAction) ? /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, c: "var(--color-warning)", children: IRREVERSIBLE_WARNING }) : null,
348
359
  /* @__PURE__ */ jsx(Text, { size: "sm", children: confirmAction?.confirmationMessage || `Are you sure you want to ${confirmAction?.label.toLowerCase()}?` }),
349
360
  /* @__PURE__ */ jsxs(Group, { justify: "flex-end", children: [
350
361
  /* @__PURE__ */ jsx(Button, { variant: "default", onClick: () => setConfirmAction(null), children: "Cancel" }),
351
362
  /* @__PURE__ */ jsx(
352
363
  Button,
353
364
  {
354
- color: confirmAction?.tone === "danger" ? "var(--color-error)" : void 0,
365
+ variant: isIrreversible(confirmAction) ? "filled" : void 0,
366
+ color: resolveActionColor(confirmAction),
355
367
  onClick: () => {
356
368
  if (confirmAction) onAction(confirmAction.id);
357
369
  setConfirmAction(null);
@@ -363,6 +375,7 @@ function ReviewActionBar({ actions, onAction, pending = false }) {
363
375
  ] }) }),
364
376
  /* @__PURE__ */ jsx(CustomModal, { opened: !!feedbackAction, onClose: () => setFeedbackAction(null), size: "sm", children: /* @__PURE__ */ jsxs(Stack, { children: [
365
377
  /* @__PURE__ */ jsx(Text, { fw: 600, children: feedbackAction?.label }),
378
+ isIrreversible(feedbackAction) ? /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, c: "var(--color-warning)", children: IRREVERSIBLE_WARNING }) : null,
366
379
  feedbackAction?.confirmationMessage ? /* @__PURE__ */ jsx(Text, { size: "sm", children: feedbackAction.confirmationMessage }) : null,
367
380
  /* @__PURE__ */ jsx(
368
381
  Textarea,
@@ -381,7 +394,8 @@ function ReviewActionBar({ actions, onAction, pending = false }) {
381
394
  /* @__PURE__ */ jsx(
382
395
  Button,
383
396
  {
384
- color: feedbackAction?.tone === "danger" ? "var(--color-error)" : void 0,
397
+ variant: isIrreversible(feedbackAction) ? "filled" : void 0,
398
+ color: resolveActionColor(feedbackAction),
385
399
  disabled: Boolean(feedbackAction?.feedbackRequired) && feedback.trim().length === 0,
386
400
  onClick: submitFeedback,
387
401
  "data-testid": "content-review-feedback-submit",
@@ -414,10 +428,10 @@ function ContentReviewCard({
414
428
  actionsPending = false
415
429
  }) {
416
430
  return /* @__PURE__ */ jsx(Card, { withBorder: true, radius: "md", padding: "xl", "data-testid": "content-review-card", children: /* @__PURE__ */ jsxs(Stack, { gap: "lg", children: [
417
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", align: "flex-start", wrap: "nowrap", children: [
418
- /* @__PURE__ */ jsx(Text, { fw: 700, size: "lg", children: title }),
431
+ title || processingSteps.length > 0 ? /* @__PURE__ */ jsxs(Group, { justify: "space-between", align: "flex-start", wrap: "nowrap", children: [
432
+ title ? /* @__PURE__ */ jsx(Text, { fw: 700, size: "lg", children: title }) : null,
419
433
  /* @__PURE__ */ jsx(ProcessingStateStrip, { steps: processingSteps })
420
- ] }),
434
+ ] }) : null,
421
435
  fidelity && !fidelity.matches && /* @__PURE__ */ jsx(
422
436
  Alert,
423
437
  {
@@ -521,52 +535,6 @@ function ContentReviewCard({
521
535
  /* @__PURE__ */ jsx(ReviewActionBar, { actions, onAction, pending: actionsPending })
522
536
  ] }) });
523
537
  }
524
- var CONTENT_SYSTEM_PATH = "content";
525
- function resolveContentStepResource(model, step) {
526
- const actionId = step.actionId;
527
- if (!actionId) return void 0;
528
- const resource = Object.values(model.resources ?? {}).find(
529
- (candidate) => candidate.systemPath === CONTENT_SYSTEM_PATH && (candidate.ontology?.primaryAction === actionId || candidate.ontology?.actions?.includes(actionId) === true)
530
- );
531
- return resource ? { resourceId: resource.id, resourceKind: resource.kind } : void 0;
532
- }
533
- var EMPTY_CONFIG = {
534
- pipelines: [],
535
- stageCatalog: {},
536
- stagesByCatalog: {},
537
- defaultPipelineId: void 0,
538
- resolveStepResource: () => void 0
539
- };
540
- function groupStagesByCatalog(stageCatalog) {
541
- const groups = {};
542
- for (const entry of Object.values(stageCatalog)) {
543
- (groups[entry.catalogKey] ??= []).push(entry);
544
- }
545
- return groups;
546
- }
547
- function getDefaultPipelineId(model, pipelines) {
548
- const configuredId = getSystem(model, CONTENT_SYSTEM_PATH)?.config?.defaultPipelineId;
549
- if (typeof configuredId === "string" && pipelines.some((pipeline) => pipeline.id === configuredId)) {
550
- return configuredId;
551
- }
552
- return pipelines[0]?.id;
553
- }
554
- function useContentConfig() {
555
- const systems = useOptionalElevasisSystems();
556
- const organizationModel = systems?.organizationModel;
557
- return useMemo(() => {
558
- if (!organizationModel) return EMPTY_CONFIG;
559
- const pipelines = getContentBuildTemplates(organizationModel);
560
- const stageCatalog = getContentStageCatalog(organizationModel);
561
- return {
562
- pipelines,
563
- stageCatalog,
564
- stagesByCatalog: groupStagesByCatalog(stageCatalog),
565
- defaultPipelineId: getDefaultPipelineId(organizationModel, pipelines),
566
- resolveStepResource: (step) => resolveContentStepResource(organizationModel, step)
567
- };
568
- }, [organizationModel]);
569
- }
570
538
 
571
539
  // src/lib/content/stage-colors.ts
572
540
  var FALLBACK_CONTENT_STAGE_COLORS = {
@@ -609,43 +577,100 @@ function ContentOverviewPage({ headerActions, footer } = {}) {
609
577
  );
610
578
  const isLoading = itemsQuery.isLoading;
611
579
  const error = itemsQuery.error;
580
+ const { vocabulary } = contentConfig;
612
581
  return /* @__PURE__ */ jsxs(SubshellContentContainer, { children: [
613
582
  /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
614
- /* @__PURE__ */ jsx(PageTitleCaption, { title: "Content Overview", caption: "Pipeline status, review load, and recent activity" }),
583
+ /* @__PURE__ */ jsx(
584
+ PageTitleCaption,
585
+ {
586
+ title: `${vocabulary.item.plural} Overview`,
587
+ caption: `${vocabulary.pipeline.singular} status, review load, and recent activity`
588
+ }
589
+ ),
615
590
  headerActions ? /* @__PURE__ */ jsx(Group, { gap: "xs", children: headerActions }) : null,
616
- isLoading ? /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) }) : error ? /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(CenteredErrorState, { error, title: "Failed to load content items" }) }) : items.length === 0 ? /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(Center, { h: 300, children: /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }), color: "gray", variant: "light", children: "No content items yet." }) }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
591
+ isLoading ? /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) }) : error ? /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(CenteredErrorState, { error, title: `Failed to load ${vocabulary.item.plural.toLowerCase()}` }) }) : items.length === 0 ? (
592
+ // Finding 11: zero items is the state every tenant starts in, and the only one
593
+ // with no path forward on screen. The one available action is to go produce
594
+ // something -- routed through the pipelines page, which itself lands a
595
+ // single-pipeline tenant straight inside its workspace (Proposal, "Design the
596
+ // first run").
597
+ /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(
598
+ EmptyState,
599
+ {
600
+ icon: IconArticle,
601
+ title: `No ${vocabulary.item.plural.toLowerCase()} yet`,
602
+ description: `${vocabulary.pipeline.plural} produce ${vocabulary.item.plural.toLowerCase()} as they move from draft through review to publish. Pick a pipeline to produce the first one.`,
603
+ action: {
604
+ label: `Go to ${vocabulary.pipeline.plural.toLowerCase()}`,
605
+ onClick: () => navigate({ to: "/content/pipelines" }),
606
+ icon: /* @__PURE__ */ jsx(IconArrowRight, { size: 14 })
607
+ }
608
+ }
609
+ ) })
610
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
617
611
  /* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, sm: 3 }, children: [
618
- /* @__PURE__ */ jsx(StatCard, { variant: "hero", icon: IconArticle, value: totalItems, label: "Total Items" }),
612
+ /* @__PURE__ */ jsx(
613
+ StatCard,
614
+ {
615
+ variant: "hero",
616
+ icon: IconArticle,
617
+ value: totalItems,
618
+ label: `Total ${vocabulary.item.plural}`
619
+ }
620
+ ),
619
621
  /* @__PURE__ */ jsx(StatCard, { variant: "hero", icon: IconClock, value: pendingReviewCount, label: "Pending Review" }),
620
- /* @__PURE__ */ jsx(StatCard, { variant: "hero", icon: IconSend, value: totalDistributions, label: "Distributions" })
622
+ /* @__PURE__ */ jsx(
623
+ StatCard,
624
+ {
625
+ variant: "hero",
626
+ icon: IconSend,
627
+ value: totalDistributions,
628
+ label: vocabulary.distribution.plural
629
+ }
630
+ )
621
631
  ] }),
622
632
  statusEntries.length > 0 && /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
623
- /* @__PURE__ */ jsx(CardHeader, { title: "By Status", subtitle: "Item counts across the content:catalog/status catalog" }),
624
- /* @__PURE__ */ jsx(Group, { gap: "xs", wrap: "wrap", children: statusEntries.map((entry) => {
633
+ /* @__PURE__ */ jsx(
634
+ CardHeader,
635
+ {
636
+ title: "By Status",
637
+ subtitle: `Where each ${vocabulary.item.singular.toLowerCase()} currently stands`
638
+ }
639
+ ),
640
+ /* @__PURE__ */ jsx(Group, { gap: 4, wrap: "wrap", children: statusEntries.map((entry, index) => {
625
641
  const colors = getContentStageColor(entry);
626
- return /* @__PURE__ */ jsxs(
627
- Badge,
628
- {
629
- size: "lg",
630
- variant: "light",
631
- styles: { root: { color: colors.accent, backgroundColor: colors.background } },
632
- children: [
633
- entry.label,
634
- ": ",
635
- statusCounts.get(entry.key) ?? 0
636
- ]
637
- },
638
- entry.key
639
- );
642
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
643
+ index > 0 && /* @__PURE__ */ jsx(IconArrowRight, { size: 14, color: "var(--color-text-dimmed)" }),
644
+ /* @__PURE__ */ jsxs(
645
+ Badge,
646
+ {
647
+ size: "lg",
648
+ variant: "light",
649
+ styles: { root: { color: colors.accent, backgroundColor: colors.background } },
650
+ children: [
651
+ entry.label,
652
+ ": ",
653
+ statusCounts.get(entry.key) ?? 0
654
+ ]
655
+ }
656
+ )
657
+ ] }, entry.key);
640
658
  }) })
641
659
  ] }) }),
642
660
  /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
643
- /* @__PURE__ */ jsx(Group, { justify: "space-between", align: "flex-start", gap: "md", children: /* @__PURE__ */ jsx(CardHeader, { title: "Recently Updated", subtitle: "Jump into an item's review page" }) }),
661
+ /* @__PURE__ */ jsx(
662
+ CardHeader,
663
+ {
664
+ title: "Recently Updated",
665
+ subtitle: `Jump into a ${vocabulary.item.singular.toLowerCase()}'s review page`
666
+ }
667
+ ),
644
668
  /* @__PURE__ */ jsxs(Table, { children: [
645
669
  /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
646
- /* @__PURE__ */ jsx(Table.Th, { children: "Title" }),
670
+ /* @__PURE__ */ jsx(Table.Th, { children: vocabulary.item.singular }),
647
671
  /* @__PURE__ */ jsx(Table.Th, { children: "Status" }),
648
672
  /* @__PURE__ */ jsx(Table.Th, { children: "Pillar" }),
673
+ /* @__PURE__ */ jsx(Table.Th, { children: "Age" }),
649
674
  /* @__PURE__ */ jsx(Table.Th, { children: "Reviewed" })
650
675
  ] }) }),
651
676
  /* @__PURE__ */ jsx(Table.Tbody, { children: recentItems.map((item) => {
@@ -656,9 +681,10 @@ function ContentOverviewPage({ headerActions, footer } = {}) {
656
681
  style: { cursor: "pointer" },
657
682
  onClick: () => navigate({ to: "/content/items/$itemId", params: { itemId: item.id } }),
658
683
  children: [
659
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, children: item.title }) }),
684
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, children: getContentItemIdentity(item) }) }),
660
685
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: statusEntry?.label ?? item.status }) }),
661
686
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: item.pillar ?? "\u2014" }) }),
687
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatTimeAgo(item.updatedAt) }) }),
662
688
  /* @__PURE__ */ jsx(Table.Td, { children: item.reviewedAt ? /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-success)", children: "Reviewed" }) : /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-warning)", children: "Pending" }) })
663
689
  ]
664
690
  },
@@ -676,10 +702,12 @@ var PAGE_SIZE = 20;
676
702
  function ContentItemsPage({ headerActions } = {}) {
677
703
  const navigate = useNavigate();
678
704
  const contentConfig = useContentConfig();
705
+ const rawSearch = useSearch({ strict: false });
706
+ const reviewFilterActive = rawSearch.review === "pending";
679
707
  const [searchQuery, setSearchQuery] = useState("");
680
708
  const [statusFilter, setStatusFilter] = useState("");
681
709
  const [pillarFilter, setPillarFilter] = useState("");
682
- const pagination = usePaginationState(PAGE_SIZE, [searchQuery, statusFilter, pillarFilter]);
710
+ const pagination = usePaginationState(PAGE_SIZE, [searchQuery, statusFilter, pillarFilter, reviewFilterActive]);
683
711
  const itemsQuery = useContentItems({
684
712
  search: searchQuery.trim() || void 0,
685
713
  status: statusFilter || void 0,
@@ -687,7 +715,8 @@ function ContentItemsPage({ headerActions } = {}) {
687
715
  limit: PAGE_SIZE,
688
716
  offset: pagination.offset
689
717
  });
690
- const items = itemsQuery.data?.data ?? [];
718
+ const fetchedItems = itemsQuery.data?.data ?? [];
719
+ const items = reviewFilterActive ? fetchedItems.filter((item) => item.reviewedAt === null) : fetchedItems;
691
720
  const total = itemsQuery.data?.total ?? 0;
692
721
  const statusOptions = [
693
722
  { value: "", label: "All statuses" },
@@ -698,7 +727,13 @@ function ContentItemsPage({ headerActions } = {}) {
698
727
  ...(contentConfig.stagesByCatalog.pillar ?? []).map((entry) => ({ value: entry.key, label: entry.label }))
699
728
  ];
700
729
  return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
701
- /* @__PURE__ */ jsx(PageTitleCaption, { title: "Content Items", caption: "Every item across the content pipeline" }),
730
+ /* @__PURE__ */ jsx(
731
+ PageTitleCaption,
732
+ {
733
+ title: contentConfig.vocabulary.item.plural,
734
+ caption: reviewFilterActive ? `${contentConfig.vocabulary.item.plural} awaiting review, across every pipeline` : `Every ${contentConfig.vocabulary.item.singular.toLowerCase()} across the content pipeline`
735
+ }
736
+ ),
702
737
  headerActions ? /* @__PURE__ */ jsx(Group, { gap: "xs", children: headerActions }) : null,
703
738
  /* @__PURE__ */ jsx(Paper, { children: /* @__PURE__ */ jsxs(Stack, { children: [
704
739
  /* @__PURE__ */ jsxs(FilterBar, { children: [
@@ -740,11 +775,11 @@ function ContentItemsPage({ headerActions } = {}) {
740
775
  EmptyState,
741
776
  {
742
777
  icon: IconFileText,
743
- title: searchQuery.trim() || statusFilter || pillarFilter ? "No items match your filters" : "No content items yet"
778
+ title: searchQuery.trim() || statusFilter || pillarFilter ? "No items match your filters" : reviewFilterActive ? "Nothing awaiting review" : `No ${contentConfig.vocabulary.item.plural.toLowerCase()} yet`
744
779
  }
745
780
  ) : /* @__PURE__ */ jsxs(Table, { children: [
746
781
  /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
747
- /* @__PURE__ */ jsx(Table.Th, { children: "Title" }),
782
+ /* @__PURE__ */ jsx(Table.Th, { children: contentConfig.vocabulary.item.singular }),
748
783
  /* @__PURE__ */ jsx(Table.Th, { children: "Status" }),
749
784
  /* @__PURE__ */ jsx(Table.Th, { children: "Pillar" }),
750
785
  /* @__PURE__ */ jsx(Table.Th, { children: "Reviewed" }),
@@ -760,7 +795,7 @@ function ContentItemsPage({ headerActions } = {}) {
760
795
  style: { cursor: "pointer" },
761
796
  onClick: () => navigate({ to: "/content/items/$itemId", params: { itemId: item.id } }),
762
797
  children: [
763
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, children: item.title }) }),
798
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, children: getContentItemIdentity(item) }) }),
764
799
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(
765
800
  Badge,
766
801
  {
@@ -774,7 +809,7 @@ function ContentItemsPage({ headerActions } = {}) {
774
809
  ) }),
775
810
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: pillarEntry?.label ?? item.pillar ?? "\u2014" }) }),
776
811
  /* @__PURE__ */ jsx(Table.Td, { children: item.reviewedAt ? /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-success)", children: "Reviewed" }) : /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-warning)", children: "Pending" }) }),
777
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: new Date(item.updatedAt).toLocaleString() }) })
812
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatRelativeTime(item.updatedAt) }) })
778
813
  ]
779
814
  },
780
815
  item.id
@@ -837,38 +872,42 @@ function toPayloadDisplayFields(fields, payload) {
837
872
  const payloadRecord = payload;
838
873
  return fields.filter((field) => field.key in payloadRecord).map((field) => ({ key: field.key, label: field.label, kind: toDisplayKind(field.type) }));
839
874
  }
840
- var REVIEW_ACTIONS = [
841
- {
842
- id: "approved",
843
- label: "Approve",
844
- icon: IconCheck,
845
- // Decision 7, resolved 2026-08-12: approve collects an OPTIONAL note, which is what
846
- // finally gives `content_items.reviewer_feedback` a writer -- the page has always
847
- // rendered it and the API has always accepted it, but nothing in the UI could set it.
848
- // `feedbackRequired` is deliberately omitted: a reviewer with nothing to add still
849
- // approves in two clicks, and a blank note leaves the column NULL rather than ''.
850
- requiresFeedback: true,
851
- feedbackLabel: "Note (optional)",
852
- feedbackPlaceholder: "Anything the next step or a later reader should know?",
853
- confirmationMessage: "Approve this item? It moves on to the next pipeline step."
854
- },
855
- {
856
- id: "rejected",
857
- label: "Reject",
858
- icon: IconX,
859
- tone: "danger",
860
- requiresFeedback: true,
861
- feedbackLabel: "Reason for rejection",
862
- feedbackPlaceholder: "What needs to change before this can go out?",
863
- feedbackRequired: true
864
- }
865
- ];
875
+ var REVIEW_QUEUE_FETCH_LIMIT = 100;
876
+ function buildReviewActions(isTerminalPipeline) {
877
+ return [
878
+ {
879
+ id: "approved",
880
+ label: "Approve",
881
+ icon: IconCheck,
882
+ // Decision 7, resolved 2026-08-12: approve collects an OPTIONAL note, which is what
883
+ // finally gives `content_items.reviewer_feedback` a writer -- the page has always
884
+ // rendered it and the API has always accepted it, but nothing in the UI could set it.
885
+ // `feedbackRequired` is deliberately omitted: a reviewer with nothing to add still
886
+ // approves in two clicks, and a blank note leaves the column NULL rather than ''.
887
+ requiresFeedback: true,
888
+ feedbackLabel: "Note (optional)",
889
+ feedbackPlaceholder: "Anything the next step or a later reader should know?",
890
+ confirmationMessage: isTerminalPipeline ? "Approve this item? There is no further pipeline step -- this is the last internal gate before it is published." : "Approve this item? It moves on to the next pipeline step.",
891
+ consequence: isTerminalPipeline ? "irreversible" : "standard"
892
+ },
893
+ {
894
+ id: "rejected",
895
+ label: "Reject",
896
+ icon: IconX,
897
+ tone: "danger",
898
+ requiresFeedback: true,
899
+ feedbackLabel: "Reason for rejection",
900
+ feedbackPlaceholder: "What needs to change before this can go out?",
901
+ feedbackRequired: true
902
+ }
903
+ ];
904
+ }
866
905
  function DistributionMetricsCell({ metrics }) {
867
- if (!metrics || typeof metrics !== "object") {
906
+ const entries = metrics && typeof metrics === "object" ? Object.entries(metrics) : [];
907
+ if (entries.length === 0) {
868
908
  return /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "\u2014" });
869
909
  }
870
- const entries = Object.entries(metrics);
871
- const isFlat = entries.length > 0 && entries.every(([, value]) => value === null || typeof value !== "object");
910
+ const isFlat = entries.every(([, value]) => value === null || typeof value !== "object");
872
911
  if (!isFlat) {
873
912
  return /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", ff: "monospace", style: { overflowWrap: "anywhere" }, children: JSON.stringify(metrics) });
874
913
  }
@@ -890,9 +929,12 @@ function ContentItemReviewPage({ itemId, headerActions }) {
890
929
  const item = itemQuery.data;
891
930
  const pipeline = item ? contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId) : void 0;
892
931
  const pipelineSteps = useMemo(() => pipeline?.steps ?? [], [pipeline]);
932
+ const isTerminalPipeline = pipelineSteps.length <= 1;
933
+ const reviewActions = useMemo(() => buildReviewActions(isTerminalPipeline), [isTerminalPipeline]);
893
934
  const pipelinePayloadFields = useMemo(() => getPipelinePayloadFields(pipelineSteps), [pipelineSteps]);
894
935
  const primaryTextField = useMemo(() => getPrimaryPayloadTextField(pipelinePayloadFields), [pipelinePayloadFields]);
895
936
  const originalBody = item ? getPayloadText(item.payload, primaryTextField?.key) : "";
937
+ const savedBody = item?.body ?? originalBody;
896
938
  const displayPayloadFields = useMemo(
897
939
  () => toPayloadDisplayFields(pipelinePayloadFields, item?.payload),
898
940
  [pipelinePayloadFields, item?.payload]
@@ -901,9 +943,51 @@ function ContentItemReviewPage({ itemId, headerActions }) {
901
943
  () => getContentProcessingSteps(item?.processingState, pipelineSteps),
902
944
  [item?.processingState, pipelineSteps]
903
945
  );
946
+ const visibleProcessingSteps = useMemo(
947
+ () => processingSteps.some((step) => step.status !== "pending") ? processingSteps : [],
948
+ [processingSteps]
949
+ );
950
+ const pipelineItemsQuery = useContentItems({
951
+ pipelineId: item?.pipelineId ?? void 0,
952
+ limit: REVIEW_QUEUE_FETCH_LIMIT
953
+ });
954
+ const reviewQueue = useMemo(() => {
955
+ if (!item?.pipelineId) return [];
956
+ return (pipelineItemsQuery.data?.data ?? []).filter((candidate) => candidate.reviewedAt === null).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
957
+ }, [pipelineItemsQuery.data, item?.pipelineId]);
958
+ const queueIndex = item ? reviewQueue.findIndex((candidate) => candidate.id === item.id) : -1;
959
+ const queuePosition = queueIndex === -1 ? void 0 : queueIndex + 1;
960
+ const nextQueueItemId = queueIndex === -1 ? void 0 : reviewQueue[queueIndex + 1]?.id;
961
+ const goToNextInQueue = useCallback(() => {
962
+ if (nextQueueItemId) {
963
+ navigate({ to: "/content/items/$itemId", params: { itemId: nextQueueItemId } });
964
+ } else {
965
+ navigate({ to: "/content/items" });
966
+ }
967
+ }, [navigate, nextQueueItemId]);
904
968
  useEffect(() => {
905
969
  if (item) setBody(item.body ?? originalBody);
906
970
  }, [item?.id]);
971
+ useEffect(() => {
972
+ function handleKeyDown(event) {
973
+ const target = event.target;
974
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLElement && target.isContentEditable) {
975
+ return;
976
+ }
977
+ if (event.key === "a" || event.key === "A") {
978
+ event.preventDefault();
979
+ document.querySelector('[data-testid="content-review-action-approved"]')?.click();
980
+ } else if (event.key === "r" || event.key === "R") {
981
+ event.preventDefault();
982
+ document.querySelector('[data-testid="content-review-action-rejected"]')?.click();
983
+ } else if (event.key === "n" || event.key === "N" || event.key === "ArrowRight") {
984
+ event.preventDefault();
985
+ goToNextInQueue();
986
+ }
987
+ }
988
+ window.addEventListener("keydown", handleKeyDown);
989
+ return () => window.removeEventListener("keydown", handleKeyDown);
990
+ }, [goToNextInQueue]);
907
991
  const backButton = /* @__PURE__ */ jsx(
908
992
  Button,
909
993
  {
@@ -945,6 +1029,7 @@ function ContentItemReviewPage({ itemId, headerActions }) {
945
1029
  const distributions = distributionsQuery.data?.data ?? [];
946
1030
  const isSaving = updateItem.isPending || reviewItem.isPending;
947
1031
  const viewingAttempt = attempts.find((attempt) => attempt.id === viewingAttemptId);
1032
+ const identity = getContentItemIdentity(item, pipelinePayloadFields);
948
1033
  const persistBodyIfDirty = async () => {
949
1034
  if (!isDirty) return;
950
1035
  await updateItem.mutateAsync({ itemId: item.id, updates: { body } });
@@ -966,52 +1051,49 @@ function ContentItemReviewPage({ itemId, headerActions }) {
966
1051
  await reviewItem.mutateAsync({ itemId: item.id, review: { decision: "rejected", rejectReason: feedback ?? "" } });
967
1052
  showSuccessNotification("Item rejected");
968
1053
  }
1054
+ goToNextInQueue();
969
1055
  };
970
1056
  return /* @__PURE__ */ jsxs(SubshellContentContainer, { children: [
971
1057
  /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
972
- /* @__PURE__ */ jsx(
973
- PageTitleCaption,
974
- {
975
- title: item.title,
976
- caption: `Pipeline: ${pipeline?.label ?? item.pipelineId ?? "Unassigned"}`,
977
- rightSection: backButton
978
- }
979
- ),
1058
+ /* @__PURE__ */ jsx(PageTitleCaption, { title: identity }),
980
1059
  headerActions ? /* @__PURE__ */ jsx(Group, { gap: "xs", children: headerActions }) : null,
981
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", wrap: "nowrap", children: [
982
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
1060
+ /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", wrap: "nowrap", align: "center", children: [
1061
+ /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "wrap", children: [
1062
+ /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: pipeline?.label ?? item.pipelineId ?? "Unassigned" }),
983
1063
  /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: contentConfig.stageCatalog[item.status]?.label ?? item.status }),
984
1064
  item.pillar && /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: contentConfig.stageCatalog[item.pillar]?.label ?? item.pillar }),
985
- item.reviewedAt ? /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
986
- "Reviewed ",
987
- new Date(item.reviewedAt).toLocaleString()
988
- ] }) : /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Not yet reviewed" })
1065
+ /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
1066
+ item.reviewedAt ? `Reviewed ${new Date(item.reviewedAt).toLocaleDateString()}` : "Not yet reviewed",
1067
+ queuePosition ? ` \xB7 ${queuePosition} of ${reviewQueue.length}` : ""
1068
+ ] })
989
1069
  ] }),
990
- /* @__PURE__ */ jsx(
991
- Button,
992
- {
993
- size: "xs",
994
- variant: "light",
995
- onClick: handleSave,
996
- disabled: !isDirty || isSaving,
997
- loading: updateItem.isPending,
998
- children: "Save"
999
- }
1000
- )
1070
+ /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
1071
+ /* @__PURE__ */ jsx(
1072
+ Button,
1073
+ {
1074
+ size: "xs",
1075
+ variant: "light",
1076
+ onClick: handleSave,
1077
+ disabled: !isDirty || isSaving,
1078
+ loading: updateItem.isPending,
1079
+ children: "Save"
1080
+ }
1081
+ ),
1082
+ backButton
1083
+ ] })
1001
1084
  ] }),
1002
1085
  /* @__PURE__ */ jsx(
1003
1086
  ContentReviewCard,
1004
1087
  {
1005
- title: item.title,
1006
1088
  reviewMode: "live",
1007
1089
  body,
1008
1090
  onBodyChange: setBody,
1009
- originalBody,
1010
- onReset: () => setBody(originalBody),
1091
+ originalBody: savedBody,
1092
+ onReset: () => setBody(savedBody),
1011
1093
  payload: item.payload,
1012
1094
  payloadFields: displayPayloadFields,
1013
- processingSteps,
1014
- actions: REVIEW_ACTIONS,
1095
+ processingSteps: visibleProcessingSteps,
1096
+ actions: reviewActions,
1015
1097
  onAction: handleAction,
1016
1098
  actionsPending: isSaving
1017
1099
  }
@@ -1105,8 +1187,6 @@ function PipelineRow({
1105
1187
  [pipeline.steps, contentConfig]
1106
1188
  );
1107
1189
  const summary = useContentPipelineSummary(pipeline.id, statusKeys, stepResourceIds);
1108
- const artifactsQuery = useContentArtifacts({ pipelineId: pipeline.id });
1109
- const activeArtifacts = artifactsQuery.artifacts.filter((artifact) => artifact.isActive);
1110
1190
  return /* @__PURE__ */ jsxs(
1111
1191
  Table.Tr,
1112
1192
  {
@@ -1117,28 +1197,6 @@ function PipelineRow({
1117
1197
  /* @__PURE__ */ jsx(Text, { fw: 500, children: pipeline.label }),
1118
1198
  pipeline.description ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: pipeline.description }) : null
1119
1199
  ] }),
1120
- /* @__PURE__ */ jsx(Table.Td, { children: summary.isLoading ? /* @__PURE__ */ jsx(Loader, { size: "xs" }) : /* @__PURE__ */ jsx(Group, { gap: 4, wrap: "wrap", children: statusEntries.map((entry) => {
1121
- const colors = getContentStageColor(entry);
1122
- return /* @__PURE__ */ jsxs(
1123
- Badge,
1124
- {
1125
- size: "sm",
1126
- variant: "light",
1127
- styles: { root: { color: colors.accent, backgroundColor: colors.background } },
1128
- children: [
1129
- entry.label,
1130
- ": ",
1131
- summary.countsByStatus[entry.key] ?? 0
1132
- ]
1133
- },
1134
- entry.key
1135
- );
1136
- }) }) }),
1137
- /* @__PURE__ */ jsx(Table.Td, { children: activeArtifacts.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No active artifacts" }) : /* @__PURE__ */ jsx(Group, { gap: 4, wrap: "wrap", children: activeArtifacts.map((artifact) => /* @__PURE__ */ jsxs(Badge, { size: "sm", variant: "outline", children: [
1138
- artifact.kind.replace("content:", ""),
1139
- " v",
1140
- artifact.version
1141
- ] }, artifact.id)) }) }),
1142
1200
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: summary.isLoading ? /* @__PURE__ */ jsx(Loader, { size: "xs" }) : formatLastRun(summary.lastRunAt) }) })
1143
1201
  ]
1144
1202
  }
@@ -1146,158 +1204,323 @@ function PipelineRow({
1146
1204
  }
1147
1205
  function ContentPipelinesPage({ headerActions } = {}) {
1148
1206
  const contentConfig = useContentConfig();
1207
+ const { vocabulary } = contentConfig;
1149
1208
  const statusEntries = contentConfig.stagesByCatalog.status ?? [];
1150
1209
  return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
1151
- /* @__PURE__ */ jsx(PageTitleCaption, { title: "Pipelines", caption: "Every declared content pipeline and its production state" }),
1210
+ /* @__PURE__ */ jsx(
1211
+ PageTitleCaption,
1212
+ {
1213
+ title: vocabulary.pipeline.plural,
1214
+ caption: `Every declared ${vocabulary.pipeline.singular.toLowerCase()} and its production state`
1215
+ }
1216
+ ),
1152
1217
  headerActions ? /* @__PURE__ */ jsx(Group, { gap: "xs", children: headerActions }) : null,
1153
- /* @__PURE__ */ jsx(Paper, { children: /* @__PURE__ */ jsx(Stack, { children: contentConfig.pipelines.length === 0 ? /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(
1218
+ /* @__PURE__ */ jsx(Paper, { children: contentConfig.pipelines.length === 0 ? /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(
1154
1219
  EmptyState,
1155
1220
  {
1156
1221
  icon: IconListDetails,
1157
- title: "No pipelines declared",
1158
- description: "The content System's Organization Model declares no content-pipeline catalog entries yet."
1222
+ title: `No ${vocabulary.pipeline.plural.toLowerCase()} declared`,
1223
+ description: `The content System's Organization Model declares no ${vocabulary.pipeline.singular.toLowerCase()} catalog entries yet.`
1159
1224
  }
1160
1225
  ) }) : /* @__PURE__ */ jsxs(Table, { children: [
1161
1226
  /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
1162
- /* @__PURE__ */ jsx(Table.Th, { children: "Pipeline" }),
1163
- /* @__PURE__ */ jsx(Table.Th, { children: "Items by status" }),
1164
- /* @__PURE__ */ jsx(Table.Th, { children: "Active rules artifacts" }),
1227
+ /* @__PURE__ */ jsx(Table.Th, { children: vocabulary.pipeline.singular }),
1165
1228
  /* @__PURE__ */ jsx(Table.Th, { children: "Last run" })
1166
1229
  ] }) }),
1167
1230
  /* @__PURE__ */ jsx(Table.Tbody, { children: contentConfig.pipelines.map((pipeline) => /* @__PURE__ */ jsx(PipelineRow, { pipeline, statusEntries }, pipeline.id)) })
1168
- ] }) }) })
1231
+ ] }) })
1169
1232
  ] }) }) });
1170
1233
  }
1171
- function OverviewTab({ pipeline, statusEntries }) {
1234
+ var ACTOR_LEAD = {
1235
+ vendor: "An outside service",
1236
+ workflow: "An automated workflow",
1237
+ agent: "An AI agent"
1238
+ };
1239
+ var REVIEW_MODE_TAIL = {
1240
+ live: "and you review it live, in the moment",
1241
+ queued: "then it waits for your review",
1242
+ none: "and moves on without review"
1243
+ };
1244
+ function describeStep(step) {
1245
+ return `${ACTOR_LEAD[step.actor]} handles "${step.label}," ${REVIEW_MODE_TAIL[step.reviewMode]}.`;
1246
+ }
1247
+ function StepRow({ step, resourceId }) {
1248
+ return /* @__PURE__ */ jsxs(Group, { gap: "sm", align: "flex-start", wrap: "nowrap", children: [
1249
+ /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", style: { flexShrink: 0 }, children: step.order }),
1250
+ /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
1251
+ /* @__PURE__ */ jsx(Text, { size: "sm", children: describeStep(step) }),
1252
+ step.description ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: step.description }) : null,
1253
+ resourceId ? /* @__PURE__ */ jsx(Text, { size: "xs", ff: "monospace", c: "dimmed", children: resourceId }) : null
1254
+ ] })
1255
+ ] });
1256
+ }
1257
+ function HowItWorks({ pipeline }) {
1172
1258
  const contentConfig = useContentConfig();
1173
- const statusKeys = useMemo(() => statusEntries.map((entry) => entry.key), [statusEntries]);
1174
- const stepResourceIds = useMemo(
1175
- () => pipeline.steps.map((step) => contentConfig.resolveStepResource(step)?.resourceId).filter((resourceId) => !!resourceId),
1176
- [pipeline.steps, contentConfig]
1177
- );
1178
- const summary = useContentPipelineSummary(pipeline.id, statusKeys, stepResourceIds);
1179
- return /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
1180
- /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
1181
- /* @__PURE__ */ jsx(CardHeader, { title: "Funnel", subtitle: "Item counts across content:catalog/status for this pipeline" }),
1182
- summary.isLoading ? /* @__PURE__ */ jsx(Center, { p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : statusEntries.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No content-stage catalog declared for status yet." }) : /* @__PURE__ */ jsx(Group, { gap: "xs", wrap: "wrap", children: statusEntries.map((entry) => {
1183
- const colors = getContentStageColor(entry);
1184
- return /* @__PURE__ */ jsxs(
1185
- Badge,
1186
- {
1187
- size: "lg",
1188
- variant: "light",
1189
- styles: { root: { color: colors.accent, backgroundColor: colors.background } },
1190
- children: [
1191
- entry.label,
1192
- ": ",
1193
- summary.countsByStatus[entry.key] ?? 0
1194
- ]
1195
- },
1196
- entry.key
1197
- );
1198
- }) })
1199
- ] }) }),
1200
- /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
1201
- /* @__PURE__ */ jsx(CardHeader, { title: "Declared config", subtitle: `${pipeline.steps.length} step(s), in catalog order` }),
1202
- /* @__PURE__ */ jsxs(Table, { children: [
1203
- /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
1204
- /* @__PURE__ */ jsx(Table.Th, { children: "Order" }),
1205
- /* @__PURE__ */ jsx(Table.Th, { children: "Step" }),
1206
- /* @__PURE__ */ jsx(Table.Th, { children: "Actor" }),
1207
- /* @__PURE__ */ jsx(Table.Th, { children: "Review mode" }),
1208
- /* @__PURE__ */ jsx(Table.Th, { children: "Advances to" }),
1209
- /* @__PURE__ */ jsx(Table.Th, { children: "Resource" })
1210
- ] }) }),
1211
- /* @__PURE__ */ jsx(Table.Tbody, { children: pipeline.steps.map((step) => {
1212
- const binding = contentConfig.resolveStepResource(step);
1213
- return /* @__PURE__ */ jsxs(Table.Tr, { children: [
1214
- /* @__PURE__ */ jsx(Table.Td, { children: step.order }),
1215
- /* @__PURE__ */ jsxs(Table.Td, { children: [
1216
- /* @__PURE__ */ jsx(Text, { fw: 500, children: step.label }),
1217
- step.description ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: step.description }) : null
1218
- ] }),
1219
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: step.actor }) }),
1220
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: step.reviewMode }) }),
1221
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: step.advancesTo ?? "\u2014" }) }),
1222
- /* @__PURE__ */ jsx(Table.Td, { children: binding ? /* @__PURE__ */ jsx(Text, { size: "xs", ff: "monospace", c: "dimmed", children: binding.resourceId }) : /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "No resource configured" }) })
1223
- ] }, step.key);
1224
- }) })
1225
- ] })
1259
+ return /* @__PURE__ */ jsx(Accordion, { variant: "separated", children: /* @__PURE__ */ jsxs(Accordion.Item, { value: "how-it-works", children: [
1260
+ /* @__PURE__ */ jsx(Accordion.Control, { icon: /* @__PURE__ */ jsx(IconInfoCircle, { size: 16 }), children: /* @__PURE__ */ jsxs(Text, { size: "sm", fw: 500, children: [
1261
+ "How this ",
1262
+ contentConfig.vocabulary.pipeline.singular.toLowerCase(),
1263
+ " works"
1226
1264
  ] }) }),
1227
- pipeline.color ? /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
1228
- /* @__PURE__ */ jsx(Title, { order: 6, c: "dimmed", children: "Color" }),
1229
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "outline", color: pipeline.color, children: pipeline.color })
1265
+ /* @__PURE__ */ jsx(Accordion.Panel, { children: /* @__PURE__ */ jsx(Stack, { gap: "sm", children: pipeline.steps.map((step) => /* @__PURE__ */ jsx(StepRow, { step, resourceId: contentConfig.resolveStepResource(step)?.resourceId }, step.key)) }) })
1266
+ ] }) });
1267
+ }
1268
+ var BOARD_FETCH_LIMIT = 100;
1269
+ function formatAge(ms) {
1270
+ const seconds = Math.floor(ms / 1e3);
1271
+ if (seconds < 60) return `${seconds}s`;
1272
+ const minutes = Math.floor(seconds / 60);
1273
+ if (minutes < 60) return `${minutes}m`;
1274
+ const hours = Math.floor(minutes / 60);
1275
+ if (hours < 24) return `${hours}h`;
1276
+ return `${Math.floor(hours / 24)}d`;
1277
+ }
1278
+ function BoardItemCard({ card, onOpen }) {
1279
+ return /* @__PURE__ */ jsx(Card, { withBorder: true, padding: "xs", onClick: () => onOpen(card.item.id), style: { cursor: "pointer" }, children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
1280
+ /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, lineClamp: 2, children: getContentItemIdentity(card.item) }),
1281
+ /* @__PURE__ */ jsxs(Group, { gap: 6, justify: "space-between", wrap: "nowrap", children: [
1282
+ /* @__PURE__ */ jsxs(Group, { gap: 4, wrap: "nowrap", children: [
1283
+ /* @__PURE__ */ jsx(IconClockHour4, { size: 12, style: { color: "var(--color-text-dimmed)" } }),
1284
+ /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: formatAge(card.ageMs) })
1285
+ ] }),
1286
+ card.awaitingReview ? /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "light", children: "Awaiting review" }) : null
1287
+ ] })
1288
+ ] }) });
1289
+ }
1290
+ function BoardColumn({ column, onOpen }) {
1291
+ return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "sm", style: { minWidth: 260, maxWidth: 260, flexShrink: 0 }, children: /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
1292
+ /* @__PURE__ */ jsxs(Group, { justify: "space-between", wrap: "nowrap", children: [
1293
+ /* @__PURE__ */ jsx(Text, { fw: 600, size: "sm", truncate: true, children: column.step.label }),
1294
+ /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: column.cards.length })
1295
+ ] }),
1296
+ column.cards.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Nothing here" }) : /* @__PURE__ */ jsx(Stack, { gap: "xs", children: column.cards.map((card) => /* @__PURE__ */ jsx(BoardItemCard, { card, onOpen }, card.item.id)) })
1297
+ ] }) });
1298
+ }
1299
+ function ColumnBoard({ board, onOpen }) {
1300
+ return /* @__PURE__ */ jsx(ScrollArea, { type: "auto", offsetScrollbars: true, children: /* @__PURE__ */ jsx(Group, { gap: "xs", align: "flex-start", wrap: "nowrap", children: board.columns.map((column) => /* @__PURE__ */ jsx(BoardColumn, { column, onOpen }, column.step.key)) }) });
1301
+ }
1302
+ function CollapsedBoard({
1303
+ board,
1304
+ itemNounPlural,
1305
+ onOpen
1306
+ }) {
1307
+ const column = board.columns[0];
1308
+ if (!column) {
1309
+ return /* @__PURE__ */ jsx(
1310
+ EmptyState,
1311
+ {
1312
+ icon: IconRoute,
1313
+ title: "No steps declared",
1314
+ description: "This pipeline has no steps yet, so nothing has anywhere to sit."
1315
+ }
1316
+ );
1317
+ }
1318
+ const waiting = column.cards.filter((card) => card.awaitingReview);
1319
+ const settled = column.cards.filter((card) => !card.awaitingReview);
1320
+ const hasGate = !!column.gate;
1321
+ return /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
1322
+ /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
1323
+ /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, children: hasGate ? "Waiting on review" : column.step.label }),
1324
+ waiting.length === 0 ? column.cards.length === 0 ? /* @__PURE__ */ jsx(
1325
+ EmptyState,
1326
+ {
1327
+ icon: IconChecklist,
1328
+ title: `No ${itemNounPlural.toLowerCase()} yet`,
1329
+ description: `Produce one and it lands at "${column.step.label}."`
1330
+ }
1331
+ ) : /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "Nothing is waiting on review right now." }) : /* @__PURE__ */ jsx(Stack, { gap: "xs", children: waiting.map((card) => /* @__PURE__ */ jsx(BoardItemCard, { card, onOpen }, card.item.id)) })
1332
+ ] }),
1333
+ settled.length > 0 ? /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
1334
+ /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", tt: "uppercase", fw: 600, children: hasGate ? "Already reviewed" : "In progress" }),
1335
+ /* @__PURE__ */ jsx(Stack, { gap: "xs", children: settled.map((card) => /* @__PURE__ */ jsx(BoardItemCard, { card, onOpen }, card.item.id)) })
1230
1336
  ] }) : null
1231
1337
  ] });
1232
1338
  }
1233
- function SourcesTab() {
1234
- return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "xl", children: /* @__PURE__ */ jsx(
1235
- EmptyState,
1236
- {
1237
- icon: IconFolderOff,
1238
- title: "Source assets aren't wired up yet",
1239
- description: "content_source_assets has no API route or reader today. This tab will list a pipeline's source footage and files once that surface exists."
1240
- }
1241
- ) });
1339
+ function BoardOverflowSection({
1340
+ title,
1341
+ description,
1342
+ cards,
1343
+ onOpen
1344
+ }) {
1345
+ return /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
1346
+ /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
1347
+ /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", tt: "uppercase", fw: 600, children: title }),
1348
+ /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: description })
1349
+ ] }),
1350
+ /* @__PURE__ */ jsx(Group, { gap: "xs", align: "flex-start", wrap: "wrap", children: cards.map((card) => /* @__PURE__ */ jsx(Box, { style: { width: 260 }, children: /* @__PURE__ */ jsx(BoardItemCard, { card, onOpen }) }, card.item.id)) })
1351
+ ] });
1242
1352
  }
1243
- function DefaultProduceTab({ pipeline }) {
1353
+ function BoardTab({ pipeline }) {
1354
+ const navigate = useNavigate();
1244
1355
  const contentConfig = useContentConfig();
1245
- const canManageContent = useAccess(AccessKeys.contentManage);
1246
- const [contentItemId, setContentItemId] = useState(null);
1247
- const itemsQuery = useContentItems({ pipelineId: pipeline.id, limit: 100 });
1248
- const items = itemsQuery.data?.data ?? [];
1356
+ const itemsQuery = useContentItems({ pipelineId: pipeline.id, limit: BOARD_FETCH_LIMIT });
1357
+ const items = useMemo(() => itemsQuery.data?.data ?? [], [itemsQuery.data]);
1358
+ const board = useMemo(() => deriveContentBoard(items, pipeline, Date.now()), [items, pipeline]);
1359
+ const openItem = (itemId) => navigate({ to: "/content/items/$itemId", params: { itemId } });
1360
+ if (itemsQuery.isLoading) {
1361
+ return /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) });
1362
+ }
1363
+ if (itemsQuery.isError) {
1364
+ return /* @__PURE__ */ jsx(
1365
+ CenteredErrorState,
1366
+ {
1367
+ error: itemsQuery.error,
1368
+ title: `Failed to load ${contentConfig.vocabulary.item.plural.toLowerCase()}`
1369
+ }
1370
+ );
1371
+ }
1372
+ if (items.length === 0) {
1373
+ return /* @__PURE__ */ jsx(
1374
+ EmptyState,
1375
+ {
1376
+ icon: IconChecklist,
1377
+ title: `No ${contentConfig.vocabulary.item.plural.toLowerCase()} in this pipeline yet`,
1378
+ description: "Produce something from the Produce tab and it will show up here."
1379
+ }
1380
+ );
1381
+ }
1249
1382
  return /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
1250
- /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsx(
1251
- Select,
1383
+ board.isCollapsed ? /* @__PURE__ */ jsx(CollapsedBoard, { board, itemNounPlural: contentConfig.vocabulary.item.plural, onOpen: openItem }) : /* @__PURE__ */ jsx(ColumnBoard, { board, onOpen: openItem }),
1384
+ board.done.length > 0 ? /* @__PURE__ */ jsx(
1385
+ BoardOverflowSection,
1252
1386
  {
1253
- label: "Content item",
1254
- description: "The item each step runs against. Steps stay disabled until one is chosen.",
1255
- placeholder: items.length > 0 ? "Select a content item" : "No items in this pipeline yet",
1256
- data: items.map((item) => ({ value: item.id, label: `${item.title} (${item.status})` })),
1257
- value: contentItemId,
1258
- onChange: setContentItemId,
1259
- disabled: itemsQuery.isLoading || items.length === 0,
1260
- searchable: true,
1261
- clearable: true
1387
+ title: "Done",
1388
+ description: "Every declared step has succeeded for these.",
1389
+ cards: board.done,
1390
+ onOpen: openItem
1262
1391
  }
1263
- ) }),
1264
- contentItemId === null && items.length > 0 && /* @__PURE__ */ jsx(Alert, { variant: "light", children: "Select a content item above to run any of this pipeline's steps." }),
1265
- pipeline.steps.map((step) => {
1266
- const binding = contentConfig.resolveStepResource(step);
1267
- const isRunnable = binding?.resourceKind === "workflow" || binding?.resourceKind === "agent";
1268
- return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Group, { justify: "space-between", align: "flex-start", wrap: "nowrap", children: [
1269
- /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
1270
- /* @__PURE__ */ jsx(CardHeader, { title: step.label, subtitle: step.description ?? `Step ${step.order}` }),
1271
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
1272
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: step.actor }),
1273
- /* @__PURE__ */ jsxs(Badge, { size: "sm", variant: "light", children: [
1274
- step.reviewMode,
1275
- " review"
1276
- ] })
1277
- ] })
1278
- ] }),
1279
- isRunnable && canManageContent.allowed ? /* @__PURE__ */ jsx(
1280
- RunResourceButton,
1281
- {
1282
- resourceId: binding.resourceId,
1283
- resourceType: binding.resourceKind,
1284
- icon: /* @__PURE__ */ jsx(IconPlayerPlay, { size: 14 }),
1285
- label: "Run",
1286
- title: step.label,
1287
- disabled: contentItemId === null,
1288
- baseInput: contentItemId !== null ? { contentItemId } : void 0,
1289
- getInput: () => step.payloadFields.length > 0 ? { schema: compileContentPayloadValidator(step.payloadFields) } : {}
1290
- }
1291
- ) : /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", maw: 220, ta: "right", children: !canManageContent.allowed ? "You do not have permission to run content pipeline steps." : binding ? `Resource kind "${binding.resourceKind}" isn't directly runnable from here.` : "No resource configured for this step yet." })
1292
- ] }) }, step.key);
1293
- })
1392
+ ) : null,
1393
+ board.unplaced.length > 0 ? /* @__PURE__ */ jsx(
1394
+ BoardOverflowSection,
1395
+ {
1396
+ title: "Not on this pipeline's current steps",
1397
+ description: "Their recorded steps aren't in the step catalog anymore -- shown honestly rather than dropped into the first column.",
1398
+ cards: board.unplaced,
1399
+ onOpen: openItem
1400
+ }
1401
+ ) : null,
1402
+ /* @__PURE__ */ jsx(HowItWorks, { pipeline })
1294
1403
  ] });
1295
1404
  }
1296
- function ProduceTab({ pipeline, render }) {
1297
- if (render) return /* @__PURE__ */ jsx(Fragment, { children: render({ pipeline }) });
1298
- return /* @__PURE__ */ jsx(DefaultProduceTab, { pipeline });
1405
+ var PAGE_SIZE2 = 20;
1406
+ var FETCH_LIMIT = 100;
1407
+ function summarizeMetrics(metrics) {
1408
+ if (!metrics || typeof metrics !== "object" || Array.isArray(metrics)) return "\u2014";
1409
+ const entries = Object.entries(metrics);
1410
+ if (entries.length === 0) return "\u2014";
1411
+ const shown = entries.slice(0, 2).map(([key, value]) => `${key} ${String(value)}`).join(" \xB7 ");
1412
+ return entries.length > 2 ? `${shown} +${entries.length - 2}` : shown;
1299
1413
  }
1300
1414
  function formatDateTime(value) {
1415
+ return value ? new Date(value).toLocaleDateString() : "\u2014";
1416
+ }
1417
+ function DistributionsTab({ pipeline }) {
1418
+ const navigate = useNavigate();
1419
+ const contentConfig = useContentConfig();
1420
+ const distributionLabel = contentConfig.vocabulary.distribution.plural.toLowerCase();
1421
+ const itemsQuery = useContentItems({ pipelineId: pipeline.id, limit: FETCH_LIMIT });
1422
+ const distributionsQuery = useContentDistributions({ limit: FETCH_LIMIT });
1423
+ const itemById = useMemo(() => {
1424
+ const map = /* @__PURE__ */ new Map();
1425
+ for (const item of itemsQuery.data?.data ?? []) map.set(item.id, item);
1426
+ return map;
1427
+ }, [itemsQuery.data]);
1428
+ const distributions = useMemo(
1429
+ () => (distributionsQuery.data?.data ?? []).filter((distribution) => itemById.has(distribution.contentItemId)),
1430
+ [distributionsQuery.data, itemById]
1431
+ );
1432
+ const pagination = usePaginationState(PAGE_SIZE2, [pipeline.id], distributions.length);
1433
+ const paginatedDistributions = distributions.slice(pagination.offset, pagination.offset + PAGE_SIZE2);
1434
+ const isLoading = itemsQuery.isLoading || distributionsQuery.isLoading;
1435
+ const error = itemsQuery.error ?? distributionsQuery.error;
1436
+ const resolveIdentity = (distribution) => {
1437
+ const item = itemById.get(distribution.contentItemId);
1438
+ return item ? getContentItemIdentity(item) : distribution.contentItemId;
1439
+ };
1440
+ if (isLoading) {
1441
+ return /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) });
1442
+ }
1443
+ if (itemsQuery.isError || distributionsQuery.isError) {
1444
+ return /* @__PURE__ */ jsx(CenteredErrorState, { error, title: `Failed to load ${distributionLabel}` });
1445
+ }
1446
+ if (distributions.length === 0) {
1447
+ return /* @__PURE__ */ jsx(
1448
+ EmptyState,
1449
+ {
1450
+ icon: IconShare,
1451
+ title: `No ${distributionLabel} yet`,
1452
+ description: `Nothing from this pipeline has been distributed anywhere yet.`
1453
+ }
1454
+ );
1455
+ }
1456
+ return /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
1457
+ /* @__PURE__ */ jsxs(Table, { children: [
1458
+ /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
1459
+ /* @__PURE__ */ jsx(Table.Th, { children: contentConfig.vocabulary.item.singular }),
1460
+ /* @__PURE__ */ jsx(Table.Th, { children: "Platform" }),
1461
+ /* @__PURE__ */ jsx(Table.Th, { style: { whiteSpace: "nowrap" }, children: "Publish method" }),
1462
+ /* @__PURE__ */ jsx(Table.Th, { style: { whiteSpace: "nowrap" }, children: "Platform post ID" }),
1463
+ /* @__PURE__ */ jsx(Table.Th, { children: "Metrics" }),
1464
+ /* @__PURE__ */ jsx(Table.Th, { style: { whiteSpace: "nowrap" }, children: "Metrics updated" }),
1465
+ /* @__PURE__ */ jsx(Table.Th, { children: "Published" }),
1466
+ /* @__PURE__ */ jsx(Table.Th, {})
1467
+ ] }) }),
1468
+ /* @__PURE__ */ jsx(Table.Tbody, { children: paginatedDistributions.map((distribution) => {
1469
+ const platformEntry = contentConfig.stageCatalog[distribution.platform];
1470
+ const platformColors = getContentStageColor(platformEntry);
1471
+ return /* @__PURE__ */ jsxs(
1472
+ Table.Tr,
1473
+ {
1474
+ style: { cursor: "pointer" },
1475
+ onClick: () => navigate({
1476
+ to: "/content/distributions/$distributionId",
1477
+ params: { distributionId: distribution.id }
1478
+ }),
1479
+ children: [
1480
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, lineClamp: 1, children: resolveIdentity(distribution) }) }),
1481
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(
1482
+ Badge,
1483
+ {
1484
+ size: "sm",
1485
+ variant: "light",
1486
+ styles: { root: { color: platformColors.accent, backgroundColor: platformColors.background } },
1487
+ children: platformEntry?.label ?? distribution.platform
1488
+ }
1489
+ ) }),
1490
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: distribution.publishMethod ?? "\u2014" }) }),
1491
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "xs", ff: "monospace", c: "dimmed", children: distribution.platformPostId ?? "\u2014" }) }),
1492
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: summarizeMetrics(distribution.metrics) }) }),
1493
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDateTime(distribution.metricsUpdatedAt) }) }),
1494
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDateTime(distribution.publishedAt) }) }),
1495
+ /* @__PURE__ */ jsx(Table.Td, { children: distribution.platformUrl ? /* @__PURE__ */ jsx(
1496
+ Anchor,
1497
+ {
1498
+ href: distribution.platformUrl,
1499
+ target: "_blank",
1500
+ rel: "noopener noreferrer",
1501
+ style: { display: "inline-flex" },
1502
+ onClick: (event) => event.stopPropagation(),
1503
+ children: /* @__PURE__ */ jsx(IconExternalLink, { size: 16 })
1504
+ }
1505
+ ) : null })
1506
+ ]
1507
+ },
1508
+ distribution.id
1509
+ );
1510
+ }) })
1511
+ ] }),
1512
+ distributions.length > PAGE_SIZE2 ? /* @__PURE__ */ jsx(Group, { justify: "center", children: /* @__PURE__ */ jsx(
1513
+ Pagination,
1514
+ {
1515
+ value: pagination.page,
1516
+ onChange: pagination.setPage,
1517
+ total: pagination.totalPages(distributions.length),
1518
+ size: "sm"
1519
+ }
1520
+ ) }) : null
1521
+ ] });
1522
+ }
1523
+ function formatDateTime2(value) {
1301
1524
  return value ? new Date(value).toLocaleString() : "\u2014";
1302
1525
  }
1303
1526
  function formatDuration(startTime, endTime) {
@@ -1338,7 +1561,7 @@ function StepRunsSection({ step, resourceId }) {
1338
1561
  /* @__PURE__ */ jsx(Table.Tbody, { children: executions.map((execution) => /* @__PURE__ */ jsxs(Table.Tr, { children: [
1339
1562
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "xs", ff: "monospace", c: "dimmed", children: execution.id }) }),
1340
1563
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", styles: { root: { color: getStatusColor(execution.status) } }, children: execution.status }) }),
1341
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDateTime(execution.startTime) }) }),
1564
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDateTime2(execution.startTime) }) }),
1342
1565
  /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDuration(execution.startTime, execution.endTime) }) })
1343
1566
  ] }, execution.id)) })
1344
1567
  ] })
@@ -1348,19 +1571,18 @@ function RunsTab({ pipeline }) {
1348
1571
  const contentConfig = useContentConfig();
1349
1572
  return /* @__PURE__ */ jsx(Stack, { gap: "md", children: pipeline.steps.map((step) => /* @__PURE__ */ jsx(StepRunsSection, { step, resourceId: contentConfig.resolveStepResource(step)?.resourceId }, step.key)) });
1350
1573
  }
1351
- var PIPELINE_WORKSPACE_TABS = ["overview", "sources", "produce", "runs"];
1574
+ var PIPELINE_WORKSPACE_TABS = ["board", "distributions", "runs"];
1352
1575
  function isPipelineWorkspaceTab(value) {
1353
1576
  return typeof value === "string" && PIPELINE_WORKSPACE_TABS.includes(value);
1354
1577
  }
1355
1578
  function ContentPipelineWorkspacePage({
1356
1579
  pipelineId,
1357
- headerActions,
1358
- renderProduce
1580
+ headerActions
1359
1581
  }) {
1360
1582
  const navigate = useNavigate();
1361
1583
  const contentConfig = useContentConfig();
1362
1584
  const rawSearch = useSearch({ strict: false });
1363
- const activeTab = isPipelineWorkspaceTab(rawSearch.tab) ? rawSearch.tab : "overview";
1585
+ const activeTab = isPipelineWorkspaceTab(rawSearch.tab) ? rawSearch.tab : "board";
1364
1586
  const handleTabChange = useCallback(
1365
1587
  (value) => {
1366
1588
  if (!isPipelineWorkspaceTab(value) || value === activeTab) return;
@@ -1373,7 +1595,7 @@ function ContentPipelineWorkspacePage({
1373
1595
  [activeTab, navigate]
1374
1596
  );
1375
1597
  const pipeline = contentConfig.pipelines.find((candidate) => candidate.id === pipelineId);
1376
- const statusEntries = contentConfig.stagesByCatalog.status ?? [];
1598
+ const vocabulary = contentConfig.vocabulary;
1377
1599
  const backButton = /* @__PURE__ */ jsx(
1378
1600
  Button,
1379
1601
  {
@@ -1411,109 +1633,19 @@ function ContentPipelineWorkspacePage({
1411
1633
  }
1412
1634
  ),
1413
1635
  headerActions ? /* @__PURE__ */ jsx(Group, { gap: "xs", children: headerActions }) : null,
1414
- /* @__PURE__ */ jsx(Group, { gap: "xs", children: /* @__PURE__ */ jsxs(Badge, { size: "sm", variant: "light", children: [
1415
- pipeline.steps.length,
1416
- " step(s)"
1417
- ] }) }),
1418
- /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsxs(Tabs, { value: activeTab, onChange: handleTabChange, keepMounted: false, children: [
1636
+ /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Tabs, { value: activeTab, onChange: handleTabChange, keepMounted: false, children: [
1419
1637
  /* @__PURE__ */ jsxs(Tabs.List, { children: [
1420
- /* @__PURE__ */ jsx(Tabs.Tab, { value: "overview", leftSection: /* @__PURE__ */ jsx(IconLayoutGrid, { size: 14 }), children: "Overview" }),
1421
- /* @__PURE__ */ jsx(Tabs.Tab, { value: "sources", leftSection: /* @__PURE__ */ jsx(IconFolder, { size: 14 }), children: "Sources" }),
1422
- /* @__PURE__ */ jsx(Tabs.Tab, { value: "produce", leftSection: /* @__PURE__ */ jsx(IconBolt, { size: 14 }), children: "Produce" }),
1638
+ /* @__PURE__ */ jsx(Tabs.Tab, { value: "board", leftSection: /* @__PURE__ */ jsx(IconLayoutGrid, { size: 14 }), children: vocabulary.board }),
1639
+ /* @__PURE__ */ jsx(Tabs.Tab, { value: "distributions", leftSection: /* @__PURE__ */ jsx(IconShare, { size: 14 }), children: vocabulary.distribution.plural }),
1423
1640
  /* @__PURE__ */ jsx(Tabs.Tab, { value: "runs", leftSection: /* @__PURE__ */ jsx(IconPlayerPlay, { size: 14 }), children: "Runs" })
1424
1641
  ] }),
1425
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "overview", pt: "sm", children: /* @__PURE__ */ jsx(OverviewTab, { pipeline, statusEntries }) }),
1426
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "sources", pt: "sm", children: /* @__PURE__ */ jsx(SourcesTab, {}) }),
1427
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "produce", pt: "sm", children: /* @__PURE__ */ jsx(ProduceTab, { pipeline, render: renderProduce }) }),
1428
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "runs", pt: "sm", children: /* @__PURE__ */ jsx(RunsTab, { pipeline }) })
1642
+ /* @__PURE__ */ jsx(Tabs.Panel, { value: "board", pt: "md", children: /* @__PURE__ */ jsx(BoardTab, { pipeline }) }),
1643
+ /* @__PURE__ */ jsx(Tabs.Panel, { value: "distributions", pt: "md", children: /* @__PURE__ */ jsx(DistributionsTab, { pipeline }) }),
1644
+ /* @__PURE__ */ jsx(Tabs.Panel, { value: "runs", pt: "md", children: /* @__PURE__ */ jsx(RunsTab, { pipeline }) })
1429
1645
  ] }) })
1430
1646
  ] }) }) });
1431
1647
  }
1432
- function formatDateTime2(value) {
1433
- return new Date(value).toLocaleString();
1434
- }
1435
- function ContentSettingsPage({ headerActions } = {}) {
1436
- const contentConfig = useContentConfig();
1437
- const [selectedPipelineId, setSelectedPipelineId] = useState(void 0);
1438
- const pipelineId = selectedPipelineId ?? contentConfig.defaultPipelineId;
1439
- const pipelineOptions = useMemo(
1440
- () => contentConfig.pipelines.map((pipeline) => ({ value: pipeline.id, label: pipeline.label })),
1441
- [contentConfig.pipelines]
1442
- );
1443
- const artifactsQuery = useContentArtifacts({ pipelineId });
1444
- const artifactsByKind = useMemo(() => {
1445
- const groups = /* @__PURE__ */ new Map();
1446
- for (const artifact of artifactsQuery.artifacts) {
1447
- const existing = groups.get(artifact.kind) ?? [];
1448
- existing.push(artifact);
1449
- groups.set(artifact.kind, existing);
1450
- }
1451
- for (const [, entries] of groups) {
1452
- entries.sort((a, b) => b.version - a.version);
1453
- }
1454
- return groups;
1455
- }, [artifactsQuery.artifacts]);
1456
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
1457
- /* @__PURE__ */ jsx(
1458
- PageTitleCaption,
1459
- {
1460
- title: "Rules and Artifacts",
1461
- caption: "Browse content: rules documents and their version history"
1462
- }
1463
- ),
1464
- headerActions ? /* @__PURE__ */ jsx(Group, { gap: "xs", children: headerActions }) : null,
1465
- pipelineOptions.length === 0 ? /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }), color: "yellow", variant: "light", children: "No content pipelines are declared yet, so there is nothing to scope artifacts to." }) }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1466
- /* @__PURE__ */ jsx(Group, { gap: "xs", align: "flex-end", children: /* @__PURE__ */ jsx(
1467
- Select,
1468
- {
1469
- label: "Pipeline",
1470
- data: pipelineOptions,
1471
- value: pipelineId ?? null,
1472
- onChange: (value) => setSelectedPipelineId(value ?? void 0),
1473
- allowDeselect: false,
1474
- w: 280
1475
- }
1476
- ) }),
1477
- /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsx(Stack, { gap: "md", children: artifactsQuery.isLoading ? /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) : artifactsQuery.isError ? /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }), color: "red", variant: "light", children: "Failed to load artifacts." }) : artifactsByKind.size === 0 ? /* @__PURE__ */ jsx(
1478
- EmptyState,
1479
- {
1480
- icon: IconFileText,
1481
- title: "No content: artifacts for this pipeline",
1482
- description: "Rules documents (idea banks, ICP docs, style guides) created through the artifacts tool will appear here, grouped by kind."
1483
- }
1484
- ) : [...artifactsByKind.entries()].map(([kind, versions]) => /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
1485
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
1486
- /* @__PURE__ */ jsx(Text, { fw: 600, children: kind }),
1487
- /* @__PURE__ */ jsxs(Badge, { size: "sm", variant: "light", children: [
1488
- versions.length,
1489
- " version",
1490
- versions.length === 1 ? "" : "s"
1491
- ] })
1492
- ] }),
1493
- /* @__PURE__ */ jsxs(Table, { children: [
1494
- /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
1495
- /* @__PURE__ */ jsx(Table.Th, { children: "Version" }),
1496
- /* @__PURE__ */ jsx(Table.Th, { children: "Active" }),
1497
- /* @__PURE__ */ jsx(Table.Th, { children: "Created" }),
1498
- /* @__PURE__ */ jsx(Table.Th, { children: "Updated" }),
1499
- /* @__PURE__ */ jsx(Table.Th, { children: "Source execution" })
1500
- ] }) }),
1501
- /* @__PURE__ */ jsx(Table.Tbody, { children: versions.map((artifact) => /* @__PURE__ */ jsxs(Table.Tr, { children: [
1502
- /* @__PURE__ */ jsxs(Table.Td, { children: [
1503
- "v",
1504
- artifact.version
1505
- ] }),
1506
- /* @__PURE__ */ jsx(Table.Td, { children: artifact.isActive ? /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-success)", children: "Active" }) : /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-text-dimmed)", children: "Inactive" }) }),
1507
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDateTime2(artifact.createdAt) }) }),
1508
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDateTime2(artifact.updatedAt) }) }),
1509
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", ff: "monospace", children: artifact.sourceExecutionId ?? "\u2014" }) })
1510
- ] }, artifact.id)) })
1511
- ] })
1512
- ] }, kind)) }) })
1513
- ] })
1514
- ] }) }) });
1515
- }
1516
- var PAGE_SIZE2 = 20;
1648
+ var PAGE_SIZE3 = 20;
1517
1649
  var QUERY_LIMIT = 100;
1518
1650
  function formatDate(value) {
1519
1651
  return value ? new Date(value).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "\u2014";
@@ -1553,11 +1685,11 @@ function ContentDistributionsPage({ headerActions } = {}) {
1553
1685
  ...(contentConfig.stagesByCatalog.platform ?? []).map((entry) => ({ value: entry.key, label: entry.label }))
1554
1686
  ];
1555
1687
  const pagination = usePaginationState(
1556
- PAGE_SIZE2,
1688
+ PAGE_SIZE3,
1557
1689
  [platformFilter, statusFilter, formatFilter],
1558
1690
  filteredDistributions.length
1559
1691
  );
1560
- const paginatedDistributions = filteredDistributions.slice(pagination.offset, pagination.offset + PAGE_SIZE2);
1692
+ const paginatedDistributions = filteredDistributions.slice(pagination.offset, pagination.offset + PAGE_SIZE3);
1561
1693
  const resolveTitle = (distribution) => itemTitleById.get(distribution.contentItemId) ?? distribution.contentItemId;
1562
1694
  return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
1563
1695
  /* @__PURE__ */ jsx(
@@ -1662,7 +1794,7 @@ function ContentDistributionsPage({ headerActions } = {}) {
1662
1794
  );
1663
1795
  }) })
1664
1796
  ] }),
1665
- filteredDistributions.length > PAGE_SIZE2 && /* @__PURE__ */ jsx(Group, { justify: "center", children: /* @__PURE__ */ jsx(
1797
+ filteredDistributions.length > PAGE_SIZE3 && /* @__PURE__ */ jsx(Group, { justify: "center", children: /* @__PURE__ */ jsx(
1666
1798
  Pagination,
1667
1799
  {
1668
1800
  value: pagination.page,
@@ -2177,4 +2309,4 @@ function ContentDistributionDetailPage({
2177
2309
  ] });
2178
2310
  }
2179
2311
 
2180
- export { AlternatesPanel, ContentDistributionDetailPage, ContentDistributionsPage, ContentItemReviewPage, ContentItemsPage, ContentOverviewPage, ContentPipelineWorkspacePage, ContentPipelinesPage, ContentReviewCard, ContentSettingsPage, PayloadBody, ProcessingStateStrip, ReviewActionBar, firstLine, formatElapsed, lastLine, resolveContentStepResource, swapFirstLine, swapLastLine, useContentConfig };
2312
+ export { AlternatesPanel, ContentDistributionDetailPage, ContentDistributionsPage, ContentItemReviewPage, ContentItemsPage, ContentOverviewPage, ContentPipelineWorkspacePage, ContentPipelinesPage, ContentReviewCard, PayloadBody, ProcessingStateStrip, ReviewActionBar, firstLine, formatElapsed, lastLine, swapFirstLine, swapLastLine };