@monygroupcorp/micro-web3 0.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@monygroupcorp/micro-web3",
3
- "version": "0.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "A lean, reusable Web3 toolkit with components for wallet connection, IPFS, and common Web3 UI patterns.",
5
- "main": "dist/micro-web3.cjs.js",
5
+ "main": "dist/micro-web3.cjs",
6
6
  "module": "dist/micro-web3.esm.js",
7
7
  "type": "module",
8
8
  "scripts": {
package/rollup.config.cjs CHANGED
@@ -7,7 +7,7 @@ module.exports = [
7
7
  input: 'src/index.js',
8
8
  output: [
9
9
  {
10
- file: 'dist/micro-web3.cjs.js',
10
+ file: 'dist/micro-web3.cjs',
11
11
  format: 'cjs',
12
12
  sourcemap: true,
13
13
  },
@@ -1,5 +1,6 @@
1
1
  import { Component, eventBus } from '@monygroupcorp/microact';
2
2
  import WalletModal from '../Wallet/WalletModal.js';
3
+ import { SettingsModal } from '../SettingsModal/SettingsModal.js';
3
4
  import { ethers } from 'ethers';
4
5
 
5
6
  /**
@@ -12,6 +13,7 @@ export class FloatingWalletButton extends Component {
12
13
  super();
13
14
  this.walletService = props.walletService;
14
15
  this.walletModal = null;
16
+ this.settingsModal = null;
15
17
  this.state = {
16
18
  walletConnected: false,
17
19
  address: null,
@@ -230,13 +232,19 @@ export class FloatingWalletButton extends Component {
230
232
 
231
233
  // Create WalletModal if not exists
232
234
  if (!this.walletModal) {
233
- this.walletModal = new WalletModal(
235
+ this.walletModal = new WalletModal({
234
236
  providerMap,
235
237
  walletIcons,
236
- async (walletType) => {
238
+ onWalletSelected: async (walletType) => {
237
239
  await this.handleWalletSelection(walletType);
238
- }
239
- );
240
+ },
241
+ });
242
+
243
+ if (!this.modalRoot) {
244
+ this.modalRoot = document.createElement('div');
245
+ document.body.appendChild(this.modalRoot);
246
+ }
247
+ this.walletModal.mount(this.modalRoot);
240
248
  }
241
249
 
242
250
  this.walletModal.show();
@@ -637,6 +645,7 @@ export class FloatingWalletButton extends Component {
637
645
  this.handleMenuItemClick(route);
638
646
  } else if (action === 'settings') {
639
647
  this.setState({ menuOpen: false });
648
+ this.openSettings();
640
649
  }
641
650
  });
642
651
  });
@@ -657,12 +666,25 @@ export class FloatingWalletButton extends Component {
657
666
  }
658
667
  }
659
668
 
669
+ openSettings() {
670
+ if (!this.settingsModal) {
671
+ this.settingsModal = new SettingsModal({ eventBus: eventBus });
672
+ }
673
+ this.settingsModal.show();
674
+ }
675
+
660
676
  onUnmount() {
661
677
  // Clean up wallet modal
662
678
  if (this.walletModal) {
663
679
  this.walletModal.hide();
664
680
  this.walletModal = null;
665
681
  }
682
+
683
+ // Clean up settings modal
684
+ if (this.settingsModal) {
685
+ this.settingsModal.hide();
686
+ this.settingsModal = null;
687
+ }
666
688
  }
667
689
 
668
690
  escapeHtml(text) {
@@ -672,4 +694,3 @@ export class FloatingWalletButton extends Component {
672
694
  return div.innerHTML;
673
695
  }
674
696
  }
675
-
@@ -0,0 +1,371 @@
1
+ // src/components/SettingsModal/SettingsModal.js
2
+
3
+ import { IndexerSettings } from '../../storage/index.js';
4
+
5
+ /**
6
+ * SettingsModal - Modal for user settings including indexer storage controls.
7
+ *
8
+ * @example
9
+ * const modal = new SettingsModal({ eventBus });
10
+ * modal.show();
11
+ */
12
+ class SettingsModal {
13
+ constructor({ eventBus }) {
14
+ this.eventBus = eventBus;
15
+ this.element = null;
16
+ this.overlay = null;
17
+ this.isVisible = false;
18
+ this.state = {
19
+ storageEnabled: true,
20
+ storageUsed: 0,
21
+ storageQuota: 0,
22
+ clearing: false
23
+ };
24
+ }
25
+
26
+ async show() {
27
+ if (this.isVisible) return;
28
+
29
+ // Load current settings
30
+ const settings = IndexerSettings.get();
31
+ const estimate = await IndexerSettings.getStorageEstimate();
32
+
33
+ this.state = {
34
+ storageEnabled: settings.storageEnabled,
35
+ storageUsed: estimate.used || 0,
36
+ storageQuota: estimate.quota || 0,
37
+ clearing: false
38
+ };
39
+
40
+ this._createModal();
41
+ this.isVisible = true;
42
+ }
43
+
44
+ hide() {
45
+ if (!this.isVisible) return;
46
+
47
+ if (this.overlay && this.overlay.parentNode) {
48
+ this.overlay.parentNode.removeChild(this.overlay);
49
+ }
50
+ this.overlay = null;
51
+ this.element = null;
52
+ this.isVisible = false;
53
+ }
54
+
55
+ _createModal() {
56
+ // Inject styles if needed
57
+ SettingsModal.injectStyles();
58
+
59
+ // Create overlay
60
+ this.overlay = document.createElement('div');
61
+ this.overlay.className = 'mw3-settings-overlay';
62
+ this.overlay.addEventListener('click', (e) => {
63
+ if (e.target === this.overlay) this.hide();
64
+ });
65
+
66
+ // Create modal
67
+ this.element = document.createElement('div');
68
+ this.element.className = 'mw3-settings-modal';
69
+ this.element.innerHTML = this._render();
70
+
71
+ this.overlay.appendChild(this.element);
72
+ document.body.appendChild(this.overlay);
73
+
74
+ this._setupEventListeners();
75
+ }
76
+
77
+ _render() {
78
+ const { storageEnabled, storageUsed, storageQuota, clearing } = this.state;
79
+ const usedMB = (storageUsed / (1024 * 1024)).toFixed(1);
80
+ const quotaMB = (storageQuota / (1024 * 1024)).toFixed(0);
81
+
82
+ return `
83
+ <div class="mw3-settings-header">
84
+ <h2>Settings</h2>
85
+ <button class="mw3-settings-close" data-action="close">&times;</button>
86
+ </div>
87
+
88
+ <div class="mw3-settings-content">
89
+ <div class="mw3-settings-section">
90
+ <h3>Data Storage</h3>
91
+ <p class="mw3-settings-description">
92
+ Event data is cached locally to improve performance. You can disable this to save space.
93
+ </p>
94
+
95
+ <div class="mw3-settings-row">
96
+ <label class="mw3-settings-toggle">
97
+ <input type="checkbox" ${storageEnabled ? 'checked' : ''} data-setting="storageEnabled">
98
+ <span class="mw3-toggle-slider"></span>
99
+ <span class="mw3-toggle-label">Enable local event storage</span>
100
+ </label>
101
+ </div>
102
+
103
+ <div class="mw3-settings-info">
104
+ <span>Storage used: ${usedMB} MB${quotaMB > 0 ? ` / ${quotaMB} MB` : ''}</span>
105
+ </div>
106
+
107
+ <div class="mw3-settings-row">
108
+ <button class="mw3-settings-btn mw3-settings-btn--danger" data-action="clearData" ${clearing ? 'disabled' : ''}>
109
+ ${clearing ? 'Clearing...' : 'Clear All Cached Data'}
110
+ </button>
111
+ </div>
112
+
113
+ <p class="mw3-settings-note">
114
+ Disabling storage means event history will need to be re-fetched each session.
115
+ </p>
116
+ </div>
117
+ </div>
118
+
119
+ <div class="mw3-settings-footer">
120
+ <button class="mw3-settings-btn mw3-settings-btn--primary" data-action="close">Done</button>
121
+ </div>
122
+ `;
123
+ }
124
+
125
+ _setupEventListeners() {
126
+ // Close button
127
+ this.element.querySelectorAll('[data-action="close"]').forEach(btn => {
128
+ btn.addEventListener('click', () => this.hide());
129
+ });
130
+
131
+ // Storage toggle
132
+ const toggle = this.element.querySelector('[data-setting="storageEnabled"]');
133
+ if (toggle) {
134
+ toggle.addEventListener('change', (e) => {
135
+ const enabled = e.target.checked;
136
+ IndexerSettings.set({ storageEnabled: enabled });
137
+ this.state.storageEnabled = enabled;
138
+
139
+ // Emit event so indexer can react
140
+ this.eventBus.emit('settings:storageChanged', { enabled });
141
+ });
142
+ }
143
+
144
+ // Clear data button
145
+ const clearBtn = this.element.querySelector('[data-action="clearData"]');
146
+ if (clearBtn) {
147
+ clearBtn.addEventListener('click', async () => {
148
+ if (this.state.clearing) return;
149
+
150
+ this.state.clearing = true;
151
+ this._updateContent();
152
+
153
+ try {
154
+ await IndexerSettings.clearAllData();
155
+
156
+ // Update storage estimate
157
+ const estimate = await IndexerSettings.getStorageEstimate();
158
+ this.state.storageUsed = estimate.used || 0;
159
+ this.state.storageQuota = estimate.quota || 0;
160
+
161
+ // Emit event so indexer knows to resync
162
+ this.eventBus.emit('settings:dataCleared', {});
163
+ } catch (error) {
164
+ console.error('[SettingsModal] Failed to clear data:', error);
165
+ }
166
+
167
+ this.state.clearing = false;
168
+ this._updateContent();
169
+ });
170
+ }
171
+
172
+ // ESC key to close
173
+ this._escHandler = (e) => {
174
+ if (e.key === 'Escape') this.hide();
175
+ };
176
+ document.addEventListener('keydown', this._escHandler);
177
+ }
178
+
179
+ _updateContent() {
180
+ if (this.element) {
181
+ this.element.innerHTML = this._render();
182
+ this._setupEventListeners();
183
+ }
184
+ }
185
+
186
+ static injectStyles() {
187
+ if (document.getElementById('mw3-settings-styles')) return;
188
+
189
+ const style = document.createElement('style');
190
+ style.id = 'mw3-settings-styles';
191
+ style.textContent = `
192
+ .mw3-settings-overlay {
193
+ position: fixed;
194
+ top: 0;
195
+ left: 0;
196
+ right: 0;
197
+ bottom: 0;
198
+ background: rgba(0, 0, 0, 0.7);
199
+ display: flex;
200
+ align-items: center;
201
+ justify-content: center;
202
+ z-index: 10000;
203
+ backdrop-filter: blur(4px);
204
+ }
205
+
206
+ .mw3-settings-modal {
207
+ background: linear-gradient(135deg, #1a1a2e, #16213e);
208
+ border: 1px solid rgba(218, 165, 32, 0.3);
209
+ border-radius: 12px;
210
+ width: 90%;
211
+ max-width: 400px;
212
+ color: #fff;
213
+ font-family: 'Lato', system-ui, sans-serif;
214
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
215
+ }
216
+
217
+ .mw3-settings-header {
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: space-between;
221
+ padding: 1rem 1.25rem;
222
+ border-bottom: 1px solid rgba(218, 165, 32, 0.2);
223
+ }
224
+
225
+ .mw3-settings-header h2 {
226
+ margin: 0;
227
+ font-size: 1.125rem;
228
+ font-weight: 600;
229
+ color: rgba(218, 165, 32, 0.95);
230
+ }
231
+
232
+ .mw3-settings-close {
233
+ background: none;
234
+ border: none;
235
+ color: rgba(255, 255, 255, 0.6);
236
+ font-size: 1.5rem;
237
+ cursor: pointer;
238
+ padding: 0;
239
+ line-height: 1;
240
+ }
241
+
242
+ .mw3-settings-close:hover {
243
+ color: #fff;
244
+ }
245
+
246
+ .mw3-settings-content {
247
+ padding: 1.25rem;
248
+ }
249
+
250
+ .mw3-settings-section h3 {
251
+ margin: 0 0 0.5rem 0;
252
+ font-size: 0.9rem;
253
+ font-weight: 600;
254
+ color: rgba(255, 255, 255, 0.9);
255
+ }
256
+
257
+ .mw3-settings-description {
258
+ margin: 0 0 1rem 0;
259
+ font-size: 0.8rem;
260
+ color: rgba(255, 255, 255, 0.6);
261
+ line-height: 1.4;
262
+ }
263
+
264
+ .mw3-settings-row {
265
+ margin-bottom: 1rem;
266
+ }
267
+
268
+ .mw3-settings-toggle {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 0.75rem;
272
+ cursor: pointer;
273
+ }
274
+
275
+ .mw3-settings-toggle input {
276
+ display: none;
277
+ }
278
+
279
+ .mw3-toggle-slider {
280
+ width: 44px;
281
+ height: 24px;
282
+ background: rgba(255, 255, 255, 0.2);
283
+ border-radius: 12px;
284
+ position: relative;
285
+ transition: background 0.2s;
286
+ }
287
+
288
+ .mw3-toggle-slider::after {
289
+ content: '';
290
+ position: absolute;
291
+ top: 2px;
292
+ left: 2px;
293
+ width: 20px;
294
+ height: 20px;
295
+ background: #fff;
296
+ border-radius: 50%;
297
+ transition: transform 0.2s;
298
+ }
299
+
300
+ .mw3-settings-toggle input:checked + .mw3-toggle-slider {
301
+ background: rgba(218, 165, 32, 0.8);
302
+ }
303
+
304
+ .mw3-settings-toggle input:checked + .mw3-toggle-slider::after {
305
+ transform: translateX(20px);
306
+ }
307
+
308
+ .mw3-toggle-label {
309
+ font-size: 0.875rem;
310
+ color: rgba(255, 255, 255, 0.9);
311
+ }
312
+
313
+ .mw3-settings-info {
314
+ font-size: 0.75rem;
315
+ color: rgba(255, 255, 255, 0.5);
316
+ margin-bottom: 1rem;
317
+ }
318
+
319
+ .mw3-settings-note {
320
+ font-size: 0.75rem;
321
+ color: rgba(255, 255, 255, 0.4);
322
+ margin: 0;
323
+ font-style: italic;
324
+ }
325
+
326
+ .mw3-settings-btn {
327
+ padding: 0.625rem 1rem;
328
+ border-radius: 6px;
329
+ font-size: 0.875rem;
330
+ font-weight: 500;
331
+ cursor: pointer;
332
+ border: none;
333
+ transition: all 0.2s;
334
+ }
335
+
336
+ .mw3-settings-btn--primary {
337
+ background: rgba(218, 165, 32, 0.9);
338
+ color: #1a1a2e;
339
+ }
340
+
341
+ .mw3-settings-btn--primary:hover {
342
+ background: rgba(218, 165, 32, 1);
343
+ }
344
+
345
+ .mw3-settings-btn--danger {
346
+ background: rgba(255, 99, 71, 0.2);
347
+ color: rgba(255, 99, 71, 0.9);
348
+ border: 1px solid rgba(255, 99, 71, 0.3);
349
+ }
350
+
351
+ .mw3-settings-btn--danger:hover {
352
+ background: rgba(255, 99, 71, 0.3);
353
+ }
354
+
355
+ .mw3-settings-btn:disabled {
356
+ opacity: 0.5;
357
+ cursor: not-allowed;
358
+ }
359
+
360
+ .mw3-settings-footer {
361
+ padding: 1rem 1.25rem;
362
+ border-top: 1px solid rgba(218, 165, 32, 0.2);
363
+ display: flex;
364
+ justify-content: flex-end;
365
+ }
366
+ `;
367
+ document.head.appendChild(style);
368
+ }
369
+ }
370
+
371
+ export { SettingsModal };
@@ -0,0 +1,238 @@
1
+ // src/components/SyncProgressBar/SyncProgressBar.js
2
+
3
+ /**
4
+ * SyncProgressBar - UI component showing indexer sync status.
5
+ *
6
+ * @example
7
+ * import { SyncProgressBar } from '@monygroupcorp/micro-web3/components';
8
+ * const progressBar = new SyncProgressBar({ indexer, eventBus });
9
+ * progressBar.mount(document.getElementById('sync-status'));
10
+ */
11
+ class SyncProgressBar {
12
+ constructor({ indexer, eventBus }) {
13
+ if (!indexer) {
14
+ throw new Error('SyncProgressBar requires indexer instance');
15
+ }
16
+ if (!eventBus) {
17
+ throw new Error('SyncProgressBar requires eventBus instance');
18
+ }
19
+
20
+ this.indexer = indexer;
21
+ this.eventBus = eventBus;
22
+ this.element = null;
23
+ this.unsubscribers = [];
24
+
25
+ this.state = {
26
+ status: 'initializing',
27
+ progress: 0,
28
+ currentBlock: 0,
29
+ latestBlock: 0,
30
+ error: null
31
+ };
32
+ }
33
+
34
+ mount(container) {
35
+ if (typeof container === 'string') {
36
+ container = document.querySelector(container);
37
+ }
38
+ if (!container) {
39
+ throw new Error('SyncProgressBar: Invalid container');
40
+ }
41
+
42
+ // Create element
43
+ this.element = document.createElement('div');
44
+ this.element.className = 'mw3-sync-progress';
45
+ container.appendChild(this.element);
46
+
47
+ // Subscribe to events
48
+ this._setupListeners();
49
+
50
+ // Initial render
51
+ this._updateState(this.indexer.sync.getStatus());
52
+ this._render();
53
+
54
+ return this;
55
+ }
56
+
57
+ unmount() {
58
+ // Cleanup subscriptions
59
+ for (const unsub of this.unsubscribers) {
60
+ unsub();
61
+ }
62
+ this.unsubscribers = [];
63
+
64
+ // Remove element
65
+ if (this.element && this.element.parentNode) {
66
+ this.element.parentNode.removeChild(this.element);
67
+ }
68
+ this.element = null;
69
+ }
70
+
71
+ _setupListeners() {
72
+ const listeners = [
73
+ ['indexer:syncProgress', (data) => {
74
+ this._updateState({
75
+ status: 'syncing',
76
+ progress: data.progress,
77
+ currentBlock: data.currentBlock,
78
+ latestBlock: data.latestBlock
79
+ });
80
+ }],
81
+ ['indexer:syncComplete', () => {
82
+ this._updateState({ status: 'synced', progress: 1 });
83
+ }],
84
+ ['indexer:error', (data) => {
85
+ this._updateState({ status: 'error', error: data.message });
86
+ }],
87
+ ['indexer:paused', () => {
88
+ this._updateState({ status: 'paused' });
89
+ }],
90
+ ['indexer:resumed', () => {
91
+ this._updateState({ status: 'syncing' });
92
+ }]
93
+ ];
94
+
95
+ for (const [event, handler] of listeners) {
96
+ const unsub = this.eventBus.on(event, handler);
97
+ this.unsubscribers.push(unsub);
98
+ }
99
+ }
100
+
101
+ _updateState(updates) {
102
+ this.state = { ...this.state, ...updates };
103
+ this._render();
104
+ }
105
+
106
+ _render() {
107
+ if (!this.element) return;
108
+
109
+ const { status, progress, currentBlock, latestBlock, error } = this.state;
110
+
111
+ // Update class for state
112
+ this.element.className = `mw3-sync-progress mw3-sync-progress--${status}`;
113
+
114
+ if (status === 'syncing') {
115
+ const percent = Math.round(progress * 100);
116
+ this.element.innerHTML = `
117
+ <div class="mw3-sync-progress__bar" style="width: ${percent}%"></div>
118
+ <span class="mw3-sync-progress__text">Syncing... ${percent}%</span>
119
+ `;
120
+ } else if (status === 'synced') {
121
+ this.element.innerHTML = `
122
+ <span class="mw3-sync-progress__text">Synced to block ${latestBlock.toLocaleString()}</span>
123
+ `;
124
+ } else if (status === 'error') {
125
+ this.element.innerHTML = `
126
+ <span class="mw3-sync-progress__text">Sync failed: ${error || 'Unknown error'}</span>
127
+ <button class="mw3-sync-progress__retry">Retry</button>
128
+ `;
129
+
130
+ // Add retry handler
131
+ const retryBtn = this.element.querySelector('.mw3-sync-progress__retry');
132
+ if (retryBtn) {
133
+ retryBtn.onclick = () => this.indexer.sync.resync();
134
+ }
135
+ } else if (status === 'paused') {
136
+ this.element.innerHTML = `
137
+ <span class="mw3-sync-progress__text">Sync paused</span>
138
+ <button class="mw3-sync-progress__resume">Resume</button>
139
+ `;
140
+
141
+ const resumeBtn = this.element.querySelector('.mw3-sync-progress__resume');
142
+ if (resumeBtn) {
143
+ resumeBtn.onclick = () => this.indexer.sync.resume();
144
+ }
145
+ } else {
146
+ this.element.innerHTML = `
147
+ <span class="mw3-sync-progress__text">Initializing...</span>
148
+ `;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Inject default styles into document.
154
+ * Call once at app startup if not using custom CSS.
155
+ */
156
+ static injectStyles() {
157
+ if (document.getElementById('mw3-sync-progress-styles')) return;
158
+
159
+ const style = document.createElement('style');
160
+ style.id = 'mw3-sync-progress-styles';
161
+ style.textContent = `
162
+ .mw3-sync-progress {
163
+ position: relative;
164
+ height: 24px;
165
+ background: var(--mw3-sync-bg, #f0f0f0);
166
+ border-radius: 4px;
167
+ overflow: hidden;
168
+ font-family: system-ui, sans-serif;
169
+ font-size: 12px;
170
+ }
171
+
172
+ .mw3-sync-progress__bar {
173
+ position: absolute;
174
+ top: 0;
175
+ left: 0;
176
+ height: 100%;
177
+ background: var(--mw3-sync-bar, #4caf50);
178
+ transition: width 0.3s ease;
179
+ }
180
+
181
+ .mw3-sync-progress__text {
182
+ position: relative;
183
+ z-index: 1;
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: center;
187
+ height: 100%;
188
+ color: var(--mw3-sync-text, #333);
189
+ padding: 0 8px;
190
+ }
191
+
192
+ .mw3-sync-progress--synced {
193
+ background: var(--mw3-sync-bar, #4caf50);
194
+ }
195
+
196
+ .mw3-sync-progress--synced .mw3-sync-progress__text {
197
+ color: white;
198
+ }
199
+
200
+ .mw3-sync-progress--error {
201
+ background: var(--mw3-sync-error, #f44336);
202
+ }
203
+
204
+ .mw3-sync-progress--error .mw3-sync-progress__text {
205
+ color: white;
206
+ justify-content: space-between;
207
+ }
208
+
209
+ .mw3-sync-progress__retry,
210
+ .mw3-sync-progress__resume {
211
+ background: rgba(255,255,255,0.2);
212
+ border: 1px solid rgba(255,255,255,0.4);
213
+ border-radius: 3px;
214
+ color: white;
215
+ padding: 2px 8px;
216
+ cursor: pointer;
217
+ font-size: 11px;
218
+ }
219
+
220
+ .mw3-sync-progress__retry:hover,
221
+ .mw3-sync-progress__resume:hover {
222
+ background: rgba(255,255,255,0.3);
223
+ }
224
+
225
+ .mw3-sync-progress--paused {
226
+ background: var(--mw3-sync-paused, #ff9800);
227
+ }
228
+
229
+ .mw3-sync-progress--paused .mw3-sync-progress__text {
230
+ color: white;
231
+ justify-content: space-between;
232
+ }
233
+ `;
234
+ document.head.appendChild(style);
235
+ }
236
+ }
237
+
238
+ export { SyncProgressBar };