@juspay/svelte-ui-components 2.135.0 → 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.
- package/dist/Chat/Chat.svelte +12 -0
- package/dist/Chat/properties.d.ts +7 -0
- package/dist/ChatMessageList/ChatMessageList.svelte +54 -1
- package/dist/CheckListItem/CheckListItem.svelte +4 -1
- package/dist/ChipInput/ChipInput.svelte +150 -10
- package/dist/ChipInput/properties.d.ts +18 -1
- package/dist/CommandMenu/CommandMenu.svelte +7 -3
- package/dist/EmptyState/EmptyState.svelte +17 -3
- package/dist/Input/Input.svelte +1 -0
- package/dist/ListItem/ListItem.svelte +14 -12
- package/dist/ListItem/properties.d.ts +7 -0
- package/dist/Menu/Menu.svelte +2 -1
- package/dist/Menu/properties.d.ts +2 -0
- package/dist/Modal/Modal.svelte +3 -3
- package/dist/Pill/Pill.svelte +2 -0
- package/dist/Pill/properties.d.ts +1 -0
- package/dist/Sheet/Sheet.svelte +3 -23
- package/dist/StatCard/StatCard.svelte +64 -1
- package/dist/StatCard/properties.d.ts +44 -2
- package/dist/Status/Status.svelte +2 -1
- package/dist/Status/properties.d.ts +10 -0
- package/dist/Stepper/Step.svelte +95 -2
- package/dist/Stepper/Stepper.svelte +52 -7
- package/dist/Stepper/properties.d.ts +34 -1
- package/dist/Table/BuiltinCell.svelte +46 -23
- package/dist/Table/Table.svelte +63 -40
- package/dist/Table/cellData.js +3 -0
- package/dist/Table/properties.d.ts +70 -0
- package/dist/TypewriterText/TypewriterText.svelte +67 -2
- package/dist/TypewriterText/properties.d.ts +97 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +30 -0
- package/dist-wc/index.js +1186 -926
- 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={
|
|
147
|
-
testID={
|
|
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={
|
|
161
|
-
testID={
|
|
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={
|
|
173
|
-
testID={
|
|
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 ?? (
|
|
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={
|
|
231
|
-
testID={
|
|
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={
|
|
241
|
-
testID={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
494
|
-
testID={
|
|
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={
|
|
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={
|
|
515
|
-
testID={
|
|
537
|
+
data-pw={generatedTestId('linkCopied')}
|
|
538
|
+
testID={generatedTestId('linkCopied')}>Copied</span
|
|
516
539
|
>
|
|
517
540
|
{/if}
|
|
518
541
|
{/if}
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -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={
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
? `${
|
|
1030
|
-
:
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
<
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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>
|
package/dist/Table/cellData.js
CHANGED
|
@@ -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).
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount, onDestroy, untrack } from 'svelte';
|
|
3
|
-
import type { TypewriterTextProperties } from './properties';
|
|
3
|
+
import type { TypewriterTextProperties, TypewriterCharacterDelayRange } from './properties';
|
|
4
4
|
|
|
5
5
|
let {
|
|
6
6
|
text,
|
|
7
7
|
speed = 15,
|
|
8
8
|
isStreaming = false,
|
|
9
9
|
renderText,
|
|
10
|
+
variableDelay,
|
|
11
|
+
resolveDelay,
|
|
12
|
+
onProgress,
|
|
13
|
+
renderCharacter,
|
|
10
14
|
testId,
|
|
11
15
|
classes
|
|
12
16
|
}: TypewriterTextProperties = $props();
|
|
@@ -18,11 +22,63 @@
|
|
|
18
22
|
// Where typing left off, so newly streamed-in text continues rather than restarts.
|
|
19
23
|
let previousTextLength = $state(0);
|
|
20
24
|
|
|
25
|
+
// Whitespace characters revealed so far — the state `resolveDelay` reads via
|
|
26
|
+
// `TypewriterDelayContext.wordCount`. Reset alongside the other typing state
|
|
27
|
+
// whenever `text` is replaced rather than continued (see the mount $effect below).
|
|
28
|
+
let revealedWordCount = $state(0);
|
|
29
|
+
|
|
30
|
+
const WHITESPACE_CHARACTERS = new Set([' ', '\n']);
|
|
31
|
+
const PUNCTUATION_CHARACTERS = new Set([',', '.', '?', '!']);
|
|
32
|
+
const DIGIT_PATTERN = /\d/;
|
|
33
|
+
|
|
34
|
+
const randomDelayInRange = (range: TypewriterCharacterDelayRange): number => {
|
|
35
|
+
return range.min + Math.random() * (range.max - range.min);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Falls straight through to the flat `speed` — byte-identical to the pre-`variableDelay`
|
|
39
|
+
// behaviour — for any consumer that never sets `variableDelay`/`resolveDelay`, or leaves
|
|
40
|
+
// a class out of `variableDelay`. `resolveDelay`, when set, takes over entirely: it is
|
|
41
|
+
// the more general mechanism (see its doc comment in properties.ts) and is expected to
|
|
42
|
+
// own the full pacing decision rather than compose with `variableDelay`/`speed`.
|
|
43
|
+
const resolveTypingDelay = (character: string, characterIndex: number): number => {
|
|
44
|
+
if (typeof resolveDelay === 'function') {
|
|
45
|
+
return resolveDelay({ character, index: characterIndex, wordCount: revealedWordCount });
|
|
46
|
+
}
|
|
47
|
+
if (!variableDelay) {
|
|
48
|
+
return speed;
|
|
49
|
+
}
|
|
50
|
+
if (DIGIT_PATTERN.test(character) && variableDelay.digit) {
|
|
51
|
+
return randomDelayInRange(variableDelay.digit);
|
|
52
|
+
}
|
|
53
|
+
if (WHITESPACE_CHARACTERS.has(character) && variableDelay.whitespace) {
|
|
54
|
+
return randomDelayInRange(variableDelay.whitespace);
|
|
55
|
+
}
|
|
56
|
+
if (PUNCTUATION_CHARACTERS.has(character) && variableDelay.punctuation) {
|
|
57
|
+
return randomDelayInRange(variableDelay.punctuation);
|
|
58
|
+
}
|
|
59
|
+
if (variableDelay.default) {
|
|
60
|
+
return randomDelayInRange(variableDelay.default);
|
|
61
|
+
}
|
|
62
|
+
return speed;
|
|
63
|
+
};
|
|
64
|
+
|
|
21
65
|
const typeNextCharacter = (): void => {
|
|
22
66
|
if (currentIndex < text.length) {
|
|
67
|
+
const revealedCharacter = text[currentIndex];
|
|
68
|
+
const revealedCharacterIndex = currentIndex;
|
|
69
|
+
// Word count is updated for THIS character before it is used to resolve THIS
|
|
70
|
+
// character's own delay — the ordering `TypewriterDelayContext.wordCount` documents
|
|
71
|
+
// and that a cyclical/positional `resolveDelay` depends on.
|
|
72
|
+
if (WHITESPACE_CHARACTERS.has(revealedCharacter)) {
|
|
73
|
+
revealedWordCount++;
|
|
74
|
+
}
|
|
23
75
|
displayedText = text.substring(0, currentIndex + 1);
|
|
24
76
|
currentIndex++;
|
|
25
|
-
|
|
77
|
+
onProgress?.({ index: currentIndex, total: text.length, displayedText });
|
|
78
|
+
timeoutId = setTimeout(
|
|
79
|
+
typeNextCharacter,
|
|
80
|
+
resolveTypingDelay(revealedCharacter, revealedCharacterIndex)
|
|
81
|
+
);
|
|
26
82
|
}
|
|
27
83
|
};
|
|
28
84
|
|
|
@@ -47,6 +103,7 @@
|
|
|
47
103
|
displayedText = '';
|
|
48
104
|
currentIndex = 0;
|
|
49
105
|
previousTextLength = 0;
|
|
106
|
+
revealedWordCount = 0;
|
|
50
107
|
}
|
|
51
108
|
if (nextText.length > 0 && nextText.length > previousTextLength) {
|
|
52
109
|
if (currentIndex >= previousTextLength) {
|
|
@@ -66,8 +123,12 @@
|
|
|
66
123
|
if (timeoutId !== null) {
|
|
67
124
|
clearTimeout(timeoutId);
|
|
68
125
|
}
|
|
126
|
+
const hadRemainingText = currentIndex < text.length;
|
|
69
127
|
displayedText = text;
|
|
70
128
|
currentIndex = text.length;
|
|
129
|
+
if (hadRemainingText) {
|
|
130
|
+
onProgress?.({ index: currentIndex, total: text.length, displayedText });
|
|
131
|
+
}
|
|
71
132
|
}
|
|
72
133
|
});
|
|
73
134
|
|
|
@@ -94,6 +155,10 @@
|
|
|
94
155
|
{#if typeof renderText === 'function'}
|
|
95
156
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
96
157
|
{@html renderText(displayedText)}
|
|
158
|
+
{:else if renderCharacter}
|
|
159
|
+
{#each displayedText.split('') as character, index (index)}
|
|
160
|
+
{@render renderCharacter({ character, index })}
|
|
161
|
+
{/each}
|
|
97
162
|
{:else}
|
|
98
163
|
{displayedText}
|
|
99
164
|
{/if}
|
|
@@ -1,9 +1,72 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
export type TypewriterTextProperties = OptionalTypewriterTextProperties & MandatoryTypewriterTextProperties;
|
|
2
3
|
export type MandatoryTypewriterTextProperties = {
|
|
3
4
|
text: string;
|
|
4
5
|
};
|
|
6
|
+
/** A random delay in `[min, max]` milliseconds is picked per character in that class. Pass equal `min`/`max` for a fixed delay. */
|
|
7
|
+
export type TypewriterCharacterDelayRange = {
|
|
8
|
+
min: number;
|
|
9
|
+
max: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Per-character-class pacing, in place of the flat `speed`. Reach for this when a
|
|
13
|
+
* conversation needs to slow down for numbers (prices, OTPs, phone numbers) or add a
|
|
14
|
+
* beat at punctuation, the way a person reading it aloud would. Any class you omit —
|
|
15
|
+
* including `default` — falls back to `speed` for that character.
|
|
16
|
+
*/
|
|
17
|
+
export type TypewriterVariableDelay = {
|
|
18
|
+
/** Digits (`0`–`9`). */
|
|
19
|
+
digit?: TypewriterCharacterDelayRange;
|
|
20
|
+
/** A space or newline — the natural word-boundary pause. */
|
|
21
|
+
whitespace?: TypewriterCharacterDelayRange;
|
|
22
|
+
/** Sentence punctuation: `,` `.` `?` `!`. */
|
|
23
|
+
punctuation?: TypewriterCharacterDelayRange;
|
|
24
|
+
/** Every other character (letters and anything not covered above). */
|
|
25
|
+
default?: TypewriterCharacterDelayRange;
|
|
26
|
+
};
|
|
27
|
+
export type TypewriterProgress = {
|
|
28
|
+
/** Characters revealed so far. */
|
|
29
|
+
index: number;
|
|
30
|
+
/** Length of `text` at the time of this update — compare against `index` for a percentage. */
|
|
31
|
+
total: number;
|
|
32
|
+
/** The text revealed so far — the same value driving what's on screen. */
|
|
33
|
+
displayedText: string;
|
|
34
|
+
};
|
|
35
|
+
export type TypewriterCharacterContext = {
|
|
36
|
+
/** The character being rendered. */
|
|
37
|
+
character: string;
|
|
38
|
+
/** Its position within the full `text` string. */
|
|
39
|
+
index: number;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Everything `resolveDelay` gets to decide the next character's pacing from — a
|
|
43
|
+
* superset of `TypewriterCharacterContext` because pacing can depend on more than the
|
|
44
|
+
* character alone (see `wordCount`).
|
|
45
|
+
*/
|
|
46
|
+
export type TypewriterDelayContext = TypewriterCharacterContext & {
|
|
47
|
+
/**
|
|
48
|
+
* Whitespace characters (space or newline) revealed so far — INCLUDING this one, if
|
|
49
|
+
* `character` itself is whitespace. The component always updates this count before
|
|
50
|
+
* calling `resolveDelay`, so a cadence that keys off word position (a cyclical
|
|
51
|
+
* acceleration window, slowing down only for the first word of a sentence, etc.) can
|
|
52
|
+
* rely on that ordering instead of inferring it through a side channel like
|
|
53
|
+
* `onProgress`. Counts only whitespace, matching the `whitespace` class in
|
|
54
|
+
* `TypewriterVariableDelay` — it is not a word index in any richer sense.
|
|
55
|
+
*/
|
|
56
|
+
wordCount: number;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Computes the delay before the NEXT character types, given the one that was just
|
|
60
|
+
* revealed and enough state (`index`, `wordCount`) to vary that delay with position —
|
|
61
|
+
* not just character class. Reach for this instead of `variableDelay` when the cadence
|
|
62
|
+
* needs to depend on where typing currently is, not only on what the character is: e.g.
|
|
63
|
+
* an accelerating window every N words, or a pause that only applies to the first
|
|
64
|
+
* occurrence of a character class. `variableDelay` stays the right tool for a pure
|
|
65
|
+
* per-class rule with no positional state.
|
|
66
|
+
*/
|
|
67
|
+
export type TypewriterDelayResolver = (context: TypewriterDelayContext) => number;
|
|
5
68
|
export type OptionalTypewriterTextProperties = {
|
|
6
|
-
/** Milliseconds between characters. */
|
|
69
|
+
/** Milliseconds between characters. Ignored per character class covered by `variableDelay`. */
|
|
7
70
|
speed?: number;
|
|
8
71
|
/**
|
|
9
72
|
* While `true`, text revealed so far stays and new text keeps typing as `text` grows.
|
|
@@ -16,6 +79,39 @@ export type OptionalTypewriterTextProperties = {
|
|
|
16
79
|
* text can contain untrusted input. When omitted, the text renders as plain text.
|
|
17
80
|
*/
|
|
18
81
|
renderText?: (text: string) => string;
|
|
82
|
+
/**
|
|
83
|
+
* Opt into per-character-class pacing (digits, whitespace, punctuation, everything
|
|
84
|
+
* else) instead of the flat `speed`. Omit to keep the flat `speed` for every character.
|
|
85
|
+
*/
|
|
86
|
+
variableDelay?: TypewriterVariableDelay;
|
|
87
|
+
/**
|
|
88
|
+
* Opt into a fully custom pacing function, called once per character in place of
|
|
89
|
+
* `variableDelay`/`speed`. Needed when the cadence isn't a pure function of the
|
|
90
|
+
* character's own class — e.g. an accelerating window that recurs every N words and,
|
|
91
|
+
* while active, collapses whitespace/punctuation/default pacing to a single flat
|
|
92
|
+
* range regardless of class, with only digits kept slow throughout. `variableDelay`
|
|
93
|
+
* cannot express that fourth, position-driven case because it re-evaluates from
|
|
94
|
+
* scratch per character with no notion of where typing currently is; `resolveDelay`
|
|
95
|
+
* closes that gap by handing the resolver `index` and `wordCount` alongside the
|
|
96
|
+
* character (see `TypewriterDelayContext` for the ordering guarantee on `wordCount`),
|
|
97
|
+
* so state like a running cycle position can live in the resolver's own closure
|
|
98
|
+
* instead of a side channel like swapping `variableDelay` from inside `onProgress`.
|
|
99
|
+
* Takes priority over `variableDelay` when both are set. Omit to keep
|
|
100
|
+
* `variableDelay`/`speed` resolution exactly as today.
|
|
101
|
+
*/
|
|
102
|
+
resolveDelay?: TypewriterDelayResolver;
|
|
103
|
+
/**
|
|
104
|
+
* Called every time a character is revealed (and once more if `isStreaming` turns
|
|
105
|
+
* `false` while text remains, since the rest appears at once) — scroll a container to
|
|
106
|
+
* follow the reveal, or show how far along it is. Not called when omitted.
|
|
107
|
+
*/
|
|
108
|
+
onProgress?: (progress: TypewriterProgress) => void;
|
|
109
|
+
/**
|
|
110
|
+
* Render each revealed character yourself — highlight a token, wrap a number — instead
|
|
111
|
+
* of the plain text node. Ignored when `renderText` is set, since that renderer already
|
|
112
|
+
* owns the full markup. Falls back to plain text per character when omitted.
|
|
113
|
+
*/
|
|
114
|
+
renderCharacter?: Snippet<[TypewriterCharacterContext]>;
|
|
19
115
|
testId?: string;
|
|
20
116
|
classes?: string;
|
|
21
117
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -120,6 +120,7 @@ export type * from './Table/properties';
|
|
|
120
120
|
export type { NormalizedColumns } from './Table/normalizeColumns';
|
|
121
121
|
export type { SortTableRowsOptions, TableSortType } from './Table/sortEngine';
|
|
122
122
|
export type * from './Stepper/properties';
|
|
123
|
+
export type { Step as StepperStep } from './Stepper/properties';
|
|
123
124
|
export type * from './Toast/properties';
|
|
124
125
|
export type * from './IconStack/properties';
|
|
125
126
|
export type * from './Img/properties';
|
|
@@ -197,5 +198,5 @@ export type * from './MediaPlayer/properties';
|
|
|
197
198
|
export type * from './MediaUpload/properties';
|
|
198
199
|
export type * from './Gallery/properties';
|
|
199
200
|
export { createSoundKit } from './soundKit/soundKit';
|
|
200
|
-
export { validateInput } from './utils';
|
|
201
|
+
export { validateInput, lockBodyScroll, unlockBodyScroll } from './utils';
|
|
201
202
|
export { formatNumberIndian } from './_chart/format';
|
package/dist/index.js
CHANGED
|
@@ -101,5 +101,5 @@ export { ChatController } from './Chat/controller.svelte';
|
|
|
101
101
|
export { partyOf } from './Chat/roles';
|
|
102
102
|
export { SpeechToTextController } from './SpeechToText/controller.svelte';
|
|
103
103
|
export { createSoundKit } from './soundKit/soundKit';
|
|
104
|
-
export { validateInput } from './utils';
|
|
104
|
+
export { validateInput, lockBodyScroll, unlockBodyScroll } from './utils';
|
|
105
105
|
export { formatNumberIndian } from './_chart/format';
|