@epoch32/visage 1.3.0 → 1.3.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.
Files changed (60) 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-h6tjqk6e.js → index-1jcd8gjf.js} +28 -17
  7. package/dist/{index-6kfe1nr4.js → index-1yhbg34h.js} +41 -9
  8. package/dist/{index-9fvarkrx.js → index-3y73zgbk.js} +1 -1
  9. package/dist/{index-y9bq7h9g.js → index-5jhzwtdz.js} +2 -2
  10. package/dist/{index-a4xns6cc.js → index-65j7a9et.js} +1 -1
  11. package/dist/{index-dyvgtp43.js → index-9bez1tmf.js} +2 -2
  12. package/dist/{index-df5e614s.js → index-awcrz19g.js} +1 -1
  13. package/dist/{index-061jf06a.js → index-cgepvvd4.js} +1 -1
  14. package/dist/{index-1xtrtz1t.js → index-dmhmde2z.js} +2 -2
  15. package/dist/{index-zr6cvset.js → index-dz6n88aq.js} +5 -5
  16. package/dist/{index-0madxn60.js → index-enc8cmea.js} +1 -1
  17. package/dist/{index-js199er4.js → index-g1drdx7k.js} +12 -1
  18. package/dist/{index-grhdtcb5.js → index-k7zd3awa.js} +14 -5
  19. package/dist/{index-8x8xz80v.js → index-kykzcs7p.js} +5 -4
  20. package/dist/{index-dvq5f3s8.js → index-vbsc8z8h.js} +121 -52
  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.d.ts +1 -1
  25. package/dist/index.js +20 -18
  26. package/dist/jsx-runtime/index.js +4 -4
  27. package/dist/query/index.js +3 -3
  28. package/dist/router/index.js +5 -5
  29. package/dist/router/router.d.ts +1 -0
  30. package/dist/rpc/index.js +4 -4
  31. package/dist/server/index.js +2 -2
  32. package/dist/signals/index.js +1 -1
  33. package/dist/signals/system.d.ts +2 -0
  34. package/dist/style/index.js +6 -6
  35. package/dist/template/index.js +2 -2
  36. package/dist/ui/dropdown-menu.d.ts +1 -1
  37. package/dist/ui/index.d.ts +1 -0
  38. package/dist/ui/index.js +9 -7
  39. package/dist/ui/overlay.d.ts +53 -0
  40. package/dist/ui/popover.d.ts +1 -1
  41. package/dist/ui/utils.d.ts +1 -1
  42. package/dist/virtual/index.js +5 -5
  43. package/package.json +1 -1
  44. package/src/dom/control-flow.ts +31 -21
  45. package/src/form/form.ts +29 -17
  46. package/src/index.ts +2 -0
  47. package/src/router/router.tsx +12 -2
  48. package/src/signals/effect.ts +2 -2
  49. package/src/signals/scope.ts +10 -8
  50. package/src/signals/store.ts +32 -1
  51. package/src/signals/system.ts +8 -0
  52. package/src/template/template.ts +12 -1
  53. package/src/ui/command-dialog.tsx +18 -1
  54. package/src/ui/dialog.tsx +16 -14
  55. package/src/ui/dropdown-menu.tsx +17 -14
  56. package/src/ui/index.ts +4 -0
  57. package/src/ui/overlay.ts +130 -0
  58. package/src/ui/popover.tsx +20 -22
  59. package/src/ui/utils.ts +8 -2
  60. package/src/virtual/virtualizer.ts +31 -18
package/dist/rpc/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  query3,
6
6
  mutation3,
7
7
  subscription3
8
- } from "../index-df5e614s.js";
8
+ } from "../index-awcrz19g.js";
9
9
  import {
10
10
  ERROR_CODE_TO_STATUS2,
11
11
  STATUS_TO_ERROR_CODE2,
@@ -19,9 +19,9 @@ import {
19
19
  validateWithSchema2,
20
20
  v2
21
21
  } from "../index-2zz0g2n2.js";
22
- import"../index-y9bq7h9g.js";
23
- import"../index-a4xns6cc.js";
24
- import"../index-6kfe1nr4.js";
22
+ import"../index-5jhzwtdz.js";
23
+ import"../index-65j7a9et.js";
24
+ import"../index-1yhbg34h.js";
25
25
  export {
26
26
  ERROR_CODE_TO_STATUS2 as ERROR_CODE_TO_STATUS,
27
27
  RPCError2 as RPCError,
@@ -3,8 +3,8 @@ import {
3
3
  renderChildToString2,
4
4
  renderToString2,
5
5
  renderToStream2
6
- } from "../index-x2rs3vab.js";
7
- import"../index-6kfe1nr4.js";
6
+ } from "../index-y3nprgn7.js";
7
+ import"../index-1yhbg34h.js";
8
8
  export {
9
9
  hydrate2 as hydrate,
10
10
  renderChildToString2 as renderChildToString,
@@ -24,7 +24,7 @@ import {
24
24
  createStore2,
25
25
  mutateStore2,
26
26
  unwrapStore2
27
- } from "../index-6kfe1nr4.js";
27
+ } from "../index-1yhbg34h.js";
28
28
  export {
29
29
  COMPUTED_BRAND2 as COMPUTED_BRAND,
30
30
  Computed2 as Computed,
@@ -14,6 +14,8 @@ export declare function setCurrentScope(scope: any): void;
14
14
  export declare function getCurrentScope(): any;
15
15
  export declare function addPendingEffect(eff: any): void;
16
16
  export declare function removePendingEffect(eff: any): void;
17
+ export declare function getBatchDepth(): number;
18
+ export declare function getIsFlushing(): boolean;
17
19
  export declare function flushEffects(): void;
18
20
  /**
19
21
  * Batch multiple signal mutations so downstreams and effects update synchronously once at completion.
@@ -16,9 +16,9 @@ import {
16
16
  radius2,
17
17
  glass2,
18
18
  formatStyleProp2
19
- } from "../index-6kfe1nr4.js";
20
- import"../index-5s255b3w.js";
21
- import"../index-9fvarkrx.js";
19
+ } from "../index-1yhbg34h.js";
20
+ import"../index-xby428n5.js";
21
+ import"../index-3y73zgbk.js";
22
22
  import {
23
23
  hashString2,
24
24
  getScopeId2,
@@ -32,15 +32,15 @@ import {
32
32
  css2,
33
33
  useStyle2,
34
34
  Style2
35
- } from "../index-0madxn60.js";
35
+ } from "../index-enc8cmea.js";
36
36
  import {
37
37
  Stack2,
38
38
  Row2,
39
39
  Center2,
40
40
  Grid2,
41
41
  Spacer2
42
- } from "../index-1xtrtz1t.js";
43
- import"../index-a4xns6cc.js";
42
+ } from "../index-dmhmde2z.js";
43
+ import"../index-65j7a9et.js";
44
44
  export {
45
45
  Center2 as Center,
46
46
  DIMENSION_KEYWORDS2 as DIMENSION_KEYWORDS,
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  template2,
3
3
  html2
4
- } from "../index-js199er4.js";
5
- import"../index-6kfe1nr4.js";
4
+ } from "../index-g1drdx7k.js";
5
+ import"../index-1yhbg34h.js";
6
6
  export {
7
7
  html2 as html,
8
8
  template2 as template
@@ -1,5 +1,5 @@
1
1
  import type { Child } from "../dom/render";
2
- import { type ReadonlySignal } from "../signals";
2
+ import type { ReadonlySignal } from "../signals";
3
3
  export interface DropdownMenuContextValue {
4
4
  open: ReadonlySignal<boolean>;
5
5
  setOpen: (open: boolean) => void;
@@ -6,6 +6,7 @@ export { Dialog, DialogClose, DialogContent, type DialogContentProps, DialogDesc
6
6
  export { DropdownMenu, DropdownMenuContent, DropdownMenuItem, type DropdownMenuItemProps, DropdownMenuPortal, type DropdownMenuProps, DropdownMenuSeparator, DropdownMenuTrigger, useDropdownMenuContext } from "./dropdown-menu";
7
7
  export { createIcon, type IconNode, type IconProps } from "./icon";
8
8
  export { type BaseLayoutProps, Center, type CenterProps, Grid, type GridProps, Row, type RowProps, Spacer, type SpacerProps, Stack, type StackProps } from "./layout";
9
+ export { type UseDismissOptions, useDismiss } from "./overlay";
9
10
  export { Popover, PopoverClose, PopoverContent, type PopoverContentProps, PopoverPortal, type PopoverProps, PopoverTrigger, usePopoverContext } from "./popover";
10
11
  export { Switch, type SwitchProps, SwitchThumb, type SwitchThumbProps } from "./switch";
11
12
  export { type CreateTabsOptions, createTabs, Tabs, TabsContent, type TabsContentProps, TabsList, type TabsProps, TabsTrigger, useTabsContext } from "./tabs";
package/dist/ui/index.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  BottomBarRoot2,
13
13
  BottomBarSeparator2,
14
14
  BottomBar2,
15
+ useDismiss2,
15
16
  CommandDialog2,
16
17
  useCommandContext2,
17
18
  CommandInput2,
@@ -54,7 +55,7 @@ import {
54
55
  ToastDescription2,
55
56
  ToastClose2,
56
57
  Toast2
57
- } from "../index-dvq5f3s8.js";
58
+ } from "../index-vbsc8z8h.js";
58
59
  import {
59
60
  createId2,
60
61
  createControllableSignal2,
@@ -73,18 +74,18 @@ import {
73
74
  Tooltip3,
74
75
  SwitchThumb2,
75
76
  Switch2
76
- } from "../index-8x8xz80v.js";
77
+ } from "../index-kykzcs7p.js";
77
78
  import {
78
79
  Stack2,
79
80
  Row2,
80
81
  Center2,
81
82
  Grid2,
82
83
  Spacer2
83
- } from "../index-1xtrtz1t.js";
84
- import"../index-9fvarkrx.js";
85
- import"../index-5s255b3w.js";
86
- import"../index-a4xns6cc.js";
87
- import"../index-6kfe1nr4.js";
84
+ } from "../index-dmhmde2z.js";
85
+ import"../index-3y73zgbk.js";
86
+ import"../index-xby428n5.js";
87
+ import"../index-65j7a9et.js";
88
+ import"../index-1yhbg34h.js";
88
89
  export {
89
90
  Accordion2 as Accordion,
90
91
  AccordionContent2 as AccordionContent,
@@ -158,6 +159,7 @@ export {
158
159
  useBottomBarContext2 as useBottomBarContext,
159
160
  useCommandContext2 as useCommandContext,
160
161
  useDialogContext2 as useDialogContext,
162
+ useDismiss2 as useDismiss,
161
163
  useDropdownMenuContext2 as useDropdownMenuContext,
162
164
  usePopoverContext2 as usePopoverContext,
163
165
  useTabsContext2 as useTabsContext,
@@ -0,0 +1,53 @@
1
+ export interface UseDismissOptions {
2
+ /**
3
+ * Whether dismiss listeners are active.
4
+ * Defaults to true.
5
+ */
6
+ enabled?: boolean | (() => boolean);
7
+ /**
8
+ * Callback fired when dismissal is requested (Escape, click outside, or close button).
9
+ */
10
+ onDismiss: () => void;
11
+ /**
12
+ * Close on 'Escape' key press.
13
+ * Defaults to true.
14
+ */
15
+ escapeKey?: boolean;
16
+ /**
17
+ * Close on pointer interaction outside the target element(s).
18
+ * Defaults to true.
19
+ */
20
+ outsidePointer?: boolean;
21
+ /**
22
+ * Additional element(s) to exclude from outside click detection (e.g. trigger button).
23
+ */
24
+ outsideIgnore?: Array<HTMLElement | null | undefined> | HTMLElement | null | undefined | (() => Array<HTMLElement | null | undefined>);
25
+ /**
26
+ * Trap keyboard focus inside the container when Tab / Shift+Tab is pressed.
27
+ * Defaults to false.
28
+ */
29
+ trapFocus?: boolean;
30
+ /**
31
+ * Close selector for click events bubbling inside the container (e.g. '[data-dialog-close]').
32
+ */
33
+ closeSelector?: string;
34
+ /**
35
+ * Callback fired when an outside pointer interaction occurs.
36
+ */
37
+ onOutsidePointer?: (event: MouseEvent | TouchEvent) => void;
38
+ /**
39
+ * Callback fired when an Escape keydown occurs.
40
+ */
41
+ onEscapeKeyDown?: (event: KeyboardEvent) => void;
42
+ }
43
+ /**
44
+ * Headless hook to manage overlay dismissal:
45
+ * - Outside pointer dismissal (click outside)
46
+ * - Escape key dismissal
47
+ * - Keyboard focus trapping (Tab cycle)
48
+ * - Close button selector dismissal delegation
49
+ */
50
+ export declare function useDismiss(element: () => HTMLElement | null | undefined, options: UseDismissOptions): {
51
+ handleKeyDown: (e: KeyboardEvent) => void;
52
+ handleClick: (e: MouseEvent) => void;
53
+ };
@@ -1,5 +1,5 @@
1
1
  import type { Child } from "../dom/render";
2
- import { type ReadonlySignal } from "../signals";
2
+ import type { ReadonlySignal } from "../signals";
3
3
  export interface PopoverContextValue {
4
4
  open: ReadonlySignal<boolean>;
5
5
  setOpen: (open: boolean) => void;
@@ -18,4 +18,4 @@ export declare function trapFocus(container: HTMLElement, event: KeyboardEvent):
18
18
  /**
19
19
  * Listen for click outside a target element
20
20
  */
21
- export declare function onClickOutside(elements: Array<HTMLElement | null | undefined> | HTMLElement | null | undefined, handler: (event: MouseEvent | TouchEvent) => void): () => void;
21
+ export declare function onClickOutside(elements: Array<HTMLElement | null | undefined> | HTMLElement | null | undefined | (() => Array<HTMLElement | null | undefined>), handler: (event: MouseEvent | TouchEvent) => void): () => void;
@@ -2,11 +2,11 @@ import {
2
2
  createVirtualizer2,
3
3
  VirtualList2,
4
4
  VirtualGrid2
5
- } from "../index-8czhpea8.js";
6
- import"../index-9fvarkrx.js";
7
- import"../index-5s255b3w.js";
8
- import"../index-a4xns6cc.js";
9
- import"../index-6kfe1nr4.js";
5
+ } from "../index-x6whpv54.js";
6
+ import"../index-3y73zgbk.js";
7
+ import"../index-xby428n5.js";
8
+ import"../index-65j7a9et.js";
9
+ import"../index-1yhbg34h.js";
10
10
  export {
11
11
  VirtualGrid2 as VirtualGrid,
12
12
  VirtualList2 as VirtualList,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epoch32/visage",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Lightning-fast, zero-VDOM, fine-grained reactive UI library with full TypeScript support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,4 +1,13 @@
1
- import { effect, getActiveScope, onCleanup, runInScope, Scope, untrack } from "../signals";
1
+ import {
2
+ effect,
3
+ getActiveScope,
4
+ onCleanup,
5
+ runInScope,
6
+ Scope,
7
+ signal,
8
+ untrack,
9
+ type WritableSignal
10
+ } from "../signals";
2
11
  import { type Child, insertChild, type ReactiveValue, resolveValue } from "./render";
3
12
 
4
13
  export interface ShowProps<T> {
@@ -120,7 +129,7 @@ interface ItemRecord<T> {
120
129
  item: T;
121
130
  scope: Scope;
122
131
  nodes: Node[];
123
- indexSignal: (val?: number) => number;
132
+ indexSignal: WritableSignal<number>;
124
133
  }
125
134
 
126
135
  /**
@@ -206,32 +215,31 @@ export function For<T>(props: ForProps<T>): any {
206
215
  if (!parent) return;
207
216
 
208
217
  const newItems: ItemRecord<T>[] = [];
209
- const prevMap = new Map<T, ItemRecord<T>>();
218
+ const prevMap = new Map<T, ItemRecord<T>[]>();
210
219
 
211
220
  for (const rec of items) {
212
- prevMap.set(rec.item, rec);
221
+ const group = prevMap.get(rec.item);
222
+ if (group) {
223
+ group.push(rec);
224
+ } else {
225
+ prevMap.set(rec.item, [rec]);
226
+ }
213
227
  }
214
228
 
215
229
  // Fast-path: map existing or create new item scopes
216
230
  for (let i = 0; i < list.length; i++) {
217
231
  const item = list[i]!;
218
- const existing = prevMap.get(item);
232
+ const existingGroup = prevMap.get(item);
233
+ const existing =
234
+ existingGroup && existingGroup.length > 0 ? existingGroup.shift() : undefined;
219
235
 
220
236
  if (existing) {
221
- existing.indexSignal(i);
237
+ untrack(() => {
238
+ existing.indexSignal.set(i);
239
+ });
222
240
  newItems.push(existing);
223
- prevMap.delete(item);
224
241
  } else {
225
- let indexVal = i;
226
- const indexSubscribers = new Set<() => void>();
227
- const indexSignal = (...args: [number?]): number => {
228
- if (args.length > 0 && args[0] !== undefined && args[0] !== indexVal) {
229
- indexVal = args[0];
230
- for (const sub of Array.from(indexSubscribers)) sub();
231
- }
232
- return indexVal;
233
- };
234
-
242
+ const indexSignal = signal(i, { internal: true });
235
243
  const itemScope = new Scope(getActiveScope());
236
244
  let itemNodes: Node[] = [];
237
245
 
@@ -254,10 +262,12 @@ export function For<T>(props: ForProps<T>): any {
254
262
  }
255
263
 
256
264
  // Clean up removed items
257
- for (const [, removed] of prevMap) {
258
- removed.scope.dispose();
259
- for (const node of removed.nodes) {
260
- if (node.parentNode) node.parentNode.removeChild(node);
265
+ for (const [, remainingGroup] of prevMap) {
266
+ for (const removed of remainingGroup) {
267
+ removed.scope.dispose();
268
+ for (const node of removed.nodes) {
269
+ if (node.parentNode) node.parentNode.removeChild(node);
270
+ }
261
271
  }
262
272
  }
263
273
 
package/src/form/form.ts CHANGED
@@ -54,6 +54,32 @@ export function createForm<T extends Record<string, any> = Record<string, any>>(
54
54
  const submitCount = signal(0);
55
55
  const customErrors = signal<Partial<Record<keyof T, string>>>({});
56
56
 
57
+ const getValues = (): T => {
58
+ const res: Record<string, any> = {};
59
+ for (const key in fields) {
60
+ res[key] = (fields as any)[key].value();
61
+ }
62
+ return res as T;
63
+ };
64
+
65
+ const schemaValidation = computed(() => {
66
+ if (!options.schema) {
67
+ return { success: true, issuesMap: new Map<string, string>() };
68
+ }
69
+ const currentValues = getValues();
70
+ const res = validateWithSchema(options.schema, currentValues);
71
+ const issuesMap = new Map<string, string>();
72
+ if (!res.success && res.issues) {
73
+ for (const issue of res.issues) {
74
+ const pathKey = String(issue.path);
75
+ if (!issuesMap.has(pathKey)) {
76
+ issuesMap.set(pathKey, issue.message);
77
+ }
78
+ }
79
+ }
80
+ return { success: res.success, issuesMap };
81
+ });
82
+
57
83
  for (const key in initialValues) {
58
84
  const initVal = initialValues[key];
59
85
  const valSignal = signal(initVal);
@@ -74,25 +100,12 @@ export function createForm<T extends Record<string, any> = Record<string, any>>(
74
100
 
75
101
  if (!options.schema) return null;
76
102
 
77
- const currentValues = getValues();
78
- const res = validateWithSchema(options.schema, currentValues);
79
- if (!res.success && res.issues) {
80
- const match = res.issues.find((i) => i.path === key || i.path === `${key}`);
81
- return match ? match.message : null;
82
- }
83
- return null;
103
+ const validation = schemaValidation();
104
+ return validation.issuesMap.get(key) ?? validation.issuesMap.get(String(key)) ?? null;
84
105
  })
85
106
  };
86
107
  }
87
108
 
88
- const getValues = (): T => {
89
- const res: Record<string, any> = {};
90
- for (const key in fields) {
91
- res[key] = (fields as any)[key].value();
92
- }
93
- return res as T;
94
- };
95
-
96
109
  const errors = computed<Partial<Record<keyof T, string>>>(() => {
97
110
  const errMap: Partial<Record<keyof T, string>> = {};
98
111
  for (const key in fields) {
@@ -118,8 +131,7 @@ export function createForm<T extends Record<string, any> = Record<string, any>>(
118
131
 
119
132
  const validate = (): boolean => {
120
133
  if (!options.schema) return true;
121
- const res = validateWithSchema(options.schema, getValues());
122
- return res.success;
134
+ return schemaValidation().success;
123
135
  };
124
136
 
125
137
  const handleSubmit = async (e?: Event): Promise<boolean> => {
package/src/index.ts CHANGED
@@ -405,11 +405,13 @@ export {
405
405
  TooltipTrigger,
406
406
  toast,
407
407
  trapFocus,
408
+ type UseDismissOptions,
408
409
  useAccordionContext,
409
410
  useAccordionItemContext,
410
411
  useBottomBarContext,
411
412
  useCommandContext,
412
413
  useDialogContext,
414
+ useDismiss,
413
415
  useDropdownMenuContext,
414
416
  usePopoverContext,
415
417
  useTabsContext,
@@ -253,6 +253,7 @@ export function Routes(props: RoutesProps): Child {
253
253
  export interface LinkProps {
254
254
  href: string;
255
255
  class?: string | (() => string);
256
+ className?: string | (() => string);
256
257
  activeClass?: string;
257
258
  replace?: boolean;
258
259
  children: Child;
@@ -264,7 +265,16 @@ export interface LinkProps {
264
265
  */
265
266
  export function Link(props: LinkProps): Child {
266
267
  const router = useContext(RouterContext);
267
- const { href, class: className, activeClass = "active", replace, children, ...rest } = props;
268
+ const {
269
+ href,
270
+ class: classProp,
271
+ className: classNameProp,
272
+ activeClass = "active",
273
+ replace,
274
+ children,
275
+ ...rest
276
+ } = props;
277
+ const rawClass = classProp ?? classNameProp;
268
278
 
269
279
  const handleClick = (e: MouseEvent) => {
270
280
  if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.button !== 0) {
@@ -286,7 +296,7 @@ export function Link(props: LinkProps): Child {
286
296
  });
287
297
 
288
298
  const computedClass = computed(() => {
289
- const baseCls = typeof className === "function" ? className() : (className ?? "");
299
+ const baseCls = typeof rawClass === "function" ? rawClass() : (rawClass ?? "");
290
300
  if (activeClass && isActive()) {
291
301
  return baseCls ? `${baseCls} ${activeClass}` : activeClass;
292
302
  }
@@ -5,8 +5,8 @@ import { Computed } from "./computed";
5
5
  import type { Scope } from "./scope";
6
6
  import {
7
7
  addPendingEffect,
8
- batchDepth,
9
8
  flushEffects,
9
+ getBatchDepth,
10
10
  getCurrentScope,
11
11
  getCurrentSubscriber,
12
12
  removePendingEffect,
@@ -57,7 +57,7 @@ export class Effect implements ISubscriber {
57
57
  this.state = targetState;
58
58
  addPendingEffect(this);
59
59
 
60
- if (batchDepth === 0) {
60
+ if (getBatchDepth() === 0) {
61
61
  flushEffects();
62
62
  }
63
63
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Visage Reactive Scope & Lifecycle Management
3
3
  */
4
- import { currentScope, currentSubscriber, getCurrentScope, setCurrentScope } from "./system";
4
+ import { getCurrentScope, getCurrentSubscriber, setCurrentScope } from "./system";
5
5
  import type { CleanupFn, ISubscriber } from "./types";
6
6
 
7
7
  /**
@@ -14,7 +14,7 @@ export class Scope {
14
14
  isDisposed = false;
15
15
  context?: Map<symbol, any>;
16
16
 
17
- constructor(parent: Scope | null = currentScope) {
17
+ constructor(parent: Scope | null = getCurrentScope()) {
18
18
  this.parent = parent;
19
19
  if (parent && !parent.isDisposed) {
20
20
  parent.cleanups.push(() => this.dispose());
@@ -72,10 +72,12 @@ export class Scope {
72
72
  * ```
73
73
  */
74
74
  export function onCleanup(fn: CleanupFn): void {
75
- if (currentSubscriber && "addCleanup" in currentSubscriber) {
76
- (currentSubscriber as any).addCleanup(fn);
77
- } else if (currentScope) {
78
- currentScope.addCleanup(fn);
75
+ const curSub = getCurrentSubscriber();
76
+ const curScope = getCurrentScope();
77
+ if (curSub && "addCleanup" in curSub) {
78
+ (curSub as any).addCleanup(fn);
79
+ } else if (curScope) {
80
+ curScope.addCleanup(fn);
79
81
  }
80
82
  }
81
83
 
@@ -101,7 +103,7 @@ export function onCleanup(fn: CleanupFn): void {
101
103
  * @returns The return value of `fn`
102
104
  */
103
105
  export function createRoot<T>(fn: (dispose: () => void) => T): T {
104
- const prevScope = currentScope;
106
+ const prevScope = getCurrentScope();
105
107
  const scope = new Scope(null);
106
108
  setCurrentScope(scope);
107
109
  try {
@@ -115,7 +117,7 @@ export function createRoot<T>(fn: (dispose: () => void) => T): T {
115
117
  * Run a function with a specified reactive scope active.
116
118
  */
117
119
  export function runInScope<T>(scope: Scope | null, fn: () => T): T {
118
- const prevScope = currentScope;
120
+ const prevScope = getCurrentScope();
119
121
  setCurrentScope(scope);
120
122
  try {
121
123
  return fn();
@@ -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) {