diff --git a/lib/folk-holon-explorer.ts b/lib/folk-holon-explorer.ts
index 82a2c780..e6e4c6c3 100644
--- a/lib/folk-holon-explorer.ts
+++ b/lib/folk-holon-explorer.ts
@@ -27,6 +27,7 @@ interface ExplorerNode {
}
type NavMode = 'h3' | 'space';
+type ViewMode = 'holon' | 'graph';
// ── Appreciation normalization (from Holons project) ──
@@ -41,6 +42,17 @@ function normalize(values: number[], changedIndex: number): number[] {
);
}
+// ── Hex polygon helper ──
+
+function hexPoly(cx: number, cy: number, r: number): string {
+ const pts: string[] = [];
+ for (let i = 0; i < 6; i++) {
+ const a = (Math.PI / 3) * i - Math.PI / 2;
+ pts.push(`${cx + r * Math.cos(a)},${cy + r * Math.sin(a)}`);
+ }
+ return pts.join(' ');
+}
+
// ── MetatronGrid SVG ──
function metatronGrid(size: number): string {
@@ -103,9 +115,18 @@ const styles = css`
.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; }
+ .view-strip {
+ display: flex; background: #1e293b; border-bottom: 1px solid rgba(255,255,255,0.08);
+ }
+ .view-btn {
+ flex: 1; font-size: 11px; color: #64748b; background: transparent; border: none;
+ padding: 6px 0; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.15s;
+ }
+ .view-btn:hover { color: #94a3b8; }
+ .view-btn.active { color: #10b981; border-bottom-color: #10b981; background: rgba(16,185,129,0.07); }
.content {
display: flex; flex-direction: column; overflow: hidden;
- height: calc(100% - 66px); /* header + breadcrumb */
+ height: calc(100% - 94px); /* header + breadcrumb + view-strip */
}
.svg-wrap { flex: 1; min-height: 0; position: relative; }
.svg-wrap svg { width: 100%; height: 100%; }
@@ -159,6 +180,7 @@ export class FolkHolonExplorer extends FolkShape {
// ── State ──
#mode: NavMode = 'space';
+ #view: ViewMode = 'holon';
#rootId = '';
#spaceSlug = '';
#breadcrumb: { id: string; label: string }[] = [];
@@ -171,6 +193,7 @@ export class FolkHolonExplorer extends FolkShape {
const root = super.createRenderRoot();
this.#mode = (this.getAttribute('mode') as NavMode) || 'space';
+ this.#view = (this.getAttribute('view') as ViewMode) || 'holon';
this.#rootId = this.getAttribute('root-id') || '';
this.#spaceSlug = this.getAttribute('space-slug') || (window as any).__rspaceSpace || '';
@@ -189,6 +212,10 @@ export class FolkHolonExplorer extends FolkShape {
@@ -540,6 +535,101 @@ export class FolkHolonExplorer extends FolkShape {
});
}
+ #buildHolonSvg(visible: ExplorerNode[], overflow: number, W: number, H: number): string {
+ const cx = W / 2, cy = H / 2;
+ let orbit = Math.min(W, H) * 0.35;
+ if (this.#children.length > 8) orbit *= 0.85;
+
+ const gridSvg = metatronGrid(Math.min(W, H));
+ const linesSvg: string[] = [];
+ const nodesSvg: 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));
+
+ linesSvg.push(
+ `
`
+ );
+
+ 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)}
+
+ `);
+ }
+
+ const centerLabel = esc(this.#breadcrumb[this.#breadcrumb.length - 1]?.label.slice(0, 8) || '?');
+ const centerSvg = `
+
+
${centerLabel}
+ `;
+
+ const overflowSvg = overflow > 0
+ ? `
+${overflow} more`
+ : '';
+
+ return `${gridSvg}${linesSvg.join('')}${centerSvg}${nodesSvg.join('')}${overflowSvg}`;
+ }
+
+ #buildGraphSvg(visible: ExplorerNode[], overflow: number, W: number, H: number): string {
+ const cx = W / 2, cy = H / 2;
+ const orbit = Math.min(W, H) * 0.32;
+
+ const gridSvg = metatronGrid(Math.min(W, H));
+ const linesSvg: string[] = [];
+ const nodesSvg: string[] = [];
+ const N = visible.length;
+
+ for (let i = 0; i < N; i++) {
+ const child = visible[i];
+ const angleDeg = (360 / N) * i - 90;
+ const rad = angleDeg * Math.PI / 180;
+ const nx = cx + orbit * Math.cos(rad);
+ const ny = cy + orbit * Math.sin(rad);
+ const hr = Math.min(24, Math.max(12, 12 + child.weight * 0.2));
+
+ linesSvg.push(
+ `
`
+ );
+
+ const fill = `rgba(16,185,129,${(0.1 + child.weight * 0.005).toFixed(3)})`;
+ const label = esc(child.label.length > 14 ? child.label.slice(0, 12) + '…' : child.label);
+
+ // Radial label placement
+ const labelDist = hr + 12;
+ const lx = nx + labelDist * Math.cos(rad);
+ const ly = ny + labelDist * Math.sin(rad);
+ const normDeg = ((angleDeg % 360) + 360) % 360;
+ const anchor = normDeg > 90 && normDeg < 270 ? 'end' : normDeg === 90 || normDeg === 270 ? 'middle' : 'start';
+
+ nodesSvg.push(`
+
+
+ ${label}
+
+ `);
+ }
+
+ const centerLabel = esc(this.#breadcrumb[this.#breadcrumb.length - 1]?.label.slice(0, 8) || '?');
+ const centerSvg = `
+
+
${centerLabel}
+ `;
+
+ const overflowSvg = overflow > 0
+ ? `
+${overflow} more`
+ : '';
+
+ return `${gridSvg}${linesSvg.join('')}${centerSvg}${nodesSvg.join('')}${overflowSvg}`;
+ }
+
// ── Serialization ──
override toJSON() {
@@ -547,6 +637,7 @@ export class FolkHolonExplorer extends FolkShape {
...super.toJSON(),
type: 'folk-holon-explorer',
mode: this.#mode,
+ view: this.#view,
rootId: this.#rootId,
spaceSlug: this.#spaceSlug,
breadcrumb: this.#breadcrumb,
@@ -556,6 +647,7 @@ export class FolkHolonExplorer extends FolkShape {
static override fromData(data: Record
): FolkHolonExplorer {
const shape = FolkShape.fromData(data) as FolkHolonExplorer;
if (data.mode) shape.setAttribute('mode', data.mode);
+ if (data.view) shape.setAttribute('view', data.view);
if (data.rootId) shape.setAttribute('root-id', data.rootId);
if (data.spaceSlug) shape.setAttribute('space-slug', data.spaceSlug);
return shape;
@@ -568,6 +660,7 @@ export class FolkHolonExplorer extends FolkShape {
const spaceChanged = data.spaceSlug && data.spaceSlug !== this.#spaceSlug;
if (data.mode) this.#mode = data.mode;
+ if (data.view) this.#view = data.view;
if (data.rootId) this.#rootId = data.rootId;
if (data.spaceSlug) this.#spaceSlug = data.spaceSlug;
if (data.breadcrumb) this.#breadcrumb = data.breadcrumb;