holosphere 2.0.0-alpha7 → 2.0.0-alpha8

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 (40) hide show
  1. package/dist/cjs/holosphere.cjs +1 -1
  2. package/dist/esm/holosphere.js +1 -1
  3. package/dist/{index-d6f4RJBM.js → index-4XHHKe6S.js} +356 -58
  4. package/dist/index-4XHHKe6S.js.map +1 -0
  5. package/dist/{index-jmTHEbR2.js → index-BjP1TXGz.js} +2 -2
  6. package/dist/{index-jmTHEbR2.js.map → index-BjP1TXGz.js.map} +1 -1
  7. package/dist/{index-C-IlLYlk.cjs → index-CKffQDmQ.cjs} +2 -2
  8. package/dist/{index-C-IlLYlk.cjs.map → index-CKffQDmQ.cjs.map} +1 -1
  9. package/dist/index-Dz5kOZMI.cjs +5 -0
  10. package/dist/index-Dz5kOZMI.cjs.map +1 -0
  11. package/dist/{indexeddb-storage-a8GipaDr.cjs → indexeddb-storage-DD7EFBVc.cjs} +2 -2
  12. package/dist/{indexeddb-storage-a8GipaDr.cjs.map → indexeddb-storage-DD7EFBVc.cjs.map} +1 -1
  13. package/dist/{indexeddb-storage-D8kOl0oK.js → indexeddb-storage-lExjjFlV.js} +2 -2
  14. package/dist/{indexeddb-storage-D8kOl0oK.js.map → indexeddb-storage-lExjjFlV.js.map} +1 -1
  15. package/dist/{memory-storage-DBQK622V.js → memory-storage-C68adso2.js} +2 -2
  16. package/dist/{memory-storage-DBQK622V.js.map → memory-storage-C68adso2.js.map} +1 -1
  17. package/dist/{memory-storage-gfRovk2O.cjs → memory-storage-DD_6yyXT.cjs} +2 -2
  18. package/dist/{memory-storage-gfRovk2O.cjs.map → memory-storage-DD_6yyXT.cjs.map} +1 -1
  19. package/dist/{secp256k1-BCAPF45D.cjs → secp256k1-DYELiqgx.cjs} +2 -2
  20. package/dist/{secp256k1-BCAPF45D.cjs.map → secp256k1-DYELiqgx.cjs.map} +1 -1
  21. package/dist/{secp256k1-DYm_CMqW.js → secp256k1-OM8siPyy.js} +2 -2
  22. package/dist/{secp256k1-DYm_CMqW.js.map → secp256k1-OM8siPyy.js.map} +1 -1
  23. package/examples/holosphere-widget.js +1242 -0
  24. package/examples/widget-demo.html +274 -0
  25. package/examples/widget.html +703 -0
  26. package/package.json +3 -1
  27. package/src/cdn-entry.js +22 -0
  28. package/src/contracts/queries.js +16 -1
  29. package/src/core/holosphere.js +2 -2
  30. package/src/crypto/nostr-utils.js +36 -2
  31. package/src/federation/handshake.js +16 -4
  32. package/src/index.js +16 -2
  33. package/src/storage/backends/gundb-backend.js +293 -9
  34. package/src/storage/gun-wrapper.js +64 -16
  35. package/src/storage/nostr-async.js +40 -25
  36. package/src/storage/unified-storage.js +31 -1
  37. package/vite.config.cdn.js +60 -0
  38. package/dist/index-Bvwyvd0T.cjs +0 -5
  39. package/dist/index-Bvwyvd0T.cjs.map +0 -1
  40. package/dist/index-d6f4RJBM.js.map +0 -1
@@ -0,0 +1,1242 @@
1
+ /**
2
+ * HoloSphere Visual Widget
3
+ *
4
+ * A floating widget that provides:
5
+ * - Zoomable H3 hexagon map
6
+ * - Holonic data explorer
7
+ * - Real-time data visualization
8
+ *
9
+ * Usage:
10
+ * <script src="holosphere.min.js"></script>
11
+ * <script src="holosphere-widget.js"></script>
12
+ * <script>
13
+ * HoloSphereWidget.init({ appName: 'my-app' });
14
+ * </script>
15
+ */
16
+
17
+ (function(global) {
18
+ 'use strict';
19
+
20
+ // Widget state
21
+ let hs = null;
22
+ let map = null;
23
+ let currentHolon = null;
24
+ let currentResolution = 9;
25
+ let hexagonLayers = {};
26
+ let selectedHexLayer = null;
27
+ let isOpen = false;
28
+ let config = {};
29
+
30
+ // H3 resolution to zoom level mapping
31
+ const resolutionToZoom = {
32
+ 0: 2, 1: 3, 2: 4, 3: 5, 4: 6, 5: 7,
33
+ 6: 8, 7: 9, 8: 10, 9: 11, 10: 12, 11: 13,
34
+ 12: 14, 13: 15, 14: 16, 15: 17
35
+ };
36
+
37
+ const zoomToResolution = Object.fromEntries(
38
+ Object.entries(resolutionToZoom).map(([k, v]) => [v, parseInt(k)])
39
+ );
40
+
41
+ // Color palette for hexagons
42
+ const colors = {
43
+ empty: 'rgba(102, 126, 234, 0.2)',
44
+ hasData: 'rgba(102, 126, 234, 0.6)',
45
+ selected: 'rgba(118, 75, 162, 0.8)',
46
+ hover: 'rgba(102, 126, 234, 0.4)',
47
+ stroke: '#667eea',
48
+ strokeSelected: '#764ba2'
49
+ };
50
+
51
+ // Widget HTML template
52
+ const widgetHTML = `
53
+ <div id="hs-widget-button" class="hs-widget-button">
54
+ <svg viewBox="0 0 100 100" width="40" height="40">
55
+ <polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5"
56
+ fill="none" stroke="currentColor" stroke-width="4"/>
57
+ <polygon points="50,25 72.5,37.5 72.5,62.5 50,75 27.5,62.5 27.5,37.5"
58
+ fill="currentColor" opacity="0.3"/>
59
+ <circle cx="50" cy="50" r="8" fill="currentColor"/>
60
+ </svg>
61
+ </div>
62
+
63
+ <div id="hs-widget-modal" class="hs-widget-modal">
64
+ <div class="hs-widget-container">
65
+ <div class="hs-widget-header">
66
+ <h2>
67
+ <svg viewBox="0 0 100 100" width="24" height="24" style="vertical-align: middle; margin-right: 8px;">
68
+ <polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5"
69
+ fill="none" stroke="currentColor" stroke-width="6"/>
70
+ </svg>
71
+ HoloSphere Explorer
72
+ </h2>
73
+ <button id="hs-close-btn" class="hs-close-btn">&times;</button>
74
+ </div>
75
+
76
+ <div class="hs-widget-body">
77
+ <div class="hs-map-panel">
78
+ <div id="hs-map"></div>
79
+ <div class="hs-map-controls">
80
+ <div class="hs-resolution-control">
81
+ <label>Resolution: <span id="hs-resolution-value">9</span></label>
82
+ <input type="range" id="hs-resolution-slider" min="0" max="15" value="9">
83
+ </div>
84
+ <div class="hs-coords-display" id="hs-coords">
85
+ Click map to select holon
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="hs-explorer-panel">
91
+ <div class="hs-tabs">
92
+ <button class="hs-tab active" data-tab="data">Data</button>
93
+ <button class="hs-tab" data-tab="hierarchy">Hierarchy</button>
94
+ <button class="hs-tab" data-tab="lenses">Lenses</button>
95
+ </div>
96
+
97
+ <div class="hs-tab-content active" id="hs-tab-data">
98
+ <div class="hs-holon-info">
99
+ <div class="hs-holon-id" id="hs-current-holon">No holon selected</div>
100
+ </div>
101
+ <div class="hs-lens-selector">
102
+ <select id="hs-lens-select">
103
+ <option value="">Select lens...</option>
104
+ </select>
105
+ <button id="hs-add-lens-btn" class="hs-btn-small">+ Add Lens</button>
106
+ </div>
107
+ <div class="hs-data-list" id="hs-data-list">
108
+ <div class="hs-empty-state">Select a holon on the map to view data</div>
109
+ </div>
110
+ <div class="hs-data-actions">
111
+ <button id="hs-write-btn" class="hs-btn" disabled>Write Data</button>
112
+ </div>
113
+ </div>
114
+
115
+ <div class="hs-tab-content" id="hs-tab-hierarchy">
116
+ <div class="hs-hierarchy-view" id="hs-hierarchy">
117
+ <div class="hs-empty-state">Select a holon to explore hierarchy</div>
118
+ </div>
119
+ </div>
120
+
121
+ <div class="hs-tab-content" id="hs-tab-lenses">
122
+ <div class="hs-lenses-list" id="hs-lenses-list">
123
+ <div class="hs-empty-state">Select a holon to view available lenses</div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="hs-widget-footer">
130
+ <span class="hs-status" id="hs-status">
131
+ <span class="hs-status-dot"></span>
132
+ <span id="hs-status-text">Initializing...</span>
133
+ </span>
134
+ <span class="hs-metrics" id="hs-metrics"></span>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Write Data Modal -->
140
+ <div id="hs-write-modal" class="hs-write-modal">
141
+ <div class="hs-write-container">
142
+ <h3>Write Data to Holon</h3>
143
+ <div class="hs-form-group">
144
+ <label>Holon ID</label>
145
+ <input type="text" id="hs-write-holon" readonly>
146
+ </div>
147
+ <div class="hs-form-group">
148
+ <label>Lens</label>
149
+ <input type="text" id="hs-write-lens" placeholder="e.g., events, notes, places">
150
+ </div>
151
+ <div class="hs-form-group">
152
+ <label>Data (JSON)</label>
153
+ <textarea id="hs-write-data" placeholder='{"title": "My Data", "description": "..."}'></textarea>
154
+ </div>
155
+ <div class="hs-form-actions">
156
+ <button id="hs-write-cancel" class="hs-btn-secondary">Cancel</button>
157
+ <button id="hs-write-submit" class="hs-btn">Write</button>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ `;
162
+
163
+ // Widget CSS
164
+ const widgetCSS = `
165
+ .hs-widget-button {
166
+ position: fixed;
167
+ bottom: 20px;
168
+ right: 20px;
169
+ width: 60px;
170
+ height: 60px;
171
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
172
+ border-radius: 50%;
173
+ cursor: pointer;
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: center;
177
+ color: white;
178
+ box-shadow: 0 4px 20px rgba(102, 126, 234, 0.5);
179
+ transition: transform 0.3s, box-shadow 0.3s;
180
+ z-index: 999998;
181
+ }
182
+ .hs-widget-button:hover {
183
+ transform: scale(1.1);
184
+ box-shadow: 0 6px 30px rgba(102, 126, 234, 0.7);
185
+ }
186
+ .hs-widget-button.active {
187
+ transform: rotate(30deg);
188
+ }
189
+
190
+ .hs-widget-modal {
191
+ position: fixed;
192
+ top: 0;
193
+ left: 0;
194
+ width: 100%;
195
+ height: 100%;
196
+ background: rgba(0, 0, 0, 0.5);
197
+ display: none;
198
+ align-items: center;
199
+ justify-content: center;
200
+ z-index: 999999;
201
+ opacity: 0;
202
+ transition: opacity 0.3s;
203
+ }
204
+ .hs-widget-modal.open {
205
+ display: flex;
206
+ opacity: 1;
207
+ }
208
+
209
+ .hs-widget-container {
210
+ width: 95%;
211
+ max-width: 1200px;
212
+ height: 85vh;
213
+ background: white;
214
+ border-radius: 16px;
215
+ display: flex;
216
+ flex-direction: column;
217
+ overflow: hidden;
218
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
219
+ transform: translateY(20px);
220
+ transition: transform 0.3s;
221
+ }
222
+ .hs-widget-modal.open .hs-widget-container {
223
+ transform: translateY(0);
224
+ }
225
+
226
+ .hs-widget-header {
227
+ padding: 16px 24px;
228
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
229
+ color: white;
230
+ display: flex;
231
+ justify-content: space-between;
232
+ align-items: center;
233
+ }
234
+ .hs-widget-header h2 {
235
+ margin: 0;
236
+ font-size: 1.3em;
237
+ font-weight: 600;
238
+ }
239
+ .hs-close-btn {
240
+ background: none;
241
+ border: none;
242
+ color: white;
243
+ font-size: 28px;
244
+ cursor: pointer;
245
+ width: 40px;
246
+ height: 40px;
247
+ border-radius: 50%;
248
+ transition: background 0.2s;
249
+ }
250
+ .hs-close-btn:hover {
251
+ background: rgba(255,255,255,0.2);
252
+ }
253
+
254
+ .hs-widget-body {
255
+ flex: 1;
256
+ display: flex;
257
+ overflow: hidden;
258
+ }
259
+
260
+ .hs-map-panel {
261
+ flex: 1;
262
+ display: flex;
263
+ flex-direction: column;
264
+ border-right: 1px solid #e0e0e0;
265
+ }
266
+ #hs-map {
267
+ flex: 1;
268
+ min-height: 300px;
269
+ }
270
+ .hs-map-controls {
271
+ padding: 12px 16px;
272
+ background: #f8f9fa;
273
+ border-top: 1px solid #e0e0e0;
274
+ }
275
+ .hs-resolution-control {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 12px;
279
+ margin-bottom: 8px;
280
+ }
281
+ .hs-resolution-control label {
282
+ font-size: 13px;
283
+ color: #666;
284
+ min-width: 100px;
285
+ }
286
+ .hs-resolution-control input[type="range"] {
287
+ flex: 1;
288
+ accent-color: #667eea;
289
+ }
290
+ .hs-coords-display {
291
+ font-family: monospace;
292
+ font-size: 12px;
293
+ color: #666;
294
+ padding: 8px;
295
+ background: white;
296
+ border-radius: 4px;
297
+ border: 1px solid #e0e0e0;
298
+ }
299
+
300
+ .hs-explorer-panel {
301
+ width: 400px;
302
+ display: flex;
303
+ flex-direction: column;
304
+ background: #fafafa;
305
+ }
306
+ .hs-tabs {
307
+ display: flex;
308
+ background: white;
309
+ border-bottom: 1px solid #e0e0e0;
310
+ }
311
+ .hs-tab {
312
+ flex: 1;
313
+ padding: 12px;
314
+ background: none;
315
+ border: none;
316
+ cursor: pointer;
317
+ font-size: 13px;
318
+ font-weight: 500;
319
+ color: #666;
320
+ border-bottom: 2px solid transparent;
321
+ transition: all 0.2s;
322
+ }
323
+ .hs-tab:hover {
324
+ color: #667eea;
325
+ }
326
+ .hs-tab.active {
327
+ color: #667eea;
328
+ border-bottom-color: #667eea;
329
+ }
330
+
331
+ .hs-tab-content {
332
+ display: none;
333
+ flex: 1;
334
+ flex-direction: column;
335
+ overflow: hidden;
336
+ }
337
+ .hs-tab-content.active {
338
+ display: flex;
339
+ }
340
+
341
+ .hs-holon-info {
342
+ padding: 12px 16px;
343
+ background: white;
344
+ border-bottom: 1px solid #e0e0e0;
345
+ }
346
+ .hs-holon-id {
347
+ font-family: monospace;
348
+ font-size: 13px;
349
+ color: #333;
350
+ word-break: break-all;
351
+ padding: 8px;
352
+ background: #f0f0f0;
353
+ border-radius: 4px;
354
+ }
355
+
356
+ .hs-lens-selector {
357
+ padding: 12px 16px;
358
+ display: flex;
359
+ gap: 8px;
360
+ background: white;
361
+ border-bottom: 1px solid #e0e0e0;
362
+ }
363
+ .hs-lens-selector select {
364
+ flex: 1;
365
+ padding: 8px 12px;
366
+ border: 1px solid #ddd;
367
+ border-radius: 6px;
368
+ font-size: 13px;
369
+ }
370
+ .hs-btn-small {
371
+ padding: 8px 12px;
372
+ background: #667eea;
373
+ color: white;
374
+ border: none;
375
+ border-radius: 6px;
376
+ cursor: pointer;
377
+ font-size: 12px;
378
+ white-space: nowrap;
379
+ }
380
+ .hs-btn-small:hover {
381
+ background: #5a6fd6;
382
+ }
383
+
384
+ .hs-data-list {
385
+ flex: 1;
386
+ overflow-y: auto;
387
+ padding: 12px 16px;
388
+ }
389
+ .hs-data-item {
390
+ background: white;
391
+ border-radius: 8px;
392
+ padding: 12px;
393
+ margin-bottom: 8px;
394
+ border: 1px solid #e0e0e0;
395
+ cursor: pointer;
396
+ transition: border-color 0.2s;
397
+ }
398
+ .hs-data-item:hover {
399
+ border-color: #667eea;
400
+ }
401
+ .hs-data-item-title {
402
+ font-weight: 500;
403
+ color: #333;
404
+ margin-bottom: 4px;
405
+ }
406
+ .hs-data-item-meta {
407
+ font-size: 11px;
408
+ color: #999;
409
+ }
410
+ .hs-data-item-preview {
411
+ font-size: 12px;
412
+ color: #666;
413
+ margin-top: 8px;
414
+ padding-top: 8px;
415
+ border-top: 1px solid #f0f0f0;
416
+ max-height: 60px;
417
+ overflow: hidden;
418
+ }
419
+
420
+ .hs-empty-state {
421
+ text-align: center;
422
+ color: #999;
423
+ padding: 40px 20px;
424
+ font-size: 14px;
425
+ }
426
+
427
+ .hs-data-actions {
428
+ padding: 12px 16px;
429
+ background: white;
430
+ border-top: 1px solid #e0e0e0;
431
+ }
432
+ .hs-btn {
433
+ width: 100%;
434
+ padding: 12px;
435
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
436
+ color: white;
437
+ border: none;
438
+ border-radius: 8px;
439
+ cursor: pointer;
440
+ font-size: 14px;
441
+ font-weight: 500;
442
+ transition: opacity 0.2s;
443
+ }
444
+ .hs-btn:hover:not(:disabled) {
445
+ opacity: 0.9;
446
+ }
447
+ .hs-btn:disabled {
448
+ opacity: 0.5;
449
+ cursor: not-allowed;
450
+ }
451
+ .hs-btn-secondary {
452
+ background: #6c757d;
453
+ }
454
+
455
+ .hs-hierarchy-view {
456
+ flex: 1;
457
+ overflow-y: auto;
458
+ padding: 16px;
459
+ }
460
+ .hs-hierarchy-item {
461
+ padding: 10px 12px;
462
+ margin: 4px 0;
463
+ background: white;
464
+ border-radius: 6px;
465
+ border: 1px solid #e0e0e0;
466
+ cursor: pointer;
467
+ transition: all 0.2s;
468
+ display: flex;
469
+ align-items: center;
470
+ gap: 8px;
471
+ }
472
+ .hs-hierarchy-item:hover {
473
+ border-color: #667eea;
474
+ background: #f8f9ff;
475
+ }
476
+ .hs-hierarchy-item.current {
477
+ border-color: #764ba2;
478
+ background: linear-gradient(135deg, rgba(102,126,234,0.1), rgba(118,75,162,0.1));
479
+ }
480
+ .hs-hierarchy-item .resolution {
481
+ font-size: 10px;
482
+ background: #667eea;
483
+ color: white;
484
+ padding: 2px 6px;
485
+ border-radius: 10px;
486
+ }
487
+ .hs-hierarchy-item .holon-id {
488
+ font-family: monospace;
489
+ font-size: 12px;
490
+ color: #666;
491
+ flex: 1;
492
+ overflow: hidden;
493
+ text-overflow: ellipsis;
494
+ }
495
+ .hs-hierarchy-item .data-count {
496
+ font-size: 11px;
497
+ color: #999;
498
+ }
499
+ .hs-hierarchy-section {
500
+ margin-bottom: 16px;
501
+ }
502
+ .hs-hierarchy-section-title {
503
+ font-size: 11px;
504
+ text-transform: uppercase;
505
+ color: #999;
506
+ margin-bottom: 8px;
507
+ padding-left: 4px;
508
+ }
509
+
510
+ .hs-lenses-list {
511
+ flex: 1;
512
+ overflow-y: auto;
513
+ padding: 16px;
514
+ }
515
+ .hs-lens-item {
516
+ background: white;
517
+ border-radius: 8px;
518
+ padding: 12px;
519
+ margin-bottom: 8px;
520
+ border: 1px solid #e0e0e0;
521
+ display: flex;
522
+ justify-content: space-between;
523
+ align-items: center;
524
+ }
525
+ .hs-lens-name {
526
+ font-weight: 500;
527
+ color: #333;
528
+ }
529
+ .hs-lens-count {
530
+ font-size: 12px;
531
+ color: #999;
532
+ }
533
+
534
+ .hs-widget-footer {
535
+ padding: 12px 24px;
536
+ background: #f8f9fa;
537
+ border-top: 1px solid #e0e0e0;
538
+ display: flex;
539
+ justify-content: space-between;
540
+ align-items: center;
541
+ font-size: 12px;
542
+ }
543
+ .hs-status {
544
+ display: flex;
545
+ align-items: center;
546
+ gap: 8px;
547
+ }
548
+ .hs-status-dot {
549
+ width: 8px;
550
+ height: 8px;
551
+ border-radius: 50%;
552
+ background: #ffc107;
553
+ }
554
+ .hs-status-dot.connected {
555
+ background: #28a745;
556
+ animation: pulse 2s infinite;
557
+ }
558
+ @keyframes pulse {
559
+ 0%, 100% { opacity: 1; }
560
+ 50% { opacity: 0.5; }
561
+ }
562
+ .hs-metrics {
563
+ color: #666;
564
+ }
565
+
566
+ /* Write Modal */
567
+ .hs-write-modal {
568
+ position: fixed;
569
+ top: 0;
570
+ left: 0;
571
+ width: 100%;
572
+ height: 100%;
573
+ background: rgba(0,0,0,0.5);
574
+ display: none;
575
+ align-items: center;
576
+ justify-content: center;
577
+ z-index: 1000000;
578
+ }
579
+ .hs-write-modal.open {
580
+ display: flex;
581
+ }
582
+ .hs-write-container {
583
+ background: white;
584
+ border-radius: 12px;
585
+ padding: 24px;
586
+ width: 90%;
587
+ max-width: 500px;
588
+ }
589
+ .hs-write-container h3 {
590
+ margin: 0 0 20px;
591
+ color: #333;
592
+ }
593
+ .hs-form-group {
594
+ margin-bottom: 16px;
595
+ }
596
+ .hs-form-group label {
597
+ display: block;
598
+ font-size: 13px;
599
+ color: #666;
600
+ margin-bottom: 6px;
601
+ }
602
+ .hs-form-group input,
603
+ .hs-form-group textarea {
604
+ width: 100%;
605
+ padding: 10px 12px;
606
+ border: 1px solid #ddd;
607
+ border-radius: 6px;
608
+ font-size: 14px;
609
+ box-sizing: border-box;
610
+ }
611
+ .hs-form-group textarea {
612
+ min-height: 120px;
613
+ font-family: monospace;
614
+ resize: vertical;
615
+ }
616
+ .hs-form-group input:focus,
617
+ .hs-form-group textarea:focus {
618
+ outline: none;
619
+ border-color: #667eea;
620
+ }
621
+ .hs-form-actions {
622
+ display: flex;
623
+ gap: 12px;
624
+ margin-top: 20px;
625
+ }
626
+ .hs-form-actions button {
627
+ flex: 1;
628
+ }
629
+
630
+ /* Responsive */
631
+ @media (max-width: 900px) {
632
+ .hs-widget-body {
633
+ flex-direction: column;
634
+ }
635
+ .hs-explorer-panel {
636
+ width: 100%;
637
+ height: 50%;
638
+ }
639
+ .hs-map-panel {
640
+ border-right: none;
641
+ border-bottom: 1px solid #e0e0e0;
642
+ }
643
+ }
644
+ `;
645
+
646
+ // Initialize widget
647
+ function init(options = {}) {
648
+ config = Object.assign({
649
+ appName: 'holosphere-widget',
650
+ relays: [],
651
+ position: 'bottom-right',
652
+ theme: 'default',
653
+ defaultLocation: [37.7749, -122.4194],
654
+ defaultResolution: 9
655
+ }, options);
656
+
657
+ currentResolution = config.defaultResolution;
658
+
659
+ // Inject styles
660
+ const style = document.createElement('style');
661
+ style.textContent = widgetCSS;
662
+ document.head.appendChild(style);
663
+
664
+ // Inject Leaflet CSS if not present
665
+ if (!document.querySelector('link[href*="leaflet"]')) {
666
+ const leafletCSS = document.createElement('link');
667
+ leafletCSS.rel = 'stylesheet';
668
+ leafletCSS.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
669
+ document.head.appendChild(leafletCSS);
670
+ }
671
+
672
+ // Inject Leaflet JS if not present
673
+ if (!window.L) {
674
+ const leafletJS = document.createElement('script');
675
+ leafletJS.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
676
+ leafletJS.onload = () => setupWidget();
677
+ document.body.appendChild(leafletJS);
678
+ } else {
679
+ setupWidget();
680
+ }
681
+ }
682
+
683
+ function setupWidget() {
684
+ // Inject HTML
685
+ const container = document.createElement('div');
686
+ container.id = 'hs-widget-root';
687
+ container.innerHTML = widgetHTML;
688
+ document.body.appendChild(container);
689
+
690
+ // Initialize HoloSphere
691
+ if (typeof HoloSphere === 'undefined') {
692
+ console.error('HoloSphere not found. Include holosphere.min.js before the widget.');
693
+ return;
694
+ }
695
+
696
+ hs = new HoloSphere({
697
+ appName: config.appName,
698
+ relays: config.relays
699
+ });
700
+
701
+ // Store globally for debugging
702
+ window.hsWidget = { hs, map, config };
703
+
704
+ // Setup event listeners
705
+ setupEventListeners();
706
+
707
+ // Initialize map
708
+ initMap();
709
+
710
+ // Update status
711
+ updateStatus('connected', 'Ready');
712
+ }
713
+
714
+ function setupEventListeners() {
715
+ // Toggle button
716
+ document.getElementById('hs-widget-button').addEventListener('click', toggleModal);
717
+
718
+ // Close button
719
+ document.getElementById('hs-close-btn').addEventListener('click', closeModal);
720
+
721
+ // Close on backdrop click
722
+ document.getElementById('hs-widget-modal').addEventListener('click', (e) => {
723
+ if (e.target.id === 'hs-widget-modal') closeModal();
724
+ });
725
+
726
+ // Resolution slider
727
+ document.getElementById('hs-resolution-slider').addEventListener('input', (e) => {
728
+ currentResolution = parseInt(e.target.value);
729
+ document.getElementById('hs-resolution-value').textContent = currentResolution;
730
+ if (currentHolon) {
731
+ const center = map.getCenter();
732
+ selectHolonAt(center.lat, center.lng);
733
+ }
734
+ });
735
+
736
+ // Tabs
737
+ document.querySelectorAll('.hs-tab').forEach(tab => {
738
+ tab.addEventListener('click', () => {
739
+ document.querySelectorAll('.hs-tab').forEach(t => t.classList.remove('active'));
740
+ document.querySelectorAll('.hs-tab-content').forEach(c => c.classList.remove('active'));
741
+ tab.classList.add('active');
742
+ document.getElementById(`hs-tab-${tab.dataset.tab}`).classList.add('active');
743
+ });
744
+ });
745
+
746
+ // Lens selector
747
+ document.getElementById('hs-lens-select').addEventListener('change', loadLensData);
748
+
749
+ // Add lens button
750
+ document.getElementById('hs-add-lens-btn').addEventListener('click', () => {
751
+ const lens = prompt('Enter new lens name:');
752
+ if (lens) {
753
+ addLensOption(lens);
754
+ document.getElementById('hs-lens-select').value = lens;
755
+ loadLensData();
756
+ }
757
+ });
758
+
759
+ // Write button
760
+ document.getElementById('hs-write-btn').addEventListener('click', openWriteModal);
761
+ document.getElementById('hs-write-cancel').addEventListener('click', closeWriteModal);
762
+ document.getElementById('hs-write-submit').addEventListener('click', submitWriteData);
763
+
764
+ // Keyboard shortcuts
765
+ document.addEventListener('keydown', (e) => {
766
+ if (e.key === 'Escape' && isOpen) closeModal();
767
+ });
768
+ }
769
+
770
+ function initMap() {
771
+ map = L.map('hs-map').setView(config.defaultLocation, resolutionToZoom[currentResolution]);
772
+
773
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
774
+ attribution: '&copy; OpenStreetMap contributors'
775
+ }).addTo(map);
776
+
777
+ // Click handler
778
+ map.on('click', (e) => {
779
+ selectHolonAt(e.latlng.lat, e.latlng.lng);
780
+ });
781
+
782
+ // Zoom handler
783
+ map.on('zoomend', () => {
784
+ const zoom = map.getZoom();
785
+ const newRes = getResolutionForZoom(zoom);
786
+ if (newRes !== currentResolution) {
787
+ currentResolution = newRes;
788
+ document.getElementById('hs-resolution-slider').value = newRes;
789
+ document.getElementById('hs-resolution-value').textContent = newRes;
790
+ if (currentHolon) {
791
+ const center = map.getCenter();
792
+ selectHolonAt(center.lat, center.lng);
793
+ }
794
+ }
795
+ });
796
+
797
+ // Mousemove for coordinates
798
+ map.on('mousemove', (e) => {
799
+ const lat = e.latlng.lat.toFixed(6);
800
+ const lng = e.latlng.lng.toFixed(6);
801
+ document.getElementById('hs-coords').innerHTML =
802
+ `Lat: ${lat}, Lng: ${lng} | Resolution: ${currentResolution}`;
803
+ });
804
+ }
805
+
806
+ function getResolutionForZoom(zoom) {
807
+ // Find closest resolution for zoom level
808
+ let closest = 9;
809
+ let minDiff = Infinity;
810
+ for (const [res, z] of Object.entries(resolutionToZoom)) {
811
+ const diff = Math.abs(z - zoom);
812
+ if (diff < minDiff) {
813
+ minDiff = diff;
814
+ closest = parseInt(res);
815
+ }
816
+ }
817
+ return Math.min(15, Math.max(0, closest));
818
+ }
819
+
820
+ async function selectHolonAt(lat, lng) {
821
+ try {
822
+ const holon = await hs.toHolon(lat, lng, currentResolution);
823
+ currentHolon = holon;
824
+
825
+ // Update UI
826
+ document.getElementById('hs-current-holon').textContent = holon;
827
+ document.getElementById('hs-write-btn').disabled = false;
828
+
829
+ // Draw hexagon on map
830
+ await drawHexagon(holon);
831
+
832
+ // Load hierarchy
833
+ await loadHierarchy(holon);
834
+
835
+ // Load available lenses
836
+ await loadLenses(holon);
837
+
838
+ // Update coordinates display
839
+ document.getElementById('hs-coords').innerHTML =
840
+ `<strong>Holon:</strong> ${holon.substring(0, 12)}... | Resolution: ${currentResolution}`;
841
+
842
+ } catch (error) {
843
+ console.error('Error selecting holon:', error);
844
+ updateStatus('error', error.message);
845
+ }
846
+ }
847
+
848
+ async function drawHexagon(holon) {
849
+ // Remove previous selection
850
+ if (selectedHexLayer) {
851
+ map.removeLayer(selectedHexLayer);
852
+ }
853
+
854
+ // Get hexagon boundary using h3-js
855
+ // The HoloSphere CDN bundle exposes h3 as window.h3
856
+ try {
857
+ const h3 = window.h3;
858
+
859
+ if (h3 && h3.cellToBoundary) {
860
+ const boundary = h3.cellToBoundary(holon);
861
+ const latLngs = boundary.map(([lat, lng]) => [lat, lng]);
862
+
863
+ selectedHexLayer = L.polygon(latLngs, {
864
+ color: colors.strokeSelected,
865
+ weight: 3,
866
+ fillColor: colors.selected,
867
+ fillOpacity: 0.5
868
+ }).addTo(map);
869
+
870
+ // Add popup with holon info
871
+ selectedHexLayer.bindPopup(`
872
+ <strong>Holon:</strong> ${holon}<br>
873
+ <strong>Resolution:</strong> ${h3.getResolution(holon)}<br>
874
+ <strong>Area:</strong> ${(h3.cellArea(holon, 'km2')).toFixed(2)} km&sup2;
875
+ `);
876
+
877
+ // Fit map to hexagon with animation
878
+ map.fitBounds(selectedHexLayer.getBounds(), {
879
+ padding: [50, 50],
880
+ animate: true,
881
+ duration: 0.5
882
+ });
883
+
884
+ // Also draw neighboring hexagons (ring 1) for context
885
+ drawNeighborHexagons(holon, 1);
886
+ } else {
887
+ // Fallback: draw a circle at the location
888
+ const center = map.getCenter();
889
+ selectedHexLayer = L.circleMarker([center.lat, center.lng], {
890
+ radius: 20,
891
+ color: colors.strokeSelected,
892
+ fillColor: colors.selected,
893
+ fillOpacity: 0.5
894
+ }).addTo(map);
895
+ }
896
+ } catch (e) {
897
+ console.warn('Could not draw hexagon boundary:', e);
898
+ }
899
+ }
900
+
901
+ function drawNeighborHexagons(centerHolon, ring) {
902
+ // Clear previous neighbor layers
903
+ Object.values(hexagonLayers).forEach(layer => {
904
+ if (layer !== selectedHexLayer) map.removeLayer(layer);
905
+ });
906
+ hexagonLayers = {};
907
+
908
+ const h3 = window.h3;
909
+ if (!h3 || !h3.gridDisk) return;
910
+
911
+ try {
912
+ const neighbors = h3.gridDisk(centerHolon, ring);
913
+
914
+ neighbors.forEach(neighbor => {
915
+ if (neighbor === centerHolon) return; // Skip center
916
+
917
+ const boundary = h3.cellToBoundary(neighbor);
918
+ const latLngs = boundary.map(([lat, lng]) => [lat, lng]);
919
+
920
+ const layer = L.polygon(latLngs, {
921
+ color: colors.stroke,
922
+ weight: 1,
923
+ fillColor: colors.empty,
924
+ fillOpacity: 0.3,
925
+ className: 'hs-neighbor-hex'
926
+ }).addTo(map);
927
+
928
+ // Make neighbors clickable
929
+ layer.on('click', () => {
930
+ selectHolonAt(...h3.cellToLatLng(neighbor));
931
+ });
932
+
933
+ layer.on('mouseover', () => {
934
+ layer.setStyle({ fillColor: colors.hover, fillOpacity: 0.5 });
935
+ });
936
+
937
+ layer.on('mouseout', () => {
938
+ layer.setStyle({ fillColor: colors.empty, fillOpacity: 0.3 });
939
+ });
940
+
941
+ hexagonLayers[neighbor] = layer;
942
+ });
943
+ } catch (e) {
944
+ console.warn('Could not draw neighbor hexagons:', e);
945
+ }
946
+ }
947
+
948
+ async function loadHierarchy(holon) {
949
+ const container = document.getElementById('hs-hierarchy');
950
+ container.innerHTML = '<div class="hs-empty-state">Loading hierarchy...</div>';
951
+
952
+ try {
953
+ // Get parents
954
+ const parents = await hs.getParents(holon, 0);
955
+
956
+ // Get children (only if resolution allows)
957
+ let children = [];
958
+ const res = await getHolonResolution(holon);
959
+ if (res < 15) {
960
+ children = await hs.getChildren(holon);
961
+ }
962
+
963
+ let html = '';
964
+
965
+ // Parents section
966
+ if (parents && parents.length > 0) {
967
+ html += '<div class="hs-hierarchy-section">';
968
+ html += '<div class="hs-hierarchy-section-title">Parent Holons (Larger Areas)</div>';
969
+ for (const parent of parents.reverse()) {
970
+ const pRes = await getHolonResolution(parent);
971
+ html += `
972
+ <div class="hs-hierarchy-item" data-holon="${parent}">
973
+ <span class="resolution">${pRes}</span>
974
+ <span class="holon-id">${parent}</span>
975
+ </div>
976
+ `;
977
+ }
978
+ html += '</div>';
979
+ }
980
+
981
+ // Current holon
982
+ html += '<div class="hs-hierarchy-section">';
983
+ html += '<div class="hs-hierarchy-section-title">Current Holon</div>';
984
+ html += `
985
+ <div class="hs-hierarchy-item current">
986
+ <span class="resolution">${res}</span>
987
+ <span class="holon-id">${holon}</span>
988
+ </div>
989
+ `;
990
+ html += '</div>';
991
+
992
+ // Children section
993
+ if (children && children.length > 0) {
994
+ html += '<div class="hs-hierarchy-section">';
995
+ html += '<div class="hs-hierarchy-section-title">Child Holons (Smaller Areas)</div>';
996
+ const childRes = res + 1;
997
+ for (const child of children.slice(0, 7)) { // Show first 7
998
+ html += `
999
+ <div class="hs-hierarchy-item" data-holon="${child}">
1000
+ <span class="resolution">${childRes}</span>
1001
+ <span class="holon-id">${child}</span>
1002
+ </div>
1003
+ `;
1004
+ }
1005
+ if (children.length > 7) {
1006
+ html += `<div class="hs-empty-state">+${children.length - 7} more children</div>`;
1007
+ }
1008
+ html += '</div>';
1009
+ }
1010
+
1011
+ container.innerHTML = html;
1012
+
1013
+ // Add click handlers
1014
+ container.querySelectorAll('.hs-hierarchy-item:not(.current)').forEach(item => {
1015
+ item.addEventListener('click', async () => {
1016
+ const targetHolon = item.dataset.holon;
1017
+ const targetRes = await getHolonResolution(targetHolon);
1018
+ currentResolution = targetRes;
1019
+ document.getElementById('hs-resolution-slider').value = targetRes;
1020
+ document.getElementById('hs-resolution-value').textContent = targetRes;
1021
+
1022
+ currentHolon = targetHolon;
1023
+ document.getElementById('hs-current-holon').textContent = targetHolon;
1024
+
1025
+ await drawHexagon(targetHolon);
1026
+ await loadHierarchy(targetHolon);
1027
+ await loadLenses(targetHolon);
1028
+ });
1029
+ });
1030
+
1031
+ } catch (error) {
1032
+ container.innerHTML = `<div class="hs-empty-state">Error: ${error.message}</div>`;
1033
+ }
1034
+ }
1035
+
1036
+ async function getHolonResolution(holon) {
1037
+ // Get resolution from h3-js (exposed as window.h3 by the CDN bundle)
1038
+ const h3 = window.h3;
1039
+ if (h3 && h3.getResolution) {
1040
+ return h3.getResolution(holon);
1041
+ }
1042
+ // Fallback: estimate from holon ID length (not accurate but works)
1043
+ return Math.min(15, Math.max(0, holon.length - 8));
1044
+ }
1045
+
1046
+ async function loadLenses(holon) {
1047
+ const container = document.getElementById('hs-lenses-list');
1048
+ const select = document.getElementById('hs-lens-select');
1049
+
1050
+ // Default lenses
1051
+ const defaultLenses = ['notes', 'events', 'places', 'resources', 'projects'];
1052
+
1053
+ // Clear and populate select
1054
+ select.innerHTML = '<option value="">Select lens...</option>';
1055
+ defaultLenses.forEach(lens => addLensOption(lens));
1056
+
1057
+ // Update lenses list
1058
+ let html = '';
1059
+ for (const lens of defaultLenses) {
1060
+ try {
1061
+ const data = await hs.getAll(holon, lens);
1062
+ const count = data ? data.length : 0;
1063
+ html += `
1064
+ <div class="hs-lens-item" data-lens="${lens}">
1065
+ <span class="hs-lens-name">${lens}</span>
1066
+ <span class="hs-lens-count">${count} items</span>
1067
+ </div>
1068
+ `;
1069
+ } catch (e) {
1070
+ html += `
1071
+ <div class="hs-lens-item" data-lens="${lens}">
1072
+ <span class="hs-lens-name">${lens}</span>
1073
+ <span class="hs-lens-count">0 items</span>
1074
+ </div>
1075
+ `;
1076
+ }
1077
+ }
1078
+
1079
+ container.innerHTML = html || '<div class="hs-empty-state">No data in this holon</div>';
1080
+
1081
+ // Click handlers
1082
+ container.querySelectorAll('.hs-lens-item').forEach(item => {
1083
+ item.addEventListener('click', () => {
1084
+ select.value = item.dataset.lens;
1085
+ loadLensData();
1086
+ document.querySelector('.hs-tab[data-tab="data"]').click();
1087
+ });
1088
+ });
1089
+ }
1090
+
1091
+ function addLensOption(lens) {
1092
+ const select = document.getElementById('hs-lens-select');
1093
+ if (!select.querySelector(`option[value="${lens}"]`)) {
1094
+ const option = document.createElement('option');
1095
+ option.value = lens;
1096
+ option.textContent = lens;
1097
+ select.appendChild(option);
1098
+ }
1099
+ }
1100
+
1101
+ async function loadLensData() {
1102
+ const lens = document.getElementById('hs-lens-select').value;
1103
+ const container = document.getElementById('hs-data-list');
1104
+
1105
+ if (!lens || !currentHolon) {
1106
+ container.innerHTML = '<div class="hs-empty-state">Select a lens to view data</div>';
1107
+ return;
1108
+ }
1109
+
1110
+ container.innerHTML = '<div class="hs-empty-state">Loading...</div>';
1111
+
1112
+ try {
1113
+ const data = await hs.getAll(currentHolon, lens);
1114
+
1115
+ if (!data || data.length === 0) {
1116
+ container.innerHTML = '<div class="hs-empty-state">No data in this lens</div>';
1117
+ return;
1118
+ }
1119
+
1120
+ let html = '';
1121
+ for (const item of data) {
1122
+ const title = item.title || item.name || item.id || 'Untitled';
1123
+ const preview = JSON.stringify(item, null, 2).substring(0, 150);
1124
+ const meta = item._meta ? new Date(item._meta.timestamp).toLocaleString() : '';
1125
+
1126
+ html += `
1127
+ <div class="hs-data-item" data-id="${item.id}">
1128
+ <div class="hs-data-item-title">${escapeHtml(title)}</div>
1129
+ <div class="hs-data-item-meta">${meta}</div>
1130
+ <div class="hs-data-item-preview"><code>${escapeHtml(preview)}...</code></div>
1131
+ </div>
1132
+ `;
1133
+ }
1134
+
1135
+ container.innerHTML = html;
1136
+
1137
+ // Click handlers
1138
+ container.querySelectorAll('.hs-data-item').forEach(item => {
1139
+ item.addEventListener('click', () => {
1140
+ const id = item.dataset.id;
1141
+ const dataItem = data.find(d => d.id === id);
1142
+ if (dataItem) {
1143
+ alert(JSON.stringify(dataItem, null, 2));
1144
+ }
1145
+ });
1146
+ });
1147
+
1148
+ } catch (error) {
1149
+ container.innerHTML = `<div class="hs-empty-state">Error: ${error.message}</div>`;
1150
+ }
1151
+ }
1152
+
1153
+ function openWriteModal() {
1154
+ document.getElementById('hs-write-holon').value = currentHolon;
1155
+ document.getElementById('hs-write-lens').value = document.getElementById('hs-lens-select').value || '';
1156
+ document.getElementById('hs-write-data').value = '{\n "title": "",\n "description": ""\n}';
1157
+ document.getElementById('hs-write-modal').classList.add('open');
1158
+ }
1159
+
1160
+ function closeWriteModal() {
1161
+ document.getElementById('hs-write-modal').classList.remove('open');
1162
+ }
1163
+
1164
+ async function submitWriteData() {
1165
+ const holon = document.getElementById('hs-write-holon').value;
1166
+ const lens = document.getElementById('hs-write-lens').value;
1167
+ const dataStr = document.getElementById('hs-write-data').value;
1168
+
1169
+ if (!holon || !lens) {
1170
+ alert('Please enter holon and lens');
1171
+ return;
1172
+ }
1173
+
1174
+ try {
1175
+ const data = JSON.parse(dataStr);
1176
+ await hs.write(holon, lens, data);
1177
+
1178
+ closeWriteModal();
1179
+ addLensOption(lens);
1180
+ document.getElementById('hs-lens-select').value = lens;
1181
+ loadLensData();
1182
+ loadLenses(holon);
1183
+
1184
+ updateStatus('connected', 'Data written successfully');
1185
+ } catch (error) {
1186
+ alert('Error: ' + error.message);
1187
+ }
1188
+ }
1189
+
1190
+ function toggleModal() {
1191
+ if (isOpen) {
1192
+ closeModal();
1193
+ } else {
1194
+ openModal();
1195
+ }
1196
+ }
1197
+
1198
+ function openModal() {
1199
+ document.getElementById('hs-widget-modal').classList.add('open');
1200
+ document.getElementById('hs-widget-button').classList.add('active');
1201
+ isOpen = true;
1202
+
1203
+ // Trigger map resize after modal opens
1204
+ setTimeout(() => {
1205
+ if (map) map.invalidateSize();
1206
+ }, 300);
1207
+ }
1208
+
1209
+ function closeModal() {
1210
+ document.getElementById('hs-widget-modal').classList.remove('open');
1211
+ document.getElementById('hs-widget-button').classList.remove('active');
1212
+ isOpen = false;
1213
+ }
1214
+
1215
+ function updateStatus(type, message) {
1216
+ const dot = document.querySelector('.hs-status-dot');
1217
+ const text = document.getElementById('hs-status-text');
1218
+
1219
+ dot.classList.remove('connected', 'error');
1220
+ if (type === 'connected') dot.classList.add('connected');
1221
+ text.textContent = message;
1222
+ }
1223
+
1224
+ function escapeHtml(str) {
1225
+ const div = document.createElement('div');
1226
+ div.textContent = str;
1227
+ return div.innerHTML;
1228
+ }
1229
+
1230
+ // Public API
1231
+ global.HoloSphereWidget = {
1232
+ init,
1233
+ open: openModal,
1234
+ close: closeModal,
1235
+ toggle: toggleModal,
1236
+ selectHolon: selectHolonAt,
1237
+ getHoloSphere: () => hs,
1238
+ getMap: () => map,
1239
+ getCurrentHolon: () => currentHolon
1240
+ };
1241
+
1242
+ })(typeof window !== 'undefined' ? window : this);