@corbits/react-ui 0.1.0 → 0.3.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 (170) hide show
  1. package/LICENSE +174 -499
  2. package/README.md +228 -85
  3. package/dist/blocks/login/auth-layout.d.ts +1 -1
  4. package/dist/blocks/login/auth-layout.js +3 -33
  5. package/dist/blocks/login/login-form.js +6 -119
  6. package/dist/hooks/use-command-palette-navigation.js +18 -17
  7. package/dist/hooks/use-controllable-state.d.ts +2 -2
  8. package/dist/hooks/use-controllable-state.js +23 -15
  9. package/dist/hooks/use-delayed-autofocus.js +10 -11
  10. package/dist/hooks/use-dismissable-popover.d.ts +2 -2
  11. package/dist/hooks/use-dismissable-popover.js +21 -29
  12. package/dist/hooks/use-list-selection.js +54 -85
  13. package/dist/hooks/use-prefers-reduced-motion.js +8 -4
  14. package/dist/hooks/use-scroll-current-into-view.js +5 -7
  15. package/dist/index.d.ts +0 -11
  16. package/dist/index.js +10 -19
  17. package/dist/lib/artifact.js +16 -35
  18. package/dist/lib/chart-geometry.js +33 -20
  19. package/dist/lib/chart-palette.js +17 -31
  20. package/dist/lib/chat-message.js +32 -13
  21. package/dist/lib/chat-parts.js +2 -1
  22. package/dist/lib/csv.js +42 -26
  23. package/dist/lib/metrics.js +30 -48
  24. package/dist/lib/now-item.js +26 -25
  25. package/dist/lib/relative-time.js +14 -29
  26. package/dist/lib/theme.js +32 -30
  27. package/dist/lib/url.js +9 -8
  28. package/dist/lib/workflow-display-flow.js +37 -33
  29. package/dist/lib/workflow-registry.d.ts +0 -41
  30. package/dist/lib/workflow-registry.js +13 -23
  31. package/dist/lib/workflow-run-error.js +16 -20
  32. package/dist/lib/workflow-run-progress.js +94 -52
  33. package/dist/lib/workflow-run.js +17 -13
  34. package/dist/styles.css +257 -19
  35. package/dist/test/render-hook.js +11 -12
  36. package/dist/theme.css +92 -14
  37. package/dist/ui/agent-turn.d.ts +1 -1
  38. package/dist/ui/agent-turn.js +4 -37
  39. package/dist/ui/animated-number.js +15 -28
  40. package/dist/ui/approval-card.js +5 -90
  41. package/dist/ui/artifact-body.js +47 -125
  42. package/dist/ui/artifact-notice.d.ts +1 -1
  43. package/dist/ui/artifact-notice.js +9 -16
  44. package/dist/ui/avatar.d.ts +21 -4
  45. package/dist/ui/avatar.js +40 -49
  46. package/dist/ui/badge.js +4 -12
  47. package/dist/ui/bar-chart.d.ts +1 -1
  48. package/dist/ui/bar-chart.js +22 -74
  49. package/dist/ui/block-card.js +7 -45
  50. package/dist/ui/boot-screen.d.ts +1 -1
  51. package/dist/ui/boot-screen.js +4 -33
  52. package/dist/ui/bulk-action-bar.d.ts +1 -1
  53. package/dist/ui/bulk-action-bar.js +14 -33
  54. package/dist/ui/button.js +10 -24
  55. package/dist/ui/card.js +5 -25
  56. package/dist/ui/category-bars.js +14 -59
  57. package/dist/ui/chart-frame.d.ts +2 -2
  58. package/dist/ui/chart-frame.js +6 -85
  59. package/dist/ui/chat-composer.js +4 -16
  60. package/dist/ui/chat-dock-timing.js +7 -3
  61. package/dist/ui/chat-input.js +20 -100
  62. package/dist/ui/chat-panel.d.ts +1 -1
  63. package/dist/ui/chat-panel.js +7 -41
  64. package/dist/ui/chat-thread.js +13 -31
  65. package/dist/ui/checkbox.js +10 -52
  66. package/dist/ui/command-palette.js +85 -246
  67. package/dist/ui/compare-body.d.ts +1 -1
  68. package/dist/ui/compare-body.js +28 -98
  69. package/dist/ui/confirm-button.js +8 -15
  70. package/dist/ui/corbits-mark.js +7 -16
  71. package/dist/ui/csv-table.d.ts +1 -1
  72. package/dist/ui/csv-table.js +8 -50
  73. package/dist/ui/dialog.d.ts +1 -1
  74. package/dist/ui/dialog.js +32 -62
  75. package/dist/ui/dictation-status-line.js +7 -25
  76. package/dist/ui/dither-canvas.d.ts +1 -1
  77. package/dist/ui/dither-canvas.js +31 -56
  78. package/dist/ui/embed-body.d.ts +1 -1
  79. package/dist/ui/embed-body.js +7 -56
  80. package/dist/ui/empty-state.d.ts +1 -1
  81. package/dist/ui/empty-state.js +3 -26
  82. package/dist/ui/file-input.js +17 -37
  83. package/dist/ui/filter-bar.d.ts +1 -1
  84. package/dist/ui/filter-bar.js +11 -71
  85. package/dist/ui/filter-chip.d.ts +1 -1
  86. package/dist/ui/filter-chip.js +7 -15
  87. package/dist/ui/gate-block.d.ts +2 -1
  88. package/dist/ui/gate-block.js +4 -29
  89. package/dist/ui/horizontal-stepper.d.ts +1 -1
  90. package/dist/ui/horizontal-stepper.js +18 -45
  91. package/dist/ui/input.js +1 -6
  92. package/dist/ui/inspector-shell.d.ts +5 -5
  93. package/dist/ui/inspector-shell.js +14 -81
  94. package/dist/ui/intake-form.d.ts +1 -1
  95. package/dist/ui/intake-form.js +21 -97
  96. package/dist/ui/kind-card-grid.js +9 -31
  97. package/dist/ui/library-page-header.js +4 -32
  98. package/dist/ui/list-detail.js +5 -35
  99. package/dist/ui/live-run-banner.d.ts +1 -1
  100. package/dist/ui/live-run-banner.js +5 -15
  101. package/dist/ui/live-status-line.js +6 -24
  102. package/dist/ui/menu.js +7 -24
  103. package/dist/ui/message-bubble.d.ts +3 -3
  104. package/dist/ui/message-bubble.js +5 -44
  105. package/dist/ui/mic-button.d.ts +1 -1
  106. package/dist/ui/mic-button.js +15 -21
  107. package/dist/ui/mic-permission-dialog.js +5 -41
  108. package/dist/ui/notifications-bell.js +9 -43
  109. package/dist/ui/now-cards.d.ts +2 -2
  110. package/dist/ui/now-cards.js +16 -94
  111. package/dist/ui/page-panel.js +11 -11
  112. package/dist/ui/page-shell.js +4 -11
  113. package/dist/ui/parts-renderer.js +51 -149
  114. package/dist/ui/profile-card.js +7 -111
  115. package/dist/ui/progress-checklist.js +11 -49
  116. package/dist/ui/provider-mark.d.ts +2 -2
  117. package/dist/ui/provider-mark.js +9 -16
  118. package/dist/ui/quick-reply-chips.js +6 -14
  119. package/dist/ui/quote-card.js +12 -32
  120. package/dist/ui/reasoning-block.d.ts +1 -1
  121. package/dist/ui/reasoning-block.js +14 -33
  122. package/dist/ui/research-body.js +32 -213
  123. package/dist/ui/rich-empty-state.js +6 -48
  124. package/dist/ui/run-now-button.js +14 -15
  125. package/dist/ui/section.d.ts +1 -1
  126. package/dist/ui/section.js +4 -40
  127. package/dist/ui/select.js +1 -15
  128. package/dist/ui/selection-checkbox.d.ts +1 -1
  129. package/dist/ui/selection-checkbox.js +11 -29
  130. package/dist/ui/settings-panel.js +7 -50
  131. package/dist/ui/shimmer-text.js +3 -6
  132. package/dist/ui/sidebar-item-row.d.ts +1 -1
  133. package/dist/ui/sidebar-item-row.js +9 -39
  134. package/dist/ui/sidebar-panel.js +15 -40
  135. package/dist/ui/sidebar.js +15 -76
  136. package/dist/ui/skeleton.js +3 -7
  137. package/dist/ui/sortable-table.js +29 -72
  138. package/dist/ui/sparkline.js +14 -38
  139. package/dist/ui/stat-grid.js +17 -52
  140. package/dist/ui/status-dot.d.ts +1 -1
  141. package/dist/ui/status-dot.js +8 -20
  142. package/dist/ui/step-list.d.ts +1 -0
  143. package/dist/ui/step-list.js +11 -50
  144. package/dist/ui/switch.js +4 -16
  145. package/dist/ui/table.js +7 -32
  146. package/dist/ui/tabs.js +39 -54
  147. package/dist/ui/textarea.js +10 -19
  148. package/dist/ui/theme-provider.js +54 -78
  149. package/dist/ui/theme-toggle.js +13 -16
  150. package/dist/ui/thinking-indicator.js +3 -15
  151. package/dist/ui/thinking-label.js +16 -35
  152. package/dist/ui/thinking-mark.d.ts +1 -1
  153. package/dist/ui/thinking-mark.js +4 -145
  154. package/dist/ui/time-series-chart.js +43 -174
  155. package/dist/ui/toast.js +12 -20
  156. package/dist/ui/token-mosaic.js +5 -43
  157. package/dist/ui/tool-block.js +18 -70
  158. package/dist/ui/tool-narrative.d.ts +1 -1
  159. package/dist/ui/tool-narrative.js +10 -57
  160. package/dist/ui/tool-picker.js +57 -145
  161. package/dist/ui/tooltip.d.ts +1 -1
  162. package/dist/ui/tooltip.js +4 -33
  163. package/dist/ui/top-bar.js +14 -72
  164. package/dist/ui/trace-waterfall.d.ts +1 -1
  165. package/dist/ui/trace-waterfall.js +21 -93
  166. package/dist/ui/typing-indicator.d.ts +1 -1
  167. package/dist/ui/typing-indicator.js +4 -18
  168. package/dist/ui/view-toggle.js +10 -29
  169. package/dist/ui/voice-waveform.js +3 -11
  170. package/package.json +96 -535
package/dist/theme.css CHANGED
@@ -274,7 +274,11 @@
274
274
  a `.dark` override of their own. */
275
275
  :root {
276
276
  --sidebar-row-radius: var(--radius);
277
- --sidebar-row-selected-bg: color-mix(in srgb, var(--primary) 10%, transparent);
277
+ --sidebar-row-selected-bg: color-mix(
278
+ in srgb,
279
+ var(--primary) 10%,
280
+ transparent
281
+ );
278
282
  }
279
283
 
280
284
  @theme inline {
@@ -601,28 +605,102 @@
601
605
  }
602
606
  }
603
607
 
608
+ /* Avatar working orbit. The conic angle is a registered property so the arc
609
+ sweeps while the masked ring (and the avatar it encircles) never rotates.
610
+ Used by ui/avatar. */
611
+ @property --corbits-avatar-angle {
612
+ syntax: "<angle>";
613
+ inherits: false;
614
+ initial-value: 0deg;
615
+ }
616
+ @keyframes corbits-avatar-orbit {
617
+ to {
618
+ --corbits-avatar-angle: 360deg;
619
+ }
620
+ }
621
+ .corbits-avatar-orbit {
622
+ position: absolute;
623
+ inset: -4px;
624
+ padding: 2px;
625
+ background:
626
+ conic-gradient(
627
+ from var(--corbits-avatar-angle),
628
+ var(--primary-emphasis) 0deg 90deg,
629
+ transparent 90deg 360deg
630
+ ),
631
+ color-mix(in srgb, var(--foreground) 12%, transparent);
632
+ -webkit-mask:
633
+ linear-gradient(#000 0 0) content-box,
634
+ linear-gradient(#000 0 0);
635
+ -webkit-mask-composite: xor;
636
+ mask:
637
+ linear-gradient(#000 0 0) content-box exclude,
638
+ linear-gradient(#000 0 0);
639
+ animation: corbits-avatar-orbit 1.4s linear infinite;
640
+ pointer-events: none;
641
+ }
642
+ @media (prefers-reduced-motion: reduce) {
643
+ .corbits-avatar-orbit {
644
+ animation: none;
645
+ --corbits-avatar-angle: 120deg;
646
+ }
647
+ }
648
+
604
649
  /* Ridge-study motion: keep geometry and timing in the original 100 x 70 space. */
605
650
  @keyframes corbits-thinking-sweep {
606
- 0% { transform: translateX(8px); opacity: 0; }
607
- 8%, 86% { opacity: 1; }
608
- 100% { transform: translateX(130px); opacity: 0; }
651
+ 0% {
652
+ transform: translateX(8px);
653
+ opacity: 0;
654
+ }
655
+ 8%,
656
+ 86% {
657
+ opacity: 1;
658
+ }
659
+ 100% {
660
+ transform: translateX(130px);
661
+ opacity: 0;
662
+ }
609
663
  }
610
664
  @keyframes corbits-thinking-rise {
611
- 0% { transform: translateY(88px); }
612
- 100% { transform: translateY(-30px); }
665
+ 0% {
666
+ transform: translateY(88px);
667
+ }
668
+ 100% {
669
+ transform: translateY(-30px);
670
+ }
613
671
  }
614
672
  @keyframes corbits-thinking-wave {
615
- 0% { transform: scale(.03); opacity: 0; }
616
- 10% { opacity: 1; }
617
- 75% { opacity: .9; }
618
- 100% { transform: scale(1.35); opacity: 0; }
673
+ 0% {
674
+ transform: scale(0.03);
675
+ opacity: 0;
676
+ }
677
+ 10% {
678
+ opacity: 1;
679
+ }
680
+ 75% {
681
+ opacity: 0.9;
682
+ }
683
+ 100% {
684
+ transform: scale(1.35);
685
+ opacity: 0;
686
+ }
619
687
  }
620
688
  @media (prefers-reduced-motion: reduce) {
621
689
  /* Static poses land the gradient mid-stroke. These offsets mirror the SVG
622
690
  geometry in thinking-mark.tsx — change the rect/circle geometry there and
623
691
  these poses have to be re-picked to match. */
624
- .corbits-thinking-silk, .corbits-thinking-strata, .corbits-thinking-echo { animation: none; }
625
- .corbits-thinking-silk { transform: translateX(44px); }
626
- .corbits-thinking-strata { transform: translateY(30px); }
627
- .corbits-thinking-echo { transform: scale(.6); }
692
+ .corbits-thinking-silk,
693
+ .corbits-thinking-strata,
694
+ .corbits-thinking-echo {
695
+ animation: none;
696
+ }
697
+ .corbits-thinking-silk {
698
+ transform: translateX(44px);
699
+ }
700
+ .corbits-thinking-strata {
701
+ transform: translateY(30px);
702
+ }
703
+ .corbits-thinking-echo {
704
+ transform: scale(0.6);
705
+ }
628
706
  }
@@ -5,7 +5,7 @@ export type AgentTurnProps = {
5
5
  readonly identity: AgentIdentity;
6
6
  /** Rich body for the answer — a markdown renderer. */
7
7
  readonly children?: ReactNode;
8
- readonly now?: number;
8
+ readonly now?: number | undefined;
9
9
  /** e.g. "Thought for 4s". Shown on the reasoning block once it stops streaming. */
10
10
  readonly reasoningDurationLabel?: string;
11
11
  readonly className?: string;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { agentInitials, isTurnWorking, messageText, reasoningText, toolParts } from "../lib/chat-message.js";
2
+ import { agentInitials, isTurnWorking, messageText, reasoningText, toolParts, } from "../lib/chat-message.js";
3
3
  import { cn } from "../lib/utils.js";
4
4
  import { MessageBubble } from "./message-bubble.js";
5
5
  import { ReasoningBlock } from "./reasoning-block.js";
@@ -16,45 +16,12 @@ import { ToolNarrative } from "./tool-narrative.js";
16
16
  * The avatar is `aria-hidden` and the turn is a labelled region instead: a
17
17
  * screen reader should hear "Ada said …" once, not two initials followed by the
18
18
  * name.
19
- */ export function AgentTurn({ message, identity, children, now, reasoningDurationLabel, className }) {
19
+ */
20
+ export function AgentTurn({ message, identity, children, now, reasoningDurationLabel, className, }) {
20
21
  const working = isTurnWorking(message);
21
22
  const answer = messageText(message);
22
23
  const thinking = reasoningText(message);
23
24
  const tools = toolParts(message);
24
25
  const hasWork = thinking.length > 0 || tools.length > 0;
25
- return /*#__PURE__*/ _jsxs("article", {
26
- "data-slot": "agent-turn",
27
- "aria-label": `${identity.name} replied`,
28
- className: cn("flex gap-3", className),
29
- children: [
30
- /*#__PURE__*/ _jsx("span", {
31
- "aria-hidden": true,
32
- className: "mt-0.5 grid size-7 shrink-0 place-items-center rounded-full bg-secondary text-[10px] font-bold text-secondary-foreground",
33
- children: agentInitials(identity)
34
- }),
35
- /*#__PURE__*/ _jsxs("div", {
36
- className: "flex min-w-0 flex-1 flex-col gap-1.5",
37
- children: [
38
- hasWork ? /*#__PURE__*/ _jsxs("div", {
39
- className: "flex flex-col",
40
- children: [
41
- /*#__PURE__*/ _jsx(ReasoningBlock, {
42
- text: thinking,
43
- streaming: working,
44
- durationLabel: reasoningDurationLabel
45
- }),
46
- tools.map((part)=>/*#__PURE__*/ _jsx(ToolNarrative, {
47
- part: part
48
- }, part.toolCallId))
49
- ]
50
- }) : null,
51
- answer.length > 0 || children !== undefined ? /*#__PURE__*/ _jsx(MessageBubble, {
52
- message: message,
53
- now: now,
54
- children: children
55
- }) : null
56
- ]
57
- })
58
- ]
59
- });
26
+ return (_jsxs("article", { "data-slot": "agent-turn", "aria-label": `${identity.name} replied`, className: cn("flex gap-3", className), children: [_jsx("span", { "aria-hidden": true, className: "mt-0.5 grid size-7 shrink-0 place-items-center rounded-full bg-secondary text-[10px] font-bold text-secondary-foreground", children: agentInitials(identity) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [hasWork ? (_jsxs("div", { className: "flex flex-col", children: [_jsx(ReasoningBlock, { text: thinking, streaming: working, durationLabel: reasoningDurationLabel }), tools.map((part) => (_jsx(ToolNarrative, { part: part }, part.toolCallId)))] })) : null, answer.length > 0 || children !== undefined ? (_jsx(MessageBubble, { message: message, now: now, children: children })) : null] })] }));
60
27
  }
@@ -1,8 +1,9 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useEffect, useRef, useState } from "react";
3
4
  import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
4
5
  import { cn } from "../lib/utils.js";
5
- const easeOut = (t)=>1 - (1 - t) ** 3;
6
+ const easeOut = (t) => 1 - (1 - t) ** 3;
6
7
  /**
7
8
  * A number that counts to its new value instead of snapping.
8
9
  *
@@ -17,12 +18,13 @@ const easeOut = (t)=>1 - (1 - t) ** 3;
17
18
  * Reduced motion is checked here rather than left to the theme's CSS override.
18
19
  * The theme can collapse a CSS animation, but this is a JS interpolation the
19
20
  * cascade cannot reach, so it snaps to the value instead.
20
- */ export function AnimatedNumber({ value, decimals = 0, format, durationMs = 600, className }) {
21
+ */
22
+ export function AnimatedNumber({ value, decimals = 0, format, durationMs = 600, className, }) {
21
23
  const [display, setDisplay] = useState(value);
22
24
  const fromRef = useRef(value);
23
25
  const frameRef = useRef(0);
24
26
  const reduce = usePrefersReducedMotion();
25
- useEffect(()=>{
27
+ useEffect(() => {
26
28
  const from = fromRef.current;
27
29
  if (reduce || from === value) {
28
30
  fromRef.current = value;
@@ -30,34 +32,19 @@ const easeOut = (t)=>1 - (1 - t) ** 3;
30
32
  return;
31
33
  }
32
34
  const start = performance.now();
33
- const step = (now)=>{
35
+ const step = (now) => {
34
36
  const t = Math.min((now - start) / durationMs, 1);
35
- setDisplay(from + (value - from) * easeOut(t));
36
- if (t < 1) {
37
+ const current = from + (value - from) * easeOut(t);
38
+ // A value change mid-count continues from what is on screen, not from
39
+ // the previous target.
40
+ fromRef.current = current;
41
+ setDisplay(current);
42
+ if (t < 1)
37
43
  frameRef.current = requestAnimationFrame(step);
38
- } else {
39
- fromRef.current = value;
40
- }
41
44
  };
42
45
  frameRef.current = requestAnimationFrame(step);
43
- return ()=>cancelAnimationFrame(frameRef.current);
44
- }, [
45
- value,
46
- durationMs,
47
- reduce
48
- ]);
46
+ return () => cancelAnimationFrame(frameRef.current);
47
+ }, [value, durationMs, reduce]);
49
48
  const text = format === undefined ? display.toFixed(decimals) : format(display);
50
- return /*#__PURE__*/ _jsxs("span", {
51
- className: cn("tabular-nums", className),
52
- children: [
53
- /*#__PURE__*/ _jsx("span", {
54
- "aria-hidden": true,
55
- children: text
56
- }),
57
- /*#__PURE__*/ _jsx("span", {
58
- className: "sr-only",
59
- children: format === undefined ? value.toFixed(decimals) : format(value)
60
- })
61
- ]
62
- });
49
+ return (_jsxs("span", { className: cn("tabular-nums", className), children: [_jsx("span", { "aria-hidden": true, children: text }), _jsx("span", { className: "sr-only", children: format === undefined ? value.toFixed(decimals) : format(value) })] }));
63
50
  }
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useState } from "react";
3
4
  import { cn } from "../lib/utils.js";
@@ -17,100 +18,14 @@ const LONG_VALUE = 140;
17
18
  * Every argument the action will run with is shown. A card that hides them
18
19
  * behind "details" is asking for approval of something the user cannot see, so
19
20
  * long values clamp rather than disappear.
20
- */ export function ApprovalCard({ request, onApprove, onReject, onAllowAlways, state = "idle", error = null, className, children }) {
21
+ */
22
+ export function ApprovalCard({ request, onApprove, onReject, onAllowAlways, state = "idle", error = null, className, children, }) {
21
23
  const busy = state !== "idle";
22
24
  const canAllowAlways = onAllowAlways !== undefined && request.actionKey !== undefined;
23
- return /*#__PURE__*/ _jsxs("section", {
24
- "aria-label": `Approval requested: ${request.headline}`,
25
- className: cn("flex flex-col gap-3 rounded-lg border border-primary-emphasis bg-card p-4", className),
26
- children: [
27
- /*#__PURE__*/ _jsxs("div", {
28
- className: "flex flex-wrap items-center gap-2",
29
- children: [
30
- /*#__PURE__*/ _jsx(Badge, {
31
- tone: "accent",
32
- children: "Needs your approval"
33
- }),
34
- /*#__PURE__*/ _jsxs("span", {
35
- className: "text-xs text-muted-foreground",
36
- children: [
37
- "Requested by ",
38
- request.requestedBy
39
- ]
40
- })
41
- ]
42
- }),
43
- /*#__PURE__*/ _jsx("p", {
44
- className: "text-sm font-semibold",
45
- children: request.headline
46
- }),
47
- request.details === undefined || request.details.length === 0 ? null : /*#__PURE__*/ _jsx("dl", {
48
- className: "flex flex-col gap-1.5 rounded-md bg-muted p-3 text-xs",
49
- children: request.details.map((detail)=>/*#__PURE__*/ _jsx(ApprovalDetailRow, {
50
- detail: detail
51
- }, detail.label))
52
- }),
53
- children,
54
- error === null ? null : /*#__PURE__*/ _jsx("p", {
55
- role: "alert",
56
- className: "text-sm text-destructive",
57
- children: error
58
- }),
59
- /*#__PURE__*/ _jsxs("div", {
60
- className: "flex flex-wrap items-center gap-2",
61
- children: [
62
- /*#__PURE__*/ _jsx(Button, {
63
- size: "sm",
64
- onClick: onApprove,
65
- disabled: busy,
66
- children: state === "approving" ? "Approving…" : "Approve"
67
- }),
68
- /*#__PURE__*/ _jsx(Button, {
69
- variant: "outline",
70
- size: "sm",
71
- onClick: onReject,
72
- disabled: busy,
73
- children: state === "rejecting" ? "Rejecting…" : "Reject"
74
- }),
75
- canAllowAlways ? /*#__PURE__*/ _jsx(Button, {
76
- variant: "ghost",
77
- size: "sm",
78
- onClick: ()=>onAllowAlways(request.actionKey),
79
- disabled: busy,
80
- className: "ml-auto",
81
- children: state === "allowing-always" ? "Saving…" : "Always allow this"
82
- }) : null
83
- ]
84
- })
85
- ]
86
- });
25
+ return (_jsxs("section", { "aria-label": `Approval requested: ${request.headline}`, className: cn("flex flex-col gap-3 rounded-lg border border-primary-emphasis bg-card p-4", className), children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Badge, { tone: "accent", children: "Needs your approval" }), _jsxs("span", { className: "text-xs text-muted-foreground", children: ["Requested by ", request.requestedBy] })] }), _jsx("p", { className: "text-sm font-semibold", children: request.headline }), request.details === undefined || request.details.length === 0 ? null : (_jsx("dl", { className: "flex flex-col gap-1.5 rounded-md bg-muted p-3 text-xs", children: request.details.map((detail) => (_jsx(ApprovalDetailRow, { detail: detail }, detail.label))) })), children, error === null ? null : (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: error })), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Button, { size: "sm", onClick: onApprove, disabled: busy, children: state === "approving" ? "Approving…" : "Approve" }), _jsx(Button, { variant: "outline", size: "sm", onClick: onReject, disabled: busy, children: state === "rejecting" ? "Rejecting…" : "Reject" }), canAllowAlways ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => onAllowAlways(request.actionKey), disabled: busy, className: "ml-auto", children: state === "allowing-always" ? "Saving…" : "Always allow this" })) : null] })] }));
87
26
  }
88
27
  function ApprovalDetailRow({ detail }) {
89
28
  const [expanded, setExpanded] = useState(false);
90
29
  const long = detail.value.length > LONG_VALUE;
91
- return /*#__PURE__*/ _jsxs("div", {
92
- className: "grid grid-cols-[minmax(0,8rem)_1fr] gap-2",
93
- children: [
94
- /*#__PURE__*/ _jsx("dt", {
95
- className: "truncate text-muted-foreground",
96
- children: detail.label
97
- }),
98
- /*#__PURE__*/ _jsxs("dd", {
99
- className: "min-w-0",
100
- children: [
101
- /*#__PURE__*/ _jsx("span", {
102
- className: cn("block break-words whitespace-pre-wrap", long && !expanded && "line-clamp-2"),
103
- children: detail.value
104
- }),
105
- long ? /*#__PURE__*/ _jsx("button", {
106
- type: "button",
107
- onClick: ()=>setExpanded((value)=>!value),
108
- "aria-expanded": expanded,
109
- className: "mt-0.5 text-primary-emphasis hover:underline",
110
- children: expanded ? "Show less" : "Show more"
111
- }) : null
112
- ]
113
- })
114
- ]
115
- });
30
+ return (_jsxs("div", { className: "grid grid-cols-[minmax(0,8rem)_1fr] gap-2", children: [_jsx("dt", { className: "truncate text-muted-foreground", children: detail.label }), _jsxs("dd", { className: "min-w-0", children: [_jsx("span", { className: cn("block break-words whitespace-pre-wrap", long && !expanded && "line-clamp-2"), children: detail.value }), long ? (_jsx("button", { type: "button", onClick: () => setExpanded((value) => !value), "aria-expanded": expanded, className: "mt-0.5 text-primary-emphasis hover:underline", children: expanded ? "Show less" : "Show more" })) : null] })] }));
116
31
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Download } from "lucide-react";
3
- import { artifactForm } from "../lib/artifact.js";
3
+ import { artifactForm, } from "../lib/artifact.js";
4
4
  import { toSafeHref } from "../lib/url.js";
5
5
  import { cn } from "../lib/utils.js";
6
6
  import { ArtifactNotice } from "./artifact-notice.js";
@@ -22,133 +22,57 @@ import { ResearchBody, parseResearchArtifact } from "./research-body.js";
22
22
  * "empty" is a real state here — artifacts are frequently rendered while they
23
23
  * are still being written — and each form's own empty rendering (a table with
24
24
  * no rows, a frame with no src) reads as a fault rather than as progress.
25
- */ export function ArtifactBody({ artifact, form, className }) {
25
+ */
26
+ export function ArtifactBody({ artifact, form, className }) {
26
27
  const resolved = form ?? artifactForm(artifact.kind);
27
28
  const content = artifact.content;
28
29
  const blank = content.trim() === "";
29
- return /*#__PURE__*/ _jsx("div", {
30
- className: cn("w-full", className),
31
- children: body()
32
- });
30
+ return _jsx("div", { className: cn("w-full", className), children: body() });
33
31
  function body() {
34
- switch(resolved){
35
- case "image":
36
- {
37
- // The thumbnail URL, not the content: an image artifact's bytes live
38
- // behind a route, and `content` for one is either empty or a caption.
39
- const src = toSafeHref(artifact.thumbnailUrl ?? artifact.downloadUrl);
40
- if (src === undefined) return /*#__PURE__*/ _jsx(ArtifactNotice, {
41
- pending: artifact.pending
42
- });
43
- return /*#__PURE__*/ _jsxs("figure", {
44
- className: "flex flex-col gap-3",
45
- children: [
46
- /*#__PURE__*/ _jsx("img", {
47
- src: src,
48
- alt: artifact.title,
49
- className: "max-h-[480px] max-w-full rounded-lg border border-border"
50
- }),
51
- blank ? null : /*#__PURE__*/ _jsx("figcaption", {
52
- className: "text-xs text-muted-foreground",
53
- children: content
54
- })
55
- ]
56
- });
57
- }
58
- case "download":
59
- {
60
- const downloadUrl = toSafeHref(artifact.downloadUrl);
61
- if (downloadUrl === undefined) {
62
- return /*#__PURE__*/ _jsx(ArtifactNotice, {
63
- message: "This file has no download location, so it cannot be retrieved."
64
- });
65
- }
66
- return /*#__PURE__*/ _jsxs("a", {
67
- href: downloadUrl,
68
- download: true,
69
- className: buttonVariants({
70
- variant: "outline",
71
- size: "sm"
72
- }),
73
- children: [
74
- /*#__PURE__*/ _jsx(Download, {
75
- "aria-hidden": true
76
- }),
77
- "Download ",
78
- artifact.title
79
- ]
80
- });
32
+ switch (resolved) {
33
+ case "image": {
34
+ // The thumbnail URL, not the content: an image artifact's bytes live
35
+ // behind a route, and `content` for one is either empty or a caption.
36
+ const src = toSafeHref(artifact.thumbnailUrl ?? artifact.downloadUrl);
37
+ if (src === undefined)
38
+ return _jsx(ArtifactNotice, { pending: artifact.pending });
39
+ return (_jsxs("figure", { className: "flex flex-col gap-3", children: [_jsx("img", { src: src, alt: artifact.title, className: "max-h-[480px] max-w-full rounded-lg border border-border" }), blank ? null : (_jsx("figcaption", { className: "text-xs text-muted-foreground", children: content }))] }));
40
+ }
41
+ case "download": {
42
+ const downloadUrl = toSafeHref(artifact.downloadUrl);
43
+ if (downloadUrl === undefined) {
44
+ return (_jsx(ArtifactNotice, { message: "This file has no download location, so it cannot be retrieved." }));
81
45
  }
46
+ return (_jsxs("a", { href: downloadUrl, download: true, className: buttonVariants({ variant: "outline", size: "sm" }), children: [_jsx(Download, { "aria-hidden": true }), "Download ", artifact.title] }));
47
+ }
82
48
  case "embed":
83
- return /*#__PURE__*/ _jsx(EmbedBody, {
84
- url: content.trim(),
85
- title: artifact.title,
86
- ...artifact.downloadUrl === undefined ? {} : {
87
- downloadUrl: artifact.downloadUrl
88
- }
89
- });
90
- case "table":
91
- {
92
- if (blank) return /*#__PURE__*/ _jsx(ArtifactNotice, {
93
- pending: artifact.pending
94
- });
95
- const downloadUrl = toSafeHref(artifact.downloadUrl);
96
- return /*#__PURE__*/ _jsxs("div", {
97
- className: "flex flex-col gap-3",
98
- children: [
99
- downloadUrl === undefined ? null : /*#__PURE__*/ _jsxs("a", {
100
- href: downloadUrl,
101
- download: true,
102
- className: cn(buttonVariants({
103
- variant: "outline",
104
- size: "sm"
105
- }), "self-start"),
106
- children: [
107
- /*#__PURE__*/ _jsx(Download, {
108
- "aria-hidden": true
109
- }),
110
- "Download"
111
- ]
112
- }),
113
- /*#__PURE__*/ _jsx(CsvTable, {
114
- text: content,
115
- caption: artifact.title
116
- })
117
- ]
118
- });
119
- }
49
+ return (_jsx(EmbedBody, { url: content.trim(), title: artifact.title, ...(artifact.downloadUrl === undefined
50
+ ? {}
51
+ : { downloadUrl: artifact.downloadUrl }) }));
52
+ case "table": {
53
+ if (blank)
54
+ return _jsx(ArtifactNotice, { pending: artifact.pending });
55
+ const downloadUrl = toSafeHref(artifact.downloadUrl);
56
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [downloadUrl === undefined ? null : (_jsxs("a", { href: downloadUrl, download: true, className: cn(buttonVariants({ variant: "outline", size: "sm" }), "self-start"), children: [_jsx(Download, { "aria-hidden": true }), "Download"] })), _jsx(CsvTable, { text: content, caption: artifact.title })] }));
57
+ }
120
58
  case "comparison":
121
- return /*#__PURE__*/ _jsx(CompareBodyFromJson, {
122
- content: content,
123
- ...artifact.pending === undefined ? {} : {
124
- pending: artifact.pending
125
- }
126
- });
127
- case "research":
128
- {
129
- if (blank) return /*#__PURE__*/ _jsx(ArtifactNotice, {
130
- pending: artifact.pending
131
- });
132
- const parsed = parseResearchArtifact(content);
133
- // Unparseable is not a failure worth an error card — the payload is
134
- // still the artifact, so fall through to prose and let it be read.
135
- if (parsed === null) return /*#__PURE__*/ _jsx(Prose, {
136
- text: content
137
- });
138
- return /*#__PURE__*/ _jsx(ResearchBody, {
139
- brief: parsed.brief,
140
- ...parsed.body === undefined ? {} : {
141
- body: parsed.body
142
- }
143
- });
144
- }
59
+ return (_jsx(CompareBodyFromJson, { content: content, ...(artifact.pending === undefined
60
+ ? {}
61
+ : { pending: artifact.pending }) }));
62
+ case "research": {
63
+ if (blank)
64
+ return _jsx(ArtifactNotice, { pending: artifact.pending });
65
+ const parsed = parseResearchArtifact(content);
66
+ // Unparseable is not a failure worth an error card — the payload is
67
+ // still the artifact, so fall through to prose and let it be read.
68
+ if (parsed === null)
69
+ return _jsx(Prose, { text: content });
70
+ return (_jsx(ResearchBody, { brief: parsed.brief, ...(parsed.body === undefined ? {} : { body: parsed.body }) }));
71
+ }
145
72
  case "prose":
146
- if (blank) return /*#__PURE__*/ _jsx(ArtifactNotice, {
147
- pending: artifact.pending
148
- });
149
- return /*#__PURE__*/ _jsx(Prose, {
150
- text: content
151
- });
73
+ if (blank)
74
+ return _jsx(ArtifactNotice, { pending: artifact.pending });
75
+ return _jsx(Prose, { text: content });
152
76
  }
153
77
  }
154
78
  }
@@ -162,9 +86,7 @@ import { ResearchBody, parseResearchArtifact } from "./research-body.js";
162
86
  *
163
87
  * `max-w-prose` caps the measure at roughly 65 characters. Full-bleed body text
164
88
  * on a wide screen is unreadable regardless of how good the type is.
165
- */ function Prose({ text }) {
166
- return /*#__PURE__*/ _jsx("div", {
167
- className: "max-w-prose text-sm leading-relaxed whitespace-pre-wrap",
168
- children: text
169
- });
89
+ */
90
+ function Prose({ text }) {
91
+ return (_jsx("div", { className: "max-w-prose text-sm leading-relaxed whitespace-pre-wrap", children: text }));
170
92
  }
@@ -16,6 +16,6 @@ export declare function ArtifactNotice({
16
16
  pending,
17
17
  /** Overrides the pending/empty wording — use for a specific failure. */
18
18
  message, }: {
19
- readonly pending?: boolean;
19
+ readonly pending?: boolean | undefined;
20
20
  readonly message?: string;
21
21
  }): import("react").JSX.Element;
@@ -13,24 +13,17 @@ import { EmptyState } from "./empty-state.js";
13
13
  * artifact means the producer has not finished writing; an empty payload on a
14
14
  * settled one means there is genuinely nothing. Same markup, opposite message,
15
15
  * and getting it backwards makes a working system look broken.
16
- */ export function ArtifactNotice({ /** The producer is still writing. Changes the message, not the layout. */ pending = false, /** Overrides the pending/empty wording — use for a specific failure. */ message }) {
16
+ */
17
+ export function ArtifactNotice({
18
+ /** The producer is still writing. Changes the message, not the layout. */
19
+ pending = false,
20
+ /** Overrides the pending/empty wording — use for a specific failure. */
21
+ message, }) {
17
22
  if (message !== undefined) {
18
- return /*#__PURE__*/ _jsx(EmptyState, {
19
- icon: /*#__PURE__*/ _jsx(FileWarning, {}),
20
- title: "Nothing to preview",
21
- description: message
22
- });
23
+ return (_jsx(EmptyState, { icon: _jsx(FileWarning, {}), title: "Nothing to preview", description: message }));
23
24
  }
24
25
  if (pending) {
25
- return /*#__PURE__*/ _jsx(EmptyState, {
26
- icon: /*#__PURE__*/ _jsx(Loader2, {}),
27
- title: "Still being produced",
28
- description: "This artifact has no content yet — check back shortly."
29
- });
26
+ return (_jsx(EmptyState, { icon: _jsx(Loader2, {}), title: "Still being produced", description: "This artifact has no content yet \u2014 check back shortly." }));
30
27
  }
31
- return /*#__PURE__*/ _jsx(EmptyState, {
32
- icon: /*#__PURE__*/ _jsx(FileWarning, {}),
33
- title: "No content",
34
- description: "This artifact is empty."
35
- });
28
+ return (_jsx(EmptyState, { icon: _jsx(FileWarning, {}), title: "No content", description: "This artifact is empty." }));
36
29
  }