@juspay/svelte-ui-components 3.4.1 → 3.5.1

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 (154) hide show
  1. package/README.md +1 -1
  2. package/dist/Accordion/Accordion.svelte +1 -5
  3. package/dist/Accordion/properties.d.ts +0 -1
  4. package/dist/AreaChart/AreaChart.svelte +2 -8
  5. package/dist/AreaChart/properties.d.ts +0 -10
  6. package/dist/AttachmentChipRow/AttachmentChipRow.svelte +30 -30
  7. package/dist/AttachmentChipRow/properties.d.ts +6 -6
  8. package/dist/Banner/Banner.svelte +1 -5
  9. package/dist/Banner/properties.d.ts +0 -1
  10. package/dist/BarChart/BarChart.svelte +7 -13
  11. package/dist/BarChart/properties.d.ts +1 -9
  12. package/dist/Book/Book.svelte +1 -5
  13. package/dist/Book/properties.d.ts +0 -1
  14. package/dist/Button/Button.svelte +2 -0
  15. package/dist/Button/properties.d.ts +5 -0
  16. package/dist/Calendar/Calendar.svelte +3 -11
  17. package/dist/Calendar/properties.d.ts +0 -11
  18. package/dist/Chat/Chat.svelte +9 -29
  19. package/dist/Chat/properties.d.ts +0 -9
  20. package/dist/Chat/types.d.ts +7 -0
  21. package/dist/ChatBubble/ChatBubble.svelte +3 -11
  22. package/dist/ChatBubble/properties.d.ts +0 -3
  23. package/dist/ChatComposer/ChatComposer.svelte +18 -44
  24. package/dist/ChatComposer/properties.d.ts +0 -12
  25. package/dist/ChatHeader/ChatHeader.svelte +1 -5
  26. package/dist/ChatHeader/properties.d.ts +0 -1
  27. package/dist/ChatMessage/ChatMessage.svelte +164 -15
  28. package/dist/ChatMessage/properties.d.ts +34 -3
  29. package/dist/ChatMessageList/ChatMessageList.svelte +5 -11
  30. package/dist/ChatMessageList/properties.d.ts +0 -6
  31. package/dist/ChatSuggestions/ChatSuggestions.svelte +1 -5
  32. package/dist/ChatSuggestions/properties.d.ts +0 -1
  33. package/dist/CheckListItem/CheckListItem.svelte +1 -5
  34. package/dist/CheckListItem/properties.d.ts +0 -1
  35. package/dist/Checkbox/Checkbox.svelte +1 -5
  36. package/dist/Checkbox/properties.d.ts +0 -1
  37. package/dist/ChipInput/ChipInput.svelte +10 -20
  38. package/dist/ChipInput/properties.d.ts +0 -4
  39. package/dist/Choicebox/Choicebox.svelte +1 -5
  40. package/dist/Choicebox/properties.d.ts +0 -1
  41. package/dist/ColorPicker/ColorPicker.svelte +4 -10
  42. package/dist/ColorPicker/properties.d.ts +0 -2
  43. package/dist/Combobox/Combobox.svelte +15 -33
  44. package/dist/Combobox/properties.d.ts +0 -8
  45. package/dist/CommandMenu/CommandMenu.svelte +2 -8
  46. package/dist/CommandMenu/properties.d.ts +0 -2
  47. package/dist/ContextMenu/ContextMenu.svelte +3 -11
  48. package/dist/ContextMenu/properties.d.ts +0 -3
  49. package/dist/DateRangePicker/DateRangePicker.svelte +6 -20
  50. package/dist/DateRangePicker/properties.d.ts +0 -19
  51. package/dist/Draggable/Draggable.svelte +7 -7
  52. package/dist/Draggable/properties.d.ts +3 -3
  53. package/dist/DualAxisBarChart/DualAxisBarChart.svelte +1 -5
  54. package/dist/DualAxisBarChart/properties.d.ts +0 -4
  55. package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +1 -5
  56. package/dist/FileDropzoneTrigger/properties.d.ts +0 -1
  57. package/dist/FileInput/FileInput.svelte +2 -8
  58. package/dist/FileInput/properties.d.ts +0 -2
  59. package/dist/FunnelChart/FunnelChart.svelte +2 -8
  60. package/dist/FunnelChart/properties.d.ts +0 -8
  61. package/dist/Gallery/Gallery.svelte +15 -15
  62. package/dist/Gallery/properties.d.ts +6 -6
  63. package/dist/HITL/HITL.svelte +10 -8
  64. package/dist/HITL/properties.d.ts +2 -2
  65. package/dist/IframeViewer/IframeViewer.svelte +3 -2
  66. package/dist/IframeViewer/properties.d.ts +1 -1
  67. package/dist/Img/Img.svelte +119 -39
  68. package/dist/Img/properties.d.ts +0 -1
  69. package/dist/Input/Input.svelte +22 -37
  70. package/dist/Input/properties.d.ts +3 -10
  71. package/dist/InputButton/InputButton.svelte +2 -2
  72. package/dist/LineChart/LineChart.svelte +4 -10
  73. package/dist/LineChart/properties.d.ts +1 -11
  74. package/dist/ListItem/ListItem.svelte +10 -22
  75. package/dist/ListItem/properties.d.ts +5 -10
  76. package/dist/LottiePlayer/LottiePlayer.svelte +2 -8
  77. package/dist/LottiePlayer/properties.d.ts +0 -2
  78. package/dist/MediaPlayer/MediaPlayer.svelte +1 -5
  79. package/dist/MediaPlayer/properties.d.ts +0 -1
  80. package/dist/MediaUpload/MediaUpload.svelte +6 -6
  81. package/dist/MediaUpload/properties.d.ts +3 -3
  82. package/dist/Menu/Menu.svelte +3 -11
  83. package/dist/Menu/properties.d.ts +0 -3
  84. package/dist/Modal/Modal.svelte +15 -31
  85. package/dist/Modal/properties.d.ts +5 -11
  86. package/dist/Pagination/Pagination.svelte +3 -7
  87. package/dist/Pagination/properties.d.ts +1 -2
  88. package/dist/PieChart/PieChart.svelte +4 -10
  89. package/dist/PieChart/properties.d.ts +1 -9
  90. package/dist/Pill/Pill.svelte +1 -5
  91. package/dist/Pill/properties.d.ts +0 -1
  92. package/dist/Radio/Radio.svelte +1 -5
  93. package/dist/Radio/properties.d.ts +0 -1
  94. package/dist/Resizable/Resizable.svelte +3 -11
  95. package/dist/Resizable/properties.d.ts +0 -3
  96. package/dist/SankeyChart/SankeyChart.svelte +4 -14
  97. package/dist/SankeyChart/properties.d.ts +0 -12
  98. package/dist/Scroller/Scroller.svelte +1 -5
  99. package/dist/Scroller/properties.d.ts +0 -1
  100. package/dist/Select/Select.svelte +3 -11
  101. package/dist/Select/properties.d.ts +0 -3
  102. package/dist/Sheet/Sheet.svelte +3 -11
  103. package/dist/Sheet/properties.d.ts +0 -3
  104. package/dist/Slider/Slider.svelte +2 -8
  105. package/dist/Slider/properties.d.ts +0 -2
  106. package/dist/Snippet/Snippet.svelte +1 -5
  107. package/dist/Snippet/properties.d.ts +0 -1
  108. package/dist/SplitButton/SplitButton.svelte +1 -5
  109. package/dist/SplitButton/properties.d.ts +0 -1
  110. package/dist/SplitInput/SplitInput.svelte +6 -14
  111. package/dist/SplitInput/properties.d.ts +0 -3
  112. package/dist/StatCard/StatCard.svelte +2 -2
  113. package/dist/StatCard/properties.d.ts +1 -1
  114. package/dist/Status/Status.svelte +2 -6
  115. package/dist/Status/properties.d.ts +1 -2
  116. package/dist/Stepper/Stepper.svelte +2 -12
  117. package/dist/Stepper/properties.d.ts +1 -14
  118. package/dist/Table/BuiltinCell.svelte +1 -1
  119. package/dist/Table/Table.svelte +16 -17
  120. package/dist/Table/properties.d.ts +3 -35
  121. package/dist/Tabs/Tabs.svelte +2 -8
  122. package/dist/Tabs/properties.d.ts +0 -2
  123. package/dist/TaskList/TaskList.svelte +1 -4
  124. package/dist/TaskList/properties.d.ts +0 -1
  125. package/dist/ThemeSwitcher/ThemeSwitcher.svelte +1 -5
  126. package/dist/ThemeSwitcher/properties.d.ts +0 -1
  127. package/dist/ThinkingIndicator/ThinkingIndicator.svelte +4 -10
  128. package/dist/ThinkingIndicator/properties.d.ts +1 -3
  129. package/dist/Toast/Toast.svelte +2 -2
  130. package/dist/Toast/properties.d.ts +1 -1
  131. package/dist/Toggle/Toggle.svelte +1 -5
  132. package/dist/Toggle/properties.d.ts +0 -1
  133. package/dist/ToolCallLog/ToolCallLog.svelte +1 -10
  134. package/dist/ToolCallLog/properties.d.ts +0 -1
  135. package/dist/Toolbar/Toolbar.svelte +2 -6
  136. package/dist/Toolbar/properties.d.ts +1 -2
  137. package/dist/TypewriterText/TypewriterText.svelte +3 -3
  138. package/dist/TypewriterText/properties.d.ts +1 -1
  139. package/dist-codemod/bin.js +32 -0
  140. package/dist-codemod/cli.js +175 -0
  141. package/dist-codemod/legacy-pairs.js +228 -0
  142. package/dist-codemod/transform.js +278 -0
  143. package/dist-codemod/wc-children.js +66 -0
  144. package/dist-wc/index.js +4629 -5218
  145. package/package.json +9 -7
  146. package/scripts/postinstall.mjs +14 -12
  147. package/dist/deprecation.d.ts +0 -42
  148. package/dist/deprecation.js +0 -57
  149. package/scripts/codemod/README.md +0 -76
  150. package/scripts/codemod/cli.ts +0 -206
  151. package/scripts/codemod/map.ts +0 -118
  152. package/scripts/codemod/transform.ts +0 -549
  153. package/scripts/codemod/tsconfig.json +0 -15
  154. package/scripts/codemod/wc-children.ts +0 -75
@@ -26,18 +26,12 @@
26
26
  richVideoTooltip,
27
27
  richRemoveIcon,
28
28
  richFileIcon,
29
- onremoverichimage: onremoverichimageLegacy,
30
- onRemoveRichImage,
31
- onremoverichfile: onremoverichfileLegacy,
32
- onRemoveRichFile,
33
- onremoverichvideo: onremoverichvideoLegacy,
34
- onRemoveRichVideo,
35
- onopenrichimage: onopenrichimageLegacy,
36
- onOpenRichImage,
37
- onopenrichvideo: onopenrichvideoLegacy,
38
- onOpenRichVideo,
39
- onopenrichfile: onopenrichfileLegacy,
40
- onOpenRichFile,
29
+ onremoverichimage,
30
+ onremoverichfile,
31
+ onremoverichvideo,
32
+ onopenrichimage,
33
+ onopenrichvideo,
34
+ onopenrichfile,
41
35
  sendable = null,
42
36
  accept = '',
43
37
  multiple = false,
@@ -52,21 +46,15 @@
52
46
  actionIcon,
53
47
  actionLabel = 'Voice conversation',
54
48
  leading,
55
- onsubmit: onsubmitLegacy,
56
- onSubmit,
49
+ onsubmit,
57
50
  oninput,
58
51
  onkeydown,
59
52
  onpaste,
60
- onstop: onstopLegacy,
61
- onStop,
62
- onvoice: onvoiceLegacy,
63
- onVoice,
64
- onattach: onattachLegacy,
65
- onAttach,
66
- onattachclick: onattachclickLegacy,
67
- onAttachClick,
68
- onaction: onactionLegacy,
69
- onAction,
53
+ onstop,
54
+ onvoice,
55
+ onattach,
56
+ onattachclick,
57
+ onaction,
70
58
  testId,
71
59
  inputTestId,
72
60
  inputAriaLabel,
@@ -79,20 +67,6 @@
79
67
  classes
80
68
  }: ChatComposerProperties = $props();
81
69
 
82
- // Event-casing phase 1: both spellings accepted, the correct one wins.
83
- const onaction = $derived(onAction ?? onactionLegacy);
84
- const onattach = $derived(onAttach ?? onattachLegacy);
85
- const onattachclick = $derived(onAttachClick ?? onattachclickLegacy);
86
- const onopenrichfile = $derived(onOpenRichFile ?? onopenrichfileLegacy);
87
- const onopenrichimage = $derived(onOpenRichImage ?? onopenrichimageLegacy);
88
- const onopenrichvideo = $derived(onOpenRichVideo ?? onopenrichvideoLegacy);
89
- const onremoverichfile = $derived(onRemoveRichFile ?? onremoverichfileLegacy);
90
- const onremoverichimage = $derived(onRemoveRichImage ?? onremoverichimageLegacy);
91
- const onremoverichvideo = $derived(onRemoveRichVideo ?? onremoverichvideoLegacy);
92
- const onstop = $derived(onStop ?? onstopLegacy);
93
- const onsubmit = $derived(onSubmit ?? onsubmitLegacy);
94
- const onvoice = $derived(onVoice ?? onvoiceLegacy);
95
-
96
70
  let fileInput: HTMLInputElement | null = $state(null);
97
71
 
98
72
  let showVoice = $derived(typeof onvoice === 'function');
@@ -170,12 +144,12 @@
170
144
  images={richImages}
171
145
  videos={richVideos}
172
146
  files={richFiles}
173
- onRemoveImage={onremoverichimage}
174
- onRemoveVideo={onremoverichvideo}
175
- onRemoveFile={onremoverichfile}
176
- onOpenImage={onopenrichimage}
177
- onOpenVideo={onopenrichvideo}
178
- onOpenFile={onopenrichfile}
147
+ onremoveimage={onremoverichimage}
148
+ onremovevideo={onremoverichvideo}
149
+ onremovefile={onremoverichfile}
150
+ onopenimage={onopenrichimage}
151
+ onopenvideo={onopenrichvideo}
152
+ onopenfile={onopenrichfile}
179
153
  imageTooltip={richImageTooltip}
180
154
  videoTooltip={richVideoTooltip}
181
155
  removeIcon={richRemoveIcon}
@@ -73,49 +73,37 @@ export type OptionalChatComposerProperties = {
73
73
  };
74
74
  export type ChatComposerEventProperties = {
75
75
  onsubmit?: (value: string, attachments: File[]) => void;
76
- onSubmit?: (value: string, attachments: File[]) => void;
77
76
  oninput?: (value: string, event: Event) => void;
78
77
  onkeydown?: (event: KeyboardEvent) => void;
79
78
  onpaste?: (event: ClipboardEvent) => void;
80
79
  onstop?: () => void;
81
- onStop?: () => void;
82
80
  onvoice?: () => void;
83
- onVoice?: () => void;
84
81
  onattach?: (files: File[]) => void;
85
- onAttach?: (files: File[]) => void;
86
82
  /**
87
83
  * Intercepts the attach button: when provided, clicking it calls this instead of
88
84
  * opening the built-in file picker — for apps with their own chooser (camera /
89
85
  * gallery / files).
90
86
  */
91
87
  onattachclick?: () => void;
92
- onAttachClick?: () => void;
93
88
  /**
94
89
  * Removal callbacks for the rich strip's chips. Omit them to render the rich
95
90
  * chips read-only (no remove buttons), mirroring AttachmentChipRow.
96
91
  */
97
92
  onremoverichimage?: (id: string) => void;
98
- onRemoveRichImage?: (id: string) => void;
99
93
  onremoverichfile?: (id: string) => void;
100
- onRemoveRichFile?: (id: string) => void;
101
94
  onremoverichvideo?: (id: string) => void;
102
- onRemoveRichVideo?: (id: string) => void;
103
95
  /**
104
96
  * Open/preview callbacks for the rich strip's chips — when provided, a chip's
105
97
  * tile becomes a real button and clicking it fires with the attachment (e.g.
106
98
  * open a lightbox, play the video).
107
99
  */
108
100
  onopenrichimage?: (image: AttachmentChipImage) => void;
109
- onOpenRichImage?: (image: AttachmentChipImage) => void;
110
101
  onopenrichvideo?: (video: AttachmentChipVideo) => void;
111
- onOpenRichVideo?: (video: AttachmentChipVideo) => void;
112
102
  onopenrichfile?: (file: AttachmentChipFile) => void;
113
- onOpenRichFile?: (file: AttachmentChipFile) => void;
114
103
  /**
115
104
  * The composer's idle action (e.g. voice conversation mode). While the value is
116
105
  * empty, nothing is sendable and no reply is streaming, the send button gives way
117
106
  * to an action button that fires this.
118
107
  */
119
108
  onaction?: () => void;
120
- onAction?: () => void;
121
109
  };
@@ -14,16 +14,12 @@
14
14
  closeIcon,
15
15
  closeLabel = 'Close',
16
16
  showClose,
17
- onclose: oncloseLegacy,
18
- onClose,
17
+ onclose,
19
18
  children,
20
19
  testId,
21
20
  classes
22
21
  }: ChatHeaderProperties = $props();
23
22
 
24
- // Event-casing phase 1: both spellings accepted, the correct one wins.
25
- const onclose = $derived(onClose ?? oncloseLegacy);
26
-
27
23
  let displayClose = $derived(showClose ?? typeof onclose === 'function');
28
24
  </script>
29
25
 
@@ -16,5 +16,4 @@ export type OptionalChatHeaderProperties = {
16
16
  };
17
17
  export type ChatHeaderEventProperties = {
18
18
  onclose?: () => void;
19
- onClose?: () => void;
20
19
  };
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import Button from '../Button/Button.svelte';
3
3
  import LoadingDots from '../LoadingDots/LoadingDots.svelte';
4
+ import TypewriterText from '../TypewriterText/TypewriterText.svelte';
4
5
  import copySvg from '../assets/copy.svg?raw';
5
6
  import checkmarkSvg from '../assets/checkmark.svg?raw';
6
7
  import retrySvg from '../assets/retry.svg?raw';
@@ -17,6 +18,10 @@
17
18
  markdown,
18
19
  body,
19
20
  streaming = false,
21
+ typewriter = false,
22
+ typewriterSpeed,
23
+ clampLines = 0,
24
+ marker = false,
20
25
  status,
21
26
  avatar,
22
27
  header,
@@ -25,22 +30,48 @@
25
30
  actions,
26
31
  copyLabel = 'Copy',
27
32
  retryLabel = 'Retry',
33
+ expandLabel = 'Expand message',
34
+ collapseLabel = 'Collapse message',
28
35
  feedbackUpLabel = 'Good response',
29
36
  feedbackDownLabel = 'Bad response',
30
- onretry: onretryLegacy,
31
- onRetry,
32
- onfeedback: onfeedbackLegacy,
33
- onFeedback,
34
- oncopy: oncopyLegacy,
35
- onCopy,
37
+ onretry,
38
+ onfeedback,
39
+ oncopy,
36
40
  testId,
37
41
  classes
38
42
  }: ChatMessageProperties = $props();
39
43
 
40
- // Event-casing phase 1: both spellings accepted, the correct one wins.
41
- const oncopy = $derived(onCopy ?? oncopyLegacy);
42
- const onfeedback = $derived(onFeedback ?? onfeedbackLegacy);
43
- const onretry = $derived(onRetry ?? onretryLegacy);
44
+ // A long message can be collapsed to a few lines and opened by click or keyboard.
45
+ // Consumers had been rebuilding this around the `body` snippet; it belongs here so
46
+ // the chrome, the clamp and the expanded state stay one component's concern.
47
+ let expanded = $state(false);
48
+ let clampable = $derived(typeof clampLines === 'number' && clampLines > 0);
49
+ let clampActive = $derived(clampable && !expanded);
50
+ // Turning clamping off must not leave the message stuck open: without this, a parent
51
+ // that sets clampLines to 0 and later restores it would get an already-expanded bubble
52
+ // instead of the clamped default. A prop going away cannot be observed from a $derived
53
+ // without writing to state, so this is the sanctioned escape hatch rather than a
54
+ // reactive convenience.
55
+ // eslint-disable-next-line no-restricted-syntax
56
+ $effect(() => {
57
+ if (!clampable) {
58
+ expanded = false;
59
+ }
60
+ });
61
+ // Ties the toggle to the region it expands. Per-instance, so two clamped messages
62
+ // on one page never collide.
63
+ const uid = $props.id();
64
+ const bodyId = `chat-message-body-${uid}`;
65
+ // Spread rather than a ternary: Button's testId is `string | undefined` and the
66
+ // repo forbids the `undefined` literal, so the prop is omitted instead of blanked.
67
+ let clampToggleTestId = $derived(
68
+ typeof testId === 'string' ? { testId: `${testId}-clamp-toggle` } : {}
69
+ );
70
+ const toggleExpanded = () => {
71
+ if (clampable) {
72
+ expanded = !expanded;
73
+ }
74
+ };
44
75
 
45
76
  let party = $derived(partyOf(role));
46
77
 
@@ -75,8 +106,40 @@
75
106
  hasMarkdown && renderMarkdownFn !== null ? renderMarkdownFn(markdown ?? '') : html
76
107
  );
77
108
  let hasHtml = $derived(typeof effectiveHtml === 'string' && effectiveHtml.length > 0);
109
+
78
110
  let hasBody = $derived(typeof body === 'function');
79
- let hasContent = $derived(hasBody || hasHtml || content.length > 0);
111
+
112
+ /* The typewriter reveals SOURCE text, not rendered HTML: typing a pre-rendered
113
+ `html` string character by character would put its tags on screen. So it drives
114
+ off `markdown` or `content` and leaves an `html`-only message to the normal
115
+ path. `markdown` is typed through the same pipeline the static branch uses, so
116
+ rich text reveals as rich text rather than as raw syntax that reformats at the
117
+ end. */
118
+ let typewriterSource = $derived(hasMarkdown ? (markdown ?? '') : content);
119
+ /* Gated on the pipeline for a markdown source. The renderer loads asynchronously, and
120
+ without this the typewriter starts on the SOURCE: `**bold**` is typed literally and
121
+ then reformats when the module resolves. Waiting matches what the static branch
122
+ already does — degrade to the `html`/`content` fallback until the renderer is there —
123
+ rather than showing syntax the consumer never asked to display. */
124
+ let typewriterActive = $derived(
125
+ typewriter &&
126
+ !hasBody &&
127
+ typewriterSource.length > 0 &&
128
+ (!hasMarkdown || renderMarkdownFn !== null)
129
+ );
130
+ let typewriterRenderText = $derived(
131
+ hasMarkdown && renderMarkdownFn !== null ? { renderText: renderMarkdownFn } : {}
132
+ );
133
+ // Clamped at the boundary the way LoadingDots clamps `dots`, so the prop's floor is part
134
+ // of this component's contract rather than something a consumer discovers. The finite
135
+ // check is not redundant: `typeof NaN === 'number'`, and `Math.max(1, NaN)` is `NaN`, so
136
+ // without it the documented floor is not actually enforced.
137
+ let typewriterPacing = $derived(
138
+ typeof typewriterSpeed === 'number' && Number.isFinite(typewriterSpeed)
139
+ ? { speed: Math.max(1, typewriterSpeed) }
140
+ : {}
141
+ );
142
+ let hasContent = $derived(hasBody || hasHtml || content.length > 0 || typewriterActive);
80
143
  let showTyping = $derived(streaming && !hasContent);
81
144
  let showRetry = $derived(typeof onretry === 'function');
82
145
  let showFeedback = $derived(typeof onfeedback === 'function');
@@ -136,14 +199,64 @@
136
199
  <div class="header">{@render header()}</div>
137
200
  {/if}
138
201
 
139
- <div class="bubble">
202
+ <!-- The bubble deliberately carries no role and no handler of its own. A button has
203
+ presentational children, so making the bubble one would flatten the paragraphs and
204
+ lists a message body renders; and a click target that is not focusable is a control
205
+ keyboard users cannot reach. The Button below is the whole control. -->
206
+ <div
207
+ class="bubble"
208
+ class:clampable
209
+ class:has-marker={marker}
210
+ style={clampable ? `--chat-message-clamp-lines-prop: ${clampLines};` : null}
211
+ data-clamped={clampActive ? 'true' : null}
212
+ data-expanded={clampable ? String(expanded) : null}
213
+ >
214
+ {#if marker}
215
+ <span class="marker" aria-hidden="true"></span>
216
+ {/if}
217
+
140
218
  {#if hasBody}
141
- <div class="body">{@render body?.()}</div>
219
+ <div class="body" id={bodyId}>{@render body?.()}</div>
220
+ {:else if typewriterActive}
221
+ <!-- ChatMessageList is a role="log" aria-live="polite" region, so a body whose text
222
+ grows one character at a time would be announced as it grows. Silence it only
223
+ WHILE it is growing: leaving it off once streaming ends would suppress the
224
+ settled message too, which is the announcement the live region exists for.
225
+ `aria-live="off"` is the spec answer (the innermost live setting wins), and
226
+ `aria-busy` is the purpose-built one — "being modified, wait before exposing" —
227
+ so the two together do not depend on a single behaviour being implemented. -->
228
+ <div
229
+ class="body"
230
+ id={bodyId}
231
+ aria-live={streaming ? 'off' : null}
232
+ aria-busy={streaming ? 'true' : null}
233
+ >
234
+ <TypewriterText
235
+ text={typewriterSource}
236
+ isStreaming={streaming}
237
+ {...typewriterRenderText}
238
+ {...typewriterPacing}
239
+ />
240
+ </div>
142
241
  {:else if hasHtml}
143
242
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
144
- <div class="body">{@html effectiveHtml}</div>
243
+ <div class="body" id={bodyId}>{@html effectiveHtml}</div>
145
244
  {:else if content.length > 0}
146
- <div class="body text">{content}</div>
245
+ <div class="body text" id={bodyId}>{content}</div>
246
+ {/if}
247
+
248
+ {#if clampable}
249
+ <div class="clamp-toggle">
250
+ <Button
251
+ variant="ghost"
252
+ size="sm"
253
+ onclick={toggleExpanded}
254
+ ariaExpanded={expanded}
255
+ ariaControls={bodyId}
256
+ text={expanded ? collapseLabel : expandLabel}
257
+ {...clampToggleTestId}
258
+ />
259
+ </div>
147
260
  {/if}
148
261
 
149
262
  {#if showTyping}
@@ -264,6 +377,27 @@
264
377
  overflow-wrap: anywhere;
265
378
  }
266
379
 
380
+ /* Only the marker variant becomes a containing block: making every bubble relative
381
+ would silently re-base any absolutely-positioned content a consumer renders in a
382
+ body snippet. */
383
+ .bubble.has-marker {
384
+ position: relative;
385
+ }
386
+
387
+ .marker {
388
+ /* Decorative and absolutely positioned, so it sits over the bubble's leading
389
+ padding — where a link at the start of a responder message also sits. Without
390
+ this it silently eats those clicks. */
391
+ pointer-events: none;
392
+ position: absolute;
393
+ inset-block: var(--chat-message-marker-inset-block, 0);
394
+ /* Logical, so the bar follows `role` alignment into RTL instead of pinning left. */
395
+ inset-inline-start: var(--chat-message-marker-offset, 0px);
396
+ width: var(--chat-message-marker-width, 2px);
397
+ border-radius: var(--chat-message-marker-border-radius, 0);
398
+ background: var(--chat-message-marker-color, #6d28d9);
399
+ }
400
+
267
401
  .chat-message.party-sender .bubble {
268
402
  color: var(--chat-message-sender-color, #ffffff);
269
403
  background: var(--chat-message-sender-background, #18181b);
@@ -341,6 +475,21 @@
341
475
  }
342
476
  }
343
477
 
478
+ .clamp-toggle {
479
+ display: flex;
480
+ margin-top: var(--chat-message-clamp-toggle-margin-top, 4px);
481
+ }
482
+
483
+ /* Clamp the rendered body, not the source: the message keeps its markup and its
484
+ copy text, and expanding is a state change rather than a re-render. */
485
+ .bubble[data-clamped='true'] .body {
486
+ display: -webkit-box;
487
+ line-clamp: var(--chat-message-clamp-lines, var(--chat-message-clamp-lines-prop, 2));
488
+ -webkit-line-clamp: var(--chat-message-clamp-lines, var(--chat-message-clamp-lines-prop, 2));
489
+ -webkit-box-orient: vertical;
490
+ overflow: hidden;
491
+ }
492
+
344
493
  .body :global(p) {
345
494
  margin: var(--chat-message-paragraph-margin, 0 0 0.5em 0);
346
495
  }
@@ -27,7 +27,35 @@ export type OptionalChatMessageProperties = {
27
27
  * action still has something to copy.
28
28
  */
29
29
  body?: Snippet | null;
30
+ /**
31
+ * Collapse the rendered body to this many lines and render a `Button` below it
32
+ * that expands and re-collapses the message; that button is the only control.
33
+ * `0` or omitted leaves the message uncollapsed and renders no control.
34
+ * The clamp applies to the rendered body, so `content` — and therefore the copy
35
+ * action — always carries the whole message. A consumer stylesheet setting
36
+ * `--chat-message-clamp-lines` takes priority over this value, so a theme can set
37
+ * the line count for every message without touching each call site.
38
+ */
39
+ clampLines?: number;
40
+ /**
41
+ * Draw an accent bar along the bubble's leading edge — the quote-bar shape a chat
42
+ * surface uses to mark one party's turns without giving them a filled bubble.
43
+ * Decorative: it carries no text and is hidden from assistive technology, so the
44
+ * distinction it makes must also be carried by `role`, which it is.
45
+ */
46
+ marker?: boolean;
30
47
  streaming?: boolean;
48
+ /**
49
+ * Reveal the message progressively instead of all at once, using `TypewriterText`.
50
+ * Drives off `markdown` or `content` — never `html`, since typing a pre-rendered
51
+ * string would put its tags on screen — and types `markdown` through the same
52
+ * pipeline the static branch uses, so rich text reveals as rich text. `streaming`
53
+ * decides the mode: while it is true, text keeps typing as it grows; the moment it
54
+ * turns false the remainder is shown at once. Ignored when a `body` snippet is set.
55
+ */
56
+ typewriter?: boolean;
57
+ /** Milliseconds between characters while typing, clamped to a minimum of 1. A non-finite value is ignored. Defaults to `TypewriterText`'s own. */
58
+ typewriterSpeed?: number;
31
59
  status?: ChatMessageStatus;
32
60
  avatar?: Snippet;
33
61
  header?: Snippet;
@@ -36,6 +64,12 @@ export type OptionalChatMessageProperties = {
36
64
  actions?: Snippet;
37
65
  copyLabel?: string;
38
66
  retryLabel?: string;
67
+ /**
68
+ * Label on the expand/collapse button rendered when `clampLines` is set, swapped
69
+ * on the expanded state.
70
+ */
71
+ expandLabel?: string;
72
+ collapseLabel?: string;
39
73
  feedbackUpLabel?: string;
40
74
  feedbackDownLabel?: string;
41
75
  testId?: string;
@@ -43,9 +77,6 @@ export type OptionalChatMessageProperties = {
43
77
  };
44
78
  export type ChatMessageEventProperties = {
45
79
  onretry?: (() => void) | null;
46
- onRetry?: (() => void) | null;
47
80
  onfeedback?: ((value: ChatMessageFeedback) => void) | null;
48
- onFeedback?: ((value: ChatMessageFeedback) => void) | null;
49
81
  oncopy?: (text: string) => void;
50
- onCopy?: (text: string) => void;
51
82
  };
@@ -23,21 +23,13 @@
23
23
  jumpLabel = 'Jump to latest',
24
24
  jumpIcon,
25
25
  allowCopy = false,
26
- onscrollstate: onscrollstateLegacy,
27
- onScrollState,
28
- onretry: onretryLegacy,
29
- onRetry,
30
- onfeedback: onfeedbackLegacy,
31
- onFeedback,
26
+ onscrollstate,
27
+ onretry,
28
+ onfeedback,
32
29
  testId,
33
30
  classes
34
31
  }: ChatMessageListProperties = $props();
35
32
 
36
- // Event-casing phase 1: both spellings accepted, the correct one wins.
37
- const onfeedback = $derived(onFeedback ?? onfeedbackLegacy);
38
- const onretry = $derived(onRetry ?? onretryLegacy);
39
- const onscrollstate = $derived(onScrollState ?? onscrollstateLegacy);
40
-
41
33
  let listEl: HTMLElement | null = $state(null);
42
34
  let innerEl: HTMLElement | null = $state(null);
43
35
  let atBottom = $state(true);
@@ -311,6 +303,8 @@
311
303
  markdown={msg.markdown}
312
304
  body={typeof messageBody === 'function' ? bodyFor : null}
313
305
  streaming={msg.streaming}
306
+ typewriter={msg.typewriter}
307
+ typewriterSpeed={msg.typewriterSpeed}
314
308
  status={msg.status}
315
309
  allowCopy={allowCopy && partyOf(msg.role) === 'responder'}
316
310
  attachments={typeof messageAttachments === 'function' ? attachmentsFor : null}
@@ -49,12 +49,6 @@ export type ChatMessageListEventProperties = {
49
49
  atBottom: boolean;
50
50
  scrollable: boolean;
51
51
  }) => void;
52
- onScrollState?: (state: {
53
- atBottom: boolean;
54
- scrollable: boolean;
55
- }) => void;
56
52
  onretry?: () => void;
57
- onRetry?: () => void;
58
53
  onfeedback?: (value: ChatMessageFeedback, message: ChatMessageData) => void;
59
- onFeedback?: (value: ChatMessageFeedback, message: ChatMessageData) => void;
60
54
  };
@@ -13,15 +13,11 @@
13
13
  loading = false,
14
14
  icon,
15
15
  chipClasses,
16
- onselect: onselectLegacy,
17
- onSelect,
16
+ onselect,
18
17
  testId,
19
18
  classes
20
19
  }: ChatSuggestionsProperties = $props();
21
20
 
22
- // Event-casing phase 1: both spellings accepted, the correct one wins.
23
- const onselect = $derived(onSelect ?? onselectLegacy);
24
-
25
21
  const labelOf = (item: ChatSuggestion): string => (typeof item === 'string' ? item : item.label);
26
22
 
27
23
  const valueOf = (item: ChatSuggestion): string => {
@@ -55,5 +55,4 @@ export type OptionalChatSuggestionsProperties = {
55
55
  };
56
56
  export type ChatSuggestionsEventProperties = {
57
57
  onselect?: (value: string, index: number) => void;
58
- onSelect?: (value: string, index: number) => void;
59
58
  };
@@ -8,14 +8,10 @@
8
8
  disabled = false,
9
9
  checkboxLabel,
10
10
  testId,
11
- onclick: onclickLegacy,
12
- onClick,
11
+ onclick,
13
12
  classes
14
13
  }: CheckListItemProperties = $props();
15
14
 
16
- // Event-casing phase 1: both spellings accepted, the correct one wins.
17
- const onclick = $derived(onClick ?? onclickLegacy);
18
-
19
15
  function handleClick(value: boolean): void {
20
16
  checked = value;
21
17
  onclick?.(checked);
@@ -12,5 +12,4 @@ export type OptionalCheckListItemProperties = {
12
12
  };
13
13
  export type CheckListItemEventProperties = {
14
14
  onclick?: (checked: boolean) => void;
15
- onClick?: (checked: boolean) => void;
16
15
  };
@@ -11,16 +11,12 @@
11
11
  testId,
12
12
  checkedIcon,
13
13
  indeterminateIcon,
14
- onclick: onclickLegacy,
15
- onClick,
14
+ onclick,
16
15
  classes,
17
16
  ariaControls,
18
17
  ariaLabel
19
18
  }: CheckboxProperties = $props();
20
19
 
21
- // Event-casing phase 1: both spellings accepted, the correct one wins.
22
- const onclick = $derived(onClick ?? onclickLegacy);
23
-
24
20
  function handleClick(): void {
25
21
  if (disabled) {
26
22
  return;
@@ -20,5 +20,4 @@ export type OptionalCheckboxProperties = {
20
20
  };
21
21
  export type CheckboxEventProperties = {
22
22
  onclick?: (checked: boolean) => void;
23
- onClick?: (checked: boolean) => void;
24
23
  };
@@ -11,22 +11,12 @@
11
11
  editable = false,
12
12
  testId,
13
13
  classes,
14
- onadd: onaddLegacy,
15
- onAdd,
16
- ondismiss: ondismissLegacy,
17
- onDismiss,
18
- onedit: oneditLegacy,
19
- onEdit,
20
- onchange: onchangeLegacy,
21
- onChange
14
+ onadd,
15
+ ondismiss,
16
+ onedit,
17
+ onchange
22
18
  }: ChipInputProperties = $props();
23
19
 
24
- // Event-casing phase 1: both spellings accepted, the correct one wins.
25
- const onadd = $derived(onAdd ?? onaddLegacy);
26
- const onchange = $derived(onChange ?? onchangeLegacy);
27
- const ondismiss = $derived(onDismiss ?? ondismissLegacy);
28
- const onedit = $derived(onEdit ?? oneditLegacy);
29
-
30
20
  let draft = $state('');
31
21
 
32
22
  // The chip currently swapped for an inline edit field, held by VALUE rather than by slot
@@ -153,11 +143,11 @@
153
143
  actionInput={false}
154
144
  classes="chip-input-edit"
155
145
  bind:this={editInputRef}
156
- onInput={(nextValue) => {
146
+ oninput={(nextValue) => {
157
147
  editDraft = nextValue;
158
148
  }}
159
- onKeyDown={(event) => handleEditKeyDown(event, chip)}
160
- onBlur={() => handleEditBlur(chip)}
149
+ onkeydown={(event) => handleEditKeyDown(event, chip)}
150
+ onblur={() => handleEditBlur(chip)}
161
151
  {...typeof testId === 'string' ? { testId: `${testId}-item-${index}-edit` } : {}}
162
152
  />
163
153
  </div>
@@ -186,11 +176,11 @@
186
176
  actionInput={false}
187
177
  disable={disabled}
188
178
  classes="chip-input-draft"
189
- onInput={(nextValue) => {
179
+ oninput={(nextValue) => {
190
180
  draft = nextValue;
191
181
  }}
192
- onKeyDown={handleKeyDown}
193
- onBlur={addChip}
182
+ onkeydown={handleKeyDown}
183
+ onblur={addChip}
194
184
  {...typeof testId === 'string' ? { testId: `${testId}-add` } : {}}
195
185
  />
196
186
  </div>
@@ -34,16 +34,12 @@ export type OptionalChipInputProperties = {
34
34
  };
35
35
  export type ChipInputEventProperties = {
36
36
  onadd?: (value: string) => void;
37
- onAdd?: (value: string) => void;
38
37
  ondismiss?: (value: string) => void;
39
- onDismiss?: (value: string) => void;
40
38
  /**
41
39
  * Fires after an in-place edit (see `editable`) commits a value that actually changed. Not
42
40
  * fired when the edit is cancelled (Escape) or committed with the text unchanged.
43
41
  */
44
42
  onedit?: (value: string, previousValue: string) => void;
45
- onEdit?: (value: string, previousValue: string) => void;
46
43
  /** Fires alongside `onadd`/`ondismiss`/`onedit`, after any of them has already updated `values`. */
47
44
  onchange?: (values: string[]) => void;
48
- onChange?: (values: string[]) => void;
49
45
  };