@epoch32/visage 1.3.0 → 1.3.1

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.
Files changed (44) hide show
  1. package/dist/api/index.js +4 -4
  2. package/dist/charts/index.js +5 -5
  3. package/dist/devtools/index.js +7 -7
  4. package/dist/dom/index.js +3 -3
  5. package/dist/form/index.js +2 -2
  6. package/dist/{index-grhdtcb5.js → index-0qat98zs.js} +3 -3
  7. package/dist/{index-h6tjqk6e.js → index-1jcd8gjf.js} +28 -17
  8. package/dist/{index-6kfe1nr4.js → index-1yhbg34h.js} +41 -9
  9. package/dist/{index-9fvarkrx.js → index-3y73zgbk.js} +1 -1
  10. package/dist/{index-y9bq7h9g.js → index-5jhzwtdz.js} +2 -2
  11. package/dist/{index-a4xns6cc.js → index-65j7a9et.js} +1 -1
  12. package/dist/{index-dyvgtp43.js → index-9bez1tmf.js} +2 -2
  13. package/dist/{index-dvq5f3s8.js → index-aagpn02x.js} +4 -4
  14. package/dist/{index-df5e614s.js → index-awcrz19g.js} +1 -1
  15. package/dist/{index-8x8xz80v.js → index-b7emqmej.js} +3 -3
  16. package/dist/{index-061jf06a.js → index-cgepvvd4.js} +1 -1
  17. package/dist/{index-1xtrtz1t.js → index-dmhmde2z.js} +2 -2
  18. package/dist/{index-0madxn60.js → index-enc8cmea.js} +1 -1
  19. package/dist/{index-js199er4.js → index-g1drdx7k.js} +12 -1
  20. package/dist/{index-zr6cvset.js → index-ptj8rt6b.js} +5 -5
  21. package/dist/{index-8czhpea8.js → index-x6whpv54.js} +30 -19
  22. package/dist/{index-5s255b3w.js → index-xby428n5.js} +23 -22
  23. package/dist/{index-x2rs3vab.js → index-y3nprgn7.js} +1 -1
  24. package/dist/index.js +18 -18
  25. package/dist/jsx-runtime/index.js +4 -4
  26. package/dist/query/index.js +3 -3
  27. package/dist/router/index.js +5 -5
  28. package/dist/rpc/index.js +4 -4
  29. package/dist/server/index.js +2 -2
  30. package/dist/signals/index.js +1 -1
  31. package/dist/signals/system.d.ts +2 -0
  32. package/dist/style/index.js +6 -6
  33. package/dist/template/index.js +2 -2
  34. package/dist/ui/index.js +7 -7
  35. package/dist/virtual/index.js +5 -5
  36. package/package.json +1 -1
  37. package/src/dom/control-flow.ts +31 -21
  38. package/src/form/form.ts +29 -17
  39. package/src/signals/effect.ts +2 -2
  40. package/src/signals/scope.ts +10 -8
  41. package/src/signals/store.ts +32 -1
  42. package/src/signals/system.ts +8 -0
  43. package/src/template/template.ts +12 -1
  44. package/src/virtual/virtualizer.ts +31 -18
@@ -5,6 +5,7 @@ import { isSignal } from "./types";
5
5
  const STORE_RAW = Symbol("visage.store.raw");
6
6
  const STORE_SIGNALS = Symbol("visage.store.signals");
7
7
  const STORE_PROXY = Symbol("visage.store.proxy");
8
+ const STORE_ITERATE = Symbol("visage.store.iterate");
8
9
 
9
10
  export type Store<T> = T;
10
11
 
@@ -61,9 +62,29 @@ function createStoreProxy<T extends object>(target: T): T {
61
62
  return value;
62
63
  },
63
64
 
65
+ has(target, prop) {
66
+ if (prop === STORE_RAW || prop === STORE_PROXY) return true;
67
+ if (typeof prop === "string" || typeof prop === "symbol") {
68
+ const sig = getOrCreatePropertySignal(target, prop);
69
+ sig.get();
70
+ }
71
+ return Reflect.has(target, prop);
72
+ },
73
+
74
+ ownKeys(target) {
75
+ const iterSig = getOrCreatePropertySignal(target, STORE_ITERATE);
76
+ iterSig.get();
77
+ return Reflect.ownKeys(target);
78
+ },
79
+
80
+ getOwnPropertyDescriptor(target, prop) {
81
+ return Reflect.getOwnPropertyDescriptor(target, prop);
82
+ },
83
+
64
84
  set(target, prop, value, receiver) {
85
+ const hadProp = Reflect.has(target, prop);
65
86
  const prevValue = Reflect.get(target, prop, receiver);
66
- if (Object.is(prevValue, value)) {
87
+ if (hadProp && Object.is(prevValue, value)) {
67
88
  return true;
68
89
  }
69
90
 
@@ -72,10 +93,18 @@ function createStoreProxy<T extends object>(target: T): T {
72
93
  const sig = getOrCreatePropertySignal(target, prop);
73
94
  sig.set(value);
74
95
 
96
+ // If a new property was added, notify key iteration
97
+ if (!hadProp) {
98
+ const iterSig = getOrCreatePropertySignal(target, STORE_ITERATE);
99
+ iterSig.update((prev: number = 0) => prev + 1);
100
+ }
101
+
75
102
  // If array length or property keys changed, notify length/keys
76
103
  if (Array.isArray(target) && prop !== "length") {
77
104
  const lenSig = getOrCreatePropertySignal(target, "length");
78
105
  lenSig.set(target.length);
106
+ const iterSig = getOrCreatePropertySignal(target, STORE_ITERATE);
107
+ iterSig.update((prev: number = 0) => prev + 1);
79
108
  }
80
109
  }
81
110
  return success;
@@ -87,6 +116,8 @@ function createStoreProxy<T extends object>(target: T): T {
87
116
  if (hadProp && success) {
88
117
  const sig = getOrCreatePropertySignal(target, prop);
89
118
  sig.set(undefined);
119
+ const iterSig = getOrCreatePropertySignal(target, STORE_ITERATE);
120
+ iterSig.update((prev: number = 0) => prev + 1);
90
121
  }
91
122
  return success;
92
123
  }
@@ -59,6 +59,14 @@ export function removePendingEffect(eff: any): void {
59
59
  ctx.pendingEffects.delete(eff);
60
60
  }
61
61
 
62
+ export function getBatchDepth(): number {
63
+ return ctx.batchDepth;
64
+ }
65
+
66
+ export function getIsFlushing(): boolean {
67
+ return ctx.isFlushing;
68
+ }
69
+
62
70
  const MAX_FLUSH_COUNT = 100;
63
71
 
64
72
  export function flushEffects(): void {
@@ -66,11 +66,22 @@ export function html(
66
66
 
67
67
  for (const bind of bindings) {
68
68
  if (bind.isAttr && bind.attrName) {
69
- const targetEl = findElementWithAttributeValue(root, bind.attrName, bind.id);
69
+ const targetEl = findElementWithAttributeValue(root as HTMLElement, bind.attrName, bind.id);
70
70
  if (targetEl) {
71
71
  targetEl.removeAttribute(bind.attrName);
72
72
  setAttribute(targetEl, bind.attrName, bind.value);
73
73
  }
74
+ } else if (bind.isAttr && !bind.attrName) {
75
+ const propAttr = `data-visage-prop-${bind.id.replace(MARKER_PREFIX, "")}`;
76
+ const targetEl = findElementWithAttributeValue(root as HTMLElement, propAttr, bind.id);
77
+ if (targetEl) {
78
+ targetEl.removeAttribute(propAttr);
79
+ if (typeof bind.value === "object" && bind.value !== null) {
80
+ for (const key of Object.keys(bind.value)) {
81
+ setAttribute(targetEl, key, bind.value[key]);
82
+ }
83
+ }
84
+ }
74
85
  } else if (!bind.isAttr) {
75
86
  const commentNode = findCommentNode(root, bind.id);
76
87
  if (commentNode?.parentNode) {
@@ -56,30 +56,41 @@ export function createVirtualizer(options: VirtualizerOptions): Virtualizer {
56
56
  return getItemHeight(index);
57
57
  };
58
58
 
59
- // Compute prefix offsets for fast range calculation
60
- const totalSize = computed(() => {
61
- const count = getCount();
62
- if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
63
- return count * options.itemHeight;
64
- }
65
- let total = 0;
66
- for (let i = 0; i < count; i++) {
67
- total += getItemSize(i);
68
- }
69
- return total;
70
- });
59
+ // Cached prefix offsets for O(1) offset lookups and true O(log N) binary search
60
+ const offsetsCache: number[] = [0];
61
+ let offsetsValidUpTo = 0;
62
+
63
+ const invalidateOffsetsFrom = (index: number) => {
64
+ offsetsValidUpTo = Math.min(offsetsValidUpTo, index);
65
+ };
71
66
 
72
67
  const getOffsetForIndex = (targetIndex: number): number => {
68
+ if (targetIndex <= 0) return 0;
73
69
  if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
74
70
  return targetIndex * options.itemHeight;
75
71
  }
76
- let offset = 0;
77
- for (let i = 0; i < targetIndex; i++) {
78
- offset += getItemSize(i);
72
+ if (targetIndex <= offsetsValidUpTo) {
73
+ return offsetsCache[targetIndex]!;
79
74
  }
80
- return offset;
75
+ for (let i = offsetsValidUpTo; i < targetIndex; i++) {
76
+ offsetsCache[i + 1] = (offsetsCache[i] ?? 0) + getItemSize(i);
77
+ }
78
+ offsetsValidUpTo = targetIndex;
79
+ return offsetsCache[targetIndex]!;
81
80
  };
82
81
 
82
+ const measurementVersion = signal(0, { internal: true });
83
+
84
+ // Compute prefix offsets for fast range calculation
85
+ const totalSize = computed(() => {
86
+ measurementVersion();
87
+ const count = getCount();
88
+ if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
89
+ return count * options.itemHeight;
90
+ }
91
+ return getOffsetForIndex(count);
92
+ });
93
+
83
94
  const findIndexAtOffset = (offset: number): number => {
84
95
  const count = getCount();
85
96
  if (count === 0) return 0;
@@ -110,12 +121,14 @@ export function createVirtualizer(options: VirtualizerOptions): Virtualizer {
110
121
  };
111
122
 
112
123
  const startIndex = computed(() => {
124
+ measurementVersion();
113
125
  const offset = scrollOffset();
114
126
  const rawStart = findIndexAtOffset(offset);
115
127
  return Math.max(0, rawStart - overscan);
116
128
  });
117
129
 
118
130
  const endIndex = computed(() => {
131
+ measurementVersion();
119
132
  const count = getCount();
120
133
  if (count === 0) return 0;
121
134
  const endOffset = scrollOffset() + (containerSize() || 500);
@@ -255,8 +268,8 @@ export function createVirtualizer(options: VirtualizerOptions): Virtualizer {
255
268
  : element.getBoundingClientRect().height;
256
269
  if (measuredSize > 0 && dynamicSizeCache.get(index) !== measuredSize) {
257
270
  dynamicSizeCache.set(index, measuredSize);
258
- // Force recalculation by touching scrollOffset
259
- scrollOffset.update((o) => o);
271
+ invalidateOffsetsFrom(index);
272
+ measurementVersion.update((v) => v + 1);
260
273
  }
261
274
  };
262
275