@keboola/design 11.5.0 → 11.6.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.
package/AGENTS.md CHANGED
@@ -112,12 +112,13 @@ This file answers **which** component to use; [`COMPONENT-API.md`](./COMPONENT-A
112
112
 
113
113
  ### Loading
114
114
 
115
- | Use | When |
116
- | ------------------ | ----------------------------------------------------------------------------------------------- |
117
- | `Skeleton` | Layout-aware placeholder shown in the slot the real content will occupy (initial load, refetch) |
118
- | `SkeletonList` | Pre-composed list shell (icon + row, repeated) for Table / Card-list pages |
119
- | `Button isLoading` | In-flight action on a known button — swaps the icon for a spinner and disables the button |
120
- | `DotsLoader` | Indeterminate background work without a layout slot (small inline progress signal) |
115
+ | Use | When |
116
+ | ------------------- | -------------------------------------------------------------------------------------------------- |
117
+ | `Skeleton` | Layout-aware placeholder shown in the slot the real content will occupy (initial load, refetch) |
118
+ | `SkeletonList` | Pre-composed list shell (icon + row, repeated) for Table / Card-list pages |
119
+ | `Button isLoading` | In-flight action on a known button — swaps the icon for a spinner and disables the button |
120
+ | `DotsLoader` | Indeterminate background work without a layout slot (small inline progress signal) |
121
+ | `ThinkingIndicator` | An agent turn that is in flight with nothing to show yet — every Keboola chat says it the same way |
121
122
 
122
123
  **Never** build a new "Loader" / "BoxLoader" / "ActionLoader" wrapper component. Compose `Skeleton` directly into the slot the real content will occupy.
123
124
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @keboola/design
2
2
 
3
+ ## 11.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `MarkdownTable` ships from `AIKit`: a table in an answer with the fact that it is cut off on the right said out loud. It draws a rail above the grid for the host's own actions, a fade at the cut edge that clears at the end of the scroll, and — once the table is past 1.5× the width it has — an `Open wider` action. Where wider leads is the host's: the action is a prop (`onExpand`), so a docked panel can hand it its own fullscreen switch and a standalone app a modal, and a host with nowhere wider to go omits it and keeps the fade. `surface` names the token the host paints behind the table (`card`, the default, or `background`), which is the colour the fade ends in — naming the wrong one draws a contrasting strip over the cells instead of hiding them. The table passed in has to keep its intrinsic width (`tw:min-w-max`): `Table` is `w-full` with auto layout, so a grid left to wrap into its column never overflows and neither affordance can appear.
8
+
9
+ `FullScreenModal` now moves focus into itself when it opens and returns it on close. Its `onEscapeKeyDown` is a `keydown` on the modal element, so until focus was inside, Escape reached nothing and keyboard readers went on walking the page behind. Focus is left alone where the modal's own content has already claimed it, and Escape is now answered only by the modal it was pressed in — a nested modal is portalled beside its parent but React routes its events up the logical tree, so one keypress used to dismiss both. Which modal that is comes off the DOM, where the nesting is not a lie, rather than from `stopPropagation`: a synthetic `stopPropagation` also stops the native event, and React mounts a portal's listeners on `document.body`, so it would cut off every `document`-level `keydown` handler on the page. This is not yet a focus trap: Tab still leaves for the obscured page.
10
+
11
+ - `Progress` gains `warning` and `error` variants, on the same ramps `Badge` maps `orange` and `red` to, so a meter can carry the state its label already names
12
+
13
+ ### Patch Changes
14
+
15
+ - `ThinkingIndicator` now announces itself as a live region, so a screen reader hears that an agent's turn is in flight rather than only seeing the spinner.
16
+
17
+ - Retry a failed diagram chunk instead of rendering source for the rest of the session
18
+
19
+ `Markdown` loaded `MermaidDiagram` and `FlintChart` through `lazy()`, which memoizes the settled
20
+ import for the life of the page. One failed fetch — a dev server restarting, a stale tab after a
21
+ deploy, a flaky network — left every later `mermaid` and `flint` fence in that tab rendering as
22
+ raw source until a full reload. The import is now attempted per mount and the failed promise is
23
+ cleared, and a block that fell back to source carries a Try again button — so one bad fetch costs
24
+ that one block rather than the session.
25
+
26
+ - `formatPercentage` prints whole numbers from 1% up, keeping one decimal only below 1%
27
+
3
28
  ## 11.5.0
4
29
 
5
30
  ### Minor Changes
package/COMPONENT-API.md CHANGED
@@ -637,11 +637,11 @@ Source: `src/components/Progress/Progress.tsx`
637
637
 
638
638
  > Single-value determinate progress bar.
639
639
 
640
- | Prop | Type | Required | Default | Description |
641
- | --------- | -------------------------------- | -------- | ------- | ----------- |
642
- | `asChild` | `boolean` | no | — | |
643
- | `size` | `"sm" \| "lg" \| null` | no | — | |
644
- | `variant` | `"success" \| "default" \| null` | no | — | |
640
+ | Prop | Type | Required | Default | Description |
641
+ | --------- | -------------------------------------------------------- | -------- | ------- | ----------- |
642
+ | `asChild` | `boolean` | no | — | |
643
+ | `size` | `"sm" \| "lg" \| null` | no | — | |
644
+ | `variant` | `"warning" \| "error" \| "success" \| "default" \| null` | no | — | |
645
645
 
646
646
  ### `SpotlightBackdrop`
647
647
 
@@ -4,7 +4,7 @@ import { AlertCircle, ChevronDown, ChevronLeft, ChevronRight, CircleCheck, Circl
4
4
  import { Button } from "./Button-DsRrU5F1.js";
5
5
  import { ButtonGroup } from "./internal/components/ButtonGroup.js";
6
6
  import { ClipboardButton } from "./ClipboardButton-CwZJdRaz.js";
7
- import { FullScreenModal, FullScreenModalBody, FullScreenModalHeader } from "./FullScreenModal-BvPuYibe.js";
7
+ import { FullScreenModal, FullScreenModalBody, FullScreenModalHeader } from "./FullScreenModal-CN9nAs72.js";
8
8
  import { Truncated } from "./internal/components/Truncated.js";
9
9
  import { HoverCard } from "./HoverCard-B9ZMJxjn.js";
10
10
  import { Command } from "./Command-Bfa1kpbt.js";
@@ -14,9 +14,10 @@ import { Badge } from "./Badge-D0pXdekt.js";
14
14
  import { Textarea } from "./internal/blocks/Textarea.js";
15
15
  import { Table } from "./Table-CvYuTuNU.js";
16
16
  import { DiagramSkeleton } from "./internal/blocks/DiagramShared/DiagramSkeleton.js";
17
+ import { DiagramSource } from "./DiagramSource-C5_d8Gdj.js";
17
18
  import { Progress } from "./internal/components/Progress.js";
18
19
  import { Slot } from "@radix-ui/react-slot";
19
- import { Suspense, isValidElement, lazy, useCallback, useEffect, useId, useRef, useState } from "react";
20
+ import { isValidElement, useCallback, useEffect, useId, useRef, useState } from "react";
20
21
  import { cva } from "class-variance-authority";
21
22
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
22
23
  import TextareaAutosize from "react-textarea-autosize";
@@ -188,15 +189,84 @@ const AnswerSummary = ({ items, variant = "success", noAnswerLabel = "No answer"
188
189
  });
189
190
  };
190
191
  //#endregion
192
+ //#region src/blocks/AIKit/DiagramBlock.tsx
193
+ /**
194
+ * One diagram block: a skeleton while its chunk loads, the fence source plus a retry if it fails.
195
+ *
196
+ * - `load` runs once per mount and once per press of the retry button. The loader drops its own
197
+ * rejected promise, so every attempt is a fresh fetch.
198
+ * - Recovery is a gesture, not a timer: nothing on the page can tell when a chunk became
199
+ * reachable again, and a tab left open across a deploy points at a URL that is gone for good —
200
+ * a background loop would retry that one forever.
201
+ * - The failure is already logged by the loader, which knows the chunk's name.
202
+ * - A new `load` resets the block: every fence renders this same component type, so a block that
203
+ * switches dialect would otherwise keep drawing through the dialect it had before.
204
+ * - Both the renderer and the loader are functions held in state, so they go in through the
205
+ * setter form — React runs a bare function as a lazy initializer or an updater instead.
206
+ */
207
+ const DiagramBlock = ({ load, code }) => {
208
+ const [Diagram, setDiagram] = useState(null);
209
+ const [failed, setFailed] = useState(false);
210
+ const [attempt, setAttempt] = useState(0);
211
+ const [prevLoad, setPrevLoad] = useState(() => load);
212
+ if (load !== prevLoad) {
213
+ setPrevLoad(() => load);
214
+ setDiagram(null);
215
+ setFailed(false);
216
+ }
217
+ useEffect(() => {
218
+ let cancelled = false;
219
+ load().then((component) => {
220
+ if (!cancelled) setDiagram(() => component);
221
+ }, () => {
222
+ if (!cancelled) setFailed(true);
223
+ });
224
+ return () => {
225
+ cancelled = true;
226
+ };
227
+ }, [load, attempt]);
228
+ if (Diagram) return /* @__PURE__ */ jsx(Diagram, { code });
229
+ if (failed) return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(DiagramSource, { code }), /* @__PURE__ */ jsx(Button, {
230
+ variant: "inline",
231
+ size: "small",
232
+ className: "tw:mb-3",
233
+ onClick: () => {
234
+ setFailed(false);
235
+ setAttempt((count) => count + 1);
236
+ },
237
+ children: "Try again"
238
+ })] });
239
+ return /* @__PURE__ */ jsx(DiagramSkeleton, {});
240
+ };
241
+ //#endregion
242
+ //#region src/blocks/AIKit/diagramChunk.ts
243
+ /**
244
+ * An on-demand diagram chunk — shared between mounts, and retryable.
245
+ *
246
+ * - `lazy()` is deliberately not used here: it memoizes the settled import for the life of the
247
+ * page, so one failed fetch (a dev server restarting, a stale tab after a deploy, a flaky
248
+ * network) leaves every diagram in that tab rendering raw source until a full reload.
249
+ * - A resolved import is kept, so the chunk is fetched once however many blocks mount.
250
+ * - A failed one is cleared, so the next mount imports again. Same contract as
251
+ * `getFlintRuntime()` one level down, inside `FlintChart`.
252
+ * - The failure is logged here, where the chunk's name is known, so the swap to plain source is
253
+ * never silent and a retry that fails again says so.
254
+ */
255
+ const diagramChunk = (name, load) => {
256
+ let pending = null;
257
+ return () => {
258
+ pending ??= load().catch((error) => {
259
+ console.error(`[AIKit] failed to load the ${name} chunk`, error);
260
+ pending = null;
261
+ throw error;
262
+ });
263
+ return pending;
264
+ };
265
+ };
266
+ //#endregion
191
267
  //#region src/blocks/AIKit/Markdown.tsx
192
- const MermaidDiagram = lazy(() => import("@keboola/design/mermaid").then((module) => ({ default: module.MermaidDiagram }), (error) => {
193
- console.error("[AIKit] failed to load the MermaidDiagram chunk", error);
194
- return { default: ({ code }) => /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: code }) }) };
195
- }));
196
- const FlintChart = lazy(() => import("@keboola/design/flint").then((module) => ({ default: module.FlintChart }), (error) => {
197
- console.error("[AIKit] failed to load the FlintChart chunk", error);
198
- return { default: ({ code }) => /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: code }) }) };
199
- }));
268
+ const loadMermaid = diagramChunk("MermaidDiagram", () => import("@keboola/design/mermaid").then((module) => module.MermaidDiagram));
269
+ const loadFlint = diagramChunk("FlintChart", () => import("@keboola/design/flint").then((module) => module.FlintChart));
200
270
  const proseClass = "tw:prose tw:prose-sm tw:max-w-none tw:break-words tw:[&_pre]:bg-muted tw:[&_pre]:text-foreground tw:[&_:not(pre)>code]:bg-muted tw:[&_:not(pre)>code]:text-foreground tw:[&_:not(pre)>code]:rounded tw:[&_:not(pre)>code]:px-1 tw:[&_:not(pre)>code]:py-0.5 tw:[&_:not(pre)>code]:before:content-none tw:[&_:not(pre)>code]:after:content-none";
201
271
  const getText = (node) => {
202
272
  if (typeof node === "string") return node;
@@ -221,7 +291,7 @@ const getLanguage = (element) => {
221
291
  */
222
292
  const baseRemarkPlugins = [remarkGfm];
223
293
  /**
224
- * Fence language → the renderer that draws it.
294
+ * Fence language → the on-demand chunk that draws it.
225
295
  *
226
296
  * - Both dialects are the agent's, not this package's: kai-agent's prompt tells the model to
227
297
  * write ```mermaid for structure and ```flint for quantitative data, so a chat that renders
@@ -229,13 +299,13 @@ const baseRemarkPlugins = [remarkGfm];
229
299
  * - `flint-chart` is an alias — it is the fence tag flint's own docs use, so a model emits it
230
300
  * unprompted often enough to matter. `markdownComponents.tsx` in kbc-ui carries the same pair.
231
301
  * - Read through `Object.hasOwn`, never by plain index: a fence tagged ```constructor or
232
- * ```valueOf otherwise answers with an `Object.prototype` member, which is truthy, and mounting
233
- * it as a component throws — taking the whole message subtree down with it.
302
+ * ```valueOf otherwise answers with an `Object.prototype` member, which is truthy, and calling
303
+ * it as a loader throws — taking the whole message subtree down with it.
234
304
  */
235
- const DIAGRAM_RENDERERS = {
236
- mermaid: MermaidDiagram,
237
- flint: FlintChart,
238
- "flint-chart": FlintChart
305
+ const DIAGRAM_CHUNKS = {
306
+ mermaid: loadMermaid,
307
+ flint: loadFlint,
308
+ "flint-chart": loadFlint
239
309
  };
240
310
  /**
241
311
  * The fence languages this renderer draws instead of printing.
@@ -247,18 +317,18 @@ const DIAGRAM_RENDERERS = {
247
317
  * - Frozen, because it is that shared contract: a consumer that pushed onto it would change how
248
318
  * every other app in the same bundle holds a fence back.
249
319
  */
250
- const DIAGRAM_LANGUAGES = Object.freeze(Object.keys(DIAGRAM_RENDERERS));
320
+ const DIAGRAM_LANGUAGES = Object.freeze(Object.keys(DIAGRAM_CHUNKS));
251
321
  const baseComponents = {
252
322
  pre({ children, ...props }) {
253
323
  const child = Array.isArray(children) ? children[0] : children;
254
324
  const code = getText(child).replace(/\n$/, "");
255
325
  const language = getLanguage(child);
256
- const Diagram = language !== void 0 && Object.hasOwn(DIAGRAM_RENDERERS, language) ? DIAGRAM_RENDERERS[language] : void 0;
257
- if (Diagram) {
326
+ const load = language !== void 0 && Object.hasOwn(DIAGRAM_CHUNKS, language) ? DIAGRAM_CHUNKS[language] : void 0;
327
+ if (load) {
258
328
  if (!code.trim()) return null;
259
- return /* @__PURE__ */ jsx(Suspense, {
260
- fallback: /* @__PURE__ */ jsx(DiagramSkeleton, {}),
261
- children: /* @__PURE__ */ jsx(Diagram, { code })
329
+ return /* @__PURE__ */ jsx(DiagramBlock, {
330
+ load,
331
+ code
262
332
  });
263
333
  }
264
334
  const { node: _node, ...rest } = props;
@@ -343,6 +413,95 @@ const Markdown = ({ children, className, remarkPlugins, components: componentOve
343
413
  });
344
414
  };
345
415
  //#endregion
416
+ //#region src/blocks/AIKit/MarkdownTable.tsx
417
+ /** How much wider than its column a table has to be before the column is the wrong surface. */
418
+ const EXPAND_RATIO = 1.5;
419
+ /**
420
+ * The token the host paints the table on, which is the colour the fade has to end in.
421
+ *
422
+ * - A gradient hides the cut-off cells by ending in the surface behind them. End it in any other
423
+ * token and it draws a contrasting strip over the data instead of hiding it.
424
+ * - Full class strings, not a composed `tw:from-${token}`: Tailwind reads literals.
425
+ */
426
+ const FADE_SURFACE = {
427
+ card: "tw:from-card",
428
+ background: "tw:from-background"
429
+ };
430
+ /**
431
+ * A table in an answer, with the fact that it is cut off on the right said out loud.
432
+ *
433
+ * - `overflow-x: auto` scrolls silently, so nothing on screen says there is more to the right.
434
+ * - The controls sit in a rail above the grid, never over the header row or the last row — the
435
+ * data a reader reaches for them to get at. The rail holds its height, so revealing hover
436
+ * controls or crossing the ratio cannot shift the table under the pointer.
437
+ * - The fade marks the cut edge only while content remains, so it never reads as a border.
438
+ * - A painted gradient rather than the `tw:scroll-fade-x` mask, which needs scroll-driven
439
+ * animations: the build floor is `baseline-widely-available` (Safari 16, Firefox 104), and there
440
+ * the utility either fades both edges of a table that fits or, behind the `supports-` guard
441
+ * `PageLayout` uses, says nothing at all — which is the state this component exists to end. The
442
+ * mask also has to sit on the scrolling element, and that is `Table`'s own container, not ours.
443
+ * - Past `EXPAND_RATIO` the fade is joined by `onExpand`'s action, drawn without waiting for a
444
+ * hover: it reports that content is cut off, which the reader needs before going looking.
445
+ */
446
+ const MarkdownTable = ({ children, actions, onExpand, surface = "card", className }) => {
447
+ const frameRef = useRef(null);
448
+ const [overflow, setOverflow] = useState({
449
+ scrollable: false,
450
+ atEnd: true,
451
+ ratio: 1
452
+ });
453
+ const measure = useCallback(() => {
454
+ const box = frameRef.current?.firstElementChild;
455
+ if (!box) return;
456
+ const { scrollLeft, scrollWidth, clientWidth } = box;
457
+ const hidden = scrollWidth - clientWidth;
458
+ setOverflow({
459
+ scrollable: hidden > 1,
460
+ atEnd: scrollLeft >= hidden - 1,
461
+ ratio: clientWidth > 0 ? scrollWidth / clientWidth : 1
462
+ });
463
+ }, []);
464
+ useEffect(() => {
465
+ const box = frameRef.current?.firstElementChild;
466
+ if (!box) return;
467
+ measure();
468
+ box.addEventListener("scroll", measure, { passive: true });
469
+ const observer = new ResizeObserver(measure);
470
+ observer.observe(box);
471
+ if (box.firstElementChild) observer.observe(box.firstElementChild);
472
+ return () => {
473
+ box.removeEventListener("scroll", measure);
474
+ observer.disconnect();
475
+ };
476
+ }, [measure]);
477
+ const showExpand = !!onExpand && overflow.scrollable && overflow.ratio >= EXPAND_RATIO;
478
+ return /* @__PURE__ */ jsxs("div", {
479
+ className,
480
+ children: [(actions || onExpand) && /* @__PURE__ */ jsxs("div", {
481
+ className: "tw:flex tw:h-8 tw:items-center tw:justify-end tw:gap-1",
482
+ children: [actions, showExpand && /* @__PURE__ */ jsx(Tooltip, {
483
+ tooltip: "This table is wider than the space it has",
484
+ placement: "top",
485
+ triggerAsChild: true,
486
+ children: /* @__PURE__ */ jsxs(Button, {
487
+ size: "small",
488
+ variant: "invisible",
489
+ onClick: onExpand,
490
+ children: [/* @__PURE__ */ jsx(Maximize2, { className: "tw:size-3.5" }), "Open wider"]
491
+ })
492
+ })]
493
+ }), /* @__PURE__ */ jsxs("div", {
494
+ ref: frameRef,
495
+ className: "tw:relative",
496
+ children: [children, overflow.scrollable && !overflow.atEnd && /* @__PURE__ */ jsx("span", {
497
+ "aria-hidden": true,
498
+ "data-testid": "markdown-table-fade",
499
+ className: cn("tw:pointer-events-none tw:absolute tw:inset-y-px tw:right-px tw:w-12 tw:rounded-r-[10px] tw:bg-gradient-to-l tw:to-transparent", FADE_SURFACE[surface])
500
+ })]
501
+ })]
502
+ });
503
+ };
504
+ //#endregion
346
505
  //#region src/blocks/AIKit/MessageBubble.tsx
347
506
  const containerClasses = cva([
348
507
  "tw:flex",
@@ -1248,11 +1407,24 @@ const SuggestedPrompt = ({ label, disabled, onClick, icon, className }) => {
1248
1407
  };
1249
1408
  //#endregion
1250
1409
  //#region src/blocks/AIKit/ThinkingIndicator.tsx
1410
+ /**
1411
+ * The agent's turn is in flight and has nothing to show yet.
1412
+ *
1413
+ * - Quiet chrome, matching the step rows it precedes (muted ink, no italic), so the hand-off
1414
+ * from "Thinking..." to the first step is seamless.
1415
+ * - `role="status"` because a spinner is silence to a screen reader, and for much of a turn
1416
+ * this is all a transcript shows. The label is the announcement, so it is never decorative.
1417
+ * - The icon is hidden from the tree: it says the same thing as the label beside it.
1418
+ */
1251
1419
  const ThinkingIndicator = ({ label = "Thinking..." }) => {
1252
1420
  return /* @__PURE__ */ jsxs("div", {
1253
1421
  className: "tw:flex tw:items-center tw:gap-1.5",
1422
+ role: "status",
1254
1423
  "data-testid": "chat-thinking-indicator",
1255
- children: [/* @__PURE__ */ jsx(Loader2, { className: "tw:size-3.5 tw:animate-spin tw:text-muted-foreground" }), /* @__PURE__ */ jsx("span", {
1424
+ children: [/* @__PURE__ */ jsx(Loader2, {
1425
+ "aria-hidden": true,
1426
+ className: "tw:size-3.5 tw:animate-spin tw:text-muted-foreground"
1427
+ }), /* @__PURE__ */ jsx("span", {
1256
1428
  className: "tw:text-xs tw:font-medium tw:text-muted-foreground",
1257
1429
  children: label
1258
1430
  })]
@@ -1470,16 +1642,18 @@ const ToolCallStatusHeader = ({ icon: IconComponent, iconSpin, status, text, nee
1470
1642
  */
1471
1643
  const clampPercentage = (value) => Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : 0;
1472
1644
  /**
1473
- * A percentage as text — `72%`, `1.1%`, `99.9%`.
1645
+ * A percentage as text — `72%`, `0.2%`, `99%`.
1474
1646
  *
1475
- * - One decimal place, trimmed to a whole number, so `11/1000` reads as `1.1%` rather than `1%`.
1476
- * - Floored, not rounded: rounding `99.96` up to `100%` disagrees with the at-limit state computed
1647
+ * - Whole numbers from 1% up; a decimal only below 1%, where rounding would read as `0%`.
1648
+ * - A non-zero share never prints as `0.0%` — it floors at `0.1%`.
1649
+ * - Never `100%` for a value under 100: the text would disagree with the at-limit state computed
1477
1650
  * from the raw value, exactly at the boundary everything else turns on.
1478
- * - The `1e-9` nudge is for floating-point noise — `11 / 1000 * 100` is `1.0999999999999999`.
1479
1651
  */
1480
1652
  const formatPercentage = (value) => {
1481
- const floored = Math.floor(value * 10 + 1e-9) / 10;
1482
- return `${Number.isInteger(floored) ? floored.toFixed(0) : floored.toFixed(1)}%`;
1653
+ const oneDecimal = Math.round(value * 10) / 10;
1654
+ if (value > 0 && oneDecimal < 1) return `${Math.max(.1, oneDecimal).toFixed(1)}%`;
1655
+ const rounded = Math.round(value);
1656
+ return `${value < 100 ? Math.min(rounded, 99) : rounded}%`;
1483
1657
  };
1484
1658
  //#endregion
1485
1659
  //#region src/blocks/AIKit/UsageStatsRing.tsx
@@ -1541,6 +1715,6 @@ const UsageStats = ({ value, alert = false, children, className, ariaLabel, alig
1541
1715
  });
1542
1716
  };
1543
1717
  //#endregion
1544
- export { AnswerActions, AnswerSummary, CollapsibleStepHeader, DIAGRAM_LANGUAGES, InlineStepDetail, InlineStepLabel, Markdown, MessageBubble, PlanApproval, PlanRewritingIndicator, PromptInput, Question, QuestionBlock, SuggestedPrompt, ThinkingIndicator, ToolCallApproval, ToolCallChip, ToolCallStatusHeader, UsageStats, UsageStatsRing, clampPercentage, formatPercentage };
1718
+ export { AnswerActions, AnswerSummary, CollapsibleStepHeader, DIAGRAM_LANGUAGES, InlineStepDetail, InlineStepLabel, Markdown, MarkdownTable, MessageBubble, PlanApproval, PlanRewritingIndicator, PromptInput, Question, QuestionBlock, SuggestedPrompt, ThinkingIndicator, ToolCallApproval, ToolCallChip, ToolCallStatusHeader, UsageStats, UsageStatsRing, clampPercentage, formatPercentage };
1545
1719
 
1546
- //# sourceMappingURL=AIKit-CNX6aiKm.js.map
1720
+ //# sourceMappingURL=AIKit-BExvt9pt.js.map