@happyvertical/smrt-ui 0.57.5 → 0.57.7
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/charts/Chart.svelte +211 -11
- package/dist/charts/Chart.svelte.d.ts +21 -0
- package/dist/charts/Chart.svelte.d.ts.map +1 -1
- package/dist/charts/format.d.ts +13 -1
- package/dist/charts/format.d.ts.map +1 -1
- package/dist/charts/format.js +46 -3
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +1 -1
- package/dist/charts/strings.d.ts +4 -0
- package/dist/charts/strings.d.ts.map +1 -1
- package/dist/charts/strings.js +4 -0
- package/dist/components/chat/assistant-request.d.ts +25 -0
- package/dist/components/chat/assistant-request.d.ts.map +1 -0
- package/dist/components/chat/assistant-request.js +24 -0
- package/dist/components/chat/index.d.ts +3 -1
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +3 -1
- package/package.json +2 -2
package/dist/charts/Chart.svelte
CHANGED
|
@@ -20,12 +20,37 @@ export interface Props {
|
|
|
20
20
|
title?: string;
|
|
21
21
|
/** Locale used to format displayed numeric values. */
|
|
22
22
|
locale?: string;
|
|
23
|
+
/**
|
|
24
|
+
* How much room the chart takes. `compact` is a sparkline for a card (a
|
|
25
|
+
* line with no axis, labels or readout); `tall` gives a page's main chart
|
|
26
|
+
* room. Default `regular`.
|
|
27
|
+
*/
|
|
28
|
+
size?: 'compact' | 'regular' | 'tall';
|
|
29
|
+
/**
|
|
30
|
+
* Line: label the value axis (top, middle and bottom values, short form)
|
|
31
|
+
* with light gridlines. Default on for `tall`.
|
|
32
|
+
*/
|
|
33
|
+
valueAxis?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Line: let the pointer and the arrow keys read each value in a tooltip
|
|
36
|
+
* (a slider whose value text is the point). Default on for `tall`.
|
|
37
|
+
*/
|
|
38
|
+
explore?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Line: the peak and latest values under the chart. Default on; a page
|
|
41
|
+
* that shows its own headline figures turns it off.
|
|
42
|
+
*/
|
|
43
|
+
readout?: boolean;
|
|
23
44
|
}
|
|
24
45
|
let {
|
|
25
46
|
data,
|
|
26
47
|
style: requestedStyle = 'bar',
|
|
27
48
|
title = '',
|
|
28
49
|
locale = 'en',
|
|
50
|
+
size = 'regular',
|
|
51
|
+
valueAxis,
|
|
52
|
+
explore,
|
|
53
|
+
readout = true,
|
|
29
54
|
}: Props = $props();
|
|
30
55
|
|
|
31
56
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -33,6 +58,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
const { t } = useI18n();
|
|
61
|
+
const uid = $props.id();
|
|
36
62
|
const MAX_POINTS = 60;
|
|
37
63
|
|
|
38
64
|
const chart = $derived(isRecord(data) ? data : null);
|
|
@@ -55,6 +81,10 @@ const STYLES = ['bar', 'line', 'table', 'pie', 'stat'] as const;
|
|
|
55
81
|
const style = $derived(
|
|
56
82
|
STYLES.find((candidate) => candidate === requestedStyle) ?? 'bar',
|
|
57
83
|
);
|
|
84
|
+
/** Whether the line draws its value axis (`showAxis`, readable before it). */
|
|
85
|
+
function requestedAxis(): boolean {
|
|
86
|
+
return size !== 'compact' && (valueAxis ?? size === 'tall');
|
|
87
|
+
}
|
|
58
88
|
const fmt = (value: number): string =>
|
|
59
89
|
formatWidgetValue(value, {
|
|
60
90
|
format: chart?.format ?? 'integer',
|
|
@@ -63,7 +93,17 @@ const fmt = (value: number): string =>
|
|
|
63
93
|
});
|
|
64
94
|
const max = $derived(Math.max(0, ...points.map((point) => point.value)));
|
|
65
95
|
const min = $derived(Math.min(0, ...points.map((point) => point.value)));
|
|
66
|
-
|
|
96
|
+
/** A round number at or above `value` (1, 2, 2.5 or 5 times a power of ten). */
|
|
97
|
+
function niceCeil(value: number): number {
|
|
98
|
+
if (value <= 0) return 0;
|
|
99
|
+
const power = 10 ** Math.floor(Math.log10(value));
|
|
100
|
+
const step = [1, 2, 2.5, 5, 10].find((n) => value / power <= n) ?? 10;
|
|
101
|
+
return step * power;
|
|
102
|
+
}
|
|
103
|
+
// The line's top: the peak, or with a value axis two round steps above zero
|
|
104
|
+
// (so the ticks read $0 / $50K / $100K and the peak never meets the frame).
|
|
105
|
+
const top = $derived(requestedAxis() && max > 0 ? niceCeil(max / 2) * 2 : max);
|
|
106
|
+
const span = $derived(top - min || 1);
|
|
67
107
|
const polyline = $derived(
|
|
68
108
|
points
|
|
69
109
|
.map((point, index) => {
|
|
@@ -122,9 +162,83 @@ const deltaText = $derived(
|
|
|
122
162
|
},
|
|
123
163
|
),
|
|
124
164
|
);
|
|
165
|
+
// Line axis: at most three labels (first, middle, last), so long or many
|
|
166
|
+
// labels never overflow the chart's width.
|
|
167
|
+
const axisLabels = $derived.by(() => {
|
|
168
|
+
if (points.length <= 2) return points.map((point) => point.label);
|
|
169
|
+
const middle = points[Math.floor((points.length - 1) / 2)];
|
|
170
|
+
return [points[0].label, middle.label, points[points.length - 1].label];
|
|
171
|
+
});
|
|
172
|
+
const peak = $derived(
|
|
173
|
+
points.reduce<{ label: string; value: number } | null>(
|
|
174
|
+
(best, point) => (!best || point.value > best.value ? point : best),
|
|
175
|
+
null,
|
|
176
|
+
),
|
|
177
|
+
);
|
|
125
178
|
const summary = $derived(
|
|
126
179
|
`${title}. ${t(M['ui.overview.chart.summary'], { count: points.length })}`,
|
|
127
180
|
);
|
|
181
|
+
|
|
182
|
+
// Line extras: the value axis and the explorable readout.
|
|
183
|
+
const compact = $derived(size === 'compact');
|
|
184
|
+
const showAxis = $derived(requestedAxis());
|
|
185
|
+
const explorable = $derived(!compact && (explore ?? size === 'tall'));
|
|
186
|
+
const short = (value: number): string =>
|
|
187
|
+
formatWidgetValue(value, {
|
|
188
|
+
format: chart?.format ?? 'integer',
|
|
189
|
+
currency: chart?.currency,
|
|
190
|
+
locale,
|
|
191
|
+
compact: true,
|
|
192
|
+
});
|
|
193
|
+
const axisValues = $derived([top, (top + min) / 2, min]);
|
|
194
|
+
/** A point's place in the 100 x 40 plot. */
|
|
195
|
+
function place(index: number): { x: number; y: number } {
|
|
196
|
+
const point = points[index];
|
|
197
|
+
const x = points.length === 1 ? 50 : (index / (points.length - 1)) * 100;
|
|
198
|
+
const y = 36 - (((point?.value ?? 0) - min) / span) * 32;
|
|
199
|
+
return { x, y };
|
|
200
|
+
}
|
|
201
|
+
let active = $state<number | null>(null);
|
|
202
|
+
const activePoint = $derived(
|
|
203
|
+
active !== null && points[active]
|
|
204
|
+
? { ...points[active], ...place(active) }
|
|
205
|
+
: null,
|
|
206
|
+
);
|
|
207
|
+
const activeText = $derived(
|
|
208
|
+
activePoint
|
|
209
|
+
? t(M['ui.overview.chart.point'], {
|
|
210
|
+
label: activePoint.label,
|
|
211
|
+
value: fmt(activePoint.value),
|
|
212
|
+
})
|
|
213
|
+
: '',
|
|
214
|
+
);
|
|
215
|
+
function pointAt(event: PointerEvent) {
|
|
216
|
+
const box = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
217
|
+
if (box.width <= 0 || points.length === 0) return;
|
|
218
|
+
const ratio = Math.min(
|
|
219
|
+
1,
|
|
220
|
+
Math.max(0, (event.clientX - box.left) / box.width),
|
|
221
|
+
);
|
|
222
|
+
active = Math.round(ratio * (points.length - 1));
|
|
223
|
+
}
|
|
224
|
+
function step(event: KeyboardEvent) {
|
|
225
|
+
const last = points.length - 1;
|
|
226
|
+
if (last < 0) return;
|
|
227
|
+
const current = active ?? -1;
|
|
228
|
+
const next =
|
|
229
|
+
event.key === 'ArrowRight' || event.key === 'ArrowUp'
|
|
230
|
+
? Math.min(last, current + 1)
|
|
231
|
+
: event.key === 'ArrowLeft' || event.key === 'ArrowDown'
|
|
232
|
+
? Math.max(0, current < 0 ? last : current - 1)
|
|
233
|
+
: event.key === 'Home'
|
|
234
|
+
? 0
|
|
235
|
+
: event.key === 'End'
|
|
236
|
+
? last
|
|
237
|
+
: null;
|
|
238
|
+
if (next === null) return;
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
active = next;
|
|
241
|
+
}
|
|
128
242
|
</script>
|
|
129
243
|
|
|
130
244
|
{#snippet dataTable(hidden: boolean)}
|
|
@@ -144,6 +258,36 @@ const summary = $derived(
|
|
|
144
258
|
</table>
|
|
145
259
|
{/snippet}
|
|
146
260
|
|
|
261
|
+
{#snippet linePlot(interactive: boolean)}
|
|
262
|
+
<svg
|
|
263
|
+
class="smrt-chart__line"
|
|
264
|
+
viewBox="0 0 100 40"
|
|
265
|
+
preserveAspectRatio="none"
|
|
266
|
+
role={interactive ? undefined : 'img'}
|
|
267
|
+
aria-label={interactive ? undefined : summary}
|
|
268
|
+
aria-hidden={interactive ? 'true' : undefined}
|
|
269
|
+
>
|
|
270
|
+
{#if showAxis}
|
|
271
|
+
{#each [4, 20, 36] as y (y)}
|
|
272
|
+
<line class="smrt-chart__grid" x1="0" x2="100" y1={y} y2={y} vector-effect="non-scaling-stroke" />
|
|
273
|
+
{/each}
|
|
274
|
+
{/if}
|
|
275
|
+
<polyline points={polyline} fill="none" stroke="currentColor" stroke-width="2" vector-effect="non-scaling-stroke" />
|
|
276
|
+
{#if activePoint}
|
|
277
|
+
<line class="smrt-chart__guide" x1={activePoint.x} x2={activePoint.x} y1="0" y2="40" vector-effect="non-scaling-stroke" />
|
|
278
|
+
{/if}
|
|
279
|
+
</svg>
|
|
280
|
+
{#if activePoint}
|
|
281
|
+
<span class="smrt-chart__dot" style:inset-inline-start="{activePoint.x}%" style:inset-block-start="{(activePoint.y / 40) * 100}%" aria-hidden="true"></span>
|
|
282
|
+
<span
|
|
283
|
+
class="smrt-chart__tip"
|
|
284
|
+
data-edge={activePoint.x < 15 ? 'start' : activePoint.x > 85 ? 'end' : undefined}
|
|
285
|
+
style:inset-inline-start="{activePoint.x}%"
|
|
286
|
+
aria-hidden="true"
|
|
287
|
+
><span>{activePoint.label}</span><strong>{fmt(activePoint.value)}</strong></span>
|
|
288
|
+
{/if}
|
|
289
|
+
{/snippet}
|
|
290
|
+
|
|
147
291
|
{#if points.length === 0}
|
|
148
292
|
<p class="smrt-chart__empty" data-smrt-component="Chart">{t(M['ui.overview.no_data'])}</p>
|
|
149
293
|
{:else if style === 'table'}
|
|
@@ -200,14 +344,50 @@ const summary = $derived(
|
|
|
200
344
|
<p class="smrt-chart__empty" data-smrt-component="Chart">{t(M['ui.overview.no_data'])}</p>
|
|
201
345
|
{/if}
|
|
202
346
|
{:else if style === 'line'}
|
|
203
|
-
<div class="smrt-chart" data-smrt-component="Chart">
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
347
|
+
<div class="smrt-chart smrt-chart--line" data-size={size} data-smrt-component="Chart">
|
|
348
|
+
<div class="smrt-chart__frame" class:smrt-chart__frame--axis={showAxis}>
|
|
349
|
+
{#if showAxis}
|
|
350
|
+
<p class="smrt-chart__values" aria-hidden="true">
|
|
351
|
+
{#each axisValues as value, index (index)}<span>{short(value)}</span>{/each}
|
|
352
|
+
</p>
|
|
353
|
+
{/if}
|
|
354
|
+
<div class="smrt-chart__body">
|
|
355
|
+
{#if explorable}
|
|
356
|
+
<div
|
|
357
|
+
class="smrt-chart__plot"
|
|
358
|
+
role="slider"
|
|
359
|
+
tabindex="0"
|
|
360
|
+
aria-label={summary}
|
|
361
|
+
aria-describedby="{uid}-explore"
|
|
362
|
+
aria-orientation="horizontal"
|
|
363
|
+
aria-valuemin={0}
|
|
364
|
+
aria-valuemax={Math.max(0, points.length - 1)}
|
|
365
|
+
aria-valuenow={active ?? points.length - 1}
|
|
366
|
+
aria-valuetext={activeText || (latest ? t(M['ui.overview.chart.point'], { label: latest.label, value: fmt(latest.value) }) : '')}
|
|
367
|
+
onpointermove={pointAt}
|
|
368
|
+
onpointerleave={() => (active = null)}
|
|
369
|
+
onkeydown={step}
|
|
370
|
+
onblur={() => (active = null)}
|
|
371
|
+
>
|
|
372
|
+
{@render linePlot(true)}
|
|
373
|
+
</div>
|
|
374
|
+
{:else}
|
|
375
|
+
<div class="smrt-chart__plot">{@render linePlot(false)}</div>
|
|
376
|
+
{/if}
|
|
377
|
+
{#if !compact}
|
|
378
|
+
<p class="smrt-chart__axis" aria-hidden="true">
|
|
379
|
+
{#each axisLabels as label, index (index)}<span>{label}</span>{/each}
|
|
380
|
+
</p>
|
|
381
|
+
{/if}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
{#if explorable}<span id="{uid}-explore" class="smrt-chart__table">{t(M['ui.overview.chart.explore'])}</span>{/if}
|
|
385
|
+
{#if readout && !compact && peak && latest}
|
|
386
|
+
<p class="smrt-chart__readout">
|
|
387
|
+
<span>{t(M['ui.overview.chart.peak'], { value: fmt(peak.value), label: peak.label })}</span>
|
|
388
|
+
<span>{t(M['ui.overview.chart.latest'], { value: fmt(latest.value), label: latest.label })}</span>
|
|
389
|
+
</p>
|
|
390
|
+
{/if}
|
|
211
391
|
{@render dataTable(true)}
|
|
212
392
|
</div>
|
|
213
393
|
{:else}
|
|
@@ -228,8 +408,28 @@ const summary = $derived(
|
|
|
228
408
|
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
229
409
|
@layer smrt.theme {
|
|
230
410
|
.smrt-chart { display: grid; gap: var(--smrt-spacing-1); color: var(--smrt-color-primary); }
|
|
231
|
-
.smrt-chart__line { inline-size: 100%; block-size: 7rem; overflow: visible; }
|
|
232
|
-
.smrt-
|
|
411
|
+
.smrt-chart__line { display: block; inline-size: 100%; block-size: 7rem; overflow: visible; }
|
|
412
|
+
.smrt-chart--line[data-size='compact'] .smrt-chart__line { block-size: 2.5rem; }
|
|
413
|
+
.smrt-chart--line[data-size='tall'] .smrt-chart__line { block-size: 14rem; }
|
|
414
|
+
.smrt-chart__frame { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--smrt-spacing-2); }
|
|
415
|
+
.smrt-chart__frame--axis { grid-template-columns: auto minmax(0, 1fr); }
|
|
416
|
+
.smrt-chart__body { display: grid; gap: var(--smrt-spacing-1); min-inline-size: 0; }
|
|
417
|
+
.smrt-chart__values { display: flex; flex-direction: column; justify-content: space-between; margin: 0; block-size: 14rem; padding-block: calc(14rem * 0.1 - 0.5lh) calc(14rem * 0.1 - 0.5lh); box-sizing: border-box; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-label-small-size); font-variant-numeric: tabular-nums; text-align: end; }
|
|
418
|
+
.smrt-chart--line:not([data-size='tall']) .smrt-chart__values { block-size: 7rem; padding-block: calc(7rem * 0.1 - 0.5lh); }
|
|
419
|
+
.smrt-chart__grid { stroke: var(--smrt-color-outline-variant); stroke-width: 1; stroke-dasharray: 2 3; }
|
|
420
|
+
.smrt-chart__plot { position: relative; border-radius: var(--smrt-radius-small); outline: none; touch-action: pan-y; }
|
|
421
|
+
.smrt-chart__plot[role='slider'] { cursor: crosshair; }
|
|
422
|
+
.smrt-chart__plot:focus-visible { outline: var(--smrt-focus-ring-width) solid var(--smrt-focus-ring-color); outline-offset: var(--smrt-focus-ring-offset); }
|
|
423
|
+
.smrt-chart__guide { stroke: var(--smrt-color-outline); stroke-width: 1; }
|
|
424
|
+
.smrt-chart__dot { position: absolute; inline-size: 0.625rem; block-size: 0.625rem; border-radius: var(--smrt-radius-full); background: var(--smrt-color-primary); outline: var(--smrt-focus-ring-width) solid var(--smrt-color-surface); transform: translate(-50%, -50%); pointer-events: none; }
|
|
425
|
+
.smrt-chart__tip { position: absolute; inset-block-start: 0; display: grid; gap: 0; padding: var(--smrt-spacing-1) var(--smrt-spacing-2); border-radius: var(--smrt-radius-small); background: var(--smrt-color-inverse-surface); color: var(--smrt-color-inverse-on-surface); font-size: var(--smrt-typography-label-medium-size); line-height: 1.3; white-space: nowrap; transform: translate(-50%, calc(-100% - var(--smrt-spacing-1))); pointer-events: none; box-shadow: var(--smrt-elevation-2); font-variant-numeric: tabular-nums; }
|
|
426
|
+
.smrt-chart__tip[data-edge='start'] { transform: translate(0, calc(-100% - var(--smrt-spacing-1))); }
|
|
427
|
+
.smrt-chart__tip[data-edge='end'] { transform: translate(-100%, calc(-100% - var(--smrt-spacing-1))); }
|
|
428
|
+
|
|
429
|
+
.smrt-chart__axis { display: flex; justify-content: space-between; gap: var(--smrt-spacing-2); margin: 0; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-label-large-size, 0.75rem); }
|
|
430
|
+
.smrt-chart__axis span { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
431
|
+
.smrt-chart__axis span:last-child { text-align: end; }
|
|
432
|
+
.smrt-chart__readout { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--smrt-spacing-1) var(--smrt-spacing-3); margin: 0; color: var(--smrt-color-on-surface); font-size: var(--smrt-typography-body-small-size, 0.75rem); font-variant-numeric: tabular-nums; }
|
|
233
433
|
.smrt-chart__bars { display: grid; gap: var(--smrt-spacing-2); margin: 0; padding: 0; list-style: none; }
|
|
234
434
|
.smrt-chart__row { display: grid; grid-template-columns: minmax(4rem, 30%) minmax(0, 1fr) auto; align-items: center; gap: var(--smrt-spacing-2); font-size: var(--smrt-typography-body-medium-size, 0.875rem); color: var(--smrt-color-on-surface); }
|
|
235
435
|
.smrt-chart__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -8,6 +8,27 @@ export interface Props {
|
|
|
8
8
|
title?: string;
|
|
9
9
|
/** Locale used to format displayed numeric values. */
|
|
10
10
|
locale?: string;
|
|
11
|
+
/**
|
|
12
|
+
* How much room the chart takes. `compact` is a sparkline for a card (a
|
|
13
|
+
* line with no axis, labels or readout); `tall` gives a page's main chart
|
|
14
|
+
* room. Default `regular`.
|
|
15
|
+
*/
|
|
16
|
+
size?: 'compact' | 'regular' | 'tall';
|
|
17
|
+
/**
|
|
18
|
+
* Line: label the value axis (top, middle and bottom values, short form)
|
|
19
|
+
* with light gridlines. Default on for `tall`.
|
|
20
|
+
*/
|
|
21
|
+
valueAxis?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Line: let the pointer and the arrow keys read each value in a tooltip
|
|
24
|
+
* (a slider whose value text is the point). Default on for `tall`.
|
|
25
|
+
*/
|
|
26
|
+
explore?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Line: the peak and latest values under the chart. Default on; a page
|
|
29
|
+
* that shows its own headline figures turns it off.
|
|
30
|
+
*/
|
|
31
|
+
readout?: boolean;
|
|
11
32
|
}
|
|
12
33
|
declare const Chart: import("svelte").Component<Props, {}, "">;
|
|
13
34
|
type Chart = ReturnType<typeof Chart>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chart.svelte.d.ts","sourceRoot":"","sources":["../../src/charts/Chart.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,MAAM,WAAW,KAAK;IACpB,mEAAmE;IACnE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"Chart.svelte.d.ts","sourceRoot":"","sources":["../../src/charts/Chart.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,MAAM,WAAW,KAAK;IACpB,mEAAmE;IACnE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA+UD,QAAA,MAAM,KAAK,2CAAwC,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}
|
package/dist/charts/format.d.ts
CHANGED
|
@@ -4,10 +4,22 @@ export interface FormatWidgetValueOptions {
|
|
|
4
4
|
/** ISO 4217 code for `money` (default USD). */
|
|
5
5
|
currency?: string;
|
|
6
6
|
locale: string;
|
|
7
|
+
/** Short form for tight spaces such as an axis (`$83K`, `1.2M`). */
|
|
8
|
+
compact?: boolean;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* Format a metric value. `money` takes integer minor units, `percent` takes a
|
|
10
12
|
* fraction (0.25 is 25%). Non-finite input renders as an en dash.
|
|
11
13
|
*/
|
|
12
|
-
export declare function formatWidgetValue(value: unknown, { format, currency, locale }: FormatWidgetValueOptions): string;
|
|
14
|
+
export declare function formatWidgetValue(value: unknown, { format, currency, locale, compact, }: FormatWidgetValueOptions): string;
|
|
15
|
+
/** Time bucket granularities `formatTimeBucket` names. */
|
|
16
|
+
export type TimeBucketUnit = 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
|
|
17
|
+
/**
|
|
18
|
+
* A time bucket's start (an ISO-8601 instant, or a SQL `YYYY-MM-DD HH:MM:SS`)
|
|
19
|
+
* as a short label for its granularity, in UTC, so buckets read the same
|
|
20
|
+
* everywhere: `Jan 2025` for a month, `Q1 2025`, `2025`, `Jan 6` for a day or
|
|
21
|
+
* week, `Jan 6, 14:00` for an hour or minute. Unparseable input is returned
|
|
22
|
+
* unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatTimeBucket(value: string, unit: TimeBucketUnit, locale: string): string;
|
|
13
25
|
//# sourceMappingURL=format.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/charts/format.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/charts/format.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAYD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,EACE,MAAkB,EAClB,QAAgB,EAChB,MAAM,EACN,OAAe,GAChB,EAAE,wBAAwB,GAC1B,MAAM,CAkCR;AAED,0DAA0D;AAC1D,MAAM,MAAM,cAAc,GACtB,QAAQ,GACR,MAAM,GACN,KAAK,GACL,MAAM,GACN,OAAO,GACP,SAAS,GACT,MAAM,CAAC;AAEX;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,GACb,MAAM,CA2BR"}
|
package/dist/charts/format.js
CHANGED
|
@@ -20,10 +20,13 @@ function safeLocale(locale) {
|
|
|
20
20
|
* Format a metric value. `money` takes integer minor units, `percent` takes a
|
|
21
21
|
* fraction (0.25 is 25%). Non-finite input renders as an en dash.
|
|
22
22
|
*/
|
|
23
|
-
export function formatWidgetValue(value, { format = 'integer', currency = 'USD', locale }) {
|
|
23
|
+
export function formatWidgetValue(value, { format = 'integer', currency = 'USD', locale, compact = false, }) {
|
|
24
24
|
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
25
25
|
return EN_DASH;
|
|
26
26
|
const tag = safeLocale(locale);
|
|
27
|
+
const short = compact
|
|
28
|
+
? { notation: 'compact', maximumFractionDigits: 1 }
|
|
29
|
+
: {};
|
|
27
30
|
switch (format) {
|
|
28
31
|
case 'money': {
|
|
29
32
|
const code = /^[A-Za-z]{3}$/.test(currency)
|
|
@@ -33,6 +36,7 @@ export function formatWidgetValue(value, { format = 'integer', currency = 'USD',
|
|
|
33
36
|
return new Intl.NumberFormat(tag, {
|
|
34
37
|
style: 'currency',
|
|
35
38
|
currency: code,
|
|
39
|
+
...short,
|
|
36
40
|
}).format(value / 10 ** digits);
|
|
37
41
|
}
|
|
38
42
|
case 'percent':
|
|
@@ -41,8 +45,47 @@ export function formatWidgetValue(value, { format = 'integer', currency = 'USD',
|
|
|
41
45
|
maximumFractionDigits: 1,
|
|
42
46
|
}).format(value);
|
|
43
47
|
case 'decimal':
|
|
44
|
-
return new Intl.NumberFormat(tag, {
|
|
48
|
+
return new Intl.NumberFormat(tag, {
|
|
49
|
+
maximumFractionDigits: 2,
|
|
50
|
+
...short,
|
|
51
|
+
}).format(value);
|
|
52
|
+
default:
|
|
53
|
+
return new Intl.NumberFormat(tag, {
|
|
54
|
+
maximumFractionDigits: 0,
|
|
55
|
+
...short,
|
|
56
|
+
}).format(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A time bucket's start (an ISO-8601 instant, or a SQL `YYYY-MM-DD HH:MM:SS`)
|
|
61
|
+
* as a short label for its granularity, in UTC, so buckets read the same
|
|
62
|
+
* everywhere: `Jan 2025` for a month, `Q1 2025`, `2025`, `Jan 6` for a day or
|
|
63
|
+
* week, `Jan 6, 14:00` for an hour or minute. Unparseable input is returned
|
|
64
|
+
* unchanged.
|
|
65
|
+
*/
|
|
66
|
+
export function formatTimeBucket(value, unit, locale) {
|
|
67
|
+
const date = new Date(/^\d{4}-\d{2}-\d{2} \d/.test(value) ? `${value.replace(' ', 'T')}Z` : value);
|
|
68
|
+
if (Number.isNaN(date.getTime()))
|
|
69
|
+
return value;
|
|
70
|
+
const tag = safeLocale(locale);
|
|
71
|
+
const format = (options) => new Intl.DateTimeFormat(tag, { timeZone: 'UTC', ...options }).format(date);
|
|
72
|
+
switch (unit) {
|
|
73
|
+
case 'year':
|
|
74
|
+
return format({ year: 'numeric' });
|
|
75
|
+
case 'quarter':
|
|
76
|
+
return `Q${Math.floor(date.getUTCMonth() / 3) + 1} ${date.getUTCFullYear()}`;
|
|
77
|
+
case 'month':
|
|
78
|
+
return format({ month: 'short', year: 'numeric' });
|
|
79
|
+
case 'week':
|
|
80
|
+
case 'day':
|
|
81
|
+
return format({ month: 'short', day: 'numeric' });
|
|
45
82
|
default:
|
|
46
|
-
return
|
|
83
|
+
return format({
|
|
84
|
+
month: 'short',
|
|
85
|
+
day: 'numeric',
|
|
86
|
+
hour: '2-digit',
|
|
87
|
+
minute: '2-digit',
|
|
88
|
+
hourCycle: 'h23',
|
|
89
|
+
});
|
|
47
90
|
}
|
|
48
91
|
}
|
package/dist/charts/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { default as Chart } from './Chart.svelte';
|
|
2
|
-
export type { FormatWidgetValueOptions, WidgetValueFormat } from './format.js';
|
|
3
|
-
export { formatWidgetValue } from './format.js';
|
|
2
|
+
export type { FormatWidgetValueOptions, TimeBucketUnit, WidgetValueFormat, } from './format.js';
|
|
3
|
+
export { formatTimeBucket, formatWidgetValue } from './format.js';
|
|
4
4
|
export type { ChartData } from './types.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/charts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/charts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EACV,wBAAwB,EACxB,cAAc,EACd,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/charts/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Chart } from './Chart.svelte';
|
|
2
|
-
export { formatWidgetValue } from './format.js';
|
|
2
|
+
export { formatTimeBucket, formatWidgetValue } from './format.js';
|
package/dist/charts/strings.d.ts
CHANGED
|
@@ -6,5 +6,9 @@ export declare const M: {
|
|
|
6
6
|
readonly 'ui.overview.chart.table': "ui.overview.chart.table";
|
|
7
7
|
readonly 'ui.overview.chart.label_column': "ui.overview.chart.label_column";
|
|
8
8
|
readonly 'ui.overview.chart.value_column': "ui.overview.chart.value_column";
|
|
9
|
+
readonly 'ui.overview.chart.peak': "ui.overview.chart.peak";
|
|
10
|
+
readonly 'ui.overview.chart.latest': "ui.overview.chart.latest";
|
|
11
|
+
readonly 'ui.overview.chart.explore': "ui.overview.chart.explore";
|
|
12
|
+
readonly 'ui.overview.chart.point': "ui.overview.chart.point";
|
|
9
13
|
};
|
|
10
14
|
//# sourceMappingURL=strings.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../../src/charts/strings.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,CAAC
|
|
1
|
+
{"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../../src/charts/strings.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,CAAC;;;;;;;;;;;;CAaZ,CAAC"}
|
package/dist/charts/strings.js
CHANGED
|
@@ -7,4 +7,8 @@ export const M = defineMessages({
|
|
|
7
7
|
'ui.overview.chart.table': 'Chart data',
|
|
8
8
|
'ui.overview.chart.label_column': 'Label',
|
|
9
9
|
'ui.overview.chart.value_column': 'Value',
|
|
10
|
+
'ui.overview.chart.peak': 'Peak {value} · {label}',
|
|
11
|
+
'ui.overview.chart.latest': 'Latest {value} · {label}',
|
|
12
|
+
'ui.overview.chart.explore': 'Use the arrow keys to read each value; Home and End jump to the first and last.',
|
|
13
|
+
'ui.overview.chart.point': '{label}: {value}',
|
|
10
14
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking the page's assistant from anywhere on the page: a "New report" button,
|
|
3
|
+
* an empty state, a help hint. Pure DOM, so a widget can ask without knowing
|
|
4
|
+
* which assistant (if any) the host mounted, and the assistant can listen
|
|
5
|
+
* without importing the widget's package.
|
|
6
|
+
*
|
|
7
|
+
* The requester dispatches a bubbling, cancelable event with the prompt to put
|
|
8
|
+
* in the composer. Whoever takes it calls `preventDefault()`:
|
|
9
|
+
* - an app shell with an assistant dock opens the dock and re-dispatches the
|
|
10
|
+
* request from inside it, so a dock that mounts only when open still gets it;
|
|
11
|
+
* - the assistant dock fills its composer with the prompt and focuses it. The
|
|
12
|
+
* prompt is never sent on the person's behalf: they edit and send it.
|
|
13
|
+
*
|
|
14
|
+
* `requestAssistant` returns whether anything took it, so the requester can
|
|
15
|
+
* explain where to ask instead when no assistant is on the page.
|
|
16
|
+
*/
|
|
17
|
+
/** The bubbling DOM event that asks the page's assistant. */
|
|
18
|
+
export declare const ASSISTANT_REQUEST_EVENT = "smrt-assistant-request";
|
|
19
|
+
export interface AssistantRequestDetail {
|
|
20
|
+
/** Text to put in the assistant's composer, for the person to edit and send. */
|
|
21
|
+
prompt: string;
|
|
22
|
+
}
|
|
23
|
+
/** Ask the page's assistant from `from`; `true` when something took the request. */
|
|
24
|
+
export declare function requestAssistant(from: Element, detail: AssistantRequestDetail): boolean;
|
|
25
|
+
//# sourceMappingURL=assistant-request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-request.d.ts","sourceRoot":"","sources":["../../../src/components/chat/assistant-request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,6DAA6D;AAC7D,eAAO,MAAM,uBAAuB,2BAA2B,CAAC;AAEhE,MAAM,WAAW,sBAAsB;IACrC,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAOT"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking the page's assistant from anywhere on the page: a "New report" button,
|
|
3
|
+
* an empty state, a help hint. Pure DOM, so a widget can ask without knowing
|
|
4
|
+
* which assistant (if any) the host mounted, and the assistant can listen
|
|
5
|
+
* without importing the widget's package.
|
|
6
|
+
*
|
|
7
|
+
* The requester dispatches a bubbling, cancelable event with the prompt to put
|
|
8
|
+
* in the composer. Whoever takes it calls `preventDefault()`:
|
|
9
|
+
* - an app shell with an assistant dock opens the dock and re-dispatches the
|
|
10
|
+
* request from inside it, so a dock that mounts only when open still gets it;
|
|
11
|
+
* - the assistant dock fills its composer with the prompt and focuses it. The
|
|
12
|
+
* prompt is never sent on the person's behalf: they edit and send it.
|
|
13
|
+
*
|
|
14
|
+
* `requestAssistant` returns whether anything took it, so the requester can
|
|
15
|
+
* explain where to ask instead when no assistant is on the page.
|
|
16
|
+
*/
|
|
17
|
+
/** The bubbling DOM event that asks the page's assistant. */
|
|
18
|
+
export const ASSISTANT_REQUEST_EVENT = 'smrt-assistant-request';
|
|
19
|
+
/** Ask the page's assistant from `from`; `true` when something took the request. */
|
|
20
|
+
export function requestAssistant(from, detail) {
|
|
21
|
+
const event = new CustomEvent(ASSISTANT_REQUEST_EVENT, { bubbles: true, composed: true, cancelable: true, detail });
|
|
22
|
+
from.dispatchEvent(event);
|
|
23
|
+
return event.defaultPrevented;
|
|
24
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chat primitives — subpath `@happyvertical/smrt-ui/chat`.
|
|
3
3
|
*
|
|
4
|
-
* MessageBubble (user/agent/system), ReactionPicker, TypingIndicator
|
|
4
|
+
* MessageBubble (user/agent/system), ReactionPicker, TypingIndicator, and
|
|
5
|
+
* `requestAssistant` (ask the page's assistant from any element). Generic,
|
|
5
6
|
* tokenised, a11y-checked building blocks for chat UIs (L3 #1422).
|
|
6
7
|
*/
|
|
8
|
+
export { ASSISTANT_REQUEST_EVENT, type AssistantRequestDetail, requestAssistant, } from './assistant-request.js';
|
|
7
9
|
export { default as MessageBubble } from './MessageBubble.svelte';
|
|
8
10
|
export { default as ReactionPicker } from './ReactionPicker.svelte';
|
|
9
11
|
export { default as TypingIndicator } from './TypingIndicator.svelte';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/chat/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/chat/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,uBAAuB,EACvB,KAAK,sBAAsB,EAC3B,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chat primitives — subpath `@happyvertical/smrt-ui/chat`.
|
|
3
3
|
*
|
|
4
|
-
* MessageBubble (user/agent/system), ReactionPicker, TypingIndicator
|
|
4
|
+
* MessageBubble (user/agent/system), ReactionPicker, TypingIndicator, and
|
|
5
|
+
* `requestAssistant` (ask the page's assistant from any element). Generic,
|
|
5
6
|
* tokenised, a11y-checked building blocks for chat UIs (L3 #1422).
|
|
6
7
|
*/
|
|
8
|
+
export { ASSISTANT_REQUEST_EVENT, requestAssistant, } from './assistant-request.js';
|
|
7
9
|
export { default as MessageBubble } from './MessageBubble.svelte';
|
|
8
10
|
export { default as ReactionPicker } from './ReactionPicker.svelte';
|
|
9
11
|
export { default as TypingIndicator } from './TypingIndicator.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-ui",
|
|
3
|
-
"version": "0.57.
|
|
3
|
+
"version": "0.57.7",
|
|
4
4
|
"smrtJsdoc": "strict",
|
|
5
5
|
"description": "Domain-agnostic Svelte 5 UI runtime for SMRT: primitives, i18n client, theme system, and module UI registry",
|
|
6
6
|
"type": "module",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"access": "public"
|
|
165
165
|
},
|
|
166
166
|
"dependencies": {
|
|
167
|
-
"@happyvertical/smrt-types": "0.57.
|
|
167
|
+
"@happyvertical/smrt-types": "0.57.7",
|
|
168
168
|
"esm-env": "^1.2.2"
|
|
169
169
|
},
|
|
170
170
|
"peerDependencies": {
|