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.
@@ -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
- pending = null;
42
- open = false;
43
- current?.resolve(result);
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-[var(--scrim-bg,rgba(0,0,0,0.3))] backdrop-blur-sm"
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
- let { name, value = $bindable(), disabled = false, ...selectProps } = $props();
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
- // Easing function (easeOutExpo)
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
- setTimeout(() => {
33
- animationFrame = requestAnimationFrame(animate);
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
- if (animationFrame) cancelAnimationFrame(animationFrame);
78
+ cancelRunning();
41
79
  };
42
80
  });
43
- // Re-animate if value changes significantly (optional)
81
+ // value เปลี่ยนหลัง visible → re-animate จาก display ปัจจุบันไป target ใหม่
44
82
  $effect(() => {
45
- if (isVisible && value !== undefined) {}
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
- bind:this={nodeRef}
55
- class={twMerge(styles.base() as string, className)}
56
- {...restProps}
96
+ <span
97
+ bind:this={nodeRef}
98
+ class={twMerge(styles.base() as string, className)}
99
+ {...restProps}
57
100
  >
58
- {formattedValue}
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(undefined);
20
- let localColumnPinning = $state(undefined);
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
- const next =
61
- value && typeof value === 'object' && !Array.isArray(value)
62
- ? value
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
- const next =
74
- value && typeof value === 'object' && !Array.isArray(value)
75
- ? value
76
- : ({} as Record<string, number>)
77
- columnSizing = next
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
  />
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "version": 1,
3
3
  "packageName": "sv5ui-plus",
4
- "packageVersion": "5.0.2",
5
- "generatedAt": "2026-08-16T09:25:54.305Z",
4
+ "packageVersion": "5.0.4",
5
+ "generatedAt": "2026-08-20T08:57:41.119Z",
6
6
  "slugs": {
7
7
  "components": [],
8
8
  "hooks": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sv5ui-plus",
3
- "version": "5.0.2",
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.8",
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.2",
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.10",
131
+ "@vitest/browser-playwright": "^4.1.11",
132
132
  "playwright": "^1.62.1",
133
- "publint": "^0.3.23",
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.1",
140
- "vitest": "^4.1.10",
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.8.0",
167
- "bits-ui": "^2.18.1",
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",