@calliopelabs/ui 0.7.0 → 0.12.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 (124) hide show
  1. package/README.md +266 -3
  2. package/dist/appearance/AppearancePicker.js +4 -2
  3. package/dist/appearance/AppearanceProvider.d.ts +10 -2
  4. package/dist/appearance/AppearanceProvider.js +6 -2
  5. package/dist/appearance/AppearanceScript.d.ts +15 -4
  6. package/dist/appearance/AppearanceScript.js +11 -5
  7. package/dist/appearance/appearance.js +1 -1
  8. package/dist/appearance/boot.d.ts +13 -2
  9. package/dist/appearance/boot.js +35 -5
  10. package/dist/appearance/product.d.ts +13 -0
  11. package/dist/appearance/product.js +2 -2
  12. package/dist/appearance/runtime-brand.d.ts +106 -0
  13. package/dist/appearance/runtime-brand.js +420 -0
  14. package/dist/css/appearance.js +147 -0
  15. package/dist/css/calliope.css +1221 -0
  16. package/dist/css/components.css +729 -0
  17. package/dist/css/tokens.css +491 -0
  18. package/dist/data/DefinitionList.d.ts +16 -0
  19. package/dist/data/DefinitionList.js +9 -0
  20. package/dist/data/Stat.d.ts +25 -0
  21. package/dist/data/Stat.js +19 -0
  22. package/dist/data/Tag.d.ts +21 -0
  23. package/dist/data/Tag.js +18 -0
  24. package/dist/data/types.d.ts +3 -2
  25. package/dist/data/use-local-table.d.ts +36 -0
  26. package/dist/data/use-local-table.js +127 -0
  27. package/dist/index.d.ts +44 -3
  28. package/dist/index.js +40 -1
  29. package/dist/nav/Nav.d.ts +2 -1
  30. package/dist/nav/Nav.js +34 -15
  31. package/dist/nav/model.d.ts +21 -0
  32. package/dist/page/PageHeader.d.ts +5 -3
  33. package/dist/page/PageHeader.js +5 -4
  34. package/dist/page/Panel.d.ts +19 -0
  35. package/dist/page/Panel.js +12 -0
  36. package/dist/primitives/Avatar.d.ts +37 -0
  37. package/dist/primitives/Avatar.js +43 -0
  38. package/dist/primitives/Field.d.ts +33 -0
  39. package/dist/primitives/Field.js +30 -0
  40. package/dist/primitives/Segmented.d.ts +30 -0
  41. package/dist/primitives/Segmented.js +20 -0
  42. package/dist/primitives/Select.d.ts +40 -0
  43. package/dist/primitives/Select.js +47 -0
  44. package/dist/primitives/Switch.d.ts +7 -0
  45. package/dist/primitives/Switch.js +8 -0
  46. package/dist/primitives/Textarea.d.ts +3 -0
  47. package/dist/primitives/Textarea.js +6 -0
  48. package/dist/provider/messages.d.ts +430 -0
  49. package/dist/provider/messages.js +666 -0
  50. package/dist/shell/BrandMark.d.ts +25 -3
  51. package/dist/shell/BrandMark.js +34 -4
  52. package/dist/shell/RailPanel.js +2 -2
  53. package/dist/shell/RailTree.js +2 -2
  54. package/dist/shell/Sidebar.d.ts +9 -2
  55. package/dist/shell/Sidebar.js +10 -4
  56. package/dist/shell/UserMenu.d.ts +3 -1
  57. package/dist/shell/UserMenu.js +3 -11
  58. package/dist/shell/rail-boot.d.ts +28 -0
  59. package/dist/shell/rail-boot.js +45 -0
  60. package/dist/shell/rail-state.d.ts +6 -1
  61. package/dist/shell/rail-state.js +18 -2
  62. package/dist/styles/preset.css +4 -0
  63. package/dist/styles/theme.css +36 -13
  64. package/dist/theme/css-layer.d.ts +19 -0
  65. package/dist/theme/css-layer.js +171 -0
  66. package/dist/theme/css.d.ts +1 -0
  67. package/dist/theme/css.js +45 -9
  68. package/dist/workflow/ArtifactCard.d.ts +47 -0
  69. package/dist/workflow/ArtifactCard.js +45 -0
  70. package/dist/workflow/DocumentReview.d.ts +74 -0
  71. package/dist/workflow/DocumentReview.js +54 -0
  72. package/dist/workflow/FormReview.d.ts +53 -0
  73. package/dist/workflow/FormReview.js +83 -0
  74. package/dist/workflow/GateBadge.d.ts +11 -0
  75. package/dist/workflow/GateBadge.js +14 -0
  76. package/dist/workflow/MediaReview.d.ts +63 -0
  77. package/dist/workflow/MediaReview.js +166 -0
  78. package/dist/workflow/ReviewPanel.d.ts +41 -0
  79. package/dist/workflow/ReviewPanel.js +59 -0
  80. package/dist/workflow/RunTimeline.d.ts +25 -0
  81. package/dist/workflow/RunTimeline.js +56 -0
  82. package/dist/workflow/WorkflowCard.d.ts +39 -0
  83. package/dist/workflow/WorkflowCard.js +56 -0
  84. package/dist/workflow/WorkflowGallery.d.ts +43 -0
  85. package/dist/workflow/WorkflowGallery.js +69 -0
  86. package/dist/workflow/WorkflowQuestionCard.d.ts +18 -0
  87. package/dist/workflow/WorkflowQuestionCard.js +78 -0
  88. package/dist/workflow/canvas/WorkflowCanvas.d.ts +39 -0
  89. package/dist/workflow/canvas/WorkflowCanvas.js +648 -0
  90. package/dist/workflow/definition.d.ts +408 -0
  91. package/dist/workflow/definition.js +411 -0
  92. package/dist/workflow/editor/WorkflowDiff.d.ts +17 -0
  93. package/dist/workflow/editor/WorkflowDiff.js +25 -0
  94. package/dist/workflow/editor/WorkflowImportPreview.d.ts +19 -0
  95. package/dist/workflow/editor/WorkflowImportPreview.js +50 -0
  96. package/dist/workflow/editor/WorkflowInspector.d.ts +40 -0
  97. package/dist/workflow/editor/WorkflowInspector.js +349 -0
  98. package/dist/workflow/editor/WorkflowJsonView.d.ts +16 -0
  99. package/dist/workflow/editor/WorkflowJsonView.js +40 -0
  100. package/dist/workflow/editor/WorkflowPalette.d.ts +16 -0
  101. package/dist/workflow/editor/WorkflowPalette.js +36 -0
  102. package/dist/workflow/editor/WorkflowProblems.d.ts +20 -0
  103. package/dist/workflow/editor/WorkflowProblems.js +51 -0
  104. package/dist/workflow/editor/WorkflowWorkspace.d.ts +49 -0
  105. package/dist/workflow/editor/WorkflowWorkspace.js +154 -0
  106. package/dist/workflow/gallery.d.ts +21 -0
  107. package/dist/workflow/gallery.js +29 -0
  108. package/dist/workflow/layout.d.ts +49 -0
  109. package/dist/workflow/layout.js +162 -0
  110. package/dist/workflow/model.d.ts +50 -0
  111. package/dist/workflow/model.js +7 -0
  112. package/dist/workflow/palette.d.ts +34 -0
  113. package/dist/workflow/palette.js +70 -0
  114. package/dist/workflow/portable.d.ts +162 -0
  115. package/dist/workflow/portable.js +547 -0
  116. package/dist/workflow/problems.d.ts +38 -0
  117. package/dist/workflow/problems.js +409 -0
  118. package/dist/workflow/question.d.ts +26 -0
  119. package/dist/workflow/question.js +1 -0
  120. package/dist/workflow/run.d.ts +94 -0
  121. package/dist/workflow/run.js +31 -0
  122. package/dist/workflow/workspace.d.ts +33 -0
  123. package/dist/workflow/workspace.js +17 -0
  124. package/package.json +6 -1
@@ -0,0 +1,11 @@
1
+ import { type Gate } from "./run.js";
2
+ export interface GateBadgeProps {
3
+ gate: Pick<Gate, "label" | "status" | "approvals" | "required">;
4
+ className?: string;
5
+ }
6
+ /**
7
+ * A gate in one pill: what it asks, where it stands, and the quorum so far.
8
+ * The tone is the gate's status: waiting is a warning, approved ok, rejected
9
+ * an error, expired and skipped muted.
10
+ */
11
+ export declare function GateBadge({ gate, className }: GateBadgeProps): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useUI } from "../provider/UIProvider.js";
4
+ import { StatusBadge } from "../status/StatusBadge.js";
5
+ import { GATE_TONE } from "./run.js";
6
+ /**
7
+ * A gate in one pill: what it asks, where it stands, and the quorum so far.
8
+ * The tone is the gate's status: waiting is a warning, approved ok, rejected
9
+ * an error, expired and skipped muted.
10
+ */
11
+ export function GateBadge({ gate, className }) {
12
+ const { messages } = useUI();
13
+ return (_jsx(StatusBadge, { tone: GATE_TONE[gate.status], "data-slot": "gate-badge", className: className, children: messages.run.gate(gate.label, gate.status, gate.approvals, gate.required) }));
14
+ }
@@ -0,0 +1,63 @@
1
+ import * as React from "react";
2
+ /** A rectangle on an image or a frame, as fractions of its width and height (0 to 1). */
3
+ export interface MediaRegion {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ /** Where a media comment points: a moment, a region, or both (a region of a frame). */
10
+ export interface MediaAnchor {
11
+ /** Seconds from the start. */
12
+ time?: number;
13
+ region?: MediaRegion;
14
+ }
15
+ /** A comment pinned to the media. */
16
+ export interface MediaComment extends MediaAnchor {
17
+ id: string;
18
+ author: string;
19
+ body: string;
20
+ resolved?: boolean;
21
+ }
22
+ export interface MediaCaptionTrack {
23
+ src: string;
24
+ srcLang: string;
25
+ label: string;
26
+ }
27
+ export interface MediaReviewProps {
28
+ kind: "video" | "audio" | "image";
29
+ src: string;
30
+ /** Names the media: an image's alternative text, a player's label. */
31
+ title: string;
32
+ /** A video's still before it plays. */
33
+ poster?: string;
34
+ captions?: readonly MediaCaptionTrack[];
35
+ /** Seconds, for placing markers before the media's metadata loads. */
36
+ duration?: number;
37
+ comments: readonly MediaComment[];
38
+ activeCommentId?: string;
39
+ /** A marker was chosen: the viewer seeks to it; the host shows the comment. */
40
+ onCommentSelect?: (comment: MediaComment) => void;
41
+ /**
42
+ * The person picked where their next comment points: the current time of a
43
+ * player, or a region drawn on an image. The host labels it (see
44
+ * `formatTimecode`) and hands it to ReviewPanel as `commentAnchor`.
45
+ */
46
+ onAnchor?: (anchor: MediaAnchor) => void;
47
+ className?: string;
48
+ }
49
+ /** Seconds as a timecode: "00:01:12". */
50
+ export declare function formatTimecode(seconds: number): string;
51
+ /**
52
+ * Media review for the media module: a video, audio or image with comments
53
+ * pinned to timecodes (markers on a rail under the player) or to regions
54
+ * (boxes over the image). Choosing a marker seeks the player and tells the
55
+ * host; "comment here" hands the host the current time, and on an image
56
+ * drawing a box hands it the region. "Mark a region" is the keyboard path on
57
+ * an image or a video frame: it drops a box that the arrow keys move and
58
+ * Shift with the arrows size, Enter hands the host the region (with the
59
+ * current time on a video) and Escape cancels. Plugs into ArtifactCard's
60
+ * preview and feeds ReviewPanel's anchors. The host supplies the media URL; the viewer
61
+ * fetches nothing itself beyond what the browser's media element does.
62
+ */
63
+ export declare function MediaReview({ kind, src, title, poster, captions, duration: durationProp, comments, activeCommentId, onCommentSelect, onAnchor, className, }: MediaReviewProps): React.JSX.Element;
@@ -0,0 +1,166 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { CrosshairIcon, MessageSquarePlusIcon, SquareDashedIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { Button } from "../primitives/Button.js";
7
+ import { useUI } from "../provider/UIProvider.js";
8
+ /** Seconds as a timecode: "00:01:12". */
9
+ export function formatTimecode(seconds) {
10
+ const s = Math.max(0, Math.floor(seconds));
11
+ const pad = (n) => String(n).padStart(2, "0");
12
+ return `${pad(Math.floor(s / 3600))}:${pad(Math.floor((s % 3600) / 60))}:${pad(s % 60)}`;
13
+ }
14
+ const percent = (fraction) => `${Math.min(100, Math.max(0, fraction * 100))}%`;
15
+ /** Where the keyboard box starts, and how far one arrow press moves or sizes it. */
16
+ const START = { x: 0.35, y: 0.35, width: 0.3, height: 0.3 };
17
+ const STEP = 0.05;
18
+ /**
19
+ * One arrow press on the keyboard box: move it, or with Shift size it, never
20
+ * past the picture's edges or smaller than one step.
21
+ */
22
+ function nudge(region, key, resize) {
23
+ const dx = key === "ArrowRight" ? STEP : key === "ArrowLeft" ? -STEP : 0;
24
+ const dy = key === "ArrowDown" ? STEP : key === "ArrowUp" ? -STEP : 0;
25
+ if (!dx && !dy)
26
+ return null;
27
+ const clamp = (n, low, high) => Math.round(Math.min(high, Math.max(low, n)) * 100) / 100;
28
+ const { x, y, width, height } = region;
29
+ return resize
30
+ ? { x, y, width: clamp(width + dx, STEP, 1 - x), height: clamp(height + dy, STEP, 1 - y) }
31
+ : { x: clamp(x + dx, 0, 1 - width), y: clamp(y + dy, 0, 1 - height), width, height };
32
+ }
33
+ /**
34
+ * Media review for the media module: a video, audio or image with comments
35
+ * pinned to timecodes (markers on a rail under the player) or to regions
36
+ * (boxes over the image). Choosing a marker seeks the player and tells the
37
+ * host; "comment here" hands the host the current time, and on an image
38
+ * drawing a box hands it the region. "Mark a region" is the keyboard path on
39
+ * an image or a video frame: it drops a box that the arrow keys move and
40
+ * Shift with the arrows size, Enter hands the host the region (with the
41
+ * current time on a video) and Escape cancels. Plugs into ArtifactCard's
42
+ * preview and feeds ReviewPanel's anchors. The host supplies the media URL; the viewer
43
+ * fetches nothing itself beyond what the browser's media element does.
44
+ */
45
+ export function MediaReview({ kind, src, title, poster, captions = [], duration: durationProp, comments, activeCommentId, onCommentSelect, onAnchor, className, }) {
46
+ const { messages } = useUI();
47
+ const t = messages.media;
48
+ const player = React.useRef(null);
49
+ const [loaded, setLoaded] = React.useState(null);
50
+ const duration = loaded ?? durationProp ?? 0;
51
+ const timed = comments.filter((c) => c.time != null);
52
+ const regions = comments.filter((c) => c.region);
53
+ const id = React.useId();
54
+ const [mark, setMark] = React.useState(null);
55
+ const canMark = Boolean(onAnchor) && kind !== "audio";
56
+ // The host took commenting away: the open box goes, and does not come back with it.
57
+ if (mark && !canMark)
58
+ setMark(null);
59
+ const marking = canMark ? mark : null;
60
+ const box = React.useRef(null);
61
+ const markButton = React.useRef(null);
62
+ const wasMarking = React.useRef(false);
63
+ const isMarking = marking != null;
64
+ // Focus follows the box: onto it when it appears, back to "Mark a region" when it goes.
65
+ React.useEffect(() => {
66
+ if (isMarking)
67
+ box.current?.focus();
68
+ else if (wasMarking.current)
69
+ markButton.current?.focus();
70
+ wasMarking.current = isMarking;
71
+ }, [isMarking]);
72
+ function endMark(region) {
73
+ setMark(null);
74
+ if (region) {
75
+ onAnchor?.(kind === "video" ? { time: player.current?.currentTime ?? 0, region } : { region });
76
+ }
77
+ }
78
+ function select(comment) {
79
+ if (comment.time != null && player.current) {
80
+ try {
81
+ player.current.currentTime = comment.time;
82
+ }
83
+ catch {
84
+ // A media element without metadata refuses a seek; the host still shows the comment.
85
+ }
86
+ }
87
+ onCommentSelect?.(comment);
88
+ }
89
+ const regionButtons = regions.map((c) => (_jsx("button", { type: "button", onClick: () => select(c), "aria-label": t.regionComment(c.author, c.time != null ? formatTimecode(c.time) : undefined), "aria-current": c.id === activeCommentId || undefined, style: {
90
+ left: percent(c.region.x),
91
+ top: percent(c.region.y),
92
+ width: percent(c.region.width),
93
+ height: percent(c.region.height),
94
+ }, className: cn("absolute rounded-sm border-2 border-warning outline-none focus-visible:ring-2 focus-visible:ring-ring", c.id === activeCommentId && "border-primary bg-primary/10", c.resolved && "border-dashed opacity-60") }, c.id)));
95
+ const whole = (fraction) => Math.round(fraction * 100);
96
+ /*
97
+ * The box is an application: a two-dimensional move and resize has no
98
+ * widget role, and the role puts a screen reader in focus mode so the
99
+ * arrow keys reach the box. jsx-a11y counts the role as non-interactive.
100
+ */
101
+ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/no-noninteractive-tabindex */
102
+ const regionMarker = marking && (_jsx("div", { ref: box, role: "application", tabIndex: 0, "aria-label": t.regionBox, "aria-describedby": `${id}-help ${id}-bounds`, onKeyDown: (event) => {
103
+ if (event.key === "Enter") {
104
+ event.preventDefault();
105
+ endMark(marking);
106
+ }
107
+ else if (event.key === "Escape") {
108
+ // Cancels the box, not a dialog the review sits in.
109
+ event.preventDefault();
110
+ event.stopPropagation();
111
+ endMark();
112
+ }
113
+ else {
114
+ const next = nudge(marking, event.key, event.shiftKey);
115
+ if (next) {
116
+ event.preventDefault();
117
+ setMark(next);
118
+ }
119
+ }
120
+ }, style: {
121
+ left: percent(marking.x),
122
+ top: percent(marking.y),
123
+ width: percent(marking.width),
124
+ height: percent(marking.height),
125
+ }, className: "pointer-events-auto absolute rounded-sm border-2 border-dashed border-primary bg-primary/10 outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background" }));
126
+ /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/no-noninteractive-tabindex */
127
+ return (_jsxs("figure", { "data-slot": "media-review", "data-kind": kind, className: cn("flex min-w-0 flex-col gap-2", className), children: [_jsx("div", { className: "relative min-w-0 overflow-hidden rounded-lg border bg-muted/40", children: kind === "image" ? (_jsxs(RegionDrawer, { enabled: Boolean(onAnchor) && !marking, onRegion: (region) => onAnchor?.({ region }), children: [_jsx("img", { src: src, alt: title, className: "block h-auto w-full" }), regionButtons, regionMarker] })) : kind === "video" ? (_jsxs("div", { className: "relative", children: [_jsx("video", { ref: player, src: src, poster: poster, controls: true, preload: "metadata", "aria-label": title, onLoadedMetadata: (event) => setLoaded(event.currentTarget.duration || null), className: "block h-auto w-full", children: captions.map((track) => (_jsx("track", { kind: "captions", ...track }, track.src))) }), _jsxs("div", { className: "pointer-events-none absolute inset-0 [&>button]:pointer-events-auto", children: [regionButtons, regionMarker] })] })) : (_jsx("audio", { ref: player, src: src, controls: true, preload: "metadata", "aria-label": title, onLoadedMetadata: (event) => setLoaded(event.currentTarget.duration || null), className: "block w-full p-2", children: captions.map((track) => (_jsx("track", { kind: "captions", ...track }, track.src))) })) }), kind !== "image" && (_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx("ol", { "aria-label": t.markers, className: "relative h-6 min-w-0 flex-1 rounded-full bg-muted", children: timed.map((c) => (_jsx("li", { className: "absolute top-1/2 -translate-x-1/2 -translate-y-1/2", style: { left: duration ? percent(c.time / duration) : "0%" }, children: _jsx("button", { type: "button", onClick: () => select(c), "aria-label": t.timeComment(formatTimecode(c.time), c.author), "aria-current": c.id === activeCommentId || undefined, className: cn("block size-3 rounded-full border-2 border-background bg-warning outline-none focus-visible:ring-2 focus-visible:ring-ring", c.id === activeCommentId && "size-4 bg-primary", c.resolved && "bg-muted-foreground") }) }, c.id))) }), onAnchor && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => onAnchor({ time: player.current?.currentTime ?? 0 }), children: [_jsx(MessageSquarePlusIcon, { "aria-hidden": true }), t.commentHere] }))] })), canMark &&
128
+ (marking ? (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2", children: [_jsx("p", { id: `${id}-help`, className: "min-w-0 basis-full text-xs text-muted-foreground", children: t.regionHelp }), _jsx("span", { id: `${id}-bounds`, "aria-live": "polite", className: "min-w-0 flex-1 basis-56 font-mono text-xs tabular-nums", children: t.regionBounds(whole(marking.x), whole(marking.y), whole(marking.width), whole(marking.height)) }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => endMark(), children: t.cancel }), _jsxs(Button, { size: "sm", onClick: () => endMark(marking), children: [_jsx(MessageSquarePlusIcon, { "aria-hidden": true }), t.confirmRegion] })] })] })) : (_jsx("div", { className: "flex min-w-0", children: _jsxs(Button, { ref: markButton, size: "sm", variant: "outline", onClick: () => setMark(START), children: [_jsx(SquareDashedIcon, { "aria-hidden": true }), t.markRegion] }) }))), _jsxs("figcaption", { className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [_jsx("span", { className: "min-w-0 [overflow-wrap:anywhere]", children: title }), _jsx("span", { children: t.count(comments.length) }), kind === "image" && onAnchor && (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx(CrosshairIcon, { "aria-hidden": true, className: "size-3.5" }), t.drawHint] }))] })] }));
129
+ }
130
+ /** Drag on the image to draw a region: the pointer path ("Mark a region" is the keyboard one). */
131
+ function RegionDrawer({ enabled, onRegion, children, }) {
132
+ const [start, setStart] = React.useState(null);
133
+ const [box, setBox] = React.useState(null);
134
+ const at = (event) => {
135
+ const rect = event.currentTarget.getBoundingClientRect();
136
+ return {
137
+ x: rect.width ? (event.clientX - rect.left) / rect.width : 0,
138
+ y: rect.height ? (event.clientY - rect.top) / rect.height : 0,
139
+ };
140
+ };
141
+ const span = (a, b) => ({
142
+ x: Math.min(a.x, b.x),
143
+ y: Math.min(a.y, b.y),
144
+ width: Math.abs(a.x - b.x),
145
+ height: Math.abs(a.y - b.y),
146
+ });
147
+ return (_jsxs("div", { className: cn("relative", enabled && "cursor-crosshair"), onPointerDown: (event) => {
148
+ if (!enabled || event.target !== event.currentTarget.querySelector("img"))
149
+ return;
150
+ event.currentTarget.setPointerCapture(event.pointerId);
151
+ setStart(at(event));
152
+ }, onPointerMove: (event) => start && setBox(span(start, at(event))), onPointerUp: (event) => {
153
+ if (!start)
154
+ return;
155
+ const region = span(start, at(event));
156
+ setStart(null);
157
+ setBox(null);
158
+ if (region.width > 0.01 && region.height > 0.01)
159
+ onRegion(region);
160
+ }, children: [children, box && (_jsx("span", { "aria-hidden": true, style: {
161
+ left: percent(box.x),
162
+ top: percent(box.y),
163
+ width: percent(box.width),
164
+ height: percent(box.height),
165
+ }, className: "pointer-events-none absolute rounded-sm border-2 border-dashed border-primary bg-primary/10" }))] }));
166
+ }
@@ -0,0 +1,41 @@
1
+ import * as React from "react";
2
+ import { type Gate, type ReviewComment, type ReviewDecision, type ReviewVerdict } from "./run.js";
3
+ export interface ReviewPanelProps {
4
+ /** What is under review, for the heading ("Review: Promo cut v3"). */
5
+ subject?: string;
6
+ verdict: ReviewVerdict;
7
+ /** The gate this review decides, when it decides one. */
8
+ gate?: Gate;
9
+ /** The decision that closed the review, when it is closed. */
10
+ decision?: ReviewDecision;
11
+ comments: readonly ReviewComment[];
12
+ /** Posts a comment. The composer shows only with it. A rejection keeps the draft and shows the error. */
13
+ onComment?: (body: string, anchor?: string) => Promise<unknown> | unknown;
14
+ /**
15
+ * Where the next comment points, picked in a module viewer (a timecode, a
16
+ * region, a clause, a field). Shown on the composer, cleared with `onClearAnchor`.
17
+ */
18
+ commentAnchor?: string;
19
+ onClearAnchor?: () => void;
20
+ /** Makes each comment's anchor a button, so the viewer can go to it. */
21
+ onCommentSelect?: (comment: ReviewComment) => void;
22
+ activeCommentId?: string;
23
+ /** Approves, after the confirm step. Awaited; see ConfirmDialog. */
24
+ onApprove?: (reason: string) => Promise<unknown> | unknown;
25
+ /** Rejects, after the confirm step, with a required reason. */
26
+ onReject?: (reason: string) => Promise<unknown> | unknown;
27
+ /** Ask for a reason on approval too. */
28
+ approveReasonRequired?: boolean;
29
+ /** Why the person may not decide: the decision buttons show disabled with it. */
30
+ decisionDenied?: string;
31
+ headingLevel?: "h2" | "h3";
32
+ className?: string;
33
+ }
34
+ /**
35
+ * The review of one artifact or stage: its verdict and gate, the comments
36
+ * (each with its anchor in the artifact), a composer, and approve and reject
37
+ * through the confirm flow, a rejection always with a reason. Module viewers
38
+ * sit beside it and feed it anchors; it never decides anything itself, the
39
+ * host's callbacks do.
40
+ */
41
+ export declare function ReviewPanel({ subject, verdict, gate, decision, comments, onComment, commentAnchor, onClearAnchor, onCommentSelect, activeCommentId, onApprove, onReject, approveReasonRequired, decisionDenied, headingLevel, className, }: ReviewPanelProps): React.JSX.Element;
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { CheckIcon, XIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { ConfirmDialog } from "../overlays/ConfirmDialog.js";
7
+ import { Button } from "../primitives/Button.js";
8
+ import { Textarea } from "../primitives/Textarea.js";
9
+ import { useUI } from "../provider/UIProvider.js";
10
+ import { StatusBadge } from "../status/StatusBadge.js";
11
+ import { GateBadge } from "./GateBadge.js";
12
+ import { VERDICT_TONE, } from "./run.js";
13
+ /**
14
+ * The review of one artifact or stage: its verdict and gate, the comments
15
+ * (each with its anchor in the artifact), a composer, and approve and reject
16
+ * through the confirm flow, a rejection always with a reason. Module viewers
17
+ * sit beside it and feed it anchors; it never decides anything itself, the
18
+ * host's callbacks do.
19
+ */
20
+ export function ReviewPanel({ subject, verdict, gate, decision, comments, onComment, commentAnchor, onClearAnchor, onCommentSelect, activeCommentId, onApprove, onReject, approveReasonRequired = false, decisionDenied, headingLevel = "h2", className, }) {
21
+ const { messages } = useUI();
22
+ const t = messages.review;
23
+ const Heading = headingLevel;
24
+ const SubHeading = headingLevel === "h2" ? "h3" : "h4";
25
+ const id = React.useId();
26
+ const [confirming, setConfirming] = React.useState(null);
27
+ const open = !decision && (!gate || gate.status === "waiting");
28
+ const canDecide = open && Boolean(onApprove || onReject);
29
+ return (_jsxs("section", { "aria-labelledby": `${id}-title`, "data-slot": "review-panel", className: cn("flex min-w-0 flex-col gap-4 rounded-lg border bg-card p-(--panel-pad) text-card-foreground", className), children: [_jsxs("header", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx(Heading, { id: `${id}-title`, className: "text-sm font-medium [overflow-wrap:anywhere]", children: subject ? t.titleFor(subject) : t.title }), _jsx(StatusBadge, { tone: VERDICT_TONE[verdict], children: t.verdict(verdict) }), gate && _jsx(GateBadge, { gate: gate })] }), decision && (_jsxs("p", { className: "text-sm [overflow-wrap:anywhere] text-muted-foreground", children: [t.decided(decision.verdict, decision.by, decision.at), decision.reason && (_jsxs(_Fragment, { children: [" ", _jsx("span", { className: "text-foreground", children: decision.reason })] }))] })), canDecide && (_jsxs("div", { className: "flex min-w-0 flex-col gap-1.5", children: [_jsxs("div", { className: "flex flex-wrap gap-2", children: [onApprove && (_jsxs(Button, { size: "sm", disabled: decisionDenied != null, "aria-describedby": decisionDenied != null ? `${id}-denied` : undefined, onClick: () => setConfirming("approve"), children: [_jsx(CheckIcon, { "aria-hidden": true }), t.approve] })), onReject && (_jsxs(Button, { size: "sm", variant: "destructive", disabled: decisionDenied != null, "aria-describedby": decisionDenied != null ? `${id}-denied` : undefined, onClick: () => setConfirming("reject"), children: [_jsx(XIcon, { "aria-hidden": true }), t.reject] }))] }), decisionDenied != null && (_jsx("p", { id: `${id}-denied`, className: "text-xs text-muted-foreground", children: decisionDenied }))] }))] }), _jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(SubHeading, { className: "font-mono text-2xs font-medium tracking-wider text-muted-foreground uppercase", children: t.comments(comments.length) }), comments.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t.noComments })) : (_jsx("ol", { className: "flex min-w-0 flex-col gap-2", children: comments.map((comment) => (_jsxs("li", { "aria-current": comment.id === activeCommentId || undefined, className: cn("flex min-w-0 flex-col gap-1 rounded-lg border p-2.5", comment.id === activeCommentId && "border-primary", comment.resolved && "opacity-70"), children: [_jsxs("p", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs", children: [_jsx("span", { className: "font-medium", children: comment.author }), comment.at && _jsx("span", { className: "text-muted-foreground", children: comment.at }), comment.anchor &&
30
+ (onCommentSelect ? (_jsx("button", { type: "button", onClick: () => onCommentSelect(comment), "aria-label": t.goToAnchor(comment.anchor), className: "rounded-sm font-mono text-primary underline-offset-2 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring", children: comment.anchor })) : (_jsx("span", { className: "font-mono text-muted-foreground", children: comment.anchor }))), comment.resolved && (_jsx(StatusBadge, { tone: "muted", className: "ml-auto", children: t.resolved }))] }), _jsx("p", { className: "text-sm [overflow-wrap:anywhere] whitespace-pre-wrap", children: comment.body })] }, comment.id))) })), onComment && (_jsx(Composer, { onComment: onComment, anchor: commentAnchor, onClearAnchor: onClearAnchor }))] }), _jsx(ConfirmDialog, { open: confirming === "approve", onOpenChange: (next) => !next && setConfirming(null), title: t.approveTitle, description: t.approveDescription, confirmLabel: t.approve, reason: approveReasonRequired ? { label: t.reasonLabel } : undefined, onConfirm: (reason) => onApprove?.(reason) }), _jsx(ConfirmDialog, { open: confirming === "reject", onOpenChange: (next) => !next && setConfirming(null), title: t.rejectTitle, description: t.rejectDescription, confirmLabel: t.reject, destructive: true, reason: { label: t.reasonLabel }, onConfirm: (reason) => onReject?.(reason) })] }));
31
+ }
32
+ function Composer({ onComment, anchor, onClearAnchor, }) {
33
+ const { messages } = useUI();
34
+ const t = messages.review;
35
+ const id = React.useId();
36
+ const [draft, setDraft] = React.useState("");
37
+ const [pending, setPending] = React.useState(false);
38
+ const [error, setError] = React.useState(null);
39
+ async function submit(event) {
40
+ event.preventDefault();
41
+ const body = draft.trim();
42
+ if (!body || pending)
43
+ return;
44
+ setPending(true);
45
+ setError(null);
46
+ try {
47
+ const result = await onComment(body, anchor);
48
+ if (result !== false)
49
+ setDraft("");
50
+ }
51
+ catch (err) {
52
+ setError(err instanceof Error && err.message ? err.message : messages.confirm.failed);
53
+ }
54
+ finally {
55
+ setPending(false);
56
+ }
57
+ }
58
+ return (_jsxs("form", { onSubmit: submit, className: "flex min-w-0 flex-col gap-2", children: [_jsx("label", { htmlFor: id, className: "sr-only", children: t.addComment }), anchor && (_jsxs("p", { className: "flex min-w-0 items-center gap-2 text-xs text-muted-foreground", children: [_jsx("span", { className: "[overflow-wrap:anywhere]", children: t.commentOn(anchor) }), onClearAnchor && (_jsx(Button, { type: "button", size: "xs", variant: "ghost", onClick: onClearAnchor, children: t.clearAnchor }))] })), _jsx(Textarea, { id: id, value: draft, rows: 2, disabled: pending, placeholder: t.addComment, "aria-invalid": error != null || undefined, "aria-describedby": error ? `${id}-error` : undefined, onChange: (event) => setDraft(event.target.value) }), error && (_jsx("p", { id: `${id}-error`, role: "alert", className: "text-xs text-danger-fg", children: error })), _jsx(Button, { type: "submit", size: "sm", variant: "outline", disabled: pending || !draft.trim(), className: "self-end", children: pending ? messages.confirm.working : t.post })] }));
59
+ }
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ import { type RunStage } from "./run.js";
3
+ export interface RunTimelineProps {
4
+ stages: readonly RunStage[];
5
+ /** Names the timeline. Defaults to "Run stages". */
6
+ label?: string;
7
+ loading?: boolean;
8
+ error?: {
9
+ message: string;
10
+ } | null;
11
+ onRetry?: () => void;
12
+ /** Makes each stage's name a button, for a host that shows the stage beside the timeline. */
13
+ onStageSelect?: (stage: RunStage) => void;
14
+ selectedStageId?: string;
15
+ /** Stages whose inputs and outputs start open. */
16
+ defaultExpanded?: readonly string[];
17
+ className?: string;
18
+ }
19
+ /**
20
+ * A run's stages in order, for any module: each with its status, timing,
21
+ * attempt and retries, a one-line detail, its gate when it is one, and its
22
+ * inputs and outputs summarized behind a disclosure. Process and automation
23
+ * runs read here as they are; other modules put their review surfaces beside it.
24
+ */
25
+ export declare function RunTimeline({ stages, label, loading, error, onRetry, onStageSelect, selectedStageId, defaultExpanded, className, }: RunTimelineProps): React.JSX.Element;
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronRightIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { Skeleton } from "../primitives/Skeleton.js";
7
+ import { useUI } from "../provider/UIProvider.js";
8
+ import { EmptyState } from "../states/EmptyState.js";
9
+ import { ErrorState } from "../states/ErrorState.js";
10
+ import { StatusDot } from "../status/StatusDot.js";
11
+ import { GateBadge } from "./GateBadge.js";
12
+ import { RUN_STAGE_TONE } from "./run.js";
13
+ /**
14
+ * A run's stages in order, for any module: each with its status, timing,
15
+ * attempt and retries, a one-line detail, its gate when it is one, and its
16
+ * inputs and outputs summarized behind a disclosure. Process and automation
17
+ * runs read here as they are; other modules put their review surfaces beside it.
18
+ */
19
+ export function RunTimeline({ stages, label, loading = false, error, onRetry, onStageSelect, selectedStageId, defaultExpanded = [], className, }) {
20
+ const { messages } = useUI();
21
+ const t = messages.run;
22
+ const name = label ?? t.timeline;
23
+ const id = React.useId();
24
+ const [expanded, setExpanded] = React.useState(() => new Set(defaultExpanded));
25
+ if (error) {
26
+ return (_jsx(ErrorState, { title: t.loadFailed(name), error: error, onRetry: onRetry, className: className }));
27
+ }
28
+ if (loading) {
29
+ return (_jsxs("div", { "aria-busy": true, className: cn("flex flex-col gap-4", className), children: [_jsx("p", { role: "status", className: "sr-only", children: messages.states.loading }), Array.from({ length: 4 }, (_, i) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Skeleton, { className: "size-3 rounded-full" }), _jsx(Skeleton, { className: "h-4 flex-1" }), _jsx(Skeleton, { className: "h-4 w-16" })] }, i)))] }));
30
+ }
31
+ if (stages.length === 0) {
32
+ return (_jsx(EmptyState, { title: t.noStages, description: t.noStagesDescription, className: className }));
33
+ }
34
+ return (_jsx("ol", { "aria-label": name, "data-slot": "run-timeline", className: cn("flex min-w-0 flex-col", className), children: stages.map((stage, index) => {
35
+ const open = expanded.has(stage.id);
36
+ const hasValues = Boolean(stage.inputs?.length || stage.outputs?.length);
37
+ const valuesId = `${id}-${index}-values`;
38
+ const last = index === stages.length - 1;
39
+ const selected = stage.id === selectedStageId;
40
+ const attempt = stage.attempt && (stage.attempt > 1 || stage.maxAttempts)
41
+ ? t.attempt(stage.attempt, stage.maxAttempts)
42
+ : null;
43
+ return (_jsxs("li", { "data-status": stage.status, "aria-current": selected ? "step" : undefined, className: "relative flex min-w-0 gap-3 pb-4 last:pb-0", children: [!last && (_jsx("span", { "aria-hidden": true, className: "absolute top-4 bottom-0 left-1 w-px -translate-x-1/2 bg-border" })), _jsx(StatusDot, { tone: RUN_STAGE_TONE[stage.status], pulse: stage.status === "running", className: "mt-1.5 self-start" }), _jsxs("div", { className: cn("-my-1 flex min-w-0 flex-1 flex-col gap-1 rounded-lg px-2 py-1", selected && "bg-muted"), children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1", children: [_jsx("span", { className: "min-w-0 text-sm font-medium [overflow-wrap:anywhere]", children: onStageSelect ? (_jsx("button", { type: "button", onClick: () => onStageSelect(stage), className: "rounded-sm text-left outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring", children: stage.label })) : (stage.label) }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: t.stageStatus(stage.status) }), stage.gate && _jsx(GateBadge, { gate: stage.gate }), _jsxs("span", { className: "ml-auto flex flex-wrap gap-x-3 font-mono text-xs text-muted-foreground tabular-nums", children: [attempt && _jsx("span", { children: attempt }), stage.startedAt && _jsx("span", { children: t.startedAt(stage.startedAt) }), stage.duration && _jsx("span", { children: t.duration(stage.duration) })] })] }), stage.detail && (_jsx("p", { className: "text-sm [overflow-wrap:anywhere] text-muted-foreground", children: stage.detail })), hasValues && (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("button", { type: "button", "aria-expanded": open, "aria-controls": valuesId, onClick: () => setExpanded((current) => {
44
+ const next = new Set(current);
45
+ if (open)
46
+ next.delete(stage.id);
47
+ else
48
+ next.add(stage.id);
49
+ return next;
50
+ }), className: "inline-flex w-fit items-center gap-1 rounded-sm text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx(ChevronRightIcon, { "aria-hidden": true, className: cn("size-3.5 transition-transform", open && "rotate-90") }), t.inputsOutputs(stage.inputs?.length ?? 0, stage.outputs?.length ?? 0)] }), open && (_jsxs("div", { id: valuesId, className: "grid min-w-0 gap-3 sm:grid-cols-2", children: [_jsx(Values, { title: t.inputs, values: stage.inputs }), _jsx(Values, { title: t.outputs, values: stage.outputs })] }))] }))] })] }, stage.id));
51
+ }) }));
52
+ }
53
+ function Values({ title, values }) {
54
+ const { messages } = useUI();
55
+ return (_jsxs("div", { className: "flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-2", children: [_jsx("p", { className: "font-mono text-2xs font-medium tracking-wider text-muted-foreground uppercase", children: title }), values?.length ? (_jsx("dl", { className: "grid min-w-0 grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 text-xs", children: values.map((v, i) => (_jsxs(React.Fragment, { children: [_jsx("dt", { className: "text-muted-foreground", children: v.label }), _jsx("dd", { className: "font-mono [overflow-wrap:anywhere]", children: v.value })] }, i))) })) : (_jsx("p", { className: "text-xs text-muted-foreground", children: messages.run.none }))] }));
56
+ }
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ import type { WorkflowPermissions, WorkflowSummary } from "./model.js";
3
+ /** What a card can do, as the host permits it. An action renders only when its handler is given. */
4
+ export interface WorkflowCardActions {
5
+ /** Opens the workflow: the title becomes a link through the app's link component. */
6
+ href?: string;
7
+ /** Opens the workflow without a route: the title becomes a button. Ignored with `href`. */
8
+ onOpen?: () => void;
9
+ onEdit?: () => void;
10
+ /** Enabled only when the workflow is available here and `permissions.start` is not false. */
11
+ onStart?: () => void;
12
+ onExport?: () => void;
13
+ /** An action set to false is shown disabled, with the reason. */
14
+ permissions?: WorkflowPermissions;
15
+ }
16
+ export interface WorkflowCardProps extends WorkflowCardActions {
17
+ workflow: WorkflowSummary;
18
+ /** The host's picture of the workflow (a flow diagram, a thumbnail). Decorative unless it names itself. */
19
+ preview?: React.ReactNode;
20
+ /** A card in a grid, or a row in a list. */
21
+ layout?: "card" | "row";
22
+ /** For the document outline. Defaults to h3, under a page's h1 and a section's h2. */
23
+ headingLevel?: "h2" | "h3" | "h4";
24
+ className?: string;
25
+ }
26
+ /**
27
+ * One saved workflow definition, as a gallery shows it: module and version,
28
+ * name and description, scope, revision and draft or published state, an
29
+ * optional host preview, and the actions the host permits. A workflow this
30
+ * factory cannot run, or that the person may not start, never shows an
31
+ * enabled start: the card says why instead. The card only reports intent;
32
+ * the host keeps authorization and execution.
33
+ */
34
+ export declare function WorkflowCard({ workflow, preview, layout, headingLevel, href, onOpen, onEdit, onStart, onExport, permissions, className, }: WorkflowCardProps): React.JSX.Element;
35
+ /** A card's placeholder while the gallery loads, with the card's geometry. */
36
+ export declare function WorkflowCardSkeleton({ layout, className, }: {
37
+ layout?: "card" | "row";
38
+ className?: string;
39
+ }): React.JSX.Element;
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { Button } from "../primitives/Button.js";
6
+ import { Skeleton } from "../primitives/Skeleton.js";
7
+ import { useUI } from "../provider/UIProvider.js";
8
+ import { StatusBadge } from "../status/StatusBadge.js";
9
+ const STATUS_TONE = { draft: "info", published: "ok" };
10
+ const AVAILABILITY_TONE = { unsupported: "muted", "missing-capabilities": "warn" };
11
+ /**
12
+ * One saved workflow definition, as a gallery shows it: module and version,
13
+ * name and description, scope, revision and draft or published state, an
14
+ * optional host preview, and the actions the host permits. A workflow this
15
+ * factory cannot run, or that the person may not start, never shows an
16
+ * enabled start: the card says why instead. The card only reports intent;
17
+ * the host keeps authorization and execution.
18
+ */
19
+ export function WorkflowCard({ workflow, preview, layout = "card", headingLevel = "h3", href, onOpen, onEdit, onStart, onExport, permissions = {}, className, }) {
20
+ const { Link, messages } = useUI();
21
+ const t = messages.workflow;
22
+ const id = React.useId();
23
+ const titleId = `${id}-title`;
24
+ const noteId = `${id}-note`;
25
+ const Heading = headingLevel;
26
+ const { availability } = workflow;
27
+ const row = layout === "row";
28
+ const handlers = [
29
+ ["edit", onEdit],
30
+ ["start", onStart],
31
+ ["export", onExport],
32
+ ];
33
+ const offered = handlers.filter(([, handler]) => handler);
34
+ const denied = offered.filter(([action]) => permissions[action] === false).map(([a]) => a);
35
+ const startBlocked = Boolean(onStart) && availability.status !== "available";
36
+ const notes = [];
37
+ if (startBlocked) {
38
+ notes.push(availability.reason ??
39
+ (availability.status === "unsupported"
40
+ ? t.unsupported
41
+ : t.missingCapabilities(availability.missing ?? [])));
42
+ }
43
+ if (denied.length)
44
+ notes.push(t.denied(denied.map((a) => t.action(a))));
45
+ const titleClass = "outline-none after:absolute after:inset-0 after:rounded-lg hover:underline focus-visible:after:ring-2 focus-visible:after:ring-ring focus-visible:after:ring-inset";
46
+ const title = href ? (_jsx(Link, { href: href, className: titleClass, children: workflow.name })) : onOpen ? (_jsx("button", { type: "button", onClick: onOpen, className: cn(titleClass, "text-left"), children: workflow.name })) : (workflow.name);
47
+ return (_jsxs("article", { "data-slot": "workflow-card", "data-layout": layout, "data-status": workflow.status, "data-availability": availability.status, "aria-labelledby": titleId, className: cn("relative flex min-w-0 overflow-hidden rounded-lg border bg-card text-card-foreground", row ? "flex-col sm:flex-row sm:items-stretch" : "flex-col", className), children: [preview != null && (_jsx("div", { role: "img", "aria-label": t.preview(workflow.name), className: cn("flex shrink-0 items-center justify-center overflow-hidden bg-muted/40 text-muted-foreground", row ? "border-b sm:w-48 sm:border-r sm:border-b-0" : "h-28 border-b"), children: preview })), _jsxs("div", { className: cn("flex min-w-0 flex-1 gap-3 p-(--panel-pad)", row ? "flex-col md:flex-row md:items-start" : "flex-col"), children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", children: [_jsx("span", { className: "min-w-0 truncate font-mono text-xs text-muted-foreground", children: t.module(workflow.module.label, workflow.module.version) }), _jsx(StatusBadge, { tone: STATUS_TONE[workflow.status], children: t.status(workflow.status) }), availability.status !== "available" && (_jsx(StatusBadge, { tone: AVAILABILITY_TONE[availability.status], children: t.availability(availability.status) }))] }), _jsx(Heading, { id: titleId, className: "line-clamp-2 text-sm font-medium [overflow-wrap:anywhere]", children: title }), workflow.description && (_jsx("p", { className: "line-clamp-3 text-sm [overflow-wrap:anywhere] text-muted-foreground", children: workflow.description })), _jsxs("p", { className: "flex min-w-0 flex-wrap gap-x-3 font-mono text-xs text-muted-foreground", children: [_jsx("span", { children: t.scope(workflow.scope) }), workflow.revision && (_jsx("span", { className: "min-w-0 truncate", children: t.revision(workflow.revision) }))] }), notes.length > 0 && (_jsx("p", { id: noteId, className: "text-xs [overflow-wrap:anywhere] text-muted-foreground", children: notes.join(" ") }))] }), offered.length > 0 && (_jsx("div", { className: cn("relative z-10 flex shrink-0 flex-wrap items-center gap-1.5", !row && "mt-auto border-t pt-3"), children: offered.map(([action, handler]) => {
48
+ const label = t.action(action);
49
+ const disabled = permissions[action] === false || (action === "start" && startBlocked);
50
+ return (_jsx(Button, { size: "sm", variant: action === "start" ? "default" : "outline", disabled: disabled, "aria-label": t.actionName(label, workflow.name), "aria-describedby": disabled && notes.length ? noteId : undefined, onClick: handler, children: label }, action));
51
+ }) }))] })] }));
52
+ }
53
+ /** A card's placeholder while the gallery loads, with the card's geometry. */
54
+ export function WorkflowCardSkeleton({ layout = "card", className, }) {
55
+ return (_jsxs("div", { "data-slot": "workflow-card-skeleton", "aria-hidden": true, className: cn("flex min-w-0 gap-3 overflow-hidden rounded-lg border bg-card p-(--panel-pad)", layout === "row" ? "flex-row items-center" : "flex-col", className), children: [layout === "card" && _jsx(Skeleton, { className: "h-20 w-full" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: [_jsx(Skeleton, { className: "h-3 w-24" }), _jsx(Skeleton, { className: "h-4 w-2/3" }), _jsx(Skeleton, { className: "h-3 w-full" })] }), _jsx(Skeleton, { className: "h-7 w-20" })] }));
56
+ }
@@ -0,0 +1,43 @@
1
+ import * as React from "react";
2
+ import { type WorkflowGalleryFilters } from "./gallery.js";
3
+ import type { WorkflowModule, WorkflowSummary } from "./model.js";
4
+ import { type WorkflowCardActions } from "./WorkflowCard.js";
5
+ export type WorkflowGalleryView = "grid" | "list";
6
+ export interface WorkflowGalleryProps {
7
+ /** The page title. Defaults to "Workflows". */
8
+ title?: React.ReactNode;
9
+ description?: React.ReactNode;
10
+ /** The workflows to show, already filtered by the host (see `filterWorkflows`). */
11
+ workflows: readonly WorkflowSummary[];
12
+ /** The modules the module filter offers. Leave empty to hide that filter. */
13
+ modules: readonly WorkflowModule[];
14
+ filters: WorkflowGalleryFilters;
15
+ onFiltersChange: (filters: WorkflowGalleryFilters) => void;
16
+ /** Grid of cards or list of rows. Uncontrolled without it. */
17
+ view?: WorkflowGalleryView;
18
+ defaultView?: WorkflowGalleryView;
19
+ onViewChange?: (view: WorkflowGalleryView) => void;
20
+ loading?: boolean;
21
+ error?: {
22
+ message: string;
23
+ } | null;
24
+ onRetry?: () => void;
25
+ /** Shown only when the person may create. */
26
+ onCreate?: () => void;
27
+ /** Shown only when the person may import. */
28
+ onImport?: () => void;
29
+ /** The actions the host permits on each workflow. */
30
+ actionsFor?: (workflow: WorkflowSummary) => WorkflowCardActions;
31
+ /** The host's preview of each workflow. */
32
+ renderPreview?: (workflow: WorkflowSummary) => React.ReactNode;
33
+ className?: string;
34
+ }
35
+ /**
36
+ * The workflow gallery: a full page of saved definitions over WorkflowCard,
37
+ * with search, a module filter, scope and status filters, grid and list
38
+ * views, and create and import when the host permits them. Host-driven: the
39
+ * filters are controlled and the host returns the matching workflows (a host
40
+ * holding the whole set uses `filterWorkflows`). Empty and no-matches are
41
+ * different states.
42
+ */
43
+ export declare function WorkflowGallery({ title, description, workflows, modules, filters, onFiltersChange, view: viewProp, defaultView, onViewChange, loading, error, onRetry, onCreate, onImport, actionsFor, renderPreview, className, }: WorkflowGalleryProps): React.JSX.Element;