sv5ui-plus 5.0.2 → 5.0.3

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.
@@ -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.3",
5
+ "generatedAt": "2026-08-16T11:23:34.196Z",
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.3",
4
4
  "description": "Community companion components and hooks for SV5UI and Svelte 5",
5
5
  "packageManager": "bun@1.3.14",
6
6
  "author": "asphum",