@juspay/svelte-ui-components 4.27.6 → 4.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +140 -36
  2. package/dist/Animations/ModalAnimation.svelte +15 -1
  3. package/dist/Animations/OverlayAnimation.svelte +7 -1
  4. package/dist/AreaChart/AreaChart.svelte +366 -85
  5. package/dist/AspectRatio/AspectRatio.svelte +51 -0
  6. package/dist/AspectRatio/AspectRatio.svelte.d.ts +3 -0
  7. package/dist/AspectRatio/properties.d.ts +27 -0
  8. package/dist/AspectRatio/properties.js +1 -0
  9. package/dist/Banner/Banner.svelte +10 -4
  10. package/dist/Banner/properties.d.ts +8 -0
  11. package/dist/BarChart/BarChart.svelte +156 -49
  12. package/dist/Book/Book.svelte +14 -1
  13. package/dist/BrandLoader/BrandLoader.svelte +76 -12
  14. package/dist/Button/Button.svelte +56 -11
  15. package/dist/Calendar/Calendar.svelte +88 -61
  16. package/dist/Card/Card.svelte +1 -1
  17. package/dist/Carousel/Carousel.svelte +6 -3
  18. package/dist/ChatComposer/ChatComposer.svelte +54 -4
  19. package/dist/ChatComposer/dictationState.d.ts +15 -0
  20. package/dist/ChatComposer/dictationState.js +9 -0
  21. package/dist/ChatComposer/properties.d.ts +28 -1
  22. package/dist/ChatMessage/ChatMessage.svelte +27 -1
  23. package/dist/CheckListItem/CheckListItem.svelte +3 -3
  24. package/dist/Checkbox/Checkbox.svelte +84 -3
  25. package/dist/Checkbox/properties.d.ts +45 -13
  26. package/dist/ChipInput/ChipInput.svelte +70 -3
  27. package/dist/ChipInput/properties.d.ts +16 -0
  28. package/dist/Choicebox/Choicebox.svelte +156 -1
  29. package/dist/Choicebox/group.d.ts +63 -0
  30. package/dist/Choicebox/group.js +127 -0
  31. package/dist/Choicebox/properties.d.ts +46 -0
  32. package/dist/ColorPicker/ColorPicker.svelte +169 -20
  33. package/dist/ColorPicker/properties.d.ts +16 -0
  34. package/dist/Combobox/Combobox.svelte +87 -25
  35. package/dist/Combobox/properties.d.ts +23 -0
  36. package/dist/CommandMenu/CommandMenu.svelte +27 -11
  37. package/dist/ContextMenu/ContextMenu.svelte +31 -37
  38. package/dist/DateRangePicker/DateRangePicker.svelte +171 -73
  39. package/dist/DeltaIndicator/DeltaIndicator.svelte +3 -3
  40. package/dist/Draggable/Draggable.svelte +12 -7
  41. package/dist/DualAxisBarChart/DualAxisBarChart.svelte +11 -4
  42. package/dist/EmptyState/EmptyState.svelte +1 -1
  43. package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +1 -1
  44. package/dist/FileInput/FileInput.svelte +50 -1
  45. package/dist/FileInput/properties.d.ts +7 -0
  46. package/dist/FunnelChart/FunnelChart.svelte +40 -12
  47. package/dist/FunnelChart/properties.d.ts +7 -1
  48. package/dist/Gallery/Gallery.svelte +20 -13
  49. package/dist/Gallery/properties.d.ts +8 -0
  50. package/dist/GridItem/GridItem.svelte +31 -1
  51. package/dist/HITL/HITL.svelte +35 -9
  52. package/dist/Icon/Icon.svelte +19 -3
  53. package/dist/Input/Input.svelte +24 -10
  54. package/dist/Input/properties.d.ts +37 -1
  55. package/dist/InputButton/InputButton.svelte +86 -11
  56. package/dist/InputButton/properties.d.ts +10 -1
  57. package/dist/Label/Label.svelte +54 -0
  58. package/dist/Label/Label.svelte.d.ts +3 -0
  59. package/dist/Label/properties.d.ts +26 -0
  60. package/dist/Label/properties.js +1 -0
  61. package/dist/LineChart/LineChart.svelte +385 -94
  62. package/dist/ListItem/ListItem.svelte +121 -15
  63. package/dist/Loader/Loader.svelte +22 -2
  64. package/dist/LoadingDots/LoadingDots.svelte +21 -0
  65. package/dist/MediaUpload/MediaUpload.svelte +13 -3
  66. package/dist/Menu/Menu.svelte +56 -28
  67. package/dist/Modal/Modal.svelte +77 -42
  68. package/dist/Pagination/Pagination.svelte +9 -2
  69. package/dist/Phone/Phone.svelte +4 -0
  70. package/dist/PieChart/PieChart.svelte +282 -38
  71. package/dist/PieChart/properties.d.ts +9 -0
  72. package/dist/Pill/Pill.svelte +14 -5
  73. package/dist/Progress/Progress.svelte +20 -0
  74. package/dist/Radio/Radio.svelte +58 -1
  75. package/dist/Radio/properties.d.ts +23 -0
  76. package/dist/RatingGroup/RatingGroup.svelte +283 -0
  77. package/dist/RatingGroup/RatingGroup.svelte.d.ts +4 -0
  78. package/dist/RatingGroup/properties.d.ts +73 -0
  79. package/dist/RatingGroup/properties.js +1 -0
  80. package/dist/RelativeTime/RelativeTime.svelte +1 -1
  81. package/dist/SankeyChart/SankeyChart.svelte +272 -50
  82. package/dist/SankeyChart/properties.d.ts +9 -0
  83. package/dist/Scroller/Scroller.svelte +37 -2
  84. package/dist/Select/Select.svelte +166 -46
  85. package/dist/Select/properties.d.ts +7 -0
  86. package/dist/Separator/Separator.svelte +48 -0
  87. package/dist/Separator/Separator.svelte.d.ts +3 -0
  88. package/dist/Separator/properties.d.ts +22 -0
  89. package/dist/Separator/properties.js +1 -0
  90. package/dist/Sheet/Sheet.svelte +60 -21
  91. package/dist/Shimmer/Shimmer.svelte +15 -0
  92. package/dist/Slider/Slider.svelte +87 -6
  93. package/dist/Slider/properties.d.ts +26 -0
  94. package/dist/Snippet/Snippet.svelte +4 -0
  95. package/dist/Snippet/copyState.svelte.d.ts +13 -1
  96. package/dist/Snippet/copyState.svelte.js +27 -1
  97. package/dist/Snippet/properties.d.ts +7 -0
  98. package/dist/SoundKit/SoundKit.js +9 -8
  99. package/dist/SplitInput/SplitInput.svelte +68 -2
  100. package/dist/SplitInput/properties.d.ts +23 -0
  101. package/dist/StatCard/StatCard.svelte +6 -6
  102. package/dist/Status/Status.svelte +2 -2
  103. package/dist/Stepper/Step.svelte +19 -6
  104. package/dist/Stepper/Stepper.svelte +108 -35
  105. package/dist/Table/BuiltinCell.svelte +2 -2
  106. package/dist/Table/Table.svelte +373 -55
  107. package/dist/Table/properties.d.ts +134 -3
  108. package/dist/Tabs/Tabs.svelte +263 -65
  109. package/dist/Tabs/properties.d.ts +23 -0
  110. package/dist/TaskList/TaskList.svelte +14 -5
  111. package/dist/ThemeSwitcher/ThemeSwitcher.svelte +23 -0
  112. package/dist/ThinkingIndicator/ThinkingIndicator.svelte +13 -5
  113. package/dist/Toast/Toast.svelte +27 -2
  114. package/dist/Toggle/Toggle.svelte +79 -7
  115. package/dist/Toggle/Toggle.svelte.d.ts +1 -1
  116. package/dist/Toggle/properties.d.ts +30 -7
  117. package/dist/ToolCallLog/ToolCallLog.svelte +59 -21
  118. package/dist/ToolCallLog/properties.d.ts +10 -0
  119. package/dist/Toolbar/Toolbar.svelte +1 -1
  120. package/dist/Tooltip/Tooltip.svelte +20 -2
  121. package/dist/Tooltip/tooltip-action.js +39 -4
  122. package/dist/TypewriterText/TypewriterText.svelte +34 -2
  123. package/dist/TypewriterText/properties.d.ts +18 -5
  124. package/dist/_chart/ChartContainer.svelte +12 -0
  125. package/dist/_chart/ChartTooltip.svelte +18 -4
  126. package/dist/_chart/Legend.svelte +21 -0
  127. package/dist/_chart/Legend.svelte.d.ts +15 -0
  128. package/dist/_chart/geometry.d.ts +54 -1
  129. package/dist/_chart/geometry.js +122 -14
  130. package/dist/_chart/interactions.d.ts +5 -1
  131. package/dist/_chart/interactions.js +9 -5
  132. package/dist/_chart/scales.js +16 -0
  133. package/dist/_chart/temporal.d.ts +45 -0
  134. package/dist/_chart/temporal.js +116 -0
  135. package/dist/_field/description.d.ts +50 -0
  136. package/dist/_field/description.js +51 -0
  137. package/dist/_interaction/dismissal.d.ts +67 -0
  138. package/dist/_interaction/dismissal.js +137 -0
  139. package/dist/_interaction/focus.d.ts +73 -0
  140. package/dist/_interaction/focus.js +143 -0
  141. package/dist/assets/star.svg +5 -0
  142. package/dist/index.d.ts +13 -1
  143. package/dist/index.js +21 -1
  144. package/dist/reduced-motion.svelte.d.ts +37 -0
  145. package/dist/reduced-motion.svelte.js +44 -0
  146. package/dist/styles/theme-dark.css +778 -0
  147. package/dist/types.d.ts +4 -2
  148. package/dist/utils.d.ts +62 -1
  149. package/dist/utils.js +80 -21
  150. package/dist-codemod/{bin.js → codemod/bin.js} +13 -2
  151. package/dist-codemod/{cli.js → codemod/cli.js} +48 -3
  152. package/dist-codemod/codemod/migrate-audit.js +395 -0
  153. package/dist-codemod/codemod/migration-assets/legacy-display.css +120 -0
  154. package/dist-codemod/codemod/migration-assets/legacy-palette.css +106 -0
  155. package/dist-codemod/codemod/migration-assets/wc-display.json +492 -0
  156. package/dist-codemod/migrate/analyze.js +804 -0
  157. package/dist-codemod/migrate/chart-tooltip-selector.js +301 -0
  158. package/dist-wc/index.js +13992 -4323
  159. package/package.json +13 -6
  160. package/dist/Modal/focus-trap.d.ts +0 -42
  161. package/dist/Modal/focus-trap.js +0 -57
  162. /package/dist-codemod/{legacy-pairs.js → codemod/legacy-pairs.js} +0 -0
  163. /package/dist-codemod/{transform.js → codemod/transform.js} +0 -0
  164. /package/dist-codemod/{wc-children.js → codemod/wc-children.js} +0 -0
@@ -140,19 +140,19 @@
140
140
 
141
141
  .variant-secondary {
142
142
  --_btn-color: transparent;
143
- --_btn-text-color: #3a4550;
144
- --_btn-border: 1px solid #cbd5e1;
145
- --_btn-hover-color: #f1f5f9;
146
- --_btn-hover-text-color: #3a4550;
147
- --_btn-hover-border: 1px solid #cbd5e1;
143
+ --_btn-text-color: var(--button-secondary-text-color, #3a4550);
144
+ --_btn-border: 1px solid var(--button-secondary-border-color, #cbd5e1);
145
+ --_btn-hover-color: var(--button-secondary-hover-background, #f1f5f9);
146
+ --_btn-hover-text-color: var(--button-secondary-text-color, #3a4550);
147
+ --_btn-hover-border: 1px solid var(--button-secondary-border-color, #cbd5e1);
148
148
  }
149
149
 
150
150
  .variant-ghost {
151
151
  --_btn-color: transparent;
152
- --_btn-text-color: #3a4550;
152
+ --_btn-text-color: var(--button-ghost-text-color, #3a4550);
153
153
  --_btn-border: none;
154
- --_btn-hover-color: #f1f5f9;
155
- --_btn-hover-text-color: #3a4550;
154
+ --_btn-hover-color: var(--button-ghost-hover-background, #f1f5f9);
155
+ --_btn-hover-text-color: var(--button-ghost-text-color, #3a4550);
156
156
  --_btn-hover-border: none;
157
157
  }
158
158
 
@@ -224,7 +224,10 @@
224
224
  would reset a consumer's own background-image/position/size layers
225
225
  whenever the hook is unset. Gradients go here; solids stay on --button-color. */
226
226
  background-image: var(--button-background, none);
227
- color: var(--button-text-color, var(--_btn-text-color, white));
227
+ color: var(
228
+ --button-text-color,
229
+ var(--_btn-text-color, var(--button-neutral-text-color, white))
230
+ );
228
231
  height: var(--button-height, fit-content);
229
232
  padding: var(--button-padding, var(--_btn-padding, 16px));
230
233
  margin: var(--button-margin);
@@ -256,8 +259,8 @@
256
259
  cursor: var(--disabled-cursor, not-allowed);
257
260
  opacity: var(--disabled-opacity, 0.4);
258
261
  color: var(--disabled-text-color, var(--button-text-color, var(--_btn-text-color, white)));
259
- font-size: var(--disabled-font-size);
260
- font-weight: var(--disabled-font-weight);
262
+ font-size: var(--disabled-font-size, inherit);
263
+ font-weight: var(--disabled-font-weight, inherit);
261
264
  text-decoration: var(--button-disabled-text-decoration, var(--button-text-decoration, none));
262
265
  /* Preserve the variant border when disabled so secondary (bordered) stays distinct from ghost. */
263
266
  border: var(--disabled-border, var(--button-border, var(--_btn-border, none)));
@@ -342,4 +345,46 @@
342
345
  width: 100%;
343
346
  }
344
347
  }
348
+
349
+ /* This bar is not indefinite like a spinner -- its width IS the meaning:
350
+ "how much of `--button-progress-loader-duration` has elapsed", and
351
+ `showProgressBar` keeps the button disabled for that whole span. A bare
352
+ `animation: none` would freeze it at the base rule above (`width: 100%`),
353
+ showing a FULL, "finished" bar for up to 8s while the button is actually
354
+ still counting down and still disabled -- a frame that means something
355
+ else is worse than the motion it removed. Freezing at 0% instead would
356
+ lie the other way once the real duration elapses and the bar should read
357
+ complete. Neither static width is honest for the whole span, so motion is
358
+ thinned rather than removed: `steps()` swaps the continuous ease for
359
+ discrete jumps that hold the PRIOR interval's value until each jump (the
360
+ `jump-end` default), so the displayed width can only ever under-report
361
+ elapsed time -- never claim to be further along than it really is -- while
362
+ still losing the smooth per-frame slide prefers-reduced-motion targets.
363
+ The step count is a LITERAL, deliberately not a token. `steps()` is invalid
364
+ for a count below 1, and an invalid value does not fall back to a safer
365
+ stepped default -- the whole declaration is dropped and the property takes
366
+ the base rule's `ease`, restoring exactly the continuous slide this block
367
+ exists to remove. A consumer passing `0` to "just freeze it" would silently
368
+ switch the guard off for every user who asked for reduced motion. A guard
369
+ that fails OPEN, quietly, is worse than no guard, and one tunable is not
370
+ worth that.
371
+ This block lives in the component's own <style> because that is the only
372
+ stylesheet that reaches inside the shadow root a custom-element consumer
373
+ gets. */
374
+ @media (prefers-reduced-motion: reduce) {
375
+ .button-progress-bar {
376
+ animation-timing-function: steps(5, jump-end);
377
+ }
378
+
379
+ /* The loader is not the only motion here. `--button-transition` defaults to
380
+ `none`, so this rule does nothing until a consumer opts in -- and the
381
+ documented way to opt in is exactly a hover/press animation, which is
382
+ what a user asking for reduced motion is asking not to have. The sweep
383
+ that covered every transition in ThemeSwitcher stopped at the one
384
+ animation the brief named in this file. */
385
+ .button-el,
386
+ .button-container {
387
+ transition: none;
388
+ }
389
+ }
345
390
  </style>
@@ -148,6 +148,21 @@
148
148
  return days;
149
149
  });
150
150
 
151
+ // A calendar grid is one tab stop, the same roving-tabindex pattern Tabs uses: the
152
+ // day being actively navigated while inside the grid, else the selected day, else
153
+ // today if it's in view and enabled, else the first enabled day -- so a month with
154
+ // no selection, or whose selection has scrolled out of view, is still reachable by
155
+ // Tab. A fully disabled month in view resolves to null, and the grid container
156
+ // becomes the fallback tab stop so the calendar can still be tabbed to.
157
+ const tabStopDay: number | null = $derived(
158
+ focusedDay ??
159
+ calendarDays.find((d) => d.isCurrentMonth && (d.isSelected || d.isRangeStart || d.isRangeEnd))
160
+ ?.day ??
161
+ calendarDays.find((d) => d.isCurrentMonth && d.isToday && !d.isDisabled)?.day ??
162
+ calendarDays.find((d) => d.isCurrentMonth && !d.isDisabled)?.day ??
163
+ null
164
+ );
165
+
151
166
  function normalizeDate(d: Date): Date {
152
167
  const n = new SvelteDate(d);
153
168
  n.setHours(0, 0, 0, 0);
@@ -222,79 +237,79 @@
222
237
  }
223
238
  }
224
239
 
225
- function daysInMonth(year: number, month: number): number {
226
- return new SvelteDate(year, month + 1, 0).getDate();
240
+ function dateAtDay(year: number, month: number, day: number): Date {
241
+ const date = new SvelteDate(year, month, day);
242
+ date.setHours(0, 0, 0, 0);
243
+ return date;
244
+ }
245
+
246
+ // disabledDates can be a predicate that disables every day it's asked about, so the
247
+ // walk is capped rather than open-ended -- ~10 years of daily steps is far past any
248
+ // realistic disabled range but still guarantees the keyboard handler terminates
249
+ // instead of hanging when a whole month (or more) is disabled.
250
+ const MAX_DAY_SKIP = 3660;
251
+
252
+ function findEnabledDate(from: Date, stepDays: number): Date | null {
253
+ const candidate = new SvelteDate(from);
254
+ for (let i = 0; i < MAX_DAY_SKIP; i++) {
255
+ candidate.setDate(candidate.getDate() + stepDays);
256
+ candidate.setHours(0, 0, 0, 0);
257
+ if (!isDateDisabled(candidate)) {
258
+ return candidate;
259
+ }
260
+ }
261
+ return null;
262
+ }
263
+
264
+ // Moves the roving tab stop to `date`, crossing into a different displayed month
265
+ // (by any number of months, since findEnabledDate can skip past several) when the
266
+ // target lands outside the currently visible one.
267
+ function moveFocusTo(date: Date): void {
268
+ const changesMonth =
269
+ date.getFullYear() !== displayDate.getFullYear() ||
270
+ date.getMonth() !== displayDate.getMonth();
271
+ if (changesMonth) {
272
+ displayDate.setFullYear(date.getFullYear(), date.getMonth(), 1);
273
+ onmonthchange?.({ year: displayDate.getFullYear(), month: displayDate.getMonth() });
274
+ }
275
+ focusedDay = date.getDate();
276
+ focusDayCell();
227
277
  }
228
278
 
229
279
  function handleKeyDown(event: KeyboardEvent): void {
230
280
  const year = displayDate.getFullYear();
231
281
  const month = displayDate.getMonth();
232
- const totalDays = daysInMonth(year, month);
233
-
234
- if (focusedDay === null) {
235
- if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
236
- event.preventDefault();
237
- focusedDay = 1;
238
- focusDayCell();
239
- return;
240
- }
241
- }
242
282
 
243
283
  switch (event.key) {
244
284
  case 'ArrowRight':
245
- event.preventDefault();
246
- if (focusedDay !== null && focusedDay < totalDays) {
247
- focusedDay++;
248
- } else if (focusedDay === totalDays) {
249
- navigateMonth(1);
250
- focusedDay = 1;
251
- }
252
- focusDayCell();
253
- break;
254
285
  case 'ArrowLeft':
255
- event.preventDefault();
256
- if (focusedDay !== null && focusedDay > 1) {
257
- focusedDay--;
258
- } else if (focusedDay === 1) {
259
- navigateMonth(-1);
260
- focusedDay = daysInMonth(displayDate.getFullYear(), displayDate.getMonth());
261
- }
262
- focusDayCell();
263
- break;
264
286
  case 'ArrowDown':
287
+ case 'ArrowUp': {
265
288
  event.preventDefault();
266
- if (focusedDay !== null) {
267
- if (focusedDay + 7 <= totalDays) {
268
- focusedDay += 7;
269
- } else {
270
- const overflow = focusedDay + 7 - totalDays;
271
- navigateMonth(1);
272
- focusedDay = overflow;
273
- }
289
+ // Nothing is focusable in the grid (a fully disabled month with no
290
+ // selection or today in view) -- there's nowhere to move from.
291
+ if (tabStopDay === null) {
292
+ break;
274
293
  }
275
- focusDayCell();
276
- break;
277
- case 'ArrowUp':
278
- event.preventDefault();
279
- if (focusedDay !== null) {
280
- if (focusedDay - 7 >= 1) {
281
- focusedDay -= 7;
282
- } else {
283
- const currentDay = focusedDay;
284
- const prevTotal = daysInMonth(year, month - 1);
285
- navigateMonth(-1);
286
- focusedDay = prevTotal + (currentDay - 7);
287
- }
294
+ const stepDays =
295
+ event.key === 'ArrowRight'
296
+ ? 1
297
+ : event.key === 'ArrowLeft'
298
+ ? -1
299
+ : event.key === 'ArrowDown'
300
+ ? 7
301
+ : -7;
302
+ const target = findEnabledDate(dateAtDay(year, month, tabStopDay), stepDays);
303
+ if (target !== null) {
304
+ moveFocusTo(target);
288
305
  }
289
- focusDayCell();
290
306
  break;
307
+ }
291
308
  case 'Enter':
292
309
  case ' ':
293
310
  event.preventDefault();
294
- if (focusedDay !== null) {
295
- const date = new SvelteDate(year, month, focusedDay);
296
- date.setHours(0, 0, 0, 0);
297
- selectDate(date);
311
+ if (tabStopDay !== null) {
312
+ selectDate(dateAtDay(year, month, tabStopDay));
298
313
  }
299
314
  break;
300
315
  }
@@ -338,7 +353,13 @@
338
353
  {/each}
339
354
  </div>
340
355
 
341
- <div class="grid" bind:this={gridRef} tabindex="0" role="grid" onkeydown={handleKeyDown}>
356
+ <div
357
+ class="grid"
358
+ bind:this={gridRef}
359
+ tabindex={tabStopDay === null ? 0 : -1}
360
+ role="grid"
361
+ onkeydown={handleKeyDown}
362
+ >
342
363
  {#each calendarDays as dayInfo (dayInfo.date.getTime())}
343
364
  {#if dayInfo.isCurrentMonth}
344
365
  <button
@@ -352,7 +373,7 @@
352
373
  class:disabled={dayInfo.isDisabled}
353
374
  data-day={dayInfo.day}
354
375
  disabled={dayInfo.isDisabled}
355
- tabindex={focusedDay === dayInfo.day ? 0 : -1}
376
+ tabindex={tabStopDay === dayInfo.day ? 0 : -1}
356
377
  aria-label={dayInfo.date.toLocaleDateString(locale)}
357
378
  onclick={() => selectDate(dayInfo.date)}
358
379
  >
@@ -422,7 +443,7 @@
422
443
  .day-name {
423
444
  font-size: var(--calendar-day-name-font-size, 12px);
424
445
  font-weight: var(--calendar-day-name-font-weight, 600);
425
- color: var(--calendar-day-name-color, #999999);
446
+ color: var(--calendar-day-name-color, #595959);
426
447
  padding: var(--calendar-day-name-padding, 4px 0);
427
448
  }
428
449
 
@@ -490,13 +511,19 @@
490
511
  border-radius: 0;
491
512
  }
492
513
 
514
+ /* #cccccc (1.61:1 on white) read as blank space, not as a disabled digit --
515
+ disabled controls are contrast-exempt under WCAG 1.4.3, but the demo's own
516
+ disabledDates run (10th-15th) is information the user must be able to read,
517
+ not just infer. #949494 lands near the 3:1 floor WCAG 1.4.11 uses elsewhere
518
+ for "must stay perceivable" content, while staying well short of the 4.5:1
519
+ enabled-text weight so disabled still reads as visually recessive. */
493
520
  .disabled {
494
- color: var(--calendar-disabled-color, #cccccc);
521
+ color: var(--calendar-disabled-color, #949494);
495
522
  cursor: var(--calendar-disabled-cursor, not-allowed);
496
523
  }
497
524
 
498
525
  .outside-month {
499
- color: var(--calendar-outside-month-color, #cccccc);
526
+ color: var(--calendar-outside-month-color, #6b7280);
500
527
  cursor: default;
501
528
  }
502
529
  </style>
@@ -231,7 +231,7 @@
231
231
  .card-description {
232
232
  font-size: var(--card-description-font-size, 14px);
233
233
  color: var(--card-description-color, inherit);
234
- opacity: var(--card-description-opacity, 0.6);
234
+ opacity: var(--card-description-opacity, 0.8);
235
235
  margin-top: var(--card-description-margin-top, 4px);
236
236
  }
237
237
 
@@ -230,7 +230,8 @@
230
230
  .slidesDiv {
231
231
  display: flex;
232
232
  transform: translateX(0);
233
- transition: transform 0.5s ease-in-out;
233
+ transition: transform var(--carousel-track-transition-duration, var(--motion-duration, 0.5s))
234
+ var(--carousel-track-transition-easing, var(--motion-easing, ease-in-out));
234
235
  }
235
236
  .dots-wrapper {
236
237
  gap: var(--dot-gap, 10px);
@@ -244,7 +245,8 @@
244
245
  border-radius: 50%;
245
246
  background: var(--carousel-dot-color, #c4c4c4);
246
247
  cursor: pointer;
247
- transition: 0.3s ease;
248
+ transition: all var(--carousel-dot-transition-duration, var(--motion-duration, 0.3s))
249
+ var(--carousel-dot-transition-easing, var(--motion-easing, ease));
248
250
  }
249
251
 
250
252
  .active-dot {
@@ -253,7 +255,8 @@
253
255
  border-radius: 50%;
254
256
  cursor: pointer;
255
257
  background: var(--carousel-dot-active-color, #000000);
256
- transition: 0.3s ease;
258
+ transition: all var(--carousel-dot-transition-duration, var(--motion-duration, 0.3s))
259
+ var(--carousel-dot-transition-easing, var(--motion-easing, ease));
257
260
  }
258
261
 
259
262
  .dot:focus-visible,
@@ -10,6 +10,7 @@
10
10
  import type { ChatComposerProperties } from './properties';
11
11
  import { resolveControlDisabled } from './controlDisabled';
12
12
  import { shouldApplyClear } from './submitResult';
13
+ import { normalizeDictationState } from './dictationState';
13
14
 
14
15
  let {
15
16
  value = $bindable(''),
@@ -51,12 +52,15 @@
51
52
  actionIcon,
52
53
  actionLabel = 'Voice conversation',
53
54
  leading,
55
+ statusText,
56
+ statusTestId,
54
57
  onsubmit,
55
58
  oninput,
56
59
  onkeydown,
57
60
  onpaste,
58
61
  onstop,
59
62
  onvoice,
63
+ oncanceldictation,
60
64
  onattach,
61
65
  onattachclick,
62
66
  onaction,
@@ -84,7 +88,14 @@
84
88
  // unset, so a caller who never passes the new props keeps the original
85
89
  // single-`disabled`-gates-everything behaviour exactly.
86
90
  let resolvedTextDisabled = $derived(resolveControlDisabled(textDisabled, disabled));
87
- let resolvedVoiceDisabled = $derived(resolveControlDisabled(voiceDisabled, disabled));
91
+ // 'busy' (transcribing) is not cosmetic -- the control must not accept
92
+ // a new press, so busy forces the voice button off regardless of what the
93
+ // caller passed for `voiceDisabled`/`disabled`, the same way `disabled`
94
+ // itself is not something a specific flag can override back on.
95
+ let dictationState = $derived(normalizeDictationState(recording));
96
+ let resolvedVoiceDisabled = $derived(
97
+ resolveControlDisabled(voiceDisabled, disabled) || dictationState === 'busy'
98
+ );
88
99
  let resolvedSendDisabled = $derived(resolveControlDisabled(sendDisabled, disabled));
89
100
 
90
101
  let canSend = $derived(
@@ -131,6 +142,13 @@
131
142
  event.preventDefault();
132
143
  submit();
133
144
  }
145
+ // Escape cancels dictation when recording, and only then -- not
146
+ // while idle (nothing to cancel) and not while busy (transcribing is
147
+ // already past the point Escape can interrupt).
148
+ if (event.key === 'Escape' && dictationState === 'recording') {
149
+ event.preventDefault();
150
+ oncanceldictation?.();
151
+ }
134
152
  onkeydown?.(event);
135
153
  }
136
154
 
@@ -167,6 +185,13 @@
167
185
  data-pw={typeof testId === 'string' ? testId : null}
168
186
  testID={typeof testId === 'string' ? testId : null}
169
187
  >
188
+ {#if typeof statusText === 'string'}
189
+ <!-- Generic region, role and politeness only -- the sentence is
190
+ whatever the caller passed in `statusText`, never hardcoded here. -->
191
+ <div class="sr-only" role="status" aria-live="polite" data-pw={statusTestId ?? null}>
192
+ {statusText}
193
+ </div>
194
+ {/if}
170
195
  {#if typeof attachmentsPreview === 'function'}
171
196
  {@render attachmentsPreview()}
172
197
  {:else if hasRichAttachments}
@@ -250,7 +275,11 @@
250
275
  ></textarea>
251
276
 
252
277
  {#if showVoice}
253
- <div class="control voice" class:recording>
278
+ <div
279
+ class="control voice"
280
+ class:recording={dictationState === 'recording'}
281
+ class:busy={dictationState === 'busy'}
282
+ >
254
283
  <Button
255
284
  onclick={() => onvoice?.()}
256
285
  disabled={resolvedVoiceDisabled}
@@ -278,7 +307,7 @@
278
307
  {/if}
279
308
  </Button>
280
309
  </div>
281
- {:else if typeof onaction === 'function' && !canSend && !recording}
310
+ {:else if typeof onaction === 'function' && !canSend && dictationState === 'idle'}
282
311
  <div class="control send action" data-pw={sendSlotTestId ?? null}>
283
312
  <Button
284
313
  onclick={() => onaction()}
@@ -368,7 +397,7 @@
368
397
  }
369
398
 
370
399
  .input::placeholder {
371
- color: var(--chat-composer-placeholder-color, #a1a1aa);
400
+ color: var(--chat-composer-placeholder-color, #52525b);
372
401
  }
373
402
 
374
403
  .control {
@@ -398,6 +427,13 @@
398
427
  --button-text-color: var(--chat-composer-voice-recording-color, #dc2626);
399
428
  }
400
429
 
430
+ /* No dedicated busy palette: the disabled `Button` already dims itself
431
+ (`--disabled-opacity`), which is enough to distinguish "busy" from the
432
+ active "recording" red without inventing new color tokens that would
433
+ need a matching dark-theme entry outside this component's scope. The
434
+ `.busy` class is still applied to the wrapper (see markup) so a caller
435
+ can target it with their own CSS if they want a distinct look. */
436
+
401
437
  .send {
402
438
  --button-width: var(--chat-composer-send-size, 40px);
403
439
  --button-height: var(--chat-composer-send-size, 40px);
@@ -413,4 +449,18 @@
413
449
  --button-color: var(--chat-composer-stop-background-color, #18181b);
414
450
  --button-text-color: var(--chat-composer-stop-color, #ffffff);
415
451
  }
452
+
453
+ /* Visually hidden but still read by assistive technology -- same pattern
454
+ Table's caption and Label's required marker use. */
455
+ .sr-only {
456
+ position: absolute;
457
+ width: 1px;
458
+ height: 1px;
459
+ padding: 0;
460
+ overflow: hidden;
461
+ clip: rect(0, 0, 0, 0);
462
+ clip-path: inset(50%);
463
+ white-space: nowrap;
464
+ border-width: 0;
465
+ }
416
466
  </style>
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Loom's composer models dictation as a tri-state
3
+ * `'idle' | 'recording' | 'busy'` against this library's boolean `recording`.
4
+ * The third state is not cosmetic -- while transcribing, the control must not
5
+ * accept a new press, so "recording" and "busy" have to be states a consumer
6
+ * can actually branch on, not one flag with two meanings.
7
+ *
8
+ * `recording` widens to accept the tri-state directly. The boolean is kept
9
+ * working, unchanged, as an alias for one minor cycle: `true` reads as
10
+ * 'recording' and `false` (the default) reads as 'idle' -- the only two
11
+ * states a boolean caller could ever express -- so a caller who never touches
12
+ * this keeps exactly today's behaviour.
13
+ */
14
+ export type ChatComposerDictationState = 'idle' | 'recording' | 'busy';
15
+ export declare function normalizeDictationState(recording: boolean | ChatComposerDictationState): ChatComposerDictationState;
@@ -0,0 +1,9 @@
1
+ export function normalizeDictationState(recording) {
2
+ if (recording === true) {
3
+ return 'recording';
4
+ }
5
+ if (recording === false) {
6
+ return 'idle';
7
+ }
8
+ return recording;
9
+ }
@@ -24,7 +24,16 @@ export type OptionalChatComposerProperties = {
24
24
  submitOnEnter?: boolean;
25
25
  maxLength?: number;
26
26
  streaming?: boolean;
27
- recording?: boolean;
27
+ /**
28
+ * Voice/dictation state. Accepts the tri-state `'idle' | 'recording' |
29
+ * 'busy'` -- 'busy' means the control is transcribing: it must not accept a
30
+ * new press, so the voice button disables itself while busy regardless of
31
+ * `voiceDisabled`/`disabled`.
32
+ *
33
+ * `boolean` is kept working, unchanged, as an alias for one minor cycle:
34
+ * `true` reads as 'recording', `false` (the default) reads as 'idle'.
35
+ */
36
+ recording?: boolean | 'idle' | 'recording' | 'busy';
28
37
  attachments?: File[];
29
38
  /**
30
39
  * Replaces the built-in attachment pill strip above the input row — render a
@@ -72,6 +81,17 @@ export type OptionalChatComposerProperties = {
72
81
  actionIcon?: Snippet;
73
82
  actionLabel?: string;
74
83
  leading?: Snippet;
84
+ /**
85
+ * Opt-in sr-only status region (`role="status" aria-live="polite"`) for
86
+ * narrating composer state changes -- e.g. "Recording. Press Escape to stop
87
+ * and transcribe." Rendered only when set (an empty string still renders an
88
+ * emptied region, so a caller can clear it without unmounting). The library
89
+ * owns only the region, its role and its politeness; the sentences are
90
+ * product voice and must come from the caller -- never hardcoded here.
91
+ */
92
+ statusText?: string;
93
+ /** Test id on the status region (see `statusText`). */
94
+ statusTestId?: string;
75
95
  testId?: string;
76
96
  /** Test id emitted as `data-pw` on the textarea itself (none by default). */
77
97
  inputTestId?: string;
@@ -100,6 +120,13 @@ export type ChatComposerEventProperties = {
100
120
  onpaste?: (event: ClipboardEvent) => void;
101
121
  onstop?: () => void;
102
122
  onvoice?: () => void;
123
+ /**
124
+ * Fires when Escape is pressed while `recording` is 'recording' (or the
125
+ * legacy `true`), and only then -- Escape has no effect at any other
126
+ * dictation state. The library owns no microphone or media APIs, so this is
127
+ * the seam a caller wires to actually stop dictation.
128
+ */
129
+ oncanceldictation?: () => void;
103
130
  onattach?: (files: File[]) => void;
104
131
  /**
105
132
  * Intercepts the attach button: when provided, clicking it calls this instead of
@@ -455,6 +455,23 @@
455
455
  --button-hover-color: var(--chat-message-action-hover-background-color, #f4f4f5);
456
456
  }
457
457
 
458
+ /* The sender bubble is a dark fill in light mode, so the neutral action grey
459
+ above lands at 1.81:1 on it. Actions inside that bubble take their own ink. */
460
+ .chat-message.party-sender .actions {
461
+ --button-text-color: var(--chat-message-sender-action-color, #d4d4d8);
462
+ }
463
+
464
+ /* The sender bubble is a dark fill in light mode and a light one in dark, so
465
+ anything rendered inside it -- links and inline buttons included -- has to
466
+ invert with it rather than take one fixed colour. */
467
+ .chat-message.party-sender .bubble :global(a) {
468
+ color: var(--chat-message-sender-link-color, light-dark(#a5b4fc, #4338ca));
469
+ }
470
+
471
+ .chat-message.party-sender .bubble :global(.button-text) {
472
+ color: var(--chat-message-sender-action-color, light-dark(#d4d4d8, #27272a));
473
+ }
474
+
458
475
  .chat-message:hover .actions,
459
476
  .chat-message:focus-within .actions {
460
477
  opacity: 1;
@@ -499,7 +516,7 @@
499
516
  }
500
517
 
501
518
  .body :global(a) {
502
- color: var(--chat-message-link-color, #6d28d9);
519
+ color: var(--chat-message-link-color, light-dark(#6d28d9, #a5b4fc));
503
520
  text-decoration: underline;
504
521
  text-underline-offset: 2px;
505
522
  }
@@ -638,4 +655,13 @@
638
655
  border-top: 1px solid var(--chat-message-hr-color, rgba(0, 0, 0, 0.12));
639
656
  margin: 0.8em 0;
640
657
  }
658
+ /* The actions row is revealed on hover by fading opacity 0 -> 1. Under a
659
+ reduced-motion preference it appears at once instead: the row is a set of
660
+ controls the user is reaching for, so arriving instantly is the better
661
+ outcome, not a degraded one. */
662
+ @media (prefers-reduced-motion: reduce) {
663
+ .actions {
664
+ transition: none;
665
+ }
666
+ }
641
667
  </style>
@@ -58,11 +58,11 @@
58
58
 
59
59
  .text {
60
60
  font-size: var(--check-list-item-text-size, 14px);
61
- color: var(--check-list-item-text-color);
61
+ color: var(--check-list-item-text-color, inherit);
62
62
  }
63
63
 
64
64
  .text.checked {
65
- color: var(--check-list-item-checked-text-color);
66
- font-weight: var(--check-list-item-checked-font-weight);
65
+ color: var(--check-list-item-checked-text-color, inherit);
66
+ font-weight: var(--check-list-item-checked-font-weight, inherit);
67
67
  }
68
68
  </style>