@uistate/examples 1.0.0

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 (137) hide show
  1. package/README.md +40 -0
  2. package/cssState/.gitkeep +0 -0
  3. package/eventState/001-counter/README.md +44 -0
  4. package/eventState/001-counter/index.html +33 -0
  5. package/eventState/002-counter-improved/README.md +44 -0
  6. package/eventState/002-counter-improved/index.html +47 -0
  7. package/eventState/003-input-reactive/README.md +44 -0
  8. package/eventState/003-input-reactive/index.html +33 -0
  9. package/eventState/004-computed-state/README.md +45 -0
  10. package/eventState/004-computed-state/index.html +65 -0
  11. package/eventState/005-conditional-rendering/README.md +42 -0
  12. package/eventState/005-conditional-rendering/index.html +39 -0
  13. package/eventState/006-list-rendering/README.md +49 -0
  14. package/eventState/006-list-rendering/index.html +63 -0
  15. package/eventState/007-form-validation/README.md +52 -0
  16. package/eventState/007-form-validation/index.html +102 -0
  17. package/eventState/008-undo-redo/README.md +70 -0
  18. package/eventState/008-undo-redo/index.html +108 -0
  19. package/eventState/009-localStorage-side-effects/README.md +72 -0
  20. package/eventState/009-localStorage-side-effects/index.html +57 -0
  21. package/eventState/010-decoupled-components/README.md +74 -0
  22. package/eventState/010-decoupled-components/index.html +93 -0
  23. package/eventState/011-async-patterns/README.md +98 -0
  24. package/eventState/011-async-patterns/index.html +132 -0
  25. package/eventState/028-counter-improved-eventTest/LICENSE +55 -0
  26. package/eventState/028-counter-improved-eventTest/README.md +131 -0
  27. package/eventState/028-counter-improved-eventTest/app/store.js +9 -0
  28. package/eventState/028-counter-improved-eventTest/index.html +49 -0
  29. package/eventState/028-counter-improved-eventTest/runtime/core/behaviors.runtime.js +282 -0
  30. package/eventState/028-counter-improved-eventTest/runtime/core/eventState.js +100 -0
  31. package/eventState/028-counter-improved-eventTest/runtime/core/eventStateNew.js +149 -0
  32. package/eventState/028-counter-improved-eventTest/runtime/core/helpers.js +212 -0
  33. package/eventState/028-counter-improved-eventTest/runtime/core/router.js +271 -0
  34. package/eventState/028-counter-improved-eventTest/store.d.ts +8 -0
  35. package/eventState/028-counter-improved-eventTest/style.css +170 -0
  36. package/eventState/028-counter-improved-eventTest/tests/README.md +208 -0
  37. package/eventState/028-counter-improved-eventTest/tests/counter.test.js +116 -0
  38. package/eventState/028-counter-improved-eventTest/tests/eventTest.js +176 -0
  39. package/eventState/028-counter-improved-eventTest/tests/generateTypes.js +168 -0
  40. package/eventState/028-counter-improved-eventTest/tests/run.js +20 -0
  41. package/eventState/030-todo-app-with-eventTest/LICENSE +55 -0
  42. package/eventState/030-todo-app-with-eventTest/README.md +121 -0
  43. package/eventState/030-todo-app-with-eventTest/app/router.js +25 -0
  44. package/eventState/030-todo-app-with-eventTest/app/store.js +16 -0
  45. package/eventState/030-todo-app-with-eventTest/app/views/home.js +11 -0
  46. package/eventState/030-todo-app-with-eventTest/app/views/todoDemo.js +88 -0
  47. package/eventState/030-todo-app-with-eventTest/index.html +65 -0
  48. package/eventState/030-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  49. package/eventState/030-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  50. package/eventState/030-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
  51. package/eventState/030-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  52. package/eventState/030-todo-app-with-eventTest/runtime/core/router.js +271 -0
  53. package/eventState/030-todo-app-with-eventTest/store.d.ts +18 -0
  54. package/eventState/030-todo-app-with-eventTest/style.css +170 -0
  55. package/eventState/030-todo-app-with-eventTest/tests/README.md +208 -0
  56. package/eventState/030-todo-app-with-eventTest/tests/eventTest.js +176 -0
  57. package/eventState/030-todo-app-with-eventTest/tests/generateTypes.js +189 -0
  58. package/eventState/030-todo-app-with-eventTest/tests/run.js +20 -0
  59. package/eventState/030-todo-app-with-eventTest/tests/todos.test.js +167 -0
  60. package/eventState/031-todo-app-with-eventTest/LICENSE +55 -0
  61. package/eventState/031-todo-app-with-eventTest/README.md +54 -0
  62. package/eventState/031-todo-app-with-eventTest/TUTORIAL.md +390 -0
  63. package/eventState/031-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
  64. package/eventState/031-todo-app-with-eventTest/app/bridges.js +113 -0
  65. package/eventState/031-todo-app-with-eventTest/app/router.js +26 -0
  66. package/eventState/031-todo-app-with-eventTest/app/store.js +15 -0
  67. package/eventState/031-todo-app-with-eventTest/app/views/home.js +46 -0
  68. package/eventState/031-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
  69. package/eventState/031-todo-app-with-eventTest/devtools/dock.js +41 -0
  70. package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
  71. package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
  72. package/eventState/031-todo-app-with-eventTest/devtools/telemetry.js +104 -0
  73. package/eventState/031-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
  74. package/eventState/031-todo-app-with-eventTest/index.html +103 -0
  75. package/eventState/031-todo-app-with-eventTest/package-lock.json +2184 -0
  76. package/eventState/031-todo-app-with-eventTest/package.json +24 -0
  77. package/eventState/031-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  78. package/eventState/031-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  79. package/eventState/031-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
  80. package/eventState/031-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  81. package/eventState/031-todo-app-with-eventTest/runtime/core/router.js +271 -0
  82. package/eventState/031-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
  83. package/eventState/031-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
  84. package/eventState/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
  85. package/eventState/031-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
  86. package/eventState/031-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
  87. package/eventState/031-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
  88. package/eventState/031-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
  89. package/eventState/031-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
  90. package/eventState/031-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
  91. package/eventState/031-todo-app-with-eventTest/store.d.ts +23 -0
  92. package/eventState/031-todo-app-with-eventTest/style.css +170 -0
  93. package/eventState/031-todo-app-with-eventTest/tests/README.md +208 -0
  94. package/eventState/031-todo-app-with-eventTest/tests/eventTest.js +176 -0
  95. package/eventState/031-todo-app-with-eventTest/tests/generateTypes.js +191 -0
  96. package/eventState/031-todo-app-with-eventTest/tests/run.js +20 -0
  97. package/eventState/031-todo-app-with-eventTest/tests/todos.test.js +192 -0
  98. package/eventState/032-todo-app-with-eventTest/LICENSE +55 -0
  99. package/eventState/032-todo-app-with-eventTest/README.md +54 -0
  100. package/eventState/032-todo-app-with-eventTest/TUTORIAL.md +390 -0
  101. package/eventState/032-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
  102. package/eventState/032-todo-app-with-eventTest/app/actions/index.js +153 -0
  103. package/eventState/032-todo-app-with-eventTest/app/bridges.js +113 -0
  104. package/eventState/032-todo-app-with-eventTest/app/router.js +26 -0
  105. package/eventState/032-todo-app-with-eventTest/app/store.js +15 -0
  106. package/eventState/032-todo-app-with-eventTest/app/views/home.js +46 -0
  107. package/eventState/032-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
  108. package/eventState/032-todo-app-with-eventTest/devtools/dock.js +41 -0
  109. package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
  110. package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
  111. package/eventState/032-todo-app-with-eventTest/devtools/telemetry.js +104 -0
  112. package/eventState/032-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
  113. package/eventState/032-todo-app-with-eventTest/index.html +87 -0
  114. package/eventState/032-todo-app-with-eventTest/package-lock.json +2184 -0
  115. package/eventState/032-todo-app-with-eventTest/package.json +24 -0
  116. package/eventState/032-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  117. package/eventState/032-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  118. package/eventState/032-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
  119. package/eventState/032-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  120. package/eventState/032-todo-app-with-eventTest/runtime/core/router.js +271 -0
  121. package/eventState/032-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
  122. package/eventState/032-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
  123. package/eventState/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
  124. package/eventState/032-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
  125. package/eventState/032-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
  126. package/eventState/032-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
  127. package/eventState/032-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
  128. package/eventState/032-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
  129. package/eventState/032-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
  130. package/eventState/032-todo-app-with-eventTest/store.d.ts +23 -0
  131. package/eventState/032-todo-app-with-eventTest/style.css +170 -0
  132. package/eventState/032-todo-app-with-eventTest/tests/README.md +208 -0
  133. package/eventState/032-todo-app-with-eventTest/tests/eventTest.js +176 -0
  134. package/eventState/032-todo-app-with-eventTest/tests/generateTypes.js +191 -0
  135. package/eventState/032-todo-app-with-eventTest/tests/run.js +20 -0
  136. package/eventState/032-todo-app-with-eventTest/tests/todos.test.js +192 -0
  137. package/package.json +27 -0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * UIstate - Event-based hierarchical state management module
3
+ * Part of the UIstate declarative state management system
4
+ * Uses DOM events for pub/sub with hierarchical path support
5
+ */
6
+
7
+ const createEventState = (initial = {}) => {
8
+ // // Clone the initial state to avoid direct mutations to the passed object
9
+ const store = JSON.parse(JSON.stringify(initial));
10
+
11
+ // // Create a dedicated DOM element to use as an event bus
12
+ // const bus = document.createElement("x-store");
13
+
14
+ // // Optional: Keep the bus element off the actual DOM for better encapsulation
15
+ // // but this isn't strictly necessary for functionality
16
+ // bus.style.display = "none";
17
+ // document.documentElement.appendChild(bus);
18
+
19
+ const bus = new EventTarget();
20
+ return {
21
+ // get a value from the store by path
22
+ get: (path) => {
23
+ if (!path) return store;
24
+ return path
25
+ .split(".")
26
+ .reduce(
27
+ (obj, prop) =>
28
+ obj && obj[prop] !== undefined ? obj[prop] : undefined,
29
+ store
30
+ );
31
+ },
32
+
33
+ // set a value in the store by path
34
+ set: (path, value) => {
35
+ if(!path) return;
36
+
37
+ // Update the store
38
+ let target = store;
39
+ const parts = path.split(".");
40
+ const last = parts.pop();
41
+
42
+ // Create the path if it doesn't exist
43
+ parts.forEach((part) => {
44
+ if (!target[part] || typeof target[part] !== "object") {
45
+ target[part] = {};
46
+ }
47
+ target = target[part];
48
+ });
49
+
50
+ // Set the value
51
+ target[last] = value;
52
+
53
+ // Notify subscribers with a DOM event
54
+ bus.dispatchEvent(new CustomEvent(path, { detail: value }));
55
+
56
+ // Also dispatch events for parent paths to support wildcards
57
+ if (parts.length > 0) {
58
+ let parentPath = "";
59
+ for (const part of parts) {
60
+ parentPath = parentPath ? `${parentPath}.${part}` : part;
61
+ bus.dispatchEvent(
62
+ new CustomEvent(`${parentPath}.*`, {
63
+ detail: { path, value },
64
+ })
65
+ );
66
+ }
67
+
68
+ // Dispatch root wildcard for any state change
69
+ bus.dispatchEvent(
70
+ new CustomEvent("*", {
71
+ detail: { path, value},
72
+ })
73
+ );
74
+ }
75
+
76
+ return value;
77
+ },
78
+
79
+ // Subscribe to changes on a path
80
+ subscribe: (path, callback) => {
81
+ if (!path || typeof callback !== "function") return () => {};
82
+
83
+ const handler = (e) => callback(e.detail, path);
84
+ bus.addEventListener(path, handler);
85
+
86
+ return () => bus.removeEventListener(path, handler);
87
+ },
88
+
89
+ // Optional method to clean up resources
90
+ destroy: () => {
91
+ if (bus.parentNode) {
92
+ bus.parentNode.removeChild(bus);
93
+ }
94
+ },
95
+
96
+ };
97
+ };
98
+
99
+ export default createEventState;
100
+ export { createEventState };
@@ -0,0 +1,149 @@
1
+ /**
2
+ * EventState v2 - Optimized Path-Based State Management
3
+ *
4
+ * A lightweight, performant state management library using path-based subscriptions.
5
+ * Optimized for selective notifications and granular updates.
6
+ *
7
+ * Features:
8
+ * - Path-based get/set operations (e.g., 'user.profile.name')
9
+ * - Selective subscriptions (only relevant subscribers fire)
10
+ * - Wildcard subscriptions (e.g., 'user.*' catches all user changes)
11
+ * - Global subscriptions (e.g., '*' catches all changes)
12
+ * - Zero dependencies
13
+ * - ~2KB minified
14
+ *
15
+ * Performance characteristics:
16
+ * - 2-9x faster than Zustand for selective subscriptions
17
+ * - Competitive overall performance
18
+ * - Minimal rendering overhead (1.27x faster paint times)
19
+ *
20
+ * @example
21
+ * const store = createEventState({ count: 0, user: { name: 'Alice' } });
22
+ *
23
+ * // Subscribe to specific path (receives value directly)
24
+ * const unsub = store.subscribe('count', (value) => {
25
+ * console.log('Count changed:', value);
26
+ * });
27
+ *
28
+ * // Update state
29
+ * store.set('count', 1);
30
+ *
31
+ * // Get state
32
+ * const count = store.get('count');
33
+ *
34
+ * // Wildcard subscription
35
+ * store.subscribe('user.*', ({ path, value }) => {
36
+ * console.log(`User field ${path} changed to:`, value);
37
+ * });
38
+ *
39
+ * // Global subscription
40
+ * store.subscribe('*', ({ path, value }) => {
41
+ * console.log(`State changed at ${path}:`, value);
42
+ * });
43
+ *
44
+ * // Cleanup
45
+ * unsub();
46
+ * store.destroy();
47
+ */
48
+
49
+ export function createEventState(initial = {}) {
50
+ const state = JSON.parse(JSON.stringify(initial));
51
+ const listeners = new Map();
52
+ let destroyed = false;
53
+
54
+ return {
55
+ /**
56
+ * Get value at path
57
+ * @param {string} path - Dot-separated path (e.g., 'user.profile.name')
58
+ * @returns {*} Value at path, or entire state if no path provided
59
+ */
60
+ get(path) {
61
+ if (destroyed) throw new Error('Cannot get from destroyed store');
62
+ if (!path) return state;
63
+ return path.split(".").reduce((obj, key) => obj?.[key], state);
64
+ },
65
+
66
+ /**
67
+ * Set value at path and notify subscribers
68
+ * @param {string} path - Dot-separated path (e.g., 'user.profile.name')
69
+ * @param {*} value - New value
70
+ * @returns {*} The value that was set
71
+ */
72
+ set(path, value) {
73
+ if (destroyed) throw new Error('Cannot set on destroyed store');
74
+ if (!path) return value;
75
+
76
+ const parts = path.split(".");
77
+ const key = parts.pop();
78
+ let cur = state;
79
+
80
+ // Navigate to parent object, creating nested objects as needed
81
+ for (const p of parts) {
82
+ if (!cur[p]) cur[p] = {};
83
+ cur = cur[p];
84
+ }
85
+
86
+ const oldValue = cur[key];
87
+ cur[key] = value;
88
+
89
+ if (!destroyed) {
90
+ const detail = { path, value, oldValue };
91
+
92
+ // Notify exact path subscribers (pass value directly for backwards compatibility)
93
+ const exactListeners = listeners.get(path);
94
+ if (exactListeners) {
95
+ exactListeners.forEach(cb => cb(value));
96
+ }
97
+
98
+ // Notify wildcard subscribers for all parent paths (pass detail object)
99
+ for (let i = 0; i < parts.length; i++) {
100
+ const parentPath = parts.slice(0, i + 1).join('.');
101
+ const wildcardListeners = listeners.get(`${parentPath}.*`);
102
+ if (wildcardListeners) {
103
+ wildcardListeners.forEach(cb => cb(detail));
104
+ }
105
+ }
106
+
107
+ // Notify global subscribers (pass detail object)
108
+ const globalListeners = listeners.get('*');
109
+ if (globalListeners) {
110
+ globalListeners.forEach(cb => cb(detail));
111
+ }
112
+ }
113
+
114
+ return value;
115
+ },
116
+
117
+ /**
118
+ * Subscribe to changes at path
119
+ * @param {string} path - Path to subscribe to (supports wildcards: 'user.*', '*')
120
+ * @param {Function} handler - Callback function receiving { path, value, oldValue }
121
+ * @returns {Function} Unsubscribe function
122
+ */
123
+ subscribe(path, handler) {
124
+ if (destroyed) throw new Error('Cannot subscribe to destroyed store');
125
+ if (!path || typeof handler !== 'function') {
126
+ throw new TypeError('subscribe requires path and handler');
127
+ }
128
+
129
+ if (!listeners.has(path)) {
130
+ listeners.set(path, new Set());
131
+ }
132
+ listeners.get(path).add(handler);
133
+
134
+ return () => listeners.get(path)?.delete(handler);
135
+ },
136
+
137
+ /**
138
+ * Destroy store and clear all subscriptions
139
+ */
140
+ destroy() {
141
+ if (!destroyed) {
142
+ destroyed = true;
143
+ listeners.clear();
144
+ }
145
+ }
146
+ };
147
+ }
148
+
149
+ export default createEventState;
@@ -0,0 +1,212 @@
1
+ // helpers.js
2
+ export const intent = (store, name, payload = true) => store.set(`intent.${name}`, payload);
3
+
4
+ export const bindIntentClicks = (root, store, payloadFromEvent) => {
5
+ root.addEventListener('click', (e) => {
6
+ const t = e.target.closest('[data-intent]');
7
+ if (!t) return;
8
+ const name = t.dataset.intent;
9
+ const payload = payloadFromEvent ? payloadFromEvent(e, t) : true;
10
+ store.set(`intent.${name}`, payload);
11
+ });
12
+ };
13
+
14
+ export const mount = (a, b) => {
15
+ // Overloads:
16
+ // - mount(selectorsMap) -> root defaults to document
17
+ // - mount(root, selectorsMap)
18
+ let root, selectors;
19
+ if (typeof b === 'undefined') {
20
+ selectors = a;
21
+ root = document;
22
+ } else {
23
+ root = a;
24
+ selectors = b;
25
+ }
26
+ const entries = Object.entries(selectors).map(([k, sel]) => {
27
+ const el = typeof sel === 'string' ? root.querySelector(sel) : sel;
28
+ return [k, el];
29
+ });
30
+ return Object.fromEntries(entries);
31
+ };
32
+
33
+ export const renderJson = (el, getSnapshot) => {
34
+ try { el.textContent = JSON.stringify(getSnapshot(), null, 2); }
35
+ catch { el.textContent = String(getSnapshot()); }
36
+ };
37
+
38
+ // Simple plug-and-play state panel. Attempts to use a provided target (selector or element),
39
+ // then '#state' if present, otherwise creates a floating <pre> panel in the bottom-right.
40
+ export const showStatePanel = (store, target) => {
41
+ let el = null;
42
+ if (typeof target === 'string') {
43
+ el = document.querySelector(target);
44
+ } else if (target && target.nodeType === 1) {
45
+ el = target; // DOM Element
46
+ }
47
+ if (!el) {
48
+ el = document.querySelector('#state');
49
+ }
50
+ if (!el) {
51
+ el = document.createElement('pre');
52
+ el.setAttribute('id', 'state');
53
+ Object.assign(el.style, {
54
+ position: 'fixed', right: '8px', bottom: '8px',
55
+ minWidth: '240px', maxWidth: '40vw', maxHeight: '40vh', overflow: 'auto',
56
+ padding: '8px', background: 'rgba(0,0,0,0.7)', color: '#0f0',
57
+ font: '12px/1.4 monospace', borderRadius: '6px', zIndex: 99999,
58
+ boxShadow: '0 2px 12px rgba(0,0,0,0.35)'
59
+ });
60
+ document.body.appendChild(el);
61
+ }
62
+ const render = () => {
63
+ try { el.textContent = JSON.stringify(store.get(), null, 2); }
64
+ catch { el.textContent = String(store.get()); }
65
+ };
66
+ store.subscribe('*', render);
67
+ render();
68
+ return el;
69
+ };
70
+
71
+ // Subscribe multiple path/handler pairs at once. Returns a SubGroup with:
72
+ // - dispose(): unsubscribe all
73
+ // - unsubs: individual unsubscribe functions (in pair order)
74
+ // - byPath: Record<string, Function[]> to selectively dispose by path
75
+ // - size: number of subscriptions created
76
+ export const groupSubs = (store, ...args) => {
77
+ if (args.length % 2 !== 0) {
78
+ throw new Error('groupSubs expects alternating path/handler pairs');
79
+ }
80
+ const unsubs = [];
81
+ const byPath = Object.create(null);
82
+ for (let i = 0; i < args.length; i += 2) {
83
+ const path = args[i];
84
+ const handler = args[i + 1];
85
+ const unsub = store.subscribe(path, handler);
86
+ unsubs.push(unsub);
87
+ (byPath[path] || (byPath[path] = [])).push(unsub);
88
+ }
89
+ const dispose = () => {
90
+ for (let i = unsubs.length - 1; i >= 0; i--) {
91
+ try { unsubs[i](); } catch (_) { /* noop */ }
92
+ }
93
+ };
94
+ return { dispose, unsubs, byPath, size: unsubs.length };
95
+ };
96
+
97
+ // Console logger for state changes; subscribes to '*' wildcard and logs path/value.
98
+ // Optional custom formatter receives the event object `{ path, value }`.
99
+ export const consoleLogState = (store, formatter) => {
100
+ const handler = (evt) => {
101
+ const payload = evt && typeof evt === 'object' && 'path' in evt ? evt : { path: '*', value: store.get() };
102
+ if (formatter) return formatter(payload);
103
+ console.log('[state]', payload.path, payload.value);
104
+ };
105
+ return store.subscribe('*', handler);
106
+ };
107
+
108
+ // JS-only wiring helpers (no DOM data-* exposure)
109
+ export const onClick = (el, handler) => {
110
+ el.addEventListener('click', handler);
111
+ return () => el.removeEventListener('click', handler);
112
+ };
113
+
114
+ export const bump = (store, path, delta = 1) => {
115
+ const n = (store.get(path) || 0) + delta;
116
+ store.set(path, n);
117
+ };
118
+ export const inc = (store, path) => bump(store, path, 1);
119
+ export const dec = (store, path) => bump(store, path, -1);
120
+
121
+ // Intent wiring without using data-* attributes
122
+ export const bindIntent = (store, el, name, payloadFromEvent) => {
123
+ const listener = (e) => {
124
+ const payload = payloadFromEvent ? payloadFromEvent(e, el) : true;
125
+ store.set(`intent.${name}`, payload);
126
+ };
127
+ el.addEventListener('click', listener);
128
+ return () => el.removeEventListener('click', listener);
129
+ };
130
+
131
+ export const bindIntents = (store, tuples) => {
132
+ const unsubs = tuples.map(([el, name, payloadFromEvent]) => bindIntent(store, el, name, payloadFromEvent));
133
+ return () => { for (let i = unsubs.length - 1; i >= 0; i--) { try { unsubs[i](); } catch (_) {} } };
134
+ };
135
+
136
+ // Orchestrator: initialize a view by mounting elements, subscribing handlers, wiring events, and optional dev tools.
137
+ // API:
138
+ // initView({
139
+ // store, // required
140
+ // mount: selectors | [root, selectors],
141
+ // paths: { ALIAS: 'a.b.c' }, // optional path aliases
142
+ // view: [ ['path', (value, els, store, p) => { /* render */ }], ... ],
143
+ // dev: { log: true, panel: true },
144
+ // events: (els, store, p) => [ /* array of unsubs */ ],
145
+ // })
146
+ export const initView = (cfg) => {
147
+ if (!cfg || !cfg.store) throw new Error('initView: cfg.store is required');
148
+ const store = cfg.store;
149
+ const mountCfg = cfg.mount;
150
+ const p = Object.assign({}, cfg.paths || {});
151
+ const dev = cfg.dev || {};
152
+
153
+ // Resolve elements once
154
+ const els = Array.isArray(mountCfg) ? mount(mountCfg[0], mountCfg[1]) : mount(mountCfg || {});
155
+
156
+ // Build grouped subscriptions from view tuples, wrapping to inject (els, store, p)
157
+ const viewTuples = (cfg.view || []).flatMap(([path, handler]) => {
158
+ let wrapped;
159
+ if (typeof handler === 'string') {
160
+ const key = handler;
161
+ wrapped = (value) => {
162
+ const el = els[key];
163
+ if (el) el.textContent = String(value);
164
+ };
165
+ } else {
166
+ wrapped = (value) => handler && handler(value, els, store, p);
167
+ }
168
+ return [path, wrapped];
169
+ });
170
+ const subs = viewTuples.length ? groupSubs(store, ...viewTuples) : { dispose(){} };
171
+
172
+ // Dev tools
173
+ const devUnsubs = [];
174
+ if (dev.log) {
175
+ try { devUnsubs.push(consoleLogState(store)); } catch (_) {}
176
+ }
177
+ let panelEl = null;
178
+ if (dev.panel) {
179
+ try { panelEl = showStatePanel(store); } catch (_) {}
180
+ }
181
+
182
+ // Events wiring
183
+ let eventUnsubs = [];
184
+ if (typeof cfg.events === 'function') {
185
+ try { eventUnsubs = cfg.events(els, store, p) || []; } catch (_) { eventUnsubs = []; }
186
+ } else if (cfg.events && typeof cfg.events === 'object') {
187
+ // Sugar: events map { elKey: (store, paths, el) => void | unsub }
188
+ const map = cfg.events;
189
+ eventUnsubs = Object.entries(map).map(([key, fn]) => {
190
+ const el = els[key];
191
+ if (!el || typeof fn !== 'function') return () => {};
192
+ // Default to click wiring; allow handler to return its own unsub.
193
+ const handler = () => fn(store, p, el);
194
+ el.addEventListener('click', handler);
195
+ return () => el.removeEventListener('click', handler);
196
+ });
197
+ }
198
+
199
+ // Unified disposer
200
+ const dispose = () => {
201
+ try { subs && subs.dispose && subs.dispose(); } catch (_) {}
202
+ for (let i = eventUnsubs.length - 1; i >= 0; i--) {
203
+ try { eventUnsubs[i] && eventUnsubs[i](); } catch (_) {}
204
+ }
205
+ for (let i = devUnsubs.length - 1; i >= 0; i--) {
206
+ try { devUnsubs[i] && devUnsubs[i](); } catch (_) {}
207
+ }
208
+ // Note: showStatePanel returns an element; we do not auto-remove it by default.
209
+ };
210
+
211
+ return { els, paths: p, subs, eventUnsubs, devUnsubs, dispose };
212
+ };