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,831 @@
1
+ import { eventBus } from './EventBus.js';
2
+ import { DOMUpdater } from './DOMUpdater.js';
3
+ import { getUpdateScheduler } from './UpdateScheduler.js';
4
+
5
+ export class Component {
6
+ constructor() {
7
+ this.element = null;
8
+ this.state = {};
9
+ this.mounted = false;
10
+ this.boundEvents = new Map();
11
+ // Cleanup registry for tracking all cleanup functions
12
+ this._cleanupRegistry = new Set();
13
+ // Child components registry for automatic cleanup
14
+ this._children = new Map();
15
+ // Event subscriptions registry for automatic cleanup
16
+ this._subscriptions = new Set();
17
+ // DOM updater for granular updates
18
+ this._domUpdater = new DOMUpdater();
19
+ // Element reference cache
20
+ this._refs = new Map();
21
+ // Context storage
22
+ this._context = new Map();
23
+ // Parent component reference for context traversal
24
+ this._parent = null;
25
+ }
26
+
27
+ /**
28
+ * Initialize state with default values
29
+ * @param {Object} initialState
30
+ */
31
+ setState(newState) {
32
+ const oldState = {...this.state};
33
+ this.state = { ...this.state, ...newState };
34
+
35
+ // Only update if we should based on state changes
36
+ if (this.shouldUpdate(oldState, this.state)) {
37
+ this.update();
38
+ this.onStateUpdate(oldState, this.state);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Determines if the component should update based on state changes
44
+ * Override in child classes for custom comparison logic
45
+ * @param {Object} oldState - Previous state
46
+ * @param {Object} newState - New state
47
+ * @returns {boolean} - Whether component should update
48
+ */
49
+ shouldUpdate(oldState, newState) {
50
+ // Default shallow comparison of top-level state properties
51
+ // Check if any properties have changed
52
+ if (!oldState || !newState) return true;
53
+
54
+ // Check if object references are the same
55
+ if (oldState === newState) return false;
56
+
57
+ // Do a shallow comparison of properties
58
+ const oldKeys = Object.keys(oldState);
59
+ const newKeys = Object.keys(newState);
60
+
61
+ // If they have different number of keys, they changed
62
+ if (oldKeys.length !== newKeys.length) return true;
63
+
64
+ // Check if any key's value has changed
65
+ return oldKeys.some(key => oldState[key] !== newState[key]);
66
+ }
67
+
68
+ /**
69
+ * Lifecycle hook called after state is updated but before rendering
70
+ * Override in child classes to handle state updates
71
+ * @param {Object} oldState
72
+ * @param {Object} newState
73
+ */
74
+ onStateUpdate(oldState, newState) {
75
+ // Default implementation does nothing
76
+ }
77
+
78
+ /**
79
+ * Mount component to DOM
80
+ * @param {HTMLElement} container
81
+ */
82
+ mount(element) {
83
+ try {
84
+ this.element = element;
85
+ this.mounted = true;
86
+
87
+ // Apply styles if they exist
88
+ if (this.constructor.styles) {
89
+ const styleElement = document.createElement('style');
90
+ styleElement.textContent = this.constructor.styles;
91
+ document.head.appendChild(styleElement);
92
+ this.styleElement = styleElement;
93
+
94
+ // Register cleanup for style element
95
+ this.registerCleanup(() => {
96
+ if (this.styleElement && this.styleElement.parentNode) {
97
+ this.styleElement.remove();
98
+ }
99
+ });
100
+ }
101
+
102
+ this.update();
103
+ if (this.onMount) {
104
+ this.onMount();
105
+ }
106
+ } catch (error) {
107
+ this._handleError(error, { phase: 'mount' });
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Remove component from DOM
113
+ */
114
+ unmount() {
115
+ if (!this.mounted) return;
116
+
117
+
118
+
119
+ // Execute all registered cleanup functions
120
+ this._executeCleanup();
121
+
122
+ // Unbind all events
123
+ this.unbindEvents();
124
+
125
+ // Clear all refs
126
+ this.invalidateRefs();
127
+
128
+ // Remove element if it exists
129
+ if (this.element) {
130
+ this.element.innerHTML = '';
131
+ }
132
+ this.element = null;
133
+
134
+ // Call lifecycle method
135
+ this.mounted = false;
136
+ if (this.onUnmount) {
137
+ this.onUnmount();
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Bind DOM events based on this.events()
143
+ */
144
+ bindEvents() {
145
+ // First unbind any existing events
146
+ this.unbindEvents();
147
+
148
+ if (!this.element || typeof this.events !== 'function') {
149
+ return;
150
+ }
151
+
152
+ const events = this.events();
153
+ if (!events) return;
154
+
155
+ const entries = events instanceof Map ? Array.from(events.entries()) : Object.entries(events);
156
+
157
+ for (const [eventSelector, handlerReference] of entries) {
158
+ if (!eventSelector) continue;
159
+
160
+ const descriptor = eventSelector.trim();
161
+ if (!descriptor) continue;
162
+
163
+ const [eventName, ...selectorParts] = descriptor.split(/\s+/);
164
+ if (!eventName) {
165
+ console.warn(`[Component] Invalid event descriptor '${eventSelector}' in component '${this.constructor.name}'.`);
166
+ continue;
167
+ }
168
+ const selector = selectorParts.join(' ');
169
+
170
+ const handler = this._resolveEventHandler(handlerReference, eventSelector);
171
+ if (!handler) {
172
+ continue;
173
+ }
174
+
175
+ if (selector) {
176
+ // Delegated event
177
+ const eventHandler = (e) => {
178
+ const target = e?.target;
179
+ if (!target) return;
180
+ const matchedTarget = typeof target.closest === 'function'
181
+ ? target.closest(selector)
182
+ : (target.matches(selector) ? target : null);
183
+ if (matchedTarget && this.element.contains(matchedTarget)) {
184
+ handler(e);
185
+ }
186
+ };
187
+ this.element.addEventListener(eventName, eventHandler);
188
+ this.boundEvents.set(eventSelector, eventHandler);
189
+ } else {
190
+ // Direct event
191
+ this.element.addEventListener(eventName, handler);
192
+ this.boundEvents.set(eventSelector, handler);
193
+ }
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Resolve event handler definitions passed via this.events()
199
+ * Supports both handler name strings and direct function references.
200
+ * @private
201
+ */
202
+ _resolveEventHandler(handlerReference, eventSelector) {
203
+ let handler = handlerReference;
204
+
205
+ if (typeof handlerReference === 'string') {
206
+ handler = this[handlerReference];
207
+ }
208
+
209
+ if (typeof handler !== 'function') {
210
+ console.warn(`[Component] Event handler '${handlerReference}' for event '${eventSelector}' is not a function on component '${this.constructor.name}'.`);
211
+ return null;
212
+ }
213
+
214
+ // Bind handler to component instance when possible
215
+ if (typeof handler.bind === 'function') {
216
+ return handler.bind(this);
217
+ }
218
+
219
+ return (...args) => handler.apply(this, args);
220
+ }
221
+
222
+ /**
223
+ * Unbind all DOM events
224
+ */
225
+ unbindEvents() {
226
+ if (!this.element) {
227
+ this.boundEvents.clear();
228
+ return;
229
+ }
230
+
231
+ for (const [eventSelector, handler] of this.boundEvents.entries()) {
232
+ const [eventName] = eventSelector.trim().split(/\s+/);
233
+ if (!eventName) continue;
234
+ this.element.removeEventListener(eventName, handler);
235
+ }
236
+ this.boundEvents.clear();
237
+ }
238
+
239
+ /**
240
+ * Update component after state change
241
+ */
242
+ update() {
243
+ if (!this.element) return;
244
+
245
+ try {
246
+ // Get new content
247
+ const newContent = this.render();
248
+
249
+ // Always update on first render or when content changes
250
+ // First render is detected by checking if innerHTML is empty
251
+ if (!this.element.innerHTML || this.element.innerHTML !== newContent) {
252
+ // Try granular update first (preserves focus/scroll)
253
+ const granularSuccess = this._domUpdater.updateGranular(this.element, newContent);
254
+
255
+ if (!granularSuccess) {
256
+ // Fall back to full replacement for complex structural changes
257
+ // Note: This will destroy child components, so they need to be re-mounted
258
+ this.element.innerHTML = newContent;
259
+ }
260
+
261
+ // Invalidate refs cache after DOM update
262
+ this.invalidateRefs();
263
+
264
+ // Mount child components
265
+ this._mountChildren();
266
+
267
+ // Bind events if events() method exists
268
+ if (this.events && typeof this.events === 'function') {
269
+ this.bindEvents();
270
+ }
271
+ }
272
+ } catch (error) {
273
+ this._handleError(error, { phase: 'update' });
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Mount declared child components.
279
+ * This method is called after the parent component's DOM is updated.
280
+ * @private
281
+ */
282
+ _mountChildren() {
283
+ if (typeof this.children !== 'function') {
284
+ return;
285
+ }
286
+ const childrenToMount = this.children();
287
+ if (!childrenToMount) {
288
+ return;
289
+ }
290
+
291
+ for (const selector in childrenToMount) {
292
+ const container = this.getRef(selector);
293
+ if (container) {
294
+ const child = childrenToMount[selector];
295
+
296
+ // If a different component is already in this container, unmount it.
297
+ if (container.component && container.component !== child) {
298
+ container.component.unmount();
299
+ }
300
+
301
+ // If the child is not already mounted in this specific container, mount it.
302
+ if (child.element !== container) {
303
+ child.mount(container);
304
+ container.component = child; // Associate component with element for cleanup.
305
+ this.registerCleanup(() => child.unmount());
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Declares child components for the component.
313
+ * @returns {Object.<string, Component>} A map where keys are CSS selectors
314
+ * for the container elements and values are the child component instances.
315
+ * e.g., { '#child-container': this.childComponent }
316
+ */
317
+ children() {
318
+ return {};
319
+ }
320
+
321
+ /**
322
+ * Schedule a component update with optional priority
323
+ *
324
+ * This method allows components to opt-in to requestAnimationFrame batching
325
+ * for better performance when multiple components update simultaneously.
326
+ *
327
+ * @param {Object} options - Update options
328
+ * @param {boolean} options.immediate - If true, update immediately (bypass batching).
329
+ * Use for critical updates (user input, errors).
330
+ * Default: false (batched)
331
+ *
332
+ * @example
333
+ * // Batched update (default) - good for price updates, balance updates
334
+ * this.scheduleUpdate();
335
+ *
336
+ * // Immediate update - good for user input, error displays
337
+ * this.scheduleUpdate({ immediate: true });
338
+ */
339
+ scheduleUpdate(options = {}) {
340
+ if (options.immediate) {
341
+ // Critical update - execute immediately
342
+ this.update();
343
+ } else {
344
+ // Non-critical update - queue for batching
345
+ const scheduler = getUpdateScheduler();
346
+ scheduler.queue(this);
347
+ }
348
+ }
349
+
350
+ // Lifecycle methods (to be overridden by child classes)
351
+ onMount() {}
352
+ onUnmount() {}
353
+ onUpdate(oldState) {}
354
+
355
+ /**
356
+ * Error handler lifecycle hook
357
+ * Override in child classes to handle errors
358
+ * @param {Error} error - The error that occurred
359
+ * @param {Object} errorInfo - Additional error information
360
+ */
361
+ onError(error, errorInfo) {
362
+ // Default implementation does nothing
363
+ // Child classes can override to handle errors
364
+ }
365
+
366
+ // Methods to be implemented by child classes
367
+ render() {
368
+ try {
369
+ return this.template ? this.template() : '';
370
+ } catch (error) {
371
+ this._handleError(error, { phase: 'render' });
372
+ return '<div class="component-error">Error rendering component</div>';
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Handle errors and propagate to nearest ErrorBoundary
378
+ * @private
379
+ */
380
+ _handleError(error, errorInfo = {}) {
381
+ // Call component's error handler if it exists
382
+ if (this.onError) {
383
+ try {
384
+ this.onError(error, errorInfo);
385
+ } catch (handlerError) {
386
+ console.error('[Component] Error in onError handler:', handlerError);
387
+ }
388
+ }
389
+
390
+ // Propagate to parent ErrorBoundary if it exists
391
+ // Check by constructor name to avoid circular dependency
392
+ let parent = this._parent;
393
+ while (parent) {
394
+ if (parent.constructor && parent.constructor.name === 'ErrorBoundary') {
395
+ if (parent._errorHandler) {
396
+ parent._errorHandler(error, { ...errorInfo, component: this.constructor.name });
397
+ }
398
+ return;
399
+ }
400
+ parent = parent._parent;
401
+ }
402
+
403
+ // If no ErrorBoundary found, log to console
404
+ console.error(`[Component] Unhandled error in ${this.constructor.name}:`, error, errorInfo);
405
+ }
406
+
407
+ events() {
408
+ return {};
409
+ }
410
+
411
+ /**
412
+ * Register a cleanup function to be called on unmount
413
+ * @param {Function} cleanupFn - Function to call during cleanup
414
+ * @returns {Function} - Unregister function to remove this cleanup
415
+ */
416
+ registerCleanup(cleanupFn) {
417
+ if (typeof cleanupFn !== 'function') {
418
+ console.warn('[Component] registerCleanup called with non-function:', cleanupFn);
419
+ return () => {};
420
+ }
421
+
422
+ this._cleanupRegistry.add(cleanupFn);
423
+
424
+ // Return unregister function
425
+ return () => {
426
+ this._cleanupRegistry.delete(cleanupFn);
427
+ };
428
+ }
429
+
430
+ /**
431
+ * Execute all registered cleanup functions
432
+ * @private
433
+ */
434
+ _executeCleanup() {
435
+ this._cleanupRegistry.forEach(cleanupFn => {
436
+ try {
437
+ cleanupFn();
438
+ } catch (error) {
439
+ console.error('[Component] Error during cleanup:', error);
440
+ }
441
+ });
442
+ this._cleanupRegistry.clear();
443
+ }
444
+
445
+ /**
446
+ * Unmount all child components
447
+ * @private
448
+ */
449
+ _unmountChildren() {
450
+ for (const [key, child] of this._children.entries()) {
451
+ try {
452
+ if (child && typeof child.unmount === 'function') {
453
+ child.unmount();
454
+ }
455
+ } catch (error) {
456
+ console.error(`[Component] Error unmounting child "${key}":`, error);
457
+ }
458
+ }
459
+ this._children.clear();
460
+ }
461
+
462
+ /**
463
+ * Create and track a child component
464
+ * @param {string} key - Unique key for this child component
465
+ * @param {Component} childComponent - Child component instance
466
+ * @returns {Component} - The child component
467
+ */
468
+ createChild(key, childComponent) {
469
+ if (this._children.has(key)) {
470
+ console.warn(`[Component] Child with key "${key}" already exists, unmounting previous instance`);
471
+ const previous = this._children.get(key);
472
+ if (previous && typeof previous.unmount === 'function') {
473
+ previous.unmount();
474
+ }
475
+ }
476
+
477
+ // Set parent reference for context traversal
478
+ childComponent._parent = this;
479
+
480
+ // Inherit parent context
481
+ this._context.forEach((value, key) => {
482
+ childComponent._context.set(key, value);
483
+ });
484
+
485
+ this._children.set(key, childComponent);
486
+
487
+ // Register cleanup to unmount child
488
+ this.registerCleanup(() => {
489
+ if (childComponent && typeof childComponent.unmount === 'function') {
490
+ childComponent.unmount();
491
+ }
492
+ this._children.delete(key);
493
+ });
494
+
495
+ return childComponent;
496
+ }
497
+
498
+ /**
499
+ * Wrapper for setTimeout that automatically registers cleanup
500
+ * @param {Function} callback - Function to call after delay
501
+ * @param {number} delay - Delay in milliseconds
502
+ * @returns {number} - Timer ID (can be used with clearTimeout)
503
+ */
504
+ setTimeout(callback, delay) {
505
+ const timerId = window.setTimeout(callback, delay);
506
+
507
+ // Register cleanup to clear the timer
508
+ this.registerCleanup(() => {
509
+ window.clearTimeout(timerId);
510
+ });
511
+
512
+ return timerId;
513
+ }
514
+
515
+ /**
516
+ * Wrapper for setInterval that automatically registers cleanup
517
+ * @param {Function} callback - Function to call repeatedly
518
+ * @param {number} delay - Interval in milliseconds
519
+ * @returns {number} - Timer ID (can be used with clearInterval)
520
+ */
521
+ setInterval(callback, delay) {
522
+ const timerId = window.setInterval(callback, delay);
523
+
524
+ // Register cleanup to clear the interval
525
+ this.registerCleanup(() => {
526
+ window.clearInterval(timerId);
527
+ });
528
+
529
+ return timerId;
530
+ }
531
+
532
+ /**
533
+ * Subscribe to an event with automatic cleanup on unmount
534
+ * @param {string} eventName - Name of the event to subscribe to
535
+ * @param {Function} callback - Callback function to call when event is emitted
536
+ * @returns {Function} - Unsubscribe function (also auto-called on unmount)
537
+ */
538
+ subscribe(eventName, callback) {
539
+ // Subscribe to the event
540
+ const unsubscribe = eventBus.on(eventName, callback);
541
+
542
+ // Track the subscription for automatic cleanup
543
+ this._subscriptions.add(unsubscribe);
544
+
545
+ // Register cleanup to unsubscribe
546
+ this.registerCleanup(() => {
547
+ unsubscribe();
548
+ this._subscriptions.delete(unsubscribe);
549
+ });
550
+
551
+ return unsubscribe;
552
+ }
553
+
554
+ /**
555
+ * Subscribe to an event for one-time use with automatic cleanup
556
+ * @param {string} eventName - Name of the event to subscribe to
557
+ * @param {Function} callback - Callback function to call when event is emitted (once)
558
+ * @returns {Function} - Unsubscribe function (also auto-called on unmount or after first call)
559
+ */
560
+ subscribeOnce(eventName, callback) {
561
+ // Subscribe to the event once
562
+ const unsubscribe = eventBus.once(eventName, callback);
563
+
564
+ // Track the subscription for automatic cleanup
565
+ this._subscriptions.add(unsubscribe);
566
+
567
+ // Register cleanup
568
+ this.registerCleanup(() => {
569
+ unsubscribe();
570
+ this._subscriptions.delete(unsubscribe);
571
+ });
572
+
573
+ return unsubscribe;
574
+ }
575
+
576
+ /**
577
+ * Hook to subscribe to store state changes
578
+ * Automatically updates component when selected store state changes
579
+ *
580
+ * STATE OWNERSHIP RULES:
581
+ * - UI-only state (focus, hover, temporary UI state) → use this.state
582
+ * - Shared/global state (balances, price, wallet, contract data) → use store
583
+ * - Derived state → use selectors
584
+ *
585
+ * Usage examples:
586
+ * // Using a selector method
587
+ * const isPhase2 = this.useStore(tradingStore, () => tradingStore.selectIsPhase2());
588
+ *
589
+ * // Using a direct state selector
590
+ * const price = this.useStore(tradingStore, (state) => state.price.current);
591
+ *
592
+ * // With update callback
593
+ * this.useStore(tradingStore, () => tradingStore.selectIsPhase2(), (newValue, oldValue) => {
594
+ * console.log('Phase 2 changed:', newValue);
595
+ * });
596
+ *
597
+ * @param {Store} store - Store instance to subscribe to
598
+ * @param {Function} selector - Function that selects state (can be store method or state selector)
599
+ * @param {Function} onUpdate - Optional callback when selected state changes
600
+ * @returns {any} - Current selected state value
601
+ */
602
+ useStore(store, selector, onUpdate) {
603
+ if (!store || typeof selector !== 'function') {
604
+ console.warn('[Component] useStore called with invalid arguments');
605
+ return null;
606
+ }
607
+
608
+ // Store the last value for comparison
609
+ if (!this._storeValues) {
610
+ this._storeValues = new Map();
611
+ }
612
+
613
+ const selectorKey = selector.toString(); // Use function string as key (not perfect but works)
614
+ let lastValue = this._storeValues.get(selectorKey);
615
+
616
+ // Get initial value
617
+ const getCurrentValue = () => {
618
+ // Try calling as store method first (e.g., tradingStore.selectIsPhase2())
619
+ try {
620
+ const result = selector.call(store);
621
+ if (result !== undefined) {
622
+ return result;
623
+ }
624
+ } catch (e) {
625
+ // Not a method, try as state selector
626
+ }
627
+
628
+ // Try as state selector (e.g., (state) => state.price)
629
+ return selector(store.getState());
630
+ };
631
+
632
+ const currentValue = getCurrentValue();
633
+ this._storeValues.set(selectorKey, currentValue);
634
+
635
+ // Subscribe to store changes
636
+ const unsubscribe = store.subscribe(() => {
637
+ const newValue = getCurrentValue();
638
+ const oldValue = lastValue;
639
+
640
+ // Only update if value actually changed (shallow comparison)
641
+ if (this._hasValueChanged(oldValue, newValue)) {
642
+ // Update stored value
643
+ this._storeValues.set(selectorKey, newValue);
644
+ lastValue = newValue;
645
+
646
+ // Call optional update callback
647
+ if (typeof onUpdate === 'function') {
648
+ onUpdate(newValue, oldValue);
649
+ }
650
+
651
+ // Trigger component update
652
+ this.update();
653
+ }
654
+ });
655
+
656
+ // Register cleanup to unsubscribe
657
+ this.registerCleanup(() => {
658
+ unsubscribe();
659
+ if (this._storeValues) {
660
+ this._storeValues.delete(selectorKey);
661
+ }
662
+ });
663
+
664
+ return currentValue;
665
+ }
666
+
667
+ /**
668
+ * Check if two values have changed (shallow comparison)
669
+ * @private
670
+ */
671
+ _hasValueChanged(oldValue, newValue) {
672
+ // Primitive comparison
673
+ if (oldValue === newValue) return false;
674
+
675
+ // Null/undefined handling
676
+ if (oldValue == null || newValue == null) return oldValue !== newValue;
677
+
678
+ // Object comparison (shallow)
679
+ if (typeof oldValue === 'object' && typeof newValue === 'object') {
680
+ const oldKeys = Object.keys(oldValue);
681
+ const newKeys = Object.keys(newValue);
682
+
683
+ if (oldKeys.length !== newKeys.length) return true;
684
+
685
+ return oldKeys.some(key => oldValue[key] !== newValue[key]);
686
+ }
687
+
688
+ return true;
689
+ }
690
+
691
+ /**
692
+ * Get a cached element reference by name and selector
693
+ * @param {string} name - Reference name (for caching)
694
+ * @param {string} selector - CSS selector to find element
695
+ * @returns {HTMLElement|null} - Element or null if not found
696
+ */
697
+ _getRefCached(name, selector) {
698
+ if (!this.element) return null;
699
+
700
+ // Check cache first
701
+ if (this._refs.has(name)) {
702
+ const cached = this._refs.get(name);
703
+ // Verify element is still in DOM
704
+ if (cached && this.element.contains(cached)) {
705
+ return cached;
706
+ }
707
+ // Stale reference, remove from cache
708
+ this._refs.delete(name);
709
+ }
710
+
711
+ // Query DOM if not cached
712
+ const element = this.element.querySelector(selector);
713
+ if (element) {
714
+ this._refs.set(name, element);
715
+ }
716
+
717
+ return element;
718
+ }
719
+
720
+ /**
721
+ * Manually update a cached reference
722
+ * @param {string} name - Reference name
723
+ * @param {HTMLElement} element - Element to cache
724
+ */
725
+ updateRef(name, element) {
726
+ if (element) {
727
+ this._refs.set(name, element);
728
+ } else {
729
+ this._refs.delete(name);
730
+ }
731
+ }
732
+
733
+ /**
734
+ * Invalidate all cached references
735
+ * Should be called when DOM structure changes significantly
736
+ */
737
+ invalidateRefs() {
738
+ this._refs.clear();
739
+ }
740
+
741
+ /**
742
+ * Invalidate a specific cached reference
743
+ * @param {string} name - Reference name to invalidate
744
+ */
745
+ invalidateRef(name) {
746
+ this._refs.delete(name);
747
+ }
748
+
749
+ /**
750
+ * Get a single DOM element within this component's element
751
+ * Convenience wrapper around querySelector with null-safety check
752
+ * @param {string} selector - CSS selector to query
753
+ * @returns {HTMLElement|null} The first matching element or null
754
+ */
755
+ getRef(selector) {
756
+ if (!this.element) {
757
+ console.warn(`[Component] getRef called on ${this.constructor.name} before element exists`);
758
+ return null;
759
+ }
760
+ return this.element.querySelector(selector);
761
+ }
762
+
763
+ /**
764
+ * Get multiple DOM elements within this component's element
765
+ * Convenience wrapper around querySelectorAll with null-safety check
766
+ * @param {string} selector - CSS selector to query
767
+ * @returns {Array<HTMLElement>} Array of matching elements (empty array if none found)
768
+ */
769
+ getRefs(selector) {
770
+ if (!this.element) {
771
+ console.warn(`[Component] getRefs called on ${this.constructor.name} before element exists`);
772
+ return [];
773
+ }
774
+ // Convert NodeList to Array for easier manipulation
775
+ return Array.from(this.element.querySelectorAll(selector));
776
+ }
777
+
778
+ /**
779
+ * Provide a context value to child components
780
+ * @param {string} key - Context key
781
+ * @param {any} value - Context value
782
+ */
783
+ provideContext(key, value) {
784
+ this._context.set(key, value);
785
+
786
+ // Propagate to existing children
787
+ this._children.forEach(child => {
788
+ if (child && typeof child._context !== 'undefined') {
789
+ child._context.set(key, value);
790
+ }
791
+ });
792
+ }
793
+
794
+ /**
795
+ * Get a context value, searching up the component tree
796
+ * @param {string} key - Context key
797
+ * @returns {any} - Context value or undefined if not found
798
+ */
799
+ getContext(key) {
800
+ // Check own context first
801
+ if (this._context.has(key)) {
802
+ return this._context.get(key);
803
+ }
804
+
805
+ // Search up the tree
806
+ let parent = this._parent;
807
+ while (parent) {
808
+ if (parent._context && parent._context.has(key)) {
809
+ return parent._context.get(key);
810
+ }
811
+ parent = parent._parent;
812
+ }
813
+
814
+ return undefined;
815
+ }
816
+
817
+ /**
818
+ * Remove a context value
819
+ * @param {string} key - Context key to remove
820
+ */
821
+ removeContext(key) {
822
+ this._context.delete(key);
823
+
824
+ // Remove from children
825
+ this._children.forEach(child => {
826
+ if (child && typeof child._context !== 'undefined') {
827
+ child._context.delete(key);
828
+ }
829
+ });
830
+ }
831
+ }