@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.
- package/CLAUDE.md +24 -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/ImageReveal.svelte +353 -0
- package/dist/components/ImageReveal.svelte.d.ts +31 -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/imageReveal.d.ts +51 -0
- package/dist/components/imageReveal.js +83 -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 +15 -2
- package/dist/index.js +13 -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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
void
|
|
120
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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;
|