@parallelworks/ui 0.80.0 → 0.82.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 (54) hide show
  1. package/README.md +7 -1
  2. package/dist/ai/adapter/types.d.ts +1 -0
  3. package/dist/ai/ai.css +3 -92
  4. package/dist/ai/components/agent/AgentMessageParts.js +1 -0
  5. package/dist/ai/core/ChatProvider.js +10 -5
  6. package/dist/ai/strings.d.ts +3 -0
  7. package/dist/ai/strings.js +2 -0
  8. package/dist/ai/types.d.ts +5 -0
  9. package/dist/components/Avatar.d.ts +5 -3
  10. package/dist/components/Avatar.js +6 -2
  11. package/dist/components/Card.d.ts +16 -1
  12. package/dist/components/Card.js +13 -2
  13. package/dist/components/CodeBlock.d.ts +40 -0
  14. package/dist/components/CodeBlock.js +159 -0
  15. package/dist/components/CopyButton.d.ts +11 -4
  16. package/dist/components/CopyButton.js +86 -11
  17. package/dist/components/CreateModal.d.ts +3 -1
  18. package/dist/components/CreateModal.js +31 -11
  19. package/dist/components/DescriptionList.js +1 -1
  20. package/dist/components/Drawer.js +1 -1
  21. package/dist/components/Input.d.ts +5 -0
  22. package/dist/components/Input.js +4 -3
  23. package/dist/components/Listbox.d.ts +12 -3
  24. package/dist/components/Listbox.js +38 -15
  25. package/dist/components/MarkdownImpl.js +89 -2
  26. package/dist/components/MenuEntries.d.ts +3 -0
  27. package/dist/components/MenuEntries.js +19 -6
  28. package/dist/components/Modal.d.ts +1 -1
  29. package/dist/components/Modal.js +6 -5
  30. package/dist/components/PageHeader.d.ts +7 -1
  31. package/dist/components/PageHeader.js +11 -5
  32. package/dist/components/Provider.d.ts +3 -0
  33. package/dist/components/Provider.js +1 -0
  34. package/dist/components/Select.js +1 -1
  35. package/dist/components/SettingsCard.d.ts +7 -2
  36. package/dist/components/SettingsCard.js +42 -7
  37. package/dist/components/ToggleGroup.d.ts +3 -1
  38. package/dist/components/ToggleGroup.js +2 -2
  39. package/dist/components/Tooltip.js +35 -2
  40. package/dist/components/codeHighlight.d.ts +9 -0
  41. package/dist/components/codeHighlight.js +29 -0
  42. package/dist/components/pageHeaderStore.d.ts +4 -0
  43. package/dist/components/pageHeaderStore.js +5 -2
  44. package/dist/file-explorer/FileExplorer.js +1 -1
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.js +3 -2
  47. package/dist/list/ListFilterBar.js +6 -5
  48. package/dist/list/filterModel.d.ts +4 -0
  49. package/dist/list/filterModel.js +7 -1
  50. package/dist/styles/theme.css +1 -0
  51. package/dist/styles.css +1 -1
  52. package/dist/theme/derive.d.ts +1 -1
  53. package/dist/theme/derive.js +3 -1
  54. package/package.json +1 -1
@@ -4,7 +4,7 @@ import { CheckIcon, CopyIcon, EyeIcon, EyeOffIcon } from "../icons.js";
4
4
  import { useStrings } from "./Provider.js";
5
5
  import { useCopy } from "./useCopy.js";
6
6
  import { IconButton } from "./IconButton.js";
7
- import { useState } from "react";
7
+ import { useEffectEvent, useLayoutEffect, useRef, useState } from "react";
8
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/components/CopyButton.tsx
10
10
  /** The one copy control: a button that copies its text, turns into a check for a
@@ -40,12 +40,60 @@ function CopyButton({ text, label, quiet = false, showLabel = false, variant = s
40
40
  onClick
41
41
  });
42
42
  }
43
+ var NO_ENDS = {
44
+ start: false,
45
+ end: false
46
+ };
47
+ function useHiddenEnds(active) {
48
+ const ref = useRef(null);
49
+ const [ends, setEnds] = useState(NO_ENDS);
50
+ const measureNow = () => {
51
+ const el = ref.current;
52
+ if (!el || !active) {
53
+ setEnds(NO_ENDS);
54
+ return;
55
+ }
56
+ const scrolled = Math.abs(el.scrollLeft);
57
+ const start = scrolled > 1;
58
+ const end = scrolled + el.clientWidth < el.scrollWidth - 1;
59
+ setEnds((was) => was.start === start && was.end === end ? was : {
60
+ start,
61
+ end
62
+ });
63
+ };
64
+ const measure = useEffectEvent(measureNow);
65
+ useLayoutEffect(() => {
66
+ measure();
67
+ });
68
+ useLayoutEffect(() => {
69
+ const el = ref.current;
70
+ if (!el || !active || typeof ResizeObserver === "undefined") return;
71
+ const observer = new ResizeObserver(() => measure());
72
+ observer.observe(el);
73
+ return () => observer.disconnect();
74
+ }, [active]);
75
+ return {
76
+ ref,
77
+ ends,
78
+ onScroll: measureNow
79
+ };
80
+ }
81
+ var FADE = "1.25rem";
82
+ function fadeMask({ start, end }) {
83
+ if (!start && !end) return;
84
+ const image = `linear-gradient(to var(--fade-end, right), ${start ? `transparent, #000 ${FADE}` : "#000"}, ${end ? `#000 calc(100% - ${FADE}), transparent` : "#000"})`;
85
+ return {
86
+ maskImage: image,
87
+ WebkitMaskImage: image
88
+ };
89
+ }
43
90
  var MASK = "•".repeat(24);
44
91
  /** A block of code, such as a command to run, with a button that copies it. The
45
92
  * buttons show while the pointer is over the block or focus is in it, and always
46
- * where there is no hover to reveal them. With `secret` it holds a value shown once,
47
- * such as a new API key: masked until the eye shows it, and copied either way. */
48
- function CopyCodeBlock({ text, label, children, secret = false, revealed: revealedProp, defaultRevealed = false, onRevealedChange, wrap = false, className }) {
93
+ * where there is no hover to reveal them. With `inline` it is a compact value in the line
94
+ * instead, such as a token shown once, its buttons always beside it. With `secret` it
95
+ * masks the value until the eye shows it, and copies it either way. */
96
+ function CopyCodeBlock({ text, label, children, secret = false, revealed: revealedProp, defaultRevealed = false, onRevealedChange, inline = false, wrap = false, className }) {
49
97
  const { common: t } = useStrings();
50
98
  const [ownRevealed, setOwnRevealed] = useState(defaultRevealed);
51
99
  const revealed = revealedProp ?? ownRevealed;
@@ -63,6 +111,39 @@ function CopyCodeBlock({ text, label, children, secret = false, revealed: reveal
63
111
  className: "sr-only",
64
112
  children: t.hiddenValue
65
113
  })] });
114
+ const scroller = useHiddenEnds(inline && !wrap);
115
+ const overflows = scroller.ends.start || scroller.ends.end;
116
+ const revealButton = secret && /* @__PURE__ */ jsx(IconButton, {
117
+ label: t.showValue,
118
+ icon: revealed ? /* @__PURE__ */ jsx(EyeIcon, {}) : /* @__PURE__ */ jsx(EyeOffIcon, {}),
119
+ pressed: revealed,
120
+ onClick: () => setRevealed(!revealed),
121
+ size: inline ? "xs" : "sm",
122
+ className: inline ? void 0 : cx("bg-(--theme-muted-panel-bg)", reveal)
123
+ });
124
+ if (inline) return /* @__PURE__ */ jsxs("span", {
125
+ className: cx("inline-flex max-w-full items-start gap-1 rounded-md border border-current/15 bg-(--theme-panel-bg)/70 py-px ps-2 pe-px align-middle", "has-[code:focus-visible]:outline-2 has-[code:focus-visible]:outline-offset-1 has-[code:focus-visible]:outline-(--theme-focus)", className),
126
+ children: [/* @__PURE__ */ jsx("code", {
127
+ ref: scroller.ref,
128
+ onScroll: scroller.onScroll,
129
+ ...overflows ? {
130
+ tabIndex: 0,
131
+ role: "region",
132
+ "aria-label": label ?? t.codeOf("")
133
+ } : {},
134
+ style: fadeMask(scroller.ends),
135
+ className: cx("min-w-0 py-[3px] font-mono text-[0.8125rem] leading-[18px] text-(--theme-app)", "outline-none", "rtl:[--fade-end:left]", wrap ? "break-all whitespace-pre-wrap" : "overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"),
136
+ children: shown
137
+ }), /* @__PURE__ */ jsxs("span", {
138
+ className: "flex shrink-0 items-center",
139
+ children: [/* @__PURE__ */ jsx(CopyButton, {
140
+ text,
141
+ label,
142
+ size: "xs",
143
+ quiet: true
144
+ }), revealButton]
145
+ })]
146
+ });
66
147
  return /* @__PURE__ */ jsxs("div", {
67
148
  className: cx("group/code relative", className),
68
149
  children: [/* @__PURE__ */ jsx("pre", {
@@ -70,13 +151,7 @@ function CopyCodeBlock({ text, label, children, secret = false, revealed: reveal
70
151
  children: shown
71
152
  }), /* @__PURE__ */ jsxs("div", {
72
153
  className: "absolute top-2 right-2 flex items-center gap-1",
73
- children: [secret && /* @__PURE__ */ jsx(IconButton, {
74
- label: t.showValue,
75
- icon: revealed ? /* @__PURE__ */ jsx(EyeIcon, {}) : /* @__PURE__ */ jsx(EyeOffIcon, {}),
76
- pressed: revealed,
77
- onClick: () => setRevealed(!revealed),
78
- className: cx("bg-(--theme-muted-panel-bg)", reveal)
79
- }), /* @__PURE__ */ jsx(CopyButton, {
154
+ children: [revealButton, /* @__PURE__ */ jsx(CopyButton, {
80
155
  text,
81
156
  label,
82
157
  className: cx("bg-(--theme-muted-panel-bg)", reveal)
@@ -40,7 +40,9 @@ interface CreateModalProps {
40
40
  dirty?: boolean;
41
41
  /** Show a favorite toggle in the footer. */
42
42
  showFavorite?: boolean;
43
- /** Muted text at the footer's start, such as which keys submit. `error` takes its place. */
43
+ /** Muted text at the footer's start, such as which keys submit, written with `Kbd` so each
44
+ * reader sees their own keyboard's modifier. Hidden on a touch screen, where keys mean
45
+ * nothing. `error` takes its place. */
44
46
  footerHint?: ReactNode;
45
47
  /** Why the last submit failed, at the footer's start, read out as it appears. */
46
48
  error?: ReactNode;
@@ -6,12 +6,13 @@ import { useStrings } from "./Provider.js";
6
6
  import { ActionMenu } from "./ActionMenu.js";
7
7
  import { Button } from "./Button.js";
8
8
  import { ErrorText } from "./Field.js";
9
+ import { useAutosize } from "./Input.js";
9
10
  import { comboboxLeaves } from "./comboboxItems.js";
10
11
  import { Combobox } from "./Combobox.js";
11
12
  import { Modal } from "./Modal.js";
12
13
  import { Disclosure } from "./Disclosure.js";
13
14
  import { Switch } from "./Switch.js";
14
- import { useId, useRef, useState } from "react";
15
+ import { useEffect, useId, useRef, useState } from "react";
15
16
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
16
17
  import { Toggle } from "@base-ui/react/toggle";
17
18
  //#region src/components/CreateModal.tsx
@@ -30,6 +31,7 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
30
31
  const [nameTouched, setNameTouched] = useState(false);
31
32
  const [interacted, setInteracted] = useState(false);
32
33
  const nameRef = useRef(null);
34
+ const { element: nameSizer, resize: resizeName } = useAutosize(open && !hideName, name);
33
35
  const nameMessageId = useId();
34
36
  const session = open ? initialName ?? "" : null;
35
37
  const [shownSession, setShownSession] = useState(session);
@@ -48,7 +50,17 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
48
50
  const hint = !nameError && name.trim() ? nameHint?.(name.trim()) : null;
49
51
  const canSubmit = (hideName || nameOptional || name.trim().length > 0) && !nameError && !submitting && !submitDisabled;
50
52
  const isDirty = !submitting && (interacted || name !== (initialName ?? "") || description !== "" || favorite || dirty);
51
- const nameMissing = !hideName && !nameOptional && (!name.trim() || !!nameError) && (nameTouched || description !== "" || favorite || dirty);
53
+ const nameMissing = !hideName && !nameOptional && (!name.trim() || !!nameError) && nameTouched;
54
+ const focusInside = useRef(false);
55
+ const leaveCheck = useRef(void 0);
56
+ useEffect(() => () => clearTimeout(leaveCheck.current), []);
57
+ const checkLeft = () => {
58
+ focusInside.current = false;
59
+ clearTimeout(leaveCheck.current);
60
+ leaveCheck.current = setTimeout(() => {
61
+ if (!focusInside.current && document.hasFocus() && !nameRef.current?.value.trim()) setNameTouched(true);
62
+ });
63
+ };
52
64
  const flashName = () => {
53
65
  setNameTouched(true);
54
66
  nameRef.current?.focus();
@@ -78,15 +90,18 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
78
90
  size,
79
91
  dismissible: !isDirty,
80
92
  initialFocus: () => nameRef.current ?? true,
93
+ onFocus: () => {
94
+ focusInside.current = true;
95
+ },
81
96
  onKeyDown: (e) => {
82
97
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleSubmit(true);
83
98
  },
84
99
  onMouseDown: (e) => {
85
- if (e.target instanceof Element && !e.target.closest("input, textarea, button, a, select, label, code, pre, .select-all")) e.preventDefault();
100
+ if (e.target instanceof Element && !e.target.closest("input, textarea, button, a, select, label, code, pre, .select-all, [contenteditable]:not([contenteditable=\"false\"])")) e.preventDefault();
86
101
  },
87
102
  footer: /* @__PURE__ */ jsxs(Fragment$1, { children: [
88
103
  (showFavorite || footerHint || error) && /* @__PURE__ */ jsxs("div", {
89
- className: "me-auto flex min-w-0 items-center gap-3",
104
+ className: cx("me-auto flex min-w-0 items-center gap-3", !showFavorite && !error && "pointer-coarse:hidden"),
90
105
  children: [showFavorite && /* @__PURE__ */ jsx(Tooltip, {
91
106
  content: favorite ? t.favoriteHintOn : t.favoriteHintOff,
92
107
  children: /* @__PURE__ */ jsxs(Toggle, {
@@ -100,7 +115,7 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
100
115
  children: [favorite ? /* @__PURE__ */ jsx(FavoriteIcon, { className: "text-(--theme-favorite)" }) : /* @__PURE__ */ jsx(FavoriteOutlineIcon, {}), favorite ? t.favorited : t.favorite]
101
116
  })
102
117
  }), error ? /* @__PURE__ */ jsx(ErrorText, { children: error }) : footerHint && /* @__PURE__ */ jsx("p", {
103
- className: "m-0 min-w-0 text-xs text-(--theme-muted-text-color)",
118
+ className: "m-0 min-w-0 text-xs text-(--theme-muted-text-color) pointer-coarse:hidden",
104
119
  children: footerHint
105
120
  })]
106
121
  }),
@@ -141,19 +156,24 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
141
156
  children: [typeIcon, typeLabel]
142
157
  }),
143
158
  hideName ? /* @__PURE__ */ jsx("div", { className: "mt-3" }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
144
- /* @__PURE__ */ jsx("input", {
145
- ref: nameRef,
146
- type: "text",
159
+ /* @__PURE__ */ jsx("textarea", {
160
+ ref: (element) => {
161
+ nameRef.current = element;
162
+ nameSizer.current = element;
163
+ },
164
+ rows: 1,
147
165
  autoComplete: "off",
148
166
  "data-1p-ignore": true,
149
167
  value: name,
150
168
  onChange: (e) => {
151
- const next = sanitizeName ? sanitizeName(e.target.value) : e.target.value;
169
+ const joined = e.target.value.replace(/\s*\n+\s*/g, " ");
170
+ const next = sanitizeName ? sanitizeName(joined) : joined;
152
171
  setName(next);
153
172
  onNameChange?.(next);
173
+ resizeName?.();
154
174
  },
155
175
  onBlur: (e) => {
156
- if (!e.target.value.trim()) setNameTouched(true);
176
+ if (!e.target.value.trim()) checkLeft();
157
177
  },
158
178
  onKeyDown: (e) => {
159
179
  if (e.key === "Enter" && !e.nativeEvent.isComposing && !e.metaKey && !e.ctrlKey) {
@@ -168,7 +188,7 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
168
188
  "aria-invalid": nameMissing || !!nameError,
169
189
  "aria-describedby": nameError || hint ? nameMessageId : void 0,
170
190
  onAnimationEnd: () => setNameFlash(false),
171
- className: cx("mt-2 -mb-1 -mx-1.5 w-[calc(100%+0.75rem)] rounded-md border-0 px-1.5 py-1 bg-transparent shadow-none text-lg font-medium text-(--theme-app) placeholder:text-(--theme-placeholder) focus:outline-none", nameFlash && "animate-field-flash", nameMissing ? "ring-1 ring-(--theme-error)" : "focus:ring-0")
191
+ className: cx("mt-2 -mb-1 -mx-1.5 block w-[calc(100%+0.75rem)] resize-none overflow-hidden rounded-md border-0 px-1.5 py-1 bg-transparent shadow-none text-lg leading-snug font-medium text-(--theme-app) [field-sizing:content] placeholder:text-(--theme-placeholder) focus:outline-none", nameFlash && "animate-field-flash", nameMissing ? "ring-1 ring-(--theme-error)" : "focus:ring-0")
172
192
  }),
173
193
  nameError && /* @__PURE__ */ jsx("p", {
174
194
  id: nameMessageId,
@@ -1,8 +1,8 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { Tooltip } from "./Tooltip.js";
3
3
  import { useStrings } from "./Provider.js";
4
- import { InfoTip } from "./InfoTip.js";
5
4
  import { CopyButton } from "./CopyButton.js";
5
+ import { InfoTip } from "./InfoTip.js";
6
6
  import { withPathBreaks } from "./pathBreaks.js";
7
7
  import { createContext, useContext, useId, useRef } from "react";
8
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -47,7 +47,7 @@ function Drawer$1({ open, onClose, title, description, onBack, backLabel, footer
47
47
  container: contained ? container : portal.container,
48
48
  keepMounted,
49
49
  className: contained ? "contents" : portal.className,
50
- children: [modal !== false && /* @__PURE__ */ jsx(Drawer.Backdrop, { className: cx(area, "z-(--layer-drawer) bg-black/40 opacity-[calc(1-var(--drawer-swipe-progress))] transition-opacity duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-swiping:duration-0 motion-reduce:transition-none") }), /* @__PURE__ */ jsx(Drawer.Viewport, {
50
+ children: [modal !== false && /* @__PURE__ */ jsx(Drawer.Backdrop, { className: cx(area, "z-(--layer-drawer) bg-(--theme-backdrop) opacity-[calc(1-var(--drawer-swipe-progress))] transition-opacity duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-swiping:duration-0 motion-reduce:transition-none") }), /* @__PURE__ */ jsx(Drawer.Viewport, {
51
51
  ref: viewport,
52
52
  className: cx(area, "pointer-events-none z-(--layer-drawer) flex", side === "right" ? "justify-end pl-[max(var(--app-inset-start,0px),2.5rem)]" : "justify-start pl-(--app-inset-start,0px) pr-10"),
53
53
  children: /* @__PURE__ */ jsxs(Drawer.Popup, {
@@ -46,6 +46,11 @@ export declare function framed({ label, hint, help, actions, description, error,
46
46
  /** A single-line text field. `type="password"` adds a button that shows what was typed. Placed in
47
47
  * a Field, it takes the Field's label, description and invalid state. */
48
48
  export declare function Input({ label, hint, help, description, error, className, mono, size, type, ref, ...props }: InputProps): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
49
+ /** Sets a textarea's height to its text's while the browser can't, from its value. */
50
+ export declare function useAutosize(enabled: boolean, value: unknown): {
51
+ element: import("react").RefObject<HTMLTextAreaElement | null>;
52
+ resize: (() => void) | undefined;
53
+ };
49
54
  /** A multi-line text field that grows by hand, or with its text with `autosize`. Placed in a
50
55
  * Field, it takes the Field's label, description and invalid state. */
51
56
  export declare function Textarea({ label, hint, help, description, error, className, mono, size, ref, autosize, bare, secret, style, ...props }: TextareaProps): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
@@ -168,7 +168,7 @@ function TitleInput({ value, onCommit, autoSelect = false, size = "xl", readOnly
168
168
  setShown(value);
169
169
  if (!editing.current) setDraft(value);
170
170
  }
171
- const { resize } = useAutosize(true, draft);
171
+ const { element: sizer, resize } = useAutosize(true, draft);
172
172
  useEffect(() => {
173
173
  if (autoSelect && element.current) {
174
174
  element.current.focus();
@@ -177,9 +177,10 @@ function TitleInput({ value, onCommit, autoSelect = false, size = "xl", readOnly
177
177
  }, []);
178
178
  const setRef = useCallback((el) => {
179
179
  element.current = el;
180
+ sizer.current = el;
180
181
  if (typeof ref === "function") ref(el);
181
182
  else if (ref) ref.current = el;
182
- }, [ref]);
183
+ }, [ref, sizer]);
183
184
  return /* @__PURE__ */ jsx("textarea", {
184
185
  ...props,
185
186
  ref: setRef,
@@ -218,4 +219,4 @@ function TitleInput({ value, onCommit, autoSelect = false, size = "xl", readOnly
218
219
  });
219
220
  }
220
221
  //#endregion
221
- export { Input$1 as Input, Textarea, TitleInput, framed };
222
+ export { Input$1 as Input, Textarea, TitleInput, framed, useAutosize };
@@ -6,7 +6,9 @@ export interface ListboxNavigationOptions<T> {
6
6
  onChoose: (item: T, index: number) => void;
7
7
  /** Called on Escape, such as to close the list until the text changes. */
8
8
  onDismiss?: (() => void) | undefined;
9
- /** False while the list isn't shown: the keys are then the host's own. Default true. */
9
+ /** False while the list isn't shown: the keys are then the host's own. A list that hides
10
+ * itself while empty passes false then too, or Escape dismisses what isn't there. Default
11
+ * true. */
10
12
  open?: boolean | undefined;
11
13
  /** Past the end comes the start, and before the start the end. Default true. */
12
14
  loop?: boolean | undefined;
@@ -91,10 +93,17 @@ export interface ListboxOptionProps {
91
93
  inline?: boolean | undefined;
92
94
  /** Short muted text at the row's end, such as a key. */
93
95
  hint?: ReactNode;
96
+ /** In a list of choices that stay chosen, such as the labels on an issue: true shows the
97
+ * check at the row's end and tells assistive technology, false leaves it unchecked. Leave
98
+ * it out for a list that only acts. */
99
+ checked?: boolean | undefined;
100
+ /** The option opens more choices, such as a submenu or a next step: a chevron ends the row. */
101
+ submenu?: boolean | undefined;
94
102
  className?: string | undefined;
95
103
  }
96
- /** One option of a ListboxPanel: an icon, its label and description, and a hint at its end. */
97
- export declare function ListboxOption({ option, icon, label, description, inline, hint, className, }: ListboxOptionProps): import("react").JSX.Element;
104
+ /** One option of a ListboxPanel: an icon, its label and description, and at its end a hint, a
105
+ * check while it is chosen and a chevron when it opens more. */
106
+ export declare function ListboxOption({ option, icon, label, description, inline, hint, checked, submenu, className, }: ListboxOptionProps): import("react").JSX.Element;
98
107
  /** Options under a heading in a ListboxPanel. */
99
108
  export declare function ListboxGroup({ label, children }: {
100
109
  label: ReactNode;
@@ -1,8 +1,9 @@
1
1
  import { cx } from "../cx.js";
2
+ import { CheckIcon, ChevronRightIcon } from "../icons.js";
2
3
  import { panelClassName, panelItemClassName } from "./floating.js";
3
4
  import { Popover } from "./Popover.js";
4
5
  import "./comboboxItems.js";
5
- import { useEffect, useId, useState } from "react";
6
+ import { Children, useEffect, useId, useState } from "react";
6
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
8
  //#region src/components/Listbox.tsx
8
9
  /** The parts a highlighted list needs while focus stays on the box the reader types in, such
@@ -44,7 +45,14 @@ function useListboxNavigation({ items, onChoose, onDismiss, open = true, loop =
44
45
  /** Handles a key the list owns, and says whether it did; leave the rest to the host. */
45
46
  const handleKey = (event) => {
46
47
  const composing = "nativeEvent" in event ? event.nativeEvent.isComposing : event.isComposing;
47
- if (!shownOpen || composing) return false;
48
+ if (!open || composing) return false;
49
+ if (event.key === "Escape") {
50
+ if (!onDismiss) return false;
51
+ onDismiss();
52
+ event.preventDefault();
53
+ return true;
54
+ }
55
+ if (!shownOpen) return false;
48
56
  const ctrlOnly = event.ctrlKey && !event.metaKey && !event.altKey;
49
57
  switch (true) {
50
58
  case event.key === "ArrowDown" || ctrlOnly && event.key === "n":
@@ -59,10 +67,6 @@ function useListboxNavigation({ items, onChoose, onDismiss, open = true, loop =
59
67
  case homeEnd && event.key === "End":
60
68
  setActive(step(count, -1));
61
69
  break;
62
- case event.key === "Escape":
63
- if (!onDismiss) return false;
64
- onDismiss();
65
- break;
66
70
  case event.key === "Enter" || chooseKeys.includes(event.key):
67
71
  if (event.shiftKey || event.altKey || event.metaKey || event.ctrlKey) return false;
68
72
  choose(shown);
@@ -110,7 +114,7 @@ function useListboxNavigation({ items, onChoose, onDismiss, open = true, loop =
110
114
  }
111
115
  /** The panel of a highlighted list that the reader drives from a box that keeps focus. */
112
116
  function ListboxPanel({ listProps, "aria-label": label, footer, empty, at, onDismiss, className, children }) {
113
- const hasOptions = Array.isArray(children) ? children.some(Boolean) : Boolean(children);
117
+ const hasOptions = Children.toArray(children).length > 0;
114
118
  const body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
115
119
  id: listProps.id,
116
120
  role: "listbox",
@@ -143,11 +147,17 @@ function ListboxPanel({ listProps, "aria-label": label, footer, empty, at, onDis
143
147
  children: body
144
148
  });
145
149
  }
146
- /** One option of a ListboxPanel: an icon, its label and description, and a hint at its end. */
147
- function ListboxOption({ option, icon, label, description, inline = false, hint, className }) {
150
+ /** One option of a ListboxPanel: an icon, its label and description, and at its end a hint, a
151
+ * check while it is chosen and a chevron when it opens more. */
152
+ function ListboxOption({ option, icon, label, description, inline = false, hint, checked, submenu = false, className }) {
153
+ const checkable = checked !== void 0;
148
154
  return /* @__PURE__ */ jsxs("div", {
149
155
  ...option,
150
- className: cx(panelItemClassName, inline && "items-baseline", className),
156
+ role: "option",
157
+ tabIndex: -1,
158
+ "aria-checked": checked,
159
+ "data-checked": checked || void 0,
160
+ className: cx(panelItemClassName, inline && "items-baseline", checked && "font-medium", className),
151
161
  children: [
152
162
  icon && /* @__PURE__ */ jsx("span", {
153
163
  className: "flex shrink-0 items-center self-center",
@@ -163,12 +173,25 @@ function ListboxOption({ option, icon, label, description, inline = false, hint,
163
173
  children: description
164
174
  })]
165
175
  }),
166
- hint && /* @__PURE__ */ jsx("span", {
176
+ (hint || checkable || submenu) && /* @__PURE__ */ jsxs("span", {
167
177
  className: "ml-auto flex h-[1lh] shrink-0 items-center gap-2 self-start pl-2",
168
- children: /* @__PURE__ */ jsx("span", {
169
- className: "text-xs tabular-nums text-(--theme-muted-text-color)",
170
- children: hint
171
- })
178
+ children: [
179
+ hint && /* @__PURE__ */ jsx("span", {
180
+ className: "text-xs tabular-nums text-(--theme-muted-text-color)",
181
+ children: hint
182
+ }),
183
+ checkable && (checked || hint) && /* @__PURE__ */ jsx("span", {
184
+ className: cx("flex items-center", hint && "w-3.5"),
185
+ children: checked && /* @__PURE__ */ jsx(CheckIcon, {
186
+ "aria-hidden": "true",
187
+ className: "size-3.5"
188
+ })
189
+ }),
190
+ submenu && /* @__PURE__ */ jsx(ChevronRightIcon, {
191
+ "aria-hidden": "true",
192
+ className: "size-3 opacity-60"
193
+ })
194
+ ]
172
195
  })
173
196
  ]
174
197
  });
@@ -1,15 +1,16 @@
1
1
  import { cx } from "../cx.js";
2
+ import { CodeBlock } from "./CodeBlock.js";
2
3
  import { ExternalLink } from "./ExternalLink.js";
3
4
  import { readIsDark } from "./useCssIsDark.js";
4
5
  import { CODE_THEMES } from "./codeTheme.js";
5
6
  import { rehypeHttpsImagesOnly } from "./httpsImages.js";
6
7
  import { rehypeCodeBreaks } from "./markdownCodeBreaks.js";
7
- import { useEffect, useState } from "react";
8
+ import { useEffect, useMemo, useState } from "react";
8
9
  import { jsx } from "react/jsx-runtime";
9
10
  import { code } from "@streamdown/code";
10
11
  import { math } from "@streamdown/math";
11
12
  import "katex/dist/katex.min.css";
12
- import { Streamdown, defaultRehypePlugins } from "streamdown";
13
+ import { Streamdown, defaultComponents, defaultRehypePlugins, useIsCodeFenceIncomplete } from "streamdown";
13
14
  //#region src/components/MarkdownImpl.tsx
14
15
  var basePlugins = {
15
16
  code,
@@ -35,6 +36,81 @@ function loadMermaid() {
35
36
  }
36
37
  var rehypePlugins = [...Object.values(defaultRehypePlugins), rehypeCodeBreaks];
37
38
  var httpsImagesRehypePlugins = [...Object.entries(defaultRehypePlugins).flatMap(([name, plugin]) => name === "harden" ? [rehypeHttpsImagesOnly, plugin] : [plugin]), rehypeCodeBreaks];
39
+ function codeControls(controls) {
40
+ if (controls === false) return {
41
+ copy: false,
42
+ download: null
43
+ };
44
+ const code = controls === void 0 || controls === true ? true : controls.code;
45
+ if (code === false) return {
46
+ copy: false,
47
+ download: null
48
+ };
49
+ if (code === void 0 || code === true) return {
50
+ copy: true,
51
+ download: "file"
52
+ };
53
+ const { download } = code;
54
+ return {
55
+ copy: code.copy !== false,
56
+ download: download === false ? null : typeof download === "object" ? download.filename : "file"
57
+ };
58
+ }
59
+ var EXTENSIONS = {
60
+ javascript: "js",
61
+ typescript: "ts",
62
+ python: "py",
63
+ ruby: "rb",
64
+ rust: "rs",
65
+ golang: "go",
66
+ bash: "sh",
67
+ shell: "sh",
68
+ shellscript: "sh",
69
+ zsh: "sh",
70
+ console: "sh",
71
+ markdown: "md",
72
+ text: "txt",
73
+ plaintext: "txt",
74
+ csharp: "cs",
75
+ kotlin: "kt",
76
+ powershell: "ps1",
77
+ dockerfile: "Dockerfile"
78
+ };
79
+ function extensionFor(language) {
80
+ const lang = language.toLowerCase();
81
+ return EXTENSIONS[lang] ?? (/^[a-z0-9+]{1,10}$/.test(lang) ? lang : "txt");
82
+ }
83
+ var LANGUAGE = /language-([^\s]+)/;
84
+ var START_LINE = /startLine=(\d+)/;
85
+ var NO_LINE_NUMBERS = /\bnoLineNumbers\b/;
86
+ var DefaultCode = defaultComponents.code;
87
+ function textOf(children) {
88
+ if (typeof children === "string") return children;
89
+ if (Array.isArray(children)) return children.map(textOf).join("");
90
+ if (children && typeof children === "object" && "props" in children) return textOf(children.props.children);
91
+ return "";
92
+ }
93
+ function makeCode(controls, diagrams) {
94
+ return function MarkdownCode(props) {
95
+ const incomplete = useIsCodeFenceIncomplete();
96
+ const language = props.className?.match(LANGUAGE)?.[1] ?? "";
97
+ if (!("data-block" in props) || diagrams && language === "mermaid") return /* @__PURE__ */ jsx(DefaultCode, { ...props });
98
+ const meta = props.node?.properties?.["metastring"];
99
+ const metastring = typeof meta === "string" ? meta : "";
100
+ const start = Number.parseInt(metastring.match(START_LINE)?.[1] ?? "", 10);
101
+ return /* @__PURE__ */ jsx(CodeBlock, {
102
+ className: "my-3.5",
103
+ code: textOf(props.children),
104
+ language,
105
+ copy: controls.copy,
106
+ filename: controls.download === null ? void 0 : `${controls.download}.${extensionFor(language)}`,
107
+ lineNumbers: NO_LINE_NUMBERS.test(metastring) ? false : void 0,
108
+ startLine: start >= 1 ? start : 1,
109
+ maxHeight: 400,
110
+ streaming: incomplete
111
+ });
112
+ };
113
+ }
38
114
  function MarkdownImpl({ children, isStreaming, controls, components, httpsImagesOnly, compact }) {
39
115
  const needsMermaid = !!children && children.includes("```mermaid");
40
116
  const [, setMermaidReady] = useState(() => !!pluginsWithMermaid);
@@ -49,6 +125,16 @@ function MarkdownImpl({ children, isStreaming, controls, components, httpsImages
49
125
  };
50
126
  }, [needsMermaid]);
51
127
  const plugins = needsMermaid && pluginsWithMermaid ? pluginsWithMermaid : basePlugins;
128
+ const { copy, download } = codeControls(controls);
129
+ const diagrams = plugins === pluginsWithMermaid;
130
+ const code = useMemo(() => makeCode({
131
+ copy,
132
+ download
133
+ }, diagrams), [
134
+ copy,
135
+ download,
136
+ diagrams
137
+ ]);
52
138
  return /* @__PURE__ */ jsx("div", {
53
139
  className: cx("markdown whitespace-normal text-start w-full", compact && "markdown-compact"),
54
140
  children: /* @__PURE__ */ jsx(Streamdown, {
@@ -63,6 +149,7 @@ function MarkdownImpl({ children, isStreaming, controls, components, httpsImages
63
149
  rehypePlugins: httpsImagesOnly ? httpsImagesRehypePlugins : rehypePlugins,
64
150
  shikiTheme: CODE_THEMES,
65
151
  components: {
152
+ code,
66
153
  a: ({ node, ...props }) => /* @__PURE__ */ jsx(ExternalLink, {
67
154
  ...props,
68
155
  className: cx("link wrap-break-word", props.className)
@@ -9,6 +9,9 @@ interface MenuActionBase {
9
9
  /** The key that does the same from the page, such as `S` or `⌘K`, in plain text at the
10
10
  * row's end. The menu doesn't bind it; the host does. */
11
11
  shortcut?: string | undefined;
12
+ /** Muted text at the row's end that tells rows apart, such as a team's key `CORE`. It is
13
+ * read with the label, and a menu's search matches it. For a key to press, use `shortcut`. */
14
+ hint?: string | undefined;
12
15
  /** A small mark at the row's end, such as a dot for something unseen. It is hidden from
13
16
  * screen readers, so say what it means in `label`. */
14
17
  badge?: ReactNode;
@@ -121,7 +121,7 @@ function SearchedEntries({ entries, search, query }) {
121
121
  const matches = useQueryMatch();
122
122
  if (!search) return /* @__PURE__ */ jsx(EntryRows, { entries });
123
123
  const needle = query.trim();
124
- const shown = needle ? searchMatches(entries, (a) => matches(needle, a.label, a.description, a.keywords), (submenu) => matches(needle, submenu.label, submenu.keywords)) : entries;
124
+ const shown = needle ? searchMatches(entries, (a) => matches(needle, a.label, a.description, a.keywords, a.hint), (submenu) => matches(needle, submenu.label, submenu.keywords)) : entries;
125
125
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
126
126
  /* @__PURE__ */ jsx("div", {
127
127
  className: "sticky top-0 z-[1] bg-(--theme-app-bg) px-1 pt-0.5 pb-1",
@@ -183,6 +183,7 @@ function RowEnd({ children, className }) {
183
183
  children
184
184
  });
185
185
  }
186
+ var hintClassName = "ps-3 text-xs text-(--theme-muted-text-color) in-data-[size=sm]:text-[11px]";
186
187
  var shortcutClassName = "ps-3 text-xs text-(--theme-muted-text-color) tabular-nums in-data-[size=sm]:text-[11px]";
187
188
  /** A row's icon, label, second line and end details, laid out alike in every kind of row. */
188
189
  function RowContent({ icon, label, description, end }) {
@@ -290,11 +291,19 @@ function ActionRow({ action }) {
290
291
  const runAction = useRunAction();
291
292
  const className = cx(action.description && "items-start", action.destructive && destructiveClassName, action.current && "font-medium");
292
293
  const keyshortcuts = ariaKeyShortcut(action.shortcut);
294
+ const heard = action.hint && /* @__PURE__ */ jsxs("span", {
295
+ className: "sr-only",
296
+ children: [". ", action.hint]
297
+ });
293
298
  const end = (trailing) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
294
299
  action.badge !== void 0 && action.badge !== null && action.badge !== false && /* @__PURE__ */ jsx(RowEnd, {
295
300
  className: "ps-2",
296
301
  children: action.badge
297
302
  }),
303
+ action.hint && /* @__PURE__ */ jsx(RowEnd, {
304
+ className: hintClassName,
305
+ children: action.hint
306
+ }),
298
307
  action.shortcut && /* @__PURE__ */ jsx(RowEnd, {
299
308
  className: shortcutClassName,
300
309
  children: action.shortcut
@@ -314,7 +323,7 @@ function ActionRow({ action }) {
314
323
  className,
315
324
  children: /* @__PURE__ */ jsx(RowContent, {
316
325
  icon: action.icon,
317
- label: action.label,
326
+ label: /* @__PURE__ */ jsxs(Fragment$1, { children: [action.label, heard] }),
318
327
  description: action.description,
319
328
  end: end(action.current ? /* @__PURE__ */ jsx(RowEnd, {
320
329
  className: "ps-2",
@@ -330,10 +339,14 @@ function ActionRow({ action }) {
330
339
  const reason = action.disabled ? action.disabledReason : void 0;
331
340
  const content = (trailing) => /* @__PURE__ */ jsx(RowContent, {
332
341
  icon: action.icon,
333
- label: /* @__PURE__ */ jsxs(Fragment$1, { children: [action.label, reason && /* @__PURE__ */ jsxs("span", {
334
- className: "sr-only",
335
- children: [". ", reason]
336
- })] }),
342
+ label: /* @__PURE__ */ jsxs(Fragment$1, { children: [
343
+ action.label,
344
+ heard,
345
+ reason && /* @__PURE__ */ jsxs("span", {
346
+ className: "sr-only",
347
+ children: [". ", reason]
348
+ })
349
+ ] }),
337
350
  description: action.description,
338
351
  end: end(trailing)
339
352
  });
@@ -1,7 +1,7 @@
1
1
  import { Dialog } from '@base-ui/react/dialog';
2
2
  import { type FormEvent, type ReactNode } from 'react';
3
3
  /** The panel a modal draws by default; pass `panel={false}` to paint your own. */
4
- export declare const modalPanelClassName = "rounded-xl border border-(--theme-border) bg-(--theme-app-bg) text-(--theme-app) shadow-2xl";
4
+ export declare const modalPanelClassName = "rounded-xl border border-(--theme-border) bg-(--theme-panel-bg) text-(--theme-app) shadow-2xl";
5
5
  /** Names the modal for assistive technology; renders an `<h2>`. */
6
6
  export declare const ModalTitle: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react").DialogTitleProps, "ref"> & import("react").RefAttributes<HTMLHeadingElement>>;
7
7
  /** Describes the modal for assistive technology; renders a `<p>`. */