feat(rdata): add data membrane visualization to landing page

Add interactive concentric zone visualization showing Personal (encrypted/local),
Permissioned, and Public data membranes. Drag-and-drop objects between zones
triggers permission change confirmations. Mobile-optimized with tap-to-select,
responsive node sizing, and scroll prevention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-10 00:00:36 +00:00
parent dd38dcb631
commit 69da9b0ee7
1 changed files with 139 additions and 1 deletions

View File

@ -131,6 +131,20 @@ export function renderLanding(): string {
</div> </div>
</section> </section>
<!-- Data Membrane -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<span class="rl-badge" style="background:rgba(248,113,113,0.1);color:#f87171;border:1px solid rgba(248,113,113,0.2);padding:0.3rem 0.85rem;border-radius:100px;font-size:0.7rem;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;display:inline-block;margin-bottom:1.25rem;">Interactive Demo</span>
<h2 class="rl-heading">Your Data, Your Membranes</h2>
<p class="rl-subtext" style="max-width:640px;margin:0 auto 2rem">
Every piece of knowledge lives inside a permissioned boundary. Your personal data sits in encrypted local storage,
shared data flows through permissioned spaces, and public data is open to all.
<strong>Drag any object between zones</strong> to see how rData manages permission transitions.
</p>
<div id="membrane-mount"></div>
</div>
</section>
<!-- Your Data, Protected --> <!-- Your Data, Protected -->
<section class="rl-section rl-section--alt"> <section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center"> <div class="rl-container" style="text-align:center">
@ -172,5 +186,129 @@ export function renderLanding(): string {
<div class="rl-back"> <div class="rl-back">
<a href="/">&larr; Back to rSpace</a> <a href="/">&larr; Back to rSpace</a>
</div>`; </div>
<script>${getMembraneScript()}</script>
<script>DataMembrane.init('#membrane-mount');</script>`;
}
function getMembraneScript(): string {
return `
(function(){
'use strict';
var mob=function(){return window.innerWidth<600;};
var ZONES=[
{id:'public',label:'Public',sublabel:'Open to everyone',color:'#34d399',colorFaded:'rgba(52,211,153,0.08)',borderStyle:'solid',icon:'\\u{1F310}'},
{id:'permissioned',label:'Permissioned Space',sublabel:'Shared with members',color:'#fbbf24',colorFaded:'rgba(251,191,36,0.08)',borderStyle:'solid',icon:'\\u{1F511}'},
{id:'personal',label:'Personal Storage',sublabel:'Encrypted \\u00b7 Local only',color:'#f87171',colorFaded:'rgba(248,113,113,0.10)',borderStyle:'dotted',icon:'\\u{1F512}'}
];
var DEMO_OBJECTS=[
{id:'obj1',label:'Product Roadmap',icon:'\\u{1F4DD}',zone:'personal',tags:['planning']},
{id:'obj2',label:'Meeting Notes',icon:'\\u{1F4D3}',zone:'personal',tags:['meetings']},
{id:'obj3',label:'Private Journal',icon:'\\u{1F510}',zone:'personal',tags:['personal']},
{id:'obj4',label:'Team Calendar',icon:'\\u{1F4C5}',zone:'permissioned',tags:['team']},
{id:'obj5',label:'Budget Q2',icon:'\\u{1F4B0}',zone:'permissioned',tags:['finance']},
{id:'obj6',label:'Design System',icon:'\\u{1F3A8}',zone:'permissioned',tags:['dev']},
{id:'obj7',label:'API Docs',icon:'\\u{1F4C4}',zone:'public',tags:['dev']},
{id:'obj8',label:'Community Wiki',icon:'\\u{1F4DA}',zone:'public',tags:['community']},
{id:'obj9',label:'Open Proposals',icon:'\\u{1F5F3}',zone:'public',tags:['governance']}
];
var objects=JSON.parse(JSON.stringify(DEMO_OBJECTS));
var dragState=null,popup=null,hoveredZone=null,containerEl=null,canvasEl=null,tooltipObj=null,selectedObj=null;
function nodeR(){return mob()?22:28;}
function getZoneGeometry(W,H){var cx=W/2,cy=H/2,maxR=Math.min(cx,cy)-(mob()?8:16),zones=[];
for(var i=0;i<ZONES.length;i++){var outerR=maxR*(1-i*0.3);var innerR=i===ZONES.length-1?0:maxR*(1-(i+1)*0.3);zones.push(Object.assign({},ZONES[i],{cx:cx,cy:cy,outerR:outerR,innerR:innerR}));}return zones;}
function pointInZone(x,y,zg){var dx=x-zg.cx,dy=y-zg.cy,dist=Math.sqrt(dx*dx+dy*dy);return dist<=zg.outerR&&dist>zg.innerR;}
function findZoneAtPoint(x,y,zgs){for(var i=zgs.length-1;i>=0;i--){if(pointInZone(x,y,zgs[i]))return zgs[i];}return null;}
function computeObjectPositions(zgs){var positions={},byZone={},nr=nodeR();
objects.forEach(function(o){if(!byZone[o.zone])byZone[o.zone]=[];byZone[o.zone].push(o);});
zgs.forEach(function(zg){var items=byZone[zg.id]||[];var midR=(zg.outerR+zg.innerR)/2;var count=items.length;
for(var i=0;i<count;i++){var angle=(2*Math.PI*i)/count-Math.PI/2;var circ=2*Math.PI*midR;var spacing=circ/count;var r=midR;
if(spacing<nr*2.2&&count>1){r=midR+(i%2===0?-nr*0.6:nr*0.6);}
positions[items[i].id]={x:zg.cx+r*Math.cos(angle),y:zg.cy+r*Math.sin(angle)};}});return positions;}
function escSvg(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
function escHtml(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function truncate(s,max){return s.length>max?s.slice(0,max-1)+'\\u2026':s;}
function render(){if(!containerEl)return;var rect=containerEl.getBoundingClientRect();var W=rect.width;var m=mob();
var H=m?Math.max(360,Math.min(W*0.95,480)):Math.max(480,Math.min(600,W*0.65));containerEl.style.height=H+'px';
var zgs=getZoneGeometry(W,H);var positions=computeObjectPositions(zgs);var nr=nodeR();var svg='';
svg+='<defs><filter id="glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="6" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>';
var zls=m?10:13,zsls=m?8:10;
zgs.forEach(function(zg){var da=zg.borderStyle==='dotted'?'8,6':'none';
svg+='<circle cx="'+zg.cx+'" cy="'+zg.cy+'" r="'+zg.outerR+'" fill="'+zg.colorFaded+'" stroke="none"/>';
svg+='<circle cx="'+zg.cx+'" cy="'+zg.cy+'" r="'+zg.outerR+'" fill="none" stroke="'+zg.color+'" stroke-width="'+(hoveredZone===zg.id?3:2)+'" stroke-dasharray="'+da+'" opacity="'+(hoveredZone===zg.id?1:0.6)+'"/>';
var ly=zg.cy-zg.outerR+(m?16:24);var lbl=m&&zg.id==='permissioned'?'Permissioned':zg.label;
svg+='<text x="'+zg.cx+'" y="'+ly+'" text-anchor="middle" fill="'+zg.color+'" font-size="'+zls+'" font-weight="700" font-family="system-ui,sans-serif" opacity="0.9">'+zg.icon+' '+escSvg(lbl)+'</text>';
svg+='<text x="'+zg.cx+'" y="'+(ly+(m?12:16))+'" text-anchor="middle" fill="'+zg.color+'" font-size="'+zsls+'" font-family="system-ui,sans-serif" opacity="0.55">'+escSvg(zg.sublabel)+'</text>';});
var ifs=m?14:18,lfs=m?7.5:9;
objects.forEach(function(obj){var pos=positions[obj.id];if(!pos)return;var ox=pos.x,oy=pos.y;
if(dragState&&dragState.objId===obj.id){ox=dragState.currentX;oy=dragState.currentY;}
var isDragging=dragState&&dragState.objId===obj.id;var isSel=selectedObj===obj.id;var zc=ZONES.find(function(z){return z.id===obj.zone;});var nc=zc?zc.color:'#94a3b8';
svg+='<g class="membrane-obj" data-obj-id="'+obj.id+'" transform="translate('+ox+','+oy+')" style="cursor:grab;"'+(isDragging?' filter="url(#glow)"':'')+'>';
var hitR=m?nr+4:nr;svg+='<circle r="'+hitR+'" fill="transparent" stroke="none"/>';
svg+='<circle r="'+nr+'" fill="rgba(15,23,42,0.85)" stroke="'+nc+'" stroke-width="'+(isDragging||isSel?2.5:1.5)+'" opacity="'+(isDragging||isSel?1:0.8)+'"/>';
if(isSel&&m){svg+='<circle r="'+(nr+5)+'" fill="none" stroke="'+nc+'" stroke-width="1.5" stroke-dasharray="4,3" opacity="0.6"><animate attributeName="stroke-dashoffset" from="0" to="-14" dur="1s" repeatCount="indefinite"/></circle>';}
svg+='<text y="'+(m?-1:-2)+'" text-anchor="middle" font-size="'+ifs+'" dominant-baseline="central">'+obj.icon+'</text>';
var tly=m?nr-6:18;svg+='<text y="'+tly+'" text-anchor="middle" font-size="'+lfs+'" fill="#cbd5e1" font-family="system-ui,sans-serif" dominant-baseline="central">'+escSvg(truncate(obj.label,m?12:14))+'</text>';
svg+='</g>';});
if(dragState&&hoveredZone){var tzg=zgs.find(function(z){return z.id===hoveredZone;});if(tzg){var dObj=objects.find(function(o){return o.id===dragState.objId;});
if(dObj&&dObj.zone!==hoveredZone){svg+='<circle cx="'+tzg.cx+'" cy="'+tzg.cy+'" r="'+tzg.outerR+'" fill="none" stroke="'+tzg.color+'" stroke-width="3" stroke-dasharray="12,4" opacity="0.8"><animate attributeName="stroke-dashoffset" from="0" to="-32" dur="1s" repeatCount="indefinite"/></circle>';}}}
if(m&&!dragState&&!popup&&!selectedObj){svg+='<text x="'+(W/2)+'" y="'+(H-8)+'" text-anchor="middle" fill="#64748b" font-size="10" font-family="system-ui,sans-serif" opacity="0.7">Tap an object, then drag to move between zones</text>';}
canvasEl.setAttribute('viewBox','0 0 '+W+' '+H);canvasEl.style.width=W+'px';canvasEl.style.height=H+'px';canvasEl.innerHTML=svg;renderPopup();}
function renderPopup(){var ep=containerEl.querySelector('.membrane-popup-overlay');if(!popup){if(ep)ep.remove();return;}
if(!ep){ep=document.createElement('div');ep.className='membrane-popup-overlay';containerEl.appendChild(ep);}
var obj=objects.find(function(o){return o.id===popup.objId;});var fz=ZONES.find(function(z){return z.id===popup.fromZone;});var tz=ZONES.find(function(z){return z.id===popup.toZone;});
if(!obj||!fz||!tz)return;var zo={personal:0,permissioned:1,public:2};var isExp=zo[popup.toZone]>zo[popup.fromZone];var isRes=zo[popup.toZone]<zo[popup.fromZone];
var wi='\\u26a0\\ufe0f',wt='Change Sharing Permissions',wm='',cl='Confirm Move',cc='#fbbf24';
if(isExp){wi='\\u{1F513}';wt='Expanding Access';wm='Moving <strong>'+escHtml(obj.label)+'</strong> from <span style="color:'+fz.color+'">'+fz.icon+' '+fz.label+'</span> to <span style="color:'+tz.color+'">'+tz.icon+' '+tz.label+'</span> will make this data accessible to more people.';
if(popup.toZone==='public'){wm+='<br><br><strong>This will make the data publicly visible.</strong> Anyone will be able to view it.';cl='Make Public';cc='#34d399';}
else{wm+='<br><br>Members of this permissioned space will be able to view and potentially edit this data.';cl='Share with Space';cc='#fbbf24';}}
else if(isRes){wi='\\u{1F512}';wt='Restricting Access';wm='Moving <strong>'+escHtml(obj.label)+'</strong> from <span style="color:'+fz.color+'">'+fz.icon+' '+fz.label+'</span> to <span style="color:'+tz.color+'">'+tz.icon+' '+tz.label+'</span> will reduce who can access this data.';
if(popup.toZone==='personal'){wm+='<br><br>This data will be <strong>encrypted and stored locally in your browser only</strong>. No one else will have access.';cl='Make Private';cc='#f87171';}
else{wm+='<br><br>Only members of this permissioned space will retain access.';cl='Restrict Access';cc='#fbbf24';}}
ep.innerHTML='<div class="membrane-popup"><div class="membrane-popup-icon">'+wi+'</div><h3 class="membrane-popup-title">'+wt+'</h3><p class="membrane-popup-msg">'+wm+'</p><div class="membrane-popup-visual"><div class="membrane-popup-zone" style="border-color:'+fz.color+';color:'+fz.color+'">'+fz.icon+' '+fz.label+'</div><div class="membrane-popup-arrow">'+(isExp?'\\u2192':'\\u2190')+'</div><div class="membrane-popup-zone" style="border-color:'+tz.color+';color:'+tz.color+'">'+tz.icon+' '+tz.label+'</div></div><div class="membrane-popup-actions"><button class="membrane-popup-btn membrane-popup-cancel">Cancel</button><button class="membrane-popup-btn membrane-popup-confirm" style="background:'+cc+';color:#0b1120;">'+cl+'</button></div></div>';
ep.querySelector('.membrane-popup-cancel').onclick=function(){popup=null;selectedObj=null;render();};
ep.querySelector('.membrane-popup-confirm').onclick=function(){var o=objects.find(function(x){return x.id===popup.objId;});if(o)o.zone=popup.toZone;popup=null;selectedObj=null;render();};
ep.onclick=function(e){if(e.target===ep){popup=null;selectedObj=null;render();}};}
function getPointerPos(e){var evt=e.touches?e.touches[0]:e;var rect=canvasEl.getBoundingClientRect();var sx=canvasEl.viewBox.baseVal.width/rect.width;var sy=canvasEl.viewBox.baseVal.height/rect.height;return{x:(evt.clientX-rect.left)*sx,y:(evt.clientY-rect.top)*sy};}
function onPointerDown(e){if(popup)return;var t=e.target.closest('.membrane-obj');
if(!t){if(mob()&&selectedObj){selectedObj=null;render();}return;}
var objId=t.getAttribute('data-obj-id');
if(mob()&&e.touches&&selectedObj!==objId){e.preventDefault();selectedObj=objId;render();return;}
e.preventDefault();var pos=getPointerPos(e);
var rect=containerEl.getBoundingClientRect();var W=rect.width;var H=parseInt(containerEl.style.height)||480;var zgs=getZoneGeometry(W,H);var positions=computeObjectPositions(zgs);var op=positions[objId];
dragState={objId:objId,startX:pos.x,startY:pos.y,currentX:op?op.x:pos.x,currentY:op?op.y:pos.y,offsetX:op?pos.x-op.x:0,offsetY:op?pos.y-op.y:0};
selectedObj=objId;canvasEl.style.cursor='grabbing';
if(e.touches){document.body.style.overflow='hidden';document.body.style.touchAction='none';}}
function onPointerMove(e){if(!dragState)return;e.preventDefault();var pos=getPointerPos(e);dragState.currentX=pos.x-dragState.offsetX;dragState.currentY=pos.y-dragState.offsetY;
var rect=containerEl.getBoundingClientRect();var W=rect.width;var H=parseInt(containerEl.style.height)||480;var zgs=getZoneGeometry(W,H);var zone=findZoneAtPoint(dragState.currentX,dragState.currentY,zgs);hoveredZone=zone?zone.id:null;render();}
function onPointerUp(){if(!dragState)return;document.body.style.overflow='';document.body.style.touchAction='';
var obj=objects.find(function(o){return o.id===dragState.objId;});
if(obj&&hoveredZone&&hoveredZone!==obj.zone){popup={objId:obj.id,fromZone:obj.zone,toZone:hoveredZone};}
dragState=null;hoveredZone=null;canvasEl.style.cursor='';render();}
function initDrag(){canvasEl.addEventListener('mousedown',onPointerDown);canvasEl.addEventListener('touchstart',onPointerDown,{passive:false});
document.addEventListener('mousemove',onPointerMove);document.addEventListener('touchmove',onPointerMove,{passive:false});document.addEventListener('mouseup',onPointerUp);document.addEventListener('touchend',onPointerUp);}
function initTooltip(){if('ontouchstart' in window)return;
canvasEl.addEventListener('mouseover',function(e){var t=e.target.closest('.membrane-obj');if(t){var id=t.getAttribute('data-obj-id');tooltipObj=objects.find(function(o){return o.id===id;})||null;}else{tooltipObj=null;}renderTooltip(e);});
canvasEl.addEventListener('mouseout',function(e){if(!e.target.closest('.membrane-obj')){tooltipObj=null;renderTooltip(e);}});}
function renderTooltip(e){var tip=containerEl.querySelector('.membrane-tooltip');if(!tooltipObj){if(tip)tip.style.display='none';return;}
if(!tip){tip=document.createElement('div');tip.className='membrane-tooltip';containerEl.appendChild(tip);}
var zone=ZONES.find(function(z){return z.id===tooltipObj.zone;});
tip.innerHTML='<strong>'+tooltipObj.icon+' '+escHtml(tooltipObj.label)+'</strong><br><span style="color:'+zone.color+'">'+zone.icon+' '+zone.label+'</span>'+(tooltipObj.tags.length?'<br><span style="opacity:0.6">'+tooltipObj.tags.map(function(t){return'#'+t;}).join(' ')+'</span>':'')+'<br><span style="opacity:0.5;font-size:0.75em;">Drag to change permissions</span>';
tip.style.display='block';var rect=containerEl.getBoundingClientRect();tip.style.left=Math.min(e.clientX-rect.left+12,rect.width-180)+'px';tip.style.top=(e.clientY-rect.top-70)+'px';}
function injectStyles(){if(document.getElementById('membrane-styles'))return;var s=document.createElement('style');s.id='membrane-styles';
s.textContent='.membrane-container{position:relative;width:100%;overflow:hidden;border-radius:16px;background:rgba(10,14,26,0.6);border:1px solid rgba(255,255,255,0.06);-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;touch-action:none}.membrane-container svg{display:block}.membrane-tooltip{position:absolute;display:none;background:rgba(15,23,42,0.95);border:1px solid rgba(255,255,255,0.15);border-radius:8px;padding:8px 12px;font-size:0.8rem;color:#e2e8f0;pointer-events:none;z-index:20;line-height:1.5;max-width:200px;backdrop-filter:blur(8px)}.membrane-legend{display:flex;justify-content:center;gap:1.5rem;padding:0.75rem 1rem;flex-wrap:wrap}.membrane-legend-item{display:flex;align-items:center;gap:0.5rem;font-size:0.8rem;color:#94a3b8}.membrane-legend-swatch{display:inline-block;width:16px;height:16px;border-radius:50%;flex-shrink:0}.membrane-reset-btn{position:absolute;top:12px;right:12px;padding:4px 12px;border-radius:6px;border:1px solid rgba(255,255,255,0.12);background:rgba(15,23,42,0.8);color:#94a3b8;font-size:0.75rem;cursor:pointer;z-index:10;transition:border-color 0.2s,color 0.2s}.membrane-reset-btn:hover{border-color:rgba(255,255,255,0.3);color:#e2e8f0}.membrane-popup-overlay{position:absolute;inset:0;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:50;border-radius:16px}.membrane-popup{background:#1e293b;border:1px solid rgba(255,255,255,0.12);border-radius:16px;padding:2rem;max-width:420px;width:90%;text-align:center;box-shadow:0 25px 50px -12px rgba(0,0,0,0.5)}.membrane-popup-icon{font-size:2.5rem;margin-bottom:0.75rem}.membrane-popup-title{font-size:1.2rem;color:#f1f5f9;margin-bottom:0.75rem;font-weight:700}.membrane-popup-msg{font-size:0.9rem;color:#94a3b8;line-height:1.6;margin-bottom:1.25rem}.membrane-popup-visual{display:flex;align-items:center;justify-content:center;gap:1rem;margin-bottom:1.5rem}.membrane-popup-zone{padding:0.5rem 1rem;border:2px solid;border-radius:10px;font-size:0.8rem;font-weight:600;background:rgba(255,255,255,0.03)}.membrane-popup-arrow{font-size:1.5rem;color:#64748b}.membrane-popup-actions{display:flex;gap:0.75rem;justify-content:center}.membrane-popup-btn{padding:0.6rem 1.5rem;border-radius:8px;font-size:0.9rem;font-weight:600;cursor:pointer;border:none;transition:transform 0.15s,opacity 0.15s}.membrane-popup-btn:hover{transform:translateY(-1px)}.membrane-popup-cancel{background:rgba(255,255,255,0.06);color:#cbd5e1;border:1px solid rgba(255,255,255,0.12)}.membrane-popup-cancel:hover{border-color:rgba(255,255,255,0.25)}@media(max-width:600px){.membrane-container{border-radius:12px}.membrane-legend{gap:0.6rem;padding:0.5rem}.membrane-legend-item{font-size:0.7rem;gap:0.35rem}.membrane-legend-swatch{width:12px;height:12px}.membrane-reset-btn{top:8px;right:8px;padding:3px 8px;font-size:0.65rem}.membrane-popup{padding:1.25rem 1rem;border-radius:12px;width:94%}.membrane-popup-icon{font-size:2rem;margin-bottom:0.5rem}.membrane-popup-title{font-size:1rem}.membrane-popup-msg{font-size:0.8rem;margin-bottom:1rem}.membrane-popup-visual{flex-direction:column;gap:0.5rem}.membrane-popup-arrow{transform:rotate(90deg)}.membrane-popup-zone{padding:0.4rem 0.75rem;font-size:0.75rem}.membrane-popup-btn{padding:0.5rem 1.25rem;font-size:0.85rem}}';
document.head.appendChild(s);}
function addResetButton(){var btn=document.createElement('button');btn.className='membrane-reset-btn';btn.textContent='Reset Demo';btn.title='Reset all objects to their default zones';
btn.onclick=function(){objects=JSON.parse(JSON.stringify(DEMO_OBJECTS));popup=null;dragState=null;hoveredZone=null;selectedObj=null;render();};containerEl.appendChild(btn);}
function addLegend(){var legend=document.createElement('div');legend.className='membrane-legend';
legend.innerHTML=ZONES.slice().reverse().map(function(z){return'<div class="membrane-legend-item"><span class="membrane-legend-swatch" style="border:2px '+z.borderStyle+' '+z.color+';background:'+z.colorFaded+';"></span><span>'+z.icon+' '+z.label+'</span></div>';}).join('');
containerEl.appendChild(legend);}
function init(sel){injectStyles();var wrapper=document.querySelector(sel);if(!wrapper)return;
containerEl=document.createElement('div');containerEl.className='membrane-container';
canvasEl=document.createElementNS('http://www.w3.org/2000/svg','svg');canvasEl.setAttribute('xmlns','http://www.w3.org/2000/svg');
containerEl.appendChild(canvasEl);wrapper.appendChild(containerEl);addResetButton();addLegend();initDrag();initTooltip();render();
var rt;window.addEventListener('resize',function(){clearTimeout(rt);rt=setTimeout(render,100);});}
window.DataMembrane={init:init};
})();`;
} }