@keboola/design 13.0.1 → 13.1.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 (39) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +2 -2
  4. package/dist/{AIKit-VB7CXeQk.js → AIKit-BVCWP9Ks.js} +261 -69
  5. package/dist/AIKit-BVCWP9Ks.js.map +1 -0
  6. package/dist/{CodeEditor-jZMw1ioj.js → CodeEditor-Df7VVx08.js} +2 -2
  7. package/dist/{CodeEditor-jZMw1ioj.js.map → CodeEditor-Df7VVx08.js.map} +1 -1
  8. package/dist/{ColorModeSwitch-BBzn1D0u.js → ColorModeSwitch-D8Cki137.js} +2 -2
  9. package/dist/{ColorModeSwitch-BBzn1D0u.js.map → ColorModeSwitch-D8Cki137.js.map} +1 -1
  10. package/dist/{DropdownMenu-C5FLPkvR.js → DropdownMenu-BJGLjpVc.js} +3 -3
  11. package/dist/{DropdownMenu-C5FLPkvR.js.map → DropdownMenu-BJGLjpVc.js.map} +1 -1
  12. package/dist/{SectionNav-BTBiitiQ.js → SectionNav-I2p2uXTd.js} +2 -2
  13. package/dist/{SectionNav-BTBiitiQ.js.map → SectionNav-I2p2uXTd.js.map} +1 -1
  14. package/dist/{Sidebar-BTv636Cq.js → Sidebar-D9ytIs8V.js} +2 -2
  15. package/dist/{Sidebar-BTv636Cq.js.map → Sidebar-D9ytIs8V.js.map} +1 -1
  16. package/dist/Toast-NrOdDDru.js +76 -0
  17. package/dist/Toast-NrOdDDru.js.map +1 -0
  18. package/dist/editors.js +1 -1
  19. package/dist/index.d.ts +46 -12
  20. package/dist/index.js +9 -8
  21. package/dist/internal/blocks/AIKit.js +1 -1
  22. package/dist/internal/blocks/ColorModeSwitch.js +1 -1
  23. package/dist/internal/blocks/ExportMenu.js +1 -1
  24. package/dist/internal/blocks/SectionNav.js +1 -1
  25. package/dist/internal/blocks/SplitButton.js +1 -1
  26. package/dist/internal/components/DropdownMenu.js +1 -1
  27. package/dist/internal/components/Sidebar.js +1 -1
  28. package/dist/internal/components/Toast.js +1 -1
  29. package/dist/internal/hooks/useAltDigitHotkey.js +51 -0
  30. package/dist/internal/hooks/useAltDigitHotkey.js.map +1 -0
  31. package/dist/markdown-editor.d.ts +22 -1
  32. package/dist/markdown-editor.js +443 -201
  33. package/dist/markdown-editor.js.map +1 -1
  34. package/dist/rjsf.js +1 -1
  35. package/dist/styles.css +1 -1
  36. package/package.json +1 -1
  37. package/dist/AIKit-VB7CXeQk.js.map +0 -1
  38. package/dist/Toast-PfQf3vj-.js +0 -49
  39. package/dist/Toast-PfQf3vj-.js.map +0 -1
package/AGENTS.md CHANGED
@@ -99,6 +99,8 @@ This file answers **which** component to use; [`COMPONENT-API.md`](./COMPONENT-A
99
99
 
100
100
  `FileUpload` composes `FileUpload.DropZone` (the dashed box, owning the drag / error visuals) with `FileUpload.FileList` (selected files; `invalid` tints the rows). It deliberately ships **no** rejection copy — the wording is product copy, so read the current rejections with `useFileUploadRejections()` inside `FileUpload` and write the sentence yourself. Reach for `useFileDropTarget` only when there is no box to render: it returns `getRootProps()` / `isDragActive` for an affordance you style, and `noClick` / `noKeyboard` keep an already-interactive subtree (a composer, a form) from opening the file dialog on every click.
101
101
 
102
+ **A `MarkdownEditor` whose markdown another reader renders** (a reply posted to a ticket, a comment shown with `react-markdown`) takes `autoLink` and `pasteMarkdown`: without them a bare URL exports escaped (`a\_b`) and links elsewhere to the wrong address, and a plain-text paste of a fence, list or table lands one paragraph per line. Leave them off where a stored value is saved back on every `onChange`: `autoLink` exports an escaped stored URL unescaped, so it rewrites that value when it is opened.
103
+
102
104
  **`InputGroup.Addon` (`align="inline-start"` / `"inline-end"`) keeps its own content width** — it no longer gives up space to the input, so a badge or a rendered selection stays fully visible instead of being squeezed to an ellipsis. Bound long, caller-controlled content yourself (a `max-w-[…] overflow-hidden` on the addon) if it isn't a fixed-size icon or button. The exception is `TextInput`'s `rightSuffixVariant="addon"` suffix, which shrinks and ellipsizes inline content instead. `InputGroup`'s padding/sizing rules reach any descendant `input`/`textarea`, not just a direct child, so a consumer that wraps the input in its own layout (chips, an overlay) picks up that padding automatically — cancel it with `!px-0`/`!py-0` where the wrapping layout already owns its own edge padding (see `MultiCombobox`).
103
105
 
104
106
  ### Status surfaces
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @keboola/design
2
2
 
3
+ ## 13.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `Question` answers to the keyboard: every option and "Other" carry a number, a digit or Enter picks (a picked key turns into a check), ↑↓ move, ⌘/Ctrl+Enter sends, Space skips. `autoFocus` focuses the card; `enableShortcuts` adds ⌥1–9 from anywhere. New `useAltDigitHotkey` hook. Kai's question card uses both (DES-209)
8
+
9
+ - Kai's follow-ups are message-shaped bubbles (`SuggestedPrompt variant="bubble"`): ↑ from an empty composer enters them, ↑ / ↓ / Tab move, Enter sends and returns the caret (DES-215)
10
+
11
+ - `MarkdownEditor` takes two opt-in props, both off by default and ignored with `singleLine`; with both off the editor and its markdown are unchanged.
12
+ - `autoLink`: a typed, pasted or loaded bare URL (`https://`, `http://`, `www.`) or email address exports as typed, `https://example.com/a_b` instead of `https://example.com/a\_b`, so any CommonMark or GFM reader links the same address, and no `*x*` inside it turns italic. What counts as a URL and where it ends follow GFM's autolink rules (`remark-gfm`): trailing `!"'*,.:;?_~]`, an unbalanced `)` and an entity like `&amp;` stay outside. In the box it becomes a link where Lexical's autolink accepts it, with whitespace or `.,;` on both sides; one right after `(` or `<` still exports as typed but shows as text. Code blocks hold no links and inline code exports as code. A value stored with the old escaping (`https://example.com/a\_b`) loads as the same URL and exports unescaped, so the editor emits `onChange` once, on load, with the healed value.
13
+ - `pasteMarkdown`: a plain-text paste (`text/plain` with no `text/html` and no files) is read as markdown, with the same rules as `value`, and inserted at the selection, so a pasted fence, list, nested list, table or log keeps its shape instead of becoming one paragraph per line. HTML pastes, file pastes and pastes inside a code block keep the current behaviour. One undo removes the whole paste.
14
+
15
+ - `MarkdownEditor` takes `toolbarMode` and starts the caret where the placeholder is.
16
+ - `toolbarMode="fixed"` pins the formatting toolbar to the top of the editor instead of floating it over the selection, so a comment or reply box shows it takes Markdown before anything is selected. It renders in the editor rather than portalled to `document.body`, so it inherits the consumer's token scope.
17
+ - `showToolbar` still decides whether the toolbar shows at all; `toolbarMode` defaults to `floating`, so nothing changes for existing consumers.
18
+ - The placeholder now covers the editor's content box and repeats its padding instead of guessing an offset, and the editable's first and last blocks drop the `prose` margins that `prose` zeroes only for its own direct children. An empty editor's placeholder and caret line up, and non-`compact` content no longer starts 1.25em below the top padding.
19
+
20
+ - `useNotification` takes `remindAfter` (ms): a notification the user dismissed comes back after that long while it is still enabled. A visible notification follows its `title` and `description` when they are strings, and a notification without an `id` keeps one id for the component's lifetime — so unmounting before the first frame no longer dismisses every toast on screen.
21
+
22
+ ### Patch Changes
23
+
24
+ - The README's Storybook links point at `design-keboola-engineering.vercel.app`, the Storybook CI publishes on every release.
25
+
26
+ ## 13.0.2
27
+
28
+ ### Patch Changes
29
+
30
+ - `DropdownMenu.RadioItem` with `indicator="none"` tints its checked row from theme tokens — `secondary` on legacy chrome, `primary/10` on argo — so the row stays legible under the dark scheme instead of painting a light band behind light text.
31
+
32
+ - Kai composer submit button: the gradient no longer shows a 1px sliver along its edge.
33
+
3
34
  ## 13.0.1
4
35
 
5
36
  ### Patch Changes
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  The Keboola design system — shadcn-aligned, Radix-underneath, Lucide icons.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@keboola/design.svg)](https://www.npmjs.com/package/@keboola/design)
6
- [![Storybook](https://img.shields.io/badge/Storybook-live-FF4785)](https://design-keboola.vercel.app/)
6
+ [![Storybook](https://img.shields.io/badge/Storybook-live-FF4785)](https://design-keboola-engineering.vercel.app/)
7
7
 
8
- 📦 **Storybook:** https://design-keboola.vercel.app/
8
+ 📦 **Storybook:** https://design-keboola-engineering.vercel.app/
9
9
 
10
10
  📖 **Decision tree:** [`AGENTS.md`](./AGENTS.md) — which component to use when (ships in the tarball).
11
11
  🔧 **Prop signatures:** [`COMPONENT-API.md`](./COMPONENT-API.md) — generated, version-pinned (ships in the tarball).
@@ -9,13 +9,14 @@ import { Truncated } from "./internal/components/Truncated.js";
9
9
  import { HoverCard } from "./HoverCard-B8OKu9XZ.js";
10
10
  import { Command } from "./Command-BQPiZCNz.js";
11
11
  import { TextInput } from "./TextInput-BWYXbfs4.js";
12
- import { Checkbox } from "./Checkbox-8yIIgY7g.js";
13
12
  import { Badge } from "./Badge-Dz0cj6rj.js";
14
13
  import { Textarea } from "./internal/blocks/Textarea.js";
15
14
  import { Table as Table$1 } from "./Table-BaVT5Tkn.js";
16
15
  import { DiagramSkeleton } from "./internal/blocks/DiagramShared/DiagramSkeleton.js";
17
16
  import { DiagramSource } from "./DiagramSource-C5_d8Gdj.js";
17
+ import { Kbd, keys } from "./Kbd-J4n1J6i4.js";
18
18
  import { Progress } from "./internal/components/Progress.js";
19
+ import { digitFromKeyCode, useAltDigitHotkey } from "./internal/hooks/useAltDigitHotkey.js";
19
20
  import { Slot } from "@radix-ui/react-slot";
20
21
  import { createContext, isValidElement, use, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
21
22
  import { cva } from "class-variance-authority";
@@ -1444,7 +1445,7 @@ const PromptInputSubmit = ({ ref, disabled, status = "ready" }) => {
1444
1445
  size: "icon-sm",
1445
1446
  variant: "primary",
1446
1447
  isLoading: status === "submitted",
1447
- className: cn("tw:ml-auto tw:flex tw:items-center tw:rounded-full", "tw:bg-gradient-to-br tw:from-[#61E4FF] tw:via-[#1F8FFF] tw:to-[#7C5CFF]", "tw:hover:not-disabled:brightness-110"),
1448
+ className: cn("tw:ml-auto tw:flex tw:items-center tw:rounded-full", "tw:bg-gradient-to-br tw:bg-origin-border tw:from-[#61E4FF] tw:via-[#1F8FFF] tw:to-[#7C5CFF]", "tw:hover:not-disabled:brightness-110"),
1448
1449
  children: status !== "submitted" && icon
1449
1450
  }) })
1450
1451
  }), getTooltipText() && /* @__PURE__ */ jsx(Tooltip.Content, {
@@ -1740,33 +1741,153 @@ PromptInput.Action = PromptInputAction;
1740
1741
  PromptInput.MenuTrigger = PromptInputMenuTrigger;
1741
1742
  //#endregion
1742
1743
  //#region src/blocks/AIKit/Question.tsx
1744
+ const isEditableTarget = (target) => target instanceof HTMLElement && (target.isContentEditable || [
1745
+ "INPUT",
1746
+ "TEXTAREA",
1747
+ "SELECT"
1748
+ ].includes(target.tagName));
1749
+ const isFieldWithText = (element) => {
1750
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) return element.value !== "";
1751
+ return element instanceof HTMLElement && element.isContentEditable && !!element.textContent;
1752
+ };
1753
+ const hasCommandModifier = (e) => e.altKey || e.ctrlKey || e.metaKey;
1754
+ const SEND_KEY_CLASS = "tw:!bg-white/20 tw:!text-primary-foreground";
1755
+ const keyRange = (count) => count > 1 ? `1–${count}` : "1";
1756
+ /** An option's key, or a blank slot past the ninth; a picked option's turns into a check. */
1757
+ const OptionKey = ({ digit, checked }) => digit === null && !checked ? /* @__PURE__ */ jsx("span", {
1758
+ "aria-hidden": true,
1759
+ className: "tw:w-5 tw:shrink-0"
1760
+ }) : /* @__PURE__ */ jsx(Kbd, {
1761
+ "aria-hidden": true,
1762
+ className: cn("tw:-my-0.5", checked && "tw:!bg-primary tw:!text-primary-foreground"),
1763
+ "data-testid": "question-option-digit",
1764
+ children: checked ? /* @__PURE__ */ jsx(Check, { className: "tw:size-3 tw:animate-in tw:fade-in tw:zoom-in-50 tw:duration-150" }) : digit
1765
+ });
1743
1766
  const handleKeyDown = (callback) => (e) => {
1767
+ if (hasCommandModifier(e) || e.repeat) return;
1744
1768
  if (e.key === "Enter" || e.key === " ") {
1745
1769
  e.preventDefault();
1746
1770
  callback();
1747
1771
  }
1748
1772
  };
1749
- const Question = ({ question, header, options, multiSelect = true, selectedOptionIds = [], onSelectionChange, customAnswer = "", onCustomAnswerChange, onCustomAnswerConfirm, showCustomAnswer = true, customAnswerPlaceholder = "Type your answer…", showSkip = true, onSkip, onClose, onNext, submitTooltip, onPrevious, currentStep, totalSteps, renderPreview, className }) => {
1773
+ const Question = ({ question, header, options, multiSelect = true, selectedOptionIds = [], onSelectionChange, customAnswer = "", onCustomAnswerChange, onCustomAnswerConfirm, showCustomAnswer = true, customAnswerPlaceholder = "Type your answer…", showSkip = true, onSkip, onClose, onNext, submitTooltip, onPrevious, currentStep, totalSteps, renderPreview, autoFocus = false, enableShortcuts = false, className }) => {
1750
1774
  const [isCustomExpanded, setIsCustomExpanded] = useState(false);
1775
+ const [activeRow, setActiveRow] = useState(0);
1776
+ const [pendingId, setPendingId] = useState(null);
1777
+ const rootRef = useRef(null);
1778
+ const rowsRef = useRef([]);
1779
+ const customInputRef = useRef(null);
1751
1780
  const isCustomSelected = isCustomExpanded || customAnswer.length > 0;
1752
1781
  const hasSteps = currentStep != null && totalSteps != null;
1753
1782
  const showStepControls = hasSteps && totalSteps > 1;
1754
1783
  const showSkipButton = showSkip && onSkip != null;
1755
- const showSubmitButton = multiSelect || isCustomSelected;
1784
+ const chosenId = multiSelect ? null : pendingId ?? selectedOptionIds[0] ?? null;
1785
+ const showSubmitButton = multiSelect || isCustomSelected || chosenId !== null;
1756
1786
  const showFooter = showStepControls || showSkipButton || showSubmitButton;
1757
1787
  const progressValue = hasSteps ? currentStep / totalSteps * 100 : 0;
1758
1788
  const isFirstStep = currentStep === 1;
1759
1789
  const isLastStep = hasSteps && currentStep === totalSteps;
1790
+ const numberedCount = Math.min(options.length, 9);
1791
+ const otherDigit = showCustomAnswer && options.length < 9 ? options.length + 1 : null;
1792
+ const rowCount = options.length + (showCustomAnswer ? 1 : 0);
1793
+ const tabbableRow = activeRow < rowCount ? activeRow : 0;
1760
1794
  const handleOptionToggle = (optionId) => {
1761
1795
  if (multiSelect) {
1762
1796
  const nextIds = selectedOptionIds.includes(optionId) ? selectedOptionIds.filter((id) => id !== optionId) : [...selectedOptionIds, optionId];
1763
1797
  onSelectionChange?.(nextIds);
1764
1798
  } else {
1765
1799
  setIsCustomExpanded(false);
1800
+ setPendingId(null);
1766
1801
  onCustomAnswerChange?.("");
1767
1802
  onSelectionChange?.([optionId]);
1768
1803
  }
1769
1804
  };
1805
+ const handleOptionKey = (optionId) => {
1806
+ if (multiSelect) handleOptionToggle(optionId);
1807
+ else {
1808
+ setIsCustomExpanded(false);
1809
+ onCustomAnswerChange?.("");
1810
+ setPendingId(optionId);
1811
+ }
1812
+ };
1813
+ useAltDigitHotkey({
1814
+ count: numberedCount,
1815
+ onDigit: (index) => handleOptionKey(options[index].id),
1816
+ enabled: enableShortcuts,
1817
+ ref: rootRef
1818
+ });
1819
+ useEffect(() => {
1820
+ if (!autoFocus || isFieldWithText(document.activeElement)) return;
1821
+ rootRef.current?.focus({ preventScroll: true });
1822
+ }, [autoFocus]);
1823
+ const focusRow = (index) => {
1824
+ setActiveRow(index);
1825
+ rowsRef.current[index]?.focus();
1826
+ };
1827
+ /**
1828
+ * The card's own keys, never fired from a text field inside it; a held key acts once.
1829
+ *
1830
+ * 1. ↑ / ↓ move focus; a digit picks or toggles; Enter on a row selects or toggles it.
1831
+ * 2. ⌘/Ctrl+Enter sends: the selected option (single-select) or the next step; Space skips.
1832
+ * 3. Any other character starts a custom answer: focus moves to "Other", which receives it.
1833
+ * 4. A key the card takes stops here, so page shortcuts (`a`, `/`) never see it.
1834
+ */
1835
+ const handleCardKeyDown = (e) => {
1836
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !e.altKey && !e.repeat) {
1837
+ if (!showSubmitButton) return;
1838
+ send();
1839
+ e.preventDefault();
1840
+ e.stopPropagation();
1841
+ return;
1842
+ }
1843
+ if (e.key === "ArrowUp" && e.target === customInputRef.current && !hasCommandModifier(e)) {
1844
+ e.preventDefault();
1845
+ focusRow(options.length);
1846
+ return;
1847
+ }
1848
+ if (e.defaultPrevented || hasCommandModifier(e) || isEditableTarget(e.target)) return;
1849
+ if ((e.key === "ArrowDown" || e.key === "ArrowUp") && rowCount > 0) {
1850
+ e.preventDefault();
1851
+ const delta = e.key === "ArrowDown" ? 1 : -1;
1852
+ const target = e.target;
1853
+ const current = rowsRef.current.findIndex((row) => target instanceof Node && !!row?.contains(target));
1854
+ focusRow(((current === -1 ? delta > 0 ? -1 : rowCount : current) + delta + rowCount) % rowCount);
1855
+ return;
1856
+ }
1857
+ if (e.repeat) return;
1858
+ const digit = digitFromKeyCode(e.code);
1859
+ if (digit !== null && digit === otherDigit) {
1860
+ e.preventDefault();
1861
+ e.stopPropagation();
1862
+ customInputRef.current?.focus();
1863
+ return;
1864
+ }
1865
+ if (digit !== null && digit <= numberedCount) {
1866
+ e.preventDefault();
1867
+ e.stopPropagation();
1868
+ handleOptionKey(options[digit - 1].id);
1869
+ return;
1870
+ }
1871
+ if (e.key === " " && e.target === e.currentTarget) {
1872
+ e.preventDefault();
1873
+ e.stopPropagation();
1874
+ if (showSkipButton) onSkip?.();
1875
+ return;
1876
+ }
1877
+ if (showCustomAnswer && digit === null && e.key.length === 1 && e.key !== " ") {
1878
+ e.stopPropagation();
1879
+ customInputRef.current?.focus();
1880
+ return;
1881
+ }
1882
+ };
1883
+ const send = () => {
1884
+ if (!multiSelect && !isCustomSelected && chosenId) handleOptionToggle(chosenId);
1885
+ else onNext?.();
1886
+ };
1887
+ const handleNextStep = () => {
1888
+ if (!multiSelect && pendingId) handleOptionToggle(pendingId);
1889
+ else onNext?.();
1890
+ };
1770
1891
  const handleCustomToggle = (checked) => {
1771
1892
  setIsCustomExpanded(checked);
1772
1893
  if (checked) {
@@ -1775,14 +1896,37 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1775
1896
  };
1776
1897
  const handleCustomFocus = () => {
1777
1898
  setIsCustomExpanded(true);
1899
+ setPendingId(null);
1778
1900
  if (!multiSelect) onSelectionChange?.([]);
1779
1901
  };
1780
1902
  const handleCustomRowClick = () => {
1781
1903
  if (multiSelect) handleCustomToggle(!isCustomSelected);
1782
1904
  else handleCustomFocus();
1783
1905
  };
1906
+ const handleCustomRowKey = () => {
1907
+ const opens = !multiSelect || !isCustomSelected;
1908
+ handleCustomRowClick();
1909
+ if (opens) customInputRef.current?.focus();
1910
+ };
1911
+ const submitButton = /* @__PURE__ */ jsxs(Button, {
1912
+ variant: "primary",
1913
+ size: "small",
1914
+ onClick: send,
1915
+ "data-testid": "question-next-button",
1916
+ children: [isLastStep ? "Submit" : "Next", /* @__PURE__ */ jsx(Kbd, {
1917
+ "aria-hidden": true,
1918
+ casing: "as-written",
1919
+ className: SEND_KEY_CLASS,
1920
+ children: `${keys.mod.symbol} Enter`
1921
+ })]
1922
+ });
1784
1923
  return /* @__PURE__ */ jsxs("div", {
1785
- className: cn("tw:flex tw:max-w-[800px] tw:flex-col tw:gap-4 tw:overflow-hidden tw:rounded-xl tw:border tw:border-solid tw:border-border tw:bg-card tw:text-card-foreground tw:shadow-lg", className),
1924
+ ref: rootRef,
1925
+ role: "group",
1926
+ "aria-label": question,
1927
+ tabIndex: -1,
1928
+ onKeyDown: handleCardKeyDown,
1929
+ className: cn("tw:flex tw:max-w-[800px] tw:flex-col tw:gap-4 tw:overflow-hidden tw:rounded-xl tw:border tw:border-solid tw:border-border tw:bg-card tw:text-card-foreground tw:shadow-lg tw:outline-none", className),
1786
1930
  "data-testid": "question-root",
1787
1931
  "data-question-step": currentStep,
1788
1932
  "data-question-total-steps": totalSteps,
@@ -1812,56 +1956,76 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1812
1956
  className: "tw:px-4",
1813
1957
  children: /* @__PURE__ */ jsxs("div", {
1814
1958
  className: "tw:flex tw:flex-col tw:gap-4 tw:border-0 tw:border-l tw:border-solid tw:border-primary tw:px-4 tw:py-2",
1815
- children: [options.map((option) => /* @__PURE__ */ jsxs("div", {
1816
- className: "tw:-mx-2 tw:-my-1.5 tw:flex tw:cursor-pointer tw:items-start tw:gap-2 tw:rounded-md tw:px-2 tw:py-1.5 tw:hover:bg-accent",
1817
- onClick: () => handleOptionToggle(option.id),
1818
- onKeyDown: handleKeyDown(() => handleOptionToggle(option.id)),
1819
- role: "button",
1820
- tabIndex: 0,
1821
- "data-testid": `question-option-${option.id}`,
1822
- children: [multiSelect && /* @__PURE__ */ jsx(Checkbox, {
1823
- checked: selectedOptionIds.includes(option.id),
1824
- onCheckedChange: () => handleOptionToggle(option.id),
1825
- onClick: (e) => e.stopPropagation(),
1826
- className: "tw:shrink-0"
1827
- }), /* @__PURE__ */ jsxs("div", {
1828
- className: "tw:flex tw:min-w-0 tw:flex-1 tw:items-start tw:gap-3",
1829
- children: [/* @__PURE__ */ jsxs("div", {
1830
- className: "tw:flex tw:flex-col tw:gap-1.5",
1831
- children: [/* @__PURE__ */ jsx("span", {
1832
- className: "tw:text-sm tw:font-medium tw:leading-none",
1833
- children: option.label
1834
- }), option.description && /* @__PURE__ */ jsx("span", {
1835
- className: "tw:text-sm tw:font-normal tw:leading-5 tw:text-muted-foreground",
1836
- children: option.description
1959
+ children: [options.map((option, index) => {
1960
+ const digit = index < numberedCount ? index + 1 : null;
1961
+ const isSelected = selectedOptionIds.includes(option.id);
1962
+ return /* @__PURE__ */ jsxs("div", {
1963
+ ref: (row) => {
1964
+ rowsRef.current[index] = row;
1965
+ },
1966
+ className: "tw:-mx-2 tw:-my-1.5 tw:flex tw:cursor-pointer tw:items-start tw:gap-2 tw:rounded-md tw:px-2 tw:py-1.5 tw:outline-none tw:hover:bg-accent tw:focus-visible:ring-2 tw:focus-visible:ring-ring",
1967
+ onClick: () => handleOptionToggle(option.id),
1968
+ onKeyDown: (e) => {
1969
+ if (hasCommandModifier(e) || e.repeat) return;
1970
+ if (e.key === " " || e.key === "Enter") {
1971
+ e.preventDefault();
1972
+ handleOptionKey(option.id);
1973
+ }
1974
+ },
1975
+ onFocus: () => setActiveRow(index),
1976
+ role: "button",
1977
+ "aria-pressed": multiSelect ? isSelected : chosenId === option.id,
1978
+ tabIndex: index === tabbableRow ? 0 : -1,
1979
+ "aria-keyshortcuts": digit === null ? void 0 : [String(digit), enableShortcuts && `Alt+${digit}`].filter(Boolean).join(" "),
1980
+ "data-question-option": true,
1981
+ "data-testid": `question-option-${option.id}`,
1982
+ children: [/* @__PURE__ */ jsx(OptionKey, {
1983
+ digit,
1984
+ checked: multiSelect ? isSelected : chosenId === option.id
1985
+ }), /* @__PURE__ */ jsxs("div", {
1986
+ className: "tw:flex tw:min-w-0 tw:flex-1 tw:items-start tw:gap-3",
1987
+ children: [/* @__PURE__ */ jsxs("div", {
1988
+ className: "tw:flex tw:flex-col tw:gap-1.5",
1989
+ children: [/* @__PURE__ */ jsx("span", {
1990
+ className: "tw:text-sm tw:font-medium tw:leading-none",
1991
+ children: option.label
1992
+ }), option.description && /* @__PURE__ */ jsx("span", {
1993
+ className: "tw:text-sm tw:font-normal tw:leading-5 tw:text-muted-foreground",
1994
+ children: option.description
1995
+ })]
1996
+ }), option.preview && /* @__PURE__ */ jsx("div", {
1997
+ "data-testid": "question-option-preview",
1998
+ className: cn("tw:ml-auto tw:flex tw:max-h-40 tw:min-w-0 tw:max-w-[50%] tw:flex-col tw:overflow-hidden tw:rounded-md", !renderPreview && "tw:overflow-auto tw:border tw:border-solid tw:border-border tw:bg-muted tw:p-2 tw:text-foreground"),
1999
+ children: renderPreview ? renderPreview(option.preview) : /* @__PURE__ */ jsx("pre", {
2000
+ className: "tw:m-0 tw:overflow-visible tw:border-0 tw:bg-transparent tw:p-0 tw:font-mono tw:text-xs tw:leading-5 tw:text-foreground",
2001
+ children: option.preview
2002
+ })
1837
2003
  })]
1838
- }), option.preview && /* @__PURE__ */ jsx("div", {
1839
- "data-testid": "question-option-preview",
1840
- className: cn("tw:ml-auto tw:flex tw:max-h-40 tw:min-w-0 tw:max-w-[50%] tw:flex-col tw:overflow-hidden tw:rounded-md", !renderPreview && "tw:overflow-auto tw:border tw:border-solid tw:border-border tw:bg-muted tw:p-2 tw:text-foreground"),
1841
- children: renderPreview ? renderPreview(option.preview) : /* @__PURE__ */ jsx("pre", {
1842
- className: "tw:m-0 tw:overflow-visible tw:border-0 tw:bg-transparent tw:p-0 tw:font-mono tw:text-xs tw:leading-5 tw:text-foreground",
1843
- children: option.preview
1844
- })
1845
2004
  })]
1846
- })]
1847
- }, option.id)), showCustomAnswer && /* @__PURE__ */ jsxs("div", {
2005
+ }, option.id);
2006
+ }), showCustomAnswer && /* @__PURE__ */ jsxs("div", {
1848
2007
  className: "tw:flex tw:flex-col tw:gap-2",
1849
2008
  children: [/* @__PURE__ */ jsxs("div", {
1850
- className: "tw:-mx-2 tw:-my-1.5 tw:flex tw:cursor-pointer tw:items-start tw:gap-2 tw:rounded-md tw:px-2 tw:py-1.5 tw:hover:bg-accent",
2009
+ ref: (row) => {
2010
+ rowsRef.current[options.length] = row;
2011
+ },
2012
+ className: "tw:-mx-2 tw:-my-1.5 tw:flex tw:cursor-pointer tw:items-start tw:gap-2 tw:rounded-md tw:px-2 tw:py-1.5 tw:outline-none tw:hover:bg-accent tw:focus-visible:ring-2 tw:focus-visible:ring-ring",
1851
2013
  onClick: handleCustomRowClick,
1852
- onKeyDown: handleKeyDown(handleCustomRowClick),
2014
+ onKeyDown: handleKeyDown(handleCustomRowKey),
2015
+ onFocus: () => setActiveRow(options.length),
1853
2016
  role: "button",
1854
- tabIndex: 0,
1855
- children: [multiSelect && /* @__PURE__ */ jsx(Checkbox, {
1856
- checked: isCustomSelected,
1857
- onCheckedChange: handleCustomToggle,
1858
- onClick: (e) => e.stopPropagation(),
1859
- className: "tw:shrink-0"
2017
+ "aria-pressed": isCustomSelected,
2018
+ "aria-keyshortcuts": otherDigit === null ? void 0 : String(otherDigit),
2019
+ tabIndex: options.length === tabbableRow ? 0 : -1,
2020
+ children: [/* @__PURE__ */ jsx(OptionKey, {
2021
+ digit: otherDigit,
2022
+ checked: isCustomSelected
1860
2023
  }), /* @__PURE__ */ jsx("span", {
1861
- className: cn("tw:text-sm tw:font-medium tw:leading-none", !multiSelect && isCustomSelected && "tw:text-primary"),
2024
+ className: cn("tw:flex-1 tw:text-sm tw:font-medium tw:leading-none", !multiSelect && isCustomSelected && "tw:text-primary"),
1862
2025
  children: "Other"
1863
2026
  })]
1864
2027
  }), /* @__PURE__ */ jsx(TextInput, {
2028
+ ref: customInputRef,
1865
2029
  value: customAnswer,
1866
2030
  onValueChange: (value) => onCustomAnswerChange?.(value),
1867
2031
  onFocus: handleCustomFocus,
@@ -1878,6 +2042,31 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1878
2042
  })]
1879
2043
  })
1880
2044
  }),
2045
+ numberedCount > 0 && /* @__PURE__ */ jsxs("div", {
2046
+ "aria-hidden": true,
2047
+ className: "tw:-mt-1 tw:flex tw:flex-wrap tw:items-center tw:gap-x-3 tw:gap-y-1 tw:px-4 tw:text-xs tw:text-muted-foreground",
2048
+ "data-testid": "question-keyboard-hint",
2049
+ children: [
2050
+ /* @__PURE__ */ jsxs("span", {
2051
+ className: "tw:inline-flex tw:items-center tw:gap-1",
2052
+ children: [/* @__PURE__ */ jsx(Kbd, {
2053
+ casing: "as-written",
2054
+ children: keyRange(numberedCount + (otherDigit === null ? 0 : 1))
2055
+ }), multiSelect ? "toggle" : "choose"]
2056
+ }),
2057
+ rowCount > 1 && /* @__PURE__ */ jsxs("span", {
2058
+ className: "tw:inline-flex tw:items-center tw:gap-1",
2059
+ children: [/* @__PURE__ */ jsxs(Kbd, { children: [/* @__PURE__ */ jsx(ArrowUp, { className: "tw:size-3" }), /* @__PURE__ */ jsx(ArrowDown, { className: "tw:size-3" })] }), "move"]
2060
+ }),
2061
+ /* @__PURE__ */ jsxs("span", {
2062
+ className: "tw:inline-flex tw:items-center tw:gap-1",
2063
+ children: [/* @__PURE__ */ jsx(Kbd, {
2064
+ casing: "as-written",
2065
+ children: "Enter"
2066
+ }), multiSelect ? "toggle" : "select"]
2067
+ })
2068
+ ]
2069
+ }),
1881
2070
  /* @__PURE__ */ jsxs("div", {
1882
2071
  className: "tw:flex tw:flex-col",
1883
2072
  children: [showStepControls && /* @__PURE__ */ jsx(Progress, {
@@ -1910,37 +2099,35 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1910
2099
  type: "button",
1911
2100
  disabled: currentStep === totalSteps,
1912
2101
  className: "tw:flex tw:cursor-pointer tw:items-center tw:justify-center tw:bg-transparent tw:p-0 tw:disabled:cursor-default tailwindcss-preflight",
1913
- onClick: onNext,
2102
+ onClick: handleNextStep,
1914
2103
  "aria-label": "Next step",
1915
2104
  children: /* @__PURE__ */ jsx(ChevronRight, { className: cn("tw:size-5", currentStep === totalSteps ? "tw:text-primary/25" : "tw:text-primary") })
1916
2105
  })
1917
2106
  ]
1918
2107
  }),
1919
- showSkipButton && /* @__PURE__ */ jsx(Button, {
1920
- variant: "outline",
1921
- size: "small",
1922
- onClick: onSkip,
1923
- "data-testid": "question-skip-button",
1924
- children: "Skip"
1925
- }),
1926
- showSubmitButton && (submitTooltip ? /* @__PURE__ */ jsx(Tooltip, {
1927
- tooltip: submitTooltip,
2108
+ showSkipButton && /* @__PURE__ */ jsx(Tooltip, {
2109
+ tooltip: /* @__PURE__ */ jsx(Kbd, {
2110
+ variant: "primary",
2111
+ casing: "as-written",
2112
+ children: "Space"
2113
+ }),
1928
2114
  placement: "top",
1929
2115
  triggerAsChild: true,
1930
2116
  children: /* @__PURE__ */ jsx(Button, {
1931
- variant: "primary",
2117
+ variant: "outline",
1932
2118
  size: "small",
1933
- onClick: onNext,
1934
- "data-testid": "question-next-button",
1935
- children: isLastStep ? "Submit" : "Next"
2119
+ onClick: onSkip,
2120
+ "aria-keyshortcuts": "Space",
2121
+ "data-testid": "question-skip-button",
2122
+ children: "Skip"
1936
2123
  })
1937
- }) : /* @__PURE__ */ jsx(Button, {
1938
- variant: "primary",
1939
- size: "small",
1940
- onClick: onNext,
1941
- "data-testid": "question-next-button",
1942
- children: isLastStep ? "Submit" : "Next"
1943
- }))
2124
+ }),
2125
+ showSubmitButton && (submitTooltip ? /* @__PURE__ */ jsx(Tooltip, {
2126
+ tooltip: submitTooltip,
2127
+ placement: "top",
2128
+ triggerAsChild: true,
2129
+ children: submitButton
2130
+ }) : submitButton)
1944
2131
  ]
1945
2132
  })]
1946
2133
  })
@@ -1993,11 +2180,16 @@ const QuestionBlock = ({ steps, showSkip = true, onSubmit, onClose, renderPrevie
1993
2180
  };
1994
2181
  //#endregion
1995
2182
  //#region src/blocks/AIKit/SuggestedPrompt.tsx
1996
- const SuggestedPrompt = ({ label, disabled, onClick, icon, className }) => {
2183
+ const SuggestedPrompt = ({ label, disabled, onClick, icon, variant = "row", className }) => {
1997
2184
  return /* @__PURE__ */ jsxs("button", {
1998
2185
  type: "button",
1999
2186
  disabled,
2000
- className: cn("tailwindcss-preflight tw:flex tw:items-center tw:justify-start tw:gap-2.5 tw:whitespace-nowrap", "tw:rounded-lg tw:px-3 tw:py-2", "tw:text-left tw:text-sm tw:font-medium tw:text-foreground", "tw:transition-colors tw:not-disabled:hover:bg-accent", "tw:focus-visible:outline-none tw:focus-visible:ring-2 tw:focus-visible:ring-ring tw:focus-visible:ring-offset-1", "tw:disabled:cursor-not-allowed tw:disabled:opacity-50", className),
2187
+ "data-variant": variant,
2188
+ className: cn("tailwindcss-preflight tw:flex tw:items-center tw:justify-start tw:gap-2.5 tw:whitespace-nowrap", "tw:text-left tw:text-sm tw:text-foreground tw:transition-colors", "tw:focus-visible:outline-none tw:focus-visible:ring-2 tw:focus-visible:ring-ring tw:focus-visible:ring-offset-1", "tw:disabled:cursor-not-allowed tw:disabled:opacity-50", variant === "bubble" ? [
2189
+ "tw:w-fit tw:max-w-[min(85%,40rem)] tw:rounded-xl tw:px-3 tw:py-2",
2190
+ "tw:border tw:border-solid tw:border-border tw:bg-card",
2191
+ "tw:not-disabled:hover:bg-muted tw:focus-visible:bg-muted"
2192
+ ] : "tw:rounded-lg tw:px-3 tw:py-2 tw:font-medium tw:not-disabled:hover:bg-accent", className),
2001
2193
  onClick,
2002
2194
  children: [icon ?? /* @__PURE__ */ jsx(Sparkles, { className: "tw:size-4 tw:shrink-0 tw:basis-4 tw:[&_*]:stroke-aigradient-600" }), /* @__PURE__ */ jsx("div", {
2003
2195
  className: "tw:whitespace-normal tw:text-left",
@@ -2337,4 +2529,4 @@ const UsageStats = ({ value, alert = false, children, className, ariaLabel, alig
2337
2529
  //#endregion
2338
2530
  export { AnswerActions, AnswerSummary, ChatInlineIcon, CollapsibleStepHeader, DIAGRAM_LANGUAGES, FileDownloadChip, InlineStepDetail, InlineStepGroup, InlineStepLabel, Markdown, MarkdownTable, MessageBubble, PROMPT_INPUT_CONTAINER, PlanApproval, PlanRewritingIndicator, PromptInput, Question, QuestionBlock, SuggestedPrompt, ThinkingIndicator, ToolCallApproval, ToolCallChip, ToolCallStatusHeader, UsageBar, UsageStats, UsageStatsRing, clampPercentage, findMentionToken, findSlashToken, formatPercentage, getChatInlineIconName };
2339
2531
 
2340
- //# sourceMappingURL=AIKit-VB7CXeQk.js.map
2532
+ //# sourceMappingURL=AIKit-BVCWP9Ks.js.map