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 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>&#9654; <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.0",
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": ["wireframe", "prototype", "sketch", "mockup", "hand-drawn", "wired-elements", "roughjs", "ux", "static-site-generator"],
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": { "type": "git", "url": "git+https://github.com/mediuswareltd/openink.git" },
9
- "homepage": "https://github.com/mediuswareltd/openink#readme",
10
- "bugs": { "url": "https://github.com/mediuswareltd/openink/issues" },
11
- "publishConfig": { "access": "public" },
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": { "openink": "bin/openink.js" },
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": ["bin", "src", "templates", "schema", "docs", "!docs/img", "LICENSE", "README.md", "CHANGELOG.md"],
21
- "engines": { "node": ">=20" },
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
- await new Promise((resolve, reject) => server.once("error", reject).listen(port, resolve));
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
- // Print media shows every screen; the ones that were hidden have never been drawn, so force a render.
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(() => document.querySelectorAll("*").forEach((e) => e.wiredRender?.(true)));
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 });
@@ -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
- /** Show one screen (and the header nav set it asks for). */
4
- export function go(id) {
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
- go(location.hash.slice(1) || config.first);
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
+ }
@@ -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; }