@shivaedev/work-board 0.6.1 → 0.7.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/CHANGELOG.md +35 -2
- package/README.md +19 -0
- package/assets/native.js +1 -1
- package/dist/board.d.ts.map +1 -1
- package/dist/board.js +6 -0
- package/dist/board.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/http/page.d.ts.map +1 -1
- package/dist/http/page.js +4 -0
- package/dist/http/page.js.map +1 -1
- package/dist/http/start.d.ts +4 -0
- package/dist/http/start.d.ts.map +1 -0
- package/dist/http/start.js +12 -0
- package/dist/http/start.js.map +1 -0
- package/dist/page/navigationScript.js +1 -1
- package/dist/page/shell.d.ts +1 -1
- package/dist/page/shell.d.ts.map +1 -1
- package/dist/page/shell.js +1 -1
- package/dist/page/shell.js.map +1 -1
- package/dist/page/startHtml.d.ts +2 -0
- package/dist/page/startHtml.d.ts.map +1 -0
- package/dist/page/startHtml.js +10 -0
- package/dist/page/startHtml.js.map +1 -0
- package/dist/page/startTemplates.d.ts +14 -0
- package/dist/page/startTemplates.d.ts.map +1 -0
- package/dist/page/startTemplates.js +104 -0
- package/dist/page/startTemplates.js.map +1 -0
- package/dist/page/style.d.ts.map +1 -1
- package/dist/page/style.js +37 -1
- package/dist/page/style.js.map +1 -1
- package/dist/render/board.d.ts.map +1 -1
- package/dist/render/board.js +5 -2
- package/dist/render/board.js.map +1 -1
- package/dist/render/callouts.d.ts +3 -0
- package/dist/render/callouts.d.ts.map +1 -0
- package/dist/render/callouts.js +27 -0
- package/dist/render/callouts.js.map +1 -0
- package/dist/render/markdown.d.ts.map +1 -1
- package/dist/render/markdown.js +4 -1
- package/dist/render/markdown.js.map +1 -1
- package/dist/render/visual.d.ts +3 -0
- package/dist/render/visual.d.ts.map +1 -0
- package/dist/render/visual.js +90 -0
- package/dist/render/visual.js.map +1 -0
- package/dist/render/visualDocuments.d.ts +5 -0
- package/dist/render/visualDocuments.d.ts.map +1 -0
- package/dist/render/visualDocuments.js +57 -0
- package/dist/render/visualDocuments.js.map +1 -0
- package/dist/render/visualFields.d.ts +12 -0
- package/dist/render/visualFields.d.ts.map +1 -0
- package/dist/render/visualFields.js +13 -0
- package/dist/render/visualFields.js.map +1 -0
- package/dist/rpc/contract.js +1 -1
- package/dist/rpc/contract.js.map +1 -1
- package/dist/search/entries.d.ts.map +1 -1
- package/dist/search/entries.js +4 -1
- package/dist/search/entries.js.map +1 -1
- package/package.json +4 -2
- package/src/board.ts +6 -0
- package/src/cli.ts +1 -1
- package/src/http/page.ts +4 -0
- package/src/http/start.ts +16 -0
- package/src/page/navigationScript.ts +1 -1
- package/src/page/shell.ts +2 -2
- package/src/page/startHtml.ts +10 -0
- package/src/page/startTemplates.ts +103 -0
- package/src/page/style.ts +38 -1
- package/src/render/board.ts +5 -2
- package/src/render/callouts.ts +29 -0
- package/src/render/markdown.ts +4 -1
- package/src/render/visual.ts +100 -0
- package/src/render/visualDocuments.ts +68 -0
- package/src/render/visualFields.ts +17 -0
- package/src/rpc/contract.ts +1 -1
- package/src/search/entries.ts +4 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export const startTemplates = [
|
|
2
|
+
{
|
|
3
|
+
file: "project.md",
|
|
4
|
+
label: "Project",
|
|
5
|
+
source: `---
|
|
6
|
+
id: example.project
|
|
7
|
+
kind: project
|
|
8
|
+
criteria:
|
|
9
|
+
- id: readable-result
|
|
10
|
+
text: A reviewer can trace the result to its reasoning and recorded evidence.
|
|
11
|
+
---
|
|
12
|
+
# Project
|
|
13
|
+
|
|
14
|
+
## Goal
|
|
15
|
+
Describe the outcome and who needs it.
|
|
16
|
+
|
|
17
|
+
## Scope and constraints
|
|
18
|
+
Name what belongs here and what must remain outside this work.
|
|
19
|
+
|
|
20
|
+
## Reading path
|
|
21
|
+
- [Investigation](investigation.md)
|
|
22
|
+
- [Agent result](result.md)
|
|
23
|
+
|
|
24
|
+
## Acceptance
|
|
25
|
+
The criterion above is a target, not a recorded pass.
|
|
26
|
+
Record what was checked, the source revision, and remaining limitations.
|
|
27
|
+
`,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
file: "investigation.md",
|
|
31
|
+
label: "Investigation",
|
|
32
|
+
source: `---
|
|
33
|
+
id: example.investigation
|
|
34
|
+
kind: investigation
|
|
35
|
+
relationships:
|
|
36
|
+
- kind: informs
|
|
37
|
+
target: example.project
|
|
38
|
+
criteria:
|
|
39
|
+
- id: explained-choice
|
|
40
|
+
text: Options, evidence, tradeoffs and the recommendation are explicit.
|
|
41
|
+
---
|
|
42
|
+
# Investigation
|
|
43
|
+
|
|
44
|
+
## Question and background
|
|
45
|
+
Explain the decision in context; do not require the reader to reconstruct it.
|
|
46
|
+
|
|
47
|
+
## Options and tradeoffs
|
|
48
|
+
| Option | Benefit | Cost | Evidence |
|
|
49
|
+
| --- | --- | --- | --- |
|
|
50
|
+
| First option | Not recorded | Not recorded | Not recorded |
|
|
51
|
+
| Second option | Not recorded | Not recorded | Not recorded |
|
|
52
|
+
|
|
53
|
+
## Recommendation
|
|
54
|
+
State your take and what remains uncertain.
|
|
55
|
+
|
|
56
|
+
## Response needed
|
|
57
|
+
Name the exact question and the choices. Do not imply that a response was recorded.
|
|
58
|
+
|
|
59
|
+
[Project and acceptance target](project.md)
|
|
60
|
+
`,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
file: "result.md",
|
|
64
|
+
label: "Agent result",
|
|
65
|
+
source: `---
|
|
66
|
+
id: example.result
|
|
67
|
+
kind: result
|
|
68
|
+
relationships:
|
|
69
|
+
- kind: implements
|
|
70
|
+
target: example.investigation
|
|
71
|
+
- kind: relates_to
|
|
72
|
+
target: example.project
|
|
73
|
+
# Add evidence only after a real observation; replace every placeholder:
|
|
74
|
+
# evidence:
|
|
75
|
+
# - criterion: example.project#readable-result
|
|
76
|
+
# source: ./evidence/reading-check.md
|
|
77
|
+
# method: Describe the actual check.
|
|
78
|
+
# outcome: Describe the observed result and its limits.
|
|
79
|
+
# observed_at: Use the actual ISO timestamp with timezone.
|
|
80
|
+
# checked_revision: Use the actual full source commit SHA.
|
|
81
|
+
---
|
|
82
|
+
# Agent result
|
|
83
|
+
|
|
84
|
+
## Outcome and changes
|
|
85
|
+
Explain what changed and why. Link the relevant source files or patch.
|
|
86
|
+
|
|
87
|
+
## Reasoning
|
|
88
|
+
[Investigation and alternatives](investigation.md)
|
|
89
|
+
|
|
90
|
+
## Checks and evidence
|
|
91
|
+
Not recorded. Replace this with actual commands, observations and local evidence links.
|
|
92
|
+
Recorded evidence does not by itself establish verified acceptance.
|
|
93
|
+
|
|
94
|
+
## Limits and unresolved questions
|
|
95
|
+
Name failures, skipped checks, uncertainty and any response needed.
|
|
96
|
+
|
|
97
|
+
## Next action
|
|
98
|
+
State the next action and who needs to respond, if known.
|
|
99
|
+
|
|
100
|
+
[Project and acceptance target](project.md)
|
|
101
|
+
`,
|
|
102
|
+
},
|
|
103
|
+
] as const;
|
package/src/page/style.ts
CHANGED
|
@@ -58,5 +58,42 @@ figure.diagram[data-state="failed"] .diagram-source { display: block; }
|
|
|
58
58
|
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
|
|
59
59
|
`;
|
|
60
60
|
|
|
61
|
+
const documents = `
|
|
62
|
+
.start-open { display: block; padding: 0 12px 18px; color: var(--sidebar-text); }
|
|
63
|
+
.onboarding { max-width: 76ch; }
|
|
64
|
+
.start-template textarea { display: block; box-sizing: border-box; width: 100%; margin: 12px 0; padding: 12px; font-family: var(--mono); font-size: 13px; color: var(--text); background: var(--surface); border: 1px solid var(--border); border-radius: 6px; }
|
|
65
|
+
@media print {
|
|
66
|
+
.sidebar, .bar, .preferences, .reading-tools, .visual-tools { display: none !important; }
|
|
67
|
+
.workspace { display: block !important; }
|
|
68
|
+
.page { width: 100%; }
|
|
69
|
+
#doc { padding: 0; }
|
|
70
|
+
.doc pre, .doc table { white-space: pre-wrap; overflow: visible; }
|
|
71
|
+
figure.diagram[data-state="pending"] .diagram-source, figure.diagram[data-state="failed"] .diagram-source { display: block; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.visual-document, .visual-callout { border: 1px solid var(--border); border-radius: 12px; padding: 1em 1.2em; margin: 1.2em 0; overflow-wrap: anywhere; }
|
|
75
|
+
.visual-document > :last-child, .visual-callout > :last-child { margin-bottom: 0; }
|
|
76
|
+
.visual-label { font-weight: 650; margin-top: 0; }
|
|
77
|
+
.visual-value { font-size: 1.35em; font-variant-numeric: tabular-nums; }
|
|
78
|
+
.visual-document progress { display: block; width: 100%; height: 1em; accent-color: var(--link); }
|
|
79
|
+
.visual-timeline-entries { border-left: 2px solid var(--border); padding-left: 1.6em; }
|
|
80
|
+
.visual-timeline-entries > li { padding: .3em 0 .7em .2em; }
|
|
81
|
+
.visual-callout { border-left: 4px solid var(--link); background: var(--surface); }
|
|
82
|
+
.visual-warning, .visual-caution { border-left-color: #b97720; }
|
|
83
|
+
.visual-diagnostic { font-weight: 600; color: var(--muted); }
|
|
84
|
+
@media print { .visual-document, .visual-callout { break-inside: avoid; } }
|
|
85
|
+
`;
|
|
86
|
+
|
|
61
87
|
export const style =
|
|
62
|
-
theme
|
|
88
|
+
theme
|
|
89
|
+
+ layout
|
|
90
|
+
+ prose
|
|
91
|
+
+ board
|
|
92
|
+
+ diagrams
|
|
93
|
+
+ readingStyles()
|
|
94
|
+
+ searchStyles
|
|
95
|
+
+ metadataStyles
|
|
96
|
+
+ workStyle()
|
|
97
|
+
+ attentionStyle()
|
|
98
|
+
+ visualStyles
|
|
99
|
+
+ documents;
|
package/src/render/board.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { RootContent } from "mdast";
|
|
2
|
-
import
|
|
2
|
+
import remarkDirective from "remark-directive";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import remarkParse from "remark-parse";
|
|
5
|
+
import { unified } from "unified";
|
|
3
6
|
|
|
4
7
|
export interface Section {
|
|
5
8
|
readonly heading: string;
|
|
@@ -58,7 +61,7 @@ const joined = (blocks: readonly string[]): string => blocks.join("\n\n");
|
|
|
58
61
|
|
|
59
62
|
export const boardOf = (source: string): Board => {
|
|
60
63
|
const draft: Draft = { definitions: [], footer: undefined, intro: [], sections: [], title: "" };
|
|
61
|
-
const root =
|
|
64
|
+
const root = unified().use(remarkParse).use(remarkGfm).use(remarkDirective).parse(source);
|
|
62
65
|
const nodes = root.type === "root" ? root.children : [];
|
|
63
66
|
const lastHeading = nodes.findLastIndex((node) => node.type === "heading");
|
|
64
67
|
nodes.forEach((node, index) => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Root } from "mdast";
|
|
2
|
+
import { visit as visitNodes } from "unist-util-visit";
|
|
3
|
+
|
|
4
|
+
const alerts = new Set(["NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"]);
|
|
5
|
+
|
|
6
|
+
export function callouts() {
|
|
7
|
+
return function transform(tree: Root) {
|
|
8
|
+
visitNodes(tree, "blockquote", (node) => {
|
|
9
|
+
const [first] = node.children;
|
|
10
|
+
const text = first?.type === "paragraph" ? first.children.at(0) : undefined;
|
|
11
|
+
if (text?.type !== "text") {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const match = /^\[!(?<kind>[A-Z]+)\](?:\n|$)/u.exec(text.value);
|
|
15
|
+
const named = match?.groups?.kind;
|
|
16
|
+
if (!(named && alerts.has(named))) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const kind = named.toLowerCase();
|
|
20
|
+
text.value = text.value.slice(match?.[0].length ?? 0);
|
|
21
|
+
node.children.unshift({
|
|
22
|
+
children: [{ type: "text", value: named[0] + kind.slice(1) }],
|
|
23
|
+
data: { hProperties: { className: ["visual-label"] } },
|
|
24
|
+
type: "paragraph",
|
|
25
|
+
});
|
|
26
|
+
node.data = { ...node.data, hName: "aside", hProperties: { className: ["visual-callout", `visual-${kind}`], role: "note" } };
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
}
|
package/src/render/markdown.ts
CHANGED
|
@@ -3,8 +3,11 @@ import { createElement } from "react";
|
|
|
3
3
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
4
|
import { MarkdownAsync } from "react-markdown";
|
|
5
5
|
import rehypeRaw from "rehype-raw";
|
|
6
|
+
import remarkDirective from "remark-directive";
|
|
6
7
|
import remarkGfm from "remark-gfm";
|
|
7
8
|
import type { PluggableList } from "unified";
|
|
9
|
+
import { callouts } from "#render/callouts.ts";
|
|
10
|
+
import { visualDocuments } from "#render/visualDocuments.ts";
|
|
8
11
|
import { codeBlocks } from "./code.ts";
|
|
9
12
|
import type { documentHeadings } from "./documentHeadings.ts";
|
|
10
13
|
import { documentLinks } from "./documentLinks.ts";
|
|
@@ -38,7 +41,7 @@ export const renderMarkdown = Effect.fn("WorkBoard.renderMarkdown")(function* (s
|
|
|
38
41
|
children: source,
|
|
39
42
|
components: { img: ({ node: _node, ...props }) => createElement("img", { ...props, loading: "lazy" }) },
|
|
40
43
|
rehypePlugins: plugins,
|
|
41
|
-
remarkPlugins: [remarkGfm],
|
|
44
|
+
remarkPlugins: [remarkGfm, remarkDirective, visualDocuments, callouts],
|
|
42
45
|
}),
|
|
43
46
|
),
|
|
44
47
|
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
import type { Paragraph, RootContent } from "mdast";
|
|
3
|
+
import type { ContainerDirective } from "mdast-util-directive";
|
|
4
|
+
import { MetricFields, ProgressFields } from "./visualFields.ts";
|
|
5
|
+
|
|
6
|
+
function textOf(node: RootContent): string {
|
|
7
|
+
if ("value" in node) {
|
|
8
|
+
return node.value;
|
|
9
|
+
}
|
|
10
|
+
return "children" in node ? node.children.map((child) => textOf(child)).join("") : "";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function paragraph(value: string, className?: string): Paragraph {
|
|
14
|
+
return { children: [{ type: "text", value }], data: { hProperties: { className: className ? [className] : [] } }, type: "paragraph" };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasLink(node: RootContent): boolean {
|
|
18
|
+
if (node.type === "link") {
|
|
19
|
+
return node.url.trim().length > 0;
|
|
20
|
+
}
|
|
21
|
+
if (node.type === "linkReference") {
|
|
22
|
+
return node.identifier.trim().length > 0;
|
|
23
|
+
}
|
|
24
|
+
return "children" in node && node.children.some((child) => hasLink(child));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function provenance(node: ContainerDirective): boolean {
|
|
28
|
+
return node.children.some((child) => child.type === "paragraph" && textOf(child).toLowerCase().startsWith("source:") && hasLink(child));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fields<A>(schema: Schema.ConstraintDecoder<A, never>, node: ContainerDirective): A {
|
|
32
|
+
const parsed = Schema.decodeUnknownOption(schema, { onExcessProperty: "error" })(node.attributes ?? {});
|
|
33
|
+
if (Option.isNone(parsed)) {
|
|
34
|
+
throw new Error("Invalid or unknown attributes.");
|
|
35
|
+
}
|
|
36
|
+
return parsed.value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function metric(node: ContainerDirective) {
|
|
40
|
+
const { unit, value } = fields(MetricFields, node);
|
|
41
|
+
if (!provenance(node)) {
|
|
42
|
+
throw new Error("Name a source with a Source: Markdown link.");
|
|
43
|
+
}
|
|
44
|
+
node.children.splice(1, 0, paragraph(value === undefined || value === "unknown" ? "Not recorded" : `${value} ${unit}`, "visual-value"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function progress(node: ContainerDirective, label: string) {
|
|
48
|
+
const { completed, total } = fields(ProgressFields, node);
|
|
49
|
+
if (!provenance(node)) {
|
|
50
|
+
throw new Error("Name a source with a Source: Markdown link.");
|
|
51
|
+
}
|
|
52
|
+
const known = typeof completed === "number" && typeof total === "number";
|
|
53
|
+
if (known && completed > total) {
|
|
54
|
+
throw new Error("Completed count exceeds total.");
|
|
55
|
+
}
|
|
56
|
+
function display(value: number | "unknown" | undefined) {
|
|
57
|
+
return typeof value === "number" ? String(value) : "Not recorded";
|
|
58
|
+
}
|
|
59
|
+
const tally = `Recorded tally: ${display(completed)} of ${display(total)}`;
|
|
60
|
+
node.children.splice(1, 0, paragraph(tally + (known && total > 0 ? "" : ". Percentage unavailable."), "visual-value"));
|
|
61
|
+
if (known && total > 0) {
|
|
62
|
+
node.children.splice(2, 0, {
|
|
63
|
+
children: [{ type: "text", value: tally }],
|
|
64
|
+
data: { hName: "progress", hProperties: { ariaLabel: `${label}: ${tally}`, max: String(total), value: String(completed) } },
|
|
65
|
+
type: "paragraph",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function timeline(node: ContainerDirective) {
|
|
71
|
+
fields(Schema.Struct({}), node);
|
|
72
|
+
const lists = node.children.filter((child) => child.type === "list");
|
|
73
|
+
const [list] = lists;
|
|
74
|
+
if (lists.length !== 1 || !list?.ordered || list.children.length === 0 || list.children.length > 100) {
|
|
75
|
+
throw new Error("Use one ordered list with 1–100 entries.");
|
|
76
|
+
}
|
|
77
|
+
if (list.children.some((item) => !hasLink(item) || textOf(item).trim().length === 0)) {
|
|
78
|
+
throw new Error("Each timeline entry needs text and a source link.");
|
|
79
|
+
}
|
|
80
|
+
list.data = { ...list.data, hProperties: { className: ["visual-timeline-entries"] } };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function renderVisual(node: ContainerDirective) {
|
|
84
|
+
const [first] = node.children;
|
|
85
|
+
if (first?.type !== "paragraph" || textOf(first).trim().length === 0 || textOf(first).toLowerCase().startsWith("source:")) {
|
|
86
|
+
throw new Error("Start with a Markdown label paragraph.");
|
|
87
|
+
}
|
|
88
|
+
const label = textOf(first).trim();
|
|
89
|
+
if (node.name === "metric") {
|
|
90
|
+
metric(node);
|
|
91
|
+
}
|
|
92
|
+
if (node.name === "progress") {
|
|
93
|
+
progress(node, label);
|
|
94
|
+
}
|
|
95
|
+
if (node.name === "timeline") {
|
|
96
|
+
timeline(node);
|
|
97
|
+
}
|
|
98
|
+
first.data = { ...first.data, hProperties: { className: ["visual-label"] } };
|
|
99
|
+
node.data = { ...node.data, hName: "section", hProperties: { ariaLabel: label, className: ["visual-document", `visual-${node.name}`] } };
|
|
100
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Root, RootContent } from "mdast";
|
|
2
|
+
import type { ContainerDirective, LeafDirective, TextDirective } from "mdast-util-directive";
|
|
3
|
+
import { SKIP, visit as visitNodes } from "unist-util-visit";
|
|
4
|
+
import { renderVisual } from "./visual.ts";
|
|
5
|
+
|
|
6
|
+
type Directive = ContainerDirective | LeafDirective | TextDirective;
|
|
7
|
+
const names = new Set(["metric", "progress", "timeline"]);
|
|
8
|
+
|
|
9
|
+
function isDirective(node: { readonly type: string }): node is Directive {
|
|
10
|
+
return node.type === "containerDirective" || node.type === "leafDirective" || node.type === "textDirective";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function nested(node: RootContent): boolean {
|
|
14
|
+
return isDirective(node) || ("children" in node && node.children.some((child) => nested(child)));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function failureOf(node: Directive, source: string): string | undefined {
|
|
18
|
+
try {
|
|
19
|
+
if (!names.has(node.name)) {
|
|
20
|
+
throw new Error(`Unknown visual directive ${node.name}.`);
|
|
21
|
+
}
|
|
22
|
+
if (node.type !== "containerDirective") {
|
|
23
|
+
throw new Error("Use a ::: container directive.");
|
|
24
|
+
}
|
|
25
|
+
const length = new TextEncoder().encode(source.slice(node.position?.start.offset, node.position?.end.offset)).byteLength;
|
|
26
|
+
if (length > 65_536) {
|
|
27
|
+
throw new Error("Visual directive exceeds 64 KiB.");
|
|
28
|
+
}
|
|
29
|
+
if (node.children.some((child) => nested(child))) {
|
|
30
|
+
throw new Error("Nested visual directives are not supported.");
|
|
31
|
+
}
|
|
32
|
+
renderVisual(node);
|
|
33
|
+
return undefined;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return error instanceof Error ? error.message : String(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function visualDocuments() {
|
|
40
|
+
return function transform(tree: Root, file: { readonly value: unknown }) {
|
|
41
|
+
const source = String(file.value);
|
|
42
|
+
visitNodes(tree, ["containerDirective", "leafDirective", "textDirective"], (directive, index, parent) => {
|
|
43
|
+
if (!isDirective(directive) || index === undefined || parent === undefined) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const failure = failureOf(directive, source);
|
|
47
|
+
if (failure === undefined) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const original = source.slice(directive.position?.start.offset, directive.position?.end.offset);
|
|
51
|
+
if (directive.type === "textDirective") {
|
|
52
|
+
parent.children.splice(index, 1, { type: "text", value: original });
|
|
53
|
+
return [SKIP, index + 1] as const;
|
|
54
|
+
}
|
|
55
|
+
parent.children.splice(
|
|
56
|
+
index,
|
|
57
|
+
1,
|
|
58
|
+
{
|
|
59
|
+
children: [{ type: "text", value: `Visual component could not be rendered: ${failure}` }],
|
|
60
|
+
data: { hProperties: { className: ["visual-diagnostic"] } },
|
|
61
|
+
type: "paragraph",
|
|
62
|
+
},
|
|
63
|
+
{ type: "code", value: original },
|
|
64
|
+
);
|
|
65
|
+
return [SKIP, index + 2] as const;
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Schema, SchemaTransformation } from "effect";
|
|
2
|
+
|
|
3
|
+
const NumberValue = Schema.String.check(Schema.isPattern(/\S/u)).pipe(Schema.decodeTo(Schema.Finite, SchemaTransformation.numberFromString));
|
|
4
|
+
const Count = NumberValue.check(Schema.makeFilter((value) => (Number.isSafeInteger(value) && value >= 0) || "Expected a nonnegative safe integer"));
|
|
5
|
+
const UnknownValue = Schema.Literal("unknown");
|
|
6
|
+
|
|
7
|
+
export const MetricFields = Schema.Struct({
|
|
8
|
+
unit: Schema.String.check(Schema.isPattern(/\S/u)),
|
|
9
|
+
value: Schema.optional(Schema.Union([NumberValue, UnknownValue])),
|
|
10
|
+
});
|
|
11
|
+
export const ProgressFields = Schema.Struct({
|
|
12
|
+
completed: Schema.optional(Schema.Union([Count, UnknownValue])),
|
|
13
|
+
total: Schema.optional(Schema.Union([Count, UnknownValue])),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type MetricFields = typeof MetricFields.Type;
|
|
17
|
+
export type ProgressFields = typeof ProgressFields.Type;
|
package/src/rpc/contract.ts
CHANGED
|
@@ -18,7 +18,7 @@ const PageUrl = Schema.String.check(
|
|
|
18
18
|
const path = new URL(value, "http://127.0.0.1").pathname;
|
|
19
19
|
return (
|
|
20
20
|
path === "/"
|
|
21
|
-
|| ["/_board/work", "/_board/overview", "/_board/changes"].includes(path)
|
|
21
|
+
|| ["/_board/work", "/_board/overview", "/_board/changes", "/_board/start"].includes(path)
|
|
22
22
|
|| /^\/_board\/item\/[^/]+\/$/u.exec(path) !== null
|
|
23
23
|
|| /\.md$/iu.exec(path) !== null
|
|
24
24
|
|| "Expected a local reading URL"
|
package/src/search/entries.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { MarkdownAsync } from "react-markdown";
|
|
2
2
|
import rehypeRaw from "rehype-raw";
|
|
3
|
+
import remarkDirective from "remark-directive";
|
|
3
4
|
import remarkGfm from "remark-gfm";
|
|
4
5
|
import { fileUrl } from "#files/url.ts";
|
|
5
6
|
import { boardOf } from "#render/board.ts";
|
|
7
|
+
import { callouts } from "#render/callouts.ts";
|
|
6
8
|
import { documentHeadings } from "#render/documentHeadings.ts";
|
|
9
|
+
import { visualDocuments } from "#render/visualDocuments.ts";
|
|
7
10
|
import { searchCollector } from "./collector.ts";
|
|
8
11
|
import { linkCollector } from "./linkCollector.ts";
|
|
9
12
|
|
|
@@ -46,7 +49,7 @@ export async function entriesOf(source: string, file: string, home: boolean, bod
|
|
|
46
49
|
await MarkdownAsync({
|
|
47
50
|
children: board?.definitions ? `${fragment}\n\n${board.definitions}` : fragment,
|
|
48
51
|
rehypePlugins: [rehypeRaw, headings.plugin, plugin, linkCollector(links)],
|
|
49
|
-
remarkPlugins: [remarkGfm],
|
|
52
|
+
remarkPlugins: [remarkGfm, remarkDirective, visualDocuments, callouts],
|
|
50
53
|
});
|
|
51
54
|
}
|
|
52
55
|
}
|