@juspay/svelte-ui-components 4.20.0 → 4.22.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.
@@ -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
+ };
@@ -126,6 +126,14 @@ export type LegendItem = {
126
126
  color: string;
127
127
  /** True when the series is toggled off via an interactive legend. */
128
128
  hidden?: boolean;
129
+ /**
130
+ * Formatted per-series aggregate (e.g. `"$1.2K"`, `"45%"`) shown beside the
131
+ * label. `null`/omitted renders nothing -- both the "no `aggregate` was
132
+ * configured for this series" and "the series had no data to aggregate"
133
+ * cases resolve here, via `computeSeriesAggregate`/`formatSeriesAggregate`
134
+ * in `_chart/aggregate.ts`.
135
+ */
136
+ aggregateLabel?: string | null;
129
137
  };
130
138
  /** Data-space anchor for point/category-anchored tooltips (coords are container px). */
131
139
  export type TooltipAnchor = {
package/dist/index.d.ts CHANGED
@@ -185,6 +185,7 @@ export type * from './ProportionBar/properties';
185
185
  export type * from './ChipInput/properties';
186
186
  export type * from './FileDropzoneTrigger/properties';
187
187
  export type * from './_chart/highlight';
188
+ export type * from './_chart/narrationHighlighter';
188
189
  export type * from './Chat/properties';
189
190
  export type * from './Chat/types';
190
191
  export type * from './SpeechToText/types';
@@ -205,3 +206,4 @@ export type * from './Gallery/properties';
205
206
  export { createSoundKit } from './SoundKit/SoundKit';
206
207
  export { validateInput, lockBodyScroll, unlockBodyScroll } from './utils';
207
208
  export { formatNumberIndian } from './_chart/format';
209
+ export { createNarrationHighlighter } from './_chart/narrationHighlighter';
package/dist/index.js CHANGED
@@ -106,3 +106,4 @@ export { createCopyState } from './Snippet/copyState.svelte';
106
106
  export { createSoundKit } from './SoundKit/SoundKit';
107
107
  export { validateInput, lockBodyScroll, unlockBodyScroll } from './utils';
108
108
  export { formatNumberIndian } from './_chart/format';
109
+ export { createNarrationHighlighter } from './_chart/narrationHighlighter';