@adecore/agents-react 0.28.0 → 0.29.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 (30) hide show
  1. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +19 -9
  2. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +4 -2
  3. package/dist/chat/ui/intelligent-ui/UiReply.js +3 -3
  4. package/dist/chat/ui/intelligent-ui/renderers/chart.js +2 -2
  5. package/dist/chat/ui/intelligent-ui/renderers/choices.js +13 -8
  6. package/dist/chat/ui/intelligent-ui/renderers/content.js +15 -14
  7. package/dist/chat/ui/intelligent-ui/renderers/data.js +19 -6
  8. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +10 -9
  9. package/dist/chat/ui/intelligent-ui/renderers/links.js +5 -3
  10. package/dist/chat/ui/intelligent-ui/renderers/map.js +7 -7
  11. package/dist/chat/ui/intelligent-ui/renderers/structure.js +5 -5
  12. package/dist/chat/ui/intelligent-ui/renderers/text.js +25 -14
  13. package/dist/chat/ui/intelligent-ui/tones.d.ts +2 -0
  14. package/dist/chat/ui/intelligent-ui/tones.js +16 -0
  15. package/dist/theme.css +38 -0
  16. package/package.json +8 -8
  17. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +26 -13
  18. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +7 -4
  19. package/src/chat/ui/intelligent-ui/UiReply.tsx +3 -3
  20. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +2 -2
  21. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +33 -19
  22. package/src/chat/ui/intelligent-ui/renderers/content.tsx +36 -23
  23. package/src/chat/ui/intelligent-ui/renderers/data.tsx +61 -19
  24. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +21 -14
  25. package/src/chat/ui/intelligent-ui/renderers/links.tsx +7 -4
  26. package/src/chat/ui/intelligent-ui/renderers/map.tsx +15 -12
  27. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +6 -6
  28. package/src/chat/ui/intelligent-ui/renderers/text.tsx +63 -42
  29. package/src/chat/ui/intelligent-ui/tones.ts +18 -0
  30. package/src/theme.css +38 -0
@@ -3,7 +3,8 @@ import { useId, useLayoutEffect, useRef, useState } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Info, Lock, Radio, TriangleAlert } from 'lucide-react';
6
- import { Icon, Skeleton, Spinner, Tooltip, useTickingText } from '@adecore/ui';
6
+ import { Icon, KEY_SHORTCUTS, matchesShortcut, Skeleton, Spinner, Tooltip, useTickingText } from '@adecore/ui';
7
+ import { useChatHost } from '../../../host.js';
7
8
  import { formatAgo, formatMoment } from '@adecore/ui/format';
8
9
  import { uiBlockHead, uiReasonText } from './node-text.js';
9
10
  import { UiHeadContext } from './render-context.js';
@@ -24,7 +25,7 @@ function LivePart({ live }) {
24
25
  return fresh ? t('blocks.live.read', { time }) : time;
25
26
  });
26
27
  const source = live.source ?? live.sources[0] ?? '';
27
- const icon = slow ? (_jsx(Spinner, { size: 12 })) : live.state === 'failed' ? (_jsx(Icon, { icon: TriangleAlert, size: 12, className: "text-status-needs-you" })) : live.state === 'refused' ? (_jsx(Icon, { icon: Lock, size: 12 })) : (_jsx(Icon, { icon: Radio, size: 12, className: "text-status-idle" }));
28
+ const icon = slow ? (_jsx(Spinner, { size: 12 })) : live.state === 'failed' ? (_jsx(Icon, { icon: TriangleAlert, size: 12, className: "text-status-needs-you" })) : live.state === 'refused' ? (_jsx(Icon, { icon: Lock, size: 12 })) : (_jsx(Icon, { icon: Radio, size: 12, className: "text-positive" }));
28
29
  const words = live.state === 'failed'
29
30
  ? t('blocks.live.failed', { source })
30
31
  : live.state === 'refused'
@@ -35,10 +36,7 @@ function LivePart({ live }) {
35
36
  const body = (_jsxs("span", { tabIndex: hint === null ? undefined : 0, className: "flex items-center gap-1.5", children: [icon, _jsx("span", { children: words }), readAt !== null && live.state !== 'refused' && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("time", { dateTime: new Date(readAt).toISOString(), children: _jsx("span", { ref: ago, className: "tabular-nums" }) })] })), readAt === null && fresh && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("span", { children: t('blocks.live.notRead') })] }))] }));
36
37
  return hint === null ? body : _jsx(Tooltip, { label: hint, children: body });
37
38
  }
38
- /*
39
- * The line under a block, only when it has something to say: live, answered, noted or drawn as
40
- * text, in that order. A hairline of the soft border runs the full width of the card over it.
41
- */
39
+ /* The line under a block, only when it has something to say: live, answered, noted or drawn as text, in that order. */
42
40
  function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAsText = null }) {
43
41
  const { t } = useTranslation('agent-chat');
44
42
  const [fixesOpen, setFixesOpen] = useState(false);
@@ -46,7 +44,7 @@ function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAs
46
44
  if (live === null && answered === null && fixes.length === 0 && shownAsText === null) {
47
45
  return null;
48
46
  }
49
- return (_jsxs("div", { className: "-mx-2.25 mt-2.25 -mb-2.25 flex flex-col gap-1.5 border-t border-border-soft px-4.25 py-2 text-xs text-text-faint", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [live !== null && _jsx(LivePart, { live: live }), answered !== null && (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { children: t('blocks.answered', { label: answered.label }) }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("time", { dateTime: new Date(answered.at).toISOString(), children: formatMoment(answered.at) })] })), fixes.length > 0 && (_jsxs("button", { type: "button", "aria-expanded": fixesOpen, "aria-controls": listId, className: "flex items-center gap-1.5 rounded-sm hover:text-text-muted", onClick: () => setFixesOpen(!fixesOpen), children: [_jsx(Icon, { icon: Info, size: 12 }), t('blocks.notes', { count: fixes.length })] })), shownAsText !== null && (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { children: t('blocks.shownAsText') }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("span", { children: shownAsText })] }))] }), fixes.length > 0 && (_jsx("ul", { id: listId, hidden: !fixesOpen, className: "flex flex-col gap-0.5 text-text-muted", children: fixes.map((fix, index) => (_jsxs("li", { className: "flex gap-2", children: [_jsx("span", { className: "shrink-0 font-mono", children: fix.code }), _jsx("span", { className: "min-w-0", children: fix.message })] }, index))) }))] }));
47
+ return (_jsxs("div", { className: "flex flex-col gap-1.5 text-2xs text-text-faint", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [live !== null && _jsx(LivePart, { live: live }), answered !== null && (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { children: t('blocks.answered', { label: answered.label }) }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("time", { dateTime: new Date(answered.at).toISOString(), children: formatMoment(answered.at) })] })), fixes.length > 0 && (_jsxs("button", { type: "button", "aria-expanded": fixesOpen, "aria-controls": listId, className: "flex items-center gap-1.5 rounded-sm hover:text-text-muted", onClick: () => setFixesOpen(!fixesOpen), children: [_jsx(Icon, { icon: Info, size: 12 }), t('blocks.notes', { count: fixes.length })] })), shownAsText !== null && (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { children: t('blocks.shownAsText') }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("span", { children: shownAsText })] }))] }), fixes.length > 0 && (_jsx("ul", { id: listId, hidden: !fixesOpen, className: "flex flex-col gap-0.5 text-text-muted", children: fixes.map((fix, index) => (_jsxs("li", { className: "flex gap-2", children: [_jsx("span", { className: "shrink-0 font-mono", children: fix.code }), _jsx("span", { className: "min-w-0", children: fix.message })] }, index))) }))] }));
50
48
  }
51
49
  const RELEASE_MS = 200;
52
50
  /*
@@ -89,15 +87,27 @@ function useRisingFloor(active) {
89
87
  return [ref, floor];
90
88
  }
91
89
  /*
92
- * The card of one block, with its footer. Named by its Summary and busy while it streams, but never
90
+ * The band of one block, with its footer. Named by its Summary and busy while it streams, but never
93
91
  * live: reading out every compile would be noise. A node fades in as it arrives, without a stagger,
94
92
  * and for a person who asked for less motion it only fades; a node inside another only fades
95
93
  * (`chat-ui-nodes`). Until the first node a skeleton holds the place of the head.
96
94
  */
97
95
  export function UiBlockFrame({ nodes, phase, children, className, ...footer }) {
98
96
  const { t } = useTranslation('agent-chat');
97
+ const host = useChatHost();
99
98
  const streaming = phase === 'streaming';
100
99
  const head = uiBlockHead(nodes);
101
100
  const [ref, floor] = useRisingFloor(streaming);
102
- return (_jsx(UiHeadContext, { value: head?.id ?? null, children: _jsxs("div", { ref: ref, role: "group", "aria-label": head === null || head.label === '' ? t('blocks.label') : head.label, "aria-busy": streaming || undefined, className: clsx('flex min-h-11 flex-col rounded-2xl border border-border bg-surface-raised p-2.25', className), style: streaming && floor > 0 ? { minHeight: floor } : undefined, children: [_jsx("div", { className: "flex flex-col gap-3 *:transition-[opacity,translate] *:duration-160 *:ease-out *:starting:opacity-0 motion-safe:*:starting:translate-y-0.5", children: nodes.length === 0 && streaming ? _jsx(Skeleton, { className: "mx-2 my-1 w-2/5" }) : children }), _jsx(UiBlockFooter, { ...footer })] }) }));
101
+ // Only while the focus is inside the block, so the composer keeps the same keys for sending.
102
+ const pickPrimary = (event) => {
103
+ if (!matchesShortcut(KEY_SHORTCUTS.modEnter, event, host.isApplePlatform())) {
104
+ return;
105
+ }
106
+ const primary = event.currentTarget.querySelector('[data-ui-primary-choice]');
107
+ if (primary !== null) {
108
+ event.preventDefault();
109
+ primary.click();
110
+ }
111
+ };
112
+ return (_jsx(UiHeadContext, { value: head?.id ?? null, children: _jsx("div", { ref: ref, role: "group", "aria-label": head === null || head.label === '' ? t('blocks.label') : head.label, "aria-busy": streaming || undefined, className: clsx('chat-ui-band flex min-h-11 flex-col', className), style: streaming && floor > 0 ? { minHeight: floor } : undefined, onKeyDown: pickPrimary, children: _jsxs("div", { className: "flex flex-col gap-3 py-5", children: [_jsx("div", { className: "flex flex-col gap-3.5 *:transition-[opacity,translate] *:duration-160 *:ease-out *:starting:opacity-0 motion-safe:*:starting:translate-y-0.5", children: nodes.length === 0 && streaming ? _jsx(Skeleton, { className: "my-1 w-2/5" }) : children }), _jsx(UiBlockFooter, { ...footer })] }) }) }));
103
113
  }
@@ -1,11 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
+ import { Info } from 'lucide-react';
4
+ import { Icon } from '@adecore/ui';
3
5
  import { Markdown } from '../Markdown.js';
4
6
  /*
5
- * One part of a block as the markdown it stands for, in a quiet surface with a line that says why.
7
+ * One part of a block as the markdown it stands for, on a card in a quiet tile under a line that says why.
6
8
  * A person sees it as a repair, never as an error, so nothing here is red.
7
9
  */
8
10
  export function UiFallbackPart({ fallback, problem }) {
9
11
  const { t } = useTranslation('agent-chat');
10
- return (_jsxs("div", { className: "flex flex-col gap-1 rounded-md bg-surface-hover p-2", children: [_jsx("span", { className: "text-xs text-text-faint", children: problem.kind === 'unknown' ? t('blocks.unknownComponent', { component: problem.component }) : t('blocks.renderFailed') }), fallback !== '' && (_jsx("div", { className: "min-w-0 wrap-anywhere", children: _jsx(Markdown, { text: fallback, fileLinks: false }) }))] }));
12
+ return (_jsxs("div", { className: "chat-ui-flat flex flex-col gap-1.5 rounded-xl bg-ui-tile p-1.5", children: [_jsxs("span", { className: "chat-ui-flat-inner flex items-center gap-1.5 px-1.5 pt-0.5 text-2xs text-text-muted", children: [_jsx(Icon, { icon: Info, size: 12, className: "shrink-0" }), problem.kind === 'unknown' ? t('blocks.unknownComponent', { component: problem.component }) : t('blocks.renderFailed')] }), fallback !== '' && (_jsx("div", { className: "min-w-0 rounded-lg bg-ui-inset px-3 py-2.5 wrap-anywhere shadow-[inset_0_0_0_1px_var(--border)]", children: _jsx(Markdown, { text: fallback, fileLinks: false }) }))] }));
11
13
  }
@@ -40,8 +40,8 @@ const BLOCK = '$block';
40
40
  const LITERAL_PARENTS = new Set(['CodeBlock', 'Tag', 'Choice', 'Item', 'Switch', 'Slider', 'Segmented', 'Option', 'Button', 'Column']);
41
41
  /* Parents that stack their children, where a run of text and inline nodes is one paragraph. */
42
42
  const PARAGRAPHS = {
43
- [BLOCK]: 'px-2 text-xs text-text',
44
- Tab: 'px-2 text-xs text-text',
43
+ [BLOCK]: 'text-xs text-text',
44
+ Tab: 'text-xs text-text',
45
45
  Section: 'text-xs text-text-muted'
46
46
  };
47
47
  const LINKS = new Set(['File', 'Diff', 'Commit', 'Node']);
@@ -138,7 +138,7 @@ function ProseLink({ href, children }) {
138
138
  }
139
139
  const PROSE_COMPONENTS = {
140
140
  p: ({ children }) => children,
141
- code: ({ children }) => _jsx("code", { className: "rounded-sm bg-surface-sunken px-1 py-px font-mono text-code", children: children }),
141
+ code: ({ children }) => _jsx("code", { className: "rounded-sm bg-ui-tile px-1 py-px font-mono text-code", children: children }),
142
142
  a: ({ href, children }) => _jsx(ProseLink, { href: href, children: children }),
143
143
  span: ({ node, children, className }) => node?.properties['data-ui-citation'] !== undefined ? (_jsx(ProseCitation, { number: Number(node.properties['data-ui-citation']), children: children })) : (_jsx("span", { className: className, children: children }))
144
144
  };
@@ -168,7 +168,7 @@ function DrawnChart({ chart, requested, data, unit, context, nodeId }) {
168
168
  const [measureRows, rowsCut] = useTruncated(true);
169
169
  const values = useMemo(() => uiChartValues(data), [data]);
170
170
  const { kind } = chart;
171
- return (_jsxs("figure", { className: "flex flex-col gap-2 px-2", children: [chart.series.length > 1 && _jsx(Legend, { chart: chart }), kind !== 'stacked' && unitApart(unit) && _jsx("p", { className: "text-xs text-text-faint", children: unit }), _jsx("div", { ref: measure, role: "img", "aria-label": chartSummary(values, unit, t), className: "relative", children: kind === 'hbar' || kind === 'stacked' ? (_jsx("div", { ref: measureRows, children: kind === 'hbar' ? _jsx(HorizontalChart, { chart: chart, unit: unit }) : _jsx(StackedChart, { chart: chart, unit: unit }) })) : (_jsx("div", { style: { height: HEIGHT }, children: width > 0 && _jsx(VerticalChart, { chart: chart, kind: kind, unit: unit, width: width, character: character }) })) }), _jsx(ChartNotes, { chart: chart, requested: requested, cut: rowsCut || labelsCut(chart, unit, width, character), values: values, unit: unit, context: context, nodeId: nodeId })] }));
171
+ return (_jsxs("figure", { className: "chat-ui-flat flex flex-col gap-2.5 rounded-xl bg-ui-tile p-3.5", children: [chart.series.length > 1 && _jsx(Legend, { chart: chart }), kind !== 'stacked' && unitApart(unit) && _jsx("p", { className: "text-xs text-text-faint", children: unit }), _jsx("div", { ref: measure, role: "img", "aria-label": chartSummary(values, unit, t), className: "relative", children: kind === 'hbar' || kind === 'stacked' ? (_jsx("div", { ref: measureRows, children: kind === 'hbar' ? _jsx(HorizontalChart, { chart: chart, unit: unit }) : _jsx(StackedChart, { chart: chart, unit: unit }) })) : (_jsx("div", { style: { height: HEIGHT }, children: width > 0 && _jsx(VerticalChart, { chart: chart, kind: kind, unit: unit, width: width, character: character }) })) }), _jsx(ChartNotes, { chart: chart, requested: requested, cut: rowsCut || labelsCut(chart, unit, width, character), values: values, unit: unit, context: context, nodeId: nodeId })] }));
172
172
  }
173
173
  /*
174
174
  * Draws only once its node closed; until then a quiet surface of the height it will take holds its
@@ -178,7 +178,7 @@ export function ChartRenderer({ node, context }) {
178
178
  const { kind, data, unit } = node.props;
179
179
  const chart = useMemo(() => (node.complete ? uiChartData(data, kind) : null), [node.complete, data, kind]);
180
180
  if (!node.complete) {
181
- return _jsx("div", { "aria-hidden": true, className: "mx-2 rounded-md bg-surface-hover", style: { height: chartHeight(kind, data.length) } });
181
+ return _jsx("div", { "aria-hidden": true, className: "rounded-xl bg-ui-tile", style: { height: chartHeight(kind, data.length) } });
182
182
  }
183
183
  if (chart === null) {
184
184
  return _jsx(UiFallbackPart, { fallback: node.fallback, problem: { kind: 'failed' } });
@@ -2,9 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useId, useState } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import { useTranslation } from 'react-i18next';
5
- import { ArrowRight, Check } from 'lucide-react';
5
+ import { Check } from 'lucide-react';
6
6
  import { uiChoiceLabel } from '@adecore/intelligent-ui';
7
- import { DisabledReason, Icon, Spinner, Tooltip } from '@adecore/ui';
7
+ import { DisabledReason, formatShortcut, Icon, KEY_SHORTCUTS, Spinner, Tooltip } from '@adecore/ui';
8
+ import { useChatHost } from '../../../../host.js';
8
9
  import { UiFallbackPart } from '../UiFallbackPart.js';
9
10
  import { useLater } from '../use-later.js';
10
11
  /* A context this long may run past its two lines, so the row offers all of it in a tooltip. */
@@ -14,16 +15,18 @@ const SPINNER_AFTER_MS = 300;
14
15
  export function ChoicesRenderer({ children, context }) {
15
16
  const { t } = useTranslation('agent-chat');
16
17
  const [streamed] = useState(context.phase === 'streaming');
17
- return (_jsxs("div", { role: "group", "aria-label": t('blocks.choices'), className: "chat-ui-nodes flex flex-col gap-1.5", children: [children, _jsx("span", { className: "sr-only", role: "status", children: streamed && context.phase === 'final' ? t('blocks.choice.ready') : '' })] }));
18
+ return (_jsxs("div", { role: "group", "aria-label": t('blocks.choices'), className: "chat-ui-nodes @container flex flex-wrap items-start gap-2", children: [children, _jsx("span", { className: "sr-only", role: "status", children: streamed && context.phase === 'final' ? t('blocks.choice.ready') : '' })] }));
18
19
  }
19
20
  /*
20
- * A row that shows what it sends: the label, and under it the context the agent receives. It stays
21
+ * A button that shows what it sends: the label, and under it the context the agent receives. It stays
21
22
  * closed while the reply streams, once the block is answered and without a host to send it, and
22
23
  * remains focusable then so a screen reader hears why. A finished Choice the host would refuse for
23
- * its label is no row at all, but its text.
24
+ * its label is no button at all, but its text. The primary one says the key that picks it while the
25
+ * focus is in the block (`UiBlockFrame`).
24
26
  */
25
27
  export function ChoiceRenderer({ node, children, context }) {
26
28
  const { t } = useTranslation('agent-chat');
29
+ const host = useChatHost();
27
30
  const descriptionId = useId();
28
31
  const { context: detail, primary, disabled } = node.props;
29
32
  const { answer, onChoose } = context;
@@ -42,10 +45,12 @@ export function ChoiceRenderer({ node, children, context }) {
42
45
  ? ((disabled === true ? null : context.choiceReason) ?? t('blocks.choice.unavailable'))
43
46
  : null;
44
47
  const described = detail !== undefined && detail !== '';
45
- const row = (_jsxs("button", { type: "button", "aria-disabled": closed || undefined, "aria-describedby": described ? descriptionId : undefined, className: clsx('flex w-full items-start gap-2 rounded-md border px-2 py-1.5 text-left transition-[opacity,scale,background-color] duration-150 motion-reduce:transition-none', chosen ? 'border-accent bg-accent-soft' : failed ? 'border-status-error bg-surface-hover' : 'border-border bg-surface-hover', !closed && 'hover:bg-surface-active active:scale-99', waiting ? 'opacity-60' : closed && !chosen && 'opacity-50'), onClick: () => {
48
+ const strong = primary === true;
49
+ const button = (_jsxs("button", { type: "button", "aria-disabled": closed || undefined, "aria-describedby": described ? descriptionId : undefined, "aria-keyshortcuts": strong && !closed ? 'Meta+Enter Control+Enter' : undefined, "data-ui-primary-choice": strong && !closed ? '' : undefined, className: clsx('flex max-w-full min-h-9.5 items-center gap-2.5 rounded-xl px-4 py-2 text-left text-xs transition-[opacity,scale,background-color,filter] duration-150 motion-reduce:transition-none', strong ? 'bg-accent text-accent-text' : 'bg-ui-tile text-text', chosen && 'ring-2 ring-accent ring-offset-2 ring-offset-surface', failed && 'ring-2 ring-status-error', !closed && (strong ? 'hover:brightness-110' : 'hover:bg-surface-active'), !closed && 'active:scale-99', waiting ? 'opacity-60' : closed && !chosen && 'opacity-50'), onClick: () => {
46
50
  if (!closed) {
47
51
  onChoose?.(node.id);
48
52
  }
49
- }, children: [_jsx(Icon, { icon: ArrowRight, size: 14, className: clsx('mt-1 shrink-0', primary === true ? 'text-accent' : 'text-text-faint') }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "text-sm font-semibold wrap-anywhere text-text", children: children }), described && (_jsx("span", { id: descriptionId, className: "line-clamp-2 text-xs wrap-anywhere text-text-muted", children: detail }))] }), chosen && (_jsxs("span", { className: "mt-0.5 flex shrink-0 items-center gap-1 text-xs text-accent", children: [slow ? _jsx(Spinner, { size: 12 }) : _jsx(Icon, { icon: Check, size: 12 }), answer.state === 'queued' ? t('blocks.choice.queued') : t('blocks.choice.sent')] }))] }));
50
- return (_jsxs("div", { className: "flex flex-col gap-1", children: [reason !== null ? (_jsx(DisabledReason, { reason: reason, children: row })) : described && detail.length > LONG_CONTEXT ? (_jsx(Tooltip, { label: detail, children: row })) : (row), failed && (_jsx("p", { role: "alert", className: "px-2 text-xs text-status-error", children: t('blocks.choice.failed') }))] }));
53
+ }, children: [_jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: clsx('wrap-anywhere', strong ? 'font-semibold' : 'font-medium'), children: children }), described && (_jsx("span", { id: descriptionId, className: clsx('line-clamp-2 text-2xs wrap-anywhere', strong ? 'text-accent-text/80' : 'text-text-muted'), children: detail }))] }), strong && !closed && (_jsx("kbd", { "aria-hidden": true, className: "shrink-0 rounded-md bg-accent-text/18 px-1.5 font-sans text-2xs font-medium", children: formatShortcut(KEY_SHORTCUTS.modEnter, host.isApplePlatform()) })), chosen && (_jsxs("span", { className: clsx('flex shrink-0 items-center gap-1 text-2xs', strong ? 'text-accent-text' : 'text-accent'), children: [slow ? _jsx(Spinner, { size: 12 }) : _jsx(Icon, { icon: Check, size: 12 }), answer.state === 'queued' ? t('blocks.choice.queued') : t('blocks.choice.sent')] }))] }));
54
+ // At most half the row less half the gap, so two long choices stand side by side and end on one edge; a narrow block gives each the row.
55
+ return (_jsxs("div", { className: "flex max-w-full flex-col gap-1 @min-[420px]:max-w-[calc(50%-4px)]", children: [reason !== null ? (_jsx(DisabledReason, { reason: reason, children: button })) : described && detail.length > LONG_CONTEXT ? (_jsx(Tooltip, { label: detail, children: button })) : (button), failed && (_jsx("p", { role: "alert", className: "px-1 text-2xs text-status-error", children: t('blocks.choice.failed') }))] }));
51
56
  }
@@ -15,7 +15,8 @@ import { useTruncated } from '../use-truncated.js';
15
15
  const SourceNumbers = createContext(new Map());
16
16
  /* Plain text in the same measure until the node closes, so highlighting never makes it jump. */
17
17
  export function CodeBlockRenderer({ node }) {
18
- return (_jsx(CodeStreamingContext.Provider, { value: !node.complete, children: _jsx(CodeBlock, { code: uiNodeText(node).replace(/^\n|\n$/g, ''), lang: node.props.language ?? 'text' }) }));
18
+ const { language } = node.props;
19
+ return (_jsxs("div", { className: "chat-ui-code chat-ui-flat flex flex-col rounded-xl bg-ui-tile p-1.5", children: [language !== undefined && language !== '' && (_jsx("div", { className: "chat-ui-flat-inner flex px-1.5 pt-0.5 pb-1.5", children: _jsx("span", { className: "rounded-md bg-ui-inset px-1.5 font-mono text-2xs text-text shadow-[inset_0_0_0_1px_var(--border)]", children: language }) })), _jsx(CodeStreamingContext.Provider, { value: !node.complete, children: _jsx(CodeBlock, { code: uiNodeText(node).replace(/^\n|\n$/g, ''), lang: language ?? 'text' }) })] }));
19
20
  }
20
21
  // Set by a Gallery, so the images inside it draw as cells of its grid.
21
22
  const GalleryContext = createContext(false);
@@ -27,7 +28,7 @@ function AttachedImage({ scopeId, chatId, attachmentId, alt, cell, unnamed }) {
27
28
  const host = useChatHost();
28
29
  const source = host.attachments.useUrl(scopeId, chatId, attachmentId);
29
30
  if (!cell) {
30
- return _jsx(ImageThumb, { source: source, alt: alt, className: "max-h-70 max-w-full object-contain" });
31
+ return _jsx(ImageThumb, { source: source, alt: alt, className: "max-h-70 max-w-full rounded-xl object-contain" });
31
32
  }
32
33
  return (_jsxs(_Fragment, { children: [_jsx(ImageThumb, { source: source, alt: alt, aspect: 1 }), unnamed && source.failure !== null && _jsx("figcaption", { className: "text-xs wrap-anywhere text-text-faint", children: alt })] }));
33
34
  }
@@ -41,12 +42,12 @@ export function ImageRenderer({ node, children, context }) {
41
42
  const { attachment, alt } = node.props;
42
43
  const caption = uiNodeLabel(node);
43
44
  const name = alt ?? (caption === '' ? t('blocks.image.label') : caption);
44
- return (_jsxs("figure", { className: cell ? 'flex min-w-0 flex-col gap-1' : 'flex flex-col items-start gap-1.5', children: [attachment === undefined ? (_jsx(ImageThumb, { source: { url: null, failure: t('blocks.image.unavailable') }, alt: name, aspect: cell ? 1 : undefined })) : (_jsx(AttachedImage, { scopeId: context.scopeId, chatId: context.chatId, attachmentId: attachment, alt: name, cell: cell, unnamed: caption === '' })), caption !== '' && _jsx("figcaption", { className: cell ? 'text-xs wrap-anywhere text-text-muted' : 'px-2 text-xs text-text-muted', children: children }), cell && caption === '' && attachment === undefined && (_jsx("figcaption", { className: "text-xs wrap-anywhere text-text-faint", children: alt ?? t('blocks.image.unavailable') }))] }));
45
+ return (_jsxs("figure", { className: cell ? 'flex min-w-0 flex-col gap-1' : 'flex flex-col items-start gap-1.5', children: [attachment === undefined ? (_jsx(ImageThumb, { source: { url: null, failure: t('blocks.image.unavailable') }, alt: name, aspect: cell ? 1 : undefined })) : (_jsx(AttachedImage, { scopeId: context.scopeId, chatId: context.chatId, attachmentId: attachment, alt: name, cell: cell, unnamed: caption === '' })), caption !== '' && _jsx("figcaption", { className: "px-0.5 text-2xs wrap-anywhere text-text-muted", children: children }), cell && caption === '' && attachment === undefined && (_jsx("figcaption", { className: "text-xs wrap-anywhere text-text-faint", children: alt ?? t('blocks.image.unavailable') }))] }));
45
46
  }
46
47
  /* Three columns, two in a narrow block; the container decides, not the window, since a chat can be any width. */
47
48
  export function GalleryRenderer({ node, children }) {
48
49
  const { t } = useTranslation('agent-chat');
49
- return (_jsx(GalleryContext, { value: true, children: _jsx("div", { role: "group", "aria-label": t('blocks.gallery.label', { count: uiChildrenOf(node, 'Image').length }), className: "@container px-2", children: _jsx("div", { className: "chat-ui-nodes grid grid-cols-2 gap-2 @min-[420px]:grid-cols-3", children: children }) }) }));
50
+ return (_jsx(GalleryContext, { value: true, children: _jsx("div", { role: "group", "aria-label": t('blocks.gallery.label', { count: uiChildrenOf(node, 'Image').length }), className: "@container", children: _jsx("div", { className: "chat-ui-nodes grid grid-cols-2 gap-2 @min-[420px]:grid-cols-3", children: children }) }) }));
50
51
  }
51
52
  // The shape of the frame until the first picture reports its own.
52
53
  const COMPARE_ASPECT = 4 / 3;
@@ -76,7 +77,7 @@ export function CompareRenderer({ node, children, context }) {
76
77
  body = _jsx(ImageThumb, { source: { url: null, failure: null }, alt: label, aspect: COMPARE_ASPECT });
77
78
  }
78
79
  else if (first.url !== null && second.url !== null) {
79
- body = (_jsxs("div", { className: "relative max-h-96 overflow-hidden rounded-lg border border-border bg-surface-sunken", style: { aspectRatio: aspect, width: `min(100%, ${COMPARE_MAX_HEIGHT * aspect}px)` }, children: [_jsx(Wipe, { value: split, onValueChange: setSplit, label: label, before: _jsx("img", { src: first.url, alt: describe(beforeLabel), draggable: false, className: "size-full object-contain select-none", onLoad: (event) => {
80
+ body = (_jsxs("div", { className: "relative max-h-96 overflow-hidden rounded-xl bg-ui-tile", style: { aspectRatio: aspect, width: `min(100%, ${COMPARE_MAX_HEIGHT * aspect}px)` }, children: [_jsx(Wipe, { value: split, onValueChange: setSplit, label: label, before: _jsx("img", { src: first.url, alt: describe(beforeLabel), draggable: false, className: "size-full object-contain select-none", onLoad: (event) => {
80
81
  const { naturalWidth, naturalHeight } = event.currentTarget;
81
82
  if (naturalWidth > 0 && naturalHeight > 0) {
82
83
  setAspect(naturalWidth / naturalHeight);
@@ -88,9 +89,9 @@ export function CompareRenderer({ node, children, context }) {
88
89
  }
89
90
  else {
90
91
  const shown = first.url !== null ? { source: first, name: describe(beforeLabel) } : { source: second, name: describe(afterLabel) };
91
- body = (_jsxs(_Fragment, { children: [_jsx(ImageThumb, { source: shown.source, alt: shown.name, className: "max-h-70 max-w-full object-contain" }), _jsx("span", { className: "px-2 text-xs text-text-faint", children: t('blocks.compare.missing') })] }));
92
+ body = (_jsxs(_Fragment, { children: [_jsx(ImageThumb, { source: shown.source, alt: shown.name, className: "max-h-70 max-w-full object-contain" }), _jsx("span", { className: "text-2xs text-text-faint", children: t('blocks.compare.missing') })] }));
92
93
  }
93
- return (_jsxs("figure", { className: "flex flex-col items-start gap-1.5 px-2", children: [body, caption !== '' && _jsx("figcaption", { className: "text-xs text-text-muted", children: children })] }));
94
+ return (_jsxs("figure", { className: "flex w-full flex-col items-start gap-1.5", children: [body, caption !== '' && _jsx("figcaption", { className: "px-0.5 text-2xs text-text-muted", children: children })] }));
94
95
  }
95
96
  /* A chip on the picture gets a ground of its own, since a pill's alpha ground would vanish over an image. */
96
97
  function CompareChip({ side, label }) {
@@ -111,21 +112,21 @@ export function ModelRenderer({ node, children, context }) {
111
112
  const View = host.ModelView;
112
113
  let body;
113
114
  if (View !== undefined && source.url !== null) {
114
- body = (_jsx("div", { role: "group", "aria-label": label, className: "relative h-80 w-full overflow-hidden rounded-lg border border-border bg-surface-sunken", children: _jsx(ErrorBoundary, { label: t('blocks.model.crashed'), resetKeys: [attachment, source.url], compact: true, children: _jsx(View, { scopeId: context.scopeId, chatId: context.chatId, attachmentId: attachment, url: source.url, label: label, alt: alt, caption: caption }) }) }));
115
+ body = (_jsx("div", { role: "group", "aria-label": label, className: "relative h-80 w-full overflow-hidden rounded-xl bg-ui-tile", children: _jsx(ErrorBoundary, { label: t('blocks.model.crashed'), resetKeys: [attachment, source.url], compact: true, children: _jsx(View, { scopeId: context.scopeId, chatId: context.chatId, attachmentId: attachment, url: source.url, label: label, alt: alt, caption: caption }) }) }));
115
116
  }
116
117
  else if (View !== undefined && source.failure === null) {
117
- body = _jsx("div", { "aria-busy": "true", "aria-label": label, className: "h-80 w-full rounded-lg border border-border bg-surface-sunken" });
118
+ body = _jsx("div", { "aria-busy": "true", "aria-label": label, className: "h-80 w-full rounded-xl bg-ui-tile" });
118
119
  }
119
120
  else {
120
- body = (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-2 rounded-lg border border-border px-3 py-2 text-xs", children: [_jsx(Icon, { icon: Box, size: 14, className: "shrink-0 text-text-muted" }), _jsx("span", { className: "min-w-0 truncate text-text", children: alt ?? attachment }), _jsx("span", { className: "ml-auto shrink-0 text-text-faint", children: source.failure === null ? t('blocks.model.label') : t('blocks.model.unavailable') })] }));
121
+ body = (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-2 rounded-xl bg-ui-tile px-3 py-2 text-xs", children: [_jsx(Icon, { icon: Box, size: 14, className: "shrink-0 text-text-muted" }), _jsx("span", { className: "min-w-0 truncate text-text", children: alt ?? attachment }), _jsx("span", { className: "ml-auto shrink-0 text-text-faint", children: source.failure === null ? t('blocks.model.label') : t('blocks.model.unavailable') })] }));
121
122
  }
122
- return (_jsxs("figure", { className: "flex flex-col items-start gap-1.5 px-2", children: [body, caption !== '' && _jsx("figcaption", { className: "text-xs text-text-muted", children: children })] }));
123
+ return (_jsxs("figure", { className: "flex w-full flex-col items-start gap-1.5", children: [body, caption !== '' && _jsx("figcaption", { className: "px-0.5 text-2xs text-text-muted", children: children })] }));
123
124
  }
124
125
  export function SourcesRenderer({ node, children }) {
125
126
  const { t } = useTranslation('agent-chat');
126
127
  const headingId = useId();
127
128
  const numbers = new Map(uiChildrenOf(node, 'Source').map((source, index) => [source.id, index + 1]));
128
- return (_jsx(SourceNumbers, { value: numbers, children: _jsxs("section", { "aria-labelledby": headingId, className: "flex flex-col", children: [_jsx("span", { id: headingId, className: "px-2 pb-1 text-xs font-medium text-text-muted", children: t('blocks.sources') }), _jsx("ol", { className: "chat-ui-nodes flex flex-col", children: children })] }) }));
129
+ return (_jsx(SourceNumbers, { value: numbers, children: _jsxs("section", { "aria-labelledby": headingId, className: "chat-ui-flat flex flex-col rounded-xl bg-ui-tile p-1.5", children: [_jsx("span", { id: headingId, className: "chat-ui-flat-inner px-1.5 pt-0.5 pb-1 text-2xs font-medium text-text-muted", children: t('blocks.sources') }), _jsx("ol", { className: "chat-ui-nodes flex flex-col", children: children })] }) }));
129
130
  }
130
131
  /*
131
132
  * A source number in running text. Resting on it or focusing it shows the title and the domain, and
@@ -145,6 +146,6 @@ export function SourceRenderer({ node, context }) {
145
146
  const { title, url } = node.props;
146
147
  const { openUrl } = context;
147
148
  const [measureDomain, domainCut] = useTruncated();
148
- const face = (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-5 shrink-0 self-start text-right text-text-faint tabular-nums", children: number }), _jsx("span", { className: "min-w-0 grow wrap-anywhere text-text", children: title }), _jsx("span", { ref: measureDomain, className: "max-w-1/2 min-w-0 shrink-0 self-start truncate text-text-faint", children: uiUrlDomain(url) })] }));
149
- return (_jsx("li", { "data-ui-source": node.id, tabIndex: -1, className: "chat-ui-source scroll-my-2 rounded-md", children: _jsx(Tooltip, { label: _jsxs(_Fragment, { children: [_jsx("span", { className: "block", children: title }), _jsx("span", { className: "block text-text-muted", children: url })] }), children: openUrl === undefined ? (_jsx("span", { tabIndex: domainCut ? 0 : undefined, className: "flex min-h-8 items-start gap-2 rounded-md px-2 py-2 text-xs", children: face })) : (_jsx("button", { type: "button", className: "flex min-h-8 w-full items-start gap-2 rounded-md px-2 py-2 text-left text-xs hover:bg-surface-hover", onClick: () => openUrl(url), children: face })) }) }));
149
+ const face = (_jsxs(_Fragment, { children: [_jsx("span", { className: "grid size-5 shrink-0 place-items-center rounded-md bg-ui-inset text-2xs font-semibold text-text tabular-nums", children: number }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "font-medium wrap-anywhere text-text", children: title }), _jsx("span", { ref: measureDomain, className: "truncate text-text-muted", children: uiUrlDomain(url) })] })] }));
150
+ return (_jsx("li", { "data-ui-source": node.id, tabIndex: -1, className: "chat-ui-source scroll-my-2 rounded-md", children: _jsx(Tooltip, { label: _jsxs(_Fragment, { children: [_jsx("span", { className: "block", children: title }), _jsx("span", { className: "block text-text-muted", children: url })] }), children: openUrl === undefined ? (_jsx("span", { tabIndex: domainCut ? 0 : undefined, className: "chat-ui-flat-inner flex min-h-8 items-start gap-2.5 rounded-lg px-1.5 py-1.5 text-2xs", children: face })) : (_jsx("button", { type: "button", className: "chat-ui-flat-inner flex min-h-8 w-full items-start gap-2.5 rounded-lg px-1.5 py-1.5 text-left text-2xs hover:bg-text/4", onClick: () => openUrl(url), children: face })) }) }));
150
151
  }
@@ -1,16 +1,20 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useContext } from 'react';
2
3
  import clsx from 'clsx';
3
4
  import { useTranslation } from 'react-i18next';
4
5
  import { ArrowDown, ArrowRight, ArrowUp } from 'lucide-react';
5
- import { Button, Icon, KeyValueList, Pill } from '@adecore/ui';
6
+ import { Button, Icon, KeyValueList, Pill, Tooltip } from '@adecore/ui';
6
7
  import { formatBytes, formatDuration, formatMoment, formatNumber, formatPercent, formatRounded } from '@adecore/ui/format';
7
8
  import { useBlockLocal } from '../block-local.js';
8
9
  import { uiChildrenOf, uiWithUnit } from '../node-text.js';
9
10
  import { isNumericColumn, UI_TABLE_ROWS, uiTableCell, uiTableColumns } from '../table-data.js';
10
- import { UI_TONE_TEXT } from '../tones.js';
11
+ import { UI_TONE_PILL } from '../tones.js';
11
12
  import { UiFallbackPart } from '../UiFallbackPart.js';
12
13
  /* EntityList draws a hairline between its rows from this many on. */
13
14
  const DIVIDED_FROM = 7;
15
+ /* Up to this many entries an EntityList is a row of tiles; a longer one is a list in one tile. */
16
+ const TILED_UP_TO = 4;
17
+ const EntityTiles = createContext(false);
14
18
  /*
15
19
  * A number that live readings keep up to date. Keyed by its text, so a value that changed mounts
16
20
  * again and lights up once, even when it changes again before the last light settled.
@@ -22,7 +26,7 @@ function LiveFigure({ text, previous, live, className }) {
22
26
  export function StatsRenderer({ children }) {
23
27
  return _jsx("div", { className: "chat-ui-nodes grid grid-cols-[repeat(auto-fit,minmax(128px,1fr))] gap-2", children: children });
24
28
  }
25
- /* The arrow and the number say which way a value went; only a tone says whether that is good. */
29
+ /* The arrow and the number say which way a value went; only a tone says whether that is good. The tag says the change, its tooltip from what. */
26
30
  export function StatRenderer({ node, context }) {
27
31
  const { t } = useTranslation('agent-chat');
28
32
  const { label, value, previous, unit, tone } = node.props;
@@ -32,12 +36,21 @@ export function StatRenderer({ node, context }) {
32
36
  const from = previous === undefined ? '' : uiWithUnit(formatRounded(previous, 2), unit);
33
37
  // A change from zero has no percentage.
34
38
  const percent = change === null || previous === undefined || previous === 0 ? null : formatPercent(Math.abs((change / previous) * 100));
35
- return (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5 rounded-md bg-surface-hover px-2 py-1.5", children: [_jsx("span", { className: "truncate text-xs text-text-muted", children: label }), _jsx(LiveFigure, { text: uiWithUnit(formatRounded(value, 2), unit), previous: typeof live?.previous === 'number' ? uiWithUnit(formatRounded(live.previous, 2), unit) : undefined, live: live, className: "text-lg font-semibold tabular-nums" }), change !== null && (_jsxs("span", { className: clsx('flex items-center gap-1 text-xs tabular-nums', tone === undefined ? 'text-text-faint' : UI_TONE_TEXT[tone]), children: [_jsx(Icon, { icon: arrow, size: 12, className: "shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: percent === null ? t('blocks.stat.from', { previous: from }) : t('blocks.stat.change', { change: percent, previous: from }) })] }))] }));
39
+ const said = percent === null ? t('blocks.stat.from', { previous: from }) : t('blocks.stat.change', { change: percent, previous: from });
40
+ return (_jsxs("div", { className: "flex min-w-0 flex-col items-start gap-1.5 rounded-xl bg-ui-tile p-3", children: [_jsx("span", { className: "max-w-full truncate text-2xs text-text-muted", children: label }), _jsx(LiveFigure, { text: uiWithUnit(formatRounded(value, 2), unit), previous: typeof live?.previous === 'number' ? uiWithUnit(formatRounded(live.previous, 2), unit) : undefined, live: live, className: "max-w-full truncate text-lg font-semibold tabular-nums" }), change !== null && (_jsx(Tooltip, { label: said, children: _jsx("span", { tabIndex: 0, className: "max-w-full rounded-md", children: _jsxs(Pill, { shape: "tag", size: "sm", tone: tone === undefined ? 'muted' : UI_TONE_PILL[tone], icon: _jsx(Icon, { icon: arrow, size: 12, className: "shrink-0" }), children: [_jsx("span", { "aria-hidden": true, className: "tabular-nums", children: percent ?? from }), _jsx("span", { className: "sr-only", children: said })] }) }) }))] }));
36
41
  }
37
42
  export function EntityListRenderer({ node, children }) {
38
- return (_jsx(KeyValueList.Root, { divided: uiChildrenOf(node, 'Entry').length >= DIVIDED_FROM, className: "chat-ui-nodes px-2", children: children }));
43
+ const count = uiChildrenOf(node, 'Entry').length;
44
+ if (count <= TILED_UP_TO) {
45
+ return (_jsx(EntityTiles, { value: true, children: _jsx("div", { className: "chat-ui-nodes grid grid-cols-[repeat(auto-fit,minmax(140px,1fr))] gap-2", children: children }) }));
46
+ }
47
+ return (_jsx(KeyValueList.Root, { divided: count >= DIVIDED_FROM, className: "chat-ui-nodes chat-ui-flat rounded-xl bg-ui-tile px-3 py-2", children: children }));
39
48
  }
40
49
  export function EntryRenderer({ node, children }) {
50
+ const tiled = useContext(EntityTiles);
51
+ if (tiled) {
52
+ return (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5 rounded-xl bg-ui-tile px-3 py-2", children: [_jsx("span", { className: "truncate text-2xs text-text-muted", children: node.props.label }), _jsx("span", { className: "text-2xs font-semibold wrap-break-word text-text", children: children })] }));
53
+ }
41
54
  return (_jsxs(KeyValueList.Item, { children: [_jsx(KeyValueList.Name, { children: node.props.label }), _jsx(KeyValueList.Value, { children: children })] }));
42
55
  }
43
56
  function numberText(cell, unit) {
@@ -76,5 +89,5 @@ export function TableRenderer({ node, context }) {
76
89
  }
77
90
  const { rows } = node.props;
78
91
  const shown = all ? rows : rows.slice(0, UI_TABLE_ROWS);
79
- return (_jsxs("div", { className: "flex flex-col items-start gap-1.5", children: [_jsx("div", { className: "max-h-80 w-full overflow-auto rounded-md border border-border-soft", children: _jsxs("table", { "aria-label": t('blocks.table.label', { columns: columns.map((column) => column.title).join(', ') }), className: "w-full border-collapse text-xs tabular-nums", children: [_jsx("thead", { className: "sticky top-0 z-1 bg-surface-hover", children: _jsx("tr", { children: columns.map((column) => (_jsx("th", { scope: "col", className: clsx('h-8 px-2 font-medium whitespace-nowrap text-text-muted', isNumericColumn(column) ? 'text-right' : 'text-left'), children: column.title }, column.key))) }) }), _jsx("tbody", { children: shown.map((row, index) => (_jsx("tr", { className: "border-t border-border-soft", children: columns.map((column) => (_jsx("td", { className: clsx('h-8 px-2 text-text', isNumericColumn(column) ? 'text-right whitespace-nowrap' : 'text-left'), children: _jsx(Cell, { value: row[column.key], column: column, live: isNumericColumn(column) ? context.liveValue?.(node.id, 'rows', [index, column.key]) : undefined }) }, column.key))) }, index))) })] }) }), !all && rows.length > UI_TABLE_ROWS && (_jsx(Button, { size: "xs", variant: "secondary", onClick: () => setAll(true), children: t('blocks.table.showAll', { total: formatNumber(rows.length) }) }))] }));
92
+ return (_jsxs("div", { className: "flex flex-col items-start gap-1.5", children: [_jsx("div", { className: "chat-ui-flat max-h-80 w-full overflow-auto rounded-xl bg-ui-tile px-1.5 pb-1.5", children: _jsxs("table", { "aria-label": t('blocks.table.label', { columns: columns.map((column) => column.title).join(', ') }), className: clsx('w-full border-separate border-spacing-0 text-xs tabular-nums', '[&_tbody_tr:first-child_td:first-child]:rounded-tl-lg [&_tbody_tr:first-child_td:last-child]:rounded-tr-lg', '[&_tbody_tr:last-child_td:first-child]:rounded-bl-lg [&_tbody_tr:last-child_td:last-child]:rounded-br-lg'), children: [_jsx("thead", { className: "sticky top-0 z-1 bg-ui-tile", children: _jsx("tr", { children: columns.map((column) => (_jsx("th", { scope: "col", className: clsx('h-8 px-2 text-2xs font-medium whitespace-nowrap text-text-muted', isNumericColumn(column) ? 'text-right' : 'text-left'), children: column.title }, column.key))) }) }), _jsx("tbody", { children: shown.map((row, index) => (_jsx("tr", { children: columns.map((column, at) => (_jsx("td", { className: clsx('h-9 bg-ui-inset px-2 text-text', index > 0 && 'border-t border-border-soft', at === 0 && 'font-medium', isNumericColumn(column) ? 'text-right whitespace-nowrap' : 'text-left'), children: _jsx(Cell, { value: row[column.key], column: column, live: isNumericColumn(column) ? context.liveValue?.(node.id, 'rows', [index, column.key]) : undefined }) }, column.key))) }, index))) })] }) }), !all && rows.length > UI_TABLE_ROWS && (_jsx(Button, { size: "xs", variant: "secondary", onClick: () => setAll(true), children: t('blocks.table.showAll', { total: formatNumber(rows.length) }) }))] }));
80
93
  }
@@ -11,7 +11,7 @@ const ChecklistContext = createContext({ values: [], enabled: false, toggle: ()
11
11
  /* The rows of a checklist as one named group. */
12
12
  function ChecklistGroup({ pending, children }) {
13
13
  const { t } = useTranslation('agent-chat');
14
- return (_jsx("div", { role: "group", "aria-label": t('blocks.checklist'), className: clsx('chat-ui-nodes flex flex-col', pending && PENDING), children: children }));
14
+ return (_jsx("div", { role: "group", "aria-label": t('blocks.checklist'), className: clsx('chat-ui-nodes flex flex-col gap-1.5', pending && PENDING), children: children }));
15
15
  }
16
16
  export function ChecklistRenderer({ node, children, context }) {
17
17
  const binding = bindingOf(node, context);
@@ -21,20 +21,21 @@ export function ChecklistRenderer({ node, children, context }) {
21
21
  };
22
22
  return (_jsx(ChecklistContext, { value: { values, enabled: binding !== null, toggle }, children: _jsx(ChecklistGroup, { pending: !node.complete, children: children }) }));
23
23
  }
24
- /* The whole row is the label; a link inside it keeps its own click and checks nothing. */
24
+ /* The whole tile is the label, ringed in the accent while checked; a link inside it keeps its own click and checks nothing. */
25
25
  export function ItemRenderer({ node, children }) {
26
26
  const list = useContext(ChecklistContext);
27
27
  const { value } = node.props;
28
+ const checked = list.values.includes(value);
28
29
  const toggleRow = (event) => {
29
30
  if (list.enabled && !event.target.closest('button, a')) {
30
31
  list.toggle(value);
31
32
  }
32
33
  };
33
- return (_jsxs("div", { className: clsx('flex min-h-8 items-center gap-2 rounded-md px-2 text-sm text-text', list.enabled && 'hover:bg-surface-hover'), onClick: toggleRow, children: [_jsx(Checkbox, { checked: list.values.includes(value), onCheckedChange: () => list.toggle(value), label: uiNodeLabel(node), disabled: !list.enabled }), _jsx("div", { className: "min-w-0 grow", children: children })] }));
34
+ return (_jsxs("div", { className: clsx('flex min-h-10 items-start gap-3 rounded-xl bg-ui-tile px-3 py-2.5 text-xs text-text', checked && 'ring-2 ring-accent ring-inset', list.enabled && 'hover:bg-surface-active'), onClick: toggleRow, children: [_jsx(Checkbox, { className: "mt-0.5", checked: checked, onCheckedChange: () => list.toggle(value), label: uiNodeLabel(node), disabled: !list.enabled }), _jsx("div", { className: "min-w-0 grow", children: children })] }));
34
35
  }
35
36
  export function SwitchRenderer({ node, children, context }) {
36
37
  const binding = bindingOf(node, context);
37
- return (_jsxs("div", { className: clsx('flex min-h-7 items-center justify-between gap-3 px-2 text-sm', !node.complete && PENDING), children: [_jsx("span", { className: "min-w-0 text-text", children: children }), _jsx(Switch, { checked: binding?.value ?? node.props.value, onCheckedChange: (next) => binding?.onValueChange(next), label: uiNodeLabel(node), disabled: binding === null })] }));
38
+ return (_jsxs("div", { className: clsx('chat-ui-flat flex min-h-11 items-center justify-between gap-3 rounded-xl bg-ui-tile px-3 py-2 text-xs', !node.complete && PENDING), children: [_jsx("span", { className: "min-w-0 font-medium text-text", children: children }), _jsx(Switch, { checked: binding?.value ?? node.props.value, onCheckedChange: (next) => binding?.onValueChange(next), label: uiNodeLabel(node), disabled: binding === null })] }));
38
39
  }
39
40
  /* Its children are the label, read as text: the slider writes its own label line. */
40
41
  export function SliderRenderer({ node, context }) {
@@ -42,7 +43,7 @@ export function SliderRenderer({ node, context }) {
42
43
  const { value, min, max, step, unit } = node.props;
43
44
  return (_jsx(Slider, { value: binding?.value ?? value, onValueChange: (next) => binding?.onValueChange(next), min: min, max: max, step: step,
44
45
  // The slider prints its unit right after the number, so the space a unit stands apart with comes from here.
45
- unit: uiWithUnit('', unit), label: uiNodeLabel(node), disabled: binding === null, className: clsx('px-2', !node.complete && PENDING) }));
46
+ unit: uiWithUnit('', unit), label: uiNodeLabel(node), disabled: binding === null, className: clsx('chat-ui-flat rounded-xl bg-ui-tile p-3', !node.complete && PENDING) }));
46
47
  }
47
48
  /* A label longer than this reads as a sentence, which wraps badly beside another; the options then stand in a column. */
48
49
  const SIDE_BY_SIDE_LABEL = 24;
@@ -53,7 +54,7 @@ export function SegmentedRenderer({ node, context }) {
53
54
  const current = binding?.value ?? node.props.value;
54
55
  const options = uiChildrenOf(node, 'Option').map((option, index) => ({ id: String(index), label: uiNodeLabel(option), value: option.props.value }));
55
56
  const picked = options.find((option) => option.value === current)?.id ?? '';
56
- return (_jsx("div", { className: clsx('px-2', !node.complete && PENDING), children: _jsx(Segmented, { value: picked, options: options, label: t('blocks.options'), orientation: options.some((option) => option.label.length > SIDE_BY_SIDE_LABEL) ? 'vertical' : 'horizontal', disabled: binding === null, onValueChange: (id) => {
57
+ return (_jsx("div", { className: clsx(!node.complete && PENDING), children: _jsx(Segmented, { value: picked, options: options, label: t('blocks.options'), orientation: options.some((option) => option.label.length > SIDE_BY_SIDE_LABEL) ? 'vertical' : 'horizontal', disabled: binding === null, onValueChange: (id) => {
57
58
  const option = options[Number(id)];
58
59
  if (option !== undefined) {
59
60
  binding?.onValueChange(option.value);
@@ -76,7 +77,7 @@ export function TextFieldRenderer({ node, children, context }) {
76
77
  const value = binding?.value ?? node.props.value;
77
78
  const countId = `${id}-count`;
78
79
  const counting = value.length >= maxLength * COUNT_FROM;
79
- return (_jsxs("div", { className: clsx('flex flex-col gap-1 px-2', !node.complete && PENDING), children: [_jsx("label", { htmlFor: id, className: "text-sm text-text-muted", children: children }), _jsx(Input, { id: id, value: value, size: "sm", maxLength: maxLength, placeholder: placeholder, disabled: binding === null, "aria-describedby": counting ? countId : undefined, className: "max-w-96 bg-surface-sunken", onChange: (event) => binding?.onValueChange(event.target.value) }), counting && (_jsx("span", { id: countId, className: "text-xs text-text-muted tabular-nums", children: t('blocks.textField.count', { used: formatNumber(value.length), max: formatNumber(maxLength) }) }))] }));
80
+ return (_jsxs("div", { className: clsx('flex flex-col gap-1.5', !node.complete && PENDING), children: [_jsx("label", { htmlFor: id, className: "px-0.5 text-2xs text-text-muted", children: children }), _jsx(Input, { id: id, value: value, size: "sm", maxLength: maxLength, placeholder: placeholder, disabled: binding === null, "aria-describedby": counting ? countId : undefined, className: "border-transparent bg-ui-tile", onChange: (event) => binding?.onValueChange(event.target.value) }), counting && (_jsx("span", { id: countId, className: "text-xs text-text-muted tabular-nums", children: t('blocks.textField.count', { used: formatNumber(value.length), max: formatNumber(maxLength) }) }))] }));
80
81
  }
81
82
  /*
82
83
  * Left aligned, never the full width. Like Segmented, each Option is an item by its place, since a value may be any scalar.
@@ -88,7 +89,7 @@ export function SelectRenderer({ node, context }) {
88
89
  const current = binding?.value ?? node.props.value;
89
90
  const options = uiChildrenOf(node, 'Option').map((option, index) => ({ id: String(index), label: uiNodeLabel(option), value: option.props.value }));
90
91
  const label = uiOwnLabel(node);
91
- return (_jsxs("div", { className: clsx('flex flex-col items-start gap-1 px-2', !node.complete && PENDING), children: [label !== '' && _jsx("span", { className: "text-sm text-text-muted", children: label }), _jsx(Select, { value: options.find((option) => option.value === current)?.id ?? null, items: options.map((option) => ({ value: option.id, label: option.label })), label: label === '' ? t('blocks.options') : label, disabled: binding === null || options.length === 0, onValueChange: (id) => {
92
+ return (_jsxs("div", { className: clsx('flex flex-col items-start gap-1.5', !node.complete && PENDING), children: [label !== '' && _jsx("span", { className: "px-0.5 text-2xs text-text-muted", children: label }), _jsx(Select, { value: options.find((option) => option.value === current)?.id ?? null, items: options.map((option) => ({ value: option.id, label: option.label })), label: label === '' ? t('blocks.options') : label, disabled: binding === null || options.length === 0, onValueChange: (id) => {
92
93
  const option = options[Number(id)];
93
94
  if (option !== undefined) {
94
95
  binding?.onValueChange(option.value);
@@ -100,5 +101,5 @@ export function ButtonRenderer({ node, children, context }) {
100
101
  const enabled = node.onAction !== undefined && node.complete && context.answer === null && node.props.disabled !== true;
101
102
  const label = uiNodeLabel(node);
102
103
  const [measure, truncated] = useTruncated();
103
- return (_jsx("div", { className: clsx('px-2', !node.complete && PENDING), children: _jsx(Tooltip, { label: label, disabled: !truncated, children: _jsx(Button, { variant: "secondary", size: "sm", className: "max-w-full", disabled: !enabled, onClick: () => node.onAction?.(), children: _jsx("span", { ref: measure, className: "min-w-0 truncate", children: children }) }) }) }));
104
+ return (_jsx("div", { className: clsx(!node.complete && PENDING), children: _jsx(Tooltip, { label: label, disabled: !truncated, children: _jsx(Button, { variant: "secondary", size: "sm", className: "max-w-full", disabled: !enabled, onClick: () => node.onAction?.(), children: _jsx("span", { ref: measure, className: "min-w-0 truncate", children: children }) }) }) }));
104
105
  }
@@ -3,9 +3,11 @@ import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { FileDiff, GitCommitHorizontal, SquareDashed } from 'lucide-react';
5
5
  import { FileIcon, Icon, Tooltip } from '@adecore/ui';
6
- import { CHIP_IN_MESSAGE, MENTION_TONE } from '../../chips.js';
6
+ import { CHIP_IN_MESSAGE } from '../../chips.js';
7
7
  import { uiReasonText } from '../node-text.js';
8
8
  const PLAIN = { state: 'plain' };
9
+ /* A link in a block is a quiet card on its tile, unlike a mention in a message, which wears the accent. */
10
+ const UI_CHIP = 'bg-ui-inset font-mono text-text shadow-[inset_0_0_0_1px_var(--border)]';
9
11
  function baseName(path) {
10
12
  const trimmed = path.replace(/\/+$/, '');
11
13
  return trimmed.slice(trimmed.lastIndexOf('/') + 1) || path;
@@ -62,7 +64,7 @@ export function UiLinkChip({ target, context }) {
62
64
  }
63
65
  const { face, hint } = chipFace(target, link);
64
66
  const { openLink } = context;
65
- const chip = openLink === undefined ? (_jsx("span", { className: clsx(CHIP_IN_MESSAGE, MENTION_TONE, 'transition-opacity duration-120 starting:opacity-0'), children: face })) : (_jsx("button", { type: "button", className: clsx(CHIP_IN_MESSAGE, MENTION_TONE, 'transition-opacity duration-120 hover:brightness-95 starting:opacity-0'), onClick: () => openLink(target), children: face }));
67
+ const chip = openLink === undefined ? (_jsx("span", { className: clsx(CHIP_IN_MESSAGE, UI_CHIP, 'transition-opacity duration-120 starting:opacity-0'), children: face })) : (_jsx("button", { type: "button", className: clsx(CHIP_IN_MESSAGE, UI_CHIP, 'transition-opacity duration-120 hover:bg-surface-hover starting:opacity-0'), onClick: () => openLink(target), children: face }));
66
68
  return hint === null ? chip : _jsx(Tooltip, { label: hint, children: chip });
67
69
  }
68
70
  /* A link with children is a row: the chip and what the agent says about it. */
@@ -70,7 +72,7 @@ function LinkRow({ target, context, described, children }) {
70
72
  if (!described) {
71
73
  return _jsx(UiLinkChip, { target: target, context: context });
72
74
  }
73
- return (_jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2 px-2 text-sm", children: [_jsx("span", { className: "max-w-full min-w-0", children: _jsx(UiLinkChip, { target: target, context: context }) }), _jsx("span", { className: "min-w-0 wrap-break-word text-text-muted", children: children })] }));
75
+ return (_jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2 text-xs", children: [_jsx("span", { className: "max-w-full min-w-0", children: _jsx(UiLinkChip, { target: target, context: context }) }), _jsx("span", { className: "min-w-0 wrap-break-word text-text-muted", children: children })] }));
74
76
  }
75
77
  export function FileRenderer({ node, children, context }) {
76
78
  const { path, line } = node.props;
@@ -18,14 +18,14 @@ const MapView = lazyNamed(() => import('./map-view.js'), 'MapView');
18
18
  const LOADING_SPINNER_MS = 1000;
19
19
  /* The number that ties a row to its marker. The list numbers itself for a screen reader, so the chip is only for the eye. */
20
20
  function NumberChip({ number, selected }) {
21
- return (_jsx("span", { "aria-hidden": true, className: clsx('mt-px flex size-5 shrink-0 items-center justify-center rounded-full text-xs tabular-nums', selected ? 'bg-accent text-accent-text' : 'bg-map-marker text-map-marker-text'), children: number }));
21
+ return (_jsx("span", { "aria-hidden": true, className: clsx('mt-px flex size-5 shrink-0 items-center justify-center rounded-md text-2xs font-semibold tabular-nums', selected ? 'bg-accent text-accent-text' : 'bg-ui-inset text-text'), children: number }));
22
22
  }
23
23
  function PlaceRow({ listed, number, selected, onPick }) {
24
24
  const { t } = useTranslation('agent-chat');
25
25
  const { detail, address } = listed.node.props;
26
- const body = (_jsxs(_Fragment, { children: [_jsx(NumberChip, { number: number, selected: selected }), _jsxs("span", { className: "flex min-w-0 grow flex-col gap-0.5", children: [_jsx("span", { className: "text-sm wrap-break-word text-text", children: listed.name }), detail !== undefined && detail !== '' && _jsx("span", { className: "text-xs wrap-break-word text-text-muted", children: detail }), address !== undefined && address !== '' && _jsx("span", { className: "text-xs wrap-break-word text-text-faint", children: address }), listed.point === undefined && _jsx("span", { className: "text-xs text-text-faint", children: t('blocks.map.lookingUp') }), listed.point === null && _jsx("span", { className: "text-xs text-text-faint", children: t('blocks.map.notFound') })] })] }));
27
- const row = 'flex w-full min-h-8 items-start gap-2 rounded-md px-2 py-1.5 text-left';
28
- return (_jsx("li", { children: onPick === null ? (_jsx("div", { className: clsx(row, selected && 'bg-surface-hover'), children: body })) : (_jsx("button", { type: "button", "aria-pressed": selected, className: clsx(row, 'hover:bg-surface-hover', selected && 'bg-surface-hover'), onClick: onPick, children: body })) }));
26
+ const body = (_jsxs(_Fragment, { children: [_jsx(NumberChip, { number: number, selected: selected }), _jsxs("span", { className: "flex min-w-0 grow flex-col gap-0.5", children: [_jsx("span", { className: "text-xs font-medium wrap-break-word text-text", children: listed.name }), detail !== undefined && detail !== '' && _jsx("span", { className: "text-xs wrap-break-word text-text-muted", children: detail }), address !== undefined && address !== '' && _jsx("span", { className: "text-xs wrap-break-word text-text-faint", children: address }), listed.point === undefined && _jsx("span", { className: "text-xs text-text-faint", children: t('blocks.map.lookingUp') }), listed.point === null && _jsx("span", { className: "text-xs text-text-faint", children: t('blocks.map.notFound') })] })] }));
27
+ const row = 'flex h-full w-full min-h-12 items-start gap-2.5 rounded-xl bg-ui-tile px-3 py-2 text-left';
28
+ return (_jsx("li", { children: onPick === null ? (_jsx("div", { className: clsx(row, selected && 'ring-2 ring-accent ring-inset'), children: body })) : (_jsx("button", { type: "button", "aria-pressed": selected, className: clsx(row, 'hover:bg-surface-active', selected && 'ring-2 ring-accent ring-inset'), onClick: onPick, children: body })) }));
29
29
  }
30
30
  /* Why the map is not the host's: its own reason when the source has none, or what the view found out on the way. */
31
31
  function noteFor(t, source, status) {
@@ -75,7 +75,7 @@ function MapFrame({ markers, source, selected, onPick }) {
75
75
  else if (drawn) {
76
76
  body = (_jsx(DiffFallbackBoundary, { fallback: quiet(t('blocks.map.unavailable')), resetKeys: [], children: _jsx(LazyBoundary, { children: _jsx(MapView, { markers: markers, source: source ?? { state: 'off', reason: 'unavailable' }, selected: selected, onPick: onPick, onStatus: setStatus }, attempt) }) }));
77
77
  }
78
- return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: setElement, className: "relative h-50 overflow-hidden rounded-lg border border-border bg-surface-sunken @min-[420px]:h-60", children: [body, waiting && (_jsx("div", { className: "pointer-events-none absolute inset-0 grid place-items-center", children: _jsx(Spinner, { size: 16 }) }))] }), note !== null && _jsx("p", { className: "px-0.5 text-xs text-text-faint", children: note })] }));
78
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: setElement, className: "relative h-50 overflow-hidden rounded-xl bg-ui-tile @min-[420px]:h-60", children: [body, waiting && (_jsx("div", { className: "pointer-events-none absolute inset-0 grid place-items-center", children: _jsx(Spinner, { size: 16 }) }))] }), note !== null && _jsx("p", { className: "px-0.5 text-xs text-text-faint", children: note })] }));
79
79
  }
80
80
  /* Asks the host for its tiles in a component of its own, since a host that offers none has no hook to call. */
81
81
  function HostedFrame({ maps, scopeId, ...frame }) {
@@ -106,8 +106,8 @@ export function MapRenderer({ node, children, context }) {
106
106
  };
107
107
  const onPick = binding !== null ? pick : null;
108
108
  const frame = maps ? (_jsx(HostedFrame, { maps: maps, scopeId: context.scopeId, markers: markers, selected: selectedId, onPick: onPick })) : (_jsx(MapFrame, { markers: markers, source: null, selected: selectedId, onPick: onPick }));
109
- return (_jsxs("div", { className: clsx('@container flex flex-col gap-1.5 px-2', !node.complete && PENDING), children: [frame, _jsx("ol", { "aria-label": t('blocks.map.label', { count: places.length }), className: "flex flex-col", children: places.map((listed, index) => {
109
+ return (_jsxs("div", { className: clsx('@container flex flex-col gap-1.5', !node.complete && PENDING), children: [frame, _jsx("ol", { "aria-label": t('blocks.map.label', { count: places.length }), className: "grid gap-1.5 @min-[420px]:grid-cols-2", children: places.map((listed, index) => {
110
110
  const { value } = listed.node.props;
111
111
  return (_jsx(PlaceRow, { listed: listed, number: index + 1, selected: value !== undefined && value === current, onPick: binding !== null && value !== undefined ? () => binding.onValueChange(value) : null }, listed.node.id));
112
- }) }), node.omitted !== undefined && node.omitted > 0 && (_jsx("p", { className: "px-2 text-xs text-text-faint", children: t('blocks.map.omitted', { count: node.omitted }) })), _jsx("div", { className: "chat-ui-nodes flex flex-col gap-1.5 empty:hidden", children: children })] }));
112
+ }) }), node.omitted !== undefined && node.omitted > 0 && _jsx("p", { className: "text-2xs text-text-faint", children: t('blocks.map.omitted', { count: node.omitted }) }), _jsx("div", { className: "chat-ui-nodes flex flex-col gap-1.5 empty:hidden", children: children })] }));
113
113
  }
@@ -11,22 +11,22 @@ export function TabsRenderer({ node, children, context }) {
11
11
  const tabs = uiChildrenOf(node, 'Tab');
12
12
  const [picked, setPicked] = useBlockLocal(context, node.id, () => null);
13
13
  const value = picked !== null && tabs.some((tab) => tab.id === picked) ? picked : (tabs[0]?.id ?? null);
14
- return (_jsxs(Tabs.Root, { value: value, onValueChange: (next) => setPicked(String(next)), className: "flex flex-col gap-3", children: [_jsx(Tabs.List, { className: "px-2", children: tabs.map((tab) => (_jsx(Tabs.Tab, { value: tab.id, "data-ui-reveal": tab.id, children: tab.props.title }, tab.id))) }), children] }));
14
+ return (_jsxs(Tabs.Root, { value: value, onValueChange: (next) => setPicked(String(next)), className: "flex flex-col gap-3", children: [_jsx(Tabs.List, { variant: "pill", overflow: "scroll", className: "w-fit max-w-full rounded-xl bg-ui-tile p-0.75", children: tabs.map((tab) => (_jsx(Tabs.Tab, { value: tab.id, "data-ui-reveal": tab.id, children: tab.props.title }, tab.id))) }), children] }));
15
15
  }
16
16
  export function TabRenderer({ node, children }) {
17
17
  return (_jsx(Tabs.Panel, { value: node.id, className: "chat-ui-nodes flex flex-col gap-3", children: children }));
18
18
  }
19
19
  export function SectionsRenderer({ node, children, context }) {
20
20
  const [touched, setTouched] = useBlockLocal(context, node.id, () => false);
21
- return (_jsx(SectionsContext, { value: { touched, touch: () => setTouched(true) }, children: _jsx("div", { className: "chat-ui-nodes flex flex-col divide-y divide-border-soft", children: children }) }));
21
+ return (_jsx(SectionsContext, { value: { touched, touch: () => setTouched(true) }, children: _jsx("div", { className: "chat-ui-nodes flex flex-col gap-1.5", children: children }) }));
22
22
  }
23
- /* Arrives open until the person folds a section of the set themselves; the content starts under the title. */
23
+ /* Arrives open until the person folds a section of the set themselves; the content keeps one margin on both sides. */
24
24
  export function SectionRenderer({ node, children, context }) {
25
25
  const fold = useContext(SectionsContext);
26
26
  const [open, setOpen] = useBlockLocal(context, node.id, () => !fold.touched);
27
27
  const contentId = useId();
28
- return (_jsxs("section", { children: [_jsxs("button", { type: "button", "data-ui-reveal": node.id, "aria-expanded": open, "aria-controls": contentId, className: "flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-sm font-medium text-text hover:bg-surface-hover", onClick: () => {
28
+ return (_jsxs("section", { className: "rounded-xl bg-ui-tile", children: [_jsxs("button", { type: "button", "data-ui-reveal": node.id, "aria-expanded": open, "aria-controls": contentId, className: "flex min-h-10 w-full items-center gap-2 rounded-xl px-3 text-left text-xs font-medium text-text hover:bg-text/4", onClick: () => {
29
29
  fold.touch();
30
30
  setOpen(!open);
31
- }, children: [_jsx(Icon, { icon: ChevronRight, size: 14, className: clsx('shrink-0 text-text-faint transition-transform duration-150 motion-reduce:transition-none', open && 'rotate-90') }), _jsx("span", { className: "min-w-0 truncate", children: node.props.title })] }), _jsx("div", { id: contentId, hidden: !open, className: "chat-ui-nodes flex flex-col gap-3 pt-1 pr-2 pb-2 pl-7.5", children: children })] }));
31
+ }, children: [_jsx(Icon, { icon: ChevronRight, size: 14, className: clsx('shrink-0 text-text-faint transition-transform duration-150 motion-reduce:transition-none', open && 'rotate-90') }), _jsx("span", { className: "min-w-0 truncate", children: node.props.title })] }), _jsx("div", { id: contentId, hidden: !open, className: "chat-ui-section-body chat-ui-nodes flex flex-col gap-3 px-3 pb-3", children: children })] }));
32
32
  }