sv5ui-plus 5.0.2 → 5.0.4
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/dist/components/ConfirmDialog/ConfirmDialog.svelte +13 -6
- package/dist/components/FormSelect/FormSelect.svelte +2 -1
- package/dist/components/NumberTicker/NumberTicker.svelte +70 -27
- package/dist/components/SafeTable/SafeTable.svelte +41 -24
- package/dist/mcp/sv5ui-plus-docs.data.json +2 -2
- package/package.json +9 -9
|
@@ -31,16 +31,23 @@ export function show(options = {}) {
|
|
|
31
31
|
currentInputValue = options.inputValue ?? "";
|
|
32
32
|
pending = {
|
|
33
33
|
options,
|
|
34
|
-
resolve
|
|
34
|
+
resolve,
|
|
35
|
+
settled: false
|
|
35
36
|
};
|
|
36
37
|
open = true;
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
function settle(result) {
|
|
40
41
|
const current = pending;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
current
|
|
42
|
+
if (!current || current.settled) return;
|
|
43
|
+
current.settled = true;
|
|
44
|
+
current.resolve(result);
|
|
45
|
+
// ถอด dialog หลัง pointer/click จบ — กัน derived_inert จาก bits-ui ที่ยังอ่าน derived
|
|
46
|
+
queueMicrotask(() => {
|
|
47
|
+
if (pending !== current) return;
|
|
48
|
+
pending = null;
|
|
49
|
+
open = false;
|
|
50
|
+
});
|
|
44
51
|
}
|
|
45
52
|
function handleConfirm() {
|
|
46
53
|
pending?.options.onConfirm?.(currentInputValue);
|
|
@@ -51,7 +58,7 @@ function handleCancel() {
|
|
|
51
58
|
settle(false);
|
|
52
59
|
}
|
|
53
60
|
function handleOpenChange(value) {
|
|
54
|
-
if (!value && pending) {
|
|
61
|
+
if (!value && pending && !pending.settled) {
|
|
55
62
|
handleCancel();
|
|
56
63
|
return;
|
|
57
64
|
}
|
|
@@ -71,7 +78,7 @@ const contentProps = $derived.by(() => {
|
|
|
71
78
|
<Dialog.Root bind:open onOpenChange={handleOpenChange}>
|
|
72
79
|
<Dialog.Portal>
|
|
73
80
|
<Dialog.Overlay
|
|
74
|
-
class="fixed inset-0 z-50 bg-
|
|
81
|
+
class="fixed inset-0 z-50 bg-(--scrim-bg,rgba(0,0,0,0.3)) backdrop-blur-sm"
|
|
75
82
|
/>
|
|
76
83
|
{#if pending}
|
|
77
84
|
<Dialog.Content
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
</script>
|
|
3
3
|
|
|
4
4
|
<script lang="ts">import { Select } from "sv5ui";
|
|
5
|
-
|
|
5
|
+
// default เป็น string ว่าง — อย่าใช้ $bindable() เปล่า (UNINITIALIZED ตอน async)
|
|
6
|
+
let { name, value = $bindable(""), disabled = false, ...selectProps } = $props();
|
|
6
7
|
</script>
|
|
7
8
|
|
|
8
9
|
<Select {...selectProps} bind:value {disabled} />
|
|
@@ -1,48 +1,91 @@
|
|
|
1
1
|
<script lang="ts">import { twMerge } from "tailwind-merge";
|
|
2
|
-
import { onMount } from "svelte";
|
|
2
|
+
import { onMount, untrack } from "svelte";
|
|
3
3
|
import { numberTickerVariants } from "./number-ticker.variants.js";
|
|
4
4
|
let { value, duration = 2e3, decimals = 0, delay = 0, class: className, ...restProps } = $props();
|
|
5
5
|
let styles = $derived(numberTickerVariants());
|
|
6
6
|
let displayValue = $state(0);
|
|
7
7
|
let isVisible = $state(false);
|
|
8
8
|
let nodeRef = $state(null);
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* อ่าน value อย่างปลอดภัย — parent expression (เช่น overview.lowStock.length)
|
|
11
|
+
* อาจ throw ตอน nav/refresh ถ้า overview เป็น undefined
|
|
12
|
+
*/
|
|
13
|
+
const readSafeValue = () => {
|
|
14
|
+
try {
|
|
15
|
+
const n = Number(value);
|
|
16
|
+
return Number.isFinite(n) ? n : 0;
|
|
17
|
+
} catch {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
10
21
|
const easeOutExpo = (x) => {
|
|
11
22
|
return x === 1 ? 1 : 1 - 2 ** (-10 * x);
|
|
12
23
|
};
|
|
24
|
+
let animationFrame = 0;
|
|
25
|
+
let delayTimeout;
|
|
26
|
+
/** target ล่าสุดที่เริ่ม animate แล้ว — ใช้กัน re-start ซ้ำ */
|
|
27
|
+
let lastAnimatedTarget;
|
|
28
|
+
const cancelRunning = () => {
|
|
29
|
+
if (animationFrame) cancelAnimationFrame(animationFrame);
|
|
30
|
+
animationFrame = 0;
|
|
31
|
+
if (delayTimeout !== undefined) {
|
|
32
|
+
clearTimeout(delayTimeout);
|
|
33
|
+
delayTimeout = undefined;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* ใช้ targetSnapshot ใน rAF เท่านั้น — ห้ามอ่าน value prop ระหว่าง frame
|
|
38
|
+
* (การอ่าน value จะ re-evaluate expression ของ parent และอาจ throw)
|
|
39
|
+
*/
|
|
40
|
+
const startAnimation = (from, targetSnapshot, withDelay) => {
|
|
41
|
+
cancelRunning();
|
|
42
|
+
lastAnimatedTarget = targetSnapshot;
|
|
43
|
+
const run = () => {
|
|
44
|
+
let startTime = null;
|
|
45
|
+
const animate = (timestamp) => {
|
|
46
|
+
if (!startTime) startTime = timestamp;
|
|
47
|
+
const progress = timestamp - startTime;
|
|
48
|
+
if (progress < duration) {
|
|
49
|
+
const percentage = progress / duration;
|
|
50
|
+
const easedProgress = easeOutExpo(percentage);
|
|
51
|
+
displayValue = from + (targetSnapshot - from) * easedProgress;
|
|
52
|
+
animationFrame = requestAnimationFrame(animate);
|
|
53
|
+
} else {
|
|
54
|
+
displayValue = targetSnapshot;
|
|
55
|
+
animationFrame = 0;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
animationFrame = requestAnimationFrame(animate);
|
|
59
|
+
};
|
|
60
|
+
if (withDelay && delay > 0) {
|
|
61
|
+
delayTimeout = setTimeout(run, delay);
|
|
62
|
+
} else {
|
|
63
|
+
run();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
13
66
|
onMount(() => {
|
|
14
67
|
if (!nodeRef) return;
|
|
15
|
-
let startTime = null;
|
|
16
|
-
let animationFrame;
|
|
17
|
-
const animate = (timestamp) => {
|
|
18
|
-
if (!startTime) startTime = timestamp;
|
|
19
|
-
const progress = timestamp - startTime;
|
|
20
|
-
if (progress < duration) {
|
|
21
|
-
const percentage = progress / duration;
|
|
22
|
-
const easedProgress = easeOutExpo(percentage);
|
|
23
|
-
displayValue = value * easedProgress;
|
|
24
|
-
animationFrame = requestAnimationFrame(animate);
|
|
25
|
-
} else {
|
|
26
|
-
displayValue = value;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
68
|
const observer = new IntersectionObserver((entries) => {
|
|
30
69
|
if (entries[0].isIntersecting && !isVisible) {
|
|
31
70
|
isVisible = true;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}, delay);
|
|
71
|
+
// snapshot ตอนเริ่ม intersection เท่านั้น
|
|
72
|
+
startAnimation(0, readSafeValue(), true);
|
|
35
73
|
}
|
|
36
74
|
}, { threshold: .1 });
|
|
37
75
|
observer.observe(nodeRef);
|
|
38
76
|
return () => {
|
|
39
77
|
observer.disconnect();
|
|
40
|
-
|
|
78
|
+
cancelRunning();
|
|
41
79
|
};
|
|
42
80
|
});
|
|
43
|
-
//
|
|
81
|
+
// value เปลี่ยนหลัง visible → re-animate จาก display ปัจจุบันไป target ใหม่
|
|
44
82
|
$effect(() => {
|
|
45
|
-
|
|
83
|
+
const nextTarget = readSafeValue();
|
|
84
|
+
if (!isVisible) return;
|
|
85
|
+
if (lastAnimatedTarget === nextTarget) return;
|
|
86
|
+
// untrack เพื่อไม่ให้ effect ไล่ตามทุก frame ของ displayValue
|
|
87
|
+
const from = untrack(() => displayValue);
|
|
88
|
+
startAnimation(from, nextTarget, false);
|
|
46
89
|
});
|
|
47
90
|
let formattedValue = $derived(new Intl.NumberFormat("en-US", {
|
|
48
91
|
minimumFractionDigits: decimals,
|
|
@@ -50,10 +93,10 @@ let formattedValue = $derived(new Intl.NumberFormat("en-US", {
|
|
|
50
93
|
}).format(displayValue));
|
|
51
94
|
</script>
|
|
52
95
|
|
|
53
|
-
<span
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
96
|
+
<span
|
|
97
|
+
bind:this={nodeRef}
|
|
98
|
+
class={twMerge(styles.base() as string, className)}
|
|
99
|
+
{...restProps}
|
|
57
100
|
>
|
|
58
|
-
|
|
101
|
+
{formattedValue}
|
|
59
102
|
</span>
|
|
@@ -16,9 +16,12 @@ let localColumnSizing = $state({});
|
|
|
16
16
|
let localGlobalFilter = $state("");
|
|
17
17
|
let localPage = $state(0);
|
|
18
18
|
let localRef = $state(null);
|
|
19
|
-
let localColumnVisibility = $state(
|
|
20
|
-
let localColumnPinning = $state(
|
|
19
|
+
let localColumnVisibility = $state({});
|
|
20
|
+
let localColumnPinning = $state({});
|
|
21
21
|
let { ref = $bindable(localRef), sorting = $bindable(localSorting), selectedRows = $bindable(localSelectedRows), pinnedRows = $bindable(localPinnedRows), expandedRows = $bindable(localExpandedRows), columnFilters = $bindable(localColumnFilters), columnSizing = $bindable(localColumnSizing), globalFilter = $bindable(localGlobalFilter), page = $bindable(localPage), columnVisibility = $bindable(localColumnVisibility), columnPinning = $bindable(localColumnPinning), ...rest } = $props();
|
|
22
|
+
function isPlainObject(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
22
25
|
</script>
|
|
23
26
|
|
|
24
27
|
<Table
|
|
@@ -52,34 +55,48 @@ let { ref = $bindable(localRef), sorting = $bindable(localSorting), selectedRows
|
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
bind:columnFilters={
|
|
55
|
-
() =>
|
|
56
|
-
columnFilters && typeof columnFilters === 'object' && !Array.isArray(columnFilters)
|
|
57
|
-
? columnFilters
|
|
58
|
-
: localColumnFilters,
|
|
58
|
+
() => (isPlainObject(columnFilters) ? columnFilters : localColumnFilters),
|
|
59
59
|
(value) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: ({} as Record<string, string>)
|
|
64
|
-
columnFilters = next
|
|
60
|
+
columnFilters = isPlainObject(value)
|
|
61
|
+
? (value as Record<string, string>)
|
|
62
|
+
: ({} as Record<string, string>)
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
65
|
bind:columnSizing={
|
|
68
|
-
() =>
|
|
69
|
-
columnSizing && typeof columnSizing === 'object' && !Array.isArray(columnSizing)
|
|
70
|
-
? columnSizing
|
|
71
|
-
: localColumnSizing,
|
|
66
|
+
() => (isPlainObject(columnSizing) ? columnSizing : localColumnSizing),
|
|
72
67
|
(value) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
columnSizing = isPlainObject(value)
|
|
69
|
+
? (value as Record<string, number>)
|
|
70
|
+
: ({} as Record<string, number>)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
bind:globalFilter={
|
|
74
|
+
() => (typeof globalFilter === 'string' ? globalFilter : localGlobalFilter),
|
|
75
|
+
(value) => {
|
|
76
|
+
globalFilter = typeof value === 'string' ? value : ''
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
bind:page={
|
|
80
|
+
() => (typeof page === 'number' && Number.isFinite(page) ? page : localPage),
|
|
81
|
+
(value) => {
|
|
82
|
+
page = typeof value === 'number' && Number.isFinite(value) ? value : 0
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
bind:columnVisibility={
|
|
86
|
+
() => (isPlainObject(columnVisibility) ? columnVisibility : localColumnVisibility),
|
|
87
|
+
(value) => {
|
|
88
|
+
columnVisibility = isPlainObject(value)
|
|
89
|
+
? (value as Record<string, boolean>)
|
|
90
|
+
: ({} as Record<string, boolean>)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
bind:columnPinning={
|
|
94
|
+
() => (isPlainObject(columnPinning) ? columnPinning : localColumnPinning),
|
|
95
|
+
(value) => {
|
|
96
|
+
columnPinning = isPlainObject(value)
|
|
97
|
+
? (value as { left?: string[]; right?: string[] })
|
|
98
|
+
: ({} as { left?: string[]; right?: string[] })
|
|
78
99
|
}
|
|
79
100
|
}
|
|
80
|
-
bind:globalFilter
|
|
81
|
-
bind:page
|
|
82
|
-
bind:columnVisibility
|
|
83
|
-
bind:columnPinning
|
|
84
101
|
{...rest}
|
|
85
102
|
/>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv5ui-plus",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4",
|
|
4
4
|
"description": "Community companion components and hooks for SV5UI and Svelte 5",
|
|
5
5
|
"packageManager": "bun@1.3.14",
|
|
6
6
|
"author": "asphum",
|
|
@@ -120,24 +120,24 @@
|
|
|
120
120
|
}
|
|
121
121
|
},
|
|
122
122
|
"devDependencies": {
|
|
123
|
-
"@biomejs/biome": "2.5.
|
|
123
|
+
"@biomejs/biome": "2.5.9",
|
|
124
124
|
"@inlang/paraglide-js": "^2.24.1",
|
|
125
125
|
"@sveltejs/adapter-vercel": "^6.3.4",
|
|
126
|
-
"@sveltejs/kit": "^2.70.
|
|
126
|
+
"@sveltejs/kit": "^2.70.3",
|
|
127
127
|
"@sveltejs/package": "^2.5.8",
|
|
128
128
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
129
129
|
"@tailwindcss/vite": "^4.3.3",
|
|
130
130
|
"@types/node": "^26.2.0",
|
|
131
|
-
"@vitest/browser-playwright": "^4.1.
|
|
131
|
+
"@vitest/browser-playwright": "^4.1.11",
|
|
132
132
|
"playwright": "^1.62.1",
|
|
133
|
-
"publint": "^0.3.
|
|
133
|
+
"publint": "^0.3.24",
|
|
134
134
|
"sv5ui": "^2.5.0",
|
|
135
135
|
"svelte": "^5.56.9",
|
|
136
136
|
"svelte-check": "^4.7.6",
|
|
137
137
|
"tailwindcss": "^4.3.3",
|
|
138
138
|
"typescript": "6.0.3",
|
|
139
|
-
"vite": "^8.2.
|
|
140
|
-
"vitest": "^4.1.
|
|
139
|
+
"vite": "^8.2.2",
|
|
140
|
+
"vitest": "^4.1.11",
|
|
141
141
|
"vitest-browser-svelte": "^3.0.0"
|
|
142
142
|
},
|
|
143
143
|
"keywords": [
|
|
@@ -163,8 +163,8 @@
|
|
|
163
163
|
"@dnd-kit/svelte": "^0.5.0",
|
|
164
164
|
"@internationalized/date": "^3.12.3",
|
|
165
165
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
166
|
-
"apexcharts": "^6.
|
|
167
|
-
"bits-ui": "^2.
|
|
166
|
+
"apexcharts": "^6.10.0",
|
|
167
|
+
"bits-ui": "^2.19.0",
|
|
168
168
|
"shiki": "^4.4.3",
|
|
169
169
|
"tailwind-merge": "^3.6.0",
|
|
170
170
|
"tailwind-variants": "^3.3.1",
|