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