@juspay/svelte-ui-components 2.134.1 → 2.136.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 (38) hide show
  1. package/dist/Chat/Chat.svelte +38 -2
  2. package/dist/Chat/properties.d.ts +7 -0
  3. package/dist/ChatMessageList/ChatMessageList.svelte +54 -1
  4. package/dist/CheckListItem/CheckListItem.svelte +4 -1
  5. package/dist/ChipInput/ChipInput.svelte +150 -10
  6. package/dist/ChipInput/properties.d.ts +18 -1
  7. package/dist/CommandMenu/CommandMenu.svelte +7 -3
  8. package/dist/EmptyState/EmptyState.svelte +17 -3
  9. package/dist/Input/Input.svelte +1 -0
  10. package/dist/ListItem/ListItem.svelte +14 -12
  11. package/dist/ListItem/properties.d.ts +7 -0
  12. package/dist/Menu/Menu.svelte +2 -1
  13. package/dist/Menu/properties.d.ts +2 -0
  14. package/dist/Modal/Modal.svelte +3 -3
  15. package/dist/Pill/Pill.svelte +2 -0
  16. package/dist/Pill/properties.d.ts +1 -0
  17. package/dist/Sheet/Sheet.svelte +3 -23
  18. package/dist/StatCard/StatCard.svelte +64 -1
  19. package/dist/StatCard/properties.d.ts +44 -2
  20. package/dist/Status/Status.svelte +2 -1
  21. package/dist/Status/properties.d.ts +10 -0
  22. package/dist/Stepper/Step.svelte +95 -2
  23. package/dist/Stepper/Stepper.svelte +52 -7
  24. package/dist/Stepper/properties.d.ts +34 -1
  25. package/dist/Table/BuiltinCell.svelte +46 -23
  26. package/dist/Table/Table.svelte +63 -40
  27. package/dist/Table/cellData.js +3 -0
  28. package/dist/Table/properties.d.ts +70 -0
  29. package/dist/ThinkingIndicator/ThinkingIndicator.svelte +93 -5
  30. package/dist/ThinkingIndicator/properties.d.ts +7 -3
  31. package/dist/TypewriterText/TypewriterText.svelte +67 -2
  32. package/dist/TypewriterText/properties.d.ts +97 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +1 -1
  35. package/dist/utils.d.ts +2 -0
  36. package/dist/utils.js +30 -0
  37. package/dist-wc/index.js +2937 -2658
  38. package/package.json +1 -1
@@ -58,6 +58,32 @@
58
58
  : ''
59
59
  );
60
60
 
61
+ const defaultTestIdSuffixes = {
62
+ icon: 'icon',
63
+ thumbnail: 'thumb',
64
+ thumbnailPlaceholder: 'thumb-placeholder',
65
+ tag: 'tag',
66
+ trendUp: 'trend-up',
67
+ trendDown: 'trend-down',
68
+ menu: 'menu',
69
+ menuTrigger: 'menu-trigger',
70
+ popup: 'popup',
71
+ popupTrigger: 'popup-trigger',
72
+ link: 'link',
73
+ copy: 'copy',
74
+ linkCopied: 'link-copied'
75
+ } as const;
76
+
77
+ const generatedTestId = (suffixName: keyof typeof defaultTestIdSuffixes, index?: number) => {
78
+ if (!column.testId) {
79
+ return;
80
+ }
81
+ const suffix = column.testIdSuffixes?.[suffixName] ?? defaultTestIdSuffixes[suffixName];
82
+ return typeof index === 'number'
83
+ ? `${column.testId}-${suffix}-${index}`
84
+ : `${column.testId}-${suffix}`;
85
+ };
86
+
61
87
  let copied = $state(false);
62
88
  let copyResetTimer: ReturnType<typeof setTimeout> | null = null;
63
89
 
@@ -143,8 +169,8 @@
143
169
  {#each icons as iconSrc, iconIndex (`${iconIndex}-${iconSrc}`)}
144
170
  <span
145
171
  class="builtin-icon-label-icon"
146
- data-pw={column.testId ? `${column.testId}-icon-${iconIndex}` : null}
147
- testID={column.testId ? `${column.testId}-icon-${iconIndex}` : null}
172
+ data-pw={generatedTestId('icon', iconIndex)}
173
+ testID={generatedTestId('icon', iconIndex)}
148
174
  >
149
175
  <Img inlineSvg src={String(iconSrc)} alt="" fallback="" />
150
176
  </span>
@@ -157,8 +183,8 @@
157
183
  {#if typeof data.imageUrl === 'string' && data.imageUrl}
158
184
  <span
159
185
  class="builtin-thumb"
160
- data-pw={column.testId ? `${column.testId}-thumb` : null}
161
- testID={column.testId ? `${column.testId}-thumb` : null}
186
+ data-pw={generatedTestId('thumbnail')}
187
+ testID={generatedTestId('thumbnail')}
162
188
  >
163
189
  <Img
164
190
  src={data.imageUrl}
@@ -169,8 +195,8 @@
169
195
  {:else}
170
196
  <span
171
197
  class="builtin-thumb builtin-thumb-placeholder"
172
- data-pw={column.testId ? `${column.testId}-thumb-placeholder` : null}
173
- testID={column.testId ? `${column.testId}-thumb-placeholder` : null}
198
+ data-pw={generatedTestId('thumbnailPlaceholder')}
199
+ testID={generatedTestId('thumbnailPlaceholder')}
174
200
  >{typeof data.text1 === 'string' && data.text1
175
201
  ? data.text1.charAt(0).toUpperCase()
176
202
  : ''}</span
@@ -190,7 +216,7 @@
190
216
  <Pill
191
217
  text={tag.text}
192
218
  classes={tag.classes ?? ''}
193
- testId={tag.testId ?? (column.testId && `${column.testId}-tag-${tagIndex}`)}
219
+ testId={tag.testId ?? generatedTestId('tag', tagIndex)}
194
220
  />
195
221
  {/each}
196
222
  </div>
@@ -227,8 +253,8 @@
227
253
  {#if compare.trendPercent > 0}
228
254
  <span
229
255
  class="builtin-trend builtin-trend-up"
230
- data-pw={column.testId ? `${column.testId}-trend-up` : null}
231
- testID={column.testId ? `${column.testId}-trend-up` : null}
256
+ data-pw={generatedTestId('trendUp')}
257
+ testID={generatedTestId('trendUp')}
232
258
  >
233
259
  <!-- eslint-disable svelte/no-at-html-tags -->
234
260
  <span class="builtin-trend-icon">{@html trendUpSvg}</span>
@@ -237,8 +263,8 @@
237
263
  {:else if compare.trendPercent < 0}
238
264
  <span
239
265
  class="builtin-trend builtin-trend-down"
240
- data-pw={column.testId ? `${column.testId}-trend-down` : null}
241
- testID={column.testId ? `${column.testId}-trend-down` : null}
266
+ data-pw={generatedTestId('trendDown')}
267
+ testID={generatedTestId('trendDown')}
242
268
  >
243
269
  <!-- eslint-disable svelte/no-at-html-tags -->
244
270
  <span class="builtin-trend-icon">{@html trendDownSvg}</span>
@@ -419,16 +445,13 @@
419
445
  danger: item.danger,
420
446
  separator: item.separator
421
447
  }))}
422
- testId={column.testId && `${column.testId}-menu-${rowIndex}`}
448
+ testId={generatedTestId('menu', rowIndex)}
423
449
  {usePortal}
424
450
  onselect={(menuItem) => column.onMenuAction?.(rowIndex, menuItem.value, originalIndex)}
425
451
  >
426
452
  {#snippet trigger()}
427
453
  <span class="builtin-icon-button">
428
- <Button
429
- ariaLabel="More actions"
430
- testId={column.testId && `${column.testId}-menu-trigger-${rowIndex}`}
431
- >
454
+ <Button ariaLabel="More actions" testId={generatedTestId('menuTrigger', rowIndex)}>
432
455
  {#snippet icon()}
433
456
  <!-- eslint-disable svelte/no-at-html-tags -->
434
457
  <span class="builtin-menu-dots">{@html dotsSvg}</span>
@@ -458,7 +481,7 @@
458
481
  danger: item.danger,
459
482
  separator: item.separator
460
483
  }))}
461
- testId={column.testId && `${column.testId}-popup-${rowIndex}`}
484
+ testId={generatedTestId('popup', rowIndex)}
462
485
  {usePortal}
463
486
  onselect={(menuItem) => column.onMenuAction?.(rowIndex, menuItem.value, originalIndex)}
464
487
  >
@@ -466,7 +489,7 @@
466
489
  <span class="builtin-icon-button">
467
490
  <Button
468
491
  ariaLabel={popupData.ariaLabel ?? 'More actions'}
469
- testId={column.testId && `${column.testId}-popup-trigger-${rowIndex}`}
492
+ testId={generatedTestId('popupTrigger', rowIndex)}
470
493
  >
471
494
  {#snippet icon()}
472
495
  <!-- eslint-disable svelte/no-at-html-tags -->
@@ -490,8 +513,8 @@
490
513
  target="_blank"
491
514
  rel="noopener noreferrer"
492
515
  class="builtin-link-anchor"
493
- data-pw={column.testId ? `${column.testId}-link-${rowIndex}` : null}
494
- testID={column.testId ? `${column.testId}-link-${rowIndex}` : null}
516
+ data-pw={generatedTestId('link', rowIndex)}
517
+ testID={generatedTestId('link', rowIndex)}
495
518
  >
496
519
  {link.label ?? link.url}
497
520
  </a>
@@ -499,7 +522,7 @@
499
522
  <span class="builtin-link-copy">
500
523
  <Button
501
524
  ariaLabel={copied ? 'Link copied' : 'Copy link'}
502
- testId={column.testId && `${column.testId}-copy-${rowIndex}`}
525
+ testId={generatedTestId('copy', rowIndex)}
503
526
  onclick={() => handleCopy(link.url)}
504
527
  >
505
528
  {#snippet icon()}
@@ -511,8 +534,8 @@
511
534
  {#if copied}
512
535
  <span
513
536
  class="builtin-link-copied"
514
- data-pw={column.testId ? `${column.testId}-link-copied` : null}
515
- testID={column.testId ? `${column.testId}-link-copied` : null}>Copied</span
537
+ data-pw={generatedTestId('linkCopied')}
538
+ testID={generatedTestId('linkCopied')}>Copied</span
516
539
  >
517
540
  {/if}
518
541
  {/if}
@@ -375,6 +375,14 @@
375
375
  return total > 0 ? `${from}-${to} of ${total}` : '';
376
376
  });
377
377
  let pageSizeOptions = $derived(pagination?.pageSizeOptions ?? [10, 25, 50, 100]);
378
+ // `hideControls` remains a shorthand for hiding both at once — existing
379
+ // consumers of it are unaffected. `hidePageSizeSelector`/`hideSteppers` let
380
+ // a consumer suppress either independently, e.g. a working external
381
+ // paginator that only wants Table's range text plus its own steppers.
382
+ let hidePageSizeSelector = $derived(
383
+ (pagination?.hideControls || pagination?.hidePageSizeSelector) ?? false
384
+ );
385
+ let hideSteppers = $derived((pagination?.hideControls || pagination?.hideSteppers) ?? false);
378
386
 
379
387
  const handlePageChange = (page: number): void => {
380
388
  pageOverride = page;
@@ -722,6 +730,7 @@
722
730
  data-pw={headerColumn?.testId ?? null}
723
731
  testID={headerColumn?.testId ?? null}
724
732
  style:text-align={headerColumn?.align ?? null}
733
+ style:width={headerColumn?.width ?? null}
725
734
  style:max-width={headerColumn?.maxWidth ?? null}
726
735
  >
727
736
  <span
@@ -965,6 +974,7 @@
965
974
  ? getCellTestId(row, cellValue, rowIndex)
966
975
  : null}
967
976
  style:text-align={keyedColumn?.align ?? null}
977
+ style:width={keyedColumn?.width ?? null}
968
978
  style:max-width={keyedColumn?.maxWidth ?? null}
969
979
  title={keyedColumn?.maxWidth && isScalarCell ? String(cellValue) : null}
970
980
  >
@@ -1007,10 +1017,15 @@
1007
1017
  <div class="table-footer">
1008
1018
  {@render paginatorSlot()}
1009
1019
  </div>
1010
- {:else if pagination && (paginationTotalPages > 1 || pagination.hasMore)}
1020
+ {:else if pagination && (paginationTotalPages > 1 || pagination.hasMore || pagination.showFooterOnSinglePage || (hidePageSizeSelector && hideSteppers))}
1011
1021
  <!-- DataGrid parity: pagination chrome only renders when the data spans
1012
1022
  more than one page (or the server reports more chunks); a
1013
- single-page table shows no footer. -->
1023
+ single-page table shows no footer. `showFooterOnSinglePage` opts
1024
+ a consumer out of that default, e.g. to keep the page-size
1025
+ selector reachable even for a one-page result. Suppressing BOTH
1026
+ controls (via `hideControls`, or `hidePageSizeSelector` +
1027
+ `hideSteppers` together) implies the same — a count-only footer
1028
+ has nothing to hide behind "already on the only page". -->
1014
1029
  <div
1015
1030
  class="table-footer table-paginator"
1016
1031
  data-pw={pagination.testId ?? null}
@@ -1018,47 +1033,55 @@
1018
1033
  >
1019
1034
  <span
1020
1035
  class="table-paginator-range"
1021
- data-pw={typeof testId === 'string'
1022
- ? `${testId}-paginator-range`
1023
- : pagination?.testId
1024
- ? `${pagination.testId}-range`
1025
- : null}
1026
- testID={typeof testId === 'string'
1027
- ? `${testId}-paginator-range`
1028
- : pagination?.testId
1029
- ? `${pagination.testId}-range`
1030
- : null}>{paginationRangeText}</span
1036
+ data-pw={pagination.rangeTestId ??
1037
+ (typeof testId === 'string'
1038
+ ? `${testId}-paginator-range`
1039
+ : pagination?.testId
1040
+ ? `${pagination.testId}-range`
1041
+ : null)}
1042
+ testID={pagination.rangeTestId ??
1043
+ (typeof testId === 'string'
1044
+ ? `${testId}-paginator-range`
1045
+ : pagination?.testId
1046
+ ? `${pagination.testId}-range`
1047
+ : null)}>{paginationRangeText}</span
1031
1048
  >
1032
- <span class="table-paginator-controls">
1033
- {#if pageSizeOptions.length > 0}
1034
- <span class="table-paginator-size">
1035
- <Select
1036
- items={pageSizeOptions.map((sizeOption) => ({
1037
- id: String(sizeOption),
1038
- label: String(sizeOption)
1039
- }))}
1040
- value={[String(effectivePageSize)]}
1049
+ {#if !hidePageSizeSelector || !hideSteppers}
1050
+ <span class="table-paginator-controls">
1051
+ {#if !hidePageSizeSelector && pageSizeOptions.length > 0}
1052
+ <span class="table-paginator-size">
1053
+ <Select
1054
+ items={pageSizeOptions.map((sizeOption) => ({
1055
+ id: String(sizeOption),
1056
+ label: String(sizeOption)
1057
+ }))}
1058
+ value={[String(effectivePageSize)]}
1059
+ disabled={pagination.isLoading ?? false}
1060
+ usePortal
1061
+ testId={pagination.testId && `${pagination.testId}-page-size`}
1062
+ onchange={(selectedSizes) => {
1063
+ if (selectedSizes.length > 0) {
1064
+ handlePageSizeChange(Number(selectedSizes[0]));
1065
+ }
1066
+ }}
1067
+ />
1068
+ </span>
1069
+ {/if}
1070
+ {#if !hideSteppers}
1071
+ <Pagination
1072
+ totalPages={paginationTotalPages}
1073
+ currentPage={effectivePage}
1074
+ hasMore={pagination.hasMore ?? false}
1041
1075
  disabled={pagination.isLoading ?? false}
1042
- usePortal
1043
- testId={pagination.testId && `${pagination.testId}-page-size`}
1044
- onchange={(selectedSizes) => {
1045
- if (selectedSizes.length > 0) {
1046
- handlePageSizeChange(Number(selectedSizes[0]));
1047
- }
1048
- }}
1076
+ testId={pagination.testId && `${pagination.testId}-pages`}
1077
+ prevButtonTestId={pagination.prevButtonTestId}
1078
+ nextButtonTestId={pagination.nextButtonTestId}
1079
+ onchange={handlePageChange}
1080
+ onLoadMore={pagination.onLoadMore}
1049
1081
  />
1050
- </span>
1051
- {/if}
1052
- <Pagination
1053
- totalPages={paginationTotalPages}
1054
- currentPage={effectivePage}
1055
- hasMore={pagination.hasMore ?? false}
1056
- disabled={pagination.isLoading ?? false}
1057
- testId={pagination.testId && `${pagination.testId}-pages`}
1058
- onchange={handlePageChange}
1059
- onLoadMore={pagination.onLoadMore}
1060
- />
1061
- </span>
1082
+ {/if}
1083
+ </span>
1084
+ {/if}
1062
1085
  </div>
1063
1086
  {/if}
1064
1087
  </div>
@@ -68,6 +68,9 @@ export const asTagArrayItems = (value) => {
68
68
  if (typeof record.classes === 'string') {
69
69
  item.classes = record.classes;
70
70
  }
71
+ if (typeof record.testId === 'string') {
72
+ item.testId = record.testId;
73
+ }
71
74
  items.push(item);
72
75
  }
73
76
  return items;
@@ -201,6 +201,27 @@ export type TablePopupMenuCellData = {
201
201
  * the options, selection state, and filtering itself belong to the consumer.
202
202
  * Selecting the already-selected option clears the filter (emits `null`).
203
203
  */
204
+ /**
205
+ * Customizes the generated `data-pw` suffixes of built-in cells for one
206
+ * column. Values replace only the named suffix; row and item indices remain
207
+ * appended where the default includes them. Cell-data `testId` values still
208
+ * take precedence for renderers that accept one.
209
+ */
210
+ export type TableBuiltinCellTestIdSuffixes = {
211
+ icon?: string;
212
+ thumbnail?: string;
213
+ thumbnailPlaceholder?: string;
214
+ tag?: string;
215
+ trendUp?: string;
216
+ trendDown?: string;
217
+ menu?: string;
218
+ menuTrigger?: string;
219
+ popup?: string;
220
+ popupTrigger?: string;
221
+ link?: string;
222
+ copy?: string;
223
+ linkCopied?: string;
224
+ };
204
225
  export type TableColumnFilterConfig = {
205
226
  options: Array<{
206
227
  label: string;
@@ -246,11 +267,18 @@ export type TableColumn = {
246
267
  * cells follow the table-wide `--table-text-align` (left by default).
247
268
  */
248
269
  align?: 'left' | 'center' | 'right';
270
+ /** Fixed/preferred column width (any CSS length), applied inline to its header and cells. */
271
+ width?: string;
249
272
  /**
250
273
  * Caps the column width (any CSS length). Overflowing scalar cell text
251
274
  * ellipsizes with the full value available on the native title tooltip.
252
275
  */
253
276
  maxWidth?: string;
277
+ /**
278
+ * Per-built-in generated `data-pw` suffix overrides for this column. Omitted
279
+ * entries preserve the existing suffixes exactly; row/item indices remain.
280
+ */
281
+ testIdSuffixes?: TableBuiltinCellTestIdSuffixes;
254
282
  /**
255
283
  * Paints this column's header and body cells with the highlight wash —
256
284
  * `--table-col-highlight-background` (body) and
@@ -342,12 +370,54 @@ export type TablePaginationConfig = {
342
370
  hasMore?: boolean;
343
371
  /** Disables the paginator and page-size selector during a fetch. */
344
372
  isLoading?: boolean;
373
+ /**
374
+ * Keeps the paginator footer (range text, page-size selector, steppers)
375
+ * visible even when the data fits on a single page. Default `false`
376
+ * matches DataGrid parity — a single page renders no footer at all.
377
+ */
378
+ showFooterOnSinglePage?: boolean;
379
+ /**
380
+ * Renders only the range summary text ("{from}-{to} of {total}") and
381
+ * suppresses the page-size selector and page steppers — for a bare
382
+ * "Showing X-Y of Z" affordance with no navigation controls. Implies
383
+ * `showFooterOnSinglePage`: a count-only footer has nothing to hide behind
384
+ * "already on the only page". Shorthand for `hidePageSizeSelector: true`
385
+ * plus `hideSteppers: true`; reach for those two directly when you only
386
+ * want to suppress one of the pair. Default `false`.
387
+ */
388
+ hideControls?: boolean;
389
+ /**
390
+ * Suppresses just the page-size Select, independently of the steppers —
391
+ * e.g. a fixed page size with no reason to expose the selector, while
392
+ * still keeping working page navigation. Default `false`.
393
+ */
394
+ hidePageSizeSelector?: boolean;
395
+ /**
396
+ * Suppresses just the Pagination steppers, independently of the page-size
397
+ * Select — for a call site with its own working paginator elsewhere that
398
+ * only wants Table's range text (and, optionally, its page-size selector).
399
+ * Default `false`.
400
+ */
401
+ hideSteppers?: boolean;
345
402
  /** Range text override; default "{from}-{to} of {total}". */
346
403
  rangeLabel?: (from: number, to: number, total: number) => string;
404
+ /**
405
+ * Explicit `data-pw`/`testID` for the range summary span. Wins over the
406
+ * derived id — the default derives from the table's own `testId`
407
+ * (`${testId}-paginator-range`), falling back to `${pagination.testId}-range`
408
+ * only when the table has none, so a call site whose table `testId` is
409
+ * already load-bearing (built-in cell ids derive from it) has no way to
410
+ * give the range span an independent locator without this.
411
+ */
412
+ rangeTestId?: string;
347
413
  onPageChange?: (page: number) => void;
348
414
  onPageSizeChange?: (pageSize: number) => void;
349
415
  onLoadMore?: () => void;
350
416
  testId?: string;
417
+ /** Forwarded to Pagination's previous-page button without modification. */
418
+ prevButtonTestId?: string;
419
+ /** Forwarded to Pagination's next-page (or load-more) button without modification. */
420
+ nextButtonTestId?: string;
351
421
  };
352
422
  /**
353
423
  * Configuration for the built-in search bar (C2-3).
@@ -40,16 +40,20 @@
40
40
 
41
41
  // A detail string or a trace is what makes the indicator expandable. Without either
42
42
  // there is nothing to reveal, so it renders as a plain live status line instead of a
43
- // disclosure control. `bare` overrides that entirely.
43
+ // disclosure control. `bare` and `chip` both override that entirely — neither has
44
+ // room (or a documented reason) to expand.
44
45
  const isExpandable = $derived(
45
- variant !== 'bare' && (hasTraceMode || (typeof detail === 'string' && detail.length > 0))
46
+ variant === 'default' && (hasTraceMode || (typeof detail === 'string' && detail.length > 0))
46
47
  );
47
48
 
48
49
  // Backward-compatible shimmer rule: without an explicit `busy`, the expandable
49
50
  // summary holds still (settled) and every other shape shimmers (live) — exactly
50
- // today's released behaviour. Passing `busy` takes direct control of the shimmer
51
- // in every shape, including `bare`.
52
- const labelIsBusy = $derived(busy ?? !isExpandable);
51
+ // today's released behaviour. `chip` is the one exception, defaulting to static:
52
+ // it exists to be a drop-in for ChatToolStatus, which never shimmered, so a
53
+ // caller porting that usage 1:1 shouldn't have to also learn `busy={false}` just
54
+ // to avoid an unexpected shimmer. Passing `busy` takes direct control of the
55
+ // shimmer in every shape, including `bare` and `chip`.
56
+ const labelIsBusy = $derived(busy ?? (variant === 'chip' ? false : !isExpandable));
53
57
 
54
58
  // Set once a person clicks the disclosure open/closed — from then on the automatic
55
59
  // busy-driven machine below leaves `expanded` alone for the rest of this mount.
@@ -406,6 +410,21 @@
406
410
  >{label}</span
407
411
  >{:else}{label}{/if}</span
408
412
  >
413
+ {:else if variant === 'chip'}
414
+ <div
415
+ class="thinking-indicator-chip {classes ?? ''}"
416
+ aria-live="polite"
417
+ aria-atomic="true"
418
+ data-pw={typeof testId === 'string' ? testId : null}
419
+ testID={typeof testId === 'string' ? testId : null}
420
+ >
421
+ <span class="chip-icon">
422
+ {#if avatar}{@render avatar()}{:else}<Loader />{/if}
423
+ </span>
424
+ <span class="chip-label" class:static-label={!labelIsBusy} data-pw={labelTestId ?? null}
425
+ >{label}</span
426
+ >
427
+ </div>
409
428
  {:else}
410
429
  <div
411
430
  class="thinking-indicator status-host {classes ?? ''}"
@@ -558,6 +577,69 @@
558
577
  -webkit-text-fill-color: var(--thinking-indicator-label-color, #858585);
559
578
  }
560
579
 
580
+ /* ---- chip variant: a self-contained floating pill (no ancestor supplies
581
+ background/layout, unlike bare) — deliberately not `width: 100%`/box-sizing
582
+ from `.thinking-indicator`, since a pill hugs its content. */
583
+ .thinking-indicator-chip {
584
+ box-sizing: border-box;
585
+ display: inline-flex;
586
+ align-items: center;
587
+ gap: var(--thinking-indicator-chip-gap, 8px);
588
+ width: fit-content;
589
+ padding: var(--thinking-indicator-chip-padding, 8px 14px);
590
+ background: var(--thinking-indicator-chip-background, #ffffff);
591
+ border: var(--thinking-indicator-chip-border, 1px solid #e4e4e7);
592
+ border-radius: var(--thinking-indicator-chip-border-radius, 999px);
593
+ box-shadow: var(--thinking-indicator-chip-box-shadow, 0 6px 20px rgba(0, 0, 0, 0.08));
594
+ max-width: var(--thinking-indicator-chip-max-width, 100%);
595
+ }
596
+
597
+ .chip-icon {
598
+ display: inline-flex;
599
+ align-items: center;
600
+ flex-shrink: 0;
601
+ color: var(--thinking-indicator-chip-icon-color, currentColor);
602
+ --loader-foreground: var(--thinking-indicator-chip-spinner-color, currentColor);
603
+ --loader-foreground-end: var(--thinking-indicator-chip-spinner-color-end, transparent);
604
+ --loader-width: var(--thinking-indicator-chip-spinner-size, 14px);
605
+ --loader-height: var(--thinking-indicator-chip-spinner-size, 14px);
606
+ --loader-before-width: 7px;
607
+ --loader-before-height: 7px;
608
+ --loader-after-width: 11px;
609
+ --loader-after-height: 11px;
610
+ --loader-background: var(--thinking-indicator-chip-background, #ffffff);
611
+ }
612
+
613
+ .chip-label {
614
+ flex: 0 1 auto;
615
+ min-width: 0;
616
+ white-space: nowrap;
617
+ overflow: hidden;
618
+ text-overflow: ellipsis;
619
+ font-size: var(--thinking-indicator-chip-font-size, 0.85rem);
620
+ font-weight: var(--thinking-indicator-chip-font-weight, 500);
621
+ color: var(--thinking-indicator-chip-color, #52525b);
622
+ background: var(
623
+ --thinking-indicator-chip-shimmer-gradient,
624
+ linear-gradient(90deg, #52525b 0%, #a0a0a0 50%, #52525b 100%)
625
+ );
626
+ background-size: 200% 100%;
627
+ background-clip: text;
628
+ -webkit-text-fill-color: transparent;
629
+ animation: thinking-indicator-shimmer var(--thinking-indicator-chip-shimmer-duration, 2s) linear
630
+ infinite;
631
+ }
632
+
633
+ /* Chip defaults to a static label (unlike the default/bare variants) since it
634
+ mirrors ChatToolStatus, which never shimmered — pass `busy` explicitly to
635
+ opt in. Higher specificity than the generic `.static-label` rule above, so
636
+ it correctly overrides with the chip's own color token, not the default
637
+ variant's. */
638
+ .chip-label.static-label {
639
+ animation: none;
640
+ -webkit-text-fill-color: var(--thinking-indicator-chip-color, #52525b);
641
+ }
642
+
561
643
  .arrow {
562
644
  display: inline-flex;
563
645
  align-items: center;
@@ -784,6 +866,7 @@
784
866
 
785
867
  @media (prefers-reduced-motion: reduce) {
786
868
  .status-label,
869
+ .chip-label,
787
870
  .trace-row,
788
871
  .trace-more {
789
872
  animation-duration: 0.001s;
@@ -799,5 +882,10 @@
799
882
  animation: none;
800
883
  -webkit-text-fill-color: var(--thinking-indicator-label-color, #858585);
801
884
  }
885
+
886
+ .chip-label {
887
+ animation: none;
888
+ -webkit-text-fill-color: var(--thinking-indicator-chip-color, #52525b);
889
+ }
802
890
  }
803
891
  </style>
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
- export type ThinkingIndicatorVariant = 'default' | 'bare';
2
+ export type ThinkingIndicatorVariant = 'default' | 'bare' | 'chip';
3
3
  export type ThinkingIndicatorKind = 'steps' | 'reasoning' | 'search' | 'coding';
4
4
  export type ThinkingIndicatorTraceRow = {
5
5
  /** The step, sentence, source title, or file action. */
@@ -29,14 +29,18 @@ export type OptionalThinkingIndicatorProperties = {
29
29
  expanded?: boolean;
30
30
  /**
31
31
  * `bare` renders only the shimmering label — for chat bubbles where the surrounding
32
- * UI already supplies the avatar and layout. It never becomes expandable.
32
+ * UI already supplies the avatar and layout. `chip` renders a self-contained pill
33
+ * (bordered, shadowed, its own background) for a live status floating above other
34
+ * content, e.g. a tool-call indicator above a composer — pass `busy={false}` for a
35
+ * static (non-shimmering) label, matching a plain status badge. Neither `bare` nor
36
+ * `chip` ever becomes expandable.
33
37
  */
34
38
  variant?: ThinkingIndicatorVariant;
35
39
  /**
36
40
  * Renders an elapsed `Ns` counter while the label is live. Starts at 0 when a busy
37
41
  * phase begins, ticks every second, and freezes at its final value once the label
38
42
  * settles (driven by `busy` when set, otherwise by the legacy status-line/detail
39
- * shape). No effect on the `bare` variant.
43
+ * shape). No effect on the `bare` or `chip` variants.
40
44
  */
41
45
  showElapsed?: boolean;
42
46
  onToggle?: () => void;