konpeki 0.3.0 → 0.4.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/AGENTS.md +21 -28
- package/AUTHORING.md +68 -229
- package/CONTRIBUTING.md +10 -23
- package/README.md +96 -87
- package/SETUP.md +50 -114
- package/docs/development.md +43 -248
- package/docs/workflow.md +6 -108
- package/html/README.md +140 -0
- package/html/browser.ts +103 -0
- package/html/document.ts +38 -0
- package/html/floor.ts +34 -0
- package/html/index.html +5 -0
- package/html/inspect.ts +313 -0
- package/html/preview.css +62 -0
- package/html/preview.tsx +463 -0
- package/html/review-hints.ts +63 -0
- package/html/server.ts +99 -0
- package/html/source.ts +104 -0
- package/html/starter.ts +6 -0
- package/html/theme-authoring.md +176 -0
- package/html/theme.md +61 -0
- package/index.html +9 -9
- package/package.json +21 -35
- package/plugin.json +1 -1
- package/public/og.png +0 -0
- package/runtime/browser-B-29TH1a.mjs +595 -0
- package/runtime/floor-Cmk7G3pU.mjs +41 -0
- package/runtime/konpeki.mjs +90 -1410
- package/runtime/server-BAqC_5WD.mjs +2 -0
- package/runtime/server-DfRpfcY9.mjs +205 -0
- package/runtime/source-B_Ui9tMp.mjs +141 -0
- package/runtime/source-CZQq9GUO.mjs +2 -0
- package/skills/konpeki/SKILL.md +88 -172
- package/skills/konpeki/assets/blank.html +17 -0
- package/skills/konpeki/floor.md +76 -0
- package/skills/konpeki/references/cover.md +27 -0
- package/skills/konpeki/references/long-document.md +40 -0
- package/skills/konpeki/references/one-pager.md +27 -0
- package/skills/konpeki/references/patterns.md +118 -0
- package/skills/konpeki/references/resume.md +28 -0
- package/skills/konpeki/references/slides.md +28 -0
- package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
- package/skills/konpeki/scripts/prepare-document.mjs +25 -14
- package/src/components/PageBoard.tsx +156 -0
- package/src/lib/alignment.ts +21 -0
- package/src/lib/page-board.ts +25 -0
- package/src/lib/review-position.ts +19 -0
- package/src/styles/base.css +4 -6
- package/src/styles/feedback.css +97 -61
- package/src/styles/shell.css +95 -323
- package/theme-base.css +90 -0
- package/theme.css +56 -0
- package/vite.config.ts +2 -5
- package/composition/README.md +0 -156
- package/composition/compile.ts +0 -227
- package/composition/document.ts +0 -600
- package/composition/schema.json +0 -3001
- package/composition/schema.ts +0 -437
- package/composition/theme-tokens.ts +0 -16
- package/composition/types.ts +0 -269
- package/composition/validate.ts +0 -226
- package/composition/vector.ts +0 -143
- package/composition/visualizations.ts +0 -319
- package/design/README.md +0 -17
- package/design/palettes/README.md +0 -14
- package/design/palettes/base.ts +0 -14
- package/design/palettes/candidates.ts +0 -19
- package/design/palettes/index.ts +0 -78
- package/design/review/color-theme.md +0 -44
- package/design/review/layout.md +0 -16
- package/design/review/text.md +0 -18
- package/design/review/typography.md +0 -15
- package/design/review/visuals.md +0 -31
- package/design/semantic-patterns.md +0 -43
- package/design/themes/README.md +0 -40
- package/design/themes/index.ts +0 -24
- package/design/visual-languages/technical-product.md +0 -17
- package/design/visual-review.md +0 -88
- package/lib/assets.d.ts +0 -8
- package/lib/charts.ts +0 -18
- package/lib/contrast.ts +0 -16
- package/lib/layouts.ts +0 -50
- package/lib/slide.tsx +0 -42
- package/lib/taste.ts +0 -17
- package/lib/text.tsx +0 -89
- package/lib/typeface.ts +0 -44
- package/scripts/migrate-react-page.ts +0 -120
- package/skills/konpeki/assets/blank.json +0 -23
- package/slides/README.md +0 -153
- package/slides/architecture/PROMPT.md +0 -31
- package/slides/architecture/index.tsx +0 -102
- package/slides/article-brief/PROMPT.md +0 -35
- package/slides/article-brief/index.tsx +0 -71
- package/slides/bar-chart/PROMPT.md +0 -39
- package/slides/bar-chart/index.tsx +0 -97
- package/slides/comparison/PROMPT.md +0 -29
- package/slides/comparison/index.tsx +0 -95
- package/slides/decision-memo/PROMPT.md +0 -34
- package/slides/decision-memo/index.tsx +0 -85
- package/slides/delivery-plan/PROMPT.md +0 -45
- package/slides/delivery-plan/index.tsx +0 -105
- package/slides/experiment/PROMPT.md +0 -44
- package/slides/experiment/index.tsx +0 -127
- package/slides/incident-workflow/PROMPT.md +0 -57
- package/slides/incident-workflow/index.tsx +0 -78
- package/slides/introducing-konpeki/PROMPT.md +0 -40
- package/slides/introducing-konpeki/README.md +0 -76
- package/slides/introducing-konpeki/SOURCE.md +0 -26
- package/slides/introducing-konpeki/author.ts +0 -165
- package/slides/introducing-konpeki/composition.json +0 -3270
- package/slides/line-chart/PROMPT.md +0 -40
- package/slides/line-chart/index.tsx +0 -72
- package/slides/migration/PROMPT.md +0 -38
- package/slides/migration/index.tsx +0 -89
- package/slides/og-images/PROMPT.md +0 -21
- package/slides/og-images/index.tsx +0 -76
- package/slides/product-introduction/PROMPT.md +0 -24
- package/slides/product-introduction/index.tsx +0 -105
- package/slides/research-brief/PROMPT.md +0 -40
- package/slides/research-brief/index.tsx +0 -104
- package/slides/results-explanation/PROMPT.md +0 -32
- package/slides/results-explanation/index.tsx +0 -96
- package/slides/retrospective/PROMPT.md +0 -43
- package/slides/retrospective/index.tsx +0 -105
- package/slides/sankey/PROMPT.md +0 -11
- package/slides/sankey/index.tsx +0 -93
- package/slides/teaching/PROMPT.md +0 -45
- package/slides/teaching/index.tsx +0 -124
- package/slides/vertical-bar-charts/PROMPT.md +0 -13
- package/slides/vertical-bar-charts/index.tsx +0 -97
- package/src/app/App.tsx +0 -820
- package/src/components/BuildOrb.tsx +0 -40
- package/src/components/Canvas.tsx +0 -1185
- package/src/components/DiagramTypeIcon.tsx +0 -78
- package/src/components/InspectorPanel.tsx +0 -773
- package/src/components/LeftPanel.tsx +0 -120
- package/src/components/PageSizePicker.tsx +0 -30
- package/src/components/Presentation.tsx +0 -105
- package/src/components/RevisionNotes.tsx +0 -56
- package/src/components/RightPanel.tsx +0 -201
- package/src/components/VectorOverflowWarning.tsx +0 -46
- package/src/components/WorkspaceChrome.tsx +0 -288
- package/src/components/ui.tsx +0 -53
- package/src/lib/examples/react-page-migration.json +0 -1295
- package/src/lib/examples.ts +0 -42
- package/src/lib/export-png.ts +0 -104
- package/src/lib/file-session.ts +0 -87
- package/src/lib/history.ts +0 -53
- package/src/lib/model.ts +0 -188
- package/src/lib/page-size.ts +0 -24
- package/src/lib/presentation.ts +0 -17
- package/src/lib/review.ts +0 -26
- package/src/lib/storage.ts +0 -71
- package/src/lib/theme.ts +0 -25
- package/src/lib/use-file-session.ts +0 -227
- package/src/main.tsx +0 -29
- package/src/styles/canvas.css +0 -299
- package/src/styles/chrome.css +0 -384
- package/src/styles/component-previews.css +0 -386
- package/src/styles/left-panel.css +0 -166
- package/src/styles/presentation.css +0 -72
- package/src/styles/right-panel.css +0 -1215
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { t as fileSource } from "./source-B_Ui9tMp.mjs";
|
|
2
|
+
import { dirname, extname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { createServer as createServer$1 } from "vite";
|
|
8
|
+
import { parse, parseFragment, serialize } from "parse5";
|
|
9
|
+
//#region html/document.ts
|
|
10
|
+
var documentPolicy = "default-src 'none'; script-src 'none'; style-src 'unsafe-inline' 'self' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data:; base-uri 'none'; form-action 'none'";
|
|
11
|
+
/** The only external resources permitted by CLI inspection/export. */
|
|
12
|
+
function isGoogleFontResource(url, resourceType) {
|
|
13
|
+
const resource = new URL(url);
|
|
14
|
+
if (resource.protocol !== "https:" || resource.port || resource.username || resource.password) return false;
|
|
15
|
+
return resourceType === "stylesheet" && resource.hostname === "fonts.googleapis.com" && ["/css", "/css2"].includes(resource.pathname) || resourceType === "font" && resource.hostname === "fonts.gstatic.com" && resource.pathname.startsWith("/s/");
|
|
16
|
+
}
|
|
17
|
+
/** A view of the source, never a serialization written back to it. Keep every
|
|
18
|
+
* page in the DOM so nth-child selectors and shared SVG definitions stay stable. */
|
|
19
|
+
function documentHTML(source, pageId) {
|
|
20
|
+
const tree = parse(source);
|
|
21
|
+
const html = tree.childNodes.find((n) => n.nodeName === "html");
|
|
22
|
+
const head = html.childNodes.find((n) => n.nodeName === "head");
|
|
23
|
+
const body = html.childNodes.find((n) => n.nodeName === "body");
|
|
24
|
+
for (const child of body.childNodes) {
|
|
25
|
+
if (!("attrs" in child)) continue;
|
|
26
|
+
child.attrs = child.attrs.filter((a) => a.name !== "data-konpeki-current");
|
|
27
|
+
if (child.attrs.some((a) => a.name === "data-page") && child.attrs.some((a) => a.name === "id" && a.value === pageId)) child.attrs.push({
|
|
28
|
+
name: "data-konpeki-current",
|
|
29
|
+
value: ""
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
head.childNodes.unshift(...parseFragment(`<meta http-equiv="Content-Security-Policy" content="${documentPolicy}">`).childNodes);
|
|
33
|
+
head.childNodes.push(...parseFragment(`<style>
|
|
34
|
+
html, body { margin: 0 !important; padding: 0 !important; }
|
|
35
|
+
body { display: flex !important; flex-direction: column !important; align-items: flex-start !important; }
|
|
36
|
+
body > [data-page] { margin: 0 !important; flex: none !important; }
|
|
37
|
+
body > [data-page]:not([data-konpeki-current]) { display: none !important; }
|
|
38
|
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
39
|
+
@media print { body > [data-page] { break-before: auto !important; break-after: auto !important; } }
|
|
40
|
+
</style>`).childNodes);
|
|
41
|
+
return serialize(tree);
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region html/server.ts
|
|
45
|
+
var types = {
|
|
46
|
+
".css": "text/css",
|
|
47
|
+
".png": "image/png",
|
|
48
|
+
".jpg": "image/jpeg",
|
|
49
|
+
".jpeg": "image/jpeg",
|
|
50
|
+
".webp": "image/webp",
|
|
51
|
+
".gif": "image/gif",
|
|
52
|
+
".svg": "image/svg+xml",
|
|
53
|
+
".woff": "font/woff",
|
|
54
|
+
".woff2": "font/woff2",
|
|
55
|
+
".ttf": "font/ttf",
|
|
56
|
+
".otf": "font/otf"
|
|
57
|
+
};
|
|
58
|
+
var sessionRoute = "/__konpeki/html";
|
|
59
|
+
/** Same authenticated document/asset surface for preview and browser exports. */
|
|
60
|
+
function htmlSession(path, token = randomBytes(24).toString("base64url")) {
|
|
61
|
+
const store = fileSource(resolve(path));
|
|
62
|
+
const prefix = `${sessionRoute}/${token}`;
|
|
63
|
+
function send(res, status, value) {
|
|
64
|
+
res.writeHead(status, {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
"Cache-Control": "no-store",
|
|
67
|
+
"Referrer-Policy": "no-referrer"
|
|
68
|
+
});
|
|
69
|
+
res.end(JSON.stringify(value));
|
|
70
|
+
}
|
|
71
|
+
async function handler(req, res, next) {
|
|
72
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
73
|
+
if (!url.pathname.startsWith("/__konpeki/html")) return next();
|
|
74
|
+
try {
|
|
75
|
+
if (url.pathname === "/__konpeki/html") {
|
|
76
|
+
if (req.headers["x-konpeki-session"] !== token) return send(res, 403, { error: "Invalid preview session." });
|
|
77
|
+
if (req.method === "GET") return send(res, 200, await store.read());
|
|
78
|
+
if (req.method !== "PATCH") return send(res, 405, { error: "Method not allowed." });
|
|
79
|
+
let size = 0;
|
|
80
|
+
const chunks = [];
|
|
81
|
+
for await (const chunk of req) {
|
|
82
|
+
size += chunk.length;
|
|
83
|
+
if (size > 16384) throw Object.assign(/* @__PURE__ */ new Error("Correction is too large."), { status: 413 });
|
|
84
|
+
chunks.push(chunk);
|
|
85
|
+
}
|
|
86
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
87
|
+
if (!body || typeof body.revision !== "string" || !body.edit || ![
|
|
88
|
+
"move",
|
|
89
|
+
"delete",
|
|
90
|
+
"undo"
|
|
91
|
+
].includes(body.edit.kind)) return send(res, 400, { error: "Invalid correction." });
|
|
92
|
+
return send(res, 200, await store.edit(body.revision, body.edit));
|
|
93
|
+
}
|
|
94
|
+
if (!url.pathname.startsWith(`${prefix}/`)) return send(res, 403, { error: "Invalid preview session." });
|
|
95
|
+
if (req.method !== "GET") return send(res, 405, { error: "Method not allowed." });
|
|
96
|
+
if (url.pathname === `${prefix}/document/`) {
|
|
97
|
+
const snapshot = await store.read();
|
|
98
|
+
if (url.searchParams.has("revision") && url.searchParams.get("revision") !== snapshot.revision) return send(res, 409, { error: "The source changed. Retry with its latest revision." });
|
|
99
|
+
const page = url.searchParams.get("page") ?? snapshot.pages[0];
|
|
100
|
+
if (!snapshot.pages.includes(page)) return send(res, 404, { error: "Unknown page." });
|
|
101
|
+
res.writeHead(200, {
|
|
102
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
103
|
+
"Content-Security-Policy": documentPolicy,
|
|
104
|
+
"Cache-Control": "no-store",
|
|
105
|
+
"Referrer-Policy": "no-referrer",
|
|
106
|
+
"X-Content-Type-Options": "nosniff"
|
|
107
|
+
});
|
|
108
|
+
return res.end(documentHTML(snapshot.source, page));
|
|
109
|
+
}
|
|
110
|
+
const assets = `${prefix}/document/`;
|
|
111
|
+
if (!url.pathname.startsWith(assets)) return send(res, 404, { error: "Unknown resource." });
|
|
112
|
+
const name = decodeURIComponent(url.pathname.slice(assets.length));
|
|
113
|
+
const type = types[extname(name).toLowerCase()];
|
|
114
|
+
if (!type || name.split(/[\\/]/).some((part) => part.startsWith("."))) return send(res, 403, { error: "Only local image, font and CSS assets are served." });
|
|
115
|
+
let base = await realpath(dirname(resolve(path))), file;
|
|
116
|
+
try {
|
|
117
|
+
file = await realpath(resolve(base, name));
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error.code !== "ENOENT" || !/^theme(?:-base)?\.css$/.test(name)) throw error;
|
|
120
|
+
base = fileURLToPath(new URL("../", import.meta.url));
|
|
121
|
+
file = await realpath(resolve(base, name));
|
|
122
|
+
}
|
|
123
|
+
const within = relative(base, file);
|
|
124
|
+
if (isAbsolute(within) || within.startsWith("..")) return send(res, 403, { error: "Asset is outside the document directory." });
|
|
125
|
+
const bytes = await readFile(file);
|
|
126
|
+
res.writeHead(200, {
|
|
127
|
+
"Content-Type": type,
|
|
128
|
+
"Cache-Control": "no-store",
|
|
129
|
+
"Content-Security-Policy": documentPolicy,
|
|
130
|
+
"X-Content-Type-Options": "nosniff"
|
|
131
|
+
});
|
|
132
|
+
res.end(bytes);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
send(res, error instanceof SyntaxError || error instanceof URIError ? 400 : error.code === "ENOENT" ? 404 : Number(error.status ?? 500), { error: error instanceof Error ? error.message : "Preview failed." });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
token,
|
|
139
|
+
prefix,
|
|
140
|
+
store,
|
|
141
|
+
handler
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async function documentServer(path) {
|
|
145
|
+
const session = htmlSession(path);
|
|
146
|
+
await session.store.read();
|
|
147
|
+
const server = createServer((req, res) => void session.handler(req, res, () => {
|
|
148
|
+
res.writeHead(404);
|
|
149
|
+
res.end();
|
|
150
|
+
}));
|
|
151
|
+
await new Promise((resolve, reject) => {
|
|
152
|
+
server.once("error", reject);
|
|
153
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
154
|
+
});
|
|
155
|
+
const address = server.address();
|
|
156
|
+
return {
|
|
157
|
+
...session,
|
|
158
|
+
origin: `http://127.0.0.1:${address.port}`,
|
|
159
|
+
close: () => new Promise((resolve, reject) => server.close((e) => e ? reject(e) : resolve()))
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function previewHTML(path, host, port, root, strictPort = false) {
|
|
163
|
+
const session = htmlSession(path);
|
|
164
|
+
await session.store.read();
|
|
165
|
+
const server = await createServer$1({
|
|
166
|
+
root,
|
|
167
|
+
configFile: false,
|
|
168
|
+
logLevel: "silent",
|
|
169
|
+
server: {
|
|
170
|
+
host,
|
|
171
|
+
port,
|
|
172
|
+
strictPort,
|
|
173
|
+
fs: {
|
|
174
|
+
allow: [root, dirname(fileURLToPath(import.meta.resolve("@fontsource/ibm-plex-sans/package.json")))],
|
|
175
|
+
deny: [
|
|
176
|
+
".env",
|
|
177
|
+
".env.*",
|
|
178
|
+
"*.{crt,pem,key,p12,pfx}",
|
|
179
|
+
".npmrc",
|
|
180
|
+
"**/.git/**"
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
plugins: [{
|
|
185
|
+
name: "konpeki-html",
|
|
186
|
+
configureServer(server) {
|
|
187
|
+
server.middlewares.use((req, res, next) => void session.handler(req, res, next));
|
|
188
|
+
}
|
|
189
|
+
}]
|
|
190
|
+
});
|
|
191
|
+
try {
|
|
192
|
+
await server.listen();
|
|
193
|
+
} catch (e) {
|
|
194
|
+
await server.close();
|
|
195
|
+
throw e;
|
|
196
|
+
}
|
|
197
|
+
const address = server.httpServer.address();
|
|
198
|
+
const displayHost = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
199
|
+
return {
|
|
200
|
+
server,
|
|
201
|
+
url: `http://${displayHost.includes(":") ? `[${displayHost}]` : displayHost}:${address.port}/html/?session=${session.token}`
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
export { isGoogleFontResource as a, sessionRoute as i, htmlSession as n, previewHTML as r, documentServer as t };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
2
|
+
import { readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { parse } from "parse5";
|
|
5
|
+
import postcss from "postcss";
|
|
6
|
+
//#region html/source.ts
|
|
7
|
+
var revision = (source) => createHash("sha256").update(source).digest("hex");
|
|
8
|
+
var attr = (node, name) => node.attrs.find((a) => a.name === name)?.value;
|
|
9
|
+
function fail(message, status = 422) {
|
|
10
|
+
throw Object.assign(new Error(message), { status });
|
|
11
|
+
}
|
|
12
|
+
function inspectSource(source) {
|
|
13
|
+
const tree = parse(source, {
|
|
14
|
+
sourceCodeLocationInfo: true,
|
|
15
|
+
onParseError(error) {
|
|
16
|
+
if (error.code === "duplicate-attribute") fail("Duplicate HTML attributes are ambiguous; remove them before editing.");
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const elements = [];
|
|
20
|
+
function visit(node) {
|
|
21
|
+
if ("tagName" in node) elements.push(node);
|
|
22
|
+
if ("childNodes" in node) node.childNodes.forEach(visit);
|
|
23
|
+
}
|
|
24
|
+
visit(tree);
|
|
25
|
+
const pages = elements.filter((node) => attr(node, "data-page") !== void 0);
|
|
26
|
+
if (!pages.length) fail("Add an id and data-page to each page element.");
|
|
27
|
+
const ids = /* @__PURE__ */ new Set();
|
|
28
|
+
for (const node of elements) {
|
|
29
|
+
if ([
|
|
30
|
+
"script",
|
|
31
|
+
"iframe",
|
|
32
|
+
"object",
|
|
33
|
+
"embed",
|
|
34
|
+
"base",
|
|
35
|
+
"template",
|
|
36
|
+
"form",
|
|
37
|
+
"input",
|
|
38
|
+
"button",
|
|
39
|
+
"textarea",
|
|
40
|
+
"select",
|
|
41
|
+
"animate",
|
|
42
|
+
"animatetransform",
|
|
43
|
+
"set"
|
|
44
|
+
].includes(node.tagName.toLowerCase()) || node.tagName === "meta" && attr(node, "http-equiv") !== void 0 || node.attrs.some((a) => /^on/i.test(a.name) || [
|
|
45
|
+
"href",
|
|
46
|
+
"src",
|
|
47
|
+
"action"
|
|
48
|
+
].includes(a.name) && /^\s*javascript:/i.test(a.value))) fail("Documents must be static HTML/CSS and inline SVG, without scripts, event handlers, embedded applications or controls.");
|
|
49
|
+
const id = attr(node, "id");
|
|
50
|
+
if (id !== void 0 && (!id || ids.has(id))) fail("Element IDs must be nonempty and unique within the HTML file.");
|
|
51
|
+
if (id) ids.add(id);
|
|
52
|
+
}
|
|
53
|
+
for (const page of pages) if (!attr(page, "id") || page.parentNode?.nodeName !== "body") fail("Pages need an id and must be direct children of body.");
|
|
54
|
+
return {
|
|
55
|
+
pages,
|
|
56
|
+
elements
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function patchSource(source, edit) {
|
|
60
|
+
const { pages, elements } = inspectSource(source);
|
|
61
|
+
const page = pages.find((node) => attr(node, "id") === edit.page);
|
|
62
|
+
const node = elements.find((node) => attr(node, "id") === edit.id);
|
|
63
|
+
if (!page || !node) fail("The selected page or element no longer exists.");
|
|
64
|
+
let ancestor = node;
|
|
65
|
+
while (ancestor && ancestor !== page) ancestor = "parentNode" in ancestor ? ancestor.parentNode ?? void 0 : void 0;
|
|
66
|
+
if (!ancestor || node === page) fail("Select an element inside the page.");
|
|
67
|
+
const location = node.sourceCodeLocation;
|
|
68
|
+
if (!location?.startTag) fail("This element does not have an unambiguous source location.");
|
|
69
|
+
if (edit.kind === "delete") return source.slice(0, location.startOffset) + source.slice(location.endOffset);
|
|
70
|
+
if (edit.kind !== "move" || typeof edit.translate !== "string" || !/^-?\d+(?:\.\d+)?px -?\d+(?:\.\d+)?px$/.test(edit.translate) || edit.translate.length > 100) fail("Invalid correction.");
|
|
71
|
+
let css;
|
|
72
|
+
try {
|
|
73
|
+
css = postcss.parse(attr(node, "style") ?? "", { map: false });
|
|
74
|
+
} catch {
|
|
75
|
+
fail("The inline style could not be parsed. Correct it in source before moving.");
|
|
76
|
+
}
|
|
77
|
+
const declarations = css.nodes.filter((n) => n.type === "decl" && n.prop.toLowerCase() === "translate");
|
|
78
|
+
for (const declaration of declarations) if (declaration.type === "decl") declaration.value = edit.translate;
|
|
79
|
+
if (!declarations.length) css.append({
|
|
80
|
+
prop: "translate",
|
|
81
|
+
value: edit.translate
|
|
82
|
+
});
|
|
83
|
+
css.raws.semicolon = true;
|
|
84
|
+
const escaped = css.toString().replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<");
|
|
85
|
+
const style = location.attrs?.style;
|
|
86
|
+
const start = style?.startOffset ?? location.startTag.endOffset - (source[location.startTag.endOffset - 2] === "/" ? 2 : 1);
|
|
87
|
+
const end = style?.endOffset ?? start;
|
|
88
|
+
return source.slice(0, start) + `${style ? "" : " "}style="${escaped}"` + source.slice(end);
|
|
89
|
+
}
|
|
90
|
+
function fileSource(path) {
|
|
91
|
+
path = resolve(path);
|
|
92
|
+
let queue = Promise.resolve();
|
|
93
|
+
const history = [];
|
|
94
|
+
async function read() {
|
|
95
|
+
const source = await readFile(path, "utf8");
|
|
96
|
+
const { pages } = inspectSource(source);
|
|
97
|
+
return {
|
|
98
|
+
source,
|
|
99
|
+
revision: revision(source),
|
|
100
|
+
name: basename(path),
|
|
101
|
+
path,
|
|
102
|
+
key: revision(path),
|
|
103
|
+
pages: pages.map((p) => attr(p, "id"))
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function edit(expected, change) {
|
|
107
|
+
const result = queue.then(async () => {
|
|
108
|
+
const current = await read();
|
|
109
|
+
if (current.revision !== expected) fail("Source changed outside this preview. Reloaded the latest file; retry your correction.", 409);
|
|
110
|
+
const previous = history.at(-1);
|
|
111
|
+
if (change.kind === "undo" && (!previous || previous.after !== current.revision)) fail("Nothing safe to undo: the source may have changed externally.", 409);
|
|
112
|
+
const next = change.kind === "undo" ? previous.before : patchSource(current.source, change);
|
|
113
|
+
inspectSource(next);
|
|
114
|
+
const temporary = join(dirname(path), `.${basename(path)}.${randomBytes(8).toString("hex")}.tmp`);
|
|
115
|
+
try {
|
|
116
|
+
await writeFile(temporary, next, { flag: "wx" });
|
|
117
|
+
if (revision(await readFile(path, "utf8")) !== current.revision) fail("Source changed while saving. Retry your correction.", 409);
|
|
118
|
+
await rename(temporary, path);
|
|
119
|
+
} finally {
|
|
120
|
+
await rm(temporary, { force: true });
|
|
121
|
+
}
|
|
122
|
+
if (change.kind === "undo") history.pop();
|
|
123
|
+
else {
|
|
124
|
+
history.push({
|
|
125
|
+
before: current.source,
|
|
126
|
+
after: revision(next)
|
|
127
|
+
});
|
|
128
|
+
if (history.length > 30) history.shift();
|
|
129
|
+
}
|
|
130
|
+
return read();
|
|
131
|
+
});
|
|
132
|
+
queue = result.catch(() => {});
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
read,
|
|
137
|
+
edit
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { revision as i, inspectSource as n, patchSource as r, fileSource as t };
|
package/skills/konpeki/SKILL.md
CHANGED
|
@@ -1,186 +1,102 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: konpeki
|
|
3
|
-
description:
|
|
4
|
-
compatibility: Requires Node.js 24+,
|
|
3
|
+
description: Creates, checks, renders, and revises static HTML visuals. Use for landscape slides, résumés, multi-page documents, one-pagers/cards, and covers/social graphics, including charts and diagrams within them.
|
|
4
|
+
compatibility: Requires Node.js 24+, file and command access, and a matching Konpeki runtime installed in the workspace or a prepared checkout. Inspection and export require pinned Chromium.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Konpeki
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
##
|
|
31
|
-
|
|
32
|
-
|
|
9
|
+
Turn a brief and evidence into a finished visual with HTML as editable source and
|
|
10
|
+
PNG/PDF as delivery artifacts. Infer a suitable form and complete the work rather
|
|
11
|
+
than requiring an outline or design checkpoint, unless the user requests one.
|
|
12
|
+
|
|
13
|
+
## Before authoring
|
|
14
|
+
|
|
15
|
+
1. Resolve the runtime. From an installed skill, run
|
|
16
|
+
`node scripts/ensure-runtime.mjs` with no arguments; it reports a compatible
|
|
17
|
+
checkout or locally installed package. Its `root` locates the guides below;
|
|
18
|
+
run its `cli` path with Node. See `SETUP.md` if no runtime is found.
|
|
19
|
+
2. From that root, read `AUTHORING.md` for accuracy and editorial judgment and
|
|
20
|
+
`html/README.md` for all HTML, file, theme, diagnostic, preview, and CLI
|
|
21
|
+
rules. Do not duplicate or override those rules here.
|
|
22
|
+
3. Ground the work in the requested audience, takeaway, supplied evidence,
|
|
23
|
+
destination, and output format. Ask only when missing facts or conflicting
|
|
24
|
+
constraints prevent faithful work.
|
|
25
|
+
4. Infer the format and read its guide below. Read only the relevant sections
|
|
26
|
+
of [content patterns](references/patterns.md) when the work mixes text with
|
|
27
|
+
visuals or includes a chart, table, comparison, process, system map,
|
|
28
|
+
hierarchy, or annotated detail.
|
|
29
|
+
|
|
30
|
+
## Choose the output guide
|
|
31
|
+
|
|
32
|
+
These guides cover content, layout, and review decisions, not fixed templates
|
|
33
|
+
or a placement grid. The brief overrides format defaults, never accuracy,
|
|
34
|
+
readability, or the floor's bans. Do not ask the user to choose a taxonomy.
|
|
35
|
+
|
|
36
|
+
| Requested output | Read |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| Landscape presentation or deck | [Slides](references/slides.md) |
|
|
39
|
+
| Résumé or CV | [Résumé](references/resume.md) |
|
|
40
|
+
| Report, essay, proposal, or other sustained multi-page reading | [Long document](references/long-document.md) |
|
|
41
|
+
| Bounded brief, reference sheet, one-pager, or card at A4 or another size | [One-pager/card](references/one-pager.md) |
|
|
42
|
+
| Cover, thumbnail, link preview, or social graphic | [Cover/social graphic](references/cover.md) |
|
|
43
|
+
|
|
44
|
+
For a mixed-format request, read each applicable guide. A one-page résumé still
|
|
45
|
+
uses the résumé guide; a diagram within a report uses the long-document guide
|
|
46
|
+
and the relevant pattern section. Start freely when none fits exactly.
|
|
47
|
+
|
|
48
|
+
## Create or revise
|
|
49
|
+
|
|
50
|
+
1. Choose the user's document path. For a new document, run
|
|
51
|
+
`node scripts/prepare-document.mjs <cli> <document.html>` to place a validated
|
|
52
|
+
starter and theme stylesheets beside it without overwriting existing files.
|
|
53
|
+
The default theme uses Google Fonts; use local or embedded fonts for offline
|
|
54
|
+
or reproducible rendering.
|
|
55
|
+
2. Reread any existing source before changing it. Preserve stable IDs, facts,
|
|
56
|
+
provenance, deliberate human edits, and unrelated or newer changes.
|
|
57
|
+
3. Plan each page before writing HTML, in your working notes: its format and
|
|
58
|
+
viewing size, the headline as a claim, the elements it carries in reading
|
|
59
|
+
order, and what the format's budget forces out.
|
|
60
|
+
4. Read [floor.md](floor.md) immediately before writing or revising HTML. Its
|
|
61
|
+
bans have no exceptions; its defaults yield only to an explicit brief.
|
|
62
|
+
5. Read `html/theme.md` for the theme API. Link the default Cobalt `theme.css`.
|
|
63
|
+
When the brief brings its own look, adapt a copy using the same contract.
|
|
64
|
+
Read CSS implementation only when changing a theme, not when using one.
|
|
65
|
+
Author the HTML and page-specific composition; use complete type roles and
|
|
66
|
+
theme tokens.
|
|
67
|
+
6. Run `node <cli> validate <document.html>` and fix source-contract errors.
|
|
68
|
+
7. Run `node <cli> inspect <document.html>`; use `--page N` or `--details` when useful.
|
|
69
|
+
Use each page's resolved `theme.type` to plan type and line lengths.
|
|
70
|
+
Resolve errors. Fix each warning, or keep it with a reason you will report;
|
|
71
|
+
floor warnings use their rule ID as the diagnostic code.
|
|
72
|
+
8. Render every affected page at 2x PNG. `render` prints the floor's review
|
|
73
|
+
checklist: check the page against it at intended size and enlarged, along
|
|
74
|
+
with facts, readability, clipping and consistency.
|
|
75
|
+
9. Repeat editing, validation, inspection, and visual review until sound. Inspect
|
|
76
|
+
a requested PDF separately because it is a distinct artifact.
|
|
77
|
+
|
|
78
|
+
Example focused loop:
|
|
33
79
|
|
|
34
80
|
```sh
|
|
35
|
-
node
|
|
81
|
+
node <cli> validate document.html
|
|
82
|
+
node <cli> inspect document.html
|
|
83
|
+
node <cli> render document.html --page 1 --format png --scale 2 --output page-1.png
|
|
36
84
|
```
|
|
37
85
|
|
|
38
|
-
|
|
39
|
-
or a previously installed cache. If none exists, follow the host's permission
|
|
40
|
-
flow, then rerun with `--install`. This installs the pinned npm release in a
|
|
41
|
-
user cache, not in the user's project, and does not change their agent guidance.
|
|
42
|
-
If Node.js 24+ or npm is missing, report that prerequisite and follow the host's
|
|
43
|
-
toolchain setup rules; never claim the runtime is ready when setup failed.
|
|
44
|
-
|
|
45
|
-
The script returns JSON with `root` (runtime resources) and `cli` (the executable
|
|
46
|
-
path). In the instructions below, `<root>` and `<cli>` mean those returned absolute
|
|
47
|
-
paths. Quote paths in shell commands. They may be outside the skill directory;
|
|
48
|
-
do not assume a copied skill contains the runtime. Read resources from `<root>`
|
|
49
|
-
and keep authored documents in the user's workspace, outside the runtime/cache.
|
|
86
|
+
## Human feedback
|
|
50
87
|
|
|
51
|
-
|
|
88
|
+
Use `node <cli> preview <document.html>` only when interactive review is useful. After
|
|
89
|
+
the reviewer uses **Copy & clear**, use the full local path in the prompt to
|
|
90
|
+
reread current source, then address feedback by stable target ID. If a target no
|
|
91
|
+
longer exists or feedback conflicts with newer work, report that instead of
|
|
92
|
+
guessing. Re-run the verification loop after source-backed moves, deletions, or
|
|
93
|
+
agent-authored revisions.
|
|
52
94
|
|
|
53
|
-
|
|
54
|
-
conversation. With neither, use `slides/untitled/composition.json`. If several
|
|
55
|
-
documents are plausible, ask which to open instead of guessing.
|
|
95
|
+
## Deliver
|
|
56
96
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
It creates a validated blank page only when the file is missing. Existing files
|
|
64
|
-
are validated without rewriting them, and unreadable files are left untouched.
|
|
65
|
-
It returns `{ compositionPath, created }`; this means the document is prepared,
|
|
66
|
-
not that its browser is open. Do not replace a failed document with an example.
|
|
67
|
-
|
|
68
|
-
Follow **Open the file-backed preview** below. Verify the intended document loads,
|
|
69
|
-
then return its path and usable editor link. For a new document, verify an empty
|
|
70
|
-
page with editing controls. Stop here until the person asks to generate or revise.
|
|
71
|
-
Do not start an agent listener just because an editor is open.
|
|
72
|
-
|
|
73
|
-
## generate — interpret the brief
|
|
74
|
-
|
|
75
|
-
Read `<root>/AGENTS.md` and `<root>/AUTHORING.md`. Use the user's existing prompt as the
|
|
76
|
-
brief; do not ask them to repeat it in the canvas. Establish the source facts,
|
|
77
|
-
audience, takeaway and destination. Ask only when missing facts or conflicting
|
|
78
|
-
requirements prevent a faithful result.
|
|
79
|
-
|
|
80
|
-
Continue the document opened by init or the current conversation unless the
|
|
81
|
-
person requests a new one. Before changing an existing document, inspect
|
|
82
|
-
`node "<cli>" request "<composition.json>"`. Claim a `submitted` request with
|
|
83
|
-
`wait` and follow **Review handoff** below. A `working` request may belong to
|
|
84
|
-
another agent; coordinate ownership before resuming, rather than claiming it again.
|
|
85
|
-
With no active request, work from the brief and current composition normally.
|
|
86
|
-
|
|
87
|
-
Use `default` authoring mode unless requested otherwise. Explicit visual
|
|
88
|
-
preferences override taste defaults, not factual fidelity or readability. Use
|
|
89
|
-
the comparison workflow in `<root>/AUTHORING.md`
|
|
90
|
-
only when requested or accepted. A single page is a complete creation; choose
|
|
91
|
-
dimensions for its destination rather than assuming a slide deck.
|
|
92
|
-
|
|
93
|
-
### Author or revise the composition
|
|
94
|
-
|
|
95
|
-
Read `<root>/composition/README.md`. For a new visual,
|
|
96
|
-
create `slides/<name>/composition.json` unless the user supplies another path.
|
|
97
|
-
Use an unused destination; do not overwrite a different document at that path.
|
|
98
|
-
Save the creative brief in `PROMPT.md` and substantial facts, citations and asset
|
|
99
|
-
provenance in `SOURCE.md`. Separate assumptions from supplied facts. Preserve
|
|
100
|
-
creative requests accurately, but omit private coordination and environment or
|
|
101
|
-
agent metadata; label excerpts and redactions rather than calling them verbatim.
|
|
102
|
-
|
|
103
|
-
For revisions, reread the current file first. Preserve unrelated content,
|
|
104
|
-
component/vector IDs and human-edited geometry. Honor explicit chart or diagram
|
|
105
|
-
choices. Recompose for a new aspect ratio instead of stretching or cropping.
|
|
106
|
-
|
|
107
|
-
Prefer native text and semantic components. When standard drafts cannot express
|
|
108
|
-
the visual, use editable vectors inside their owning component. Reuse
|
|
109
|
-
`<root>/design/README.md` and reference examples as needed.
|
|
110
|
-
Trusted React may generate SVG for conversion, but imported JSX must not execute
|
|
111
|
-
in the canvas or become a second document source.
|
|
112
|
-
|
|
113
|
-
### Validate, render and repair
|
|
114
|
-
|
|
115
|
-
```sh
|
|
116
|
-
node "<cli>" validate "<composition.json>"
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Follow **Open the file-backed preview** below, then inspect
|
|
120
|
-
every affected page and requested theme at presentation and smaller review sizes
|
|
121
|
-
after fonts load. Check factual fidelity, text bounds, contrast, reading order
|
|
122
|
-
and relationships. Repair consequential issues and inspect fresh renders.
|
|
123
|
-
|
|
124
|
-
Follow `<root>/docs/development.md` verification guidance for
|
|
125
|
-
code changes. Validation alone is not visual review. If a required check cannot
|
|
126
|
-
run, state the limitation; do not claim it passed. Honor requested checkpoints;
|
|
127
|
-
otherwise continue to finished output.
|
|
128
|
-
|
|
129
|
-
### Deliver and continue revisions
|
|
130
|
-
|
|
131
|
-
Return the composition path, usable preview, reviewed images or requested
|
|
132
|
-
exports, source attribution and verification limitations. Verify exports
|
|
133
|
-
separately; browser images do not prove editable PDF/PPTX or font fidelity.
|
|
134
|
-
Include any generator source while keeping composition JSON authoritative.
|
|
135
|
-
|
|
136
|
-
Accept follow-up chat feedback without requiring another generate invocation.
|
|
137
|
-
Reread the current composition each time, preserve human edits, and repeat the
|
|
138
|
-
render/repair checks. For canvas feedback, the person submits notes with **Build
|
|
139
|
-
it**. A submitted request can be handled by invoking generate again or by an
|
|
140
|
-
agent already waiting for it. Do not treat unsent notes as a submitted request.
|
|
141
|
-
|
|
142
|
-
## Open the file-backed preview
|
|
143
|
-
|
|
144
|
-
Reuse a known live preview for the same absolute document path; verify it still
|
|
145
|
-
loads that document. Otherwise start:
|
|
146
|
-
|
|
147
|
-
```sh
|
|
148
|
-
node "<cli>" preview "<composition.json>"
|
|
149
|
-
```
|
|
97
|
+
Deliver requested PNG/PDF artifacts first. Keep editable HTML and local resources
|
|
98
|
+
in the agent workspace; share them when useful or requested. Report every
|
|
99
|
+
warning you kept and why, and any review limitations. Do not claim checks you did
|
|
100
|
+
not run.
|
|
150
101
|
|
|
151
|
-
|
|
152
|
-
exact printed session URL in the host's in-app browser when available, otherwise
|
|
153
|
-
the person's regular browser. For a remote workspace, use its authenticated
|
|
154
|
-
preview or port forwarding and preserve the session query; a remote loopback URL
|
|
155
|
-
is not a usable handoff. Session URLs grant editing access: do not publish them.
|
|
156
|
-
Verify the intended document actually loads before calling setup complete. If
|
|
157
|
-
browser access is unavailable, return the usable link and state it was not checked.
|
|
158
|
-
Do not substitute the standalone playground: it cannot save to the agent's file
|
|
159
|
-
or submit canvas review requests.
|
|
160
|
-
|
|
161
|
-
## Review handoff
|
|
162
|
-
|
|
163
|
-
When explicitly waiting for ongoing canvas reviews, run
|
|
164
|
-
`node "<cli>" wait "<composition.json>"` alongside the preview using the host's
|
|
165
|
-
supported long-running tool. Report whether a listener is actually running.
|
|
166
|
-
**Build it** cannot wake an idle agent. Without a listener, use its **Copy prompt**
|
|
167
|
-
handoff or ask the person to resume generate in agent chat; do not claim a permanent
|
|
168
|
-
connection. On receiving a request,
|
|
169
|
-
reread the named file and compare its revision with the request. If it changed,
|
|
170
|
-
reconcile against the latest document instead of applying a stale rewrite.
|
|
171
|
-
Apply every attached note to its named page, component or vector-element ID;
|
|
172
|
-
without notes, respect the selected scope. Preserve unrelated edits and stable
|
|
173
|
-
IDs. A request is already marked working when `wait` returns; it is not deleted.
|
|
174
|
-
Validate, render, inspect and repair the result, then run
|
|
175
|
-
`node "<cli>" finish "<composition.json>" <request-id> --message "Updated and checked"`.
|
|
176
|
-
File changes alone do not resolve notes. If blocked, finish with
|
|
177
|
-
`--status needs-clarification --message "..."` or `--status failed --message "..."`;
|
|
178
|
-
unresolved notes remain for retry. Never mark a partially handled batch done.
|
|
179
|
-
Use `node "<cli>" request "<composition.json>"` to recover an interrupted request and
|
|
180
|
-
verify it is still active before further writes. Cancellation does not stop your
|
|
181
|
-
process: stop work if the request is no longer active. Do not edit the feedback
|
|
182
|
-
sidecar directly. Return to `wait` only when the person requested a continuing loop.
|
|
183
|
-
|
|
184
|
-
See `<root>/docs/workflow.md` for the full handoff contract.
|
|
185
|
-
Never bypass revision checks or mutate hidden browser storage to replace the
|
|
186
|
-
document. Do not publish without permission.
|
|
102
|
+
Do not publish, push, deploy, or create a release without explicit permission.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Untitled visual</title>
|
|
6
|
+
<link rel="stylesheet" href="theme.css">
|
|
7
|
+
<style>
|
|
8
|
+
h1 { max-width: 850px; }
|
|
9
|
+
p { max-width: 760px; margin-top: var(--kp-space-3); color: var(--kp-muted); }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<main id="page-1" data-page data-size="link">
|
|
14
|
+
<h1 id="title">Untitled visual</h1>
|
|
15
|
+
</main>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|