non-linear-docs/non-linear.html

1174 lines
44 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Non-Linear — Layered View</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
--bg: #0c0d10;
--bg-1: #111216;
--bg-2: #16181d;
--bg-3: #1c1f26;
--bg-4: #232730;
--line: #23262d;
--line-2: #2c3038;
--text: #e6e7ea;
--text-dim: #9aa0aa;
--text-mute: #5f6571;
--accent: #7c8cff; /* indigo */
--accent-2: #9fb1ff;
--accent-soft: rgba(124,140,255,0.16);
--accent-line: rgba(124,140,255,0.55);
--ancestor: #f4b860; /* warm */
--ancestor-soft: rgba(244,184,96,0.14);
--ancestor-line: rgba(244,184,96,0.55);
--descendant: #6ed1a4; /* mint */
--descendant-soft: rgba(110,209,164,0.14);
--descendant-line: rgba(110,209,164,0.55);
--shadow: 0 6px 24px rgba(0,0,0,0.35);
--st-todo: #6c7280;
--st-prog: #f4b860;
--st-rev: #7c8cff;
--st-done: #6ed1a4;
--st-block: #e08197;
--st-back: #4d525c;
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
background: var(--bg);
color: var(--text);
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 13px;
line-height: 1.45;
-webkit-font-smoothing: antialiased;
height: 100vh;
overflow: hidden;
}
.mono { font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace; }
/* ====== APP SHELL ====== */
.app {
display: grid;
grid-template-columns: 56px 240px 1fr;
grid-template-rows: 44px 1fr;
grid-template-areas:
"rail topbar topbar"
"rail sidebar canvas";
height: 100vh;
}
/* Top bar */
.topbar {
grid-area: topbar;
border-bottom: 1px solid var(--line);
display: flex;
align-items: center;
padding: 0 14px;
gap: 14px;
background: var(--bg);
position: relative;
z-index: 5;
}
.breadcrumb {
display: flex; align-items: center; gap: 6px;
color: var(--text-dim);
font-size: 12.5px;
}
.breadcrumb .sep { color: var(--text-mute); }
.breadcrumb .here { color: var(--text); font-weight: 500; }
.breadcrumb .proj-dot {
width: 14px; height: 14px; border-radius: 4px;
background: linear-gradient(135deg, #7c8cff, #6ed1a4);
display: inline-block;
}
.topbar .spacer { flex: 1; }
.topbar .layer-toggles {
display: flex; align-items: center; gap: 0;
border: 1px solid var(--line);
border-radius: 7px;
padding: 2px;
background: var(--bg-1);
}
.layer-toggle {
font-size: 11.5px;
padding: 3px 9px;
border-radius: 5px;
color: var(--text-dim);
cursor: pointer;
user-select: none;
display: flex; align-items: center; gap: 5px;
}
.layer-toggle .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-mute); }
.layer-toggle.active { background: var(--bg-3); color: var(--text); }
.layer-toggle.active .dot { background: var(--accent); box-shadow: 0 0 0 2px rgba(124,140,255,0.18); }
.layer-toggle.disabled { opacity: 0.5; cursor: not-allowed; }
.pill-btn {
border: 1px solid var(--line);
background: var(--bg-1);
color: var(--text-dim);
border-radius: 6px;
padding: 4px 9px;
font-size: 12px;
cursor: pointer;
display: flex; align-items: center; gap: 6px;
}
.pill-btn:hover { color: var(--text); border-color: var(--line-2); }
.kbd {
font-family: ui-monospace, monospace;
font-size: 10.5px;
color: var(--text-mute);
border: 1px solid var(--line);
background: var(--bg-2);
padding: 1px 5px;
border-radius: 4px;
}
/* Left rail */
.rail {
grid-area: rail;
border-right: 1px solid var(--line);
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0;
gap: 4px;
}
.rail .logo {
width: 30px; height: 30px;
border-radius: 8px;
background: linear-gradient(135deg, #1f2330, #0c0d10);
border: 1px solid var(--line-2);
display: grid; place-items: center;
margin-bottom: 8px;
position: relative;
}
.rail .logo svg { width: 18px; height: 18px; }
.rail-btn {
width: 34px; height: 34px;
border-radius: 8px;
display: grid; place-items: center;
color: var(--text-mute);
cursor: pointer;
position: relative;
}
.rail-btn:hover { color: var(--text); background: var(--bg-2); }
.rail-btn.active { color: var(--text); background: var(--bg-3); }
.rail-btn.active::before {
content:""; position: absolute; left: -8px; top: 8px; bottom: 8px; width: 2px;
background: var(--accent); border-radius: 2px;
}
.rail-btn svg { width: 16px; height: 16px; }
.rail .grow { flex: 1; }
/* Sidebar */
.sidebar {
grid-area: sidebar;
border-right: 1px solid var(--line);
background: var(--bg);
overflow-y: auto;
padding: 10px 8px 16px;
}
.side-section { margin-top: 12px; }
.side-section:first-child { margin-top: 4px; }
.side-h {
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-mute);
padding: 6px 8px 4px;
display: flex; justify-content: space-between; align-items: center;
}
.side-row {
display: flex; align-items: center; gap: 8px;
padding: 4px 8px;
border-radius: 6px;
color: var(--text-dim);
font-size: 12.5px;
cursor: pointer;
user-select: none;
}
.side-row:hover { background: var(--bg-2); color: var(--text); }
.side-row.active { background: var(--bg-3); color: var(--text); }
.side-row svg { width: 13px; height: 13px; opacity: 0.85; }
.side-row .badge {
margin-left: auto; font-size: 10.5px; color: var(--text-mute);
}
.side-tree { padding-left: 4px; }
.tree-row {
display: flex; align-items: center; gap: 6px;
padding: 3px 8px;
border-radius: 5px;
color: var(--text-dim);
font-size: 12px;
cursor: pointer;
line-height: 1.3;
}
.tree-row:hover { background: var(--bg-2); color: var(--text); }
.tree-row .chev { width: 10px; color: var(--text-mute); }
.tree-row .ico { width: 12px; height: 12px; flex: none; }
.tree-row .id { color: var(--text-mute); font-family: ui-monospace, monospace; font-size: 10.5px; margin-left: auto; }
.tree-row.active { background: var(--accent-soft); color: var(--text); }
/* ====== CANVAS ====== */
.canvas-wrap {
grid-area: canvas;
position: relative;
overflow: hidden;
background:
radial-gradient(1200px 700px at 30% -10%, rgba(124,140,255,0.06), transparent 60%),
var(--bg);
}
.canvas {
position: absolute; inset: 0;
overflow-x: auto;
overflow-y: hidden;
padding: 18px 18px 56px;
}
.columns {
display: flex;
gap: 14px;
height: 100%;
align-items: stretch;
min-width: max-content;
}
.column {
display: flex;
flex-direction: column;
width: 320px;
min-width: 320px;
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: 10px;
overflow: hidden;
height: 100%;
flex: none;
}
.column.wide { width: 360px; min-width: 360px; }
.col-head {
padding: 10px 12px;
border-bottom: 1px solid var(--line);
display: flex; align-items: center; gap: 8px;
background: var(--bg-1);
}
.col-head .lvl {
font-size: 10px;
color: var(--text-mute);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.col-head .title {
font-weight: 600;
font-size: 12.5px;
color: var(--text);
}
.col-head .meta {
margin-left: auto; font-size: 11px; color: var(--text-mute);
display: flex; align-items: center; gap: 6px;
}
.col-head .arrow {
width: 14px; height: 14px; color: var(--text-mute);
}
.col-body {
flex: 1;
overflow-y: auto;
padding: 8px;
display: flex; flex-direction: column; gap: 6px;
}
/* Group label inside a column */
.group-label {
font-size: 10.5px;
color: var(--text-mute);
padding: 8px 6px 2px;
text-transform: uppercase;
letter-spacing: 0.08em;
display: flex; align-items: center; gap: 6px;
}
.group-label::after {
content:""; flex:1; height:1px; background: var(--line);
}
/* Node card */
.node {
--node-accent: var(--text-mute);
border: 1px solid var(--line);
border-radius: 8px;
padding: 9px 10px;
background: var(--bg-2);
cursor: pointer;
transition: background 120ms, border-color 120ms, box-shadow 120ms, transform 120ms;
position: relative;
}
.node:hover { background: var(--bg-3); border-color: var(--line-2); }
.node .top {
display: flex; align-items: center; gap: 8px; margin-bottom: 4px;
}
.node .id {
font-family: ui-monospace, monospace;
font-size: 10.5px;
color: var(--text-mute);
}
.node .title-row {
display: flex; align-items: flex-start; gap: 8px;
}
.node .title-row .ico { flex: none; margin-top: 2px; }
.node .title {
font-size: 12.5px;
color: var(--text);
line-height: 1.35;
flex: 1;
}
.node .title b { font-weight: 600; }
.node .meta {
margin-top: 6px;
display: flex; align-items: center; gap: 8px;
color: var(--text-mute);
font-size: 11px;
}
.node .labels { display: flex; gap: 4px; flex-wrap: wrap; }
.label {
font-size: 10px;
padding: 1px 6px;
border-radius: 9px;
background: var(--bg-4);
color: var(--text-dim);
border: 1px solid var(--line-2);
line-height: 1.5;
}
.label.p0 { color: #f1a3b3; background: rgba(224,129,151,0.1); border-color: rgba(224,129,151,0.3); }
.label.bug { color: #f1a3b3; background: rgba(224,129,151,0.1); border-color: rgba(224,129,151,0.3); }
.label.feat { color: var(--accent-2); background: var(--accent-soft); border-color: var(--accent-line); }
.label.chore { color: var(--text-dim); }
.label.frontend { color: #d8b6ff; background: rgba(180,140,255,0.1); border-color: rgba(180,140,255,0.3); }
.label.backend { color: #98ddff; background: rgba(120,200,255,0.1); border-color: rgba(120,200,255,0.3); }
.label.infra { color: #f4cf86; background: rgba(244,184,96,0.1); border-color: rgba(244,184,96,0.3); }
.avatar {
width: 16px; height: 16px; border-radius: 50%;
background: var(--bg-4);
display: inline-grid; place-items: center;
font-size: 9px; color: var(--text);
border: 1px solid var(--line-2);
}
.avatar.a-jori { background: linear-gradient(135deg,#7c8cff,#5563d6); }
.avatar.a-mira { background: linear-gradient(135deg,#6ed1a4,#3aa178); }
.avatar.a-karri { background: linear-gradient(135deg,#f4b860,#c98933); }
.avatar.a-codex { background: linear-gradient(135deg,#1c1f26,#0c0d10); border-color: var(--line-2); position: relative; }
.avatar.a-codex::after { content: "★"; font-size: 8px; color: var(--accent-2); }
.avatar.a-triage { background: linear-gradient(135deg,#e08197,#9b3a52); }
.avatar.bot { color: var(--accent-2); }
/* Status dot */
.st {
width: 12px; height: 12px; border-radius: 50%;
border: 2px solid var(--st-todo);
background: transparent;
flex: none;
position: relative;
}
.st.todo { border-color: var(--st-todo); }
.st.prog { border-color: var(--st-prog); background: conic-gradient(var(--st-prog) 60%, transparent 0); }
.st.rev { border-color: var(--st-rev); background: conic-gradient(var(--st-rev) 75%, transparent 0); }
.st.done { border-color: var(--st-done); background: var(--st-done); }
.st.done::after { content:""; position:absolute; left:2px; top: 1px; width:5px; height:2.5px; border-left:1.5px solid var(--bg-1); border-bottom: 1.5px solid var(--bg-1); transform: rotate(-45deg); }
.st.back { border-color: var(--st-back); border-style: dashed; }
.st.cancel { border-color: var(--st-back); background: var(--st-back); position: relative; }
.st.cancel::after { content:"×"; position:absolute; inset:0; display:grid; place-items:center; font-size: 10px; color: var(--bg-1); line-height: 1; }
/* Component icon (cube) */
.cube { color: var(--text-dim); }
.node[data-kind="component"] .cube { color: var(--accent-2); }
/* Connector lines from each card to the column on the right (children expansion indicator) */
.node .child-handle {
position: absolute;
right: -8px; top: 50%; transform: translateY(-50%);
width: 14px; height: 14px;
border-radius: 50%;
background: var(--bg-1);
border: 1px solid var(--line-2);
display: grid; place-items: center;
color: var(--text-mute);
font-size: 10px;
opacity: 0;
transition: opacity 120ms;
pointer-events: none;
}
.node:hover .child-handle { opacity: 1; }
/* Highlight states */
.node.is-hover {
background: var(--bg-3);
border-color: var(--accent-line);
box-shadow: 0 0 0 1px var(--accent-line), 0 6px 16px rgba(0,0,0,0.4);
}
.node.is-ancestor {
border-color: var(--ancestor-line);
background: var(--ancestor-soft);
}
.node.is-ancestor .id, .node.is-ancestor .title { color: var(--text); }
.node.is-descendant {
border-color: var(--descendant-line);
background: var(--descendant-soft);
}
.node.is-dim {
opacity: 0.32;
}
/* Lineage edges layer */
.edges {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 3;
}
.edges path {
fill: none;
stroke-linecap: round;
}
/* Bottom mini overview / horizontal scroll indicator */
.minimap {
position: absolute;
left: 0; right: 0; bottom: 0;
height: 38px;
border-top: 1px solid var(--line);
background: linear-gradient(180deg, rgba(12,13,16,0) 0%, var(--bg) 30%);
display: flex; align-items: center;
padding: 0 14px;
gap: 12px;
z-index: 4;
}
.minimap .arrow-btn {
width: 22px; height: 22px;
border-radius: 6px;
border: 1px solid var(--line);
background: var(--bg-1);
display: grid; place-items: center;
color: var(--text-dim);
cursor: pointer;
}
.minimap .arrow-btn:hover { color: var(--text); }
.mini-track {
flex: 1;
height: 8px;
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: 5px;
position: relative;
overflow: hidden;
}
.mini-track .blocks {
position: absolute; inset: 0;
display: flex; padding: 1px; gap: 2px;
}
.mini-track .blocks > div {
flex: 1;
border-radius: 2px;
background: var(--bg-3);
}
.mini-track .blocks > div.viewing { background: var(--accent); opacity: 0.7; }
.mini-track .blocks > div.viewing.partial { background: var(--accent); opacity: 0.35; }
.mini-label { font-size: 11px; color: var(--text-mute); }
/* Direction indicator above columns */
.axis-strip {
position: absolute;
left: 18px; right: 18px; top: 6px;
display: flex; justify-content: space-between;
color: var(--text-mute);
font-size: 10.5px;
pointer-events: none;
text-transform: uppercase; letter-spacing: 0.1em;
z-index: 1;
}
/* Detail rail */
.detail {
position: absolute;
top: 18px; right: 18px; bottom: 56px;
width: 320px;
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: 10px;
box-shadow: var(--shadow);
overflow: hidden;
display: flex; flex-direction: column;
z-index: 6;
}
.detail-head {
padding: 12px 14px 10px;
border-bottom: 1px solid var(--line);
display: flex; align-items: center; gap: 8px;
}
.detail-head .id { color: var(--text-mute); font-family: ui-monospace,monospace; font-size: 11px; }
.detail-head .x { margin-left:auto; cursor:pointer; color: var(--text-mute); }
.detail-body { padding: 14px; overflow-y: auto; }
.detail-body h3 { margin: 0 0 8px; font-size: 15px; font-weight: 600; }
.detail-body p { color: var(--text-dim); margin: 0 0 12px; font-size: 12.5px; }
.meta-grid {
display: grid;
grid-template-columns: 80px 1fr;
gap: 6px 10px;
font-size: 11.5px;
}
.meta-grid .k { color: var(--text-mute); }
.meta-grid .v { color: var(--text); display: flex; align-items: center; gap: 6px; }
/* Scrollbars */
.canvas::-webkit-scrollbar, .col-body::-webkit-scrollbar, .sidebar::-webkit-scrollbar, .detail-body::-webkit-scrollbar {
height: 10px; width: 10px;
}
.canvas::-webkit-scrollbar-thumb, .col-body::-webkit-scrollbar-thumb, .sidebar::-webkit-scrollbar-thumb, .detail-body::-webkit-scrollbar-thumb {
background: var(--bg-3); border-radius: 6px; border: 2px solid var(--bg);
}
.canvas::-webkit-scrollbar-track, .col-body::-webkit-scrollbar-track { background: transparent; }
/* Add child button */
.add-row {
margin-top: 4px;
display: flex; align-items: center; gap: 8px;
color: var(--text-mute);
font-size: 11.5px;
padding: 6px 8px;
border: 1px dashed var(--line-2);
border-radius: 7px;
cursor: pointer;
}
.add-row:hover { color: var(--text); border-color: var(--accent-line); background: var(--accent-soft); }
/* Edge legend (shown when hovering) */
.legend {
position: absolute;
bottom: 46px; right: 18px;
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: 8px;
padding: 8px 10px;
display: flex; gap: 14px;
font-size: 11px;
color: var(--text-dim);
box-shadow: var(--shadow);
z-index: 4;
opacity: 0;
transform: translateY(4px);
transition: opacity 140ms, transform 140ms;
pointer-events: none;
}
.legend.visible { opacity: 1; transform: translateY(0); }
.legend .swatch { display: inline-block; width: 18px; height: 2px; margin-right: 6px; vertical-align: middle; border-radius: 2px; }
</style>
</head>
<body>
<!-- Icon defs -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<symbol id="i-cube" viewBox="0 0 16 16"><path d="M8 1.5l5.5 3v7L8 14.5 2.5 11.5v-7L8 1.5zM8 1.5v6m0 0L2.5 4.5M8 7.5l5.5-3M8 7.5v7" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></symbol>
<symbol id="i-folder" viewBox="0 0 16 16"><path d="M2 4.5A1.5 1.5 0 0 1 3.5 3h2.7l1.4 1.5h5A1.5 1.5 0 0 1 14 6v6.5A1.5 1.5 0 0 1 12.5 14h-9A1.5 1.5 0 0 1 2 12.5v-8z" fill="none" stroke="currentColor" stroke-width="1.2"/></symbol>
<symbol id="i-issue" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="1.2"/><circle cx="8" cy="8" r="2" fill="currentColor"/></symbol>
<symbol id="i-search" viewBox="0 0 16 16"><circle cx="7" cy="7" r="4.5" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M10.5 10.5L14 14" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></symbol>
<symbol id="i-plus" viewBox="0 0 16 16"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></symbol>
<symbol id="i-arrow-right" viewBox="0 0 16 16"><path d="M3 8h9m0 0L8.5 4.5M12 8L8.5 11.5" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="i-arrow-left" viewBox="0 0 16 16"><path d="M13 8H4m0 0L7.5 4.5M4 8l3.5 3.5" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="i-chev" viewBox="0 0 16 16"><path d="M5 4l5 4-5 4" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="i-inbox" viewBox="0 0 16 16"><path d="M2 8.5L4 3h8l2 5.5V13H2zM2 8.5h3.5l1 1.5h3l1-1.5H14" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></symbol>
<symbol id="i-graph" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.5" fill="currentColor"/><circle cx="13" cy="4" r="1.5" fill="currentColor"/><circle cx="13" cy="12" r="1.5" fill="currentColor"/><path d="M4.5 8L11.5 4M4.5 8l7 4" stroke="currentColor" stroke-width="1.2"/></symbol>
<symbol id="i-cycle" viewBox="0 0 16 16"><path d="M3 8a5 5 0 0 1 9-3M13 8a5 5 0 0 1-9 3" fill="none" stroke="currentColor" stroke-width="1.2"/><path d="M11 2v3h-3M5 14v-3h3" fill="none" stroke="currentColor" stroke-width="1.2"/></symbol>
<symbol id="i-list" viewBox="0 0 16 16"><path d="M3 4h10M3 8h10M3 12h10" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></symbol>
<symbol id="i-settings" viewBox="0 0 16 16"><circle cx="8" cy="8" r="2" fill="none" stroke="currentColor" stroke-width="1.2"/><path d="M8 1.5v2m0 9v2M1.5 8h2m9 0h2M3.3 3.3l1.4 1.4m6.6 6.6l1.4 1.4M3.3 12.7l1.4-1.4m6.6-6.6l1.4-1.4" stroke="currentColor" stroke-width="1.1"/></symbol>
<symbol id="i-palette" viewBox="0 0 16 16"><path d="M8 14a6 6 0 1 1 6-6 2 2 0 0 1-2 2H9.5a1 1 0 0 0-.7 1.7l.4.4A1.4 1.4 0 0 1 8 14z" fill="none" stroke="currentColor" stroke-width="1.2"/></symbol>
<symbol id="i-link" viewBox="0 0 16 16"><path d="M6.5 9.5a3 3 0 0 1 0-4l1.5-1.5a3 3 0 0 1 4 4M9.5 6.5a3 3 0 0 1 0 4L8 12a3 3 0 0 1-4-4" fill="none" stroke="currentColor" stroke-width="1.2"/></symbol>
<symbol id="i-x" viewBox="0 0 16 16"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></symbol>
<symbol id="i-bot" viewBox="0 0 16 16"><rect x="3" y="5" width="10" height="8" rx="2" fill="none" stroke="currentColor" stroke-width="1.2"/><circle cx="6.5" cy="9" r="1" fill="currentColor"/><circle cx="9.5" cy="9" r="1" fill="currentColor"/><path d="M8 5V3" stroke="currentColor" stroke-width="1.2"/></symbol>
</defs>
</svg>
<div class="app">
<!-- Rail -->
<aside class="rail">
<div class="logo" title="Non-Linear">
<svg viewBox="0 0 24 24" fill="none">
<path d="M4 18 L 10 12 L 14 16 L 20 6" stroke="url(#g)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="4" cy="18" r="1.6" fill="#7c8cff"/>
<circle cx="20" cy="6" r="1.6" fill="#6ed1a4"/>
<defs><linearGradient id="g" x1="0" x2="24"><stop stop-color="#7c8cff"/><stop offset="1" stop-color="#6ed1a4"/></linearGradient></defs>
</svg>
</div>
<div class="rail-btn" title="Inbox"><svg><use href="#i-inbox"/></svg></div>
<div class="rail-btn active" title="Layered View"><svg><use href="#i-graph"/></svg></div>
<div class="rail-btn" title="List / Board"><svg><use href="#i-list"/></svg></div>
<div class="rail-btn" title="Cycles"><svg><use href="#i-cycle"/></svg></div>
<div class="grow"></div>
<div class="rail-btn" title="Settings"><svg><use href="#i-settings"/></svg></div>
</aside>
<!-- Topbar -->
<header class="topbar">
<div class="breadcrumb">
<span class="proj-dot"></span>
<span>Aurora</span>
<span class="sep">/</span>
<span>Backend</span>
<span class="sep">/</span>
<span class="here">Layered View</span>
</div>
<div class="spacer"></div>
<div class="layer-toggles" role="tablist" title="Layer visibility (Alt+1…4)">
<div class="layer-toggle active"><span class="dot"></span>Structure</div>
<div class="layer-toggle active"><span class="dot"></span>Work</div>
<div class="layer-toggle disabled"><span class="dot"></span>Connections</div>
<div class="layer-toggle disabled"><span class="dot"></span>Artifacts</div>
</div>
<button class="pill-btn" title="Filter"><svg width="12" height="12"><use href="#i-list"/></svg>Filter</button>
<button class="pill-btn" title="Search"><svg width="12" height="12"><use href="#i-search"/></svg>Search<span class="kbd">⌘K</span></button>
</header>
<!-- Sidebar -->
<aside class="sidebar">
<div class="side-section">
<div class="side-row"><svg><use href="#i-inbox"/></svg>Triage Inbox<span class="badge">12</span></div>
<div class="side-row"><svg><use href="#i-cycle"/></svg>Cycle 7<span class="badge">23</span></div>
<div class="side-row active"><svg><use href="#i-graph"/></svg>Layered View</div>
<div class="side-row"><svg><use href="#i-list"/></svg>All issues<span class="badge">145</span></div>
</div>
<div class="side-section">
<div class="side-h">Projects <span>+</span></div>
<div class="side-tree">
<div class="tree-row" style="font-weight:500;color:var(--text)">
<svg class="chev" viewBox="0 0 16 16"><path d="M5 4l5 4-5 4" fill="none" stroke="currentColor" stroke-width="1.2"/></svg>
<svg class="ico"><use href="#i-cube"/></svg>
Aurora SaaS
</div>
<div style="padding-left:14px">
<div class="tree-row">
<svg class="chev" viewBox="0 0 16 16" style="transform:rotate(90deg)"><path d="M5 4l5 4-5 4" fill="none" stroke="currentColor" stroke-width="1.2"/></svg>
<svg class="ico" style="color:var(--accent-2)"><use href="#i-cube"/></svg>
Backend <span class="id">C-04</span>
</div>
<div style="padding-left:14px">
<div class="tree-row active">
<span class="chev"></span>
<svg class="ico" style="color:var(--accent-2)"><use href="#i-cube"/></svg>
auth-service <span class="id">C-12</span>
</div>
<div class="tree-row">
<span class="chev"></span>
<svg class="ico" style="color:var(--accent-2)"><use href="#i-cube"/></svg>
billing-service <span class="id">C-18</span>
</div>
<div class="tree-row">
<span class="chev"></span>
<svg class="ico" style="color:var(--accent-2)"><use href="#i-cube"/></svg>
notifications <span class="id">C-21</span>
</div>
</div>
<div class="tree-row">
<svg class="chev" viewBox="0 0 16 16"><path d="M5 4l5 4-5 4" fill="none" stroke="currentColor" stroke-width="1.2"/></svg>
<svg class="ico" style="color:var(--accent-2)"><use href="#i-cube"/></svg>
Frontend <span class="id">C-05</span>
</div>
<div class="tree-row">
<svg class="chev" viewBox="0 0 16 16"><path d="M5 4l5 4-5 4" fill="none" stroke="currentColor" stroke-width="1.2"/></svg>
<svg class="ico" style="color:var(--accent-2)"><use href="#i-cube"/></svg>
Infra <span class="id">C-06</span>
</div>
</div>
</div>
</div>
<div class="side-section">
<div class="side-h">Favorites</div>
<div class="side-row"><svg><use href="#i-issue"/></svg>NL-203 OAuth refresh</div>
<div class="side-row"><svg><use href="#i-cube"/></svg>auth-service</div>
<div class="side-row"><svg><use href="#i-bot"/></svg>Triage Agent</div>
</div>
</aside>
<!-- Canvas -->
<main class="canvas-wrap">
<div class="axis-strip">
<div>← lower (children)</div>
<div>higher (parents) →</div>
</div>
<div class="canvas" id="canvas">
<div class="columns" id="columns">
<!-- Columns are rendered by JS -->
</div>
<svg class="edges" id="edges" preserveAspectRatio="none"></svg>
</div>
<div class="legend" id="legend">
<div><span class="swatch" style="background:var(--ancestor)"></span>parents (toward right)</div>
<div><span class="swatch" style="background:var(--accent)"></span>focused</div>
<div><span class="swatch" style="background:var(--descendant)"></span>children (toward left)</div>
</div>
<div class="minimap">
<div class="arrow-btn" id="scroll-left" title="Scroll left"><svg width="12" height="12"><use href="#i-arrow-left"/></svg></div>
<div class="mini-track" id="minitrack">
<div class="blocks" id="miniblocks"></div>
</div>
<div class="arrow-btn" id="scroll-right" title="Scroll right"><svg width="12" height="12"><use href="#i-arrow-right"/></svg></div>
<div class="mini-label" id="mini-label">depth 0 → 5</div>
</div>
<!-- Detail panel -->
<aside class="detail" id="detail" style="display:none">
<div class="detail-head">
<span class="st prog"></span>
<span class="id">NL-203</span>
<span class="x" id="detail-x"><svg width="12" height="12"><use href="#i-x"/></svg></span>
</div>
<div class="detail-body">
<h3>Implement refresh-token rotation</h3>
<p>Rotate refresh tokens on every use. Detect reuse and revoke the entire family. Required to ship enterprise SSO.</p>
<div class="meta-grid">
<div class="k">Status</div> <div class="v"><span class="st prog"></span> In Progress</div>
<div class="k">Assignee</div> <div class="v"><span class="avatar a-jori">J</span> jori</div>
<div class="k">Parent</div> <div class="v"><svg width="12" height="12" style="color:var(--accent-2)"><use href="#i-cube"/></svg> oauth-module</div>
<div class="k">Cycle</div> <div class="v">Cycle 7 · ends Fri</div>
<div class="k">Labels</div> <div class="v"><span class="label feat">feature</span><span class="label backend">backend</span><span class="label p0">p0</span></div>
<div class="k">Blocked by</div> <div class="v">NL-198 (token store schema)</div>
<div class="k">Blocks</div> <div class="v">NL-241, NL-258</div>
<div class="k">Repo</div> <div class="v mono" style="font-size:11px">aurora/auth-service · /oauth</div>
</div>
</div>
</aside>
</main>
</div>
<!-- ====== DATA + LOGIC ====== -->
<script>
/* ---------- Graph data ---------- */
/*
Each node: { id, kind, title, parent, status?, labels?, assignee?, repo?, depth }
Columns are arranged by depth (lower-numbered depth = root, on the right; higher depth = leaves, on the left).
In the UI, leftmost column = lowest layer (children), rightmost = highest (root parent).
*/
const NODES = [
// Root
{ id:'C-00', kind:'component', title:'Aurora SaaS', depth:0, parent:null,
labels:[], owner:'jori', repo:'github.com/aurora' },
// Depth 1 — top-level components
{ id:'C-04', kind:'component', title:'Backend', depth:1, parent:'C-00', labels:['backend'], owner:'mira' },
{ id:'C-05', kind:'component', title:'Frontend', depth:1, parent:'C-00', labels:['frontend'], owner:'karri' },
{ id:'C-06', kind:'component', title:'Infra', depth:1, parent:'C-00', labels:['infra'], owner:'codex' },
// Depth 2 — services
{ id:'C-12', kind:'component', title:'auth-service', depth:2, parent:'C-04', labels:['backend'], owner:'jori', repo:'aurora/auth-service' },
{ id:'C-18', kind:'component', title:'billing-service', depth:2, parent:'C-04', labels:['backend'], owner:'mira', repo:'aurora/billing' },
{ id:'C-21', kind:'component', title:'notifications', depth:2, parent:'C-04', labels:['backend'], owner:'codex', repo:'aurora/notify' },
{ id:'C-31', kind:'component', title:'web-app', depth:2, parent:'C-05', labels:['frontend'], owner:'karri' },
{ id:'C-32', kind:'component', title:'design-system', depth:2, parent:'C-05', labels:['frontend'], owner:'karri' },
// Depth 3 — modules
{ id:'C-44', kind:'component', title:'oauth-module', depth:3, parent:'C-12', labels:['backend'], owner:'jori', repo:'aurora/auth-service /oauth' },
{ id:'C-45', kind:'component', title:'session-manager', depth:3, parent:'C-12', labels:['backend'], owner:'jori' },
{ id:'C-46', kind:'component', title:'mfa-module', depth:3, parent:'C-12', labels:['backend'], owner:'mira' },
// Depth 4 — issues attached to oauth-module
{ id:'NL-203', kind:'issue', title:'Implement refresh-token rotation', depth:4, parent:'C-44',
status:'prog', labels:['feature','backend','p0'], assignee:'jori' },
{ id:'NL-204', kind:'issue', title:'Reject reused refresh tokens', depth:4, parent:'C-44',
status:'todo', labels:['feature','backend'], assignee:'jori' },
{ id:'NL-205', kind:'issue', title:'OAuth callback fails on Safari 17', depth:4, parent:'C-44',
status:'rev', labels:['bug','frontend'], assignee:'karri' },
{ id:'NL-206', kind:'issue', title:'Document PKCE flow for SDK consumers', depth:4, parent:'C-44',
status:'back', labels:['chore'], assignee:'mira' },
// Issues under session-manager
{ id:'NL-241', kind:'issue', title:'Add Redis-backed session store', depth:4, parent:'C-45',
status:'prog', labels:['feature','backend'], assignee:'codex' },
{ id:'NL-242', kind:'issue', title:'Idle timeout config per workspace', depth:4, parent:'C-45',
status:'todo', labels:['feature'], assignee:'jori' },
// Issues under mfa-module
{ id:'NL-258', kind:'issue', title:'TOTP enrollment screen', depth:4, parent:'C-46',
status:'todo', labels:['feature','frontend'], assignee:'karri' },
// Depth 5 — sub-issues under NL-203
{ id:'NL-203a', kind:'issue', title:'Schema: refresh_token_family', depth:5, parent:'NL-203',
status:'done', labels:['chore','backend'], assignee:'jori' },
{ id:'NL-203b', kind:'issue', title:'Rotation handler + tests', depth:5, parent:'NL-203',
status:'prog', labels:['feature','backend'], assignee:'jori' },
{ id:'NL-203c', kind:'issue', title:'Audit log entry on reuse-detect', depth:5, parent:'NL-203',
status:'todo', labels:['feature'], assignee:'codex' },
];
/* Build lookup */
const byId = Object.fromEntries(NODES.map(n => [n.id, n]));
const childrenOf = {};
NODES.forEach(n => {
if (n.parent) (childrenOf[n.parent] = childrenOf[n.parent] || []).push(n.id);
});
/* Group by depth */
const maxDepth = Math.max(...NODES.map(n=>n.depth));
const byDepth = Array.from({length: maxDepth+1}, ()=>[]);
NODES.forEach(n => byDepth[n.depth].push(n));
/* ---------- Render ---------- */
const colsEl = document.getElementById('columns');
const ICON = {
component: '<svg class="ico cube" width="14" height="14"><use href="#i-cube"/></svg>',
issue: function(s){
return `<span class="st ${s||'todo'}"></span>`;
}
};
const AVATARS = {
jori:'J', mira:'M', karri:'K', codex:'★', triage:'T'
};
function avatar(who) {
if (!who) return '';
const klass = 'a-' + who;
const letter = AVATARS[who] || who[0].toUpperCase();
return `<span class="avatar ${klass}">${letter}</span>`;
}
function labelChips(labels){
if (!labels || !labels.length) return '';
return `<div class="labels">${labels.map(l=>`<span class="label ${l}">${l}</span>`).join('')}</div>`;
}
function nodeCard(n) {
const isComp = n.kind === 'component';
const icoLeft = isComp
? '<svg class="ico cube" width="14" height="14" style="color:var(--accent-2)"><use href="#i-cube"/></svg>'
: `<span class="st ${n.status||'todo'}"></span>`;
const idLabel = n.id;
const meta = [];
if (n.assignee) meta.push(avatar(n.assignee));
if (n.repo) meta.push(`<span class="mono" style="font-size:10.5px">${n.repo}</span>`);
if (isComp) {
const c = (childrenOf[n.id]||[]).length;
if (c) meta.push(`<span style="color:var(--text-mute)">${c} child${c===1?'':'ren'}</span>`);
}
return `
<div class="node" data-id="${n.id}" data-kind="${n.kind}">
<div class="title-row">
${icoLeft}
<div class="title">${n.title}</div>
<span class="id">${idLabel}</span>
</div>
<div class="meta">
${labelChips(n.labels)}
<span style="margin-left:auto;display:flex;gap:6px;align-items:center">${meta.join('')}</span>
</div>
<div class="child-handle"></div>
</div>
`;
}
/* Render columns RIGHT to LEFT order = root on right, leaves on left.
Display order in DOM = leftmost first (highest depth) ... */
function renderColumns() {
const frag = document.createDocumentFragment();
for (let d = maxDepth; d >= 0; d--) {
const col = document.createElement('section');
col.className = 'column';
col.dataset.depth = d;
// group nodes in this depth by parent component title (for higher signal)
const nodes = byDepth[d];
// Header label
let lvlLabel;
if (d === 0) lvlLabel = { lvl:'Layer 0 · root', title:'Project' };
else if (d === 1) lvlLabel = { lvl:`Layer ${d} · area`, title:'Top-level components' };
else if (d === 2) lvlLabel = { lvl:`Layer ${d} · service`, title:'Services' };
else if (d === 3) lvlLabel = { lvl:`Layer ${d} · module`, title:'Modules' };
else if (d === 4) lvlLabel = { lvl:`Layer ${d} · issues`, title:'Tasks' };
else lvlLabel = { lvl:`Layer ${d} · sub-issues`, title:'Sub-tasks' };
col.innerHTML = `
<div class="col-head">
<span class="lvl">${lvlLabel.lvl}</span>
<span class="title">${lvlLabel.title}</span>
<span class="meta">${nodes.length} ${nodes.length===1?'node':'nodes'}
<svg class="arrow" width="12" height="12">
<use href="#${d===0?'i-arrow-right':'i-arrow-right'}"></use>
</svg>
</span>
</div>
<div class="col-body" data-depth="${d}"></div>
`;
// Group children by parent so visual rhythm reflects parents above
const body = col.querySelector('.col-body');
if (d === 0) {
body.innerHTML = nodes.map(nodeCard).join('');
} else {
// Group by parent (descending order: parents in current visible col)
const groups = {};
nodes.forEach(n => { (groups[n.parent] = groups[n.parent] || []).push(n); });
// Order by parent's order in (d-1) column
const parentOrder = byDepth[d-1].map(p=>p.id);
parentOrder.forEach(pid => {
if (!groups[pid]) return;
const parent = byId[pid];
const groupHtml = `
<div class="group-label" data-parent-of="${pid}">
<span style="color:var(--text-dim)">under</span>
<span style="color:var(--text)">${parent.title}</span>
<span class="mono" style="color:var(--text-mute)">${pid}</span>
</div>
${groups[pid].map(nodeCard).join('')}
<div class="add-row" data-add-under="${pid}">
<svg width="12" height="12"><use href="#i-plus"/></svg>
Add ${d>=4 ? 'issue' : 'child'} under ${parent.title}
</div>
`;
body.insertAdjacentHTML('beforeend', groupHtml);
});
// Orphans (parents not in prev col, e.g. depth 0 only has 1 root)
Object.keys(groups).forEach(pid => {
if (parentOrder.includes(pid)) return;
body.insertAdjacentHTML('beforeend', groups[pid].map(nodeCard).join(''));
});
}
frag.appendChild(col);
}
colsEl.appendChild(frag);
}
renderColumns();
/* ---------- Hover highlighting ---------- */
const canvas = document.getElementById('canvas');
const edgesSvg = document.getElementById('edges');
const legend = document.getElementById('legend');
function ancestorsOf(id) {
const out = [];
let cur = byId[id]?.parent;
while (cur) { out.push(cur); cur = byId[cur].parent; }
return out;
}
function descendantsOf(id) {
const out = [];
const stack = [...(childrenOf[id]||[])];
while (stack.length) {
const x = stack.pop();
out.push(x);
(childrenOf[x]||[]).forEach(c => stack.push(c));
}
return out;
}
function nodeEl(id) {
return canvas.querySelector(`.node[data-id="${CSS.escape(id)}"]`);
}
function clearHighlights() {
canvas.querySelectorAll('.node').forEach(el => {
el.classList.remove('is-hover','is-ancestor','is-descendant','is-dim');
});
edgesSvg.innerHTML = '';
legend.classList.remove('visible');
}
function applyHighlight(id) {
const anc = ancestorsOf(id);
const desc = descendantsOf(id);
const set = new Set([id, ...anc, ...desc]);
canvas.querySelectorAll('.node').forEach(el => {
const nid = el.dataset.id;
if (nid === id) el.classList.add('is-hover');
else if (anc.includes(nid)) el.classList.add('is-ancestor');
else if (desc.includes(nid)) el.classList.add('is-descendant');
else el.classList.add('is-dim');
});
drawEdges(id, anc, desc);
legend.classList.add('visible');
}
function drawEdges(focusId, anc, desc) {
edgesSvg.innerHTML = '';
const canvasRect = canvas.getBoundingClientRect();
const scrollLeft = canvas.scrollLeft;
const scrollTop = canvas.scrollTop;
function center(el) {
const r = el.getBoundingClientRect();
return {
x: r.left - canvasRect.left + scrollLeft + r.width/2,
y: r.top - canvasRect.top + scrollTop + r.height/2,
r
};
}
function edgePoint(el, side) { // 'left' or 'right'
const r = el.getBoundingClientRect();
return {
x: (side === 'right' ? r.right : r.left) - canvasRect.left + scrollLeft,
y: r.top - canvasRect.top + scrollTop + r.height/2
};
}
// Set svg dims to canvas scroll content size
const w = canvas.scrollWidth;
const h = canvas.scrollHeight;
edgesSvg.setAttribute('width', w);
edgesSvg.setAttribute('height', h);
edgesSvg.setAttribute('viewBox', `0 0 ${w} ${h}`);
edgesSvg.style.width = w + 'px';
edgesSvg.style.height = h + 'px';
function drawCurve(a, b, color) {
// a is on the right of left node, b is on the left of right node
const dx = Math.max(40, Math.abs(b.x - a.x) * 0.5);
const d = `M ${a.x} ${a.y} C ${a.x + dx} ${a.y}, ${b.x - dx} ${b.y}, ${b.x} ${b.y}`;
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('d', d);
p.setAttribute('stroke', color);
p.setAttribute('stroke-width', '1.6');
p.setAttribute('opacity', '0.85');
p.setAttribute('stroke-dasharray', '0');
edgesSvg.appendChild(p);
}
const focusEl = nodeEl(focusId);
if (!focusEl) return;
// ancestors: focus(left) -> parent(right) chain
let cur = focusId;
for (const aid of anc) {
const a = nodeEl(cur);
const b = nodeEl(aid);
if (a && b) {
drawCurve(edgePoint(a, 'right'), edgePoint(b, 'left'), 'rgba(244,184,96,0.7)');
}
cur = aid;
}
// descendants: child(left) -> focus(right). For each direct edge in tree
function drawDescendants(parentId) {
(childrenOf[parentId]||[]).forEach(cid => {
if (!desc.includes(cid)) return;
const a = nodeEl(cid);
const b = nodeEl(parentId);
if (a && b) {
drawCurve(edgePoint(a, 'right'), edgePoint(b, 'left'), 'rgba(110,209,164,0.7)');
}
drawDescendants(cid);
});
}
drawDescendants(focusId);
}
let hoverDebounce;
canvas.addEventListener('mouseover', (e) => {
const node = e.target.closest('.node');
if (!node) return;
clearTimeout(hoverDebounce);
clearHighlights();
applyHighlight(node.dataset.id);
});
canvas.addEventListener('mouseleave', () => {
hoverDebounce = setTimeout(clearHighlights, 80);
});
// also clear when leaving any column body's interactive region
document.addEventListener('mouseout', (e) => {
if (e.target === document.documentElement) clearHighlights();
});
/* Click → set as detail focus (cosmetic) */
canvas.addEventListener('click', (e) => {
const node = e.target.closest('.node');
if (!node) return;
const n = byId[node.dataset.id];
if (!n) return;
// Update detail panel header / body minimally
const det = document.getElementById('detail');
det.querySelector('.detail-head .id').textContent = n.id;
const stEl = det.querySelector('.detail-head .st');
stEl.className = 'st ' + (n.status || (n.kind==='component' ? 'done' : 'todo'));
det.querySelector('.detail-body h3').textContent = n.title;
det.style.display = 'flex';
});
document.getElementById('detail-x').addEventListener('click', () => {
document.getElementById('detail').style.display = 'none';
});
/* ---------- Minimap / scroll ---------- */
const blocksEl = document.getElementById('miniblocks');
const cols = colsEl.children.length;
for (let i=0;i<cols;i++) {
const b = document.createElement('div');
b.dataset.col = i;
blocksEl.appendChild(b);
}
function updateMini() {
const sl = canvas.scrollLeft, w = canvas.clientWidth, sw = canvas.scrollWidth;
// determine which columns are visible
const colsArr = [...colsEl.children];
const visible = new Set();
colsArr.forEach((c,i) => {
const r = c.getBoundingClientRect();
const cr = canvas.getBoundingClientRect();
const left = r.left - cr.left;
const right = r.right - cr.left;
if (right > 0 && left < cr.width) visible.add(i);
});
[...blocksEl.children].forEach((b,i) => {
b.classList.toggle('viewing', visible.has(i));
});
}
canvas.addEventListener('scroll', updateMini);
window.addEventListener('resize', updateMini);
setTimeout(updateMini, 50);
document.getElementById('scroll-left').onclick = () => canvas.scrollBy({left:-360, behavior:'smooth'});
document.getElementById('scroll-right').onclick = () => canvas.scrollBy({left: 360, behavior:'smooth'});
// Click minimap blocks to jump to a specific column
blocksEl.addEventListener('click', (e) => {
const b = e.target.closest('div[data-col]');
if (!b) return;
const i = parseInt(b.dataset.col);
const c = colsEl.children[i];
c.scrollIntoView({behavior:'smooth', inline:'start', block:'nearest'});
});
/* Keyboard shortcuts */
window.addEventListener('keydown', (e) => {
if (e.target.matches('input,textarea')) return;
if (e.key === 'ArrowLeft' && (e.altKey || e.metaKey === false && e.ctrlKey === false && !e.shiftKey)) {
canvas.scrollBy({left:-360, behavior:'smooth'});
} else if (e.key === 'ArrowRight' && (e.altKey)) {
canvas.scrollBy({left:360, behavior:'smooth'});
}
});
/* Initial scroll: center on the column with the focused issue (depth 4 → leftmost area) */
window.addEventListener('load', () => {
// Scroll so the depth-3 column (modules) is roughly central
const target = colsEl.querySelector('.column[data-depth="3"]');
if (target) target.scrollIntoView({behavior:'instant', inline:'center', block:'nearest'});
setTimeout(updateMini, 60);
});
</script>
</body>
</html>