okf 1.0.0
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.
- checksums.yaml +7 -0
- data/.okf/capabilities/agent-skill.md +46 -0
- data/.okf/capabilities/graph-server.md +60 -0
- data/.okf/capabilities/index.md +20 -0
- data/.okf/capabilities/library-api.md +67 -0
- data/.okf/capabilities/linter.md +49 -0
- data/.okf/capabilities/read-views.md +84 -0
- data/.okf/capabilities/validator.md +40 -0
- data/.okf/cli.md +52 -0
- data/.okf/design/core-shell-split.md +58 -0
- data/.okf/design/index.md +10 -0
- data/.okf/design/ruby-floor.md +45 -0
- data/.okf/design/runtime-dependencies.md +44 -0
- data/.okf/design/server-trust-boundary.md +35 -0
- data/.okf/format/citations.md +33 -0
- data/.okf/format/cross-links.md +52 -0
- data/.okf/format/frontmatter.md +38 -0
- data/.okf/format/index.md +9 -0
- data/.okf/format/okf-format.md +43 -0
- data/.okf/index.md +18 -0
- data/.okf/log.md +9 -0
- data/.okf/model/bundle.md +38 -0
- data/.okf/model/concept.md +44 -0
- data/.okf/model/graph.md +44 -0
- data/.okf/model/index.md +8 -0
- data/.okf/overview.md +66 -0
- data/CHANGELOG.md +54 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +201 -0
- data/NOTICE +10 -0
- data/README.md +276 -0
- data/exe/okf +6 -0
- data/lib/okf/bundle/folder.rb +94 -0
- data/lib/okf/bundle/graph.rb +118 -0
- data/lib/okf/bundle/linter/report.rb +56 -0
- data/lib/okf/bundle/linter.rb +416 -0
- data/lib/okf/bundle/reader.rb +60 -0
- data/lib/okf/bundle/validator/result.rb +35 -0
- data/lib/okf/bundle/validator.rb +131 -0
- data/lib/okf/bundle/writer.rb +137 -0
- data/lib/okf/bundle.rb +216 -0
- data/lib/okf/cli.rb +910 -0
- data/lib/okf/concept/file.rb +63 -0
- data/lib/okf/concept.rb +101 -0
- data/lib/okf/markdown/citations.rb +49 -0
- data/lib/okf/markdown/frontmatter.rb +55 -0
- data/lib/okf/markdown/links.rb +98 -0
- data/lib/okf/path.rb +34 -0
- data/lib/okf/server/app.rb +120 -0
- data/lib/okf/server/graph.rb +112 -0
- data/lib/okf/server/runner.rb +78 -0
- data/lib/okf/server/templates/graph.html.erb +803 -0
- data/lib/okf/skill/SKILL.md +133 -0
- data/lib/okf/skill/reference/APACHE-2.0.txt +202 -0
- data/lib/okf/skill/reference/SPEC.md +460 -0
- data/lib/okf/skill/reference/authoring.md +218 -0
- data/lib/okf/skill/reference/cli.md +196 -0
- data/lib/okf/skill/templates/concept.md +24 -0
- data/lib/okf/skill/templates/index.md +8 -0
- data/lib/okf/skill/templates/log.md +6 -0
- data/lib/okf/skill/templates/root-index.md +12 -0
- data/lib/okf/skill.rb +82 -0
- data/lib/okf/version.rb +5 -0
- data/lib/okf.rb +55 -0
- metadata +142 -0
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
2
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3
|
+
<title>OKF — <%= escaped_name %></title>
|
|
4
|
+
<meta property="og:title" content="<%= og_title %>">
|
|
5
|
+
<meta property="og:description" content="<%= og_desc %>">
|
|
6
|
+
<meta property="og:type" content="website">
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='24' fill='%231a1a1a'/><polygon points='38,44 62,44 50,82' fill='%237a0a1e'/><polygon points='18,44 38,44 50,82' fill='%23a8112c'/><polygon points='62,44 82,44 50,82' fill='%23a8112c'/><polygon points='35,28 18,44 38,44' fill='%23dc1e3c'/><polygon points='65,28 82,44 62,44' fill='%23dc1e3c'/><polygon points='35,28 65,28 62,44 38,44' fill='%23f43f5e'/><polygon points='36,29 49,29 42,42' fill='%23fff' opacity='.38'/><polygon points='35,28 65,28 82,44 50,82 18,44' fill='none' stroke='%23ff6b7f' stroke-width='2' stroke-linejoin='round'/></svg>">
|
|
8
|
+
<script>/* Resolve theme before first paint so there is no flash. */
|
|
9
|
+
(function(){try{var t=localStorage.getItem('okf-theme')||(matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
|
|
10
|
+
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.30.2/dist/cytoscape.min.js"></script>
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/marked@14/marked.min.js"></script>
|
|
14
|
+
<style>
|
|
15
|
+
:root{
|
|
16
|
+
--bg:#ffffff; --panel:#ffffff; --panel-2:#f6f7f8; --canvas:#f4f5f7; --ink:#1f2328; --ink-soft:#333333;
|
|
17
|
+
--muted:#63697a; --faint:#9298a4; --line:#e6e8eb; --line-2:#eef0f2;
|
|
18
|
+
--accent:#e21e1e; --accent-ink:#c81a1a; --accent-soft:#fdecec; --edge:#cfd4db; --ok:#1a9e5f;
|
|
19
|
+
--rail:#15171c; --rail-ink:#8b919c; --rail-ink-hi:#dfe2e7;
|
|
20
|
+
--shadow:0 1px 2px rgba(20,24,33,.05),0 8px 24px rgba(20,24,33,.07);
|
|
21
|
+
}
|
|
22
|
+
:root[data-theme=dark]{
|
|
23
|
+
--bg:#17191e; --panel:#1d2026; --panel-2:#232833; --canvas:#111318; --ink:#eceef1; --ink-soft:#d7dae0;
|
|
24
|
+
--muted:#9aa0aa; --faint:#6b7178; --line:#2a2e36; --line-2:#232830;
|
|
25
|
+
--accent:#f5433b; --accent-ink:#ff726b; --accent-soft:#3a1f1e; --edge:#333c49; --ok:#37c07f;
|
|
26
|
+
--rail:#0d0e11; --rail-ink:#868c98; --rail-ink-hi:#e2e5ea;
|
|
27
|
+
--shadow:0 1px 2px rgba(0,0,0,.4),0 10px 28px rgba(0,0,0,.42);
|
|
28
|
+
}
|
|
29
|
+
@media (prefers-color-scheme:dark){:root:not([data-theme=light]){
|
|
30
|
+
--bg:#17191e; --panel:#1d2026; --panel-2:#232833; --canvas:#111318; --ink:#eceef1; --ink-soft:#d7dae0;
|
|
31
|
+
--muted:#9aa0aa; --faint:#6b7178; --line:#2a2e36; --line-2:#232830;
|
|
32
|
+
--accent:#f5433b; --accent-ink:#ff726b; --accent-soft:#3a1f1e; --edge:#333c49; --ok:#37c07f;
|
|
33
|
+
--rail:#0d0e11; --rail-ink:#868c98; --rail-ink-hi:#e2e5ea;
|
|
34
|
+
--shadow:0 1px 2px rgba(0,0,0,.4),0 10px 28px rgba(0,0,0,.42);
|
|
35
|
+
}}
|
|
36
|
+
*{box-sizing:border-box}
|
|
37
|
+
html,body{margin:0;height:100%}
|
|
38
|
+
body{background:var(--bg);color:var(--ink);font:14px/1.55 'Poppins',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;-webkit-font-smoothing:antialiased}
|
|
39
|
+
@property --side-w{syntax:'<length>';inherits:true;initial-value:380px}
|
|
40
|
+
h1,h2,h3,h4{margin:0}
|
|
41
|
+
|
|
42
|
+
/* ── app shell: rail + main ── */
|
|
43
|
+
#app{display:grid;grid-template-columns:76px 1fr;height:100vh;height:100dvh}
|
|
44
|
+
#rail{grid-column:1;background:var(--rail);display:flex;flex-direction:column;align-items:center;gap:3px;padding:12px 8px;overflow:hidden}
|
|
45
|
+
.rail-brand{width:34px;height:34px;margin:2px 0 10px;flex:none}
|
|
46
|
+
.rail-brand svg{width:34px;height:34px;display:block}
|
|
47
|
+
.rail-item{position:relative;width:60px;padding:9px 2px;border:0;background:none;border-radius:12px;cursor:pointer;
|
|
48
|
+
display:flex;flex-direction:column;align-items:center;gap:5px;color:var(--rail-ink);transition:background .15s,color .15s;font-family:inherit}
|
|
49
|
+
.rail-item .ri-ico svg{width:21px;height:21px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
|
50
|
+
.rail-item .ri-lbl{font-size:10.5px;font-weight:500;letter-spacing:.005em}
|
|
51
|
+
.rail-item:hover{color:var(--rail-ink-hi);background:rgba(255,255,255,.06)}
|
|
52
|
+
.rail-item:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
|
53
|
+
.rail-item.active{color:#fff}
|
|
54
|
+
.rail-item.active .ri-ico{color:var(--accent)}
|
|
55
|
+
.rail-item.active::before{content:"";position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:24px;border-radius:0 3px 3px 0;background:var(--accent);
|
|
56
|
+
transition:transform .18s cubic-bezier(.22,1,.36,1)}
|
|
57
|
+
.rail-sp{flex:1}
|
|
58
|
+
.rail-tools{display:flex;flex-direction:column;gap:6px;align-items:center}
|
|
59
|
+
.rail-tools .btn{color:var(--rail-ink);border-color:transparent;background:rgba(255,255,255,.05)}
|
|
60
|
+
.rail-tools .btn:hover{color:var(--rail-ink-hi);background:rgba(255,255,255,.1);border-color:transparent}
|
|
61
|
+
|
|
62
|
+
/* ── main column ── */
|
|
63
|
+
#main{grid-column:2;display:flex;flex-direction:column;min-width:0}
|
|
64
|
+
#topbar{flex:none;display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:1px solid var(--line);background:var(--panel)}
|
|
65
|
+
.ident{font-size:13.5px;font-weight:600;color:var(--ink);white-space:nowrap;letter-spacing:-.01em}
|
|
66
|
+
.ident .muted{color:var(--muted);font-weight:400;margin-left:9px;font-size:12.5px}
|
|
67
|
+
.ident .src{color:var(--accent-ink);text-decoration:none;margin-left:8px}
|
|
68
|
+
.ident .src:hover{text-decoration:underline}
|
|
69
|
+
#views{flex:1;min-height:0;position:relative;background:var(--bg)}
|
|
70
|
+
.view{position:absolute;inset:0;display:none;flex-direction:column;min-height:0}
|
|
71
|
+
.view.active{display:flex}
|
|
72
|
+
|
|
73
|
+
/* ── shared controls ── */
|
|
74
|
+
.field{height:34px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink);
|
|
75
|
+
font:inherit;font-size:13px;padding:0 10px;outline:none;transition:border-color .15s,box-shadow .15s}
|
|
76
|
+
.field:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
|
|
77
|
+
.field::placeholder{color:var(--muted)}
|
|
78
|
+
.search{position:relative;display:flex;align-items:center;flex:1 1 320px;min-width:130px;max-width:520px}
|
|
79
|
+
.search svg{position:absolute;left:11px;width:15px;height:15px;stroke:var(--muted);fill:none;stroke-width:1.8;pointer-events:none}
|
|
80
|
+
.search input{width:100%;padding-left:33px}
|
|
81
|
+
.selwrap{position:relative;display:inline-flex;flex:none}
|
|
82
|
+
.selwrap::after{content:"";position:absolute;right:12px;top:50%;width:7px;height:7px;border-right:1.6px solid var(--muted);border-bottom:1.6px solid var(--muted);transform:translateY(-70%) rotate(45deg);pointer-events:none}
|
|
83
|
+
select.field{appearance:none;-webkit-appearance:none;padding-right:30px;cursor:pointer}
|
|
84
|
+
select.field:disabled{opacity:.45;cursor:not-allowed}
|
|
85
|
+
.btn{cursor:pointer;display:inline-grid;place-items:center;width:34px;height:34px;border-radius:9px;border:1px solid var(--line);
|
|
86
|
+
background:var(--panel);color:var(--muted);transition:color .15s,border-color .15s,background .15s;font-family:inherit;position:relative}
|
|
87
|
+
.btn:hover{color:var(--ink);border-color:var(--accent)}
|
|
88
|
+
.btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
89
|
+
.btn[aria-pressed=true]{color:var(--accent);border-color:var(--accent);background:var(--accent-soft)}
|
|
90
|
+
.btn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
|
91
|
+
.btn.text{width:auto;display:inline-flex;align-items:center;padding:0 12px;gap:7px;font-size:13px;font-weight:500;color:var(--ink)}
|
|
92
|
+
.btn .fbadge{position:absolute;top:-6px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:9px;background:var(--accent);
|
|
93
|
+
color:#fff;font-size:10px;font-weight:600;display:none;place-items:center;line-height:1}
|
|
94
|
+
.btn.on-filter .fbadge{display:grid}
|
|
95
|
+
#graph-controls{display:none;align-items:center;gap:9px}
|
|
96
|
+
#app[data-view=graph] #graph-controls{display:flex}
|
|
97
|
+
#search-wrap{display:flex;flex:1}
|
|
98
|
+
#app[data-view=stats] #search-wrap,#app[data-view=files] #search-wrap{visibility:hidden}
|
|
99
|
+
#btn-theme .sun{display:none} :root[data-theme=dark] #btn-theme .sun{display:block} :root[data-theme=dark] #btn-theme .moon{display:none}
|
|
100
|
+
#btn-full .compress{display:none} #btn-full[aria-pressed=true] .compress{display:block} #btn-full[aria-pressed=true] .expand{display:none}
|
|
101
|
+
|
|
102
|
+
/* ── section head with the whysimplicity red underline ── */
|
|
103
|
+
.shead{display:flex;align-items:baseline;gap:12px;margin:0 0 16px}
|
|
104
|
+
.shead h2{font-size:17px;font-weight:600;letter-spacing:-.01em;position:relative;padding-bottom:7px}
|
|
105
|
+
.shead h2::after{content:"";position:absolute;left:0;bottom:0;width:34px;height:3px;border-radius:3px;background:var(--accent)}
|
|
106
|
+
.shead .cnt{font-size:12.5px;color:var(--faint)}
|
|
107
|
+
|
|
108
|
+
/* ── chips / badges ── */
|
|
109
|
+
.chip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:999px;font-size:12.5px;
|
|
110
|
+
color:var(--muted);background:var(--panel);border:1px solid var(--line);cursor:pointer;user-select:none;transition:.14s;font-family:inherit}
|
|
111
|
+
.chip:hover{color:var(--ink);border-color:var(--accent)}
|
|
112
|
+
.chip.off{opacity:.38} .chip.on{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
113
|
+
.chip .dot{width:9px;height:9px;border-radius:50%} .chip .c{color:var(--faint);font-variant-numeric:tabular-nums}
|
|
114
|
+
.chip.on .c{color:rgba(255,255,255,.8)}
|
|
115
|
+
.badge{display:inline-flex;align-items:center;gap:7px;font-size:11.5px;font-weight:600;letter-spacing:.02em;color:var(--ink);
|
|
116
|
+
background:var(--line-2);border:1px solid var(--line);border-radius:7px;padding:3px 9px}
|
|
117
|
+
.badge .dot{width:8px;height:8px;border-radius:50%}
|
|
118
|
+
.status{font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;padding:2px 7px;border-radius:5px;color:var(--muted);background:var(--line-2)}
|
|
119
|
+
.status.shipped{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}
|
|
120
|
+
.tag{background:var(--line-2);border:1px solid var(--line);border-radius:6px;padding:2px 8px;font-size:11px;color:var(--muted)}
|
|
121
|
+
|
|
122
|
+
/* ── graph view ── */
|
|
123
|
+
.graph-body{display:grid;grid-template-columns:1fr var(--side-w);height:100%;min-height:0;
|
|
124
|
+
--side-w:380px;transition:--side-w .22s cubic-bezier(.22,1,.36,1)}
|
|
125
|
+
.graph-body[data-side=hidden]{--side-w:0px}
|
|
126
|
+
.graph-body[data-side=wide]{--side-w:70vw}
|
|
127
|
+
#stage{position:relative;min-width:0;overflow:hidden;background:var(--canvas)}
|
|
128
|
+
#cy{position:absolute;inset:0}
|
|
129
|
+
.ghint{position:absolute;right:16px;bottom:14px;z-index:3;font-size:11px;color:var(--faint);pointer-events:none}
|
|
130
|
+
#side{min-width:0;background:var(--panel);border-left:1px solid var(--line);display:flex;flex-direction:column;overflow:hidden}
|
|
131
|
+
.graph-body[data-side=hidden] #side{border-left:0}
|
|
132
|
+
#side-head{display:flex;align-items:center;gap:6px;padding:9px 12px;border-bottom:1px solid var(--line);flex:none}
|
|
133
|
+
#side-head .lbl{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--faint);margin-right:auto}
|
|
134
|
+
#side-head .btn{width:30px;height:30px}
|
|
135
|
+
#side-body{padding:16px 18px;overflow:auto;flex:1}
|
|
136
|
+
|
|
137
|
+
/* ── filters slide-over (graph + catalog) ── */
|
|
138
|
+
.slideover{position:absolute;top:0;right:0;bottom:0;width:300px;max-width:82%;z-index:6;background:var(--panel);
|
|
139
|
+
border-left:1px solid var(--line);box-shadow:var(--shadow);display:flex;flex-direction:column;
|
|
140
|
+
transform:translateX(100%);transition:transform .2s cubic-bezier(.22,1,.36,1)}
|
|
141
|
+
.slideover.open{transform:none}
|
|
142
|
+
.slideover .fhead{display:flex;align-items:center;gap:8px;padding:12px 14px;border-bottom:1px solid var(--line)}
|
|
143
|
+
.slideover .fhead h3{font-size:13px;font-weight:600;margin-right:auto}
|
|
144
|
+
.slideover .fbody{padding:14px;overflow:auto;flex:1}
|
|
145
|
+
.slideover .fgroup{margin-bottom:18px}
|
|
146
|
+
.slideover .fgroup h4{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--faint);margin:0 0 9px}
|
|
147
|
+
.slideover .fchips{display:flex;flex-wrap:wrap;gap:6px}
|
|
148
|
+
.cat-types{align-items:center;gap:10px} .cat-types #cat-types{display:flex;flex-wrap:wrap;gap:7px}
|
|
149
|
+
.ftree-head .combo{width:100%} .ftree-head .combo+.combo{margin-top:8px}
|
|
150
|
+
.link{background:none;border:0;color:var(--accent-ink);font:inherit;font-size:12.5px;cursor:pointer;padding:0;font-weight:500}
|
|
151
|
+
.link:hover{text-decoration:underline}
|
|
152
|
+
|
|
153
|
+
/* ── inspector content ── */
|
|
154
|
+
.type{display:inline-flex;align-items:center;gap:7px;font-size:11.5px;font-weight:600;color:var(--ink);
|
|
155
|
+
background:var(--line-2);border:1px solid var(--line);border-radius:7px;padding:3px 9px}
|
|
156
|
+
.type .dot{width:9px;height:9px;border-radius:50%}
|
|
157
|
+
.title{font-size:18px;font-weight:600;margin:.5em 0 0;letter-spacing:-.01em;line-height:1.3;text-wrap:balance}
|
|
158
|
+
.desc{color:var(--muted);margin:10px 0 14px}
|
|
159
|
+
.tags{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px}
|
|
160
|
+
.rel{margin:14px 0 0}
|
|
161
|
+
.rel h4{margin:0 0 5px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--faint)}
|
|
162
|
+
.rel a{display:block;color:var(--accent-ink);cursor:pointer;font-size:13px;padding:2px 0;text-decoration:none}
|
|
163
|
+
.rel a:hover{text-decoration:underline}
|
|
164
|
+
.empty{color:var(--muted)} .empty b{color:var(--ink);font-weight:600} .loading{color:var(--faint)}
|
|
165
|
+
|
|
166
|
+
/* ── markdown body ── */
|
|
167
|
+
.body{border-top:1px solid var(--line);padding-top:14px;margin-top:16px;font-size:14px;line-height:1.62;color:var(--ink-soft)}
|
|
168
|
+
.body>*:first-child{margin-top:0}
|
|
169
|
+
.body h1{font-size:19px}.body h2{font-size:16px}.body h3{font-size:14px}.body h1,.body h2,.body h3{line-height:1.3;font-weight:600;margin:1.2em 0 .5em}
|
|
170
|
+
.body a{color:var(--accent-ink)}
|
|
171
|
+
.body code{background:var(--line-2);border:1px solid var(--line);padding:1px 5px;border-radius:5px;font-size:.85em;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
172
|
+
.body pre{background:var(--line-2);border:1px solid var(--line);padding:12px;border-radius:10px;overflow:auto}
|
|
173
|
+
.body pre code{background:none;border:0;padding:0}
|
|
174
|
+
.body table{border-collapse:collapse;display:block;overflow:auto}
|
|
175
|
+
.body td,.body th{border:1px solid var(--line);padding:5px 9px}
|
|
176
|
+
.body img{max-width:100%}
|
|
177
|
+
.body blockquote{margin:.6em 0;padding:.2em 0 .2em 14px;border-left:3px solid var(--line);color:var(--muted)}
|
|
178
|
+
.mermaid{margin:14px 0;display:flex;justify-content:center;background:var(--panel-2);border:1px solid var(--line);border-radius:10px;padding:12px;overflow:auto}
|
|
179
|
+
.mermaid svg{max-width:100%;height:auto}
|
|
180
|
+
|
|
181
|
+
/* ── catalog ── */
|
|
182
|
+
.pad{padding:22px 24px;overflow:auto;flex:1}
|
|
183
|
+
.cat-types{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:18px}
|
|
184
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px}
|
|
185
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:15px 16px 13px;cursor:pointer;
|
|
186
|
+
display:flex;flex-direction:column;gap:9px;box-shadow:var(--shadow);transition:transform .16s cubic-bezier(.22,1,.36,1),border-color .16s}
|
|
187
|
+
.card:hover{transform:translateY(-3px);border-color:var(--accent)}
|
|
188
|
+
.card:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
189
|
+
.card .r1{display:flex;align-items:center;gap:8px}
|
|
190
|
+
.card .area{margin-left:auto;font-size:11.5px;color:var(--faint);font-family:ui-monospace,monospace}
|
|
191
|
+
.card h3{font-size:15.5px;font-weight:600;letter-spacing:-.01em;line-height:1.28}
|
|
192
|
+
.card .cd{font-size:13px;color:var(--muted);margin:0;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
|
193
|
+
.card .cm{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:auto;padding-top:9px;border-top:1px solid var(--line-2)}
|
|
194
|
+
.card .mini{font-size:11.5px;color:var(--faint);display:inline-flex;gap:4px;align-items:center}
|
|
195
|
+
.card .mini.links{color:var(--accent-ink);margin-left:auto}
|
|
196
|
+
.none{grid-column:1/-1;text-align:center;color:var(--faint);padding:48px;border:1px dashed var(--line);border-radius:12px}
|
|
197
|
+
|
|
198
|
+
/* ── files ── */
|
|
199
|
+
.files-grid{display:grid;grid-template-columns:320px 1fr;height:100%;min-height:0}
|
|
200
|
+
.ftree{border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0;background:var(--panel)}
|
|
201
|
+
.ftree-head{padding:12px;border-bottom:1px solid var(--line);flex:none}
|
|
202
|
+
.ftree-list{overflow:auto;flex:1;padding:8px}
|
|
203
|
+
.ffolder{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--faint);padding:12px 8px 5px;font-weight:600}
|
|
204
|
+
.file{display:block;width:100%;text-align:left;border:0;background:none;border-radius:8px;padding:7px 9px;cursor:pointer;font-family:inherit;color:var(--ink)}
|
|
205
|
+
.file:hover{background:var(--panel-2)} .file.sel{background:var(--accent-soft)}
|
|
206
|
+
.file .fn{font-size:13px;font-weight:500;display:flex;align-items:center;gap:7px}
|
|
207
|
+
.file .fn .dot{width:7px;height:7px;border-radius:50%;flex:none}
|
|
208
|
+
.file .fd{font-size:11.5px;color:var(--faint);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
209
|
+
.fpreview{min-width:0;overflow:auto;display:flex;flex-direction:column}
|
|
210
|
+
.fp-head{display:flex;align-items:center;gap:10px;padding:14px 22px;border-bottom:1px solid var(--line);flex:none}
|
|
211
|
+
.fp-head .title{margin:0;font-size:16px}
|
|
212
|
+
.fp-body{padding:20px 24px;overflow:auto;flex:1;max-width:820px}
|
|
213
|
+
.fp-body .body{border-top:0;padding-top:0;margin-top:0}
|
|
214
|
+
|
|
215
|
+
/* ── tags ── */
|
|
216
|
+
.cloud{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:24px;align-items:center}
|
|
217
|
+
.tcloud{border:1px solid color-mix(in srgb,var(--accent) var(--heat,0%),var(--line));background:color-mix(in srgb,var(--accent) var(--heat,0%),var(--panel));border-radius:999px;padding:6px 13px;cursor:pointer;color:var(--ink);font-family:inherit;font-size:13px;transition:.14s}
|
|
218
|
+
.tcloud:hover{border-color:var(--accent);color:var(--accent-ink)} .tcloud.on{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
219
|
+
.tcloud b{color:var(--faint);font-weight:600;font-variant-numeric:tabular-nums;font-size:.8em;margin-left:5px} .tcloud.on b{color:rgba(255,255,255,.75)}
|
|
220
|
+
.tlist{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:9px}
|
|
221
|
+
.titem{display:flex;align-items:center;gap:9px;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:var(--panel);cursor:pointer;transition:.14s;font-family:inherit;text-align:left;color:var(--ink)}
|
|
222
|
+
.titem:hover{border-color:var(--accent);transform:translateY(-2px)}
|
|
223
|
+
.titem .dot{width:9px;height:9px;border-radius:50%;flex:none} .titem .tt{font-size:13.5px;font-weight:500}
|
|
224
|
+
.titem .ty{font-size:11px;color:var(--faint);margin-left:auto}
|
|
225
|
+
|
|
226
|
+
/* ── stats ── */
|
|
227
|
+
.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:13px;margin-bottom:26px}
|
|
228
|
+
.stat{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:17px 18px;box-shadow:var(--shadow)}
|
|
229
|
+
.stat .n{font-size:30px;font-weight:700;letter-spacing:-.02em;line-height:1;font-variant-numeric:tabular-nums}
|
|
230
|
+
.stat .l{font-size:11.5px;color:var(--muted);margin-top:7px;text-transform:uppercase;letter-spacing:.05em;font-weight:500}
|
|
231
|
+
.stat .n .u{color:var(--accent)}
|
|
232
|
+
.bars{display:flex;flex-direction:column;gap:9px;max-width:640px}
|
|
233
|
+
.bar{display:grid;grid-template-columns:150px 1fr 40px;align-items:center;gap:12px}
|
|
234
|
+
.bar .bl{font-size:12.5px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
235
|
+
.bar .bt{height:9px;border-radius:5px;background:var(--line-2);overflow:hidden}
|
|
236
|
+
.bar .bf{height:100%;border-radius:5px;transform-origin:left;transition:transform .5s cubic-bezier(.22,1,.36,1)}
|
|
237
|
+
.bar .bv{font-size:12px;color:var(--faint);text-align:right;font-variant-numeric:tabular-nums}
|
|
238
|
+
.bar.clickable{cursor:pointer;border-radius:8px;padding:3px 7px;margin:0 -7px;transition:background .13s}
|
|
239
|
+
.bar.clickable:hover{background:var(--panel-2)} .bar.clickable:hover .bl{color:var(--accent-ink)}
|
|
240
|
+
.bar.clickable:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
|
241
|
+
.two{display:grid;grid-template-columns:1fr 1fr;gap:30px} @media(max-width:820px){.two{grid-template-columns:1fr}}
|
|
242
|
+
|
|
243
|
+
/* ── type autocomplete (graph filters + catalog) ── */
|
|
244
|
+
.combo{position:relative;flex:none;width:170px}
|
|
245
|
+
.combo input{width:100%;padding-right:26px}
|
|
246
|
+
.combo .clear{position:absolute;right:6px;top:50%;transform:translateY(-50%);border:0;background:none;color:var(--muted);cursor:pointer;width:20px;height:20px;border-radius:6px;display:none;place-items:center;padding:0}
|
|
247
|
+
.combo .clear svg{width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round}
|
|
248
|
+
.combo.has .clear{display:grid}
|
|
249
|
+
.listbox{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:60;margin:0;padding:6px;list-style:none;background:var(--panel);
|
|
250
|
+
border:1px solid var(--line);border-radius:11px;box-shadow:var(--shadow);max-height:320px;overflow:auto;animation:pop .13s cubic-bezier(.22,1,.36,1)}
|
|
251
|
+
.listbox[hidden]{display:none}
|
|
252
|
+
.opt{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:7px;font-size:13px;color:var(--ink);cursor:pointer;white-space:nowrap}
|
|
253
|
+
.opt .sw{width:9px;height:9px;border-radius:50%;flex:none} .opt .c{margin-left:auto;color:var(--faint);font-size:12px}
|
|
254
|
+
.opt:hover,.opt.active{background:var(--line-2)} .opt.sel{color:var(--accent-ink);font-weight:600}
|
|
255
|
+
@keyframes pop{from{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:none}}
|
|
256
|
+
|
|
257
|
+
@media (max-width:720px){#rail{width:64px}.rail-item{width:52px}.card,.grid{}}
|
|
258
|
+
@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
|
|
259
|
+
</style></head>
|
|
260
|
+
<body>
|
|
261
|
+
<div id="app" data-view="graph">
|
|
262
|
+
<nav id="rail" aria-label="Views">
|
|
263
|
+
<span class="rail-brand" title="<%= escaped_name %> — <%= @graph.nodes.length %> concepts · <%= @graph.edges.length %> links">
|
|
264
|
+
<svg viewBox="0 0 100 100"><rect width="100" height="100" rx="24" fill="#1a1a1a"/><polygon points="38,44 62,44 50,82" fill="#7a0a1e"/><polygon points="18,44 38,44 50,82" fill="#a8112c"/><polygon points="62,44 82,44 50,82" fill="#a8112c"/><polygon points="35,28 18,44 38,44" fill="#dc1e3c"/><polygon points="65,28 82,44 62,44" fill="#dc1e3c"/><polygon points="35,28 65,28 62,44 38,44" fill="#f43f5e"/><polygon points="36,29 49,29 42,42" fill="#fff" opacity=".38"/><polygon points="35,28 65,28 82,44 50,82 18,44" fill="none" stroke="#ff6b7f" stroke-width="2" stroke-linejoin="round"/></svg>
|
|
265
|
+
</span>
|
|
266
|
+
<button class="rail-item active" data-view="graph"><span class="ri-ico"><svg viewBox="0 0 24 24"><circle cx="6" cy="6" r="2.4"/><circle cx="18" cy="9" r="2.4"/><circle cx="9" cy="18" r="2.4"/><path d="M7.7 7.3 15.9 8.6M7.5 16 8.2 8.3M10.9 16.4 16.3 10.5"/></svg></span><span class="ri-lbl">Graph</span></button>
|
|
267
|
+
<button class="rail-item" data-view="catalog"><span class="ri-ico"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="7.5" height="7.5" rx="1.6"/><rect x="13.5" y="4" width="7.5" height="7.5" rx="1.6"/><rect x="3" y="14.5" width="7.5" height="5.5" rx="1.6"/><rect x="13.5" y="14.5" width="7.5" height="5.5" rx="1.6"/></svg></span><span class="ri-lbl">Catalog</span></button>
|
|
268
|
+
<button class="rail-item" data-view="files"><span class="ri-ico"><svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 0 1 2-2h3.5l2 2H19a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span><span class="ri-lbl">Files</span></button>
|
|
269
|
+
<button class="rail-item" data-view="tags"><span class="ri-ico"><svg viewBox="0 0 24 24"><path d="M4 4.5h6.8L20 13.7l-6.3 6.3L4.5 10.8V4.5Z"/><circle cx="8.4" cy="8.4" r="1.4" fill="currentColor" stroke="none"/></svg></span><span class="ri-lbl">Tags</span></button>
|
|
270
|
+
<button class="rail-item" data-view="stats"><span class="ri-ico"><svg viewBox="0 0 24 24"><path d="M5 20V11M12 20V4M19 20v-6"/></svg></span><span class="ri-lbl">Stats</span></button>
|
|
271
|
+
<div class="rail-sp"></div>
|
|
272
|
+
<div class="rail-tools">
|
|
273
|
+
<button class="btn" id="btn-theme" type="button" aria-label="Toggle theme">
|
|
274
|
+
<svg class="moon" viewBox="0 0 24 24"><path d="M20 14.5A8 8 0 0 1 9.5 4 8 8 0 1 0 20 14.5Z"/></svg>
|
|
275
|
+
<svg class="sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4.2"/><path d="M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.2 5.2l1.5 1.5M17.3 17.3l1.5 1.5M18.8 5.2l-1.5 1.5M6.7 17.3l-1.5 1.5"/></svg></button>
|
|
276
|
+
<button class="btn" id="btn-full" type="button" aria-label="Toggle full screen" aria-pressed="false">
|
|
277
|
+
<svg class="expand" viewBox="0 0 24 24"><path d="M8 3H4a1 1 0 0 0-1 1v4M16 3h4a1 1 0 0 1 1 1v4M8 21H4a1 1 0 0 1-1-1v-4M16 21h4a1 1 0 0 0 1-1v-4"/></svg>
|
|
278
|
+
<svg class="compress" viewBox="0 0 24 24"><path d="M4 8h4V4M20 8h-4V4M4 16h4v4M20 16h-4v4"/></svg></button>
|
|
279
|
+
</div>
|
|
280
|
+
</nav>
|
|
281
|
+
|
|
282
|
+
<div id="main">
|
|
283
|
+
<header id="topbar">
|
|
284
|
+
<span class="ident"><%= escaped_name %><span class="muted"><%= @graph.nodes.length %> concepts · <%= @graph.edges.length %> links</span><%= source_link %></span>
|
|
285
|
+
<div id="search-wrap"><label class="search"><svg viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
|
286
|
+
<input id="search" class="field" placeholder="search concepts…" autocomplete="off" aria-label="Search"></label></div>
|
|
287
|
+
<div id="graph-controls">
|
|
288
|
+
<button class="btn text" id="btn-filters" type="button" aria-label="Filters"><svg viewBox="0 0 24 24"><path d="M3 5h18l-7 8v6l-4-2v-4Z"/></svg>Filters<span class="fbadge">0</span></button>
|
|
289
|
+
<span class="selwrap"><select id="layout" class="field" aria-label="Layout">
|
|
290
|
+
<optgroup label="built-in">
|
|
291
|
+
<option value="cose">force</option><option value="concentric">concentric</option>
|
|
292
|
+
<option value="breadthfirst">breadth-first</option><option value="circle">circle</option><option value="grid">grid</option>
|
|
293
|
+
</optgroup>
|
|
294
|
+
<optgroup label="extended">
|
|
295
|
+
<option value="fcose">organic</option><option value="dagre">hierarchy</option><option value="cola">physics</option>
|
|
296
|
+
</optgroup>
|
|
297
|
+
</select></span>
|
|
298
|
+
<button class="btn" id="btn-fit" type="button" aria-label="Fit graph to screen" title="Fit graph to screen">
|
|
299
|
+
<svg viewBox="0 0 24 24"><path d="M3 9V6a2 2 0 0 1 2-2h3M21 9V6a2 2 0 0 0-2-2h-3M3 15v3a2 2 0 0 0 2 2h3M21 15v3a2 2 0 0 0-2 2h-3"/><rect x="9.5" y="9.5" width="5" height="5" rx="1"/></svg></button>
|
|
300
|
+
<button class="btn" id="btn-cluster" type="button" aria-label="Cluster by area" aria-pressed="false" title="Cluster nodes by area">
|
|
301
|
+
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="4" opacity=".45"/><circle cx="9" cy="9" r="1.9" fill="currentColor" stroke="none"/><circle cx="15" cy="10" r="1.9" fill="currentColor" stroke="none"/><circle cx="11" cy="15" r="1.9" fill="currentColor" stroke="none"/></svg></button>
|
|
302
|
+
<button class="btn" id="btn-panel" type="button" aria-label="Toggle inspector" aria-pressed="true"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M15 4v16"/></svg></button>
|
|
303
|
+
</div>
|
|
304
|
+
</header>
|
|
305
|
+
|
|
306
|
+
<div id="views">
|
|
307
|
+
<!-- GRAPH -->
|
|
308
|
+
<section class="view active" id="view-graph">
|
|
309
|
+
<div class="graph-body" data-side="default">
|
|
310
|
+
<div id="stage">
|
|
311
|
+
<div id="cy"></div>
|
|
312
|
+
<div class="ghint">click a concept · scroll to zoom · drag to pan</div>
|
|
313
|
+
<aside id="filters" class="slideover" aria-label="Graph filters">
|
|
314
|
+
<div class="fhead"><h3>Filters</h3><button class="link" id="filters-reset" type="button">Reset</button>
|
|
315
|
+
<button class="btn" id="filters-close" type="button" aria-label="Close filters" style="width:28px;height:28px"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div>
|
|
316
|
+
<div class="fbody">
|
|
317
|
+
<label class="search" style="max-width:none;margin-bottom:12px"><svg viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
|
318
|
+
<input id="filter-search" class="field" placeholder="find a type, area, tag…" autocomplete="off" aria-label="Find a filter"></label>
|
|
319
|
+
<div class="fgroup"><h4>Types</h4><div class="fchips" id="ftypes"></div></div>
|
|
320
|
+
<div class="fgroup"><h4>Areas</h4><div class="fchips" id="fareas"></div></div>
|
|
321
|
+
<div class="fgroup"><h4>Tags</h4><div class="fchips" id="ftags"></div></div>
|
|
322
|
+
</div>
|
|
323
|
+
</aside>
|
|
324
|
+
</div>
|
|
325
|
+
<aside id="side">
|
|
326
|
+
<div id="side-head"><span class="lbl">Inspector</span>
|
|
327
|
+
<button class="btn" id="side-widen" type="button" aria-label="Widen panel" aria-pressed="false" title="Widen / restore">
|
|
328
|
+
<svg class="wide" viewBox="0 0 24 24"><path d="M13 6l-6 6 6 6M19 6l-6 6 6 6"/></svg>
|
|
329
|
+
<svg class="narrow" viewBox="0 0 24 24" style="display:none"><path d="M11 6l6 6-6 6M5 6l6 6-6 6"/></svg></button>
|
|
330
|
+
<button class="btn" id="side-close" type="button" aria-label="Hide panel" title="Hide panel"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div>
|
|
331
|
+
<div id="side-body"><p class="empty">Select a concept — <b>click a node</b> in the graph, or search above — to read its full markdown here.</p></div>
|
|
332
|
+
</aside>
|
|
333
|
+
</div>
|
|
334
|
+
</section>
|
|
335
|
+
|
|
336
|
+
<!-- CATALOG -->
|
|
337
|
+
<section class="view" id="view-catalog">
|
|
338
|
+
<div class="pad">
|
|
339
|
+
<div class="shead"><h2>Catalog</h2><span class="cnt" id="cat-cnt"></span></div>
|
|
340
|
+
<div class="cat-types">
|
|
341
|
+
<div id="cat-types"></div>
|
|
342
|
+
<button class="btn text" id="cat-filters-btn" type="button" aria-label="Filters"><svg viewBox="0 0 24 24"><path d="M3 5h18l-7 8v6l-4-2v-4Z"/></svg>Filters<span class="fbadge">0</span></button>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="grid" id="cat-grid"><div class="none">Loading concepts…</div></div>
|
|
345
|
+
</div>
|
|
346
|
+
<aside id="cat-filters" class="slideover" aria-label="Catalog filters">
|
|
347
|
+
<div class="fhead"><h3>Filters</h3><button class="link" id="cat-filters-reset" type="button">Reset</button>
|
|
348
|
+
<button class="btn" id="cat-filters-close" type="button" aria-label="Close filters" style="width:28px;height:28px"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div>
|
|
349
|
+
<div class="fbody">
|
|
350
|
+
<label class="search" style="max-width:none;margin-bottom:12px"><svg viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
|
351
|
+
<input id="cat-filter-search" class="field" placeholder="find a type, area, tag…" autocomplete="off"></label>
|
|
352
|
+
<div class="fgroup"><h4>Types</h4><div class="fchips" id="cat-ftypes"></div></div>
|
|
353
|
+
<div class="fgroup"><h4>Areas</h4><div class="fchips" id="cat-fareas"></div></div>
|
|
354
|
+
<div class="fgroup"><h4>Tags</h4><div class="fchips" id="cat-ftags"></div></div>
|
|
355
|
+
</div>
|
|
356
|
+
</aside>
|
|
357
|
+
</section>
|
|
358
|
+
|
|
359
|
+
<!-- FILES -->
|
|
360
|
+
<section class="view" id="view-files">
|
|
361
|
+
<div class="files-grid">
|
|
362
|
+
<div class="ftree">
|
|
363
|
+
<div class="ftree-head"><label class="search" style="max-width:none;margin-bottom:8px"><svg viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
|
364
|
+
<input id="file-filter" class="field" placeholder="filter files…" autocomplete="off"></label>
|
|
365
|
+
<div class="combo" id="file-type-combo"><input id="file-type-input" class="field" placeholder="all types" autocomplete="off" role="combobox" aria-expanded="false" aria-controls="file-type-list" aria-autocomplete="list" aria-label="Filter files by type"><button class="clear" id="file-type-clear" type="button" aria-label="Clear type filter"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button><ul class="listbox" id="file-type-list" role="listbox" hidden></ul></div>
|
|
366
|
+
<div class="combo" id="file-tag-combo"><input id="file-tag-input" class="field" placeholder="all tags" autocomplete="off" role="combobox" aria-expanded="false" aria-controls="file-tag-list" aria-autocomplete="list" aria-label="Filter files by tag"><button class="clear" id="file-tag-clear" type="button" aria-label="Clear tag filter"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button><ul class="listbox" id="file-tag-list" role="listbox" hidden></ul></div></div>
|
|
367
|
+
<div class="ftree-list" id="ftree-list"><p class="empty" style="padding:14px">Loading files…</p></div>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="fpreview">
|
|
370
|
+
<div class="fp-head" id="fp-head" hidden><span class="type" id="fp-type"></span><h2 class="title" id="fp-title"></h2>
|
|
371
|
+
<button class="btn text" id="fp-graph" type="button" style="margin-left:auto"><svg viewBox="0 0 24 24"><circle cx="6" cy="6" r="2.4"/><circle cx="18" cy="9" r="2.4"/><circle cx="9" cy="18" r="2.4"/><path d="M7.7 7.3 15.9 8.6M10.9 16.4 16.3 10.5"/></svg>Open in graph</button></div>
|
|
372
|
+
<div class="fp-body" id="fp-body"><p class="empty">Pick a file on the left to read it here — Mermaid diagrams render inline.</p></div>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</section>
|
|
376
|
+
|
|
377
|
+
<!-- TAGS -->
|
|
378
|
+
<section class="view" id="view-tags">
|
|
379
|
+
<div class="pad">
|
|
380
|
+
<div class="shead" style="align-items:center"><h2>Tags</h2><span class="cnt" id="tag-cnt"></span>
|
|
381
|
+
<button class="btn text" id="tag-filters-btn" type="button" aria-label="Filters" style="margin-left:auto"><svg viewBox="0 0 24 24"><path d="M3 5h18l-7 8v6l-4-2v-4Z"/></svg>Filters<span class="fbadge">0</span></button></div>
|
|
382
|
+
<div class="cloud" id="tag-cloud"></div>
|
|
383
|
+
<div id="tag-detail"><p class="empty">Select a tag to see the concepts that carry it.</p></div>
|
|
384
|
+
</div>
|
|
385
|
+
<aside id="tag-filters" class="slideover" aria-label="Tag filters">
|
|
386
|
+
<div class="fhead"><h3>Filters</h3><button class="link" id="tag-filters-reset" type="button">Reset</button>
|
|
387
|
+
<button class="btn" id="tag-filters-close" type="button" aria-label="Close filters" style="width:28px;height:28px"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div>
|
|
388
|
+
<div class="fbody">
|
|
389
|
+
<div class="fgroup"><h4>Types</h4><div class="fchips" id="tag-ftypes"></div></div>
|
|
390
|
+
<div class="fgroup"><h4>Areas</h4><div class="fchips" id="tag-fareas"></div></div>
|
|
391
|
+
</div>
|
|
392
|
+
</aside>
|
|
393
|
+
</section>
|
|
394
|
+
|
|
395
|
+
<!-- STATS -->
|
|
396
|
+
<section class="view" id="view-stats">
|
|
397
|
+
<div class="pad">
|
|
398
|
+
<div class="shead"><h2>Overview</h2><span class="cnt">the bundle at a glance</span></div>
|
|
399
|
+
<div class="stat-grid" id="stat-grid"></div>
|
|
400
|
+
<div class="two">
|
|
401
|
+
<div><div class="shead"><h2>By type</h2></div><div class="bars" id="bars-type"></div></div>
|
|
402
|
+
<div><div class="shead"><h2>By area</h2></div><div class="bars" id="bars-area"></div></div>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
</section>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<script>
|
|
411
|
+
const NODES=<%= nodes_json %>, EDGES=<%= edges_json %>, TYPES=<%= types_json %>, TAGS=<%= tags_json %>;
|
|
412
|
+
const NODE_ENDPOINT=<%= @node_endpoint.to_json %>, META_ENDPOINT=<%= @meta_endpoint.to_json %>;
|
|
413
|
+
const CATALOG_ENDPOINT="catalog";
|
|
414
|
+
const MIN=<%= OKF::Server::Graph::MIN_SIZE %>, MAX=<%= OKF::Server::Graph::MAX_SIZE %>;
|
|
415
|
+
const PALETTE=["#6E56CF","#D97757","#22C55E","#3B82F6","#EAB308","#EC4899","#14B8A6","#F97316","#A855F7","#0EA5E9","#84CC16","#EF4444","#64748B"];
|
|
416
|
+
const root=document.documentElement;
|
|
417
|
+
const byId=Object.fromEntries(NODES.map(n=>[n.id,n]));
|
|
418
|
+
const outL={}, inL={};
|
|
419
|
+
NODES.forEach(n=>{outL[n.id]=[];inL[n.id]=[];});
|
|
420
|
+
EDGES.forEach(e=>{outL[e.source].push(e.target);inL[e.target].push(e.source);});
|
|
421
|
+
const typeOf={};Object.entries(TYPES).forEach(([t,ids])=>ids.forEach(id=>typeOf[id]=t));
|
|
422
|
+
const tagsOf={};NODES.forEach(n=>tagsOf[n.id]=[]);Object.entries(TAGS).forEach(([t,ids])=>ids.forEach(id=>{(tagsOf[id]=tagsOf[id]||[]).push(t);}));
|
|
423
|
+
const types=Object.keys(TYPES).sort();
|
|
424
|
+
const color=Object.fromEntries(types.map((t,i)=>[t,PALETTE[i%PALETTE.length]]));
|
|
425
|
+
const deg=id=>(outL[id]||[]).length+(inL[id]||[]).length;
|
|
426
|
+
const szOf=id=>Math.max(MIN,Math.min(MAX,MIN+deg(id)*4));
|
|
427
|
+
const esc=s=>(s==null?'':String(s)).replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));
|
|
428
|
+
const cvar=n=>getComputedStyle(root).getPropertyValue(n).trim();
|
|
429
|
+
const isDark=()=>root.getAttribute('data-theme')==='dark';
|
|
430
|
+
const reduce=()=>matchMedia('(prefers-reduced-motion:reduce)').matches;
|
|
431
|
+
const areaOf=id=>id.includes('/')?id.split('/')[0]:'(root)';
|
|
432
|
+
|
|
433
|
+
/* ── Cytoscape ── */
|
|
434
|
+
function styleSheet(){const ink=cvar('--ink'),halo=cvar('--canvas'),edge=cvar('--edge');
|
|
435
|
+
return [
|
|
436
|
+
{selector:'node',style:{'background-color':'data(c)','label':'data(title)','color':ink,'font-family':'Poppins,sans-serif',
|
|
437
|
+
'font-size':10,'font-weight':500,'text-wrap':'wrap','text-max-width':120,'text-valign':'bottom','text-margin-y':5,
|
|
438
|
+
'text-outline-width':2.5,'text-outline-color':halo,'min-zoomed-font-size':7,'border-width':0,'width':'data(sz)','height':'data(sz)'}},
|
|
439
|
+
{selector:'edge',style:{'width':1.2,'line-color':edge,'target-arrow-color':edge,'target-arrow-shape':'triangle','arrow-scale':.8,'curve-style':'bezier','opacity':.8}},
|
|
440
|
+
{selector:'.dim',style:{'opacity':.1}},
|
|
441
|
+
{selector:'.hl',style:{'border-width':3,'border-color':cvar('--accent')}},
|
|
442
|
+
{selector:':parent',style:{'background-color':cvar('--ink'),'background-opacity':0.05,'border-width':1,'border-color':cvar('--edge'),'shape':'round-rectangle','padding':26,'label':'data(label)','color':cvar('--ink'),'font-size':13,'font-weight':600,'text-valign':'top','text-halign':'center','text-margin-y':-8,'min-zoomed-font-size':0,'text-outline-width':3,'text-outline-color':cvar('--canvas')}}
|
|
443
|
+
];}
|
|
444
|
+
const cy=cytoscape({container:document.getElementById('cy'),minZoom:.2,maxZoom:1.6,wheelSensitivity:.2,
|
|
445
|
+
elements:[...NODES.map(n=>({data:{id:n.id,title:n.title,c:color[typeOf[n.id]||'Untyped']||'#64748b',sz:szOf(n.id)}})),...EDGES.map(e=>({data:e}))],
|
|
446
|
+
style:styleSheet(),layout:{name:'<%= @layout %>',animate:false,nodeRepulsion:9000,idealEdgeLength:90,padding:40}});
|
|
447
|
+
function applyCyTheme(){cy.style(styleSheet());}
|
|
448
|
+
/* Debounced so the panel width transition (--side-w, .22s) fires cy.resize()
|
|
449
|
+
once after it settles instead of on every frame — avoids canvas flicker. */
|
|
450
|
+
let cyResizeTimer;
|
|
451
|
+
new ResizeObserver(()=>{clearTimeout(cyResizeTimer);cyResizeTimer=setTimeout(()=>cy.resize(),240);}).observe(document.getElementById('cy'));
|
|
452
|
+
const cyEl=document.getElementById('cy');
|
|
453
|
+
cy.on('mouseover','node',()=>cyEl.style.cursor='pointer');
|
|
454
|
+
cy.on('mouseout','node',()=>cyEl.style.cursor='');
|
|
455
|
+
|
|
456
|
+
/* ── Mermaid (lazy) ── */
|
|
457
|
+
let _mermaid=null;
|
|
458
|
+
function loadMermaid(){return _mermaid||(_mermaid=new Promise((res,rej)=>{const s=document.createElement('script');
|
|
459
|
+
s.src='https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';s.onload=()=>{window.mermaid.initialize({startOnLoad:false,securityLevel:'strict',theme:isDark()?'dark':'default'});res(window.mermaid);};s.onerror=rej;document.head.appendChild(s);}));}
|
|
460
|
+
let mermaidSeq=0;
|
|
461
|
+
function processMermaid(el){const blocks=[...el.querySelectorAll('code.language-mermaid')];if(!blocks.length)return;
|
|
462
|
+
loadMermaid().then(m=>{const nodes=[];blocks.forEach(b=>{const pre=b.closest('pre');const d=document.createElement('div');
|
|
463
|
+
d.className='mermaid';d.dataset.src=b.textContent;d.id='mmd-'+(++mermaidSeq);d.textContent=b.textContent;pre.replaceWith(d);nodes.push(d);});
|
|
464
|
+
m.run({nodes}).catch(()=>{});}).catch(()=>{});}
|
|
465
|
+
function rethemeMermaid(){if(!window.mermaid)return;const nodes=[...document.querySelectorAll('.mermaid')];if(!nodes.length)return;
|
|
466
|
+
window.mermaid.initialize({startOnLoad:false,securityLevel:'strict',theme:isDark()?'dark':'default'});
|
|
467
|
+
nodes.forEach(d=>{if(d.dataset.src){d.removeAttribute('data-processed');d.innerHTML='';d.textContent=d.dataset.src;}});
|
|
468
|
+
window.mermaid.run({nodes}).catch(()=>{});}
|
|
469
|
+
function renderMarkdown(el,text){el.innerHTML=text?marked.parse(text):'<span class="empty">empty body</span>';processMermaid(el);}
|
|
470
|
+
|
|
471
|
+
/* ── inspector ── */
|
|
472
|
+
const gbody=document.querySelector('.graph-body'), sideBody=document.getElementById('side-body');
|
|
473
|
+
const btnPanel=document.getElementById('btn-panel'), sideWiden=document.getElementById('side-widen');
|
|
474
|
+
function setSide(s){gbody.setAttribute('data-side',s);btnPanel.setAttribute('aria-pressed',String(s!=='hidden'));
|
|
475
|
+
sideWiden.setAttribute('aria-pressed',String(s==='wide'));
|
|
476
|
+
sideWiden.querySelector('.wide').style.display=s==='wide'?'none':'block';
|
|
477
|
+
sideWiden.querySelector('.narrow').style.display=s==='wide'?'block':'none';}
|
|
478
|
+
function openPanel(){if(gbody.getAttribute('data-side')==='hidden')setSide('default');}
|
|
479
|
+
btnPanel.onclick=()=>setSide(gbody.getAttribute('data-side')==='hidden'?'default':'hidden');
|
|
480
|
+
document.getElementById('side-close').onclick=()=>setSide('hidden');
|
|
481
|
+
sideWiden.onclick=()=>setSide(gbody.getAttribute('data-side')==='wide'?'default':'wide');
|
|
482
|
+
function relList(t,arr){if(!arr.length)return'';return `<div class="rel"><h4>${t}</h4>${arr.map(id=>`<a data-go="${esc(id)}">${esc((byId[id]||{}).title||id)}</a>`).join('')}</div>`;}
|
|
483
|
+
function show(id){const n=byId[id];if(!n)return;const ty=typeOf[id]||'Untyped';const c=color[ty]||'#64748b';
|
|
484
|
+
openPanel();sideBody.scrollTop=0;
|
|
485
|
+
sideBody.innerHTML=`<span class="type"><span class="dot" style="background:${c}"></span>${esc(ty)}</span>
|
|
486
|
+
<h2 class="title">${esc(n.title||id)}</h2><div class="desc loading" id="desc">loading…</div>
|
|
487
|
+
<div class="tags">${(tagsOf[id]||[]).map(t=>`<span class="tag">${esc(t)}</span>`).join('')}</div>
|
|
488
|
+
${relList('Links to',outL[id])}${relList('Linked from',inL[id])}<div class="body" id="body"></div>`;
|
|
489
|
+
sideBody.querySelectorAll('[data-go]').forEach(a=>a.onclick=()=>select(a.getAttribute('data-go')));
|
|
490
|
+
const eid=encodeURIComponent(id);
|
|
491
|
+
fetch(META_ENDPOINT+'?id='+eid).then(r=>r.ok?r.text():'').then(h=>{const d=document.getElementById('desc');if(d){d.innerHTML=h||'<span class="empty">no description</span>';d.classList.remove('loading');}}).catch(()=>{});
|
|
492
|
+
const bodyEl=sideBody.querySelector('#body');
|
|
493
|
+
fetch(NODE_ENDPOINT+'?id='+eid).then(r=>r.ok?r.text():'').then(md=>{if(bodyEl.isConnected)renderMarkdown(bodyEl,md);}).catch(()=>{});}
|
|
494
|
+
function select(id){const ele=cy.getElementById(id);if(!ele.length)return;show(id);
|
|
495
|
+
cy.elements().removeClass('hl').addClass('dim');const nb=ele.closedNeighborhood();nb.removeClass('dim');ele.addClass('hl');
|
|
496
|
+
cy.animate({center:{eles:ele},duration:250});
|
|
497
|
+
try{if(decodeURIComponent((location.hash||'').slice(1))!==id)location.hash=encodeURIComponent(id);}catch(e){}}
|
|
498
|
+
cy.on('tap','node',e=>{if(e.target.isParent())return;select(e.target.id());});
|
|
499
|
+
cy.on('tap',e=>{if(e.target===cy)cy.elements().removeClass('dim hl');});
|
|
500
|
+
|
|
501
|
+
/* ── graph filters (slide-over) ── */
|
|
502
|
+
const hiddenTypes=new Set(), activeTags=new Set(), activeAreas=new Set();
|
|
503
|
+
const searchInput=document.getElementById('search');
|
|
504
|
+
const q={graph:'',catalog:'',files:'',tags:''};
|
|
505
|
+
function applyGraphFilter(){const s=q.graph.toLowerCase();
|
|
506
|
+
cy.nodes().forEach(n=>{if(n.isParent())return;const id=n.data('id');const t=typeOf[id]||'Untyped';const nt=tagsOf[id]||[];
|
|
507
|
+
const hay=(n.data('title')+' '+t+' '+nt.join(' ')).toLowerCase();
|
|
508
|
+
const m=(!s||hay.includes(s))&&!hiddenTypes.has(t)&&(activeTags.size===0||nt.some(x=>activeTags.has(x)))&&(activeAreas.size===0||activeAreas.has(areaOf(id)));
|
|
509
|
+
n.style('display',m?'element':'none');});}
|
|
510
|
+
function fbadge(){const c=hiddenTypes.size+activeTags.size+activeAreas.size;const b=document.getElementById('btn-filters');
|
|
511
|
+
b.classList.toggle('on-filter',c>0);b.querySelector('.fbadge').textContent=c;}
|
|
512
|
+
const ftypes=document.getElementById('ftypes'), ftags=document.getElementById('ftags'), fareas=document.getElementById('fareas');
|
|
513
|
+
const graphAreas=[...new Set(NODES.map(n=>areaOf(n.id)))].sort();
|
|
514
|
+
const areaCount={};NODES.forEach(n=>{const a=areaOf(n.id);areaCount[a]=(areaCount[a]||0)+1;});
|
|
515
|
+
const tagsByCount=Object.entries(TAGS).sort((a,b)=>b[1].length-a[1].length).map(e=>e[0]);
|
|
516
|
+
const dotFor=t=>`<span class="dot" style="background:${color[t]}"></span>`;
|
|
517
|
+
const toggleSet=(s,v)=>{s.has(v)?s.delete(v):s.add(v);};
|
|
518
|
+
/* One renderer for every filter chip group (graph, catalog and tags views):
|
|
519
|
+
paints the items with their current state class and wires the toggle. */
|
|
520
|
+
function chipRow(el,items,attr,stateCls,isOn,onToggle){
|
|
521
|
+
el.innerHTML=items.map(o=>`<span class="chip${isOn(o.v)?' '+stateCls:''}" ${attr}="${esc(o.v)}">${o.dot||''}${esc(o.v)} <span class="c">${o.n}</span></span>`).join('');
|
|
522
|
+
el.querySelectorAll('.chip').forEach(ch=>ch.onclick=()=>onToggle(ch.getAttribute(attr)));}
|
|
523
|
+
const typeItems=list=>list.map(t=>({v:t,n:(TYPES[t]||[]).length,dot:dotFor(t)}));
|
|
524
|
+
const areaItems=list=>list.map(a=>({v:a,n:areaCount[a]}));
|
|
525
|
+
const tagItems=list=>list.map(t=>({v:t,n:(TAGS[t]||[]).length}));
|
|
526
|
+
/* The find box narrows the chips themselves; while it is empty, tags cap at the
|
|
527
|
+
top 40 so a busy bundle doesn't flood the panel — searching reaches ALL tags. */
|
|
528
|
+
const fsearch=document.getElementById('filter-search');
|
|
529
|
+
function syncFilterChips(){const qq=fsearch.value.trim().toLowerCase();const match=v=>!qq||v.toLowerCase().includes(qq);
|
|
530
|
+
const after=()=>{syncFilterChips();applyGraphFilter();fbadge();};
|
|
531
|
+
chipRow(ftypes,typeItems(types.filter(match)),'data-t','off',t=>hiddenTypes.has(t),t=>{toggleSet(hiddenTypes,t);after();});
|
|
532
|
+
chipRow(fareas,areaItems(graphAreas.filter(match)),'data-area','on',a=>activeAreas.has(a),a=>{toggleSet(activeAreas,a);after();});
|
|
533
|
+
chipRow(ftags,tagItems(qq?tagsByCount.filter(match):tagsByCount.slice(0,40)),'data-tag','on',t=>activeTags.has(t),t=>{toggleSet(activeTags,t);after();});}
|
|
534
|
+
fsearch.oninput=syncFilterChips;
|
|
535
|
+
syncFilterChips();
|
|
536
|
+
// Fit the graph to the currently-visible nodes after a view swap settles (two rAFs
|
|
537
|
+
// so the container has resized), reusing the working fitGraph implementation.
|
|
538
|
+
function fitVisible(){requestAnimationFrame(()=>requestAnimationFrame(fitGraph));}
|
|
539
|
+
function focusGraphType(t){setView('graph');hiddenTypes.clear();activeTags.clear();activeAreas.clear();types.forEach(x=>{if(x!==t)hiddenTypes.add(x);});syncFilterChips();applyGraphFilter();fbadge();fitVisible();}
|
|
540
|
+
function focusGraphArea(a){setView('graph');hiddenTypes.clear();activeTags.clear();activeAreas.clear();activeAreas.add(a);syncFilterChips();applyGraphFilter();fbadge();fitVisible();}
|
|
541
|
+
const filtersEl=document.getElementById('filters');
|
|
542
|
+
document.getElementById('btn-filters').onclick=()=>filtersEl.classList.toggle('open');
|
|
543
|
+
document.getElementById('filters-close').onclick=()=>filtersEl.classList.remove('open');
|
|
544
|
+
document.getElementById('filters-reset').onclick=()=>{hiddenTypes.clear();activeTags.clear();activeAreas.clear();syncFilterChips();applyGraphFilter();fbadge();};
|
|
545
|
+
|
|
546
|
+
/* ── layout: 5 built-ins + extension layouts loaded lazily on first use ── */
|
|
547
|
+
const layoutSel=document.getElementById('layout');
|
|
548
|
+
const LAYOUT_EXT={
|
|
549
|
+
fcose:{srcs:['https://cdn.jsdelivr.net/npm/layout-base@2.0.1/layout-base.js','https://cdn.jsdelivr.net/npm/cose-base@2.2.0/cose-base.js','https://cdn.jsdelivr.net/npm/cytoscape-fcose@2.2.0/cytoscape-fcose.js'],reg:'cytoscapeFcose'},
|
|
550
|
+
dagre:{srcs:['https://cdn.jsdelivr.net/npm/dagre@0.8.5/dist/dagre.min.js','https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.5.0/cytoscape-dagre.min.js'],reg:'cytoscapeDagre'},
|
|
551
|
+
cola:{srcs:['https://cdn.jsdelivr.net/npm/webcola@3.4.0/WebCola/cola.min.js','https://cdn.jsdelivr.net/npm/cytoscape-cola@2.5.1/cytoscape-cola.min.js'],reg:'cytoscapeCola'}
|
|
552
|
+
};
|
|
553
|
+
const loadedExt={};
|
|
554
|
+
function loadScript(src){return new Promise((res,rej)=>{const s=document.createElement('script');s.src=src;s.onload=res;s.onerror=()=>rej(new Error(src));document.head.appendChild(s);});}
|
|
555
|
+
async function ensureLayout(name){const ext=LAYOUT_EXT[name];if(!ext||loadedExt[name])return true;
|
|
556
|
+
try{for(const src of ext.srcs)await loadScript(src);cytoscape.use(window[ext.reg]);loadedExt[name]=true;return true;}
|
|
557
|
+
catch(e){console.warn('layout '+name+' failed to load',e);return false;}}
|
|
558
|
+
function layoutOpts(name){const base={animate:true,padding:40};
|
|
559
|
+
if(name==='fcose')return{...base,name:'fcose',quality:'default',nodeRepulsion:6500,idealEdgeLength:95,nodeSeparation:80,packComponents:true};
|
|
560
|
+
if(name==='dagre')return{...base,name:'dagre',rankDir:'TB',nodeSep:26,rankSep:55,edgeSep:12};
|
|
561
|
+
if(name==='cola')return{...base,name:'cola',maxSimulationTime:2500,edgeLength:95,nodeSpacing:8};
|
|
562
|
+
if(name==='cose')return{...base,name:'cose',nodeRepulsion:9000,idealEdgeLength:90};
|
|
563
|
+
return{...base,name};}
|
|
564
|
+
async function runLayout(name){if(await ensureLayout(name))cy.layout(layoutOpts(name)).run();else layoutSel.value='cose';}
|
|
565
|
+
layoutSel.onchange=e=>{if(clustered)return;runLayout(e.target.value);};
|
|
566
|
+
layoutSel.value='<%= @layout %>';
|
|
567
|
+
|
|
568
|
+
/* ── cluster by area (compound parent nodes) ── */
|
|
569
|
+
let clustered=false;const btnCluster=document.getElementById('btn-cluster');
|
|
570
|
+
function setClustered(on){if(on===clustered)return;clustered=on;btnCluster.setAttribute('aria-pressed',String(on));layoutSel.disabled=on;
|
|
571
|
+
if(on){const as=[...new Set(NODES.map(n=>areaOf(n.id)))];
|
|
572
|
+
cy.add(as.map(a=>({group:'nodes',data:{id:'area::'+a,label:a}})));
|
|
573
|
+
cy.batch(()=>NODES.forEach(n=>{const e=cy.getElementById(n.id);if(e.length)e.move({parent:'area::'+areaOf(n.id)});}));
|
|
574
|
+
cy.layout({name:'cose',animate:true,padding:40,nodeRepulsion:8000,idealEdgeLength:70,nestingFactor:1.2,gravity:.7}).run();
|
|
575
|
+
}else{
|
|
576
|
+
cy.batch(()=>NODES.forEach(n=>{const e=cy.getElementById(n.id);if(e.length&&e.isChild())e.move({parent:null});}));
|
|
577
|
+
cy.nodes().filter(n=>n.id().indexOf('area::')===0).remove();
|
|
578
|
+
cy.layout({name:layoutSel.value,animate:true,padding:40,nodeRepulsion:9000,idealEdgeLength:90}).run();}}
|
|
579
|
+
btnCluster.onclick=()=>setClustered(!clustered);
|
|
580
|
+
|
|
581
|
+
/* ── fit to screen ── */
|
|
582
|
+
// cy.animate({fit:…}) is a no-op in this Cytoscape build, so compute the target
|
|
583
|
+
// zoom + pan and animate those (which do work): fits the visible nodes, re-centered.
|
|
584
|
+
function fitGraph(){const vis=cy.nodes(':visible').filter(n=>!n.isParent());const eles=vis.length?vis:cy.nodes();if(!eles.length)return;
|
|
585
|
+
const bb=eles.boundingBox(),pad=60,w=cy.width(),h=cy.height();
|
|
586
|
+
const z=Math.max(cy.minZoom(),Math.min(cy.maxZoom(),Math.min((w-2*pad)/bb.w,(h-2*pad)/bb.h)));
|
|
587
|
+
cy.stop();cy.animate({zoom:z,pan:{x:w/2-z*(bb.x1+bb.w/2),y:h/2-z*(bb.y1+bb.h/2)}},{duration:300,easing:'ease-out-cubic'});}
|
|
588
|
+
document.getElementById('btn-fit').onclick=fitGraph;
|
|
589
|
+
|
|
590
|
+
/* ── view switching ── */
|
|
591
|
+
let view='graph';
|
|
592
|
+
const inited={};
|
|
593
|
+
function goToGraph(id){setView('graph');requestAnimationFrame(()=>select(id));}
|
|
594
|
+
function setView(v){if(v===view)return;view=v;document.getElementById('app').setAttribute('data-view',v);
|
|
595
|
+
document.querySelectorAll('.rail-item').forEach(b=>b.classList.toggle('active',b.dataset.view===v));
|
|
596
|
+
document.querySelectorAll('.view').forEach(s=>s.classList.toggle('active',s.id==='view-'+v));
|
|
597
|
+
const ph={graph:'search concepts…',catalog:'search concepts…',files:'',tags:'search tags…'}[v];
|
|
598
|
+
if(ph!==undefined){searchInput.placeholder=ph;searchInput.value=q[v]||'';}
|
|
599
|
+
if(v==='graph')requestAnimationFrame(()=>cy.resize());
|
|
600
|
+
if(v==='catalog'&&!inited.catalog)initCatalog();
|
|
601
|
+
if(v==='files'&&!inited.files)initFiles();
|
|
602
|
+
if(v==='tags'&&!inited.tags)initTags();
|
|
603
|
+
if(v==='stats'&&!inited.stats)initStats();}
|
|
604
|
+
document.querySelectorAll('.rail-item').forEach(b=>b.onclick=()=>setView(b.dataset.view));
|
|
605
|
+
searchInput.oninput=()=>{q[view]=searchInput.value;
|
|
606
|
+
if(view==='graph')applyGraphFilter();else if(view==='catalog')renderCatalog();else if(view==='tags')renderCloud();};
|
|
607
|
+
|
|
608
|
+
/* ── catalog ── */
|
|
609
|
+
let CATALOG=null;
|
|
610
|
+
function getCatalog(){return CATALOG||(CATALOG=fetch(CATALOG_ENDPOINT).then(r=>r.json()).then(d=>d.concepts));}
|
|
611
|
+
const catActiveTypes=new Set(), catActiveAreas=new Set(), catActiveTags=new Set();
|
|
612
|
+
const catByCount=types.slice().sort((a,b)=>(TYPES[b]||[]).length-(TYPES[a]||[]).length);
|
|
613
|
+
const catChip=t=>`<span class="chip" data-t="${esc(t)}"><span class="dot" style="background:${color[t]}"></span>${esc(t)} <span class="c">${(TYPES[t]||[]).length}</span></span>`;
|
|
614
|
+
function syncCatChips(){document.querySelectorAll('#cat-types .chip').forEach(c=>c.classList.toggle('on',catActiveTypes.has(c.getAttribute('data-t'))));
|
|
615
|
+
const n=catActiveTypes.size+catActiveAreas.size+catActiveTags.size;
|
|
616
|
+
const b=document.getElementById('cat-filters-btn');b.classList.toggle('on-filter',n>0);b.querySelector('.fbadge').textContent=n;}
|
|
617
|
+
function catToggle(t){toggleSet(catActiveTypes,t);renderCatFilters();renderCatalog();}
|
|
618
|
+
function wireCatChips(el){el.querySelectorAll('.chip').forEach(ch=>ch.onclick=()=>catToggle(ch.getAttribute('data-t')));}
|
|
619
|
+
/* The slide-over mirrors the graph filters — Types, Areas, Tags — with one find
|
|
620
|
+
box narrowing the chips of all three groups (tags cap at 40 until searched). */
|
|
621
|
+
function renderCatFilters(){const qq=(document.getElementById('cat-filter-search').value||'').trim().toLowerCase();
|
|
622
|
+
const match=v=>!qq||v.toLowerCase().includes(qq);
|
|
623
|
+
const after=()=>{renderCatFilters();renderCatalog();};
|
|
624
|
+
chipRow(document.getElementById('cat-ftypes'),typeItems(catByCount.filter(match)),'data-t','on',t=>catActiveTypes.has(t),t=>{toggleSet(catActiveTypes,t);after();});
|
|
625
|
+
chipRow(document.getElementById('cat-fareas'),areaItems(graphAreas.filter(match)),'data-area','on',a=>catActiveAreas.has(a),a=>{toggleSet(catActiveAreas,a);after();});
|
|
626
|
+
chipRow(document.getElementById('cat-ftags'),tagItems(qq?tagsByCount.filter(match):tagsByCount.slice(0,40)),'data-tag','on',t=>catActiveTags.has(t),t=>{toggleSet(catActiveTags,t);after();});
|
|
627
|
+
syncCatChips();}
|
|
628
|
+
function initCatalog(){inited.catalog=true;
|
|
629
|
+
// Only the five most common types get inline chips; everything else — types,
|
|
630
|
+
// areas, tags — lives in the searchable slide-over, so a bundle with many
|
|
631
|
+
// values doesn't flood the header.
|
|
632
|
+
const quick=document.getElementById('cat-types');quick.innerHTML=catByCount.slice(0,5).map(catChip).join('');wireCatChips(quick);
|
|
633
|
+
renderCatFilters();
|
|
634
|
+
const cf=document.getElementById('cat-filters');
|
|
635
|
+
document.getElementById('cat-filters-btn').onclick=()=>cf.classList.toggle('open');
|
|
636
|
+
document.getElementById('cat-filters-close').onclick=()=>cf.classList.remove('open');
|
|
637
|
+
document.getElementById('cat-filters-reset').onclick=()=>{catActiveTypes.clear();catActiveAreas.clear();catActiveTags.clear();renderCatFilters();renderCatalog();};
|
|
638
|
+
document.getElementById('cat-filter-search').oninput=renderCatFilters;
|
|
639
|
+
getCatalog().then(renderCatalog);}
|
|
640
|
+
function renderCatalog(){if(!CATALOG)return;getCatalog().then(list=>{const s=(q.catalog||'').toLowerCase();
|
|
641
|
+
const rows=list.filter(c=>{if(catActiveTypes.size&&!catActiveTypes.has(c.type))return false;
|
|
642
|
+
if(catActiveAreas.size&&!catActiveAreas.has(areaOf(c.id)))return false;
|
|
643
|
+
if(catActiveTags.size&&!c.tags.some(t=>catActiveTags.has(t)))return false;
|
|
644
|
+
if(s){const hay=(c.title+' '+c.description+' '+c.type+' '+c.tags.join(' ')+' '+c.id).toLowerCase();if(!hay.includes(s))return false;}return true;});
|
|
645
|
+
document.getElementById('cat-cnt').textContent=rows.length+' of '+list.length+' concepts';
|
|
646
|
+
const g=document.getElementById('cat-grid');
|
|
647
|
+
if(!rows.length){g.innerHTML='<div class="none">No concepts match — try clearing the filters.</div>';return;}
|
|
648
|
+
g.innerHTML=rows.map(c=>{const cc=color[c.type]||'#64748b';
|
|
649
|
+
const st=c.status?`<span class="status ${c.status==='shipped'?'shipped':''}">${esc(c.status)}</span>`:'';
|
|
650
|
+
const tags=c.tags.slice(0,4).map(t=>`<span class="tag">${esc(t)}</span>`).join('');
|
|
651
|
+
const ts=c.timestamp?`<span class="mini">⌂ ${esc(c.timestamp)}</span>`:'';
|
|
652
|
+
const lk=(c.links_out+c.links_in)?`<span class="mini links">↳ ${c.links_out+c.links_in} links</span>`:'';
|
|
653
|
+
return `<article class="card" tabindex="0" data-id="${esc(c.id)}">
|
|
654
|
+
<div class="r1"><span class="badge"><span class="dot" style="background:${cc}"></span>${esc(c.type)}</span>${st}<span class="area">${esc(c.dir)}/</span></div>
|
|
655
|
+
<h3>${esc(c.title)}</h3><p class="cd">${esc(c.description)||'<span class="empty">No description.</span>'}</p>
|
|
656
|
+
<div class="cm">${tags}${ts}${lk}</div></article>`;}).join('');
|
|
657
|
+
g.querySelectorAll('.card').forEach(el=>{const id=el.dataset.id;el.onclick=()=>goToGraph(id);
|
|
658
|
+
el.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();goToGraph(id);}};});});}
|
|
659
|
+
|
|
660
|
+
/* ── files ── */
|
|
661
|
+
let fileSel=null;
|
|
662
|
+
function initFiles(){inited.files=true;getCatalog().then(renderTree);}
|
|
663
|
+
function renderTree(list){const s=(document.getElementById('file-filter').value||'').toLowerCase();
|
|
664
|
+
const ft=fileTypeCombo.get(), fg=fileTagCombo.get();
|
|
665
|
+
const rows=list.filter(c=>(!ft||c.type===ft)&&(!fg||c.tags.indexOf(fg)>=0)&&(!s||(c.title+' '+c.id+' '+c.description).toLowerCase().includes(s)));
|
|
666
|
+
const groups={};rows.forEach(c=>{(groups[c.dir]=groups[c.dir]||[]).push(c);});
|
|
667
|
+
const el=document.getElementById('ftree-list');
|
|
668
|
+
if(!rows.length){el.innerHTML='<p class="empty" style="padding:14px">No files match.</p>';return;}
|
|
669
|
+
el.innerHTML=Object.keys(groups).sort().map(dir=>`<div class="ffolder">${dir==='.'?'(root)':esc(dir)+'/'}</div>`+
|
|
670
|
+
groups[dir].map(c=>{const cc=color[c.type]||'#64748b';const name=c.id.split('/').pop()+'.md';
|
|
671
|
+
return `<button class="file${fileSel===c.id?' sel':''}" data-id="${esc(c.id)}"><span class="fn"><span class="dot" style="background:${cc}"></span>${esc(name)}</span>${c.description?`<span class="fd">${esc(c.description)}</span>`:''}</button>`;}).join('')).join('');
|
|
672
|
+
el.querySelectorAll('.file').forEach(b=>b.onclick=()=>openFile(b.dataset.id,list));}
|
|
673
|
+
document.getElementById('file-filter').oninput=()=>getCatalog().then(renderTree);
|
|
674
|
+
/* Searchable single-select combobox (keyboard-navigable) — one per files filter:
|
|
675
|
+
type and tag. `items` is lazy so counts always reflect the current indexes. */
|
|
676
|
+
function makeCombo(name,allLabel,items,onChange){
|
|
677
|
+
const combo=document.getElementById(name+'-combo'), input=document.getElementById(name+'-input'), list=document.getElementById(name+'-list');
|
|
678
|
+
let value=null, active=-1;
|
|
679
|
+
function options(qq){qq=(qq||'').trim().toLowerCase();
|
|
680
|
+
return [{all:true,label:allLabel,n:NODES.length}].concat(items().filter(o=>!qq||o.v.toLowerCase().includes(qq)));}
|
|
681
|
+
function render(qq){list.innerHTML=options(qq).map(o=>{const sel=(o.all&&value===null)||(!o.all&&o.v===value);
|
|
682
|
+
const sw=o.c?`<span class="sw" style="background:${o.c}"></span>`:'';const attr=o.all?'data-all="1"':`data-v="${esc(o.v)}"`;
|
|
683
|
+
return `<li role="option" ${attr} class="opt${sel?' sel':''}">${sw}<span>${esc(o.all?o.label:o.v)}</span><span class="c">${o.n}</span></li>`;}).join('');active=-1;}
|
|
684
|
+
function open(){render(input.value===(value||'')?'':input.value);list.hidden=false;input.setAttribute('aria-expanded','true');}
|
|
685
|
+
function close(){list.hidden=true;input.setAttribute('aria-expanded','false');active=-1;}
|
|
686
|
+
function pick(li){if(!li)return;value=li.dataset.all?null:li.dataset.v;input.value=value||'';combo.classList.toggle('has',!!value);close();onChange();}
|
|
687
|
+
input.addEventListener('focus',open);
|
|
688
|
+
input.addEventListener('input',()=>{render(input.value.trim().toLowerCase());list.hidden=false;input.setAttribute('aria-expanded','true');});
|
|
689
|
+
input.addEventListener('blur',()=>setTimeout(()=>{close();input.value=value||'';},130));
|
|
690
|
+
input.addEventListener('keydown',e=>{const lis=[...list.querySelectorAll('li')];
|
|
691
|
+
if(e.key==='ArrowDown'){e.preventDefault();if(list.hidden)open();active=Math.min(lis.length-1,active+1);}
|
|
692
|
+
else if(e.key==='ArrowUp'){e.preventDefault();active=Math.max(0,active-1);}
|
|
693
|
+
else if(e.key==='Enter'){e.preventDefault();pick(lis[active]||lis[0]);input.blur();return;}
|
|
694
|
+
else if(e.key==='Escape'){close();input.value=value||'';input.blur();return;}
|
|
695
|
+
else return;
|
|
696
|
+
lis.forEach((l,i)=>l.classList.toggle('active',i===active));if(lis[active])lis[active].scrollIntoView({block:'nearest'});});
|
|
697
|
+
list.addEventListener('mousedown',e=>{const li=e.target.closest('li');if(!li)return;e.preventDefault();pick(li);});
|
|
698
|
+
document.getElementById(name+'-clear').addEventListener('click',()=>{value=null;input.value='';combo.classList.remove('has');onChange();input.focus();});
|
|
699
|
+
return {get:()=>value};}
|
|
700
|
+
const refreshTree=()=>getCatalog().then(renderTree);
|
|
701
|
+
const fileTypeCombo=makeCombo('file-type','All types',()=>catByCount.map(t=>({v:t,n:(TYPES[t]||[]).length,c:color[t]})),refreshTree);
|
|
702
|
+
const fileTagCombo=makeCombo('file-tag','All tags',()=>tagsByCount.map(t=>({v:t,n:(TAGS[t]||[]).length})),refreshTree);
|
|
703
|
+
function openFile(id,list){fileSel=id;const c=(list||[]).find(x=>x.id===id)||byId[id]||{};
|
|
704
|
+
document.querySelectorAll('.file').forEach(b=>b.classList.toggle('sel',b.dataset.id===id));
|
|
705
|
+
const ty=c.type||typeOf[id]||'Untyped';const cc=color[ty]||'#64748b';
|
|
706
|
+
document.getElementById('fp-head').hidden=false;
|
|
707
|
+
document.getElementById('fp-type').innerHTML=`<span class="dot" style="background:${cc}"></span>${esc(ty)}`;
|
|
708
|
+
document.getElementById('fp-title').textContent=c.title||(byId[id]||{}).title||id;
|
|
709
|
+
document.getElementById('fp-graph').onclick=()=>goToGraph(id);
|
|
710
|
+
const body=document.getElementById('fp-body');body.innerHTML='<p class="loading">loading…</p>';
|
|
711
|
+
fetch(NODE_ENDPOINT+'?id='+encodeURIComponent(id)).then(r=>r.ok?r.text():'').then(txt=>{
|
|
712
|
+
const wrap=document.createElement('div');wrap.className='body';renderMarkdown(wrap,txt);body.innerHTML='';body.appendChild(wrap);}).catch(()=>{body.innerHTML='<p class="empty">could not load this file</p>';});}
|
|
713
|
+
|
|
714
|
+
/* ── tags ── */
|
|
715
|
+
const tagSel=new Set(), tagTypes=new Set(), tagAreas=new Set();
|
|
716
|
+
/* Does this concept survive the tags view's type/area filters? Tags are counted
|
|
717
|
+
and listed over the surviving concepts only; a tag left with none disappears. */
|
|
718
|
+
function tagMatch(id){return (tagTypes.size===0||tagTypes.has(typeOf[id]||'Untyped'))&&(tagAreas.size===0||tagAreas.has(areaOf(id)));}
|
|
719
|
+
function renderTagChips(){
|
|
720
|
+
const after=()=>{renderTagChips();renderCloud();};
|
|
721
|
+
chipRow(document.getElementById('tag-ftypes'),typeItems(types),'data-t','on',t=>tagTypes.has(t),t=>{toggleSet(tagTypes,t);after();});
|
|
722
|
+
chipRow(document.getElementById('tag-fareas'),areaItems(graphAreas),'data-area','on',a=>tagAreas.has(a),a=>{toggleSet(tagAreas,a);after();});
|
|
723
|
+
const n=tagTypes.size+tagAreas.size;const b=document.getElementById('tag-filters-btn');
|
|
724
|
+
b.classList.toggle('on-filter',n>0);b.querySelector('.fbadge').textContent=n;}
|
|
725
|
+
function initTags(){inited.tags=true;
|
|
726
|
+
const tf=document.getElementById('tag-filters');
|
|
727
|
+
document.getElementById('tag-filters-btn').onclick=()=>tf.classList.toggle('open');
|
|
728
|
+
document.getElementById('tag-filters-close').onclick=()=>tf.classList.remove('open');
|
|
729
|
+
document.getElementById('tag-filters-reset').onclick=()=>{tagTypes.clear();tagAreas.clear();renderTagChips();renderCloud();};
|
|
730
|
+
renderTagChips();renderCloud();}
|
|
731
|
+
function renderCloud(){const s=(q.tags||'').toLowerCase();
|
|
732
|
+
// Uniform chip size, ordered by frequency (descending); the background tint scales
|
|
733
|
+
// with occurrence count (a heatmap), ties keep insertion order. Max is over every
|
|
734
|
+
// surviving tag so the scale stays stable while searching.
|
|
735
|
+
const counted=Object.entries(TAGS).map(([t,ids])=>[t,ids.filter(tagMatch)]).filter(([,ids])=>ids.length>0);
|
|
736
|
+
const entries=counted.filter(([t])=>!s||t.toLowerCase().includes(s)).sort((a,b)=>b[1].length-a[1].length);
|
|
737
|
+
const max=Math.max(1,...counted.map(([,ids])=>ids.length));
|
|
738
|
+
const total=Object.keys(TAGS).length;
|
|
739
|
+
document.getElementById('tag-cnt').textContent=(counted.length===total?total:counted.length+' of '+total)+' distinct tags';
|
|
740
|
+
const el=document.getElementById('tag-cloud');
|
|
741
|
+
el.innerHTML=entries.map(([t,ids])=>
|
|
742
|
+
`<button class="tcloud${tagSel.has(t)?' on':''}" data-tag="${esc(t)}" style="--heat:${(6+ids.length/max*36).toFixed(0)}%">${esc(t)}<b>${ids.length}</b></button>`).join('');
|
|
743
|
+
el.querySelectorAll('.tcloud').forEach(b=>b.onclick=()=>{const t=b.dataset.tag;
|
|
744
|
+
if(tagSel.has(t))tagSel.delete(t);else tagSel.add(t);b.classList.toggle('on');renderTagDetail();});
|
|
745
|
+
renderTagDetail();}
|
|
746
|
+
function renderTagDetail(){const el=document.getElementById('tag-detail');
|
|
747
|
+
if(!tagSel.size){el.innerHTML='<p class="empty">Select a tag to see the concepts that carry it.</p>';return;}
|
|
748
|
+
const ids=[...new Set([...tagSel].flatMap(t=>(TAGS[t]||[]).filter(tagMatch)))].sort();
|
|
749
|
+
el.innerHTML=`<div class="shead" style="margin-top:6px"><h2>${[...tagSel].map(esc).join(' · ')}</h2><span class="cnt">${ids.length} concepts</span></div>
|
|
750
|
+
<div class="tlist">${ids.map(id=>{const ty=typeOf[id]||'Untyped';const cc=color[ty]||'#64748b';
|
|
751
|
+
return `<button class="titem" data-id="${esc(id)}"><span class="dot" style="background:${cc}"></span><span class="tt">${esc((byId[id]||{}).title||id)}</span><span class="ty">${esc(ty)}</span></button>`;}).join('')}</div>`;
|
|
752
|
+
el.querySelectorAll('.titem').forEach(b=>b.onclick=()=>goToGraph(b.dataset.id));}
|
|
753
|
+
|
|
754
|
+
/* ── stats ── */
|
|
755
|
+
function countUp(el,to){if(reduce()||to<=8){el.textContent=to;return;}let start=null;
|
|
756
|
+
function step(ts){if(!start)start=ts;const p=Math.min(1,(ts-start)/650);el.textContent=Math.round(to*(1-Math.pow(1-p,3)));if(p<1)requestAnimationFrame(step);}
|
|
757
|
+
requestAnimationFrame(step);}
|
|
758
|
+
function initStats(){inited.stats=true;
|
|
759
|
+
const areas=new Set(NODES.map(n=>areaOf(n.id)));
|
|
760
|
+
const stats=[['Concepts',NODES.length],['Areas',areas.size],['Concept types',types.length],['Cross-links',EDGES.length],['Distinct tags',Object.keys(TAGS).length]];
|
|
761
|
+
const g=document.getElementById('stat-grid');
|
|
762
|
+
g.innerHTML=stats.map(s=>`<div class="stat"><div class="n" data-to="${s[1]}">0</div><div class="l">${s[0]}</div></div>`).join('');
|
|
763
|
+
g.querySelectorAll('.n').forEach(el=>countUp(el,+el.dataset.to));
|
|
764
|
+
const byType=Object.entries(TYPES).map(([t,ids])=>[t,ids.length]).sort((a,b)=>b[1]-a[1]);
|
|
765
|
+
const areaCnt={};NODES.forEach(n=>{const a=areaOf(n.id);areaCnt[a]=(areaCnt[a]||0)+1;});
|
|
766
|
+
const byArea=Object.entries(areaCnt).sort((a,b)=>b[1]-a[1]);
|
|
767
|
+
renderBars('bars-type',byType,t=>color[t],'type');renderBars('bars-area',byArea,()=>cvar('--accent'),'area');}
|
|
768
|
+
function renderBars(id,rows,colFn,kind){const max=Math.max(1,...rows.map(r=>r[1]));const box=document.getElementById(id);
|
|
769
|
+
box.innerHTML=rows.map(([l,v])=>
|
|
770
|
+
`<div class="bar clickable" data-val="${esc(l)}" role="button" tabindex="0" title="Show ${esc(l)} in the graph"><span class="bl">${esc(l)}</span><div class="bt"><div class="bf" style="background:${colFn(l)};transform:scaleX(${reduce()?1:0})" data-w="${v/max}"></div></div><span class="bv">${v}</span></div>`).join('');
|
|
771
|
+
box.querySelectorAll('.bar').forEach(b=>{const go=()=>{const val=b.dataset.val;kind==='type'?focusGraphType(val):focusGraphArea(val);};
|
|
772
|
+
b.onclick=go;b.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();go();}};});
|
|
773
|
+
requestAnimationFrame(()=>box.querySelectorAll('.bf').forEach(f=>f.style.transform=`scaleX(${f.dataset.w})`));}
|
|
774
|
+
|
|
775
|
+
/* ── theme / fullscreen ── */
|
|
776
|
+
const btnTheme=document.getElementById('btn-theme');
|
|
777
|
+
function setTheme(t){root.setAttribute('data-theme',t);try{localStorage.setItem('okf-theme',t);}catch(e){}
|
|
778
|
+
btnTheme.setAttribute('aria-label',t==='dark'?'Switch to light theme':'Switch to dark theme');applyCyTheme();rethemeMermaid();}
|
|
779
|
+
btnTheme.onclick=()=>setTheme(isDark()?'light':'dark');
|
|
780
|
+
const appEl=document.getElementById('app'), btnFull=document.getElementById('btn-full');
|
|
781
|
+
const fsOn=()=>document.fullscreenElement===appEl;
|
|
782
|
+
btnFull.onclick=()=>{if(fsOn())document.exitFullscreen();else if(appEl.requestFullscreen)appEl.requestFullscreen();};
|
|
783
|
+
document.addEventListener('fullscreenchange',()=>btnFull.setAttribute('aria-pressed',String(fsOn())));
|
|
784
|
+
|
|
785
|
+
/* ── keyboard ── */
|
|
786
|
+
const VIEW_KEYS={'1':'graph','2':'catalog','3':'files','4':'tags','5':'stats'};
|
|
787
|
+
addEventListener('keydown',e=>{if(e.target.matches('input,select,textarea'))return;
|
|
788
|
+
if(VIEW_KEYS[e.key]){setView(VIEW_KEYS[e.key]);}
|
|
789
|
+
else if(e.key==='/'){e.preventDefault();searchInput.focus();}
|
|
790
|
+
else if(e.key==='\\'&&view==='graph'){setSide(gbody.getAttribute('data-side')==='hidden'?'default':'hidden');}
|
|
791
|
+
else if(e.key==='0'&&view==='graph'){fitGraph();}
|
|
792
|
+
else if(e.key==='f'){btnFull.click();}});
|
|
793
|
+
|
|
794
|
+
/* ── deep links ── */
|
|
795
|
+
const Q=new URLSearchParams(location.search),QL=Q.get('layout'),QS=Q.get('select');
|
|
796
|
+
if(QL&&[...layoutSel.options].some(o=>o.value===QL)){layoutSel.value=QL;runLayout(QL);}
|
|
797
|
+
function fromHash(){try{const h=decodeURIComponent((location.hash||'').slice(1));if(h&&byId[h])select(h);}catch(e){}}
|
|
798
|
+
addEventListener('hashchange',fromHash);
|
|
799
|
+
if(QS&&byId[QS])select(QS);else fromHash();
|
|
800
|
+
|
|
801
|
+
/* ── a small hello for the curious ── */
|
|
802
|
+
try{console.log('%cokf%c — knowledge as code. '+NODES.length+' concepts, one graph.','color:#e21e1e;font-weight:700;font-size:15px','color:inherit');}catch(e){}
|
|
803
|
+
</script></body></html>
|