@parallelworks/ui 0.80.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 (42) 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/CreateModal.d.ts +3 -1
  14. package/dist/components/CreateModal.js +15 -8
  15. package/dist/components/DescriptionList.js +1 -1
  16. package/dist/components/Drawer.js +1 -1
  17. package/dist/components/Input.d.ts +5 -0
  18. package/dist/components/Input.js +4 -3
  19. package/dist/components/MarkdownImpl.js +89 -2
  20. package/dist/components/MenuEntries.d.ts +3 -0
  21. package/dist/components/MenuEntries.js +19 -6
  22. package/dist/components/Modal.d.ts +1 -1
  23. package/dist/components/Modal.js +6 -5
  24. package/dist/components/PageHeader.d.ts +4 -0
  25. package/dist/components/PageHeader.js +6 -3
  26. package/dist/components/Provider.d.ts +3 -0
  27. package/dist/components/Provider.js +1 -0
  28. package/dist/components/Select.js +1 -1
  29. package/dist/components/ToggleGroup.d.ts +3 -1
  30. package/dist/components/ToggleGroup.js +2 -2
  31. package/dist/components/codeHighlight.d.ts +9 -0
  32. package/dist/components/codeHighlight.js +29 -0
  33. package/dist/components/pageHeaderStore.d.ts +4 -0
  34. package/dist/components/pageHeaderStore.js +5 -2
  35. package/dist/file-explorer/FileExplorer.js +1 -1
  36. package/dist/index.d.ts +2 -1
  37. package/dist/index.js +3 -2
  38. package/dist/styles/theme.css +1 -0
  39. package/dist/styles.css +1 -1
  40. package/dist/theme/derive.d.ts +1 -1
  41. package/dist/theme/derive.js +3 -1
  42. package/package.json +1 -1
package/README.md CHANGED
@@ -131,7 +131,8 @@ Each need has one component. Reach for it before styling markup by hand.
131
131
  | A live relative time or duration | `RelativeTime` (with a `fallback`, an `exact` style for its tooltip, or `show="absolute"`), `Elapsed` |
132
132
  | Fetching again on demand | `RefreshButton`; `variant="inline"` in a line of details; `loading` on an `ActionToolbar` action |
133
133
  | Rendered Markdown | `Markdown` |
134
- | Code or YAML to read or edit | `Editor` from `./editor`; `EditorField` as a form field, `FormCodePanel` for a form's values as JSON |
134
+ | Code or YAML to read, such as a run's resolved workflow | `CodeBlock`: highlighted, numbered, with copy and, given a `filename`, download; `fill` for a view that is the code |
135
+ | Code or YAML to edit | `Editor` from `./editor`; `EditorField` as a form field, `FormCodePanel` for a form's values as JSON |
135
136
  | A run's jobs, steps, logs, annotations and warnings | `DependencyGraph` from `./graph`, as a graph or a list |
136
137
  | A workflow's jobs drawn from its YAML, or edited by hand | `DependencyGraphPreview` from `./graph`, editable with an `editor`; `InputsFormEditor` for its run form |
137
138
  | A value that survives a reload | `usePersistedState` |
@@ -168,6 +169,11 @@ Some pairs look alike and are split on purpose:
168
169
  its tone, for anything. A `RunStatusIcon` shows the status of something that
169
170
  ran as an icon beside its name, and turns while it runs; `runStatusTone` gives
170
171
  a badge for the same status the same tone.
172
+ - **CodeBlock or CopyCodeBlock.** A `CodeBlock` is code to read, such as a
173
+ file or a workflow: a header names it, its lines are highlighted and
174
+ numbered, and it can fill a pane. A `CopyCodeBlock` is a command or a value
175
+ to copy, such as an install line or a new API key, with its copy button over
176
+ it and `secret` to mask it.
171
177
  - **Select or Combobox.** A `Select` picks from a short list it shows in full.
172
178
  A `Combobox` filters as you type, for a list long enough to search, and is the
173
179
  one that takes several values, new ones, or free text (`Autocomplete`).
@@ -29,6 +29,7 @@ export interface ToolApprovalAnswer {
29
29
  allowed: boolean;
30
30
  feedback?: string;
31
31
  tool?: string;
32
+ args?: string;
32
33
  }
33
34
  export interface StreamHandlers {
34
35
  onContent: (content: string) => void;
package/dist/ai/ai.css CHANGED
@@ -46,8 +46,8 @@
46
46
  padding: 0;
47
47
  }
48
48
 
49
- /* Inline code — code blocks are styled by Streamdown. A tint of the ink and
50
- * no border: a hard outline turns every identifier into a keycap. */
49
+ /* Inline code; a fenced block is a CodeBlock, which styles itself. A tint of
50
+ * the ink and no border: a hard outline turns every identifier into a keycap. */
51
51
  .markdown code {
52
52
  /* rem, not em: a table cell or list already sized down would size it twice. */
53
53
  font-size: 0.875rem;
@@ -226,95 +226,6 @@
226
226
  }
227
227
  }
228
228
 
229
- /* Streamdown code blocks: one surface, not a frame around an inset panel.
230
- * The theme's code surface, which the syntax colors are solved against, with a
231
- * hairline edge holds the language on its top edge,
232
- * the copy and download controls quiet beside it, and the code on the same
233
- * ground, so the block reads as part of the answer. */
234
- .markdown [data-streamdown="code-block"] {
235
- gap: 0;
236
- margin: 0.875rem 0;
237
- padding: 0;
238
- border: 0;
239
- border-radius: 0.75rem;
240
- overflow: hidden;
241
- background: var(--theme-code-bg, #f3f4f6);
242
- box-shadow: 0 0 0 1px color-mix(in oklab, var(--theme-app, #000) 8%, transparent);
243
- }
244
- .markdown [data-streamdown="code-block-header"] {
245
- height: 2.25rem;
246
- padding: 0 0.75rem 0 0.75rem;
247
- font-size: 0.75rem;
248
- color: var(--theme-muted-text-color, #6a7282);
249
- }
250
- .markdown [data-streamdown="code-block-actions"] {
251
- gap: 0.125rem;
252
- padding: 0;
253
- border: 0;
254
- background: transparent;
255
- backdrop-filter: none;
256
- }
257
- .markdown [data-streamdown="code-block-actions"] button {
258
- padding: 0.3125rem;
259
- border-radius: 0.375rem;
260
- color: var(--theme-muted-text-color, #6a7282);
261
- transition:
262
- background-color 150ms ease-out,
263
- color 150ms ease-out;
264
- }
265
- .markdown [data-streamdown="code-block-actions"] button:hover {
266
- color: var(--theme-app, #000);
267
- background: color-mix(in oklab, var(--theme-app, #000) 7%, transparent);
268
- }
269
- .markdown [data-streamdown="code-block-body"] {
270
- border: 0;
271
- border-radius: 0;
272
- padding: 0;
273
- background: transparent;
274
- }
275
- .markdown [data-streamdown="code-block-body"] pre {
276
- margin: 0;
277
- padding: 0.125rem 1rem 0.875rem;
278
- overflow-x: auto;
279
- /* Code reads looser than prose; Streamdown's text-sm line is too tight. */
280
- line-height: 1.6;
281
- }
282
-
283
- /* Shiki dual-theme color overrides (standard Shiki approach).
284
- Streamdown uses Tailwind dark: arbitrary-value classes for theme switching,
285
- but Tailwind v4's source scanner can misparse them from minified JS —
286
- generating broken selectors that apply dark bg unconditionally while
287
- missing the text color classes entirely. Override both light and dark
288
- explicitly to ensure correct colors in all modes.
289
- See: https://shiki.style/guide/dual-themes#class-based-dark-mode */
290
-
291
- /* Light mode: the primary Shiki theme's colors (--sdm-c) on the block's own
292
- surface, which stands in for the theme's background. */
293
- .markdown [data-streamdown="code-block-body"] pre {
294
- /* biome-ignore lint/complexity/noImportantStyles: the highlighter sets the background as an inline style */
295
- background-color: transparent !important;
296
- color: var(--sdm-fg, inherit);
297
- }
298
- .markdown [data-streamdown="code-block-body"] pre code > span > span {
299
- color: var(--sdm-c, inherit);
300
- }
301
-
302
- /* Dark mode: the --shiki-dark colors, again on the block's surface. */
303
- .dark .markdown [data-streamdown="code-block-body"] pre {
304
- /* biome-ignore lint/complexity/noImportantStyles: the highlighter sets the background as an inline style */
305
- background-color: transparent !important;
306
- color: var(--shiki-dark, var(--sdm-fg, inherit));
307
- }
308
- .dark .markdown [data-streamdown="code-block-body"] pre code > span > span {
309
- color: var(--shiki-dark, inherit);
310
- font-style: var(--shiki-dark-font-style, inherit);
311
- font-weight: var(--shiki-dark-font-weight, inherit);
312
- text-decoration: var(--shiki-dark-text-decoration, inherit);
313
- }
314
- .dark .markdown [data-streamdown="code-block-body"] pre code > span > span[style*="--sdm-tbg"] {
315
- background-color: var(--shiki-dark-bg, var(--sdm-tbg));
316
- }
317
-
318
229
  /* Remove prose backtick pseudo-elements on inline code */
319
230
  .markdown code::before,
320
231
  .markdown code::after {
@@ -531,7 +442,7 @@
531
442
  /* A paste in the reader's bubble is a code block like the assistant's, set on
532
443
  * the panel so it stands off the bubble's tint, with the bubble's spacing
533
444
  * rather than the answer's. */
534
- .chat-paste-code .markdown [data-streamdown="code-block"] {
445
+ .chat-paste-code .markdown [data-code-block] {
535
446
  margin: 0;
536
447
  background: var(--theme-panel-bg, #fff);
537
448
  white-space: normal;
@@ -302,6 +302,7 @@ function decisionLabel(d, t) {
302
302
  switch (d.status) {
303
303
  case "approved":
304
304
  if (d.by === "auto-reviewer") return t.allowedByReviewer;
305
+ if (d.edited) return d.name ? t.allowedWithChangesBy(d.name) : t.allowedWithChanges;
305
306
  return d.name ? t.allowedBy(d.name) : t.allowed;
306
307
  case "denied":
307
308
  if (d.by === "auto-reviewer") return t.deniedByReviewer;
@@ -627,6 +627,9 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
627
627
  id: approvalId,
628
628
  answer: answer.allowed ? t.approvalAllowed : t.approvalDenied
629
629
  });
630
+ const instead = answer.allowed ? answer.instead : void 0;
631
+ const asked = pausedParts.find((p) => p.kind === "tool_call" && p.id === approvalId);
632
+ const args = answer.allowed && answer.args !== asked?.args ? answer.args : void 0;
630
633
  applyToPaused({
631
634
  type: "tool_decision",
632
635
  id: approvalId,
@@ -634,14 +637,15 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
634
637
  status: answer.allowed ? "approved" : "denied",
635
638
  by: "user",
636
639
  name: currentUser.name || currentUser.username,
637
- at: DateTime.now().toUTC().toISO()
640
+ at: DateTime.now().toUTC().toISO(),
641
+ ...args !== void 0 ? { edited: true } : {}
638
642
  }
639
643
  });
640
- const instead = answer.allowed ? answer.instead : void 0;
641
- if (instead) {
644
+ if (instead || args !== void 0) {
642
645
  pausedParts = pausedParts.map((p) => p.kind === "tool_call" && p.id === approvalId ? {
643
646
  ...p,
644
- name: instead
647
+ ...instead ? { name: instead } : {},
648
+ ...args !== void 0 ? { args } : {}
645
649
  } : p);
646
650
  updatePaused({ parts: pausedParts });
647
651
  }
@@ -661,7 +665,8 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
661
665
  toolCallId: approvalId,
662
666
  allowed: answer.allowed,
663
667
  ...answer.denyMessage ? { feedback: answer.denyMessage } : {},
664
- ...instead ? { tool: instead } : {}
668
+ ...instead ? { tool: instead } : {},
669
+ ...args !== void 0 ? { args } : {}
665
670
  }
666
671
  }, paused.id, (message) => updatePaused({ error: message }), (delta) => {
667
672
  if (delta.type !== "tool_end" || delta.id !== approvalId) return false;
@@ -245,8 +245,11 @@ export interface ChatStrings {
245
245
  answered: string;
246
246
  /** Under a call someone answered: who allowed or denied it. */
247
247
  allowedBy: (name: string) => string;
248
+ /** Allowed after the reader changed the call's arguments. */
249
+ allowedWithChangesBy: (name: string) => string;
248
250
  deniedBy: (name: string) => string;
249
251
  allowed: string;
252
+ allowedWithChanges: string;
250
253
  denied: string;
251
254
  allowedByReviewer: string;
252
255
  deniedByReviewer: string;
@@ -258,8 +258,10 @@ var defaultChatStrings = {
258
258
  feedbackPlaceholder: "Tell the agent what to do instead…",
259
259
  answered: "Answered",
260
260
  allowedBy: (name) => `Allowed by ${name}`,
261
+ allowedWithChangesBy: (name) => `Allowed with changes by ${name}`,
261
262
  deniedBy: (name) => `Denied by ${name}`,
262
263
  allowed: "Allowed",
264
+ allowedWithChanges: "Allowed with changes",
263
265
  denied: "Denied",
264
266
  allowedByReviewer: "Allowed by the auto-reviewer",
265
267
  deniedByReviewer: "Denied by the auto-reviewer",
@@ -67,6 +67,8 @@ export interface ToolDecision {
67
67
  note?: string;
68
68
  /** Why it was settled this way, such as the auto-reviewer's rationale. */
69
69
  reason?: string;
70
+ /** The reader changed the call's arguments before allowing it. */
71
+ edited?: boolean;
70
72
  }
71
73
  export interface ToolCallPart {
72
74
  kind: 'tool_call';
@@ -103,6 +105,9 @@ export type ApprovalAnswerValue = {
103
105
  /** A sibling tool to run in place of the one asked for, with the same
104
106
  * arguments, such as destroying a cluster the model asked to stop. */
105
107
  instead?: string;
108
+ /** The call's arguments as the reader changed them, as JSON text, to
109
+ * run in place of the model's suggestion. */
110
+ args?: string;
106
111
  } | {
107
112
  kind: 'ask';
108
113
  text: string;
@@ -12,6 +12,15 @@ declare const paddingClasses: {
12
12
  md: string;
13
13
  lg: string;
14
14
  };
15
+ declare const toneColors: {
16
+ readonly accent: 'var(--theme-accent)';
17
+ readonly success: 'var(--theme-success)';
18
+ readonly warning: 'var(--theme-warning)';
19
+ readonly error: 'var(--theme-error)';
20
+ readonly info: 'var(--theme-info)';
21
+ };
22
+ /** A status's tone, or any CSS color a host picks, such as `var(--agent-color)`. */
23
+ export type CardTone = keyof typeof toneColors | (string & {});
15
24
  export type CardSize = keyof typeof widthClasses;
16
25
  export type CardPadding = keyof typeof paddingClasses;
17
26
  export interface CardProps extends LinkTarget {
@@ -32,6 +41,12 @@ export interface CardProps extends LinkTarget {
32
41
  /** Tints the border while a field inside has focus, for a card that frames an
33
42
  * input, such as a composer. */
34
43
  focusWithin?: boolean | undefined;
44
+ /** Tints the border in a color, setting the card apart by what it is, such as the work of
45
+ * one kind of author: a status's tone, or any CSS color, such as `var(--agent-color)`.
46
+ * It is `--card-tone` inside the card, for its own content to use. `highlight` deepens it. */
47
+ tone?: CardTone | undefined;
48
+ /** With `tone`, a faint fill of it too, for a card that should stand out at a glance. */
49
+ tinted?: boolean | undefined;
35
50
  /** Renders the card as this element, such as a button, a list or a section. */
36
51
  render?: ReactElement | undefined;
37
52
  /** The whole card presses: it lifts on hover and shows focus. On by default for
@@ -43,7 +58,7 @@ export interface CardProps extends LinkTarget {
43
58
  /** A surface holding one task, such as a sign-in form or a choice among a few
44
59
  * options, raised over the page; or, with `size="fill"`, a flat panel among the
45
60
  * page's content. */
46
- export declare function Card({ children, size, padding, divided, elevated, highlight, focusWithin, render: renderProp, interactive, href, reloadDocument, target, className, }: CardProps): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
61
+ export declare function Card({ children, size, padding, divided, elevated, highlight, focusWithin, tone, tinted, render: renderProp, interactive, href, reloadDocument, target, className, }: CardProps): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
47
62
  export interface CardHeaderProps {
48
63
  badge?: ReactNode;
49
64
  title: string;
@@ -21,10 +21,17 @@ var sizePadding = {
21
21
  lg: "p-6 sm:p-8",
22
22
  fill: paddingClasses.md
23
23
  };
24
+ var toneColors = {
25
+ accent: "var(--theme-accent)",
26
+ success: "var(--theme-success)",
27
+ warning: "var(--theme-warning)",
28
+ error: "var(--theme-error)",
29
+ info: "var(--theme-info)"
30
+ };
24
31
  /** A surface holding one task, such as a sign-in form or a choice among a few
25
32
  * options, raised over the page; or, with `size="fill"`, a flat panel among the
26
33
  * page's content. */
27
- function Card({ children, size = "md", padding, divided = false, elevated = false, highlight = false, focusWithin = false, render: renderProp, interactive, href, reloadDocument, target, className }) {
34
+ function Card({ children, size = "md", padding, divided = false, elevated = false, highlight = false, focusWithin = false, tone, tinted = false, render: renderProp, interactive, href, reloadDocument, target, className }) {
28
35
  const render = href === void 0 ? renderProp : /* @__PURE__ */ jsx(AppLink, {
29
36
  href,
30
37
  reloadDocument,
@@ -32,10 +39,14 @@ function Card({ children, size = "md", padding, divided = false, elevated = fals
32
39
  });
33
40
  const fill = size === "fill";
34
41
  const pressable = interactive ?? render !== void 0;
42
+ const toned = tone !== void 0;
43
+ const toneColor = toned ? toneColors[tone] ?? tone : void 0;
44
+ const fillTint = toned && tinted;
35
45
  return useRender({
36
46
  render: render ?? /* @__PURE__ */ jsx("div", {}),
37
47
  props: {
38
- className: cx("block w-full rounded-xl border bg-(--theme-panel-bg)", highlight ? "border-(--theme-accent)/30" : "border-(--theme-border)", focusWithin && "focus-within:border-(--theme-focus)/60", !fill && "backdrop-blur-xl", !fill && (elevated ? "shadow-(--theme-shadow-xl)" : "shadow-(--theme-shadow)"), widthClasses[size], padding === void 0 ? sizePadding[size] : paddingClasses[padding], divided && "divide-y divide-(--theme-border)", pressable && "cursor-pointer text-left text-inherit no-underline transition-[background-color,box-shadow,border-color] duration-150 hover:bg-(--theme-panel-hover) hover:shadow-(--theme-shadow-lg) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--theme-link) motion-reduce:transition-none", !pressable && focusWithin && "transition-[border-color] duration-150 motion-reduce:transition-none", className),
48
+ style: toneColor ? { "--card-tone": toneColor } : void 0,
49
+ className: cx("block w-full rounded-xl border", fillTint ? "bg-[color-mix(in_oklab,var(--card-tone)_7%,var(--theme-panel-bg))]" : "bg-(--theme-panel-bg)", toned ? highlight ? "border-(--card-tone)/80" : "border-(--card-tone)/45" : highlight ? "border-(--theme-accent)/30" : "border-(--theme-border)", focusWithin && "focus-within:border-(--theme-focus)/60", !fill && "backdrop-blur-xl", !fill && (elevated ? "shadow-(--theme-shadow-xl)" : "shadow-(--theme-shadow)"), widthClasses[size], padding === void 0 ? sizePadding[size] : paddingClasses[padding], divided && "divide-y divide-(--theme-border)", pressable && "cursor-pointer text-left text-inherit no-underline transition-[background-color,box-shadow,border-color] duration-150 hover:shadow-(--theme-shadow-lg) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--theme-link) motion-reduce:transition-none", pressable && (fillTint ? "hover:bg-[color-mix(in_oklab,var(--card-tone)_11%,var(--theme-panel-hover))]" : "hover:bg-(--theme-panel-hover)"), pressable && toned && "hover:border-(--card-tone)/70", !pressable && focusWithin && "transition-[border-color] duration-150 motion-reduce:transition-none", className),
39
50
  children
40
51
  }
41
52
  });
@@ -0,0 +1,40 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface CodeBlockProps {
3
+ /** The code, as it is copied and downloaded. */
4
+ code: string;
5
+ /** The language to highlight it as, such as `yaml`; the header names it
6
+ * unless `title` says otherwise. Without it the code reads as plain text. */
7
+ language?: string | undefined;
8
+ /** The header's text in place of the language, such as a file's name. */
9
+ title?: ReactNode;
10
+ /** Names the code for assistive technology where `title` is not plain text;
11
+ * by default the title, or the language ("yaml code"). */
12
+ label?: string | undefined;
13
+ /** Adds a download button that saves the code under this name. */
14
+ filename?: string | undefined;
15
+ /** Controls of the host's own, before download and copy. */
16
+ headerActions?: ReactNode;
17
+ /** The copy button; on by default. */
18
+ copy?: boolean | undefined;
19
+ /** Numbers the lines; on by default when there is more than one. */
20
+ lineNumbers?: boolean | undefined;
21
+ /** The first line's number, for an excerpt. */
22
+ startLine?: number | undefined;
23
+ /** Breaks long lines instead of scrolling them sideways. */
24
+ wrap?: boolean | undefined;
25
+ /** Grows to the height of its container and scrolls inside it, for a view
26
+ * that is the code, such as a tab. Without it the block is as tall as its
27
+ * code, up to `maxHeight`. */
28
+ fill?: boolean | undefined;
29
+ /** The tallest it grows before it scrolls, as a CSS length or in pixels. */
30
+ maxHeight?: number | string | undefined;
31
+ /** The code is still arriving: highlighting is not cached, and the block
32
+ * keeps its end in view while it is scrolled there. */
33
+ streaming?: boolean | undefined;
34
+ className?: string | undefined;
35
+ }
36
+ /** Code to read on a page, such as a workflow's YAML: highlighted in the
37
+ * theme's code colors, with numbered lines, a copy button and, given a
38
+ * `filename`, a download button. Markdown's fenced code renders as one, so
39
+ * both read the same. */
40
+ export declare function CodeBlock({ code, language, title, label, filename, headerActions, copy, lineNumbers, startLine, wrap, fill, maxHeight, streaming, className, }: CodeBlockProps): import("react").JSX.Element;
@@ -0,0 +1,159 @@
1
+ import { cx } from "../cx.js";
2
+ import { DownloadIcon } from "../icons.js";
3
+ import { useStrings } from "./Provider.js";
4
+ import { IconButton } from "./IconButton.js";
5
+ import { CopyButton } from "./CopyButton.js";
6
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/components/CodeBlock.tsx
9
+ function useHighlighted(source, language, streaming) {
10
+ const [state, setState] = useState(null);
11
+ const latest = useRef({
12
+ source,
13
+ language
14
+ });
15
+ latest.current = {
16
+ source,
17
+ language
18
+ };
19
+ useEffect(() => {
20
+ let mounted = true;
21
+ const accept = (lines) => {
22
+ const now = latest.current;
23
+ if (mounted && now.language === language && now.source.startsWith(source)) setState({
24
+ source,
25
+ language,
26
+ lines
27
+ });
28
+ };
29
+ import("./codeHighlight.js").then(({ highlightCode }) => {
30
+ const ready = highlightCode(source, language, streaming, accept);
31
+ if (ready) accept(ready);
32
+ }).catch(() => {});
33
+ return () => {
34
+ mounted = false;
35
+ };
36
+ }, [
37
+ source,
38
+ language,
39
+ streaming
40
+ ]);
41
+ return state;
42
+ }
43
+ function tokensFor(state, source, language, count) {
44
+ const out = new Array(count).fill(null);
45
+ if (!state || state.language !== language || !source.startsWith(state.source)) return out;
46
+ if (state.source === source) {
47
+ if (state.lines.length === count) return state.lines;
48
+ return out;
49
+ }
50
+ const settled = Math.min(state.source.split("\n").length - 1, state.lines.length, count);
51
+ for (let i = 0; i < settled; i++) out[i] = state.lines[i] ?? null;
52
+ return out;
53
+ }
54
+ function isScrollable(el) {
55
+ return el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1;
56
+ }
57
+ function downloadText(text, name) {
58
+ const url = URL.createObjectURL(new Blob([text], { type: "text/plain" }));
59
+ const a = document.createElement("a");
60
+ a.href = url;
61
+ a.download = name;
62
+ a.click();
63
+ setTimeout(() => URL.revokeObjectURL(url));
64
+ }
65
+ /** Code to read on a page, such as a workflow's YAML: highlighted in the
66
+ * theme's code colors, with numbered lines, a copy button and, given a
67
+ * `filename`, a download button. Markdown's fenced code renders as one, so
68
+ * both read the same. */
69
+ function CodeBlock({ code, language = "", title, label, filename, headerActions, copy = true, lineNumbers, startLine = 1, wrap = false, fill = false, maxHeight, streaming = false, className }) {
70
+ const { common: t } = useStrings();
71
+ const source = code.endsWith("\n") ? code.slice(0, -1) : code;
72
+ const lines = useMemo(() => source.split("\n"), [source]);
73
+ const tokens = tokensFor(useHighlighted(source, language, streaming), source, language, lines.length);
74
+ const numbered = lineNumbers ?? lines.length > 1;
75
+ const lastLine = startLine + lines.length - 1;
76
+ const body = useRef(null);
77
+ const [scrollable, setScrollable] = useState(false);
78
+ useLayoutEffect(() => {
79
+ const el = body.current;
80
+ if (!el) return;
81
+ setScrollable(isScrollable(el));
82
+ if (typeof ResizeObserver === "undefined") return;
83
+ const observer = new ResizeObserver(() => setScrollable(isScrollable(el)));
84
+ observer.observe(el);
85
+ if (el.firstElementChild) observer.observe(el.firstElementChild);
86
+ return () => observer.disconnect();
87
+ }, []);
88
+ const atEnd = useRef(true);
89
+ useLayoutEffect(() => {
90
+ const el = body.current;
91
+ if (streaming && el && atEnd.current && source) el.scrollTop = el.scrollHeight;
92
+ }, [source, streaming]);
93
+ const name = label ?? (typeof title === "string" ? title : t.codeOf(language));
94
+ const heading = title ?? language;
95
+ const bodyStyle = {};
96
+ if (!fill && maxHeight !== void 0) bodyStyle.maxHeight = typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight;
97
+ const codeStyle = {
98
+ counterReset: `line ${startLine - 1}`,
99
+ "--code-gutter": `max(1.5rem, ${String(lastLine).length}ch)`
100
+ };
101
+ return /* @__PURE__ */ jsxs("div", {
102
+ "data-code-block": "",
103
+ className: cx("not-prose flex flex-col overflow-hidden rounded-xl bg-(--theme-code-bg) text-(--theme-code-text) shadow-[0_0_0_1px_color-mix(in_oklab,var(--theme-app)_8%,transparent)]", fill && "h-full min-h-0", className),
104
+ children: [/* @__PURE__ */ jsxs("div", {
105
+ className: "flex h-9 shrink-0 items-center gap-2 pr-1.5 pl-4",
106
+ children: [/* @__PURE__ */ jsx("div", {
107
+ className: "min-w-0 flex-1 truncate text-xs text-(--theme-muted-text-color)",
108
+ children: heading
109
+ }), /* @__PURE__ */ jsxs("div", {
110
+ className: "flex shrink-0 items-center gap-0.5",
111
+ children: [
112
+ headerActions,
113
+ filename && /* @__PURE__ */ jsx(IconButton, {
114
+ label: t.download,
115
+ icon: /* @__PURE__ */ jsx(DownloadIcon, {}),
116
+ onClick: () => downloadText(code, filename)
117
+ }),
118
+ copy && /* @__PURE__ */ jsx(CopyButton, { text: code })
119
+ ]
120
+ })]
121
+ }), /* @__PURE__ */ jsx("div", {
122
+ ref: body,
123
+ dir: "ltr",
124
+ className: cx("overflow-auto focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--theme-focus)", fill && "min-h-0 flex-1"),
125
+ style: bodyStyle,
126
+ onScroll: (e) => {
127
+ const el = e.currentTarget;
128
+ atEnd.current = el.scrollTop + el.clientHeight >= el.scrollHeight - 4;
129
+ },
130
+ ...scrollable ? {
131
+ tabIndex: 0,
132
+ role: "region",
133
+ "aria-label": name
134
+ } : {},
135
+ children: /* @__PURE__ */ jsx("pre", {
136
+ className: cx("m-0 px-4 pt-0.5 pb-3.5 font-mono text-sm leading-[1.6]", wrap ? "w-full" : "w-max min-w-full"),
137
+ children: /* @__PURE__ */ jsx("code", {
138
+ className: "block",
139
+ style: codeStyle,
140
+ children: lines.map((line, i) => /* @__PURE__ */ jsx("span", {
141
+ className: cx("flex min-h-[1lh] items-baseline", numbered && "before:mr-4 before:w-(--code-gutter) before:shrink-0 before:text-right before:font-mono before:text-[0.8125rem] before:text-(--theme-muted-text-color)/60 before:content-[counter(line)] before:[counter-increment:line]"),
142
+ children: /* @__PURE__ */ jsx("span", {
143
+ className: cx("min-w-0", wrap ? "whitespace-pre-wrap break-words" : "whitespace-pre"),
144
+ children: tokens[i] ? tokens[i].map((token, j) => /* @__PURE__ */ jsx("span", {
145
+ style: token.color || token.fontStyle ? {
146
+ color: token.color,
147
+ fontStyle: token.fontStyle
148
+ } : void 0,
149
+ children: token.content
150
+ }, j)) : line
151
+ })
152
+ }, i))
153
+ })
154
+ })
155
+ })]
156
+ });
157
+ }
158
+ //#endregion
159
+ export { CodeBlock };
@@ -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";
@@ -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;