@r2digisolutions/components 0.8.3 → 0.8.5

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.
@@ -29,6 +29,8 @@
29
29
  dots?: CalendarDot[];
30
30
  /** Show border/shadow chrome. */
31
31
  framed?: boolean;
32
+ /** BCP 47 locale for weekday and month names. */
33
+ locale?: string;
32
34
  class?: string;
33
35
  onchange?: (detail: {
34
36
  mode: CalendarMode;
@@ -52,6 +54,7 @@
52
54
  enabledDates,
53
55
  dots = [],
54
56
  framed = true,
57
+ locale = 'en',
55
58
  class: className = '',
56
59
  onchange
57
60
  }: CalendarProps = $props();
@@ -64,9 +67,15 @@
64
67
  /** Which month column owns the month/year picker when months=2. */
65
68
  let pickerOffset = $state(0);
66
69
 
67
- const weekDays = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
68
- const monthNames = Array.from({ length: 12 }, (_, i) =>
69
- new Date(2000, i, 1).toLocaleString('en', { month: 'short' })
70
+ const weekDays = $derived(
71
+ Array.from({ length: 7 }, (_, i) =>
72
+ new Date(2024, 0, 1 + i).toLocaleDateString(locale, { weekday: 'short' })
73
+ )
74
+ );
75
+ const monthNames = $derived(
76
+ Array.from({ length: 12 }, (_, i) =>
77
+ new Date(2000, i, 1).toLocaleString(locale, { month: 'short' })
78
+ )
70
79
  );
71
80
 
72
81
  const viewYear = $derived(view.getFullYear());
@@ -253,7 +262,7 @@
253
262
  const isToday = iso === todayIso;
254
263
 
255
264
  return [
256
- 'relative flex aspect-square min-h-9 items-center justify-center rounded-lg text-sm transition-colors',
265
+ 'relative flex size-9 items-center justify-center rounded-lg text-sm transition-colors',
257
266
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
258
267
  disabled && 'cursor-not-allowed opacity-30',
259
268
  !disabled && !selected && !mid && 'text-primary hover:bg-surface-overlay',
@@ -294,7 +303,7 @@
294
303
  'max-w-full bg-surface-elevated',
295
304
  framed && 'rounded-2xl border border-border p-3 shadow-sm',
296
305
  !framed && 'p-0.5',
297
- dual && panel === 'days' ? 'w-fit' : framed ? 'w-80' : 'w-full',
306
+ dual && panel === 'days' ? 'w-fit' : 'w-80',
298
307
  className
299
308
  ]}
300
309
  >
@@ -360,7 +369,7 @@
360
369
  <div class="grid grid-cols-7 gap-1.5">
361
370
  {#each grid.cells as cell, i (cell.date || `e-${grid.offset}-${i}`)}
362
371
  {#if !cell.inMonth}
363
- <span class="aspect-square min-h-9"></span>
372
+ <span class="size-9"></span>
364
373
  {:else}
365
374
  {@const color = dotMap.get(cell.date)}
366
375
  <button
@@ -27,6 +27,8 @@ interface CalendarProps {
27
27
  dots?: CalendarDot[];
28
28
  /** Show border/shadow chrome. */
29
29
  framed?: boolean;
30
+ /** BCP 47 locale for weekday and month names. */
31
+ locale?: string;
30
32
  class?: string;
31
33
  onchange?: (detail: {
32
34
  mode: CalendarMode;
@@ -1,8 +1,12 @@
1
1
  <script lang="ts">
2
+ import { on } from 'svelte/events';
2
3
  import Calendar, {
3
4
  type CalendarDot,
4
5
  type CalendarMode
5
6
  } from '../Calendar/Calendar.svelte';
7
+ import { i18n } from '../../../utils/i18n.svelte.js';
8
+ import { resolveLocaleTag } from '../../../utils/i18n.js';
9
+ import { createId } from '../../../utils/id.js';
6
10
 
7
11
  export type DatePickerPlacement =
8
12
  | 'auto'
@@ -43,6 +47,8 @@
43
47
  * alignment from available viewport space.
44
48
  */
45
49
  placement?: DatePickerPlacement;
50
+ /** BCP 47 locale for displayed dates. Defaults to the components i18n locale. */
51
+ locale?: string;
46
52
  class?: string;
47
53
  onchange?: (detail: {
48
54
  mode: CalendarMode;
@@ -76,25 +82,30 @@
76
82
  variant = 'field',
77
83
  closeOnSelect = true,
78
84
  placement = 'auto',
85
+ locale,
79
86
  class: className = '',
80
87
  onchange
81
88
  }: DatePickerProps = $props();
82
89
 
83
- let rootEl = $state<HTMLDivElement | null>(null);
90
+ const dateLocale = $derived(locale || resolveLocaleTag(i18n.locale));
91
+
92
+ let triggerEl = $state<HTMLElement | null>(null);
84
93
  let panelEl = $state<HTMLDivElement | null>(null);
94
+ let panelStyle = $state('margin:0;inset:auto;');
85
95
  let activeField = $state<'start' | 'end' | 'value'>('value');
86
- let resolved = $state<{ side: 'top' | 'bottom'; align: 'start' | 'center' | 'end' | 'stretch' }>({
87
- side: 'bottom',
88
- align: months === 1 ? 'stretch' : 'center'
96
+ let panelId = $state('');
97
+
98
+ $effect(() => {
99
+ panelId ||= createId('datepicker');
89
100
  });
90
101
 
91
102
  function formatIso(iso: string) {
92
103
  if (!iso) return '';
93
104
  const [y, m, d] = iso.split('-').map(Number);
94
105
  const date = new Date(y, m - 1, d);
95
- return date.toLocaleDateString('en', {
96
- month: 'short',
106
+ return date.toLocaleDateString(dateLocale, {
97
107
  day: 'numeric',
108
+ month: 'short',
98
109
  year: 'numeric'
99
110
  });
100
111
  }
@@ -114,100 +125,114 @@
114
125
  function parsePlacement(p: DatePickerPlacement) {
115
126
  if (p === 'auto') return null;
116
127
  const [side, align] = p.split('-') as ['top' | 'bottom', 'start' | 'center' | 'end' | undefined];
117
- return {
118
- side,
119
- align: (align ?? (months === 1 ? 'stretch' : 'center')) as
120
- | 'start'
121
- | 'center'
122
- | 'end'
123
- | 'stretch'
124
- };
128
+ return { side, align: align ?? (months === 2 ? 'center' : 'start') };
125
129
  }
126
130
 
127
- function updatePlacement() {
128
- if (!rootEl || !panelEl) return;
131
+ function positionPanel() {
132
+ if (!triggerEl || !panelEl) return;
133
+ if (!panelEl.matches(':popover-open')) return;
129
134
 
130
- const fixed = parsePlacement(placement);
131
- if (fixed) {
132
- resolved = {
133
- side: fixed.side,
134
- align: months === 1 && fixed.align === 'center' ? 'stretch' : fixed.align
135
- };
136
- return;
137
- }
135
+ const trigger = triggerEl.getBoundingClientRect();
136
+ if (trigger.width < 2 && trigger.height < 2) return;
138
137
 
139
- const trigger = rootEl.getBoundingClientRect();
140
- const panel = panelEl.getBoundingClientRect();
138
+ const vv = window.visualViewport;
139
+ const viewW = vv?.width ?? window.innerWidth;
140
+ const viewH = vv?.height ?? window.innerHeight;
141
+ const viewLeft = vv?.offsetLeft ?? 0;
142
+ const viewTop = vv?.offsetTop ?? 0;
141
143
  const gap = 8;
142
- const vw = window.innerWidth;
143
- const vh = window.innerHeight;
144
-
145
- const spaceBelow = vh - trigger.bottom - gap;
146
- const spaceAbove = trigger.top - gap;
147
- const side: 'top' | 'bottom' =
148
- spaceBelow < panel.height && spaceAbove > spaceBelow ? 'top' : 'bottom';
149
-
150
- let align: 'start' | 'center' | 'end' | 'stretch' = months === 1 ? 'stretch' : 'center';
151
-
152
- if (months === 2 || align !== 'stretch') {
153
- const centerLeft = trigger.left + trigger.width / 2 - panel.width / 2;
154
- const startLeft = trigger.left;
155
- const endLeft = trigger.right - panel.width;
156
-
157
- const fits = (left: number) => left >= gap && left + panel.width <= vw - gap;
158
-
159
- if (fits(centerLeft)) align = 'center';
160
- else if (fits(startLeft)) align = 'start';
161
- else if (fits(endLeft)) align = 'end';
162
- else {
163
- // pick least overflow
164
- const overflows = [
165
- { align: 'center' as const, overflow: Math.max(0, -centerLeft) + Math.max(0, centerLeft + panel.width - vw) },
166
- { align: 'start' as const, overflow: Math.max(0, -startLeft) + Math.max(0, startLeft + panel.width - vw) },
167
- { align: 'end' as const, overflow: Math.max(0, -endLeft) + Math.max(0, endLeft + panel.width - vw) }
168
- ];
169
- overflows.sort((a, b) => a.overflow - b.overflow);
170
- align = overflows[0].align;
171
- }
144
+ const pad = 8;
145
+
146
+ const panelW = Math.min(panelEl.offsetWidth || 320, viewW - pad * 2);
147
+ const panelH = Math.min(panelEl.offsetHeight || 360, 420);
148
+
149
+ const fixed = parsePlacement(placement);
150
+ let side: 'top' | 'bottom' = fixed?.side ?? 'bottom';
151
+ if (!fixed) {
152
+ const spaceBelow = viewTop + viewH - trigger.bottom - gap - pad;
153
+ const spaceAbove = trigger.top - viewTop - gap - pad;
154
+ if (spaceBelow < panelH && spaceAbove > spaceBelow) side = 'top';
172
155
  }
173
156
 
174
- resolved = { side, align };
157
+ const align = fixed?.align ?? (months === 2 ? 'center' : 'start');
158
+
159
+ let top = side === 'bottom' ? trigger.bottom + gap : trigger.top - panelH - gap;
160
+ let left =
161
+ align === 'end'
162
+ ? trigger.right - panelW
163
+ : align === 'center'
164
+ ? trigger.left + trigger.width / 2 - panelW / 2
165
+ : trigger.left;
166
+
167
+ left = Math.min(Math.max(viewLeft + pad, left), viewLeft + viewW - panelW - pad);
168
+ top = Math.min(Math.max(viewTop + pad, top), viewTop + viewH - panelH - pad);
169
+
170
+ panelStyle = [
171
+ 'margin:0',
172
+ 'inset:auto',
173
+ `top:${Math.round(top)}px`,
174
+ `left:${Math.round(left)}px`,
175
+ 'right:auto',
176
+ 'bottom:auto',
177
+ 'width:max-content'
178
+ ].join(';');
179
+ }
180
+
181
+ function schedulePosition() {
182
+ queueMicrotask(() => {
183
+ positionPanel();
184
+ requestAnimationFrame(() => {
185
+ positionPanel();
186
+ requestAnimationFrame(positionPanel);
187
+ });
188
+ });
175
189
  }
176
190
 
177
- $effect(() => {
178
- if (!open) return;
179
- const id = requestAnimationFrame(() => updatePlacement());
180
- const onWin = () => updatePlacement();
181
- window.addEventListener('resize', onWin);
182
- window.addEventListener('scroll', onWin, true);
183
- return () => {
184
- cancelAnimationFrame(id);
185
- window.removeEventListener('resize', onWin);
186
- window.removeEventListener('scroll', onWin, true);
187
- };
188
- });
191
+ function syncNative() {
192
+ if (!panelEl) return;
193
+ const isOpen = panelEl.matches(':popover-open');
194
+ try {
195
+ if (open && !isOpen) panelEl.showPopover();
196
+ else if (!open && isOpen) panelEl.hidePopover();
197
+ } catch {
198
+ /* ignore */
199
+ }
200
+ }
189
201
 
190
202
  function setOpen(next: boolean) {
191
- if (disabled) return;
203
+ if (disabled && next) return;
192
204
  open = next;
193
205
  }
194
206
 
195
- function openField(field: 'start' | 'end' | 'value') {
196
- if (disabled) return;
197
- activeField = field;
198
- open = true;
207
+ function handleBeforeToggle(event: ToggleEvent) {
208
+ if (event.newState === 'open' && disabled) {
209
+ event.preventDefault();
210
+ }
199
211
  }
200
212
 
201
- function onDocPointerDown(e: PointerEvent) {
202
- if (!open || !rootEl) return;
203
- const path = typeof e.composedPath === 'function' ? e.composedPath() : [];
204
- if (path.includes(rootEl) || rootEl.contains(e.target as Node)) return;
205
- setOpen(false);
213
+ function handleToggle(event: ToggleEvent) {
214
+ const next = event.newState === 'open';
215
+ open = next;
216
+ if (next) schedulePosition();
206
217
  }
207
218
 
208
- function onKey(e: KeyboardEvent) {
209
- if (e.key === 'Escape' && open) setOpen(false);
210
- }
219
+ $effect(() => {
220
+ open;
221
+ queueMicrotask(() => {
222
+ syncNative();
223
+ if (open) positionPanel();
224
+ });
225
+ });
226
+
227
+ $effect(() => {
228
+ if (!open) return;
229
+ const offResize = on(window, 'resize', () => positionPanel());
230
+ const offScroll = on(window, 'scroll', () => positionPanel(), { capture: true });
231
+ return () => {
232
+ offResize();
233
+ offScroll();
234
+ };
235
+ });
211
236
 
212
237
  function handleChange(detail: {
213
238
  mode: CalendarMode;
@@ -241,46 +266,21 @@
241
266
  }
242
267
 
243
268
  const hasValue = $derived(
244
- mode === 'single'
245
- ? !!value
246
- : mode === 'multiple'
247
- ? values.length > 0
248
- : !!(start || end)
269
+ mode === 'single' ? !!value : mode === 'multiple' ? values.length > 0 : !!(start || end)
249
270
  );
250
271
 
251
- const panelClass = $derived.by(() => {
252
- const { side, align } = resolved;
253
- const vertical =
254
- side === 'bottom' ? 'top-full mt-2' : 'bottom-full mb-2';
255
-
256
- const horizontal =
257
- align === 'stretch'
258
- ? 'left-0 right-0'
259
- : align === 'center'
260
- ? 'left-1/2 -translate-x-1/2'
261
- : align === 'end'
262
- ? 'right-0'
263
- : 'left-0';
264
-
265
- return [
266
- 'absolute z-50 rounded-2xl border border-border bg-surface-elevated p-2 shadow-xl',
267
- vertical,
268
- horizontal,
269
- months === 2 && align !== 'stretch' ? 'w-max max-w-[min(100vw-1rem,42rem)]' : '',
270
- months === 1 || align === 'stretch' ? 'w-auto' : ''
271
- ];
272
- });
272
+ const triggerAction = $derived(variant === 'split' ? 'show' : 'toggle');
273
273
  </script>
274
274
 
275
- <svelte:document onpointerdown={onDocPointerDown} onkeydown={onKey} />
276
-
277
275
  <div
278
276
  class={[
279
- 'relative',
280
- months === 2 ? 'w-full max-w-xl' : 'w-full min-w-[18rem] max-w-[20rem]',
277
+ variant === 'split'
278
+ ? 'w-full'
279
+ : months === 2
280
+ ? 'w-full max-w-xl'
281
+ : 'w-full min-w-[18rem] max-w-[20rem]',
281
282
  className
282
283
  ]}
283
- bind:this={rootEl}
284
284
  >
285
285
  {#if label && variant === 'field'}
286
286
  <span class="mb-1.5 block text-sm font-medium text-primary">{label}</span>
@@ -288,6 +288,7 @@
288
288
 
289
289
  {#if variant === 'split' && mode === 'range'}
290
290
  <div
291
+ bind:this={triggerEl}
291
292
  class={[
292
293
  'flex overflow-hidden rounded-xl border border-border bg-surface-elevated',
293
294
  open && 'border-brand-500 ring-2 ring-brand-500/20',
@@ -297,14 +298,21 @@
297
298
  <button
298
299
  type="button"
299
300
  {disabled}
300
- onclick={() => openField('start')}
301
+ popovertarget={panelId}
302
+ popovertargetaction={triggerAction}
303
+ onclick={() => {
304
+ activeField = 'start';
305
+ }}
306
+ aria-expanded={open}
307
+ aria-haspopup="dialog"
308
+ aria-controls={panelId}
301
309
  class={[
302
310
  'flex min-w-0 flex-1 flex-col gap-0.5 px-3.5 py-2.5 text-left transition-colors',
303
311
  'hover:bg-surface-overlay focus-visible:outline-none',
304
312
  activeField === 'start' && open && 'bg-surface-overlay'
305
313
  ]}
306
314
  >
307
- <span class="text-[11px] font-medium uppercase tracking-wide text-muted">{startLabel}</span>
315
+ <span class="text-[11px] font-medium tracking-wide text-muted uppercase">{startLabel}</span>
308
316
  <span class={['truncate text-sm', start ? 'text-primary' : 'text-muted']}>
309
317
  {start ? formatIso(start) : startPlaceholder}
310
318
  </span>
@@ -313,14 +321,21 @@
313
321
  <button
314
322
  type="button"
315
323
  {disabled}
316
- onclick={() => openField('end')}
324
+ popovertarget={panelId}
325
+ popovertargetaction={triggerAction}
326
+ onclick={() => {
327
+ activeField = 'end';
328
+ }}
329
+ aria-expanded={open}
330
+ aria-haspopup="dialog"
331
+ aria-controls={panelId}
317
332
  class={[
318
333
  'flex min-w-0 flex-1 flex-col gap-0.5 px-3.5 py-2.5 text-left transition-colors',
319
334
  'hover:bg-surface-overlay focus-visible:outline-none',
320
335
  activeField === 'end' && open && 'bg-surface-overlay'
321
336
  ]}
322
337
  >
323
- <span class="text-[11px] font-medium uppercase tracking-wide text-muted">{endLabel}</span>
338
+ <span class="text-[11px] font-medium tracking-wide text-muted uppercase">{endLabel}</span>
324
339
  <span class={['truncate text-sm', end ? 'text-primary' : 'text-muted']}>
325
340
  {end ? formatIso(end) : endPlaceholder}
326
341
  </span>
@@ -330,7 +345,7 @@
330
345
  type="button"
331
346
  onclick={clear}
332
347
  class="px-3 text-muted hover:text-primary"
333
- aria-label="Clear dates"
348
+ aria-label={i18n.t('clearAll')}
334
349
  >
335
350
  <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
336
351
  <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
@@ -340,6 +355,7 @@
340
355
  </div>
341
356
  {:else}
342
357
  <div
358
+ bind:this={triggerEl}
343
359
  class={[
344
360
  'flex h-10 w-full items-center overflow-hidden rounded-xl border border-border bg-surface-elevated transition-colors',
345
361
  open && 'border-brand-500 ring-2 ring-brand-500/20',
@@ -349,8 +365,14 @@
349
365
  <button
350
366
  type="button"
351
367
  {disabled}
352
- onclick={() => (open ? setOpen(false) : openField('value'))}
368
+ popovertarget={panelId}
369
+ popovertargetaction="toggle"
370
+ onclick={() => {
371
+ activeField = 'value';
372
+ }}
353
373
  aria-expanded={open}
374
+ aria-haspopup="dialog"
375
+ aria-controls={panelId}
354
376
  class={[
355
377
  'flex h-full min-w-0 flex-1 items-center gap-2 px-3.5 text-left text-sm',
356
378
  'hover:bg-surface-overlay focus-visible:outline-none',
@@ -382,7 +404,13 @@
382
404
  class="h-full px-3 text-muted hover:bg-surface-overlay hover:text-primary"
383
405
  aria-label="Clear"
384
406
  >
385
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
407
+ <svg
408
+ class="h-3.5 w-3.5"
409
+ viewBox="0 0 24 24"
410
+ fill="none"
411
+ stroke="currentColor"
412
+ stroke-width="2"
413
+ >
386
414
  <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
387
415
  </svg>
388
416
  </button>
@@ -390,55 +418,83 @@
390
418
  </div>
391
419
  {/if}
392
420
 
393
- {#if open}
394
- <div
395
- bind:this={panelEl}
396
- role="dialog"
397
- aria-label="Choose date"
398
- class={panelClass}
399
- onpointerdown={(e) => e.stopPropagation()}
400
- >
401
- {#if mode === 'single'}
402
- <Calendar
403
- mode="single"
404
- {months}
405
- bind:value
406
- {min}
407
- {max}
408
- {disabledDates}
409
- {enabledDates}
410
- {dots}
411
- framed={false}
412
- onchange={handleChange}
413
- />
414
- {:else if mode === 'multiple'}
415
- <Calendar
416
- mode="multiple"
417
- {months}
418
- bind:values
419
- {min}
420
- {max}
421
- {disabledDates}
422
- {enabledDates}
423
- {dots}
424
- framed={false}
425
- onchange={handleChange}
426
- />
427
- {:else}
428
- <Calendar
429
- mode="range"
430
- {months}
431
- bind:start
432
- bind:end
433
- {min}
434
- {max}
435
- {disabledDates}
436
- {enabledDates}
437
- {dots}
438
- framed={false}
439
- onchange={handleChange}
440
- />
441
- {/if}
442
- </div>
443
- {/if}
421
+ <!-- Native popover (top layer + light dismiss). Not <dialog>: date picker is non-modal. -->
422
+ <div
423
+ bind:this={panelEl}
424
+ id={panelId}
425
+ popover="auto"
426
+ role="dialog"
427
+ aria-label="Choose date"
428
+ onbeforetoggle={handleBeforeToggle}
429
+ ontoggle={handleToggle}
430
+ style={panelStyle}
431
+ class={[
432
+ 'datepicker-popover m-0 w-max rounded-2xl border border-border bg-surface-elevated p-2 shadow-xl outline-none',
433
+ months === 2 ? 'max-w-[min(42rem,calc(100vw-1rem))]' : 'max-w-[20rem]'
434
+ ]}
435
+ >
436
+ {#if mode === 'single'}
437
+ <Calendar
438
+ mode="single"
439
+ {months}
440
+ bind:value
441
+ {min}
442
+ {max}
443
+ {disabledDates}
444
+ {enabledDates}
445
+ {dots}
446
+ framed={false}
447
+ locale={dateLocale}
448
+ onchange={handleChange}
449
+ />
450
+ {:else if mode === 'multiple'}
451
+ <Calendar
452
+ mode="multiple"
453
+ {months}
454
+ bind:values
455
+ {min}
456
+ {max}
457
+ {disabledDates}
458
+ {enabledDates}
459
+ {dots}
460
+ framed={false}
461
+ locale={dateLocale}
462
+ onchange={handleChange}
463
+ />
464
+ {:else}
465
+ <Calendar
466
+ mode="range"
467
+ {months}
468
+ bind:start
469
+ bind:end
470
+ {min}
471
+ {max}
472
+ {disabledDates}
473
+ {enabledDates}
474
+ {dots}
475
+ framed={false}
476
+ locale={dateLocale}
477
+ onchange={handleChange}
478
+ />
479
+ {/if}
480
+ </div>
444
481
  </div>
482
+
483
+ <style>
484
+ /* UA popover is inset:0 + margin:auto (viewport-centered and stretched). */
485
+ .datepicker-popover {
486
+ position: fixed;
487
+ inset: unset;
488
+ margin: 0;
489
+ width: max-content;
490
+ height: max-content;
491
+ }
492
+
493
+ .datepicker-popover:popover-open {
494
+ display: block;
495
+ }
496
+
497
+ .datepicker-popover:not(:popover-open) {
498
+ display: none;
499
+ }
500
+ </style>
@@ -30,6 +30,8 @@ interface DatePickerProps {
30
30
  * alignment from available viewport space.
31
31
  */
32
32
  placement?: DatePickerPlacement;
33
+ /** BCP 47 locale for displayed dates. Defaults to the components i18n locale. */
34
+ locale?: string;
33
35
  class?: string;
34
36
  onchange?: (detail: {
35
37
  mode: CalendarMode;
@@ -23,6 +23,7 @@
23
23
  variant?: 'field' | 'split';
24
24
  closeOnSelect?: boolean;
25
25
  placement?: DatePickerPlacement;
26
+ locale?: string;
26
27
  class?: string;
27
28
  onchange?: (detail: { start: string; end: string }) => void;
28
29
  }
@@ -47,6 +48,7 @@
47
48
  variant = 'split',
48
49
  closeOnSelect = true,
49
50
  placement = 'auto',
51
+ locale,
50
52
  class: className = '',
51
53
  onchange
52
54
  }: DateRangePickerProps = $props();
@@ -73,6 +75,7 @@
73
75
  {variant}
74
76
  {closeOnSelect}
75
77
  {placement}
78
+ {locale}
76
79
  class={className}
77
80
  onchange={(detail) => onchange?.({ start: detail.start, end: detail.end })}
78
81
  />
@@ -21,6 +21,7 @@ interface DateRangePickerProps {
21
21
  variant?: 'field' | 'split';
22
22
  closeOnSelect?: boolean;
23
23
  placement?: DatePickerPlacement;
24
+ locale?: string;
24
25
  class?: string;
25
26
  onchange?: (detail: {
26
27
  start: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",