@monygroupcorp/micro-web3 0.1.3 → 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.3",
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,
@@ -643,6 +645,7 @@ export class FloatingWalletButton extends Component {
643
645
  this.handleMenuItemClick(route);
644
646
  } else if (action === 'settings') {
645
647
  this.setState({ menuOpen: false });
648
+ this.openSettings();
646
649
  }
647
650
  });
648
651
  });
@@ -663,12 +666,25 @@ export class FloatingWalletButton extends Component {
663
666
  }
664
667
  }
665
668
 
669
+ openSettings() {
670
+ if (!this.settingsModal) {
671
+ this.settingsModal = new SettingsModal({ eventBus: eventBus });
672
+ }
673
+ this.settingsModal.show();
674
+ }
675
+
666
676
  onUnmount() {
667
677
  // Clean up wallet modal
668
678
  if (this.walletModal) {
669
679
  this.walletModal.hide();
670
680
  this.walletModal = null;
671
681
  }
682
+
683
+ // Clean up settings modal
684
+ if (this.settingsModal) {
685
+ this.settingsModal.hide();
686
+ this.settingsModal = null;
687
+ }
672
688
  }
673
689
 
674
690
  escapeHtml(text) {
@@ -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 };