@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.
Files changed (45) hide show
  1. package/CLAUDE.md +23 -3
  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/FileDropzone.svelte +165 -3
  6. package/dist/components/FileDropzone.svelte.d.ts +9 -0
  7. package/dist/components/TextInput.svelte +71 -20
  8. package/dist/components/TextInput.svelte.d.ts +7 -0
  9. package/dist/components/ThumbnailGrid.svelte +106 -1
  10. package/dist/components/ThumbnailGrid.svelte.d.ts +7 -0
  11. package/dist/components/basic/Alert.svelte +113 -16
  12. package/dist/components/basic/Alert.svelte.d.ts +17 -1
  13. package/dist/components/basic/Checkbox.svelte +53 -1
  14. package/dist/components/basic/Checkbox.svelte.d.ts +6 -0
  15. package/dist/components/basic/RadioGroup.svelte +59 -3
  16. package/dist/components/basic/RadioGroup.svelte.d.ts +15 -0
  17. package/dist/components/basic/Slider.svelte +257 -9
  18. package/dist/components/basic/Slider.svelte.d.ts +14 -2
  19. package/dist/components/basic/sliderMath.d.ts +29 -0
  20. package/dist/components/basic/sliderMath.js +64 -0
  21. package/dist/components/field/FieldHint.svelte +29 -0
  22. package/dist/components/field/FieldHint.svelte.d.ts +17 -0
  23. package/dist/components/field/FieldLabel.svelte +100 -0
  24. package/dist/components/field/FieldLabel.svelte.d.ts +29 -0
  25. package/dist/components/form-cards/CheckGlyph.svelte +138 -0
  26. package/dist/components/form-cards/CheckGlyph.svelte.d.ts +29 -0
  27. package/dist/components/form-cards/CheckboxCard.svelte +120 -0
  28. package/dist/components/form-cards/CheckboxCard.svelte.d.ts +33 -0
  29. package/dist/components/form-cards/ChoiceCard.svelte +156 -0
  30. package/dist/components/form-cards/ChoiceCard.svelte.d.ts +42 -0
  31. package/dist/components/form-cards/SwitchCard.svelte +114 -0
  32. package/dist/components/form-cards/SwitchCard.svelte.d.ts +28 -0
  33. package/dist/components/toast/ToastHost.svelte +56 -3
  34. package/dist/components/toast/ToastHost.svelte.d.ts +9 -1
  35. package/dist/components/toast/ToastItem.svelte +75 -23
  36. package/dist/components/toast/ToastItem.svelte.d.ts +2 -0
  37. package/dist/components/uploadProgress.d.ts +30 -0
  38. package/dist/components/uploadProgress.js +47 -0
  39. package/dist/index.d.ts +12 -2
  40. package/dist/index.js +11 -1
  41. package/dist/stores/toast.d.ts +35 -0
  42. package/dist/stores/toast.js +15 -1
  43. package/dist/styles/motion.css +72 -0
  44. package/dist/styles/rail.css +51 -0
  45. 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?: '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,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;