@shivaedev/work-board 0.2.0 → 0.3.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.
- package/CHANGELOG.md +86 -0
- package/README.md +141 -3
- package/dist/board.d.ts +9 -3
- package/dist/board.d.ts.map +1 -1
- package/dist/board.js +17 -9
- package/dist/board.js.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/files/changes.d.ts +4 -3
- package/dist/files/changes.d.ts.map +1 -1
- package/dist/files/changes.js +2 -2
- package/dist/files/changes.js.map +1 -1
- package/dist/files/list.d.ts +1 -1
- package/dist/files/list.d.ts.map +1 -1
- package/dist/files/list.js +1 -1
- package/dist/files/list.js.map +1 -1
- package/dist/files/url.d.ts +2 -0
- package/dist/files/url.d.ts.map +1 -0
- package/dist/files/url.js +4 -0
- package/dist/files/url.js.map +1 -0
- package/dist/http/assets.d.ts.map +1 -1
- package/dist/http/assets.js +21 -6
- package/dist/http/assets.js.map +1 -1
- package/dist/http/events.d.ts +1 -1
- package/dist/http/events.d.ts.map +1 -1
- package/dist/http/events.js +1 -1
- package/dist/http/events.js.map +1 -1
- package/dist/http/identity.d.ts +7 -0
- package/dist/http/identity.d.ts.map +1 -0
- package/dist/http/identity.js +38 -0
- package/dist/http/identity.js.map +1 -0
- package/dist/http/loopback.js +2 -2
- package/dist/http/loopback.js.map +1 -1
- package/dist/http/page.d.ts +4 -3
- package/dist/http/page.d.ts.map +1 -1
- package/dist/http/page.js +37 -15
- package/dist/http/page.js.map +1 -1
- package/dist/http/search.d.ts +5 -0
- package/dist/http/search.d.ts.map +1 -0
- package/dist/http/search.js +11 -0
- package/dist/http/search.js.map +1 -0
- package/dist/http/work.d.ts +6 -0
- package/dist/http/work.d.ts.map +1 -0
- package/dist/http/work.js +22 -0
- package/dist/http/work.js.map +1 -0
- package/dist/metadata/backlinks.d.ts +8 -0
- package/dist/metadata/backlinks.d.ts.map +1 -0
- package/dist/metadata/backlinks.js +29 -0
- package/dist/metadata/backlinks.js.map +1 -0
- package/dist/metadata/fields.d.ts +29 -0
- package/dist/metadata/fields.d.ts.map +1 -0
- package/dist/metadata/fields.js +25 -0
- package/dist/metadata/fields.js.map +1 -0
- package/dist/metadata/links.d.ts +4 -0
- package/dist/metadata/links.d.ts.map +1 -0
- package/dist/metadata/links.js +18 -0
- package/dist/metadata/links.js.map +1 -0
- package/dist/metadata/locations.d.ts +3 -0
- package/dist/metadata/locations.d.ts.map +1 -0
- package/dist/metadata/locations.js +35 -0
- package/dist/metadata/locations.js.map +1 -0
- package/dist/metadata/model.d.ts +15 -0
- package/dist/metadata/model.d.ts.map +1 -0
- package/dist/metadata/model.js +13 -0
- package/dist/metadata/model.js.map +1 -0
- package/dist/metadata/parse.d.ts +16 -0
- package/dist/metadata/parse.d.ts.map +1 -0
- package/dist/metadata/parse.js +48 -0
- package/dist/metadata/parse.js.map +1 -0
- package/dist/metadata/problems.d.ts +4 -0
- package/dist/metadata/problems.d.ts.map +1 -0
- package/dist/metadata/problems.js +57 -0
- package/dist/metadata/problems.js.map +1 -0
- package/dist/metadata/relationshipLabel.d.ts +2 -0
- package/dist/metadata/relationshipLabel.d.ts.map +1 -0
- package/dist/metadata/relationshipLabel.js +13 -0
- package/dist/metadata/relationshipLabel.js.map +1 -0
- package/dist/metadata/schema.d.ts +34 -0
- package/dist/metadata/schema.d.ts.map +1 -0
- package/dist/metadata/schema.js +27 -0
- package/dist/metadata/schema.js.map +1 -0
- package/dist/metadata/sourceLinks.d.ts +5 -0
- package/dist/metadata/sourceLinks.d.ts.map +1 -0
- package/dist/metadata/sourceLinks.js +44 -0
- package/dist/metadata/sourceLinks.js.map +1 -0
- package/dist/page/backlinksHtml.d.ts +3 -0
- package/dist/page/backlinksHtml.d.ts.map +1 -0
- package/dist/page/backlinksHtml.js +16 -0
- package/dist/page/backlinksHtml.js.map +1 -0
- package/dist/page/board.d.ts +2 -2
- package/dist/page/board.d.ts.map +1 -1
- package/dist/page/board.js +16 -6
- package/dist/page/board.js.map +1 -1
- package/dist/page/client.d.ts +2 -1
- package/dist/page/client.d.ts.map +1 -1
- package/dist/page/client.js +65 -7
- package/dist/page/client.js.map +1 -1
- package/dist/page/criteriaEvidence.d.ts +4 -0
- package/dist/page/criteriaEvidence.d.ts.map +1 -0
- package/dist/page/criteriaEvidence.js +27 -0
- package/dist/page/criteriaEvidence.js.map +1 -0
- package/dist/page/diagrams.d.ts +1 -1
- package/dist/page/diagrams.d.ts.map +1 -1
- package/dist/page/diagrams.js +12 -4
- package/dist/page/diagrams.js.map +1 -1
- package/dist/page/document.d.ts +2 -2
- package/dist/page/document.d.ts.map +1 -1
- package/dist/page/document.js +7 -4
- package/dist/page/document.js.map +1 -1
- package/dist/page/escape.d.ts.map +1 -1
- package/dist/page/escape.js +2 -2
- package/dist/page/escape.js.map +1 -1
- package/dist/page/libraryScript.d.ts +2 -0
- package/dist/page/libraryScript.d.ts.map +1 -0
- package/dist/page/libraryScript.js +73 -0
- package/dist/page/libraryScript.js.map +1 -0
- package/dist/page/metadata.d.ts +5 -0
- package/dist/page/metadata.d.ts.map +1 -0
- package/dist/page/metadata.js +34 -0
- package/dist/page/metadata.js.map +1 -0
- package/dist/page/metadataDetails.d.ts +4 -0
- package/dist/page/metadataDetails.d.ts.map +1 -0
- package/dist/page/metadataDetails.js +21 -0
- package/dist/page/metadataDetails.js.map +1 -0
- package/dist/page/metadataLinks.d.ts +6 -0
- package/dist/page/metadataLinks.d.ts.map +1 -0
- package/dist/page/metadataLinks.js +31 -0
- package/dist/page/metadataLinks.js.map +1 -0
- package/dist/page/nav.d.ts +2 -2
- package/dist/page/nav.d.ts.map +1 -1
- package/dist/page/nav.js +4 -4
- package/dist/page/nav.js.map +1 -1
- package/dist/page/navigationScript.d.ts +2 -0
- package/dist/page/navigationScript.d.ts.map +1 -0
- package/dist/page/navigationScript.js +104 -0
- package/dist/page/navigationScript.js.map +1 -0
- package/dist/page/readingStateScript.d.ts +2 -0
- package/dist/page/readingStateScript.d.ts.map +1 -0
- package/dist/page/readingStateScript.js +62 -0
- package/dist/page/readingStateScript.js.map +1 -0
- package/dist/page/readingStyles.d.ts +2 -0
- package/dist/page/readingStyles.d.ts.map +1 -0
- package/dist/page/readingStyles.js +29 -0
- package/dist/page/readingStyles.js.map +1 -0
- package/dist/page/readingTools.d.ts +3 -0
- package/dist/page/readingTools.d.ts.map +1 -0
- package/dist/page/readingTools.js +10 -0
- package/dist/page/readingTools.js.map +1 -0
- package/dist/page/recordedEvidence.d.ts +4 -0
- package/dist/page/recordedEvidence.d.ts.map +1 -0
- package/dist/page/recordedEvidence.js +10 -0
- package/dist/page/recordedEvidence.js.map +1 -0
- package/dist/page/searchDialog.d.ts +3 -0
- package/dist/page/searchDialog.d.ts.map +1 -0
- package/dist/page/searchDialog.js +16 -0
- package/dist/page/searchDialog.js.map +1 -0
- package/dist/page/searchResultsScript.d.ts +2 -0
- package/dist/page/searchResultsScript.d.ts.map +1 -0
- package/dist/page/searchResultsScript.js +64 -0
- package/dist/page/searchResultsScript.js.map +1 -0
- package/dist/page/searchScript.d.ts +2 -0
- package/dist/page/searchScript.d.ts.map +1 -0
- package/dist/page/searchScript.js +73 -0
- package/dist/page/searchScript.js.map +1 -0
- package/dist/page/shell.d.ts +1 -1
- package/dist/page/shell.d.ts.map +1 -1
- package/dist/page/shell.js +23 -5
- package/dist/page/shell.js.map +1 -1
- package/dist/page/stateScript.d.ts +2 -0
- package/dist/page/stateScript.d.ts.map +1 -0
- package/dist/page/stateScript.js +49 -0
- package/dist/page/stateScript.js.map +1 -0
- package/dist/page/style.d.ts.map +1 -1
- package/dist/page/style.js +14 -34
- package/dist/page/style.js.map +1 -1
- package/dist/page/swap.d.ts +1 -1
- package/dist/page/swap.d.ts.map +1 -1
- package/dist/page/swap.js +4 -1
- package/dist/page/swap.js.map +1 -1
- package/dist/page/workspaceStyles.d.ts +3 -0
- package/dist/page/workspaceStyles.d.ts.map +1 -0
- package/dist/page/workspaceStyles.js +79 -0
- package/dist/page/workspaceStyles.js.map +1 -0
- package/dist/render/board.d.ts +8 -8
- package/dist/render/board.d.ts.map +1 -1
- package/dist/render/board.js +7 -7
- package/dist/render/board.js.map +1 -1
- package/dist/render/code.d.ts +1 -1
- package/dist/render/code.js +5 -5
- package/dist/render/code.js.map +1 -1
- package/dist/render/documentHeadings.d.ts +667 -0
- package/dist/render/documentHeadings.d.ts.map +1 -0
- package/dist/render/documentHeadings.js +45 -0
- package/dist/render/documentHeadings.js.map +1 -0
- package/dist/render/documentLinks.d.ts +659 -0
- package/dist/render/documentLinks.d.ts.map +1 -0
- package/dist/render/documentLinks.js +19 -0
- package/dist/render/documentLinks.js.map +1 -0
- package/dist/render/footnotes.d.ts +1 -1
- package/dist/render/footnotes.js +2 -2
- package/dist/render/footnotes.js.map +1 -1
- package/dist/render/highlighter.d.ts +1 -1
- package/dist/render/highlighter.js +2 -2
- package/dist/render/highlighter.js.map +1 -1
- package/dist/render/markdown.d.ts +7 -1
- package/dist/render/markdown.d.ts.map +1 -1
- package/dist/render/markdown.js +13 -2
- package/dist/render/markdown.js.map +1 -1
- package/dist/search/collector.d.ts +13 -0
- package/dist/search/collector.d.ts.map +1 -0
- package/dist/search/collector.js +40 -0
- package/dist/search/collector.js.map +1 -0
- package/dist/search/entries.d.ts +10 -0
- package/dist/search/entries.d.ts.map +1 -0
- package/dist/search/entries.js +37 -0
- package/dist/search/entries.js.map +1 -0
- package/dist/search/linkCollector.d.ts +8 -0
- package/dist/search/linkCollector.d.ts.map +1 -0
- package/dist/search/linkCollector.js +16 -0
- package/dist/search/linkCollector.js.map +1 -0
- package/dist/search/match.d.ts +14 -0
- package/dist/search/match.d.ts.map +1 -0
- package/dist/search/match.js +26 -0
- package/dist/search/match.js.map +1 -0
- package/dist/search/metadataEntries.d.ts +4 -0
- package/dist/search/metadataEntries.d.ts.map +1 -0
- package/dist/search/metadataEntries.js +40 -0
- package/dist/search/metadataEntries.js.map +1 -0
- package/dist/search/snapshot.d.ts +13 -0
- package/dist/search/snapshot.d.ts.map +1 -0
- package/dist/search/snapshot.js +50 -0
- package/dist/search/snapshot.js.map +1 -0
- package/dist/serve.d.ts +1 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +1 -1
- package/dist/serve.js.map +1 -1
- package/dist/views/items.d.ts +11 -0
- package/dist/views/items.d.ts.map +1 -0
- package/dist/views/items.js +48 -0
- package/dist/views/items.js.map +1 -0
- package/dist/views/projection.d.ts +15 -0
- package/dist/views/projection.d.ts.map +1 -0
- package/dist/views/projection.js +47 -0
- package/dist/views/projection.js.map +1 -0
- package/dist/views/savedScript.d.ts +2 -0
- package/dist/views/savedScript.d.ts.map +1 -0
- package/dist/views/savedScript.js +89 -0
- package/dist/views/savedScript.js.map +1 -0
- package/dist/views/state.d.ts +15 -0
- package/dist/views/state.d.ts.map +1 -0
- package/dist/views/state.js +45 -0
- package/dist/views/state.js.map +1 -0
- package/dist/views/tools.d.ts +3 -0
- package/dist/views/tools.d.ts.map +1 -0
- package/dist/views/tools.js +11 -0
- package/dist/views/tools.js.map +1 -0
- package/dist/views/workCards.d.ts +4 -0
- package/dist/views/workCards.d.ts.map +1 -0
- package/dist/views/workCards.js +23 -0
- package/dist/views/workCards.js.map +1 -0
- package/dist/views/workControls.d.ts +4 -0
- package/dist/views/workControls.d.ts.map +1 -0
- package/dist/views/workControls.js +32 -0
- package/dist/views/workControls.js.map +1 -0
- package/dist/views/workDetail.d.ts +13 -0
- package/dist/views/workDetail.d.ts.map +1 -0
- package/dist/views/workDetail.js +26 -0
- package/dist/views/workDetail.js.map +1 -0
- package/dist/views/workHtml.d.ts +15 -0
- package/dist/views/workHtml.d.ts.map +1 -0
- package/dist/views/workHtml.js +35 -0
- package/dist/views/workHtml.js.map +1 -0
- package/dist/views/workStyle.d.ts +2 -0
- package/dist/views/workStyle.d.ts.map +1 -0
- package/dist/views/workStyle.js +18 -0
- package/dist/views/workStyle.js.map +1 -0
- package/dist/views/workTable.d.ts +4 -0
- package/dist/views/workTable.d.ts.map +1 -0
- package/dist/views/workTable.js +19 -0
- package/dist/views/workTable.js.map +1 -0
- package/package.json +42 -33
- package/src/board.ts +25 -12
- package/src/cli.ts +3 -3
- package/src/files/changes.ts +7 -6
- package/src/files/list.ts +3 -3
- package/src/files/url.ts +3 -0
- package/src/http/assets.ts +21 -6
- package/src/http/events.ts +2 -2
- package/src/http/identity.ts +62 -0
- package/src/http/loopback.ts +2 -2
- package/src/http/page.ts +56 -19
- package/src/http/search.ts +13 -0
- package/src/http/work.ts +41 -0
- package/src/metadata/backlinks.ts +39 -0
- package/src/metadata/fields.ts +26 -0
- package/src/metadata/links.ts +19 -0
- package/src/metadata/locations.ts +34 -0
- package/src/metadata/model.ts +27 -0
- package/src/metadata/parse.ts +62 -0
- package/src/metadata/problems.ts +62 -0
- package/src/metadata/relationshipLabel.ts +13 -0
- package/src/metadata/schema.ts +36 -0
- package/src/metadata/sourceLinks.ts +46 -0
- package/src/page/backlinksHtml.ts +18 -0
- package/src/page/board.ts +16 -6
- package/src/page/client.ts +65 -7
- package/src/page/criteriaEvidence.ts +36 -0
- package/src/page/diagrams.ts +12 -4
- package/src/page/document.ts +8 -5
- package/src/page/escape.ts +2 -2
- package/src/page/libraryScript.ts +72 -0
- package/src/page/metadata.ts +40 -0
- package/src/page/metadataDetails.ts +24 -0
- package/src/page/metadataLinks.ts +35 -0
- package/src/page/nav.ts +9 -8
- package/src/page/navigationScript.ts +103 -0
- package/src/page/readingStateScript.ts +61 -0
- package/src/page/readingStyles.ts +28 -0
- package/src/page/readingTools.ts +12 -0
- package/src/page/recordedEvidence.ts +12 -0
- package/src/page/searchDialog.ts +16 -0
- package/src/page/searchResultsScript.ts +63 -0
- package/src/page/searchScript.ts +72 -0
- package/src/page/shell.ts +24 -5
- package/src/page/stateScript.ts +48 -0
- package/src/page/style.ts +14 -35
- package/src/page/swap.ts +4 -1
- package/src/page/workspaceStyles.ts +79 -0
- package/src/render/board.ts +19 -19
- package/src/render/code.ts +5 -5
- package/src/render/documentHeadings.ts +52 -0
- package/src/render/documentLinks.ts +19 -0
- package/src/render/footnotes.ts +2 -2
- package/src/render/highlighter.ts +2 -2
- package/src/render/markdown.ts +21 -4
- package/src/search/collector.ts +44 -0
- package/src/search/entries.ts +51 -0
- package/src/search/linkCollector.ts +16 -0
- package/src/search/match.ts +28 -0
- package/src/search/metadataEntries.ts +42 -0
- package/src/search/snapshot.ts +60 -0
- package/src/serve.ts +1 -1
- package/src/views/items.ts +55 -0
- package/src/views/projection.ts +59 -0
- package/src/views/savedScript.ts +88 -0
- package/src/views/state.ts +47 -0
- package/src/views/tools.ts +11 -0
- package/src/views/workCards.ts +26 -0
- package/src/views/workControls.ts +38 -0
- package/src/views/workDetail.ts +28 -0
- package/src/views/workHtml.ts +39 -0
- package/src/views/workStyle.ts +17 -0
- package/src/views/workTable.ts +21 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/src/index.ts +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { identityUrl } from "#metadata/links.ts";
|
|
2
|
+
import type { MetadataModel } from "#metadata/model.ts";
|
|
3
|
+
import type { ParsedMetadata } from "#metadata/parse.ts";
|
|
4
|
+
import { stateOf, viewUrl } from "#views/state.ts";
|
|
5
|
+
import { backlinksHtml } from "./backlinksHtml.ts";
|
|
6
|
+
import { escapeHtml } from "./escape.ts";
|
|
7
|
+
import { metadataDetails } from "./metadataDetails.ts";
|
|
8
|
+
|
|
9
|
+
export function metadataHtml(parsed: ParsedMetadata, file: string, model: MetadataModel): string {
|
|
10
|
+
const backlinks = backlinksHtml(file, model);
|
|
11
|
+
const diagnostics = model.diagnostics.get(file) ?? parsed.diagnostics;
|
|
12
|
+
if (parsed.raw === undefined && diagnostics.length === 0) {
|
|
13
|
+
return backlinks;
|
|
14
|
+
}
|
|
15
|
+
const fields = parsed.fields;
|
|
16
|
+
const unique = model.unavailable.length === 0 && fields.id !== undefined && model.ids.get(fields.id)?.length === 1;
|
|
17
|
+
const problems =
|
|
18
|
+
diagnostics.length > 0
|
|
19
|
+
? `<div class="metadata-problems" role="status"><b>Source diagnostics</b><ul>${diagnostics.map((problem) => `<li>${escapeHtml(file)}:${problem.line} — ${escapeHtml(problem.message)}</li>`).join("")}</ul></div>`
|
|
20
|
+
: "";
|
|
21
|
+
const identity =
|
|
22
|
+
unique && fields.id
|
|
23
|
+
? `<a href="${identityUrl(fields.id)}">Item link: ${escapeHtml(fields.id)}</a>`
|
|
24
|
+
: `ID: ${escapeHtml(fields.id ?? "Not recorded")}`;
|
|
25
|
+
const details = metadataDetails(parsed, file, model);
|
|
26
|
+
const boardLink =
|
|
27
|
+
unique && fields.kind === "board" && fields.id
|
|
28
|
+
? `<p><a href="${escapeHtml(viewUrl(stateOf("/"), { board: fields.id }))}">View this board</a></p>`
|
|
29
|
+
: "";
|
|
30
|
+
return `<section class="work-meta" aria-label="Recorded work"><p class="metadata-summary">Recorded work · ${identity}${fields.status ? ` · ${escapeHtml(fields.status)}` : ""}${fields.owner ? ` · ${escapeHtml(fields.owner)}` : ""}</p>${fields.nextAction ? `<p>Next action: ${escapeHtml(fields.nextAction)}</p>` : ""}${boardLink}${problems}<details data-key="work-metadata"><summary>Work details and source</summary>${details}<p>Source: ${escapeHtml(file)}:${parsed.lines.id ?? 1}; prose starts at line ${parsed.bodyLine}.</p>${parsed.raw ? `<details data-key="frontmatter-source"><summary>Original frontmatter</summary><pre>${escapeHtml(parsed.raw)}</pre></details>` : ""}</details></section>${backlinks}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const metadataStyles = `
|
|
34
|
+
.work-meta{font-size:.9rem;margin:16px 0 24px;padding:12px 16px;border:1px solid var(--border);border-radius:8px;background:var(--surface);overflow-wrap:anywhere}
|
|
35
|
+
.metadata-summary{font-weight:600;margin:0}.work-meta>p{margin-bottom:8px}.work-meta details{margin:0}.work-meta summary{color:var(--link)}
|
|
36
|
+
.work-meta dl{display:grid;grid-template-columns:minmax(80px,auto) minmax(0,1fr);gap:4px 12px;margin:12px 0}.work-meta dt{color:var(--muted)}.work-meta dd{margin:0}
|
|
37
|
+
.work-meta pre{max-height:300px;overflow:auto;margin-top:12px}.metadata-problems{color:var(--danger);margin:12px 0}.metadata-problems ul{margin:4px 0 12px;padding-left:20px}
|
|
38
|
+
.work-context{font-size:.9rem;margin:12px 0 24px;padding:12px 16px;border:1px solid var(--border);border-radius:8px;overflow-wrap:anywhere}.work-context details{margin:0}.work-context ul{margin:12px 0}.work-context p{font-size:13px;color:var(--muted);margin:8px 0 0}
|
|
39
|
+
[id^="recorded-evidence-"],[id^="criterion-"]{scroll-margin-top:20px}
|
|
40
|
+
`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MetadataModel } from "#metadata/model.ts";
|
|
2
|
+
import type { ParsedMetadata } from "#metadata/parse.ts";
|
|
3
|
+
import { relationshipLabel } from "#metadata/relationshipLabel.ts";
|
|
4
|
+
import { criteriaEvidence } from "./criteriaEvidence.ts";
|
|
5
|
+
import { escapeHtml } from "./escape.ts";
|
|
6
|
+
import { referenceHtml, valueHtml } from "./metadataLinks.ts";
|
|
7
|
+
import { recordedEvidence } from "./recordedEvidence.ts";
|
|
8
|
+
|
|
9
|
+
export function metadataDetails(parsed: ParsedMetadata, file: string, model: MetadataModel): string {
|
|
10
|
+
const fields = parsed.fields;
|
|
11
|
+
const details = [
|
|
12
|
+
`<dl><dt>Kind</dt><dd>${valueHtml(fields.kind)}</dd><dt>Status</dt><dd>${valueHtml(fields.status)}</dd><dt>Owner</dt><dd>${valueHtml(fields.owner)}</dd><dt>Next action</dt><dd>${valueHtml(fields.nextAction)}</dd></dl>`,
|
|
13
|
+
fields.relationships?.length
|
|
14
|
+
? `<h3>Relationships</h3><ul>${fields.relationships.map((link) => `<li>${escapeHtml(relationshipLabel(link.kind))}: ${referenceHtml(link.target, link.target, model)}</li>`).join("")}</ul>`
|
|
15
|
+
: "",
|
|
16
|
+
fields.items?.length
|
|
17
|
+
? `<h3>Declared board membership</h3><ul>${fields.items.map((id) => `<li>${referenceHtml(id, id, model)}</li>`).join("")}</ul>`
|
|
18
|
+
: "",
|
|
19
|
+
|
|
20
|
+
criteriaEvidence(parsed, model),
|
|
21
|
+
recordedEvidence(parsed, file, model),
|
|
22
|
+
].join("");
|
|
23
|
+
return details;
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { fileUrl } from "#files/url.ts";
|
|
2
|
+
import { referenceTarget } from "#metadata/links.ts";
|
|
3
|
+
import type { MetadataModel } from "#metadata/model.ts";
|
|
4
|
+
import { sourceFile, sourceHref } from "#metadata/sourceLinks.ts";
|
|
5
|
+
import { escapeHtml } from "./escape.ts";
|
|
6
|
+
|
|
7
|
+
export function referenceHtml(label: string, target: string, model: MetadataModel): string {
|
|
8
|
+
const href = referenceTarget(target, model);
|
|
9
|
+
return href
|
|
10
|
+
? `<a href="${escapeHtml(href)}">${escapeHtml(label)}</a>`
|
|
11
|
+
: `<span>${escapeHtml(label)} (unresolved, ambiguous, or unavailable)</span>`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function sourceHtml(source: string, file: string, model: MetadataModel): string {
|
|
15
|
+
const href = sourceHref(source, file);
|
|
16
|
+
if (!href) {
|
|
17
|
+
return escapeHtml(source);
|
|
18
|
+
}
|
|
19
|
+
const target = sourceFile(source, file, model);
|
|
20
|
+
const missing = target && !model.documents.some((document) => document.file === target);
|
|
21
|
+
let problem = "";
|
|
22
|
+
if (missing) {
|
|
23
|
+
problem = model.unavailable.includes(target) ? " (source unavailable)" : " (missing Markdown source)";
|
|
24
|
+
}
|
|
25
|
+
return `<a href="${escapeHtml(href)}" rel="noreferrer">${escapeHtml(source)}</a>${problem}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function documentReference(file: string, model: MetadataModel, hash = ""): string {
|
|
29
|
+
const id = model.documents.find((document) => document.file === file)?.parsed.fields.id;
|
|
30
|
+
return ((id ? referenceTarget(id, model) : undefined) ?? fileUrl(file)) + hash;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function valueHtml(text: string | undefined): string {
|
|
34
|
+
return escapeHtml(text ?? "Not recorded");
|
|
35
|
+
}
|
package/src/page/nav.ts
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
import type { MarkdownFile } from "
|
|
1
|
+
import type { MarkdownFile } from "#files/list.ts";
|
|
2
|
+
import { fileUrl } from "#files/url.ts";
|
|
2
3
|
import { escapeHtml } from "./escape.ts";
|
|
3
4
|
|
|
4
|
-
const nameOf = (path: string): string => path.slice(path.lastIndexOf("/") + 1).replace(/\.md
|
|
5
|
+
const nameOf = (path: string): string => path.slice(path.lastIndexOf("/") + 1).replace(/\.md$/u, "");
|
|
5
6
|
|
|
6
7
|
const folderOf = (path: string): string => path.slice(0, Math.max(0, path.lastIndexOf("/")));
|
|
7
8
|
|
|
8
|
-
const hrefOf = (path: string): string => `/${path.split("/").map(encodeURIComponent).join("/")}`;
|
|
9
|
-
|
|
10
9
|
const link = (file: MarkdownFile, current: string): string => {
|
|
11
10
|
const here = file.path === current ? ' aria-current="page"' : "";
|
|
12
11
|
const age = `<span class="age short" data-modified="${file.modified}"></span>`;
|
|
13
|
-
return `<a href="${
|
|
12
|
+
return `<a href="${fileUrl(file.path)}"${here}>${escapeHtml(nameOf(file.path))}</a>${age}`;
|
|
14
13
|
};
|
|
15
14
|
|
|
16
|
-
const folder = (name: string, files:
|
|
15
|
+
const folder = (name: string, files: readonly MarkdownFile[], current: string): string => {
|
|
17
16
|
const links = files.map((file) => `<li>${link(file, current)}</li>`).join("");
|
|
18
17
|
const open = files.some((file) => file.path === current) ? " open" : "";
|
|
19
18
|
return `<details data-key="folder:${escapeHtml(name)}"${open}><summary>${escapeHtml(name)}/ (${files.length})</summary><ul>${links}</ul></details>`;
|
|
20
19
|
};
|
|
21
20
|
|
|
22
|
-
export const navHtml = (files:
|
|
21
|
+
export const navHtml = (files: readonly MarkdownFile[], current: string, home: string | undefined): string => {
|
|
23
22
|
const ordered = [...files.filter((file) => file.path === home), ...files.filter((file) => file.path !== home)];
|
|
24
23
|
const folders = Map.groupBy(ordered, (file) => (file.path === home ? "" : folderOf(file.path)));
|
|
25
24
|
return [...folders]
|
|
26
|
-
.map(([name, grouped]) =>
|
|
25
|
+
.map(([name, grouped]) =>
|
|
26
|
+
name === "" ? `<ul>${grouped.map((file) => `<li>${link(file, current)}</li>`).join("")}</ul>` : folder(name, grouped, current),
|
|
27
|
+
)
|
|
27
28
|
.join("");
|
|
28
29
|
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export function navigationScript(): string {
|
|
2
|
+
return `
|
|
3
|
+
import { applyPage, capturePage } from "/_board/page-state.js";
|
|
4
|
+
import { reportMissingPassage, restoreReading, scrollToPassage } from "/_board/reading-state.js";
|
|
5
|
+
import { refreshLibrary } from "/_board/library.js";
|
|
6
|
+
|
|
7
|
+
const cache = new Map();
|
|
8
|
+
const readingKey = "work-board:reading:" + document.documentElement.dataset.workspace;
|
|
9
|
+
let savedReading = {};
|
|
10
|
+
try {
|
|
11
|
+
const saved = JSON.parse(sessionStorage.getItem(readingKey) ?? "{}");
|
|
12
|
+
if (saved && typeof saved === "object" && !Array.isArray(saved)) savedReading = saved;
|
|
13
|
+
} catch {}
|
|
14
|
+
let count = 0;
|
|
15
|
+
const newId = () => Date.now() + "-" + ++count;
|
|
16
|
+
let displayedId = history.state?.workBoard?.id ?? newId();
|
|
17
|
+
let version = 0;
|
|
18
|
+
let controller;
|
|
19
|
+
export const pageVersion = () => version;
|
|
20
|
+
const stateWith = (id, reading = null) => ({ ...history.state, workBoard: { id, reading } });
|
|
21
|
+
history.replaceState(stateWith(displayedId, history.state?.workBoard?.reading), "");
|
|
22
|
+
history.scrollRestoration = "manual";
|
|
23
|
+
document.getElementById("doc").dataset.scheme = document.documentElement.dataset.scheme;
|
|
24
|
+
refreshLibrary(true);
|
|
25
|
+
const initialReading = savedReading[displayedId] ?? history.state.workBoard.reading;
|
|
26
|
+
requestAnimationFrame(() => {
|
|
27
|
+
if (initialReading) restoreReading(initialReading);
|
|
28
|
+
else scrollToPassage(location.hash);
|
|
29
|
+
reportMissingPassage();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const saveCurrent = (write = true) => {
|
|
33
|
+
const snapshot = capturePage();
|
|
34
|
+
cache.delete(displayedId);
|
|
35
|
+
cache.set(displayedId, snapshot);
|
|
36
|
+
delete savedReading[displayedId];
|
|
37
|
+
savedReading[displayedId] = snapshot.reading;
|
|
38
|
+
savedReading = Object.fromEntries(Object.entries(savedReading).slice(-30));
|
|
39
|
+
try { sessionStorage.setItem(readingKey, JSON.stringify(savedReading)); } catch {}
|
|
40
|
+
if (cache.size > 30) cache.delete(cache.keys().next().value);
|
|
41
|
+
if (write && history.state?.workBoard?.id === displayedId) history.replaceState(stateWith(displayedId, snapshot.reading), "");
|
|
42
|
+
};
|
|
43
|
+
window.addEventListener("pagehide", () => saveCurrent(false));
|
|
44
|
+
|
|
45
|
+
const navigate = async (target, entry) => {
|
|
46
|
+
const sameDocument = document.getElementById("favorite-toggle") !== null && displayedId === history.state?.workBoard?.id && target.pathname === location.pathname && target.search === location.search;
|
|
47
|
+
saveCurrent(!entry);
|
|
48
|
+
const id = entry?.id ?? newId();
|
|
49
|
+
if (!entry) history.pushState(stateWith(id), "", target);
|
|
50
|
+
else if (history.state?.workBoard?.id !== id) history.replaceState(stateWith(id, entry.reading), "");
|
|
51
|
+
const mine = ++version;
|
|
52
|
+
controller?.abort();
|
|
53
|
+
controller = new AbortController();
|
|
54
|
+
const status = document.getElementById("navigation-status");
|
|
55
|
+
status.textContent = "";
|
|
56
|
+
if (!entry && sameDocument) {
|
|
57
|
+
displayedId = id;
|
|
58
|
+
scrollToPassage(target.hash);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const cached = cache.get(id);
|
|
62
|
+
if (cached) {
|
|
63
|
+
applyPage(cached.page);
|
|
64
|
+
displayedId = id;
|
|
65
|
+
restoreReading(cached.reading);
|
|
66
|
+
} else status.textContent = "Opening document…";
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(target.pathname + target.search, { cache: "no-store", signal: controller.signal });
|
|
69
|
+
const page = new DOMParser().parseFromString(await response.text(), "text/html");
|
|
70
|
+
if (mine !== version) return;
|
|
71
|
+
if (!applyPage(page, Boolean(cached))) throw new Error("Invalid page");
|
|
72
|
+
displayedId = id;
|
|
73
|
+
status.textContent = response.ok ? "" : response.status === 404 ? "This document is missing or was renamed." : "This document could not be opened.";
|
|
74
|
+
if (response.ok) reportMissingPassage();
|
|
75
|
+
if (!cached) {
|
|
76
|
+
document.getElementById("doc").focus({ preventScroll: true });
|
|
77
|
+
if (savedReading[id] ?? entry?.reading) restoreReading(savedReading[id] ?? entry.reading);
|
|
78
|
+
else scrollToPassage(target.hash);
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (mine !== version || error.name === "AbortError") return;
|
|
82
|
+
status.textContent = "Could not open this document. Retry the link or reload the page.";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
document.addEventListener("click", (event) => {
|
|
86
|
+
const link = event.target.closest?.("a[href]");
|
|
87
|
+
if (!link || event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey || link.hasAttribute("download") || (link.target && link.target !== "_self")) return;
|
|
88
|
+
const target = new URL(link.href, location.href);
|
|
89
|
+
if (target.origin !== location.origin || !(target.pathname === "/" || (target.pathname === "/_board/work" || target.pathname.startsWith("/_board/item/")) || /\\.md$/i.test(target.pathname))) return;
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
navigate(target);
|
|
92
|
+
});
|
|
93
|
+
document.addEventListener("submit", (event) => {
|
|
94
|
+
const form = event.target;
|
|
95
|
+
if (!form.matches?.("#work-filters")) return;
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
const target = new URL(form.action, location.href);
|
|
98
|
+
target.search = new URLSearchParams([...new FormData(form)].map(([key, value]) => [key, String(value)])).toString();
|
|
99
|
+
navigate(target);
|
|
100
|
+
});
|
|
101
|
+
window.addEventListener("popstate", (event) => navigate(new URL(location.href), event.state?.workBoard ?? { id: newId() }));
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function readingStateScript(): string {
|
|
2
|
+
return `
|
|
3
|
+
const pathOf = (node, root) => {
|
|
4
|
+
const path = [];
|
|
5
|
+
for (; node !== root; node = node.parentNode) {
|
|
6
|
+
if (!node?.parentNode) return null;
|
|
7
|
+
path.unshift([...node.parentNode.childNodes].indexOf(node));
|
|
8
|
+
}
|
|
9
|
+
return path;
|
|
10
|
+
};
|
|
11
|
+
const nodeAt = (path, root) => path.reduce((node, index) => node?.childNodes[index], root);
|
|
12
|
+
|
|
13
|
+
export const captureReading = () => {
|
|
14
|
+
const root = document.getElementById("doc");
|
|
15
|
+
const selection = document.getSelection();
|
|
16
|
+
const anchor = selection?.anchorNode && pathOf(selection.anchorNode, root);
|
|
17
|
+
const focus = selection?.focusNode && pathOf(selection.focusNode, root);
|
|
18
|
+
return {
|
|
19
|
+
scroll: [scrollX, scrollY],
|
|
20
|
+
sidebar: document.getElementById("sidebar").scrollTop,
|
|
21
|
+
details: [...root.querySelectorAll("details")].map((detail) => detail.open),
|
|
22
|
+
selection: anchor && focus ? { anchor, focus, a: selection.anchorOffset, f: selection.focusOffset } : null,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const restoreReading = (state, restoreDetails = true) => {
|
|
27
|
+
if (!state || !Array.isArray(state.scroll) || state.scroll.length !== 2 || !state.scroll.every(Number.isFinite)) return;
|
|
28
|
+
const root = document.getElementById("doc");
|
|
29
|
+
[...root.querySelectorAll("details")].forEach((detail, index) => {
|
|
30
|
+
if (restoreDetails && typeof state.details?.[index] === "boolean") detail.open = state.details[index];
|
|
31
|
+
});
|
|
32
|
+
document.getElementById("sidebar").scrollTop = state.sidebar ?? 0;
|
|
33
|
+
const saved = state.selection;
|
|
34
|
+
if (saved && Array.isArray(saved.anchor) && Array.isArray(saved.focus) && Number.isInteger(saved.a) && Number.isInteger(saved.f) && saved.a >= 0 && saved.f >= 0) {
|
|
35
|
+
const anchor = nodeAt(saved.anchor, root);
|
|
36
|
+
const focus = nodeAt(saved.focus, root);
|
|
37
|
+
const limit = (node) => node.nodeType === 3 ? node.textContent.length : node.childNodes.length;
|
|
38
|
+
if (anchor && focus) document.getSelection()?.setBaseAndExtent(anchor, Math.min(saved.a, limit(anchor)), focus, Math.min(saved.f, limit(focus)));
|
|
39
|
+
}
|
|
40
|
+
scrollTo(...state.scroll);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const passageAt = (hash) => {
|
|
44
|
+
try { return document.getElementById(decodeURIComponent(hash.slice(1))); } catch { return null; }
|
|
45
|
+
};
|
|
46
|
+
export const reportMissingPassage = () => {
|
|
47
|
+
if (!location.hash || !document.getElementById("favorite-toggle")) return;
|
|
48
|
+
const status = document.getElementById("navigation-status");
|
|
49
|
+
if (!passageAt(location.hash)) status.textContent = "This passage is no longer in the document.";
|
|
50
|
+
else if (status.textContent === "This passage is no longer in the document.") status.textContent = "";
|
|
51
|
+
};
|
|
52
|
+
export const scrollToPassage = (hash) => {
|
|
53
|
+
if (!hash) { scrollTo(0, 0); return; }
|
|
54
|
+
const target = passageAt(hash);
|
|
55
|
+
if (!target) { reportMissingPassage(); return; }
|
|
56
|
+
for (let parent = target.parentElement; parent; parent = parent.parentElement) if (parent.tagName === "DETAILS") parent.open = true;
|
|
57
|
+
target.scrollIntoView();
|
|
58
|
+
target.focus({ preventScroll: true });
|
|
59
|
+
};
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function readingStyles(): string {
|
|
2
|
+
return `
|
|
3
|
+
.reading-tools { display: flex; align-items: start; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; font-size: 13px; }
|
|
4
|
+
.reading-tools button[aria-pressed="true"] { background: var(--surface); border-color: var(--link); }
|
|
5
|
+
.outline { margin: 0; min-width: 180px; max-width: 100%; }
|
|
6
|
+
.outline > summary { padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; color: var(--text); }
|
|
7
|
+
.outline nav { margin-top: 12px; padding: 0 12px; max-height: 40vh; overflow-y: auto; }
|
|
8
|
+
.outline ol { padding-left: 18px; }
|
|
9
|
+
.outline li { overflow-wrap: anywhere; }
|
|
10
|
+
.outline li[data-depth="3"], .outline li[data-depth="4"] { margin-left: 12px; }
|
|
11
|
+
.outline li[data-depth="5"], .outline li[data-depth="6"] { margin-left: 24px; }
|
|
12
|
+
.heading-anchor { display: inline-block; margin-left: 8px; font-size: 0.7em; text-decoration: none; opacity: 0.35; }
|
|
13
|
+
.heading-anchor::after { content: "#"; }
|
|
14
|
+
.heading-anchor:hover, .heading-anchor:focus-visible { opacity: 1; }
|
|
15
|
+
[id^="heading-"] { scroll-margin-top: 20px; }
|
|
16
|
+
.reading-library { font-size: 13px; margin-bottom: 24px; }
|
|
17
|
+
.reading-library[hidden] { display: none; }
|
|
18
|
+
.reading-library ul { list-style: none; padding: 0 12px; margin: 0 0 20px; }
|
|
19
|
+
.reading-library a { color: var(--sidebar-text); overflow-wrap: anywhere; }
|
|
20
|
+
.reading-library a[aria-current="page"] { font-weight: 600; }
|
|
21
|
+
.reading-library .missing { color: var(--sidebar-muted); }
|
|
22
|
+
.library-status { padding: 0 12px; color: var(--sidebar-muted); }
|
|
23
|
+
#navigation-status { color: var(--danger); overflow-wrap: anywhere; }
|
|
24
|
+
#navigation-status:empty { display: none; }
|
|
25
|
+
.bar:has(#navigation-status:not(:empty)) { flex-wrap: wrap; }
|
|
26
|
+
#navigation-status:not(:empty) { flex-basis: 100%; }
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Heading } from "#render/documentHeadings.ts";
|
|
2
|
+
import { escapeHtml } from "./escape.ts";
|
|
3
|
+
|
|
4
|
+
export function readingTools(headings: readonly Heading[]): string {
|
|
5
|
+
const outline =
|
|
6
|
+
headings.length === 0
|
|
7
|
+
? ""
|
|
8
|
+
: `<details class="outline" data-key="outline"><summary>On this page</summary><nav aria-label="Document outline"><ol>${headings
|
|
9
|
+
.map(({ depth, id, title }) => `<li data-depth="${depth}"><a href="#${escapeHtml(id)}">${escapeHtml(title || "Section")}</a></li>`)
|
|
10
|
+
.join("")}</ol></nav></details>`;
|
|
11
|
+
return `<div class="reading-tools"><button id="favorite-toggle" type="button" aria-pressed="false" disabled>Save favorite</button>${outline}</div>`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MetadataModel } from "#metadata/model.ts";
|
|
2
|
+
import type { ParsedMetadata } from "#metadata/parse.ts";
|
|
3
|
+
import { escapeHtml } from "./escape.ts";
|
|
4
|
+
import { referenceHtml, sourceHtml, valueHtml } from "./metadataLinks.ts";
|
|
5
|
+
|
|
6
|
+
export function recordedEvidence(parsed: ParsedMetadata, file: string, model: MetadataModel): string {
|
|
7
|
+
const records = parsed.fields.evidence ?? [];
|
|
8
|
+
if (records.length === 0) {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
return `<h3>Recorded evidence</h3><p>Source claims — not independently verified. A recorded outcome does not establish human acceptance. Work Board does not check recorded revisions against the current workspace.</p><ul>${records.map((item, index) => `<li id="recorded-evidence-${index}" tabindex="-1">${sourceHtml(item.source, file, model)}<dl><dt>Recorded in</dt><dd>${escapeHtml(file)}:${parsed.lines[`evidence.${index}.source`] ?? parsed.lines.evidence ?? 1}</dd><dt>Criterion</dt><dd>${item.criterion ? referenceHtml(item.criterion, item.criterion, model) : "Not recorded"}</dd><dt>Checked revision</dt><dd>${valueHtml(item.checkedRevision)}</dd><dt>Observed time</dt><dd>${valueHtml(item.observedAt)}</dd><dt>Method</dt><dd>${valueHtml(item.method)}</dd><dt>Recorded outcome</dt><dd>${valueHtml(item.outcome)}</dd></dl></li>`).join("")}</ul>`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const searchDialog = `<dialog id="search-dialog" aria-labelledby="search-title">
|
|
2
|
+
<div class="search-header"><h2 id="search-title">Find work</h2><button type="button" id="search-close" aria-label="Close search">Close</button></div>
|
|
3
|
+
<form id="search-form"><label for="search-query">Search documents, headings, and passages</label><input id="search-query" type="search" autocomplete="off" maxlength="200" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="search-results"><button type="submit">Search</button></form>
|
|
4
|
+
<p class="search-help">Ctrl/Cmd+K to open · ↑/↓ to choose · Enter to open · Escape to close</p>
|
|
5
|
+
<p id="search-status" role="status"></p><ul id="search-results" role="listbox" aria-label="Search results"></ul>
|
|
6
|
+
</dialog>`;
|
|
7
|
+
|
|
8
|
+
export const searchStyles = `
|
|
9
|
+
#search-dialog{width:min(680px,calc(100vw - 32px));max-height:calc(100dvh - 32px);padding:20px;border:1px solid var(--border);border-radius:12px;background:var(--surface);color:var(--text)}
|
|
10
|
+
#search-dialog::backdrop{background:rgb(0 0 0 / .45)}
|
|
11
|
+
.search-header{display:flex;justify-content:space-between;align-items:center;gap:12px}.search-header h2{margin:0;font-size:1.3rem}
|
|
12
|
+
#search-form{display:grid;grid-template-columns:1fr auto;gap:8px;margin-top:16px}#search-form label{grid-column:1/-1}#search-query{min-width:0;width:100%;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font:inherit}
|
|
13
|
+
.search-help{margin-top:12px}.search-help,#search-status{font-size:.85rem;color:var(--muted)}#search-results{list-style:none;padding:0;margin:0}
|
|
14
|
+
#search-results a{display:block;padding:12px;border-radius:6px;text-decoration:none;overflow-wrap:anywhere}#search-results a[aria-selected="true"]{background:var(--surface);outline:2px solid var(--focus);outline-offset:-2px}
|
|
15
|
+
#search-results strong,#search-results small,#search-results p{display:block}#search-results small{color:var(--muted);font-size:.8rem}#search-results p{margin:6px 0 0;font-size:.9rem}
|
|
16
|
+
`;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function searchResultsScript(): string {
|
|
2
|
+
return `
|
|
3
|
+
const results = document.getElementById("search-results");
|
|
4
|
+
const input = document.getElementById("search-query");
|
|
5
|
+
let selected = -1;
|
|
6
|
+
const commands = [
|
|
7
|
+
{ kind: "command", title: "Open workspace", href: "/" },
|
|
8
|
+
{ kind: "command", title: "Show or hide files", command: "sidebar-toggle" },
|
|
9
|
+
{ kind: "command", title: "Toggle compact density", command: "density" },
|
|
10
|
+
{ kind: "command", title: "Toggle light or dark theme", command: "theme" },
|
|
11
|
+
];
|
|
12
|
+
export const matchingCommands = (query) => commands.filter(item => item.title.toLowerCase().includes(query.trim().toLowerCase()));
|
|
13
|
+
export const choose = (index) => {
|
|
14
|
+
const options = [...results.querySelectorAll("a")];
|
|
15
|
+
selected = options.length ? (index + options.length) % options.length : -1;
|
|
16
|
+
options.forEach((option, at) => option.setAttribute("aria-selected", String(at === selected)));
|
|
17
|
+
if (selected >= 0) {
|
|
18
|
+
input.setAttribute("aria-activedescendant", options[selected].id);
|
|
19
|
+
options[selected].scrollIntoView({ block: "nearest" });
|
|
20
|
+
} else input.removeAttribute("aria-activedescendant");
|
|
21
|
+
};
|
|
22
|
+
export const move = (delta) => choose(selected + delta);
|
|
23
|
+
export const activate = () => results.querySelector('a[aria-selected="true"]')?.click();
|
|
24
|
+
export const showResults = (items) => {
|
|
25
|
+
results.replaceChildren();
|
|
26
|
+
items.forEach((item, index) => {
|
|
27
|
+
const li = document.createElement("li");
|
|
28
|
+
li.setAttribute("role", "presentation");
|
|
29
|
+
const link = document.createElement("a");
|
|
30
|
+
link.id = "search-result-" + index;
|
|
31
|
+
link.href = item.href ?? "#";
|
|
32
|
+
link.setAttribute("role", "option");
|
|
33
|
+
if (item.command) link.dataset.command = item.command;
|
|
34
|
+
const title = document.createElement("strong");
|
|
35
|
+
title.textContent = item.title;
|
|
36
|
+
const source = document.createElement("small");
|
|
37
|
+
source.textContent = item.kind + (item.file ? " · " + item.file + (item.line ? ":" + item.line : "") : "");
|
|
38
|
+
link.append(title, source);
|
|
39
|
+
if (item.snippet) {
|
|
40
|
+
const snippet = document.createElement("p");
|
|
41
|
+
snippet.textContent = item.snippet;
|
|
42
|
+
link.append(snippet);
|
|
43
|
+
}
|
|
44
|
+
li.append(link); results.append(li);
|
|
45
|
+
});
|
|
46
|
+
choose(0);
|
|
47
|
+
};
|
|
48
|
+
results.addEventListener("click", (event) => {
|
|
49
|
+
const link = event.target.closest("a");
|
|
50
|
+
if (!link) return;
|
|
51
|
+
const control = document.getElementById(link.dataset.command);
|
|
52
|
+
if (control) {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
if (control.id === "sidebar-toggle") control.click();
|
|
55
|
+
else {
|
|
56
|
+
control.value = control.id === "density" ? (control.value === "compact" ? "comfortable" : "compact") : (control.value === "dark" ? "light" : "dark");
|
|
57
|
+
control.dispatchEvent(new Event("change"));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
document.getElementById("search-dialog").close(!control && event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey ? "navigate" : "");
|
|
61
|
+
});
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function searchScript(): string {
|
|
2
|
+
return `
|
|
3
|
+
import { activate, matchingCommands, move, showResults } from "/_board/search-results.js";
|
|
4
|
+
const dialog = document.getElementById("search-dialog");
|
|
5
|
+
const input = document.getElementById("search-query");
|
|
6
|
+
const status = document.getElementById("search-status");
|
|
7
|
+
const open = document.getElementById("search-open");
|
|
8
|
+
open.disabled = false;
|
|
9
|
+
let previous;
|
|
10
|
+
let version = 0;
|
|
11
|
+
let timer;
|
|
12
|
+
let controller;
|
|
13
|
+
const invalidate = () => {
|
|
14
|
+
++version;
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
controller?.abort();
|
|
17
|
+
};
|
|
18
|
+
const search = async () => {
|
|
19
|
+
invalidate();
|
|
20
|
+
const mine = version;
|
|
21
|
+
const query = input.value.trim();
|
|
22
|
+
const commands = matchingCommands(query);
|
|
23
|
+
showResults(commands);
|
|
24
|
+
if (!query) { status.textContent = "Type to search this workspace, or choose a command."; return; }
|
|
25
|
+
status.textContent = "Searching…";
|
|
26
|
+
controller = new AbortController();
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch("/_board/search?q=" + encodeURIComponent(query), { cache: "no-store", signal: controller.signal });
|
|
29
|
+
if (!response.ok) throw new Error("Search unavailable");
|
|
30
|
+
const body = await response.json();
|
|
31
|
+
if (mine !== version || !dialog.open) return;
|
|
32
|
+
showResults([...commands, ...body.results]);
|
|
33
|
+
status.textContent = (body.total ? (body.total > body.results.length ? "Showing " + body.results.length + " of " : "") + body.total + " matches." : "No document matches.") + (body.unavailable.length ? " Could not read " + body.unavailable.length + " files; results are incomplete." : "");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (mine !== version || error.name === "AbortError") return;
|
|
36
|
+
status.textContent = "Search could not be completed. Press Search to retry.";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
open.addEventListener("click", () => {
|
|
40
|
+
previous = document.activeElement;
|
|
41
|
+
dialog.returnValue = "";
|
|
42
|
+
dialog.showModal();
|
|
43
|
+
input.focus();
|
|
44
|
+
input.select();
|
|
45
|
+
search();
|
|
46
|
+
});
|
|
47
|
+
document.getElementById("search-close").addEventListener("click", () => dialog.close());
|
|
48
|
+
dialog.addEventListener("close", () => { invalidate(); if (dialog.returnValue !== "navigate") previous?.focus({ preventScroll: true }); });
|
|
49
|
+
dialog.addEventListener("cancel", () => invalidate());
|
|
50
|
+
dialog.addEventListener("keydown", (event) => {
|
|
51
|
+
if (event.key === "Escape") { event.preventDefault(); dialog.close(); }
|
|
52
|
+
});
|
|
53
|
+
input.addEventListener("input", () => {
|
|
54
|
+
invalidate();
|
|
55
|
+
showResults([]);
|
|
56
|
+
status.textContent = input.value.trim() ? "Searching…" : "Type to search this workspace.";
|
|
57
|
+
timer = setTimeout(search, 150);
|
|
58
|
+
});
|
|
59
|
+
document.getElementById("search-form").addEventListener("submit", (event) => { event.preventDefault(); search(); });
|
|
60
|
+
input.addEventListener("keydown", (event) => {
|
|
61
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") { event.preventDefault(); move(event.key === "ArrowDown" ? 1 : -1); }
|
|
62
|
+
if (event.key === "Enter" && document.querySelector('#search-results a[aria-selected="true"]')) { event.preventDefault(); activate(); }
|
|
63
|
+
});
|
|
64
|
+
document.addEventListener("keydown", (event) => {
|
|
65
|
+
if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "k") {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
if (dialog.open) dialog.close(); else open.click();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
document.addEventListener("board-index-change", () => { if (dialog.open) search(); });
|
|
71
|
+
`;
|
|
72
|
+
}
|
package/src/page/shell.ts
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
|
+
import { fileUrl } from "#files/url.ts";
|
|
1
2
|
import { escapeHtml } from "./escape.ts";
|
|
3
|
+
import { searchDialog } from "./searchDialog.ts";
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
function location(file: string): string {
|
|
6
|
+
return file
|
|
7
|
+
.split("/")
|
|
8
|
+
.map((part) => `<span>${escapeHtml(part)}</span>`)
|
|
9
|
+
.join('<span class="separator">/</span>');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const shell = (title: string, nav: string, main: string, workspace: string, board: boolean, identity?: string, view = false): string =>
|
|
4
13
|
[
|
|
5
|
-
|
|
14
|
+
`<!doctype html><html lang="en" data-workspace="${escapeHtml(workspace)}"><head><meta charset="utf-8">`,
|
|
6
15
|
'<meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light dark">',
|
|
7
16
|
`<title>${escapeHtml(title)}</title>`,
|
|
8
17
|
'<link rel="stylesheet" href="/_board/style.css"><script type="module" src="/_board/client.js"></script>',
|
|
9
|
-
'</head><body><
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
'</head><body><a class="skip" href="#doc">Skip to content</a><div class="workspace">',
|
|
19
|
+
'<aside id="sidebar" class="sidebar"><a class="brand" href="/">work board<span>Local workspace</span></a>',
|
|
20
|
+
'<div id="reading-library" class="reading-library" hidden></div><h2 class="nav-heading">Project files</h2>',
|
|
21
|
+
`<nav id="files" class="files" aria-label="Project files">${nav}</nav></aside>`,
|
|
22
|
+
'<div class="page"><header class="bar"><button id="sidebar-toggle" type="button" aria-controls="sidebar" aria-expanded="true">Files</button>',
|
|
23
|
+
`<nav id="breadcrumbs" class="breadcrumbs" aria-label="File location"><a href="/">Workspace</a><span class="separator">/</span>${location(title)}</nav>`,
|
|
24
|
+
`<a id="work-open" class="work-open" href="/_board/work?view=board"${view ? ' aria-current="page"' : ""}>Work</a><button id="search-open" type="button" disabled aria-haspopup="dialog" aria-controls="search-dialog">Search <kbd>⌘/Ctrl K</kbd></button><span id="live" class="live" role="status">connecting</span><span id="navigation-status" role="status"></span></header>`,
|
|
25
|
+
'<div class="preferences" role="group" aria-label="Reading preferences">',
|
|
26
|
+
'<div class="preference"><label for="theme">Theme</label><select id="theme"><option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option></select></div>',
|
|
27
|
+
'<div class="preference"><label for="density">Density</label><select id="density"><option value="comfortable">Comfortable</option><option value="compact">Compact</option></select></div>',
|
|
28
|
+
'<span id="preference-status" role="status"></span></div>',
|
|
29
|
+
`<main${view ? ' data-view="work"' : ""}${identity ? ` data-identity="${escapeHtml(identity)}"` : ""} id="doc" data-file="${escapeHtml(title)}" data-url="${escapeHtml(fileUrl(title))}" class="${board ? "board-view" : "document-view"}" tabindex="-1">${main}</main>`,
|
|
30
|
+
`</div></div>${searchDialog}</body></html>`,
|
|
12
31
|
].join("");
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function pageStateScript(): string {
|
|
2
|
+
return `
|
|
3
|
+
import { remember, swap } from "/_board/swap.js";
|
|
4
|
+
import { renderDiagrams } from "/_board/diagrams.js";
|
|
5
|
+
import { captureReading, reportMissingPassage, restoreReading } from "/_board/reading-state.js";
|
|
6
|
+
import { refreshLibrary } from "/_board/library.js";
|
|
7
|
+
|
|
8
|
+
export const capturePage = () => {
|
|
9
|
+
const page = document.implementation.createHTMLDocument(document.title);
|
|
10
|
+
for (const id of ["files", "doc", "breadcrumbs"]) page.body.append(document.getElementById(id).cloneNode(true));
|
|
11
|
+
return { page, reading: captureReading() };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const applyPage = (page, preserve = false) => {
|
|
15
|
+
const incoming = page.getElementById("doc");
|
|
16
|
+
const current = document.getElementById("doc");
|
|
17
|
+
if (!incoming || !page.getElementById("files")) return false;
|
|
18
|
+
const reading = preserve ? captureReading() : null;
|
|
19
|
+
const cachedScheme = incoming.dataset.scheme;
|
|
20
|
+
if (preserve && (current.dataset.file === incoming.dataset.file || (current.dataset.identity && current.dataset.identity === incoming.dataset.identity))) {
|
|
21
|
+
swap(current, incoming);
|
|
22
|
+
} else {
|
|
23
|
+
current.replaceWith(document.importNode(incoming, true));
|
|
24
|
+
remember(document.getElementById("doc"));
|
|
25
|
+
}
|
|
26
|
+
const doc = document.getElementById("doc");
|
|
27
|
+
doc.className = incoming.className;
|
|
28
|
+
doc.dataset.file = incoming.dataset.file;
|
|
29
|
+
if (incoming.dataset.identity) doc.dataset.identity = incoming.dataset.identity; else delete doc.dataset.identity;
|
|
30
|
+
doc.dataset.url = incoming.dataset.url;
|
|
31
|
+
if (incoming.dataset.view) doc.dataset.view = incoming.dataset.view; else delete doc.dataset.view;
|
|
32
|
+
document.getElementById("work-open")?.setAttribute("aria-current", doc.dataset.view === "work" ? "page" : "false");
|
|
33
|
+
doc.dataset.scheme = document.documentElement.dataset.scheme;
|
|
34
|
+
swap(document.getElementById("files"), page.getElementById("files"));
|
|
35
|
+
const crumbs = page.getElementById("breadcrumbs");
|
|
36
|
+
if (crumbs) document.getElementById("breadcrumbs").replaceChildren(...document.importNode(crumbs, true).childNodes);
|
|
37
|
+
if (!preserve) {
|
|
38
|
+
document.querySelector('#files a[aria-current="page"]')?.closest("details")?.setAttribute("open", "");
|
|
39
|
+
}
|
|
40
|
+
refreshLibrary(!preserve);
|
|
41
|
+
if (reading) restoreReading({ ...reading, selection: null }, false);
|
|
42
|
+
renderDiagrams(document, cachedScheme && cachedScheme !== doc.dataset.scheme ? "figure.diagram" : undefined);
|
|
43
|
+
reportMissingPassage();
|
|
44
|
+
document.dispatchEvent(new Event("board-page"));
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
`;
|
|
48
|
+
}
|