@parallelworks/ui 0.79.0 → 0.81.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 (47) 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/Card.d.ts +16 -1
  10. package/dist/components/Card.js +13 -2
  11. package/dist/components/CodeBlock.d.ts +40 -0
  12. package/dist/components/CodeBlock.js +159 -0
  13. package/dist/components/Combobox.js +56 -32
  14. package/dist/components/ContextMenu.d.ts +8 -0
  15. package/dist/components/ContextMenu.js +42 -6
  16. package/dist/components/CreateModal.d.ts +3 -1
  17. package/dist/components/CreateModal.js +15 -8
  18. package/dist/components/DescriptionList.js +1 -1
  19. package/dist/components/Disclosure.d.ts +6 -2
  20. package/dist/components/Disclosure.js +24 -2
  21. package/dist/components/Drawer.js +1 -1
  22. package/dist/components/Input.d.ts +5 -0
  23. package/dist/components/Input.js +4 -3
  24. package/dist/components/MarkdownImpl.js +89 -2
  25. package/dist/components/MenuEntries.d.ts +3 -0
  26. package/dist/components/MenuEntries.js +19 -6
  27. package/dist/components/Modal.d.ts +1 -1
  28. package/dist/components/Modal.js +6 -5
  29. package/dist/components/PageHeader.d.ts +4 -0
  30. package/dist/components/PageHeader.js +6 -3
  31. package/dist/components/Provider.d.ts +3 -0
  32. package/dist/components/Provider.js +1 -0
  33. package/dist/components/Select.js +1 -1
  34. package/dist/components/ToggleGroup.d.ts +3 -1
  35. package/dist/components/ToggleGroup.js +2 -2
  36. package/dist/components/codeHighlight.d.ts +9 -0
  37. package/dist/components/codeHighlight.js +29 -0
  38. package/dist/components/pageHeaderStore.d.ts +4 -0
  39. package/dist/components/pageHeaderStore.js +5 -2
  40. package/dist/file-explorer/FileExplorer.js +1 -1
  41. package/dist/index.d.ts +3 -2
  42. package/dist/index.js +4 -3
  43. package/dist/styles/theme.css +1 -0
  44. package/dist/styles.css +1 -1
  45. package/dist/theme/derive.d.ts +1 -1
  46. package/dist/theme/derive.js +3 -1
  47. package/package.json +1 -1
@@ -10,7 +10,11 @@ export interface OpenMenuOptions {
10
10
  size?: MenuSize | undefined;
11
11
  /** As `MenuPopup`'s: classes on the menu and its submenus, such as a terminal's colors. */
12
12
  themeClassName?: string | undefined;
13
+ /** What else closes it, for a menu over something that moves under it, such as a
14
+ * terminal's: a wheel outside the menu, the window losing focus, or the window resizing. */
15
+ closeOn?: readonly MenuCloseTrigger[] | undefined;
13
16
  }
17
+ export type MenuCloseTrigger = 'wheel' | 'blur' | 'resize';
14
18
  /** Where a menu opens: an element, such as the button that opened it, or a pointer event,
15
19
  * such as a right-click. An event with Shift held is left to the browser, whose own menu
16
20
  * then shows; any other has its default prevented. */
@@ -29,6 +33,7 @@ export type OpenMenu = (at: MenuOpener, items: MenuEntry[], options?: OpenMenuOp
29
33
  */
30
34
  export declare function useContextMenu(): {
31
35
  openMenu: OpenMenu;
36
+ closeMenu: () => void;
32
37
  contextMenu: import("react").JSX.Element | null;
33
38
  openedBy: Element | null;
34
39
  };
@@ -40,5 +45,8 @@ export declare function ContextMenuProvider({ children }: {
40
45
  /** Opens the app's one shared menu: `openMenu(e, items)` from a right-click, or
41
46
  * `openMenu(button, items)` under a button, which a second press closes. */
42
47
  export declare function useOpenMenu(): OpenMenu;
48
+ /** Closes the app's shared menu, if it is open, for a host whose surface moves out from under
49
+ * it in a way `closeOn` doesn't cover, such as a pane that switches. */
50
+ export declare function useCloseMenu(): () => void;
43
51
  /** The element the shared menu is open under, so its button can say it's expanded. */
44
52
  export declare function useMenuOpenedBy(): Element | null;
@@ -3,7 +3,7 @@ import { MenuPopup } from "./Menu.js";
3
3
  import { isDev } from "../dev.js";
4
4
  import { MenuEntries, MenuFilter } from "./MenuEntries.js";
5
5
  import { Menu } from "@base-ui/react/menu";
6
- import { createContext, use, useCallback, useRef, useState } from "react";
6
+ import { createContext, use, useCallback, useEffect, useRef, useState } from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/components/ContextMenu.tsx
9
9
  /**
@@ -44,6 +44,31 @@ function useContextMenu() {
44
44
  setOpen(true);
45
45
  return true;
46
46
  }, [release]);
47
+ /** Closes the menu, if one is open, as Escape would. */
48
+ const closeMenu = useCallback(() => {
49
+ if (current.current) {
50
+ current.current = null;
51
+ setOpen(false);
52
+ }
53
+ }, []);
54
+ const closeOn = open ? opened?.closeOn : void 0;
55
+ useEffect(() => {
56
+ if (!closeOn?.length) return;
57
+ const onWheel = (event) => {
58
+ if (!(event.target instanceof Element && event.target.closest("[role=\"menu\"]"))) closeMenu();
59
+ };
60
+ const listeners = [];
61
+ if (closeOn.includes("wheel")) listeners.push(["wheel", onWheel]);
62
+ if (closeOn.includes("blur")) listeners.push(["blur", closeMenu]);
63
+ if (closeOn.includes("resize")) listeners.push(["resize", closeMenu]);
64
+ for (const [type, listener] of listeners) window.addEventListener(type, listener, {
65
+ capture: true,
66
+ passive: true
67
+ });
68
+ return () => {
69
+ for (const [type, listener] of listeners) window.removeEventListener(type, listener, { capture: true });
70
+ };
71
+ }, [closeOn, closeMenu]);
47
72
  const onOpenChange = useCallback((next, details) => {
48
73
  if (next) return;
49
74
  const element = current.current?.element;
@@ -61,6 +86,7 @@ function useContextMenu() {
61
86
  }, [release]);
62
87
  return {
63
88
  openMenu,
89
+ closeMenu,
64
90
  contextMenu: opened && /* @__PURE__ */ jsx(MenuFilter, {
65
91
  search: opened.search,
66
92
  children: (query) => /* @__PURE__ */ jsx(Menu.Root, {
@@ -85,15 +111,19 @@ function useContextMenu() {
85
111
  }
86
112
  var OpenMenuContext = createContext(null);
87
113
  var OpenedByContext = createContext(null);
114
+ var CloseMenuContext = createContext(null);
88
115
  /** The one menu every row, button and canvas below it opens through `useOpenMenu`, so none
89
116
  * renders a menu of its own. `UIProvider` mounts it; nothing else needs to. */
90
117
  function ContextMenuProvider({ children }) {
91
- const { openMenu, contextMenu, openedBy } = useContextMenu();
118
+ const { openMenu, closeMenu, contextMenu, openedBy } = useContextMenu();
92
119
  return /* @__PURE__ */ jsx(OpenMenuContext, {
93
120
  value: openMenu,
94
- children: /* @__PURE__ */ jsxs(OpenedByContext, {
95
- value: openedBy,
96
- children: [children, contextMenu]
121
+ children: /* @__PURE__ */ jsx(CloseMenuContext, {
122
+ value: closeMenu,
123
+ children: /* @__PURE__ */ jsxs(OpenedByContext, {
124
+ value: openedBy,
125
+ children: [children, contextMenu]
126
+ })
97
127
  })
98
128
  });
99
129
  }
@@ -106,9 +136,15 @@ var warnNoProvider = () => {
106
136
  function useOpenMenu() {
107
137
  return use(OpenMenuContext) ?? warnNoProvider;
108
138
  }
139
+ var noMenu = () => {};
140
+ /** Closes the app's shared menu, if it is open, for a host whose surface moves out from under
141
+ * it in a way `closeOn` doesn't cover, such as a pane that switches. */
142
+ function useCloseMenu() {
143
+ return use(CloseMenuContext) ?? noMenu;
144
+ }
109
145
  /** The element the shared menu is open under, so its button can say it's expanded. */
110
146
  function useMenuOpenedBy() {
111
147
  return use(OpenedByContext);
112
148
  }
113
149
  //#endregion
114
- export { ContextMenuProvider, useContextMenu, useMenuOpenedBy, useOpenMenu };
150
+ export { ContextMenuProvider, useCloseMenu, useContextMenu, useMenuOpenedBy, useOpenMenu };
@@ -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,6 +6,7 @@ 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";
@@ -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);
@@ -82,11 +84,11 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
82
84
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleSubmit(true);
83
85
  },
84
86
  onMouseDown: (e) => {
85
- if (e.target instanceof Element && !e.target.closest("input, textarea, button, a, select, label, code, pre, .select-all")) e.preventDefault();
87
+ 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
88
  },
87
89
  footer: /* @__PURE__ */ jsxs(Fragment$1, { children: [
88
90
  (showFavorite || footerHint || error) && /* @__PURE__ */ jsxs("div", {
89
- className: "me-auto flex min-w-0 items-center gap-3",
91
+ className: cx("me-auto flex min-w-0 items-center gap-3", !showFavorite && !error && "pointer-coarse:hidden"),
90
92
  children: [showFavorite && /* @__PURE__ */ jsx(Tooltip, {
91
93
  content: favorite ? t.favoriteHintOn : t.favoriteHintOff,
92
94
  children: /* @__PURE__ */ jsxs(Toggle, {
@@ -100,7 +102,7 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
100
102
  children: [favorite ? /* @__PURE__ */ jsx(FavoriteIcon, { className: "text-(--theme-favorite)" }) : /* @__PURE__ */ jsx(FavoriteOutlineIcon, {}), favorite ? t.favorited : t.favorite]
101
103
  })
102
104
  }), error ? /* @__PURE__ */ jsx(ErrorText, { children: error }) : footerHint && /* @__PURE__ */ jsx("p", {
103
- className: "m-0 min-w-0 text-xs text-(--theme-muted-text-color)",
105
+ className: "m-0 min-w-0 text-xs text-(--theme-muted-text-color) pointer-coarse:hidden",
104
106
  children: footerHint
105
107
  })]
106
108
  }),
@@ -141,16 +143,21 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
141
143
  children: [typeIcon, typeLabel]
142
144
  }),
143
145
  hideName ? /* @__PURE__ */ jsx("div", { className: "mt-3" }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
144
- /* @__PURE__ */ jsx("input", {
145
- ref: nameRef,
146
- type: "text",
146
+ /* @__PURE__ */ jsx("textarea", {
147
+ ref: (element) => {
148
+ nameRef.current = element;
149
+ nameSizer.current = element;
150
+ },
151
+ rows: 1,
147
152
  autoComplete: "off",
148
153
  "data-1p-ignore": true,
149
154
  value: name,
150
155
  onChange: (e) => {
151
- const next = sanitizeName ? sanitizeName(e.target.value) : e.target.value;
156
+ const joined = e.target.value.replace(/\s*\n+\s*/g, " ");
157
+ const next = sanitizeName ? sanitizeName(joined) : joined;
152
158
  setName(next);
153
159
  onNameChange?.(next);
160
+ resizeName?.();
154
161
  },
155
162
  onBlur: (e) => {
156
163
  if (!e.target.value.trim()) setNameTouched(true);
@@ -168,7 +175,7 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
168
175
  "aria-invalid": nameMissing || !!nameError,
169
176
  "aria-describedby": nameError || hint ? nameMessageId : void 0,
170
177
  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")
178
+ 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
179
  }),
173
180
  nameError && /* @__PURE__ */ jsx("p", {
174
181
  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";
@@ -1,4 +1,4 @@
1
- import type { MouseEventHandler, ReactNode } from 'react';
1
+ import { type MouseEventHandler, type ReactNode } from 'react';
2
2
  /** The height a collapsible panel animates as it opens and closes. It clips
3
3
  * only while it moves, so focus rings and overflowing content show once open. */
4
4
  export declare const collapsePanelClassName = "h-(--collapsible-panel-height) transition-[height,opacity] duration-200 ease-out motion-reduce:transition-none data-starting-style:h-0 data-starting-style:overflow-hidden data-starting-style:opacity-0 data-ending-style:h-0 data-ending-style:overflow-hidden data-ending-style:opacity-0";
@@ -23,6 +23,10 @@ export interface DisclosureProps {
23
23
  /** Controls beside the summary, outside the toggle, so a row can hold buttons without
24
24
  * nesting them in it. */
25
25
  actions?: ReactNode;
26
+ /** Controls that read as part of the summary, right after it on its line, such as a button
27
+ * that says why a row failed. They sit outside the toggle, so no button holds another and
28
+ * each is its own stop for Tab; a click on the rest of the line still toggles. */
29
+ summaryControls?: ReactNode;
26
30
  /** On the whole disclosure, summary and detail, such as a row's own menu. */
27
31
  onContextMenu?: MouseEventHandler<HTMLDivElement> | undefined;
28
32
  children: ReactNode;
@@ -30,7 +34,7 @@ export interface DisclosureProps {
30
34
  /** A line inside content that reveals more detail beneath it, such as a tool
31
35
  * call's arguments or a model's reasoning. For a titled part of a page or
32
36
  * form, which belongs in the page's outline, use Section. */
33
- export declare function Disclosure({ summary, open, onOpenChange, defaultOpen, disabled, keepMounted, className, summaryClassName, panelClassName, chevron, actions, onContextMenu, children, }: DisclosureProps): import("react").JSX.Element;
37
+ export declare function Disclosure({ summary, open, onOpenChange, defaultOpen, disabled, keepMounted, className, summaryClassName, panelClassName, chevron, actions, summaryControls, onContextMenu, children, }: DisclosureProps): import("react").JSX.Element;
34
38
  /** The revealing panel alone, for a toggle that lives elsewhere, such as a
35
39
  * tree row's chevron that names this panel with `aria-controls`. */
36
40
  export declare function DisclosurePanel({ open, id, keepMounted, className, children, }: {
@@ -1,5 +1,6 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { ChevronRightIcon } from "../icons.js";
3
+ import { useRef } from "react";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  import { Collapsible } from "@base-ui/react/collapsible";
5
6
  //#region src/components/Disclosure.tsx
@@ -9,8 +10,11 @@ var collapsePanelClassName = "h-(--collapsible-panel-height) transition-[height,
9
10
  /** A line inside content that reveals more detail beneath it, such as a tool
10
11
  * call's arguments or a model's reasoning. For a titled part of a page or
11
12
  * form, which belongs in the page's outline, use Section. */
12
- function Disclosure({ summary, open, onOpenChange, defaultOpen = false, disabled = false, keepMounted = false, className, summaryClassName, panelClassName, chevron = true, actions, onContextMenu, children }) {
13
+ function Disclosure({ summary, open, onOpenChange, defaultOpen = false, disabled = false, keepMounted = false, className, summaryClassName, panelClassName, chevron = true, actions, summaryControls, onContextMenu, children }) {
14
+ const trigger = useRef(null);
15
+ const hasControls = summaryControls !== void 0 && summaryControls !== null && summaryControls !== false;
13
16
  const toggle = /* @__PURE__ */ jsxs(Collapsible.Trigger, {
17
+ ref: trigger,
14
18
  className: cx("group/disclosure flex min-w-0 items-start gap-2 rounded-sm text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--theme-focus)", disabled ? "cursor-default" : "cursor-pointer", summaryClassName),
15
19
  children: [summary, !disabled && chevron && /* @__PURE__ */ jsx(ChevronRightIcon, {
16
20
  "aria-hidden": "true",
@@ -24,7 +28,25 @@ function Disclosure({ summary, open, onOpenChange, defaultOpen = false, disabled
24
28
  disabled,
25
29
  className,
26
30
  onContextMenu,
27
- children: [actions ? /* @__PURE__ */ jsxs("div", {
31
+ children: [hasControls ? /* @__PURE__ */ jsxs("div", {
32
+ className: "flex min-w-0 items-center gap-2",
33
+ children: [
34
+ toggle,
35
+ /* @__PURE__ */ jsx("div", {
36
+ className: "flex shrink-0 items-center gap-1",
37
+ children: summaryControls
38
+ }),
39
+ /* @__PURE__ */ jsx("div", {
40
+ "aria-hidden": "true",
41
+ className: cx("min-w-0 flex-1 self-stretch", !disabled && "cursor-pointer"),
42
+ onClick: () => trigger.current?.click()
43
+ }),
44
+ actions && /* @__PURE__ */ jsx("div", {
45
+ className: "flex shrink-0 items-center gap-1",
46
+ children: actions
47
+ })
48
+ ]
49
+ }) : actions ? /* @__PURE__ */ jsxs("div", {
28
50
  className: "flex min-w-0 items-center gap-2",
29
51
  children: [/* @__PURE__ */ jsx("div", {
30
52
  className: "flex min-w-0 flex-1",
@@ -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 };
@@ -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>`. */
@@ -10,7 +10,8 @@ import { AlertDialog } from "@base-ui/react/alert-dialog";
10
10
  import { Dialog } from "@base-ui/react/dialog";
11
11
  //#region src/components/Modal.tsx
12
12
  /** The panel a modal draws by default; pass `panel={false}` to paint your own. */
13
- var modalPanelClassName = "rounded-xl border border-(--theme-border) bg-(--theme-app-bg) text-(--theme-app) shadow-2xl";
13
+ var modalPanelClassName = "rounded-xl border border-(--theme-border) bg-(--theme-panel-bg) text-(--theme-app) shadow-2xl";
14
+ var phoneFooterClassName = "max-sm:flex-wrap max-sm:[&>:is(button,a)]:h-10 max-sm:[&>:is(button,a)]:flex-1 max-sm:[&>:not(button,a)]:basis-full";
14
15
  /** Names the modal for assistive technology; renders an `<h2>`. */
15
16
  var ModalTitle = Dialog.Title;
16
17
  /** Describes the modal for assistive technology; renders a `<p>`. */
@@ -32,7 +33,7 @@ function firstControlIn(panel) {
32
33
  * and focus returns to where it was on close. Give it a `title`, a `description`
33
34
  * and a `footer` for the standard layout, as a Drawer has, or lay out the whole
34
35
  * panel with the children. */
35
- function Modal({ open, onClose, title, description, footer, size, alert = false, dismissible = true, align = "top", panel = true, backdropClassName = "bg-black/40", closeButton = false, onSubmit, className, children, initialFocus, ...popupProps }) {
36
+ function Modal({ open, onClose, title, description, footer, size, alert = false, dismissible = true, align = "top", panel = true, backdropClassName = "bg-(--theme-backdrop)", closeButton = false, onSubmit, className, children, initialFocus, ...popupProps }) {
36
37
  const portal = usePopupContainer();
37
38
  const { common } = useStrings();
38
39
  const panelElement = useRef(null);
@@ -67,14 +68,14 @@ function Modal({ open, onClose, title, description, footer, size, alert = false,
67
68
  children
68
69
  }),
69
70
  footer !== void 0 && /* @__PURE__ */ jsx("div", {
70
- className: "flex shrink-0 items-center justify-end gap-2 border-t border-(--theme-border) px-4 py-3",
71
+ className: cx("flex shrink-0 items-center justify-end gap-2 border-t border-(--theme-border) px-4 py-3", phoneFooterClassName),
71
72
  children: footer
72
73
  })
73
74
  ] });
74
75
  const content = /* @__PURE__ */ jsxs(Dialog.Portal, {
75
76
  ...portal,
76
77
  children: [/* @__PURE__ */ jsx(Dialog.Backdrop, { className: cx("fixed inset-0 z-(--layer-modal)", backdropMotion, backdropClassName) }), /* @__PURE__ */ jsx(Dialog.Viewport, {
77
- className: cx("fixed inset-0 z-(--layer-modal) flex justify-center px-4 pt-20 pb-8", align === "center" ? "items-center" : "items-start sm:pt-[max(14vh,5rem)]"),
78
+ className: cx("fixed inset-0 z-(--layer-modal) flex justify-center px-4 pt-20 pb-8 max-sm:px-2 max-sm:pb-[max(8px,env(safe-area-inset-bottom))]", align === "center" ? "items-center" : "items-start sm:pt-[max(14vh,5rem)]"),
78
79
  children: /* @__PURE__ */ jsx(Dialog.Popup, {
79
80
  ...popupProps,
80
81
  ref: (element) => {
@@ -84,7 +85,7 @@ function Modal({ open, onClose, title, description, footer, size, alert = false,
84
85
  else if (forward) forward.current = element;
85
86
  },
86
87
  initialFocus: initialFocus ?? (closeButton ? () => firstControlIn(panelElement.current) : void 0),
87
- className: cx(panel && "rounded-xl border border-(--theme-border) bg-(--theme-app-bg) text-(--theme-app) shadow-2xl", size && [sizeClasses[size], "max-w-full"], laidOut && "flex max-h-full flex-col", "outline-none", popupMotion, className),
88
+ className: cx(panel && "rounded-xl border border-(--theme-border) bg-(--theme-panel-bg) text-(--theme-app) shadow-2xl", size && [sizeClasses[size], "max-w-full"], panel && (size || laidOut) && "max-sm:w-full", laidOut && "flex max-h-full flex-col", "outline-none", popupMotion, className),
88
89
  children: !laidOut ? children : onSubmit ? /* @__PURE__ */ jsx("form", {
89
90
  className: "flex min-h-0 flex-1 flex-col",
90
91
  onSubmit: (event) => {
@@ -13,6 +13,10 @@ export interface PageHeaderProps {
13
13
  * `h1`. A crumb with an empty label, such as a name still loading, shows a
14
14
  * placeholder. */
15
15
  breadcrumbs?: readonly Breadcrumb[] | undefined;
16
+ /** False keeps the last crumb out of the page's outline, for a page whose `h1` is in its
17
+ * body, such as a title field or a section's heading: the crumb still says it is the
18
+ * current page, and the page has one `h1`. */
19
+ heading?: boolean | undefined;
16
20
  /** Beside the title: actions on the page's subject, such as edit, rename or
17
21
  * favorite, or its `PageHeaderMenu`. */
18
22
  titleActions?: ReactNode;