@heyramzi/ui 3.0.0 → 3.2.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/charts/dual-axis.js +38 -0
- package/dist/lib/sliding-pill.js +2 -2
- package/dist/record/measure-clamp.js +23 -0
- package/dist/shadcn/button-group/index.js +6 -0
- package/dist/shadcn/checkbox/checkbox-box.js +3 -0
- package/dist/shadcn/input-group/index.js +9 -0
- package/dist/shadcn/item/index.js +13 -0
- package/dist/shadcn/native-select/index.js +6 -0
- package/dist/shadcn/pagination/index.js +12 -0
- package/dist/slides/deck-controller.svelte.js +62 -15
- package/dist/slides/layout.js +3 -2
- package/package.json +9 -2
- package/scripts/svelte-dts.mjs +3 -0
- package/scripts/sync-shadcn.mjs +0 -5
- package/src/ai/generation-progress/GenerationProgress.svelte +14 -12
- package/src/canvas/pen/PenOverlay.svelte +1 -30
- package/src/canvas/screen/PartView.svelte +13 -8
- package/src/canvas/screen/ScreenBody.svelte +18 -11
- package/src/canvas/ui/BarTooltip.svelte +46 -0
- package/src/canvas/ui/BarTooltip.svelte.d.ts +17 -0
- package/src/canvas/ui/CanvasToolbar.svelte +170 -156
- package/src/canvas/ui/StylePanel.svelte +179 -102
- package/src/canvas/ui/canvas-ui.css +12 -76
- package/src/charts/area-chart.css +6 -0
- package/src/charts/area-chart.svelte +115 -17
- package/src/charts/area-chart.svelte.d.ts +11 -2
- package/src/charts/chart-tokens.css +4 -3
- package/src/charts/dashboard/data-table.svelte +80 -80
- package/src/charts/dashboard/hint.svelte +34 -0
- package/src/charts/dashboard/hint.svelte.d.ts +10 -0
- package/src/charts/dashboard/rating-chip.svelte +4 -3
- package/src/charts/dual-axis.test.ts +40 -0
- package/src/charts/dual-axis.ts +43 -0
- package/src/charts/legend.css +0 -8
- package/src/charts/legend.svelte +26 -30
- package/src/components/copy/copy-button.svelte +10 -2
- package/src/components/copy/copy-button.test.ts +7 -0
- package/src/components/data-table/table-skeleton.svelte +15 -7
- package/src/components/data-table/table-skeleton.svelte.d.ts +4 -0
- package/src/components/docs/on-this-page.svelte +2 -2
- package/src/components/hold-to-confirm/hold-to-confirm.svelte +2 -1
- package/src/components/list-page/filter-item.svelte +20 -7
- package/src/components/list-page/filter-popover.svelte +41 -16
- package/src/components/list-page/filter-popover.svelte.d.ts +1 -1
- package/src/components/list-page/grouped-list.svelte +41 -23
- package/src/components/picker/picker.svelte +8 -3
- package/src/components/picker/picker.svelte.d.ts +2 -1
- package/src/components/segmented/segmented-filter.svelte +19 -10
- package/src/components/segmented/segmented-track.svelte +11 -3
- package/src/components/table-view/table-view.svelte +22 -6
- package/src/components/tags-input/tags-input.svelte +94 -77
- package/src/components/toolbar/toolbar.svelte +2 -1
- package/src/components/toolbar/toolbar.test.ts +5 -4
- package/src/components/work-parts/group-head.svelte +28 -9
- package/src/components/work-parts/group-head.svelte.d.ts +5 -2
- package/src/editor/editor-toolbar.svelte +231 -193
- package/src/editor/editor.svelte +57 -39
- package/src/editor/image-toolbar.svelte +99 -37
- package/src/editor/image-toolbar.svelte.d.ts +4 -5
- package/src/editor/url-dialog.svelte +96 -0
- package/src/editor/url-dialog.svelte.d.ts +15 -0
- package/src/effects/theme-toggle/ThemeToggle.css +8 -8
- package/src/effects/theme-toggle/ThemeToggle.svelte +20 -6
- package/src/inbox/thread-inbox.svelte +9 -1
- package/src/lib/live-region.svelte +12 -4
- package/src/lib/live-region.svelte.d.ts +5 -1
- package/src/lib/sliding-pill.ts +3 -2
- package/src/record/info-list.svelte +33 -59
- package/src/record/measure-clamp.ts +31 -0
- package/src/record/record-shell.svelte +38 -39
- package/src/record/record-timeline.svelte +10 -5
- package/src/record/steps-rail.svelte +50 -36
- package/src/record/where-we-are.svelte +5 -19
- package/src/shadcn/button-group/button-group-separator.svelte +23 -0
- package/src/shadcn/button-group/button-group-separator.svelte.d.ts +13 -0
- package/src/shadcn/button-group/button-group-text.svelte +28 -0
- package/src/shadcn/button-group/button-group-text.svelte.d.ts +11 -0
- package/src/shadcn/button-group/button-group.svelte +46 -0
- package/src/shadcn/button-group/button-group.svelte.d.ts +16 -0
- package/src/shadcn/button-group/index.ts +15 -0
- package/src/shadcn/checkbox/checkbox-box.ts +4 -0
- package/src/shadcn/checkbox/checkbox.svelte +2 -4
- package/src/shadcn/input-group/index.ts +22 -0
- package/src/shadcn/input-group/input-group-addon.svelte +51 -0
- package/src/shadcn/input-group/input-group-addon.svelte.d.ts +18 -0
- package/src/shadcn/input-group/input-group-button.svelte +49 -0
- package/src/shadcn/input-group/input-group-button.svelte.d.ts +18 -0
- package/src/shadcn/input-group/input-group-input.svelte +20 -0
- package/src/shadcn/input-group/input-group-input.svelte.d.ts +11 -0
- package/src/shadcn/input-group/input-group-text.svelte +19 -0
- package/src/shadcn/input-group/input-group-text.svelte.d.ts +5 -0
- package/src/shadcn/input-group/input-group-textarea.svelte +20 -0
- package/src/shadcn/input-group/input-group-textarea.svelte.d.ts +3 -0
- package/src/shadcn/input-group/input-group.svelte +24 -0
- package/src/shadcn/input-group/input-group.svelte.d.ts +5 -0
- package/src/shadcn/item/index.ts +34 -0
- package/src/shadcn/item/item-actions.svelte +20 -0
- package/src/shadcn/item/item-actions.svelte.d.ts +5 -0
- package/src/shadcn/item/item-content.svelte +23 -0
- package/src/shadcn/item/item-content.svelte.d.ts +5 -0
- package/src/shadcn/item/item-description.svelte +23 -0
- package/src/shadcn/item/item-description.svelte.d.ts +5 -0
- package/src/shadcn/item/item-footer.svelte +20 -0
- package/src/shadcn/item/item-footer.svelte.d.ts +5 -0
- package/src/shadcn/item/item-group.svelte +21 -0
- package/src/shadcn/item/item-group.svelte.d.ts +5 -0
- package/src/shadcn/item/item-header.svelte +20 -0
- package/src/shadcn/item/item-header.svelte.d.ts +5 -0
- package/src/shadcn/item/item-media.svelte +42 -0
- package/src/shadcn/item/item-media.svelte.d.ts +17 -0
- package/src/shadcn/item/item-separator.svelte +19 -0
- package/src/shadcn/item/item-separator.svelte.d.ts +13 -0
- package/src/shadcn/item/item-title.svelte +20 -0
- package/src/shadcn/item/item-title.svelte.d.ts +5 -0
- package/src/shadcn/item/item.svelte +61 -0
- package/src/shadcn/item/item.svelte.d.ts +28 -0
- package/src/shadcn/native-select/index.ts +13 -0
- package/src/shadcn/native-select/native-select-opt-group.svelte +20 -0
- package/src/shadcn/native-select/native-select-opt-group.svelte.d.ts +5 -0
- package/src/shadcn/native-select/native-select-option.svelte +20 -0
- package/src/shadcn/native-select/native-select-option.svelte.d.ts +5 -0
- package/src/shadcn/native-select/native-select.svelte +45 -0
- package/src/shadcn/native-select/native-select.svelte.d.ts +8 -0
- package/src/shadcn/pagination/index.ts +31 -0
- package/src/shadcn/pagination/pagination-content.svelte +20 -0
- package/src/shadcn/pagination/pagination-content.svelte.d.ts +5 -0
- package/src/shadcn/pagination/pagination-ellipsis.svelte +22 -0
- package/src/shadcn/pagination/pagination-ellipsis.svelte.d.ts +5 -0
- package/src/shadcn/pagination/pagination-item.svelte +14 -0
- package/src/shadcn/pagination/pagination-item.svelte.d.ts +5 -0
- package/src/shadcn/pagination/pagination-link.svelte +41 -0
- package/src/shadcn/pagination/pagination-link.svelte.d.ts +9 -0
- package/src/shadcn/pagination/pagination-next-button.svelte +34 -0
- package/src/shadcn/pagination/pagination-next-button.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination-next.svelte +26 -0
- package/src/shadcn/pagination/pagination-next.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination-prev-button.svelte +34 -0
- package/src/shadcn/pagination/pagination-prev-button.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination-previous.svelte +30 -0
- package/src/shadcn/pagination/pagination-previous.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination.svelte +27 -0
- package/src/shadcn/pagination/pagination.svelte.d.ts +4 -0
- package/src/slides/Controls.svelte +39 -36
- package/src/slides/Deck.svelte +23 -10
- package/src/slides/Overview.svelte +10 -4
- package/src/slides/README.md +1 -1
- package/src/slides/Slide.svelte +17 -11
- package/src/slides/deck-controller.svelte.ts +62 -16
- package/src/slides/layout.ts +3 -2
- package/src/slides/theme.css +34 -5
- package/src/thread/thread.svelte +15 -8
- package/src/work/detail/task-detail.svelte +1 -1
- package/src/work/gantt/gantt-task-row.svelte +3 -6
- package/src/work/inbox/inbox-card.svelte +7 -5
- package/src/work/inbox/inbox-trace.svelte +14 -10
- package/src/work/kanban-board.svelte +40 -25
- package/src/work/kanban-hidden-rail.svelte +67 -65
- package/src/work/task-list.svelte +145 -124
- package/src/work/task-list.svelte.test.ts +5 -2
- package/src/work/time/time-tracker.svelte +35 -10
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { resolveEdgeFade } from "./edge-fade.js";
|
|
24
24
|
import Figure from "./figure.svelte";
|
|
25
25
|
import HoverFocus from "./hover-focus.svelte";
|
|
26
|
+
import { dualAxis } from "./dual-axis.js";
|
|
26
27
|
import { DOMAIN_MOTION, TOOLTIP_GAP, TOOLTIP_TOP } from "./motion.js";
|
|
27
28
|
import { valueTick } from "./primitives/chart-utils.js";
|
|
28
29
|
import Reveal from "./reveal.svelte";
|
|
@@ -44,6 +45,11 @@
|
|
|
44
45
|
key: keyof TData & string;
|
|
45
46
|
label?: string;
|
|
46
47
|
color?: string;
|
|
48
|
+
/**
|
|
49
|
+
* `"right"` plots the series on a second value axis with its own scale, for a measure in a
|
|
50
|
+
* different unit or about 10x larger than the rest. It draws as a line, without the fill.
|
|
51
|
+
*/
|
|
52
|
+
axis?: "left" | "right";
|
|
47
53
|
};
|
|
48
54
|
|
|
49
55
|
interface Props {
|
|
@@ -68,8 +74,15 @@
|
|
|
68
74
|
xTicks?: number;
|
|
69
75
|
/** A target drawn across the plot, like the deal a cost runs up to. */
|
|
70
76
|
reference?: ChartReference;
|
|
71
|
-
/**
|
|
72
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The headline while no point is hovered, like a period total. Defaults to the last point.
|
|
79
|
+
* `values`, keyed by series, shows one figure per series side by side in its colour, so it also reads as the legend.
|
|
80
|
+
*/
|
|
81
|
+
resting?: {
|
|
82
|
+
value?: number;
|
|
83
|
+
values?: Partial<Record<keyof TData & string, number>>;
|
|
84
|
+
label: string;
|
|
85
|
+
};
|
|
73
86
|
/** Smooths the drawn line by the window shown; the resting figure stays the caller's total. */
|
|
74
87
|
smoothing?: Smoothing;
|
|
75
88
|
/** The chart's accessible name. Its data range and values are read after it. */
|
|
@@ -146,11 +159,14 @@
|
|
|
146
159
|
key: s.key,
|
|
147
160
|
label: s.label ?? s.key,
|
|
148
161
|
color: s.color ?? `var(--chart-series-${(i % 6) + 1})`,
|
|
162
|
+
axis: s.axis === "right" ? ("right" as const) : ("left" as const),
|
|
149
163
|
}))
|
|
150
164
|
: yKey
|
|
151
|
-
? [{ key: yKey, label: yKey, color }]
|
|
165
|
+
? [{ key: yKey, label: yKey, color, axis: "left" as const }]
|
|
152
166
|
: [],
|
|
153
167
|
);
|
|
168
|
+
const leftSeries = $derived(series.filter((s) => s.axis === "left"));
|
|
169
|
+
const rightSeries = $derived(series.filter((s) => s.axis === "right"));
|
|
154
170
|
const smoothed = $derived(
|
|
155
171
|
smoothing
|
|
156
172
|
? smoothSeries(
|
|
@@ -167,12 +183,41 @@
|
|
|
167
183
|
const averageLabel = $derived(mode === "rolling" ? averageName(smoothed.window) : null);
|
|
168
184
|
const dataMax = $derived.by(() => {
|
|
169
185
|
if (stacked) {
|
|
170
|
-
return Math.max(
|
|
186
|
+
return Math.max(
|
|
187
|
+
0,
|
|
188
|
+
...data.map((d) => leftSeries.reduce((s, k) => s + Number(d[k.key] ?? 0), 0)),
|
|
189
|
+
);
|
|
171
190
|
}
|
|
172
|
-
return Math.max(0, ...data.flatMap((d) =>
|
|
191
|
+
return Math.max(0, ...data.flatMap((d) => leftSeries.map((s) => Number(d[s.key] ?? 0))));
|
|
173
192
|
});
|
|
174
193
|
// A target above every point would sit off the plot.
|
|
175
194
|
const yMax = $derived(Math.max(dataMax, reference?.value ?? 0));
|
|
195
|
+
// A right-axis series lives on the chart's one y scale, rescaled so its domain fills the same
|
|
196
|
+
// height; the right axis labels that height in the series' own unit.
|
|
197
|
+
const axes = $derived.by(() => {
|
|
198
|
+
if (rightSeries.length === 0) return null;
|
|
199
|
+
const rightMax = Math.max(
|
|
200
|
+
0,
|
|
201
|
+
...data.flatMap((d) => rightSeries.map((s) => Number(d[s.key] ?? 0))),
|
|
202
|
+
);
|
|
203
|
+
return dualAxis(yMax, rightMax);
|
|
204
|
+
});
|
|
205
|
+
const plotKey = (s: { key: string; axis: "left" | "right" }) =>
|
|
206
|
+
s.axis === "right" ? `__hr_right_${s.key}` : s.key;
|
|
207
|
+
const plotData = $derived.by((): TData[] => {
|
|
208
|
+
if (!axes) return data;
|
|
209
|
+
const factor = axes.left.max / axes.right.max;
|
|
210
|
+
return data.map((d) => {
|
|
211
|
+
const row: Record<string, unknown> = { ...d };
|
|
212
|
+
// A gap stays a gap: null times a factor would draw a 0.
|
|
213
|
+
for (const s of rightSeries)
|
|
214
|
+
row[plotKey(s)] = d[s.key] == null ? null : Number(d[s.key]) * factor;
|
|
215
|
+
return row as TData;
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
// An axis takes its series' colour only when one series owns it; with two on a side the colour would name one of them.
|
|
219
|
+
const leftTone = $derived(axes && leftSeries.length === 1 ? leftSeries[0].color : undefined);
|
|
220
|
+
const rightTone = $derived(rightSeries.length === 1 ? rightSeries[0].color : undefined);
|
|
176
221
|
const curve = $derived(smooth ? curveMonotoneX : undefined);
|
|
177
222
|
const fade = $derived(resolveEdgeFade(edgeFade));
|
|
178
223
|
const fadeMask = $derived(fade > 0 ? `url(#${uid}-fade)` : undefined);
|
|
@@ -206,7 +251,16 @@
|
|
|
206
251
|
<!-- WHY: leaving only resets the hover readout; the keyboard path is ChartPlot's. -->
|
|
207
252
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
208
253
|
<div class="hr-area-chart {className ?? ''}" onpointerleave={() => (active = null)}>
|
|
209
|
-
{#if resting && !active
|
|
254
|
+
{#if resting?.values && !active}
|
|
255
|
+
<Figure
|
|
256
|
+
series={series
|
|
257
|
+
.filter((s) => resting.values?.[s.key] != null)
|
|
258
|
+
.map((s) => ({ value: Number(resting.values?.[s.key]), label: s.label, color: s.color }))}
|
|
259
|
+
eyebrow={resting.label}
|
|
260
|
+
format={formatValue}
|
|
261
|
+
size="md"
|
|
262
|
+
/>
|
|
263
|
+
{:else if resting && resting.value != null && !active && series[0]}
|
|
210
264
|
<Figure
|
|
211
265
|
series={[{ value: resting.value, label: series[0].label, color: series[0].color }]}
|
|
212
266
|
eyebrow={resting.label}
|
|
@@ -226,21 +280,43 @@
|
|
|
226
280
|
<div class="empty"><p>No data</p></div>
|
|
227
281
|
{:else}
|
|
228
282
|
<Chart
|
|
229
|
-
{
|
|
283
|
+
data={plotData}
|
|
230
284
|
x={xKey}
|
|
231
|
-
y={series[0]
|
|
232
|
-
yDomain={[0, yMax || 1]}
|
|
233
|
-
yNice
|
|
234
|
-
padding={{ left: 48, bottom: 28, top: 8, right: 12 }}
|
|
285
|
+
y={series[0] ? plotKey(series[0]) : undefined}
|
|
286
|
+
yDomain={[0, axes ? axes.left.max : yMax || 1]}
|
|
287
|
+
yNice={!axes}
|
|
288
|
+
padding={{ left: 48, bottom: 28, top: 8, right: axes ? 48 : 12 }}
|
|
235
289
|
tooltipContext={{ mode: "quadtree-x" }}
|
|
236
290
|
motion={DOMAIN_MOTION}
|
|
237
291
|
bind:context
|
|
238
292
|
>
|
|
239
293
|
<Svg aria-hidden="true">
|
|
240
|
-
<
|
|
294
|
+
<g class="hr-axis-tone" style:--hr-axis-tone={leftTone}>
|
|
295
|
+
<Axis
|
|
296
|
+
placement="left"
|
|
297
|
+
format={(v) => tick(Number(v))}
|
|
298
|
+
ticks={axes ? axes.left.ticks : 4}
|
|
299
|
+
/>
|
|
300
|
+
</g>
|
|
301
|
+
{#if axes}
|
|
302
|
+
<g class="hr-axis-tone" style:--hr-axis-tone={rightTone}>
|
|
303
|
+
<Axis
|
|
304
|
+
placement="right"
|
|
305
|
+
domain={[0, axes.right.max]}
|
|
306
|
+
format={(v) => tick(Number(v))}
|
|
307
|
+
ticks={axes.right.ticks}
|
|
308
|
+
/>
|
|
309
|
+
</g>
|
|
310
|
+
{/if}
|
|
241
311
|
<Axis placement="bottom" format={(v) => formatX(v)} ticks={ticksForWidth(xTicks)} />
|
|
242
312
|
<g class="hr-area-grid">
|
|
243
|
-
<Axis
|
|
313
|
+
<Axis
|
|
314
|
+
placement="left"
|
|
315
|
+
grid
|
|
316
|
+
rule={false}
|
|
317
|
+
ticks={axes ? axes.left.ticks : 4}
|
|
318
|
+
format={() => ""}
|
|
319
|
+
/>
|
|
244
320
|
</g>
|
|
245
321
|
{#if fade > 0}
|
|
246
322
|
<EdgeFade id="{uid}-fade" amount={fade} />
|
|
@@ -261,17 +337,39 @@
|
|
|
261
337
|
<!-- WHY `y1` and not `y`: layerchart's Area has no `y` prop, so every
|
|
262
338
|
series drew the first series' shape and the gradients stacked on
|
|
263
339
|
one path. See `Area.shared.svelte.js`. -->
|
|
264
|
-
|
|
265
|
-
|
|
340
|
+
{#if s.axis === "left"}
|
|
341
|
+
<Area y1={s.key} {curve} fill="url(#{uid}-{i})" mask={fadeMask} />
|
|
342
|
+
{/if}
|
|
343
|
+
<Spline
|
|
344
|
+
y={plotKey(s)}
|
|
345
|
+
{curve}
|
|
346
|
+
style="stroke: {s.color}; fill: none"
|
|
347
|
+
stroke-width="2"
|
|
348
|
+
/>
|
|
266
349
|
{/each}
|
|
267
350
|
</Reveal>
|
|
268
351
|
{#snippet segment()}
|
|
269
352
|
{#each series as s (s.key)}
|
|
270
|
-
<Spline
|
|
353
|
+
<Spline
|
|
354
|
+
y={plotKey(s)}
|
|
355
|
+
{curve}
|
|
356
|
+
style="stroke: {s.color}; fill: none"
|
|
357
|
+
stroke-width="2"
|
|
358
|
+
/>
|
|
271
359
|
{/each}
|
|
272
360
|
{/snippet}
|
|
273
361
|
</HoverFocus>
|
|
274
|
-
|
|
362
|
+
{#if axes}
|
|
363
|
+
<!-- WHY `data`: without it Highlight places dots from the tooltip's raw values, off the rescaled right series. -->
|
|
364
|
+
<Highlight lines={{ style: `stroke: url(#${uid}-cross)` }} />
|
|
365
|
+
{#if context?.tooltip.data}
|
|
366
|
+
{#each series as s (s.key)}
|
|
367
|
+
<Highlight data={context.tooltip.data} y={plotKey(s)} points={{ fill: s.color }} />
|
|
368
|
+
{/each}
|
|
369
|
+
{/if}
|
|
370
|
+
{:else}
|
|
371
|
+
<Highlight points lines={{ style: `stroke: url(#${uid}-cross)` }} />
|
|
372
|
+
{/if}
|
|
275
373
|
</Svg>
|
|
276
374
|
<Tooltip.Root x="data" y={TOOLTIP_TOP} xOffset={TOOLTIP_GAP}>
|
|
277
375
|
{#snippet children({ data: d })}
|
|
@@ -11,6 +11,11 @@ declare function $$render<TData extends Record<string, unknown>>(): {
|
|
|
11
11
|
key: keyof TData & string;
|
|
12
12
|
label?: string;
|
|
13
13
|
color?: string;
|
|
14
|
+
/**
|
|
15
|
+
* `"right"` plots the series on a second value axis with its own scale, for a measure in a
|
|
16
|
+
* different unit or about 10x larger than the rest. It draws as a line, without the fill.
|
|
17
|
+
*/
|
|
18
|
+
axis?: "left" | "right";
|
|
14
19
|
}[];
|
|
15
20
|
color?: string;
|
|
16
21
|
height?: number;
|
|
@@ -25,9 +30,13 @@ declare function $$render<TData extends Record<string, unknown>>(): {
|
|
|
25
30
|
xTicks?: number;
|
|
26
31
|
/** A target drawn across the plot, like the deal a cost runs up to. */
|
|
27
32
|
reference?: ChartReference;
|
|
28
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The headline while no point is hovered, like a period total. Defaults to the last point.
|
|
35
|
+
* `values`, keyed by series, shows one figure per series side by side in its colour, so it also reads as the legend.
|
|
36
|
+
*/
|
|
29
37
|
resting?: {
|
|
30
|
-
value
|
|
38
|
+
value?: number;
|
|
39
|
+
values?: Partial<Record<keyof TData & string, number>>;
|
|
31
40
|
label: string;
|
|
32
41
|
};
|
|
33
42
|
/** Smooths the drawn line by the window shown; the resting figure stays the caller's total. */
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
`@heyramzi/ui/charts/dashboard` keeps its card edge without the barrel. */
|
|
5
5
|
|
|
6
6
|
@layer components {
|
|
7
|
-
/* Shared chart tokens, held at zero specificity by `:where()`. The
|
|
8
|
-
|
|
7
|
+
/* Shared chart tokens, held at zero specificity by `:where()`. The tooltip portals to
|
|
8
|
+
`body`, outside its chart, so it is listed for its series dots to match the lines. */
|
|
9
9
|
:where(
|
|
10
10
|
.hr-area-chart,
|
|
11
11
|
.hr-bar-chart,
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
.hr-ts-chart,
|
|
18
18
|
.hr-donut-chart,
|
|
19
19
|
.hr-ring,
|
|
20
|
-
.hr-gauge
|
|
20
|
+
.hr-gauge,
|
|
21
|
+
.lc-tooltip-container
|
|
21
22
|
) {
|
|
22
23
|
/* Series palette: brand, then hues round the wheel, blue and orange first for colour
|
|
23
24
|
blindness. Each is pulled 15% toward the text, clearing 3:1 on the card in both themes. */
|
|
@@ -43,9 +43,11 @@
|
|
|
43
43
|
*/
|
|
44
44
|
import { cn } from "../../lib/utils.js";
|
|
45
45
|
import RatingChip from "./rating-chip.svelte";
|
|
46
|
+
import Hint from "./hint.svelte";
|
|
46
47
|
import {
|
|
47
48
|
Pagination,
|
|
48
49
|
SortableHead,
|
|
50
|
+
TableSkeleton,
|
|
49
51
|
paginateRows,
|
|
50
52
|
totalPages,
|
|
51
53
|
type SortState,
|
|
@@ -127,6 +129,18 @@
|
|
|
127
129
|
});
|
|
128
130
|
|
|
129
131
|
const visible = $derived(pageSize ? paginateRows(sorted, page, pageSize) : sorted);
|
|
132
|
+
|
|
133
|
+
// Placeholder rows hold the 40px of a lead cell's padding, touch or not, and the rule sits
|
|
134
|
+
// outside it as before. Each cell keeps its column's padding, rule and breakpoint.
|
|
135
|
+
const loadingCell = "box-content h-10 any-pointer-coarse:h-10";
|
|
136
|
+
const loadingColumns = $derived([
|
|
137
|
+
{ width: "w-2/3", class: cn("px-4", loadingCell) },
|
|
138
|
+
...columns.map((column) => ({
|
|
139
|
+
width: "w-3/4",
|
|
140
|
+
align: column.align === "start" ? ("left" as const) : ("right" as const),
|
|
141
|
+
class: cn(columnClass(column), loadingCell),
|
|
142
|
+
})),
|
|
143
|
+
]);
|
|
130
144
|
const opens = $derived(Boolean(rowHref || onRowClick));
|
|
131
145
|
|
|
132
146
|
// Fewer rows than the page you stand on would draw an empty table.
|
|
@@ -182,96 +196,82 @@
|
|
|
182
196
|
<th
|
|
183
197
|
scope="col"
|
|
184
198
|
class={cn(column.align === "start" ? dataThClass : dataThNumClass, columnClass(column))}
|
|
185
|
-
title={column.hint}
|
|
186
199
|
>
|
|
187
|
-
{column.label}
|
|
200
|
+
<Hint text={column.hint}>{column.label}</Hint>
|
|
188
201
|
</th>
|
|
189
202
|
{/if}
|
|
190
203
|
{/each}
|
|
191
204
|
</tr>
|
|
192
205
|
</thead>
|
|
193
|
-
|
|
194
|
-
{
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
</td>
|
|
200
|
-
{#each columns as column (column.key)}
|
|
201
|
-
<td class={cn(dataTdNumClass, columnClass(column), "align-middle")}>
|
|
202
|
-
<span
|
|
203
|
-
class={cn(
|
|
204
|
-
"block h-4 w-3/4 animate-pulse rounded bg-muted",
|
|
205
|
-
column.align !== "start" && "ml-auto",
|
|
206
|
-
)}
|
|
207
|
-
></span>
|
|
208
|
-
</td>
|
|
209
|
-
{/each}
|
|
206
|
+
{#if loading}
|
|
207
|
+
<TableSkeleton rows={LOADING_ROWS} columns={loadingColumns} rowClass={dataTrClass} />
|
|
208
|
+
{:else}
|
|
209
|
+
<tbody>
|
|
210
|
+
{#if visible.length === 0 && empty}
|
|
211
|
+
<tr>
|
|
212
|
+
<td colspan={columns.length + 1}>{@render empty()}</td>
|
|
210
213
|
</tr>
|
|
211
|
-
{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<a href={rowHref(row)} class={stretch}>{@render lead(row)}</a>
|
|
222
|
-
{:else if onRowClick}
|
|
223
|
-
<button type="button" class={stretch} onclick={() => onRowClick(row)}>
|
|
224
|
-
{@render lead(row)}
|
|
225
|
-
</button>
|
|
226
|
-
{:else}
|
|
227
|
-
{@render lead(row)}
|
|
228
|
-
{/if}
|
|
229
|
-
</td>
|
|
230
|
-
{#each columns as column (column.key)}
|
|
231
|
-
<td
|
|
232
|
-
class={cn(
|
|
233
|
-
dataTdNumClass,
|
|
234
|
-
columnClass(column),
|
|
235
|
-
"align-middle whitespace-nowrap",
|
|
236
|
-
column.strong && "font-medium",
|
|
237
|
-
isMuted(column, row) && "text-muted-foreground",
|
|
238
|
-
column.negative?.(row) && "text-destructive-ink",
|
|
239
|
-
)}
|
|
240
|
-
title={column.title?.(row)}
|
|
241
|
-
>
|
|
242
|
-
{#if column.cell}
|
|
243
|
-
{@render column.cell(row)}
|
|
244
|
-
{:else if column.rating}
|
|
245
|
-
{@const rating = column.rating(row)}
|
|
246
|
-
{#if rating}
|
|
247
|
-
<RatingChip grade={rating.grade} hint={rating.hint} />
|
|
248
|
-
{:else}
|
|
249
|
-
<span class="text-muted-foreground">{emptyLabel}</span>
|
|
250
|
-
{/if}
|
|
214
|
+
{:else}
|
|
215
|
+
{#each visible as row (rowKey(row))}
|
|
216
|
+
<tr class={cn(dataTrClass, "group", opens && "relative cursor-pointer")}>
|
|
217
|
+
<td class={cn(dataTdClass, "py-3")}>
|
|
218
|
+
{#if rowHref}
|
|
219
|
+
<a href={rowHref(row)} class={stretch}>{@render lead(row)}</a>
|
|
220
|
+
{:else if onRowClick}
|
|
221
|
+
<button type="button" class={stretch} onclick={() => onRowClick(row)}>
|
|
222
|
+
{@render lead(row)}
|
|
223
|
+
</button>
|
|
251
224
|
{:else}
|
|
252
|
-
{@
|
|
253
|
-
{#if value === null}
|
|
254
|
-
<span class="text-muted-foreground">{emptyLabel}</span>
|
|
255
|
-
{:else if column.bar}
|
|
256
|
-
<span class="flex items-center justify-end gap-3">
|
|
257
|
-
<span class="h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-muted">
|
|
258
|
-
<span
|
|
259
|
-
class="block h-full origin-left rounded-full bg-primary/70"
|
|
260
|
-
style:transform="scaleX({Math.min(1, Math.max(0, column.bar(row)))})"
|
|
261
|
-
></span>
|
|
262
|
-
</span>
|
|
263
|
-
<span>{value}</span>
|
|
264
|
-
</span>
|
|
265
|
-
{:else}
|
|
266
|
-
{value}
|
|
267
|
-
{/if}
|
|
225
|
+
{@render lead(row)}
|
|
268
226
|
{/if}
|
|
269
227
|
</td>
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
228
|
+
{#each columns as column (column.key)}
|
|
229
|
+
<td
|
|
230
|
+
class={cn(
|
|
231
|
+
dataTdNumClass,
|
|
232
|
+
columnClass(column),
|
|
233
|
+
"align-middle whitespace-nowrap",
|
|
234
|
+
column.strong && "font-medium",
|
|
235
|
+
isMuted(column, row) && "text-muted-foreground",
|
|
236
|
+
column.negative?.(row) && "text-destructive-ink",
|
|
237
|
+
)}
|
|
238
|
+
>
|
|
239
|
+
<Hint text={column.title?.(row)} class="block">
|
|
240
|
+
{#if column.cell}
|
|
241
|
+
{@render column.cell(row)}
|
|
242
|
+
{:else if column.rating}
|
|
243
|
+
{@const rating = column.rating(row)}
|
|
244
|
+
{#if rating}
|
|
245
|
+
<RatingChip grade={rating.grade} hint={rating.hint} />
|
|
246
|
+
{:else}
|
|
247
|
+
<span class="text-muted-foreground">{emptyLabel}</span>
|
|
248
|
+
{/if}
|
|
249
|
+
{:else}
|
|
250
|
+
{@const value = column.value?.(row) ?? null}
|
|
251
|
+
{#if value === null}
|
|
252
|
+
<span class="text-muted-foreground">{emptyLabel}</span>
|
|
253
|
+
{:else if column.bar}
|
|
254
|
+
<span class="flex items-center justify-end gap-3">
|
|
255
|
+
<span class="h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-muted">
|
|
256
|
+
<span
|
|
257
|
+
class="block h-full origin-left rounded-full bg-primary/70"
|
|
258
|
+
style:transform="scaleX({Math.min(1, Math.max(0, column.bar(row)))})"
|
|
259
|
+
></span>
|
|
260
|
+
</span>
|
|
261
|
+
<span>{value}</span>
|
|
262
|
+
</span>
|
|
263
|
+
{:else}
|
|
264
|
+
{value}
|
|
265
|
+
{/if}
|
|
266
|
+
{/if}
|
|
267
|
+
</Hint>
|
|
268
|
+
</td>
|
|
269
|
+
{/each}
|
|
270
|
+
</tr>
|
|
271
|
+
{/each}
|
|
272
|
+
{/if}
|
|
273
|
+
</tbody>
|
|
274
|
+
{/if}
|
|
275
275
|
</table>
|
|
276
276
|
{#if pageSize && !loading}
|
|
277
277
|
<Pagination {page} {pageSize} total={sorted.length} onPageChange={(next) => (page = next)} />
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// A span whose `text` shows as a tooltip on hover and on focus. A native title never
|
|
3
|
+
// showed on focus, so the keyboard never reached it.
|
|
4
|
+
import type { Snippet } from "svelte";
|
|
5
|
+
import * as Tooltip from "../../shadcn/tooltip/index.js";
|
|
6
|
+
import { cn } from "../../lib/utils.js";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Left out, the span is plain and takes no focus. */
|
|
10
|
+
text?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { text, class: className, children }: Props = $props();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#if text}
|
|
19
|
+
<Tooltip.Root>
|
|
20
|
+
<Tooltip.Trigger>
|
|
21
|
+
{#snippet child({ props })}
|
|
22
|
+
<span
|
|
23
|
+
{...props}
|
|
24
|
+
class={cn("rounded-xs outline-none focus-visible:shadow-(--ring-focus)", className)}
|
|
25
|
+
>
|
|
26
|
+
{@render children()}
|
|
27
|
+
</span>
|
|
28
|
+
{/snippet}
|
|
29
|
+
</Tooltip.Trigger>
|
|
30
|
+
<Tooltip.Content>{text}</Tooltip.Content>
|
|
31
|
+
</Tooltip.Root>
|
|
32
|
+
{:else}
|
|
33
|
+
<span class={className}>{@render children()}</span>
|
|
34
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Left out, the span is plain and takes no focus. */
|
|
4
|
+
text?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const $$Component: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type $$Component = ReturnType<typeof $$Component>;
|
|
10
|
+
export default $$Component;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// One A to E grade on the shared tone scale, so a C means one thing everywhere.
|
|
3
3
|
import { TONE_TINT, type Tone } from "../../lib/tone.js";
|
|
4
4
|
import { cn } from "../../lib/utils.js";
|
|
5
|
+
import Hint from "./hint.svelte";
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
grade: string;
|
|
@@ -21,13 +22,13 @@
|
|
|
21
22
|
};
|
|
22
23
|
</script>
|
|
23
24
|
|
|
24
|
-
<
|
|
25
|
+
<Hint
|
|
26
|
+
text={hint}
|
|
25
27
|
class={cn(
|
|
26
28
|
"inline-flex h-6 min-w-6 items-center justify-center rounded-md px-1.5 text-xs font-semibold tabular-nums",
|
|
27
29
|
TONE_TINT[GRADE_TONE[grade] ?? "neutral"],
|
|
28
30
|
className,
|
|
29
31
|
)}
|
|
30
|
-
title={hint}
|
|
31
32
|
>
|
|
32
33
|
{grade}
|
|
33
|
-
</
|
|
34
|
+
</Hint>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
2
|
+
import { dualAxis, niceStep } from "./dual-axis.js";
|
|
3
|
+
|
|
4
|
+
describe("niceStep", () => {
|
|
5
|
+
it("rounds up to 1, 2, 2.5 or 5 times a power of ten", () => {
|
|
6
|
+
expect(niceStep(40)).toBe(50);
|
|
7
|
+
expect(niceStep(1500)).toBe(2000);
|
|
8
|
+
expect(niceStep(2100)).toBe(2500);
|
|
9
|
+
expect(niceStep(0.3)).toBeCloseTo(0.5);
|
|
10
|
+
expect(niceStep(0)).toBe(1);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("dualAxis", () => {
|
|
15
|
+
it("gives clicks and impressions the same number of grid lines", () => {
|
|
16
|
+
const { left, right } = dualAxis(160, 6000);
|
|
17
|
+
expect(left).toEqual({ max: 200, ticks: [0, 50, 100, 150, 200] });
|
|
18
|
+
expect(right).toEqual({ max: 8000, ticks: [0, 2000, 4000, 6000, 8000] });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("keeps every point inside both domains", () => {
|
|
22
|
+
for (const [l, r] of [
|
|
23
|
+
[7, 9_999],
|
|
24
|
+
[123_456, 3],
|
|
25
|
+
[1, 1],
|
|
26
|
+
[0.42, 87],
|
|
27
|
+
]) {
|
|
28
|
+
const { left, right } = dualAxis(l, r);
|
|
29
|
+
expect(left.max).toBeGreaterThanOrEqual(l);
|
|
30
|
+
expect(right.max).toBeGreaterThanOrEqual(r);
|
|
31
|
+
expect(left.ticks.length).toBe(right.ticks.length);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("draws one interval for an empty series instead of dividing by zero", () => {
|
|
36
|
+
const { left, right } = dualAxis(0, 0);
|
|
37
|
+
expect(left.ticks).toEqual([0, 1]);
|
|
38
|
+
expect(right.ticks).toEqual([0, 1]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two value axes that share one set of grid lines.
|
|
3
|
+
*
|
|
4
|
+
* A right axis with its own nice ticks lands its labels between the left axis's grid lines, so a
|
|
5
|
+
* reader can't trace either. Both axes here get the same number of intervals: the left picks a nice
|
|
6
|
+
* step for its data, and the right picks the smallest nice step that fits its data in that many.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type AxisScale = { max: number; ticks: number[] };
|
|
10
|
+
|
|
11
|
+
const NICE = [1, 2, 2.5, 5, 10];
|
|
12
|
+
|
|
13
|
+
/** The smallest 1, 2, 2.5 or 5 times a power of ten that is at least `raw`. */
|
|
14
|
+
export function niceStep(raw: number): number {
|
|
15
|
+
if (!(raw > 0) || !Number.isFinite(raw)) return 1;
|
|
16
|
+
const power = 10 ** Math.floor(Math.log10(raw));
|
|
17
|
+
// WHY the epsilon: 0.3 / 0.1 is 2.9999999999999996, which would skip 3 x 0.1 for the next step.
|
|
18
|
+
const multiple = NICE.find((m) => m * power >= raw * (1 - 1e-9)) ?? 10;
|
|
19
|
+
return multiple * power;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ticksOf(step: number, count: number): number[] {
|
|
23
|
+
// Rounded so 0.1 x 3 reads 0.3 on the axis, not 0.30000000000000004.
|
|
24
|
+
return Array.from({ length: count + 1 }, (_, i) => Number((i * step).toPrecision(12)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Left and right domains from 0, with the same tick count so both axes sit on one grid.
|
|
29
|
+
* `target` is the number of intervals wanted; the left data may round it to one more or less.
|
|
30
|
+
*/
|
|
31
|
+
export function dualAxis(
|
|
32
|
+
leftMax: number,
|
|
33
|
+
rightMax: number,
|
|
34
|
+
target = 4,
|
|
35
|
+
): { left: AxisScale; right: AxisScale } {
|
|
36
|
+
const leftStep = niceStep(Math.max(0, leftMax) / target);
|
|
37
|
+
const count = Math.max(1, Math.ceil(Math.max(0, leftMax) / leftStep - 1e-9));
|
|
38
|
+
const rightStep = niceStep(Math.max(0, rightMax) / count);
|
|
39
|
+
return {
|
|
40
|
+
left: { max: leftStep * count, ticks: ticksOf(leftStep, count) },
|
|
41
|
+
right: { max: rightStep * count, ticks: ticksOf(rightStep, count) },
|
|
42
|
+
};
|
|
43
|
+
}
|
package/src/charts/legend.css
CHANGED
|
@@ -39,13 +39,10 @@
|
|
|
39
39
|
align-items: center;
|
|
40
40
|
gap: 0.5rem 0.625rem;
|
|
41
41
|
padding: 0.375rem 0.5rem;
|
|
42
|
-
border: 0;
|
|
43
42
|
border-radius: 0.5rem;
|
|
44
43
|
background: transparent;
|
|
45
|
-
font: inherit;
|
|
46
44
|
color: var(--foreground);
|
|
47
45
|
text-align: left;
|
|
48
|
-
cursor: pointer;
|
|
49
46
|
transition:
|
|
50
47
|
background 160ms ease,
|
|
51
48
|
opacity 160ms ease;
|
|
@@ -57,11 +54,6 @@
|
|
|
57
54
|
padding: 0.25rem 0.375rem;
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
.hr-legend-item:focus-visible {
|
|
61
|
-
background: var(--muted);
|
|
62
|
-
outline: none;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
57
|
@media (hover: hover) and (pointer: fine) {
|
|
66
58
|
.hr-legend-item:hover {
|
|
67
59
|
background: var(--muted);
|