@sentropic/design-system-svelte 0.34.69 → 0.34.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,618 @@
1
+ <script module lang="ts">
2
+ // TimeRangePicker — value contract. Kept identical to timeRange.ts's types
3
+ // (re-exported here so consumers can `import type { TimeRange } from
4
+ // "@sentropic/design-system-svelte"` from the component's own module block,
5
+ // matching the Svelte convention used across the library).
6
+ export type TimeRangeMode = "relative" | "absolute";
7
+ export type TimeRange = {
8
+ mode: TimeRangeMode;
9
+ relative?: string;
10
+ from: number;
11
+ to: number;
12
+ };
13
+ export type TimeRangePreset =
14
+ | string
15
+ | { token: string; label?: string; durationMs?: number };
16
+ </script>
17
+
18
+ <script lang="ts">
19
+ // Composes EXISTING DS primitives — no new low-level control is introduced.
20
+ // Trigger is bespoke (there is no generic "field trigger button" primitive);
21
+ // everything else (popover placement/outside-click/Escape, tabs, list,
22
+ // calendar, time-of-day, text input, buttons) is delegated. MenuPopover does
23
+ // NOT trap focus or return it to the trigger on close (see its own docs), so
24
+ // both are added here (mirrors Modal.svelte's trapFocus pattern).
25
+ import { tick, untrack } from "svelte";
26
+ import { Clock, ChevronDown } from "@lucide/svelte";
27
+ import MenuPopover from "./MenuPopover.svelte";
28
+ import ContentSwitcher from "./ContentSwitcher.svelte";
29
+ import SelectableList from "./SelectableList.svelte";
30
+ import SelectableRow from "./SelectableRow.svelte";
31
+ import Calendar from "./Calendar.svelte";
32
+ import type { CalendarValue } from "./Calendar.svelte";
33
+ import TimePicker from "./TimePicker.svelte";
34
+ import Input from "./Input.svelte";
35
+ import Button from "./Button.svelte";
36
+ import {
37
+ DEFAULT_TIME_RANGE_PRESETS,
38
+ parsePresetMs,
39
+ resolveRelative,
40
+ splitAbsolute,
41
+ composeAbsolute,
42
+ formatTriggerLabel as defaultFormatTriggerLabel,
43
+ formatPresetLabel as defaultFormatPresetLabel
44
+ } from "./timeRange";
45
+
46
+ type TimeRangePickerProps = {
47
+ value?: TimeRange;
48
+ defaultValue?: TimeRange;
49
+ onChange?: (value: TimeRange) => void;
50
+ presets?: TimeRangePreset[];
51
+ min?: number;
52
+ max?: number;
53
+ locale?: string;
54
+ timeFormat?: "24" | "12";
55
+ timeStep?: number;
56
+ calendarMonths?: 1 | 2;
57
+ disabled?: boolean;
58
+ label?: string;
59
+ placement?: "bottom-start" | "bottom-end" | "top-start" | "top-end";
60
+ align?: "start" | "end" | "center";
61
+ size?: "sm" | "md" | "lg";
62
+ class?: string;
63
+ formatRange?: (value: TimeRange, locale: string) => string;
64
+ formatPresetLabel?: (token: string, locale: string) => string;
65
+ };
66
+
67
+ let {
68
+ value = $bindable(),
69
+ defaultValue,
70
+ onChange,
71
+ presets = DEFAULT_TIME_RANGE_PRESETS,
72
+ min,
73
+ max,
74
+ locale = "fr-FR",
75
+ timeFormat = "24",
76
+ timeStep = 15,
77
+ calendarMonths = 2,
78
+ disabled = false,
79
+ label,
80
+ placement = "bottom-start",
81
+ align,
82
+ size = "md",
83
+ class: className,
84
+ formatRange,
85
+ formatPresetLabel: formatPresetLabelProp
86
+ }: TimeRangePickerProps = $props();
87
+
88
+ const isFr = $derived((locale ?? "fr-FR").toLowerCase().startsWith("fr"));
89
+
90
+ function computeDefaultValue(): TimeRange {
91
+ if (defaultValue) return defaultValue;
92
+ const now = Date.now();
93
+ const resolved = resolveRelative("30m", now, { min, max });
94
+ return resolved ?? { mode: "relative", relative: "30m", from: now - 30 * 60_000, to: now };
95
+ }
96
+
97
+ // Controlled/uncontrolled: mirrors DatePicker.svelte — `value` is the single
98
+ // source of truth; when the consumer never sets it, seed it once so the rest
99
+ // of the component can just read `value` directly.
100
+ $effect.pre(() => {
101
+ if (value !== undefined) return;
102
+ value = computeDefaultValue();
103
+ });
104
+
105
+ // Guards the (unreachable in practice, but type-safe) first-paint window
106
+ // before the $effect.pre above has committed.
107
+ const current = $derived<TimeRange>(value ?? computeDefaultValue());
108
+
109
+ const triggerLabel = $derived(
110
+ formatRange ? formatRange(current, locale) : defaultFormatTriggerLabel(current, locale)
111
+ );
112
+
113
+ function resolvePresetLabel(token: string): string {
114
+ return formatPresetLabelProp
115
+ ? formatPresetLabelProp(token, locale)
116
+ : defaultFormatPresetLabel(token, locale);
117
+ }
118
+
119
+ /** Resolves ONE preset entry (string or {token,durationMs}) against `now`. */
120
+ function resolvePresetEntry(preset: TimeRangePreset, now: number): TimeRange | null {
121
+ const token = typeof preset === "string" ? preset : preset.token;
122
+ const durationMs = typeof preset === "string" ? parsePresetMs(token) : (preset.durationMs ?? parsePresetMs(token));
123
+ if (durationMs == null) return null;
124
+ let to = now;
125
+ let from = now - durationMs;
126
+ if (max != null) to = Math.min(to, max);
127
+ if (min != null) from = Math.max(from, min);
128
+ if (from > to) return null;
129
+ return { mode: "relative", relative: token, from, to };
130
+ }
131
+
132
+ type NormalizedPreset = { token: string; label: string; disabled: boolean };
133
+
134
+ const resolvedPresets = $derived.by<NormalizedPreset[]>(() => {
135
+ const now = Date.now();
136
+ return presets.map((p) => {
137
+ const token = typeof p === "string" ? p : p.token;
138
+ const customLabel = typeof p === "string" ? undefined : p.label;
139
+ const label = customLabel ?? resolvePresetLabel(token);
140
+ const resolved = resolvePresetEntry(p, now);
141
+ return { token, label, disabled: resolved === null };
142
+ });
143
+ });
144
+
145
+ const selectedPresetValue = $derived<string | null>(
146
+ current.mode === "relative" ? (current.relative ?? null) : null
147
+ );
148
+
149
+ function commit(next: TimeRange) {
150
+ value = next;
151
+ onChange?.(next);
152
+ }
153
+
154
+ function onPresetSelect(next: string | string[] | null) {
155
+ // SelectableList is single-select here, so `next` is always a string or
156
+ // null in practice; the wider union is only SelectableList's shared
157
+ // onchange contract (it also serves `multiple` lists).
158
+ const nextToken = Array.isArray(next) ? (next[0] ?? null) : next;
159
+ // SelectableList (single-select) toggles OFF when re-activating the row
160
+ // that is already selected, emitting null. Re-clicking the currently
161
+ // active preset is exactly that case here (we seed `value` as the
162
+ // controlled selection), and it must still resolve+close (refresh "now")
163
+ // rather than no-op — recover the token from the current value.
164
+ const token = nextToken ?? (current.mode === "relative" ? current.relative : undefined);
165
+ if (!token) return;
166
+ const preset = presets.find((p) => (typeof p === "string" ? p : p.token) === token) ?? token;
167
+ const resolved = resolvePresetEntry(preset, Date.now());
168
+ if (!resolved) return;
169
+ commit(resolved);
170
+ panelOpen = false;
171
+ }
172
+
173
+ // --- Panel open state + focus management ----------------------------------
174
+ let panelOpen = $state(false);
175
+ let triggerEl = $state<HTMLButtonElement | null>(null);
176
+ let panelWrapperEl = $state<HTMLDivElement | null>(null);
177
+ let activeTab = $state<TimeRangeMode>("relative");
178
+
179
+ function monthOf(iso: string, offsetMonths: number): string {
180
+ const [y, m] = iso.split("-").map(Number);
181
+ const d = new Date(y, (m - 1) + offsetMonths, 1);
182
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
183
+ }
184
+
185
+ let leftMonth = $state<string>("");
186
+ let rightMonth = $state<string>("");
187
+
188
+ type Draft = { fromDate: string; fromTime: string; toDate: string; toTime: string; fromText: string; toText: string };
189
+
190
+ let draft = $state<Draft>({ fromDate: "", fromTime: "", toDate: "", toTime: "", fromText: "", toText: "" });
191
+
192
+ function seedDraft() {
193
+ const split = splitAbsolute(current.from, current.to);
194
+ draft = {
195
+ fromDate: split.fromDate,
196
+ fromTime: split.fromTime,
197
+ toDate: split.toDate,
198
+ toTime: split.toTime,
199
+ fromText: `${split.fromDate} ${split.fromTime}`,
200
+ toText: `${split.toDate} ${split.toTime}`
201
+ };
202
+ leftMonth = monthOf(split.fromDate, 0);
203
+ rightMonth = monthOf(split.fromDate, 1);
204
+ }
205
+
206
+ let previousFocus: HTMLElement | null = null;
207
+
208
+ const focusableSelector = [
209
+ "a[href]",
210
+ "button:not([disabled])",
211
+ "input:not([disabled])",
212
+ "select:not([disabled])",
213
+ "textarea:not([disabled])",
214
+ "[tabindex]:not([tabindex='-1'])"
215
+ ].join(",");
216
+
217
+ function focusFirstInPanel() {
218
+ if (!panelWrapperEl) return;
219
+ const focusable = panelWrapperEl.querySelector<HTMLElement>(focusableSelector);
220
+ (focusable ?? panelWrapperEl).focus();
221
+ }
222
+
223
+ function trapFocus(event: KeyboardEvent) {
224
+ if (!panelWrapperEl || event.key !== "Tab") return;
225
+ const focusable = Array.from(panelWrapperEl.querySelectorAll<HTMLElement>(focusableSelector));
226
+ if (focusable.length === 0) return;
227
+ const first = focusable[0];
228
+ const last = focusable.at(-1);
229
+ const active = document.activeElement;
230
+ if (!panelWrapperEl.contains(active)) {
231
+ event.preventDefault();
232
+ first.focus();
233
+ return;
234
+ }
235
+ if (event.shiftKey && active === first) {
236
+ event.preventDefault();
237
+ last?.focus();
238
+ } else if (!event.shiftKey && active === last) {
239
+ event.preventDefault();
240
+ first.focus();
241
+ }
242
+ }
243
+
244
+ // Open transition: seed the tab + draft from the current value, capture the
245
+ // element to restore focus to, then move focus into the panel. Close
246
+ // transition: restore focus to whatever had it before opening (the trigger,
247
+ // in the overwhelming majority of cases). Reads other than `panelOpen` are
248
+ // untracked so a controlled `value` change while the panel stays open does
249
+ // NOT clobber in-progress edits.
250
+ $effect(() => {
251
+ if (panelOpen) {
252
+ untrack(() => {
253
+ activeTab = current.mode;
254
+ seedDraft();
255
+ previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
256
+ });
257
+ tick().then(focusFirstInPanel);
258
+ } else {
259
+ const el = previousFocus;
260
+ previousFocus = null;
261
+ if (el) tick().then(() => el.focus());
262
+ }
263
+ });
264
+
265
+ function toggle() {
266
+ if (disabled) return;
267
+ panelOpen = !panelOpen;
268
+ }
269
+
270
+ // Tab-trap is WINDOW-scoped (mirrors Modal.svelte's own trapFocus wiring) —
271
+ // not an onkeydown handler on a specific div — so it never needs its own
272
+ // ARIA role/tabindex to satisfy the a11y linter for keyboard handlers.
273
+ function onWindowKeydown(event: KeyboardEvent) {
274
+ if (!panelOpen) return;
275
+ trapFocus(event);
276
+ }
277
+
278
+ function onCalendarChange(next: CalendarValue) {
279
+ if (!Array.isArray(next)) return;
280
+ const [startIso, endIso] = next;
281
+ draft.fromDate = startIso ?? "";
282
+ draft.toDate = endIso ?? "";
283
+ draft.fromText = draft.fromDate ? `${draft.fromDate} ${draft.fromTime || "00:00"}` : "";
284
+ draft.toText = draft.toDate ? `${draft.toDate} ${draft.toTime || "00:00"}` : "";
285
+ }
286
+
287
+ const calendarValue = $derived<CalendarValue>([draft.fromDate || null, draft.toDate || null]);
288
+ const calendarMinIso = $derived(min != null ? splitAbsolute(min, min).fromDate : undefined);
289
+ const calendarMaxIso = $derived(max != null ? splitAbsolute(max, max).fromDate : undefined);
290
+
291
+ function onFromTimeChange(v: string) {
292
+ draft.fromTime = v;
293
+ if (draft.fromDate) draft.fromText = `${draft.fromDate} ${v}`;
294
+ }
295
+ function onToTimeChange(v: string) {
296
+ draft.toTime = v;
297
+ if (draft.toDate) draft.toText = `${draft.toDate} ${v}`;
298
+ }
299
+
300
+ function parseDateTimeText(text: string): { date: string; time: string } | null {
301
+ const match = /^(\d{4}-\d{2}-\d{2})[ T](\d{1,2}:\d{2})$/.exec(text.trim());
302
+ if (!match) return null;
303
+ return { date: match[1], time: match[2] };
304
+ }
305
+
306
+ function commitTypedFrom() {
307
+ const parsed = parseDateTimeText(draft.fromText);
308
+ if (!parsed) return;
309
+ draft.fromDate = parsed.date;
310
+ draft.fromTime = parsed.time;
311
+ }
312
+ function commitTypedTo() {
313
+ const parsed = parseDateTimeText(draft.toText);
314
+ if (!parsed) return;
315
+ draft.toDate = parsed.date;
316
+ draft.toTime = parsed.time;
317
+ }
318
+ function onFromTextKeydown(event: KeyboardEvent) {
319
+ if (event.key === "Enter") {
320
+ event.preventDefault();
321
+ commitTypedFrom();
322
+ }
323
+ }
324
+ function onToTextKeydown(event: KeyboardEvent) {
325
+ if (event.key === "Enter") {
326
+ event.preventDefault();
327
+ commitTypedTo();
328
+ }
329
+ }
330
+
331
+ const composedDraft = $derived(
332
+ composeAbsolute({
333
+ fromDate: draft.fromDate,
334
+ fromTime: draft.fromTime,
335
+ toDate: draft.toDate,
336
+ toTime: draft.toTime
337
+ })
338
+ );
339
+ const draftValid = $derived(composedDraft !== null);
340
+ const draftIncomplete = $derived(!draft.fromDate || !draft.fromTime || !draft.toDate || !draft.toTime);
341
+ const draftError = $derived.by<string | null>(() => {
342
+ if (draftValid) return null;
343
+ if (draftIncomplete) {
344
+ return isFr
345
+ ? "Renseignez une date et une heure pour le début et la fin."
346
+ : "Provide a date and time for both the start and the end.";
347
+ }
348
+ return isFr
349
+ ? "La date de début doit précéder la date de fin."
350
+ : "The start date must be before the end date.";
351
+ });
352
+
353
+ function onApply() {
354
+ if (!composedDraft) return;
355
+ commit({ mode: "absolute", from: composedDraft.from, to: composedDraft.to });
356
+ panelOpen = false;
357
+ }
358
+ function onCancel() {
359
+ panelOpen = false;
360
+ }
361
+
362
+ // --- Labels (fr/en) --------------------------------------------------------
363
+ const panelLabel = $derived(label ?? (isFr ? "Sélecteur de plage temporelle" : "Time range selector"));
364
+ const relativeTabLabel = $derived(isFr ? "Relatif" : "Relative");
365
+ const customTabLabel = $derived(isFr ? "Personnalisé" : "Custom");
366
+ const relativeListLabel = $derived(isFr ? "Plages relatives" : "Relative ranges");
367
+ const fromLabel = $derived(isFr ? "Début" : "From");
368
+ const toLabel = $derived(isFr ? "Fin" : "To");
369
+ const applyLabel = $derived(isFr ? "Appliquer" : "Apply");
370
+ const cancelLabel = $derived(isFr ? "Annuler" : "Cancel");
371
+ const dateTimePlaceholder = "AAAA-MM-JJ HH:mm";
372
+
373
+ const tabItems = $derived([
374
+ { value: "relative", label: relativeTabLabel },
375
+ { value: "absolute", label: customTabLabel }
376
+ ]);
377
+
378
+ function onTabChange(next: string) {
379
+ activeTab = next === "absolute" ? "absolute" : "relative";
380
+ }
381
+
382
+ const rootClasses = $derived(
383
+ ["st-timeRangePicker", `st-timeRangePicker--${size}`, className].filter(Boolean).join(" ")
384
+ );
385
+
386
+ const uid = Math.random().toString(36).slice(2, 9);
387
+ const fieldLabelId = `st-timerangepicker-label-${uid}`;
388
+ const triggerTextId = `st-timerangepicker-text-${uid}`;
389
+ </script>
390
+
391
+ <svelte:window onkeydown={onWindowKeydown} />
392
+
393
+ <div class={rootClasses}>
394
+ {#if label}
395
+ <span class="st-timeRangePicker__label" id={fieldLabelId}>{label}</span>
396
+ {/if}
397
+ <button
398
+ bind:this={triggerEl}
399
+ type="button"
400
+ class="st-timeRangePicker__trigger"
401
+ aria-haspopup="dialog"
402
+ aria-expanded={panelOpen ? "true" : "false"}
403
+ aria-labelledby={label ? `${fieldLabelId} ${triggerTextId}` : undefined}
404
+ {disabled}
405
+ onclick={toggle}
406
+ >
407
+ <Clock size={16} aria-hidden="true" />
408
+ <span class="st-timeRangePicker__triggerLabel" id={triggerTextId}>{triggerLabel}</span>
409
+ <ChevronDown size={16} aria-hidden="true" />
410
+ </button>
411
+
412
+ <MenuPopover bind:open={panelOpen} trigger={triggerEl} {placement} {align} label={panelLabel} class="st-timeRangePicker__popover">
413
+ <div
414
+ class="st-timeRangePicker__panel"
415
+ bind:this={panelWrapperEl}
416
+ >
417
+ <ContentSwitcher items={tabItems} value={activeTab} onchange={onTabChange} size="sm" label={panelLabel} />
418
+
419
+ {#if activeTab === "relative"}
420
+ <SelectableList label={relativeListLabel} value={selectedPresetValue} onchange={onPresetSelect}>
421
+ {#each resolvedPresets as preset (preset.token)}
422
+ <SelectableRow value={preset.token} disabled={preset.disabled}>{preset.label}</SelectableRow>
423
+ {/each}
424
+ </SelectableList>
425
+ {:else}
426
+ <div class="st-timeRangePicker__custom">
427
+ <div class="st-timeRangePicker__calendars">
428
+ <Calendar
429
+ range
430
+ value={calendarValue}
431
+ onChange={onCalendarChange}
432
+ min={calendarMinIso}
433
+ max={calendarMaxIso}
434
+ {locale}
435
+ month={leftMonth}
436
+ />
437
+ {#if calendarMonths === 2}
438
+ <Calendar
439
+ range
440
+ value={calendarValue}
441
+ onChange={onCalendarChange}
442
+ min={calendarMinIso}
443
+ max={calendarMaxIso}
444
+ {locale}
445
+ month={rightMonth}
446
+ />
447
+ {/if}
448
+ </div>
449
+
450
+ <div class="st-timeRangePicker__bounds">
451
+ <div class="st-timeRangePicker__bound">
452
+ <Input
453
+ label={fromLabel}
454
+ bind:value={draft.fromText}
455
+ placeholder={dateTimePlaceholder}
456
+ {size}
457
+ onblur={commitTypedFrom}
458
+ onkeydown={onFromTextKeydown}
459
+ />
460
+ <TimePicker value={draft.fromTime} onChange={onFromTimeChange} step={timeStep} format={timeFormat} {size} />
461
+ </div>
462
+ <div class="st-timeRangePicker__bound">
463
+ <Input
464
+ label={toLabel}
465
+ bind:value={draft.toText}
466
+ placeholder={dateTimePlaceholder}
467
+ {size}
468
+ onblur={commitTypedTo}
469
+ onkeydown={onToTextKeydown}
470
+ />
471
+ <TimePicker value={draft.toTime} onChange={onToTimeChange} step={timeStep} format={timeFormat} {size} />
472
+ </div>
473
+ </div>
474
+
475
+ {#if draftError}
476
+ <p class="st-timeRangePicker__error" role="alert">{draftError}</p>
477
+ {/if}
478
+
479
+ <div class="st-timeRangePicker__actions">
480
+ <Button type="button" variant="ghost" {size} onclick={onCancel}>{cancelLabel}</Button>
481
+ <Button type="button" variant="primary" {size} disabled={!draftValid} onclick={onApply}>{applyLabel}</Button>
482
+ </div>
483
+ </div>
484
+ {/if}
485
+ </div>
486
+ </MenuPopover>
487
+ </div>
488
+
489
+ <style>
490
+ .st-timeRangePicker {
491
+ display: inline-flex;
492
+ flex-direction: column;
493
+ gap: var(--st-component-field-gap, 0.5rem);
494
+ position: relative;
495
+ }
496
+
497
+ .st-timeRangePicker__label {
498
+ color: var(--st-component-field-labelText, var(--st-semantic-text-primary));
499
+ font-size: var(--st-component-field-labelTypography-size, 0.875rem);
500
+ font-weight: var(--st-component-field-labelTypography-weight, 600);
501
+ }
502
+
503
+ .st-timeRangePicker__trigger {
504
+ align-items: center;
505
+ background: var(--st-component-control-background, var(--st-semantic-surface-default));
506
+ border: var(--st-component-control-anatomy-shape-borderWidth, 1px)
507
+ var(--st-component-control-anatomy-shape-borderStyle, solid)
508
+ var(--st-component-control-border, var(--st-semantic-border-subtle));
509
+ border-radius: var(--st-component-control-anatomy-shape-radius, 0.375rem);
510
+ color: var(--st-component-control-text, var(--st-semantic-text-primary));
511
+ cursor: pointer;
512
+ display: inline-flex;
513
+ font: inherit;
514
+ gap: var(--st-spacing-2, 0.5rem);
515
+ transition:
516
+ border-color var(--st-motion-fast, 120ms) var(--st-motion-easing, ease),
517
+ box-shadow var(--st-motion-fast, 120ms) var(--st-motion-easing, ease);
518
+ }
519
+
520
+ .st-timeRangePicker--sm .st-timeRangePicker__trigger {
521
+ min-height: var(--st-component-control-smHeight, 2rem);
522
+ padding: 0 0.625rem;
523
+ font-size: 0.8125rem;
524
+ }
525
+
526
+ .st-timeRangePicker--md .st-timeRangePicker__trigger {
527
+ min-height: var(--st-component-control-mdHeight, 2.5rem);
528
+ padding: 0 0.75rem;
529
+ font-size: 0.875rem;
530
+ }
531
+
532
+ .st-timeRangePicker--lg .st-timeRangePicker__trigger {
533
+ min-height: var(--st-component-control-lgHeight, 3rem);
534
+ padding: 0 0.875rem;
535
+ font-size: 1rem;
536
+ }
537
+
538
+ .st-timeRangePicker__trigger:hover:not(:disabled) {
539
+ background: var(--st-component-control-hoverBackground, var(--st-semantic-surface-subtle));
540
+ border-color: var(--st-component-control-hoverBorder, var(--st-semantic-border-strong));
541
+ }
542
+
543
+ .st-timeRangePicker__trigger:focus-visible {
544
+ border-color: var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
545
+ outline: var(--st-component-control-anatomy-focus-outline, none);
546
+ outline-offset: var(--st-component-control-anatomy-focus-offset, 0);
547
+ box-shadow: var(--st-component-control-anatomy-focus-boxShadow,
548
+ 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive)));
549
+ }
550
+
551
+ .st-timeRangePicker__trigger:disabled {
552
+ color: var(--st-component-control-disabledText, var(--st-semantic-text-muted));
553
+ cursor: not-allowed;
554
+ opacity: 0.65;
555
+ }
556
+
557
+ .st-timeRangePicker__triggerLabel {
558
+ max-width: 20rem;
559
+ overflow: hidden;
560
+ text-overflow: ellipsis;
561
+ white-space: nowrap;
562
+ }
563
+
564
+ /* Panel content — a single stacked column under the tab switcher. NO left
565
+ rail (proscribed DS anti-pattern): tabs on top, one column beneath. */
566
+ .st-timeRangePicker__panel {
567
+ display: flex;
568
+ flex-direction: column;
569
+ gap: var(--st-spacing-3, 0.75rem);
570
+ min-width: 20rem;
571
+ padding: var(--st-spacing-3, 0.75rem);
572
+ }
573
+
574
+ .st-timeRangePicker__panel:focus {
575
+ outline: none;
576
+ }
577
+
578
+ .st-timeRangePicker__custom {
579
+ display: flex;
580
+ flex-direction: column;
581
+ gap: var(--st-spacing-3, 0.75rem);
582
+ }
583
+
584
+ .st-timeRangePicker__calendars {
585
+ display: flex;
586
+ flex-wrap: wrap;
587
+ gap: var(--st-spacing-3, 0.75rem);
588
+ }
589
+
590
+ .st-timeRangePicker__calendars :global(.st-calendar) {
591
+ flex: 1 1 16rem;
592
+ min-width: 16rem;
593
+ }
594
+
595
+ .st-timeRangePicker__bounds {
596
+ display: grid;
597
+ gap: var(--st-spacing-3, 0.75rem);
598
+ grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
599
+ }
600
+
601
+ .st-timeRangePicker__bound {
602
+ display: grid;
603
+ gap: var(--st-spacing-2, 0.5rem);
604
+ }
605
+
606
+ .st-timeRangePicker__error {
607
+ color: var(--st-component-field-errorText, var(--st-semantic-feedback-error));
608
+ font-size: 0.8125rem;
609
+ line-height: 1.4;
610
+ margin: 0;
611
+ }
612
+
613
+ .st-timeRangePicker__actions {
614
+ display: flex;
615
+ gap: var(--st-spacing-2, 0.5rem);
616
+ justify-content: flex-end;
617
+ }
618
+ </style>
@@ -0,0 +1,36 @@
1
+ export type TimeRangeMode = "relative" | "absolute";
2
+ export type TimeRange = {
3
+ mode: TimeRangeMode;
4
+ relative?: string;
5
+ from: number;
6
+ to: number;
7
+ };
8
+ export type TimeRangePreset = string | {
9
+ token: string;
10
+ label?: string;
11
+ durationMs?: number;
12
+ };
13
+ type TimeRangePickerProps = {
14
+ value?: TimeRange;
15
+ defaultValue?: TimeRange;
16
+ onChange?: (value: TimeRange) => void;
17
+ presets?: TimeRangePreset[];
18
+ min?: number;
19
+ max?: number;
20
+ locale?: string;
21
+ timeFormat?: "24" | "12";
22
+ timeStep?: number;
23
+ calendarMonths?: 1 | 2;
24
+ disabled?: boolean;
25
+ label?: string;
26
+ placement?: "bottom-start" | "bottom-end" | "top-start" | "top-end";
27
+ align?: "start" | "end" | "center";
28
+ size?: "sm" | "md" | "lg";
29
+ class?: string;
30
+ formatRange?: (value: TimeRange, locale: string) => string;
31
+ formatPresetLabel?: (token: string, locale: string) => string;
32
+ };
33
+ declare const TimeRangePicker: import("svelte").Component<TimeRangePickerProps, {}, "value">;
34
+ type TimeRangePicker = ReturnType<typeof TimeRangePicker>;
35
+ export default TimeRangePicker;
36
+ //# sourceMappingURL=TimeRangePicker.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimeRangePicker.svelte.d.ts","sourceRoot":"","sources":["../src/lib/TimeRangePicker.svelte.ts"],"names":[],"mappings":"AAOE,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,CAAC;AACpD,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AACF,MAAM,MAAM,eAAe,GACvB,MAAM,GACN;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AA+B3D,KAAK,oBAAoB,GAAG;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,SAAS,CAAC;IACpE,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;IACnC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3D,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/D,CAAC;AA0ZJ,QAAA,MAAM,eAAe,+DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=TimeRangePicker.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimeRangePicker.test.d.ts","sourceRoot":"","sources":["../src/lib/TimeRangePicker.test.ts"],"names":[],"mappings":""}