@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.
Files changed (107) hide show
  1. package/README.md +1 -1
  2. package/dist/components/Calendar/calendar.variants.d.ts +63 -63
  3. package/dist/components/Chat/A2UIView/A2UINode.svelte +20 -39
  4. package/dist/components/Chat/A2UIView/A2UIView.svelte +47 -50
  5. package/dist/components/Chat/A2UIView/a2ui-basic-catalog.d.ts +17 -0
  6. package/dist/components/Chat/A2UIView/a2ui-basic-catalog.js +69 -0
  7. package/dist/components/Chat/A2UIView/a2ui-catalog.d.ts +95 -0
  8. package/dist/components/Chat/A2UIView/a2ui-catalog.js +87 -0
  9. package/dist/components/Chat/A2UIView/a2ui-node-common.d.ts +40 -0
  10. package/dist/components/Chat/A2UIView/a2ui-node-common.js +72 -0
  11. package/dist/components/Chat/A2UIView/a2ui-prompt.d.ts +21 -10
  12. package/dist/components/Chat/A2UIView/a2ui-prompt.js +170 -24
  13. package/dist/components/Chat/A2UIView/a2ui-registry.d.ts +1 -1
  14. package/dist/components/Chat/A2UIView/a2ui-registry.js +2 -1
  15. package/dist/components/Chat/A2UIView/a2ui-render.d.ts +30 -0
  16. package/dist/components/Chat/A2UIView/a2ui-render.js +61 -23
  17. package/dist/components/Chat/A2UIView/a2ui-router.d.ts +136 -0
  18. package/dist/components/Chat/A2UIView/a2ui-router.js +264 -0
  19. package/dist/components/Chat/A2UIView/a2ui-schema.d.ts +45 -0
  20. package/dist/components/Chat/A2UIView/a2ui-schema.js +133 -0
  21. package/dist/components/Chat/A2UIView/a2ui-stream.d.ts +120 -0
  22. package/dist/components/Chat/A2UIView/a2ui-stream.js +406 -0
  23. package/dist/components/Chat/A2UIView/a2ui-transcript.d.ts +65 -0
  24. package/dist/components/Chat/A2UIView/a2ui-transcript.js +101 -0
  25. package/dist/components/Chat/A2UIView/a2ui-validate.d.ts +33 -1
  26. package/dist/components/Chat/A2UIView/a2ui-validate.js +167 -63
  27. package/dist/components/Chat/A2UIView/a2ui.types.d.ts +50 -0
  28. package/dist/components/Chat/A2UIView/a2ui.types.js +38 -1
  29. package/dist/components/Chat/A2UIView/index.d.ts +35 -2
  30. package/dist/components/Chat/A2UIView/index.js +13 -0
  31. package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte +831 -0
  32. package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte.d.ts +4 -0
  33. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.d.ts +14 -0
  34. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.js +87 -0
  35. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.d.ts +78 -0
  36. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.js +783 -0
  37. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.d.ts +43 -0
  38. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.js +33 -0
  39. package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.d.ts +7 -0
  40. package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.js +86 -0
  41. package/dist/components/Chat/ChatMessage/ChatMessage.svelte +93 -67
  42. package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +26 -19
  43. package/dist/components/Chat/ChatMessage/chat-message.variants.js +45 -12
  44. package/dist/components/Chat/ChatMessage/index.d.ts +20 -5
  45. package/dist/components/Chat/CodeBlock/CodeBlock.svelte +59 -33
  46. package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +7 -0
  47. package/dist/components/Chat/CodeBlock/code-block.variants.js +28 -13
  48. package/dist/components/Chat/CodeBlock/index.d.ts +39 -4
  49. package/dist/components/Chat/PromptInput/PromptInput.svelte +9 -16
  50. package/dist/components/Chat/PromptInput/prompt-input.variants.js +16 -15
  51. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +9 -4
  52. package/dist/components/Chat/ToolCallCard/index.d.ts +4 -3
  53. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +0 -3
  54. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +4 -4
  55. package/dist/components/CopyButton/CopyButton.svelte +9 -19
  56. package/dist/components/CopyButton/copy-button.variants.d.ts +4 -4
  57. package/dist/components/FileUpload/fileUpload.variants.js +15 -4
  58. package/dist/components/PinInput/PinInput.svelte +1 -0
  59. package/dist/components/PinInput/pin-input.variants.js +14 -1
  60. package/dist/components/TimeInput/time-input.variants.js +16 -1
  61. package/dist/i18n/index.d.ts +398 -2
  62. package/dist/internal/copy-state.svelte.d.ts +60 -0
  63. package/dist/internal/copy-state.svelte.js +76 -0
  64. package/dist/internal/field-chrome.d.ts +18 -1
  65. package/dist/internal/field-chrome.js +19 -2
  66. package/dist/primitives/Alert/alert.variants.d.ts +8 -8
  67. package/dist/primitives/Avatar/avatar.variants.js +4 -1
  68. package/dist/primitives/Badge/badge.variants.js +11 -2
  69. package/dist/primitives/Button/button.variants.js +4 -1
  70. package/dist/primitives/Card/card.variants.js +4 -1
  71. package/dist/primitives/Checkbox/checkbox.variants.js +7 -1
  72. package/dist/primitives/Combobox/Combobox.svelte +9 -1
  73. package/dist/primitives/Combobox/combobox.variants.d.ts +25 -0
  74. package/dist/primitives/Combobox/combobox.variants.js +43 -7
  75. package/dist/primitives/Combobox/index.d.ts +8 -2
  76. package/dist/primitives/Dialog/dialog.variants.js +4 -1
  77. package/dist/primitives/Drawer/drawer.variants.js +4 -1
  78. package/dist/primitives/Input/input.variants.js +17 -1
  79. package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +22 -22
  80. package/dist/primitives/Menu/menu.variants.js +5 -1
  81. package/dist/primitives/Progress/progress.variants.js +5 -2
  82. package/dist/primitives/RadioGroup/radioGroup.variants.js +74 -10
  83. package/dist/primitives/Select/select.variants.js +14 -5
  84. package/dist/primitives/Slider/Slider.svelte +4 -1
  85. package/dist/primitives/Slider/slider.variants.js +9 -2
  86. package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
  87. package/dist/primitives/Stepper/stepper.variants.d.ts +11 -11
  88. package/dist/primitives/Stepper/stepper.variants.js +2 -1
  89. package/dist/primitives/Tab/tab.variants.js +4 -1
  90. package/dist/primitives/Textarea/textarea.variants.js +16 -2
  91. package/dist/primitives/Toast/toast.variants.d.ts +12 -12
  92. package/dist/primitives/Toast/toast.variants.js +5 -1
  93. package/dist/primitives/Toggle/toggle.variants.js +94 -18
  94. package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
  95. package/dist/primitives/Tooltip/tooltip.variants.js +5 -1
  96. package/dist/style/foundation.css +19 -9
  97. package/dist/style/semantic.css +49 -2
  98. package/dist/style/themes/forest.css +2 -2
  99. package/dist/style/themes/neutral.css +2 -2
  100. package/dist/style/themes/ocean.css +2 -2
  101. package/dist/style/themes/rose.css +2 -2
  102. package/dist/style/themes/sunset.css +2 -2
  103. package/dist/utils/figma-token-export.js +7 -3
  104. package/dist/utils/persistent-state.svelte.d.ts +25 -1
  105. package/dist/utils/persistent-state.svelte.js +98 -10
  106. package/docs/MIGRATION-v5.md +29 -0
  107. 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
- statusAlert: ['mt-1'],
34
- // footer row holding actions + metadata
35
- footer: ['flex items-center gap-2'],
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
- 'inline-flex items-center justify-center rounded-modify p-1.5',
43
- 'text-text-tertiary cursor-pointer',
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:outline-none focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1'
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) bubble packs to the right edge.
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
- bubble: 'max-w-[85%] rounded-contain bg-primary-subtle'
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
- bubble: 'max-w-[85%] rounded-contain bg-surface-elevated'
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
- bubble: 'max-w-[90%] rounded-contain border border-border-subtle bg-surface-base text-sm text-text-secondary'
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
- /** Accessible label / tooltip for the copy action. @default 'Copy message' */
71
+ /**
72
+ * Accessible label / tooltip for the copy action.
73
+ * @default the `accessibility.copy` translation
74
+ */
72
75
  copyLabel?: string;
73
- /** Accessible label shown briefly after a successful copy. @default 'Copied' */
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 | attachmentSize |
90
- * sourcesFooter | placeholder | statusAlert | footer | actions | actionButton |
91
- * metadata. Reasoning and tool-call parts render through ReasoningDisclosure /
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 code',
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
- const variantProps: CodeBlockVariants = $derived({ wrap });
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
- const showHeader = $derived(!!lang || showCopy || !!actions);
49
- const regionLabel = $derived(lang ? `${lang} code` : 'Code');
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
- let copied = $state(false);
52
- let resetTimer: ReturnType<typeof setTimeout> | undefined;
71
+ const copyState = createCopyState();
53
72
 
54
- async function copyCode() {
55
- try {
56
- await navigator.clipboard.writeText(code);
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
- } catch (err) {
65
- // Never swallow silently — but leave state untouched so the UI does not
66
- // falsely confirm a copy that failed (e.g. denied clipboard permission).
67
- console.error('CodeBlock: failed to copy code', err);
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
- // Clear a pending reset on unmount so the timer can't fire into a torn-down component.
72
- $effect(() => () => {
73
- if (resetTimer) clearTimeout(resetTimer);
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')}>{lang ?? ''}</span>
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={copyCode}
88
- aria-label={copied ? copiedLabel : copyLabel}
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 confirmation for screen readers. A label change on the button the user
115
- just activated is not a reliable announcement — a live status region is. It
116
- must exist in the DOM before `copied` flips, so it always renders and only
117
- its text content changes.
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">{copied ? copiedLabel : ''}</span>
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
- // Architectural surface an elevated card, structure radius, clipped so the
5
- // scrollable body respects the rounded corners.
6
- root: [
7
- 'relative block w-full overflow-hidden box-border',
8
- 'bg-surface-elevated border border-border-subtle rounded-contain'
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 px-3 py-2.5',
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 card with a one-click copy interaction: the copy
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
- /** Accessible label / tooltip for the copy button in its idle state. */
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
- /** Accessible label shown for two seconds after a successful copy. */
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
- /** Per-slot class overrides. Slots: root | header | langLabel | copyButton | pre | code */
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
- <button
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
- </button>
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
- <button
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
- </button>
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
- <button
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
- </button>
402
+ </CoreIconButton>
409
403
  {:else}
410
- <button
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
- </button>
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
- 'inline-flex items-center justify-center shrink-0',
34
- 'size-5 rounded-modify',
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:outline-none focus-visible:ring-2 focus-visible:ring-primary'
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
- 'inline-flex items-center justify-center shrink-0',
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:outline-none focus-visible:ring-2 focus-visible:ring-primary',
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
- 'inline-flex items-center justify-center shrink-0',
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:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',
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
- 'inline-flex items-center justify-center shrink-0',
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:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2'
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.