@juspay/svelte-ui-components 4.19.1 → 4.21.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/Card/Card.svelte +22 -0
- package/dist/ChatMessage/ChatMessage.svelte +39 -0
- package/dist/Combobox/Combobox.svelte.d.ts +1 -1
- package/dist/MarkdownText/MarkdownText.svelte +4 -1
- package/dist/MarkdownText/markdown.js +24 -4
- package/dist/MarkdownText/properties.d.ts +11 -0
- package/dist/Pill/Pill.svelte +22 -0
- package/dist/Select/Select.svelte.d.ts +1 -1
- package/dist/Table/Table.svelte +12 -7
- package/dist/Table/properties.d.ts +32 -0
- package/dist/_chart/narrationHighlighter.d.ts +83 -0
- package/dist/_chart/narrationHighlighter.js +113 -0
- package/dist/attrs-discard.d.ts +56 -0
- package/dist/attrs-discard.js +64 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist-wc/index.js +257 -215
- package/package.json +1 -1
package/dist/Card/Card.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { CardProperties } from './properties';
|
|
3
|
+
import { CARD_MANAGED_ATTRS, discardedAttrWarnings } from '../attrs-discard';
|
|
3
4
|
|
|
4
5
|
let {
|
|
5
6
|
children,
|
|
@@ -22,6 +23,27 @@
|
|
|
22
23
|
as
|
|
23
24
|
}: CardProperties = $props();
|
|
24
25
|
|
|
26
|
+
/*
|
|
27
|
+
* The runtime half of the `attrs` deprecation (#576). The `@deprecated`
|
|
28
|
+
* markers on the managed keys only reach someone reading TypeScript; a
|
|
29
|
+
* plain-JS caller, or anyone driving this as a web component, has no types
|
|
30
|
+
* and would otherwise still get silence.
|
|
31
|
+
*
|
|
32
|
+
* Dev builds only: this is an authoring aid, and warning on every production
|
|
33
|
+
* render would be noise nobody asked for. `import.meta.env` is absent under
|
|
34
|
+
* bundlers that do not define it, so the guard reads it defensively and stays
|
|
35
|
+
* quiet there rather than throwing.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately at init rather than in an `$effect`, which this repo bans:
|
|
38
|
+
* once per mounted instance is what a consumer needs to act on, and it costs
|
|
39
|
+
* nothing on subsequent updates.
|
|
40
|
+
*/
|
|
41
|
+
if (import.meta.env?.DEV === true) {
|
|
42
|
+
for (const message of discardedAttrWarnings('Card', CARD_MANAGED_ATTRS, attrs)) {
|
|
43
|
+
console.warn(message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
// `as`, when given, picks the rendered tag outright. Omitted, the root keeps
|
|
26
48
|
// its existing rule: an `<a>` exactly when `href` is set, a `<div>`
|
|
27
49
|
// otherwise -- so a consumer who never passes `as` gets the identical tag
|
|
@@ -570,6 +570,11 @@
|
|
|
570
570
|
opacity: var(--chat-message-blockquote-opacity, 0.85);
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
+
/* Fallback for a bare <table> in caller-supplied `html`, which has no scroll
|
|
574
|
+
container of its own: it keeps the block treatment so a wide table scrolls
|
|
575
|
+
rather than overflowing the message. A table that came from `markdown` (or
|
|
576
|
+
from `renderMarkdown`) arrives inside `.markdown-table-wrapper`, and the
|
|
577
|
+
rules below take it back out of block layout. */
|
|
573
578
|
.body :global(table) {
|
|
574
579
|
display: block;
|
|
575
580
|
max-width: 100%;
|
|
@@ -578,6 +583,40 @@
|
|
|
578
583
|
margin: 0.5em 0;
|
|
579
584
|
}
|
|
580
585
|
|
|
586
|
+
/* The wrapper scrolls, not the table. `display: block` on a <table> is what
|
|
587
|
+
makes `overflow-x` work on the element itself, but it strips the table's
|
|
588
|
+
semantics for assistive technology -- a screen reader loses row and column
|
|
589
|
+
navigation. MarkdownText separates the scroll container from the table for
|
|
590
|
+
exactly that reason; this component rendered the same wrapper markup and
|
|
591
|
+
styled the table instead, so a chat table was scrollable and no longer a
|
|
592
|
+
table. Chat is where markdown tables overwhelmingly land. */
|
|
593
|
+
.body :global(.markdown-table-wrapper) {
|
|
594
|
+
display: block;
|
|
595
|
+
max-width: 100%;
|
|
596
|
+
overflow-x: auto;
|
|
597
|
+
margin: 0.5em 0;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* The wrapper is a tab stop (the renderer emits tabindex="0" so the scroll is
|
|
601
|
+
reachable by keyboard). The browser's default ring already applies; this
|
|
602
|
+
replaces it with the same 2px treatment the rest of the library uses, since
|
|
603
|
+
the 1px UA default is easy to miss against a message bubble. */
|
|
604
|
+
.body :global(.markdown-table-wrapper:focus-visible) {
|
|
605
|
+
outline: 2px solid var(--chat-message-table-focus-outline-color, #3b82f6);
|
|
606
|
+
outline-offset: 2px;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/* Without an intrinsic width the table is exactly its container's width and a
|
|
610
|
+
wide table compresses instead of scrolling. `max-content` supplies it;
|
|
611
|
+
`min-width` keeps a narrow table filling the bubble. */
|
|
612
|
+
.body :global(.markdown-table-wrapper table) {
|
|
613
|
+
display: table;
|
|
614
|
+
width: max-content;
|
|
615
|
+
min-width: 100%;
|
|
616
|
+
overflow-x: visible;
|
|
617
|
+
margin: 0;
|
|
618
|
+
}
|
|
619
|
+
|
|
581
620
|
.body :global(th),
|
|
582
621
|
.body :global(td) {
|
|
583
622
|
padding: 5px 10px;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComboboxProperties } from './properties';
|
|
2
2
|
declare const Combobox: import("svelte").Component<ComboboxProperties, {
|
|
3
3
|
getInputRef: () => HTMLInputElement | HTMLTextAreaElement | null;
|
|
4
|
-
}, "
|
|
4
|
+
}, "value" | "open" | "selected" | "inputValue" | "highlightedIndex">;
|
|
5
5
|
type Combobox = ReturnType<typeof Combobox>;
|
|
6
6
|
export default Combobox;
|
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
testId,
|
|
9
9
|
classes,
|
|
10
10
|
tableLabel,
|
|
11
|
+
tableWrapperClass,
|
|
11
12
|
sanitize
|
|
12
13
|
}: MarkdownTextProperties = $props();
|
|
13
14
|
|
|
14
|
-
let html = $derived(
|
|
15
|
+
let html = $derived(
|
|
16
|
+
renderMarkdown(markdown, { breaks, tableLabel, tableWrapperClass, sanitize })
|
|
17
|
+
);
|
|
15
18
|
</script>
|
|
16
19
|
|
|
17
20
|
<div
|
|
@@ -245,14 +245,32 @@ function annotateExternalLinks(html) {
|
|
|
245
245
|
scrollable region with a tabindex. */
|
|
246
246
|
const TABLE_OPEN = /<table>/g;
|
|
247
247
|
const TABLE_CLOSE = /<\/table>/g;
|
|
248
|
-
|
|
248
|
+
const DEFAULT_TABLE_WRAPPER_CLASS = 'markdown-table-wrapper';
|
|
249
|
+
function wrapTables(html, label, wrapperClass) {
|
|
249
250
|
/* `role="region"` without an accessible name announces a landmark the user
|
|
250
251
|
cannot identify, which is worse than no landmark, so the role appears only
|
|
251
252
|
when the caller supplies a name. The tabindex does not depend on it:
|
|
252
253
|
keyboard scrolling should work either way. */
|
|
254
|
+
/* A caller's class is ADDED, never substituted. The wrapper's own styling --
|
|
255
|
+
`overflow-x: auto`, the `:focus-visible` ring, and the table's max-content
|
|
256
|
+
width -- is scoped to `.markdown-table-wrapper` in MarkdownText.svelte, and
|
|
257
|
+
a Svelte scoped selector cannot be templated to a name supplied at runtime.
|
|
258
|
+
Substituting the class would leave a `tabindex="0"` element that cannot
|
|
259
|
+
scroll and shows no focus ring, undoing the fix that made a wide table
|
|
260
|
+
keyboard-scrollable in the first place. Adding costs a consumer nothing:
|
|
261
|
+
their own selector still matches, and they can still override by
|
|
262
|
+
specificity. */
|
|
263
|
+
const custom = typeof wrapperClass === 'string' ? wrapperClass.trim() : '';
|
|
264
|
+
// The caller's value lands inside a double-quoted HTML attribute, same as
|
|
265
|
+
// `label` below -- escaping it the same way is what stops a value like
|
|
266
|
+
// `" onmouseover="alert(1)` from closing the attribute early and injecting a
|
|
267
|
+
// new one. The built-in name is a literal and needs no escaping.
|
|
268
|
+
const classAttr = custom.length > 0
|
|
269
|
+
? `${DEFAULT_TABLE_WRAPPER_CLASS} ${escapeHtml(custom)}`
|
|
270
|
+
: DEFAULT_TABLE_WRAPPER_CLASS;
|
|
253
271
|
const open = typeof label === 'string' && label.length > 0
|
|
254
|
-
? `<div class="
|
|
255
|
-
:
|
|
272
|
+
? `<div class="${classAttr}" tabindex="0" role="region" aria-label="${escapeHtml(label)}">`
|
|
273
|
+
: `<div class="${classAttr}" tabindex="0">`;
|
|
256
274
|
return html.replace(TABLE_OPEN, `${open}<table>`).replace(TABLE_CLOSE, '</table></div>');
|
|
257
275
|
}
|
|
258
276
|
function instanceFor(options) {
|
|
@@ -292,5 +310,7 @@ export function renderMarkdown(markdown, options = {}) {
|
|
|
292
310
|
}
|
|
293
311
|
/* Inline parsing produces no block elements, so there is no table to wrap. */
|
|
294
312
|
const linked = annotateExternalLinks(output);
|
|
295
|
-
return options.inline === true
|
|
313
|
+
return options.inline === true
|
|
314
|
+
? linked
|
|
315
|
+
: wrapTables(linked, options.tableLabel, options.tableWrapperClass);
|
|
296
316
|
}
|
|
@@ -20,6 +20,15 @@ export type OptionalMarkdownTextProperties = {
|
|
|
20
20
|
* worse than none.
|
|
21
21
|
*/
|
|
22
22
|
tableLabel?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Extra class name added to the scrollable region wrapping each table,
|
|
25
|
+
* alongside the built-in `markdown-table-wrapper` rather than replacing it
|
|
26
|
+
* (the wrapper's scroll/focus behaviour is scoped to that class and cannot
|
|
27
|
+
* be retargeted). Lets a consumer whose own stylesheet targets a different
|
|
28
|
+
* class keep that styling too. Omitting it, or passing an empty or
|
|
29
|
+
* whitespace-only string, keeps only `markdown-table-wrapper`.
|
|
30
|
+
*/
|
|
31
|
+
tableWrapperClass?: string;
|
|
23
32
|
/**
|
|
24
33
|
* Narrow which URL protocols survive on rendered links/images. See
|
|
25
34
|
* `MarkdownSanitizeOptions`.
|
|
@@ -104,6 +113,8 @@ export type RenderMarkdownOptions = {
|
|
|
104
113
|
inline?: boolean;
|
|
105
114
|
/** Accessible name for the scroll region wrapping each table. See `MarkdownTextProperties.tableLabel`. */
|
|
106
115
|
tableLabel?: string;
|
|
116
|
+
/** Class name for the scroll region wrapping each table. See `MarkdownTextProperties.tableWrapperClass`. */
|
|
117
|
+
tableWrapperClass?: string;
|
|
107
118
|
/** Narrow the protocol allow-list. See `MarkdownSanitizeOptions`. */
|
|
108
119
|
sanitize?: MarkdownSanitizeOptions;
|
|
109
120
|
};
|
package/dist/Pill/Pill.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { PillProperties } from './properties';
|
|
3
|
+
import { PILL_MANAGED_ATTRS, discardedAttrWarnings } from '../attrs-discard';
|
|
3
4
|
import Button from '../Button/Button.svelte';
|
|
4
5
|
import closeSvg from '../assets/close.svg?raw';
|
|
5
6
|
import { pillToneClass } from './pillTone';
|
|
@@ -23,6 +24,27 @@
|
|
|
23
24
|
attrs
|
|
24
25
|
}: PillProperties = $props();
|
|
25
26
|
|
|
27
|
+
/*
|
|
28
|
+
* The runtime half of the `attrs` deprecation (#576). The `@deprecated`
|
|
29
|
+
* markers on the managed keys only reach someone reading TypeScript; a
|
|
30
|
+
* plain-JS caller, or anyone driving this as a web component, has no types
|
|
31
|
+
* and would otherwise still get silence.
|
|
32
|
+
*
|
|
33
|
+
* Dev builds only: this is an authoring aid, and warning on every production
|
|
34
|
+
* render would be noise nobody asked for. `import.meta.env` is absent under
|
|
35
|
+
* bundlers that do not define it, so the guard reads it defensively and stays
|
|
36
|
+
* quiet there rather than throwing.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately at init rather than in an `$effect`, which this repo bans:
|
|
39
|
+
* once per mounted instance is what a consumer needs to act on, and it costs
|
|
40
|
+
* nothing on subsequent updates.
|
|
41
|
+
*/
|
|
42
|
+
if (import.meta.env?.DEV === true) {
|
|
43
|
+
for (const message of discardedAttrWarnings('Pill', PILL_MANAGED_ATTRS, attrs)) {
|
|
44
|
+
console.warn(message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
let interactive = $derived(typeof onclick === 'function');
|
|
27
49
|
|
|
28
50
|
// A <button> may not contain another button, and a dismissible pill is inherently two
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { SelectProperties } from './properties';
|
|
2
|
-
declare const Select: import("svelte").Component<SelectProperties, {}, "
|
|
2
|
+
declare const Select: import("svelte").Component<SelectProperties, {}, "value" | "open">;
|
|
3
3
|
type Select = ReturnType<typeof Select>;
|
|
4
4
|
export default Select;
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
summaryRowIndex = null,
|
|
52
52
|
headerTooltipIcon,
|
|
53
53
|
headerTooltipPosition,
|
|
54
|
-
usePortal = false
|
|
54
|
+
usePortal = false,
|
|
55
|
+
labels
|
|
55
56
|
}: TableProperties = $props();
|
|
56
57
|
|
|
57
58
|
// ─── Keyed column model → positional projection ─────────────────────────────
|
|
@@ -646,7 +647,7 @@
|
|
|
646
647
|
class="table-search-clear"
|
|
647
648
|
type="button"
|
|
648
649
|
onclick={clearSearch}
|
|
649
|
-
aria-label=
|
|
650
|
+
aria-label={labels?.clearSearch ?? 'Clear search'}
|
|
650
651
|
>
|
|
651
652
|
<!-- eslint-disable svelte/no-at-html-tags -->
|
|
652
653
|
{@html closeSvg}
|
|
@@ -688,7 +689,7 @@
|
|
|
688
689
|
so the tri-state is computed here and never flipped locally. -->
|
|
689
690
|
<Checkbox
|
|
690
691
|
text=""
|
|
691
|
-
ariaLabel=
|
|
692
|
+
ariaLabel={labels?.selectAllRows ?? 'Select all rows'}
|
|
692
693
|
controlled
|
|
693
694
|
checked={headerCheckboxState === 'all'}
|
|
694
695
|
indeterminate={headerCheckboxState === 'some'}
|
|
@@ -773,7 +774,7 @@
|
|
|
773
774
|
>
|
|
774
775
|
<Button
|
|
775
776
|
{...triggerProps}
|
|
776
|
-
ariaLabel=
|
|
777
|
+
ariaLabel={labels?.filterBy?.(header) ?? `Filter by ${header}`}
|
|
777
778
|
testId={headerColumn.testId &&
|
|
778
779
|
`${headerColumn.testId}-filter-trigger`}
|
|
779
780
|
>
|
|
@@ -791,7 +792,10 @@
|
|
|
791
792
|
{/if}
|
|
792
793
|
{#if isColumnSortable(colIndex)}
|
|
793
794
|
<div class="sort-button">
|
|
794
|
-
<Button
|
|
795
|
+
<Button
|
|
796
|
+
onclick={() => handleSort(colIndex)}
|
|
797
|
+
ariaLabel={labels?.sortBy?.(header) ?? `Sort by ${header}`}
|
|
798
|
+
>
|
|
795
799
|
{#if sortColumn === colIndex && sortDirection === 'asc'}
|
|
796
800
|
{#if typeof sortAscIcon === 'function'}
|
|
797
801
|
{@render sortAscIcon()}
|
|
@@ -840,7 +844,7 @@
|
|
|
840
844
|
<Button
|
|
841
845
|
variant="ghost"
|
|
842
846
|
iconOnly={true}
|
|
843
|
-
ariaLabel=
|
|
847
|
+
ariaLabel={labels?.closeSearch ?? 'Close search'}
|
|
844
848
|
onclick={clearSearch}
|
|
845
849
|
>
|
|
846
850
|
{#snippet icon()}
|
|
@@ -921,7 +925,8 @@
|
|
|
921
925
|
>
|
|
922
926
|
<Checkbox
|
|
923
927
|
text=""
|
|
924
|
-
ariaLabel={
|
|
928
|
+
ariaLabel={labels?.selectRow?.(rowId) ??
|
|
929
|
+
`Select row ${rowId || 'non-selectable'}`}
|
|
925
930
|
controlled
|
|
926
931
|
checked={rowSelected}
|
|
927
932
|
disabled={rowDisabled}
|
|
@@ -434,7 +434,39 @@ export type TableSearchConfig = {
|
|
|
434
434
|
displayMode?: 'toolbar' | 'inline';
|
|
435
435
|
};
|
|
436
436
|
export type TableProperties = OptionalTableProperties & TableEventProperties;
|
|
437
|
+
/**
|
|
438
|
+
* Accessible names Table generates for its own controls, for callers who do
|
|
439
|
+
* not ship an English UI.
|
|
440
|
+
*
|
|
441
|
+
* Every member is optional and falls back to the English string Table has
|
|
442
|
+
* always used, so adding this prop changes nothing for existing consumers.
|
|
443
|
+
*
|
|
444
|
+
* The two that depend on a column, and the one that depends on a row, are
|
|
445
|
+
* functions rather than templates with a placeholder: a translator needs to
|
|
446
|
+
* put the header where their grammar wants it, not where an English sentence
|
|
447
|
+
* happens to leave a slot.
|
|
448
|
+
*
|
|
449
|
+
* `Search` is deliberately absent -- `searchConfig.placeholder` already names
|
|
450
|
+
* the search input and has since search shipped.
|
|
451
|
+
*/
|
|
452
|
+
export type TableLabels = {
|
|
453
|
+
/** Sort button on a column header. Default: `Sort by ${header}`. */
|
|
454
|
+
sortBy?: (header: string) => string;
|
|
455
|
+
/** Filter trigger on a column header. Default: `Filter by ${header}`. */
|
|
456
|
+
filterBy?: (header: string) => string;
|
|
457
|
+
/** Per-row selection checkbox. Receives the row id, or `''` when the row
|
|
458
|
+
* cannot be selected -- the default spells that case out rather than
|
|
459
|
+
* naming an empty row. Default: `Select row ${rowId || 'non-selectable'}`. */
|
|
460
|
+
selectRow?: (rowId: string) => string;
|
|
461
|
+
/** Header checkbox that selects every row on the page. Default: `Select all rows`. */
|
|
462
|
+
selectAllRows?: string;
|
|
463
|
+
/** Button that empties the search box. Default: `Clear search`. */
|
|
464
|
+
clearSearch?: string;
|
|
465
|
+
/** Button that dismisses the search box. Default: `Close search`. */
|
|
466
|
+
closeSearch?: string;
|
|
467
|
+
};
|
|
437
468
|
export type OptionalTableProperties = {
|
|
469
|
+
labels?: TableLabels;
|
|
438
470
|
tableTitle?: string | null;
|
|
439
471
|
tableHeaders?: string[];
|
|
440
472
|
tableData?: Array<JSONValue[]>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ChartHighlightAPI } from './highlight';
|
|
2
|
+
/**
|
|
3
|
+
* Drives a chart's highlight from a growing narration transcript.
|
|
4
|
+
*
|
|
5
|
+
* `LineChart`, `BarChart` and `PieChart` hand back a `ChartHighlightAPI` from
|
|
6
|
+
* `onChartReady`, described there as being "for imperative point highlighting
|
|
7
|
+
* from external orchestrators (e.g. voice narration sync, step-through
|
|
8
|
+
* animations)". The socket shipped; the plug did not, so the obvious consumer —
|
|
9
|
+
* an assistant speaking over a chart — hand-rolled it, together with a registry
|
|
10
|
+
* of charts and a store of pending highlights.
|
|
11
|
+
*
|
|
12
|
+
* None of that is needed here. `getCategories()` is already the label list a
|
|
13
|
+
* registry would have duplicated, and `highlight(null)` is already the clear
|
|
14
|
+
* path, so this holds one `ChartHighlightAPI` and nothing else. Passing the API
|
|
15
|
+
* straight in also removes a race the registry version had to work around: a
|
|
16
|
+
* registry keyed by chart identity has to tolerate `onChartReady` firing before
|
|
17
|
+
* the chart is registered, which is a window that does not exist when the
|
|
18
|
+
* caller simply hands over the object it was given.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* let highlighter: NarrationHighlighter | null = null;
|
|
22
|
+
*
|
|
23
|
+
* <LineChart {data} onChartReady={(chart) => {
|
|
24
|
+
* highlighter = createNarrationHighlighter({ chart });
|
|
25
|
+
* }} />
|
|
26
|
+
*
|
|
27
|
+
* // as each transcript chunk arrives
|
|
28
|
+
* highlighter?.processText(transcriptSoFar);
|
|
29
|
+
* // when the assistant starts a new answer
|
|
30
|
+
* highlighter?.reset();
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export type NarrationHighlighterOptions = {
|
|
34
|
+
/** The API handed to `onChartReady` by the chart being narrated. */
|
|
35
|
+
chart: ChartHighlightAPI;
|
|
36
|
+
/** Milliseconds a highlight stays up before clearing. Defaults to `2000`. */
|
|
37
|
+
autoClearMs?: number;
|
|
38
|
+
/** Match category labels case-sensitively. Defaults to `false`. */
|
|
39
|
+
caseSensitive?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Replaces the built-in matching rule. Receives the narration and one
|
|
42
|
+
* category label, already lower-cased unless `caseSensitive` is set.
|
|
43
|
+
*
|
|
44
|
+
* The default matches a label only as a whole word. Substring matching is
|
|
45
|
+
* available through this hook — `(narration, category) =>
|
|
46
|
+
* narration.includes(category)` — but is a poor default: category labels are
|
|
47
|
+
* frequently ordinary words like `Other`, `All` or `None`, especially when
|
|
48
|
+
* they come from a model's tool output rather than a curated list, and a
|
|
49
|
+
* substring rule fires `Other` on `otherwise` and `others`.
|
|
50
|
+
*
|
|
51
|
+
* Whole-word matching narrows that surface; it does not close it. In "on the
|
|
52
|
+
* other hand", `other` is genuinely a standalone word, so the label still
|
|
53
|
+
* matches and no lexical rule could decide otherwise without reading the
|
|
54
|
+
* sentence. A caller who cannot curate labels should require something
|
|
55
|
+
* stronger here, and should `reset()` between turns so a mis-fire cannot
|
|
56
|
+
* outlive one answer.
|
|
57
|
+
*/
|
|
58
|
+
match?: (narration: string, category: string) => boolean;
|
|
59
|
+
};
|
|
60
|
+
export type NarrationHighlighter = {
|
|
61
|
+
/**
|
|
62
|
+
* Reads the transcript so far and highlights the first not-yet-seen category
|
|
63
|
+
* it names. Returns that category's index, or `null` when the narration named
|
|
64
|
+
* nothing new.
|
|
65
|
+
*
|
|
66
|
+
* Safe to call on every chunk: the transcript is expected to grow, and a
|
|
67
|
+
* category already highlighted this turn is not highlighted again.
|
|
68
|
+
*/
|
|
69
|
+
processText: (narration: string) => number | null;
|
|
70
|
+
/**
|
|
71
|
+
* Starts a new turn: clears the highlight and forgets which categories have
|
|
72
|
+
* fired, so they can fire again for the next answer.
|
|
73
|
+
*/
|
|
74
|
+
reset: () => void;
|
|
75
|
+
/**
|
|
76
|
+
* Cancels a pending auto-clear. Call from the owner's teardown.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately does not call into the chart: teardown ordering is the
|
|
79
|
+
* caller's, and the chart may already be gone.
|
|
80
|
+
*/
|
|
81
|
+
destroy: () => void;
|
|
82
|
+
};
|
|
83
|
+
export declare const createNarrationHighlighter: (options: NarrationHighlighterOptions) => NarrationHighlighter;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const DEFAULT_AUTO_CLEAR_MS = 2000;
|
|
2
|
+
/** Letters, digits and underscore, so a label is matched only as a whole word. */
|
|
3
|
+
const WORD_CHARACTER = /[\p{L}\p{N}_]/u;
|
|
4
|
+
/**
|
|
5
|
+
* `charAt` rather than indexing: out of range it returns `''`, which is not a
|
|
6
|
+
* word character, so the string's edges need no special case.
|
|
7
|
+
*/
|
|
8
|
+
const isWordCharacter = (character) => WORD_CHARACTER.test(character);
|
|
9
|
+
/**
|
|
10
|
+
* True when `needle` appears in `haystack` bounded by non-word characters.
|
|
11
|
+
*
|
|
12
|
+
* Written as a scan rather than a `RegExp` for two reasons: a category label is
|
|
13
|
+
* caller data and would have to be escaped, and `\b` is defined over ASCII word
|
|
14
|
+
* characters, so it mismatches labels like `Café` or `北区`. Lookbehind would
|
|
15
|
+
* solve the second but is not available everywhere this library runs.
|
|
16
|
+
*/
|
|
17
|
+
const occursAsWord = (haystack, needle) => {
|
|
18
|
+
for (let from = 0; from <= haystack.length - needle.length;) {
|
|
19
|
+
const at = haystack.indexOf(needle, from);
|
|
20
|
+
if (at === -1) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const before = at === 0 ? '' : haystack.charAt(at - 1);
|
|
24
|
+
const after = haystack.charAt(at + needle.length);
|
|
25
|
+
if (!isWordCharacter(before) && !isWordCharacter(after)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
from = at + 1;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The type says `number`, but a plain-JS or web-component caller can pass
|
|
34
|
+
* anything. `NaN` reaches `setTimeout` as an immediate fire, clearing the
|
|
35
|
+
* highlight before it has been seen, so an unusable delay degrades to the
|
|
36
|
+
* default rather than breaking the affordance it controls.
|
|
37
|
+
*/
|
|
38
|
+
const resolveAutoClearMs = (value) => {
|
|
39
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
40
|
+
return DEFAULT_AUTO_CLEAR_MS;
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
/** `getCategories` is the chart's, and a torn-down chart can throw or return junk. */
|
|
45
|
+
const readCategories = (chart) => {
|
|
46
|
+
try {
|
|
47
|
+
const categories = chart.getCategories();
|
|
48
|
+
if (!Array.isArray(categories)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return categories.flatMap((category, sourceIndex) => typeof category === 'string' ? [{ label: category, sourceIndex }] : []);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export const createNarrationHighlighter = (options) => {
|
|
58
|
+
const { chart, caseSensitive = false, match } = options;
|
|
59
|
+
const autoClearMs = resolveAutoClearMs(options.autoClearMs ?? null);
|
|
60
|
+
const fired = new Set();
|
|
61
|
+
let clearTimer = null;
|
|
62
|
+
const cancelPendingClear = () => {
|
|
63
|
+
if (clearTimer !== null) {
|
|
64
|
+
clearTimeout(clearTimer);
|
|
65
|
+
clearTimer = null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const matches = (narration, category) => match ? match(narration, category) : occursAsWord(narration, category);
|
|
69
|
+
return {
|
|
70
|
+
processText: (narration) => {
|
|
71
|
+
if (typeof narration !== 'string' || narration.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const haystack = caseSensitive ? narration : narration.toLowerCase();
|
|
75
|
+
const categories = readCategories(chart);
|
|
76
|
+
/*
|
|
77
|
+
* Longest label wins. `Q1` is a whole-word match inside `Q1 Europe`, so
|
|
78
|
+
* taking the first hit would highlight the broader slice while the
|
|
79
|
+
* narration is naming the narrower one. Ties keep the earlier index,
|
|
80
|
+
* which makes the choice stable across calls.
|
|
81
|
+
*/
|
|
82
|
+
let bestIndex = null;
|
|
83
|
+
let bestLength = 0;
|
|
84
|
+
categories.forEach(({ label, sourceIndex }) => {
|
|
85
|
+
if (label.length === 0 || fired.has(sourceIndex) || label.length <= bestLength) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const needle = caseSensitive ? label : label.toLowerCase();
|
|
89
|
+
if (matches(haystack, needle)) {
|
|
90
|
+
bestIndex = sourceIndex;
|
|
91
|
+
bestLength = label.length;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
if (bestIndex === null) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
fired.add(bestIndex);
|
|
98
|
+
cancelPendingClear();
|
|
99
|
+
chart.highlight(bestIndex);
|
|
100
|
+
clearTimer = setTimeout(() => {
|
|
101
|
+
clearTimer = null;
|
|
102
|
+
chart.highlight(null);
|
|
103
|
+
}, autoClearMs);
|
|
104
|
+
return bestIndex;
|
|
105
|
+
},
|
|
106
|
+
reset: () => {
|
|
107
|
+
cancelPendingClear();
|
|
108
|
+
fired.clear();
|
|
109
|
+
chart.highlight(null);
|
|
110
|
+
},
|
|
111
|
+
destroy: cancelPendingClear
|
|
112
|
+
};
|
|
113
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime counterpart to the `@deprecated` markers on `attrs` (#576).
|
|
3
|
+
*
|
|
4
|
+
* Both Card and Pill spread `attrs` onto their root FIRST and write their own
|
|
5
|
+
* managed attributes after, so an entry for a managed key is accepted, spread,
|
|
6
|
+
* and then overwritten. 4.19.0 made that visible in the type by deprecating
|
|
7
|
+
* those keys, which reaches someone reading TypeScript in an editor and nobody
|
|
8
|
+
* else: a plain-JS consumer, or anyone driving `<sui-card>` / `<sui-pill>` as a
|
|
9
|
+
* web component, has no types at all and still gets silence.
|
|
10
|
+
*
|
|
11
|
+
* These maps carry the same guidance the `@deprecated` tags carry, as data the
|
|
12
|
+
* components can read at runtime. `attrs-discard.test.ts` pins each list
|
|
13
|
+
* against its `*StrictAttrs` type so the two cannot drift apart.
|
|
14
|
+
*/
|
|
15
|
+
/** What Card writes on its own root, and what a consumer should reach for instead. */
|
|
16
|
+
export declare const CARD_MANAGED_ATTRS: {
|
|
17
|
+
readonly class: "use the `classes` prop";
|
|
18
|
+
readonly style: "use the `cssVars` prop";
|
|
19
|
+
readonly role: "Card derives it from `onclick`/`href`; there is no prop for it";
|
|
20
|
+
readonly tabindex: "Card derives it from `onclick`/`href`; there is no prop for it";
|
|
21
|
+
readonly href: "use the `href` prop";
|
|
22
|
+
readonly target: "use the `target` prop";
|
|
23
|
+
readonly rel: "use the `rel` prop";
|
|
24
|
+
readonly onclick: "use the `onclick` prop";
|
|
25
|
+
readonly onkeydown: "Card binds its own to activate on Enter/Space; there is no prop for it";
|
|
26
|
+
readonly 'data-pw': "use the `testId` prop";
|
|
27
|
+
readonly testID: "use the `testId` prop";
|
|
28
|
+
};
|
|
29
|
+
/** What Pill writes on its own root, and what a consumer should reach for instead. */
|
|
30
|
+
export declare const PILL_MANAGED_ATTRS: {
|
|
31
|
+
readonly class: "use the `classes` prop";
|
|
32
|
+
readonly type: "Pill always supplies `type=\"button\"` on a button root; there is no prop for it";
|
|
33
|
+
readonly role: "Pill derives it from whether it is interactive; there is no prop for it";
|
|
34
|
+
readonly tabindex: "Pill derives it from whether it is interactive; there is no prop for it";
|
|
35
|
+
readonly title: "use the `title` prop";
|
|
36
|
+
readonly onclick: "use the `onclick` prop";
|
|
37
|
+
readonly onkeydown: "Pill binds its own to activate on Enter/Space; there is no prop for it";
|
|
38
|
+
readonly 'data-pw': "use the `testId` prop";
|
|
39
|
+
readonly testID: "use the `testId` prop";
|
|
40
|
+
readonly 'aria-disabled': "Pill derives it from the `disabled` prop";
|
|
41
|
+
readonly 'aria-expanded': "Pill derives it from the `ariaExpanded` prop";
|
|
42
|
+
readonly 'aria-pressed': "Pill derives it from the `ariaPressed` prop";
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The messages for every managed key present in `attrs`, in the order the
|
|
46
|
+
* component's own list declares them. Pure: the caller decides whether the
|
|
47
|
+
* environment is one that should be told, and does the reporting.
|
|
48
|
+
*
|
|
49
|
+
* `attrs` is the optional trailing parameter so an absent one is simply not
|
|
50
|
+
* passed: the repo bans the `undefined` keyword outright.
|
|
51
|
+
*
|
|
52
|
+
* Only own properties count. An object built on a prototype that happens to
|
|
53
|
+
* carry `class` did not ask for that attribute to be spread, and warning about
|
|
54
|
+
* it would be noise a consumer cannot act on.
|
|
55
|
+
*/
|
|
56
|
+
export declare const discardedAttrWarnings: (component: string, managed: Readonly<Record<string, string>>, attrs?: Readonly<Record<string, string>>) => readonly string[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime counterpart to the `@deprecated` markers on `attrs` (#576).
|
|
3
|
+
*
|
|
4
|
+
* Both Card and Pill spread `attrs` onto their root FIRST and write their own
|
|
5
|
+
* managed attributes after, so an entry for a managed key is accepted, spread,
|
|
6
|
+
* and then overwritten. 4.19.0 made that visible in the type by deprecating
|
|
7
|
+
* those keys, which reaches someone reading TypeScript in an editor and nobody
|
|
8
|
+
* else: a plain-JS consumer, or anyone driving `<sui-card>` / `<sui-pill>` as a
|
|
9
|
+
* web component, has no types at all and still gets silence.
|
|
10
|
+
*
|
|
11
|
+
* These maps carry the same guidance the `@deprecated` tags carry, as data the
|
|
12
|
+
* components can read at runtime. `attrs-discard.test.ts` pins each list
|
|
13
|
+
* against its `*StrictAttrs` type so the two cannot drift apart.
|
|
14
|
+
*/
|
|
15
|
+
/** What Card writes on its own root, and what a consumer should reach for instead. */
|
|
16
|
+
export const CARD_MANAGED_ATTRS = {
|
|
17
|
+
class: 'use the `classes` prop',
|
|
18
|
+
style: 'use the `cssVars` prop',
|
|
19
|
+
role: 'Card derives it from `onclick`/`href`; there is no prop for it',
|
|
20
|
+
tabindex: 'Card derives it from `onclick`/`href`; there is no prop for it',
|
|
21
|
+
href: 'use the `href` prop',
|
|
22
|
+
target: 'use the `target` prop',
|
|
23
|
+
rel: 'use the `rel` prop',
|
|
24
|
+
onclick: 'use the `onclick` prop',
|
|
25
|
+
onkeydown: 'Card binds its own to activate on Enter/Space; there is no prop for it',
|
|
26
|
+
'data-pw': 'use the `testId` prop',
|
|
27
|
+
testID: 'use the `testId` prop'
|
|
28
|
+
};
|
|
29
|
+
/** What Pill writes on its own root, and what a consumer should reach for instead. */
|
|
30
|
+
export const PILL_MANAGED_ATTRS = {
|
|
31
|
+
class: 'use the `classes` prop',
|
|
32
|
+
type: 'Pill always supplies `type="button"` on a button root; there is no prop for it',
|
|
33
|
+
role: 'Pill derives it from whether it is interactive; there is no prop for it',
|
|
34
|
+
tabindex: 'Pill derives it from whether it is interactive; there is no prop for it',
|
|
35
|
+
title: 'use the `title` prop',
|
|
36
|
+
onclick: 'use the `onclick` prop',
|
|
37
|
+
onkeydown: 'Pill binds its own to activate on Enter/Space; there is no prop for it',
|
|
38
|
+
'data-pw': 'use the `testId` prop',
|
|
39
|
+
testID: 'use the `testId` prop',
|
|
40
|
+
'aria-disabled': 'Pill derives it from the `disabled` prop',
|
|
41
|
+
'aria-expanded': 'Pill derives it from the `ariaExpanded` prop',
|
|
42
|
+
'aria-pressed': 'Pill derives it from the `ariaPressed` prop'
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The messages for every managed key present in `attrs`, in the order the
|
|
46
|
+
* component's own list declares them. Pure: the caller decides whether the
|
|
47
|
+
* environment is one that should be told, and does the reporting.
|
|
48
|
+
*
|
|
49
|
+
* `attrs` is the optional trailing parameter so an absent one is simply not
|
|
50
|
+
* passed: the repo bans the `undefined` keyword outright.
|
|
51
|
+
*
|
|
52
|
+
* Only own properties count. An object built on a prototype that happens to
|
|
53
|
+
* carry `class` did not ask for that attribute to be spread, and warning about
|
|
54
|
+
* it would be noise a consumer cannot act on.
|
|
55
|
+
*/
|
|
56
|
+
export const discardedAttrWarnings = (component, managed, attrs) => {
|
|
57
|
+
if (!attrs) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return Object.keys(managed)
|
|
61
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(attrs, key))
|
|
62
|
+
.map((key) => `[svelte-ui-components] ${component}: the "${key}" entry in \`attrs\` is discarded, ` +
|
|
63
|
+
`because ${component} writes its own "${key}" after spreading \`attrs\` — ${managed[key]}.`);
|
|
64
|
+
};
|