@r2digisolutions/components 0.14.15 → 0.14.16

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.
@@ -25,11 +25,11 @@ declare const meta: {
25
25
  format: "24h";
26
26
  minuteStep: number;
27
27
  label: string;
28
- closeOnSelect: true;
28
+ closeOnSelect: false;
29
29
  };
30
30
  };
31
31
  export default meta;
32
32
  type Story = StoryObj<typeof meta>;
33
33
  export declare const Default: Story;
34
34
  export declare const TwelveHour: Story;
35
- export declare const KeepOpen: Story;
35
+ export declare const InstantClose: Story;
@@ -12,13 +12,13 @@ const meta = {
12
12
  format: '24h',
13
13
  minuteStep: 5,
14
14
  label: 'Date & time',
15
- closeOnSelect: true
15
+ closeOnSelect: false
16
16
  }
17
17
  };
18
18
  export default meta;
19
19
  export const Default = {};
20
20
  export const TwelveHour = { args: { format: '12h' } };
21
- export const KeepOpen = {
22
- name: 'Keep open after select',
23
- args: { closeOnSelect: false }
21
+ export const InstantClose = {
22
+ name: 'Instant commit (legacy)',
23
+ args: { closeOnSelect: true }
24
24
  };
@@ -1,8 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { on } from 'svelte/events';
3
+ import Button from '../../atoms/Button/Button.svelte';
3
4
  import Calendar from '../Calendar/Calendar.svelte';
4
5
  import type { CalendarDot } from '../Calendar/Calendar.svelte';
5
6
  import type { TimeFormat } from '../TimePicker/TimePicker.svelte';
7
+ import { i18n } from '../../../utils/i18n.svelte.js';
6
8
  import { createId } from '../../../utils/id.js';
7
9
 
8
10
  interface DateTimePickerProps {
@@ -22,7 +24,13 @@
22
24
  dots?: CalendarDot[];
23
25
  format?: TimeFormat;
24
26
  minuteStep?: number;
27
+ /**
28
+ * When false (default), edits stay in the panel until Save; Cancel discards.
29
+ * When true, each pick commits immediately and may close the panel.
30
+ */
25
31
  closeOnSelect?: boolean;
32
+ cancelLabel?: string;
33
+ saveLabel?: string;
26
34
  class?: string;
27
35
  onchange?: (detail: { date: string; time: string; value: string }) => void;
28
36
  }
@@ -41,7 +49,9 @@
41
49
  dots = [],
42
50
  format = '24h',
43
51
  minuteStep = 5,
44
- closeOnSelect = true,
52
+ closeOnSelect = false,
53
+ cancelLabel,
54
+ saveLabel,
45
55
  class: className = '',
46
56
  onchange
47
57
  }: DateTimePickerProps = $props();
@@ -52,6 +62,14 @@
52
62
  let minuteListEl = $state<HTMLDivElement | null>(null);
53
63
  let placed = $state(false);
54
64
  let panelId = $state('');
65
+ let draftDate = $state('');
66
+ let draftTime = $state('');
67
+
68
+ const resolvedCancelLabel = $derived(cancelLabel ?? i18n.t('cancel'));
69
+ const resolvedSaveLabel = $derived(saveLabel ?? i18n.t('save'));
70
+ const confirmMode = $derived(!closeOnSelect);
71
+ const panelDate = $derived(confirmMode ? draftDate : date);
72
+ const panelTime = $derived(confirmMode ? draftTime : time);
55
73
 
56
74
  $effect(() => {
57
75
  panelId ||= createId('datetimepicker');
@@ -101,7 +119,7 @@
101
119
  }
102
120
  });
103
121
 
104
- const parsedTime = $derived(parseTime(time));
122
+ const parsedTime = $derived(parseTime(panelTime));
105
123
  const selectedH = $derived(parsedTime?.h ?? null);
106
124
  const selectedM = $derived(parsedTime?.m ?? null);
107
125
 
@@ -131,7 +149,33 @@
131
149
  });
132
150
 
133
151
  function estimatePanelSize() {
134
- return { panelW: 672, panelH: 380 };
152
+ return { panelW: 672, panelH: confirmMode ? 432 : 380 };
153
+ }
154
+
155
+ function beginDraft() {
156
+ draftDate = date;
157
+ draftTime = time || '09:00';
158
+ }
159
+
160
+ function setPanelDate(next: string) {
161
+ if (confirmMode) draftDate = next;
162
+ else date = next;
163
+ }
164
+
165
+ function setPanelTime(next: string) {
166
+ if (confirmMode) draftTime = next;
167
+ else time = next;
168
+ }
169
+
170
+ function commitDraft() {
171
+ date = draftDate;
172
+ time = draftTime || '09:00';
173
+ emit();
174
+ setOpen(false);
175
+ }
176
+
177
+ function cancelDraft() {
178
+ setOpen(false);
135
179
  }
136
180
 
137
181
  function positionPanel(opts: { measure?: boolean; show?: boolean } = {}) {
@@ -214,6 +258,7 @@
214
258
  event.preventDefault();
215
259
  return;
216
260
  }
261
+ if (confirmMode) beginDraft();
217
262
  placed = false;
218
263
  positionPanel({ measure: false });
219
264
  } else {
@@ -246,23 +291,28 @@
246
291
  });
247
292
 
248
293
  function handleDateChange(detail: { value: string }) {
249
- date = detail.value;
250
- if (!time) time = '09:00';
251
- emit();
252
- if (closeOnSelect && date && time) setOpen(false);
294
+ setPanelDate(detail.value);
295
+ const currentTime = confirmMode ? draftTime : time;
296
+ if (!currentTime) setPanelTime('09:00');
297
+ if (!confirmMode) {
298
+ emit();
299
+ if (closeOnSelect && date && time) setOpen(false);
300
+ }
253
301
  }
254
302
 
255
303
  function pickHour(h: number) {
256
304
  const m = selectedM ?? 0;
257
- time = toTime(h, m);
258
- emit();
305
+ setPanelTime(toTime(h, m));
306
+ if (!confirmMode) emit();
259
307
  }
260
308
 
261
309
  function pickMinute(m: number) {
262
310
  const h = selectedH ?? 9;
263
- time = toTime(h, m);
264
- emit();
265
- if (closeOnSelect && date && time) setOpen(false);
311
+ setPanelTime(toTime(h, m));
312
+ if (!confirmMode) {
313
+ emit();
314
+ if (closeOnSelect && date && time) setOpen(false);
315
+ }
266
316
  }
267
317
 
268
318
  function clear(e: MouseEvent) {
@@ -359,81 +409,93 @@
359
409
  onbeforetoggle={handleBeforeToggle}
360
410
  ontoggle={handleToggle}
361
411
  data-placed={placed ? true : undefined}
362
- class="datetime-picker-popover m-0 flex w-max max-w-[min(100vw-1rem,42rem)] flex-col overflow-hidden rounded-2xl border border-border bg-surface-elevated p-2 shadow-xl outline-none sm:flex-row sm:items-stretch"
412
+ class="datetime-picker-popover m-0 flex w-max max-w-[min(100vw-1rem,42rem)] flex-col overflow-hidden rounded-2xl border border-border bg-surface-elevated shadow-xl outline-none"
363
413
  >
364
414
  {#key open}
365
- <div class="w-[22rem] shrink-0 p-1.5">
366
- <Calendar
367
- mode="single"
368
- bind:value={date}
369
- {min}
370
- {max}
371
- {disabledDates}
372
- {dots}
373
- framed={false}
374
- class="w-full"
375
- onchange={handleDateChange}
376
- />
377
- </div>
378
- <div
379
- class="flex w-full shrink-0 flex-col border-t border-border sm:w-40 sm:border-l sm:border-t-0"
380
- >
381
- <span
382
- class="shrink-0 border-b border-border px-3 py-2.5 text-center text-[11px] font-medium uppercase tracking-wide text-muted"
415
+ <div class="flex flex-col sm:flex-row sm:items-stretch">
416
+ <div class="w-[22rem] shrink-0 p-1.5">
417
+ <Calendar
418
+ mode="single"
419
+ value={panelDate}
420
+ {min}
421
+ {max}
422
+ {disabledDates}
423
+ {dots}
424
+ framed={false}
425
+ class="w-full"
426
+ onchange={handleDateChange}
427
+ />
428
+ </div>
429
+ <div
430
+ class="flex w-full shrink-0 flex-col border-t border-border sm:w-40 sm:border-l sm:border-t-0"
383
431
  >
384
- Time
385
- </span>
386
- <div class="grid min-h-0 flex-1 grid-cols-2 divide-x divide-border">
387
- <div
388
- bind:this={hourListEl}
389
- class="h-[18.5rem] overflow-y-auto p-1.5"
390
- role="listbox"
391
- aria-label="Hours"
432
+ <span
433
+ class="shrink-0 border-b border-border px-3 py-2.5 text-center text-[11px] font-medium uppercase tracking-wide text-muted"
392
434
  >
393
- {#each hours24 as h (h)}
394
- <button
395
- type="button"
396
- role="option"
397
- aria-pressed={selectedH === h}
398
- aria-selected={selectedH === h}
399
- onclick={() => pickHour(h)}
400
- class={[
401
- 'w-full rounded-lg px-2 py-1.5 text-sm transition-colors',
402
- selectedH === h
403
- ? 'bg-brand-500 font-semibold text-white'
404
- : 'text-primary hover:bg-surface-overlay'
405
- ]}
406
- >
407
- {hourLabel(h)}
408
- </button>
409
- {/each}
410
- </div>
411
- <div
412
- bind:this={minuteListEl}
413
- class="h-[18.5rem] overflow-y-auto p-1.5"
414
- role="listbox"
415
- aria-label="Minutes"
416
- >
417
- {#each minutes as m (m)}
418
- <button
419
- type="button"
420
- role="option"
421
- aria-pressed={selectedM === m}
422
- aria-selected={selectedM === m}
423
- onclick={() => pickMinute(m)}
424
- class={[
425
- 'w-full rounded-lg px-2 py-1.5 text-sm transition-colors',
426
- selectedM === m
427
- ? 'bg-brand-500 font-semibold text-white'
428
- : 'text-primary hover:bg-surface-overlay'
429
- ]}
430
- >
431
- {pad(m)}
432
- </button>
433
- {/each}
435
+ Time
436
+ </span>
437
+ <div class="grid min-h-0 flex-1 grid-cols-2 divide-x divide-border">
438
+ <div
439
+ bind:this={hourListEl}
440
+ class="h-[18.5rem] overflow-y-auto p-1.5"
441
+ role="listbox"
442
+ aria-label="Hours"
443
+ >
444
+ {#each hours24 as h (h)}
445
+ <button
446
+ type="button"
447
+ role="option"
448
+ aria-pressed={selectedH === h}
449
+ aria-selected={selectedH === h}
450
+ onclick={() => pickHour(h)}
451
+ class={[
452
+ 'w-full rounded-lg px-2 py-1.5 text-sm transition-colors',
453
+ selectedH === h
454
+ ? 'bg-brand-500 font-semibold text-white'
455
+ : 'text-primary hover:bg-surface-overlay'
456
+ ]}
457
+ >
458
+ {hourLabel(h)}
459
+ </button>
460
+ {/each}
461
+ </div>
462
+ <div
463
+ bind:this={minuteListEl}
464
+ class="h-[18.5rem] overflow-y-auto p-1.5"
465
+ role="listbox"
466
+ aria-label="Minutes"
467
+ >
468
+ {#each minutes as m (m)}
469
+ <button
470
+ type="button"
471
+ role="option"
472
+ aria-pressed={selectedM === m}
473
+ aria-selected={selectedM === m}
474
+ onclick={() => pickMinute(m)}
475
+ class={[
476
+ 'w-full rounded-lg px-2 py-1.5 text-sm transition-colors',
477
+ selectedM === m
478
+ ? 'bg-brand-500 font-semibold text-white'
479
+ : 'text-primary hover:bg-surface-overlay'
480
+ ]}
481
+ >
482
+ {pad(m)}
483
+ </button>
484
+ {/each}
485
+ </div>
434
486
  </div>
435
487
  </div>
436
488
  </div>
489
+ {#if confirmMode}
490
+ <div class="flex items-center justify-end gap-2 border-t border-border px-3 py-2.5">
491
+ <Button type="button" variant="ghost" size="sm" onclick={cancelDraft}>
492
+ {resolvedCancelLabel}
493
+ </Button>
494
+ <Button type="button" size="sm" disabled={!draftDate} onclick={commitDraft}>
495
+ {resolvedSaveLabel}
496
+ </Button>
497
+ </div>
498
+ {/if}
437
499
  {/key}
438
500
  </div>
439
501
  </div>
@@ -17,7 +17,13 @@ interface DateTimePickerProps {
17
17
  dots?: CalendarDot[];
18
18
  format?: TimeFormat;
19
19
  minuteStep?: number;
20
+ /**
21
+ * When false (default), edits stay in the panel until Save; Cancel discards.
22
+ * When true, each pick commits immediately and may close the panel.
23
+ */
20
24
  closeOnSelect?: boolean;
25
+ cancelLabel?: string;
26
+ saveLabel?: string;
21
27
  class?: string;
22
28
  onchange?: (detail: {
23
29
  date: string;
@@ -6,7 +6,7 @@
6
6
  format = '24h',
7
7
  minuteStep = 5,
8
8
  label = 'Date & time',
9
- closeOnSelect = true
9
+ closeOnSelect = false
10
10
  }: {
11
11
  format?: TimeFormat;
12
12
  minuteStep?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.14.15",
3
+ "version": "0.14.16",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",