From 4f8cddaaf776b58174cc92f98313e59d786b7599 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 11 Apr 2026 12:41:35 -0400 Subject: [PATCH] feat(holons): add Holon Explorer canvas shape with hex hierarchy + appreciation New folk-holon-explorer shape unifying H3 geospatial holons and nested rSpace spaces into a zoomable circular navigator with appreciation weight normalization and MetatronGrid sacred geometry background. Endorsements logged to trust engine via new POST /api/trust/endorse endpoint. Co-Authored-By: Claude Opus 4.6 --- lib/folk-holon-explorer.ts | 592 +++++++++++++++++++++++++++++++++++++ lib/index.ts | 1 + modules/rnetwork/mod.ts | 19 ++ src/encryptid/server.ts | 30 ++ website/canvas.html | 10 +- 5 files changed, 651 insertions(+), 1 deletion(-) create mode 100644 lib/folk-holon-explorer.ts diff --git a/lib/folk-holon-explorer.ts b/lib/folk-holon-explorer.ts new file mode 100644 index 00000000..82a2c780 --- /dev/null +++ b/lib/folk-holon-explorer.ts @@ -0,0 +1,592 @@ +/** + * folk-holon-explorer — Zoomable hexagonal hierarchy navigator. + * + * Dual navigation: + * - H3 Mode: Navigate H3 resolution levels via h3-js hierarchy + * - Space Mode: Navigate nested rSpace spaces via /api/spaces/:slug/nest + * + * Features: + * - Circular SVG layout (Holons/Zircle arc placement) + * - MetatronGrid sacred geometry background + * - Appreciation weight normalization (sum ≤ 100%) + * - Endorsement logging to trust engine on weight save + */ + +import * as h3 from 'h3-js'; +import { FolkShape } from './folk-shape'; +import { css, html } from './tags'; +import { getHolonHierarchy, getResolutionName } from './holon-service'; + +// ── Types ── + +interface ExplorerNode { + id: string; + label: string; + weight: number; + ownerDID?: string; +} + +type NavMode = 'h3' | 'space'; + +// ── Appreciation normalization (from Holons project) ── + +function normalize(values: number[], changedIndex: number): number[] { + const total = values.reduce((s, v) => s + v, 0); + if (total <= 100) return values; + const excess = total - 100; + const othersSum = total - values[changedIndex]; + if (othersSum === 0) return values.map((v, i) => i === changedIndex ? 100 : 0); + return values.map((v, i) => + i === changedIndex ? v : Math.max(0, v - excess * v / othersSum) + ); +} + +// ── MetatronGrid SVG ── + +function metatronGrid(size: number): string { + const cx = size / 2, cy = size / 2, r = size * 0.42, ri = r * 0.5; + const hex = (radius: number) => { + const pts: string[] = []; + for (let i = 0; i < 6; i++) { + const a = (Math.PI / 3) * i - Math.PI / 2; + pts.push(`${cx + radius * Math.cos(a)},${cy + radius * Math.sin(a)}`); + } + return pts.join(' '); + }; + // Star of David: two overlapping triangles + const tri = (offset: number) => { + const pts: string[] = []; + for (let i = 0; i < 3; i++) { + const a = (2 * Math.PI / 3) * i + offset - Math.PI / 2; + pts.push(`${cx + r * Math.cos(a)},${cy + r * Math.sin(a)}`); + } + return pts.join(' '); + }; + return ` + + +`; +} + +// ── Styles ── + +const styles = css` + :host { + background: #0f172a; + color: #e2e8f0; + border-radius: 8px; + box-shadow: 0 2px 12px rgba(0,0,0,0.3); + min-width: 420px; + min-height: 400px; + } + .header { + display: flex; align-items: center; justify-content: space-between; + padding: 8px 12px; background: #0e7490; color: white; + border-radius: 8px 8px 0 0; font-size: 12px; font-weight: 600; cursor: move; + } + .header-left { display: flex; align-items: center; gap: 6px; min-width: 0; } + .header-left span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .header-right { display: flex; gap: 4px; flex-shrink: 0; } + .header-right button, .mode-toggle { + background: transparent; border: none; color: white; cursor: pointer; + padding: 2px 6px; border-radius: 4px; font-size: 12px; + } + .header-right button:hover, .mode-toggle:hover { background: rgba(255,255,255,0.2); } + .mode-toggle.active { background: rgba(255,255,255,0.25); font-weight: 700; } + .breadcrumb { + display: flex; gap: 4px; padding: 6px 12px; font-size: 11px; + color: #94a3b8; background: #1e293b; flex-wrap: wrap; align-items: center; + } + .breadcrumb span { cursor: pointer; } + .breadcrumb span:hover { color: #06b6d4; text-decoration: underline; } + .breadcrumb .sep { color: #475569; cursor: default; } + .breadcrumb .sep:hover { color: #475569; text-decoration: none; } + .breadcrumb .current { color: #e2e8f0; font-weight: 600; cursor: default; } + .breadcrumb .current:hover { color: #e2e8f0; text-decoration: none; } + .content { + display: flex; flex-direction: column; overflow: hidden; + height: calc(100% - 66px); /* header + breadcrumb */ + } + .svg-wrap { flex: 1; min-height: 0; position: relative; } + .svg-wrap svg { width: 100%; height: 100%; } + .slider-panel { + padding: 8px 12px; max-height: 140px; overflow-y: auto; + border-top: 1px solid rgba(255,255,255,0.08); + } + .slider-row { + display: flex; align-items: center; gap: 8px; font-size: 11px; margin-bottom: 4px; + } + .slider-row label { width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #94a3b8; } + .slider-row input[type=range] { flex: 1; accent-color: #10b981; } + .slider-row .val { width: 30px; text-align: right; color: #10b981; font-weight: 600; } + .sum-row { font-size: 11px; color: #64748b; text-align: right; padding: 2px 0; } + .connect-form { + display: flex; flex-direction: column; align-items: center; + justify-content: center; height: 100%; gap: 12px; padding: 16px; text-align: center; + } + .connect-form input { + padding: 8px 12px; border: 1px solid rgba(255,255,255,0.1); + border-radius: 6px; font-size: 13px; font-family: monospace; + background: #1e293b; color: #e2e8f0; outline: none; width: 100%; max-width: 320px; + } + .connect-form input:focus { border-color: #06b6d4; box-shadow: 0 0 0 2px rgba(6,182,212,0.2); } + .connect-form button { + padding: 8px 20px; background: #0e7490; color: white; border: none; + border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600; + } + .connect-form button:hover { background: #0891b2; } + .hint { font-size: 12px; color: #64748b; } + .empty { display: flex; align-items: center; justify-content: center; height: 100%; color: #64748b; font-size: 13px; } + .more-label { font-size: 10px; fill: #64748b; } +`; + +declare global { + interface HTMLElementTagNameMap { + 'folk-holon-explorer': FolkHolonExplorer; + } +} + +export class FolkHolonExplorer extends FolkShape { + static override tagName = 'folk-holon-explorer'; + + static { + const sheet = new CSSStyleSheet(); + const parentRules = Array.from(FolkShape.styles.cssRules).map((r) => r.cssText).join('\n'); + const childRules = Array.from(styles.cssRules).map((r) => r.cssText).join('\n'); + sheet.replaceSync(`${parentRules}\n${childRules}`); + this.styles = sheet; + } + + // ── State ── + #mode: NavMode = 'space'; + #rootId = ''; + #spaceSlug = ''; + #breadcrumb: { id: string; label: string }[] = []; + #children: ExplorerNode[] = []; + #loading = false; + #contentEl: HTMLElement | null = null; + #saveTimer: ReturnType | null = null; + + override createRenderRoot() { + const root = super.createRenderRoot(); + + this.#mode = (this.getAttribute('mode') as NavMode) || 'space'; + this.#rootId = this.getAttribute('root-id') || ''; + this.#spaceSlug = this.getAttribute('space-slug') || (window as any).__rspaceSpace || ''; + + const wrapper = document.createElement('div'); + wrapper.style.cssText = 'width:100%;height:100%;display:flex;flex-direction:column;'; + wrapper.innerHTML = html` +
+
+ 🔮 + Holon Explorer +
+
+ + + +
+
+ +
+ `; + + const slot = root.querySelector('slot'); + const containerDiv = slot?.parentElement as HTMLElement; + if (containerDiv) containerDiv.replaceWith(wrapper); + + this.#contentEl = wrapper.querySelector('.content'); + + // Wire mode toggles + wrapper.querySelectorAll('.mode-toggle').forEach((btn) => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const m = (btn as HTMLElement).dataset.mode as NavMode; + if (m !== this.#mode) { + this.#mode = m; + this.#breadcrumb = []; + this.#children = []; + wrapper.querySelectorAll('.mode-toggle').forEach((b) => + b.classList.toggle('active', (b as HTMLElement).dataset.mode === m)); + this.#render(); + } + }); + }); + + wrapper.querySelector('.close-btn')!.addEventListener('click', (e) => { + e.stopPropagation(); + this.dispatchEvent(new CustomEvent('close')); + }); + + // Auto-load + if (this.#mode === 'space' && this.#spaceSlug) { + this.#breadcrumb = [{ id: this.#spaceSlug, label: this.#spaceSlug }]; + this.#loadSpaceChildren(this.#spaceSlug); + } else if (this.#mode === 'h3' && this.#rootId) { + this.#breadcrumb = [{ id: this.#rootId, label: getResolutionName(h3.getResolution(this.#rootId)) }]; + this.#loadH3Children(this.#rootId); + } + + this.#render(); + return root; + } + + disconnectedCallback() { + if (this.#saveTimer) clearTimeout(this.#saveTimer); + } + + // ── Data loading ── + + async #loadSpaceChildren(slug: string) { + this.#loading = true; + this.#render(); + try { + const res = await fetch(`/api/spaces/${encodeURIComponent(slug)}/nest`, { signal: AbortSignal.timeout(5000) }); + if (!res.ok) { this.#children = []; this.#loading = false; this.#render(); return; } + const data = await res.json(); + const nested: any[] = data.nestedSpaces || []; + this.#children = nested.map((s: any) => ({ + id: s.slug || s.id, + label: s.name || s.slug || s.id, + weight: 100 / Math.max(nested.length, 1), + ownerDID: s.ownerDID, + })); + this.#loadSavedWeights(); + } catch { this.#children = []; } + this.#loading = false; + this.#render(); + } + + #loadH3Children(cellId: string) { + this.#loading = true; + this.#render(); + try { + const { children } = getHolonHierarchy(cellId); + // H3 can produce many children — show first 12 + const shown = children.slice(0, 12); + this.#children = shown.map((c) => ({ + id: c, + label: `${getResolutionName(h3.getResolution(c))} ${c.slice(-6)}`, + weight: 100 / Math.max(shown.length, 1), + })); + this.#loadSavedWeights(); + } catch { this.#children = []; } + this.#loading = false; + this.#render(); + } + + #drillInto(node: ExplorerNode) { + this.#breadcrumb.push({ id: node.id, label: node.label }); + this.#children = []; + if (this.#mode === 'space') { + this.#loadSpaceChildren(node.id); + } else { + this.#loadH3Children(node.id); + } + } + + #navigateTo(index: number) { + if (index >= this.#breadcrumb.length - 1) return; + const target = this.#breadcrumb[index]; + this.#breadcrumb = this.#breadcrumb.slice(0, index + 1); + this.#children = []; + if (this.#mode === 'space') { + this.#loadSpaceChildren(target.id); + } else { + this.#loadH3Children(target.id); + } + } + + // ── Appreciation persistence ── + + #weightKey(): string { + const current = this.#breadcrumb[this.#breadcrumb.length - 1]; + return current ? `holon-explorer:weights:${current.id}` : ''; + } + + #loadSavedWeights() { + const key = this.#weightKey(); + if (!key) return; + try { + const raw = localStorage.getItem(key); + if (!raw) return; + const saved: Record = JSON.parse(raw); + for (const child of this.#children) { + if (saved[child.id] != null) child.weight = saved[child.id]; + } + } catch { /* ignore */ } + } + + #saveWeights() { + const key = this.#weightKey(); + if (!key) return; + const map: Record = {}; + for (const c of this.#children) map[c.id] = c.weight; + try { localStorage.setItem(key, JSON.stringify(map)); } catch { /* ignore */ } + } + + #scheduleEndorse() { + if (this.#saveTimer) clearTimeout(this.#saveTimer); + this.#saveTimer = setTimeout(() => this.#fireEndorsements(), 1500); + } + + async #fireEndorsements() { + const space = this.#spaceSlug || (window as any).__rspaceSpace; + const token = (window as any).__rspaceAuthToken; + if (!space || !token) return; + + for (const child of this.#children) { + if (!child.ownerDID) continue; + try { + await fetch(`/${encodeURIComponent(space)}/rnetwork/api/endorse`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, + body: JSON.stringify({ + targetDid: child.ownerDID, + authority: 'gov-ops', + weight: child.weight / 100, + space, + }), + }); + } catch { /* silent */ } + } + } + + // ── Rendering ── + + #render() { + if (!this.#contentEl) return; + + // Breadcrumb + const bc = this.renderRoot.querySelector('.breadcrumb') as HTMLElement; + if (bc) { + if (this.#breadcrumb.length === 0) { + bc.innerHTML = ''; + } else { + bc.innerHTML = this.#breadcrumb.map((b, i) => { + const isLast = i === this.#breadcrumb.length - 1; + const sep = i > 0 ? '' : ''; + return isLast + ? `${sep}${esc(b.label)}` + : `${sep}${esc(b.label)}`; + }).join(''); + bc.querySelectorAll('span[data-idx]').forEach((el) => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + this.#navigateTo(parseInt((el as HTMLElement).dataset.idx!, 10)); + }); + }); + } + } + + // Content + if (this.#mode === 'h3' && this.#breadcrumb.length === 0) { + this.#renderH3Form(); + } else if (this.#loading) { + this.#contentEl.innerHTML = '
Loading...
'; + } else if (this.#children.length === 0 && this.#breadcrumb.length > 0) { + this.#contentEl.innerHTML = '
No children found
'; + } else if (this.#children.length === 0) { + this.#contentEl.innerHTML = '
Enter a space or H3 cell to explore
'; + } else { + this.#renderExplorer(); + } + } + + #renderH3Form() { + this.#contentEl!.innerHTML = html` +
+
+
H3 Hex Explorer
+
Enter an H3 cell ID to explore its hierarchy
+ + +
+ `; + const input = this.#contentEl!.querySelector('.h3-input') as HTMLInputElement; + const btn = this.#contentEl!.querySelector('.go-btn') as HTMLButtonElement; + const go = () => { + const val = input.value.trim(); + if (!val) return; + try { + if (!h3.isValidCell(val)) return; + this.#rootId = val; + this.#breadcrumb = [{ id: val, label: getResolutionName(h3.getResolution(val)) }]; + this.#loadH3Children(val); + } catch { /* invalid */ } + }; + input.addEventListener('pointerdown', (e) => e.stopPropagation()); + input.addEventListener('keydown', (e) => { e.stopPropagation(); if (e.key === 'Enter') go(); }); + btn.addEventListener('click', (e) => { e.stopPropagation(); go(); }); + } + + #renderExplorer() { + const N = this.#children.length; + const MAX_VISIBLE = 12; + const visible = this.#children.slice(0, MAX_VISIBLE); + const overflow = N > MAX_VISIBLE ? N - MAX_VISIBLE : 0; + + // SVG dimensions (use fixed viewBox, scales with container) + const W = 400, H = 300; + const cx = W / 2, cy = H / 2; + let orbit = Math.min(W, H) * 0.35; + if (N > 8) orbit *= 0.85; + + // MetatronGrid + const gridSvg = metatronGrid(Math.min(W, H)); + + // Build node positions + const nodesSvg: string[] = []; + const linesSvg: string[] = []; + + for (let i = 0; i < visible.length; i++) { + const child = visible[i]; + const angle = -110 + (220 / (visible.length + 1)) * (i + 1); + const rad = (angle - 90) * Math.PI / 180; + const nx = cx + orbit * Math.cos(rad); + const ny = cy + orbit * Math.sin(rad); + const r = Math.min(36, Math.max(16, 16 + child.weight * 0.3)); + + // Connection line + linesSvg.push( + `` + ); + + // Node circle + const fill = `rgba(16,185,129,${(0.1 + child.weight * 0.005).toFixed(3)})`; + nodesSvg.push(` + + + ${esc(child.label.length > 14 ? child.label.slice(0, 12) + '…' : child.label)} + + `); + } + + // Center node + const centerSvg = ` + + ${esc(this.#breadcrumb[this.#breadcrumb.length - 1]?.label.slice(0, 8) || '?')} + `; + + // Overflow indicator + const overflowSvg = overflow > 0 + ? `+${overflow} more` + : ''; + + // Slider panel + const sliders = visible.map((child, i) => ` +
+ + + ${Math.round(child.weight)} +
+ `).join(''); + + const sum = this.#children.reduce((s, c) => s + c.weight, 0); + + this.#contentEl!.innerHTML = html` +
+ + ${gridSvg} + ${linesSvg.join('')} + ${centerSvg} + ${nodesSvg.join('')} + ${overflowSvg} + +
+
+ ${sliders} +
Total: ${Math.round(sum)}%
+
+ `; + + // Wire node clicks + this.#contentEl!.querySelectorAll('.node').forEach((g) => { + g.addEventListener('click', (e) => { + e.stopPropagation(); + const idx = parseInt((g as HTMLElement).dataset.idx!, 10); + if (idx >= 0 && idx < visible.length) this.#drillInto(visible[idx]); + }); + }); + + // Wire sliders + this.#contentEl!.querySelectorAll('input[type=range]').forEach((el) => { + el.addEventListener('pointerdown', (e) => e.stopPropagation()); + el.addEventListener('input', (e) => { + e.stopPropagation(); + const idx = parseInt((el as HTMLElement).dataset.idx!, 10); + const val = parseFloat((el as HTMLInputElement).value); + const weights = this.#children.map((c) => c.weight); + weights[idx] = val; + const normalized = normalize(weights, idx); + for (let i = 0; i < this.#children.length; i++) this.#children[i].weight = normalized[i]; + + // Update slider UI without full re-render + this.#contentEl!.querySelectorAll('.slider-row').forEach((row, ri) => { + const inp = row.querySelector('input') as HTMLInputElement; + const span = row.querySelector('.val') as HTMLElement; + if (inp && span && ri < this.#children.length) { + inp.value = String(Math.round(this.#children[ri].weight)); + span.textContent = String(Math.round(this.#children[ri].weight)); + } + }); + const sumEl = this.#contentEl!.querySelector('.sum-row'); + if (sumEl) sumEl.textContent = `Total: ${Math.round(this.#children.reduce((s, c) => s + c.weight, 0))}%`; + + this.#saveWeights(); + this.#scheduleEndorse(); + }); + }); + } + + // ── Serialization ── + + override toJSON() { + return { + ...super.toJSON(), + type: 'folk-holon-explorer', + mode: this.#mode, + rootId: this.#rootId, + spaceSlug: this.#spaceSlug, + breadcrumb: this.#breadcrumb, + }; + } + + static override fromData(data: Record): FolkHolonExplorer { + const shape = FolkShape.fromData(data) as FolkHolonExplorer; + if (data.mode) shape.setAttribute('mode', data.mode); + if (data.rootId) shape.setAttribute('root-id', data.rootId); + if (data.spaceSlug) shape.setAttribute('space-slug', data.spaceSlug); + return shape; + } + + override applyData(data: Record): void { + super.applyData(data); + const modeChanged = data.mode && data.mode !== this.#mode; + const rootChanged = data.rootId && data.rootId !== this.#rootId; + const spaceChanged = data.spaceSlug && data.spaceSlug !== this.#spaceSlug; + + if (data.mode) this.#mode = data.mode; + if (data.rootId) this.#rootId = data.rootId; + if (data.spaceSlug) this.#spaceSlug = data.spaceSlug; + if (data.breadcrumb) this.#breadcrumb = data.breadcrumb; + + if (modeChanged || rootChanged || spaceChanged) { + if (this.#mode === 'space' && this.#spaceSlug) { + if (this.#breadcrumb.length === 0) + this.#breadcrumb = [{ id: this.#spaceSlug, label: this.#spaceSlug }]; + this.#loadSpaceChildren(this.#breadcrumb[this.#breadcrumb.length - 1].id); + } else if (this.#mode === 'h3' && this.#rootId) { + if (this.#breadcrumb.length === 0) + this.#breadcrumb = [{ id: this.#rootId, label: getResolutionName(h3.getResolution(this.#rootId)) }]; + this.#loadH3Children(this.#breadcrumb[this.#breadcrumb.length - 1].id); + } + } + this.#render(); + } +} + +function esc(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} diff --git a/lib/index.ts b/lib/index.ts index a5bd4745..f789d82d 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -109,6 +109,7 @@ export * from "./folk-spider-3d"; // Holon Shapes (H3 geospatial) export * from "./folk-holon"; export * from "./folk-holon-browser"; +export * from "./folk-holon-explorer"; export * from "./holon-service"; // Nested Space Shape diff --git a/modules/rnetwork/mod.ts b/modules/rnetwork/mod.ts index 0bdb82da..e28ab239 100644 --- a/modules/rnetwork/mod.ts +++ b/modules/rnetwork/mod.ts @@ -191,6 +191,24 @@ routes.get("/api/delegations", async (c) => { } }); +// ── API: Endorse — proxy to EncryptID trust engine ── +routes.post("/api/endorse", async (c) => { + const auth = c.req.header("Authorization"); + if (!auth) return c.json({ error: "Unauthorized" }, 401); + try { + const body = await c.req.json(); + const res = await fetch(`${ENCRYPTID_URL}/api/trust/endorse`, { + method: "POST", + headers: { "Content-Type": "application/json", "Authorization": auth }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(5000), + }); + return c.json(await res.json(), res.status as any); + } catch { + return c.json({ error: "EncryptID unreachable" }, 502); + } +}); + // ── API: Graph — transform entities to node/edge format ── routes.get("/api/graph", async (c) => { const space = c.req.param("space") || "demo"; @@ -770,6 +788,7 @@ export const networkModule: RSpaceModule = { icon: "🌐", description: "Community relationship graph visualization with CRM sync", scoping: { defaultScope: 'global', userConfigurable: false }, + canvasShapes: ["folk-holon-explorer"], routes, landingPage: renderLanding, standaloneDomain: "rnetwork.online", diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index 8b00c235..b6f08a22 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -9174,6 +9174,36 @@ app.get('/api/delegations/space', async (c) => { }); }); +// POST /api/trust/endorse — log an endorsement trust event +app.post('/api/trust/endorse', async (c) => { + const claims = await verifyTokenFromRequest(c.req.header('Authorization')); + if (!claims) return c.json({ error: 'Unauthorized' }, 401); + + const body = await c.req.json().catch(() => null); + if (!body) return c.json({ error: 'Invalid JSON body' }, 400); + + const { targetDid, authority, weight, space } = body; + if (!targetDid || typeof targetDid !== 'string') + return c.json({ error: 'targetDid required (string)' }, 400); + if (!space || typeof space !== 'string') + return c.json({ error: 'space required (string)' }, 400); + const w = typeof weight === 'number' ? Math.max(0, Math.min(1, weight)) : 0.5; + const auth = typeof authority === 'string' ? authority : 'gov-ops'; + + const event = { + id: crypto.randomUUID(), + sourceDid: claims.did || `did:key:${claims.sub.slice(0, 32)}`, + targetDid, + eventType: 'endorsement' as const, + authority: auth, + weightDelta: w, + spaceSlug: space, + }; + + await logTrustEvent(event); + return c.json({ success: true, event }); +}); + // GET /api/trust/scores — aggregated trust scores for visualization app.get('/api/trust/scores', async (c) => { const authority = c.req.query('authority') || 'gov-ops'; diff --git a/website/canvas.html b/website/canvas.html index 78f06b3e..e43108f5 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -1195,6 +1195,7 @@ #canvas.feed-mode folk-booking, #canvas.feed-mode folk-holon, #canvas.feed-mode folk-holon-browser, + #canvas.feed-mode folk-holon-explorer, #canvas.feed-mode folk-feed { position: relative !important; transform: none !important; @@ -1601,6 +1602,7 @@ folk-rapp, folk-holon, folk-holon-browser, + folk-holon-explorer, folk-multisig-email { position: absolute; } @@ -2134,6 +2136,7 @@ + @@ -2497,6 +2500,7 @@ FolkFeed, FolkHolon, FolkHolonBrowser, + FolkHolonExplorer, CommunitySync, PresenceManager, generatePeerId, @@ -2780,6 +2784,7 @@ FolkFeed.define(); FolkHolon.define(); FolkHolonBrowser.define(); + FolkHolonExplorer.define(); // Register all shapes with the shape registry shapeRegistry.register("folk-shape", FolkShape); @@ -2841,6 +2846,7 @@ shapeRegistry.register("folk-feed", FolkFeed); shapeRegistry.register("folk-holon", FolkHolon); shapeRegistry.register("folk-holon-browser", FolkHolonBrowser); + shapeRegistry.register("folk-holon-explorer", FolkHolonExplorer); // Wire shape→module affiliations from module declarations for (const mod of window.__rspaceAllModules || []) { @@ -3212,7 +3218,7 @@ "folk-splat", "folk-blender", "folk-drawfast", "folk-makereal", "folk-freecad", "folk-kicad", "folk-rapp", - "folk-holon", "folk-holon-browser", + "folk-holon", "folk-holon-browser", "folk-holon-explorer", "folk-multisig-email", "folk-feed" ].join(", "); @@ -4088,6 +4094,7 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest "folk-feed": { width: 280, height: 360 }, "folk-holon": { width: 500, height: 400 }, "folk-holon-browser": { width: 400, height: 450 }, + "folk-holon-explorer": { width: 580, height: 540 }, "folk-transaction-builder": { width: 420, height: 520 }, }; @@ -4809,6 +4816,7 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest }); document.getElementById("new-holon").addEventListener("click", () => setPendingTool("folk-holon")); document.getElementById("new-holon-browser").addEventListener("click", () => setPendingTool("folk-holon-browser")); + document.getElementById("new-holon-explorer").addEventListener("click", () => setPendingTool("folk-holon-explorer")); document.getElementById("new-image-gen").addEventListener("click", () => setPendingTool("folk-image-gen")); document.getElementById("new-video-gen").addEventListener("click", () => setPendingTool("folk-video-gen")); document.getElementById("new-zine-gen").addEventListener("click", () => setPendingTool("folk-zine-gen"));