@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
@@ -18,10 +18,18 @@ const SourceNumbers = createContext<ReadonlyMap<string, number>>(new Map());
18
18
 
19
19
  /* Plain text in the same measure until the node closes, so highlighting never makes it jump. */
20
20
  export function CodeBlockRenderer({ node }: UiRendererProps<UiProps<'CodeBlock'>>) {
21
+ const { language } = node.props;
21
22
  return (
22
- <CodeStreamingContext.Provider value={!node.complete}>
23
- <CodeBlock code={uiNodeText(node).replace(/^\n|\n$/g, '')} lang={node.props.language ?? 'text'} />
24
- </CodeStreamingContext.Provider>
23
+ <div className="chat-ui-code chat-ui-flat flex flex-col rounded-xl bg-ui-tile p-1.5">
24
+ {language !== undefined && language !== '' && (
25
+ <div className="chat-ui-flat-inner flex px-1.5 pt-0.5 pb-1.5">
26
+ <span className="rounded-md bg-ui-inset px-1.5 font-mono text-2xs text-text shadow-[inset_0_0_0_1px_var(--border)]">{language}</span>
27
+ </div>
28
+ )}
29
+ <CodeStreamingContext.Provider value={!node.complete}>
30
+ <CodeBlock code={uiNodeText(node).replace(/^\n|\n$/g, '')} lang={language ?? 'text'} />
31
+ </CodeStreamingContext.Provider>
32
+ </div>
25
33
  );
26
34
  }
27
35
 
@@ -50,7 +58,7 @@ function AttachedImage({
50
58
  const host = useChatHost();
51
59
  const source = host.attachments.useUrl(scopeId, chatId, attachmentId);
52
60
  if (!cell) {
53
- return <ImageThumb source={source} alt={alt} className="max-h-70 max-w-full object-contain" />;
61
+ return <ImageThumb source={source} alt={alt} className="max-h-70 max-w-full rounded-xl object-contain" />;
54
62
  }
55
63
  return (
56
64
  <>
@@ -77,7 +85,7 @@ export function ImageRenderer({ node, children, context }: UiRendererProps<UiPro
77
85
  ) : (
78
86
  <AttachedImage scopeId={context.scopeId} chatId={context.chatId} attachmentId={attachment} alt={name} cell={cell} unnamed={caption === ''} />
79
87
  )}
80
- {caption !== '' && <figcaption className={cell ? 'text-xs wrap-anywhere text-text-muted' : 'px-2 text-xs text-text-muted'}>{children}</figcaption>}
88
+ {caption !== '' && <figcaption className="px-0.5 text-2xs wrap-anywhere text-text-muted">{children}</figcaption>}
81
89
  {cell && caption === '' && attachment === undefined && (
82
90
  <figcaption className="text-xs wrap-anywhere text-text-faint">{alt ?? t('blocks.image.unavailable')}</figcaption>
83
91
  )}
@@ -90,7 +98,7 @@ export function GalleryRenderer({ node, children }: UiRendererProps<UiProps<'Gal
90
98
  const { t } = useTranslation('agent-chat');
91
99
  return (
92
100
  <GalleryContext value>
93
- <div role="group" aria-label={t('blocks.gallery.label', { count: uiChildrenOf(node, 'Image').length })} className="@container px-2">
101
+ <div role="group" aria-label={t('blocks.gallery.label', { count: uiChildrenOf(node, 'Image').length })} className="@container">
94
102
  <div className="chat-ui-nodes grid grid-cols-2 gap-2 @min-[420px]:grid-cols-3">{children}</div>
95
103
  </div>
96
104
  </GalleryContext>
@@ -129,7 +137,7 @@ export function CompareRenderer({ node, children, context }: UiRendererProps<UiP
129
137
  body = (
130
138
  // The frame keeps the picture's shape inside its tallest height: past it the width follows, so a portrait picture is not letterboxed.
131
139
  <div
132
- className="relative max-h-96 overflow-hidden rounded-lg border border-border bg-surface-sunken"
140
+ className="relative max-h-96 overflow-hidden rounded-xl bg-ui-tile"
133
141
  style={{ aspectRatio: aspect, width: `min(100%, ${COMPARE_MAX_HEIGHT * aspect}px)` }}
134
142
  >
135
143
  <Wipe
@@ -163,14 +171,14 @@ export function CompareRenderer({ node, children, context }: UiRendererProps<UiP
163
171
  body = (
164
172
  <>
165
173
  <ImageThumb source={shown.source} alt={shown.name} className="max-h-70 max-w-full object-contain" />
166
- <span className="px-2 text-xs text-text-faint">{t('blocks.compare.missing')}</span>
174
+ <span className="text-2xs text-text-faint">{t('blocks.compare.missing')}</span>
167
175
  </>
168
176
  );
169
177
  }
170
178
  return (
171
- <figure className="flex flex-col items-start gap-1.5 px-2">
179
+ <figure className="flex w-full flex-col items-start gap-1.5">
172
180
  {body}
173
- {caption !== '' && <figcaption className="text-xs text-text-muted">{children}</figcaption>}
181
+ {caption !== '' && <figcaption className="px-0.5 text-2xs text-text-muted">{children}</figcaption>}
174
182
  </figure>
175
183
  );
176
184
  }
@@ -204,7 +212,7 @@ export function ModelRenderer({ node, children, context }: UiRendererProps<UiPro
204
212
  let body: ReactNode;
205
213
  if (View !== undefined && source.url !== null) {
206
214
  body = (
207
- <div role="group" aria-label={label} className="relative h-80 w-full overflow-hidden rounded-lg border border-border bg-surface-sunken">
215
+ <div role="group" aria-label={label} className="relative h-80 w-full overflow-hidden rounded-xl bg-ui-tile">
208
216
  <ErrorBoundary label={t('blocks.model.crashed')} resetKeys={[attachment, source.url]} compact>
209
217
  <View
210
218
  scopeId={context.scopeId}
@@ -219,10 +227,10 @@ export function ModelRenderer({ node, children, context }: UiRendererProps<UiPro
219
227
  </div>
220
228
  );
221
229
  } else if (View !== undefined && source.failure === null) {
222
- body = <div aria-busy="true" aria-label={label} className="h-80 w-full rounded-lg border border-border bg-surface-sunken" />;
230
+ body = <div aria-busy="true" aria-label={label} className="h-80 w-full rounded-xl bg-ui-tile" />;
223
231
  } else {
224
232
  body = (
225
- <div className="flex w-full min-w-0 items-center gap-2 rounded-lg border border-border px-3 py-2 text-xs">
233
+ <div className="flex w-full min-w-0 items-center gap-2 rounded-xl bg-ui-tile px-3 py-2 text-xs">
226
234
  <Icon icon={Box} size={14} className="shrink-0 text-text-muted" />
227
235
  <span className="min-w-0 truncate text-text">{alt ?? attachment}</span>
228
236
  <span className="ml-auto shrink-0 text-text-faint">{source.failure === null ? t('blocks.model.label') : t('blocks.model.unavailable')}</span>
@@ -230,9 +238,9 @@ export function ModelRenderer({ node, children, context }: UiRendererProps<UiPro
230
238
  );
231
239
  }
232
240
  return (
233
- <figure className="flex flex-col items-start gap-1.5 px-2">
241
+ <figure className="flex w-full flex-col items-start gap-1.5">
234
242
  {body}
235
- {caption !== '' && <figcaption className="text-xs text-text-muted">{children}</figcaption>}
243
+ {caption !== '' && <figcaption className="px-0.5 text-2xs text-text-muted">{children}</figcaption>}
236
244
  </figure>
237
245
  );
238
246
  }
@@ -243,8 +251,8 @@ export function SourcesRenderer({ node, children }: UiRendererProps<UiProps<'Sou
243
251
  const numbers = new Map(uiChildrenOf(node, 'Source').map((source, index) => [source.id, index + 1]));
244
252
  return (
245
253
  <SourceNumbers value={numbers}>
246
- <section aria-labelledby={headingId} className="flex flex-col">
247
- <span id={headingId} className="px-2 pb-1 text-xs font-medium text-text-muted">
254
+ <section aria-labelledby={headingId} className="chat-ui-flat flex flex-col rounded-xl bg-ui-tile p-1.5">
255
+ <span id={headingId} className="chat-ui-flat-inner px-1.5 pt-0.5 pb-1 text-2xs font-medium text-text-muted">
248
256
  {t('blocks.sources')}
249
257
  </span>
250
258
  <ol className="chat-ui-nodes flex flex-col">{children}</ol>
@@ -305,10 +313,12 @@ export function SourceRenderer({ node, context }: UiRendererProps<UiProps<'Sourc
305
313
  const [measureDomain, domainCut] = useTruncated<HTMLSpanElement>();
306
314
  const face = (
307
315
  <>
308
- <span className="w-5 shrink-0 self-start text-right text-text-faint tabular-nums">{number}</span>
309
- <span className="min-w-0 grow wrap-anywhere text-text">{title}</span>
310
- <span ref={measureDomain} className="max-w-1/2 min-w-0 shrink-0 self-start truncate text-text-faint">
311
- {uiUrlDomain(url)}
316
+ <span className="grid size-5 shrink-0 place-items-center rounded-md bg-ui-inset text-2xs font-semibold text-text tabular-nums">{number}</span>
317
+ <span className="flex min-w-0 grow flex-col">
318
+ <span className="font-medium wrap-anywhere text-text">{title}</span>
319
+ <span ref={measureDomain} className="truncate text-text-muted">
320
+ {uiUrlDomain(url)}
321
+ </span>
312
322
  </span>
313
323
  </>
314
324
  );
@@ -324,13 +334,16 @@ export function SourceRenderer({ node, context }: UiRendererProps<UiProps<'Sourc
324
334
  }
325
335
  >
326
336
  {openUrl === undefined ? (
327
- <span tabIndex={domainCut ? 0 : undefined} className="flex min-h-8 items-start gap-2 rounded-md px-2 py-2 text-xs">
337
+ <span
338
+ tabIndex={domainCut ? 0 : undefined}
339
+ className="chat-ui-flat-inner flex min-h-8 items-start gap-2.5 rounded-lg px-1.5 py-1.5 text-2xs"
340
+ >
328
341
  {face}
329
342
  </span>
330
343
  ) : (
331
344
  <button
332
345
  type="button"
333
- 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"
346
+ 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"
334
347
  onClick={() => openUrl(url)}
335
348
  >
336
349
  {face}
@@ -1,19 +1,25 @@
1
+ import { createContext, useContext } from 'react';
1
2
  import clsx from 'clsx';
2
3
  import { useTranslation } from 'react-i18next';
3
4
  import { ArrowDown, ArrowRight, ArrowUp } from 'lucide-react';
4
5
  import type { UiProps } from '@adecore/intelligent-ui';
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';
8
9
  import { uiChildrenOf, uiWithUnit } from '../node-text';
9
10
  import type { UiLiveValue, UiRendererProps } from '../render-context';
10
11
  import { isNumericColumn, UI_TABLE_ROWS, uiTableCell, uiTableColumns, type UiTableCell, type UiTableColumn } from '../table-data';
11
- import { UI_TONE_TEXT } from '../tones';
12
+ import { UI_TONE_PILL } from '../tones';
12
13
  import { UiFallbackPart } from '../UiFallbackPart';
13
14
 
14
15
  /* EntityList draws a hairline between its rows from this many on. */
15
16
  const DIVIDED_FROM = 7;
16
17
 
18
+ /* Up to this many entries an EntityList is a row of tiles; a longer one is a list in one tile. */
19
+ const TILED_UP_TO = 4;
20
+
21
+ const EntityTiles = createContext(false);
22
+
17
23
  /*
18
24
  * A number that live readings keep up to date. Keyed by its text, so a value that changed mounts
19
25
  * again and lights up once, even when it changes again before the last light settled.
@@ -31,7 +37,7 @@ export function StatsRenderer({ children }: UiRendererProps<UiProps<'Stats'>>) {
31
37
  return <div className="chat-ui-nodes grid grid-cols-[repeat(auto-fit,minmax(128px,1fr))] gap-2">{children}</div>;
32
38
  }
33
39
 
34
- /* The arrow and the number say which way a value went; only a tone says whether that is good. */
40
+ /* 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. */
35
41
  export function StatRenderer({ node, context }: UiRendererProps<UiProps<'Stat'>>) {
36
42
  const { t } = useTranslation('agent-chat');
37
43
  const { label, value, previous, unit, tone } = node.props;
@@ -41,36 +47,63 @@ export function StatRenderer({ node, context }: UiRendererProps<UiProps<'Stat'>>
41
47
  const from = previous === undefined ? '' : uiWithUnit(formatRounded(previous, 2), unit);
42
48
  // A change from zero has no percentage.
43
49
  const percent = change === null || previous === undefined || previous === 0 ? null : formatPercent(Math.abs((change / previous) * 100));
50
+ const said = percent === null ? t('blocks.stat.from', { previous: from }) : t('blocks.stat.change', { change: percent, previous: from });
44
51
  return (
45
- <div className="flex min-w-0 flex-col gap-0.5 rounded-md bg-surface-hover px-2 py-1.5">
46
- <span className="truncate text-xs text-text-muted">{label}</span>
52
+ <div className="flex min-w-0 flex-col items-start gap-1.5 rounded-xl bg-ui-tile p-3">
53
+ <span className="max-w-full truncate text-2xs text-text-muted">{label}</span>
47
54
  <LiveFigure
48
55
  text={uiWithUnit(formatRounded(value, 2), unit)}
49
56
  previous={typeof live?.previous === 'number' ? uiWithUnit(formatRounded(live.previous, 2), unit) : undefined}
50
57
  live={live}
51
- className="text-lg font-semibold tabular-nums"
58
+ className="max-w-full truncate text-lg font-semibold tabular-nums"
52
59
  />
53
60
  {change !== null && (
54
- <span className={clsx('flex items-center gap-1 text-xs tabular-nums', tone === undefined ? 'text-text-faint' : UI_TONE_TEXT[tone])}>
55
- <Icon icon={arrow} size={12} className="shrink-0" />
56
- <span className="min-w-0 truncate">
57
- {percent === null ? t('blocks.stat.from', { previous: from }) : t('blocks.stat.change', { change: percent, previous: from })}
61
+ <Tooltip label={said}>
62
+ <span tabIndex={0} className="max-w-full rounded-md">
63
+ <Pill
64
+ shape="tag"
65
+ size="sm"
66
+ tone={tone === undefined ? 'muted' : UI_TONE_PILL[tone]}
67
+ icon={<Icon icon={arrow} size={12} className="shrink-0" />}
68
+ >
69
+ <span aria-hidden className="tabular-nums">
70
+ {percent ?? from}
71
+ </span>
72
+ <span className="sr-only">{said}</span>
73
+ </Pill>
58
74
  </span>
59
- </span>
75
+ </Tooltip>
60
76
  )}
61
77
  </div>
62
78
  );
63
79
  }
64
80
 
65
81
  export function EntityListRenderer({ node, children }: UiRendererProps<UiProps<'EntityList'>>) {
82
+ const count = uiChildrenOf(node, 'Entry').length;
83
+ if (count <= TILED_UP_TO) {
84
+ return (
85
+ <EntityTiles value>
86
+ <div className="chat-ui-nodes grid grid-cols-[repeat(auto-fit,minmax(140px,1fr))] gap-2">{children}</div>
87
+ </EntityTiles>
88
+ );
89
+ }
66
90
  return (
67
- <KeyValueList.Root divided={uiChildrenOf(node, 'Entry').length >= DIVIDED_FROM} className="chat-ui-nodes px-2">
91
+ <KeyValueList.Root divided={count >= DIVIDED_FROM} className="chat-ui-nodes chat-ui-flat rounded-xl bg-ui-tile px-3 py-2">
68
92
  {children}
69
93
  </KeyValueList.Root>
70
94
  );
71
95
  }
72
96
 
73
97
  export function EntryRenderer({ node, children }: UiRendererProps<UiProps<'Entry'>>) {
98
+ const tiled = useContext(EntityTiles);
99
+ if (tiled) {
100
+ return (
101
+ <div className="flex min-w-0 flex-col gap-0.5 rounded-xl bg-ui-tile px-3 py-2">
102
+ <span className="truncate text-2xs text-text-muted">{node.props.label}</span>
103
+ <span className="text-2xs font-semibold wrap-break-word text-text">{children}</span>
104
+ </div>
105
+ );
106
+ }
74
107
  return (
75
108
  <KeyValueList.Item>
76
109
  <KeyValueList.Name>{node.props.label}</KeyValueList.Name>
@@ -129,19 +162,23 @@ export function TableRenderer({ node, context }: UiRendererProps<UiProps<'Table'
129
162
  const shown = all ? rows : rows.slice(0, UI_TABLE_ROWS);
130
163
  return (
131
164
  <div className="flex flex-col items-start gap-1.5">
132
- <div className="max-h-80 w-full overflow-auto rounded-md border border-border-soft">
165
+ <div className="chat-ui-flat max-h-80 w-full overflow-auto rounded-xl bg-ui-tile px-1.5 pb-1.5">
133
166
  <table
134
167
  aria-label={t('blocks.table.label', { columns: columns.map((column) => column.title).join(', ') })}
135
- className="w-full border-collapse text-xs tabular-nums"
168
+ className={clsx(
169
+ 'w-full border-separate border-spacing-0 text-xs tabular-nums',
170
+ '[&_tbody_tr:first-child_td:first-child]:rounded-tl-lg [&_tbody_tr:first-child_td:last-child]:rounded-tr-lg',
171
+ '[&_tbody_tr:last-child_td:first-child]:rounded-bl-lg [&_tbody_tr:last-child_td:last-child]:rounded-br-lg'
172
+ )}
136
173
  >
137
- <thead className="sticky top-0 z-1 bg-surface-hover">
174
+ <thead className="sticky top-0 z-1 bg-ui-tile">
138
175
  <tr>
139
176
  {columns.map((column) => (
140
177
  <th
141
178
  key={column.key}
142
179
  scope="col"
143
180
  className={clsx(
144
- 'h-8 px-2 font-medium whitespace-nowrap text-text-muted',
181
+ 'h-8 px-2 text-2xs font-medium whitespace-nowrap text-text-muted',
145
182
  isNumericColumn(column) ? 'text-right' : 'text-left'
146
183
  )}
147
184
  >
@@ -153,11 +190,16 @@ export function TableRenderer({ node, context }: UiRendererProps<UiProps<'Table'
153
190
  <tbody>
154
191
  {shown.map((row, index) => (
155
192
  // Rows only ever grow at the end while a block streams, so the place of a row is a stable key.
156
- <tr key={index} className="border-t border-border-soft">
157
- {columns.map((column) => (
193
+ <tr key={index}>
194
+ {columns.map((column, at) => (
158
195
  <td
159
196
  key={column.key}
160
- className={clsx('h-8 px-2 text-text', isNumericColumn(column) ? 'text-right whitespace-nowrap' : 'text-left')}
197
+ className={clsx(
198
+ 'h-9 bg-ui-inset px-2 text-text',
199
+ index > 0 && 'border-t border-border-soft',
200
+ at === 0 && 'font-medium',
201
+ isNumericColumn(column) ? 'text-right whitespace-nowrap' : 'text-left'
202
+ )}
161
203
  >
162
204
  <Cell
163
205
  value={row[column.key]}
@@ -23,7 +23,7 @@ const ChecklistContext = createContext<ChecklistState>({ values: [], enabled: fa
23
23
  function ChecklistGroup({ pending, children }: { pending: boolean; children: ReactNode }) {
24
24
  const { t } = useTranslation('agent-chat');
25
25
  return (
26
- <div role="group" aria-label={t('blocks.checklist')} className={clsx('chat-ui-nodes flex flex-col', pending && PENDING)}>
26
+ <div role="group" aria-label={t('blocks.checklist')} className={clsx('chat-ui-nodes flex flex-col gap-1.5', pending && PENDING)}>
27
27
  {children}
28
28
  </div>
29
29
  );
@@ -42,10 +42,11 @@ export function ChecklistRenderer({ node, children, context }: UiRendererProps<U
42
42
  );
43
43
  }
44
44
 
45
- /* The whole row is the label; a link inside it keeps its own click and checks nothing. */
45
+ /* The whole tile is the label, ringed in the accent while checked; a link inside it keeps its own click and checks nothing. */
46
46
  export function ItemRenderer({ node, children }: UiRendererProps<UiProps<'Item'>>) {
47
47
  const list = useContext(ChecklistContext);
48
48
  const { value } = node.props;
49
+ const checked = list.values.includes(value);
49
50
  const toggleRow = (event: MouseEvent<HTMLDivElement>): void => {
50
51
  if (list.enabled && !(event.target as Element).closest('button, a')) {
51
52
  list.toggle(value);
@@ -53,10 +54,14 @@ export function ItemRenderer({ node, children }: UiRendererProps<UiProps<'Item'>
53
54
  };
54
55
  return (
55
56
  <div
56
- className={clsx('flex min-h-8 items-center gap-2 rounded-md px-2 text-sm text-text', list.enabled && 'hover:bg-surface-hover')}
57
+ className={clsx(
58
+ 'flex min-h-10 items-start gap-3 rounded-xl bg-ui-tile px-3 py-2.5 text-xs text-text',
59
+ checked && 'ring-2 ring-accent ring-inset',
60
+ list.enabled && 'hover:bg-surface-active'
61
+ )}
57
62
  onClick={toggleRow}
58
63
  >
59
- <Checkbox checked={list.values.includes(value)} onCheckedChange={() => list.toggle(value)} label={uiNodeLabel(node)} disabled={!list.enabled} />
64
+ <Checkbox className="mt-0.5" checked={checked} onCheckedChange={() => list.toggle(value)} label={uiNodeLabel(node)} disabled={!list.enabled} />
60
65
  <div className="min-w-0 grow">{children}</div>
61
66
  </div>
62
67
  );
@@ -65,8 +70,10 @@ export function ItemRenderer({ node, children }: UiRendererProps<UiProps<'Item'>
65
70
  export function SwitchRenderer({ node, children, context }: UiRendererProps<UiProps<'Switch'>>) {
66
71
  const binding = bindingOf(node, context);
67
72
  return (
68
- <div className={clsx('flex min-h-7 items-center justify-between gap-3 px-2 text-sm', !node.complete && PENDING)}>
69
- <span className="min-w-0 text-text">{children}</span>
73
+ <div
74
+ 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)}
75
+ >
76
+ <span className="min-w-0 font-medium text-text">{children}</span>
70
77
  <Switch
71
78
  checked={binding?.value ?? node.props.value}
72
79
  onCheckedChange={(next) => binding?.onValueChange(next)}
@@ -92,7 +99,7 @@ export function SliderRenderer({ node, context }: UiRendererProps<UiProps<'Slide
92
99
  unit={uiWithUnit('', unit)}
93
100
  label={uiNodeLabel(node)}
94
101
  disabled={binding === null}
95
- className={clsx('px-2', !node.complete && PENDING)}
102
+ className={clsx('chat-ui-flat rounded-xl bg-ui-tile p-3', !node.complete && PENDING)}
96
103
  />
97
104
  );
98
105
  }
@@ -108,7 +115,7 @@ export function SegmentedRenderer({ node, context }: UiRendererProps<UiProps<'Se
108
115
  const options = uiChildrenOf(node, 'Option').map((option, index) => ({ id: String(index), label: uiNodeLabel(option), value: option.props.value }));
109
116
  const picked = options.find((option) => option.value === current)?.id ?? '';
110
117
  return (
111
- <div className={clsx('px-2', !node.complete && PENDING)}>
118
+ <div className={clsx(!node.complete && PENDING)}>
112
119
  <Segmented
113
120
  value={picked}
114
121
  options={options}
@@ -145,8 +152,8 @@ export function TextFieldRenderer({ node, children, context }: UiRendererProps<U
145
152
  const countId = `${id}-count`;
146
153
  const counting = value.length >= maxLength * COUNT_FROM;
147
154
  return (
148
- <div className={clsx('flex flex-col gap-1 px-2', !node.complete && PENDING)}>
149
- <label htmlFor={id} className="text-sm text-text-muted">
155
+ <div className={clsx('flex flex-col gap-1.5', !node.complete && PENDING)}>
156
+ <label htmlFor={id} className="px-0.5 text-2xs text-text-muted">
150
157
  {children}
151
158
  </label>
152
159
  <Input
@@ -157,7 +164,7 @@ export function TextFieldRenderer({ node, children, context }: UiRendererProps<U
157
164
  placeholder={placeholder}
158
165
  disabled={binding === null}
159
166
  aria-describedby={counting ? countId : undefined}
160
- className="max-w-96 bg-surface-sunken"
167
+ className="border-transparent bg-ui-tile"
161
168
  onChange={(event) => binding?.onValueChange(event.target.value)}
162
169
  />
163
170
  {counting && (
@@ -180,8 +187,8 @@ export function SelectRenderer({ node, context }: UiRendererProps<UiProps<'Selec
180
187
  const options = uiChildrenOf(node, 'Option').map((option, index) => ({ id: String(index), label: uiNodeLabel(option), value: option.props.value }));
181
188
  const label = uiOwnLabel(node);
182
189
  return (
183
- <div className={clsx('flex flex-col items-start gap-1 px-2', !node.complete && PENDING)}>
184
- {label !== '' && <span className="text-sm text-text-muted">{label}</span>}
190
+ <div className={clsx('flex flex-col items-start gap-1.5', !node.complete && PENDING)}>
191
+ {label !== '' && <span className="px-0.5 text-2xs text-text-muted">{label}</span>}
185
192
  <Select
186
193
  value={options.find((option) => option.value === current)?.id ?? null}
187
194
  items={options.map((option) => ({ value: option.id, label: option.label }))}
@@ -204,7 +211,7 @@ export function ButtonRenderer({ node, children, context }: UiRendererProps<UiPr
204
211
  const label = uiNodeLabel(node);
205
212
  const [measure, truncated] = useTruncated<HTMLSpanElement>();
206
213
  return (
207
- <div className={clsx('px-2', !node.complete && PENDING)}>
214
+ <div className={clsx(!node.complete && PENDING)}>
208
215
  <Tooltip label={label} disabled={!truncated}>
209
216
  <Button variant="secondary" size="sm" className="max-w-full" disabled={!enabled} onClick={() => node.onAction?.()}>
210
217
  <span ref={measure} className="min-w-0 truncate">
@@ -4,12 +4,15 @@ import { useTranslation } from 'react-i18next';
4
4
  import { FileDiff, GitCommitHorizontal, SquareDashed } from 'lucide-react';
5
5
  import type { UiProps } from '@adecore/intelligent-ui';
6
6
  import { FileIcon, Icon, Tooltip } from '@adecore/ui';
7
- import { CHIP_IN_MESSAGE, MENTION_TONE } from '../../chips';
7
+ import { CHIP_IN_MESSAGE } from '../../chips';
8
8
  import { uiReasonText } from '../node-text';
9
9
  import type { UiLink, UiLinkTarget, UiRenderContext, UiRendererProps } from '../render-context';
10
10
 
11
11
  const PLAIN: UiLink = { state: 'plain' };
12
12
 
13
+ /* A link in a block is a quiet card on its tile, unlike a mention in a message, which wears the accent. */
14
+ const UI_CHIP = 'bg-ui-inset font-mono text-text shadow-[inset_0_0_0_1px_var(--border)]';
15
+
13
16
  function baseName(path: string): string {
14
17
  const trimmed = path.replace(/\/+$/, '');
15
18
  return trimmed.slice(trimmed.lastIndexOf('/') + 1) || path;
@@ -96,11 +99,11 @@ export function UiLinkChip({ target, context }: { target: UiLinkTarget; context:
96
99
  const { openLink } = context;
97
100
  const chip =
98
101
  openLink === undefined ? (
99
- <span className={clsx(CHIP_IN_MESSAGE, MENTION_TONE, 'transition-opacity duration-120 starting:opacity-0')}>{face}</span>
102
+ <span className={clsx(CHIP_IN_MESSAGE, UI_CHIP, 'transition-opacity duration-120 starting:opacity-0')}>{face}</span>
100
103
  ) : (
101
104
  <button
102
105
  type="button"
103
- className={clsx(CHIP_IN_MESSAGE, MENTION_TONE, 'transition-opacity duration-120 hover:brightness-95 starting:opacity-0')}
106
+ className={clsx(CHIP_IN_MESSAGE, UI_CHIP, 'transition-opacity duration-120 hover:bg-surface-hover starting:opacity-0')}
104
107
  onClick={() => openLink(target)}
105
108
  >
106
109
  {face}
@@ -115,7 +118,7 @@ function LinkRow({ target, context, described, children }: { target: UiLinkTarge
115
118
  return <UiLinkChip target={target} context={context} />;
116
119
  }
117
120
  return (
118
- <div className="flex min-w-0 flex-wrap items-baseline gap-x-2 px-2 text-sm">
121
+ <div className="flex min-w-0 flex-wrap items-baseline gap-x-2 text-xs">
119
122
  <span className="max-w-full min-w-0">
120
123
  <UiLinkChip target={target} context={context} />
121
124
  </span>
@@ -34,8 +34,8 @@ function NumberChip({ number, selected }: { number: number; selected: boolean })
34
34
  <span
35
35
  aria-hidden
36
36
  className={clsx(
37
- 'mt-px flex size-5 shrink-0 items-center justify-center rounded-full text-xs tabular-nums',
38
- selected ? 'bg-accent text-accent-text' : 'bg-map-marker text-map-marker-text'
37
+ 'mt-px flex size-5 shrink-0 items-center justify-center rounded-md text-2xs font-semibold tabular-nums',
38
+ selected ? 'bg-accent text-accent-text' : 'bg-ui-inset text-text'
39
39
  )}
40
40
  >
41
41
  {number}
@@ -50,7 +50,7 @@ function PlaceRow({ listed, number, selected, onPick }: { listed: ListedPlace; n
50
50
  <>
51
51
  <NumberChip number={number} selected={selected} />
52
52
  <span className="flex min-w-0 grow flex-col gap-0.5">
53
- <span className="text-sm wrap-break-word text-text">{listed.name}</span>
53
+ <span className="text-xs font-medium wrap-break-word text-text">{listed.name}</span>
54
54
  {detail !== undefined && detail !== '' && <span className="text-xs wrap-break-word text-text-muted">{detail}</span>}
55
55
  {address !== undefined && address !== '' && <span className="text-xs wrap-break-word text-text-faint">{address}</span>}
56
56
  {listed.point === undefined && <span className="text-xs text-text-faint">{t('blocks.map.lookingUp')}</span>}
@@ -58,13 +58,18 @@ function PlaceRow({ listed, number, selected, onPick }: { listed: ListedPlace; n
58
58
  </span>
59
59
  </>
60
60
  );
61
- const row = 'flex w-full min-h-8 items-start gap-2 rounded-md px-2 py-1.5 text-left';
61
+ 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';
62
62
  return (
63
63
  <li>
64
64
  {onPick === null ? (
65
- <div className={clsx(row, selected && 'bg-surface-hover')}>{body}</div>
65
+ <div className={clsx(row, selected && 'ring-2 ring-accent ring-inset')}>{body}</div>
66
66
  ) : (
67
- <button type="button" aria-pressed={selected} className={clsx(row, 'hover:bg-surface-hover', selected && 'bg-surface-hover')} onClick={onPick}>
67
+ <button
68
+ type="button"
69
+ aria-pressed={selected}
70
+ className={clsx(row, 'hover:bg-surface-active', selected && 'ring-2 ring-accent ring-inset')}
71
+ onClick={onPick}
72
+ >
68
73
  {body}
69
74
  </button>
70
75
  )}
@@ -150,7 +155,7 @@ function MapFrame({ markers, source, selected, onPick }: FrameProps) {
150
155
  }
151
156
  return (
152
157
  <>
153
- <div ref={setElement} className="relative h-50 overflow-hidden rounded-lg border border-border bg-surface-sunken @min-[420px]:h-60">
158
+ <div ref={setElement} className="relative h-50 overflow-hidden rounded-xl bg-ui-tile @min-[420px]:h-60">
154
159
  {body}
155
160
  {waiting && (
156
161
  <div className="pointer-events-none absolute inset-0 grid place-items-center">
@@ -200,9 +205,9 @@ export function MapRenderer({ node, children, context }: UiRendererProps<UiProps
200
205
  <MapFrame markers={markers} source={null} selected={selectedId} onPick={onPick} />
201
206
  );
202
207
  return (
203
- <div className={clsx('@container flex flex-col gap-1.5 px-2', !node.complete && PENDING)}>
208
+ <div className={clsx('@container flex flex-col gap-1.5', !node.complete && PENDING)}>
204
209
  {frame}
205
- <ol aria-label={t('blocks.map.label', { count: places.length })} className="flex flex-col">
210
+ <ol aria-label={t('blocks.map.label', { count: places.length })} className="grid gap-1.5 @min-[420px]:grid-cols-2">
206
211
  {places.map((listed, index) => {
207
212
  const { value } = listed.node.props;
208
213
  return (
@@ -216,9 +221,7 @@ export function MapRenderer({ node, children, context }: UiRendererProps<UiProps
216
221
  );
217
222
  })}
218
223
  </ol>
219
- {node.omitted !== undefined && node.omitted > 0 && (
220
- <p className="px-2 text-xs text-text-faint">{t('blocks.map.omitted', { count: node.omitted })}</p>
221
- )}
224
+ {node.omitted !== undefined && node.omitted > 0 && <p className="text-2xs text-text-faint">{t('blocks.map.omitted', { count: node.omitted })}</p>}
222
225
  {/* A place the catalog refused draws as its text, which is all its node renders. */}
223
226
  <div className="chat-ui-nodes flex flex-col gap-1.5 empty:hidden">{children}</div>
224
227
  </div>
@@ -22,7 +22,7 @@ export function TabsRenderer({ node, children, context }: UiRendererProps<UiProp
22
22
  const value = picked !== null && tabs.some((tab) => tab.id === picked) ? picked : (tabs[0]?.id ?? null);
23
23
  return (
24
24
  <Tabs.Root value={value} onValueChange={(next) => setPicked(String(next))} className="flex flex-col gap-3">
25
- <Tabs.List className="px-2">
25
+ <Tabs.List variant="pill" overflow="scroll" className="w-fit max-w-full rounded-xl bg-ui-tile p-0.75">
26
26
  {tabs.map((tab) => (
27
27
  <Tabs.Tab key={tab.id} value={tab.id} data-ui-reveal={tab.id}>
28
28
  {tab.props.title}
@@ -46,24 +46,24 @@ export function SectionsRenderer({ node, children, context }: UiRendererProps<Ui
46
46
  const [touched, setTouched] = useBlockLocal(context, node.id, () => false);
47
47
  return (
48
48
  <SectionsContext value={{ touched, touch: () => setTouched(true) }}>
49
- <div className="chat-ui-nodes flex flex-col divide-y divide-border-soft">{children}</div>
49
+ <div className="chat-ui-nodes flex flex-col gap-1.5">{children}</div>
50
50
  </SectionsContext>
51
51
  );
52
52
  }
53
53
 
54
- /* Arrives open until the person folds a section of the set themselves; the content starts under the title. */
54
+ /* Arrives open until the person folds a section of the set themselves; the content keeps one margin on both sides. */
55
55
  export function SectionRenderer({ node, children, context }: UiRendererProps<UiProps<'Section'>>) {
56
56
  const fold = useContext(SectionsContext);
57
57
  const [open, setOpen] = useBlockLocal(context, node.id, () => !fold.touched);
58
58
  const contentId = useId();
59
59
  return (
60
- <section>
60
+ <section className="rounded-xl bg-ui-tile">
61
61
  <button
62
62
  type="button"
63
63
  data-ui-reveal={node.id}
64
64
  aria-expanded={open}
65
65
  aria-controls={contentId}
66
- 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"
66
+ 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"
67
67
  onClick={() => {
68
68
  fold.touch();
69
69
  setOpen(!open);
@@ -76,7 +76,7 @@ export function SectionRenderer({ node, children, context }: UiRendererProps<UiP
76
76
  />
77
77
  <span className="min-w-0 truncate">{node.props.title}</span>
78
78
  </button>
79
- <div id={contentId} hidden={!open} className="chat-ui-nodes flex flex-col gap-3 pt-1 pr-2 pb-2 pl-7.5">
79
+ <div id={contentId} hidden={!open} className="chat-ui-section-body chat-ui-nodes flex flex-col gap-3 px-3 pb-3">
80
80
  {children}
81
81
  </div>
82
82
  </section>