openink 0.1.0 → 0.1.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 +21 -0
- package/README.md +7 -0
- package/package.json +56 -10
- package/src/cli.js +1 -1
- package/src/dev.js +16 -1
- package/src/export.js +2 -2
- package/src/runtime/index.js +2 -0
- package/src/runtime/navigation.js +9 -4
- package/src/runtime/print.js +53 -0
- package/src/styles/openink.css +10 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes are documented here. The format follows [Keep a Changelog](h
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.1] - 2026-10-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Documentation website at <https://mediuswareltd.github.io/openink/> with search, live clickable demos of every example,
|
|
12
|
+
a recipes page of ready-to-paste specs (charts, forms, pricing, tables, modals, mobile) shown beside live previews, and an
|
|
13
|
+
About page.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `openink dev` uses the next free port (3001, 3002, … up to 10 tries) when the requested one is already in use, instead of failing with `EADDRINUSE`.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- A prototype embedded in an `<iframe>` no longer scrolls the surrounding page down to itself when it loads. Opening a
|
|
22
|
+
prototype without a `#screen` in the URL no longer adds one for the start screen.
|
|
23
|
+
- A tall screen is no longer split across two PDF pages. Each screen is scaled down to fit one A4 landscape page, so the
|
|
24
|
+
PDF has exactly one page per screen. This applies to `openink pdf`, the prototype's PDF button and the browser's
|
|
25
|
+
own print (Ctrl+P).
|
|
26
|
+
- The PDF button now draws the sketched outlines of screens that had never been opened, as `openink pdf` already did.
|
|
27
|
+
|
|
7
28
|
## [0.1.0]
|
|
8
29
|
|
|
9
30
|
First release.
|
package/README.md
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
Static HTML you can host or open from disk, plus PDF and PNG export. Built on [wired-elements](https://github.com/rough-stuff/wired-elements) and [RoughJS](https://github.com/rough-stuff/rough), so it looks like a sketch and nobody mistakes it for the final design. Keep it plain pencil, or turn on colour for a whole project or just one part.
|
|
11
11
|
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://mediuswareltd.github.io/openink/"><img src="assets/openink-explainer-poster.jpg" width="720" alt="Open Ink in 35 seconds: watch the explainer video" /></a><br />
|
|
14
|
+
<sub>▶ <a href="https://mediuswareltd.github.io/openink/">Watch Open Ink in 35 seconds</a> (<a href="assets/openink-explainer.mp4">MP4</a>)</sub>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
12
17
|
<p>
|
|
13
18
|
<img src="docs/img/photo-feed.png" width="32%" alt="Photo-sharing app: feed with stories, carousel and action icons" />
|
|
14
19
|
<img src="docs/img/photo-mobile.png" width="32%" alt="The same feed inside a phone frame with a tab bar" />
|
|
@@ -139,6 +144,8 @@ npx openink dev examples/photo-sharing
|
|
|
139
144
|
|
|
140
145
|
## Documentation
|
|
141
146
|
|
|
147
|
+
**[mediuswareltd.github.io/openink](https://mediuswareltd.github.io/openink/)**: the full docs, with search and live demos of every example.
|
|
148
|
+
|
|
142
149
|
- [Getting started](docs/getting-started.md)
|
|
143
150
|
- [Spec reference](docs/spec.md): screens, colour, modals, navigation, languages, icons
|
|
144
151
|
- [Block reference](docs/blocks.md)
|
package/package.json
CHANGED
|
@@ -1,24 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openink",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Describe screens in YAML, get a clickable hand-drawn wireframe prototype (static HTML, PDF, PNG). Built on wired-elements and RoughJS.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"wireframe",
|
|
7
|
+
"prototype",
|
|
8
|
+
"sketch",
|
|
9
|
+
"mockup",
|
|
10
|
+
"hand-drawn",
|
|
11
|
+
"wired-elements",
|
|
12
|
+
"roughjs",
|
|
13
|
+
"ux",
|
|
14
|
+
"static-site-generator"
|
|
15
|
+
],
|
|
6
16
|
"license": "MIT",
|
|
7
17
|
"author": "Mediusware",
|
|
8
|
-
"repository": {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/mediuswareltd/openink.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://mediuswareltd.github.io/openink/",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/mediuswareltd/openink/issues"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
12
29
|
"type": "module",
|
|
13
|
-
"bin": {
|
|
30
|
+
"bin": {
|
|
31
|
+
"openink": "bin/openink.js"
|
|
32
|
+
},
|
|
14
33
|
"main": "./src/index.js",
|
|
15
34
|
"exports": {
|
|
16
35
|
".": "./src/index.js",
|
|
17
36
|
"./schema.json": "./schema/spec.schema.json",
|
|
18
37
|
"./package.json": "./package.json"
|
|
19
38
|
},
|
|
20
|
-
"files": [
|
|
21
|
-
|
|
39
|
+
"files": [
|
|
40
|
+
"bin",
|
|
41
|
+
"src",
|
|
42
|
+
"templates",
|
|
43
|
+
"schema",
|
|
44
|
+
"docs",
|
|
45
|
+
"!docs/img",
|
|
46
|
+
"!docs/.vitepress",
|
|
47
|
+
"!docs/public",
|
|
48
|
+
"!docs/index.md",
|
|
49
|
+
"!docs/examples.md",
|
|
50
|
+
"!docs/about.md",
|
|
51
|
+
"!docs/snippets",
|
|
52
|
+
"!docs/recipes.md",
|
|
53
|
+
"LICENSE",
|
|
54
|
+
"README.md",
|
|
55
|
+
"CHANGELOG.md"
|
|
56
|
+
],
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=20"
|
|
59
|
+
},
|
|
22
60
|
"scripts": {
|
|
23
61
|
"generate": "node scripts/generate.js",
|
|
24
62
|
"test": "node --test \"test/*.test.js\"",
|
|
@@ -26,7 +64,12 @@
|
|
|
26
64
|
"prepublishOnly": "npm test",
|
|
27
65
|
"lint:commit": "node scripts/commit-lint.js",
|
|
28
66
|
"screenshots": "node scripts/screenshots.js",
|
|
29
|
-
"dev": "node bin/openink.js dev examples/rental-portal"
|
|
67
|
+
"dev": "node bin/openink.js dev examples/rental-portal",
|
|
68
|
+
"docs:demos": "node scripts/build-demos.js",
|
|
69
|
+
"docs:dev": "npm run docs:demos && vitepress dev docs",
|
|
70
|
+
"docs:build": "npm run docs:demos && vitepress build docs",
|
|
71
|
+
"docs:preview": "vitepress preview docs",
|
|
72
|
+
"docs:deploy": "npm run docs:build && node scripts/deploy-docs.js"
|
|
30
73
|
},
|
|
31
74
|
"dependencies": {
|
|
32
75
|
"esbuild": "^0.28.2",
|
|
@@ -34,5 +77,8 @@
|
|
|
34
77
|
"roughjs": "4.3.1",
|
|
35
78
|
"wired-elements": "3.0.0-rc.6",
|
|
36
79
|
"yaml": "^2.5.0"
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"vitepress": "^1.6.4"
|
|
37
83
|
}
|
|
38
84
|
}
|
package/src/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ Commands:
|
|
|
23
23
|
|
|
24
24
|
Options:
|
|
25
25
|
--out <dir> Output directory, relative to the project (default: dist)
|
|
26
|
-
--port <n> Dev server port (default: 3000)
|
|
26
|
+
--port <n> Dev server port (default: 3000; the next free port if taken)
|
|
27
27
|
--theme <name> Try a colour theme without editing the spec: sketch, color, pastel, blueprint, dark
|
|
28
28
|
-v, --version Print the version
|
|
29
29
|
-h, --help Show this help
|
package/src/dev.js
CHANGED
|
@@ -9,6 +9,8 @@ const MIME = {
|
|
|
9
9
|
".webp": "image/webp", ".ico": "image/x-icon", ".woff2": "font/woff2", ".pdf": "application/pdf",
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
const MAX_PORT_TRIES = 10;
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Serve the project with live reload: rebuilds when files change and the browser refreshes itself.
|
|
14
16
|
* @param {{ dir?: string, out?: string, port?: number, theme?: string, log?: (msg: string) => void, onIssues?: (e: SpecError) => void }} [opts]
|
|
@@ -38,7 +40,20 @@ export async function dev({ dir = ".", out = ".openink-dev", port = 3000, theme,
|
|
|
38
40
|
res.writeHead(200, { "Content-Type": MIME[path.extname(file)] || "application/octet-stream", "Cache-Control": "no-store" });
|
|
39
41
|
fs.createReadStream(file).pipe(res);
|
|
40
42
|
});
|
|
41
|
-
|
|
43
|
+
// If the port is taken, try the next ones (3000 → 3001 → …) before giving up.
|
|
44
|
+
const wanted = port;
|
|
45
|
+
for (let attempt = 0; ; attempt++, port++) {
|
|
46
|
+
try {
|
|
47
|
+
await new Promise((resolve, reject) => server.once("error", reject).listen(port, () => { server.off("error", reject); resolve(); }));
|
|
48
|
+
break;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
if (e.code !== "EADDRINUSE" || attempt >= MAX_PORT_TRIES - 1) {
|
|
51
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
52
|
+
throw e.code === "EADDRINUSE" ? new Error(`Ports ${wanted}–${port} are all in use. Pick another with --port.`) : e;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (port !== wanted) log(`Port ${wanted} is in use, using ${port} instead.`);
|
|
42
57
|
|
|
43
58
|
let timer;
|
|
44
59
|
const watcher = fs.watch(projectDir, { recursive: true }, (_, name) => {
|
package/src/export.js
CHANGED
|
@@ -55,9 +55,9 @@ export async function exportFiles({ dir = ".", out = "dist", png = false, theme
|
|
|
55
55
|
return { files };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// Lay the page out for print and shrink any screen too tall for one page, as the PDF button does.
|
|
59
59
|
await page.emulateMediaType("print");
|
|
60
|
-
await page.evaluate(() =>
|
|
60
|
+
await page.evaluate(() => window.openink.preparePrint());
|
|
61
61
|
await wait(500);
|
|
62
62
|
const file = path.join(outDir, `${slug(spec.name)}.pdf`);
|
|
63
63
|
await page.pdf({ path: file, format: "A4", landscape: true, printBackground: true, preferCSSPageSize: true });
|
package/src/runtime/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import "./chart.js";
|
|
|
5
5
|
import { $$, redraw } from "./dom.js";
|
|
6
6
|
import { go, toast, initNavigation } from "./navigation.js";
|
|
7
7
|
import { setLang, initI18n } from "./i18n.js";
|
|
8
|
+
import { initPrint } from "./print.js";
|
|
8
9
|
|
|
9
10
|
function select(buttons, active) {
|
|
10
11
|
buttons.forEach((b) => {
|
|
@@ -61,4 +62,5 @@ document.addEventListener("toggle", (e) => e.target.tagName === "DETAILS" && red
|
|
|
61
62
|
window.addEventListener("DOMContentLoaded", () => {
|
|
62
63
|
initI18n();
|
|
63
64
|
initNavigation();
|
|
65
|
+
initPrint();
|
|
64
66
|
});
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { $$, config, redraw } from "./dom.js";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Show one screen (and the header nav set it asks for).
|
|
5
|
+
* `remember: false` leaves the URL alone: a "#screen" written before the page has loaded makes the
|
|
6
|
+
* browser scroll to it, and inside an iframe that scrolls the embedding page too.
|
|
7
|
+
*/
|
|
8
|
+
export function go(id, { remember = true } = {}) {
|
|
5
9
|
const el = document.getElementById(id);
|
|
6
10
|
if (!el || !el.classList.contains("screen")) return;
|
|
7
11
|
$$(".screen").forEach((s) => s.classList.remove("on"));
|
|
8
12
|
el.classList.add("on");
|
|
9
13
|
$$("header nav").forEach((n) => (n.hidden = n.dataset.nav !== el.dataset.nav));
|
|
10
14
|
document.title = el.dataset.title + " · " + document.title.split(" · ").pop();
|
|
11
|
-
history.replaceState(null, "", "#" + id);
|
|
15
|
+
if (remember) history.replaceState(null, "", "#" + id);
|
|
12
16
|
window.scrollTo({ top: 0 });
|
|
13
17
|
redraw();
|
|
14
18
|
}
|
|
@@ -23,5 +27,6 @@ export function toast(message) {
|
|
|
23
27
|
|
|
24
28
|
export function initNavigation() {
|
|
25
29
|
window.addEventListener("hashchange", () => go(location.hash.slice(1)));
|
|
26
|
-
|
|
30
|
+
// The start screen: the URL already says which one, or it is the first screen.
|
|
31
|
+
go(location.hash.slice(1) || config.first, { remember: false });
|
|
27
32
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { $$, redraw } from "./dom.js";
|
|
2
|
+
|
|
3
|
+
// Printable height of the @page in openink.css (A4 landscape less 10mm margins), in CSS pixels.
|
|
4
|
+
const PAGE_HEIGHT = (190 * 96) / 25.4;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lay the page out for print (every screen, one per page), shrink each screen that is taller than
|
|
8
|
+
* one printed page so it is not split across two, and draw the wired-elements that have never been
|
|
9
|
+
* visible. Runs on `beforeprint` (the PDF button, Ctrl+P) and from `openink export`.
|
|
10
|
+
*/
|
|
11
|
+
export function preparePrint() {
|
|
12
|
+
document.documentElement.classList.add("oi-print");
|
|
13
|
+
const screens = $$(".screen");
|
|
14
|
+
screens.forEach(unzoom);
|
|
15
|
+
|
|
16
|
+
// The header shares the first screen's page and the footer the last one's, so their height comes
|
|
17
|
+
// off that page's budget.
|
|
18
|
+
const docBottom = document.documentElement.scrollHeight;
|
|
19
|
+
const zooms = screens.map((s, i) => {
|
|
20
|
+
const r = s.getBoundingClientRect();
|
|
21
|
+
const top = r.top + scrollY;
|
|
22
|
+
const fixed = (i === 0 ? top : 0) + (i === screens.length - 1 ? docBottom - (top + r.height) : 0);
|
|
23
|
+
const room = (PAGE_HEIGHT - fixed) * 0.97; // a little slack for rounding and collapsed margins
|
|
24
|
+
return { zoom: r.height > room ? Math.max(room / r.height, 0.3) : 1, width: r.width };
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Draw before zooming: wired-elements size their sketch from the zoomed box, and the zoom then
|
|
28
|
+
// shrinks the sketch a second time. Keep the screen's unzoomed width so its layout (and so the
|
|
29
|
+
// sketches) only scales, and record the zoomed box as the size last drawn, so a resize
|
|
30
|
+
// (wired-card watches its own) does not redraw at that size.
|
|
31
|
+
$$("*").forEach((el) => el.wiredRender?.(true));
|
|
32
|
+
screens.forEach((s, i) => {
|
|
33
|
+
const { zoom, width } = zooms[i];
|
|
34
|
+
if (zoom === 1) return;
|
|
35
|
+
Object.assign(s.style, { zoom: String(zoom), width: `${width}px`, marginInline: "auto" });
|
|
36
|
+
$$("*", s).forEach((el) => el.canvasSize && el.lastSize && (el.lastSize = el.canvasSize()));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Undo preparePrint and redraw the screen that is showing. */
|
|
41
|
+
export function endPrint() {
|
|
42
|
+
document.documentElement.classList.remove("oi-print");
|
|
43
|
+
$$(".screen").forEach(unzoom);
|
|
44
|
+
redraw();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const unzoom = (s) => Object.assign(s.style, { zoom: "", width: "", marginInline: "" });
|
|
48
|
+
|
|
49
|
+
export function initPrint() {
|
|
50
|
+
window.addEventListener("beforeprint", preparePrint);
|
|
51
|
+
window.addEventListener("afterprint", endPrint);
|
|
52
|
+
window.openink = { ...window.openink, preparePrint, endPrint };
|
|
53
|
+
}
|
package/src/styles/openink.css
CHANGED
|
@@ -102,7 +102,7 @@ p { margin: 0; }
|
|
|
102
102
|
.row.between { justify-content: space-between; }
|
|
103
103
|
.row.center { justify-content: center; }
|
|
104
104
|
.grid { display: grid; grid-template-columns: repeat(var(--cols), 1fr); gap: var(--gap); align-items: start; }
|
|
105
|
-
@media (max-width: 720px) { .grid { grid-template-columns: 1fr; } }
|
|
105
|
+
@media (max-width: 720px) { html:not(.oi-print) .grid { grid-template-columns: 1fr; } }
|
|
106
106
|
.card { display: block; padding: 1rem; background: var(--card-bg); }
|
|
107
107
|
.card.dash { background: transparent; }
|
|
108
108
|
.center-items { justify-items: center; text-align: center; }
|
|
@@ -238,13 +238,16 @@ oi-placeholder[upload] .ph-label { inset: auto 0 22px 0; }
|
|
|
238
238
|
.toast { position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%); background: var(--ink); color: var(--paper); padding: .5rem 1rem; border-radius: 6px; opacity: 0; transition: opacity .2s; pointer-events: none; z-index: 100; }
|
|
239
239
|
.toast.show { opacity: 1; }
|
|
240
240
|
|
|
241
|
-
/* print / PDF: every screen on its own page
|
|
241
|
+
/* print / PDF: every screen on its own page. The runtime adds .oi-print before printing (and removes
|
|
242
|
+
it after) so it can measure the print layout and shrink screens too tall for one page; the layout
|
|
243
|
+
width is the @page's printable width. */
|
|
242
244
|
@page { size: A4 landscape; margin: 10mm; }
|
|
243
245
|
@media print {
|
|
244
246
|
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
245
|
-
.oi-bar nav, .lang, .print-btn, .toast, .nextbar, .modal { display: none !important; }
|
|
246
|
-
.screen, .screen.on { display: block !important; break-after: page; }
|
|
247
|
-
.screen:last-of-type { break-after: auto; }
|
|
248
|
-
.screen-title { display: block; font-family: "Caveat", cursive; font-size: 1.3rem; color: var(--muted); border-bottom: 2px dashed var(--line); }
|
|
249
|
-
.tab-panel { display: block; }
|
|
250
247
|
}
|
|
248
|
+
html.oi-print { width: 277mm; }
|
|
249
|
+
.oi-print .oi-bar nav, .oi-print .lang, .oi-print .print-btn, .oi-print .toast, .oi-print .nextbar, .oi-print .modal { display: none !important; }
|
|
250
|
+
.oi-print .screen, .oi-print .screen.on { display: block !important; break-after: page; }
|
|
251
|
+
.oi-print .screen:last-of-type { break-after: auto; }
|
|
252
|
+
.oi-print .screen-title { display: block; font-family: "Caveat", cursive; font-size: 1.3rem; color: var(--muted); border-bottom: 2px dashed var(--line); }
|
|
253
|
+
.oi-print .tab-panel { display: block; }
|