@orochibraru/ui 1.0.0 → 1.0.1

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.
Files changed (112) hide show
  1. package/README.md +84 -3
  2. package/dist/components/blocks/accent-toggle.svelte +7 -3
  3. package/dist/components/blocks/accent-toggle.svelte.d.ts +1 -1
  4. package/dist/components/blocks/bottom-nav.svelte +1 -1
  5. package/dist/components/blocks/command-palette.svelte +268 -0
  6. package/dist/components/blocks/command-palette.svelte.d.ts +37 -0
  7. package/dist/components/blocks/error-page.svelte +23 -7
  8. package/dist/components/blocks/error-page.svelte.d.ts +4 -0
  9. package/dist/components/blocks/fab.svelte +1 -1
  10. package/dist/components/blocks/install-prompt.svelte +14 -8
  11. package/dist/components/blocks/install-prompt.svelte.d.ts +2 -1
  12. package/dist/components/blocks/music-player/labelled-slider.svelte +3 -12
  13. package/dist/components/blocks/music-player/labelled-slider.svelte.d.ts +1 -2
  14. package/dist/components/blocks/music-player.svelte +13 -12
  15. package/dist/components/blocks/otp-field.svelte +77 -33
  16. package/dist/components/blocks/otp-field.svelte.d.ts +7 -0
  17. package/dist/components/blocks/password-input.svelte +56 -16
  18. package/dist/components/blocks/password-input.svelte.d.ts +3 -0
  19. package/dist/components/blocks/stepper/stepper-phone-header.svelte +93 -0
  20. package/dist/components/blocks/stepper/stepper-phone-header.svelte.d.ts +13 -0
  21. package/dist/components/blocks/stepper.svelte +175 -0
  22. package/dist/components/blocks/stepper.svelte.d.ts +30 -0
  23. package/dist/components/blocks/time-range-picker.svelte +229 -0
  24. package/dist/components/blocks/time-range-picker.svelte.d.ts +33 -0
  25. package/dist/components/blocks/time-series-chart/data-table.svelte +43 -0
  26. package/dist/components/blocks/time-series-chart/data-table.svelte.d.ts +15 -0
  27. package/dist/components/blocks/time-series-chart/legend.svelte +23 -0
  28. package/dist/components/blocks/time-series-chart/legend.svelte.d.ts +10 -0
  29. package/dist/components/blocks/time-series-chart/marks.svelte +118 -0
  30. package/dist/components/blocks/time-series-chart/marks.svelte.d.ts +29 -0
  31. package/dist/components/blocks/time-series-chart/messages.d.ts +13 -0
  32. package/dist/components/blocks/time-series-chart/messages.js +12 -0
  33. package/dist/components/blocks/time-series-chart/readout.svelte +44 -0
  34. package/dist/components/blocks/time-series-chart/readout.svelte.d.ts +15 -0
  35. package/dist/components/blocks/time-series-chart/scale.d.ts +24 -0
  36. package/dist/components/blocks/time-series-chart/scale.js +136 -0
  37. package/dist/components/blocks/time-series-chart/time.d.ts +7 -0
  38. package/dist/components/blocks/time-series-chart/time.js +96 -0
  39. package/dist/components/blocks/time-series-chart.svelte +492 -0
  40. package/dist/components/blocks/time-series-chart.svelte.d.ts +18 -0
  41. package/dist/components/blocks/top-loading-bar.svelte +1 -1
  42. package/dist/components/blocks/video-player/center-controls.svelte +7 -7
  43. package/dist/components/blocks/video-player/control-bar.svelte +41 -35
  44. package/dist/components/blocks/video-player/control-button.svelte +1 -1
  45. package/dist/components/blocks/video-player/option-menu.svelte +1 -1
  46. package/dist/components/blocks/video-player/scrub-bar.svelte +39 -12
  47. package/dist/components/blocks/video-player/scrub-bar.svelte.d.ts +2 -0
  48. package/dist/components/blocks/video-player.svelte +2 -2
  49. package/dist/components/kit/accordion.svelte +70 -0
  50. package/dist/components/kit/accordion.svelte.d.ts +37 -0
  51. package/dist/components/kit/avatar-group.svelte +84 -0
  52. package/dist/components/kit/avatar-group.svelte.d.ts +14 -0
  53. package/dist/components/kit/bar-list.svelte +104 -0
  54. package/dist/components/kit/bar-list.svelte.d.ts +22 -0
  55. package/dist/components/kit/breadcrumbs.svelte +102 -0
  56. package/dist/components/kit/breadcrumbs.svelte.d.ts +14 -0
  57. package/dist/components/kit/checkbox-list.svelte +98 -0
  58. package/dist/components/kit/checkbox-list.svelte.d.ts +15 -0
  59. package/dist/components/kit/checkbox.svelte +84 -0
  60. package/dist/components/kit/checkbox.svelte.d.ts +8 -0
  61. package/dist/components/kit/confirm-dialog.svelte +127 -0
  62. package/dist/components/kit/confirm-dialog.svelte.d.ts +19 -0
  63. package/dist/components/kit/data-table.svelte +280 -0
  64. package/dist/components/kit/data-table.svelte.d.ts +46 -0
  65. package/dist/components/kit/date-field.svelte +210 -0
  66. package/dist/components/kit/date-field.svelte.d.ts +14 -0
  67. package/dist/components/kit/field-frame.svelte +71 -0
  68. package/dist/components/kit/field-frame.svelte.d.ts +26 -0
  69. package/dist/components/kit/fieldset-frame.svelte +69 -0
  70. package/dist/components/kit/fieldset-frame.svelte.d.ts +14 -0
  71. package/dist/components/kit/file-dropzone/files.d.ts +14 -0
  72. package/dist/components/kit/file-dropzone/files.js +69 -0
  73. package/dist/components/kit/file-dropzone.svelte +253 -0
  74. package/dist/components/kit/file-dropzone.svelte.d.ts +25 -0
  75. package/dist/components/kit/form.svelte +335 -0
  76. package/dist/components/kit/form.svelte.d.ts +61 -0
  77. package/dist/components/kit/input.svelte +73 -0
  78. package/dist/components/kit/input.svelte.d.ts +9 -0
  79. package/dist/components/kit/menu.svelte +163 -0
  80. package/dist/components/kit/menu.svelte.d.ts +28 -0
  81. package/dist/components/kit/pagination.svelte +134 -0
  82. package/dist/components/kit/pagination.svelte.d.ts +17 -0
  83. package/dist/components/kit/radio-list.svelte +131 -0
  84. package/dist/components/kit/radio-list.svelte.d.ts +15 -0
  85. package/dist/components/kit/segmented-control/segment.svelte +47 -0
  86. package/dist/components/kit/segmented-control/segment.svelte.d.ts +11 -0
  87. package/dist/components/kit/segmented-control.svelte +70 -0
  88. package/dist/components/kit/segmented-control.svelte.d.ts +16 -0
  89. package/dist/components/kit/select.svelte +244 -0
  90. package/dist/components/kit/select.svelte.d.ts +26 -0
  91. package/dist/components/kit/status-badge.svelte +54 -0
  92. package/dist/components/kit/status-badge.svelte.d.ts +31 -0
  93. package/dist/components/kit/switch.svelte +84 -0
  94. package/dist/components/kit/switch.svelte.d.ts +8 -0
  95. package/dist/components/kit/tabs.svelte +86 -0
  96. package/dist/components/kit/tabs.svelte.d.ts +20 -0
  97. package/dist/components/kit/textarea.svelte +92 -0
  98. package/dist/components/kit/textarea.svelte.d.ts +11 -0
  99. package/dist/components/ui/drawer/drawer-content.svelte +1 -1
  100. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  101. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  102. package/dist/components/ui/sonner/sonner.svelte +2 -4
  103. package/dist/mcp-content.json +1 -1
  104. package/dist/mcp.js +11 -11
  105. package/dist/server/hooks.d.ts +9 -0
  106. package/dist/server/hooks.js +41 -0
  107. package/dist/server/logger.d.ts +39 -0
  108. package/dist/server/logger.js +144 -0
  109. package/dist/server/telemetry.d.ts +23 -0
  110. package/dist/server/telemetry.js +82 -0
  111. package/dist/theme.css +18 -6
  112. package/package.json +40 -1
@@ -0,0 +1,492 @@
1
+ <script lang="ts" module>
2
+ export type {
3
+ TimePoint,
4
+ TimeSeries,
5
+ } from "./time-series-chart/scale.js";
6
+ export type { TimeSeriesChartMessages } from "./time-series-chart/messages.js";
7
+
8
+ export type TimeSeriesKind = "line" | "area" | "bar";
9
+ </script>
10
+
11
+ <script lang="ts">
12
+ import DataTable from "./time-series-chart/data-table.svelte";
13
+ import Legend from "./time-series-chart/legend.svelte";
14
+ import Marks from "./time-series-chart/marks.svelte";
15
+ import {
16
+ defaultMessages,
17
+ type TimeSeriesChartMessages,
18
+ } from "./time-series-chart/messages.js";
19
+ import Readout from "./time-series-chart/readout.svelte";
20
+ import {
21
+ barPath,
22
+ columnsOf,
23
+ medianSpacing,
24
+ nearestIndex,
25
+ niceTicks,
26
+ scaleLinear,
27
+ segmentsOf,
28
+ type TimeSeries,
29
+ valueExtent,
30
+ valueMap,
31
+ } from "./time-series-chart/scale.js";
32
+ import {
33
+ formatInstant,
34
+ formatTick,
35
+ timeTicks,
36
+ } from "./time-series-chart/time.js";
37
+ import { cn } from "../../utils.js";
38
+
39
+ interface Props {
40
+ series: TimeSeries[];
41
+ title: string;
42
+ description?: string;
43
+ kind?: TimeSeriesKind;
44
+ format?: (value: number) => string;
45
+ height?: number;
46
+ messages?: Partial<TimeSeriesChartMessages>;
47
+ class?: string;
48
+ }
49
+
50
+ const {
51
+ series,
52
+ title,
53
+ description,
54
+ kind = "line",
55
+ format,
56
+ height = 240,
57
+ messages = {},
58
+ class: className,
59
+ }: Props = $props();
60
+
61
+ const id = $props.id();
62
+ const numberFormat = new Intl.NumberFormat();
63
+ const compactFormat = new Intl.NumberFormat(undefined, {
64
+ notation: "compact",
65
+ });
66
+
67
+ let width = $state(0);
68
+ let activeIndex = $state<number | null>(null);
69
+ let placement = $state<"floating" | "above">("floating");
70
+ let tableVisible = $state(false);
71
+
72
+ const text = $derived({ ...defaultMessages, ...messages });
73
+ const formatValue = $derived(
74
+ format ?? ((value: number) => numberFormat.format(value)),
75
+ );
76
+ const formatAxis = $derived(
77
+ format ?? ((value: number) => compactFormat.format(value)),
78
+ );
79
+ const colors = $derived(
80
+ series.map((_, index) =>
81
+ index < 4 ? `var(--series-${index + 1})` : "var(--series-other)",
82
+ ),
83
+ );
84
+
85
+ const columns = $derived(columnsOf(series));
86
+ const maps = $derived(series.map((entry) => valueMap(entry)));
87
+ const extent = $derived(valueExtent(series));
88
+ const spacing = $derived(medianSpacing(columns));
89
+ const first = $derived(columns[0] ?? 0);
90
+ const last = $derived(columns.at(-1) ?? 0);
91
+
92
+ const top = 10;
93
+ const bottom = 28;
94
+ const plotHeight = $derived(Math.max(40, height - top - bottom));
95
+ const yTicks = $derived(
96
+ extent
97
+ ? niceTicks(
98
+ extent[0],
99
+ extent[1],
100
+ Math.max(2, Math.floor(plotHeight / 44)),
101
+ )
102
+ : [0, 1],
103
+ );
104
+ const y = $derived(
105
+ scaleLinear([yTicks[0] ?? 0, yTicks.at(-1) ?? 1], [top + plotHeight, top]),
106
+ );
107
+ const baseline = $derived(y(0));
108
+ const left = $derived(
109
+ Math.max(...yTicks.map((tick) => formatAxis(tick).length)) * 7 + 10,
110
+ );
111
+
112
+ const ends = $derived(
113
+ series.map((entry) => {
114
+ const segment = segmentsOf(entry.points).at(-1);
115
+ const point = segment?.at(-1);
116
+ return point && point.value !== null
117
+ ? { time: point.at.getTime(), value: point.value }
118
+ : null;
119
+ }),
120
+ );
121
+ const directLabels = $derived.by(() => {
122
+ if (kind === "bar" || series.length > 4 || width < 360) {
123
+ return false;
124
+ }
125
+ const heights = ends
126
+ .filter((end) => end !== null)
127
+ .map((end) => y(end.value))
128
+ .sort((above, below) => above - below);
129
+ return heights.every(
130
+ (value, index) => index === 0 || value - (heights[index - 1] ?? 0) >= 14,
131
+ );
132
+ });
133
+ const right = $derived(
134
+ directLabels
135
+ ? Math.max(
136
+ ...ends.map((end) => (end ? formatValue(end.value).length : 0)),
137
+ ) *
138
+ 7 +
139
+ 16
140
+ : 8,
141
+ );
142
+
143
+ const plotWidth = $derived(Math.max(0, width - left - right));
144
+ const slot = $derived(plotWidth / Math.max(1, columns.length));
145
+ const xRange = $derived<[number, number]>(
146
+ kind === "bar"
147
+ ? [left + slot / 2, left + plotWidth - slot / 2]
148
+ : [left, left + plotWidth],
149
+ );
150
+ const x = $derived(scaleLinear([first, last], xRange));
151
+ const xTicks = $derived(
152
+ timeTicks(
153
+ new Date(first),
154
+ new Date(last),
155
+ Math.max(2, Math.floor(plotWidth / 84)),
156
+ ),
157
+ );
158
+
159
+ const lines = $derived(
160
+ series.map((entry, index) => {
161
+ const end = ends[index];
162
+ return {
163
+ color: colors[index] ?? "",
164
+ segments: segmentsOf(entry.points).map((segment) =>
165
+ segment.map((point) => ({
166
+ x: x(point.at.getTime()),
167
+ y: y(point.value ?? 0),
168
+ })),
169
+ ),
170
+ end: end
171
+ ? { x: x(end.time), y: y(end.value), label: formatValue(end.value) }
172
+ : null,
173
+ };
174
+ }),
175
+ );
176
+ const bars = $derived.by(() => {
177
+ if (kind !== "bar") {
178
+ return [];
179
+ }
180
+ const count = Math.max(1, series.length);
181
+ const gap = slot >= 4 ? 2 : 0;
182
+ const group = Math.min(slot - gap, count * 24 + (count - 1) * gap);
183
+ const barWidth = Math.max(0.5, (group - (count - 1) * gap) / count);
184
+ return series.map((entry, index) => ({
185
+ color: colors[index] ?? "",
186
+ bars: entry.points
187
+ .filter((point) => point.value !== null)
188
+ .map((point) => {
189
+ const column = point.at.getTime();
190
+ const barLeft = x(column) - group / 2 + index * (barWidth + gap);
191
+ return {
192
+ column,
193
+ path: barPath(barLeft, barWidth, y(point.value ?? 0), baseline),
194
+ };
195
+ }),
196
+ }));
197
+ });
198
+
199
+ const activeColumn = $derived(
200
+ activeIndex === null ? null : (columns[activeIndex] ?? null),
201
+ );
202
+ const activeRows = $derived(
203
+ series.map((entry, index) => {
204
+ const value =
205
+ activeColumn === null ? null : (maps[index]?.get(activeColumn) ?? null);
206
+ return {
207
+ name: entry.name,
208
+ color: colors[index] ?? "",
209
+ raw: value,
210
+ value: value === null ? text.noValue : formatValue(value),
211
+ };
212
+ }),
213
+ );
214
+ const activeDots = $derived(
215
+ activeColumn === null || kind === "bar"
216
+ ? []
217
+ : activeRows
218
+ .filter((row) => row.raw !== null)
219
+ .map((row) => ({
220
+ x: x(activeColumn),
221
+ y: y(row.raw ?? 0),
222
+ color: row.color,
223
+ })),
224
+ );
225
+ const activeLabel = $derived(
226
+ activeColumn === null ? "" : formatInstant(new Date(activeColumn), spacing),
227
+ );
228
+ const valueText = $derived(
229
+ activeColumn === null
230
+ ? text.keyboardHint
231
+ : `${activeLabel}: ${activeRows.map((row) => `${row.name} ${row.value}`).join(", ")}`,
232
+ );
233
+
234
+ const summary = $derived.by(() => {
235
+ if (!extent) {
236
+ return title;
237
+ }
238
+ const parts = series.map((entry, index) => {
239
+ const values = entry.points
240
+ .map((point) => point.value)
241
+ .filter((value) => value !== null);
242
+ const end = ends[index];
243
+ return values.length && end
244
+ ? text.seriesSummary(
245
+ entry.name,
246
+ formatValue(Math.min(...values)),
247
+ formatValue(Math.max(...values)),
248
+ formatValue(end.value),
249
+ )
250
+ : "";
251
+ });
252
+ return [
253
+ text.summary(
254
+ title,
255
+ formatInstant(new Date(first), spacing),
256
+ formatInstant(new Date(last), spacing),
257
+ ),
258
+ ...parts,
259
+ ]
260
+ .filter(Boolean)
261
+ .join(" ");
262
+ });
263
+ const headline = $derived.by(() => {
264
+ const values = (series[0]?.points ?? [])
265
+ .map((point) => point.value)
266
+ .filter((value) => value !== null);
267
+ if (series.length !== 1 || !values.length) {
268
+ return "";
269
+ }
270
+ const average =
271
+ values.reduce((total, value) => total + value, 0) / values.length;
272
+ return `${text.peak} ${formatValue(Math.max(...values))} · ${text.average} ${formatValue(Math.round(average * 10) / 10)}`;
273
+ });
274
+ const tableRows = $derived(
275
+ columns.map((time) => ({
276
+ time,
277
+ date: formatInstant(new Date(time), spacing),
278
+ values: maps.map((map) => {
279
+ const value = map.get(time) ?? null;
280
+ return value === null ? text.noValue : formatValue(value);
281
+ }),
282
+ })),
283
+ );
284
+
285
+ function indexAt(event: PointerEvent): number {
286
+ const target = event.currentTarget as HTMLElement;
287
+ const offset = event.clientX - target.getBoundingClientRect().left + left;
288
+ const [rangeStart, rangeEnd] = xRange;
289
+ const ratio =
290
+ rangeEnd === rangeStart
291
+ ? 0
292
+ : (offset - rangeStart) / (rangeEnd - rangeStart);
293
+ return nearestIndex(columns, first + ratio * (last - first));
294
+ }
295
+
296
+ function onpointer(event: PointerEvent) {
297
+ placement = event.pointerType === "touch" ? "above" : "floating";
298
+ activeIndex = indexAt(event);
299
+ }
300
+
301
+ function onkeydown(event: KeyboardEvent) {
302
+ const current = activeIndex ?? columns.length - 1;
303
+ const next = {
304
+ ArrowLeft: current - 1,
305
+ ArrowDown: current - 1,
306
+ ArrowRight: current + 1,
307
+ ArrowUp: current + 1,
308
+ Home: 0,
309
+ End: columns.length - 1,
310
+ }[event.key];
311
+ if (next === undefined) {
312
+ return;
313
+ }
314
+ event.preventDefault();
315
+ placement = "floating";
316
+ activeIndex = Math.min(columns.length - 1, Math.max(0, next));
317
+ }
318
+
319
+ function onfocus() {
320
+ if (activeIndex === null) {
321
+ placement = "floating";
322
+ activeIndex = columns.length - 1;
323
+ }
324
+ }
325
+ </script>
326
+
327
+ <figure class={cn("time-series-chart w-full min-w-0", className)}>
328
+ <figcaption
329
+ class="mb-3 flex flex-wrap items-baseline justify-between gap-x-4"
330
+ >
331
+ <span class="grid gap-0.5">
332
+ <span class="text-sm font-medium">{title}</span>
333
+ {#if description}
334
+ <span class="text-muted-foreground text-sm">{description}</span>
335
+ {/if}
336
+ </span>
337
+ {#if headline}
338
+ <span class="text-muted-foreground text-xs tabular">{headline}</span>
339
+ {/if}
340
+ </figcaption>
341
+ {#if !extent}
342
+ <div
343
+ class="text-muted-foreground flex items-center justify-center rounded-xl border border-dashed text-sm"
344
+ style:height="{height}px"
345
+ >
346
+ {text.empty}
347
+ </div>
348
+ {:else}
349
+ <div class="relative" style:height="{height}px" bind:clientWidth={width}>
350
+ {#if width > 0}
351
+ <svg
352
+ {width}
353
+ {height}
354
+ viewBox="0 0 {width} {height}"
355
+ role="img"
356
+ aria-label={summary}
357
+ class="block overflow-visible"
358
+ >
359
+ {#each yTicks as tick (tick)}
360
+ {@const level = Math.round(y(tick)) + 0.5}
361
+ <line
362
+ x1={left}
363
+ x2={left + plotWidth}
364
+ y1={level}
365
+ y2={level}
366
+ class={tick === 0
367
+ ? "stroke-muted-foreground/40"
368
+ : "stroke-border"}
369
+ />
370
+ <text
371
+ x={left - 8}
372
+ y={level}
373
+ text-anchor="end"
374
+ dominant-baseline="middle"
375
+ class="fill-muted-foreground text-[11px] tabular"
376
+ >{formatAxis(tick)}</text
377
+ >
378
+ {/each}
379
+ {#each xTicks.ticks as tick (tick.getTime())}
380
+ {@const position = x(tick.getTime())}
381
+ <text
382
+ x={position}
383
+ y={top + plotHeight + 18}
384
+ text-anchor={position - left < 24
385
+ ? "start"
386
+ : left + plotWidth - position < 24
387
+ ? "end"
388
+ : "middle"}
389
+ class="fill-muted-foreground text-[11px] tabular"
390
+ >{formatTick(tick, xTicks.unit)}</text
391
+ >
392
+ {/each}
393
+ {#if activeColumn !== null && kind !== "bar"}
394
+ {@const position = Math.round(x(activeColumn)) + 0.5}
395
+ <line
396
+ x1={position}
397
+ x2={position}
398
+ y1={top}
399
+ y2={top + plotHeight}
400
+ class="stroke-muted-foreground/60"
401
+ />
402
+ {/if}
403
+ <Marks
404
+ {kind}
405
+ {lines}
406
+ {bars}
407
+ {baseline}
408
+ {activeColumn}
409
+ {activeDots}
410
+ {directLabels}
411
+ />
412
+ </svg>
413
+ <div
414
+ role="slider"
415
+ tabindex="0"
416
+ aria-label={title}
417
+ aria-valuemin={0}
418
+ aria-valuemax={Math.max(0, columns.length - 1)}
419
+ aria-valuenow={activeIndex ?? Math.max(0, columns.length - 1)}
420
+ aria-valuetext={valueText}
421
+ class="focus-visible:ring-ring/50 absolute cursor-crosshair touch-pan-y rounded-md outline-none select-none focus-visible:ring-3"
422
+ style:left="{left}px"
423
+ style:top="{top}px"
424
+ style:width="{plotWidth}px"
425
+ style:height="{plotHeight}px"
426
+ onpointerdown={onpointer}
427
+ onpointermove={onpointer}
428
+ onpointerleave={() => (activeIndex = null)}
429
+ onpointercancel={() => (activeIndex = null)}
430
+ {onkeydown}
431
+ {onfocus}
432
+ onblur={() => (activeIndex = null)}
433
+ ></div>
434
+ {#if activeColumn !== null}
435
+ <Readout
436
+ label={activeLabel}
437
+ rows={activeRows}
438
+ {placement}
439
+ x={x(activeColumn)}
440
+ {width}
441
+ />
442
+ {/if}
443
+ {/if}
444
+ </div>
445
+ <div class="mt-3 flex flex-wrap items-center justify-between gap-2">
446
+ {#if series.length > 1}
447
+ <Legend
448
+ entries={series.map((entry, index) => ({
449
+ name: entry.name,
450
+ color: colors[index] ?? "",
451
+ }))}
452
+ {kind}
453
+ />
454
+ {:else}
455
+ <span></span>
456
+ {/if}
457
+ <button
458
+ type="button"
459
+ aria-expanded={tableVisible}
460
+ aria-controls="{id}-table"
461
+ class="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 -mr-2 h-11 rounded-md px-2 text-xs outline-none focus-visible:ring-3 md:h-8"
462
+ onclick={() => (tableVisible = !tableVisible)}
463
+ >{tableVisible ? text.hideTable : text.showTable}</button
464
+ >
465
+ </div>
466
+ <DataTable
467
+ id="{id}-table"
468
+ caption={title}
469
+ dateHeading={text.date}
470
+ names={series.map((entry) => entry.name)}
471
+ rows={tableRows}
472
+ visible={tableVisible}
473
+ />
474
+ {/if}
475
+ </figure>
476
+
477
+ <style>
478
+ .time-series-chart {
479
+ --series-1: oklch(0.58 0.16 var(--hue));
480
+ --series-2: oklch(0.75 0.16 calc(var(--hue) + 255));
481
+ --series-3: oklch(0.5 0.16 calc(var(--hue) + 75));
482
+ --series-4: oklch(0.69 0.16 calc(var(--hue) + 135));
483
+ --series-other: var(--muted-foreground);
484
+ }
485
+
486
+ :global(.dark) .time-series-chart {
487
+ --series-1: oklch(0.66 0.16 var(--hue));
488
+ --series-2: oklch(0.66 0.16 calc(var(--hue) + 255));
489
+ --series-3: oklch(0.5 0.16 calc(var(--hue) + 75));
490
+ --series-4: oklch(0.66 0.16 calc(var(--hue) + 135));
491
+ }
492
+ </style>
@@ -0,0 +1,18 @@
1
+ export type { TimePoint, TimeSeries, } from "./time-series-chart/scale.js";
2
+ export type { TimeSeriesChartMessages } from "./time-series-chart/messages.js";
3
+ export type TimeSeriesKind = "line" | "area" | "bar";
4
+ import { type TimeSeriesChartMessages } from "./time-series-chart/messages.js";
5
+ import { type TimeSeries } from "./time-series-chart/scale.js";
6
+ interface Props {
7
+ series: TimeSeries[];
8
+ title: string;
9
+ description?: string;
10
+ kind?: TimeSeriesKind;
11
+ format?: (value: number) => string;
12
+ height?: number;
13
+ messages?: Partial<TimeSeriesChartMessages>;
14
+ class?: string;
15
+ }
16
+ declare const TimeSeriesChart: import("svelte").Component<Props, {}, "">;
17
+ type TimeSeriesChart = ReturnType<typeof TimeSeriesChart>;
18
+ export default TimeSeriesChart;
@@ -38,7 +38,7 @@
38
38
  {#if visible}
39
39
  <div
40
40
  class="pointer-events-none fixed inset-x-0 top-0 z-60 h-0.5"
41
- style="top: env(safe-area-inset-top)"
41
+ style="top: var(--safe-top)"
42
42
  role="progressbar"
43
43
  aria-label={label}
44
44
  aria-valuemin={0}
@@ -17,7 +17,7 @@
17
17
  const { player, paused, text }: Props = $props();
18
18
 
19
19
  const side =
20
- "bg-chrome text-foreground ring-border pointer-events-auto flex size-11 items-center justify-center rounded-full ring-1 backdrop-blur-xl transition-transform active:scale-90 sm:size-14";
20
+ "pointer-events-auto flex size-14 items-center justify-center rounded-full bg-black/25 text-white ring-1 ring-white/10 backdrop-blur-md transition hover:scale-105 hover:bg-black/45 active:scale-90 [&_svg]:size-7";
21
21
  </script>
22
22
 
23
23
  <div class="flex items-center justify-center gap-8 sm:gap-12">
@@ -27,20 +27,20 @@
27
27
  class={side}
28
28
  onclick={() => player.seek(-SEEK_SECONDS)}
29
29
  >
30
- <RotateCcwIcon class="size-5 sm:size-6" />
30
+ <RotateCcwIcon />
31
31
  </button>
32
32
  <button
33
33
  type="button"
34
34
  aria-label={player.ended ? text.replay : paused ? text.play : text.pause}
35
- class="bg-primary text-primary-foreground glow pointer-events-auto flex size-14 items-center justify-center rounded-full transition-transform active:scale-90 sm:size-18"
35
+ class="pointer-events-auto flex size-16 items-center justify-center rounded-full bg-black/40 text-white shadow-lg ring-1 ring-white/20 backdrop-blur-md transition hover:scale-105 hover:bg-black/55 active:scale-90 sm:size-18 [&_svg]:size-11 sm:[&_svg]:size-12"
36
36
  onclick={player.togglePlay}
37
37
  >
38
38
  {#if player.ended}
39
- <RotateCcwIcon class="size-7 sm:size-8" />
39
+ <RotateCcwIcon />
40
40
  {:else if paused}
41
- <PlayIcon class="size-7 fill-current sm:size-8" />
41
+ <PlayIcon class="fill-current" />
42
42
  {:else}
43
- <PauseIcon class="size-7 fill-current sm:size-8" />
43
+ <PauseIcon class="fill-current" />
44
44
  {/if}
45
45
  </button>
46
46
  <button
@@ -49,6 +49,6 @@
49
49
  class={side}
50
50
  onclick={() => player.seek(SEEK_SECONDS)}
51
51
  >
52
- <RotateCwIcon class="size-5 sm:size-6" />
52
+ <RotateCwIcon />
53
53
  </button>
54
54
  </div>
@@ -58,10 +58,12 @@
58
58
  </script>
59
59
 
60
60
  <div
61
- class="glass pointer-events-auto mx-2 mb-2 rounded-2xl px-3 pt-1.5 pb-1 sm:mx-3 sm:mb-3"
61
+ class="pointer-events-auto mx-3 mb-3 flex flex-col gap-2 rounded-3xl bg-linear-to-b from-white/12 to-black/55 px-4 pt-3.5 pb-2.5 text-white shadow-[inset_0_1px_0_rgb(255_255_255/0.15),0_24px_60px_-12px_rgb(0_0_0/0.7)] ring-1 ring-white/10 backdrop-blur-2xl backdrop-saturate-150 sm:mx-auto sm:mb-4 sm:w-[min(64rem,calc(100%-3rem))] sm:px-5"
62
62
  >
63
- <div class="flex items-center gap-3 text-xs font-medium tabular">
64
- <span class="w-11 shrink-0">{formatTime(currentTime)}</span>
63
+ <div
64
+ class="flex items-center gap-3 text-xs font-medium text-white/80 tabular"
65
+ >
66
+ <span class="w-12 shrink-0">{formatTime(currentTime)}</span>
65
67
  <div class="min-w-0 flex-1">
66
68
  <ScrubBar
67
69
  {currentTime}
@@ -72,54 +74,58 @@
72
74
  onseek={player.seekTo}
73
75
  />
74
76
  </div>
75
- <span class="text-muted-foreground w-11 shrink-0 text-right"
77
+ <span class="w-12 shrink-0 text-right text-white/55"
76
78
  >-{formatTime(Math.max(0, player.duration - currentTime))}</span
77
79
  >
78
80
  </div>
79
- <div class="flex items-center gap-0.5">
81
+ <div class="flex items-center gap-2 sm:gap-3">
80
82
  <ControlButton
81
83
  label={playLabel}
82
84
  shortcut="K"
83
85
  {portalTarget}
84
86
  onclick={player.togglePlay}
85
- class="bg-primary text-primary-foreground hover:bg-primary/85 hover:text-primary-foreground dark:hover:bg-primary/85 glow"
87
+ class="size-10 bg-white text-black shadow-[0_0_20px_rgb(255_255_255/0.25)] transition-transform hover:scale-105 hover:bg-white/85 hover:text-black md:size-10 dark:hover:bg-white/85"
86
88
  >
87
89
  {#if player.ended}
88
90
  <RotateCcwIcon />
89
91
  {:else if paused}
90
- <PlayIcon class="size-5 fill-current" />
92
+ <PlayIcon class="fill-current" />
91
93
  {:else}
92
- <PauseIcon class="size-5 fill-current" />
94
+ <PauseIcon class="fill-current" />
93
95
  {/if}
94
96
  </ControlButton>
95
- <ControlButton
96
- label={silent ? text.unmute : text.mute}
97
- shortcut="M"
98
- {portalTarget}
99
- onclick={player.toggleMute}
97
+ <div class="flex items-center gap-1.5">
98
+ <ControlButton
99
+ label={silent ? text.unmute : text.mute}
100
+ shortcut="M"
101
+ {portalTarget}
102
+ onclick={player.toggleMute}
103
+ >
104
+ {#if silent}
105
+ <VolumeXIcon />
106
+ {:else if player.volume < 0.5}
107
+ <Volume1Icon />
108
+ {:else}
109
+ <Volume2Icon />
110
+ {/if}
111
+ </ControlButton>
112
+ <input
113
+ type="range"
114
+ min="0"
115
+ max="1"
116
+ step="0.05"
117
+ value={player.muted ? 0 : player.volume}
118
+ aria-label={text.volume}
119
+ oninput={(event) => {
120
+ player.volume = Number(event.currentTarget.value);
121
+ player.muted = player.volume === 0;
122
+ }}
123
+ class="pointer-fine:block hidden h-1 w-20 cursor-pointer appearance-none rounded-full bg-white/20 accent-white [&::-moz-range-thumb]:size-3 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-white [&::-webkit-slider-thumb]:size-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white"
124
+ />
125
+ </div>
126
+ <div
127
+ class="ml-auto flex items-center gap-0.5 rounded-full bg-white/5 p-1 ring-1 ring-white/10 sm:gap-1"
100
128
  >
101
- {#if silent}
102
- <VolumeXIcon />
103
- {:else if player.volume < 0.5}
104
- <Volume1Icon />
105
- {:else}
106
- <Volume2Icon />
107
- {/if}
108
- </ControlButton>
109
- <input
110
- type="range"
111
- min="0"
112
- max="1"
113
- step="0.05"
114
- value={player.muted ? 0 : player.volume}
115
- aria-label={text.volume}
116
- oninput={(event) => {
117
- player.volume = Number(event.currentTarget.value);
118
- player.muted = player.volume === 0;
119
- }}
120
- class="accent-primary pointer-fine:block hidden h-1 w-20 cursor-pointer"
121
- />
122
- <div class="ml-auto flex items-center gap-0.5">
123
129
  {#if subtitles.length > 0}
124
130
  <OptionMenu
125
131
  label={text.subtitles}
@@ -37,7 +37,7 @@
37
37
  aria-pressed={pressed}
38
38
  {onclick}
39
39
  class={cn(
40
- "size-11 rounded-full md:size-9 [&_svg:not([class*='size-'])]:size-5",
40
+ "size-11 rounded-full hover:bg-white/15 hover:text-white md:size-9 dark:hover:bg-white/15 [&_svg:not([class*='size-'])]:size-5",
41
41
  className,
42
42
  )}
43
43
  >
@@ -43,7 +43,7 @@
43
43
  size="icon"
44
44
  aria-label={label}
45
45
  class={cn(
46
- "size-11 rounded-full md:size-9 [&_svg]:size-5",
46
+ "size-11 rounded-full hover:bg-white/15 hover:text-white md:size-9 dark:hover:bg-white/15 [&_svg:not([class*='size-'])]:size-5",
47
47
  active && "text-primary",
48
48
  )}
49
49
  >