Initial commit
This commit is contained in:
1 parent
1ba45f88aa
commit
9e6804c1ab
77 files changed
+3888
No files matched your search
@@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
// Runs the real Ink app against a seeded demo vault and saves 16:10 screenshots.
|
||||
// electron scripts/capture/capture.cjs --ozone-platform=headless --force-device-scale-factor=1.5
|
||||
// Env: INK_APP (path to the Ink repo, read only), OUT (output dir), THEMES (comma list), SCENES (comma list)
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const INK = process.env.INK_APP || path.resolve(__dirname, '../../../../App/Ink');
|
||||
const OUT = process.env.OUT || path.resolve(__dirname, '../../src/assets/screens');
|
||||
const THEMES = (process.env.THEMES || 'dark,light').split(',');
|
||||
const ONLY = process.env.SCENES ? process.env.SCENES.split(',') : null;
|
||||
const { seedVault } = require('./seed-vault.cjs');
|
||||
const scenes = require('./scenes.cjs');
|
||||
|
||||
const W = 1600, H = 1000;
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const theme = process.env.THEME || THEMES[0];
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ink-shots-'));
|
||||
const vault = path.join(tmp, 'Notes');
|
||||
fs.mkdirSync(vault);
|
||||
const seeded = seedVault(vault);
|
||||
|
||||
// No network: a link card gets a fixed title and description instead of fetching.
|
||||
const linkpreview = require(path.join(INK, 'electron/linkpreview.cjs'));
|
||||
linkpreview.hooks.lookup = async () => [{ address: '93.184.216.34' }];
|
||||
linkpreview.hooks.fetch = async () => new Response(
|
||||
'<html><head><title>JSON Canvas</title><meta property="og:title" content="JSON Canvas"><meta name="description" content="An open file format for infinite canvas data.">' +
|
||||
'</head><body></body></html>', { status: 200, headers: { 'content-type': 'text/html' } });
|
||||
|
||||
app.setPath('userData', tmp);
|
||||
fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({
|
||||
vault, lastPage: seeded.lastPage, theme, bounds: { width: W, height: H },
|
||||
fontSize: 16, outline: false, sidebarCollapsed: false, tagsOpen: true,
|
||||
vaults: { [vault]: seeded.vaultSettings },
|
||||
}));
|
||||
app.commandLine.appendSwitch('lang', 'en-US');
|
||||
require(path.join(INK, 'electron/main.cjs'));
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
let win;
|
||||
for (let i = 0; i < 100 && !(win = BrowserWindow.getAllWindows()[0]); i++) await sleep(100);
|
||||
win.setContentSize(W, H);
|
||||
const wc = win.webContents;
|
||||
if (wc.isLoading()) await new Promise((r) => wc.once('did-finish-load', r));
|
||||
const js = (code) => wc.executeJavaScript(`(async () => { ${code} })()`);
|
||||
const key = async (keyCode, modifiers = []) => { wc.sendInputEvent({ type: 'keyDown', keyCode, modifiers }); wc.sendInputEvent({ type: 'keyUp', keyCode, modifiers }); await sleep(250); };
|
||||
const type = async (t, d = 45) => { for (const ch of t) { wc.sendInputEvent({ type: 'char', keyCode: ch }); await sleep(d); } await sleep(200); };
|
||||
const click = async (x, y) => { wc.sendInputEvent({ type: 'mouseMove', x, y }); wc.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount: 1 }); wc.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount: 1 }); await sleep(250); };
|
||||
const shot = async (name) => {
|
||||
await sleep(500);
|
||||
const img = await wc.capturePage();
|
||||
const file = path.join(OUT, `${name}-${theme}.png`);
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
fs.writeFileSync(file, img.toPNG());
|
||||
const s = img.getSize();
|
||||
console.log(`saved ${path.basename(file)} ${s.width}x${s.height}`);
|
||||
};
|
||||
const ctx = { win, wc, js, key, type, click, shot, sleep, vault, W, H };
|
||||
|
||||
try {
|
||||
await sleep(2200);
|
||||
for (const [name, fn] of Object.entries(scenes)) {
|
||||
if (ONLY && !ONLY.includes(name)) continue;
|
||||
try { await fn(ctx); } catch (e) { console.log(`FAILED ${name}: ${e.message}`); }
|
||||
}
|
||||
} finally {
|
||||
app.exit(0);
|
||||
}
|
||||
});
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env sh
|
||||
# Captures all website screenshots from the real Ink app (read only) into src/assets/screens/.
|
||||
# Usage: scripts/capture/run.sh (env: INK_APP, OUT, SCENES)
|
||||
set -e
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
INK_APP="${INK_APP:-$HERE/../../../../App/Ink}"
|
||||
INK_APP="$(cd "$INK_APP" && pwd)"
|
||||
export INK_APP
|
||||
for theme in dark light; do
|
||||
THEME=$theme "$INK_APP/node_modules/.bin/electron" "$HERE/capture.cjs" \
|
||||
--ozone-platform=headless --force-device-scale-factor=1.5 --no-sandbox 2>&1 | grep -E 'saved|FAILED|Error' || true
|
||||
done
|
||||
@@ -0,0 +1,135 @@
|
||||
'use strict';
|
||||
// Each scene leaves the app looking the way the website screenshot should, then calls shot(name).
|
||||
// Scenes run in order in one session; each one starts by getting back to a known state.
|
||||
|
||||
// Cosmetic: show the default notes location instead of the temp folder this run uses.
|
||||
const tidyPath = (js) => js(`
|
||||
for (const el of document.querySelectorAll('.ws-path, .mono')) {
|
||||
if (/^\\/tmp\\/ink-shots-[^/]+\\/Notes$/.test(el.textContent.trim())) { el.textContent = '~/Notes'; el.title = '~/Notes'; }
|
||||
}`);
|
||||
|
||||
const reset = async ({ key, sleep, js }) => {
|
||||
await tidyPath(js);
|
||||
for (let i = 0; i < 3; i++) await key('Escape');
|
||||
await js(`document.activeElement?.blur?.()`);
|
||||
await sleep(150);
|
||||
};
|
||||
|
||||
// Open a page the way a user would: Ctrl+K, type its name, Enter.
|
||||
const openFromSidebar = async ({ key, type, sleep }, text) => {
|
||||
await key('K', ['control']);
|
||||
await sleep(400);
|
||||
await type(text, 30);
|
||||
await sleep(700);
|
||||
await key('Return');
|
||||
await sleep(1000);
|
||||
return true;
|
||||
};
|
||||
|
||||
// The element that actually scrolls the page.
|
||||
const scrollPage = (js, y) => js(`
|
||||
const c = [...document.querySelectorAll('*')].filter((e) => { const s = getComputedStyle(e); return /(auto|scroll)/.test(s.overflowY) && e.scrollHeight > e.clientHeight + 20 && e.clientWidth > 600; })
|
||||
.sort((a, b) => b.clientWidth * b.clientHeight - a.clientWidth * a.clientHeight)[0];
|
||||
if (c) { c.scrollTop = ${y}; return c.className; } return null;`);
|
||||
|
||||
module.exports = {
|
||||
async editor(ctx) {
|
||||
const { js, sleep, shot } = ctx;
|
||||
await tidyPath(js);
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Roadmap');
|
||||
await scrollPage(js, 120);
|
||||
await sleep(300);
|
||||
await shot('editor');
|
||||
},
|
||||
|
||||
async 'slash-menu'(ctx) {
|
||||
const { js, type, sleep, shot, key } = ctx;
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Launch plan');
|
||||
await scrollPage(js, 170);
|
||||
await sleep(300);
|
||||
await js(`document.querySelector('.tail').click()`);
|
||||
await sleep(400);
|
||||
await type('/');
|
||||
await sleep(500);
|
||||
await shot('slash-menu');
|
||||
await key('Escape');
|
||||
},
|
||||
|
||||
async linking(ctx) {
|
||||
const { js, sleep, shot } = ctx;
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Reading list');
|
||||
await scrollPage(js, 2000);
|
||||
await sleep(600);
|
||||
await shot('linking');
|
||||
},
|
||||
|
||||
async canvas(ctx) {
|
||||
const { sleep, shot, key } = ctx;
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Project map');
|
||||
await sleep(1200);
|
||||
await key('1', ['shift']);
|
||||
await sleep(900);
|
||||
await shot('canvas');
|
||||
},
|
||||
|
||||
async board(ctx) {
|
||||
const { sleep, shot } = ctx;
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Sprint board');
|
||||
await sleep(900);
|
||||
await scrollPage(ctx.js, 200);
|
||||
await sleep(400);
|
||||
await shot('board');
|
||||
},
|
||||
|
||||
async graph(ctx) {
|
||||
const { js, sleep, shot, key } = ctx;
|
||||
await reset(ctx);
|
||||
await js(`[...document.querySelectorAll('.sidebar .qa')].find((b) => b.textContent.includes('Graph view')).click()`);
|
||||
await sleep(3500);
|
||||
await js(`document.querySelector('.graph-top [aria-label="Graph settings"]')?.click()`);
|
||||
await sleep(900);
|
||||
await shot('graph');
|
||||
await key('Escape');
|
||||
},
|
||||
|
||||
async palette(ctx) {
|
||||
const { sleep, shot, key, type } = ctx;
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Roadmap');
|
||||
await key('K', ['control']);
|
||||
await sleep(500);
|
||||
await type('tag:work road');
|
||||
await sleep(700);
|
||||
await shot('palette');
|
||||
await key('Escape');
|
||||
},
|
||||
|
||||
async settings(ctx) {
|
||||
const { sleep, shot, key } = ctx;
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Roadmap');
|
||||
await key(',', ['control']);
|
||||
await sleep(900);
|
||||
await tidyPath(js);
|
||||
await sleep(200);
|
||||
await shot('settings');
|
||||
await key('Escape');
|
||||
},
|
||||
|
||||
async history(ctx) {
|
||||
const { js, sleep, shot, key } = ctx;
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Roadmap');
|
||||
await js(`document.querySelector('[aria-label="Page options"]')?.click()`);
|
||||
await sleep(500);
|
||||
await js(`[...document.querySelectorAll('[role="menuitem"]')].find((m) => m.textContent.includes('Version history'))?.click()`);
|
||||
await sleep(1200);
|
||||
await shot('history');
|
||||
await key('Escape');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
'use strict';
|
||||
// Builds a believable notes folder for the website screenshots. Usage: seedVault(dir)
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const w = (root, rel, text) => {
|
||||
const f = path.join(root, rel);
|
||||
fs.mkdirSync(path.dirname(f), { recursive: true });
|
||||
fs.writeFileSync(f, text);
|
||||
};
|
||||
const fm = (o) => {
|
||||
const lines = Object.entries(o).map(([k, v]) => `${k}: ${Array.isArray(v) ? `[${v.join(', ')}]` : v}`);
|
||||
return `---\n${lines.join('\n')}\n---\n`;
|
||||
};
|
||||
|
||||
// [title, icon, tags, links, one-line body]
|
||||
const STUBS = [
|
||||
['Meeting notes', '🗓️', ['work'], ['Roadmap', 'Release checklist'], 'Weekly sync. Decided to ship Linux first and test the Windows installer on a clean machine before announcing it.'],
|
||||
['Release checklist', '✅', ['work', 'ink'], ['Roadmap', 'Windows build', 'Docs'], 'Version bump, changelog, AppImage, checksum, website config.'],
|
||||
['Windows build', '🪟', ['work', 'ink'], ['Release checklist', 'Roadmap'], 'NSIS installer is configured. Needs a test pass on a clean Windows install.'],
|
||||
['Docs', '📖', ['work', 'ink'], ['Roadmap', 'Keyboard shortcuts'], 'Where notes live, Obsidian compatibility, shortcuts, privacy.'],
|
||||
['Keyboard shortcuts', '⌨️', ['ink'], ['Docs'], 'Ctrl+K palette, Ctrl+N new page, Ctrl+G graph.'],
|
||||
['Ink website', '🌐', ['work', 'ink'], ['Roadmap', 'Docs', 'Typography'], 'Static site. Calm, dark first, no analytics.'],
|
||||
['Typography', '🔤', ['learning', 'ideas'], ['Ink website', 'Colour notes'], 'Inter for UI, a mono for code. Keep the measure around 66 characters.'],
|
||||
['Colour notes', '🎨', ['learning', 'ideas'], ['Typography', 'Ink website'], 'Warm blush accent on near-black. Keep contrast above 4.5:1.'],
|
||||
['Ideas', '💡', ['ideas'], ['Canvas experiments', 'Plugin ideas', 'Roadmap'], 'Things I might build one day.'],
|
||||
['Canvas experiments', '🧪', ['ideas', 'ink'], ['Ideas', 'Project map'], 'Alignment guides, grouping, export. What else would make a canvas calmer?'],
|
||||
['Plugin ideas', '🧩', ['ideas'], ['Ideas'], 'Probably not. Keep the core small.'],
|
||||
['Reading list', '📚', ['reading'], ['The Pragmatic Programmer', 'A Pattern Language', 'Designing Data-Intensive Applications', 'Meeting notes'], 'Books and articles I want to get to.'],
|
||||
['The Pragmatic Programmer', '📘', ['reading'], ['Reading list', 'Software craft'], 'Tracer bullets, orthogonality, DRY. Re-read every few years.'],
|
||||
['A Pattern Language', '🏛️', ['reading', 'learning'], ['Reading list', 'Software craft'], 'Patterns that make places feel alive. Applies to software more than expected.'],
|
||||
['Designing Data-Intensive Applications', '📗', ['reading', 'learning'], ['Reading list', 'SQLite notes'], 'Storage engines, replication, the trouble with distributed clocks.'],
|
||||
['Software craft', '🛠️', ['learning'], ['The Pragmatic Programmer', 'A Pattern Language', 'Code review'], 'Small tools, sharp edges, patient iteration.'],
|
||||
['Code review', '🔍', ['learning', 'work'], ['Software craft'], 'Review the change, not the person. Ask questions first.'],
|
||||
['SQLite notes', '🗃️', ['learning', 'ink'], ['Designing Data-Intensive Applications', 'Full-text search'], 'FTS5, WAL mode, and why a single file is enough.'],
|
||||
['Full-text search', '🔎', ['learning', 'ink'], ['SQLite notes', 'Roadmap'], 'Index lives next to the notes and can be rebuilt any time.'],
|
||||
['Trip to Lisbon', '✈️', ['personal'], ['Packing list', 'Food to try'], 'Four days in April. Trams, tiles and custard tarts.'],
|
||||
['Packing list', '🎒', ['personal'], ['Trip to Lisbon'], 'Light jacket, walking shoes, charger, notebook.'],
|
||||
['Food to try', '🥐', ['personal'], ['Trip to Lisbon', 'Sourdough'], 'Pastéis de nata, grilled sardines, ginjinha.'],
|
||||
['Sourdough', '🍞', ['personal'], ['Food to try'], '500 g flour, 350 g water, 100 g starter, 10 g salt. Fold every 30 minutes.'],
|
||||
['Garden', '🌱', ['personal'], ['Sourdough', 'Planting calendar'], 'Tomatoes, basil, and a lemon tree that refuses to flower.'],
|
||||
['Planting calendar', '📆', ['personal'], ['Garden'], 'March: sow tomatoes indoors. May: harden off and plant out.'],
|
||||
['Running', '🏃', ['personal'], ['Training plan'], 'Easy runs mostly. Pace is a distraction.'],
|
||||
['Training plan', '📈', ['personal'], ['Running'], 'Three runs a week, one long. Add 10% at most.'],
|
||||
['Book club', '☕', ['reading', 'personal'], ['Reading list', 'A Pattern Language'], 'First Thursday of the month. Next: The Overstory.'],
|
||||
['Gift ideas', '🎁', ['personal', 'ideas'], ['Book club'], 'Good pens, a nice notebook, tickets for something.'],
|
||||
['Home projects', '🔧', ['personal'], ['Garden', 'Gift ideas'], 'Fix the squeaky door. Paint the hallway.'],
|
||||
['Learning Rust', '🦀', ['learning'], ['Software craft', 'SQLite notes'], 'Ownership clicked after drawing it on a canvas.'],
|
||||
['Weekly review', '🗒️', ['work', 'personal'], ['Roadmap', 'Meeting notes', 'Training plan'], 'What went well, what did not, what is next.'],
|
||||
];
|
||||
|
||||
function seedVault(root) {
|
||||
const stubTitles = new Set(STUBS.map((s) => s[0]));
|
||||
|
||||
// The hero page.
|
||||
w(root, 'Roadmap.md', fm({ icon: '🗺️', tags: ['work', 'ink'], created: '2026-09-02' }) + `Where Ink is heading this quarter. Details live in [[Projects]], and every meeting ends up in [[Meeting notes]].
|
||||
|
||||
## Q4 goals
|
||||
|
||||
- [x] Ship the first public build as an AppImage
|
||||
- [x] Canvas export to PNG, SVG and PDF
|
||||
- [x] Version history for pages and canvases
|
||||
- [ ] Test the Windows installer on a clean machine
|
||||
- [ ] Write the docs, starting with [[Keyboard shortcuts]]
|
||||
- [ ] Follow the [[Launch plan]]
|
||||
|
||||
> [!tip]
|
||||
> Keep releases small. A calm app needs fewer surprises, not more features.
|
||||
|
||||
\`\`\`js
|
||||
const release = {
|
||||
version: '0.1.0',
|
||||
platforms: ['linux'],
|
||||
notes: 'plain .md files, offline, no account',
|
||||
};
|
||||
console.log(\`Shipping Ink \${release.version}\`);
|
||||
\`\`\`
|
||||
|
||||
<details>
|
||||
<summary>Why Linux first?</summary>
|
||||
|
||||
The AppImage is the only target built and tested so far. Windows and macOS are configured but still need a proper pass.
|
||||
|
||||
</details>
|
||||
`);
|
||||
|
||||
w(root, 'Projects.md', fm({ icon: '📁', tags: ['work'] }) + `# Projects
|
||||
|
||||
- [[Ink website]]
|
||||
- [[Windows build]]
|
||||
- [[Docs]]
|
||||
- [[Release checklist]]
|
||||
|
||||
Back to the [[Roadmap]]. Work in progress lives on the [[Sprint board]].
|
||||
`);
|
||||
w(root, 'Projects/Ink website.md', fm({ icon: '🌐', tags: ['work', 'ink'] }) + 'Static site, calm and dark-first. See [[Typography]] and [[Colour notes]].\n');
|
||||
|
||||
// Linking demo page.
|
||||
w(root, 'Reading list.md', fm({ icon: '📚', tags: ['reading'] }) + `# Reading list
|
||||
|
||||
Books and articles I want to get to. I keep notes on each in [[The Pragmatic Programmer]] and [[A Pattern Language]], and the monthly [[Book club]] picks the next one.
|
||||
|
||||
## Currently
|
||||
|
||||
- [x] [[The Pragmatic Programmer]]
|
||||
- [ ] [[A Pattern Language]]
|
||||
- [ ] [[Designing Data-Intensive Applications]]
|
||||
|
||||
Highlights from the weekly sync are in [[Roadmap#Q4 goals]].
|
||||
|
||||
![[Meeting notes]]
|
||||
`);
|
||||
w(root, 'Meeting notes.md', fm({ icon: '🗓️', tags: ['work'] }) + `# Meeting notes
|
||||
|
||||
Weekly sync, 7 October.
|
||||
|
||||
- Ship Linux first, announce Windows once it is tested
|
||||
- Keep the docs short and honest
|
||||
- Review the website copy on Friday
|
||||
|
||||
Follow-ups go into [[Roadmap]], [[Release checklist]] and the [[Sprint board]].
|
||||
`);
|
||||
|
||||
for (const [title, icon, tags, links, body] of STUBS) {
|
||||
if (['Meeting notes', 'Reading list'].includes(title)) continue;
|
||||
const rel = ['Ink website'].includes(title) ? null : `${title}.md`;
|
||||
if (!rel) continue;
|
||||
const refs = links.filter((l) => stubTitles.has(l) || ['Roadmap', 'Projects', 'Project map'].includes(l)).map((l) => `[[${l}]]`);
|
||||
w(root, rel, fm({ icon, tags }) + `${body}\n\nSee also ${refs.join(', ')}.\n`);
|
||||
}
|
||||
|
||||
// Daily notes.
|
||||
w(root, 'Daily/2026-10-10.md', fm({ icon: '📅', tags: ['personal'] }) + '- Morning run, easy pace\n- Reviewed the [[Roadmap]]\n- Sourdough went well: see [[Sourdough]]\n');
|
||||
w(root, 'Daily/2026-10-11.md', fm({ icon: '📅', tags: ['personal'] }) + '- [ ] Write the release notes\n- [ ] Pack for the weekend\n- [x] Call about the [[Trip to Lisbon]] flights\n');
|
||||
|
||||
// Board.
|
||||
w(root, 'Sprint board.md', `---
|
||||
kanban-plugin: basic
|
||||
icon: 🗂️
|
||||
tags: [work, ink]
|
||||
---
|
||||
|
||||
## To do
|
||||
|
||||
- [ ] Windows installer test pass
|
||||
- [ ] macOS dmg build
|
||||
- [ ] Docs: where notes are stored
|
||||
- [ ] Verify checksum instructions
|
||||
|
||||
|
||||
## Doing
|
||||
|
||||
- [ ] Website screenshots
|
||||
- [ ] Changelog for 0.1.0
|
||||
- [ ] Spellcheck language picker polish
|
||||
|
||||
|
||||
## Done
|
||||
|
||||
- [x] AppImage build
|
||||
- [x] Canvas export (PNG, SVG, PDF)
|
||||
- [x] Version history for canvases
|
||||
- [x] Graph view filters
|
||||
|
||||
|
||||
|
||||
%% kanban:settings
|
||||
\`\`\`
|
||||
{"kanban-plugin":"basic"}
|
||||
\`\`\`
|
||||
%%
|
||||
`);
|
||||
|
||||
// Canvas: cards, a live note preview, a link card, a group, and arrows.
|
||||
const id = (n) => `n${String(n).padStart(15, '0')}`;
|
||||
const canvas = {
|
||||
nodes: [
|
||||
{ id: id(1), type: 'group', label: 'Q4', x: -60, y: -60, width: 760, height: 520, color: '6' },
|
||||
{ id: id(2), type: 'text', text: '## Research\n\n- user interviews\n- survey of 40 people\n- what makes notes calm?', x: 0, y: 20, width: 300, height: 190 },
|
||||
{ id: id(3), type: 'text', text: '## Principles\n\n1. Plain files\n2. Offline first\n3. No lock-in', x: 0, y: 270, width: 300, height: 160, color: '4' },
|
||||
{ id: id(4), type: 'file', file: 'Roadmap.md', subpath: '#Q4 goals', x: 400, y: 20, width: 270, height: 330 },
|
||||
{ id: id(5), type: 'link', url: 'https://jsoncanvas.org', x: 860, y: 40, width: 300, height: 150 },
|
||||
{ id: id(6), type: 'text', text: '## Ship it\n\nAppImage first. Windows and macOS when tested.', x: 860, y: 300, width: 300, height: 140, color: '1' },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e0000000000000a1', fromNode: id(2), fromSide: 'right', toNode: id(4), toSide: 'left', label: 'informs' },
|
||||
{ id: 'e0000000000000a2', fromNode: id(3), fromSide: 'right', toNode: id(4), toSide: 'bottom' },
|
||||
{ id: 'e0000000000000a3', fromNode: id(4), fromSide: 'right', toNode: id(5), toSide: 'left', label: 'format' },
|
||||
{ id: 'e0000000000000a4', fromNode: id(4), fromSide: 'bottom', toNode: id(6), toSide: 'left' },
|
||||
],
|
||||
};
|
||||
w(root, 'Project map.canvas', JSON.stringify(canvas, null, '\t'));
|
||||
|
||||
// A short page for the slash-menu shot, with room under the last line.
|
||||
w(root, 'Launch plan.md', fm({ icon: '🚀', tags: ['work', 'ink'] }) + `Everything we need before the first public build goes out.
|
||||
|
||||
The release is a single AppImage, so the checklist is short: bump the version, write the changelog and publish the checksum next to the download.
|
||||
|
||||
Once that is done, the website can go live with a plain download button and nothing else.
|
||||
`);
|
||||
|
||||
// Earlier versions of the Roadmap page, so version history has something to show.
|
||||
const now = Date.now();
|
||||
const hist = [
|
||||
[20 * 60e3, 'Where Ink is heading this quarter.\n\n## Q4 goals\n\n- [x] Ship the first public build as an AppImage\n- [x] Canvas export to PNG, SVG and PDF\n- [x] Version history for pages and canvases\n- [ ] Test the Windows installer on a clean machine\n'],
|
||||
[3 * 3600e3, 'Where Ink is heading this quarter.\n\n## Q4 goals\n\n- [x] Ship the first public build as an AppImage\n- [x] Canvas export to PNG, SVG and PDF\n- [ ] Version history for pages and canvases\n- [ ] Test the Windows installer on a clean machine\n'],
|
||||
[26 * 3600e3, 'Where Ink is heading this quarter.\n\n## Q4 goals\n\n- [x] Ship the first public build as an AppImage\n- [ ] Canvas export to PNG, SVG and PDF\n- [ ] Windows installer\n'],
|
||||
[49 * 3600e3, 'Roadmap for the quarter.\n\n- [ ] AppImage\n- [ ] Canvas export\n- [ ] Windows installer\n'],
|
||||
[4 * 86400e3, 'Roadmap\n\n- first build\n- canvas\n'],
|
||||
[9 * 86400e3, 'Ideas for later.\n'],
|
||||
];
|
||||
for (const [ago, body] of hist) {
|
||||
w(root, `.notes/history/Roadmap/${now - ago}.md`, fm({ icon: '🗺️', tags: ['work', 'ink'], created: '2026-09-02' }) + body);
|
||||
}
|
||||
|
||||
// Sidebar niceties and tag colours are settings, returned for the caller.
|
||||
return {
|
||||
lastPage: 'Roadmap',
|
||||
vaultSettings: {
|
||||
pins: ['Roadmap', 'Reading list', 'Sprint board', 'Project map.canvas'],
|
||||
recent: ['Roadmap', 'Reading list', 'Sprint board'],
|
||||
tagColors: { work: 'blue', ink: 'pink', reading: 'yellow', learning: 'teal', personal: 'green', ideas: 'purple' },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { seedVault };
|
||||
Reference in new issue
Block a user