@urbicon-ui/blocks 6.40.4 → 6.43.2
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 +1 -1
- package/dist/components/Calendar/calendar.variants.d.ts +63 -63
- package/dist/components/Chat/A2UIView/A2UINode.svelte +20 -39
- package/dist/components/Chat/A2UIView/A2UIView.svelte +47 -50
- package/dist/components/Chat/A2UIView/a2ui-basic-catalog.d.ts +17 -0
- package/dist/components/Chat/A2UIView/a2ui-basic-catalog.js +69 -0
- package/dist/components/Chat/A2UIView/a2ui-catalog.d.ts +95 -0
- package/dist/components/Chat/A2UIView/a2ui-catalog.js +87 -0
- package/dist/components/Chat/A2UIView/a2ui-node-common.d.ts +40 -0
- package/dist/components/Chat/A2UIView/a2ui-node-common.js +72 -0
- package/dist/components/Chat/A2UIView/a2ui-prompt.d.ts +21 -10
- package/dist/components/Chat/A2UIView/a2ui-prompt.js +170 -24
- package/dist/components/Chat/A2UIView/a2ui-registry.d.ts +1 -1
- package/dist/components/Chat/A2UIView/a2ui-registry.js +2 -1
- package/dist/components/Chat/A2UIView/a2ui-render.d.ts +30 -0
- package/dist/components/Chat/A2UIView/a2ui-render.js +61 -23
- package/dist/components/Chat/A2UIView/a2ui-router.d.ts +136 -0
- package/dist/components/Chat/A2UIView/a2ui-router.js +264 -0
- package/dist/components/Chat/A2UIView/a2ui-schema.d.ts +45 -0
- package/dist/components/Chat/A2UIView/a2ui-schema.js +133 -0
- package/dist/components/Chat/A2UIView/a2ui-stream.d.ts +120 -0
- package/dist/components/Chat/A2UIView/a2ui-stream.js +406 -0
- package/dist/components/Chat/A2UIView/a2ui-transcript.d.ts +65 -0
- package/dist/components/Chat/A2UIView/a2ui-transcript.js +101 -0
- package/dist/components/Chat/A2UIView/a2ui-validate.d.ts +33 -1
- package/dist/components/Chat/A2UIView/a2ui-validate.js +167 -63
- package/dist/components/Chat/A2UIView/a2ui.types.d.ts +50 -0
- package/dist/components/Chat/A2UIView/a2ui.types.js +38 -1
- package/dist/components/Chat/A2UIView/index.d.ts +35 -2
- package/dist/components/Chat/A2UIView/index.js +13 -0
- package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte +831 -0
- package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte.d.ts +4 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.d.ts +14 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.js +87 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.d.ts +78 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.js +783 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.d.ts +43 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.js +33 -0
- package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.d.ts +7 -0
- package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.js +86 -0
- package/dist/components/Chat/ChatMessage/ChatMessage.svelte +93 -67
- package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +26 -19
- package/dist/components/Chat/ChatMessage/chat-message.variants.js +45 -12
- package/dist/components/Chat/ChatMessage/index.d.ts +20 -5
- package/dist/components/Chat/CodeBlock/CodeBlock.svelte +59 -33
- package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +7 -0
- package/dist/components/Chat/CodeBlock/code-block.variants.js +28 -13
- package/dist/components/Chat/CodeBlock/index.d.ts +39 -4
- package/dist/components/Chat/PromptInput/PromptInput.svelte +9 -16
- package/dist/components/Chat/PromptInput/prompt-input.variants.js +16 -15
- package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +9 -4
- package/dist/components/Chat/ToolCallCard/index.d.ts +4 -3
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +0 -3
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +4 -4
- package/dist/components/CopyButton/CopyButton.svelte +9 -19
- package/dist/components/CopyButton/copy-button.variants.d.ts +4 -4
- package/dist/components/FileUpload/fileUpload.variants.js +15 -4
- package/dist/components/PinInput/PinInput.svelte +1 -0
- package/dist/components/PinInput/pin-input.variants.js +14 -1
- package/dist/components/TimeInput/time-input.variants.js +16 -1
- package/dist/i18n/index.d.ts +398 -2
- package/dist/internal/copy-state.svelte.d.ts +60 -0
- package/dist/internal/copy-state.svelte.js +76 -0
- package/dist/internal/field-chrome.d.ts +18 -1
- package/dist/internal/field-chrome.js +19 -2
- package/dist/primitives/Alert/alert.variants.d.ts +8 -8
- package/dist/primitives/Avatar/avatar.variants.js +4 -1
- package/dist/primitives/Badge/badge.variants.js +11 -2
- package/dist/primitives/Button/button.variants.js +4 -1
- package/dist/primitives/Card/card.variants.js +4 -1
- package/dist/primitives/Checkbox/checkbox.variants.js +7 -1
- package/dist/primitives/Combobox/Combobox.svelte +9 -1
- package/dist/primitives/Combobox/combobox.variants.d.ts +25 -0
- package/dist/primitives/Combobox/combobox.variants.js +43 -7
- package/dist/primitives/Combobox/index.d.ts +8 -2
- package/dist/primitives/Dialog/dialog.variants.js +4 -1
- package/dist/primitives/Drawer/drawer.variants.js +4 -1
- package/dist/primitives/Input/input.variants.js +17 -1
- package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +22 -22
- package/dist/primitives/Menu/menu.variants.js +5 -1
- package/dist/primitives/Progress/progress.variants.js +5 -2
- package/dist/primitives/RadioGroup/radioGroup.variants.js +74 -10
- package/dist/primitives/Select/select.variants.js +14 -5
- package/dist/primitives/Slider/Slider.svelte +4 -1
- package/dist/primitives/Slider/slider.variants.js +9 -2
- package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
- package/dist/primitives/Stepper/stepper.variants.d.ts +11 -11
- package/dist/primitives/Stepper/stepper.variants.js +2 -1
- package/dist/primitives/Tab/tab.variants.js +4 -1
- package/dist/primitives/Textarea/textarea.variants.js +16 -2
- package/dist/primitives/Toast/toast.variants.d.ts +12 -12
- package/dist/primitives/Toast/toast.variants.js +5 -1
- package/dist/primitives/Toggle/toggle.variants.js +94 -18
- package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
- package/dist/primitives/Tooltip/tooltip.variants.js +5 -1
- package/dist/style/foundation.css +19 -9
- package/dist/style/semantic.css +49 -2
- package/dist/style/themes/forest.css +2 -2
- package/dist/style/themes/neutral.css +2 -2
- package/dist/style/themes/ocean.css +2 -2
- package/dist/style/themes/rose.css +2 -2
- package/dist/style/themes/sunset.css +2 -2
- package/dist/utils/figma-token-export.js +7 -3
- package/dist/utils/persistent-state.svelte.d.ts +25 -1
- package/dist/utils/persistent-state.svelte.js +98 -10
- package/docs/MIGRATION-v5.md +29 -0
- package/package.json +3 -3
|
@@ -11,6 +11,13 @@ export const chatMessageVariants = tv({
|
|
|
11
11
|
header: ['flex min-w-0 items-center gap-2'],
|
|
12
12
|
roleName: ['text-sm font-medium text-text-secondary select-none'],
|
|
13
13
|
avatar: ['shrink-0'],
|
|
14
|
+
// Everything that belongs UNDER the bubble — citations, the error alert, the
|
|
15
|
+
// footer — lives in this column, not as a sibling of `container`. It is what
|
|
16
|
+
// makes those rows inherit the bubble's side: a user bubble sits right, so
|
|
17
|
+
// its timestamp and citations must too. `items-*` per role does the aligning
|
|
18
|
+
// (see the compounds), and the width cap lives here rather than on `bubble`
|
|
19
|
+
// so an alert spans the column while the bubble still hugs its text.
|
|
20
|
+
column: ['flex min-w-0 flex-col'],
|
|
14
21
|
// The content bubble (bubble layout) / the full-width column (plain layout).
|
|
15
22
|
bubble: ['min-w-0 max-w-full'],
|
|
16
23
|
// Vertical stack of the rendered parts.
|
|
@@ -29,21 +36,29 @@ export const chatMessageVariants = tv({
|
|
|
29
36
|
sourcesFooter: ['flex flex-wrap items-center gap-1.5 pt-1'],
|
|
30
37
|
// streaming placeholder (zero parts)
|
|
31
38
|
placeholder: ['py-1'],
|
|
32
|
-
// status alert
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
// status alert — stretches across the column (the bubble hugs its text, an
|
|
40
|
+
// error does not).
|
|
41
|
+
statusAlert: ['mt-1 w-full'],
|
|
42
|
+
// Footer row holding actions + metadata. Its height is set by the metadata
|
|
43
|
+
// line (`min-h-5`), NOT by the action buttons: those are `opacity-0` until
|
|
44
|
+
// hover/focus, so letting them size the row reserved ~28px of blank strip
|
|
45
|
+
// under every single message. `-my-1` below lets them overhang the shorter
|
|
46
|
+
// row instead of growing it.
|
|
47
|
+
footer: ['flex min-h-5 items-center gap-2'],
|
|
36
48
|
actions: [
|
|
37
|
-
'flex items-center gap-1 opacity-0',
|
|
49
|
+
'flex items-center gap-1 -my-1 opacity-0',
|
|
38
50
|
'transition-opacity duration-[var(--blocks-duration-fast)] ease-out',
|
|
39
51
|
'group-hover/message:opacity-100 group-focus-within/message:opacity-100'
|
|
40
52
|
],
|
|
53
|
+
// Renders through CoreIconButton, so this carries only what the core does not
|
|
54
|
+
// supply — no flex centring, cursor or `focus-visible:outline-none` here (the
|
|
55
|
+
// core has no variant engine, so a repeat would resolve by stylesheet order).
|
|
41
56
|
actionButton: [
|
|
42
|
-
'
|
|
43
|
-
'text-text-tertiary
|
|
57
|
+
'rounded-modify p-1.5',
|
|
58
|
+
'text-text-tertiary',
|
|
44
59
|
'transition-[color,background-color] duration-[var(--blocks-duration-fast)] ease-out',
|
|
45
60
|
'hover:bg-surface-hover hover:text-text-primary',
|
|
46
|
-
'focus-visible:
|
|
61
|
+
'focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1'
|
|
47
62
|
],
|
|
48
63
|
metadata: ['text-xs text-text-tertiary']
|
|
49
64
|
},
|
|
@@ -52,6 +67,7 @@ export const chatMessageVariants = tv({
|
|
|
52
67
|
bubble: {},
|
|
53
68
|
// Document-like: full-width content under an avatar + role header row.
|
|
54
69
|
plain: {
|
|
70
|
+
column: 'w-full items-stretch',
|
|
55
71
|
bubble: 'w-full',
|
|
56
72
|
partsFlow: 'gap-3'
|
|
57
73
|
}
|
|
@@ -64,30 +80,46 @@ export const chatMessageVariants = tv({
|
|
|
64
80
|
density: {
|
|
65
81
|
comfortable: {
|
|
66
82
|
root: 'gap-1.5',
|
|
83
|
+
column: 'gap-1.5',
|
|
67
84
|
partsFlow: 'gap-2'
|
|
68
85
|
},
|
|
69
86
|
compact: {
|
|
70
87
|
root: 'gap-1',
|
|
88
|
+
column: 'gap-1',
|
|
71
89
|
partsFlow: 'gap-1.5'
|
|
72
90
|
}
|
|
73
91
|
}
|
|
74
92
|
},
|
|
75
93
|
compoundVariants: [
|
|
76
94
|
// ── bubble layout: role tint + alignment ────────────────────────────────
|
|
95
|
+
// Bubbles ride the BRIDGE tier, not `contain`. `contain` (2 px) is the
|
|
96
|
+
// architectural-panel radius — correct on a 600 px Card, where it reads as
|
|
97
|
+
// a precise edge, but on a ~200 px bubble the same 2 px reads as a plain
|
|
98
|
+
// rectangle. Optical radius scales with the area it turns. `bridge` (6 px)
|
|
99
|
+
// is the middle tier and stays brand-themeable, so a consumer retunes
|
|
100
|
+
// bubbles via `--radius-bridge` instead of overriding this slot.
|
|
77
101
|
{
|
|
78
102
|
layout: 'bubble',
|
|
79
103
|
role: 'user',
|
|
80
104
|
class: {
|
|
81
|
-
// Reverse so the (avatar-less)
|
|
105
|
+
// Reverse so the (avatar-less) column packs to the right edge, and
|
|
106
|
+
// right-align its rows so timestamp + citations hang off the same edge
|
|
107
|
+
// as the bubble instead of drifting to the left margin.
|
|
82
108
|
container: 'flex-row-reverse',
|
|
83
|
-
|
|
109
|
+
column: 'max-w-[85%] items-end',
|
|
110
|
+
bubble: 'rounded-bridge bg-primary-subtle',
|
|
111
|
+
// Metadata comes first in the DOM so it can sit against the bubble edge;
|
|
112
|
+
// reversing here puts that edge on the right for a user message while
|
|
113
|
+
// keeping the action bar on the inside.
|
|
114
|
+
footer: 'flex-row-reverse'
|
|
84
115
|
}
|
|
85
116
|
},
|
|
86
117
|
{
|
|
87
118
|
layout: 'bubble',
|
|
88
119
|
role: 'assistant',
|
|
89
120
|
class: {
|
|
90
|
-
|
|
121
|
+
column: 'max-w-[85%] items-start',
|
|
122
|
+
bubble: 'rounded-bridge bg-surface-elevated'
|
|
91
123
|
}
|
|
92
124
|
},
|
|
93
125
|
{
|
|
@@ -95,7 +127,8 @@ export const chatMessageVariants = tv({
|
|
|
95
127
|
role: 'system',
|
|
96
128
|
class: {
|
|
97
129
|
container: 'justify-center',
|
|
98
|
-
|
|
130
|
+
column: 'max-w-[90%] items-center',
|
|
131
|
+
bubble: 'rounded-bridge border border-border-subtle bg-surface-base text-sm text-text-secondary'
|
|
99
132
|
}
|
|
100
133
|
},
|
|
101
134
|
// ── bubble layout: density padding ──────────────────────────────────────
|
|
@@ -68,10 +68,22 @@ export interface ChatMessageProps extends Omit<ChatMessageVariants, 'role'>, Omi
|
|
|
68
68
|
partRenderers?: ChatPartRenderers;
|
|
69
69
|
/** Display names per role, shown in the `plain`-layout header. */
|
|
70
70
|
roleLabels?: Partial<Record<ChatRole, string>>;
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Accessible label / tooltip for the copy action.
|
|
73
|
+
* @default the `accessibility.copy` translation
|
|
74
|
+
*/
|
|
72
75
|
copyLabel?: string;
|
|
73
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Label shown briefly after a successful copy.
|
|
78
|
+
* @default the `accessibility.copied` translation
|
|
79
|
+
*/
|
|
74
80
|
copiedLabel?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Label shown briefly after a FAILED copy — e.g. a denied clipboard permission
|
|
83
|
+
* or a non-secure context.
|
|
84
|
+
* @default the `accessibility.copyFailed` translation
|
|
85
|
+
*/
|
|
86
|
+
copyFailedLabel?: string;
|
|
75
87
|
/** Accessible label / tooltip for the regenerate action. @default 'Regenerate' */
|
|
76
88
|
regenerateLabel?: string;
|
|
77
89
|
/** Label for the retry button in the error/aborted Alert. @default 'Retry' */
|
|
@@ -86,9 +98,12 @@ export interface ChatMessageProps extends Omit<ChatMessageVariants, 'role'>, Omi
|
|
|
86
98
|
unstyled?: boolean;
|
|
87
99
|
/**
|
|
88
100
|
* Per-slot class overrides. Slots: root | container | header | roleName | avatar |
|
|
89
|
-
* bubble | partsFlow | attachment | attachmentIcon | attachmentName |
|
|
90
|
-
* sourcesFooter | placeholder | statusAlert | footer | actions |
|
|
91
|
-
* metadata.
|
|
101
|
+
* column | bubble | partsFlow | attachment | attachmentIcon | attachmentName |
|
|
102
|
+
* attachmentSize | sourcesFooter | placeholder | statusAlert | footer | actions |
|
|
103
|
+
* actionButton | metadata. `column` wraps the bubble with everything under it
|
|
104
|
+
* (citations, status alert, footer) and carries the role-dependent alignment —
|
|
105
|
+
* override it to re-align a message's whole stack, not just the bubble.
|
|
106
|
+
* Reasoning and tool-call parts render through ReasoningDisclosure /
|
|
92
107
|
* ToolCallCard, which take their own `slotClasses`.
|
|
93
108
|
*/
|
|
94
109
|
slotClasses?: Partial<Record<ChatMessageSlots, string>>;
|
|
@@ -3,17 +3,22 @@
|
|
|
3
3
|
import { resolveIcon } from '../../../icons';
|
|
4
4
|
import CopyIconDefault from '../../../icons/CopyIcon.svelte';
|
|
5
5
|
import CheckIconDefault from '../../../icons/CheckIcon.svelte';
|
|
6
|
+
import { createCopyState } from '../../../internal/copy-state.svelte';
|
|
6
7
|
import { codeBlockVariants, type CodeBlockVariants } from './code-block.variants';
|
|
7
8
|
import type { CodeBlockProps } from './index';
|
|
8
9
|
|
|
9
10
|
let {
|
|
10
11
|
code,
|
|
11
12
|
lang,
|
|
13
|
+
label,
|
|
14
|
+
variant = 'card',
|
|
12
15
|
showCopy = true,
|
|
13
16
|
wrap = false,
|
|
14
|
-
copyLabel = 'Copy
|
|
17
|
+
copyLabel = 'Copy',
|
|
15
18
|
copiedLabel = 'Copied',
|
|
19
|
+
copyFailedLabel = 'Copy failed',
|
|
16
20
|
onCopy,
|
|
21
|
+
onCopyError,
|
|
17
22
|
actions,
|
|
18
23
|
class: className,
|
|
19
24
|
unstyled: unstyledProp = false,
|
|
@@ -28,7 +33,16 @@
|
|
|
28
33
|
const CopyIcon = resolveIcon('copy', CopyIconDefault);
|
|
29
34
|
const CheckIcon = resolveIcon('check', CheckIconDefault);
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
// Deliberately NOT routed through useBlocksI18n(). CodeBlock is a leaf with no
|
|
37
|
+
// other reason to pull the i18n registry, and doing so for three strings costs
|
|
38
|
+
// +5 KB gz here — which StreamingMarkdown and ReasoningDisclosure then inherit
|
|
39
|
+
// by embedding it (+25% each, measured against bundle-size.baseline.json). A
|
|
40
|
+
// consumer localises through the three label props; ChatMessage, which already
|
|
41
|
+
// carries the registry via Alert/Avatar/Button/Tooltip, does use the
|
|
42
|
+
// translations because there it is free. See technical-debt.md — splitting
|
|
43
|
+
// translations/en.ts per area would remove the trade-off.
|
|
44
|
+
|
|
45
|
+
const variantProps: CodeBlockVariants = $derived({ variant, wrap });
|
|
32
46
|
const styles = $derived(codeBlockVariants(variantProps));
|
|
33
47
|
const slotClasses = $derived(
|
|
34
48
|
resolveSlotClasses(blocksConfig, 'CodeBlock', preset, variantProps, slotClassesProp)
|
|
@@ -45,54 +59,66 @@
|
|
|
45
59
|
});
|
|
46
60
|
}
|
|
47
61
|
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
// `label` wins over `lang` in the header: an embedding parent (ToolCallCard)
|
|
63
|
+
// passes what the payload *is* ("Input"), which says more than the language it
|
|
64
|
+
// happens to be serialised in — and showing both is the same fact twice.
|
|
65
|
+
const headerLabel = $derived(label ?? lang);
|
|
66
|
+
const showHeader = $derived(!!headerLabel || showCopy || !!actions);
|
|
67
|
+
// The accessible name of the scroll region keeps naming the language when
|
|
68
|
+
// there is one: "Input code" is vaguer than "json code" for a screen reader.
|
|
69
|
+
const regionLabel = $derived(lang ? `${lang} code` : label ? `${label} code` : 'Code');
|
|
50
70
|
|
|
51
|
-
|
|
52
|
-
let resetTimer: ReturnType<typeof setTimeout> | undefined;
|
|
71
|
+
const copyState = createCopyState();
|
|
53
72
|
|
|
54
|
-
async function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
copied = true;
|
|
58
|
-
if (resetTimer) clearTimeout(resetTimer);
|
|
59
|
-
resetTimer = setTimeout(() => {
|
|
60
|
-
copied = false;
|
|
61
|
-
resetTimer = undefined;
|
|
62
|
-
}, 2000);
|
|
73
|
+
async function handleCopy() {
|
|
74
|
+
const result = await copyState.copy(code);
|
|
75
|
+
if (result.ok) {
|
|
63
76
|
onCopy?.(code);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
} else if (onCopyError) {
|
|
78
|
+
onCopyError(result.error);
|
|
79
|
+
} else {
|
|
80
|
+
// Never silent: a copy that did nothing is the outcome most worth
|
|
81
|
+
// reporting, and the consumer opted out of handling it.
|
|
82
|
+
console.error('CodeBlock: failed to copy code', result.error);
|
|
68
83
|
}
|
|
69
84
|
}
|
|
70
85
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
// The button reports the outcome, all three of them. A denied clipboard
|
|
87
|
+
// permission used to leave it looking untouched (console-only).
|
|
88
|
+
const buttonText = $derived(
|
|
89
|
+
copyState.phase === 'copied'
|
|
90
|
+
? copiedLabel
|
|
91
|
+
: copyState.phase === 'error'
|
|
92
|
+
? copyFailedLabel
|
|
93
|
+
: copyLabel
|
|
94
|
+
);
|
|
75
95
|
</script>
|
|
76
96
|
|
|
77
97
|
<div class={cls('root', className)} {...restProps}>
|
|
78
98
|
{#if showHeader}
|
|
79
99
|
<div class={cls('header')}>
|
|
80
|
-
<span class={cls('langLabel')}>{
|
|
100
|
+
<span class={cls('langLabel')}>{headerLabel ?? ''}</span>
|
|
81
101
|
<div class="flex items-center gap-1">
|
|
82
102
|
{@render actions?.()}
|
|
83
103
|
{#if showCopy}
|
|
104
|
+
<!--
|
|
105
|
+
Icon-only: the button sits in a header whose other half already names
|
|
106
|
+
the payload, and a text button there outweighed the caption it was
|
|
107
|
+
supposed to serve. `title` carries the label for pointer users, the
|
|
108
|
+
live region below announces the outcome.
|
|
109
|
+
-->
|
|
84
110
|
<button
|
|
85
111
|
type="button"
|
|
86
112
|
class={cls('copyButton')}
|
|
87
|
-
onclick={
|
|
88
|
-
aria-label={
|
|
113
|
+
onclick={handleCopy}
|
|
114
|
+
aria-label={buttonText}
|
|
115
|
+
title={buttonText}
|
|
89
116
|
>
|
|
90
|
-
{#if copied}
|
|
117
|
+
{#if copyState.phase === 'copied'}
|
|
91
118
|
<CheckIcon size={14} />
|
|
92
119
|
{:else}
|
|
93
120
|
<CopyIcon size={14} />
|
|
94
121
|
{/if}
|
|
95
|
-
<span>{copied ? copiedLabel : copyLabel}</span>
|
|
96
122
|
</button>
|
|
97
123
|
{/if}
|
|
98
124
|
</div>
|
|
@@ -111,10 +137,10 @@
|
|
|
111
137
|
></pre>
|
|
112
138
|
|
|
113
139
|
<!--
|
|
114
|
-
Copy
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
140
|
+
Copy outcome for screen readers. A label change on the button the user just
|
|
141
|
+
activated is not a reliable announcement — a live status region is. It must
|
|
142
|
+
exist in the DOM before the phase flips, so it always renders and only its
|
|
143
|
+
text content changes.
|
|
118
144
|
-->
|
|
119
|
-
<span class="sr-only" role="status">{
|
|
145
|
+
<span class="sr-only" role="status">{copyState.phase === 'idle' ? '' : buttonText}</span>
|
|
120
146
|
</div>
|
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
import { type SlotNames, type VariantProps } from '../../../utils/variants.js';
|
|
2
2
|
export declare const codeBlockVariants: ((props?: {
|
|
3
|
+
variant?: "card" | "plain" | undefined;
|
|
3
4
|
wrap?: boolean | undefined;
|
|
4
5
|
} | undefined) => {
|
|
5
6
|
root: (props?: ({
|
|
7
|
+
variant?: "card" | "plain" | undefined;
|
|
6
8
|
wrap?: boolean | undefined;
|
|
7
9
|
} & {
|
|
8
10
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
9
11
|
}) | undefined) => string;
|
|
10
12
|
header: (props?: ({
|
|
13
|
+
variant?: "card" | "plain" | undefined;
|
|
11
14
|
wrap?: boolean | undefined;
|
|
12
15
|
} & {
|
|
13
16
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
14
17
|
}) | undefined) => string;
|
|
15
18
|
langLabel: (props?: ({
|
|
19
|
+
variant?: "card" | "plain" | undefined;
|
|
16
20
|
wrap?: boolean | undefined;
|
|
17
21
|
} & {
|
|
18
22
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
19
23
|
}) | undefined) => string;
|
|
20
24
|
copyButton: (props?: ({
|
|
25
|
+
variant?: "card" | "plain" | undefined;
|
|
21
26
|
wrap?: boolean | undefined;
|
|
22
27
|
} & {
|
|
23
28
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
24
29
|
}) | undefined) => string;
|
|
25
30
|
pre: (props?: ({
|
|
31
|
+
variant?: "card" | "plain" | undefined;
|
|
26
32
|
wrap?: boolean | undefined;
|
|
27
33
|
} & {
|
|
28
34
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
29
35
|
}) | undefined) => string;
|
|
30
36
|
code: (props?: ({
|
|
37
|
+
variant?: "card" | "plain" | undefined;
|
|
31
38
|
wrap?: boolean | undefined;
|
|
32
39
|
} & {
|
|
33
40
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { tv } from '../../../utils/variants.js';
|
|
2
2
|
export const codeBlockVariants = tv({
|
|
3
3
|
slots: {
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
],
|
|
10
|
-
// Header only renders when there is something to show (lang / copy / actions).
|
|
11
|
-
header: [
|
|
12
|
-
'flex items-center justify-between gap-2',
|
|
13
|
-
'px-3 py-1.5 border-b border-border-subtle'
|
|
14
|
-
],
|
|
15
|
-
langLabel: ['font-mono text-xs text-text-tertiary select-none'],
|
|
4
|
+
root: ['relative block w-full overflow-hidden box-border'],
|
|
5
|
+
// Header only renders when there is something to show (label / lang / copy /
|
|
6
|
+
// actions).
|
|
7
|
+
header: ['flex items-center justify-between gap-2'],
|
|
8
|
+
langLabel: ['text-xs text-text-tertiary select-none'],
|
|
16
9
|
copyButton: [
|
|
17
10
|
'inline-flex items-center gap-1.5',
|
|
18
11
|
'px-2 py-1 rounded-modify',
|
|
@@ -26,12 +19,33 @@ export const codeBlockVariants = tv({
|
|
|
26
19
|
// never the page. Focusable + role=region so keyboard users can reach the
|
|
27
20
|
// scroll (a11y for scrollable content).
|
|
28
21
|
pre: [
|
|
29
|
-
'm-0
|
|
22
|
+
'm-0',
|
|
30
23
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset'
|
|
31
24
|
],
|
|
32
25
|
code: ['font-mono text-sm text-text-primary']
|
|
33
26
|
},
|
|
34
27
|
variants: {
|
|
28
|
+
/**
|
|
29
|
+
* `card` — the standalone block: own surface, own outline, own radius. This
|
|
30
|
+
* is the right frame when the code sits directly in a reading flow (a fenced
|
|
31
|
+
* block in streamed markdown).
|
|
32
|
+
*
|
|
33
|
+
* `plain` — the EMBEDDED block: no surface, no border, no radius, no padding
|
|
34
|
+
* of its own. A card inside a card is the single loudest source of visual
|
|
35
|
+
* noise in this family — nested outlines at the same radius, two header rows
|
|
36
|
+
* and three divider lines for one JSON payload. When the parent already
|
|
37
|
+
* frames the content (ToolCallCard), the code only needs its monospace face
|
|
38
|
+
* to read as code. The parent owns the frame; the child owns the text.
|
|
39
|
+
*/
|
|
40
|
+
variant: {
|
|
41
|
+
card: {
|
|
42
|
+
root: 'bg-surface-elevated border border-border-subtle rounded-contain',
|
|
43
|
+
header: 'px-3 py-1.5 border-b border-border-subtle',
|
|
44
|
+
langLabel: 'font-mono',
|
|
45
|
+
pre: 'px-3 py-2.5'
|
|
46
|
+
},
|
|
47
|
+
plain: {}
|
|
48
|
+
},
|
|
35
49
|
// false → single-line semantics, horizontal scroll inside the block.
|
|
36
50
|
// true → soft-wrap long lines, no horizontal scroll.
|
|
37
51
|
wrap: {
|
|
@@ -46,6 +60,7 @@ export const codeBlockVariants = tv({
|
|
|
46
60
|
}
|
|
47
61
|
},
|
|
48
62
|
defaultVariants: {
|
|
63
|
+
variant: 'card',
|
|
49
64
|
wrap: false
|
|
50
65
|
}
|
|
51
66
|
});
|
|
@@ -2,11 +2,14 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
import type { CodeBlockSlots, CodeBlockVariants } from './code-block.variants.js';
|
|
4
4
|
/**
|
|
5
|
-
* @description Read-only code display
|
|
5
|
+
* @description Read-only code display with a one-click copy interaction: the copy
|
|
6
6
|
* button swaps its icon and label to a confirmation for two seconds and fires `onCopy`.
|
|
7
7
|
* Renders raw text only — no built-in syntax highlighting; a consumer or the
|
|
8
8
|
* StreamingMarkdown renderer can layer highlighting in via a snippet. Used by
|
|
9
9
|
* StreamingMarkdown for fenced code blocks, and standalone for any code snippet.
|
|
10
|
+
* `variant="card"` (default) brings its own surface, outline and radius;
|
|
11
|
+
* `variant="plain"` drops all three for embedding inside a container that already
|
|
12
|
+
* frames the content (as ToolCallCard does), so nested outlines never stack.
|
|
10
13
|
*
|
|
11
14
|
* @tag ai
|
|
12
15
|
* @tag display
|
|
@@ -23,6 +26,13 @@ export interface CodeBlockProps extends CodeBlockVariants, Omit<HTMLAttributes<H
|
|
|
23
26
|
code: string;
|
|
24
27
|
/** Language label shown in the header. Display-only — does not drive highlighting. */
|
|
25
28
|
lang?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Header caption, shown instead of `lang`. For an embedded block, what the
|
|
31
|
+
* payload *is* ("Input", "Response body") says more than the language it is
|
|
32
|
+
* serialised in — and rendering both states the same fact twice. `lang` still
|
|
33
|
+
* names the scrollable region for screen readers when both are given.
|
|
34
|
+
*/
|
|
35
|
+
label?: string;
|
|
26
36
|
/** Show the copy button in the header. */
|
|
27
37
|
showCopy?: boolean;
|
|
28
38
|
/**
|
|
@@ -30,19 +40,44 @@ export interface CodeBlockProps extends CodeBlockVariants, Omit<HTMLAttributes<H
|
|
|
30
40
|
* block; `true` wraps with `whitespace-pre-wrap` + word breaking.
|
|
31
41
|
*/
|
|
32
42
|
wrap?: boolean;
|
|
33
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Accessible label / tooltip for the copy button at rest. These three labels
|
|
45
|
+
* are plain English defaults rather than translations on purpose: pulling the
|
|
46
|
+
* i18n registry into this leaf costs +5 KB gz, which StreamingMarkdown and
|
|
47
|
+
* ReasoningDisclosure would inherit by embedding it. Pass your own strings to
|
|
48
|
+
* localise.
|
|
49
|
+
* @default 'Copy'
|
|
50
|
+
*/
|
|
34
51
|
copyLabel?: string;
|
|
35
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Label shown for two seconds after a successful copy.
|
|
54
|
+
* @default 'Copied'
|
|
55
|
+
*/
|
|
36
56
|
copiedLabel?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Label shown for two seconds after a FAILED copy — e.g. a denied clipboard
|
|
59
|
+
* permission or a non-secure context.
|
|
60
|
+
* @default 'Copy failed'
|
|
61
|
+
*/
|
|
62
|
+
copyFailedLabel?: string;
|
|
37
63
|
/** Called with the copied code after a successful clipboard write. */
|
|
38
64
|
onCopy?: (code: string) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Called with the thrown reason when the clipboard write fails. Without a
|
|
67
|
+
* handler the failure is logged; either way the button reports it.
|
|
68
|
+
*/
|
|
69
|
+
onCopyError?: (error: unknown) => void;
|
|
39
70
|
/** Extra header actions rendered to the right, before the copy button. */
|
|
40
71
|
actions?: Snippet;
|
|
41
72
|
/** Extra classes merged onto the root element. */
|
|
42
73
|
class?: string;
|
|
43
74
|
/** Remove all default tv classes. */
|
|
44
75
|
unstyled?: boolean;
|
|
45
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Per-slot class overrides. Slots: root | header | langLabel | copyButton | pre | code.
|
|
78
|
+
* Note `variant="plain"` deliberately leaves root/header/pre without surface,
|
|
79
|
+
* outline or padding — the embedding parent supplies those.
|
|
80
|
+
*/
|
|
46
81
|
slotClasses?: Partial<Record<CodeBlockSlots, string>>;
|
|
47
82
|
/**
|
|
48
83
|
* Apply a named preset registered via `<BlocksProvider presets={{ CodeBlock: {...} }}>`.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { onDestroy, tick } from 'svelte';
|
|
3
3
|
import { useBlocksI18n } from '../../..';
|
|
4
4
|
import { getBlocksConfig, resolveSlotClasses } from '../../../provider';
|
|
5
|
+
import CoreIconButton from '../../../internal/core/CoreIconButton.svelte';
|
|
5
6
|
import { resolveIcon } from '../../../icons';
|
|
6
7
|
import SendIconDefault from '../../../icons/SendIcon.svelte';
|
|
7
8
|
import SquareIconDefault from '../../../icons/SquareIcon.svelte';
|
|
@@ -350,15 +351,14 @@
|
|
|
350
351
|
</span>
|
|
351
352
|
<span class={cls('attachmentName')} title={entry.file.name}>{entry.file.name}</span>
|
|
352
353
|
<span class={cls('attachmentSize')}>{formatFileSize(entry.file.size)}</span>
|
|
353
|
-
<
|
|
354
|
-
type="button"
|
|
354
|
+
<CoreIconButton
|
|
355
355
|
class={cls('attachmentRemove')}
|
|
356
356
|
data-attachment-remove
|
|
357
357
|
aria-label={removeAttachmentLabel(entry.file.name)}
|
|
358
358
|
onclick={() => removeAttachment(entry, i)}
|
|
359
359
|
>
|
|
360
360
|
<CloseIcon size={14} />
|
|
361
|
-
</
|
|
361
|
+
</CoreIconButton>
|
|
362
362
|
</li>
|
|
363
363
|
{/each}
|
|
364
364
|
</ul>
|
|
@@ -382,15 +382,14 @@
|
|
|
382
382
|
<div class={cls('actions')}>
|
|
383
383
|
<div class={cls('leading')}>
|
|
384
384
|
{#if allowAttachments}
|
|
385
|
-
<
|
|
386
|
-
type="button"
|
|
385
|
+
<CoreIconButton
|
|
387
386
|
class={cls('attachButton')}
|
|
388
387
|
aria-label={attachLabel}
|
|
389
388
|
{disabled}
|
|
390
389
|
onclick={openFilePicker}
|
|
391
390
|
>
|
|
392
391
|
<PaperclipIcon size={iconSize} />
|
|
393
|
-
</
|
|
392
|
+
</CoreIconButton>
|
|
394
393
|
{/if}
|
|
395
394
|
{@render leading?.()}
|
|
396
395
|
</div>
|
|
@@ -398,24 +397,18 @@
|
|
|
398
397
|
<div class={cls('trailing')}>
|
|
399
398
|
{@render trailing?.()}
|
|
400
399
|
{#if busy}
|
|
401
|
-
<
|
|
402
|
-
type="button"
|
|
403
|
-
class={cls('stopButton')}
|
|
404
|
-
aria-label={stopLabel}
|
|
405
|
-
onclick={() => onStop?.()}
|
|
406
|
-
>
|
|
400
|
+
<CoreIconButton class={cls('stopButton')} aria-label={stopLabel} onclick={() => onStop?.()}>
|
|
407
401
|
<SquareIcon size={iconSize} />
|
|
408
|
-
</
|
|
402
|
+
</CoreIconButton>
|
|
409
403
|
{:else}
|
|
410
|
-
<
|
|
411
|
-
type="button"
|
|
404
|
+
<CoreIconButton
|
|
412
405
|
class={cls('sendButton')}
|
|
413
406
|
aria-label={sendLabel}
|
|
414
407
|
disabled={!canSubmit}
|
|
415
408
|
onclick={submit}
|
|
416
409
|
>
|
|
417
410
|
<SendIcon size={iconSize} />
|
|
418
|
-
</
|
|
411
|
+
</CoreIconButton>
|
|
419
412
|
{/if}
|
|
420
413
|
</div>
|
|
421
414
|
</div>
|
|
@@ -29,13 +29,19 @@ export const promptInputVariants = tv({
|
|
|
29
29
|
],
|
|
30
30
|
attachmentName: ['truncate text-xs text-text-primary min-w-0'],
|
|
31
31
|
attachmentSize: ['shrink-0 text-2xs text-text-tertiary tabular-nums'],
|
|
32
|
+
// The icon-only controls below render through CoreIconButton, so they carry
|
|
33
|
+
// only what it does NOT supply. Its plumbing is flex centring, cursor/select,
|
|
34
|
+
// `focus-visible:outline-none` and the disabled trio (opacity, cursor,
|
|
35
|
+
// pointer-events) — repeating any of those here would be resolved by
|
|
36
|
+
// stylesheet order rather than the override ladder, since the core runs no
|
|
37
|
+
// variant engine. `disabled:hover:*` is likewise unnecessary: the core's
|
|
38
|
+
// `disabled:pointer-events-none` already stops hover from firing.
|
|
32
39
|
attachmentRemove: [
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'text-text-tertiary cursor-pointer',
|
|
40
|
+
'shrink-0 size-5 rounded-modify',
|
|
41
|
+
'text-text-tertiary',
|
|
36
42
|
'transition-[color,background-color] duration-[var(--blocks-duration-fast)] ease-out',
|
|
37
43
|
'hover:bg-surface-hover hover:text-text-primary',
|
|
38
|
-
'focus-visible:
|
|
44
|
+
'focus-visible:ring-2 focus-visible:ring-primary'
|
|
39
45
|
],
|
|
40
46
|
// Chromeless auto-growing textarea — the composer surface owns the border.
|
|
41
47
|
textarea: [
|
|
@@ -50,29 +56,24 @@ export const promptInputVariants = tv({
|
|
|
50
56
|
trailing: ['flex items-center gap-1 shrink-0'],
|
|
51
57
|
// Icon-only affordance buttons.
|
|
52
58
|
attachButton: [
|
|
53
|
-
'
|
|
54
|
-
'rounded-modify text-text-secondary cursor-pointer',
|
|
59
|
+
'shrink-0 rounded-modify text-text-secondary',
|
|
55
60
|
'transition-[color,background-color] duration-[var(--blocks-duration-fast)] ease-out',
|
|
56
61
|
'hover:bg-surface-hover hover:text-text-primary',
|
|
57
|
-
'focus-visible:
|
|
58
|
-
'disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent'
|
|
62
|
+
'focus-visible:ring-2 focus-visible:ring-primary'
|
|
59
63
|
],
|
|
60
64
|
sendButton: [
|
|
61
|
-
'
|
|
62
|
-
'rounded-modify cursor-pointer',
|
|
65
|
+
'shrink-0 rounded-modify',
|
|
63
66
|
'bg-primary text-text-on-primary',
|
|
64
67
|
'transition-[color,background-color,opacity] duration-[var(--blocks-duration-fast)] ease-out',
|
|
65
68
|
'hover:bg-primary-hover active:bg-primary-active',
|
|
66
|
-
'focus-visible:
|
|
67
|
-
'disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-primary'
|
|
69
|
+
'focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2'
|
|
68
70
|
],
|
|
69
71
|
stopButton: [
|
|
70
|
-
'
|
|
71
|
-
'rounded-modify cursor-pointer',
|
|
72
|
+
'shrink-0 rounded-modify',
|
|
72
73
|
'bg-neutral text-text-on-primary',
|
|
73
74
|
'transition-[color,background-color] duration-[var(--blocks-duration-fast)] ease-out',
|
|
74
75
|
'hover:bg-neutral-hover active:bg-neutral-active',
|
|
75
|
-
'focus-visible:
|
|
76
|
+
'focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2'
|
|
76
77
|
],
|
|
77
78
|
// Compact inline validation line (first rejection). role=status is set in
|
|
78
79
|
// the component; the empty state renders no box.
|