granite-mem 0.1.7 → 0.1.9

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.
@@ -0,0 +1,102 @@
1
+ "use strict";(()=>{(function(){const nt={};nt.detectCommunities=function(c,n){const R={};for(const a of c)R[a.slug]=[];for(const a of n)R[a.source].push(a.target),R[a.target].push(a.source);const t={};for(const a of c)t[a.slug]=a.community||a.slug;for(let a=0;a<20;a++){let A=0;const f=c.map(l=>l.slug).sort(()=>Math.random()-.5);for(const l of f){const x=R[l];if(!x.length)continue;const W={};for(const T of x)W[t[T]]=(W[t[T]]||0)+1;let P=t[l],z=-1;for(const[T,_]of Object.entries(W))(_>z||_===z&&T<P)&&(P=T,z=_);P!==t[l]&&(t[l]=P,A++)}if(A===0)break}const h=new Map,C=[],O={};for(const a of Object.keys(t)){const A=t[a];h.has(A)||(h.set(A,h.size),C.push(0));const f=h.get(A);O[a]=f,C[f]++}const U=[],M=h.size,g=137.508;for(let a=0;a<M;a++){const A=(a*g+24)%360,f=70+a*13%26,l=48+a*17%22;U.push(ft(A/360,f/100,l/100))}return{communityOf:O,sizes:C,K:M,palette:U}};function ft(k,c,n){let R,t,h;if(c===0)R=t=h=n;else{const C=(M,g,a)=>(a<0&&(a+=1),a>1&&(a-=1),a<.16666666666666666?M+(g-M)*6*a:a<.5?g:a<.6666666666666666?M+(g-M)*(.6666666666666666-a)*6:M),O=n<.5?n*(1+c):n+c-n*c,U=2*n-O;R=C(U,O,k+1/3),t=C(U,O,k),h=C(U,O,k-1/3)}return[R,t,h]}function J(k,c,n,R){this.x=k,this.y=c,this.w=n,this.h=R,this.mass=0,this.cx=0,this.cy=0,this.node=null,this.children=null}J.prototype.insert=function(c){if(this.mass+=1,this.cx+=c.x,this.cy+=c.y,!this.children&&this.node===null){this.node=c;return}if(!(!this.children&&this.w<1)){if(!this.children){const n=this.w/2,R=this.h/2;this.children=[new J(this.x,this.y,n,R),new J(this.x+n,this.y,n,R),new J(this.x,this.y+R,n,R),new J(this.x+n,this.y+R,n,R)];const t=this.node;this.node=null,this._insertIntoChild(t)}this._insertIntoChild(c)}},J.prototype._insertIntoChild=function(k){const c=this.w/2,n=this.h/2,R=k.x<this.x+c?0:1,t=k.y<this.y+n?0:1;this.children[t*2+R].insert(k)},nt.createSimulation=function(c,n,R=2e3,t=1400,h={}){const{communityOf:C,sizes:O,K:U,palette:M}=nt.detectCommunities(c,n),g=c.length,a=new Float32Array(g),A=new Float32Array(g),f=new Float32Array(g),l=new Float32Array(g),x=new Int32Array(g),W=new Int32Array(g),P=new Float32Array(g),z=new Float32Array(g),T=new Array(g),_={};for(let o=0;o<g;o++)T[o]=c[o].slug,_[c[o].slug]=o,x[o]=C[c[o].slug]??0;const it=o=>{let d=0;for(let p=0;p<o.length;p++)d=d*31+o.charCodeAt(p)|0;return Math.abs(d)},G=new Int32Array(n.length),V=new Int32Array(n.length),tt=new Float32Array(n.length),Z=new Uint8Array(n.length);for(let o=0;o<n.length;o++)G[o]=_[n[o].source],V[o]=_[n[o].target],tt[o]=1,Z[o]=x[G[o]]===x[V[o]]?1:0,W[G[o]]++,W[V[o]]++;const rt=[];for(let o=0;o<U;o++){const d=o/U*Math.PI*2;rt.push({x:R/2+Math.cos(d)*R*.28,y:t/2+Math.sin(d)*t*.28*.85})}for(let o=0;o<g;o++){const d=rt[x[o]],p=it(T[o]),w=40+p%200,F=p%360*Math.PI/180,m=(o*2654435761>>>0)/4294967295-.5,X=(o*40503>>>0)/4294967295-.5;a[o]=d.x+Math.cos(F)*w+m*8,A[o]=d.y+Math.sin(F)*w+X*8,P[o]=2.5+Math.min(14,Math.sqrt(W[o])*2.2),z[o]=1}const r={n:g,x:a,y:A,vx:f,vy:l,community:x,degree:W,radius:P,alpha:z,slugOf:T,idxOf:_,links:n,linkA:G,linkB:V,linkW:tt,linkIntra:Z,W:R,H:t,palette:M,K:U,communityOf:C,repel:h.repel??120,linkStrength:h.linkStrength??.015,linkDistance:h.linkDistance??55,bridgeMult:1.6,centerK:.004,damping:.86,theta:.9,cooling:1,coolTarget:.35,coolRate:.012,hoverX:null,hoverY:null,hoverR:120,hoverStrength:0,pinnedIdx:-1,pinX:0,pinY:0,active:-1};let et=null;return r.step=function(d){r.cooling=Math.max(r.coolTarget,r.cooling-r.coolRate);const p=r.cooling;if(p===0&&r.pinnedIdx<0)return;let w=1/0,F=-1/0,m=1/0,X=-1/0;for(let e=0;e<g;e++)a[e]<w&&(w=a[e]),a[e]>F&&(F=a[e]),A[e]<m&&(m=A[e]),A[e]>X&&(X=A[e]);const q=50,Q=F-w+q*2,$=X-m+q*2,at=Math.max(Q,$,100);et=new J(w-q,m-q,at,at);for(let e=0;e<g;e++)et.insert({x:a[e],y:A[e],i:e});const D=r.theta,ct=r.repel;for(let e=0;e<g;e++){let i=0,v=0;const y=[et];for(;y.length;){const u=y.pop();if(!u||u.mass===0)continue;const Y=u.cx/u.mass,N=u.cy/u.mass,E=a[e]-Y,I=A[e]-N,H=E*E+I*I+.01;if(u.node){if(u.node.i===e)continue;const B=ct/H;i+=E*B,v+=I*B}else if(u.children)if(u.w/Math.sqrt(H)<D){const B=ct*u.mass/H;i+=E*B,v+=I*B}else for(const B of u.children)y.push(B)}f[e]+=i*p,l[e]+=v*p}const s=r.linkStrength,b=r.linkDistance;for(let e=0;e<G.length;e++){const i=G[e],v=V[e],y=a[v]-a[i],u=A[v]-A[i],Y=Math.sqrt(y*y+u*u)+.01,N=Z[e]?b:b*1.8,E=Z[e]?s:s*r.bridgeMult,I=(Y-N)*E*tt[e],H=y/Y*I,B=u/Y*I;f[i]+=H*p,l[i]+=B*p,f[v]-=H*p,l[v]-=B*p}const K=R/2,S=t/2,L=r.centerK*p;for(let e=0;e<g;e++)f[e]+=(K-a[e])*L,l[e]+=(S-A[e])*L;if(r.hoverX!==null&&r.hoverStrength>0){const e=r.hoverR,i=e*e;for(let v=0;v<g;v++){const y=a[v]-r.hoverX,u=A[v]-r.hoverY,Y=y*y+u*u;if(Y<i){const N=Math.sqrt(Y)+.1,E=1-N/e,I=r.hoverStrength*E*E;f[v]+=y/N*I,l[v]+=u/N*I}}}r.pinnedIdx>=0&&(a[r.pinnedIdx]=r.pinX,A[r.pinnedIdx]=r.pinY,f[r.pinnedIdx]=0,l[r.pinnedIdx]=0);const ot=r.damping;for(let e=0;e<g;e++){f[e]*=ot,l[e]*=ot;const i=f[e]*f[e]+l[e]*l[e];if(i>400){const v=20/Math.sqrt(i);f[e]*=v,l[e]*=v}a[e]+=f[e],A[e]+=l[e]}},r.setHover=function(o,d,p=600){r.hoverX=o,r.hoverY=d,r.hoverStrength=p},r.clearHover=function(){r.hoverX=null,r.hoverStrength=0},r.setActive=function(o){r.active=o?_[o]??-1:-1},r.prebake=function(d=180){const p=r.cooling;r.cooling=1;for(let w=0;w<d;w++)r.step(1/60);r.cooling=p},r.freeze=function(){r.cooling=0,r.coolTarget=0;for(let d=0;d<g;d++)f[d]=0,l[d]=0},r.nudge=function(d=.35){r.coolTarget=0,r.cooling=Math.min(1,r.cooling+d)},r.edgeAlpha=1,r.pickNode=function(o,d,p=0){let w=-1,F=1/0;for(let m=0;m<g;m++){const X=o-a[m],q=d-A[m],Q=X*X+q*q,$=P[m]+6+p;Q<$*$&&Q<F&&(w=m,F=Q)}return w},r},nt.createRenderer=function(c,n,R={}){const t=c.getContext("webgl",{antialias:!0,alpha:!0,premultipliedAlpha:!1});if(!t)throw new Error("WebGL not supported");const h={tx:0,ty:0,k:1};let C=window.devicePixelRatio||1;function O(){C=window.devicePixelRatio||1;const s=c.getBoundingClientRect();c.width=Math.floor(s.width*C),c.height=Math.floor(s.height*C),t.viewport(0,0,c.width,c.height)}O();const U=`
2
+ attribute vec2 aCorner;
3
+ attribute vec2 aPos;
4
+ attribute float aRadius;
5
+ attribute vec3 aColor;
6
+ attribute float aAlpha;
7
+ attribute float aState; // 0=normal, 1=active, 2=hop1, 3=hop2, 4=dim
8
+ uniform vec2 uResolution;
9
+ uniform vec2 uTranslate;
10
+ uniform float uScale;
11
+ varying vec2 vCorner;
12
+ varying vec3 vColor;
13
+ varying float vAlpha;
14
+ varying float vState;
15
+ varying float vRadius;
16
+ void main() {
17
+ // Scale factor \u2014 active/hop1 get a halo extension
18
+ float extend = 6.0;
19
+ if (aState < 0.5) extend = 4.0;
20
+ else if (aState < 1.5) extend = 10.0; // active
21
+ else if (aState < 2.5) extend = 7.0; // hop1
22
+ float r = (aRadius + extend) * uScale;
23
+ vec2 world = aPos + uTranslate;
24
+ vec2 px = world * uScale;
25
+ vec2 corner = aCorner * r;
26
+ vec2 screen = px + corner;
27
+ vec2 clip = screen / uResolution * 2.0 - 1.0;
28
+ clip.y = -clip.y;
29
+ gl_Position = vec4(clip, 0.0, 1.0);
30
+ vCorner = aCorner;
31
+ vColor = aColor;
32
+ vAlpha = aAlpha;
33
+ vState = aState;
34
+ vRadius = aRadius * uScale;
35
+ }
36
+ `,M=`
37
+ precision mediump float;
38
+ varying vec2 vCorner;
39
+ varying vec3 vColor;
40
+ varying float vAlpha;
41
+ varying float vState;
42
+ varying float vRadius;
43
+ void main() {
44
+ float d = length(vCorner); // 0..~1 at corner
45
+ // inner disk
46
+ float rNorm = 1.0;
47
+ // Use vRadius vs. the pixel position: we want the disk at d < (r / r_ext)
48
+ // Since we built the quad at (r+extend), the disk sits at d < r/(r+extend)
49
+ float extend = 4.0;
50
+ if (vState >= 0.5 && vState < 1.5) extend = 10.0;
51
+ else if (vState >= 1.5 && vState < 2.5) extend = 7.0;
52
+ float diskEdge = vRadius / (vRadius + extend);
53
+
54
+ // Disk
55
+ float disk = smoothstep(diskEdge + 0.02, diskEdge - 0.02, d);
56
+
57
+ // Highlight inside (top-left)
58
+ vec2 hl = vCorner - vec2(-0.28, -0.28);
59
+ float highlight = (1.0 - smoothstep(0.0, 0.55, length(hl))) * disk * 0.32;
60
+
61
+ // Outer glow \u2014 strong, so each node broadcasts its community hue.
62
+ float glow = (1.0 - smoothstep(diskEdge, 1.0, d)) * 0.95;
63
+
64
+ // Active pulse ring
65
+ float pulse = 0.0;
66
+ if (vState >= 0.5 && vState < 1.5) {
67
+ float ring = smoothstep(diskEdge + 0.03, diskEdge + 0.06, d) * (1.0 - smoothstep(diskEdge + 0.06, diskEdge + 0.12, d));
68
+ pulse = ring * 0.8;
69
+ }
70
+
71
+ vec3 base = vColor;
72
+ vec3 col = base * (disk * 1.0) + vec3(highlight) + base * glow * 0.5 + vec3(pulse);
73
+ float alpha = (disk + glow * 0.55 + pulse) * vAlpha;
74
+ if (alpha < 0.003) discard;
75
+ gl_FragColor = vec4(col, alpha);
76
+ }
77
+ `,g=`
78
+ attribute vec2 aPos; // endpoint position (0 or 1 along the edge)
79
+ attribute vec2 aA;
80
+ attribute vec2 aB;
81
+ attribute vec3 aColor;
82
+ attribute float aAlpha;
83
+ uniform vec2 uResolution;
84
+ uniform vec2 uTranslate;
85
+ uniform float uScale;
86
+ varying vec3 vColor;
87
+ varying float vAlpha;
88
+ void main() {
89
+ vec2 world = mix(aA, aB, aPos.x) + uTranslate;
90
+ vec2 px = world * uScale;
91
+ vec2 clip = px / uResolution * 2.0 - 1.0;
92
+ clip.y = -clip.y;
93
+ gl_Position = vec4(clip, 0.0, 1.0);
94
+ vColor = aColor;
95
+ vAlpha = aAlpha;
96
+ }
97
+ `,a=`
98
+ precision mediump float;
99
+ varying vec3 vColor;
100
+ varying float vAlpha;
101
+ void main() { gl_FragColor = vec4(vColor, vAlpha); }
102
+ `;function A(s,b){const K=(L,ot)=>{const e=t.createShader(L);if(t.shaderSource(e,ot),t.compileShader(e),!t.getShaderParameter(e,t.COMPILE_STATUS))throw new Error(t.getShaderInfoLog(e));return e},S=t.createProgram();if(t.attachShader(S,K(t.VERTEX_SHADER,s)),t.attachShader(S,K(t.FRAGMENT_SHADER,b)),t.linkProgram(S),!t.getProgramParameter(S,t.LINK_STATUS))throw new Error(t.getProgramInfoLog(S));return S}const f=A(U,M),l=A(g,a),x=n.n,W=new Float32Array([-1,-1,1,-1,1,1,-1,-1,1,1,-1,1]),P=new Float32Array(x*6*2);for(let s=0;s<x;s++)for(let b=0;b<6;b++)P[(s*6+b)*2]=W[b*2],P[(s*6+b)*2+1]=W[b*2+1];const z=new Float32Array(x*6*2),T=new Float32Array(x*6),_=new Float32Array(x*6*3),it=new Float32Array(x*6),G=new Float32Array(x*6),V=t.createBuffer();t.bindBuffer(t.ARRAY_BUFFER,V),t.bufferData(t.ARRAY_BUFFER,P,t.STATIC_DRAW);const tt=t.createBuffer(),Z=t.createBuffer(),rt=t.createBuffer(),r=t.createBuffer(),et=t.createBuffer(),o=n.links.length,d=new Float32Array(o*2*2);for(let s=0;s<o;s++)d[s*4+0]=0,d[s*4+1]=0,d[s*4+2]=1,d[s*4+3]=0;const p=new Float32Array(o*2*2),w=new Float32Array(o*2*2),F=new Float32Array(o*2*3),m=new Float32Array(o*2),X=t.createBuffer();t.bindBuffer(t.ARRAY_BUFFER,X),t.bufferData(t.ARRAY_BUFFER,d,t.STATIC_DRAW);const q=t.createBuffer(),Q=t.createBuffer(),$=t.createBuffer(),at=t.createBuffer();function D(s,b,K,S){const L=t.getAttribLocation(s,b);L<0||(t.bindBuffer(t.ARRAY_BUFFER,K),t.enableVertexAttribArray(L),t.vertexAttribPointer(L,S,t.FLOAT,!1,0,0))}return t.enable(t.BLEND),t.blendFunc(t.SRC_ALPHA,t.ONE_MINUS_SRC_ALPHA),{view:h,setView(s){Object.assign(h,s)},resize:O,hops:{hop1:new Set,hop2:new Set},setHops(s){this.hops=s},hovered:-1,setHovered(s){this.hovered=s},draw(s=0){const b=c.width,K=c.height;t.clearColor(0,0,0,0),t.clear(t.COLOR_BUFFER_BIT);const S=n.active,L=this.hops.hop1,ot=this.hops.hop2;for(let i=0;i<o;i++){const v=n.linkA[i],y=n.linkB[i],u=n.x[v],Y=n.y[v],N=n.x[y],E=n.y[y];p[i*4+0]=u,p[i*4+1]=Y,p[i*4+2]=u,p[i*4+3]=Y,w[i*4+0]=N,w[i*4+1]=E,w[i*4+2]=N,w[i*4+3]=E;const[I,H,B]=n.palette[n.community[v]],[j,ht,ut]=n.palette[n.community[y]];let st=n.linkIntra[i]?.38:.035;S>=0&&(v===S||y===S?st=.95:L.has(n.slugOf[v])&&L.has(n.slugOf[y])?st=.18:st=0);const lt=st*(n.edgeAlpha??1);m[i*2]=lt,m[i*2+1]=lt,F[i*6+0]=I,F[i*6+1]=H,F[i*6+2]=B,F[i*6+3]=j,F[i*6+4]=ht,F[i*6+5]=ut}t.useProgram(l),t.uniform2f(t.getUniformLocation(l,"uResolution"),b,K),t.uniform2f(t.getUniformLocation(l,"uTranslate"),h.tx,h.ty),t.uniform1f(t.getUniformLocation(l,"uScale"),h.k*C),D(l,"aPos",X,2),t.bindBuffer(t.ARRAY_BUFFER,q),t.bufferData(t.ARRAY_BUFFER,p,t.DYNAMIC_DRAW),D(l,"aA",q,2),t.bindBuffer(t.ARRAY_BUFFER,Q),t.bufferData(t.ARRAY_BUFFER,w,t.DYNAMIC_DRAW),D(l,"aB",Q,2),t.bindBuffer(t.ARRAY_BUFFER,$),t.bufferData(t.ARRAY_BUFFER,F,t.DYNAMIC_DRAW),D(l,"aColor",$,3),t.bindBuffer(t.ARRAY_BUFFER,at),t.bufferData(t.ARRAY_BUFFER,m,t.DYNAMIC_DRAW),D(l,"aAlpha",at,1),t.drawArrays(t.LINES,0,o*2);const e=.6+.4*Math.sin(s*3.4);for(let i=0;i<x;i++){const v=n.slugOf[i];let y=0,u=S<0?1:.25;i===S?(y=1,u=1):L.has(v)?(y=2,u=1):ot.has(v)?(y=3,u=.75):S>=0&&(y=4,u=.25),i===this.hovered&&(y=2,u=1);const[Y,N,E]=n.palette[n.community[i]],I=n.alpha[i],H=u*I;for(let B=0;B<6;B++){const j=i*6+B;z[j*2]=n.x[i],z[j*2+1]=n.y[i],T[j]=n.radius[i],_[j*3]=Y,_[j*3+1]=N,_[j*3+2]=E,it[j]=H,G[j]=y}}t.useProgram(f),t.uniform2f(t.getUniformLocation(f,"uResolution"),b,K),t.uniform2f(t.getUniformLocation(f,"uTranslate"),h.tx,h.ty),t.uniform1f(t.getUniformLocation(f,"uScale"),h.k*C),D(f,"aCorner",V,2),t.bindBuffer(t.ARRAY_BUFFER,tt),t.bufferData(t.ARRAY_BUFFER,z,t.DYNAMIC_DRAW),D(f,"aPos",tt,2),t.bindBuffer(t.ARRAY_BUFFER,Z),t.bufferData(t.ARRAY_BUFFER,T,t.DYNAMIC_DRAW),D(f,"aRadius",Z,1),t.bindBuffer(t.ARRAY_BUFFER,rt),t.bufferData(t.ARRAY_BUFFER,_,t.DYNAMIC_DRAW),D(f,"aColor",rt,3),t.bindBuffer(t.ARRAY_BUFFER,r),t.bufferData(t.ARRAY_BUFFER,it,t.DYNAMIC_DRAW),D(f,"aAlpha",r,1),t.bindBuffer(t.ARRAY_BUFFER,et),t.bufferData(t.ARRAY_BUFFER,G,t.DYNAMIC_DRAW),D(f,"aState",et,1),t.drawArrays(t.TRIANGLES,0,x*6)},screenToWorld(s,b){return{x:s/h.k-h.tx,y:b/h.k-h.ty}},worldToScreen(s,b){return{x:(s+h.tx)*h.k,y:(b+h.ty)*h.k}}}},window.GraphEngine=nt})();})();
@@ -3,155 +3,146 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Granite</title>
6
+ <title>Granite — Constellation</title>
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
9
+ <!-- Preload the three weights that paint above-the-fold content so titles
10
+ don't flash from Georgia to Fraunces after boot. -->
11
+ <link rel="preload" as="font" type="font/woff2" crossorigin
12
+ href="https://fonts.gstatic.com/s/fraunces/v38/6NU78FyLNQOQZAnv9bYEvDiIdE9Ea92uemAk_WBq8U_9v0c2Wa0KxC9TeP2Xz5c.woff2">
13
+ <link rel="preload" as="font" type="font/woff2" crossorigin
14
+ href="https://fonts.gstatic.com/s/manrope/v20/xn7gYHE41ni1AdIRggexSvfedN4.woff2">
15
+ <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300..700&family=Manrope:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
10
16
  <link rel="stylesheet" href="/style.css">
11
17
  </head>
12
18
  <body>
13
- <div id="app">
14
- <!-- Sidebar rail + panel -->
15
- <aside id="sidebar" class="expanded">
16
- <div id="sidebar-rail">
17
- <button id="brand-mark" title="Granite">
18
- <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
19
- <rect x="1" y="1" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.9"/>
20
- <rect x="11" y="1" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.5"/>
21
- <rect x="1" y="11" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.5"/>
22
- <rect x="11" y="11" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.3"/>
23
- </svg>
24
- </button>
25
- <button id="rail-search" class="rail-btn" title="Search (⌘K)">
26
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
27
- <circle cx="7" cy="7" r="4.5"/>
28
- <path d="M10.5 10.5L14 14"/>
29
- </svg>
30
- </button>
31
- <button id="rail-graph" class="rail-btn" title="Graph (⌘G)">
32
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
33
- <circle cx="4" cy="4" r="2"/>
34
- <circle cx="12" cy="4" r="2"/>
35
- <circle cx="8" cy="13" r="2"/>
36
- <path d="M5.5 5.5L7 11M10.5 5.5L9 11M6 4h4"/>
37
- </svg>
38
- </button>
39
- <button id="rail-new" class="rail-btn" title="New note (⌘N)">
40
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
41
- <path d="M8 3v10M3 8h10"/>
42
- </svg>
43
- </button>
44
- <div style="flex:1"></div>
45
- <button id="rail-toggle" class="rail-btn" title="Toggle sidebar">
46
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
47
- <rect x="1" y="2" width="14" height="12" rx="2"/>
48
- <path d="M5.5 2v12"/>
19
+ <div class="dirA" id="root">
20
+
21
+ <!-- Top bar -->
22
+ <header class="dirA-topbar">
23
+ <div class="dirA-brand" role="banner">
24
+ <div class="dirA-brand-mark" aria-hidden="true">
25
+ <svg width="22" height="22" viewBox="0 0 24 24">
26
+ <rect x="3" y="3" width="8" height="8" rx="1.6" fill="currentColor"/>
27
+ <rect x="13" y="3" width="8" height="8" rx="1.6" fill="currentColor" opacity="0.55"/>
28
+ <rect x="3" y="13" width="8" height="8" rx="1.6" fill="currentColor" opacity="0.35"/>
29
+ <rect x="13" y="13" width="8" height="8" rx="1.6" fill="currentColor" opacity="0.8"/>
49
30
  </svg>
50
- </button>
51
- </div>
52
- <div id="sidebar-panel">
53
- <!-- Search -->
54
- <div id="sidebar-search">
55
- <div id="search-wrapper">
56
- <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.4">
57
- <circle cx="7" cy="7" r="4.5"/>
58
- <path d="M10.5 10.5L14 14"/>
59
- </svg>
60
- <input type="text" id="search-box" placeholder="Search…" autocomplete="off" spellcheck="false">
61
- <kbd>⌘K</kbd>
62
- </div>
63
31
  </div>
64
- <div id="type-filters">
65
- <button class="type-chip active" data-type="">All</button>
32
+ <div>
33
+ <div class="dirA-brand-name">Granite</div>
34
+ <div class="dirA-brand-sub">Constellation · live</div>
66
35
  </div>
36
+ </div>
37
+ <button class="dirA-search" id="search-trigger" type="button" aria-label="Open search (⌘K)">
38
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
39
+ <circle cx="11" cy="11" r="7"/><path d="m20 20-3.6-3.6"/>
40
+ </svg>
41
+ <span class="dirA-search-text">Search the vault…</span>
42
+ <span class="kbd">⌘K</span>
43
+ </button>
44
+ <div class="dirA-filters" id="type-filters" role="tablist" aria-label="Filter by note type"></div>
45
+ </header>
67
46
 
68
- <!-- Note list (default view) -->
69
- <div id="note-list"></div>
47
+ <!-- Title + subtitle -->
48
+ <div class="dirA-title">
49
+ <div class="eyebrow">Knowledge constellation · live physics</div>
50
+ <h1>Your vault,<br><em>in motion.</em></h1>
51
+ <div class="sub" id="dirA-sub">Booting the physics engine…</div>
52
+ </div>
70
53
 
71
- <!-- Creation panel (hidden by default) -->
72
- <div id="create-panel" style="display:none">
73
- <div id="create-panel-header">
74
- <span class="create-panel-title">New note</span>
75
- <button id="create-panel-close" class="create-panel-close" title="Cancel">
76
- <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
77
- <path d="M4 4l8 8M12 4l-8 8"/>
78
- </svg>
79
- </button>
80
- </div>
81
- <div id="create-form">
82
- <label class="create-label">Type</label>
83
- <select id="create-type" class="create-select"></select>
84
- <label class="create-label">Title</label>
85
- <input type="text" id="create-title" class="create-input" placeholder="Note title…" autocomplete="off" spellcheck="false">
86
- <label class="create-label">Content</label>
87
- <textarea id="create-body" class="create-textarea" placeholder="Start writing… (markdown supported)" spellcheck="false"></textarea>
88
- <button id="create-submit" class="create-submit">Create note</button>
89
- </div>
90
- </div>
91
- </div>
92
- </aside>
54
+ <!-- Pulse stats -->
55
+ <div class="dirA-pulse" id="dirA-pulse">
56
+ <div class="dirA-pulse-stat"><div class="label">Notes</div><div class="value" id="stat-notes">—</div></div>
57
+ <div class="dirA-pulse-stat"><div class="label">Edges</div><div class="value" id="stat-edges">—</div></div>
58
+ <div class="dirA-pulse-stat"><div class="label">Communities</div><div class="value" id="stat-communities">—</div></div>
59
+ <div class="dirA-pulse-stat"><div class="label">Density</div><div class="value" id="stat-density">—</div></div>
60
+ </div>
61
+
62
+ <!-- Graph stage — WebGL canvas + label overlay -->
63
+ <div class="dirA-stage webgl" id="stage">
64
+ <canvas id="graph-canvas" class="dirA-canvas"></canvas>
65
+ <div id="graph-labels" class="dirA-labels"></div>
66
+ </div>
93
67
 
94
- <!-- Main content -->
95
- <main id="main">
96
- <!-- Note view -->
97
- <div id="content-wrapper">
98
- <header id="note-header">
99
- <div id="note-breadcrumb"></div>
100
- <h1 id="note-title"></h1>
101
- <div id="note-meta">
102
- <span id="note-type-badge" class="meta-badge"></span>
103
- <span id="note-date" class="meta-date"></span>
104
- <div id="note-tags"></div>
105
- </div>
106
- </header>
68
+ <!-- Zoom control -->
69
+ <div class="dirA-zoom" id="zoom-control">
70
+ <button id="zoom-out" title="Zoom out" type="button">−</button>
71
+ <span class="zoom-val" id="zoom-val">100%</span>
72
+ <button id="zoom-in" title="Zoom in" type="button">+</button>
73
+ <button id="zoom-reset" title="Reset view" type="button" style="font-size:0.64rem">⟲</button>
74
+ </div>
107
75
 
108
- <!-- HTML rendered note body (replaces canvas) -->
109
- <article id="note-body"></article>
76
+ <!-- Empty-state hint (shown when no reader open) -->
77
+ <div class="dirA-empty-hint" id="empty-hint">
78
+ <h3>Select a star</h3>
79
+ <p>Click any node to open. Drag to pull it around, scroll to zoom, use arrow keys to step through neighbours.</p>
80
+ </div>
110
81
 
111
- <!-- Backlinks -->
112
- <section id="backlinks-panel">
113
- <div id="backlinks-header">
114
- <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.5">
115
- <path d="M10 2L6 14M4 5L1 8l3 3M12 5l3 3-3 3"/>
116
- </svg>
117
- <span id="backlinks-count">0</span> references
118
- </div>
119
- <div id="backlinks-list"></div>
120
- </section>
82
+ <!-- Reader — hidden until a note is active -->
83
+ <aside class="dirA-reader" id="reader" hidden>
84
+ <div class="dirA-reader-progress" id="reader-progress"></div>
85
+ <div class="dirA-reader-resizer" id="reader-resizer"></div>
86
+ <div class="dirA-reader-chrome">
87
+ <nav class="dirA-reader-breadcrumb" id="reader-breadcrumb">
88
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
89
+ <path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/>
90
+ </svg>
91
+ </nav>
92
+ <div class="dirA-reader-nav">
93
+ <button class="dirA-nav-btn" id="reader-prev" type="button" title="Previous neighbour (←)">
94
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
95
+ </button>
96
+ <button class="dirA-nav-btn" id="reader-next" type="button" title="Next neighbour (→)">
97
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>
98
+ </button>
99
+ <button class="dirA-nav-btn close" id="reader-close" type="button" title="Close (Esc)">
100
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
101
+ </button>
102
+ </div>
121
103
  </div>
122
104
 
123
- <!-- Graph view -->
124
- <div id="graph-view" style="display:none">
125
- <canvas id="graph-canvas"></canvas>
126
- <div id="graph-tooltip"></div>
105
+ <div class="dirA-reader-body" id="reader-body">
106
+ <div class="dirA-reader-kicker" id="reader-kicker"></div>
107
+ <h1 class="dirA-reader-title" id="reader-title"></h1>
108
+ <p class="dirA-reader-deck" id="reader-deck"></p>
109
+ <div class="dirA-reader-metaline" id="reader-metaline"></div>
110
+ <div class="dirA-reader-article" id="reader-article"></div>
111
+ <div class="dirA-links-grid" id="reader-links"></div>
127
112
  </div>
113
+ </aside>
128
114
 
129
- <!-- Empty state -->
130
- <div id="empty-state">
131
- <div id="empty-icon">
132
- <svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="0.75" opacity="0.15">
133
- <circle cx="12" cy="12" r="4"/>
134
- <circle cx="28" cy="12" r="4"/>
135
- <circle cx="20" cy="30" r="4"/>
136
- <path d="M15.5 14L18 26M24.5 14L22 26M16 12h8" stroke-dasharray="2 3"/>
115
+ <!-- Footer hints -->
116
+ <footer class="dirA-footer">
117
+ <span><span class="kbd">⌘K</span>search</span>
118
+ <span class="sep"></span>
119
+ <span><span class="kbd">←</span><span class="kbd">→</span>traverse</span>
120
+ <span class="sep"></span>
121
+ <span><span class="kbd">Esc</span>close</span>
122
+ <span class="sep"></span>
123
+ <span>drag node · hover parts crowd · scroll zoom</span>
124
+ </footer>
125
+
126
+ <!-- Wikilink hover preview -->
127
+ <div class="wl-preview" id="wikilink-preview" hidden aria-hidden="true"></div>
128
+
129
+ <!-- ⌘K command palette overlay -->
130
+ <div id="command-bar-overlay" class="overlay hidden" role="dialog" aria-label="Search" aria-modal="true">
131
+ <div id="command-bar">
132
+ <div class="command-input-wrap">
133
+ <svg class="command-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
134
+ <circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/>
137
135
  </svg>
136
+ <input type="text" id="command-input" placeholder="Search notes…" autocomplete="off" spellcheck="false" aria-label="Search notes" role="combobox" aria-expanded="false" aria-controls="command-results" aria-autocomplete="list">
137
+ <kbd class="command-kbd">esc</kbd>
138
138
  </div>
139
- <p>Select a note to begin</p>
140
- <p class="empty-hint"><kbd>⌘K</kbd> search · <kbd>⌘N</kbd> new note · <kbd>⌘G</kbd> graph</p>
139
+ <div id="command-results" role="listbox" aria-label="Search results" aria-live="polite"></div>
141
140
  </div>
142
- </main>
143
- </div>
144
-
145
- <!-- Wikilink preview popup -->
146
- <div id="wikilink-preview" class="wikilink-preview">
147
- <div class="preview-type"></div>
148
- <div class="preview-title"></div>
149
- <div class="preview-snippet"></div>
150
- <div class="preview-tags"></div>
141
+ </div>
151
142
  </div>
152
143
 
144
+ <script src="/graph-engine.js"></script>
153
145
  <script src="/markdown-renderer.js"></script>
154
- <script src="/graph.js"></script>
155
146
  <script src="/app.js"></script>
156
147
  </body>
157
148
  </html>
@@ -1,189 +1 @@
1
- /**
2
- * Granite — HTML Markdown Renderer
3
- *
4
- * Converts markdown + wikilinks to semantic HTML.
5
- * Wikilinks become real <a> elements with hover previews.
6
- */
7
-
8
- const MarkdownRenderer = (() => {
9
-
10
- // ── Inline parser — handles [[wikilinks]], **bold**, *italic*, `code`, [links](url) ──
11
- function parseInline(text, outgoingLinks) {
12
- const re = /(\[\[([^\]]+)\]\])|(`([^`]+)`)|(\*\*(.+?)\*\*)|(\*(.+?)\*)|(\[([^\]]+)\]\(([^)]+)\))/g;
13
- let result = '';
14
- let lastIndex = 0;
15
- let match;
16
-
17
- while ((match = re.exec(text)) !== null) {
18
- if (match.index > lastIndex) {
19
- result += escapeHtml(text.slice(lastIndex, match.index));
20
- }
21
-
22
- if (match[1]) {
23
- // [[wikilink]] or [[target|display]]
24
- const inner = match[2];
25
- const parts = inner.split('|');
26
- const target = parts[0].trim();
27
- const display = parts.length > 1 ? parts[1].trim() : target;
28
- const linkInfo = outgoingLinks?.find(l => l.target === target);
29
- const resolved = linkInfo?.resolved ?? false;
30
- const slug = linkInfo?.resolved_slug || null;
31
-
32
- if (resolved && slug) {
33
- result += `<a class="wikilink" data-slug="${escapeAttr(slug)}" data-resolved="true" href="javascript:void(0)">${escapeHtml(display)}</a>`;
34
- } else {
35
- result += `<span class="wikilink-broken">${escapeHtml(display)}</span>`;
36
- }
37
- } else if (match[3]) {
38
- // `inline code`
39
- result += `<code class="inline-code">${escapeHtml(match[4])}</code>`;
40
- } else if (match[5]) {
41
- // **bold**
42
- result += `<strong>${escapeHtml(match[6])}</strong>`;
43
- } else if (match[7]) {
44
- // *italic*
45
- result += `<em>${escapeHtml(match[8])}</em>`;
46
- } else if (match[9]) {
47
- // [text](url)
48
- result += `<a class="external-link" href="${escapeAttr(match[11])}" target="_blank" rel="noopener">${escapeHtml(match[10])}</a>`;
49
- }
50
-
51
- lastIndex = match.index + match[0].length;
52
- }
53
-
54
- if (lastIndex < text.length) {
55
- result += escapeHtml(text.slice(lastIndex));
56
- }
57
-
58
- return result;
59
- }
60
-
61
- // ── Block-level markdown parser ──
62
- function render(body, outgoingLinks) {
63
- const lines = body.split('\n');
64
- const blocks = [];
65
- let inCodeBlock = false;
66
- let codeBuffer = [];
67
- let codeLang = '';
68
- let inList = false;
69
- let listType = null; // 'ul' or 'ol'
70
- let listItems = [];
71
-
72
- function flushList() {
73
- if (listItems.length > 0) {
74
- const tag = listType === 'ol' ? 'ol' : 'ul';
75
- blocks.push(`<${tag} class="md-list">${listItems.join('')}</${tag}>`);
76
- listItems = [];
77
- inList = false;
78
- listType = null;
79
- }
80
- }
81
-
82
- for (let i = 0; i < lines.length; i++) {
83
- const line = lines[i];
84
-
85
- // Code block toggle
86
- if (line.startsWith('```')) {
87
- if (inCodeBlock) {
88
- const langClass = codeLang ? ` data-lang="${escapeAttr(codeLang)}"` : '';
89
- const langLabel = codeLang ? `<span class="code-lang">${escapeHtml(codeLang)}</span>` : '';
90
- blocks.push(`<div class="code-block"${langClass}>${langLabel}<pre><code>${escapeHtml(codeBuffer.join('\n'))}</code></pre></div>`);
91
- codeBuffer = [];
92
- inCodeBlock = false;
93
- } else {
94
- flushList();
95
- inCodeBlock = true;
96
- codeLang = line.slice(3).trim();
97
- }
98
- continue;
99
- }
100
-
101
- if (inCodeBlock) {
102
- codeBuffer.push(line);
103
- continue;
104
- }
105
-
106
- // Blank line
107
- if (line.trim() === '') {
108
- flushList();
109
- continue;
110
- }
111
-
112
- // Headings
113
- const hMatch = line.match(/^(#{1,3})\s+(.+)/);
114
- if (hMatch) {
115
- flushList();
116
- const level = hMatch[1].length;
117
- const content = parseInline(hMatch[2], outgoingLinks);
118
- blocks.push(`<h${level} class="md-h${level}">${content}</h${level}>`);
119
- continue;
120
- }
121
-
122
- // Horizontal rule
123
- if (/^(-{3,}|\*{3,}|_{3,})$/.test(line.trim())) {
124
- flushList();
125
- blocks.push('<hr class="md-hr">');
126
- continue;
127
- }
128
-
129
- // Blockquote
130
- if (line.startsWith('>')) {
131
- flushList();
132
- const text = line.replace(/^>\s?/, '');
133
- const content = parseInline(text, outgoingLinks);
134
- blocks.push(`<blockquote class="md-blockquote">${content}</blockquote>`);
135
- continue;
136
- }
137
-
138
- // Unordered list
139
- const bulletMatch = line.match(/^(\s*)[*\-+]\s+(.+)/);
140
- if (bulletMatch) {
141
- if (listType !== 'ul') flushList();
142
- inList = true;
143
- listType = 'ul';
144
- const content = parseInline(bulletMatch[2], outgoingLinks);
145
- listItems.push(`<li>${content}</li>`);
146
- continue;
147
- }
148
-
149
- // Ordered list
150
- const numMatch = line.match(/^(\s*)\d+\.\s+(.+)/);
151
- if (numMatch) {
152
- if (listType !== 'ol') flushList();
153
- inList = true;
154
- listType = 'ol';
155
- const content = parseInline(numMatch[2], outgoingLinks);
156
- listItems.push(`<li>${content}</li>`);
157
- continue;
158
- }
159
-
160
- // Body paragraph
161
- flushList();
162
- const content = parseInline(line, outgoingLinks);
163
- blocks.push(`<p class="md-body">${content}</p>`);
164
- }
165
-
166
- // Flush remaining
167
- flushList();
168
- if (inCodeBlock && codeBuffer.length > 0) {
169
- blocks.push(`<div class="code-block"><pre><code>${escapeHtml(codeBuffer.join('\n'))}</code></pre></div>`);
170
- }
171
-
172
- return blocks.join('\n');
173
- }
174
-
175
- // ── Helpers ──
176
- function escapeHtml(str) {
177
- return str
178
- .replace(/&/g, '&amp;')
179
- .replace(/</g, '&lt;')
180
- .replace(/>/g, '&gt;')
181
- .replace(/"/g, '&quot;');
182
- }
183
-
184
- function escapeAttr(str) {
185
- return str.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
186
- }
187
-
188
- return { render, parseInline, escapeHtml };
189
- })();
1
+ "use strict";(()=>{})();