@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,41 @@
|
|
|
1
|
+
export function visualScript(): string {
|
|
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
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export function visualViewport(): string {
|
|
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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const TYPES: Readonly<Record<string, string>> = {
|
|
2
|
+
gif: "image/gif",
|
|
3
|
+
jpeg: "image/jpeg",
|
|
4
|
+
jpg: "image/jpeg",
|
|
5
|
+
png: "image/png",
|
|
6
|
+
webp: "image/webp",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function attachmentType(file: string): string | undefined {
|
|
10
|
+
const extension = file.split(".").at(-1)?.toLowerCase() ?? "";
|
|
11
|
+
return Object.hasOwn(TYPES, extension) ? TYPES[extension] : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function attachmentHref(href: string): string | undefined {
|
|
15
|
+
if (!href.startsWith("/") || href.startsWith("//")) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const url = new URL(href, "http://work-board.local");
|
|
19
|
+
if (url.pathname.startsWith("/_board/attachment/")) {
|
|
20
|
+
return href;
|
|
21
|
+
}
|
|
22
|
+
return attachmentType(url.pathname) ? `/_board/attachment${url.pathname}${url.search}${url.hash}` : undefined;
|
|
23
|
+
}
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { defineHastPlugin } from "satteri";
|
|
2
2
|
import { fileUrl } from "#files/url.ts";
|
|
3
|
+
import { attachmentHref } from "#path/attachment.ts";
|
|
3
4
|
|
|
4
5
|
export function documentLinks(file: string) {
|
|
5
6
|
return defineHastPlugin({
|
|
6
7
|
element: {
|
|
7
|
-
filter: ["a"],
|
|
8
|
+
filter: ["a", "img"],
|
|
8
9
|
visit: (element) => {
|
|
9
|
-
const
|
|
10
|
-
|
|
10
|
+
const attribute = element.tagName === "img" ? "src" : "href";
|
|
11
|
+
const href = element.properties[attribute];
|
|
12
|
+
if (typeof href !== "string" || href.startsWith("#") || href.startsWith("//") || /^[a-z][a-z\d+.-]*:/iu.exec(href) !== null) {
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
const target = new URL(href, `http://work-board.local${fileUrl(file)}`);
|
|
14
|
-
|
|
16
|
+
const resolved = target.pathname + target.search + target.hash;
|
|
17
|
+
const image = attachmentHref(resolved);
|
|
18
|
+
return {
|
|
19
|
+
...element,
|
|
20
|
+
properties: {
|
|
21
|
+
...element.properties,
|
|
22
|
+
[attribute]: image ?? resolved,
|
|
23
|
+
...(element.tagName === "img" && image ? { dataLocalImage: image } : {}),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
15
26
|
},
|
|
16
27
|
},
|
|
17
28
|
name: "work-board-document-links",
|