@joewinke/jatui 0.42.0 → 0.44.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 (49) hide show
  1. package/CLAUDE.md +24 -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/ImageReveal.svelte +353 -0
  8. package/dist/components/ImageReveal.svelte.d.ts +31 -0
  9. package/dist/components/TextInput.svelte +71 -20
  10. package/dist/components/TextInput.svelte.d.ts +7 -0
  11. package/dist/components/ThumbnailGrid.svelte +106 -1
  12. package/dist/components/ThumbnailGrid.svelte.d.ts +7 -0
  13. package/dist/components/basic/Alert.svelte +113 -16
  14. package/dist/components/basic/Alert.svelte.d.ts +17 -1
  15. package/dist/components/basic/Checkbox.svelte +53 -1
  16. package/dist/components/basic/Checkbox.svelte.d.ts +6 -0
  17. package/dist/components/basic/RadioGroup.svelte +59 -3
  18. package/dist/components/basic/RadioGroup.svelte.d.ts +15 -0
  19. package/dist/components/basic/Slider.svelte +257 -9
  20. package/dist/components/basic/Slider.svelte.d.ts +14 -2
  21. package/dist/components/basic/sliderMath.d.ts +29 -0
  22. package/dist/components/basic/sliderMath.js +64 -0
  23. package/dist/components/field/FieldHint.svelte +29 -0
  24. package/dist/components/field/FieldHint.svelte.d.ts +17 -0
  25. package/dist/components/field/FieldLabel.svelte +100 -0
  26. package/dist/components/field/FieldLabel.svelte.d.ts +29 -0
  27. package/dist/components/form-cards/CheckGlyph.svelte +138 -0
  28. package/dist/components/form-cards/CheckGlyph.svelte.d.ts +29 -0
  29. package/dist/components/form-cards/CheckboxCard.svelte +120 -0
  30. package/dist/components/form-cards/CheckboxCard.svelte.d.ts +33 -0
  31. package/dist/components/form-cards/ChoiceCard.svelte +156 -0
  32. package/dist/components/form-cards/ChoiceCard.svelte.d.ts +42 -0
  33. package/dist/components/form-cards/SwitchCard.svelte +114 -0
  34. package/dist/components/form-cards/SwitchCard.svelte.d.ts +28 -0
  35. package/dist/components/imageReveal.d.ts +51 -0
  36. package/dist/components/imageReveal.js +83 -0
  37. package/dist/components/toast/ToastHost.svelte +56 -3
  38. package/dist/components/toast/ToastHost.svelte.d.ts +9 -1
  39. package/dist/components/toast/ToastItem.svelte +75 -23
  40. package/dist/components/toast/ToastItem.svelte.d.ts +2 -0
  41. package/dist/components/uploadProgress.d.ts +30 -0
  42. package/dist/components/uploadProgress.js +47 -0
  43. package/dist/index.d.ts +15 -2
  44. package/dist/index.js +13 -1
  45. package/dist/stores/toast.d.ts +35 -0
  46. package/dist/stores/toast.js +15 -1
  47. package/dist/styles/motion.css +72 -0
  48. package/dist/styles/rail.css +51 -0
  49. package/package.json +1 -1
@@ -0,0 +1,353 @@
1
+ <!--
2
+ ImageReveal — a frame that shows a "generating" ripple until its image
3
+ exists, then brings the image in blur-to-sharp. Built for slots whose image
4
+ arrives late: a logo being looked up, an AI generation, a thumbnail still
5
+ rendering.
6
+
7
+ src null / undefined → generating: ripple on the tone, role="status",
8
+ aria-busy
9
+ src set, not decoded → still the ripple (no half-loaded image is shown)
10
+ src loaded → blur + slight scale → sharp, ripple fades; onReveal
11
+ src '' or onerror → calm initials (from alt) or an image glyph — never a
12
+ broken image
13
+
14
+ Colours are one tone mixed with color-mix (no hex). The ripple is a
15
+ decorative loop: .jatui-motion-ambient, and under data-motion="calm" or
16
+ prefers-reduced-motion it becomes a static tint and the image swaps in
17
+ without the blur.
18
+
19
+ Clean-room (jatui-vui9q): BoardUI's image-generation effect is a Pro item;
20
+ nothing from it was read or copied.
21
+ -->
22
+ <script lang="ts">
23
+ import type { Snippet } from 'svelte';
24
+ import {
25
+ IMAGE_REVEAL_RADIUS,
26
+ imageRevealBusy,
27
+ imageRevealInitials,
28
+ imageRevealPhase,
29
+ imageRevealSize,
30
+ imageRevealTone,
31
+ type ImageRevealPhase,
32
+ type ImageRevealShape,
33
+ type ImageRevealSizeName,
34
+ type ImageRevealToneName
35
+ } from './imageReveal';
36
+
37
+ interface Props {
38
+ /** null / undefined = still generating. '' = nothing to show (fallback). */
39
+ src?: string | null;
40
+ /** Alt text for the image; also the source of the fallback initials. */
41
+ alt?: string;
42
+ shape?: ImageRevealShape;
43
+ /** Named size, px number, any CSS length, or 'fill' (100% width). Always square. */
44
+ size?: ImageRevealSizeName | number | (string & {});
45
+ /** DaisyUI semantic name or any CSS colour. */
46
+ tone?: ImageRevealToneName | (string & {});
47
+ /** Screen-reader status while generating. */
48
+ label?: string;
49
+ /** Override the letters derived from `alt`. '' forces the glyph. */
50
+ initials?: string;
51
+ /** object-fit for the image. Logos usually want 'contain'. */
52
+ fit?: 'cover' | 'contain';
53
+ /** Read-only: the current phase. */
54
+ phase?: ImageRevealPhase;
55
+ /** Fires once per src, when that image has loaded. */
56
+ onReveal?: (src: string) => void;
57
+ /** Fires once per src, when that image failed to load. */
58
+ onError?: (src: string) => void;
59
+ /** Replaces the initials / glyph fallback. */
60
+ fallback?: Snippet;
61
+ class?: string;
62
+ }
63
+
64
+ let {
65
+ src = null,
66
+ alt = '',
67
+ shape = 'rounded',
68
+ size = 'md',
69
+ tone = 'primary',
70
+ label = 'Generating image',
71
+ initials,
72
+ fit = 'cover',
73
+ phase = $bindable('generating'),
74
+ onReveal,
75
+ onError,
76
+ fallback,
77
+ class: className = ''
78
+ }: Props = $props();
79
+
80
+ let loadedSrc = $state<string | null>(null);
81
+ let failedSrc = $state<string | null>(null);
82
+ let img = $state<HTMLImageElement | null>(null);
83
+
84
+ const current = $derived(imageRevealPhase({ src, loadedSrc, failedSrc }));
85
+ const busy = $derived(imageRevealBusy(current));
86
+ const letters = $derived(initials ?? imageRevealInitials(alt));
87
+
88
+ $effect.pre(() => {
89
+ phase = current;
90
+ });
91
+
92
+ function loaded(from: string) {
93
+ if (from !== src || loadedSrc === from) return;
94
+ loadedSrc = from;
95
+ onReveal?.(from);
96
+ }
97
+
98
+ function failed(from: string) {
99
+ if (from !== src || failedSrc === from) return;
100
+ failedSrc = from;
101
+ onError?.(from);
102
+ }
103
+
104
+ // An image served from cache (or loaded before hydration) can finish before
105
+ // the load listener exists, so read its state once it is in the DOM.
106
+ $effect(() => {
107
+ if (!img || !src) return;
108
+ const el = img;
109
+ const from = src;
110
+ if (el.complete) {
111
+ if (el.naturalWidth > 0) loaded(from);
112
+ else if (el.currentSrc || el.getAttribute('src')) failed(from);
113
+ }
114
+ });
115
+ </script>
116
+
117
+ <div
118
+ class="jatui-image-reveal is-{current} {className}"
119
+ style:--jatui-ir-tone={imageRevealTone(tone)}
120
+ style:--jatui-ir-size={imageRevealSize(size)}
121
+ style:--jatui-ir-radius={IMAGE_REVEAL_RADIUS[shape]}
122
+ aria-busy={busy}
123
+ data-phase={current}
124
+ >
125
+ <span class="jatui-ir-status" role="status">{busy ? label : ''}</span>
126
+
127
+ {#if current !== 'fallback'}
128
+ <div class="jatui-ir-ripple" class:jatui-motion-ambient={busy} aria-hidden="true">
129
+ <span class="jatui-ir-ring"></span>
130
+ <span class="jatui-ir-ring"></span>
131
+ <span class="jatui-ir-ring"></span>
132
+ <span class="jatui-ir-core"></span>
133
+ </div>
134
+ {/if}
135
+
136
+ {#if src && current !== 'fallback'}
137
+ {#key src}
138
+ <img
139
+ bind:this={img}
140
+ class="jatui-ir-img"
141
+ class:is-shown={current === 'revealed'}
142
+ style:object-fit={fit}
143
+ {src}
144
+ {alt}
145
+ decoding="async"
146
+ onload={() => loaded(src!)}
147
+ onerror={() => failed(src!)}
148
+ />
149
+ {/key}
150
+ {:else if current === 'fallback'}
151
+ {#if fallback}
152
+ <div class="jatui-ir-fallback">{@render fallback()}</div>
153
+ {:else if letters}
154
+ <div class="jatui-ir-fallback" role={alt ? 'img' : undefined} aria-label={alt || undefined}>
155
+ <span class="jatui-ir-initials" aria-hidden="true">{letters}</span>
156
+ </div>
157
+ {:else}
158
+ <div class="jatui-ir-fallback" role={alt ? 'img' : undefined} aria-label={alt || undefined}>
159
+ <svg class="jatui-ir-glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
160
+ <rect x="3" y="4" width="18" height="16" rx="2" />
161
+ <circle cx="9" cy="10" r="1.75" />
162
+ <path d="m21 16-5-5-9 9" stroke-linecap="round" stroke-linejoin="round" />
163
+ </svg>
164
+ </div>
165
+ {/if}
166
+ {/if}
167
+ </div>
168
+
169
+ <style>
170
+ .jatui-image-reveal {
171
+ --jatui-ir-reveal: calc(var(--motion-move, 300ms) * 2);
172
+ position: relative;
173
+ display: inline-grid;
174
+ place-items: center;
175
+ width: var(--jatui-ir-size);
176
+ max-width: 100%;
177
+ aspect-ratio: 1;
178
+ overflow: hidden;
179
+ isolation: isolate;
180
+ container-type: inline-size;
181
+ border-radius: var(--jatui-ir-radius);
182
+ background: color-mix(in oklab, var(--jatui-ir-tone) 7%, var(--color-base-100, Canvas));
183
+ box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--jatui-ir-tone) 14%, transparent);
184
+ vertical-align: middle;
185
+ }
186
+
187
+ .jatui-ir-status {
188
+ position: absolute;
189
+ width: 1px;
190
+ height: 1px;
191
+ margin: -1px;
192
+ overflow: hidden;
193
+ clip-path: inset(50%);
194
+ white-space: nowrap;
195
+ }
196
+
197
+ /* ---- generating: ripple ---- */
198
+ .jatui-ir-ripple {
199
+ position: absolute;
200
+ inset: 0;
201
+ display: grid;
202
+ place-items: center;
203
+ background: radial-gradient(
204
+ circle at 50% 50%,
205
+ color-mix(in oklab, var(--jatui-ir-tone) 16%, transparent) 0%,
206
+ transparent 70%
207
+ );
208
+ transition: opacity var(--jatui-ir-reveal) var(--motion-glide, ease-out);
209
+ }
210
+
211
+ .jatui-ir-ring,
212
+ .jatui-ir-core {
213
+ grid-area: 1 / 1;
214
+ border-radius: 9999px;
215
+ }
216
+
217
+ .jatui-ir-ring {
218
+ width: 90%;
219
+ aspect-ratio: 1;
220
+ border: max(1px, 2cqi) solid
221
+ color-mix(in oklab, var(--jatui-ir-tone) 45%, transparent);
222
+ opacity: 0;
223
+ transform: scale(0.2);
224
+ animation: jatui-ir-ripple 2400ms var(--motion-glide, ease-out) infinite;
225
+ }
226
+
227
+ .jatui-ir-ring:nth-child(2) {
228
+ animation-delay: 800ms;
229
+ }
230
+
231
+ .jatui-ir-ring:nth-child(3) {
232
+ animation-delay: 1600ms;
233
+ }
234
+
235
+ .jatui-ir-core {
236
+ width: 22%;
237
+ aspect-ratio: 1;
238
+ background: color-mix(in oklab, var(--jatui-ir-tone) 55%, transparent);
239
+ filter: blur(4cqi);
240
+ animation: jatui-ir-breathe 2400ms ease-in-out infinite;
241
+ }
242
+
243
+ @keyframes jatui-ir-ripple {
244
+ 0% {
245
+ opacity: 0;
246
+ transform: scale(0.2);
247
+ }
248
+ 15% {
249
+ opacity: 1;
250
+ }
251
+ 100% {
252
+ opacity: 0;
253
+ transform: scale(1.35);
254
+ }
255
+ }
256
+
257
+ @keyframes jatui-ir-breathe {
258
+ 0%,
259
+ 100% {
260
+ opacity: 0.55;
261
+ transform: scale(0.85);
262
+ }
263
+ 50% {
264
+ opacity: 1;
265
+ transform: scale(1.1);
266
+ }
267
+ }
268
+
269
+ /* ---- reveal ---- */
270
+ .jatui-ir-img {
271
+ position: absolute;
272
+ inset: 0;
273
+ width: 100%;
274
+ height: 100%;
275
+ opacity: 0;
276
+ filter: blur(calc(12cqi + 4px));
277
+ transform: scale(1.06);
278
+ transition:
279
+ opacity var(--jatui-ir-reveal) var(--motion-glide, ease-out),
280
+ filter var(--jatui-ir-reveal) var(--motion-glide, ease-out),
281
+ transform var(--jatui-ir-reveal) var(--motion-glide, ease-out);
282
+ }
283
+
284
+ .jatui-ir-img.is-shown {
285
+ opacity: 1;
286
+ filter: none;
287
+ transform: none;
288
+ }
289
+
290
+ .is-revealed .jatui-ir-ripple {
291
+ opacity: 0;
292
+ visibility: hidden;
293
+ transition:
294
+ opacity var(--jatui-ir-reveal) var(--motion-glide, ease-out),
295
+ visibility 0s linear var(--jatui-ir-reveal);
296
+ }
297
+
298
+ .is-revealed {
299
+ background: transparent;
300
+ box-shadow: none;
301
+ }
302
+
303
+ /* ---- fallback ---- */
304
+ .jatui-ir-fallback {
305
+ position: absolute;
306
+ inset: 0;
307
+ display: grid;
308
+ place-items: center;
309
+ color: color-mix(in oklab, var(--jatui-ir-tone) 75%, var(--color-base-content, CanvasText));
310
+ }
311
+
312
+ .jatui-ir-initials {
313
+ font-weight: 600;
314
+ font-size: 36cqi;
315
+ line-height: 1;
316
+ letter-spacing: 0.02em;
317
+ }
318
+
319
+ .jatui-ir-glyph {
320
+ width: 42%;
321
+ height: 42%;
322
+ opacity: 0.7;
323
+ }
324
+
325
+ /* ---- calm / reduced motion: static tint, instant swap ---- */
326
+ :global([data-motion='calm']) .jatui-ir-ring,
327
+ :global([data-motion='calm']) .jatui-ir-core,
328
+ .jatui-image-reveal:global([data-motion='calm']) .jatui-ir-ring,
329
+ .jatui-image-reveal:global([data-motion='calm']) .jatui-ir-core {
330
+ animation: none;
331
+ }
332
+ :global([data-motion='calm']) .jatui-ir-ring,
333
+ .jatui-image-reveal:global([data-motion='calm']) .jatui-ir-ring {
334
+ opacity: 0;
335
+ }
336
+ :global([data-motion='calm']) .jatui-image-reveal,
337
+ .jatui-image-reveal:global([data-motion='calm']) {
338
+ --jatui-ir-reveal: 0s;
339
+ }
340
+
341
+ @media (prefers-reduced-motion: reduce) {
342
+ .jatui-image-reveal {
343
+ --jatui-ir-reveal: 0s;
344
+ }
345
+ .jatui-ir-ring,
346
+ .jatui-ir-core {
347
+ animation: none;
348
+ }
349
+ .jatui-ir-ring {
350
+ opacity: 0;
351
+ }
352
+ }
353
+ </style>
@@ -0,0 +1,31 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type ImageRevealPhase, type ImageRevealShape, type ImageRevealSizeName, type ImageRevealToneName } from './imageReveal';
3
+ interface Props {
4
+ /** null / undefined = still generating. '' = nothing to show (fallback). */
5
+ src?: string | null;
6
+ /** Alt text for the image; also the source of the fallback initials. */
7
+ alt?: string;
8
+ shape?: ImageRevealShape;
9
+ /** Named size, px number, any CSS length, or 'fill' (100% width). Always square. */
10
+ size?: ImageRevealSizeName | number | (string & {});
11
+ /** DaisyUI semantic name or any CSS colour. */
12
+ tone?: ImageRevealToneName | (string & {});
13
+ /** Screen-reader status while generating. */
14
+ label?: string;
15
+ /** Override the letters derived from `alt`. '' forces the glyph. */
16
+ initials?: string;
17
+ /** object-fit for the image. Logos usually want 'contain'. */
18
+ fit?: 'cover' | 'contain';
19
+ /** Read-only: the current phase. */
20
+ phase?: ImageRevealPhase;
21
+ /** Fires once per src, when that image has loaded. */
22
+ onReveal?: (src: string) => void;
23
+ /** Fires once per src, when that image failed to load. */
24
+ onError?: (src: string) => void;
25
+ /** Replaces the initials / glyph fallback. */
26
+ fallback?: Snippet;
27
+ class?: string;
28
+ }
29
+ declare const ImageReveal: import("svelte").Component<Props, {}, "phase">;
30
+ type ImageReveal = ReturnType<typeof ImageReveal>;
31
+ export default ImageReveal;
@@ -7,7 +7,10 @@
7
7
  * opt-in floatingLabel prop (label translates/scales on focus-or-filled, pure CSS).
8
8
  */
9
9
 
10
+ import type { Snippet } from 'svelte';
10
11
  import type { HTMLInputAttributes } from 'svelte/elements';
12
+ import FieldLabel from './field/FieldLabel.svelte';
13
+ import FieldHint from './field/FieldHint.svelte';
11
14
 
12
15
  /**
13
16
  * Any other native attribute (aria-label, aria-labelledby, data-*, title,
@@ -56,6 +59,12 @@
56
59
  * consumers are unaffected when this prop is absent/false.
57
60
  */
58
61
  floatingLabel?: boolean;
62
+ /** Leading icon / addon inside the field (e.g. a search glyph, "https://"). */
63
+ leading?: Snippet;
64
+ /** Trailing icon / addon inside the field (e.g. a unit, a clear button). */
65
+ trailing?: Snippet;
66
+ /** Info-icon hint after the label (shared `use:tooltip`). */
67
+ labelTooltip?: string;
59
68
  class?: string;
60
69
  id?: string;
61
70
  name?: string;
@@ -78,6 +87,9 @@
78
87
  pattern,
79
88
  showCount = false,
80
89
  floatingLabel = false,
90
+ leading,
91
+ trailing,
92
+ labelTooltip,
81
93
  class: className = '',
82
94
  id = `text-input-${Math.random().toString(36).substr(2, 9)}`,
83
95
  name = '',
@@ -87,7 +99,10 @@
87
99
 
88
100
  const sizeClass = $derived({ sm: 'input-sm', md: '', lg: 'input-lg' }[size]);
89
101
 
90
- const inputClass = $derived(
102
+ // With a leading/trailing addon the DaisyUI `input` skin moves to a wrapper
103
+ // (DaisyUI 5's `<label class="input">` pattern) and the <input> goes bare.
104
+ const hasAddons = $derived(!!leading || !!trailing);
105
+ const shellClass = $derived(
91
106
  [
92
107
  'input input-bordered w-full transition-shadow duration-200',
93
108
  sizeClass,
@@ -98,11 +113,13 @@
98
113
  .filter(Boolean)
99
114
  .join(' ')
100
115
  );
116
+ const inputClass = $derived(hasAddons ? 'jatui-input-bare' : shellClass);
101
117
 
102
118
  const characterCount = $derived(value?.length || 0);
103
119
  const isOverLimit = $derived(maxlength ? characterCount > maxlength : false);
104
120
 
105
121
  let inputEl: HTMLInputElement | undefined = $state();
122
+ let shellEl: HTMLDivElement | undefined = $state();
106
123
  let hadError = false;
107
124
 
108
125
  // Error shake: embedded keyframe (jatui-shake-x), no host-app dependency.
@@ -110,14 +127,15 @@
110
127
  // Self-contained reflow-restart so repeat errors re-shake.
111
128
  $effect(() => {
112
129
  const hasError = !!error;
113
- if (hasError && !hadError && inputEl) {
130
+ const target = hasAddons ? shellEl : inputEl;
131
+ if (hasError && !hadError && target) {
114
132
  const reduce =
115
133
  typeof window !== 'undefined' &&
116
134
  window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
117
135
  if (!reduce) {
118
- inputEl.classList.remove('jatui-input-shake');
119
- void inputEl.offsetWidth;
120
- inputEl.classList.add('jatui-input-shake');
136
+ target.classList.remove('jatui-input-shake');
137
+ void target.offsetWidth;
138
+ target.classList.add('jatui-input-shake');
121
139
  }
122
140
  }
123
141
  hadError = hasError;
@@ -126,17 +144,10 @@
126
144
 
127
145
  <div class="form-control w-full" class:jatui-float-wrap={floatingLabel}>
128
146
  {#if label && !floatingLabel}
129
- <label for={id} class="label">
130
- <span class="text-[0.8125rem] font-medium text-base-content/85">
131
- {label}
132
- {#if required}
133
- <span class="text-error ml-1">*</span>
134
- {/if}
135
- </span>
136
- </label>
147
+ <FieldLabel for={id} text={label} {required} tooltip={labelTooltip} />
137
148
  {/if}
138
149
 
139
- <div class:jatui-float-field={floatingLabel}>
150
+ {#snippet field()}
140
151
  <input
141
152
  {...rest}
142
153
  bind:this={inputEl}
@@ -161,6 +172,18 @@
161
172
  aria-invalid={error ? 'true' : 'false'}
162
173
  onanimationend={() => inputEl?.classList.remove('jatui-input-shake')}
163
174
  />
175
+ {/snippet}
176
+
177
+ <div class:jatui-float-field={floatingLabel}>
178
+ {#if hasAddons}
179
+ <div class="{shellClass} jatui-input-shell" bind:this={shellEl} onanimationend={() => shellEl?.classList.remove('jatui-input-shake')}>
180
+ {#if leading}<span class="jatui-input-addon text-base-content/55">{@render leading()}</span>{/if}
181
+ {@render field()}
182
+ {#if trailing}<span class="jatui-input-addon text-base-content/55">{@render trailing()}</span>{/if}
183
+ </div>
184
+ {:else}
185
+ {@render field()}
186
+ {/if}
164
187
 
165
188
  {#if label && floatingLabel}
166
189
  <label for={id} class="jatui-float-label">
@@ -181,13 +204,9 @@
181
204
  {/if}
182
205
 
183
206
  {#if error}
184
- <div class="label">
185
- <span id="{id}-error" class="text-[0.8125rem] text-error" role="alert">{error}</span>
186
- </div>
207
+ <FieldHint id="{id}-error" text={error} error />
187
208
  {:else if helpText}
188
- <div class="label">
189
- <span id="{id}-help" class="text-[0.8125rem] text-base-content/45">{helpText}</span>
190
- </div>
209
+ <FieldHint id="{id}-help" text={helpText} />
191
210
  {/if}
192
211
  </div>
193
212
 
@@ -213,6 +232,38 @@
213
232
  }
214
233
  }
215
234
 
235
+ /* ── Leading / trailing addons ────────────────────────────────
236
+ The shell carries the DaisyUI input skin; the inner input is bare and
237
+ grows. Focus ring comes from the shell's :focus-within. */
238
+ .jatui-input-shell {
239
+ display: flex;
240
+ align-items: center;
241
+ gap: var(--jatui-space-base, 8px);
242
+ }
243
+ .jatui-input-shell:focus-within {
244
+ outline: 2px solid var(--color-base-content);
245
+ outline-offset: 2px;
246
+ }
247
+ .jatui-input-shell.input-error:focus-within {
248
+ outline-color: var(--color-error);
249
+ }
250
+ :global(.jatui-input-bare) {
251
+ flex: 1 1 auto;
252
+ min-width: 0;
253
+ height: 100%;
254
+ background: transparent;
255
+ border: 0;
256
+ outline: none;
257
+ font: inherit;
258
+ color: inherit;
259
+ }
260
+ .jatui-input-addon {
261
+ display: inline-flex;
262
+ flex-shrink: 0;
263
+ align-items: center;
264
+ white-space: nowrap;
265
+ }
266
+
216
267
  /* ── Floating Label ───────────────────────────────────────────
217
268
  Label sits inside the field at rest, lifts + scales on focus or filled.
218
269
  Pure CSS: uses :placeholder-shown to detect empty state (input must have
@@ -5,6 +5,7 @@
5
5
  * Adds: motion-token error shake (embedded keyframe, no host-app dependency),
6
6
  * opt-in floatingLabel prop (label translates/scales on focus-or-filled, pure CSS).
7
7
  */
8
+ import type { Snippet } from 'svelte';
8
9
  import type { HTMLInputAttributes } from 'svelte/elements';
9
10
  /**
10
11
  * Any other native attribute (aria-label, aria-labelledby, data-*, title,
@@ -35,6 +36,12 @@ interface Props extends Omit<HTMLInputAttributes, 'value' | 'type' | 'size' | 'c
35
36
  * consumers are unaffected when this prop is absent/false.
36
37
  */
37
38
  floatingLabel?: boolean;
39
+ /** Leading icon / addon inside the field (e.g. a search glyph, "https://"). */
40
+ leading?: Snippet;
41
+ /** Trailing icon / addon inside the field (e.g. a unit, a clear button). */
42
+ trailing?: Snippet;
43
+ /** Info-icon hint after the label (shared `use:tooltip`). */
44
+ labelTooltip?: string;
38
45
  class?: string;
39
46
  id?: string;
40
47
  name?: string;