okf 1.3.0 → 1.5.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 +4 -4
- data/CHANGELOG.md +68 -0
- data/README.md +15 -15
- data/lib/okf/bundle/search.rb +155 -0
- data/lib/okf/bundle.rb +2 -2
- data/lib/okf/cli.rb +68 -4
- data/lib/okf/server/app.rb +29 -0
- data/lib/okf/server/graph/template.html.erb +385 -50
- data/lib/okf/server/graph.rb +2 -2
- data/lib/okf/skill/SKILL.md +17 -11
- data/lib/okf/skill/playbooks/consume.md +4 -2
- data/lib/okf/skill/playbooks/maintain.md +6 -4
- data/lib/okf/skill/playbooks/menu.md +10 -6
- data/lib/okf/skill/playbooks/search.md +42 -0
- data/lib/okf/skill/reference/authoring.md +2 -2
- data/lib/okf/skill/reference/cli.md +55 -17
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +1 -0
- metadata +3 -26
- data/.okf/capabilities/agent-skill.md +0 -46
- data/.okf/capabilities/graph-server.md +0 -63
- data/.okf/capabilities/index.md +0 -20
- data/.okf/capabilities/library-api.md +0 -72
- data/.okf/capabilities/linter.md +0 -49
- data/.okf/capabilities/read-views.md +0 -84
- data/.okf/capabilities/validator.md +0 -40
- data/.okf/cli.md +0 -52
- data/.okf/design/core-shell-split.md +0 -58
- data/.okf/design/index.md +0 -10
- data/.okf/design/ruby-floor.md +0 -45
- data/.okf/design/runtime-dependencies.md +0 -44
- data/.okf/design/server-trust-boundary.md +0 -40
- data/.okf/format/citations.md +0 -33
- data/.okf/format/cross-links.md +0 -52
- data/.okf/format/frontmatter.md +0 -38
- data/.okf/format/index.md +0 -9
- data/.okf/format/okf-format.md +0 -43
- data/.okf/index.md +0 -18
- data/.okf/log.md +0 -10
- data/.okf/model/bundle.md +0 -38
- data/.okf/model/concept.md +0 -44
- data/.okf/model/graph.md +0 -44
- data/.okf/model/index.md +0 -8
- data/.okf/overview.md +0 -67
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<meta property="og:site_name" content="OKF">
|
|
9
9
|
<meta property="og:title" content="<%= og_title %>">
|
|
10
10
|
<meta property="og:description" content="<%= og_desc %>">
|
|
11
|
-
<meta property="og:image" content="https://okfgem.com/og-demo.png">
|
|
11
|
+
<meta property="og:image" content="https://okfgem.com/og-demo-v2.png">
|
|
12
12
|
<meta property="og:image:type" content="image/png">
|
|
13
13
|
<meta property="og:image:width" content="1200">
|
|
14
14
|
<meta property="og:image:height" content="630">
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<meta name="twitter:card" content="summary_large_image">
|
|
17
17
|
<meta name="twitter:title" content="<%= og_title %>">
|
|
18
18
|
<meta name="twitter:description" content="<%= og_desc %>">
|
|
19
|
-
<meta name="twitter:image" content="https://okfgem.com/og-demo.png">
|
|
19
|
+
<meta name="twitter:image" content="https://okfgem.com/og-demo-v2.png">
|
|
20
20
|
<meta name="twitter:image:alt" content="An interactive Open Knowledge Format knowledge graph.">
|
|
21
21
|
<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>">
|
|
22
22
|
<script>/* Resolve theme before first paint so there is no flash. */
|
|
@@ -109,8 +109,13 @@
|
|
|
109
109
|
.btn.on-filter .fbadge{display:grid}
|
|
110
110
|
#graph-controls{display:none;align-items:center;gap:9px}
|
|
111
111
|
#app[data-view=graph] #graph-controls{display:flex}
|
|
112
|
+
#file-controls{display:none;align-items:center;gap:8px}
|
|
113
|
+
#app[data-view=files] #file-controls{display:flex}
|
|
114
|
+
/* below this width the topbar wraps in the files view so search + combos share two rows */
|
|
115
|
+
@media (max-width:700px){#app[data-view=files] #topbar{flex-wrap:wrap}
|
|
116
|
+
#app[data-view=files] #file-controls{flex:1 1 100%}#file-controls .combo{flex:1;width:auto;min-width:0}}
|
|
112
117
|
#search-wrap{display:flex;flex:1}
|
|
113
|
-
#app[data-view=stats] #search-wrap
|
|
118
|
+
#app[data-view=stats] #search-wrap{visibility:hidden}
|
|
114
119
|
#btn-theme .sun{display:none} :root[data-theme=dark] #btn-theme .sun{display:block} :root[data-theme=dark] #btn-theme .moon{display:none}
|
|
115
120
|
#btn-full .compress{display:none} #btn-full[aria-pressed=true] .compress{display:block} #btn-full[aria-pressed=true] .expand{display:none}
|
|
116
121
|
|
|
@@ -136,14 +141,20 @@
|
|
|
136
141
|
|
|
137
142
|
/* ── graph view ── */
|
|
138
143
|
.graph-body{display:grid;grid-template-columns:1fr var(--side-w);height:100%;min-height:0;
|
|
139
|
-
--side-w:380px;transition:--side-w .22s cubic-bezier(.22,1,.36,1)}
|
|
144
|
+
--side-w:var(--side-w-user,380px);transition:--side-w .22s cubic-bezier(.22,1,.36,1)}
|
|
140
145
|
.graph-body[data-side=hidden]{--side-w:0px}
|
|
141
146
|
.graph-body[data-side=wide]{--side-w:70vw}
|
|
147
|
+
.graph-body.side-drag{transition:none}
|
|
142
148
|
#stage{position:relative;min-width:0;overflow:hidden;background:var(--canvas)}
|
|
143
149
|
#cy{position:absolute;inset:0}
|
|
144
150
|
.ghint{position:absolute;right:16px;bottom:14px;z-index:3;font-size:11px;color:var(--faint);pointer-events:none}
|
|
145
|
-
#side{min-width:0;background:var(--panel);border-left:1px solid var(--line);display:flex;flex-direction:column;overflow:hidden}
|
|
151
|
+
#side{position:relative;min-width:0;background:var(--panel);border-left:1px solid var(--line);display:flex;flex-direction:column;overflow:hidden}
|
|
146
152
|
.graph-body[data-side=hidden] #side{border-left:0}
|
|
153
|
+
/* drag handle on the panel's left edge (double-click resets the width) */
|
|
154
|
+
#side-resizer{position:absolute;left:0;top:0;bottom:0;width:7px;cursor:col-resize;z-index:4;touch-action:none}
|
|
155
|
+
#side-resizer::after{content:"";position:absolute;left:0;top:0;bottom:0;width:2px;background:var(--accent);opacity:0;transition:opacity .15s}
|
|
156
|
+
#side-resizer:hover::after,.graph-body.side-drag #side-resizer::after{opacity:.7}
|
|
157
|
+
.graph-body[data-side=hidden] #side-resizer{display:none}
|
|
147
158
|
#side-head{display:flex;align-items:center;gap:6px;padding:9px 12px;border-bottom:1px solid var(--line);flex:none}
|
|
148
159
|
#side-head .lbl{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--faint);margin-right:auto}
|
|
149
160
|
#side-head .btn{width:30px;height:30px}
|
|
@@ -161,7 +172,6 @@
|
|
|
161
172
|
.slideover .fgroup h4{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--faint);margin:0 0 9px}
|
|
162
173
|
.slideover .fchips{display:flex;flex-wrap:wrap;gap:6px}
|
|
163
174
|
.cat-types{align-items:center;gap:10px} .cat-types #cat-types{display:flex;flex-wrap:wrap;gap:7px}
|
|
164
|
-
.ftree-head .combo{width:100%} .ftree-head .combo+.combo{margin-top:8px}
|
|
165
175
|
.link{background:none;border:0;color:var(--accent-ink);font:inherit;font-size:12.5px;cursor:pointer;padding:0;font-weight:500}
|
|
166
176
|
.link:hover{text-decoration:underline}
|
|
167
177
|
|
|
@@ -183,6 +193,7 @@
|
|
|
183
193
|
.body>*:first-child{margin-top:0}
|
|
184
194
|
.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}
|
|
185
195
|
.body a{color:var(--accent-ink)}
|
|
196
|
+
#side-body a.dead,.fp-body a.dead{opacity:.55;cursor:not-allowed;text-decoration:line-through}
|
|
186
197
|
.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}
|
|
187
198
|
.body pre{background:var(--line-2);border:1px solid var(--line);padding:12px;border-radius:10px;overflow:auto}
|
|
188
199
|
.body pre code{background:none;border:0;padding:0}
|
|
@@ -190,9 +201,22 @@
|
|
|
190
201
|
.body td,.body th{border:1px solid var(--line);padding:5px 9px}
|
|
191
202
|
.body img{max-width:100%}
|
|
192
203
|
.body blockquote{margin:.6em 0;padding:.2em 0 .2em 14px;border-left:3px solid var(--line);color:var(--muted)}
|
|
193
|
-
.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}
|
|
204
|
+
.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;cursor:zoom-in}
|
|
205
|
+
.mermaid:hover{border-color:var(--accent)}
|
|
206
|
+
.mermaid:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
194
207
|
.mermaid svg{max-width:100%;height:auto}
|
|
195
208
|
|
|
209
|
+
/* ── fullscreen diagram viewer (Panzoom, lazy like Mermaid itself) ── */
|
|
210
|
+
#dgv{position:fixed;inset:0;z-index:80;background:var(--canvas)}
|
|
211
|
+
#dgv[hidden]{display:none}
|
|
212
|
+
#dgv-stage{position:absolute;inset:0;overflow:hidden;touch-action:none;cursor:grab;display:flex;align-items:center;justify-content:center}
|
|
213
|
+
#dgv-stage:active{cursor:grabbing}
|
|
214
|
+
#dgv-pan{flex:none}
|
|
215
|
+
#dgv-pan svg{display:block;max-width:none}
|
|
216
|
+
.dgv-bar{position:absolute;top:14px;right:14px;display:flex;gap:8px;z-index:2}
|
|
217
|
+
.dgv-bar .btn{background:var(--panel);box-shadow:var(--shadow)}
|
|
218
|
+
.dgv-hint{position:absolute;bottom:14px;left:0;right:0;text-align:center;font-size:12px;color:var(--faint);pointer-events:none}
|
|
219
|
+
|
|
196
220
|
/* ── catalog ── */
|
|
197
221
|
.pad{padding:22px 24px;overflow:auto;flex:1}
|
|
198
222
|
.cat-types{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:18px}
|
|
@@ -211,11 +235,29 @@
|
|
|
211
235
|
.none{grid-column:1/-1;text-align:center;color:var(--faint);padding:48px;border:1px dashed var(--line);border-radius:12px}
|
|
212
236
|
|
|
213
237
|
/* ── files ── */
|
|
214
|
-
.files-grid{display:grid;grid-template-columns:320px 1fr;height:100%;min-height:0}
|
|
215
|
-
.ftree{border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0;background:var(--panel)}
|
|
216
|
-
|
|
238
|
+
.files-grid{display:grid;grid-template-columns:var(--ftree-w,320px) 1fr;height:100%;min-height:0}
|
|
239
|
+
.ftree{position:relative;border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0;background:var(--panel)}
|
|
240
|
+
/* drag handle on the file list's right edge (double-click resets the width) */
|
|
241
|
+
#ftree-resizer{position:absolute;right:-4px;top:0;bottom:0;width:7px;cursor:col-resize;z-index:4;touch-action:none}
|
|
242
|
+
#ftree-resizer::after{content:"";position:absolute;right:3px;top:0;bottom:0;width:2px;background:var(--accent);opacity:0;transition:opacity .15s}
|
|
243
|
+
#ftree-resizer:hover::after,#ftree-resizer.dragging::after{opacity:.7}
|
|
217
244
|
.ftree-list{overflow:auto;flex:1;padding:8px}
|
|
218
|
-
.ffolder{
|
|
245
|
+
.ffolder{display:flex;align-items:center;gap:6px;width:100%;text-align:left;border:0;background:none;cursor:pointer;font-family:inherit;
|
|
246
|
+
font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--faint);padding:12px 8px 5px;font-weight:600}
|
|
247
|
+
.ffolder:hover{color:var(--muted)}
|
|
248
|
+
.ffolder .chev{width:11px;height:11px;flex:none;stroke:currentColor;fill:none;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;transition:transform .15s}
|
|
249
|
+
.ffolder.closed .chev{transform:rotate(-90deg)}
|
|
250
|
+
.file.res .fn{color:var(--muted)}
|
|
251
|
+
.file .fn .ricon{width:12px;height:12px;flex:none;stroke:var(--faint);fill:none;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
|
|
252
|
+
.ftabs{display:flex;gap:2px;padding:8px 10px 0;border-bottom:1px solid var(--line);flex:none}
|
|
253
|
+
.ftab{flex:1;border:0;background:none;font:inherit;font-size:12.5px;font-weight:600;color:var(--muted);
|
|
254
|
+
padding:8px 10px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}
|
|
255
|
+
.ftab:hover{color:var(--ink)}
|
|
256
|
+
.ftab.active{color:var(--ink);border-bottom-color:var(--accent)}
|
|
257
|
+
/* the type/tag combos filter concepts; they mean nothing on the Indexes tab */
|
|
258
|
+
#app[data-ftab=indexes] #file-controls .combo{display:none}
|
|
259
|
+
.file .fn .badge-res{margin-left:auto;flex:none;font-size:9.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;
|
|
260
|
+
color:var(--faint);background:var(--line-2);border:1px solid var(--line);border-radius:999px;padding:1px 7px}
|
|
219
261
|
.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)}
|
|
220
262
|
.file:hover{background:var(--panel-2)} .file.sel{background:var(--accent-soft)}
|
|
221
263
|
.file .fn{font-size:13px;font-weight:500;display:flex;align-items:center;gap:7px}
|
|
@@ -224,7 +266,7 @@
|
|
|
224
266
|
.fpreview{min-width:0;overflow:auto;display:flex;flex-direction:column}
|
|
225
267
|
.fp-head{display:flex;align-items:center;gap:10px;padding:14px 22px;border-bottom:1px solid var(--line);flex:none}
|
|
226
268
|
.fp-head .title{margin:0;font-size:16px}
|
|
227
|
-
.fp-body{padding:20px
|
|
269
|
+
.fp-body{padding:20px 28px;overflow:auto;flex:1}
|
|
228
270
|
.fp-body .body{border-top:0;padding-top:0;margin-top:0}
|
|
229
271
|
|
|
230
272
|
/* ── tags ── */
|
|
@@ -269,6 +311,13 @@
|
|
|
269
311
|
.opt:hover,.opt.active{background:var(--line-2)} .opt.sel{color:var(--accent-ink);font-weight:600}
|
|
270
312
|
@keyframes pop{from{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:none}}
|
|
271
313
|
|
|
314
|
+
/* ── the synthesized-map listing (inspector + files preview) ── */
|
|
315
|
+
.ix-listing{margin:10px 0 0;padding:0;list-style:none}
|
|
316
|
+
.ix-listing li{padding:3px 0;font-size:13.5px}
|
|
317
|
+
.ix-listing a{color:var(--accent-ink);text-decoration:none;cursor:pointer}
|
|
318
|
+
.ix-listing a:hover{text-decoration:underline}
|
|
319
|
+
.ix-listing .d{color:var(--muted)}
|
|
320
|
+
|
|
272
321
|
@media (max-width:720px){#rail{width:64px}.rail-item{width:52px}.card,.grid{}}
|
|
273
322
|
@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
|
|
274
323
|
</style></head>
|
|
@@ -279,8 +328,9 @@
|
|
|
279
328
|
<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>
|
|
280
329
|
</span>
|
|
281
330
|
<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>
|
|
282
|
-
<button class="rail-item" data-view="
|
|
331
|
+
<button class="rail-item" data-view="index"><span class="ri-ico"><svg viewBox="0 0 24 24"><path d="M4 5h16M4 9.6h10.5M4 14.2h16M4 18.8h7.5"/></svg></span><span class="ri-lbl">Index</span></button>
|
|
283
332
|
<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>
|
|
333
|
+
<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>
|
|
284
334
|
<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>
|
|
285
335
|
<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>
|
|
286
336
|
<div class="rail-sp"></div>
|
|
@@ -299,6 +349,10 @@
|
|
|
299
349
|
<span class="ident"><%= escaped_name %><span class="muted"><%= @graph.nodes.length %> concepts · <%= @graph.edges.length %> links</span><%= source_link %></span>
|
|
300
350
|
<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>
|
|
301
351
|
<input id="search" class="field" placeholder="search concepts…" autocomplete="off" aria-label="Search"></label></div>
|
|
352
|
+
<div id="file-controls">
|
|
353
|
+
<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>
|
|
354
|
+
<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>
|
|
355
|
+
</div>
|
|
302
356
|
<div id="graph-controls">
|
|
303
357
|
<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>
|
|
304
358
|
<span class="selwrap"><select id="layout" class="field" aria-label="Layout">
|
|
@@ -314,6 +368,8 @@
|
|
|
314
368
|
<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>
|
|
315
369
|
<button class="btn" id="btn-cluster" type="button" aria-label="Cluster by area" aria-pressed="false" title="Cluster nodes by area">
|
|
316
370
|
<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>
|
|
371
|
+
<button class="btn" id="btn-tree" type="button" aria-label="File tree" aria-pressed="false" title="File tree: folders and files, parent-to-child edges only">
|
|
372
|
+
<svg viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="5" rx="1.2"/><rect x="3" y="16" width="6" height="5" rx="1.2"/><rect x="15" y="16" width="6" height="5" rx="1.2"/><path d="M12 8v4M12 12H6v4M12 12h6v4"/></svg></button>
|
|
317
373
|
<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>
|
|
318
374
|
</div>
|
|
319
375
|
</header>
|
|
@@ -338,6 +394,7 @@
|
|
|
338
394
|
</aside>
|
|
339
395
|
</div>
|
|
340
396
|
<aside id="side">
|
|
397
|
+
<div id="side-resizer" title="Drag to resize · double-click to reset" aria-hidden="true"></div>
|
|
341
398
|
<div id="side-head"><span class="lbl">Inspector</span>
|
|
342
399
|
<button class="btn" id="side-widen" type="button" aria-label="Widen panel" aria-pressed="false" title="Widen / restore">
|
|
343
400
|
<svg class="wide" viewBox="0 0 24 24"><path d="M13 6l-6 6 6 6M19 6l-6 6 6 6"/></svg>
|
|
@@ -375,11 +432,12 @@
|
|
|
375
432
|
<section class="view" id="view-files">
|
|
376
433
|
<div class="files-grid">
|
|
377
434
|
<div class="ftree">
|
|
378
|
-
<div class="
|
|
379
|
-
<
|
|
380
|
-
<
|
|
381
|
-
|
|
435
|
+
<div class="ftabs" role="tablist" aria-label="File kinds">
|
|
436
|
+
<button class="ftab active" id="ftab-files" type="button" role="tab" aria-selected="true">Files</button>
|
|
437
|
+
<button class="ftab" id="ftab-indexes" type="button" role="tab" aria-selected="false">Indexes</button>
|
|
438
|
+
</div>
|
|
382
439
|
<div class="ftree-list" id="ftree-list"><p class="empty" style="padding:14px">Loading files…</p></div>
|
|
440
|
+
<div id="ftree-resizer" title="Drag to resize · double-click to reset" aria-hidden="true"></div>
|
|
383
441
|
</div>
|
|
384
442
|
<div class="fpreview">
|
|
385
443
|
<div class="fp-head" id="fp-head" hidden><span class="type" id="fp-type"></span><h2 class="title" id="fp-title"></h2>
|
|
@@ -422,10 +480,22 @@
|
|
|
422
480
|
</div>
|
|
423
481
|
</div>
|
|
424
482
|
|
|
483
|
+
<!-- fullscreen diagram viewer — click (or tap) a rendered Mermaid block to open it -->
|
|
484
|
+
<div id="dgv" hidden role="dialog" aria-modal="true" aria-label="Diagram viewer">
|
|
485
|
+
<div id="dgv-stage"><div id="dgv-pan"></div></div>
|
|
486
|
+
<div class="dgv-bar">
|
|
487
|
+
<button class="btn" id="dgv-out" type="button" aria-label="Zoom out"><svg viewBox="0 0 24 24"><path d="M5 12h14"/></svg></button>
|
|
488
|
+
<button class="btn" id="dgv-reset" type="button" aria-label="Reset view"><svg viewBox="0 0 24 24"><path d="M9 3H4v5M15 3h5v5M9 21H4v-5M15 21h5v-5"/></svg></button>
|
|
489
|
+
<button class="btn" id="dgv-in" type="button" aria-label="Zoom in"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></button>
|
|
490
|
+
<button class="btn" id="dgv-close" type="button" aria-label="Close diagram viewer"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
|
|
491
|
+
</div>
|
|
492
|
+
<p class="dgv-hint">drag to pan · scroll or pinch to zoom · double-click resets · Esc closes</p>
|
|
493
|
+
</div>
|
|
494
|
+
|
|
425
495
|
<script>
|
|
426
496
|
const NODES=<%= nodes_json %>, EDGES=<%= edges_json %>, TYPES=<%= types_json %>, TAGS=<%= tags_json %>;
|
|
427
497
|
const NODE_ENDPOINT=<%= @node_endpoint.to_json %>, META_ENDPOINT=<%= @meta_endpoint.to_json %>;
|
|
428
|
-
const CATALOG_ENDPOINT="catalog";
|
|
498
|
+
const CATALOG_ENDPOINT="catalog",INDEX_ENDPOINT="index",LOG_ENDPOINT="log";
|
|
429
499
|
const MIN=<%= OKF::Server::Graph::MIN_SIZE %>, MAX=<%= OKF::Server::Graph::MAX_SIZE %>;
|
|
430
500
|
const PALETTE=["#6E56CF","#D97757","#22C55E","#3B82F6","#EAB308","#EC4899","#14B8A6","#F97316","#A855F7","#0EA5E9","#84CC16","#EF4444","#64748B"];
|
|
431
501
|
const root=document.documentElement;
|
|
@@ -454,11 +524,19 @@ function styleSheet(){const ink=cvar('--ink'),halo=cvar('--canvas'),edge=cvar('-
|
|
|
454
524
|
{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}},
|
|
455
525
|
{selector:'.dim',style:{'opacity':.1}},
|
|
456
526
|
{selector:'.hl',style:{'border-width':3,'border-color':cvar('--accent')}},
|
|
527
|
+
{selector:'node.dir',style:{'shape':'round-rectangle','background-color':cvar('--faint'),'background-opacity':.85,
|
|
528
|
+
'font-size':9.5,'font-weight':600,'color':cvar('--muted')}},
|
|
529
|
+
{selector:'edge.tree',style:{'width':1.1,'opacity':.7,'target-arrow-shape':'none'}},
|
|
530
|
+
{selector:'edge.linkhid',style:{'display':'none'}},
|
|
457
531
|
{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')}}
|
|
458
532
|
];}
|
|
533
|
+
/* Small screens start with the inspector hidden — it opens on the first node
|
|
534
|
+
tap (openPanel). Done before Cytoscape reads the container, so the graph
|
|
535
|
+
lays out at full width; larger screens keep the open panel as the hint. */
|
|
536
|
+
if(matchMedia('(max-width: 900px)').matches)document.querySelector('.graph-body').setAttribute('data-side','hidden');
|
|
459
537
|
const cy=cytoscape({container:document.getElementById('cy'),minZoom:.2,maxZoom:1.6,wheelSensitivity:.2,
|
|
460
538
|
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}))],
|
|
461
|
-
style:styleSheet(),layout:{name:'<%= @layout %>',animate:false,nodeRepulsion:9000,idealEdgeLength:90,padding:40}});
|
|
539
|
+
style:styleSheet(),layout:{name:'<%= @layout %>',animate:false,nodeRepulsion:9000,idealEdgeLength:90,padding:40,nodeOverlap:24,avoidOverlap:true,spacingFactor:1.2}});
|
|
462
540
|
function applyCyTheme(){cy.style(styleSheet());}
|
|
463
541
|
/* Debounced so the panel width transition (--side-w, .22s) fires cy.resize()
|
|
464
542
|
once after it settles instead of on every frame — avoids canvas flicker. */
|
|
@@ -475,7 +553,9 @@ function loadMermaid(){return _mermaid||(_mermaid=new Promise((res,rej)=>{const
|
|
|
475
553
|
let mermaidSeq=0;
|
|
476
554
|
function processMermaid(el){const blocks=[...el.querySelectorAll('code.language-mermaid')];if(!blocks.length)return;
|
|
477
555
|
loadMermaid().then(m=>{const nodes=[];blocks.forEach(b=>{const pre=b.closest('pre');const d=document.createElement('div');
|
|
478
|
-
d.className='mermaid';d.dataset.src=b.textContent;d.id='mmd-'+(++mermaidSeq);d.textContent=b.textContent;
|
|
556
|
+
d.className='mermaid';d.dataset.src=b.textContent;d.id='mmd-'+(++mermaidSeq);d.textContent=b.textContent;
|
|
557
|
+
d.tabIndex=0;d.setAttribute('role','button');d.setAttribute('aria-label','Expand diagram');d.title='Expand — pan & zoom';
|
|
558
|
+
pre.replaceWith(d);nodes.push(d);});
|
|
479
559
|
m.run({nodes}).catch(()=>{});}).catch(()=>{});}
|
|
480
560
|
function rethemeMermaid(){if(!window.mermaid)return;const nodes=[...document.querySelectorAll('.mermaid')];if(!nodes.length)return;
|
|
481
561
|
window.mermaid.initialize({startOnLoad:false,securityLevel:'strict',theme:isDark()?'dark':'default'});
|
|
@@ -483,6 +563,39 @@ function rethemeMermaid(){if(!window.mermaid)return;const nodes=[...document.que
|
|
|
483
563
|
window.mermaid.run({nodes}).catch(()=>{});}
|
|
484
564
|
function renderMarkdown(el,text){el.innerHTML=text?DOMPurify.sanitize(marked.parse(text)):'<span class="empty">empty body</span>';processMermaid(el);}
|
|
485
565
|
|
|
566
|
+
/* ── fullscreen diagram viewer — Panzoom (lazy, like Mermaid): click a rendered
|
|
567
|
+
diagram to inspect it full screen; drag pans, wheel or pinch zooms. ── */
|
|
568
|
+
let _panzoom=null;
|
|
569
|
+
function loadPanzoom(){return _panzoom||(_panzoom=loadScript('https://cdn.jsdelivr.net/npm/@panzoom/panzoom@4/dist/panzoom.min.js').then(()=>window.Panzoom));}
|
|
570
|
+
const dgv=document.getElementById('dgv'),dgvStage=document.getElementById('dgv-stage'),dgvPan=document.getElementById('dgv-pan');
|
|
571
|
+
let dgvPz=null,dgvBack=null,dgvSeq=0;
|
|
572
|
+
function openDiagram(m){const src=m.dataset.src;if(!src||!m.querySelector('svg'))return;
|
|
573
|
+
/* re-render from source rather than cloning the inline svg — mermaid scopes
|
|
574
|
+
the styles it embeds to the svg's id, so a clone would lose its colors */
|
|
575
|
+
Promise.all([ loadPanzoom(), loadMermaid() ]).then(all=>
|
|
576
|
+
all[1].render('dgv-mmd-'+(++dgvSeq),src).then(out=>{
|
|
577
|
+
dgvPan.innerHTML=out.svg;
|
|
578
|
+
const c=dgvPan.querySelector('svg'),vb=c.viewBox&&c.viewBox.baseVal;
|
|
579
|
+
const w=(vb&&vb.width)||800,h=(vb&&vb.height)||500;
|
|
580
|
+
c.style.maxWidth='none';c.setAttribute('width',w);c.setAttribute('height',h);
|
|
581
|
+
dgvBack=m;dgv.hidden=false;
|
|
582
|
+
const fit=Math.min((innerWidth-48)/w,(innerHeight-96)/h,2);
|
|
583
|
+
dgvPz=Panzoom(dgvPan,{maxScale:10,minScale:.1,startScale:fit,canvas:true,step:.2});
|
|
584
|
+
document.getElementById('dgv-close').focus();
|
|
585
|
+
})).catch(()=>{});}
|
|
586
|
+
function closeDiagram(){if(dgv.hidden)return;
|
|
587
|
+
if(dgvPz){dgvPz.destroy();dgvPz=null;}
|
|
588
|
+
dgv.hidden=true;dgvPan.innerHTML='';
|
|
589
|
+
if(dgvBack){try{dgvBack.focus();}catch(e){}dgvBack=null;}}
|
|
590
|
+
dgvStage.addEventListener('wheel',e=>{if(dgvPz)dgvPz.zoomWithWheel(e);},{passive:false});
|
|
591
|
+
dgvStage.addEventListener('dblclick',()=>{if(dgvPz)dgvPz.reset();});
|
|
592
|
+
document.getElementById('dgv-in').onclick=()=>{if(dgvPz)dgvPz.zoomIn();};
|
|
593
|
+
document.getElementById('dgv-out').onclick=()=>{if(dgvPz)dgvPz.zoomOut();};
|
|
594
|
+
document.getElementById('dgv-reset').onclick=()=>{if(dgvPz)dgvPz.reset();};
|
|
595
|
+
document.getElementById('dgv-close').onclick=closeDiagram;
|
|
596
|
+
document.addEventListener('click',e=>{const m=e.target.closest&&e.target.closest('.mermaid');if(m&&!dgv.contains(e.target))openDiagram(m);});
|
|
597
|
+
document.addEventListener('keydown',e=>{if((e.key==='Enter'||e.key===' ')&&e.target.classList&&e.target.classList.contains('mermaid')){e.preventDefault();openDiagram(e.target);}});
|
|
598
|
+
|
|
486
599
|
/* ── inspector ── */
|
|
487
600
|
const gbody=document.querySelector('.graph-body'), sideBody=document.getElementById('side-body');
|
|
488
601
|
const btnPanel=document.getElementById('btn-panel'), sideWiden=document.getElementById('side-widen');
|
|
@@ -490,13 +603,106 @@ function setSide(s){gbody.setAttribute('data-side',s);btnPanel.setAttribute('ari
|
|
|
490
603
|
sideWiden.setAttribute('aria-pressed',String(s==='wide'));
|
|
491
604
|
sideWiden.querySelector('.wide').style.display=s==='wide'?'none':'block';
|
|
492
605
|
sideWiden.querySelector('.narrow').style.display=s==='wide'?'block':'none';}
|
|
493
|
-
function openPanel(){if(gbody.getAttribute('data-side')
|
|
606
|
+
function openPanel(){if(gbody.getAttribute('data-side')!=='hidden')return false;setSide('default');return true;}
|
|
494
607
|
btnPanel.onclick=()=>setSide(gbody.getAttribute('data-side')==='hidden'?'default':'hidden');
|
|
495
608
|
document.getElementById('side-close').onclick=()=>setSide('hidden');
|
|
496
609
|
sideWiden.onclick=()=>setSide(gbody.getAttribute('data-side')==='wide'?'default':'wide');
|
|
610
|
+
/* sync the header buttons with the responsive initial state set before cy init */
|
|
611
|
+
if(gbody.getAttribute('data-side')==='hidden')setSide('hidden');
|
|
612
|
+
/* ── drag-resize splitters: inspector + files tree (persisted; dblclick resets) ── */
|
|
613
|
+
function splitter(handle,host,cssVar,key,widthAt,opts){opts=opts||{};
|
|
614
|
+
/* restore clamped to the current viewport — a width dragged on a desktop
|
|
615
|
+
must not swallow a phone screen */
|
|
616
|
+
try{const w=parseInt(localStorage.getItem(key),10);
|
|
617
|
+
if(w)host.style.setProperty(cssVar,Math.min(w,Math.round(innerWidth*.7))+'px');}catch(e){}
|
|
618
|
+
handle.addEventListener('pointerdown',e=>{e.preventDefault();
|
|
619
|
+
if(opts.onStart)opts.onStart();
|
|
620
|
+
try{handle.setPointerCapture(e.pointerId);}catch(err){}
|
|
621
|
+
handle.classList.add('dragging');host.classList.add('side-drag');
|
|
622
|
+
let lastW=0,raf=false;
|
|
623
|
+
/* Track the drag on window so it keeps working even where pointer capture
|
|
624
|
+
is unavailable — the pointer leaves the 7px handle on the first frame. */
|
|
625
|
+
const move=ev=>{lastW=Math.round(widthAt(ev));host.style.setProperty(cssVar,lastW+'px');
|
|
626
|
+
if(opts.onMove&&!raf){raf=true;requestAnimationFrame(()=>{raf=false;opts.onMove();});}};
|
|
627
|
+
const up=()=>{removeEventListener('pointermove',move);removeEventListener('pointerup',up);removeEventListener('pointercancel',up);
|
|
628
|
+
handle.classList.remove('dragging');host.classList.remove('side-drag');
|
|
629
|
+
if(opts.onEnd)opts.onEnd();
|
|
630
|
+
if(lastW){try{localStorage.setItem(key,String(lastW));}catch(e){}}};
|
|
631
|
+
addEventListener('pointermove',move);addEventListener('pointerup',up);addEventListener('pointercancel',up);});
|
|
632
|
+
handle.addEventListener('dblclick',()=>{host.style.removeProperty(cssVar);
|
|
633
|
+
try{localStorage.removeItem(key);}catch(e){}
|
|
634
|
+
if(opts.onEnd)opts.onEnd();});}
|
|
635
|
+
const clampW=(v,lo,hi)=>Math.min(Math.max(v,lo),hi);
|
|
636
|
+
splitter(document.getElementById('side-resizer'),gbody,'--side-w-user','okf-side-w',
|
|
637
|
+
ev=>{const r=gbody.getBoundingClientRect();return clampW(r.right-ev.clientX,280,r.width*.8);},
|
|
638
|
+
{onStart:()=>{if(gbody.getAttribute('data-side')==='wide')setSide('default');},
|
|
639
|
+
onMove:()=>cy.resize(),onEnd:()=>cy.resize()});
|
|
640
|
+
const fgrid=document.querySelector('.files-grid');
|
|
641
|
+
splitter(document.getElementById('ftree-resizer'),fgrid,'--ftree-w','okf-ftree-w',
|
|
642
|
+
ev=>{const r=fgrid.getBoundingClientRect();return clampW(ev.clientX-r.left,220,r.width*.55);});
|
|
497
643
|
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>`;}
|
|
498
|
-
|
|
499
|
-
|
|
644
|
+
/* Markdown links inside the inspector are GitHub-style relative file paths;
|
|
645
|
+
there is no route for them here, so a plain click 404s the whole page.
|
|
646
|
+
Resolve them against the open concept and navigate in-app instead; external
|
|
647
|
+
links open in a new tab; anything unresolvable is disabled, never a 404. */
|
|
648
|
+
let shownId=null;
|
|
649
|
+
/* Every directory that holds a concept — the sync existence check for links
|
|
650
|
+
that point at a directory or its index rather than at a concept file. */
|
|
651
|
+
const DIRSET=new Set();
|
|
652
|
+
NODES.forEach(n=>{const parts=n.id.split('/');for(let i=1;i<parts.length;i++)DIRSET.add(parts.slice(0,i).join('/'));});
|
|
653
|
+
/* A markdown href resolves to a concept ({id}) or to the authored layer
|
|
654
|
+
({res:{kind:'index',dir}} / {res:{kind:'log',path}}) — or to null (dead).
|
|
655
|
+
Directory links (`model/`, `/`) count as that directory's index. */
|
|
656
|
+
function resolveTarget(href,baseId){let p=href.split('#')[0].split('?')[0];if(!p)return null;
|
|
657
|
+
try{p=decodeURIComponent(p);}catch(e){}
|
|
658
|
+
const out=p.charAt(0)==='/'?[]:(baseId?baseId.split('/').slice(0,-1):[]);
|
|
659
|
+
for(const s of p.replace(/^\//,'').split('/')){if(!s||s==='.')continue;if(s==='..'){out.pop();continue;}out.push(s);}
|
|
660
|
+
const path=out.join('/');
|
|
661
|
+
const id=path.replace(/\.md$/i,'');
|
|
662
|
+
if(byId[id])return {id:id};
|
|
663
|
+
if(/(^|\/)index\.md$/i.test(path))return {res:{kind:'index',dir:dirOfPath(path)}};
|
|
664
|
+
if(/(^|\/)log\.md$/i.test(path))return {res:{kind:'log',path:path}};
|
|
665
|
+
if(path===''||DIRSET.has(path))return {res:{kind:'index',dir:path===''?'.':path}};
|
|
666
|
+
return null;}
|
|
667
|
+
function interceptMdLinks(container,baseIdOf,go,goRes){container.addEventListener('click',e=>{
|
|
668
|
+
const a=e.target.closest('a');
|
|
669
|
+
if(!a||!container.contains(a)||a.hasAttribute('data-go'))return;
|
|
670
|
+
const href=a.getAttribute('href')||'';
|
|
671
|
+
if(/^[a-z][a-z0-9+.-]*:/i.test(href)||href.indexOf('//')===0){e.preventDefault();window.open(a.href,'_blank','noopener');return;}
|
|
672
|
+
e.preventDefault();if(href.charAt(0)==='#')return;
|
|
673
|
+
const t=resolveTarget(href,baseIdOf());
|
|
674
|
+
if(t&&t.id)go(t.id);
|
|
675
|
+
else if(t&&t.res&&goRes)goRes(t.res);
|
|
676
|
+
else{a.classList.add('dead');a.title='not a file in this bundle';}});}
|
|
677
|
+
const ixPathOf=dir=>dir==='.'?'index.md':dir+'/index.md';
|
|
678
|
+
interceptMdLinks(sideBody,()=>shownId,select,r=>r.kind==='index'?showDir(r.dir):showLog(r.path));
|
|
679
|
+
interceptMdLinks(document.getElementById('fp-body'),()=>fileSel,id=>openFile(id),
|
|
680
|
+
r=>openReserved(r.kind,r.kind==='index'?ixPathOf(r.dir):r.path));
|
|
681
|
+
/* Clicking a folder node (tree mode) or an area box (cluster mode) opens that
|
|
682
|
+
directory's §6 index entry in the inspector: the authored map when one
|
|
683
|
+
exists, the synthesized listing when not. */
|
|
684
|
+
function showDir(dir){getIndex().then(dirs=>{const d=dirs.find(x=>x.dir===dir);if(!d)return;
|
|
685
|
+
const opened=openPanel();sideBody.scrollTop=0;
|
|
686
|
+
shownId=d.dir==='.'?'index':d.dir+'/index'; /* pseudo-id: relative links in the map resolve against its directory */
|
|
687
|
+
const label=d.dir==='.'?'(root)':d.dir+'/';
|
|
688
|
+
const types=Object.entries(d.types||{}).map(([t,n])=>esc(t||'Untyped')+' '+n).join(' · ');
|
|
689
|
+
sideBody.innerHTML=`<span class="type">${d.present?'index.md':'no index.md · synthesized'}</span>
|
|
690
|
+
<h2 class="title">${esc(label)}</h2>
|
|
691
|
+
<div class="desc">${d.count} concept${d.count===1?'':'s'}${types?' · '+types:''}</div>
|
|
692
|
+
<div class="body" id="dir-body"></div>`;
|
|
693
|
+
if(d.present)renderMarkdown(document.getElementById('dir-body'),d.body||'');
|
|
694
|
+
else document.getElementById('dir-body').innerHTML=listingHtml(d.listing);
|
|
695
|
+
sideBody.querySelectorAll('[data-go]').forEach(a=>a.onclick=()=>select(a.getAttribute('data-go')));
|
|
696
|
+
if(opened)requestAnimationFrame(()=>cy.resize());});}
|
|
697
|
+
/* A log.md link opens the history in the inspector, fetched fresh. */
|
|
698
|
+
function showLog(path){LOGS=null;getLogs().then(logs=>{const l=logs.find(x=>x.path===path);if(!l)return;
|
|
699
|
+
const opened=openPanel();sideBody.scrollTop=0;
|
|
700
|
+
shownId=path.replace(/\.md$/,'');
|
|
701
|
+
sideBody.innerHTML=`<span class="type">update log · §7</span><h2 class="title">${esc(path)}</h2><div class="body" id="dir-body"></div>`;
|
|
702
|
+
renderMarkdown(document.getElementById('dir-body'),l.content||'');
|
|
703
|
+
if(opened)requestAnimationFrame(()=>cy.resize());});}
|
|
704
|
+
function show(id){const n=byId[id];if(!n)return false;const ty=typeOf[id]||'Untyped';const c=color[ty]||'#64748b';
|
|
705
|
+
shownId=id;const opened=openPanel();sideBody.scrollTop=0;
|
|
500
706
|
sideBody.innerHTML=`<span class="type"><span class="dot" style="background:${c}"></span>${esc(ty)}</span>
|
|
501
707
|
<h2 class="title">${esc(n.title||id)}</h2><div class="desc loading" id="desc">loading…</div>
|
|
502
708
|
<div class="tags">${(tagsOf[id]||[]).map(t=>`<span class="tag">${esc(t)}</span>`).join('')}</div>
|
|
@@ -505,12 +711,25 @@ function show(id){const n=byId[id];if(!n)return;const ty=typeOf[id]||'Untyped';c
|
|
|
505
711
|
const eid=encodeURIComponent(id);
|
|
506
712
|
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(()=>{});
|
|
507
713
|
const bodyEl=sideBody.querySelector('#body');
|
|
508
|
-
fetch(NODE_ENDPOINT+'?id='+eid).then(r=>r.ok?r.text():'').then(md=>{if(bodyEl.isConnected)renderMarkdown(bodyEl,md);}).catch(()=>{});
|
|
509
|
-
|
|
714
|
+
fetch(NODE_ENDPOINT+'?id='+eid).then(r=>r.ok?r.text():'').then(md=>{if(bodyEl.isConnected)renderMarkdown(bodyEl,md);}).catch(()=>{});
|
|
715
|
+
return opened;}
|
|
716
|
+
/* One camera move per selection. When the click also opens the panel, the
|
|
717
|
+
container is still animating (--side-w, .22s), so pan now and the debounced
|
|
718
|
+
cy.resize() would shift the view again a moment later — the double movement.
|
|
719
|
+
Instead wait for the panel to settle, resize once, then pan. cy.stop() first
|
|
720
|
+
so rapid clicks replace the animation instead of queueing repeats. */
|
|
721
|
+
let centerTimer;
|
|
722
|
+
function centerOn(ele,delay){clearTimeout(centerTimer);
|
|
723
|
+
const go=()=>{cy.resize();cy.stop();cy.animate({center:{eles:ele},duration:450,easing:'ease-in-out-cubic'});};
|
|
724
|
+
if(delay)centerTimer=setTimeout(go,260);else{cy.stop();cy.animate({center:{eles:ele},duration:450,easing:'ease-in-out-cubic'});}}
|
|
725
|
+
function select(id){const ele=cy.getElementById(id);if(!ele.length)return;const opened=show(id);
|
|
510
726
|
cy.elements().removeClass('hl').addClass('dim');const nb=ele.closedNeighborhood();nb.removeClass('dim');ele.addClass('hl');
|
|
511
|
-
|
|
727
|
+
centerOn(ele,opened);
|
|
512
728
|
try{if(decodeURIComponent((location.hash||'').slice(1))!==id)location.hash=encodeURIComponent(id);}catch(e){}}
|
|
513
|
-
cy.on('tap','node',e=>{
|
|
729
|
+
cy.on('tap','node',e=>{const t=e.target;
|
|
730
|
+
if(t.hasClass('dir'))return showDir(t.id().slice(DIR.length)||'.');
|
|
731
|
+
if(t.isParent()){const a=t.id().indexOf('area::')===0?t.id().slice(6):null;return showDir(a&&a!=='(root)'?a:'.');}
|
|
732
|
+
select(t.id());});
|
|
514
733
|
cy.on('tap',e=>{if(e.target===cy)cy.elements().removeClass('dim hl');});
|
|
515
734
|
|
|
516
735
|
/* ── graph filters (slide-over) ── */
|
|
@@ -518,7 +737,7 @@ const hiddenTypes=new Set(), activeTags=new Set(), activeAreas=new Set();
|
|
|
518
737
|
const searchInput=document.getElementById('search');
|
|
519
738
|
const q={graph:'',catalog:'',files:'',tags:''};
|
|
520
739
|
function applyGraphFilter(){const s=q.graph.toLowerCase();
|
|
521
|
-
cy.nodes().forEach(n=>{if(n.isParent())return;const id=n.data('id');const t=typeOf[id]||'Untyped';const nt=tagsOf[id]||[];
|
|
740
|
+
cy.nodes().forEach(n=>{if(n.isParent()||n.hasClass('dir'))return;const id=n.data('id');const t=typeOf[id]||'Untyped';const nt=tagsOf[id]||[];
|
|
522
741
|
const hay=(n.data('title')+' '+t+' '+nt.join(' ')).toLowerCase();
|
|
523
742
|
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)));
|
|
524
743
|
n.style('display',m?'element':'none');});}
|
|
@@ -570,7 +789,10 @@ function loadScript(src){return new Promise((res,rej)=>{const s=document.createE
|
|
|
570
789
|
async function ensureLayout(name){const ext=LAYOUT_EXT[name];if(!ext||loadedExt[name])return true;
|
|
571
790
|
try{for(const src of ext.srcs)await loadScript(src);cytoscape.use(window[ext.reg]);loadedExt[name]=true;return true;}
|
|
572
791
|
catch(e){console.warn('layout '+name+' failed to load',e);return false;}}
|
|
573
|
-
|
|
792
|
+
/* nodeOverlap keeps a real gap between circles in cose; avoidOverlap +
|
|
793
|
+
spacingFactor do the same for the geometric layouts. Extras are ignored
|
|
794
|
+
by layouts that don't know them. */
|
|
795
|
+
function layoutOpts(name){const base={animate:true,padding:40,nodeOverlap:24,avoidOverlap:true,spacingFactor:1.2};
|
|
574
796
|
if(name==='fcose')return{...base,name:'fcose',quality:'default',nodeRepulsion:6500,idealEdgeLength:95,nodeSeparation:80,packComponents:true};
|
|
575
797
|
if(name==='dagre')return{...base,name:'dagre',rankDir:'TB',nodeSep:26,rankSep:55,edgeSep:12};
|
|
576
798
|
if(name==='cola')return{...base,name:'cola',maxSimulationTime:2500,edgeLength:95,nodeSpacing:8};
|
|
@@ -582,24 +804,60 @@ layoutSel.value='<%= @layout %>';
|
|
|
582
804
|
|
|
583
805
|
/* ── cluster by area (compound parent nodes) ── */
|
|
584
806
|
let clustered=false;const btnCluster=document.getElementById('btn-cluster');
|
|
585
|
-
function setClustered(on){if(on===clustered)return;
|
|
807
|
+
function setClustered(on){if(on===clustered)return;if(on&&treeMode)setTree(false);
|
|
808
|
+
clustered=on;btnCluster.setAttribute('aria-pressed',String(on));layoutSel.disabled=on;
|
|
586
809
|
if(on){const as=[...new Set(NODES.map(n=>areaOf(n.id)))];
|
|
587
810
|
cy.add(as.map(a=>({group:'nodes',data:{id:'area::'+a,label:a}})));
|
|
588
811
|
cy.batch(()=>NODES.forEach(n=>{const e=cy.getElementById(n.id);if(e.length)e.move({parent:'area::'+areaOf(n.id)});}));
|
|
589
|
-
cy.layout({name:'cose',animate:true,padding:40,nodeRepulsion:8000,idealEdgeLength:70,nestingFactor:1.2,gravity:.7}).run();
|
|
812
|
+
cy.layout({name:'cose',animate:true,padding:40,nodeRepulsion:8000,idealEdgeLength:70,nodeOverlap:24,nestingFactor:1.2,gravity:.7}).run();
|
|
590
813
|
}else{
|
|
591
814
|
cy.batch(()=>NODES.forEach(n=>{const e=cy.getElementById(n.id);if(e.length&&e.isChild())e.move({parent:null});}));
|
|
592
815
|
cy.nodes().filter(n=>n.id().indexOf('area::')===0).remove();
|
|
593
|
-
cy.layout({name:layoutSel.value,animate:true,padding:40,nodeRepulsion:9000,idealEdgeLength:90}).run();}}
|
|
816
|
+
cy.layout({name:layoutSel.value,animate:true,padding:40,nodeRepulsion:9000,idealEdgeLength:90,nodeOverlap:24,avoidOverlap:true,spacingFactor:1.2}).run();}}
|
|
594
817
|
btnCluster.onclick=()=>setClustered(!clustered);
|
|
595
818
|
|
|
819
|
+
/* ── file-tree mode: folders become nodes and the only edges are folder→child,
|
|
820
|
+
so the picture is the acyclic files-and-folders tree, not the link graph. ── */
|
|
821
|
+
let treeMode=false;const btnTree=document.getElementById('btn-tree');
|
|
822
|
+
const DIR='dir::';
|
|
823
|
+
function setTree(on){if(on===treeMode)return;if(on&&clustered)setClustered(false);
|
|
824
|
+
treeMode=on;btnTree.setAttribute('aria-pressed',String(on));layoutSel.disabled=on;btnCluster.disabled=on;
|
|
825
|
+
if(on){
|
|
826
|
+
const dirs=new Set(['']);
|
|
827
|
+
NODES.forEach(n=>{const parts=n.id.split('/');for(let i=1;i<parts.length;i++)dirs.add(parts.slice(0,i).join('/'));});
|
|
828
|
+
cy.batch(()=>{
|
|
829
|
+
cy.add([...dirs].map(d=>({group:'nodes',classes:'dir',
|
|
830
|
+
data:{id:DIR+d,title:d?d.split('/').pop()+'/':'/',c:cvar('--faint'),sz:d?20:28}})));
|
|
831
|
+
[...dirs].forEach(d=>{if(!d)return;const p=d.split('/').slice(0,-1).join('/');
|
|
832
|
+
cy.add({group:'edges',classes:'tree',data:{id:'tree::'+d,source:DIR+p,target:DIR+d}});});
|
|
833
|
+
NODES.forEach(n=>{const p=n.id.split('/').slice(0,-1).join('/');
|
|
834
|
+
cy.add({group:'edges',classes:'tree',data:{id:'tree::n::'+n.id,source:DIR+p,target:n.id}});});
|
|
835
|
+
cy.edges().not('.tree').addClass('linkhid');
|
|
836
|
+
cy.elements().removeClass('dim hl');});
|
|
837
|
+
/* Layered tree: root on top, folders as branches, files as leaves.
|
|
838
|
+
animate:false — the freshly added folder nodes have no positions yet, and
|
|
839
|
+
an animated run from (0,0) is unreliable; cut to the tree and fit. */
|
|
840
|
+
cy.layout({name:'breadthfirst',directed:true,roots:cy.getElementById(DIR),animate:false,padding:40,spacingFactor:1.6,avoidOverlap:true}).run();
|
|
841
|
+
fitGraph();
|
|
842
|
+
}else{
|
|
843
|
+
cy.batch(()=>{cy.edges('.tree').remove();cy.nodes('.dir').remove();cy.edges().removeClass('linkhid');});
|
|
844
|
+
runLayout(layoutSel.value);}}
|
|
845
|
+
btnTree.onclick=()=>setTree(!treeMode);
|
|
846
|
+
/* A reserved file's "Open in graph": switch to the file tree, select its
|
|
847
|
+
folder node, and put the directory's map in the inspector. */
|
|
848
|
+
function openInTree(dir){setView('graph');setTree(true);
|
|
849
|
+
showDir(dir);
|
|
850
|
+
requestAnimationFrame(()=>{const ele=cy.getElementById(DIR+(dir==='.'?'':dir));if(!ele.length)return;
|
|
851
|
+
cy.elements().removeClass('hl').addClass('dim');ele.closedNeighborhood().removeClass('dim');ele.addClass('hl');
|
|
852
|
+
centerOn(ele,true);});}
|
|
853
|
+
|
|
596
854
|
/* ── fit to screen ── */
|
|
597
855
|
// cy.animate({fit:…}) is a no-op in this Cytoscape build, so compute the target
|
|
598
856
|
// zoom + pan and animate those (which do work): fits the visible nodes, re-centered.
|
|
599
857
|
function fitGraph(){const vis=cy.nodes(':visible').filter(n=>!n.isParent());const eles=vis.length?vis:cy.nodes();if(!eles.length)return;
|
|
600
858
|
const bb=eles.boundingBox(),pad=60,w=cy.width(),h=cy.height();
|
|
601
859
|
const z=Math.max(cy.minZoom(),Math.min(cy.maxZoom(),Math.min((w-2*pad)/bb.w,(h-2*pad)/bb.h)));
|
|
602
|
-
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:
|
|
860
|
+
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:450,easing:'ease-in-out-cubic'});}
|
|
603
861
|
document.getElementById('btn-fit').onclick=fitGraph;
|
|
604
862
|
|
|
605
863
|
/* ── view switching ── */
|
|
@@ -607,18 +865,27 @@ let view='graph';
|
|
|
607
865
|
const inited={};
|
|
608
866
|
function goToGraph(id){setView('graph');requestAnimationFrame(()=>select(id));}
|
|
609
867
|
function setView(v){if(v===view)return;view=v;document.getElementById('app').setAttribute('data-view',v);
|
|
610
|
-
|
|
868
|
+
railActive();
|
|
611
869
|
document.querySelectorAll('.view').forEach(s=>s.classList.toggle('active',s.id==='view-'+v));
|
|
612
|
-
const ph={graph:'search concepts…',catalog:'search concepts…',files:'',tags:'search tags…'}[v];
|
|
870
|
+
const ph={graph:'search concepts…',catalog:'search concepts…',files:'filter files…',tags:'search tags…'}[v];
|
|
613
871
|
if(ph!==undefined){searchInput.placeholder=ph;searchInput.value=q[v]||'';}
|
|
614
872
|
if(v==='graph')requestAnimationFrame(()=>cy.resize());
|
|
615
873
|
if(v==='catalog'&&!inited.catalog)initCatalog();
|
|
616
874
|
if(v==='files'&&!inited.files)initFiles();
|
|
617
875
|
if(v==='tags'&&!inited.tags)initTags();
|
|
618
876
|
if(v==='stats'&&!inited.stats)initStats();}
|
|
619
|
-
|
|
877
|
+
/* The Files view answers to two rail items: Files when the Files tab is up,
|
|
878
|
+
Index when the Indexes tab is — the Index rail item is a shortcut into it. */
|
|
879
|
+
function railActive(){const v=view==='files'&&typeof ftab!=='undefined'&&ftab==='indexes'?'index':view;
|
|
880
|
+
document.querySelectorAll('.rail-item').forEach(b=>b.classList.toggle('active',b.dataset.view===v));}
|
|
881
|
+
function goIndexes(){if(view!=='files')setView('files');setFtab('indexes');railActive();}
|
|
882
|
+
document.querySelectorAll('.rail-item').forEach(b=>b.onclick=()=>{
|
|
883
|
+
if(b.dataset.view==='index')return goIndexes();
|
|
884
|
+
if(b.dataset.view==='files'&&typeof ftab!=='undefined'&&ftab!=='files'){if(view!=='files')setView('files');setFtab('files');railActive();return;}
|
|
885
|
+
setView(b.dataset.view);});
|
|
620
886
|
searchInput.oninput=()=>{q[view]=searchInput.value;
|
|
621
|
-
if(view==='graph')applyGraphFilter();else if(view==='catalog')renderCatalog();else if(view==='tags')renderCloud();
|
|
887
|
+
if(view==='graph')applyGraphFilter();else if(view==='catalog')renderCatalog();else if(view==='tags')renderCloud();
|
|
888
|
+
else if(view==='files')refreshTree();};
|
|
622
889
|
|
|
623
890
|
/* ── catalog ── */
|
|
624
891
|
let CATALOG=null;
|
|
@@ -674,18 +941,50 @@ function renderCatalog(){if(!CATALOG)return;getCatalog().then(list=>{const s=(q.
|
|
|
674
941
|
|
|
675
942
|
/* ── files ── */
|
|
676
943
|
let fileSel=null;
|
|
677
|
-
function initFiles(){inited.files=true;
|
|
678
|
-
|
|
944
|
+
function initFiles(){inited.files=true;refreshTree();}
|
|
945
|
+
/* The tree column has two tabs. Files: the per-directory concept groups,
|
|
946
|
+
foldable. Indexes: the authored layer flat — the log first (it is the
|
|
947
|
+
chronological index), then every index.md, root before nested. Reserved
|
|
948
|
+
files come from the /index and /log payloads — they are not concepts, so
|
|
949
|
+
the catalog cannot know them. */
|
|
950
|
+
let IXDIRS=null,LOGPATHS=null,ftab='files';
|
|
951
|
+
const collapsedDirs=new Set();
|
|
952
|
+
function dirOfPath(p){return p.indexOf('/')<0?'.':p.split('/').slice(0,-1).join('/');}
|
|
953
|
+
function renderTree(list){const s=(q.files||'').toLowerCase();
|
|
954
|
+
const el=document.getElementById('ftree-list');
|
|
955
|
+
if(ftab==='indexes'){
|
|
956
|
+
const reserved=[];
|
|
957
|
+
(LOGPATHS||[]).forEach(p=>{if(!s||p.toLowerCase().includes(s))reserved.push({kind:'log',path:p});});
|
|
958
|
+
(IXDIRS||[]).forEach(d=>{if(d.present&&(!s||d.index_path.toLowerCase().includes(s)))reserved.push({kind:'index',path:d.index_path});});
|
|
959
|
+
const rank=r=>[ r.kind==='log'?0:1, r.path.indexOf('/')<0?0:1, r.path ];
|
|
960
|
+
reserved.sort((a,b)=>{const x=rank(a),y=rank(b);return (x[0]-y[0])||(x[1]-y[1])||(x[2]<y[2]?-1:1);});
|
|
961
|
+
if(!reserved.length){el.innerHTML='<p class="empty" style="padding:14px">No index.md or log.md files'+(s?' match.':' yet.')+'</p>';return;}
|
|
962
|
+
el.innerHTML=reserved.map(r=>{
|
|
963
|
+
const icon=r.kind==='index'?'<path d="M4 5h16M4 9.6h10.5M4 14.2h16M4 18.8h7.5"/>':'<circle cx="12" cy="12" r="8.5"/><path d="M12 7.3V12l3.2 2"/>';
|
|
964
|
+
return `<button class="file res${fileSel===r.path?' sel':''}" data-res="${r.kind}" data-path="${esc(r.path)}"><span class="fn"><svg class="ricon" viewBox="0 0 24 24">${icon}</svg>${esc(r.path)}<span class="badge-res">${r.kind==='index'?'map':'log'}</span></span></button>`;}).join('');
|
|
965
|
+
el.querySelectorAll('.file[data-res]').forEach(b=>b.onclick=()=>openReserved(b.dataset.res,b.dataset.path));
|
|
966
|
+
return;}
|
|
679
967
|
const ft=fileTypeCombo.get(), fg=fileTagCombo.get();
|
|
680
968
|
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)));
|
|
681
969
|
const groups={};rows.forEach(c=>{(groups[c.dir]=groups[c.dir]||[]).push(c);});
|
|
682
|
-
const
|
|
683
|
-
if(!
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
970
|
+
const dirs=Object.keys(groups).sort();
|
|
971
|
+
if(!dirs.length){el.innerHTML='<p class="empty" style="padding:14px">No files match.</p>';return;}
|
|
972
|
+
const filtering=!!(s||ft||fg);
|
|
973
|
+
el.innerHTML=dirs.map(dir=>{
|
|
974
|
+
const closed=!filtering&&collapsedDirs.has(dir);
|
|
975
|
+
const head=`<button class="ffolder${closed?' closed':''}" data-dir="${esc(dir)}"><svg class="chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>${dir==='.'?'(root)':esc(dir)+'/'}</button>`;
|
|
976
|
+
if(closed)return head;
|
|
977
|
+
return head+(groups[dir]||[]).map(c=>{const cc=color[c.type]||'#64748b';const name=c.id.split('/').pop()+'.md';
|
|
978
|
+
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('');
|
|
979
|
+
el.querySelectorAll('.ffolder').forEach(b=>b.onclick=()=>{const d=b.dataset.dir;
|
|
980
|
+
if(collapsedDirs.has(d))collapsedDirs.delete(d);else collapsedDirs.add(d);renderTree(list);});
|
|
981
|
+
el.querySelectorAll('.file[data-id]').forEach(b=>b.onclick=()=>openFile(b.dataset.id,list));}
|
|
982
|
+
function setFtab(t){if(ftab===t)return;
|
|
983
|
+
ftab=t;document.getElementById('app').setAttribute('data-ftab',t);
|
|
984
|
+
[ 'files','indexes' ].forEach(x=>{const b=document.getElementById('ftab-'+x);
|
|
985
|
+
b.classList.toggle('active',x===t);b.setAttribute('aria-selected',String(x===t));});
|
|
986
|
+
railActive();refreshTree();}
|
|
987
|
+
[ 'files','indexes' ].forEach(t=>{document.getElementById('ftab-'+t).onclick=()=>setFtab(t);});
|
|
689
988
|
/* Searchable single-select combobox (keyboard-navigable) — one per files filter:
|
|
690
989
|
type and tag. `items` is lazy so counts always reflect the current indexes. */
|
|
691
990
|
function makeCombo(name,allLabel,items,onChange){
|
|
@@ -712,19 +1011,40 @@ function makeCombo(name,allLabel,items,onChange){
|
|
|
712
1011
|
list.addEventListener('mousedown',e=>{const li=e.target.closest('li');if(!li)return;e.preventDefault();pick(li);});
|
|
713
1012
|
document.getElementById(name+'-clear').addEventListener('click',()=>{value=null;input.value='';combo.classList.remove('has');onChange();input.focus();});
|
|
714
1013
|
return {get:()=>value};}
|
|
715
|
-
const refreshTree=()=>getCatalog().then(renderTree);
|
|
1014
|
+
const refreshTree=()=>Promise.all([ getCatalog(),getIndex(),getLogs() ]).then(([ list,dirs,logs ])=>{IXDIRS=dirs;LOGPATHS=logs.map(l=>l.path);renderTree(list);});
|
|
716
1015
|
const fileTypeCombo=makeCombo('file-type','All types',()=>catByCount.map(t=>({v:t,n:(TYPES[t]||[]).length,c:color[t]})),refreshTree);
|
|
717
1016
|
const fileTagCombo=makeCombo('file-tag','All tags',()=>tagsByCount.map(t=>({v:t,n:(TAGS[t]||[]).length})),refreshTree);
|
|
718
1017
|
function openFile(id,list){fileSel=id;const c=(list||[]).find(x=>x.id===id)||byId[id]||{};
|
|
1018
|
+
setFtab('files');
|
|
719
1019
|
document.querySelectorAll('.file').forEach(b=>b.classList.toggle('sel',b.dataset.id===id));
|
|
720
1020
|
const ty=c.type||typeOf[id]||'Untyped';const cc=color[ty]||'#64748b';
|
|
721
1021
|
document.getElementById('fp-head').hidden=false;
|
|
722
1022
|
document.getElementById('fp-type').innerHTML=`<span class="dot" style="background:${cc}"></span>${esc(ty)}`;
|
|
723
1023
|
document.getElementById('fp-title').textContent=c.title||(byId[id]||{}).title||id;
|
|
1024
|
+
document.getElementById('fp-graph').hidden=false;
|
|
724
1025
|
document.getElementById('fp-graph').onclick=()=>goToGraph(id);
|
|
725
1026
|
const body=document.getElementById('fp-body');body.innerHTML='<p class="loading">loading…</p>';
|
|
726
1027
|
fetch(NODE_ENDPOINT+'?id='+encodeURIComponent(id)).then(r=>r.ok?r.text():'').then(txt=>{
|
|
727
1028
|
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>';});}
|
|
1029
|
+
/* Reserved files read in the preview too: the index map from its payload, the
|
|
1030
|
+
log fetched fresh on every click so a just-appended entry shows. */
|
|
1031
|
+
function openReserved(kind,path){fileSel=path;
|
|
1032
|
+
setFtab('indexes');
|
|
1033
|
+
document.querySelectorAll('.file').forEach(b=>b.classList.toggle('sel',b.dataset.path===path));
|
|
1034
|
+
document.getElementById('fp-head').hidden=false;
|
|
1035
|
+
document.getElementById('fp-title').textContent=path;
|
|
1036
|
+
const fg=document.getElementById('fp-graph');
|
|
1037
|
+
fg.hidden=false;fg.onclick=()=>openInTree(dirOfPath(path));
|
|
1038
|
+
const type=document.getElementById('fp-type');
|
|
1039
|
+
const body=document.getElementById('fp-body');body.innerHTML='<p class="loading">loading…</p>';
|
|
1040
|
+
const done=txt=>{const wrap=document.createElement('div');wrap.className='body';renderMarkdown(wrap,txt);body.innerHTML='';body.appendChild(wrap);};
|
|
1041
|
+
if(kind==='index')getIndex().then(dirs=>{const d=dirs.find(x=>x.dir===dirOfPath(path));
|
|
1042
|
+
type.textContent=d&&d.synthesized?'directory index · synthesized':'directory index · §6';
|
|
1043
|
+
if(d&&d.synthesized){body.innerHTML='<div class="body">'+listingHtml(d.listing)+'</div>';
|
|
1044
|
+
body.querySelectorAll('[data-go]').forEach(x=>x.onclick=()=>openFile(x.getAttribute('data-go')));}
|
|
1045
|
+
else done((d&&d.body)||'');});
|
|
1046
|
+
else{type.textContent='update log · §7';
|
|
1047
|
+
LOGS=null;getLogs().then(logs=>{const l=logs.find(x=>x.path===path);done((l&&l.content)||'');});}}
|
|
728
1048
|
|
|
729
1049
|
/* ── tags ── */
|
|
730
1050
|
const tagSel=new Set(), tagTypes=new Set(), tagAreas=new Set();
|
|
@@ -797,10 +1117,22 @@ const fsOn=()=>document.fullscreenElement===appEl;
|
|
|
797
1117
|
btnFull.onclick=()=>{if(fsOn())document.exitFullscreen();else if(appEl.requestFullscreen)appEl.requestFullscreen();};
|
|
798
1118
|
document.addEventListener('fullscreenchange',()=>btnFull.setAttribute('aria-pressed',String(fsOn())));
|
|
799
1119
|
|
|
1120
|
+
/* ── the §6 map payload — feeds folder clicks, the Indexes tab, and previews ── */
|
|
1121
|
+
let INDEX=null;
|
|
1122
|
+
function getIndex(){return INDEX||(INDEX=fetch(INDEX_ENDPOINT).then(r=>r.json()).then(d=>d.directories));}
|
|
1123
|
+
function listingHtml(items){if(!items||!items.length)return '<p class="empty" style="margin:10px 0 0">No concepts directly here.</p>';
|
|
1124
|
+
return '<ul class="ix-listing">'+items.map(it=>`<li><a data-go="${esc(it.id)}">${esc(it.title||it.id)}</a>${it.description?` <span class="d">— ${esc(it.description)}</span>`:''}</li>`).join('')+'</ul>';}
|
|
1125
|
+
|
|
1126
|
+
/* ── log payload (the §7 history) — read by the Files view's log entries ── */
|
|
1127
|
+
let LOGS=null;
|
|
1128
|
+
function getLogs(){return LOGS||(LOGS=fetch(LOG_ENDPOINT).then(r=>r.json()).then(d=>d.logs));}
|
|
1129
|
+
|
|
800
1130
|
/* ── keyboard ── */
|
|
801
|
-
const VIEW_KEYS={'1':'graph','
|
|
802
|
-
addEventListener('keydown',e=>{if(e.
|
|
803
|
-
if(
|
|
1131
|
+
const VIEW_KEYS={'1':'graph','3':'files','4':'catalog','5':'tags','6':'stats'};
|
|
1132
|
+
addEventListener('keydown',e=>{if(!dgv.hidden){if(e.key==='Escape')closeDiagram();return;}
|
|
1133
|
+
if(e.target.matches('input,select,textarea'))return;
|
|
1134
|
+
if(e.key==='2'){goIndexes();}
|
|
1135
|
+
else if(VIEW_KEYS[e.key]){setView(VIEW_KEYS[e.key]);}
|
|
804
1136
|
else if(e.key==='/'){e.preventDefault();searchInput.focus();}
|
|
805
1137
|
else if(e.key==='\\'&&view==='graph'){setSide(gbody.getAttribute('data-side')==='hidden'?'default':'hidden');}
|
|
806
1138
|
else if(e.key==='0'&&view==='graph'){fitGraph();}
|
|
@@ -809,6 +1141,9 @@ addEventListener('keydown',e=>{if(e.target.matches('input,select,textarea'))retu
|
|
|
809
1141
|
/* ── deep links ── */
|
|
810
1142
|
const Q=new URLSearchParams(location.search),QL=Q.get('layout'),QS=Q.get('select');
|
|
811
1143
|
if(QL&&[...layoutSel.options].some(o=>o.value===QL)){layoutSel.value=QL;runLayout(QL);}
|
|
1144
|
+
const QV=Q.get('view');
|
|
1145
|
+
if(QV==='index')goIndexes();
|
|
1146
|
+
else if(QV&&document.querySelector('.rail-item[data-view="'+CSS.escape(QV)+'"]'))setView(QV);
|
|
812
1147
|
function fromHash(){try{const h=decodeURIComponent((location.hash||'').slice(1));if(h&&byId[h])select(h);}catch(e){}}
|
|
813
1148
|
addEventListener('hashchange',fromHash);
|
|
814
1149
|
if(QS&&byId[QS])select(QS);else fromHash();
|