@refrakt-md/plan 0.29.0 → 0.30.1
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.
- package/dist/cli-plugin.d.ts.map +1 -1
- package/dist/cli-plugin.js +0 -104
- package/dist/cli-plugin.js.map +1 -1
- package/dist/commands/enums.d.ts +26 -4
- package/dist/commands/enums.d.ts.map +1 -1
- package/dist/commands/enums.js +41 -4
- package/dist/commands/enums.js.map +1 -1
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +4 -3
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +135 -6
- package/dist/commands/validate.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/pipeline.d.ts +2 -5
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +10 -19
- package/dist/pipeline.js.map +1 -1
- package/package.json +5 -21
- package/dist/commands/build.d.ts +0 -15
- package/dist/commands/build.d.ts.map +0 -1
- package/dist/commands/build.js +0 -79
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/bundle-behaviors.d.ts +0 -10
- package/dist/commands/bundle-behaviors.d.ts.map +0 -1
- package/dist/commands/bundle-behaviors.js +0 -35
- package/dist/commands/bundle-behaviors.js.map +0 -1
- package/dist/commands/plan-behaviors.d.ts +0 -2
- package/dist/commands/plan-behaviors.d.ts.map +0 -1
- package/dist/commands/plan-behaviors.js +0 -9
- package/dist/commands/plan-behaviors.js.map +0 -1
- package/dist/commands/render-pipeline.d.ts +0 -77
- package/dist/commands/render-pipeline.d.ts.map +0 -1
- package/dist/commands/render-pipeline.js +0 -1225
- package/dist/commands/render-pipeline.js.map +0 -1
- package/dist/commands/serve.d.ts +0 -14
- package/dist/commands/serve.d.ts.map +0 -1
- package/dist/commands/serve.js +0 -230
- package/dist/commands/serve.js.map +0 -1
|
@@ -1,1225 +0,0 @@
|
|
|
1
|
-
import 'reflect-metadata';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import Markdoc from '@markdoc/markdoc';
|
|
6
|
-
import { tags as coreTags, nodes, extractHeadings, runeTagMap, defineRune, serializeTree, coreConfig, escapeFenceTags, resolveXrefs, resolveCollections } from '@refrakt-md/runes';
|
|
7
|
-
import { createTransform, mergeThemeConfig, planLayout } from '@refrakt-md/transform';
|
|
8
|
-
import { renderFullPage as htmlRenderFullPage } from '@refrakt-md/html';
|
|
9
|
-
import { createHighlightTransform } from '@refrakt-md/highlight';
|
|
10
|
-
import { plan } from '../index.js';
|
|
11
|
-
import { planPipelineHooks, setScannerDependencies, setPlanDir } from '../pipeline.js';
|
|
12
|
-
import { scanPlanFiles } from '../scanner.js';
|
|
13
|
-
import { buildBlockedByAdjacency } from '../scanner-core.js';
|
|
14
|
-
import { getGitTimestamps, getStatTimestamps } from '@refrakt-md/content';
|
|
15
|
-
import { TERMINAL_STATUS_UNION, WORK_STATUS_DISPLAY_ORDER, BUG_STATUS_DISPLAY_ORDER } from './enums.js';
|
|
16
|
-
// --- Markdoc tag registry (built once) ---
|
|
17
|
-
const pluginRunes = {};
|
|
18
|
-
for (const [name, entry] of Object.entries(plan.runes)) {
|
|
19
|
-
pluginRunes[name] = defineRune({ name, schema: entry.transform, aliases: entry.aliases });
|
|
20
|
-
}
|
|
21
|
-
const tags = { ...coreTags, ...runeTagMap(pluginRunes), ...Markdoc.tags };
|
|
22
|
-
// --- Theme resolution ---
|
|
23
|
-
function inlineCssImports(css, cssDir) {
|
|
24
|
-
return css.replace(/@import\s+['"]\.\/([^'"]+)['"]\s*;/g, (_match, file) => {
|
|
25
|
-
const importPath = path.join(cssDir, file);
|
|
26
|
-
if (fs.existsSync(importPath)) {
|
|
27
|
-
return fs.readFileSync(importPath, 'utf-8');
|
|
28
|
-
}
|
|
29
|
-
return _match;
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
/** Well-known shorthand names mapped to npm package names. */
|
|
33
|
-
const THEME_SHORTHANDS = {
|
|
34
|
-
lumina: '@refrakt-md/lumina',
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Try to read the `theme` field from refrakt.config.json in the working directory.
|
|
38
|
-
* Returns the theme package name or undefined if no config / no theme field.
|
|
39
|
-
*/
|
|
40
|
-
function readConfigTheme() {
|
|
41
|
-
const candidates = [
|
|
42
|
-
path.resolve('refrakt.config.json'),
|
|
43
|
-
path.resolve('site', 'refrakt.config.json'),
|
|
44
|
-
];
|
|
45
|
-
for (const configPath of candidates) {
|
|
46
|
-
if (!fs.existsSync(configPath))
|
|
47
|
-
continue;
|
|
48
|
-
try {
|
|
49
|
-
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
50
|
-
if (typeof raw === 'object' && raw !== null && typeof raw.theme === 'string' && raw.theme) {
|
|
51
|
-
return raw.theme;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// Malformed config — silently skip
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Resolve a theme package name to its base CSS content.
|
|
62
|
-
* Tries `require.resolve(pkg + '/base.css')` first, then the main export.
|
|
63
|
-
* Returns undefined if the package can't be resolved.
|
|
64
|
-
*/
|
|
65
|
-
function resolvePackageThemeCss(pluginName) {
|
|
66
|
-
// Try main export first (full theme with rune styles), then base.css (tokens only)
|
|
67
|
-
for (const subpath of ['', '/base.css']) {
|
|
68
|
-
try {
|
|
69
|
-
const entry = subpath
|
|
70
|
-
? import.meta.resolve(pluginName + subpath)
|
|
71
|
-
: import.meta.resolve(pluginName);
|
|
72
|
-
const resolved = entry.startsWith('file://') ? fileURLToPath(entry) : entry;
|
|
73
|
-
if (resolved.endsWith('.css') && fs.existsSync(resolved)) {
|
|
74
|
-
return inlineCssImports(fs.readFileSync(resolved, 'utf-8'), path.dirname(resolved));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// Not resolvable — continue
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
function resolveThemeCss(theme) {
|
|
84
|
-
// All styles (layout chrome, rune styling, design tokens) come from the theme package.
|
|
85
|
-
let themeCss;
|
|
86
|
-
if (theme === 'default') {
|
|
87
|
-
themeCss = resolvePackageThemeCss('@refrakt-md/lumina');
|
|
88
|
-
if (!themeCss) {
|
|
89
|
-
console.warn('[plan] Warning: @refrakt-md/lumina could not be resolved. Styling will be missing.');
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
else if (theme === 'auto') {
|
|
93
|
-
const configTheme = readConfigTheme();
|
|
94
|
-
if (configTheme) {
|
|
95
|
-
themeCss = resolvePackageThemeCss(configTheme);
|
|
96
|
-
if (!themeCss) {
|
|
97
|
-
console.warn(`[plan] Warning: Could not resolve theme "${configTheme}" from config. Falling back to Lumina.`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
if (!themeCss) {
|
|
101
|
-
themeCss = resolvePackageThemeCss('@refrakt-md/lumina');
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
// Shorthand name (e.g., "lumina" → "@refrakt-md/lumina")
|
|
106
|
-
const expanded = THEME_SHORTHANDS[theme];
|
|
107
|
-
if (expanded) {
|
|
108
|
-
themeCss = resolvePackageThemeCss(expanded);
|
|
109
|
-
if (!themeCss) {
|
|
110
|
-
throw new Error(`Theme "${theme}" (${expanded}) could not be resolved. Is it installed?`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
else if (theme.startsWith('@') || theme.includes('/')) {
|
|
114
|
-
themeCss = resolvePackageThemeCss(theme);
|
|
115
|
-
if (!themeCss) {
|
|
116
|
-
throw new Error(`Theme package "${theme}" could not be resolved. Is it installed?`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
else if (fs.existsSync(theme)) {
|
|
120
|
-
themeCss = inlineCssImports(fs.readFileSync(theme, 'utf-8'), path.dirname(theme));
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
throw new Error(`Theme not found: "${theme}". Use "default", "auto", a package name, or a path to a CSS file.`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return themeCss ?? '';
|
|
127
|
-
}
|
|
128
|
-
function buildThemeConfig() {
|
|
129
|
-
const planRuneConfigs = plan.theme?.runes ?? {};
|
|
130
|
-
return mergeThemeConfig(coreConfig, { runes: planRuneConfigs });
|
|
131
|
-
}
|
|
132
|
-
// --- Markdoc rendering ---
|
|
133
|
-
function parseAndTransform(content, filePath, fileTimestamps) {
|
|
134
|
-
const ast = Markdoc.parse(escapeFenceTags(content));
|
|
135
|
-
const headings = extractHeadings(ast);
|
|
136
|
-
const config = {
|
|
137
|
-
tags,
|
|
138
|
-
nodes,
|
|
139
|
-
variables: {
|
|
140
|
-
generatedIds: new Set(),
|
|
141
|
-
path: filePath,
|
|
142
|
-
headings,
|
|
143
|
-
__source: content,
|
|
144
|
-
...(fileTimestamps ? { file: { created: fileTimestamps.created, modified: fileTimestamps.modified } } : {}),
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
const renderable = Markdoc.transform(ast, config);
|
|
148
|
-
const title = headings.length > 0 ? headings[0].text : '';
|
|
149
|
-
// Annotate headings with known-section data from the rendered tree.
|
|
150
|
-
// buildSections adds data-known-section to heading Tags during transform.
|
|
151
|
-
annotateKnownSections(headings, renderable);
|
|
152
|
-
return { renderable, title, headings };
|
|
153
|
-
}
|
|
154
|
-
/** Walk the rendered Markdoc Tag tree and annotate HeadingInfo entries
|
|
155
|
-
* with their canonical known-section name when the heading has
|
|
156
|
-
* a `data-known-section` attribute set by buildSections. */
|
|
157
|
-
function annotateKnownSections(headings, renderable) {
|
|
158
|
-
const headingById = new Map(headings.map(h => [h.id, h]));
|
|
159
|
-
function walk(node) {
|
|
160
|
-
if (!node || typeof node !== 'object')
|
|
161
|
-
return;
|
|
162
|
-
if (Array.isArray(node)) {
|
|
163
|
-
for (const child of node)
|
|
164
|
-
walk(child);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (!Markdoc.Tag.isTag(node))
|
|
168
|
-
return;
|
|
169
|
-
const tag = node;
|
|
170
|
-
const name = tag.name;
|
|
171
|
-
if (/^h[1-6]$/.test(name) && tag.attributes['data-known-section']) {
|
|
172
|
-
const id = tag.attributes.id;
|
|
173
|
-
if (id) {
|
|
174
|
-
const heading = headingById.get(id);
|
|
175
|
-
if (heading) {
|
|
176
|
-
heading.knownSection = tag.attributes['data-known-section'];
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (tag.children) {
|
|
181
|
-
for (const child of tag.children)
|
|
182
|
-
walk(child);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
walk(renderable);
|
|
186
|
-
}
|
|
187
|
-
// --- Entity registry ---
|
|
188
|
-
function createRegistry() {
|
|
189
|
-
const entries = [];
|
|
190
|
-
const byTypeAndId = new Map();
|
|
191
|
-
const byTypeAndUrl = new Map();
|
|
192
|
-
const registry = {
|
|
193
|
-
register(entry) {
|
|
194
|
-
entries.push(entry);
|
|
195
|
-
if (!byTypeAndId.has(entry.type))
|
|
196
|
-
byTypeAndId.set(entry.type, new Map());
|
|
197
|
-
// Keep the first registration (entity's own page) — don't overwrite with
|
|
198
|
-
// duplicates from dashboard, status filter pages, etc.
|
|
199
|
-
if (!byTypeAndId.get(entry.type).has(entry.id)) {
|
|
200
|
-
byTypeAndId.get(entry.type).set(entry.id, entry);
|
|
201
|
-
}
|
|
202
|
-
// Skip URL indexing for entries without a sourceUrl. Plan content
|
|
203
|
-
// registered via SPEC-064's unconditional scan (file on disk, no
|
|
204
|
-
// site URL) lives in the primary index but doesn't participate in
|
|
205
|
-
// URL lookups. Matches EntityRegistryImpl in @refrakt-md/content.
|
|
206
|
-
if (entry.sourceUrl !== undefined) {
|
|
207
|
-
if (!byTypeAndUrl.has(entry.type))
|
|
208
|
-
byTypeAndUrl.set(entry.type, new Map());
|
|
209
|
-
if (!byTypeAndUrl.get(entry.type).has(entry.sourceUrl))
|
|
210
|
-
byTypeAndUrl.get(entry.type).set(entry.sourceUrl, []);
|
|
211
|
-
byTypeAndUrl.get(entry.type).get(entry.sourceUrl).push(entry);
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
getAll(type) {
|
|
215
|
-
return [...(byTypeAndId.get(type)?.values() ?? [])];
|
|
216
|
-
},
|
|
217
|
-
getById(type, id) {
|
|
218
|
-
return byTypeAndId.get(type)?.get(id);
|
|
219
|
-
},
|
|
220
|
-
getByUrl(type, url) {
|
|
221
|
-
return byTypeAndUrl.get(type)?.get(url) ?? [];
|
|
222
|
-
},
|
|
223
|
-
getTypes() {
|
|
224
|
-
return [...byTypeAndId.keys()];
|
|
225
|
-
},
|
|
226
|
-
};
|
|
227
|
-
return { registry, entries };
|
|
228
|
-
}
|
|
229
|
-
// --- Navigation builder ---
|
|
230
|
-
const NAV_ORDER = { milestone: 0, work: 1, bug: 2, spec: 3, decision: 4 };
|
|
231
|
-
const NAV_TITLES = {
|
|
232
|
-
milestone: 'Milestones',
|
|
233
|
-
work: 'Work Items',
|
|
234
|
-
bug: 'Bugs',
|
|
235
|
-
spec: 'Specs',
|
|
236
|
-
decision: 'Decisions',
|
|
237
|
-
};
|
|
238
|
-
/** Terminal statuses — collapsed by default in sidebar. Derived from the
|
|
239
|
-
* canonical per-type terminal sets (SPEC-117) so a new terminal status
|
|
240
|
-
* collapses correctly without a second hand-maintained list. */
|
|
241
|
-
const TERMINAL_STATUSES = TERMINAL_STATUS_UNION;
|
|
242
|
-
/** Status ordering — active statuses first, terminal last */
|
|
243
|
-
export const STATUS_ORDER = {
|
|
244
|
-
'in-progress': 0, confirmed: 1, review: 2, ready: 3, reported: 4,
|
|
245
|
-
active: 5, proposed: 6, planning: 7, draft: 8, pending: 9, blocked: 10,
|
|
246
|
-
done: 20, fixed: 21, accepted: 22, complete: 23, implemented: 24, shipped: 25,
|
|
247
|
-
superseded: 30, deprecated: 31, wontfix: 32, duplicate: 33, cancelled: 34, rejected: 35,
|
|
248
|
-
};
|
|
249
|
-
/** Human-readable status labels for group headers */
|
|
250
|
-
export const STATUS_LABELS_DISPLAY = {
|
|
251
|
-
'in-progress': 'In Progress',
|
|
252
|
-
confirmed: 'Confirmed', review: 'Review', ready: 'Ready', reported: 'Reported',
|
|
253
|
-
active: 'Active', proposed: 'Proposed', planning: 'Planning', draft: 'Draft',
|
|
254
|
-
pending: 'Pending', blocked: 'Blocked',
|
|
255
|
-
done: 'Done', fixed: 'Fixed', accepted: 'Accepted', complete: 'Complete',
|
|
256
|
-
implemented: 'Implemented', shipped: 'Shipped',
|
|
257
|
-
superseded: 'Superseded', deprecated: 'Deprecated', wontfix: "Won't Fix", duplicate: 'Duplicate',
|
|
258
|
-
cancelled: 'Cancelled', rejected: 'Rejected',
|
|
259
|
-
};
|
|
260
|
-
/** Status ordering per entity type (for nav display). Terminal states sink to
|
|
261
|
-
* the tail — `cancelled` / `superseded` join `done` there for work. */
|
|
262
|
-
export const STATUS_ORDER_BY_TYPE = {
|
|
263
|
-
work: ['in-progress', 'review', 'ready', 'blocked', 'draft', 'pending', 'done', 'cancelled', 'superseded'],
|
|
264
|
-
bug: ['in-progress', 'confirmed', 'reported', 'fixed', 'wontfix', 'duplicate'],
|
|
265
|
-
spec: ['review', 'draft', 'accepted', 'implemented', 'shipped', 'superseded', 'deprecated'],
|
|
266
|
-
decision: ['proposed', 'accepted', 'rejected', 'superseded', 'deprecated'],
|
|
267
|
-
milestone: ['active', 'planning', 'complete'],
|
|
268
|
-
};
|
|
269
|
-
function buildNavigation(entities, baseUrl) {
|
|
270
|
-
const groups = new Map();
|
|
271
|
-
for (const entity of entities) {
|
|
272
|
-
const type = entity.type;
|
|
273
|
-
if (!groups.has(type))
|
|
274
|
-
groups.set(type, []);
|
|
275
|
-
const id = entity.attributes.id || entity.attributes.name || '';
|
|
276
|
-
groups.get(type).push({
|
|
277
|
-
url: `${baseUrl}${type}/${slugify(id)}.html`,
|
|
278
|
-
label: `${id}${entity.title ? ' ' + entity.title : ''}`,
|
|
279
|
-
id,
|
|
280
|
-
status: entity.attributes.status || '',
|
|
281
|
-
priority: entity.attributes.priority,
|
|
282
|
-
tags: entity.attributes.tags,
|
|
283
|
-
assignee: entity.attributes.assignee,
|
|
284
|
-
milestone: entity.attributes.milestone,
|
|
285
|
-
severity: entity.attributes.severity,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
return [...groups.entries()]
|
|
289
|
-
.sort(([a], [b]) => (NAV_ORDER[a] ?? 99) - (NAV_ORDER[b] ?? 99))
|
|
290
|
-
.map(([type, items]) => {
|
|
291
|
-
const sorted = items.sort((a, b) => a.id.localeCompare(b.id));
|
|
292
|
-
// Group items by status
|
|
293
|
-
const byStatus = new Map();
|
|
294
|
-
for (const item of sorted) {
|
|
295
|
-
const s = item.status || 'unknown';
|
|
296
|
-
if (!byStatus.has(s))
|
|
297
|
-
byStatus.set(s, []);
|
|
298
|
-
byStatus.get(s).push(item);
|
|
299
|
-
}
|
|
300
|
-
const statusGroups = [...byStatus.entries()]
|
|
301
|
-
.sort(([a], [b]) => (STATUS_ORDER[a] ?? 15) - (STATUS_ORDER[b] ?? 15))
|
|
302
|
-
.map(([status, statusItems]) => ({
|
|
303
|
-
status,
|
|
304
|
-
label: STATUS_LABELS_DISPLAY[status] || status,
|
|
305
|
-
items: statusItems,
|
|
306
|
-
collapsed: TERMINAL_STATUSES.has(status),
|
|
307
|
-
}));
|
|
308
|
-
return {
|
|
309
|
-
title: NAV_TITLES[type] || type,
|
|
310
|
-
type,
|
|
311
|
-
items: sorted,
|
|
312
|
-
statusGroups,
|
|
313
|
-
};
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Convert NavGroup[] to a serialized tag tree for the layout's nav region.
|
|
318
|
-
* Produces BEM-classed elements matching the existing .rf-plan-sidebar__* selectors.
|
|
319
|
-
*/
|
|
320
|
-
function buildNavRegion(groups, baseUrl, activeUrl, viewDefs, statusFilterPages) {
|
|
321
|
-
const children = [];
|
|
322
|
-
// Title row with search trigger
|
|
323
|
-
children.push({
|
|
324
|
-
$$mdtype: 'Tag',
|
|
325
|
-
name: 'div',
|
|
326
|
-
attributes: { class: 'rf-plan-sidebar__title' },
|
|
327
|
-
children: [
|
|
328
|
-
'Plan',
|
|
329
|
-
{
|
|
330
|
-
$$mdtype: 'Tag',
|
|
331
|
-
name: 'button',
|
|
332
|
-
attributes: {
|
|
333
|
-
class: 'rf-plan-sidebar__search-trigger',
|
|
334
|
-
'aria-label': 'Search',
|
|
335
|
-
'data-search-trigger': '',
|
|
336
|
-
},
|
|
337
|
-
children: [{
|
|
338
|
-
$$mdtype: 'Tag',
|
|
339
|
-
name: 'svg',
|
|
340
|
-
attributes: {
|
|
341
|
-
width: '16',
|
|
342
|
-
height: '16',
|
|
343
|
-
viewBox: '0 0 24 24',
|
|
344
|
-
fill: 'none',
|
|
345
|
-
stroke: 'currentColor',
|
|
346
|
-
'stroke-width': '2',
|
|
347
|
-
'stroke-linecap': 'round',
|
|
348
|
-
'stroke-linejoin': 'round',
|
|
349
|
-
},
|
|
350
|
-
children: [
|
|
351
|
-
{
|
|
352
|
-
$$mdtype: 'Tag',
|
|
353
|
-
name: 'circle',
|
|
354
|
-
attributes: { cx: '11', cy: '11', r: '8' },
|
|
355
|
-
children: [],
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
$$mdtype: 'Tag',
|
|
359
|
-
name: 'line',
|
|
360
|
-
attributes: { x1: '21', y1: '21', x2: '16.65', y2: '16.65' },
|
|
361
|
-
children: [],
|
|
362
|
-
},
|
|
363
|
-
],
|
|
364
|
-
}],
|
|
365
|
-
},
|
|
366
|
-
],
|
|
367
|
-
});
|
|
368
|
-
// Search input (hidden by default, toggled by search-trigger button)
|
|
369
|
-
children.push({
|
|
370
|
-
$$mdtype: 'Tag',
|
|
371
|
-
name: 'input',
|
|
372
|
-
attributes: {
|
|
373
|
-
class: 'rf-plan-sidebar__search',
|
|
374
|
-
type: 'search',
|
|
375
|
-
placeholder: 'Filter items\u2026',
|
|
376
|
-
'aria-label': 'Filter items',
|
|
377
|
-
'data-search-input': '',
|
|
378
|
-
hidden: '',
|
|
379
|
-
},
|
|
380
|
-
children: [],
|
|
381
|
-
});
|
|
382
|
-
// Dashboard link
|
|
383
|
-
const dashActive = activeUrl === baseUrl || activeUrl === `${baseUrl}index.html`;
|
|
384
|
-
children.push({
|
|
385
|
-
$$mdtype: 'Tag',
|
|
386
|
-
name: 'a',
|
|
387
|
-
attributes: {
|
|
388
|
-
class: `rf-plan-sidebar__link rf-plan-sidebar__link--dashboard${dashActive ? ' rf-plan-sidebar__link--active' : ''}`,
|
|
389
|
-
href: baseUrl,
|
|
390
|
-
},
|
|
391
|
-
children: [
|
|
392
|
-
{
|
|
393
|
-
$$mdtype: 'Tag',
|
|
394
|
-
name: 'svg',
|
|
395
|
-
attributes: {
|
|
396
|
-
class: 'rf-plan-sidebar__dashboard-icon',
|
|
397
|
-
width: '14',
|
|
398
|
-
height: '14',
|
|
399
|
-
viewBox: '0 0 24 24',
|
|
400
|
-
fill: 'none',
|
|
401
|
-
stroke: 'currentColor',
|
|
402
|
-
'stroke-width': '2',
|
|
403
|
-
'stroke-linecap': 'round',
|
|
404
|
-
'stroke-linejoin': 'round',
|
|
405
|
-
},
|
|
406
|
-
children: [
|
|
407
|
-
{ $$mdtype: 'Tag', name: 'rect', attributes: { x: '3', y: '3', width: '7', height: '7', rx: '1' }, children: [] },
|
|
408
|
-
{ $$mdtype: 'Tag', name: 'rect', attributes: { x: '14', y: '3', width: '7', height: '7', rx: '1' }, children: [] },
|
|
409
|
-
{ $$mdtype: 'Tag', name: 'rect', attributes: { x: '3', y: '14', width: '7', height: '7', rx: '1' }, children: [] },
|
|
410
|
-
{ $$mdtype: 'Tag', name: 'rect', attributes: { x: '14', y: '14', width: '7', height: '7', rx: '1' }, children: [] },
|
|
411
|
-
],
|
|
412
|
-
},
|
|
413
|
-
'Dashboard',
|
|
414
|
-
],
|
|
415
|
-
});
|
|
416
|
-
// Entity groups
|
|
417
|
-
for (const group of groups) {
|
|
418
|
-
const groupChildren = [];
|
|
419
|
-
groupChildren.push({
|
|
420
|
-
$$mdtype: 'Tag',
|
|
421
|
-
name: 'div',
|
|
422
|
-
attributes: { class: 'rf-plan-sidebar__group-title' },
|
|
423
|
-
children: [group.title],
|
|
424
|
-
});
|
|
425
|
-
// Status links — each links to a filter page instead of listing individual items
|
|
426
|
-
const typeFilterPages = (statusFilterPages ?? []).filter(p => p.type === group.type);
|
|
427
|
-
if (typeFilterPages.length > 0) {
|
|
428
|
-
for (const fp of typeFilterPages) {
|
|
429
|
-
const isActive = fp.url === activeUrl;
|
|
430
|
-
groupChildren.push({
|
|
431
|
-
$$mdtype: 'Tag',
|
|
432
|
-
name: 'a',
|
|
433
|
-
attributes: {
|
|
434
|
-
class: `rf-plan-sidebar__link rf-plan-sidebar__status-link${isActive ? ' rf-plan-sidebar__link--active' : ''}`,
|
|
435
|
-
href: fp.url,
|
|
436
|
-
'data-status': fp.status,
|
|
437
|
-
},
|
|
438
|
-
children: [
|
|
439
|
-
{
|
|
440
|
-
$$mdtype: 'Tag',
|
|
441
|
-
name: 'span',
|
|
442
|
-
attributes: { class: 'rf-plan-sidebar__status-label' },
|
|
443
|
-
children: [STATUS_LABELS_DISPLAY[fp.status] || fp.status],
|
|
444
|
-
},
|
|
445
|
-
{
|
|
446
|
-
$$mdtype: 'Tag',
|
|
447
|
-
name: 'span',
|
|
448
|
-
attributes: { class: 'rf-plan-sidebar__status-count' },
|
|
449
|
-
children: [String(fp.count)],
|
|
450
|
-
},
|
|
451
|
-
],
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
// Fallback: render status sub-groups with individual item links (legacy behavior)
|
|
457
|
-
for (const sg of group.statusGroups) {
|
|
458
|
-
const subGroupChildren = [];
|
|
459
|
-
subGroupChildren.push({
|
|
460
|
-
$$mdtype: 'Tag',
|
|
461
|
-
name: 'button',
|
|
462
|
-
attributes: {
|
|
463
|
-
class: 'rf-plan-sidebar__status-header',
|
|
464
|
-
type: 'button',
|
|
465
|
-
'data-status': sg.status,
|
|
466
|
-
'aria-expanded': sg.collapsed ? 'false' : 'true',
|
|
467
|
-
},
|
|
468
|
-
children: [
|
|
469
|
-
{
|
|
470
|
-
$$mdtype: 'Tag',
|
|
471
|
-
name: 'span',
|
|
472
|
-
attributes: { class: 'rf-plan-sidebar__status-label' },
|
|
473
|
-
children: [sg.label],
|
|
474
|
-
},
|
|
475
|
-
{
|
|
476
|
-
$$mdtype: 'Tag',
|
|
477
|
-
name: 'span',
|
|
478
|
-
attributes: { class: 'rf-plan-sidebar__status-count' },
|
|
479
|
-
children: [String(sg.items.length)],
|
|
480
|
-
},
|
|
481
|
-
],
|
|
482
|
-
});
|
|
483
|
-
const itemNodes = [];
|
|
484
|
-
for (const item of sg.items) {
|
|
485
|
-
const isActive = item.url === activeUrl;
|
|
486
|
-
const blockerClass = item.hasUnresolvedBlockers ? ' rf-plan-sidebar__link--blocked' : '';
|
|
487
|
-
const attrs = {
|
|
488
|
-
class: `rf-plan-sidebar__link${isActive ? ' rf-plan-sidebar__link--active' : ''}${blockerClass}`,
|
|
489
|
-
href: item.url,
|
|
490
|
-
'data-id': item.id,
|
|
491
|
-
'data-status': item.status,
|
|
492
|
-
};
|
|
493
|
-
if (item.priority)
|
|
494
|
-
attrs['data-priority'] = item.priority;
|
|
495
|
-
if (item.tags)
|
|
496
|
-
attrs['data-tags'] = item.tags;
|
|
497
|
-
if (item.assignee)
|
|
498
|
-
attrs['data-assignee'] = item.assignee;
|
|
499
|
-
if (item.milestone)
|
|
500
|
-
attrs['data-milestone'] = item.milestone;
|
|
501
|
-
if (item.severity)
|
|
502
|
-
attrs['data-severity'] = item.severity;
|
|
503
|
-
if (item.hasUnresolvedBlockers)
|
|
504
|
-
attrs['data-has-blockers'] = 'true';
|
|
505
|
-
const linkChildren = [item.label];
|
|
506
|
-
if (item.hasUnresolvedBlockers) {
|
|
507
|
-
linkChildren.push({
|
|
508
|
-
$$mdtype: 'Tag',
|
|
509
|
-
name: 'span',
|
|
510
|
-
attributes: { class: 'rf-plan-sidebar__blocker-icon', 'aria-label': 'Has unresolved blockers' },
|
|
511
|
-
children: ['\u26A0'],
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
itemNodes.push({
|
|
515
|
-
$$mdtype: 'Tag',
|
|
516
|
-
name: 'a',
|
|
517
|
-
attributes: attrs,
|
|
518
|
-
children: linkChildren,
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
subGroupChildren.push({
|
|
522
|
-
$$mdtype: 'Tag',
|
|
523
|
-
name: 'div',
|
|
524
|
-
attributes: {
|
|
525
|
-
class: 'rf-plan-sidebar__status-items',
|
|
526
|
-
...(sg.collapsed ? { hidden: '' } : {}),
|
|
527
|
-
},
|
|
528
|
-
children: itemNodes,
|
|
529
|
-
});
|
|
530
|
-
groupChildren.push({
|
|
531
|
-
$$mdtype: 'Tag',
|
|
532
|
-
name: 'div',
|
|
533
|
-
attributes: {
|
|
534
|
-
class: 'rf-plan-sidebar__status-group',
|
|
535
|
-
'data-status': sg.status,
|
|
536
|
-
},
|
|
537
|
-
children: subGroupChildren,
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
children.push({
|
|
542
|
-
$$mdtype: 'Tag',
|
|
543
|
-
name: 'div',
|
|
544
|
-
attributes: { class: 'rf-plan-sidebar__group', 'data-type': group.type },
|
|
545
|
-
children: groupChildren,
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
// Views section
|
|
549
|
-
if (viewDefs && viewDefs.length > 0) {
|
|
550
|
-
const viewChildren = [];
|
|
551
|
-
viewChildren.push({
|
|
552
|
-
$$mdtype: 'Tag',
|
|
553
|
-
name: 'div',
|
|
554
|
-
attributes: { class: 'rf-plan-sidebar__group-title' },
|
|
555
|
-
children: ['Views'],
|
|
556
|
-
});
|
|
557
|
-
// Group view defs by field type
|
|
558
|
-
const VIEW_FIELD_LABELS = { tag: 'By Tag', assignee: 'By Assignee', milestone: 'By Milestone' };
|
|
559
|
-
const byField = new Map();
|
|
560
|
-
for (const v of viewDefs) {
|
|
561
|
-
if (!byField.has(v.field))
|
|
562
|
-
byField.set(v.field, []);
|
|
563
|
-
byField.get(v.field).push(v);
|
|
564
|
-
}
|
|
565
|
-
for (const [field, defs] of byField) {
|
|
566
|
-
const subChildren = [];
|
|
567
|
-
subChildren.push({
|
|
568
|
-
$$mdtype: 'Tag',
|
|
569
|
-
name: 'div',
|
|
570
|
-
attributes: { class: 'rf-plan-sidebar__view-header' },
|
|
571
|
-
children: [VIEW_FIELD_LABELS[field] || field],
|
|
572
|
-
});
|
|
573
|
-
const linkNodes = defs.map(v => ({
|
|
574
|
-
$$mdtype: 'Tag',
|
|
575
|
-
name: 'a',
|
|
576
|
-
attributes: {
|
|
577
|
-
class: `rf-plan-sidebar__link${v.url === activeUrl ? ' rf-plan-sidebar__link--active' : ''}`,
|
|
578
|
-
href: v.url,
|
|
579
|
-
},
|
|
580
|
-
children: [
|
|
581
|
-
{
|
|
582
|
-
$$mdtype: 'Tag',
|
|
583
|
-
name: 'span',
|
|
584
|
-
attributes: { class: 'rf-plan-sidebar__view-label' },
|
|
585
|
-
children: [v.value],
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
$$mdtype: 'Tag',
|
|
589
|
-
name: 'span',
|
|
590
|
-
attributes: { class: 'rf-plan-sidebar__view-count' },
|
|
591
|
-
children: [String(v.count)],
|
|
592
|
-
},
|
|
593
|
-
],
|
|
594
|
-
}));
|
|
595
|
-
subChildren.push({
|
|
596
|
-
$$mdtype: 'Tag',
|
|
597
|
-
name: 'div',
|
|
598
|
-
attributes: { class: 'rf-plan-sidebar__view-items' },
|
|
599
|
-
children: linkNodes,
|
|
600
|
-
});
|
|
601
|
-
viewChildren.push({
|
|
602
|
-
$$mdtype: 'Tag',
|
|
603
|
-
name: 'div',
|
|
604
|
-
attributes: { class: 'rf-plan-sidebar__view-group', 'data-field': field },
|
|
605
|
-
children: subChildren,
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
children.push({
|
|
609
|
-
$$mdtype: 'Tag',
|
|
610
|
-
name: 'div',
|
|
611
|
-
attributes: { class: 'rf-plan-sidebar__group', 'data-type': 'views' },
|
|
612
|
-
children: viewChildren,
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
return children;
|
|
616
|
-
}
|
|
617
|
-
function slugify(text) {
|
|
618
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
619
|
-
}
|
|
620
|
-
// --- Dashboard generation ---
|
|
621
|
-
function generateDashboardContent(entities) {
|
|
622
|
-
const milestones = entities.filter(e => e.type === 'milestone');
|
|
623
|
-
const activeMilestones = milestones.filter(m => m.attributes.status === 'active');
|
|
624
|
-
const allMilestones = milestones.filter(m => m.attributes.status !== 'complete');
|
|
625
|
-
const hasMultipleMilestones = allMilestones.length > 1;
|
|
626
|
-
let md = '# Plan Dashboard\n\n';
|
|
627
|
-
// Progress summary
|
|
628
|
-
md += `{% plan-progress /%}\n\n`;
|
|
629
|
-
// Recent history (git-derived activity feed)
|
|
630
|
-
md += `## Recent History\n\n`;
|
|
631
|
-
md += `{% plan-history limit="15" /%}\n\n`;
|
|
632
|
-
// Active milestone — summary card only
|
|
633
|
-
if (activeMilestones.length > 0) {
|
|
634
|
-
md += `## Active Milestone\n\n`;
|
|
635
|
-
md += `{% backlog filter="status:active" show="milestone" /%}\n\n`;
|
|
636
|
-
}
|
|
637
|
-
// Blocked items callout
|
|
638
|
-
const hasBlocked = entities.some(e => (e.type === 'work' || e.type === 'bug') && e.attributes.status === 'blocked');
|
|
639
|
-
if (hasBlocked) {
|
|
640
|
-
md += `## Blocked\n\n`;
|
|
641
|
-
md += `{% backlog filter="status:blocked" sort="priority" /%}\n\n`;
|
|
642
|
-
}
|
|
643
|
-
// Per-milestone grouping or flat layout
|
|
644
|
-
if (hasMultipleMilestones) {
|
|
645
|
-
// Group work items by milestone
|
|
646
|
-
for (const ms of allMilestones) {
|
|
647
|
-
const name = ms.attributes.name;
|
|
648
|
-
md += `## ${ms.title || name}\n\n`;
|
|
649
|
-
md += `{% backlog filter="milestone:${name}" sort="priority" group="status" /%}\n\n`;
|
|
650
|
-
}
|
|
651
|
-
// Unassigned items (no milestone)
|
|
652
|
-
md += `## Unassigned\n\n`;
|
|
653
|
-
md += `{% backlog filter="milestone:" sort="priority" group="status" /%}\n\n`;
|
|
654
|
-
}
|
|
655
|
-
else {
|
|
656
|
-
md += `## In Progress\n\n`;
|
|
657
|
-
md += `{% backlog filter="status:in-progress" sort="priority" /%}\n\n`;
|
|
658
|
-
md += `## Ready for Work\n\n`;
|
|
659
|
-
md += `{% backlog filter="status:ready" sort="priority" /%}\n\n`;
|
|
660
|
-
}
|
|
661
|
-
md += `## Recent Decisions\n\n`;
|
|
662
|
-
md += `{% decision-log sort="date" /%}\n\n`;
|
|
663
|
-
return md;
|
|
664
|
-
}
|
|
665
|
-
function generateStatusFilterPages(entities, baseUrl) {
|
|
666
|
-
const pages = [];
|
|
667
|
-
// Group entities by type, then by status
|
|
668
|
-
const byType = new Map();
|
|
669
|
-
for (const entity of entities) {
|
|
670
|
-
const type = entity.type;
|
|
671
|
-
if (!byType.has(type))
|
|
672
|
-
byType.set(type, new Map());
|
|
673
|
-
const byStatus = byType.get(type);
|
|
674
|
-
const status = entity.attributes.status || 'unknown';
|
|
675
|
-
if (!byStatus.has(status))
|
|
676
|
-
byStatus.set(status, []);
|
|
677
|
-
byStatus.get(status).push(entity);
|
|
678
|
-
}
|
|
679
|
-
// Sort fields by type
|
|
680
|
-
const SORT_FIELDS = {
|
|
681
|
-
work: 'priority', bug: 'priority',
|
|
682
|
-
spec: 'id', decision: 'id', milestone: 'id',
|
|
683
|
-
};
|
|
684
|
-
// Show attribute — work/bug use "all" (to include both), others filter to their own type
|
|
685
|
-
const SHOW_ATTRS = {
|
|
686
|
-
work: ' show="all"', bug: ' show="all"',
|
|
687
|
-
spec: ' show="spec"', decision: ' show="decision"', milestone: ' show="milestone"',
|
|
688
|
-
};
|
|
689
|
-
for (const [type, byStatus] of byType) {
|
|
690
|
-
const statusOrder = STATUS_ORDER_BY_TYPE[type] ?? [];
|
|
691
|
-
const sortedStatuses = [...byStatus.keys()].sort((a, b) => {
|
|
692
|
-
const ai = statusOrder.indexOf(a);
|
|
693
|
-
const bi = statusOrder.indexOf(b);
|
|
694
|
-
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
695
|
-
});
|
|
696
|
-
for (const status of sortedStatuses) {
|
|
697
|
-
const items = byStatus.get(status);
|
|
698
|
-
const label = STATUS_LABELS_DISPLAY[status] || status;
|
|
699
|
-
const sort = SORT_FIELDS[type] || 'priority';
|
|
700
|
-
const show = SHOW_ATTRS[type] || '';
|
|
701
|
-
const slug = slugify(status);
|
|
702
|
-
pages.push({
|
|
703
|
-
url: `${baseUrl}${type}/${slug}.html`,
|
|
704
|
-
title: `${label} ${NAV_TITLES[type] || type}`,
|
|
705
|
-
type,
|
|
706
|
-
status,
|
|
707
|
-
count: items.length,
|
|
708
|
-
content: `# ${label}\n\n{% backlog filter="status:${status}" sort="${sort}"${show} /%}\n`,
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
return pages;
|
|
713
|
-
}
|
|
714
|
-
function generateViewPages(entities, baseUrl) {
|
|
715
|
-
const views = [];
|
|
716
|
-
// Collect distinct values for tags, assignees, milestones
|
|
717
|
-
const tagCounts = new Map();
|
|
718
|
-
const assigneeCounts = new Map();
|
|
719
|
-
const milestoneCounts = new Map();
|
|
720
|
-
for (const entity of entities) {
|
|
721
|
-
// Tags (comma-separated)
|
|
722
|
-
const tags = entity.attributes.tags;
|
|
723
|
-
if (tags) {
|
|
724
|
-
for (const tag of tags.split(',').map(t => t.trim()).filter(Boolean)) {
|
|
725
|
-
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
// Assignee
|
|
729
|
-
const assignee = entity.attributes.assignee;
|
|
730
|
-
if (assignee) {
|
|
731
|
-
assigneeCounts.set(assignee, (assigneeCounts.get(assignee) ?? 0) + 1);
|
|
732
|
-
}
|
|
733
|
-
// Milestone
|
|
734
|
-
if (entity.type === 'work' || entity.type === 'bug') {
|
|
735
|
-
const milestone = entity.attributes.milestone;
|
|
736
|
-
if (milestone) {
|
|
737
|
-
milestoneCounts.set(milestone, (milestoneCounts.get(milestone) ?? 0) + 1);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
// Generate tag view pages (threshold: 3+ distinct tags)
|
|
742
|
-
if (tagCounts.size >= 3) {
|
|
743
|
-
for (const [tag, count] of [...tagCounts.entries()].sort((a, b) => b[1] - a[1])) {
|
|
744
|
-
const slug = slugify(tag);
|
|
745
|
-
views.push({
|
|
746
|
-
url: `${baseUrl}view/tag/${slug}.html`,
|
|
747
|
-
title: `Tag: ${tag}`,
|
|
748
|
-
field: 'tag',
|
|
749
|
-
value: tag,
|
|
750
|
-
count,
|
|
751
|
-
content: `# Tag: ${tag}\n\nAll items tagged **${tag}** (${count}).\n\n{% backlog filter="tags:${tag}" sort="priority" group="status" /%}\n`,
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
// Generate assignee view pages (threshold: 2+ distinct assignees)
|
|
756
|
-
if (assigneeCounts.size >= 2) {
|
|
757
|
-
for (const [assignee, count] of [...assigneeCounts.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
758
|
-
const slug = slugify(assignee);
|
|
759
|
-
views.push({
|
|
760
|
-
url: `${baseUrl}view/assignee/${slug}.html`,
|
|
761
|
-
title: `Assignee: ${assignee}`,
|
|
762
|
-
field: 'assignee',
|
|
763
|
-
value: assignee,
|
|
764
|
-
count,
|
|
765
|
-
content: `# Assignee: ${assignee}\n\nAll items assigned to **${assignee}** (${count}).\n\n{% backlog filter="assignee:${assignee}" sort="priority" group="status" /%}\n`,
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
// Generate milestone view pages (threshold: 2+ milestones)
|
|
770
|
-
if (milestoneCounts.size >= 2) {
|
|
771
|
-
for (const [milestone, count] of [...milestoneCounts.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
772
|
-
const slug = slugify(milestone);
|
|
773
|
-
views.push({
|
|
774
|
-
url: `${baseUrl}view/milestone/${slug}.html`,
|
|
775
|
-
title: `Milestone: ${milestone}`,
|
|
776
|
-
field: 'milestone',
|
|
777
|
-
value: milestone,
|
|
778
|
-
count,
|
|
779
|
-
content: `# Milestone: ${milestone}\n\nAll items in milestone **${milestone}** (${count}).\n\n{% backlog filter="milestone:${milestone}" sort="priority" group="status" /%}\n`,
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
return views;
|
|
784
|
-
}
|
|
785
|
-
const SIDEBAR_BEHAVIOR_SCRIPT = `<script>
|
|
786
|
-
(function() {
|
|
787
|
-
var STORAGE_KEY = 'plan-sidebar-collapse';
|
|
788
|
-
|
|
789
|
-
// --- Collapse toggling ---
|
|
790
|
-
function loadState() {
|
|
791
|
-
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch(e) { return {}; }
|
|
792
|
-
}
|
|
793
|
-
function saveState(state) {
|
|
794
|
-
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch(e) {}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
var state = loadState();
|
|
798
|
-
var headers = document.querySelectorAll('.rf-plan-sidebar__status-header');
|
|
799
|
-
|
|
800
|
-
headers.forEach(function(btn) {
|
|
801
|
-
var group = btn.closest('.rf-plan-sidebar__status-group');
|
|
802
|
-
if (!group) return;
|
|
803
|
-
var type = group.closest('.rf-plan-sidebar__group');
|
|
804
|
-
var typeKey = type ? type.getAttribute('data-type') : '';
|
|
805
|
-
var status = group.getAttribute('data-status');
|
|
806
|
-
var key = typeKey + ':' + status;
|
|
807
|
-
var items = group.querySelector('.rf-plan-sidebar__status-items');
|
|
808
|
-
if (!items) return;
|
|
809
|
-
|
|
810
|
-
// Restore saved state (overrides server default)
|
|
811
|
-
if (key in state) {
|
|
812
|
-
if (state[key]) {
|
|
813
|
-
items.setAttribute('hidden', '');
|
|
814
|
-
btn.setAttribute('aria-expanded', 'false');
|
|
815
|
-
} else {
|
|
816
|
-
items.removeAttribute('hidden');
|
|
817
|
-
btn.setAttribute('aria-expanded', 'true');
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
btn.addEventListener('click', function() {
|
|
822
|
-
var isHidden = items.hasAttribute('hidden');
|
|
823
|
-
if (isHidden) {
|
|
824
|
-
items.removeAttribute('hidden');
|
|
825
|
-
btn.setAttribute('aria-expanded', 'true');
|
|
826
|
-
state[key] = false;
|
|
827
|
-
} else {
|
|
828
|
-
items.setAttribute('hidden', '');
|
|
829
|
-
btn.setAttribute('aria-expanded', 'false');
|
|
830
|
-
state[key] = true;
|
|
831
|
-
}
|
|
832
|
-
saveState(state);
|
|
833
|
-
});
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
// --- Keyboard navigation ---
|
|
837
|
-
var focusIndex = -1;
|
|
838
|
-
|
|
839
|
-
function getVisibleLinks() {
|
|
840
|
-
return Array.from(document.querySelectorAll('.rf-plan-sidebar__link')).filter(function(el) {
|
|
841
|
-
return el.offsetParent !== null && el.style.display !== 'none' && !el.closest('[hidden]');
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function getGroupHeaders() {
|
|
846
|
-
return Array.from(document.querySelectorAll('.rf-plan-sidebar__status-header')).filter(function(el) {
|
|
847
|
-
return el.offsetParent !== null;
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function setFocus(links, idx) {
|
|
852
|
-
// Clear previous
|
|
853
|
-
var prev = document.querySelector('.rf-plan-sidebar__link--focused');
|
|
854
|
-
if (prev) prev.classList.remove('rf-plan-sidebar__link--focused');
|
|
855
|
-
focusIndex = idx;
|
|
856
|
-
if (idx >= 0 && idx < links.length) {
|
|
857
|
-
links[idx].classList.add('rf-plan-sidebar__link--focused');
|
|
858
|
-
links[idx].scrollIntoView({ block: 'nearest' });
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
document.addEventListener('keydown', function(e) {
|
|
863
|
-
// Don't intercept when typing in inputs (except Escape handled above)
|
|
864
|
-
var tag = document.activeElement.tagName;
|
|
865
|
-
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
|
866
|
-
|
|
867
|
-
var links = getVisibleLinks();
|
|
868
|
-
|
|
869
|
-
if (e.key === 'j') {
|
|
870
|
-
e.preventDefault();
|
|
871
|
-
var next = focusIndex + 1;
|
|
872
|
-
if (next >= links.length) next = 0;
|
|
873
|
-
setFocus(links, next);
|
|
874
|
-
} else if (e.key === 'k') {
|
|
875
|
-
e.preventDefault();
|
|
876
|
-
var prev = focusIndex - 1;
|
|
877
|
-
if (prev < 0) prev = links.length - 1;
|
|
878
|
-
setFocus(links, prev);
|
|
879
|
-
} else if (e.key === 'Enter' && focusIndex >= 0 && focusIndex < links.length) {
|
|
880
|
-
e.preventDefault();
|
|
881
|
-
links[focusIndex].click();
|
|
882
|
-
} else if (e.key === ']') {
|
|
883
|
-
e.preventDefault();
|
|
884
|
-
var headers = getGroupHeaders();
|
|
885
|
-
if (headers.length === 0) return;
|
|
886
|
-
// Find current group
|
|
887
|
-
var currentLink = focusIndex >= 0 && focusIndex < links.length ? links[focusIndex] : null;
|
|
888
|
-
var currentGroup = currentLink ? currentLink.closest('.rf-plan-sidebar__status-group') : null;
|
|
889
|
-
var currentHeader = currentGroup ? currentGroup.querySelector('.rf-plan-sidebar__status-header') : null;
|
|
890
|
-
var hi = currentHeader ? headers.indexOf(currentHeader) : -1;
|
|
891
|
-
var nextHi = hi + 1 < headers.length ? hi + 1 : 0;
|
|
892
|
-
// Find first visible link in that group
|
|
893
|
-
var targetGroup = headers[nextHi].closest('.rf-plan-sidebar__status-group');
|
|
894
|
-
var targetItems = targetGroup ? targetGroup.querySelector('.rf-plan-sidebar__status-items') : null;
|
|
895
|
-
if (targetItems && targetItems.hasAttribute('hidden')) {
|
|
896
|
-
// Expand the group first
|
|
897
|
-
targetItems.removeAttribute('hidden');
|
|
898
|
-
headers[nextHi].setAttribute('aria-expanded', 'true');
|
|
899
|
-
}
|
|
900
|
-
var firstLink = targetGroup ? targetGroup.querySelector('.rf-plan-sidebar__link') : null;
|
|
901
|
-
if (firstLink) {
|
|
902
|
-
var li = links.indexOf(firstLink);
|
|
903
|
-
if (li === -1) { links = getVisibleLinks(); li = links.indexOf(firstLink); }
|
|
904
|
-
setFocus(links, li >= 0 ? li : 0);
|
|
905
|
-
}
|
|
906
|
-
} else if (e.key === '[') {
|
|
907
|
-
e.preventDefault();
|
|
908
|
-
var headers2 = getGroupHeaders();
|
|
909
|
-
if (headers2.length === 0) return;
|
|
910
|
-
var currentLink2 = focusIndex >= 0 && focusIndex < links.length ? links[focusIndex] : null;
|
|
911
|
-
var currentGroup2 = currentLink2 ? currentLink2.closest('.rf-plan-sidebar__status-group') : null;
|
|
912
|
-
var currentHeader2 = currentGroup2 ? currentGroup2.querySelector('.rf-plan-sidebar__status-header') : null;
|
|
913
|
-
var hi2 = currentHeader2 ? headers2.indexOf(currentHeader2) : headers2.length;
|
|
914
|
-
var prevHi = hi2 - 1 >= 0 ? hi2 - 1 : headers2.length - 1;
|
|
915
|
-
var targetGroup2 = headers2[prevHi].closest('.rf-plan-sidebar__status-group');
|
|
916
|
-
var targetItems2 = targetGroup2 ? targetGroup2.querySelector('.rf-plan-sidebar__status-items') : null;
|
|
917
|
-
if (targetItems2 && targetItems2.hasAttribute('hidden')) {
|
|
918
|
-
targetItems2.removeAttribute('hidden');
|
|
919
|
-
headers2[prevHi].setAttribute('aria-expanded', 'true');
|
|
920
|
-
}
|
|
921
|
-
var firstLink2 = targetGroup2 ? targetGroup2.querySelector('.rf-plan-sidebar__link') : null;
|
|
922
|
-
if (firstLink2) {
|
|
923
|
-
var li2 = links.indexOf(firstLink2);
|
|
924
|
-
if (li2 === -1) { links = getVisibleLinks(); li2 = links.indexOf(firstLink2); }
|
|
925
|
-
setFocus(links, li2 >= 0 ? li2 : 0);
|
|
926
|
-
}
|
|
927
|
-
} else if (e.key === 'o') {
|
|
928
|
-
e.preventDefault();
|
|
929
|
-
var currentLink3 = focusIndex >= 0 && focusIndex < links.length ? links[focusIndex] : null;
|
|
930
|
-
var sg = currentLink3 ? currentLink3.closest('.rf-plan-sidebar__status-group') : null;
|
|
931
|
-
if (!sg) return;
|
|
932
|
-
var btn = sg.querySelector('.rf-plan-sidebar__status-header');
|
|
933
|
-
if (btn) btn.click();
|
|
934
|
-
}
|
|
935
|
-
});
|
|
936
|
-
})();
|
|
937
|
-
</script>`;
|
|
938
|
-
// --- HtmlTheme for the plan site ---
|
|
939
|
-
const planTheme = {
|
|
940
|
-
manifest: {
|
|
941
|
-
name: 'plan',
|
|
942
|
-
version: '0.0.0',
|
|
943
|
-
target: 'html',
|
|
944
|
-
designTokens: '',
|
|
945
|
-
layouts: {},
|
|
946
|
-
components: {},
|
|
947
|
-
routeRules: [{ pattern: '**', layout: 'plan' }],
|
|
948
|
-
},
|
|
949
|
-
layouts: {
|
|
950
|
-
plan: planLayout,
|
|
951
|
-
},
|
|
952
|
-
};
|
|
953
|
-
// --- Main pipeline ---
|
|
954
|
-
export async function runPipeline(options) {
|
|
955
|
-
const { dir, specsDir, theme, baseUrl } = options;
|
|
956
|
-
const ctx = {
|
|
957
|
-
info: () => { },
|
|
958
|
-
warn: (msg) => { console.warn(`[plan] warn: ${msg}`); },
|
|
959
|
-
error: (msg) => { console.error(`[plan] error: ${msg}`); },
|
|
960
|
-
};
|
|
961
|
-
// 1. Scan plan files
|
|
962
|
-
const entities = scanPlanFiles(dir);
|
|
963
|
-
let specsEntities = [];
|
|
964
|
-
if (specsDir && specsDir !== dir && fs.existsSync(specsDir)) {
|
|
965
|
-
specsEntities = scanPlanFiles(specsDir);
|
|
966
|
-
}
|
|
967
|
-
const allEntities = [...entities, ...specsEntities];
|
|
968
|
-
// Batch-collect git timestamps for the plan directory (and specs dir if separate)
|
|
969
|
-
const gitTimestamps = getGitTimestamps(dir);
|
|
970
|
-
const specsGitTimestamps = specsDir && specsDir !== dir ? getGitTimestamps(specsDir) : gitTimestamps;
|
|
971
|
-
// 2. Parse and transform each entity file
|
|
972
|
-
const transformedPages = [];
|
|
973
|
-
const pageMap = new Map();
|
|
974
|
-
for (const entity of allEntities) {
|
|
975
|
-
const filePath = path.resolve(dir, entity.file);
|
|
976
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
977
|
-
const url = `${baseUrl}${entity.type}/${slugify(entity.attributes.id || entity.attributes.name || '')}.html`;
|
|
978
|
-
const tsMap = specsEntities.includes(entity) ? specsGitTimestamps : gitTimestamps;
|
|
979
|
-
const ts = tsMap.get(entity.file) ?? getStatTimestamps(filePath);
|
|
980
|
-
const { renderable, title, headings } = parseAndTransform(content, entity.file, ts);
|
|
981
|
-
const page = {
|
|
982
|
-
url,
|
|
983
|
-
title: title || entity.title || '',
|
|
984
|
-
headings,
|
|
985
|
-
frontmatter: {},
|
|
986
|
-
renderable,
|
|
987
|
-
};
|
|
988
|
-
transformedPages.push(page);
|
|
989
|
-
pageMap.set(url, { entity, page });
|
|
990
|
-
}
|
|
991
|
-
// 3. Check for user dashboard or generate one
|
|
992
|
-
const indexPath = path.join(dir, 'index.md');
|
|
993
|
-
const hasIndex = fs.existsSync(indexPath);
|
|
994
|
-
let dashboardContent;
|
|
995
|
-
if (hasIndex) {
|
|
996
|
-
dashboardContent = fs.readFileSync(indexPath, 'utf-8');
|
|
997
|
-
}
|
|
998
|
-
else {
|
|
999
|
-
dashboardContent = generateDashboardContent(allEntities);
|
|
1000
|
-
}
|
|
1001
|
-
const dashboardUrl = `${baseUrl}index.html`;
|
|
1002
|
-
const { renderable: dashRenderable, title: dashTitle } = parseAndTransform(dashboardContent, 'index.md');
|
|
1003
|
-
const dashboardPage = {
|
|
1004
|
-
url: dashboardUrl,
|
|
1005
|
-
title: dashTitle || 'Plan Dashboard',
|
|
1006
|
-
headings: [],
|
|
1007
|
-
frontmatter: {},
|
|
1008
|
-
renderable: dashRenderable,
|
|
1009
|
-
};
|
|
1010
|
-
transformedPages.push(dashboardPage);
|
|
1011
|
-
// 3b. Generate status filter pages
|
|
1012
|
-
const statusFilterPages = generateStatusFilterPages(allEntities, baseUrl);
|
|
1013
|
-
const statusFilterMap = new Map(statusFilterPages.map(p => [p.url, p]));
|
|
1014
|
-
for (const sfp of statusFilterPages) {
|
|
1015
|
-
const { renderable: sfRenderable, title: sfTitle, headings: sfHeadings } = parseAndTransform(sfp.content, `${sfp.type}/${slugify(sfp.status)}.md`);
|
|
1016
|
-
const sfPage = {
|
|
1017
|
-
url: sfp.url,
|
|
1018
|
-
title: sfTitle || sfp.title,
|
|
1019
|
-
headings: sfHeadings,
|
|
1020
|
-
frontmatter: {},
|
|
1021
|
-
renderable: sfRenderable,
|
|
1022
|
-
};
|
|
1023
|
-
transformedPages.push(sfPage);
|
|
1024
|
-
}
|
|
1025
|
-
// 3c. Generate view pages (by tag, assignee, milestone)
|
|
1026
|
-
const viewDefs = generateViewPages(allEntities, baseUrl);
|
|
1027
|
-
const viewDefMap = new Map(viewDefs.map(v => [v.url, v]));
|
|
1028
|
-
for (const viewDef of viewDefs) {
|
|
1029
|
-
const { renderable: viewRenderable, title: viewTitle, headings: viewHeadings } = parseAndTransform(viewDef.content, `view/${viewDef.field}/${slugify(viewDef.value)}.md`);
|
|
1030
|
-
const viewPage = {
|
|
1031
|
-
url: viewDef.url,
|
|
1032
|
-
title: viewTitle || viewDef.title,
|
|
1033
|
-
headings: viewHeadings,
|
|
1034
|
-
frontmatter: {},
|
|
1035
|
-
renderable: viewRenderable,
|
|
1036
|
-
};
|
|
1037
|
-
transformedPages.push(viewPage);
|
|
1038
|
-
}
|
|
1039
|
-
// 4. Run pipeline hooks: register → aggregate → postProcess
|
|
1040
|
-
// Pass scanner dependency data (typed `Blocked by` / `Blocks` edges,
|
|
1041
|
-
// normalised to "A blocked by B") to the pipeline (SPEC-114).
|
|
1042
|
-
const depMap = buildBlockedByAdjacency(allEntities);
|
|
1043
|
-
setScannerDependencies(depMap);
|
|
1044
|
-
setPlanDir(dir);
|
|
1045
|
-
const { registry } = createRegistry();
|
|
1046
|
-
planPipelineHooks.register(transformedPages, registry, ctx);
|
|
1047
|
-
const aggregated = {};
|
|
1048
|
-
const planAggregated = planPipelineHooks.aggregate(registry, ctx);
|
|
1049
|
-
// Inject mtime into entity data for activity tracking
|
|
1050
|
-
const mtimeMap = new Map();
|
|
1051
|
-
for (const entity of allEntities) {
|
|
1052
|
-
const entityId = entity.attributes.id || entity.attributes.name;
|
|
1053
|
-
if (entityId && entity.mtime) {
|
|
1054
|
-
mtimeMap.set(entityId, entity.mtime);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
for (const key of ['workEntities', 'bugEntities', 'decisionEntities', 'specEntities']) {
|
|
1058
|
-
const entities = planAggregated[key];
|
|
1059
|
-
if (!entities)
|
|
1060
|
-
continue;
|
|
1061
|
-
for (const e of entities) {
|
|
1062
|
-
const mtime = mtimeMap.get(e.id);
|
|
1063
|
-
if (mtime)
|
|
1064
|
-
e.data.mtime = mtime;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
aggregated['plan'] = planAggregated;
|
|
1068
|
-
const postProcessedPages = transformedPages.map(p => planPipelineHooks.postProcess ? planPipelineHooks.postProcess(p, aggregated, ctx) : p);
|
|
1069
|
-
// 4b. Resolve `collection` sentinels — including the `backlog` /
|
|
1070
|
-
// `decision-log` / `plan-activity` runes that lower to collections
|
|
1071
|
-
// (WORK-284). The bespoke render-pipeline doesn't run the core
|
|
1072
|
-
// cross-page pipeline, so we invoke the resolver directly. Orderings
|
|
1073
|
-
// mirror the plan plugin's `theme.orderings` (the actionable-first
|
|
1074
|
-
// status override) so groups land in the same order they would in
|
|
1075
|
-
// a standard site build.
|
|
1076
|
-
const embedConfig = {
|
|
1077
|
-
tags: tags,
|
|
1078
|
-
nodes: nodes,
|
|
1079
|
-
orderings: {
|
|
1080
|
-
work: { status: [...WORK_STATUS_DISPLAY_ORDER] },
|
|
1081
|
-
bug: { status: [...BUG_STATUS_DISPLAY_ORDER] },
|
|
1082
|
-
},
|
|
1083
|
-
};
|
|
1084
|
-
const collectionResolvedPages = postProcessedPages.map(p => {
|
|
1085
|
-
const resolved = resolveCollections(p.renderable, p.url, registry, embedConfig, ctx);
|
|
1086
|
-
if (resolved === p.renderable)
|
|
1087
|
-
return p;
|
|
1088
|
-
return { ...p, renderable: resolved };
|
|
1089
|
-
});
|
|
1090
|
-
// 4c. Resolve xref placeholders into clickable links. The plan CLI render
|
|
1091
|
-
// path doesn't currently consume `refrakt.config.json#/xrefs`, so patterns
|
|
1092
|
-
// are empty here — unresolved refs render as `rf-xref--unresolved`. If CLI
|
|
1093
|
-
// consumers later want pattern fallback, a future change can thread the
|
|
1094
|
-
// compiled patterns through.
|
|
1095
|
-
const processedPages = collectionResolvedPages.map(p => {
|
|
1096
|
-
const resolved = resolveXrefs(p.renderable, p.url, registry, [], ctx);
|
|
1097
|
-
if (resolved === p.renderable)
|
|
1098
|
-
return p;
|
|
1099
|
-
return { ...p, renderable: resolved };
|
|
1100
|
-
});
|
|
1101
|
-
// 5. Identity transform + syntax highlighting
|
|
1102
|
-
const themeConfig = buildThemeConfig();
|
|
1103
|
-
const identityTransform = createTransform(themeConfig);
|
|
1104
|
-
const themeCss = resolveThemeCss(theme);
|
|
1105
|
-
const nav = buildNavigation(allEntities, baseUrl);
|
|
1106
|
-
// Mark nav items with unresolved blockers
|
|
1107
|
-
const typedPlanData = planAggregated;
|
|
1108
|
-
const planRels = typedPlanData.relationships;
|
|
1109
|
-
if (planRels) {
|
|
1110
|
-
const RESOLVED_STATUSES = TERMINAL_STATUS_UNION;
|
|
1111
|
-
for (const group of nav) {
|
|
1112
|
-
for (const sg of group.statusGroups) {
|
|
1113
|
-
for (const item of sg.items) {
|
|
1114
|
-
const rels = planRels.get(item.id);
|
|
1115
|
-
if (!rels)
|
|
1116
|
-
continue;
|
|
1117
|
-
const blockedBy = rels.filter(r => r.kind === 'blocked-by' || r.kind === 'depends-on');
|
|
1118
|
-
if (blockedBy.length === 0)
|
|
1119
|
-
continue;
|
|
1120
|
-
// Check if any blocker is unresolved
|
|
1121
|
-
const hasUnresolved = blockedBy.some(r => {
|
|
1122
|
-
const allArrays = [
|
|
1123
|
-
typedPlanData.workEntities,
|
|
1124
|
-
typedPlanData.bugEntities,
|
|
1125
|
-
typedPlanData.decisionEntities,
|
|
1126
|
-
typedPlanData.specEntities,
|
|
1127
|
-
];
|
|
1128
|
-
for (const arr of allArrays) {
|
|
1129
|
-
const target = arr.find(e => e.id === r.toId);
|
|
1130
|
-
if (target)
|
|
1131
|
-
return !RESOLVED_STATUSES.has(String(target.data.status ?? ''));
|
|
1132
|
-
}
|
|
1133
|
-
return true; // Unknown target — treat as unresolved
|
|
1134
|
-
});
|
|
1135
|
-
if (hasUnresolved)
|
|
1136
|
-
item.hasUnresolvedBlockers = true;
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
// Initialize syntax highlighting
|
|
1142
|
-
let highlightTransform = null;
|
|
1143
|
-
let highlightCss = '';
|
|
1144
|
-
try {
|
|
1145
|
-
highlightTransform = await createHighlightTransform();
|
|
1146
|
-
highlightCss = highlightTransform.css;
|
|
1147
|
-
}
|
|
1148
|
-
catch {
|
|
1149
|
-
// Highlight not available — render without syntax highlighting
|
|
1150
|
-
}
|
|
1151
|
-
const pages = [];
|
|
1152
|
-
let dashboardProcessed;
|
|
1153
|
-
for (const page of processedPages) {
|
|
1154
|
-
const serialized = serializeTree(page.renderable);
|
|
1155
|
-
let transformed = identityTransform(serialized);
|
|
1156
|
-
// Apply syntax highlighting
|
|
1157
|
-
if (highlightTransform) {
|
|
1158
|
-
transformed = highlightTransform(transformed);
|
|
1159
|
-
}
|
|
1160
|
-
const mapEntry = pageMap.get(page.url);
|
|
1161
|
-
const viewDef = viewDefMap.get(page.url);
|
|
1162
|
-
const statusFilterDef = statusFilterMap.get(page.url);
|
|
1163
|
-
const processed = {
|
|
1164
|
-
url: page.url,
|
|
1165
|
-
title: page.title,
|
|
1166
|
-
type: mapEntry?.entity.type ?? (statusFilterDef ? statusFilterDef.type : viewDef ? 'view' : 'dashboard'),
|
|
1167
|
-
entityId: mapEntry?.entity.attributes.id || mapEntry?.entity.attributes.name || '',
|
|
1168
|
-
status: mapEntry?.entity.attributes.status || (statusFilterDef?.status ?? ''),
|
|
1169
|
-
renderable: transformed,
|
|
1170
|
-
filePath: mapEntry?.entity.file ?? (statusFilterDef ? `${statusFilterDef.type}/${slugify(statusFilterDef.status)}.md` : viewDef ? `view/${viewDef.field}/${slugify(viewDef.value)}.md` : 'index.md'),
|
|
1171
|
-
headings: page.headings ?? [],
|
|
1172
|
-
};
|
|
1173
|
-
if (page.url === dashboardUrl) {
|
|
1174
|
-
dashboardProcessed = processed;
|
|
1175
|
-
}
|
|
1176
|
-
else {
|
|
1177
|
-
pages.push(processed);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
return {
|
|
1181
|
-
pages,
|
|
1182
|
-
dashboard: dashboardProcessed,
|
|
1183
|
-
nav,
|
|
1184
|
-
navRegion: buildNavRegion(nav, baseUrl, undefined, viewDefs, statusFilterPages),
|
|
1185
|
-
themeCss,
|
|
1186
|
-
highlightCss,
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
/**
|
|
1190
|
-
* Render a processed page to a full HTML document using the HTML adapter.
|
|
1191
|
-
*/
|
|
1192
|
-
export function renderPage(page, navRegion, allPageUrls, opts) {
|
|
1193
|
-
const layoutPageData = {
|
|
1194
|
-
renderable: page.renderable,
|
|
1195
|
-
regions: {
|
|
1196
|
-
nav: {
|
|
1197
|
-
name: 'nav',
|
|
1198
|
-
mode: 'replace',
|
|
1199
|
-
content: navRegion,
|
|
1200
|
-
},
|
|
1201
|
-
},
|
|
1202
|
-
title: page.title,
|
|
1203
|
-
url: page.url,
|
|
1204
|
-
pages: allPageUrls,
|
|
1205
|
-
frontmatter: {
|
|
1206
|
-
toolbarTitle: page.entityId || 'Plan',
|
|
1207
|
-
},
|
|
1208
|
-
headings: page.headings,
|
|
1209
|
-
};
|
|
1210
|
-
const shellOptions = {
|
|
1211
|
-
stylesheets: opts?.stylesheets ?? [],
|
|
1212
|
-
scripts: opts?.scripts ?? [],
|
|
1213
|
-
bodyExtra: SIDEBAR_BEHAVIOR_SCRIPT,
|
|
1214
|
-
};
|
|
1215
|
-
// Hot reload SSE script
|
|
1216
|
-
if (opts?.hotReload) {
|
|
1217
|
-
const hotReloadScript = `<script>(function(){var es=new EventSource('/__plan-reload');es.onmessage=function(e){if(e.data==='reload')location.reload();};window.addEventListener('beforeunload',function(){es.close();});})()</script>`;
|
|
1218
|
-
shellOptions.headExtra = hotReloadScript;
|
|
1219
|
-
}
|
|
1220
|
-
return htmlRenderFullPage({ theme: planTheme, page: layoutPageData }, shellOptions);
|
|
1221
|
-
}
|
|
1222
|
-
export function getThemeCss(theme) {
|
|
1223
|
-
return resolveThemeCss(theme);
|
|
1224
|
-
}
|
|
1225
|
-
//# sourceMappingURL=render-pipeline.js.map
|