@corbits/react-ui 0.1.0 → 0.2.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 +11 -11
  35. package/dist/test/render-hook.js +11 -12
  36. package/dist/theme.css +51 -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 +1 -1
  45. package/dist/ui/avatar.js +11 -48
  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 +71 -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 +94 -535
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { ChevronRight } from "lucide-react";
3
4
  import { useControllableState } from "../hooks/use-controllable-state.js";
@@ -9,7 +10,7 @@ const STATE_TEXT = {
9
10
  "output-available": "Done",
10
11
  error: "Failed",
11
12
  "approval-requested": "Needs approval",
12
- "output-denied": "Denied"
13
+ "output-denied": "Denied",
13
14
  };
14
15
  /**
15
16
  * Orange (`emphasis`) is reserved for `approval-requested` alone: it is the
@@ -18,25 +19,27 @@ const STATE_TEXT = {
18
19
  * through the dot's own pulse animation (`StatusDot`'s `live` prop) — giving
19
20
  * it orange too would stack two "look here" signals on a state that isn't
20
21
  * actually asking for anything.
21
- */ const STATE_TONE = {
22
+ */
23
+ const STATE_TONE = {
22
24
  pending: "neutral",
23
25
  running: "neutral",
24
26
  "output-available": "neutral",
25
27
  error: "danger",
26
28
  "approval-requested": "emphasis",
27
- "output-denied": "danger"
29
+ "output-denied": "danger",
28
30
  };
29
- /** True when the state carries no `error`/`running` weight in a warning sense but still opens by default. */ const OPENS_BY_DEFAULT = {
31
+ /** True when the state carries no `error`/`running` weight in a warning sense but still opens by default. */
32
+ const OPENS_BY_DEFAULT = {
30
33
  pending: false,
31
34
  running: false,
32
35
  "output-available": false,
33
36
  error: true,
34
37
  "approval-requested": true,
35
- "output-denied": true
38
+ "output-denied": true,
36
39
  };
37
40
  function humaniseToolName(name) {
38
41
  const [provider, ...rest] = name.split("__");
39
- const humanise = (value)=>{
42
+ const humanise = (value) => {
40
43
  const words = value.replace(/[_-]+/g, " ").trim();
41
44
  return words.charAt(0).toUpperCase() + words.slice(1);
42
45
  };
@@ -46,7 +49,7 @@ function humaniseToolName(name) {
46
49
  return humanise(name);
47
50
  }
48
51
  function stateDetailText(state) {
49
- switch(state.status){
52
+ switch (state.status) {
50
53
  case "output-available":
51
54
  return state.output;
52
55
  case "error":
@@ -60,7 +63,7 @@ function stateDetailText(state) {
60
63
  }
61
64
  }
62
65
  function stateDetailTitle(state) {
63
- switch(state.status){
66
+ switch (state.status) {
64
67
  case "error":
65
68
  return "Error";
66
69
  case "approval-requested":
@@ -84,87 +87,32 @@ function stateDetailTitle(state) {
84
87
  *
85
88
  * Uncontrolled by default, seeded from `defaultOpen` (or the state's own
86
89
  * default). Pass `open`/`onOpenChange` to drive it from a parent.
87
- */ export function ToolBlock({ name, label, state, input, defaultOpen, open: openProp, onOpenChange, className }) {
90
+ */
91
+ export function ToolBlock({ name, label, state, input, defaultOpen, open: openProp, onOpenChange, className, }) {
88
92
  const [open, setOpen] = useControllableState({
89
93
  value: openProp,
90
94
  defaultValue: defaultOpen ?? OPENS_BY_DEFAULT[state.status],
91
95
  onChange: onOpenChange,
92
- name: "ToolBlock"
96
+ name: "ToolBlock",
93
97
  });
94
98
  const detailText = stateDetailText(state);
95
99
  const hasDetail = input !== undefined || detailText !== undefined;
96
100
  const displayLabel = label !== undefined && label.length > 0 ? label : humaniseToolName(name);
97
- return /*#__PURE__*/ _jsxs("div", {
98
- "data-slot": "tool-block",
99
- "data-status": state.status,
100
- className: cn("text-xs", className),
101
- children: [
102
- /*#__PURE__*/ _jsxs("button", {
103
- type: "button",
104
- onClick: ()=>setOpen((value)=>!value),
105
- "aria-expanded": open,
106
- disabled: !hasDetail,
101
+ return (_jsxs("div", { "data-slot": "tool-block", "data-status": state.status, className: cn("text-xs", className), children: [_jsxs("button", { type: "button", onClick: () => setOpen((value) => !value), "aria-expanded": open, disabled: !hasDetail,
107
102
  // `relative` plus the `::after` pseudo-element below extends the
108
103
  // button's effective (vertical) hit area to 40px without inflating
109
104
  // its own padding/density — see design-engineering.md's hit-area
110
105
  // pattern. The row is already full-width, so only height needs
111
106
  // extending.
112
- className: "relative flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-muted-foreground transition-colors after:absolute after:inset-x-0 after:top-1/2 after:h-10 after:-translate-y-1/2 hover:bg-muted hover:text-foreground disabled:hover:bg-transparent",
113
- children: [
114
- /*#__PURE__*/ _jsx("span", {
115
- "data-slot": "tool-block-status",
107
+ className: "relative flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-muted-foreground transition-colors after:absolute after:inset-x-0 after:top-1/2 after:h-10 after:-translate-y-1/2 hover:bg-muted hover:text-foreground disabled:hover:bg-transparent", children: [_jsx("span", { "data-slot": "tool-block-status",
116
108
  // A pending approval is human-blocking: it needs to reach assistive
117
109
  // tech the instant it appears, not only when a reader happens to
118
110
  // tab onto this button. Scoped to this span alone (not the whole
119
111
  // block) so every other state change in a busy transcript stays
120
112
  // silent — announcing every "Working" -> "Done" tick here would be
121
113
  // spam.
122
- role: state.status === "approval-requested" ? "status" : undefined,
123
- "aria-live": state.status === "approval-requested" ? "polite" : undefined,
124
- children: /*#__PURE__*/ _jsx(StatusDot, {
125
- label: STATE_TEXT[state.status],
126
- live: state.status === "running",
127
- tone: STATE_TONE[state.status]
128
- })
129
- }),
130
- /*#__PURE__*/ _jsx("span", {
131
- className: "min-w-0 flex-1 truncate",
132
- children: displayLabel
133
- }),
134
- hasDetail ? /*#__PURE__*/ _jsx(ChevronRight, {
135
- className: cn("size-3.5 shrink-0 transition-transform duration-200 ease-out", open && "rotate-90"),
136
- "aria-hidden": true
137
- }) : null
138
- ]
139
- }),
140
- open && hasDetail ? /*#__PURE__*/ _jsxs("div", {
141
- className: "mt-1 ml-4 flex flex-col gap-2 border-l border-border pl-3 [animation:corbits-rail-block-in_200ms_var(--ease-out)_both]",
142
- children: [
143
- input === undefined ? null : /*#__PURE__*/ _jsx(Detail, {
144
- title: "Input",
145
- children: JSON.stringify(input, null, 2)
146
- }),
147
- detailText === undefined ? null : /*#__PURE__*/ _jsx(Detail, {
148
- title: stateDetailTitle(state),
149
- children: detailText
150
- })
151
- ]
152
- }) : null
153
- ]
154
- });
114
+ role: state.status === "approval-requested" ? "status" : undefined, "aria-live": state.status === "approval-requested" ? "polite" : undefined, children: _jsx(StatusDot, { label: STATE_TEXT[state.status], live: state.status === "running", tone: STATE_TONE[state.status] }) }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: displayLabel }), hasDetail ? (_jsx(ChevronRight, { className: cn("size-3.5 shrink-0 transition-transform duration-200 ease-out", open && "rotate-90"), "aria-hidden": true })) : null] }), open && hasDetail ? (_jsxs("div", { className: "mt-1 ml-4 flex flex-col gap-2 border-l border-border pl-3 [animation:corbits-rail-block-in_200ms_var(--ease-out)_both]", children: [input === undefined ? null : (_jsx(Detail, { title: "Input", children: JSON.stringify(input, null, 2) })), detailText === undefined ? null : (_jsx(Detail, { title: stateDetailTitle(state), children: detailText }))] })) : null] }));
155
115
  }
156
116
  function Detail({ title, children }) {
157
- return /*#__PURE__*/ _jsxs("div", {
158
- className: "flex flex-col gap-1",
159
- children: [
160
- /*#__PURE__*/ _jsx("p", {
161
- className: "text-[10px] font-semibold tracking-[0.1em] text-muted-foreground uppercase",
162
- children: title
163
- }),
164
- /*#__PURE__*/ _jsx("pre", {
165
- className: "max-h-48 overflow-auto rounded-md bg-muted p-2 font-mono text-[11px] whitespace-pre-wrap",
166
- children: children
167
- })
168
- ]
169
- });
117
+ return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("p", { className: "text-[10px] font-semibold tracking-[0.1em] text-muted-foreground uppercase", children: title }), _jsx("pre", { className: "max-h-48 overflow-auto rounded-md bg-muted p-2 font-mono text-[11px] whitespace-pre-wrap", children: children })] }));
170
118
  }
@@ -19,4 +19,4 @@ export type ToolNarrativeProps = {
19
19
  readonly onOpenChange?: (open: boolean) => void;
20
20
  readonly className?: string;
21
21
  };
22
- export declare function ToolNarrative({ part, open: openProp, onOpenChange, className }: ToolNarrativeProps): import("react").JSX.Element;
22
+ export declare function ToolNarrative({ part, open: openProp, onOpenChange, className, }: ToolNarrativeProps): import("react").JSX.Element;
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { ChevronRight } from "lucide-react";
3
4
  import { useControllableState } from "../hooks/use-controllable-state.js";
@@ -7,71 +8,23 @@ import { StatusDot } from "./status-dot.js";
7
8
  const STATE_TEXT = {
8
9
  running: "Working",
9
10
  done: "Done",
10
- error: "Failed"
11
+ error: "Failed",
11
12
  };
12
- export function ToolNarrative({ part, open: openProp, onOpenChange, className }) {
13
+ export function ToolNarrative({ part, open: openProp, onOpenChange, className, }) {
13
14
  const [open, setOpen] = useControllableState({
14
15
  value: openProp,
15
16
  defaultValue: part.state === "error",
16
17
  onChange: onOpenChange,
17
- name: "ToolNarrative"
18
+ name: "ToolNarrative",
18
19
  });
19
20
  const label = toolLabel(part);
20
21
  const hasDetail = part.output !== undefined || part.input !== undefined;
21
- return /*#__PURE__*/ _jsxs("div", {
22
- "data-slot": "tool-narrative",
23
- className: cn("text-xs", className),
24
- children: [
25
- /*#__PURE__*/ _jsxs("button", {
26
- type: "button",
27
- onClick: ()=>setOpen((value)=>!value),
28
- "aria-expanded": open,
29
- disabled: !hasDetail,
30
- className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:hover:bg-transparent",
31
- children: [
32
- /*#__PURE__*/ _jsx(StatusDot, {
33
- label: STATE_TEXT[part.state],
34
- live: part.state === "running",
35
- tone: part.state === "error" ? "danger" : part.state === "running" ? "emphasis" : "neutral"
36
- }),
37
- /*#__PURE__*/ _jsx("span", {
38
- className: "min-w-0 flex-1 truncate",
39
- children: label
40
- }),
41
- hasDetail ? /*#__PURE__*/ _jsx(ChevronRight, {
42
- className: cn("size-3.5 shrink-0 transition-transform", open && "rotate-90"),
43
- "aria-hidden": true
44
- }) : null
45
- ]
46
- }),
47
- open && hasDetail ? /*#__PURE__*/ _jsxs("div", {
48
- className: "mt-1 ml-4 flex flex-col gap-2 border-l border-border pl-3",
49
- children: [
50
- part.input === undefined ? null : /*#__PURE__*/ _jsx(Detail, {
51
- title: "Input",
52
- children: JSON.stringify(part.input, null, 2)
53
- }),
54
- part.output === undefined ? null : /*#__PURE__*/ _jsx(Detail, {
55
- title: part.state === "error" ? "Error" : "Output",
56
- children: part.output
57
- })
58
- ]
59
- }) : null
60
- ]
61
- });
22
+ return (_jsxs("div", { "data-slot": "tool-narrative", className: cn("text-xs", className), children: [_jsxs("button", { type: "button", onClick: () => setOpen((value) => !value), "aria-expanded": open, disabled: !hasDetail, className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:hover:bg-transparent", children: [_jsx(StatusDot, { label: STATE_TEXT[part.state], live: part.state === "running", tone: part.state === "error"
23
+ ? "danger"
24
+ : part.state === "running"
25
+ ? "emphasis"
26
+ : "neutral" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: label }), hasDetail ? (_jsx(ChevronRight, { className: cn("size-3.5 shrink-0 transition-transform", open && "rotate-90"), "aria-hidden": true })) : null] }), open && hasDetail ? (_jsxs("div", { className: "mt-1 ml-4 flex flex-col gap-2 border-l border-border pl-3", children: [part.input === undefined ? null : (_jsx(Detail, { title: "Input", children: JSON.stringify(part.input, null, 2) })), part.output === undefined ? null : (_jsx(Detail, { title: part.state === "error" ? "Error" : "Output", children: part.output }))] })) : null] }));
62
27
  }
63
28
  function Detail({ title, children }) {
64
- return /*#__PURE__*/ _jsxs("div", {
65
- className: "flex flex-col gap-1",
66
- children: [
67
- /*#__PURE__*/ _jsx("p", {
68
- className: "text-[10px] font-semibold tracking-[0.1em] text-muted-foreground uppercase",
69
- children: title
70
- }),
71
- /*#__PURE__*/ _jsx("pre", {
72
- className: "max-h-48 overflow-auto rounded-md bg-muted p-2 font-mono text-[11px] whitespace-pre-wrap",
73
- children: children
74
- })
75
- ]
76
- });
29
+ return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("p", { className: "text-[10px] font-semibold tracking-[0.1em] text-muted-foreground uppercase", children: title }), _jsx("pre", { className: "max-h-48 overflow-auto rounded-md bg-muted p-2 font-mono text-[11px] whitespace-pre-wrap", children: children })] }));
77
30
  }
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useEffect, useId, useMemo, useRef, useState } from "react";
3
4
  import { useCommandPaletteNavigation } from "../hooks/use-command-palette-navigation.js";
@@ -7,32 +8,37 @@ import { EmptyState } from "./empty-state.js";
7
8
  import { Input } from "./input.js";
8
9
  const STATUS_LABEL = {
9
10
  pending: "Pending",
10
- disabled: "Disabled"
11
+ disabled: "Disabled",
11
12
  };
12
13
  function matches(item, query) {
13
- if (query.length === 0) return true;
14
+ if (query.length === 0)
15
+ return true;
14
16
  const haystack = `${item.name} ${item.package} ${item.description ?? ""}`.toLowerCase();
15
17
  return haystack.includes(query.toLowerCase());
16
18
  }
17
19
  function groupByPackage(items) {
18
20
  const groups = new Map();
19
- for (const item of items){
21
+ for (const item of items) {
20
22
  const group = groups.get(item.package);
21
- if (group === undefined) groups.set(item.package, [
22
- item
23
- ]);
24
- else group.push(item);
23
+ if (group === undefined)
24
+ groups.set(item.package, [item]);
25
+ else
26
+ group.push(item);
25
27
  }
26
- return [
27
- ...groups.entries()
28
- ].map(([id, groupItems])=>({
29
- id,
30
- items: groupItems
31
- }));
28
+ return [...groups.entries()].map(([id, groupItems]) => ({
29
+ id,
30
+ items: groupItems,
31
+ }));
32
32
  }
33
- /** Visible group title: a display name if the catalog already sent one, else a humanized slug. */ function groupHeading(id) {
34
- if (/[A-Z]/.test(id) || /\s/.test(id)) return id;
35
- return id.split(/[-_./]+/).filter(Boolean).map((part)=>part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
33
+ /** Visible group title: a display name if the catalog already sent one, else a humanized slug. */
34
+ function groupHeading(id) {
35
+ if (/[A-Z]/.test(id) || /\s/.test(id))
36
+ return id;
37
+ return id
38
+ .split(/[-_./]+/)
39
+ .filter(Boolean)
40
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
41
+ .join(" ");
36
42
  }
37
43
  /**
38
44
  * Browse, search and select tools from a caller-supplied catalog, grouped by
@@ -43,52 +49,42 @@ function groupByPackage(items) {
43
49
  * Disabled items stay in the list rather than disappearing — a tool a
44
50
  * caller cannot pick right now is still information ("already attached
45
51
  * elsewhere", "needs a credential") that vanishing would hide.
46
- */ export function ToolPicker({ items, value, onChange, multiple = false, loading = false, empty, searchPlaceholder = "Search tools…", searchLabel = "Search tools", className }) {
52
+ */
53
+ export function ToolPicker({ items, value, onChange, multiple = false, loading = false, empty, searchPlaceholder = "Search tools…", searchLabel = "Search tools", className, }) {
47
54
  const baseId = useId();
48
55
  const [query, setQuery] = useState("");
49
56
  const listRef = useRef(null);
50
- const filtered = useMemo(()=>items.filter((item)=>matches(item, query)), [
51
- items,
52
- query
53
- ]);
54
- const groups = useMemo(()=>groupByPackage(filtered), [
55
- filtered
56
- ]);
57
- const selectedIds = useMemo(()=>new Set(value), [
58
- value
59
- ]);
60
- const toggle = (item)=>{
61
- if (item.status === "disabled") return;
57
+ const filtered = useMemo(() => items.filter((item) => matches(item, query)), [items, query]);
58
+ const groups = useMemo(() => groupByPackage(filtered), [filtered]);
59
+ const selectedIds = useMemo(() => new Set(value), [value]);
60
+ const toggle = (item) => {
61
+ if (item.status === "disabled")
62
+ return;
62
63
  const isSelected = selectedIds.has(item.id);
63
64
  if (!multiple) {
64
- onChange(isSelected ? [] : [
65
- item.id
66
- ]);
65
+ onChange(isSelected ? [] : [item.id]);
67
66
  return;
68
67
  }
69
- onChange(isSelected ? value.filter((id)=>id !== item.id) : [
70
- ...value,
71
- item.id
72
- ]);
68
+ onChange(isSelected ? value.filter((id) => id !== item.id) : [...value, item.id]);
73
69
  };
74
70
  const navigation = useCommandPaletteNavigation({
75
71
  items: filtered,
76
- onSelect: (id)=>{
77
- const item = filtered.find((candidate)=>candidate.id === id);
78
- if (item !== undefined) toggle(item);
72
+ onSelect: (id) => {
73
+ const item = filtered.find((candidate) => candidate.id === id);
74
+ if (item !== undefined)
75
+ toggle(item);
76
+ },
77
+ onClose: () => {
78
+ /* Escape is handled below so an empty query does not swallow the key. */
79
79
  },
80
- onClose: ()=>{
81
- /* Escape is handled below so an empty query does not swallow the key. */ }
82
80
  });
83
- useEffect(()=>{
84
- listRef.current?.querySelector('[data-active="true"]')?.scrollIntoView({
85
- block: "nearest"
86
- });
87
- }, [
88
- navigation.activeId
89
- ]);
90
- const optionId = (id)=>`${baseId}-option-${id}`;
91
- const onKeyDown = (event)=>{
81
+ useEffect(() => {
82
+ listRef.current
83
+ ?.querySelector('[data-active="true"]')
84
+ ?.scrollIntoView({ block: "nearest" });
85
+ }, [navigation.activeId]);
86
+ const optionId = (id) => `${baseId}-option-${id}`;
87
+ const onKeyDown = (event) => {
92
88
  if (event.key === "Escape") {
93
89
  if (query.length > 0) {
94
90
  event.preventDefault();
@@ -100,101 +96,17 @@ function groupByPackage(items) {
100
96
  };
101
97
  const emptyCatalog = items.length === 0;
102
98
  const noMatches = !emptyCatalog && filtered.length === 0;
103
- return /*#__PURE__*/ _jsxs("div", {
104
- "data-slot": "tool-picker",
105
- className: cn("flex min-h-0 flex-col gap-2", className),
106
- children: [
107
- /*#__PURE__*/ _jsx(Input, {
108
- value: query,
109
- onChange: (event)=>setQuery(event.target.value),
110
- onKeyDown: onKeyDown,
111
- placeholder: searchPlaceholder,
112
- "aria-label": searchLabel,
113
- role: "combobox",
114
- "aria-expanded": true,
115
- "aria-autocomplete": "list",
116
- "aria-controls": `${baseId}-list`,
117
- "aria-activedescendant": navigation.activeId === undefined ? undefined : optionId(navigation.activeId),
118
- "aria-busy": loading,
119
- autoComplete: "off",
120
- spellCheck: false
121
- }),
122
- /*#__PURE__*/ _jsx("div", {
123
- ref: listRef,
124
- id: `${baseId}-list`,
125
- role: "listbox",
126
- "aria-label": "Tools",
127
- "aria-multiselectable": multiple,
128
- className: "flex min-h-0 flex-1 flex-col gap-3 overflow-auto",
129
- children: loading ? /*#__PURE__*/ _jsx("p", {
130
- role: "presentation",
131
- className: "px-2 py-8 text-center text-sm text-muted-foreground",
132
- children: "Loading…"
133
- }) : emptyCatalog ? /*#__PURE__*/ _jsx("div", {
134
- role: "presentation",
135
- children: empty ?? /*#__PURE__*/ _jsx(EmptyState, {
136
- title: "No tools",
137
- description: "Nothing in the catalog yet."
138
- })
139
- }) : noMatches ? /*#__PURE__*/ _jsx("p", {
140
- role: "presentation",
141
- className: "px-2 py-8 text-center text-sm text-muted-foreground",
142
- children: "No tools match — try a different search."
143
- }) : groups.map((group)=>/*#__PURE__*/ _jsxs("div", {
144
- role: "presentation",
145
- className: "flex flex-col gap-1",
146
- children: [
147
- /*#__PURE__*/ _jsx("p", {
148
- className: "px-3.5 py-1.5 text-[11px] font-semibold uppercase tracking-[0.1em] text-muted-foreground",
149
- children: groupHeading(group.id)
150
- }),
151
- /*#__PURE__*/ _jsx("div", {
152
- className: "flex flex-col gap-1",
153
- children: group.items.map((item)=>/*#__PURE__*/ _jsx(ToolPickerOption, {
154
- id: optionId(item.id),
155
- item: item,
156
- selected: selectedIds.has(item.id),
157
- active: navigation.activeId === item.id,
158
- onSelect: ()=>toggle(item),
159
- onPointerMove: ()=>navigation.setActiveId(item.id)
160
- }, item.id))
161
- })
162
- ]
163
- }, group.id))
164
- })
165
- ]
166
- });
99
+ return (_jsxs("div", { "data-slot": "tool-picker", className: cn("flex min-h-0 flex-col gap-2", className), children: [_jsx(Input, { value: query, onChange: (event) => setQuery(event.target.value), onKeyDown: onKeyDown, placeholder: searchPlaceholder, "aria-label": searchLabel, role: "combobox", "aria-expanded": true, "aria-autocomplete": "list", "aria-controls": `${baseId}-list`, "aria-activedescendant": navigation.activeId === undefined
100
+ ? undefined
101
+ : optionId(navigation.activeId), "aria-busy": loading, autoComplete: "off", spellCheck: false }), _jsx("div", { ref: listRef, id: `${baseId}-list`, role: "listbox", "aria-label": "Tools", "aria-multiselectable": multiple, className: "flex min-h-0 flex-1 flex-col gap-3 overflow-auto", children: loading ? (_jsx("p", { role: "presentation", className: "px-2 py-8 text-center text-sm text-muted-foreground", children: "Loading\u2026" })) : emptyCatalog ? (_jsx("div", { role: "presentation", children: empty ?? (_jsx(EmptyState, { title: "No tools", description: "Nothing in the catalog yet." })) })) : noMatches ? (_jsx("p", { role: "presentation", className: "px-2 py-8 text-center text-sm text-muted-foreground", children: "No tools match \u2014 try a different search." })) : (groups.map((group) => (_jsxs("div", { role: "presentation", className: "flex flex-col gap-1", children: [_jsx("p", { className: "px-3.5 py-1.5 text-[11px] font-semibold uppercase tracking-[0.1em] text-muted-foreground", children: groupHeading(group.id) }), _jsx("div", { className: "flex flex-col gap-1", children: group.items.map((item) => (_jsx(ToolPickerOption, { id: optionId(item.id), item: item, selected: selectedIds.has(item.id), active: navigation.activeId === item.id, onSelect: () => toggle(item), onPointerMove: () => navigation.setActiveId(item.id) }, item.id))) })] }, group.id)))) })] }));
167
102
  }
168
- function ToolPickerOption({ id, item, selected, active, onSelect, onPointerMove }) {
103
+ function ToolPickerOption({ id, item, selected, active, onSelect, onPointerMove, }) {
169
104
  const disabled = item.status === "disabled";
170
- return /*#__PURE__*/ _jsxs("div", {
171
- id: id,
172
- role: "option",
173
- "aria-selected": selected,
174
- "aria-disabled": disabled,
175
- "data-active": active,
176
- onPointerMove: disabled ? undefined : onPointerMove,
177
- onClick: disabled ? undefined : onSelect,
178
- className: cn("flex w-full flex-col gap-1 rounded-lg border px-3.5 py-3 text-left transition-colors ease-out", disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:bg-muted active:brightness-95", selected ? "border-primary-emphasis bg-primary/10" : "border-border bg-card", // The keyboard cursor must stay visible on disabled rows — aria-activedescendant can land there.
179
- active && "bg-muted"),
180
- children: [
181
- /*#__PURE__*/ _jsxs("span", {
182
- className: "flex items-center gap-2",
183
- children: [
184
- /*#__PURE__*/ _jsx("span", {
185
- className: "text-sm font-semibold",
186
- children: item.name
187
- }),
188
- item.status === undefined ? null : /*#__PURE__*/ _jsx(Badge, {
189
- tone: "neutral",
190
- children: STATUS_LABEL[item.status]
191
- })
192
- ]
193
- }),
194
- item.description === undefined ? null : /*#__PURE__*/ _jsx("span", {
195
- className: "text-xs leading-snug text-muted-foreground",
196
- children: item.description
197
- })
198
- ]
199
- });
105
+ return (_jsxs("div", { id: id, role: "option", "aria-selected": selected, "aria-disabled": disabled, "data-active": active, onPointerMove: disabled ? undefined : onPointerMove, onClick: disabled ? undefined : onSelect, className: cn("flex w-full flex-col gap-1 rounded-lg border px-3.5 py-3 text-left transition-colors ease-out", disabled
106
+ ? "cursor-not-allowed opacity-50"
107
+ : "cursor-pointer hover:bg-muted active:brightness-95", selected
108
+ ? "border-primary-emphasis bg-primary/10"
109
+ : "border-border bg-card",
110
+ // The keyboard cursor must stay visible on disabled rows — aria-activedescendant can land there.
111
+ active && "bg-muted"), children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-semibold", children: item.name }), item.status === undefined ? null : (_jsx(Badge, { tone: "neutral", children: STATUS_LABEL[item.status] }))] }), item.description === undefined ? null : (_jsx("span", { className: "text-xs leading-snug text-muted-foreground", children: item.description }))] }));
200
112
  }
@@ -18,4 +18,4 @@ export type InfoTooltipProps = {
18
18
  * it is in the tab order, Radix associates the note by id, and tapping it
19
19
  * opens the tooltip on touch devices.
20
20
  */
21
- export declare function InfoTooltip({ label, triggerLabel, defaultOpen, className }: InfoTooltipProps): import("react").JSX.Element;
21
+ export declare function InfoTooltip({ label, triggerLabel, defaultOpen, className, }: InfoTooltipProps): import("react").JSX.Element;
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
3
4
  import { InfoIcon } from "lucide-react";
@@ -12,37 +13,7 @@ import { cn } from "../lib/utils.js";
12
13
  * reader, and never fires on touch. A `<button>` trigger fixes all three:
13
14
  * it is in the tab order, Radix associates the note by id, and tapping it
14
15
  * opens the tooltip on touch devices.
15
- */ export function InfoTooltip({ label, triggerLabel, defaultOpen, className }) {
16
- return /*#__PURE__*/ _jsx(TooltipPrimitive.Provider, {
17
- delayDuration: 200,
18
- children: /*#__PURE__*/ _jsxs(TooltipPrimitive.Root, {
19
- defaultOpen: defaultOpen,
20
- children: [
21
- /*#__PURE__*/ _jsx(TooltipPrimitive.Trigger, {
22
- asChild: true,
23
- children: /*#__PURE__*/ _jsx("button", {
24
- type: "button",
25
- "aria-label": triggerLabel ?? label,
26
- className: cn("inline-flex size-4 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors", "hover:text-foreground focus-visible:text-foreground focus-visible:outline-none", "focus-visible:shadow-[0_0_0_3px_color-mix(in_srgb,var(--primary)_22%,transparent)]", className),
27
- children: /*#__PURE__*/ _jsx(InfoIcon, {
28
- className: "size-full"
29
- })
30
- })
31
- }),
32
- /*#__PURE__*/ _jsx(TooltipPrimitive.Portal, {
33
- children: /*#__PURE__*/ _jsxs(TooltipPrimitive.Content, {
34
- "data-slot": "tooltip-content",
35
- sideOffset: 6,
36
- className: cn("z-50 max-w-64 rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-lg", "data-[state=delayed-open]:animate-[corbits-fade-in_150ms_ease-out] data-[state=closed]:animate-[corbits-fade-out_120ms_ease-in]"),
37
- children: [
38
- label,
39
- /*#__PURE__*/ _jsx(TooltipPrimitive.Arrow, {
40
- className: "fill-popover"
41
- })
42
- ]
43
- })
44
- })
45
- ]
46
- })
47
- });
16
+ */
17
+ export function InfoTooltip({ label, triggerLabel, defaultOpen, className, }) {
18
+ return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs(TooltipPrimitive.Root, { defaultOpen: defaultOpen ?? false, children: [_jsx(TooltipPrimitive.Trigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": triggerLabel ?? label, className: cn("inline-flex size-4 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors", "hover:text-foreground focus-visible:text-foreground focus-visible:outline-none", "focus-visible:shadow-[0_0_0_3px_color-mix(in_srgb,var(--primary)_22%,transparent)]", className), children: _jsx(InfoIcon, { className: "size-full" }) }) }), _jsx(TooltipPrimitive.Portal, { children: _jsxs(TooltipPrimitive.Content, { "data-slot": "tooltip-content", sideOffset: 6, className: cn("z-50 max-w-64 rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-lg", "data-[state=delayed-open]:animate-[corbits-fade-in_150ms_ease-out] data-[state=closed]:animate-[corbits-fade-out_120ms_ease-in]"), children: [label, _jsx(TooltipPrimitive.Arrow, { className: "fill-popover" })] }) })] }) }));
48
19
  }