@fluix-ui/svelte 0.0.2
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/LICENSE +21 -0
- package/dist/ToastItem.svelte +445 -0
- package/dist/ToastItem.svelte.d.ts +17 -0
- package/dist/ToastItem.svelte.d.ts.map +1 -0
- package/dist/Toaster.svelte +128 -0
- package/dist/Toaster.svelte.d.ts +8 -0
- package/dist/Toaster.svelte.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/toast.svelte.d.ts +8 -0
- package/dist/toast.svelte.d.ts.map +1 -0
- package/dist/toast.svelte.js +19 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Fluix Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Toaster as CoreToaster,
|
|
4
|
+
type FluixToastItem,
|
|
5
|
+
type FluixToastState,
|
|
6
|
+
TOAST_DEFAULTS,
|
|
7
|
+
type ToastMachine,
|
|
8
|
+
} from "@fluix-ui/core";
|
|
9
|
+
import type { Snippet } from "svelte";
|
|
10
|
+
|
|
11
|
+
const WIDTH = 350;
|
|
12
|
+
const HEIGHT = 40;
|
|
13
|
+
const PILL_CONTENT_PADDING = 16;
|
|
14
|
+
const HEADER_HORIZONTAL_PADDING_PX = 12;
|
|
15
|
+
const MIN_EXPAND_RATIO = 2.25;
|
|
16
|
+
const BODY_MERGE_OVERLAP = 6;
|
|
17
|
+
const DISMISS_STAGE_DELAY_MS = 260;
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
item: FluixToastItem;
|
|
21
|
+
machine: ToastMachine;
|
|
22
|
+
localState: { ready: boolean; expanded: boolean };
|
|
23
|
+
onLocalStateChange: (patch: Partial<{ ready: boolean; expanded: boolean }>) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { item, machine, localState, onLocalStateChange }: Props = $props();
|
|
27
|
+
|
|
28
|
+
// --- Element refs ---
|
|
29
|
+
let rootEl: HTMLDivElement | null = $state(null);
|
|
30
|
+
let headerEl: HTMLDivElement | null = $state(null);
|
|
31
|
+
let headerInnerEl: HTMLDivElement | null = $state(null);
|
|
32
|
+
let contentEl: HTMLDivElement | null = $state(null);
|
|
33
|
+
|
|
34
|
+
// --- Reactive measurements ---
|
|
35
|
+
let pillWidth = $state(HEIGHT);
|
|
36
|
+
let contentHeight = $state(0);
|
|
37
|
+
let frozenExpanded = $state(HEIGHT * MIN_EXPAND_RATIO);
|
|
38
|
+
|
|
39
|
+
// --- Transient flags (plain vars, NOT $state — must not trigger $effect re-runs) ---
|
|
40
|
+
let hoveringFlag = false;
|
|
41
|
+
let pendingDismissFlag = false;
|
|
42
|
+
let dismissRequestedFlag = false;
|
|
43
|
+
let dismissTimerHandle: ReturnType<typeof setTimeout> | null = null;
|
|
44
|
+
|
|
45
|
+
// --- Derived values ---
|
|
46
|
+
function getPillAlign(position: string): "left" | "center" | "right" {
|
|
47
|
+
if (position.includes("right")) return "right";
|
|
48
|
+
if (position.includes("center")) return "center";
|
|
49
|
+
return "left";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const attrs = $derived(CoreToaster.getAttrs(item, localState));
|
|
53
|
+
const hasDescription = $derived(Boolean(item.description) || Boolean(item.button));
|
|
54
|
+
const isLoading = $derived(item.state === "loading");
|
|
55
|
+
const open = $derived(hasDescription && localState.expanded && !isLoading);
|
|
56
|
+
const edge = $derived(item.position.startsWith("top") ? "bottom" : "top");
|
|
57
|
+
const pillAlign = $derived(getPillAlign(item.position));
|
|
58
|
+
const filterId = $derived(`fluix-gooey-${item.id.replace(/[^a-z0-9-]/gi, "-")}`);
|
|
59
|
+
const roundness = $derived(item.roundness ?? TOAST_DEFAULTS.roundness);
|
|
60
|
+
const blur = $derived(Math.min(10, Math.max(6, roundness * 0.45)));
|
|
61
|
+
const minExpanded = HEIGHT * MIN_EXPAND_RATIO;
|
|
62
|
+
|
|
63
|
+
const rawExpanded = $derived(
|
|
64
|
+
hasDescription ? Math.max(minExpanded, HEIGHT + contentHeight) : minExpanded,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Freeze expanded height when open
|
|
68
|
+
$effect(() => {
|
|
69
|
+
if (open) frozenExpanded = rawExpanded;
|
|
70
|
+
});
|
|
71
|
+
// Update frozen while open
|
|
72
|
+
$effect(() => {
|
|
73
|
+
if (open) frozenExpanded = rawExpanded;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const expanded = $derived(open ? rawExpanded : frozenExpanded);
|
|
77
|
+
const expandedContent = $derived(Math.max(0, expanded - HEIGHT));
|
|
78
|
+
const expandedHeight = $derived(hasDescription ? Math.max(expanded, minExpanded) : HEIGHT);
|
|
79
|
+
const resolvedPillWidth = $derived(Math.max(HEIGHT, pillWidth));
|
|
80
|
+
const pillX = $derived(
|
|
81
|
+
pillAlign === "right"
|
|
82
|
+
? WIDTH - resolvedPillWidth
|
|
83
|
+
: pillAlign === "center"
|
|
84
|
+
? (WIDTH - resolvedPillWidth) / 2
|
|
85
|
+
: 0,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const rootStyleObj = $derived({
|
|
89
|
+
"--_h": `${open ? expanded : HEIGHT}px`,
|
|
90
|
+
"--_pw": `${resolvedPillWidth}px`,
|
|
91
|
+
"--_px": `${pillX}px`,
|
|
92
|
+
"--_ht": `translateY(${open ? (edge === "bottom" ? 3 : -3) : 0}px) scale(${open ? 0.9 : 1})`,
|
|
93
|
+
"--_co": `${open ? 1 : 0}`,
|
|
94
|
+
"--_cy": `${open ? 0 : -14}px`,
|
|
95
|
+
"--_cm": `${open ? expandedContent : 0}px`,
|
|
96
|
+
"--_by": `${open ? HEIGHT - BODY_MERGE_OVERLAP : HEIGHT}px`,
|
|
97
|
+
"--_bh": `${open ? expandedContent : 0}px`,
|
|
98
|
+
"--_bo": `${open ? 1 : 0}`,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// --- Helper functions ---
|
|
102
|
+
function clearDismissTimer() {
|
|
103
|
+
if (dismissTimerHandle) {
|
|
104
|
+
clearTimeout(dismissTimerHandle);
|
|
105
|
+
dismissTimerHandle = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function requestDismiss() {
|
|
110
|
+
if (dismissRequestedFlag) return;
|
|
111
|
+
dismissRequestedFlag = true;
|
|
112
|
+
hoveringFlag = false;
|
|
113
|
+
pendingDismissFlag = false;
|
|
114
|
+
onLocalStateChange({ expanded: false });
|
|
115
|
+
clearDismissTimer();
|
|
116
|
+
const delay = hasDescription ? DISMISS_STAGE_DELAY_MS : 0;
|
|
117
|
+
dismissTimerHandle = setTimeout(() => {
|
|
118
|
+
machine.dismiss(item.id);
|
|
119
|
+
dismissTimerHandle = null;
|
|
120
|
+
}, delay);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- Effects ---
|
|
124
|
+
|
|
125
|
+
// Apply CSS custom properties to root element
|
|
126
|
+
$effect(() => {
|
|
127
|
+
const el = rootEl;
|
|
128
|
+
if (!el) return;
|
|
129
|
+
const style = rootStyleObj;
|
|
130
|
+
for (const [key, value] of Object.entries(style)) {
|
|
131
|
+
el.style.setProperty(key, value);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Measure pill width (synchronous initial measure, rAF-debounced for resize)
|
|
136
|
+
$effect(() => {
|
|
137
|
+
const headerElement = headerEl;
|
|
138
|
+
const headerInner = headerInnerEl;
|
|
139
|
+
if (!headerElement || !headerInner) return;
|
|
140
|
+
|
|
141
|
+
const measure = () => {
|
|
142
|
+
const cs = getComputedStyle(headerElement);
|
|
143
|
+
const horizontalPadding =
|
|
144
|
+
Number.parseFloat(cs.paddingLeft || "0") + Number.parseFloat(cs.paddingRight || "0");
|
|
145
|
+
const intrinsicWidth = headerInner.getBoundingClientRect().width;
|
|
146
|
+
pillWidth = intrinsicWidth + horizontalPadding + PILL_CONTENT_PADDING;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Measure synchronously so pill is correct before ready transitions enable
|
|
150
|
+
measure();
|
|
151
|
+
|
|
152
|
+
let rafId = 0;
|
|
153
|
+
const observer = new ResizeObserver(() => {
|
|
154
|
+
cancelAnimationFrame(rafId);
|
|
155
|
+
rafId = requestAnimationFrame(measure);
|
|
156
|
+
});
|
|
157
|
+
observer.observe(headerInner);
|
|
158
|
+
|
|
159
|
+
return () => {
|
|
160
|
+
cancelAnimationFrame(rafId);
|
|
161
|
+
observer.disconnect();
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Measure content height (runs after DOM update — Svelte $effect default)
|
|
166
|
+
$effect(() => {
|
|
167
|
+
if (!hasDescription) {
|
|
168
|
+
contentHeight = 0;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const el = contentEl;
|
|
172
|
+
if (!el) return;
|
|
173
|
+
|
|
174
|
+
const measure = () => {
|
|
175
|
+
contentHeight = el.scrollHeight;
|
|
176
|
+
};
|
|
177
|
+
measure();
|
|
178
|
+
|
|
179
|
+
let rafId = 0;
|
|
180
|
+
const observer = new ResizeObserver(() => {
|
|
181
|
+
cancelAnimationFrame(rafId);
|
|
182
|
+
rafId = requestAnimationFrame(measure);
|
|
183
|
+
});
|
|
184
|
+
observer.observe(el);
|
|
185
|
+
|
|
186
|
+
return () => {
|
|
187
|
+
cancelAnimationFrame(rafId);
|
|
188
|
+
observer.disconnect();
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Ready timer (32ms after mount)
|
|
193
|
+
$effect(() => {
|
|
194
|
+
const timer = setTimeout(() => {
|
|
195
|
+
onLocalStateChange({ ready: true });
|
|
196
|
+
}, 32);
|
|
197
|
+
return () => clearTimeout(timer);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Auto-dismiss timer
|
|
201
|
+
$effect(() => {
|
|
202
|
+
// Track deps for re-run
|
|
203
|
+
const id = item.id;
|
|
204
|
+
const instanceId = item.instanceId;
|
|
205
|
+
const duration = item.duration;
|
|
206
|
+
void id;
|
|
207
|
+
void instanceId;
|
|
208
|
+
|
|
209
|
+
if (duration == null || duration <= 0) return;
|
|
210
|
+
|
|
211
|
+
const timer = setTimeout(() => {
|
|
212
|
+
if (hoveringFlag) {
|
|
213
|
+
pendingDismissFlag = true;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
pendingDismissFlag = false;
|
|
217
|
+
requestDismiss();
|
|
218
|
+
}, duration);
|
|
219
|
+
|
|
220
|
+
return () => clearTimeout(timer);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Autopilot timers
|
|
224
|
+
$effect(() => {
|
|
225
|
+
if (!localState.ready) return;
|
|
226
|
+
// Track deps
|
|
227
|
+
void item.id;
|
|
228
|
+
void item.instanceId;
|
|
229
|
+
|
|
230
|
+
const timers: ReturnType<typeof setTimeout>[] = [];
|
|
231
|
+
|
|
232
|
+
if (item.autoExpandDelayMs != null && item.autoExpandDelayMs > 0) {
|
|
233
|
+
timers.push(
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
if (dismissRequestedFlag) return;
|
|
236
|
+
if (!hoveringFlag) onLocalStateChange({ expanded: true });
|
|
237
|
+
}, item.autoExpandDelayMs),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (item.autoCollapseDelayMs != null && item.autoCollapseDelayMs > 0) {
|
|
242
|
+
timers.push(
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
if (dismissRequestedFlag) return;
|
|
245
|
+
if (!hoveringFlag) onLocalStateChange({ expanded: false });
|
|
246
|
+
}, item.autoCollapseDelayMs),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return () => {
|
|
251
|
+
for (const t of timers) clearTimeout(t);
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Reset flags on instanceId change
|
|
256
|
+
$effect(() => {
|
|
257
|
+
void item.instanceId;
|
|
258
|
+
hoveringFlag = false;
|
|
259
|
+
pendingDismissFlag = false;
|
|
260
|
+
dismissRequestedFlag = false;
|
|
261
|
+
clearDismissTimer();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Connect DOM events
|
|
265
|
+
$effect(() => {
|
|
266
|
+
const el = rootEl;
|
|
267
|
+
if (!el) return;
|
|
268
|
+
|
|
269
|
+
// Capture current item for the closure
|
|
270
|
+
const currentItem = item;
|
|
271
|
+
|
|
272
|
+
const callbacks = {
|
|
273
|
+
onExpand: () => {
|
|
274
|
+
if (currentItem.exiting || dismissRequestedFlag) return;
|
|
275
|
+
onLocalStateChange({ expanded: true });
|
|
276
|
+
},
|
|
277
|
+
onCollapse: () => {
|
|
278
|
+
if (currentItem.exiting || dismissRequestedFlag) return;
|
|
279
|
+
if (currentItem.autopilot !== false) return;
|
|
280
|
+
onLocalStateChange({ expanded: false });
|
|
281
|
+
},
|
|
282
|
+
onDismiss: () => requestDismiss(),
|
|
283
|
+
onHoverStart: () => {
|
|
284
|
+
hoveringFlag = true;
|
|
285
|
+
},
|
|
286
|
+
onHoverEnd: () => {
|
|
287
|
+
hoveringFlag = false;
|
|
288
|
+
if (pendingDismissFlag && !dismissRequestedFlag) {
|
|
289
|
+
pendingDismissFlag = false;
|
|
290
|
+
requestDismiss();
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const { destroy } = CoreToaster.connect(el, callbacks, currentItem);
|
|
296
|
+
return destroy;
|
|
297
|
+
});
|
|
298
|
+
</script>
|
|
299
|
+
|
|
300
|
+
{#snippet iconFor(state: FluixToastState)}
|
|
301
|
+
{#if state === "success"}
|
|
302
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
303
|
+
<polyline points="20 6 9 17 4 12" />
|
|
304
|
+
</svg>
|
|
305
|
+
{:else if state === "error"}
|
|
306
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
307
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
308
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
309
|
+
</svg>
|
|
310
|
+
{:else if state === "warning"}
|
|
311
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
312
|
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
313
|
+
<line x1="12" y1="9" x2="12" y2="13" />
|
|
314
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
315
|
+
</svg>
|
|
316
|
+
{:else if state === "info"}
|
|
317
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
318
|
+
<circle cx="12" cy="12" r="10" />
|
|
319
|
+
<line x1="12" y1="16" x2="12" y2="12" />
|
|
320
|
+
<line x1="12" y1="8" x2="12.01" y2="8" />
|
|
321
|
+
</svg>
|
|
322
|
+
{:else if state === "loading"}
|
|
323
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-fluix-icon="spin">
|
|
324
|
+
<line x1="12" y1="2" x2="12" y2="6" />
|
|
325
|
+
<line x1="12" y1="18" x2="12" y2="22" />
|
|
326
|
+
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76" />
|
|
327
|
+
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07" />
|
|
328
|
+
<line x1="2" y1="12" x2="6" y2="12" />
|
|
329
|
+
<line x1="18" y1="12" x2="22" y2="12" />
|
|
330
|
+
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24" />
|
|
331
|
+
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93" />
|
|
332
|
+
</svg>
|
|
333
|
+
{:else if state === "action"}
|
|
334
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
335
|
+
<circle cx="12" cy="12" r="10" />
|
|
336
|
+
<polygon points="10 8 16 12 10 16 10 8" fill="currentColor" stroke="none" />
|
|
337
|
+
</svg>
|
|
338
|
+
{/if}
|
|
339
|
+
{/snippet}
|
|
340
|
+
|
|
341
|
+
<div
|
|
342
|
+
bind:this={rootEl}
|
|
343
|
+
role="button"
|
|
344
|
+
tabindex="0"
|
|
345
|
+
{...attrs.root}
|
|
346
|
+
>
|
|
347
|
+
<div {...attrs.canvas}>
|
|
348
|
+
<svg
|
|
349
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
350
|
+
data-fluix-svg=""
|
|
351
|
+
width={WIDTH}
|
|
352
|
+
height={expandedHeight}
|
|
353
|
+
viewBox="0 0 {WIDTH} {expandedHeight}"
|
|
354
|
+
aria-hidden="true"
|
|
355
|
+
style="position:absolute;left:0;top:0;overflow:visible;"
|
|
356
|
+
>
|
|
357
|
+
<defs>
|
|
358
|
+
<filter
|
|
359
|
+
id={filterId}
|
|
360
|
+
x="-20%" y="-20%" width="140%" height="140%"
|
|
361
|
+
color-interpolation-filters="sRGB"
|
|
362
|
+
>
|
|
363
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation={blur} result="blur" />
|
|
364
|
+
<feColorMatrix
|
|
365
|
+
in="blur" type="matrix"
|
|
366
|
+
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -10"
|
|
367
|
+
result="goo"
|
|
368
|
+
/>
|
|
369
|
+
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
|
|
370
|
+
</filter>
|
|
371
|
+
</defs>
|
|
372
|
+
<g filter="url(#{filterId})">
|
|
373
|
+
<rect
|
|
374
|
+
data-fluix-pill=""
|
|
375
|
+
x={pillX} y={0}
|
|
376
|
+
width={resolvedPillWidth} height={HEIGHT}
|
|
377
|
+
rx={roundness} ry={roundness}
|
|
378
|
+
fill={item.fill ?? "#FFFFFF"}
|
|
379
|
+
/>
|
|
380
|
+
<rect
|
|
381
|
+
data-fluix-body=""
|
|
382
|
+
x={0} y={HEIGHT}
|
|
383
|
+
width={WIDTH} height={0}
|
|
384
|
+
rx={0} ry={0}
|
|
385
|
+
fill={item.fill ?? "#FFFFFF"}
|
|
386
|
+
opacity={0}
|
|
387
|
+
/>
|
|
388
|
+
</g>
|
|
389
|
+
</svg>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<div
|
|
393
|
+
bind:this={headerEl}
|
|
394
|
+
{...attrs.header}
|
|
395
|
+
style="padding-left:{HEADER_HORIZONTAL_PADDING_PX}px;padding-right:{HEADER_HORIZONTAL_PADDING_PX}px"
|
|
396
|
+
>
|
|
397
|
+
<div data-fluix-header-stack="">
|
|
398
|
+
<div
|
|
399
|
+
bind:this={headerInnerEl}
|
|
400
|
+
data-fluix-header-inner=""
|
|
401
|
+
data-layer="current"
|
|
402
|
+
>
|
|
403
|
+
<div {...attrs.badge} class={item.styles?.badge}>
|
|
404
|
+
{#if item.icon != null}
|
|
405
|
+
{#if typeof item.icon === "string"}
|
|
406
|
+
<span aria-hidden="true">{item.icon}</span>
|
|
407
|
+
{/if}
|
|
408
|
+
{:else}
|
|
409
|
+
{@render iconFor(item.state)}
|
|
410
|
+
{/if}
|
|
411
|
+
</div>
|
|
412
|
+
<span {...attrs.title} class={item.styles?.title}>
|
|
413
|
+
{item.title ?? item.state}
|
|
414
|
+
</span>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
{#if hasDescription}
|
|
420
|
+
<div {...attrs.content}>
|
|
421
|
+
<div
|
|
422
|
+
bind:this={contentEl}
|
|
423
|
+
{...attrs.description}
|
|
424
|
+
class={item.styles?.description}
|
|
425
|
+
>
|
|
426
|
+
{#if typeof item.description === "string" || typeof item.description === "number"}
|
|
427
|
+
{String(item.description)}
|
|
428
|
+
{:else if typeof item.description === "function"}
|
|
429
|
+
{@render (item.description as Snippet)()}
|
|
430
|
+
{/if}
|
|
431
|
+
|
|
432
|
+
{#if item.button}
|
|
433
|
+
<button
|
|
434
|
+
{...attrs.button}
|
|
435
|
+
type="button"
|
|
436
|
+
class={item.styles?.button}
|
|
437
|
+
onclick={(e: MouseEvent) => { e.stopPropagation(); item.button?.onClick(); }}
|
|
438
|
+
>
|
|
439
|
+
{item.button.title}
|
|
440
|
+
</button>
|
|
441
|
+
{/if}
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
{/if}
|
|
445
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type FluixToastItem, type ToastMachine } from "@fluix-ui/core";
|
|
2
|
+
interface Props {
|
|
3
|
+
item: FluixToastItem;
|
|
4
|
+
machine: ToastMachine;
|
|
5
|
+
localState: {
|
|
6
|
+
ready: boolean;
|
|
7
|
+
expanded: boolean;
|
|
8
|
+
};
|
|
9
|
+
onLocalStateChange: (patch: Partial<{
|
|
10
|
+
ready: boolean;
|
|
11
|
+
expanded: boolean;
|
|
12
|
+
}>) => void;
|
|
13
|
+
}
|
|
14
|
+
declare const ToastItem: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type ToastItem = ReturnType<typeof ToastItem>;
|
|
16
|
+
export default ToastItem;
|
|
17
|
+
//# sourceMappingURL=ToastItem.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToastItem.svelte.d.ts","sourceRoot":"","sources":["../src/ToastItem.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAEN,KAAK,cAAc,EAGnB,KAAK,YAAY,EACjB,MAAM,gBAAgB,CAAC;AAIxB,UAAU,KAAK;IACd,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,YAAY,CAAC;IACtB,UAAU,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;IAClD,kBAAkB,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,KAAK,IAAI,CAAC;CACpF;AAkYD,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Toaster as CoreToaster } from "@fluix-ui/core";
|
|
3
|
+
import type { FluixPosition, FluixToastItem, FluixToasterConfig } from "@fluix-ui/core";
|
|
4
|
+
import { untrack } from "svelte";
|
|
5
|
+
import ToastItem from "./ToastItem.svelte";
|
|
6
|
+
import { createFluixToasts } from "./toast.svelte.js";
|
|
7
|
+
|
|
8
|
+
export interface ToasterProps {
|
|
9
|
+
config?: FluixToasterConfig;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { config }: ToasterProps = $props();
|
|
13
|
+
|
|
14
|
+
const store = createFluixToasts();
|
|
15
|
+
|
|
16
|
+
type ToastLocalState = Record<string, { ready: boolean; expanded: boolean }>;
|
|
17
|
+
const localState: ToastLocalState = $state({});
|
|
18
|
+
|
|
19
|
+
// Apply config when provided
|
|
20
|
+
$effect(() => {
|
|
21
|
+
if (config) store.machine.configure(config);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Sync localState with toast list (in-place add/delete to avoid invalidating all entries)
|
|
25
|
+
$effect(() => {
|
|
26
|
+
const toasts = store.toasts;
|
|
27
|
+
const ids = new Set(toasts.map((t) => t.id));
|
|
28
|
+
const current = untrack(() => localState);
|
|
29
|
+
// Add entries for new toasts
|
|
30
|
+
for (const t of toasts) {
|
|
31
|
+
if (!(t.id in current)) {
|
|
32
|
+
localState[t.id] = { ready: false, expanded: false };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Remove entries for gone toasts
|
|
36
|
+
for (const id in current) {
|
|
37
|
+
if (!ids.has(id)) {
|
|
38
|
+
delete localState[id];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Group toasts by position
|
|
44
|
+
const byPosition = $derived.by(() => {
|
|
45
|
+
const grouped = new Map<FluixPosition, FluixToastItem[]>();
|
|
46
|
+
for (const toast of store.toasts) {
|
|
47
|
+
const current = grouped.get(toast.position) ?? [];
|
|
48
|
+
current.push(toast);
|
|
49
|
+
grouped.set(toast.position, current);
|
|
50
|
+
}
|
|
51
|
+
return grouped;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const resolvedOffset = $derived(store.config?.offset ?? config?.offset);
|
|
55
|
+
const resolvedLayout = $derived(store.config?.layout ?? config?.layout ?? "stack");
|
|
56
|
+
|
|
57
|
+
function resolveOffsetValue(value: number | string): string {
|
|
58
|
+
return typeof value === "number" ? `${value}px` : value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getViewportOffsetStyle(
|
|
62
|
+
offset: FluixToasterConfig["offset"],
|
|
63
|
+
position: FluixPosition,
|
|
64
|
+
): string {
|
|
65
|
+
if (offset == null) return "";
|
|
66
|
+
|
|
67
|
+
let top: string | undefined;
|
|
68
|
+
let right: string | undefined;
|
|
69
|
+
let bottom: string | undefined;
|
|
70
|
+
let left: string | undefined;
|
|
71
|
+
|
|
72
|
+
if (typeof offset === "number" || typeof offset === "string") {
|
|
73
|
+
const resolved = resolveOffsetValue(offset);
|
|
74
|
+
top = resolved;
|
|
75
|
+
right = resolved;
|
|
76
|
+
bottom = resolved;
|
|
77
|
+
left = resolved;
|
|
78
|
+
} else {
|
|
79
|
+
if (offset.top != null) top = resolveOffsetValue(offset.top);
|
|
80
|
+
if (offset.right != null) right = resolveOffsetValue(offset.right);
|
|
81
|
+
if (offset.bottom != null) bottom = resolveOffsetValue(offset.bottom);
|
|
82
|
+
if (offset.left != null) left = resolveOffsetValue(offset.left);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const parts: string[] = [];
|
|
86
|
+
if (position.startsWith("top") && top) parts.push(`top:${top}`);
|
|
87
|
+
if (position.startsWith("bottom") && bottom) parts.push(`bottom:${bottom}`);
|
|
88
|
+
if (position.endsWith("right") && right) parts.push(`right:${right}`);
|
|
89
|
+
if (position.endsWith("left") && left) parts.push(`left:${left}`);
|
|
90
|
+
if (position.endsWith("center")) {
|
|
91
|
+
if (left) parts.push(`padding-left:${left}`);
|
|
92
|
+
if (right) parts.push(`padding-right:${right}`);
|
|
93
|
+
}
|
|
94
|
+
return parts.join(";");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function setToastLocal(id: string, patch: Partial<{ ready: boolean; expanded: boolean }>) {
|
|
98
|
+
const entry = localState[id];
|
|
99
|
+
if (entry) {
|
|
100
|
+
if (patch.ready !== undefined) entry.ready = patch.ready;
|
|
101
|
+
if (patch.expanded !== undefined) entry.expanded = patch.expanded;
|
|
102
|
+
} else {
|
|
103
|
+
localState[id] = {
|
|
104
|
+
ready: patch.ready ?? false,
|
|
105
|
+
expanded: patch.expanded ?? false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
{#each [...byPosition] as [position, positionToasts] (position)}
|
|
112
|
+
<section
|
|
113
|
+
{...CoreToaster.getViewportAttrs(position, resolvedLayout)}
|
|
114
|
+
style={getViewportOffsetStyle(resolvedOffset, position)}
|
|
115
|
+
>
|
|
116
|
+
{#each positionToasts as toastItem (toastItem.instanceId)}
|
|
117
|
+
{#key toastItem.instanceId}
|
|
118
|
+
<ToastItem
|
|
119
|
+
item={toastItem}
|
|
120
|
+
machine={store.machine}
|
|
121
|
+
localState={localState[toastItem.id] ?? { ready: false, expanded: false }}
|
|
122
|
+
onLocalStateChange={(patch: Partial<{ ready: boolean; expanded: boolean }>) =>
|
|
123
|
+
setToastLocal(toastItem.id, patch)}
|
|
124
|
+
/>
|
|
125
|
+
{/key}
|
|
126
|
+
{/each}
|
|
127
|
+
</section>
|
|
128
|
+
{/each}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FluixToasterConfig } from "@fluix-ui/core";
|
|
2
|
+
export interface ToasterProps {
|
|
3
|
+
config?: FluixToasterConfig;
|
|
4
|
+
}
|
|
5
|
+
declare const Toaster: import("svelte").Component<ToasterProps, {}, "">;
|
|
6
|
+
type Toaster = ReturnType<typeof Toaster>;
|
|
7
|
+
export default Toaster;
|
|
8
|
+
//# sourceMappingURL=Toaster.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Toaster.svelte.d.ts","sourceRoot":"","sources":["../src/Toaster.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAiC,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAMxF,MAAM,WAAW,YAAY;IAC5B,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC5B;AA0HD,QAAA,MAAM,OAAO,kDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fluix-ui/svelte — Svelte 5 adapter for Fluix UI components.
|
|
3
|
+
*
|
|
4
|
+
* Exports:
|
|
5
|
+
* - Toaster: component that renders all active toasts
|
|
6
|
+
* - createFluixToasts: rune-based store wrapper for core toast store
|
|
7
|
+
* - fluix: re-exported imperative API from @fluix-ui/core
|
|
8
|
+
*/
|
|
9
|
+
export { fluix } from "@fluix-ui/core";
|
|
10
|
+
export { default as Toaster } from "./Toaster.svelte";
|
|
11
|
+
export { createFluixToasts } from "./toast.svelte.js";
|
|
12
|
+
export type { FluixToastOptions, FluixToastPromiseOptions, FluixPosition, FluixTheme, FluixToastState, FluixToasterConfig, } from "@fluix-ui/core";
|
|
13
|
+
export type { ToasterProps } from "./Toaster.svelte";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EACX,iBAAiB,EACjB,wBAAwB,EACxB,aAAa,EACb,UAAU,EACV,eAAe,EACf,kBAAkB,GAClB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fluix-ui/svelte — Svelte 5 adapter for Fluix UI components.
|
|
3
|
+
*
|
|
4
|
+
* Exports:
|
|
5
|
+
* - Toaster: component that renders all active toasts
|
|
6
|
+
* - createFluixToasts: rune-based store wrapper for core toast store
|
|
7
|
+
* - fluix: re-exported imperative API from @fluix-ui/core
|
|
8
|
+
*/
|
|
9
|
+
export { fluix } from "@fluix-ui/core";
|
|
10
|
+
export { default as Toaster } from "./Toaster.svelte";
|
|
11
|
+
export { createFluixToasts } from "./toast.svelte.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FluixToastItem, FluixToasterConfig, ToastMachine } from "@fluix-ui/core";
|
|
2
|
+
export interface FluixToastsResult {
|
|
3
|
+
readonly toasts: FluixToastItem[];
|
|
4
|
+
readonly config: FluixToasterConfig;
|
|
5
|
+
readonly machine: ToastMachine;
|
|
6
|
+
}
|
|
7
|
+
export declare function createFluixToasts(): FluixToastsResult;
|
|
8
|
+
//# sourceMappingURL=toast.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toast.svelte.d.ts","sourceRoot":"","sources":["../src/toast.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEvF,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;CAC/B;AAED,wBAAgB,iBAAiB,IAAI,iBAAiB,CAmBrD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Toaster as CoreToaster } from "@fluix-ui/core";
|
|
2
|
+
export function createFluixToasts() {
|
|
3
|
+
const machine = CoreToaster.getMachine();
|
|
4
|
+
let snapshot = $state.raw(machine.store.getSnapshot());
|
|
5
|
+
$effect(() => {
|
|
6
|
+
return machine.store.subscribe(() => {
|
|
7
|
+
snapshot = machine.store.getSnapshot();
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
return {
|
|
11
|
+
get toasts() {
|
|
12
|
+
return snapshot.toasts;
|
|
13
|
+
},
|
|
14
|
+
get config() {
|
|
15
|
+
return snapshot.config;
|
|
16
|
+
},
|
|
17
|
+
machine,
|
|
18
|
+
};
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluix-ui/svelte",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Svelte 5 adapter for Fluix UI components.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"svelte": "./dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"svelte": "./dist/index.js",
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"svelte": ">=5",
|
|
24
|
+
"@fluix-ui/core": "0.0.2",
|
|
25
|
+
"@fluix-ui/css": "0.0.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@sveltejs/package": "^2.0.0",
|
|
29
|
+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
30
|
+
"svelte": "^5.0.0",
|
|
31
|
+
"svelte-check": "^4.0.0",
|
|
32
|
+
"typescript": "^5.7.0",
|
|
33
|
+
"@fluix-ui/core": "0.0.2"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "svelte-package -i src",
|
|
37
|
+
"dev": "svelte-package -w -i src",
|
|
38
|
+
"typecheck": "svelte-check --tsconfig ./tsconfig.json"
|
|
39
|
+
}
|
|
40
|
+
}
|