@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.
- package/CLAUDE.md +32 -6
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/dist/actions/dismissOutside.d.ts +52 -0
- package/dist/actions/dismissOutside.js +80 -0
- package/dist/components/BookingScheduler.svelte +412 -0
- package/dist/components/BookingScheduler.svelte.d.ts +66 -0
- package/dist/components/CalendarPicker.svelte +190 -205
- package/dist/components/CalendarPicker.svelte.d.ts +19 -1
- package/dist/components/DatePicker.svelte +419 -0
- package/dist/components/DatePicker.svelte.d.ts +44 -0
- package/dist/components/DateRangePicker.svelte +471 -5
- package/dist/components/DateRangePicker.svelte.d.ts +32 -3
- package/dist/components/EventCalendar.svelte +1324 -0
- package/dist/components/EventCalendar.svelte.d.ts +101 -0
- package/dist/components/FileDropzone.svelte +165 -3
- package/dist/components/FileDropzone.svelte.d.ts +9 -0
- package/dist/components/TextInput.svelte +71 -20
- package/dist/components/TextInput.svelte.d.ts +7 -0
- package/dist/components/ThumbnailGrid.svelte +106 -1
- package/dist/components/ThumbnailGrid.svelte.d.ts +7 -0
- package/dist/components/TimeSlotPicker.svelte +174 -108
- package/dist/components/TimeSlotPicker.svelte.d.ts +27 -0
- package/dist/components/basic/Alert.svelte +113 -16
- package/dist/components/basic/Alert.svelte.d.ts +17 -1
- package/dist/components/basic/Checkbox.svelte +53 -1
- package/dist/components/basic/Checkbox.svelte.d.ts +6 -0
- package/dist/components/basic/RadioGroup.svelte +59 -3
- package/dist/components/basic/RadioGroup.svelte.d.ts +15 -0
- package/dist/components/basic/Slider.svelte +257 -9
- package/dist/components/basic/Slider.svelte.d.ts +14 -2
- package/dist/components/basic/sliderMath.d.ts +29 -0
- package/dist/components/basic/sliderMath.js +64 -0
- package/dist/components/calendar/CalendarHeader.svelte +98 -0
- package/dist/components/calendar/CalendarHeader.svelte.d.ts +24 -0
- package/dist/components/calendar/MonthGrid.svelte +361 -0
- package/dist/components/calendar/MonthGrid.svelte.d.ts +55 -0
- package/dist/components/calendar/calendarGrid.d.ts +219 -0
- package/dist/components/calendar/calendarGrid.js +494 -0
- package/dist/components/calendar/createCalendarGrid.d.ts +25 -0
- package/dist/components/calendar/createCalendarGrid.js +57 -0
- package/dist/components/calendar/dayOverlap.d.ts +92 -0
- package/dist/components/calendar/dayOverlap.js +147 -0
- package/dist/components/calendar/eventCalendar.d.ts +75 -0
- package/dist/components/calendar/eventCalendar.js +187 -0
- package/dist/components/calendar/index.d.ts +32 -0
- package/dist/components/calendar/index.js +25 -0
- package/dist/components/calendar/monthPacking.d.ts +97 -0
- package/dist/components/calendar/monthPacking.js +254 -0
- package/dist/components/calendar/timeSlots.d.ts +57 -0
- package/dist/components/calendar/timeSlots.js +163 -0
- package/dist/components/field/FieldHint.svelte +29 -0
- package/dist/components/field/FieldHint.svelte.d.ts +17 -0
- package/dist/components/field/FieldLabel.svelte +100 -0
- package/dist/components/field/FieldLabel.svelte.d.ts +29 -0
- package/dist/components/form-cards/CheckGlyph.svelte +138 -0
- package/dist/components/form-cards/CheckGlyph.svelte.d.ts +29 -0
- package/dist/components/form-cards/CheckboxCard.svelte +120 -0
- package/dist/components/form-cards/CheckboxCard.svelte.d.ts +33 -0
- package/dist/components/form-cards/ChoiceCard.svelte +156 -0
- package/dist/components/form-cards/ChoiceCard.svelte.d.ts +42 -0
- package/dist/components/form-cards/SwitchCard.svelte +114 -0
- package/dist/components/form-cards/SwitchCard.svelte.d.ts +28 -0
- package/dist/components/toast/ToastHost.svelte +56 -3
- package/dist/components/toast/ToastHost.svelte.d.ts +9 -1
- package/dist/components/toast/ToastItem.svelte +75 -23
- package/dist/components/toast/ToastItem.svelte.d.ts +2 -0
- package/dist/components/uploadProgress.d.ts +30 -0
- package/dist/components/uploadProgress.js +47 -0
- package/dist/index.d.ts +29 -2
- package/dist/index.js +22 -1
- package/dist/stores/toast.d.ts +35 -0
- package/dist/stores/toast.js +15 -1
- package/dist/styles/motion.css +72 -0
- package/dist/styles/rail.css +51 -0
- 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
|
-
[
|
|
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?:
|
|
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
|
-
|
|
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={
|
|
153
|
+
value={single}
|
|
63
154
|
{disabled}
|
|
64
155
|
oninput={handleInput}
|
|
65
|
-
aria-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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?:
|
|
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;
|