@shivaedev/work-board 0.5.0 → 0.6.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 +20 -0
- package/README.md +32 -2
- package/dist/board.d.ts.map +1 -1
- package/dist/board.js +2 -0
- package/dist/board.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/files/watchDirectories.d.ts.map +1 -1
- package/dist/files/watchDirectories.js +2 -1
- package/dist/files/watchDirectories.js.map +1 -1
- package/dist/http/assets.d.ts.map +1 -1
- package/dist/http/assets.js +4 -0
- package/dist/http/assets.js.map +1 -1
- package/dist/http/attachment.d.ts +4 -0
- package/dist/http/attachment.d.ts.map +1 -0
- package/dist/http/attachment.js +43 -0
- package/dist/http/attachment.js.map +1 -0
- package/dist/metadata/sourceLinks.d.ts.map +1 -1
- package/dist/metadata/sourceLinks.js +3 -1
- package/dist/metadata/sourceLinks.js.map +1 -1
- package/dist/page/client.d.ts +1 -1
- package/dist/page/client.d.ts.map +1 -1
- package/dist/page/client.js +1 -0
- package/dist/page/client.js.map +1 -1
- package/dist/page/diagrams.d.ts +1 -1
- package/dist/page/diagrams.d.ts.map +1 -1
- package/dist/page/diagrams.js +7 -0
- package/dist/page/diagrams.js.map +1 -1
- package/dist/page/shell.d.ts.map +1 -1
- package/dist/page/shell.js +3 -2
- package/dist/page/shell.js.map +1 -1
- package/dist/page/style.d.ts.map +1 -1
- package/dist/page/style.js +3 -2
- 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 +7 -2
- package/dist/page/swap.js.map +1 -1
- package/dist/page/visualDialog.d.ts +3 -0
- package/dist/page/visualDialog.d.ts.map +1 -0
- package/dist/page/visualDialog.js +25 -0
- package/dist/page/visualDialog.js.map +1 -0
- package/dist/page/visualScript.d.ts +2 -0
- package/dist/page/visualScript.d.ts.map +1 -0
- package/dist/page/visualScript.js +42 -0
- package/dist/page/visualScript.js.map +1 -0
- package/dist/page/visualViewport.d.ts +2 -0
- package/dist/page/visualViewport.d.ts.map +1 -0
- package/dist/page/visualViewport.js +87 -0
- package/dist/page/visualViewport.js.map +1 -0
- package/dist/path/attachment.d.ts +3 -0
- package/dist/path/attachment.d.ts.map +1 -0
- package/dist/path/attachment.js +22 -0
- package/dist/path/attachment.js.map +1 -0
- package/dist/render/documentLinks.d.ts +2 -1
- package/dist/render/documentLinks.d.ts.map +1 -1
- package/dist/render/documentLinks.js +15 -4
- package/dist/render/documentLinks.js.map +1 -1
- package/package.json +1 -1
- package/src/board.ts +2 -0
- package/src/cli.ts +1 -1
- package/src/files/watchDirectories.ts +2 -1
- package/src/http/assets.ts +4 -0
- package/src/http/attachment.ts +46 -0
- package/src/metadata/sourceLinks.ts +3 -1
- package/src/page/client.ts +1 -0
- package/src/page/diagrams.ts +7 -0
- package/src/page/shell.ts +3 -2
- package/src/page/style.ts +4 -2
- package/src/page/swap.ts +7 -2
- package/src/page/visualDialog.ts +25 -0
- package/src/page/visualScript.ts +41 -0
- package/src/page/visualViewport.ts +86 -0
- package/src/path/attachment.ts +23 -0
- package/src/render/documentLinks.ts +15 -4
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const visualDialog = "<dialog id=\"visual-dialog\" aria-labelledby=\"visual-title\">\n<div id=\"visual-stage\">\n<div class=\"visual-bar\"><h2 id=\"visual-title\">Visual evidence</h2><button id=\"visual-close\" type=\"button\">Close</button></div>\n<p id=\"visual-context\"></p>\n<div class=\"visual-bar\" role=\"group\" aria-label=\"Visual controls\">\n<button id=\"visual-previous\" type=\"button\">Previous image</button><button id=\"visual-next\" type=\"button\">Next image</button>\n<button id=\"visual-out\" type=\"button\" aria-label=\"Zoom out\">\u2212</button><button id=\"visual-fit\" type=\"button\">Fit</button><button id=\"visual-actual\" type=\"button\" aria-pressed=\"false\">Actual size</button><button id=\"visual-in\" type=\"button\" aria-label=\"Zoom in\">+</button>\n<button id=\"visual-fullscreen\" type=\"button\">Fullscreen</button><a id=\"visual-download\" download>Save locally</a>\n</div><p id=\"visual-status\" role=\"status\"></p><div id=\"visual-viewport\" tabindex=\"0\" aria-label=\"Scrollable visual\"><div id=\"visual-content\"></div></div>\n<p class=\"visual-help\">Arrow keys choose images \u00B7 Zoom and scroll to inspect \u00B7 Escape returns to the source. Recorded evidence does not verify acceptance.</p>\n</div></dialog>";
|
|
2
|
+
export declare const visualStyles = "\n#visual-dialog{width:min(1100px,calc(100vw - 24px));max-height:calc(100dvh - 24px);padding:16px;border:1px solid var(--border);border-radius:12px;background:var(--card);color:var(--text)}\n#visual-dialog::backdrop{background:rgb(0 0 0 / .6)}\n.visual-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.visual-bar h2{flex:1;margin:0;font-size:1.2rem}\n.visual-bar button,.visual-bar a{font:inherit;padding:6px 10px;cursor:pointer;background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px}\n#visual-context,.visual-help,#visual-status{font-size:.85rem;overflow-wrap:anywhere;color:var(--muted)}\n#visual-viewport{overflow:auto;max-height:65dvh;min-height:150px;background:var(--surface);border:1px solid var(--border);padding:12px}\n#visual-content{width:100%;min-width:0}#visual-content img,#visual-content svg{display:block;width:100%;max-width:none;height:auto}\n#visual-stage:fullscreen{width:100vw;height:100dvh;box-sizing:border-box;padding:16px;background:var(--card);color:var(--text);overflow:auto}#visual-stage:fullscreen #visual-viewport{max-height:75dvh}\n[data-local-image]{cursor:zoom-in}[data-local-image]:focus-visible{outline:2px solid var(--focus)}\n[data-visual-tools]{display:block;margin:8px 0;font-size:.85rem}[data-visual-tools] button{font:inherit;cursor:pointer;color:var(--text);background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:5px 10px}\n[data-image-error]{display:block;color:var(--danger);font-size:.85rem}\n";
|
|
3
|
+
//# sourceMappingURL=visualDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visualDialog.d.ts","sourceRoot":"","sources":["../../src/page/visualDialog.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,6tCAUT,CAAC;AAEjB,eAAO,MAAM,YAAY,g/CAYxB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const visualDialog = `<dialog id="visual-dialog" aria-labelledby="visual-title">
|
|
2
|
+
<div id="visual-stage">
|
|
3
|
+
<div class="visual-bar"><h2 id="visual-title">Visual evidence</h2><button id="visual-close" type="button">Close</button></div>
|
|
4
|
+
<p id="visual-context"></p>
|
|
5
|
+
<div class="visual-bar" role="group" aria-label="Visual controls">
|
|
6
|
+
<button id="visual-previous" type="button">Previous image</button><button id="visual-next" type="button">Next image</button>
|
|
7
|
+
<button id="visual-out" type="button" aria-label="Zoom out">−</button><button id="visual-fit" type="button">Fit</button><button id="visual-actual" type="button" aria-pressed="false">Actual size</button><button id="visual-in" type="button" aria-label="Zoom in">+</button>
|
|
8
|
+
<button id="visual-fullscreen" type="button">Fullscreen</button><a id="visual-download" download>Save locally</a>
|
|
9
|
+
</div><p id="visual-status" role="status"></p><div id="visual-viewport" tabindex="0" aria-label="Scrollable visual"><div id="visual-content"></div></div>
|
|
10
|
+
<p class="visual-help">Arrow keys choose images · Zoom and scroll to inspect · Escape returns to the source. Recorded evidence does not verify acceptance.</p>
|
|
11
|
+
</div></dialog>`;
|
|
12
|
+
export const visualStyles = `
|
|
13
|
+
#visual-dialog{width:min(1100px,calc(100vw - 24px));max-height:calc(100dvh - 24px);padding:16px;border:1px solid var(--border);border-radius:12px;background:var(--card);color:var(--text)}
|
|
14
|
+
#visual-dialog::backdrop{background:rgb(0 0 0 / .6)}
|
|
15
|
+
.visual-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.visual-bar h2{flex:1;margin:0;font-size:1.2rem}
|
|
16
|
+
.visual-bar button,.visual-bar a{font:inherit;padding:6px 10px;cursor:pointer;background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px}
|
|
17
|
+
#visual-context,.visual-help,#visual-status{font-size:.85rem;overflow-wrap:anywhere;color:var(--muted)}
|
|
18
|
+
#visual-viewport{overflow:auto;max-height:65dvh;min-height:150px;background:var(--surface);border:1px solid var(--border);padding:12px}
|
|
19
|
+
#visual-content{width:100%;min-width:0}#visual-content img,#visual-content svg{display:block;width:100%;max-width:none;height:auto}
|
|
20
|
+
#visual-stage:fullscreen{width:100vw;height:100dvh;box-sizing:border-box;padding:16px;background:var(--card);color:var(--text);overflow:auto}#visual-stage:fullscreen #visual-viewport{max-height:75dvh}
|
|
21
|
+
[data-local-image]{cursor:zoom-in}[data-local-image]:focus-visible{outline:2px solid var(--focus)}
|
|
22
|
+
[data-visual-tools]{display:block;margin:8px 0;font-size:.85rem}[data-visual-tools] button{font:inherit;cursor:pointer;color:var(--text);background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:5px 10px}
|
|
23
|
+
[data-image-error]{display:block;color:var(--danger);font-size:.85rem}
|
|
24
|
+
`;
|
|
25
|
+
//# sourceMappingURL=visualDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visualDialog.js","sourceRoot":"","sources":["../../src/page/visualDialog.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;gBAUZ,CAAC;AAEjB,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;CAY3B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visualScript.d.ts","sourceRoot":"","sources":["../../src/page/visualScript.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,IAAI,MAAM,CAwCrC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function visualScript() {
|
|
2
|
+
return `
|
|
3
|
+
import { inspectImage, inspectDiagram, inspectAttachment } from "/_board/visual-viewport.js";
|
|
4
|
+
let revision = 0;
|
|
5
|
+
const refresh = () => {
|
|
6
|
+
revision++;
|
|
7
|
+
for (const image of document.querySelectorAll("#doc img[data-local-image]")) {
|
|
8
|
+
image.tabIndex = 0; image.setAttribute("role", "button"); image.setAttribute("aria-label", "Inspect image: " + (image.alt || "Local image"));
|
|
9
|
+
const url = new URL(image.dataset.localImage, location.href); url.searchParams.set("_wb", Date.now() + "-" + revision); image.src = url.href;
|
|
10
|
+
image.onerror = () => {
|
|
11
|
+
if (!image.isConnected || image.nextElementSibling?.hasAttribute("data-image-error")) return;
|
|
12
|
+
const error = document.createElement("span"); error.setAttribute("data-image-error", ""); error.setAttribute("role", "status");
|
|
13
|
+
error.textContent = "Image unavailable. Check the local path, supported type, workspace boundary and 16 MiB limit."; image.after(error);
|
|
14
|
+
};
|
|
15
|
+
image.onload = () => { if (image.nextElementSibling?.hasAttribute("data-image-error")) image.nextElementSibling.remove(); };
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export const diagramTools = (figure) => {
|
|
19
|
+
let tools = figure.querySelector("[data-visual-tools]");
|
|
20
|
+
if (!tools) {
|
|
21
|
+
tools = document.createElement("span"); tools.setAttribute("data-visual-tools", "");
|
|
22
|
+
const button = document.createElement("button"); button.type = "button"; button.textContent = "Inspect diagram"; button.setAttribute("data-inspect-diagram", "");
|
|
23
|
+
tools.append(button); figure.append(tools);
|
|
24
|
+
}
|
|
25
|
+
tools.querySelector("button").disabled = figure.dataset.state !== "drawn";
|
|
26
|
+
};
|
|
27
|
+
document.addEventListener("click", (event) => {
|
|
28
|
+
if (event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
|
|
29
|
+
const diagram = event.target.closest?.("#doc [data-inspect-diagram]");
|
|
30
|
+
if (diagram && !diagram.disabled) { inspectDiagram(diagram.closest("figure.diagram"), diagram); return; }
|
|
31
|
+
const link = event.target.closest?.('#doc a[href^="/_board/attachment/"]');
|
|
32
|
+
if (link && !link.hasAttribute("download") && (!link.target || link.target === "_self")) { event.preventDefault(); inspectAttachment(link); return; }
|
|
33
|
+
const image = event.target.closest?.("#doc img[data-local-image]"); if (image) { event.preventDefault(); inspectImage(image); }
|
|
34
|
+
});
|
|
35
|
+
document.addEventListener("keydown", (event) => {
|
|
36
|
+
if (["Enter", " "].includes(event.key) && event.target.matches?.("#doc img[data-local-image]")) { event.preventDefault(); inspectImage(event.target); }
|
|
37
|
+
});
|
|
38
|
+
document.addEventListener("board-page", refresh);
|
|
39
|
+
refresh();
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=visualScript.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visualScript.js","sourceRoot":"","sources":["../../src/page/visualScript.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,YAAY;IAC3B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCP,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visualViewport.d.ts","sourceRoot":"","sources":["../../src/page/visualViewport.ts"],"names":[],"mappings":"AAAA,wBAAgB,cAAc,IAAI,MAAM,CAqFvC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function visualViewport() {
|
|
2
|
+
return `
|
|
3
|
+
const dialog = document.getElementById("visual-dialog");
|
|
4
|
+
const content = document.getElementById("visual-content");
|
|
5
|
+
const viewport = document.getElementById("visual-viewport");
|
|
6
|
+
const status = document.getElementById("visual-status");
|
|
7
|
+
const download = document.getElementById("visual-download");
|
|
8
|
+
let scale = 100;
|
|
9
|
+
let baseWidth = 0;
|
|
10
|
+
let objectUrl;
|
|
11
|
+
let origin;
|
|
12
|
+
let images = [];
|
|
13
|
+
let selected = -1;
|
|
14
|
+
const id = (name) => document.getElementById("visual-" + name);
|
|
15
|
+
const release = () => { if (objectUrl) URL.revokeObjectURL(objectUrl); objectUrl = undefined; };
|
|
16
|
+
const zoom = (next) => {
|
|
17
|
+
scale = Math.max(25, Math.min(800, next));
|
|
18
|
+
content.style.width = baseWidth ? (baseWidth * scale / 100) + "px" : scale + "%";
|
|
19
|
+
id("actual").setAttribute("aria-pressed", baseWidth ? "true" : "false");
|
|
20
|
+
id("fit").textContent = scale === 100 && !baseWidth ? "Fit" : Math.round(scale) + "% · Fit";
|
|
21
|
+
};
|
|
22
|
+
const fit = () => { baseWidth = 0; zoom(100); viewport.scrollTop = 0; viewport.scrollLeft = 0; };
|
|
23
|
+
const showImage = () => {
|
|
24
|
+
release(); fit(); status.textContent = "";
|
|
25
|
+
const source = images[selected];
|
|
26
|
+
const image = new Image(); image.alt = source.alt;
|
|
27
|
+
image.onerror = () => { if (content.contains(image)) { status.textContent = "Image unavailable. Check the local path, supported type, workspace boundary and 16 MiB limit."; download.removeAttribute("href"); download.setAttribute("aria-disabled", "true"); } };
|
|
28
|
+
image.src = source.src; content.replaceChildren(image);
|
|
29
|
+
id("context").textContent = (source.alt || "Local image") + " · " + (selected + 1) + " of " + images.length + " · " + source.dataset.localImage + " · " + document.getElementById("doc").dataset.file;
|
|
30
|
+
download.href = source.dataset.localImage;
|
|
31
|
+
try { download.download = decodeURIComponent(new URL(download.href).pathname.split("/").at(-1)); } catch { download.download = "work-board-image"; }
|
|
32
|
+
download.setAttribute("aria-disabled", "false");
|
|
33
|
+
id("previous").disabled = selected <= 0; id("next").disabled = selected >= images.length - 1;
|
|
34
|
+
};
|
|
35
|
+
export const inspectImage = (source) => {
|
|
36
|
+
origin = source; images = [...document.querySelectorAll("#doc img[data-local-image]")]; selected = images.indexOf(source);
|
|
37
|
+
id("previous").hidden = false; id("next").hidden = false; showImage(); dialog.showModal();
|
|
38
|
+
};
|
|
39
|
+
export const inspectAttachment = (link) => {
|
|
40
|
+
origin = link; const source = { alt: link.textContent, dataset: { localImage: link.getAttribute("href") }, src: link.href };
|
|
41
|
+
images = [source]; selected = 0; id("previous").hidden = false; id("next").hidden = false; showImage(); dialog.showModal();
|
|
42
|
+
};
|
|
43
|
+
export const inspectDiagram = (figure, trigger) => {
|
|
44
|
+
const drawing = figure.querySelector(".diagram-svg svg");
|
|
45
|
+
if (!drawing) return;
|
|
46
|
+
origin = trigger; selected = -1; images = []; release(); fit(); status.textContent = "";
|
|
47
|
+
const copy = drawing.cloneNode(true); copy.style.maxWidth = "none"; copy.style.width = "100%"; copy.style.height = "auto";
|
|
48
|
+
content.replaceChildren(copy);
|
|
49
|
+
id("context").textContent = "Mermaid diagram · " + document.getElementById("doc").dataset.file + " · Current drawing";
|
|
50
|
+
id("previous").hidden = true; id("next").hidden = true;
|
|
51
|
+
const xml = new XMLSerializer().serializeToString(drawing);
|
|
52
|
+
objectUrl = URL.createObjectURL(new Blob([xml], { type: "image/svg+xml" }));
|
|
53
|
+
download.href = objectUrl; download.download = "work-board-diagram.svg"; dialog.showModal();
|
|
54
|
+
download.setAttribute("aria-disabled", "false");
|
|
55
|
+
};
|
|
56
|
+
const move = (offset) => { if (selected < 0) return; selected = Math.max(0, Math.min(images.length - 1, selected + offset)); showImage(); };
|
|
57
|
+
id("previous").addEventListener("click", () => move(-1)); id("next").addEventListener("click", () => move(1));
|
|
58
|
+
id("in").addEventListener("click", () => zoom(scale * 1.5)); id("out").addEventListener("click", () => zoom(scale / 1.5));
|
|
59
|
+
id("fit").addEventListener("click", fit);
|
|
60
|
+
id("actual").addEventListener("click", () => {
|
|
61
|
+
const graphic = content.firstElementChild;
|
|
62
|
+
const width = graphic?.naturalWidth || graphic?.viewBox?.baseVal?.width;
|
|
63
|
+
if (!width) { status.textContent = "Original dimensions unavailable; Fit and zoom remain available."; return; }
|
|
64
|
+
baseWidth = width; zoom(100); viewport.scrollTop = 0; viewport.scrollLeft = 0;
|
|
65
|
+
});
|
|
66
|
+
id("close").addEventListener("click", () => dialog.close());
|
|
67
|
+
id("fullscreen").addEventListener("click", async () => {
|
|
68
|
+
try { if (document.fullscreenElement) await document.exitFullscreen(); else await id("stage").requestFullscreen(); }
|
|
69
|
+
catch { status.textContent = "Fullscreen is unavailable here; the scrollable viewer remains available."; }
|
|
70
|
+
});
|
|
71
|
+
dialog.addEventListener("keydown", (event) => {
|
|
72
|
+
if (event.key === "ArrowLeft" && selected >= 0) { event.preventDefault(); move(-1); }
|
|
73
|
+
if (event.key === "ArrowRight" && selected >= 0) { event.preventDefault(); move(1); }
|
|
74
|
+
if (event.key === "Escape") { event.preventDefault(); dialog.close(); }
|
|
75
|
+
});
|
|
76
|
+
dialog.addEventListener("close", async () => {
|
|
77
|
+
release(); content.replaceChildren();
|
|
78
|
+
if (dialog.contains(document.fullscreenElement)) await document.exitFullscreen().catch(() => {});
|
|
79
|
+
(origin?.isConnected ? origin : document.getElementById("doc")).focus();
|
|
80
|
+
});
|
|
81
|
+
document.addEventListener("board-page", () => {
|
|
82
|
+
if (dialog.open) { status.textContent = "Source updated. This preview is the drawing/image opened earlier; close and reopen to inspect or save the latest source."; download.removeAttribute("href"); download.setAttribute("aria-disabled", "true"); }
|
|
83
|
+
});
|
|
84
|
+
window.addEventListener("pagehide", () => { release(); if (dialog.open) dialog.close(); });
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=visualViewport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visualViewport.js","sourceRoot":"","sources":["../../src/page/visualViewport.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,cAAc;IAC7B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFP,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment.d.ts","sourceRoot":"","sources":["../../src/path/attachment.ts"],"names":[],"mappings":"AAQA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG/D;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAS/D"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const TYPES = {
|
|
2
|
+
gif: "image/gif",
|
|
3
|
+
jpeg: "image/jpeg",
|
|
4
|
+
jpg: "image/jpeg",
|
|
5
|
+
png: "image/png",
|
|
6
|
+
webp: "image/webp",
|
|
7
|
+
};
|
|
8
|
+
export function attachmentType(file) {
|
|
9
|
+
const extension = file.split(".").at(-1)?.toLowerCase() ?? "";
|
|
10
|
+
return Object.hasOwn(TYPES, extension) ? TYPES[extension] : undefined;
|
|
11
|
+
}
|
|
12
|
+
export function attachmentHref(href) {
|
|
13
|
+
if (!href.startsWith("/") || href.startsWith("//")) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const url = new URL(href, "http://work-board.local");
|
|
17
|
+
if (url.pathname.startsWith("/_board/attachment/")) {
|
|
18
|
+
return href;
|
|
19
|
+
}
|
|
20
|
+
return attachmentType(url.pathname) ? `/_board/attachment${url.pathname}${url.search}${url.hash}` : undefined;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=attachment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment.js","sourceRoot":"","sources":["../../src/path/attachment.ts"],"names":[],"mappings":"AAAA,MAAM,KAAK,GAAqC;IAC/C,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,YAAY;IACjB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;CAClB,CAAC;AAEF,MAAM,UAAU,cAAc,CAAC,IAAY;IAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAC9D,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY;IAC1C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACpD,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,yBAAyB,CAAC,CAAC;IACrD,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,qBAAqB,CAAC,EAAE,CAAC;QACpD,OAAO,IAAI,CAAC;IACb,CAAC;IACD,OAAO,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,qBAAqB,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/G,CAAC"}
|
|
@@ -238,6 +238,7 @@ export declare function documentLinks(file: string): {
|
|
|
238
238
|
horizAdvX?: number | string | undefined;
|
|
239
239
|
horizOriginX?: number | string | undefined;
|
|
240
240
|
horizOriginY?: number | string | undefined;
|
|
241
|
+
href?: string | undefined;
|
|
241
242
|
hrefLang?: string | undefined;
|
|
242
243
|
hSpace?: number | string | undefined;
|
|
243
244
|
htmlFor?: Array<string> | undefined;
|
|
@@ -650,7 +651,7 @@ export declare function documentLinks(file: string): {
|
|
|
650
651
|
yChannelSelector?: string | undefined;
|
|
651
652
|
z?: string | undefined;
|
|
652
653
|
zoomAndPan?: string | undefined;
|
|
653
|
-
|
|
654
|
+
dataLocalImage?: string;
|
|
654
655
|
};
|
|
655
656
|
} | undefined;
|
|
656
657
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documentLinks.d.ts","sourceRoot":"","sources":["../../src/render/documentLinks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"documentLinks.d.ts","sourceRoot":"","sources":["../../src/render/documentLinks.ts"],"names":[],"mappings":"AAIA,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyBzC"}
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { defineHastPlugin } from "satteri";
|
|
2
2
|
import { fileUrl } from "#files/url.ts";
|
|
3
|
+
import { attachmentHref } from "#path/attachment.ts";
|
|
3
4
|
export function documentLinks(file) {
|
|
4
5
|
return defineHastPlugin({
|
|
5
6
|
element: {
|
|
6
|
-
filter: ["a"],
|
|
7
|
+
filter: ["a", "img"],
|
|
7
8
|
visit: (element) => {
|
|
8
|
-
const
|
|
9
|
-
|
|
9
|
+
const attribute = element.tagName === "img" ? "src" : "href";
|
|
10
|
+
const href = element.properties[attribute];
|
|
11
|
+
if (typeof href !== "string" || href.startsWith("#") || href.startsWith("//") || /^[a-z][a-z\d+.-]*:/iu.exec(href) !== null) {
|
|
10
12
|
return;
|
|
11
13
|
}
|
|
12
14
|
const target = new URL(href, `http://work-board.local${fileUrl(file)}`);
|
|
13
|
-
|
|
15
|
+
const resolved = target.pathname + target.search + target.hash;
|
|
16
|
+
const image = attachmentHref(resolved);
|
|
17
|
+
return {
|
|
18
|
+
...element,
|
|
19
|
+
properties: {
|
|
20
|
+
...element.properties,
|
|
21
|
+
[attribute]: image ?? resolved,
|
|
22
|
+
...(element.tagName === "img" && image ? { dataLocalImage: image } : {}),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
14
25
|
},
|
|
15
26
|
},
|
|
16
27
|
name: "work-board-document-links",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documentLinks.js","sourceRoot":"","sources":["../../src/render/documentLinks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"documentLinks.js","sourceRoot":"","sources":["../../src/render/documentLinks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,UAAU,aAAa,CAAC,IAAY;IACzC,OAAO,gBAAgB,CAAC;QACvB,OAAO,EAAE;YACR,MAAM,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC;YACpB,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE;gBAClB,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;gBAC7D,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;gBAC3C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;oBAC7H,OAAO;gBACR,CAAC;gBACD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,0BAA0B,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACxE,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC;gBAC/D,MAAM,KAAK,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;gBACvC,OAAO;oBACN,GAAG,OAAO;oBACV,UAAU,EAAE;wBACX,GAAG,OAAO,CAAC,UAAU;wBACrB,CAAC,SAAS,CAAC,EAAE,KAAK,IAAI,QAAQ;wBAC9B,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;qBACxE;iBACD,CAAC;YACH,CAAC;SACD;QACD,IAAI,EAAE,2BAA2B;KACjC,CAAC,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/work-board",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Serve a folder of markdown files as a live local page that updates in place when a file changes",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/work-board#readme",
|
|
6
6
|
"bugs": {
|
package/src/board.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { HttpRouter, HttpServerRequest, type HttpServerResponse } from "effect/u
|
|
|
3
3
|
import { watchChanges } from "#files/changes.ts";
|
|
4
4
|
import { type HomeMissing, homeIn } from "#files/home.ts";
|
|
5
5
|
import { ASSETS, MERMAID_ROUTE, type MermaidMissing, mermaidFile, mermaidRoot, NATIVE_ROUTE, nativeAsset } from "#http/assets.ts";
|
|
6
|
+
import { attachment } from "#http/attachment.ts";
|
|
6
7
|
import { events } from "#http/events.ts";
|
|
7
8
|
import { changesPage, history } from "#http/history.ts";
|
|
8
9
|
import { identity } from "#http/identity.ts";
|
|
@@ -40,6 +41,7 @@ const routes = (options: BoardOptions) =>
|
|
|
40
41
|
loopbackOnly((request) => Effect.provideContext(handler(request), context)),
|
|
41
42
|
);
|
|
42
43
|
yield* serve("/events", events(changes));
|
|
44
|
+
yield* serve("/_board/attachment/*", attachment(options.root, changes.realRoot));
|
|
43
45
|
const index = yield* searchSnapshot(options.root, home, changes);
|
|
44
46
|
yield* serve("/_board/search", search(index));
|
|
45
47
|
const workPage = work(index, changes, home);
|
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { Effect, Layer, Option, Path } from "effect";
|
|
|
4
4
|
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
5
5
|
import { HOST, serveBoard } from "./serve.ts";
|
|
6
6
|
|
|
7
|
-
const VERSION = "0.
|
|
7
|
+
const VERSION = "0.6.0";
|
|
8
8
|
|
|
9
9
|
const serve = Effect.fn("WorkBoard.serve")(function* (input: { readonly dir: string; readonly port: number; readonly home: Option.Option<string> }) {
|
|
10
10
|
const path = yield* Path.Path;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Data, Deferred, Effect, FileSystem, Option, Path, Stream } from "effect";
|
|
2
|
+
import { attachmentType } from "#path/attachment.ts";
|
|
2
3
|
import { isMarkdown, scanMarkdown } from "./list.ts";
|
|
3
4
|
|
|
4
5
|
class WatchEnded extends Data.TaggedError("WatchEnded") {}
|
|
@@ -45,7 +46,7 @@ export const watchDirectories = Effect.fn("WorkBoard.watchDirectories")(function
|
|
|
45
46
|
}
|
|
46
47
|
}),
|
|
47
48
|
),
|
|
48
|
-
Stream.filter(isMarkdown),
|
|
49
|
+
Stream.filter((relative) => isMarkdown(relative) || attachmentType(relative) !== undefined),
|
|
49
50
|
Stream.interruptWhen(Deferred.await(topology)),
|
|
50
51
|
Stream.groupedWithin(BATCH, SETTLE),
|
|
51
52
|
Stream.runForEach((paths) => publish([...new Set(paths)])),
|
package/src/http/assets.ts
CHANGED
|
@@ -13,11 +13,15 @@ import { searchScript } from "#page/searchScript.ts";
|
|
|
13
13
|
import { pageStateScript } from "#page/stateScript.ts";
|
|
14
14
|
import { style } from "#page/style.ts";
|
|
15
15
|
import { swap } from "#page/swap.ts";
|
|
16
|
+
import { visualScript } from "#page/visualScript.ts";
|
|
17
|
+
import { visualViewport } from "#page/visualViewport.ts";
|
|
16
18
|
import { savedScript } from "#views/savedScript.ts";
|
|
17
19
|
import { respond } from "./respond.ts";
|
|
18
20
|
|
|
19
21
|
export const ASSETS: ReadonlyArray<readonly [HttpRouter.PathInput, string, string]> = [
|
|
20
22
|
["/_board/style.css", style, "text/css"],
|
|
23
|
+
["/_board/visuals.js", visualScript(), "text/javascript"],
|
|
24
|
+
["/_board/visual-viewport.js", visualViewport(), "text/javascript"],
|
|
21
25
|
["/_board/saved-views.js", savedScript(), "text/javascript"],
|
|
22
26
|
["/_board/client.js", client, "text/javascript"],
|
|
23
27
|
["/_board/ages.js", ageScript(), "text/javascript"],
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path } from "effect";
|
|
2
|
+
import type { HttpServerRequest } from "effect/unstable/http";
|
|
3
|
+
import { within } from "#files/list.ts";
|
|
4
|
+
import { attachmentType } from "#path/attachment.ts";
|
|
5
|
+
import { respond } from "./respond.ts";
|
|
6
|
+
|
|
7
|
+
const MAX_BYTES = 16 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export function attachment(root: string, realRoot: string) {
|
|
10
|
+
return Effect.fn("WorkBoard.attachment")(function* (request: HttpServerRequest.HttpServerRequest) {
|
|
11
|
+
function missing() {
|
|
12
|
+
return respond("Local image unavailable: unsupported type, missing file or workspace boundary.", "text/plain", 404);
|
|
13
|
+
}
|
|
14
|
+
let relative: string;
|
|
15
|
+
try {
|
|
16
|
+
relative = decodeURIComponent(new URL(request.url, "http://127.0.0.1").pathname.slice("/_board/attachment/".length));
|
|
17
|
+
} catch {
|
|
18
|
+
return missing();
|
|
19
|
+
}
|
|
20
|
+
const type = attachmentType(relative);
|
|
21
|
+
if (
|
|
22
|
+
!type
|
|
23
|
+
|| relative.includes("\\")
|
|
24
|
+
|| relative.split("/").some((part) => !part || part === ".." || part.startsWith(".") || part === "node_modules")
|
|
25
|
+
) {
|
|
26
|
+
return missing();
|
|
27
|
+
}
|
|
28
|
+
const fs = yield* FileSystem.FileSystem;
|
|
29
|
+
const path = yield* Path.Path;
|
|
30
|
+
return yield* Effect.gen(function* () {
|
|
31
|
+
const real = yield* fs.realPath(path.join(root, relative));
|
|
32
|
+
if (!within(realRoot, path.sep, real)) {
|
|
33
|
+
return missing();
|
|
34
|
+
}
|
|
35
|
+
const info = yield* fs.stat(real);
|
|
36
|
+
if (info.type !== "File") {
|
|
37
|
+
return missing();
|
|
38
|
+
}
|
|
39
|
+
if (info.size > BigInt(MAX_BYTES)) {
|
|
40
|
+
return respond("Local image exceeds the 16 MiB limit.", "text/plain", 413);
|
|
41
|
+
}
|
|
42
|
+
const body = yield* fs.readFile(real);
|
|
43
|
+
return body.byteLength > MAX_BYTES ? respond("Local image exceeds the 16 MiB limit.", "text/plain", 413) : respond(body, type);
|
|
44
|
+
}).pipe(Effect.orElseSucceed(missing));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fileUrl } from "#files/url.ts";
|
|
2
|
+
import { attachmentHref } from "#path/attachment.ts";
|
|
2
3
|
import { referenceTarget } from "./links.ts";
|
|
3
4
|
import type { MetadataModel } from "./model.ts";
|
|
4
5
|
|
|
@@ -11,7 +12,8 @@ export function sourceHref(source: string, file: string): string | undefined {
|
|
|
11
12
|
if (!["http:", "https:"].includes(url.protocol)) {
|
|
12
13
|
return undefined;
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
const local = url.pathname + url.search + url.hash;
|
|
16
|
+
return absolute.exec(source) === null ? (attachmentHref(local) ?? local) : url.href;
|
|
15
17
|
} catch {
|
|
16
18
|
return undefined;
|
|
17
19
|
}
|
package/src/page/client.ts
CHANGED
package/src/page/diagrams.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const diagrams = `
|
|
2
|
+
import { diagramTools } from "/_board/visuals.js";
|
|
2
3
|
const rendered = new Map();
|
|
3
4
|
const dark = matchMedia("(prefers-color-scheme: dark)");
|
|
4
5
|
let mermaid;
|
|
@@ -43,14 +44,20 @@ const draw = async (figure) => {
|
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
figure.querySelector(".diagram-svg")?.remove();
|
|
47
|
+
figure.querySelector("[data-diagram-error]")?.remove();
|
|
46
48
|
figure.insertAdjacentHTML("afterbegin", '<div class="diagram-svg">' + svg + "</div>");
|
|
47
49
|
figure.dataset.state = "drawn";
|
|
50
|
+
diagramTools(figure);
|
|
48
51
|
} catch (error) {
|
|
49
52
|
rendered.delete(key);
|
|
50
53
|
if (scheme !== schemeNow()) return;
|
|
51
54
|
figure.querySelector(".diagram-svg")?.remove();
|
|
52
55
|
figure.dataset.state = "failed";
|
|
53
56
|
figure.dataset.error = String(error?.message ?? error);
|
|
57
|
+
figure.querySelector("[data-diagram-error]")?.remove();
|
|
58
|
+
const message = document.createElement("p"); message.setAttribute("data-diagram-error", ""); message.setAttribute("role", "status");
|
|
59
|
+
message.textContent = "Diagram could not be drawn: " + figure.dataset.error; figure.append(message);
|
|
60
|
+
diagramTools(figure);
|
|
54
61
|
}
|
|
55
62
|
};
|
|
56
63
|
|
package/src/page/shell.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fileUrl } from "#files/url.ts";
|
|
2
2
|
import { escapeHtml } from "./escape.ts";
|
|
3
3
|
import { searchDialog } from "./searchDialog.ts";
|
|
4
|
+
import { visualDialog } from "./visualDialog.ts";
|
|
4
5
|
|
|
5
6
|
function location(file: string): string {
|
|
6
7
|
return file
|
|
@@ -23,7 +24,7 @@ export const shell = (
|
|
|
23
24
|
'<meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light dark">',
|
|
24
25
|
`<title>${escapeHtml(title)}</title>`,
|
|
25
26
|
'<link rel="stylesheet" href="/_board/style.css"><script type="module" src="/_board/client.js"></script>',
|
|
26
|
-
'</head><body><a class="skip" href="#doc">Skip to content</a><div class="workspace">',
|
|
27
|
+
'</head><body><noscript><style>html:not([data-js]) figure.diagram .diagram-source{display:block}html:not([data-js]) figure.diagram[data-state="pending"]{min-height:0}</style></noscript><a class="skip" href="#doc">Skip to content</a><div class="workspace">',
|
|
27
28
|
'<aside id="sidebar" class="sidebar"><a class="brand" href="/">work board<span>Local workspace</span></a>',
|
|
28
29
|
'<div id="reading-library" class="reading-library" hidden></div><h2 class="nav-heading">Project files</h2>',
|
|
29
30
|
`<nav id="files" class="files" aria-label="Project files">${nav}</nav></aside>`,
|
|
@@ -35,5 +36,5 @@ export const shell = (
|
|
|
35
36
|
'<div class="preference"><label for="density">Density</label><select id="density"><option value="comfortable">Comfortable</option><option value="compact">Compact</option></select></div>',
|
|
36
37
|
'<button id="updates-toggle" type="button" aria-pressed="false" disabled>Pause updates</button><span id="preference-status" role="status"></span></div>',
|
|
37
38
|
`<main${view ? ` data-view="${typeof view === "string" ? 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>`,
|
|
38
|
-
`</div></div>${searchDialog}</body></html>`,
|
|
39
|
+
`</div></div>${searchDialog}${visualDialog}</body></html>`,
|
|
39
40
|
].join("");
|
package/src/page/style.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { workStyle } from "#views/workStyle.ts";
|
|
|
3
3
|
import { metadataStyles } from "./metadata.ts";
|
|
4
4
|
import { readingStyles } from "./readingStyles.ts";
|
|
5
5
|
import { searchStyles } from "./searchDialog.ts";
|
|
6
|
+
import { visualStyles } from "./visualDialog.ts";
|
|
6
7
|
import { layout, theme } from "./workspaceStyles.ts";
|
|
7
8
|
|
|
8
9
|
const prose = `
|
|
@@ -53,8 +54,9 @@ figure.diagram { margin-left: 0; margin-right: 0; }
|
|
|
53
54
|
figure.diagram[data-state="pending"] { min-height: 12rem; background: var(--surface); border-radius: 6px; }
|
|
54
55
|
figure.diagram .diagram-source { display: none; }
|
|
55
56
|
figure.diagram[data-state="failed"] .diagram-source { display: block; }
|
|
56
|
-
|
|
57
|
+
[data-diagram-error] { color: var(--danger); font-size: 0.875em; }
|
|
57
58
|
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
|
|
58
59
|
`;
|
|
59
60
|
|
|
60
|
-
export const style =
|
|
61
|
+
export const style =
|
|
62
|
+
theme + layout + prose + board + diagrams + readingStyles() + searchStyles + metadataStyles + workStyle() + attentionStyle() + visualStyles;
|
package/src/page/swap.ts
CHANGED
|
@@ -12,12 +12,13 @@ const flash = (element) => {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export const remember = (element) => {
|
|
15
|
-
pristine.set(element, element
|
|
15
|
+
pristine.set(element, normalized(element));
|
|
16
16
|
if (containers.has(element.tagName)) for (const child of element.children) remember(child);
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
const normalized = (element) => {
|
|
20
20
|
const copy = element.cloneNode(true);
|
|
21
|
+
for (const tool of copy.querySelectorAll("[data-visual-tools], [data-image-error], [data-diagram-error]")) tool.remove();
|
|
21
22
|
for (const node of [copy, ...copy.querySelectorAll("*")]) {
|
|
22
23
|
node.removeAttribute("data-live-change");
|
|
23
24
|
if (node.tagName === "DETAILS") node.removeAttribute("open");
|
|
@@ -27,6 +28,10 @@ const normalized = (element) => {
|
|
|
27
28
|
delete node.dataset.error;
|
|
28
29
|
}
|
|
29
30
|
if (node.matches("[data-modified]")) node.textContent = "";
|
|
31
|
+
if (node.matches("img[data-local-image]")) {
|
|
32
|
+
node.setAttribute("src", node.dataset.localImage);
|
|
33
|
+
for (const attribute of ["tabindex", "role", "aria-label"]) node.removeAttribute(attribute);
|
|
34
|
+
}
|
|
30
35
|
}
|
|
31
36
|
return copy.outerHTML;
|
|
32
37
|
};
|
|
@@ -75,7 +80,7 @@ const patch = (current, incoming, highlight) => {
|
|
|
75
80
|
}
|
|
76
81
|
const olds = [...current.children];
|
|
77
82
|
const news = [...incoming.children];
|
|
78
|
-
const signatures = news.map(
|
|
83
|
+
const signatures = news.map(normalized);
|
|
79
84
|
const arrive = (index) => {
|
|
80
85
|
remember(news[index]);
|
|
81
86
|
if (highlight) flash(news[index]);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const visualDialog = `<dialog id="visual-dialog" aria-labelledby="visual-title">
|
|
2
|
+
<div id="visual-stage">
|
|
3
|
+
<div class="visual-bar"><h2 id="visual-title">Visual evidence</h2><button id="visual-close" type="button">Close</button></div>
|
|
4
|
+
<p id="visual-context"></p>
|
|
5
|
+
<div class="visual-bar" role="group" aria-label="Visual controls">
|
|
6
|
+
<button id="visual-previous" type="button">Previous image</button><button id="visual-next" type="button">Next image</button>
|
|
7
|
+
<button id="visual-out" type="button" aria-label="Zoom out">−</button><button id="visual-fit" type="button">Fit</button><button id="visual-actual" type="button" aria-pressed="false">Actual size</button><button id="visual-in" type="button" aria-label="Zoom in">+</button>
|
|
8
|
+
<button id="visual-fullscreen" type="button">Fullscreen</button><a id="visual-download" download>Save locally</a>
|
|
9
|
+
</div><p id="visual-status" role="status"></p><div id="visual-viewport" tabindex="0" aria-label="Scrollable visual"><div id="visual-content"></div></div>
|
|
10
|
+
<p class="visual-help">Arrow keys choose images · Zoom and scroll to inspect · Escape returns to the source. Recorded evidence does not verify acceptance.</p>
|
|
11
|
+
</div></dialog>`;
|
|
12
|
+
|
|
13
|
+
export const visualStyles = `
|
|
14
|
+
#visual-dialog{width:min(1100px,calc(100vw - 24px));max-height:calc(100dvh - 24px);padding:16px;border:1px solid var(--border);border-radius:12px;background:var(--card);color:var(--text)}
|
|
15
|
+
#visual-dialog::backdrop{background:rgb(0 0 0 / .6)}
|
|
16
|
+
.visual-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.visual-bar h2{flex:1;margin:0;font-size:1.2rem}
|
|
17
|
+
.visual-bar button,.visual-bar a{font:inherit;padding:6px 10px;cursor:pointer;background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px}
|
|
18
|
+
#visual-context,.visual-help,#visual-status{font-size:.85rem;overflow-wrap:anywhere;color:var(--muted)}
|
|
19
|
+
#visual-viewport{overflow:auto;max-height:65dvh;min-height:150px;background:var(--surface);border:1px solid var(--border);padding:12px}
|
|
20
|
+
#visual-content{width:100%;min-width:0}#visual-content img,#visual-content svg{display:block;width:100%;max-width:none;height:auto}
|
|
21
|
+
#visual-stage:fullscreen{width:100vw;height:100dvh;box-sizing:border-box;padding:16px;background:var(--card);color:var(--text);overflow:auto}#visual-stage:fullscreen #visual-viewport{max-height:75dvh}
|
|
22
|
+
[data-local-image]{cursor:zoom-in}[data-local-image]:focus-visible{outline:2px solid var(--focus)}
|
|
23
|
+
[data-visual-tools]{display:block;margin:8px 0;font-size:.85rem}[data-visual-tools] button{font:inherit;cursor:pointer;color:var(--text);background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:5px 10px}
|
|
24
|
+
[data-image-error]{display:block;color:var(--danger);font-size:.85rem}
|
|
25
|
+
`;
|