@joewinke/jatui 0.42.0 → 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 +23 -3
- 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/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/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/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 +12 -2
- package/dist/index.js +11 -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 +1 -1
|
@@ -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,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FieldHint — the help / error line under a field. With `error` it turns
|
|
4
|
+
* error-toned and announces (`role="alert"`). Give it the id your control
|
|
5
|
+
* points `aria-describedby` at.
|
|
6
|
+
*/
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
id?: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
/** Error tone + role="alert". */
|
|
13
|
+
error?: boolean;
|
|
14
|
+
class?: string;
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { id, text, error = false, class: className = '', children }: Props = $props();
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<div class="label {className}">
|
|
22
|
+
<span
|
|
23
|
+
{id}
|
|
24
|
+
class="text-[0.8125rem] {error ? 'text-error' : 'text-base-content/45'}"
|
|
25
|
+
role={error ? 'alert' : undefined}
|
|
26
|
+
>
|
|
27
|
+
{#if children}{@render children()}{:else}{text}{/if}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FieldHint — the help / error line under a field. With `error` it turns
|
|
3
|
+
* error-toned and announces (`role="alert"`). Give it the id your control
|
|
4
|
+
* points `aria-describedby` at.
|
|
5
|
+
*/
|
|
6
|
+
import type { Snippet } from 'svelte';
|
|
7
|
+
interface Props {
|
|
8
|
+
id?: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
/** Error tone + role="alert". */
|
|
11
|
+
error?: boolean;
|
|
12
|
+
class?: string;
|
|
13
|
+
children?: Snippet;
|
|
14
|
+
}
|
|
15
|
+
declare const FieldHint: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type FieldHint = ReturnType<typeof FieldHint>;
|
|
17
|
+
export default FieldHint;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FieldLabel — the label row every jatui field uses (TextInput today), so
|
|
4
|
+
* custom controls (PhoneInput, SearchDropdown, your own) line up with it.
|
|
5
|
+
*
|
|
6
|
+
* `required` adds the asterisk; `tooltip` adds a small info button after
|
|
7
|
+
* the text that shows the hint through the shared `use:tooltip` action
|
|
8
|
+
* (hover AND keyboard focus, with `aria-describedby`). The button sits
|
|
9
|
+
* outside the `<label>` element so clicking it never focuses the input.
|
|
10
|
+
*
|
|
11
|
+
* Separate label/hint primitives after BoardUI's input parts (MIT © 2026
|
|
12
|
+
* Mertcan Dundar Esmergul); written fresh for Svelte and DaisyUI.
|
|
13
|
+
*/
|
|
14
|
+
import type { Snippet } from 'svelte';
|
|
15
|
+
import { tooltip as tooltipAction } from '../../actions/tooltip';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** id of the control this labels. */
|
|
19
|
+
for?: string;
|
|
20
|
+
text?: string;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
/** Info-icon hint after the label text. */
|
|
23
|
+
tooltip?: string;
|
|
24
|
+
/** Accessible name of the info button. Default "More information". */
|
|
25
|
+
tooltipLabel?: string;
|
|
26
|
+
id?: string;
|
|
27
|
+
class?: string;
|
|
28
|
+
children?: Snippet;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
for: htmlFor,
|
|
33
|
+
text,
|
|
34
|
+
required = false,
|
|
35
|
+
tooltip,
|
|
36
|
+
tooltipLabel = 'More information',
|
|
37
|
+
id,
|
|
38
|
+
class: className = '',
|
|
39
|
+
children
|
|
40
|
+
}: Props = $props();
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
{#snippet labelEl(extra: string)}
|
|
44
|
+
<label for={htmlFor} {id} class={extra ? `label ${extra}` : 'label'}>
|
|
45
|
+
<span class="text-[0.8125rem] font-medium text-base-content/85">
|
|
46
|
+
{#if children}{@render children()}{:else}{text}{/if}
|
|
47
|
+
{#if required}
|
|
48
|
+
<span class="text-error ml-1">*</span>
|
|
49
|
+
{/if}
|
|
50
|
+
</span>
|
|
51
|
+
</label>
|
|
52
|
+
{/snippet}
|
|
53
|
+
|
|
54
|
+
{#if tooltip}
|
|
55
|
+
<div class="jatui-field-label-row {className}">
|
|
56
|
+
{@render labelEl('')}
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
class="jatui-field-tip text-base-content/50 hover:text-base-content/80"
|
|
60
|
+
aria-label={tooltipLabel}
|
|
61
|
+
use:tooltipAction={{ text: tooltip, placement: 'top' }}
|
|
62
|
+
>
|
|
63
|
+
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
64
|
+
<circle cx="8" cy="8" r="6.25" stroke="currentColor" stroke-width="1.5" />
|
|
65
|
+
<path d="M8 7.25v3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
66
|
+
<circle cx="8" cy="5.1" r="0.85" fill="currentColor" />
|
|
67
|
+
</svg>
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
{:else}
|
|
71
|
+
{@render labelEl(className)}
|
|
72
|
+
{/if}
|
|
73
|
+
|
|
74
|
+
<style>
|
|
75
|
+
/* Without a tooltip there is no row — just the bare <label>, exactly the
|
|
76
|
+
markup TextInput rendered before FieldLabel existed. */
|
|
77
|
+
.jatui-field-label-row {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: var(--jatui-space-sm, 4px);
|
|
81
|
+
}
|
|
82
|
+
.jatui-field-tip {
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
/* 24px target (WCAG 2.5.8) around a 14px glyph */
|
|
87
|
+
width: 1.5rem;
|
|
88
|
+
height: 1.5rem;
|
|
89
|
+
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
90
|
+
cursor: help;
|
|
91
|
+
}
|
|
92
|
+
.jatui-field-tip svg {
|
|
93
|
+
width: 0.875rem;
|
|
94
|
+
height: 0.875rem;
|
|
95
|
+
}
|
|
96
|
+
.jatui-field-tip:focus-visible {
|
|
97
|
+
outline: 2px solid color-mix(in oklch, var(--color-primary) 65%, transparent);
|
|
98
|
+
outline-offset: 1px;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FieldLabel — the label row every jatui field uses (TextInput today), so
|
|
3
|
+
* custom controls (PhoneInput, SearchDropdown, your own) line up with it.
|
|
4
|
+
*
|
|
5
|
+
* `required` adds the asterisk; `tooltip` adds a small info button after
|
|
6
|
+
* the text that shows the hint through the shared `use:tooltip` action
|
|
7
|
+
* (hover AND keyboard focus, with `aria-describedby`). The button sits
|
|
8
|
+
* outside the `<label>` element so clicking it never focuses the input.
|
|
9
|
+
*
|
|
10
|
+
* Separate label/hint primitives after BoardUI's input parts (MIT © 2026
|
|
11
|
+
* Mertcan Dundar Esmergul); written fresh for Svelte and DaisyUI.
|
|
12
|
+
*/
|
|
13
|
+
import type { Snippet } from 'svelte';
|
|
14
|
+
interface Props {
|
|
15
|
+
/** id of the control this labels. */
|
|
16
|
+
for?: string;
|
|
17
|
+
text?: string;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
/** Info-icon hint after the label text. */
|
|
20
|
+
tooltip?: string;
|
|
21
|
+
/** Accessible name of the info button. Default "More information". */
|
|
22
|
+
tooltipLabel?: string;
|
|
23
|
+
id?: string;
|
|
24
|
+
class?: string;
|
|
25
|
+
children?: Snippet;
|
|
26
|
+
}
|
|
27
|
+
declare const FieldLabel: import("svelte").Component<Props, {}, "">;
|
|
28
|
+
type FieldLabel = ReturnType<typeof FieldLabel>;
|
|
29
|
+
export default FieldLabel;
|