@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 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"}
@@ -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
+ }