@joewinke/jatui 0.41.1 → 0.43.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.
Files changed (75) hide show
  1. package/CLAUDE.md +32 -6
  2. package/THIRD_PARTY_NOTICES.md +14 -0
  3. package/dist/actions/dismissOutside.d.ts +52 -0
  4. package/dist/actions/dismissOutside.js +80 -0
  5. package/dist/components/BookingScheduler.svelte +412 -0
  6. package/dist/components/BookingScheduler.svelte.d.ts +66 -0
  7. package/dist/components/CalendarPicker.svelte +190 -205
  8. package/dist/components/CalendarPicker.svelte.d.ts +19 -1
  9. package/dist/components/DatePicker.svelte +419 -0
  10. package/dist/components/DatePicker.svelte.d.ts +44 -0
  11. package/dist/components/DateRangePicker.svelte +471 -5
  12. package/dist/components/DateRangePicker.svelte.d.ts +32 -3
  13. package/dist/components/EventCalendar.svelte +1324 -0
  14. package/dist/components/EventCalendar.svelte.d.ts +101 -0
  15. package/dist/components/FileDropzone.svelte +165 -3
  16. package/dist/components/FileDropzone.svelte.d.ts +9 -0
  17. package/dist/components/TextInput.svelte +71 -20
  18. package/dist/components/TextInput.svelte.d.ts +7 -0
  19. package/dist/components/ThumbnailGrid.svelte +106 -1
  20. package/dist/components/ThumbnailGrid.svelte.d.ts +7 -0
  21. package/dist/components/TimeSlotPicker.svelte +174 -108
  22. package/dist/components/TimeSlotPicker.svelte.d.ts +27 -0
  23. package/dist/components/basic/Alert.svelte +113 -16
  24. package/dist/components/basic/Alert.svelte.d.ts +17 -1
  25. package/dist/components/basic/Checkbox.svelte +53 -1
  26. package/dist/components/basic/Checkbox.svelte.d.ts +6 -0
  27. package/dist/components/basic/RadioGroup.svelte +59 -3
  28. package/dist/components/basic/RadioGroup.svelte.d.ts +15 -0
  29. package/dist/components/basic/Slider.svelte +257 -9
  30. package/dist/components/basic/Slider.svelte.d.ts +14 -2
  31. package/dist/components/basic/sliderMath.d.ts +29 -0
  32. package/dist/components/basic/sliderMath.js +64 -0
  33. package/dist/components/calendar/CalendarHeader.svelte +98 -0
  34. package/dist/components/calendar/CalendarHeader.svelte.d.ts +24 -0
  35. package/dist/components/calendar/MonthGrid.svelte +361 -0
  36. package/dist/components/calendar/MonthGrid.svelte.d.ts +55 -0
  37. package/dist/components/calendar/calendarGrid.d.ts +219 -0
  38. package/dist/components/calendar/calendarGrid.js +494 -0
  39. package/dist/components/calendar/createCalendarGrid.d.ts +25 -0
  40. package/dist/components/calendar/createCalendarGrid.js +57 -0
  41. package/dist/components/calendar/dayOverlap.d.ts +92 -0
  42. package/dist/components/calendar/dayOverlap.js +147 -0
  43. package/dist/components/calendar/eventCalendar.d.ts +75 -0
  44. package/dist/components/calendar/eventCalendar.js +187 -0
  45. package/dist/components/calendar/index.d.ts +32 -0
  46. package/dist/components/calendar/index.js +25 -0
  47. package/dist/components/calendar/monthPacking.d.ts +97 -0
  48. package/dist/components/calendar/monthPacking.js +254 -0
  49. package/dist/components/calendar/timeSlots.d.ts +57 -0
  50. package/dist/components/calendar/timeSlots.js +163 -0
  51. package/dist/components/field/FieldHint.svelte +29 -0
  52. package/dist/components/field/FieldHint.svelte.d.ts +17 -0
  53. package/dist/components/field/FieldLabel.svelte +100 -0
  54. package/dist/components/field/FieldLabel.svelte.d.ts +29 -0
  55. package/dist/components/form-cards/CheckGlyph.svelte +138 -0
  56. package/dist/components/form-cards/CheckGlyph.svelte.d.ts +29 -0
  57. package/dist/components/form-cards/CheckboxCard.svelte +120 -0
  58. package/dist/components/form-cards/CheckboxCard.svelte.d.ts +33 -0
  59. package/dist/components/form-cards/ChoiceCard.svelte +156 -0
  60. package/dist/components/form-cards/ChoiceCard.svelte.d.ts +42 -0
  61. package/dist/components/form-cards/SwitchCard.svelte +114 -0
  62. package/dist/components/form-cards/SwitchCard.svelte.d.ts +28 -0
  63. package/dist/components/toast/ToastHost.svelte +56 -3
  64. package/dist/components/toast/ToastHost.svelte.d.ts +9 -1
  65. package/dist/components/toast/ToastItem.svelte +75 -23
  66. package/dist/components/toast/ToastItem.svelte.d.ts +2 -0
  67. package/dist/components/uploadProgress.d.ts +30 -0
  68. package/dist/components/uploadProgress.js +47 -0
  69. package/dist/index.d.ts +29 -2
  70. package/dist/index.js +22 -1
  71. package/dist/stores/toast.d.ts +35 -0
  72. package/dist/stores/toast.js +15 -1
  73. package/dist/styles/motion.css +72 -0
  74. package/dist/styles/rail.css +51 -0
  75. package/package.json +6 -1
@@ -1,8 +1,15 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import ChoiceCard from '../form-cards/ChoiceCard.svelte';
4
+
2
5
  export interface RadioOption {
3
6
  value: string;
4
7
  label: string;
5
8
  disabled?: boolean;
9
+ /** Secondary line — shown by `variant="card"`. */
10
+ description?: string;
11
+ /** Leading icon — shown by `variant="card"`. */
12
+ icon?: Snippet;
6
13
  }
7
14
 
8
15
  interface Props {
@@ -14,6 +21,16 @@
14
21
  orientation?: 'horizontal' | 'vertical';
15
22
  onchange?: (value: string) => void;
16
23
  class?: string;
24
+ /**
25
+ * `card` renders each option as a ChoiceCard (title + description +
26
+ * icon, whole card clickable). The radios stay native, so arrow keys
27
+ * still rove inside the group.
28
+ */
29
+ display?: 'default' | 'card';
30
+ /** Accessible name for the group (`aria-label`). */
31
+ label?: string;
32
+ /** Where the radio dot sits on a card. Default `start`. */
33
+ controlPosition?: 'start' | 'end';
17
34
  }
18
35
 
19
36
  let {
@@ -24,7 +41,10 @@
24
41
  variant = 'primary',
25
42
  orientation = 'vertical',
26
43
  onchange,
27
- class: className = ''
44
+ class: className = '',
45
+ display = 'default',
46
+ label,
47
+ controlPosition = 'start'
28
48
  }: Props = $props();
29
49
 
30
50
  const sizeClass = $derived({ xs: 'radio-xs', sm: 'radio-sm', md: '', lg: 'radio-lg' }[size]);
@@ -42,7 +62,15 @@
42
62
  );
43
63
  const radioClass = $derived(['radio', sizeClass, variantClass].filter(Boolean).join(' '));
44
64
  const groupClass = $derived(
45
- ['flex', orientation === 'horizontal' ? 'flex-row flex-wrap gap-4' : 'flex-col gap-2', className].join(' ')
65
+ [
66
+ 'flex',
67
+ orientation === 'horizontal'
68
+ ? display === 'card'
69
+ ? 'flex-row flex-wrap gap-3'
70
+ : 'flex-row flex-wrap gap-4'
71
+ : 'flex-col gap-2',
72
+ className
73
+ ].join(' ')
46
74
  );
47
75
 
48
76
  function handleChange(e: Event, optValue: string) {
@@ -51,9 +79,36 @@
51
79
  }
52
80
  </script>
53
81
 
54
- <div class={groupClass} role="radiogroup">
82
+ <div class={groupClass} role="radiogroup" aria-label={label}>
55
83
  {#each options as opt}
56
84
  {@const inputId = `${name}-${opt.value}`}
85
+ {#if display === 'card'}
86
+ <ChoiceCard
87
+ title={opt.label}
88
+ description={opt.description}
89
+ icon={opt.icon}
90
+ disabled={opt.disabled}
91
+ size={size === 'xs' || size === 'sm' ? 'sm' : 'md'}
92
+ {controlPosition}
93
+ for={inputId}
94
+ descriptionId={opt.description ? `${inputId}-desc` : undefined}
95
+ class={orientation === 'horizontal' ? 'min-w-48 flex-1' : ''}
96
+ >
97
+ {#snippet control()}
98
+ <input
99
+ type="radio"
100
+ id={inputId}
101
+ {name}
102
+ value={opt.value}
103
+ class={radioClass}
104
+ checked={value === opt.value}
105
+ disabled={opt.disabled}
106
+ aria-describedby={opt.description ? `${inputId}-desc` : undefined}
107
+ onchange={(e) => handleChange(e, opt.value)}
108
+ />
109
+ {/snippet}
110
+ </ChoiceCard>
111
+ {:else}
57
112
  <label class="flex cursor-pointer items-center gap-2" for={inputId}>
58
113
  <input
59
114
  type="radio"
@@ -67,5 +122,6 @@
67
122
  />
68
123
  <span class="label-text">{opt.label}</span>
69
124
  </label>
125
+ {/if}
70
126
  {/each}
71
127
  </div>
@@ -1,7 +1,12 @@
1
+ import type { Snippet } from 'svelte';
1
2
  export interface RadioOption {
2
3
  value: string;
3
4
  label: string;
4
5
  disabled?: boolean;
6
+ /** Secondary line — shown by `variant="card"`. */
7
+ description?: string;
8
+ /** Leading icon — shown by `variant="card"`. */
9
+ icon?: Snippet;
5
10
  }
6
11
  interface Props {
7
12
  value?: string;
@@ -12,6 +17,16 @@ interface Props {
12
17
  orientation?: 'horizontal' | 'vertical';
13
18
  onchange?: (value: string) => void;
14
19
  class?: string;
20
+ /**
21
+ * `card` renders each option as a ChoiceCard (title + description +
22
+ * icon, whole card clickable). The radios stay native, so arrow keys
23
+ * still rove inside the group.
24
+ */
25
+ display?: 'default' | 'card';
26
+ /** Accessible name for the group (`aria-label`). */
27
+ label?: string;
28
+ /** Where the radio dot sits on a card. Default `start`. */
29
+ controlPosition?: 'start' | 'end';
15
30
  }
16
31
  declare const RadioGroup: import("svelte").Component<Props, {}, "value">;
17
32
  type RadioGroup = ReturnType<typeof RadioGroup>;
@@ -1,16 +1,48 @@
1
1
  <script lang="ts">
2
+ /**
3
+ * Slider — single value or a two-thumb range.
4
+ *
5
+ * Pass a number for one thumb (the original DaisyUI `range`, unchanged
6
+ * markup), or a `[min, max]` pair for two. The two-thumb form stacks two
7
+ * native `<input type="range">`s over one drawn track; only the thumbs
8
+ * take pointer events, so native keys (arrows, PgUp/PgDn, Home/End) and
9
+ * screen-reader support come for free. A thumb can't cross the other one
10
+ * (`minGap` keeps them apart).
11
+ *
12
+ * `formatValue` feeds the bubble, the `showValue` readout and
13
+ * `aria-valuetext`. `bubble` shows a value bubble above each thumb:
14
+ * `always`, `drag` (while pressed or keyboard-focused) or `none`.
15
+ *
16
+ * Two-thumb + value-bubble behaviour after BoardUI's slider (MIT © 2026
17
+ * Mertcan Dundar Esmergul); written fresh on native inputs.
18
+ */
19
+ import { bubbleLeft, moveThumb, normalizePair, percentOf, topThumb } from './sliderMath';
20
+
21
+ type Variant = 'default' | 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'error' | 'info';
22
+
2
23
  interface Props {
3
- value?: number;
24
+ value?: number | [number, number];
4
25
  min?: number;
5
26
  max?: number;
6
27
  step?: number;
7
28
  size?: 'xs' | 'sm' | 'md' | 'lg';
8
- variant?: 'default' | 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'error' | 'info';
29
+ variant?: Variant;
9
30
  showValue?: boolean;
10
31
  disabled?: boolean;
11
32
  name?: string;
12
33
  id?: string;
13
34
  onchange?: (value: number) => void;
35
+ /** Fires with the pair in two-thumb mode (and with a number otherwise). */
36
+ onvaluechange?: (value: number | [number, number]) => void;
37
+ /** Formats the bubble, the readout and aria-valuetext. */
38
+ formatValue?: (value: number) => string;
39
+ /** Value bubble above each thumb. Default `none`. */
40
+ bubble?: 'always' | 'drag' | 'none';
41
+ /** Accessible name(s). A string names a single thumb; a pair names the two. */
42
+ label?: string;
43
+ thumbLabels?: [string, string];
44
+ /** Smallest allowed distance between the two thumbs. */
45
+ minGap?: number;
14
46
  class?: string;
15
47
  }
16
48
 
@@ -26,6 +58,12 @@
26
58
  name,
27
59
  id,
28
60
  onchange,
61
+ onvaluechange,
62
+ formatValue,
63
+ bubble = 'none',
64
+ label,
65
+ thumbLabels,
66
+ minGap = 0,
29
67
  class: className = ''
30
68
  }: Props = $props();
31
69
 
@@ -43,14 +81,67 @@
43
81
  }[variant]
44
82
  );
45
83
  const rangeClass = $derived(['range', sizeClass, variantClass, className].filter(Boolean).join(' '));
84
+ // DaisyUI thumb sizes: --size-selector (0.25rem) × 4/5/6/7.
85
+ const thumbPx = $derived({ xs: 16, sm: 20, md: 24, lg: 28 }[size]);
86
+
87
+ const THUMBS = [0, 1] as const;
88
+ const fmt = (v: number) => (formatValue ? formatValue(v) : String(v));
89
+ const isRange = $derived(Array.isArray(value));
90
+
91
+ // ── single ────────────────────────────────────────────────────────
92
+ const single = $derived(typeof value === 'number' ? value : 0);
93
+ let singleActive = $state(false);
46
94
 
47
95
  function handleInput(e: Event) {
48
96
  value = Number((e.target as HTMLInputElement).value);
49
97
  onchange?.(value);
98
+ onvaluechange?.(value);
99
+ }
100
+
101
+ // ── range ─────────────────────────────────────────────────────────
102
+ const pair = $derived<[number, number]>(
103
+ Array.isArray(value) ? normalizePair(value, min, max, step) : [min, max]
104
+ );
105
+ let lastActive = $state<0 | 1>(1);
106
+ let active = $state<[boolean, boolean]>([false, false]);
107
+ const onTop = $derived(topThumb(pair, min, max, lastActive));
108
+ const p0 = $derived(percentOf(pair[0], min, max));
109
+ const p1 = $derived(percentOf(pair[1], min, max));
110
+
111
+ function handlePairInput(e: Event, index: 0 | 1) {
112
+ const input = e.target as HTMLInputElement;
113
+ const next = moveThumb(pair, index, Number(input.value), min, max, step, minGap);
114
+ // The native input may have moved past the other thumb — pull it back.
115
+ input.value = String(next[index]);
116
+ lastActive = index;
117
+ value = next;
118
+ onvaluechange?.(next);
50
119
  }
120
+
121
+ function setActive(index: 0 | 1, on: boolean) {
122
+ active = index === 0 ? [on, active[1]] : [active[0], on];
123
+ if (on) lastActive = index;
124
+ }
125
+
126
+ const showBubble = (on: boolean) => bubble === 'always' || (bubble === 'drag' && on);
127
+ const thumbNames = $derived<[string, string]>(
128
+ thumbLabels ?? [label ? `${label} minimum` : 'Minimum', label ? `${label} maximum` : 'Maximum']
129
+ );
130
+ const fillClass = $derived(
131
+ {
132
+ default: 'bg-base-content',
133
+ primary: 'bg-primary',
134
+ secondary: 'bg-secondary',
135
+ accent: 'bg-accent',
136
+ success: 'bg-success',
137
+ warning: 'bg-warning',
138
+ error: 'bg-error',
139
+ info: 'bg-info'
140
+ }[variant]
141
+ );
51
142
  </script>
52
143
 
53
- <div class="flex w-full flex-col gap-1">
144
+ {#snippet singleInput()}
54
145
  <input
55
146
  type="range"
56
147
  {name}
@@ -59,14 +150,171 @@
59
150
  {min}
60
151
  {max}
61
152
  {step}
62
- value={value}
153
+ value={single}
63
154
  {disabled}
64
155
  oninput={handleInput}
65
- aria-valuenow={value}
156
+ aria-label={label}
157
+ aria-valuenow={single}
66
158
  aria-valuemin={min}
67
159
  aria-valuemax={max}
160
+ aria-valuetext={formatValue ? fmt(single) : undefined}
161
+ onpointerdown={() => (singleActive = true)}
162
+ onpointerup={() => (singleActive = false)}
163
+ onfocus={(e) => (singleActive = (e.currentTarget as HTMLElement).matches(':focus-visible'))}
164
+ onblur={() => (singleActive = false)}
68
165
  />
69
- {#if showValue}
70
- <div class="text-base-content/60 w-full text-right text-xs tabular-nums">{value}</div>
71
- {/if}
72
- </div>
166
+ {/snippet}
167
+
168
+ {#if !isRange}
169
+ <div class="flex w-full flex-col gap-1" class:jatui-slider-bubbled={bubble !== 'none'}>
170
+ {#if bubble === 'none'}
171
+ {@render singleInput()}
172
+ {:else}
173
+ <div class="jatui-slider-single">
174
+ {@render singleInput()}
175
+ <span
176
+ class="jatui-slider-bubble bg-base-content text-base-100"
177
+ class:jatui-slider-bubble-on={showBubble(singleActive)}
178
+ style:left={bubbleLeft(percentOf(single, min, max), thumbPx)}
179
+ aria-hidden="true">{fmt(single)}</span
180
+ >
181
+ </div>
182
+ {/if}
183
+ {#if showValue}
184
+ <div class="text-base-content/60 w-full text-right text-xs tabular-nums">{fmt(single)}</div>
185
+ {/if}
186
+ </div>
187
+ {:else}
188
+ <div
189
+ class="jatui-slider-range flex w-full flex-col gap-1 {className}"
190
+ class:jatui-slider-bubbled={bubble !== 'none'}
191
+ class:jatui-slider-disabled={disabled}
192
+ role="group"
193
+ aria-label={label}
194
+ >
195
+ <div class="jatui-slider-stack" style:--jatui-thumb="{thumbPx}px">
196
+ <span class="jatui-slider-track bg-base-content/10" aria-hidden="true">
197
+ <span
198
+ class="jatui-slider-fill {fillClass}"
199
+ style:left={bubbleLeft(p0, thumbPx)}
200
+ style:right="calc(100% - {bubbleLeft(p1, thumbPx)})"
201
+ ></span>
202
+ </span>
203
+ {#each THUMBS as i (i)}
204
+ <input
205
+ type="range"
206
+ name={name ? `${name}${i === 0 ? '_min' : '_max'}` : undefined}
207
+ id={id ? `${id}-${i === 0 ? 'min' : 'max'}` : undefined}
208
+ class="{['range', sizeClass, variantClass].filter(Boolean).join(' ')} jatui-slider-thumb-input"
209
+ style:z-index={onTop === i ? 2 : 1}
210
+ {min}
211
+ {max}
212
+ {step}
213
+ value={pair[i]}
214
+ {disabled}
215
+ aria-label={thumbNames[i]}
216
+ aria-valuenow={pair[i]}
217
+ aria-valuemin={min}
218
+ aria-valuemax={max}
219
+ aria-valuetext={formatValue ? fmt(pair[i]) : undefined}
220
+ oninput={(e) => handlePairInput(e, i)}
221
+ onpointerdown={() => setActive(i, true)}
222
+ onpointerup={() => setActive(i, false)}
223
+ onfocus={(e) => setActive(i, (e.currentTarget as HTMLElement).matches(':focus-visible'))}
224
+ onblur={() => setActive(i, false)}
225
+ />
226
+ {#if bubble !== 'none'}
227
+ <span
228
+ class="jatui-slider-bubble bg-base-content text-base-100"
229
+ class:jatui-slider-bubble-on={showBubble(active[i])}
230
+ style:left={bubbleLeft(i === 0 ? p0 : p1, thumbPx)}
231
+ aria-hidden="true">{fmt(pair[i])}</span
232
+ >
233
+ {/if}
234
+ {/each}
235
+ </div>
236
+ {#if showValue}
237
+ <div class="text-base-content/60 w-full text-right text-xs tabular-nums">
238
+ {fmt(pair[0])} – {fmt(pair[1])}
239
+ </div>
240
+ {/if}
241
+ </div>
242
+ {/if}
243
+
244
+ <style>
245
+ .jatui-slider-single,
246
+ .jatui-slider-stack {
247
+ position: relative;
248
+ width: 100%;
249
+ }
250
+ /* Room for the bubble above the thumb, so it never covers content. */
251
+ .jatui-slider-bubbled {
252
+ padding-top: 1.75rem;
253
+ }
254
+
255
+ .jatui-slider-bubble {
256
+ position: absolute;
257
+ bottom: calc(100% + 4px);
258
+ transform: translate(-50%, 4px) scale(0.9);
259
+ padding: 1px 6px;
260
+ border-radius: var(--jatui-radius-selector, 0.375rem);
261
+ font-size: 0.75rem;
262
+ line-height: 1.125rem;
263
+ font-variant-numeric: tabular-nums;
264
+ white-space: nowrap;
265
+ pointer-events: none;
266
+ opacity: 0;
267
+ transition:
268
+ opacity var(--motion-fast, 150ms) var(--motion-glide, ease-out),
269
+ transform var(--motion-fast, 150ms) var(--motion-glide, ease-out);
270
+ }
271
+ .jatui-slider-bubble-on {
272
+ opacity: 1;
273
+ transform: translate(-50%, 0) scale(1);
274
+ }
275
+
276
+ /* ── two thumbs ─────────────────────────────────────────────── */
277
+ .jatui-slider-stack {
278
+ height: var(--jatui-thumb);
279
+ }
280
+ .jatui-slider-track {
281
+ position: absolute;
282
+ inset-inline: 0;
283
+ top: 50%;
284
+ height: calc(var(--jatui-thumb) * 0.5);
285
+ transform: translateY(-50%);
286
+ border-radius: var(--jatui-radius-selector, 0.375rem);
287
+ overflow: hidden;
288
+ }
289
+ .jatui-slider-fill {
290
+ position: absolute;
291
+ top: 0;
292
+ bottom: 0;
293
+ }
294
+ .jatui-slider-thumb-input {
295
+ position: absolute;
296
+ inset: 0;
297
+ width: 100%;
298
+ margin: 0;
299
+ /* Turn off DaisyUI's own fill and track: the stack draws one track. */
300
+ --range-fill: 0;
301
+ --range-bg: transparent;
302
+ background: transparent;
303
+ pointer-events: none;
304
+ }
305
+ .jatui-slider-thumb-input::-webkit-slider-thumb {
306
+ pointer-events: auto;
307
+ }
308
+ .jatui-slider-thumb-input::-moz-range-thumb {
309
+ pointer-events: auto;
310
+ }
311
+ .jatui-slider-disabled {
312
+ opacity: 0.5;
313
+ }
314
+
315
+ @media (prefers-reduced-motion: reduce) {
316
+ .jatui-slider-bubble {
317
+ transition: none;
318
+ }
319
+ }
320
+ </style>
@@ -1,15 +1,27 @@
1
+ type Variant = 'default' | 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'error' | 'info';
1
2
  interface Props {
2
- value?: number;
3
+ value?: number | [number, number];
3
4
  min?: number;
4
5
  max?: number;
5
6
  step?: number;
6
7
  size?: 'xs' | 'sm' | 'md' | 'lg';
7
- variant?: 'default' | 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'error' | 'info';
8
+ variant?: Variant;
8
9
  showValue?: boolean;
9
10
  disabled?: boolean;
10
11
  name?: string;
11
12
  id?: string;
12
13
  onchange?: (value: number) => void;
14
+ /** Fires with the pair in two-thumb mode (and with a number otherwise). */
15
+ onvaluechange?: (value: number | [number, number]) => void;
16
+ /** Formats the bubble, the readout and aria-valuetext. */
17
+ formatValue?: (value: number) => string;
18
+ /** Value bubble above each thumb. Default `none`. */
19
+ bubble?: 'always' | 'drag' | 'none';
20
+ /** Accessible name(s). A string names a single thumb; a pair names the two. */
21
+ label?: string;
22
+ thumbLabels?: [string, string];
23
+ /** Smallest allowed distance between the two thumbs. */
24
+ minGap?: number;
13
25
  class?: string;
14
26
  }
15
27
  declare const Slider: import("svelte").Component<Props, {}, "value">;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Pure helpers behind `Slider` — value snapping, two-thumb collision, and
3
+ * the bubble offset that keeps a value bubble centred over a native thumb.
4
+ * DOM-free so they unit-test under node:test.
5
+ */
6
+ /** Snap `v` to the step grid anchored at `min`, then clamp into [min, max]. */
7
+ export declare function snapToStep(v: number, min: number, max: number, step: number): number;
8
+ /**
9
+ * Move one thumb of a two-thumb range. The moved thumb is clamped so it
10
+ * never crosses the other one; `minGap` keeps them at least that far apart.
11
+ * Returns a new pair; the untouched thumb is never moved.
12
+ */
13
+ export declare function moveThumb(pair: readonly [number, number], index: 0 | 1, next: number, min: number, max: number, step: number, minGap?: number): [number, number];
14
+ /** Normalise any incoming value into a sorted, snapped pair. */
15
+ export declare function normalizePair(value: readonly [number, number], min: number, max: number, step: number): [number, number];
16
+ /** Position of `v` along the track as a 0–100 percentage. */
17
+ export declare function percentOf(v: number, min: number, max: number): number;
18
+ /**
19
+ * CSS `left` for a bubble centred over a native range thumb. A native thumb
20
+ * travels from `thumb/2` to `width - thumb/2`, not 0 to width, so a plain
21
+ * `p%` drifts off the thumb toward the ends. Returns a `calc()` string.
22
+ */
23
+ export declare function bubbleLeft(percent: number, thumbPx: number): string;
24
+ /**
25
+ * Which thumb is stacked on top. When the thumbs meet, the top one must be
26
+ * one that can still move: at the max end that's the lower thumb, at the min
27
+ * end the upper one, and in between the one the user touched last.
28
+ */
29
+ export declare function topThumb(pair: readonly [number, number], min: number, max: number, lastActive?: 0 | 1): 0 | 1;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Pure helpers behind `Slider` — value snapping, two-thumb collision, and
3
+ * the bubble offset that keeps a value bubble centred over a native thumb.
4
+ * DOM-free so they unit-test under node:test.
5
+ */
6
+ /** Snap `v` to the step grid anchored at `min`, then clamp into [min, max]. */
7
+ export function snapToStep(v, min, max, step) {
8
+ if (!Number.isFinite(v))
9
+ return min;
10
+ const s = step > 0 ? step : 1;
11
+ const snapped = min + Math.round((v - min) / s) * s;
12
+ // Round off float noise from the multiplication (0.1 steps etc).
13
+ const decimals = (String(s).split('.')[1] ?? '').length;
14
+ const clean = Number(snapped.toFixed(Math.min(decimals + 2, 10)));
15
+ return Math.min(max, Math.max(min, clean));
16
+ }
17
+ /**
18
+ * Move one thumb of a two-thumb range. The moved thumb is clamped so it
19
+ * never crosses the other one; `minGap` keeps them at least that far apart.
20
+ * Returns a new pair; the untouched thumb is never moved.
21
+ */
22
+ export function moveThumb(pair, index, next, min, max, step, minGap = 0) {
23
+ const v = snapToStep(next, min, max, step);
24
+ const gap = Math.max(0, minGap);
25
+ if (index === 0) {
26
+ return [Math.min(v, pair[1] - gap), pair[1]];
27
+ }
28
+ return [pair[0], Math.max(v, pair[0] + gap)];
29
+ }
30
+ /** Normalise any incoming value into a sorted, snapped pair. */
31
+ export function normalizePair(value, min, max, step) {
32
+ const a = snapToStep(value[0], min, max, step);
33
+ const b = snapToStep(value[1], min, max, step);
34
+ return a <= b ? [a, b] : [b, a];
35
+ }
36
+ /** Position of `v` along the track as a 0–100 percentage. */
37
+ export function percentOf(v, min, max) {
38
+ if (max <= min)
39
+ return 0;
40
+ return Math.min(100, Math.max(0, ((v - min) / (max - min)) * 100));
41
+ }
42
+ /**
43
+ * CSS `left` for a bubble centred over a native range thumb. A native thumb
44
+ * travels from `thumb/2` to `width - thumb/2`, not 0 to width, so a plain
45
+ * `p%` drifts off the thumb toward the ends. Returns a `calc()` string.
46
+ */
47
+ export function bubbleLeft(percent, thumbPx) {
48
+ const offset = thumbPx * (0.5 - percent / 100);
49
+ return `calc(${percent}% + ${Number(offset.toFixed(2))}px)`;
50
+ }
51
+ /**
52
+ * Which thumb is stacked on top. When the thumbs meet, the top one must be
53
+ * one that can still move: at the max end that's the lower thumb, at the min
54
+ * end the upper one, and in between the one the user touched last.
55
+ */
56
+ export function topThumb(pair, min, max, lastActive = 1) {
57
+ if (pair[0] >= max)
58
+ return 0;
59
+ if (pair[1] <= min)
60
+ return 1;
61
+ if (pair[0] === pair[1])
62
+ return lastActive;
63
+ return 1;
64
+ }
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CalendarHeader — previous/next month buttons around a live month title.
4
+ * Shared by every month panel (CalendarPicker, DatePicker, DateRangePicker,
5
+ * BookingScheduler). `title` replaces the plain heading (EventCalendar uses it
6
+ * for its month-switcher pill).
7
+ */
8
+ import type { Snippet } from 'svelte';
9
+ import IconButton from '../IconButton.svelte';
10
+
11
+ let {
12
+ label,
13
+ onPrev,
14
+ onNext,
15
+ canPrev = true,
16
+ canNext = true,
17
+ size = 'md',
18
+ hidePrev = false,
19
+ hideNext = false,
20
+ prevLabel = 'Previous month',
21
+ nextLabel = 'Next month',
22
+ title
23
+ }: {
24
+ label: string;
25
+ onPrev?: () => void;
26
+ onNext?: () => void;
27
+ canPrev?: boolean;
28
+ canNext?: boolean;
29
+ size?: 'sm' | 'md';
30
+ /** Hide one arrow (the inner edges of a two-month view). Keeps its space. */
31
+ hidePrev?: boolean;
32
+ hideNext?: boolean;
33
+ prevLabel?: string;
34
+ nextLabel?: string;
35
+ title?: Snippet;
36
+ } = $props();
37
+ </script>
38
+
39
+ <div class="jatui-cal-header">
40
+ <span class:jatui-cal-header__ghost={hidePrev}>
41
+ <IconButton
42
+ label={prevLabel}
43
+ size={size === 'sm' ? 'xs' : 'sm'}
44
+ shape="circle"
45
+ disabled={!canPrev || hidePrev}
46
+ onclick={() => onPrev?.()}
47
+ >
48
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" class="jatui-cal-header__chev">
49
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
50
+ </svg>
51
+ </IconButton>
52
+ </span>
53
+ {#if title}
54
+ {@render title()}
55
+ {:else}
56
+ <h3 class="jatui-cal-header__title" class:is-sm={size === 'sm'} aria-live="polite">{label}</h3>
57
+ {/if}
58
+ <span class:jatui-cal-header__ghost={hideNext}>
59
+ <IconButton
60
+ label={nextLabel}
61
+ size={size === 'sm' ? 'xs' : 'sm'}
62
+ shape="circle"
63
+ disabled={!canNext || hideNext}
64
+ onclick={() => onNext?.()}
65
+ >
66
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" class="jatui-cal-header__chev">
67
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
68
+ </svg>
69
+ </IconButton>
70
+ </span>
71
+ </div>
72
+
73
+ <style>
74
+ .jatui-cal-header {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: space-between;
78
+ gap: 0.5rem;
79
+ margin-block-end: 0.5rem;
80
+ }
81
+ .jatui-cal-header__title {
82
+ margin: 0;
83
+ font-size: 1rem;
84
+ font-weight: 600;
85
+ text-align: center;
86
+ flex: 1;
87
+ }
88
+ .jatui-cal-header__title.is-sm {
89
+ font-size: 0.875rem;
90
+ }
91
+ .jatui-cal-header__ghost {
92
+ visibility: hidden;
93
+ }
94
+ /* Chevrons point the reading direction. */
95
+ :global([dir='rtl']) .jatui-cal-header__chev {
96
+ transform: scaleX(-1);
97
+ }
98
+ </style>
@@ -0,0 +1,24 @@
1
+ /**
2
+ * CalendarHeader — previous/next month buttons around a live month title.
3
+ * Shared by every month panel (CalendarPicker, DatePicker, DateRangePicker,
4
+ * BookingScheduler). `title` replaces the plain heading (EventCalendar uses it
5
+ * for its month-switcher pill).
6
+ */
7
+ import type { Snippet } from 'svelte';
8
+ type $$ComponentProps = {
9
+ label: string;
10
+ onPrev?: () => void;
11
+ onNext?: () => void;
12
+ canPrev?: boolean;
13
+ canNext?: boolean;
14
+ size?: 'sm' | 'md';
15
+ /** Hide one arrow (the inner edges of a two-month view). Keeps its space. */
16
+ hidePrev?: boolean;
17
+ hideNext?: boolean;
18
+ prevLabel?: string;
19
+ nextLabel?: string;
20
+ title?: Snippet;
21
+ };
22
+ declare const CalendarHeader: import("svelte").Component<$$ComponentProps, {}, "">;
23
+ type CalendarHeader = ReturnType<typeof CalendarHeader>;
24
+ export default CalendarHeader;