create-microact-app 1.0.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 (213) hide show
  1. package/index.js +95 -0
  2. package/package.json +21 -0
  3. package/templates/vanilla/.github/workflows/deploy.yml +38 -0
  4. package/templates/vanilla/index.html +13 -0
  5. package/templates/vanilla/node_modules/.package-lock.json +207 -0
  6. package/templates/vanilla/node_modules/@esbuild/darwin-x64/README.md +3 -0
  7. package/templates/vanilla/node_modules/@esbuild/darwin-x64/bin/esbuild +0 -0
  8. package/templates/vanilla/node_modules/@esbuild/darwin-x64/package.json +17 -0
  9. package/templates/vanilla/node_modules/@monygroupcorp/microact/README.md +154 -0
  10. package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.cjs.js +1749 -0
  11. package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.cjs.js.map +1 -0
  12. package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.esm.js +1743 -0
  13. package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.esm.js.map +1 -0
  14. package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.umd.js +2 -0
  15. package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.umd.js.map +1 -0
  16. package/templates/vanilla/node_modules/@monygroupcorp/microact/example/index.html +13 -0
  17. package/templates/vanilla/node_modules/@monygroupcorp/microact/example/index.js +63 -0
  18. package/templates/vanilla/node_modules/@monygroupcorp/microact/package.json +38 -0
  19. package/templates/vanilla/node_modules/@monygroupcorp/microact/rollup.config.cjs +30 -0
  20. package/templates/vanilla/node_modules/@monygroupcorp/microact/src/Component.js +831 -0
  21. package/templates/vanilla/node_modules/@monygroupcorp/microact/src/DOMUpdater.js +320 -0
  22. package/templates/vanilla/node_modules/@monygroupcorp/microact/src/EventBus.js +123 -0
  23. package/templates/vanilla/node_modules/@monygroupcorp/microact/src/Router.js +253 -0
  24. package/templates/vanilla/node_modules/@monygroupcorp/microact/src/UpdateScheduler.js +218 -0
  25. package/templates/vanilla/node_modules/@monygroupcorp/microact/src/index.js +6 -0
  26. package/templates/vanilla/node_modules/esbuild/LICENSE.md +21 -0
  27. package/templates/vanilla/node_modules/esbuild/README.md +3 -0
  28. package/templates/vanilla/node_modules/esbuild/bin/esbuild +0 -0
  29. package/templates/vanilla/node_modules/esbuild/install.js +287 -0
  30. package/templates/vanilla/node_modules/esbuild/lib/main.d.ts +660 -0
  31. package/templates/vanilla/node_modules/esbuild/lib/main.js +2393 -0
  32. package/templates/vanilla/node_modules/esbuild/package.json +42 -0
  33. package/templates/vanilla/node_modules/nanoid/LICENSE +20 -0
  34. package/templates/vanilla/node_modules/nanoid/README.md +39 -0
  35. package/templates/vanilla/node_modules/nanoid/async/index.browser.cjs +69 -0
  36. package/templates/vanilla/node_modules/nanoid/async/index.browser.js +34 -0
  37. package/templates/vanilla/node_modules/nanoid/async/index.cjs +71 -0
  38. package/templates/vanilla/node_modules/nanoid/async/index.d.ts +56 -0
  39. package/templates/vanilla/node_modules/nanoid/async/index.js +35 -0
  40. package/templates/vanilla/node_modules/nanoid/async/index.native.js +26 -0
  41. package/templates/vanilla/node_modules/nanoid/async/package.json +12 -0
  42. package/templates/vanilla/node_modules/nanoid/bin/nanoid.cjs +55 -0
  43. package/templates/vanilla/node_modules/nanoid/index.browser.cjs +72 -0
  44. package/templates/vanilla/node_modules/nanoid/index.browser.js +34 -0
  45. package/templates/vanilla/node_modules/nanoid/index.cjs +85 -0
  46. package/templates/vanilla/node_modules/nanoid/index.d.cts +91 -0
  47. package/templates/vanilla/node_modules/nanoid/index.d.ts +91 -0
  48. package/templates/vanilla/node_modules/nanoid/index.js +45 -0
  49. package/templates/vanilla/node_modules/nanoid/nanoid.js +1 -0
  50. package/templates/vanilla/node_modules/nanoid/non-secure/index.cjs +34 -0
  51. package/templates/vanilla/node_modules/nanoid/non-secure/index.d.ts +33 -0
  52. package/templates/vanilla/node_modules/nanoid/non-secure/index.js +21 -0
  53. package/templates/vanilla/node_modules/nanoid/non-secure/package.json +6 -0
  54. package/templates/vanilla/node_modules/nanoid/package.json +89 -0
  55. package/templates/vanilla/node_modules/nanoid/url-alphabet/index.cjs +7 -0
  56. package/templates/vanilla/node_modules/nanoid/url-alphabet/index.js +3 -0
  57. package/templates/vanilla/node_modules/nanoid/url-alphabet/package.json +6 -0
  58. package/templates/vanilla/node_modules/picocolors/LICENSE +15 -0
  59. package/templates/vanilla/node_modules/picocolors/README.md +21 -0
  60. package/templates/vanilla/node_modules/picocolors/package.json +25 -0
  61. package/templates/vanilla/node_modules/picocolors/picocolors.browser.js +4 -0
  62. package/templates/vanilla/node_modules/picocolors/picocolors.d.ts +5 -0
  63. package/templates/vanilla/node_modules/picocolors/picocolors.js +75 -0
  64. package/templates/vanilla/node_modules/picocolors/types.d.ts +51 -0
  65. package/templates/vanilla/node_modules/postcss/LICENSE +20 -0
  66. package/templates/vanilla/node_modules/postcss/README.md +29 -0
  67. package/templates/vanilla/node_modules/postcss/lib/at-rule.d.ts +140 -0
  68. package/templates/vanilla/node_modules/postcss/lib/at-rule.js +25 -0
  69. package/templates/vanilla/node_modules/postcss/lib/comment.d.ts +68 -0
  70. package/templates/vanilla/node_modules/postcss/lib/comment.js +13 -0
  71. package/templates/vanilla/node_modules/postcss/lib/container.d.ts +483 -0
  72. package/templates/vanilla/node_modules/postcss/lib/container.js +447 -0
  73. package/templates/vanilla/node_modules/postcss/lib/css-syntax-error.d.ts +248 -0
  74. package/templates/vanilla/node_modules/postcss/lib/css-syntax-error.js +133 -0
  75. package/templates/vanilla/node_modules/postcss/lib/declaration.d.ts +151 -0
  76. package/templates/vanilla/node_modules/postcss/lib/declaration.js +24 -0
  77. package/templates/vanilla/node_modules/postcss/lib/document.d.ts +69 -0
  78. package/templates/vanilla/node_modules/postcss/lib/document.js +33 -0
  79. package/templates/vanilla/node_modules/postcss/lib/fromJSON.d.ts +9 -0
  80. package/templates/vanilla/node_modules/postcss/lib/fromJSON.js +54 -0
  81. package/templates/vanilla/node_modules/postcss/lib/input.d.ts +227 -0
  82. package/templates/vanilla/node_modules/postcss/lib/input.js +265 -0
  83. package/templates/vanilla/node_modules/postcss/lib/lazy-result.d.ts +190 -0
  84. package/templates/vanilla/node_modules/postcss/lib/lazy-result.js +550 -0
  85. package/templates/vanilla/node_modules/postcss/lib/list.d.ts +60 -0
  86. package/templates/vanilla/node_modules/postcss/lib/list.js +58 -0
  87. package/templates/vanilla/node_modules/postcss/lib/map-generator.js +368 -0
  88. package/templates/vanilla/node_modules/postcss/lib/no-work-result.d.ts +46 -0
  89. package/templates/vanilla/node_modules/postcss/lib/no-work-result.js +138 -0
  90. package/templates/vanilla/node_modules/postcss/lib/node.d.ts +556 -0
  91. package/templates/vanilla/node_modules/postcss/lib/node.js +449 -0
  92. package/templates/vanilla/node_modules/postcss/lib/parse.d.ts +9 -0
  93. package/templates/vanilla/node_modules/postcss/lib/parse.js +42 -0
  94. package/templates/vanilla/node_modules/postcss/lib/parser.js +611 -0
  95. package/templates/vanilla/node_modules/postcss/lib/postcss.d.mts +69 -0
  96. package/templates/vanilla/node_modules/postcss/lib/postcss.d.ts +458 -0
  97. package/templates/vanilla/node_modules/postcss/lib/postcss.js +101 -0
  98. package/templates/vanilla/node_modules/postcss/lib/postcss.mjs +30 -0
  99. package/templates/vanilla/node_modules/postcss/lib/previous-map.d.ts +81 -0
  100. package/templates/vanilla/node_modules/postcss/lib/previous-map.js +144 -0
  101. package/templates/vanilla/node_modules/postcss/lib/processor.d.ts +115 -0
  102. package/templates/vanilla/node_modules/postcss/lib/processor.js +67 -0
  103. package/templates/vanilla/node_modules/postcss/lib/result.d.ts +205 -0
  104. package/templates/vanilla/node_modules/postcss/lib/result.js +42 -0
  105. package/templates/vanilla/node_modules/postcss/lib/root.d.ts +87 -0
  106. package/templates/vanilla/node_modules/postcss/lib/root.js +61 -0
  107. package/templates/vanilla/node_modules/postcss/lib/rule.d.ts +126 -0
  108. package/templates/vanilla/node_modules/postcss/lib/rule.js +27 -0
  109. package/templates/vanilla/node_modules/postcss/lib/stringifier.d.ts +46 -0
  110. package/templates/vanilla/node_modules/postcss/lib/stringifier.js +353 -0
  111. package/templates/vanilla/node_modules/postcss/lib/stringify.d.ts +9 -0
  112. package/templates/vanilla/node_modules/postcss/lib/stringify.js +11 -0
  113. package/templates/vanilla/node_modules/postcss/lib/symbols.js +5 -0
  114. package/templates/vanilla/node_modules/postcss/lib/terminal-highlight.js +70 -0
  115. package/templates/vanilla/node_modules/postcss/lib/tokenize.js +266 -0
  116. package/templates/vanilla/node_modules/postcss/lib/warn-once.js +13 -0
  117. package/templates/vanilla/node_modules/postcss/lib/warning.d.ts +147 -0
  118. package/templates/vanilla/node_modules/postcss/lib/warning.js +37 -0
  119. package/templates/vanilla/node_modules/postcss/package.json +88 -0
  120. package/templates/vanilla/node_modules/rollup/LICENSE.md +695 -0
  121. package/templates/vanilla/node_modules/rollup/README.md +125 -0
  122. package/templates/vanilla/node_modules/rollup/dist/bin/rollup +1715 -0
  123. package/templates/vanilla/node_modules/rollup/dist/es/getLogFilter.js +64 -0
  124. package/templates/vanilla/node_modules/rollup/dist/es/package.json +1 -0
  125. package/templates/vanilla/node_modules/rollup/dist/es/rollup.js +17 -0
  126. package/templates/vanilla/node_modules/rollup/dist/es/shared/node-entry.js +27273 -0
  127. package/templates/vanilla/node_modules/rollup/dist/es/shared/watch.js +4857 -0
  128. package/templates/vanilla/node_modules/rollup/dist/getLogFilter.d.ts +5 -0
  129. package/templates/vanilla/node_modules/rollup/dist/getLogFilter.js +69 -0
  130. package/templates/vanilla/node_modules/rollup/dist/loadConfigFile.d.ts +20 -0
  131. package/templates/vanilla/node_modules/rollup/dist/loadConfigFile.js +29 -0
  132. package/templates/vanilla/node_modules/rollup/dist/rollup.d.ts +1012 -0
  133. package/templates/vanilla/node_modules/rollup/dist/rollup.js +31 -0
  134. package/templates/vanilla/node_modules/rollup/dist/shared/fsevents-importer.js +37 -0
  135. package/templates/vanilla/node_modules/rollup/dist/shared/index.js +4571 -0
  136. package/templates/vanilla/node_modules/rollup/dist/shared/loadConfigFile.js +546 -0
  137. package/templates/vanilla/node_modules/rollup/dist/shared/rollup.js +27351 -0
  138. package/templates/vanilla/node_modules/rollup/dist/shared/watch-cli.js +561 -0
  139. package/templates/vanilla/node_modules/rollup/dist/shared/watch-proxy.js +87 -0
  140. package/templates/vanilla/node_modules/rollup/dist/shared/watch.js +316 -0
  141. package/templates/vanilla/node_modules/rollup/package.json +181 -0
  142. package/templates/vanilla/node_modules/source-map-js/LICENSE +28 -0
  143. package/templates/vanilla/node_modules/source-map-js/README.md +765 -0
  144. package/templates/vanilla/node_modules/source-map-js/lib/array-set.js +121 -0
  145. package/templates/vanilla/node_modules/source-map-js/lib/base64-vlq.js +140 -0
  146. package/templates/vanilla/node_modules/source-map-js/lib/base64.js +67 -0
  147. package/templates/vanilla/node_modules/source-map-js/lib/binary-search.js +111 -0
  148. package/templates/vanilla/node_modules/source-map-js/lib/mapping-list.js +79 -0
  149. package/templates/vanilla/node_modules/source-map-js/lib/quick-sort.js +132 -0
  150. package/templates/vanilla/node_modules/source-map-js/lib/source-map-consumer.d.ts +1 -0
  151. package/templates/vanilla/node_modules/source-map-js/lib/source-map-consumer.js +1188 -0
  152. package/templates/vanilla/node_modules/source-map-js/lib/source-map-generator.d.ts +1 -0
  153. package/templates/vanilla/node_modules/source-map-js/lib/source-map-generator.js +444 -0
  154. package/templates/vanilla/node_modules/source-map-js/lib/source-node.d.ts +1 -0
  155. package/templates/vanilla/node_modules/source-map-js/lib/source-node.js +413 -0
  156. package/templates/vanilla/node_modules/source-map-js/lib/util.js +594 -0
  157. package/templates/vanilla/node_modules/source-map-js/package.json +71 -0
  158. package/templates/vanilla/node_modules/source-map-js/source-map.d.ts +104 -0
  159. package/templates/vanilla/node_modules/source-map-js/source-map.js +8 -0
  160. package/templates/vanilla/node_modules/vite/LICENSE.md +3396 -0
  161. package/templates/vanilla/node_modules/vite/README.md +20 -0
  162. package/templates/vanilla/node_modules/vite/bin/openChrome.applescript +95 -0
  163. package/templates/vanilla/node_modules/vite/bin/vite.js +61 -0
  164. package/templates/vanilla/node_modules/vite/client.d.ts +281 -0
  165. package/templates/vanilla/node_modules/vite/dist/client/client.mjs +725 -0
  166. package/templates/vanilla/node_modules/vite/dist/client/client.mjs.map +1 -0
  167. package/templates/vanilla/node_modules/vite/dist/client/env.mjs +30 -0
  168. package/templates/vanilla/node_modules/vite/dist/client/env.mjs.map +1 -0
  169. package/templates/vanilla/node_modules/vite/dist/node/chunks/dep-7ec6f216.js +914 -0
  170. package/templates/vanilla/node_modules/vite/dist/node/chunks/dep-827b23df.js +66713 -0
  171. package/templates/vanilla/node_modules/vite/dist/node/chunks/dep-c423598f.js +561 -0
  172. package/templates/vanilla/node_modules/vite/dist/node/chunks/dep-f0c7dae0.js +7930 -0
  173. package/templates/vanilla/node_modules/vite/dist/node/chunks/dep-f1e8587f.js +7646 -0
  174. package/templates/vanilla/node_modules/vite/dist/node/cli.js +929 -0
  175. package/templates/vanilla/node_modules/vite/dist/node/constants.js +130 -0
  176. package/templates/vanilla/node_modules/vite/dist/node/index.d.ts +3548 -0
  177. package/templates/vanilla/node_modules/vite/dist/node/index.js +158 -0
  178. package/templates/vanilla/node_modules/vite/dist/node-cjs/publicUtils.cjs +4555 -0
  179. package/templates/vanilla/node_modules/vite/index.cjs +34 -0
  180. package/templates/vanilla/node_modules/vite/package.json +173 -0
  181. package/templates/vanilla/node_modules/vite/types/customEvent.d.ts +35 -0
  182. package/templates/vanilla/node_modules/vite/types/hmrPayload.d.ts +61 -0
  183. package/templates/vanilla/node_modules/vite/types/hot.d.ts +32 -0
  184. package/templates/vanilla/node_modules/vite/types/importGlob.d.ts +97 -0
  185. package/templates/vanilla/node_modules/vite/types/importMeta.d.ts +26 -0
  186. package/templates/vanilla/node_modules/vite/types/metadata.d.ts +10 -0
  187. package/templates/vanilla/node_modules/vite/types/package.json +4 -0
  188. package/templates/vanilla/package-lock.json +589 -0
  189. package/templates/vanilla/package.json +17 -0
  190. package/templates/vanilla/src/components/App.js +60 -0
  191. package/templates/vanilla/src/components/Card.js +21 -0
  192. package/templates/vanilla/src/components/Hero.js +15 -0
  193. package/templates/vanilla/src/components/InteractiveDemo.js +59 -0
  194. package/templates/vanilla/src/main.js +9 -0
  195. package/templates/vanilla/src/style/main.css +172 -0
  196. package/templates/vanilla/vite.config.js +8 -0
  197. package/templates/web3/.env.example +15 -0
  198. package/templates/web3/.github/workflows/deploy.yml +38 -0
  199. package/templates/web3/README.md +33 -0
  200. package/templates/web3/contracts/foundry.toml +11 -0
  201. package/templates/web3/contracts/script/Deploy.s.sol +13 -0
  202. package/templates/web3/contracts/src/Counter.sol +21 -0
  203. package/templates/web3/index.html +13 -0
  204. package/templates/web3/package.json +25 -0
  205. package/templates/web3/scripts/chain-start.mjs +305 -0
  206. package/templates/web3/scripts/chain-stop.mjs +34 -0
  207. package/templates/web3/scripts/deploy.mjs +155 -0
  208. package/templates/web3/scripts/setup.mjs +42 -0
  209. package/templates/web3/src/components/App.js +49 -0
  210. package/templates/web3/src/components/CounterCard.js +111 -0
  211. package/templates/web3/src/main.js +54 -0
  212. package/templates/web3/src/style/main.css +345 -0
  213. package/templates/web3/vite.config.js +29 -0
@@ -0,0 +1,1749 @@
1
+ 'use strict';
2
+
3
+ class EventBus {
4
+ constructor() {
5
+ this.listeners = new Map();
6
+ this.debugMode = false;
7
+ }
8
+
9
+ /**
10
+ * Enable or disable debug logging
11
+ * @param {boolean} enabled
12
+ */
13
+ setDebugMode(enabled) {
14
+ this.debugMode = enabled;
15
+ }
16
+
17
+ /**
18
+ * Subscribe to an event
19
+ * @param {string} eventName
20
+ * @param {Function} callback
21
+ * @returns {Function} Unsubscribe function
22
+ */
23
+ on(eventName, callback) {
24
+ if (!this.listeners.has(eventName)) {
25
+ this.listeners.set(eventName, new Set());
26
+ }
27
+
28
+ this.listeners.get(eventName).add(callback);
29
+
30
+ if (this.debugMode) {
31
+ console.log(`[EventBus] Listener added for "${eventName}"`);
32
+ }
33
+
34
+ // Return unsubscribe function
35
+ return () => this.off(eventName, callback);
36
+ }
37
+
38
+ /**
39
+ * Remove a specific event listener
40
+ * @param {string} eventName
41
+ * @param {Function} callback
42
+ */
43
+ off(eventName, callback) {
44
+ if (!this.listeners.has(eventName)) return;
45
+
46
+ this.listeners.get(eventName).delete(callback);
47
+
48
+ if (this.debugMode) {
49
+ console.log(`[EventBus] Listener removed for "${eventName}"`);
50
+ }
51
+
52
+ // Cleanup empty event sets
53
+ if (this.listeners.get(eventName).size === 0) {
54
+ this.listeners.delete(eventName);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Remove all listeners for an event
60
+ * @param {string} eventName
61
+ */
62
+ removeAllListeners(eventName) {
63
+ if (eventName) {
64
+ this.listeners.delete(eventName);
65
+ if (this.debugMode) {
66
+ console.log(`[EventBus] All listeners removed for "${eventName}"`);
67
+ }
68
+ } else {
69
+ this.listeners.clear();
70
+ if (this.debugMode) {
71
+ console.log('[EventBus] All listeners removed');
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Emit an event with data
78
+ * @param {string} eventName
79
+ * @param {any} data
80
+ */
81
+ emit(eventName, data) {
82
+ if (!this.listeners.has(eventName)) {
83
+ return;
84
+ }
85
+
86
+ if (this.debugMode) {
87
+ console.log(`[EventBus] Emitting "${eventName}"`, data);
88
+ }
89
+
90
+ this.listeners.get(eventName).forEach(callback => {
91
+ try {
92
+ callback(data);
93
+ } catch (error) {
94
+ console.error(`[EventBus] Error in listener for "${eventName}":`, error);
95
+ }
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Subscribe to an event for one-time use
101
+ * @param {string} eventName
102
+ * @param {Function} callback
103
+ * @returns {Function} Unsubscribe function
104
+ */
105
+ once(eventName, callback) {
106
+ // Create a wrapper that will call the callback and then unsubscribe
107
+ const wrappedCallback = (data) => {
108
+ // Unsubscribe first to prevent issues if the callback triggers the same event
109
+ this.off(eventName, wrappedCallback);
110
+ // Call the original callback
111
+ try {
112
+ callback(data);
113
+ } catch (error) {
114
+ console.error(`[EventBus] Error in once callback for "${eventName}":`, error);
115
+ }
116
+ };
117
+
118
+ // Register the wrapped callback
119
+ const unsubscribe = this.on(eventName, wrappedCallback);
120
+ return unsubscribe;
121
+ }
122
+ }
123
+
124
+ // Create a single instance for the application
125
+ const eventBus = new EventBus();
126
+
127
+ /**
128
+ * DOMUpdater - Utility for granular DOM updates
129
+ * Preserves focus state, scroll position, and form input values during updates
130
+ */
131
+ class DOMUpdater {
132
+ constructor() {
133
+ this._activeElement = null;
134
+ this._activeElementPath = null;
135
+ this._scrollPositions = new Map();
136
+ }
137
+
138
+ /**
139
+ * Save the currently focused element and scroll positions
140
+ * @param {HTMLElement} rootElement - Root element to save state from
141
+ */
142
+ saveState(rootElement) {
143
+ // Save active element
144
+ const activeElement = document.activeElement;
145
+ if (activeElement && rootElement.contains(activeElement)) {
146
+ this._activeElement = activeElement;
147
+ this._activeElementPath = this._getElementPath(activeElement, rootElement);
148
+ } else {
149
+ this._activeElement = null;
150
+ this._activeElementPath = null;
151
+ }
152
+
153
+ // Save scroll positions for all scrollable containers
154
+ this._scrollPositions.clear();
155
+ const scrollableElements = rootElement.querySelectorAll('[data-scroll-container], [style*="overflow"]');
156
+ scrollableElements.forEach(el => {
157
+ if (el.scrollTop !== undefined || el.scrollLeft !== undefined) {
158
+ const path = this._getElementPath(el, rootElement);
159
+ this._scrollPositions.set(path, {
160
+ scrollTop: el.scrollTop,
161
+ scrollLeft: el.scrollLeft
162
+ });
163
+ }
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Restore focus and scroll positions after DOM update
169
+ * @param {HTMLElement} rootElement - Root element to restore state in
170
+ */
171
+ restoreState(rootElement) {
172
+ // Restore focus
173
+ if (this._activeElementPath) {
174
+ const element = this._getElementByPath(rootElement, this._activeElementPath);
175
+ if (element && element.focus) {
176
+ try {
177
+ // Restore cursor position if it's an input/textarea
178
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
179
+ const selectionStart = element.selectionStart || 0;
180
+ element.focus();
181
+ if (element.setSelectionRange) {
182
+ element.setSelectionRange(selectionStart, selectionStart);
183
+ }
184
+ } else {
185
+ element.focus();
186
+ }
187
+ } catch (e) {
188
+ // Focus may fail if element is not focusable, ignore
189
+ }
190
+ }
191
+ }
192
+
193
+ // Restore scroll positions
194
+ this._scrollPositions.forEach((position, path) => {
195
+ const element = this._getElementByPath(rootElement, path);
196
+ if (element) {
197
+ element.scrollTop = position.scrollTop;
198
+ element.scrollLeft = position.scrollLeft;
199
+ }
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Get a path string to identify an element within its root
205
+ * @private
206
+ */
207
+ _getElementPath(element, root) {
208
+ const path = [];
209
+ let current = element;
210
+
211
+ while (current && current !== root && current.parentNode) {
212
+ const parent = current.parentNode;
213
+ const index = Array.from(parent.children).indexOf(current);
214
+ path.unshift(index);
215
+ current = parent;
216
+ }
217
+
218
+ return path.join('/');
219
+ }
220
+
221
+ /**
222
+ * Get an element by its path within root
223
+ * @private
224
+ */
225
+ _getElementByPath(root, path) {
226
+ const indices = path.split('/').map(Number);
227
+ let current = root;
228
+
229
+ for (const index of indices) {
230
+ if (!current.children || !current.children[index]) {
231
+ return null;
232
+ }
233
+ current = current.children[index];
234
+ }
235
+
236
+ return current;
237
+ }
238
+
239
+ /**
240
+ * Compare two HTML strings and determine if they're structurally different
241
+ * @param {string} oldHTML - Previous HTML
242
+ * @param {string} newHTML - New HTML
243
+ * @returns {boolean} - True if structure is significantly different
244
+ */
245
+ diffHTML(oldHTML, newHTML) {
246
+ if (oldHTML === newHTML) return false;
247
+
248
+ // Simple heuristic: if length difference is > 50%, likely structural change
249
+ const lengthDiff = Math.abs(oldHTML.length - newHTML.length);
250
+ const avgLength = (oldHTML.length + newHTML.length) / 2;
251
+ if (lengthDiff / avgLength > 0.5) {
252
+ return true;
253
+ }
254
+
255
+ // Check for tag changes (simplified)
256
+ const oldTags = oldHTML.match(/<[^>]+>/g) || [];
257
+ const newTags = newHTML.match(/<[^>]+>/g) || [];
258
+ if (oldTags.length !== newTags.length) {
259
+ return true;
260
+ }
261
+
262
+ return false;
263
+ }
264
+
265
+ /**
266
+ * Update text content of a node
267
+ * @param {Node} node - Node to update
268
+ * @param {string} newText - New text content
269
+ */
270
+ updateText(node, newText) {
271
+ if (node.nodeType === Node.TEXT_NODE) {
272
+ node.textContent = newText;
273
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
274
+ // For elements, update only if it's a text-only element
275
+ if (node.children.length === 0) {
276
+ node.textContent = newText;
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Update attributes of an element
283
+ * @param {HTMLElement} element - Element to update
284
+ * @param {Object} newAttrs - New attributes object
285
+ */
286
+ updateAttributes(element, newAttrs) {
287
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
288
+
289
+ // Get current attributes
290
+ const currentAttrs = {};
291
+ Array.from(element.attributes).forEach(attr => {
292
+ currentAttrs[attr.name] = attr.value;
293
+ });
294
+
295
+ // Update changed attributes
296
+ Object.keys(newAttrs).forEach(name => {
297
+ const newValue = newAttrs[name];
298
+ if (currentAttrs[name] !== newValue) {
299
+ if (newValue === null || newValue === undefined) {
300
+ element.removeAttribute(name);
301
+ } else {
302
+ element.setAttribute(name, newValue);
303
+ }
304
+ }
305
+ });
306
+
307
+ // Remove attributes that are no longer present
308
+ Object.keys(currentAttrs).forEach(name => {
309
+ if (!(name in newAttrs)) {
310
+ element.removeAttribute(name);
311
+ }
312
+ });
313
+ }
314
+
315
+ /**
316
+ * Update children of a parent element using a simple reconciliation algorithm
317
+ * @param {HTMLElement} parent - Parent element
318
+ * @param {NodeList|Array} oldChildren - Current children
319
+ * @param {DocumentFragment|HTMLElement} newChildrenContainer - Container with new children
320
+ */
321
+ updateChildren(parent, oldChildren, newChildrenContainer) {
322
+ const oldArray = Array.from(oldChildren);
323
+ const newArray = Array.from(newChildrenContainer.children || []);
324
+
325
+ // If structure is too different, replace entirely
326
+ if (Math.abs(oldArray.length - newArray.length) > oldArray.length * 0.3) {
327
+ return false; // Signal to fall back to full replacement
328
+ }
329
+
330
+ // Simple reconciliation: update in place where possible
331
+ const maxLength = Math.max(oldArray.length, newArray.length);
332
+
333
+ for (let i = 0; i < maxLength; i++) {
334
+ const oldChild = oldArray[i];
335
+ const newChild = newArray[i];
336
+
337
+ if (!oldChild && newChild) {
338
+ // New child - append
339
+ parent.appendChild(newChild);
340
+ } else if (oldChild && !newChild) {
341
+ // Old child removed
342
+ oldChild.remove();
343
+ } else if (oldChild && newChild) {
344
+ // Both exist - try to update in place
345
+ if (this._canUpdateInPlace(oldChild, newChild)) {
346
+ this._updateNodeInPlace(oldChild, newChild);
347
+ } else {
348
+ // Replace
349
+ parent.replaceChild(newChild, oldChild);
350
+ }
351
+ }
352
+ }
353
+
354
+ return true; // Successfully updated
355
+ }
356
+
357
+ /**
358
+ * Check if two nodes can be updated in place
359
+ * @private
360
+ */
361
+ _canUpdateInPlace(oldNode, newNode) {
362
+ if (oldNode.nodeType !== newNode.nodeType) return false;
363
+ if (oldNode.nodeType === Node.TEXT_NODE) return true;
364
+ if (oldNode.nodeType === Node.ELEMENT_NODE) {
365
+ return oldNode.tagName === newNode.tagName;
366
+ }
367
+ return false;
368
+ }
369
+
370
+ /**
371
+ * Update a node in place with new content
372
+ * @private
373
+ */
374
+ _updateNodeInPlace(oldNode, newNode) {
375
+ if (oldNode.nodeType === Node.TEXT_NODE) {
376
+ oldNode.textContent = newNode.textContent;
377
+ return;
378
+ }
379
+
380
+ if (oldNode.nodeType === Node.ELEMENT_NODE) {
381
+ // Update attributes
382
+ const newAttrs = {};
383
+ Array.from(newNode.attributes).forEach(attr => {
384
+ newAttrs[attr.name] = attr.value;
385
+ });
386
+ this.updateAttributes(oldNode, newAttrs);
387
+
388
+ // Update children recursively
389
+ const oldChildren = oldNode.childNodes;
390
+ const newChildren = newNode.childNodes;
391
+
392
+ // For simple text-only updates
393
+ if (oldChildren.length === 1 && newChildren.length === 1 &&
394
+ oldChildren[0].nodeType === Node.TEXT_NODE &&
395
+ newChildren[0].nodeType === Node.TEXT_NODE) {
396
+ oldChildren[0].textContent = newChildren[0].textContent;
397
+ return;
398
+ }
399
+
400
+ // For more complex structures, try to reconcile
401
+ if (!this.updateChildren(oldNode, oldChildren, newNode)) {
402
+ // Fall back to replacing content
403
+ oldNode.innerHTML = newNode.innerHTML;
404
+ }
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Perform a granular update of an element's content
410
+ * @param {HTMLElement} element - Element to update
411
+ * @param {string} newHTML - New HTML content
412
+ * @returns {boolean} - True if granular update succeeded, false if fallback needed
413
+ */
414
+ updateGranular(element, newHTML) {
415
+ // Save state before update
416
+ this.saveState(element);
417
+
418
+ // Create temporary container for new HTML
419
+ const temp = document.createElement('div');
420
+ temp.innerHTML = newHTML;
421
+
422
+ // Try to update children granularly
423
+ const oldHTML = element.innerHTML;
424
+ const isStructuralChange = this.diffHTML(oldHTML, newHTML);
425
+
426
+ if (isStructuralChange) {
427
+ // Structural change detected - fall back to full replacement
428
+ return false;
429
+ }
430
+
431
+ // Try granular update
432
+ try {
433
+ const success = this.updateChildren(element, element.childNodes, temp);
434
+ if (success) {
435
+ // Restore state after successful update
436
+ this.restoreState(element);
437
+ return true;
438
+ }
439
+ } catch (e) {
440
+ console.warn('[DOMUpdater] Granular update failed, falling back:', e);
441
+ }
442
+
443
+ return false;
444
+ }
445
+ }
446
+
447
+ /**
448
+ * UpdateScheduler - Coordinates component updates using requestAnimationFrame batching
449
+ *
450
+ * Prevents UI blocking by batching multiple component updates into a single frame.
451
+ * Components can opt-in to batching for non-critical updates, while critical updates
452
+ * (user input, errors) can execute immediately.
453
+ *
454
+ * Usage:
455
+ * // In Component.js
456
+ * this.scheduleUpdate(); // Batched (default)
457
+ * this.scheduleUpdate({ immediate: true }); // Immediate
458
+ */
459
+ class UpdateScheduler {
460
+ constructor() {
461
+ // Queue of components waiting to be updated
462
+ this._queue = new Set();
463
+
464
+ // Flag to track if requestAnimationFrame is already scheduled
465
+ this._scheduled = false;
466
+
467
+ // Performance metrics (only tracked in dev mode)
468
+ this._metrics = {
469
+ frames: 0,
470
+ updates: 0,
471
+ maxFrameTime: 0,
472
+ totalFrameTime: 0,
473
+ maxUpdatesPerFrame: 0
474
+ };
475
+
476
+ // Dev mode flag (can be enabled for performance monitoring)
477
+ this._devMode = false;
478
+
479
+ // Track if scheduler is enabled (can be disabled for debugging)
480
+ this._enabled = true;
481
+ }
482
+
483
+ /**
484
+ * Get singleton instance
485
+ * @returns {UpdateScheduler}
486
+ */
487
+ static getInstance() {
488
+ if (!UpdateScheduler._instance) {
489
+ UpdateScheduler._instance = new UpdateScheduler();
490
+ }
491
+ return UpdateScheduler._instance;
492
+ }
493
+
494
+ /**
495
+ * Enable or disable dev mode (performance tracking)
496
+ * @param {boolean} enabled
497
+ */
498
+ setDevMode(enabled) {
499
+ this._devMode = enabled;
500
+ }
501
+
502
+ /**
503
+ * Enable or disable the scheduler (for debugging)
504
+ * @param {boolean} enabled
505
+ */
506
+ setEnabled(enabled) {
507
+ this._enabled = enabled;
508
+ if (!enabled && this._scheduled) {
509
+ // Cancel pending frame if disabling
510
+ this._queue.clear();
511
+ this._scheduled = false;
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Queue a component for batched update
517
+ * @param {Component} component - Component instance to update
518
+ */
519
+ queue(component) {
520
+ if (!this._enabled) {
521
+ // If scheduler is disabled, execute immediately
522
+ try {
523
+ component.update();
524
+ } catch (error) {
525
+ console.error('[UpdateScheduler] Error updating component (immediate):', error);
526
+ }
527
+ return;
528
+ }
529
+
530
+ if (!component || typeof component.update !== 'function') {
531
+ console.warn('[UpdateScheduler] Invalid component queued:', component);
532
+ return;
533
+ }
534
+
535
+ // Add to queue (Set automatically deduplicates)
536
+ this._queue.add(component);
537
+
538
+ // Schedule flush if not already scheduled
539
+ if (!this._scheduled) {
540
+ this._scheduled = true;
541
+ requestAnimationFrame(() => this._flush());
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Execute all queued updates in a single frame
547
+ * @private
548
+ */
549
+ _flush() {
550
+ if (this._queue.size === 0) {
551
+ this._scheduled = false;
552
+ return;
553
+ }
554
+
555
+ const start = performance.now();
556
+ const updates = Array.from(this._queue);
557
+ const updateCount = updates.length;
558
+
559
+ // Clear queue and reset scheduled flag
560
+ this._queue.clear();
561
+ this._scheduled = false;
562
+
563
+ // Execute all updates
564
+ updates.forEach(component => {
565
+ try {
566
+ // Check if component is still mounted/valid before updating
567
+ if (component && component.element && typeof component.update === 'function') {
568
+ component.update();
569
+ }
570
+ } catch (error) {
571
+ // Log error but continue with other updates
572
+ console.error('[UpdateScheduler] Error updating component:', error);
573
+ console.error('[UpdateScheduler] Component:', component);
574
+ }
575
+ });
576
+
577
+ const duration = performance.now() - start;
578
+
579
+ // Track metrics in dev mode
580
+ if (this._devMode) {
581
+ this._metrics.frames++;
582
+ this._metrics.updates += updateCount;
583
+ this._metrics.totalFrameTime += duration;
584
+ this._metrics.maxFrameTime = Math.max(this._metrics.maxFrameTime, duration);
585
+ this._metrics.maxUpdatesPerFrame = Math.max(this._metrics.maxUpdatesPerFrame, updateCount);
586
+
587
+ // Warn if frame took too long (>16ms for 60fps)
588
+ if (duration > 16) {
589
+ console.warn(
590
+ `[UpdateScheduler] Frame took ${duration.toFixed(2)}ms (>16ms target). ` +
591
+ `Updated ${updateCount} component(s).`
592
+ );
593
+ }
594
+ }
595
+
596
+ // Handle nested updates (components that schedule updates during their update)
597
+ // If new updates were queued during this flush, schedule another frame
598
+ if (this._queue.size > 0) {
599
+ this._scheduled = true;
600
+ requestAnimationFrame(() => this._flush());
601
+ }
602
+ }
603
+
604
+ /**
605
+ * Get performance metrics
606
+ * @returns {Object} Metrics object
607
+ */
608
+ getMetrics() {
609
+ if (!this._devMode) {
610
+ return { enabled: false };
611
+ }
612
+
613
+ const avgFrameTime = this._metrics.frames > 0
614
+ ? this._metrics.totalFrameTime / this._metrics.frames
615
+ : 0;
616
+
617
+ const avgUpdatesPerFrame = this._metrics.frames > 0
618
+ ? this._metrics.updates / this._metrics.frames
619
+ : 0;
620
+
621
+ return {
622
+ enabled: true,
623
+ frames: this._metrics.frames,
624
+ totalUpdates: this._metrics.updates,
625
+ maxFrameTime: this._metrics.maxFrameTime,
626
+ avgFrameTime: avgFrameTime,
627
+ maxUpdatesPerFrame: this._metrics.maxUpdatesPerFrame,
628
+ avgUpdatesPerFrame: avgUpdatesPerFrame
629
+ };
630
+ }
631
+
632
+ /**
633
+ * Reset performance metrics
634
+ */
635
+ resetMetrics() {
636
+ this._metrics = {
637
+ frames: 0,
638
+ updates: 0,
639
+ maxFrameTime: 0,
640
+ totalFrameTime: 0,
641
+ maxUpdatesPerFrame: 0
642
+ };
643
+ }
644
+
645
+ /**
646
+ * Clear the update queue (useful for cleanup or testing)
647
+ */
648
+ clear() {
649
+ this._queue.clear();
650
+ this._scheduled = false;
651
+ }
652
+
653
+ /**
654
+ * Get current queue size (for debugging)
655
+ * @returns {number}
656
+ */
657
+ getQueueSize() {
658
+ return this._queue.size;
659
+ }
660
+ }
661
+
662
+ // Export singleton instance getter
663
+ const getUpdateScheduler = () => UpdateScheduler.getInstance();
664
+
665
+ class Component {
666
+ constructor() {
667
+ this.element = null;
668
+ this.state = {};
669
+ this.mounted = false;
670
+ this.boundEvents = new Map();
671
+ // Cleanup registry for tracking all cleanup functions
672
+ this._cleanupRegistry = new Set();
673
+ // Child components registry for automatic cleanup
674
+ this._children = new Map();
675
+ // Event subscriptions registry for automatic cleanup
676
+ this._subscriptions = new Set();
677
+ // DOM updater for granular updates
678
+ this._domUpdater = new DOMUpdater();
679
+ // Element reference cache
680
+ this._refs = new Map();
681
+ // Context storage
682
+ this._context = new Map();
683
+ // Parent component reference for context traversal
684
+ this._parent = null;
685
+ }
686
+
687
+ /**
688
+ * Initialize state with default values
689
+ * @param {Object} initialState
690
+ */
691
+ setState(newState) {
692
+ const oldState = {...this.state};
693
+ this.state = { ...this.state, ...newState };
694
+
695
+ // Only update if we should based on state changes
696
+ if (this.shouldUpdate(oldState, this.state)) {
697
+ this.update();
698
+ this.onStateUpdate(oldState, this.state);
699
+ }
700
+ }
701
+
702
+ /**
703
+ * Determines if the component should update based on state changes
704
+ * Override in child classes for custom comparison logic
705
+ * @param {Object} oldState - Previous state
706
+ * @param {Object} newState - New state
707
+ * @returns {boolean} - Whether component should update
708
+ */
709
+ shouldUpdate(oldState, newState) {
710
+ // Default shallow comparison of top-level state properties
711
+ // Check if any properties have changed
712
+ if (!oldState || !newState) return true;
713
+
714
+ // Check if object references are the same
715
+ if (oldState === newState) return false;
716
+
717
+ // Do a shallow comparison of properties
718
+ const oldKeys = Object.keys(oldState);
719
+ const newKeys = Object.keys(newState);
720
+
721
+ // If they have different number of keys, they changed
722
+ if (oldKeys.length !== newKeys.length) return true;
723
+
724
+ // Check if any key's value has changed
725
+ return oldKeys.some(key => oldState[key] !== newState[key]);
726
+ }
727
+
728
+ /**
729
+ * Lifecycle hook called after state is updated but before rendering
730
+ * Override in child classes to handle state updates
731
+ * @param {Object} oldState
732
+ * @param {Object} newState
733
+ */
734
+ onStateUpdate(oldState, newState) {
735
+ // Default implementation does nothing
736
+ }
737
+
738
+ /**
739
+ * Mount component to DOM
740
+ * @param {HTMLElement} container
741
+ */
742
+ mount(element) {
743
+ try {
744
+ this.element = element;
745
+ this.mounted = true;
746
+
747
+ // Apply styles if they exist
748
+ if (this.constructor.styles) {
749
+ const styleElement = document.createElement('style');
750
+ styleElement.textContent = this.constructor.styles;
751
+ document.head.appendChild(styleElement);
752
+ this.styleElement = styleElement;
753
+
754
+ // Register cleanup for style element
755
+ this.registerCleanup(() => {
756
+ if (this.styleElement && this.styleElement.parentNode) {
757
+ this.styleElement.remove();
758
+ }
759
+ });
760
+ }
761
+
762
+ this.update();
763
+ if (this.onMount) {
764
+ this.onMount();
765
+ }
766
+ } catch (error) {
767
+ this._handleError(error, { phase: 'mount' });
768
+ }
769
+ }
770
+
771
+ /**
772
+ * Remove component from DOM
773
+ */
774
+ unmount() {
775
+ if (!this.mounted) return;
776
+
777
+
778
+
779
+ // Execute all registered cleanup functions
780
+ this._executeCleanup();
781
+
782
+ // Unbind all events
783
+ this.unbindEvents();
784
+
785
+ // Clear all refs
786
+ this.invalidateRefs();
787
+
788
+ // Remove element if it exists
789
+ if (this.element) {
790
+ this.element.innerHTML = '';
791
+ }
792
+ this.element = null;
793
+
794
+ // Call lifecycle method
795
+ this.mounted = false;
796
+ if (this.onUnmount) {
797
+ this.onUnmount();
798
+ }
799
+ }
800
+
801
+ /**
802
+ * Bind DOM events based on this.events()
803
+ */
804
+ bindEvents() {
805
+ // First unbind any existing events
806
+ this.unbindEvents();
807
+
808
+ if (!this.element || typeof this.events !== 'function') {
809
+ return;
810
+ }
811
+
812
+ const events = this.events();
813
+ if (!events) return;
814
+
815
+ const entries = events instanceof Map ? Array.from(events.entries()) : Object.entries(events);
816
+
817
+ for (const [eventSelector, handlerReference] of entries) {
818
+ if (!eventSelector) continue;
819
+
820
+ const descriptor = eventSelector.trim();
821
+ if (!descriptor) continue;
822
+
823
+ const [eventName, ...selectorParts] = descriptor.split(/\s+/);
824
+ if (!eventName) {
825
+ console.warn(`[Component] Invalid event descriptor '${eventSelector}' in component '${this.constructor.name}'.`);
826
+ continue;
827
+ }
828
+ const selector = selectorParts.join(' ');
829
+
830
+ const handler = this._resolveEventHandler(handlerReference, eventSelector);
831
+ if (!handler) {
832
+ continue;
833
+ }
834
+
835
+ if (selector) {
836
+ // Delegated event
837
+ const eventHandler = (e) => {
838
+ const target = e?.target;
839
+ if (!target) return;
840
+ const matchedTarget = typeof target.closest === 'function'
841
+ ? target.closest(selector)
842
+ : (target.matches(selector) ? target : null);
843
+ if (matchedTarget && this.element.contains(matchedTarget)) {
844
+ handler(e);
845
+ }
846
+ };
847
+ this.element.addEventListener(eventName, eventHandler);
848
+ this.boundEvents.set(eventSelector, eventHandler);
849
+ } else {
850
+ // Direct event
851
+ this.element.addEventListener(eventName, handler);
852
+ this.boundEvents.set(eventSelector, handler);
853
+ }
854
+ }
855
+ }
856
+
857
+ /**
858
+ * Resolve event handler definitions passed via this.events()
859
+ * Supports both handler name strings and direct function references.
860
+ * @private
861
+ */
862
+ _resolveEventHandler(handlerReference, eventSelector) {
863
+ let handler = handlerReference;
864
+
865
+ if (typeof handlerReference === 'string') {
866
+ handler = this[handlerReference];
867
+ }
868
+
869
+ if (typeof handler !== 'function') {
870
+ console.warn(`[Component] Event handler '${handlerReference}' for event '${eventSelector}' is not a function on component '${this.constructor.name}'.`);
871
+ return null;
872
+ }
873
+
874
+ // Bind handler to component instance when possible
875
+ if (typeof handler.bind === 'function') {
876
+ return handler.bind(this);
877
+ }
878
+
879
+ return (...args) => handler.apply(this, args);
880
+ }
881
+
882
+ /**
883
+ * Unbind all DOM events
884
+ */
885
+ unbindEvents() {
886
+ if (!this.element) {
887
+ this.boundEvents.clear();
888
+ return;
889
+ }
890
+
891
+ for (const [eventSelector, handler] of this.boundEvents.entries()) {
892
+ const [eventName] = eventSelector.trim().split(/\s+/);
893
+ if (!eventName) continue;
894
+ this.element.removeEventListener(eventName, handler);
895
+ }
896
+ this.boundEvents.clear();
897
+ }
898
+
899
+ /**
900
+ * Update component after state change
901
+ */
902
+ update() {
903
+ if (!this.element) return;
904
+
905
+ try {
906
+ // Get new content
907
+ const newContent = this.render();
908
+
909
+ // Always update on first render or when content changes
910
+ // First render is detected by checking if innerHTML is empty
911
+ if (!this.element.innerHTML || this.element.innerHTML !== newContent) {
912
+ // Try granular update first (preserves focus/scroll)
913
+ const granularSuccess = this._domUpdater.updateGranular(this.element, newContent);
914
+
915
+ if (!granularSuccess) {
916
+ // Fall back to full replacement for complex structural changes
917
+ // Note: This will destroy child components, so they need to be re-mounted
918
+ this.element.innerHTML = newContent;
919
+ }
920
+
921
+ // Invalidate refs cache after DOM update
922
+ this.invalidateRefs();
923
+
924
+ // Mount child components
925
+ this._mountChildren();
926
+
927
+ // Bind events if events() method exists
928
+ if (this.events && typeof this.events === 'function') {
929
+ this.bindEvents();
930
+ }
931
+ }
932
+ } catch (error) {
933
+ this._handleError(error, { phase: 'update' });
934
+ }
935
+ }
936
+
937
+ /**
938
+ * Mount declared child components.
939
+ * This method is called after the parent component's DOM is updated.
940
+ * @private
941
+ */
942
+ _mountChildren() {
943
+ if (typeof this.children !== 'function') {
944
+ return;
945
+ }
946
+ const childrenToMount = this.children();
947
+ if (!childrenToMount) {
948
+ return;
949
+ }
950
+
951
+ for (const selector in childrenToMount) {
952
+ const container = this.getRef(selector);
953
+ if (container) {
954
+ const child = childrenToMount[selector];
955
+
956
+ // If a different component is already in this container, unmount it.
957
+ if (container.component && container.component !== child) {
958
+ container.component.unmount();
959
+ }
960
+
961
+ // If the child is not already mounted in this specific container, mount it.
962
+ if (child.element !== container) {
963
+ child.mount(container);
964
+ container.component = child; // Associate component with element for cleanup.
965
+ this.registerCleanup(() => child.unmount());
966
+ }
967
+ }
968
+ }
969
+ }
970
+
971
+ /**
972
+ * Declares child components for the component.
973
+ * @returns {Object.<string, Component>} A map where keys are CSS selectors
974
+ * for the container elements and values are the child component instances.
975
+ * e.g., { '#child-container': this.childComponent }
976
+ */
977
+ children() {
978
+ return {};
979
+ }
980
+
981
+ /**
982
+ * Schedule a component update with optional priority
983
+ *
984
+ * This method allows components to opt-in to requestAnimationFrame batching
985
+ * for better performance when multiple components update simultaneously.
986
+ *
987
+ * @param {Object} options - Update options
988
+ * @param {boolean} options.immediate - If true, update immediately (bypass batching).
989
+ * Use for critical updates (user input, errors).
990
+ * Default: false (batched)
991
+ *
992
+ * @example
993
+ * // Batched update (default) - good for price updates, balance updates
994
+ * this.scheduleUpdate();
995
+ *
996
+ * // Immediate update - good for user input, error displays
997
+ * this.scheduleUpdate({ immediate: true });
998
+ */
999
+ scheduleUpdate(options = {}) {
1000
+ if (options.immediate) {
1001
+ // Critical update - execute immediately
1002
+ this.update();
1003
+ } else {
1004
+ // Non-critical update - queue for batching
1005
+ const scheduler = getUpdateScheduler();
1006
+ scheduler.queue(this);
1007
+ }
1008
+ }
1009
+
1010
+ // Lifecycle methods (to be overridden by child classes)
1011
+ onMount() {}
1012
+ onUnmount() {}
1013
+ onUpdate(oldState) {}
1014
+
1015
+ /**
1016
+ * Error handler lifecycle hook
1017
+ * Override in child classes to handle errors
1018
+ * @param {Error} error - The error that occurred
1019
+ * @param {Object} errorInfo - Additional error information
1020
+ */
1021
+ onError(error, errorInfo) {
1022
+ // Default implementation does nothing
1023
+ // Child classes can override to handle errors
1024
+ }
1025
+
1026
+ // Methods to be implemented by child classes
1027
+ render() {
1028
+ try {
1029
+ return this.template ? this.template() : '';
1030
+ } catch (error) {
1031
+ this._handleError(error, { phase: 'render' });
1032
+ return '<div class="component-error">Error rendering component</div>';
1033
+ }
1034
+ }
1035
+
1036
+ /**
1037
+ * Handle errors and propagate to nearest ErrorBoundary
1038
+ * @private
1039
+ */
1040
+ _handleError(error, errorInfo = {}) {
1041
+ // Call component's error handler if it exists
1042
+ if (this.onError) {
1043
+ try {
1044
+ this.onError(error, errorInfo);
1045
+ } catch (handlerError) {
1046
+ console.error('[Component] Error in onError handler:', handlerError);
1047
+ }
1048
+ }
1049
+
1050
+ // Propagate to parent ErrorBoundary if it exists
1051
+ // Check by constructor name to avoid circular dependency
1052
+ let parent = this._parent;
1053
+ while (parent) {
1054
+ if (parent.constructor && parent.constructor.name === 'ErrorBoundary') {
1055
+ if (parent._errorHandler) {
1056
+ parent._errorHandler(error, { ...errorInfo, component: this.constructor.name });
1057
+ }
1058
+ return;
1059
+ }
1060
+ parent = parent._parent;
1061
+ }
1062
+
1063
+ // If no ErrorBoundary found, log to console
1064
+ console.error(`[Component] Unhandled error in ${this.constructor.name}:`, error, errorInfo);
1065
+ }
1066
+
1067
+ events() {
1068
+ return {};
1069
+ }
1070
+
1071
+ /**
1072
+ * Register a cleanup function to be called on unmount
1073
+ * @param {Function} cleanupFn - Function to call during cleanup
1074
+ * @returns {Function} - Unregister function to remove this cleanup
1075
+ */
1076
+ registerCleanup(cleanupFn) {
1077
+ if (typeof cleanupFn !== 'function') {
1078
+ console.warn('[Component] registerCleanup called with non-function:', cleanupFn);
1079
+ return () => {};
1080
+ }
1081
+
1082
+ this._cleanupRegistry.add(cleanupFn);
1083
+
1084
+ // Return unregister function
1085
+ return () => {
1086
+ this._cleanupRegistry.delete(cleanupFn);
1087
+ };
1088
+ }
1089
+
1090
+ /**
1091
+ * Execute all registered cleanup functions
1092
+ * @private
1093
+ */
1094
+ _executeCleanup() {
1095
+ this._cleanupRegistry.forEach(cleanupFn => {
1096
+ try {
1097
+ cleanupFn();
1098
+ } catch (error) {
1099
+ console.error('[Component] Error during cleanup:', error);
1100
+ }
1101
+ });
1102
+ this._cleanupRegistry.clear();
1103
+ }
1104
+
1105
+ /**
1106
+ * Unmount all child components
1107
+ * @private
1108
+ */
1109
+ _unmountChildren() {
1110
+ for (const [key, child] of this._children.entries()) {
1111
+ try {
1112
+ if (child && typeof child.unmount === 'function') {
1113
+ child.unmount();
1114
+ }
1115
+ } catch (error) {
1116
+ console.error(`[Component] Error unmounting child "${key}":`, error);
1117
+ }
1118
+ }
1119
+ this._children.clear();
1120
+ }
1121
+
1122
+ /**
1123
+ * Create and track a child component
1124
+ * @param {string} key - Unique key for this child component
1125
+ * @param {Component} childComponent - Child component instance
1126
+ * @returns {Component} - The child component
1127
+ */
1128
+ createChild(key, childComponent) {
1129
+ if (this._children.has(key)) {
1130
+ console.warn(`[Component] Child with key "${key}" already exists, unmounting previous instance`);
1131
+ const previous = this._children.get(key);
1132
+ if (previous && typeof previous.unmount === 'function') {
1133
+ previous.unmount();
1134
+ }
1135
+ }
1136
+
1137
+ // Set parent reference for context traversal
1138
+ childComponent._parent = this;
1139
+
1140
+ // Inherit parent context
1141
+ this._context.forEach((value, key) => {
1142
+ childComponent._context.set(key, value);
1143
+ });
1144
+
1145
+ this._children.set(key, childComponent);
1146
+
1147
+ // Register cleanup to unmount child
1148
+ this.registerCleanup(() => {
1149
+ if (childComponent && typeof childComponent.unmount === 'function') {
1150
+ childComponent.unmount();
1151
+ }
1152
+ this._children.delete(key);
1153
+ });
1154
+
1155
+ return childComponent;
1156
+ }
1157
+
1158
+ /**
1159
+ * Wrapper for setTimeout that automatically registers cleanup
1160
+ * @param {Function} callback - Function to call after delay
1161
+ * @param {number} delay - Delay in milliseconds
1162
+ * @returns {number} - Timer ID (can be used with clearTimeout)
1163
+ */
1164
+ setTimeout(callback, delay) {
1165
+ const timerId = window.setTimeout(callback, delay);
1166
+
1167
+ // Register cleanup to clear the timer
1168
+ this.registerCleanup(() => {
1169
+ window.clearTimeout(timerId);
1170
+ });
1171
+
1172
+ return timerId;
1173
+ }
1174
+
1175
+ /**
1176
+ * Wrapper for setInterval that automatically registers cleanup
1177
+ * @param {Function} callback - Function to call repeatedly
1178
+ * @param {number} delay - Interval in milliseconds
1179
+ * @returns {number} - Timer ID (can be used with clearInterval)
1180
+ */
1181
+ setInterval(callback, delay) {
1182
+ const timerId = window.setInterval(callback, delay);
1183
+
1184
+ // Register cleanup to clear the interval
1185
+ this.registerCleanup(() => {
1186
+ window.clearInterval(timerId);
1187
+ });
1188
+
1189
+ return timerId;
1190
+ }
1191
+
1192
+ /**
1193
+ * Subscribe to an event with automatic cleanup on unmount
1194
+ * @param {string} eventName - Name of the event to subscribe to
1195
+ * @param {Function} callback - Callback function to call when event is emitted
1196
+ * @returns {Function} - Unsubscribe function (also auto-called on unmount)
1197
+ */
1198
+ subscribe(eventName, callback) {
1199
+ // Subscribe to the event
1200
+ const unsubscribe = eventBus.on(eventName, callback);
1201
+
1202
+ // Track the subscription for automatic cleanup
1203
+ this._subscriptions.add(unsubscribe);
1204
+
1205
+ // Register cleanup to unsubscribe
1206
+ this.registerCleanup(() => {
1207
+ unsubscribe();
1208
+ this._subscriptions.delete(unsubscribe);
1209
+ });
1210
+
1211
+ return unsubscribe;
1212
+ }
1213
+
1214
+ /**
1215
+ * Subscribe to an event for one-time use with automatic cleanup
1216
+ * @param {string} eventName - Name of the event to subscribe to
1217
+ * @param {Function} callback - Callback function to call when event is emitted (once)
1218
+ * @returns {Function} - Unsubscribe function (also auto-called on unmount or after first call)
1219
+ */
1220
+ subscribeOnce(eventName, callback) {
1221
+ // Subscribe to the event once
1222
+ const unsubscribe = eventBus.once(eventName, callback);
1223
+
1224
+ // Track the subscription for automatic cleanup
1225
+ this._subscriptions.add(unsubscribe);
1226
+
1227
+ // Register cleanup
1228
+ this.registerCleanup(() => {
1229
+ unsubscribe();
1230
+ this._subscriptions.delete(unsubscribe);
1231
+ });
1232
+
1233
+ return unsubscribe;
1234
+ }
1235
+
1236
+ /**
1237
+ * Hook to subscribe to store state changes
1238
+ * Automatically updates component when selected store state changes
1239
+ *
1240
+ * STATE OWNERSHIP RULES:
1241
+ * - UI-only state (focus, hover, temporary UI state) → use this.state
1242
+ * - Shared/global state (balances, price, wallet, contract data) → use store
1243
+ * - Derived state → use selectors
1244
+ *
1245
+ * Usage examples:
1246
+ * // Using a selector method
1247
+ * const isPhase2 = this.useStore(tradingStore, () => tradingStore.selectIsPhase2());
1248
+ *
1249
+ * // Using a direct state selector
1250
+ * const price = this.useStore(tradingStore, (state) => state.price.current);
1251
+ *
1252
+ * // With update callback
1253
+ * this.useStore(tradingStore, () => tradingStore.selectIsPhase2(), (newValue, oldValue) => {
1254
+ * console.log('Phase 2 changed:', newValue);
1255
+ * });
1256
+ *
1257
+ * @param {Store} store - Store instance to subscribe to
1258
+ * @param {Function} selector - Function that selects state (can be store method or state selector)
1259
+ * @param {Function} onUpdate - Optional callback when selected state changes
1260
+ * @returns {any} - Current selected state value
1261
+ */
1262
+ useStore(store, selector, onUpdate) {
1263
+ if (!store || typeof selector !== 'function') {
1264
+ console.warn('[Component] useStore called with invalid arguments');
1265
+ return null;
1266
+ }
1267
+
1268
+ // Store the last value for comparison
1269
+ if (!this._storeValues) {
1270
+ this._storeValues = new Map();
1271
+ }
1272
+
1273
+ const selectorKey = selector.toString(); // Use function string as key (not perfect but works)
1274
+ let lastValue = this._storeValues.get(selectorKey);
1275
+
1276
+ // Get initial value
1277
+ const getCurrentValue = () => {
1278
+ // Try calling as store method first (e.g., tradingStore.selectIsPhase2())
1279
+ try {
1280
+ const result = selector.call(store);
1281
+ if (result !== undefined) {
1282
+ return result;
1283
+ }
1284
+ } catch (e) {
1285
+ // Not a method, try as state selector
1286
+ }
1287
+
1288
+ // Try as state selector (e.g., (state) => state.price)
1289
+ return selector(store.getState());
1290
+ };
1291
+
1292
+ const currentValue = getCurrentValue();
1293
+ this._storeValues.set(selectorKey, currentValue);
1294
+
1295
+ // Subscribe to store changes
1296
+ const unsubscribe = store.subscribe(() => {
1297
+ const newValue = getCurrentValue();
1298
+ const oldValue = lastValue;
1299
+
1300
+ // Only update if value actually changed (shallow comparison)
1301
+ if (this._hasValueChanged(oldValue, newValue)) {
1302
+ // Update stored value
1303
+ this._storeValues.set(selectorKey, newValue);
1304
+ lastValue = newValue;
1305
+
1306
+ // Call optional update callback
1307
+ if (typeof onUpdate === 'function') {
1308
+ onUpdate(newValue, oldValue);
1309
+ }
1310
+
1311
+ // Trigger component update
1312
+ this.update();
1313
+ }
1314
+ });
1315
+
1316
+ // Register cleanup to unsubscribe
1317
+ this.registerCleanup(() => {
1318
+ unsubscribe();
1319
+ if (this._storeValues) {
1320
+ this._storeValues.delete(selectorKey);
1321
+ }
1322
+ });
1323
+
1324
+ return currentValue;
1325
+ }
1326
+
1327
+ /**
1328
+ * Check if two values have changed (shallow comparison)
1329
+ * @private
1330
+ */
1331
+ _hasValueChanged(oldValue, newValue) {
1332
+ // Primitive comparison
1333
+ if (oldValue === newValue) return false;
1334
+
1335
+ // Null/undefined handling
1336
+ if (oldValue == null || newValue == null) return oldValue !== newValue;
1337
+
1338
+ // Object comparison (shallow)
1339
+ if (typeof oldValue === 'object' && typeof newValue === 'object') {
1340
+ const oldKeys = Object.keys(oldValue);
1341
+ const newKeys = Object.keys(newValue);
1342
+
1343
+ if (oldKeys.length !== newKeys.length) return true;
1344
+
1345
+ return oldKeys.some(key => oldValue[key] !== newValue[key]);
1346
+ }
1347
+
1348
+ return true;
1349
+ }
1350
+
1351
+ /**
1352
+ * Get a cached element reference by name and selector
1353
+ * @param {string} name - Reference name (for caching)
1354
+ * @param {string} selector - CSS selector to find element
1355
+ * @returns {HTMLElement|null} - Element or null if not found
1356
+ */
1357
+ _getRefCached(name, selector) {
1358
+ if (!this.element) return null;
1359
+
1360
+ // Check cache first
1361
+ if (this._refs.has(name)) {
1362
+ const cached = this._refs.get(name);
1363
+ // Verify element is still in DOM
1364
+ if (cached && this.element.contains(cached)) {
1365
+ return cached;
1366
+ }
1367
+ // Stale reference, remove from cache
1368
+ this._refs.delete(name);
1369
+ }
1370
+
1371
+ // Query DOM if not cached
1372
+ const element = this.element.querySelector(selector);
1373
+ if (element) {
1374
+ this._refs.set(name, element);
1375
+ }
1376
+
1377
+ return element;
1378
+ }
1379
+
1380
+ /**
1381
+ * Manually update a cached reference
1382
+ * @param {string} name - Reference name
1383
+ * @param {HTMLElement} element - Element to cache
1384
+ */
1385
+ updateRef(name, element) {
1386
+ if (element) {
1387
+ this._refs.set(name, element);
1388
+ } else {
1389
+ this._refs.delete(name);
1390
+ }
1391
+ }
1392
+
1393
+ /**
1394
+ * Invalidate all cached references
1395
+ * Should be called when DOM structure changes significantly
1396
+ */
1397
+ invalidateRefs() {
1398
+ this._refs.clear();
1399
+ }
1400
+
1401
+ /**
1402
+ * Invalidate a specific cached reference
1403
+ * @param {string} name - Reference name to invalidate
1404
+ */
1405
+ invalidateRef(name) {
1406
+ this._refs.delete(name);
1407
+ }
1408
+
1409
+ /**
1410
+ * Get a single DOM element within this component's element
1411
+ * Convenience wrapper around querySelector with null-safety check
1412
+ * @param {string} selector - CSS selector to query
1413
+ * @returns {HTMLElement|null} The first matching element or null
1414
+ */
1415
+ getRef(selector) {
1416
+ if (!this.element) {
1417
+ console.warn(`[Component] getRef called on ${this.constructor.name} before element exists`);
1418
+ return null;
1419
+ }
1420
+ return this.element.querySelector(selector);
1421
+ }
1422
+
1423
+ /**
1424
+ * Get multiple DOM elements within this component's element
1425
+ * Convenience wrapper around querySelectorAll with null-safety check
1426
+ * @param {string} selector - CSS selector to query
1427
+ * @returns {Array<HTMLElement>} Array of matching elements (empty array if none found)
1428
+ */
1429
+ getRefs(selector) {
1430
+ if (!this.element) {
1431
+ console.warn(`[Component] getRefs called on ${this.constructor.name} before element exists`);
1432
+ return [];
1433
+ }
1434
+ // Convert NodeList to Array for easier manipulation
1435
+ return Array.from(this.element.querySelectorAll(selector));
1436
+ }
1437
+
1438
+ /**
1439
+ * Provide a context value to child components
1440
+ * @param {string} key - Context key
1441
+ * @param {any} value - Context value
1442
+ */
1443
+ provideContext(key, value) {
1444
+ this._context.set(key, value);
1445
+
1446
+ // Propagate to existing children
1447
+ this._children.forEach(child => {
1448
+ if (child && typeof child._context !== 'undefined') {
1449
+ child._context.set(key, value);
1450
+ }
1451
+ });
1452
+ }
1453
+
1454
+ /**
1455
+ * Get a context value, searching up the component tree
1456
+ * @param {string} key - Context key
1457
+ * @returns {any} - Context value or undefined if not found
1458
+ */
1459
+ getContext(key) {
1460
+ // Check own context first
1461
+ if (this._context.has(key)) {
1462
+ return this._context.get(key);
1463
+ }
1464
+
1465
+ // Search up the tree
1466
+ let parent = this._parent;
1467
+ while (parent) {
1468
+ if (parent._context && parent._context.has(key)) {
1469
+ return parent._context.get(key);
1470
+ }
1471
+ parent = parent._parent;
1472
+ }
1473
+
1474
+ return undefined;
1475
+ }
1476
+
1477
+ /**
1478
+ * Remove a context value
1479
+ * @param {string} key - Context key to remove
1480
+ */
1481
+ removeContext(key) {
1482
+ this._context.delete(key);
1483
+
1484
+ // Remove from children
1485
+ this._children.forEach(child => {
1486
+ if (child && typeof child._context !== 'undefined') {
1487
+ child._context.delete(key);
1488
+ }
1489
+ });
1490
+ }
1491
+ }
1492
+
1493
+ /**
1494
+ * Simple client-side router for SPA navigation
1495
+ * Supports static routes and browser history
1496
+ */
1497
+ class Router {
1498
+ constructor() {
1499
+ this.routes = new Map();
1500
+ this.currentRoute = null;
1501
+ this.currentHandler = null;
1502
+ this.notFoundHandler = null;
1503
+
1504
+ // Bind methods
1505
+ this.handleRoute = this.handleRoute.bind(this);
1506
+ this.navigate = this.navigate.bind(this);
1507
+
1508
+ // Listen for browser back/forward
1509
+ window.addEventListener('popstate', async (e) => {
1510
+ await this.handleRoute(window.location.pathname);
1511
+ });
1512
+ }
1513
+
1514
+ /**
1515
+ * Register a route
1516
+ * @param {string} path - Route path (e.g., '/', '/cultexecs')
1517
+ * @param {Function} handler - Route handler function
1518
+ */
1519
+ on(path, handler) {
1520
+ this.routes.set(path, handler);
1521
+ }
1522
+
1523
+ /**
1524
+ * Register a 404 handler
1525
+ * @param {Function} handler - Handler for unmatched routes
1526
+ */
1527
+ notFound(handler) {
1528
+ this.notFoundHandler = handler;
1529
+ }
1530
+
1531
+ /**
1532
+ * Navigate to a route
1533
+ * @param {string} path - Route path
1534
+ * @param {boolean} replace - Whether to replace history entry
1535
+ */
1536
+ async navigate(path, replace = false) {
1537
+ if (path === window.location.pathname) {
1538
+ return; // Already on this route
1539
+ }
1540
+
1541
+ if (replace) {
1542
+ window.history.replaceState({ path }, '', path);
1543
+ } else {
1544
+ window.history.pushState({ path }, '', path);
1545
+ }
1546
+
1547
+ await this.handleRoute(path);
1548
+
1549
+
1550
+ }
1551
+
1552
+ /**
1553
+ * Match a path against a route pattern
1554
+ * @param {string} pattern - Route pattern (e.g., '/project/:id')
1555
+ * @param {string} path - Actual path to match
1556
+ * @returns {object|null} Matched params or null if no match
1557
+ * @private
1558
+ */
1559
+ _matchRoute(pattern, path) {
1560
+ // Exact match for static routes
1561
+ if (pattern === path) {
1562
+ return {};
1563
+ }
1564
+
1565
+ // Split into parts, filtering empty strings
1566
+ const patternParts = pattern.split('/').filter(p => p);
1567
+ const pathParts = path.split('/').filter(p => p);
1568
+
1569
+ // Must have same number of parts
1570
+ if (patternParts.length !== pathParts.length) {
1571
+ return null;
1572
+ }
1573
+
1574
+ const params = {};
1575
+ for (let i = 0; i < patternParts.length; i++) {
1576
+ const patternPart = patternParts[i];
1577
+ const pathPart = pathParts[i];
1578
+
1579
+ // Check if this is a parameter (starts with :)
1580
+ if (patternPart.startsWith(':')) {
1581
+ const paramName = patternPart.slice(1);
1582
+ // Decode URL component
1583
+ try {
1584
+ params[paramName] = decodeURIComponent(pathPart);
1585
+ } catch (e) {
1586
+ // If decoding fails, use raw value
1587
+ params[paramName] = pathPart;
1588
+ }
1589
+ } else if (patternPart !== pathPart) {
1590
+ // Static part doesn't match
1591
+ return null;
1592
+ }
1593
+ }
1594
+
1595
+ return params;
1596
+ }
1597
+
1598
+ /**
1599
+ * Find matching route handler
1600
+ * @param {string} path - Route path
1601
+ * @returns {object|null} { handler, params } or null
1602
+ * @private
1603
+ */
1604
+ _findRoute(path) {
1605
+ // First check for exact static route match (static routes take precedence)
1606
+ if (this.routes.has(path)) {
1607
+ return {
1608
+ handler: this.routes.get(path),
1609
+ params: {}
1610
+ };
1611
+ }
1612
+
1613
+ // Collect all dynamic routes and sort by specificity
1614
+ const dynamicRoutes = [];
1615
+ for (const [pattern, handler] of this.routes.entries()) {
1616
+ // Skip if it's an exact match (already checked above)
1617
+ if (pattern === path) {
1618
+ continue;
1619
+ }
1620
+
1621
+ // Check if pattern contains dynamic parameters
1622
+ if (pattern.includes(':')) {
1623
+ const paramCount = (pattern.match(/:/g) || []).length;
1624
+ // Count literal (non-param) parts for better specificity
1625
+ const literalCount = pattern.split('/').filter(p => p && !p.startsWith(':')).length;
1626
+ dynamicRoutes.push({ pattern, handler, paramCount, literalCount });
1627
+ }
1628
+ }
1629
+
1630
+ // Sort by literal count first (more literals = more specific), then by param count
1631
+ // This ensures routes with literal parts (like /create) are matched before fully dynamic routes
1632
+ dynamicRoutes.sort((a, b) => {
1633
+ if (b.literalCount !== a.literalCount) {
1634
+ return b.literalCount - a.literalCount;
1635
+ }
1636
+ return b.paramCount - a.paramCount;
1637
+ });
1638
+
1639
+ // Try each route in order of specificity
1640
+ for (const { pattern, handler } of dynamicRoutes) {
1641
+ const params = this._matchRoute(pattern, path);
1642
+ if (params !== null) {
1643
+ return { handler, params };
1644
+ }
1645
+ }
1646
+
1647
+ return null;
1648
+ }
1649
+
1650
+ /**
1651
+ * Handle route change
1652
+ * @param {string} path - Route path
1653
+ */
1654
+ async handleRoute(path) {
1655
+ // Clean up current handler if it exists
1656
+ if (this.currentHandler && typeof this.currentHandler.cleanup === 'function') {
1657
+ this.currentHandler.cleanup();
1658
+ }
1659
+
1660
+ // Find matching route
1661
+ const match = this._findRoute(path);
1662
+
1663
+ if (match) {
1664
+ this.currentRoute = path;
1665
+ // Call handler with params and store the result (which may include cleanup function)
1666
+ // Handle both sync and async handlers
1667
+ const result = await Promise.resolve(match.handler(match.params));
1668
+ this.currentHandler = result || null;
1669
+
1670
+
1671
+ } else if (this.notFoundHandler) {
1672
+ this.currentRoute = null;
1673
+ this.currentHandler = null;
1674
+ this.notFoundHandler(path);
1675
+ } else {
1676
+ console.warn(`No route handler for: ${path}`);
1677
+ }
1678
+ }
1679
+
1680
+ /**
1681
+ * Start the router
1682
+ */
1683
+ async start() {
1684
+ // Handle initial route
1685
+ await this.handleRoute(window.location.pathname);
1686
+ }
1687
+
1688
+ /**
1689
+ * Get current route
1690
+ */
1691
+ getCurrentRoute() {
1692
+ return this.currentRoute || window.location.pathname;
1693
+ }
1694
+
1695
+ /**
1696
+ * Encode a title for use in URL (slug)
1697
+ * @param {string} title - Title to encode
1698
+ * @returns {string} URL-safe slug
1699
+ */
1700
+ _encodeTitle(title) {
1701
+ if (!title) return '';
1702
+ return title
1703
+ .toLowerCase()
1704
+ .trim()
1705
+ .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
1706
+ .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
1707
+ }
1708
+
1709
+ /**
1710
+ * Decode a URL slug back to title (approximate)
1711
+ * @param {string} slug - URL slug
1712
+ * @returns {string} Decoded title
1713
+ */
1714
+ _decodeTitle(slug) {
1715
+ if (!slug) return '';
1716
+ return slug
1717
+ .split('-')
1718
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
1719
+ .join(' ');
1720
+ }
1721
+
1722
+ /**
1723
+ * Generate URL from chain ID, factory title, instance name, and optional piece title
1724
+ * @param {string|number} chainId - Chain ID (e.g., 1 for Ethereum mainnet)
1725
+ * @param {string} factoryTitle - Factory title
1726
+ * @param {string} instanceName - Instance name
1727
+ * @param {string} [pieceTitle] - Optional piece title (for ERC1155)
1728
+ * @returns {string} URL path
1729
+ */
1730
+ generateURL(chainId, factoryTitle, instanceName, pieceTitle = null) {
1731
+ const chainIdStr = String(chainId || '1'); // Default to 1 (Ethereum mainnet)
1732
+ const factorySlug = this._encodeTitle(factoryTitle);
1733
+ const instanceSlug = this._encodeTitle(instanceName);
1734
+
1735
+ if (pieceTitle) {
1736
+ const pieceSlug = this._encodeTitle(pieceTitle);
1737
+ return `/${chainIdStr}/${factorySlug}/${instanceSlug}/${pieceSlug}`;
1738
+ }
1739
+
1740
+ return `/${chainIdStr}/${factorySlug}/${instanceSlug}`;
1741
+ }
1742
+ }
1743
+
1744
+ exports.Component = Component;
1745
+ exports.DOMUpdater = DOMUpdater;
1746
+ exports.Router = Router;
1747
+ exports.eventBus = eventBus;
1748
+ exports.getUpdateScheduler = getUpdateScheduler;
1749
+ //# sourceMappingURL=microact.cjs.js.map