@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.
- package/README.md +7 -1
- package/dist/ai/adapter/types.d.ts +1 -0
- package/dist/ai/ai.css +3 -92
- package/dist/ai/components/agent/AgentMessageParts.js +1 -0
- package/dist/ai/core/ChatProvider.js +10 -5
- package/dist/ai/strings.d.ts +3 -0
- package/dist/ai/strings.js +2 -0
- package/dist/ai/types.d.ts +5 -0
- package/dist/components/Card.d.ts +16 -1
- package/dist/components/Card.js +13 -2
- package/dist/components/CodeBlock.d.ts +40 -0
- package/dist/components/CodeBlock.js +159 -0
- package/dist/components/CreateModal.d.ts +3 -1
- package/dist/components/CreateModal.js +15 -8
- package/dist/components/DescriptionList.js +1 -1
- package/dist/components/Drawer.js +1 -1
- package/dist/components/Input.d.ts +5 -0
- package/dist/components/Input.js +4 -3
- package/dist/components/MarkdownImpl.js +89 -2
- package/dist/components/MenuEntries.d.ts +3 -0
- package/dist/components/MenuEntries.js +19 -6
- package/dist/components/Modal.d.ts +1 -1
- package/dist/components/Modal.js +6 -5
- package/dist/components/PageHeader.d.ts +4 -0
- package/dist/components/PageHeader.js +6 -3
- package/dist/components/Provider.d.ts +3 -0
- package/dist/components/Provider.js +1 -0
- package/dist/components/Select.js +1 -1
- package/dist/components/ToggleGroup.d.ts +3 -1
- package/dist/components/ToggleGroup.js +2 -2
- package/dist/components/codeHighlight.d.ts +9 -0
- package/dist/components/codeHighlight.js +29 -0
- package/dist/components/pageHeaderStore.d.ts +4 -0
- package/dist/components/pageHeaderStore.js +5 -2
- package/dist/file-explorer/FileExplorer.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -2
- package/dist/styles/theme.css +1 -0
- package/dist/styles.css +1 -1
- package/dist/theme/derive.d.ts +1 -1
- package/dist/theme/derive.js +3 -1
- 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
|
|
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`).
|
package/dist/ai/ai.css
CHANGED
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
padding: 0;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/* Inline code
|
|
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-
|
|
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
|
-
|
|
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;
|
package/dist/ai/strings.d.ts
CHANGED
|
@@ -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;
|
package/dist/ai/strings.js
CHANGED
|
@@ -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",
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/components/Card.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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("
|
|
145
|
-
ref:
|
|
146
|
-
|
|
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
|
|
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-
|
|
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;
|