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.
Files changed (162) hide show
  1. package/AGENTS.md +21 -28
  2. package/AUTHORING.md +68 -229
  3. package/CONTRIBUTING.md +10 -23
  4. package/README.md +96 -87
  5. package/SETUP.md +50 -114
  6. package/docs/development.md +43 -248
  7. package/docs/workflow.md +6 -108
  8. package/html/README.md +140 -0
  9. package/html/browser.ts +103 -0
  10. package/html/document.ts +38 -0
  11. package/html/floor.ts +34 -0
  12. package/html/index.html +5 -0
  13. package/html/inspect.ts +313 -0
  14. package/html/preview.css +62 -0
  15. package/html/preview.tsx +463 -0
  16. package/html/review-hints.ts +63 -0
  17. package/html/server.ts +99 -0
  18. package/html/source.ts +104 -0
  19. package/html/starter.ts +6 -0
  20. package/html/theme-authoring.md +176 -0
  21. package/html/theme.md +61 -0
  22. package/index.html +9 -9
  23. package/package.json +21 -35
  24. package/plugin.json +1 -1
  25. package/public/og.png +0 -0
  26. package/runtime/browser-B-29TH1a.mjs +595 -0
  27. package/runtime/floor-Cmk7G3pU.mjs +41 -0
  28. package/runtime/konpeki.mjs +90 -1410
  29. package/runtime/server-BAqC_5WD.mjs +2 -0
  30. package/runtime/server-DfRpfcY9.mjs +205 -0
  31. package/runtime/source-B_Ui9tMp.mjs +141 -0
  32. package/runtime/source-CZQq9GUO.mjs +2 -0
  33. package/skills/konpeki/SKILL.md +88 -172
  34. package/skills/konpeki/assets/blank.html +17 -0
  35. package/skills/konpeki/floor.md +76 -0
  36. package/skills/konpeki/references/cover.md +27 -0
  37. package/skills/konpeki/references/long-document.md +40 -0
  38. package/skills/konpeki/references/one-pager.md +27 -0
  39. package/skills/konpeki/references/patterns.md +118 -0
  40. package/skills/konpeki/references/resume.md +28 -0
  41. package/skills/konpeki/references/slides.md +28 -0
  42. package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
  43. package/skills/konpeki/scripts/prepare-document.mjs +25 -14
  44. package/src/components/PageBoard.tsx +156 -0
  45. package/src/lib/alignment.ts +21 -0
  46. package/src/lib/page-board.ts +25 -0
  47. package/src/lib/review-position.ts +19 -0
  48. package/src/styles/base.css +4 -6
  49. package/src/styles/feedback.css +97 -61
  50. package/src/styles/shell.css +95 -323
  51. package/theme-base.css +90 -0
  52. package/theme.css +56 -0
  53. package/vite.config.ts +2 -5
  54. package/composition/README.md +0 -156
  55. package/composition/compile.ts +0 -227
  56. package/composition/document.ts +0 -600
  57. package/composition/schema.json +0 -3001
  58. package/composition/schema.ts +0 -437
  59. package/composition/theme-tokens.ts +0 -16
  60. package/composition/types.ts +0 -269
  61. package/composition/validate.ts +0 -226
  62. package/composition/vector.ts +0 -143
  63. package/composition/visualizations.ts +0 -319
  64. package/design/README.md +0 -17
  65. package/design/palettes/README.md +0 -14
  66. package/design/palettes/base.ts +0 -14
  67. package/design/palettes/candidates.ts +0 -19
  68. package/design/palettes/index.ts +0 -78
  69. package/design/review/color-theme.md +0 -44
  70. package/design/review/layout.md +0 -16
  71. package/design/review/text.md +0 -18
  72. package/design/review/typography.md +0 -15
  73. package/design/review/visuals.md +0 -31
  74. package/design/semantic-patterns.md +0 -43
  75. package/design/themes/README.md +0 -40
  76. package/design/themes/index.ts +0 -24
  77. package/design/visual-languages/technical-product.md +0 -17
  78. package/design/visual-review.md +0 -88
  79. package/lib/assets.d.ts +0 -8
  80. package/lib/charts.ts +0 -18
  81. package/lib/contrast.ts +0 -16
  82. package/lib/layouts.ts +0 -50
  83. package/lib/slide.tsx +0 -42
  84. package/lib/taste.ts +0 -17
  85. package/lib/text.tsx +0 -89
  86. package/lib/typeface.ts +0 -44
  87. package/scripts/migrate-react-page.ts +0 -120
  88. package/skills/konpeki/assets/blank.json +0 -23
  89. package/slides/README.md +0 -153
  90. package/slides/architecture/PROMPT.md +0 -31
  91. package/slides/architecture/index.tsx +0 -102
  92. package/slides/article-brief/PROMPT.md +0 -35
  93. package/slides/article-brief/index.tsx +0 -71
  94. package/slides/bar-chart/PROMPT.md +0 -39
  95. package/slides/bar-chart/index.tsx +0 -97
  96. package/slides/comparison/PROMPT.md +0 -29
  97. package/slides/comparison/index.tsx +0 -95
  98. package/slides/decision-memo/PROMPT.md +0 -34
  99. package/slides/decision-memo/index.tsx +0 -85
  100. package/slides/delivery-plan/PROMPT.md +0 -45
  101. package/slides/delivery-plan/index.tsx +0 -105
  102. package/slides/experiment/PROMPT.md +0 -44
  103. package/slides/experiment/index.tsx +0 -127
  104. package/slides/incident-workflow/PROMPT.md +0 -57
  105. package/slides/incident-workflow/index.tsx +0 -78
  106. package/slides/introducing-konpeki/PROMPT.md +0 -40
  107. package/slides/introducing-konpeki/README.md +0 -76
  108. package/slides/introducing-konpeki/SOURCE.md +0 -26
  109. package/slides/introducing-konpeki/author.ts +0 -165
  110. package/slides/introducing-konpeki/composition.json +0 -3270
  111. package/slides/line-chart/PROMPT.md +0 -40
  112. package/slides/line-chart/index.tsx +0 -72
  113. package/slides/migration/PROMPT.md +0 -38
  114. package/slides/migration/index.tsx +0 -89
  115. package/slides/og-images/PROMPT.md +0 -21
  116. package/slides/og-images/index.tsx +0 -76
  117. package/slides/product-introduction/PROMPT.md +0 -24
  118. package/slides/product-introduction/index.tsx +0 -105
  119. package/slides/research-brief/PROMPT.md +0 -40
  120. package/slides/research-brief/index.tsx +0 -104
  121. package/slides/results-explanation/PROMPT.md +0 -32
  122. package/slides/results-explanation/index.tsx +0 -96
  123. package/slides/retrospective/PROMPT.md +0 -43
  124. package/slides/retrospective/index.tsx +0 -105
  125. package/slides/sankey/PROMPT.md +0 -11
  126. package/slides/sankey/index.tsx +0 -93
  127. package/slides/teaching/PROMPT.md +0 -45
  128. package/slides/teaching/index.tsx +0 -124
  129. package/slides/vertical-bar-charts/PROMPT.md +0 -13
  130. package/slides/vertical-bar-charts/index.tsx +0 -97
  131. package/src/app/App.tsx +0 -820
  132. package/src/components/BuildOrb.tsx +0 -40
  133. package/src/components/Canvas.tsx +0 -1185
  134. package/src/components/DiagramTypeIcon.tsx +0 -78
  135. package/src/components/InspectorPanel.tsx +0 -773
  136. package/src/components/LeftPanel.tsx +0 -120
  137. package/src/components/PageSizePicker.tsx +0 -30
  138. package/src/components/Presentation.tsx +0 -105
  139. package/src/components/RevisionNotes.tsx +0 -56
  140. package/src/components/RightPanel.tsx +0 -201
  141. package/src/components/VectorOverflowWarning.tsx +0 -46
  142. package/src/components/WorkspaceChrome.tsx +0 -288
  143. package/src/components/ui.tsx +0 -53
  144. package/src/lib/examples/react-page-migration.json +0 -1295
  145. package/src/lib/examples.ts +0 -42
  146. package/src/lib/export-png.ts +0 -104
  147. package/src/lib/file-session.ts +0 -87
  148. package/src/lib/history.ts +0 -53
  149. package/src/lib/model.ts +0 -188
  150. package/src/lib/page-size.ts +0 -24
  151. package/src/lib/presentation.ts +0 -17
  152. package/src/lib/review.ts +0 -26
  153. package/src/lib/storage.ts +0 -71
  154. package/src/lib/theme.ts +0 -25
  155. package/src/lib/use-file-session.ts +0 -227
  156. package/src/main.tsx +0 -29
  157. package/src/styles/canvas.css +0 -299
  158. package/src/styles/chrome.css +0 -384
  159. package/src/styles/component-previews.css +0 -386
  160. package/src/styles/left-panel.css +0 -166
  161. package/src/styles/presentation.css +0 -72
  162. package/src/styles/right-panel.css +0 -1215
package/html/server.ts ADDED
@@ -0,0 +1,99 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { createServer as createHTTPServer } from "node:http";
3
+ import { readFile, realpath } from "node:fs/promises";
4
+ import { dirname, extname, isAbsolute, relative, resolve } from "node:path";
5
+ import { randomBytes } from "node:crypto";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createServer } from "vite";
8
+ import { fileSource } from "./source.ts";
9
+ import { documentHTML, documentPolicy } from "./document.ts";
10
+
11
+ const types: Record<string, string> = { ".css": "text/css", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif", ".svg": "image/svg+xml", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".otf": "font/otf" };
12
+ export const sessionRoute = "/__konpeki/html";
13
+
14
+ /** Same authenticated document/asset surface for preview and browser exports. */
15
+ export function htmlSession(path: string, token = randomBytes(24).toString("base64url")) {
16
+ const store = fileSource(resolve(path));
17
+ const prefix = `${sessionRoute}/${token}`;
18
+ function send(res: ServerResponse, status: number, value: unknown) {
19
+ res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" });
20
+ res.end(JSON.stringify(value));
21
+ }
22
+ async function handler(req: IncomingMessage, res: ServerResponse, next: () => void) {
23
+ const url = new URL(req.url ?? "/", "http://localhost");
24
+ if (!url.pathname.startsWith(sessionRoute)) return next();
25
+ try {
26
+ if (url.pathname === sessionRoute) {
27
+ if (req.headers["x-konpeki-session"] !== token) return send(res, 403, { error: "Invalid preview session." });
28
+ if (req.method === "GET") return send(res, 200, await store.read());
29
+ if (req.method !== "PATCH") return send(res, 405, { error: "Method not allowed." });
30
+ let size = 0; const chunks: Buffer[] = [];
31
+ for await (const chunk of req) {
32
+ size += chunk.length;
33
+ if (size > 16384) throw Object.assign(new Error("Correction is too large."), { status: 413 });
34
+ chunks.push(chunk);
35
+ }
36
+ const body = JSON.parse(Buffer.concat(chunks).toString());
37
+ if (!body || typeof body.revision !== "string" || !body.edit || !["move", "delete", "undo"].includes(body.edit.kind))
38
+ return send(res, 400, { error: "Invalid correction." });
39
+ return send(res, 200, await store.edit(body.revision, body.edit));
40
+ }
41
+ if (!url.pathname.startsWith(`${prefix}/`)) return send(res, 403, { error: "Invalid preview session." });
42
+ if (req.method !== "GET") return send(res, 405, { error: "Method not allowed." });
43
+ if (url.pathname === `${prefix}/document/`) {
44
+ const snapshot = await store.read();
45
+ if (url.searchParams.has("revision") && url.searchParams.get("revision") !== snapshot.revision)
46
+ return send(res, 409, { error: "The source changed. Retry with its latest revision." });
47
+ const page = url.searchParams.get("page") ?? snapshot.pages[0];
48
+ if (!snapshot.pages.includes(page)) return send(res, 404, { error: "Unknown page." });
49
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": documentPolicy, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "X-Content-Type-Options": "nosniff" });
50
+ return res.end(documentHTML(snapshot.source, page));
51
+ }
52
+ const assets = `${prefix}/document/`;
53
+ if (!url.pathname.startsWith(assets)) return send(res, 404, { error: "Unknown resource." });
54
+ const name = decodeURIComponent(url.pathname.slice(assets.length));
55
+ const type = types[extname(name).toLowerCase()];
56
+ if (!type || name.split(/[\\/]/).some(part => part.startsWith("."))) return send(res, 403, { error: "Only local image, font and CSS assets are served." });
57
+ let base = await realpath(dirname(resolve(path))), file: string;
58
+ try { file = await realpath(resolve(base, name)); }
59
+ catch (error) {
60
+ // A linked default theme is available in previews and fixtures. Copies
61
+ // made by prepare-document carry these assets beside the HTML instead.
62
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT" || !/^theme(?:-base)?\.css$/.test(name)) throw error;
63
+ base = fileURLToPath(new URL("../", import.meta.url));
64
+ file = await realpath(resolve(base, name));
65
+ }
66
+ const within = relative(base, file);
67
+ if (isAbsolute(within) || within.startsWith("..")) return send(res, 403, { error: "Asset is outside the document directory." });
68
+ const bytes = await readFile(file);
69
+ res.writeHead(200, { "Content-Type": type, "Cache-Control": "no-store", "Content-Security-Policy": documentPolicy, "X-Content-Type-Options": "nosniff" });
70
+ res.end(bytes);
71
+ } catch (error) {
72
+ const status = error instanceof SyntaxError || error instanceof URIError ? 400 : (error as { code?: string }).code === "ENOENT" ? 404 : Number((error as { status?: number }).status ?? 500);
73
+ send(res, status, { error: error instanceof Error ? error.message : "Preview failed." });
74
+ }
75
+ }
76
+ return { token, prefix, store, handler };
77
+ }
78
+
79
+ export async function documentServer(path: string) {
80
+ const session = htmlSession(path);
81
+ await session.store.read();
82
+ const server = createHTTPServer((req, res) => void session.handler(req, res, () => { res.writeHead(404); res.end(); }));
83
+ await new Promise<void>((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => resolve()); });
84
+ const address = server.address() as { port: number };
85
+ return { ...session, origin: `http://127.0.0.1:${address.port}`, close: () => new Promise<void>((resolve, reject) => server.close(e => e ? reject(e) : resolve())) };
86
+ }
87
+
88
+ export async function previewHTML(path: string, host: string, port: number, root: string, strictPort = false) {
89
+ const session = htmlSession(path);
90
+ await session.store.read();
91
+ const server = await createServer({ root, configFile: false, logLevel: "silent", server: {
92
+ host, port, strictPort,
93
+ fs: { allow: [root, dirname(fileURLToPath(import.meta.resolve("@fontsource/ibm-plex-sans/package.json")))], deny: [".env", ".env.*", "*.{crt,pem,key,p12,pfx}", ".npmrc", "**/.git/**"] },
94
+ }, plugins: [{ name: "konpeki-html", configureServer(server) { server.middlewares.use((req, res, next) => void session.handler(req, res, next)); } }] });
95
+ try { await server.listen(); } catch (e) { await server.close(); throw e; }
96
+ const address = server.httpServer!.address() as { port: number };
97
+ const displayHost = host === "0.0.0.0" || host === "::" ? "localhost" : host;
98
+ return { server, url: `http://${displayHost.includes(":") ? `[${displayHost}]` : displayHost}:${address.port}/html/?session=${session.token}` };
99
+ }
package/html/source.ts ADDED
@@ -0,0 +1,104 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { readFile, writeFile, rename, rm } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { parse, type DefaultTreeAdapterMap } from "parse5";
5
+ import postcss from "postcss";
6
+
7
+ type Element = DefaultTreeAdapterMap["element"];
8
+ export type Edit = { page: string; id: string } & ({ kind: "move"; translate: string } | { kind: "delete" });
9
+ export const revision = (source: string) => createHash("sha256").update(source).digest("hex");
10
+ const attr = (node: Element, name: string) => node.attrs.find(a => a.name === name)?.value;
11
+ function fail(message: string, status = 422): never { throw Object.assign(new Error(message), { status }); }
12
+
13
+ export function inspectSource(source: string) {
14
+ const tree = parse(source, { sourceCodeLocationInfo: true, onParseError(error) {
15
+ if (error.code === "duplicate-attribute") fail("Duplicate HTML attributes are ambiguous; remove them before editing.");
16
+ } });
17
+ const elements: Element[] = [];
18
+ function visit(node: DefaultTreeAdapterMap["node"]) {
19
+ if ("tagName" in node) elements.push(node);
20
+ if ("childNodes" in node) node.childNodes.forEach(visit);
21
+ }
22
+ visit(tree);
23
+ const pages = elements.filter(node => attr(node, "data-page") !== undefined);
24
+ if (!pages.length) fail("Add an id and data-page to each page element.");
25
+ const ids = new Set<string>();
26
+ for (const node of elements) {
27
+ if (["script", "iframe", "object", "embed", "base", "template", "form", "input", "button", "textarea", "select", "animate", "animatetransform", "set"].includes(node.tagName.toLowerCase()) ||
28
+ node.tagName === "meta" && attr(node, "http-equiv") !== undefined ||
29
+ node.attrs.some(a => /^on/i.test(a.name) || ["href", "src", "action"].includes(a.name) && /^\s*javascript:/i.test(a.value)))
30
+ fail("Documents must be static HTML/CSS and inline SVG, without scripts, event handlers, embedded applications or controls.");
31
+ const id = attr(node, "id");
32
+ if (id !== undefined && (!id || ids.has(id))) fail("Element IDs must be nonempty and unique within the HTML file.");
33
+ if (id) ids.add(id);
34
+ }
35
+ for (const page of pages) {
36
+ if (!attr(page, "id") || page.parentNode?.nodeName !== "body")
37
+ fail("Pages need an id and must be direct children of body.");
38
+ }
39
+ return { pages, elements };
40
+ }
41
+
42
+ export function patchSource(source: string, edit: Edit) {
43
+ const { pages, elements } = inspectSource(source);
44
+ const page = pages.find(node => attr(node, "id") === edit.page);
45
+ const node = elements.find(node => attr(node, "id") === edit.id);
46
+ if (!page || !node) fail("The selected page or element no longer exists.");
47
+ let ancestor: DefaultTreeAdapterMap["node"] | undefined = node;
48
+ while (ancestor && ancestor !== page) ancestor = "parentNode" in ancestor ? ancestor.parentNode ?? undefined : undefined;
49
+ if (!ancestor || node === page) fail("Select an element inside the page.");
50
+ const location = node.sourceCodeLocation;
51
+ if (!location?.startTag) fail("This element does not have an unambiguous source location.");
52
+ if (edit.kind === "delete") return source.slice(0, location.startOffset) + source.slice(location.endOffset);
53
+ if (edit.kind !== "move" || typeof edit.translate !== "string" || !/^-?\d+(?:\.\d+)?px -?\d+(?:\.\d+)?px$/.test(edit.translate) || edit.translate.length > 100)
54
+ fail("Invalid correction.");
55
+ // Parse the authored CSS, not CSSOM's normalized serialization. PostCSS keeps
56
+ // unrelated declaration values, comments and whitespace, including hex colors.
57
+ // Inline styles have no source maps; never follow authored sourceMappingURLs.
58
+ let css;
59
+ try { css = postcss.parse(attr(node, "style") ?? "", { map: false }); }
60
+ catch { fail("The inline style could not be parsed. Correct it in source before moving."); }
61
+ const declarations = css.nodes.filter(n => n.type === "decl" && n.prop.toLowerCase() === "translate");
62
+ for (const declaration of declarations) if (declaration.type === "decl") declaration.value = edit.translate;
63
+ if (!declarations.length) css.append({ prop: "translate", value: edit.translate });
64
+ css.raws.semicolon = true;
65
+ const escaped = css.toString().replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;");
66
+ const style = location.attrs?.style;
67
+ const start = style?.startOffset ?? location.startTag.endOffset - (source[location.startTag.endOffset - 2] === "/" ? 2 : 1);
68
+ const end = style?.endOffset ?? start;
69
+ return source.slice(0, start) + `${style ? "" : " "}style="${escaped}"` + source.slice(end);
70
+ }
71
+
72
+ export function fileSource(path: string) {
73
+ path = resolve(path);
74
+ let queue: Promise<unknown> = Promise.resolve();
75
+ const history: { before: string; after: string }[] = [];
76
+ async function read() {
77
+ const source = await readFile(path, "utf8");
78
+ const { pages } = inspectSource(source);
79
+ return { source, revision: revision(source), name: basename(path), path, key: revision(path), pages: pages.map(p => attr(p, "id")!) };
80
+ }
81
+ function edit(expected: string, change: Edit | { kind: "undo" }) {
82
+ const result = queue.then(async () => {
83
+ const current = await read();
84
+ if (current.revision !== expected) fail("Source changed outside this preview. Reloaded the latest file; retry your correction.", 409);
85
+ const previous = history.at(-1);
86
+ if (change.kind === "undo" && (!previous || previous.after !== current.revision))
87
+ fail("Nothing safe to undo: the source may have changed externally.", 409);
88
+ const next = change.kind === "undo" ? previous!.before : patchSource(current.source, change);
89
+ inspectSource(next);
90
+ const temporary = join(dirname(path), `.${basename(path)}.${randomBytes(8).toString("hex")}.tmp`);
91
+ try {
92
+ await writeFile(temporary, next, { flag: "wx" });
93
+ if (revision(await readFile(path, "utf8")) !== current.revision) fail("Source changed while saving. Retry your correction.", 409);
94
+ await rename(temporary, path);
95
+ } finally { await rm(temporary, { force: true }); }
96
+ if (change.kind === "undo") history.pop();
97
+ else { history.push({ before: current.source, after: revision(next) }); if (history.length > 30) history.shift(); }
98
+ return read();
99
+ });
100
+ queue = result.catch(() => {});
101
+ return result;
102
+ }
103
+ return { read, edit };
104
+ }
@@ -0,0 +1,6 @@
1
+ import starter from "../skills/konpeki/assets/blank.html?raw";
2
+ import themeURL from "../theme.css?url";
3
+
4
+ export function starterSource() {
5
+ return starter.replace('href="theme.css"', `href="${new URL(themeURL, location.href).href}"`);
6
+ }
@@ -0,0 +1,176 @@
1
+ # Author a theme: contract v1
2
+
3
+ A theme is a local CSS entry point plus its fonts and other licensed assets.
4
+ Documents use the names below rather than depending on a particular palette or
5
+ font. Replace `theme.css` to change the visual treatment without rewriting HTML.
6
+ Different font metrics can still change wrapping and fit: inspect every page
7
+ after a swap.
8
+
9
+ Every public theme custom property uses the `--kp-` prefix. A compatible theme
10
+ entry point sets `--kp-theme: 1` on `:root`; the shared base intentionally does
11
+ not set this marker, so loading `theme-base.css` alone does not opt into theme
12
+ checks.
13
+
14
+ `theme.css` supplies reusable appearance. [AUTHORING.md](../AUTHORING.md) and
15
+ design skills supply judgment: what to emphasize, how to arrange evidence, and
16
+ when a treatment is appropriate. Document CSS owns composition and data geometry.
17
+
18
+ Document authors only need [Use a theme](theme.md). Read this full contract when
19
+ creating or changing a stylesheet. A theme may carry short optional `NOTES.md`
20
+ beside its CSS for look-specific judgment; keep general authoring guidance in
21
+ AUTHORING.md. Notes do not add tokens or change the checks.
22
+
23
+ ## Type roles
24
+
25
+ Every role has `--kp-font-ROLE`, `--kp-leading-ROLE`, `--kp-weight-ROLE`, and
26
+ `--kp-tracking-ROLE`. Sizes and leading resolve through custom properties to
27
+ literal positive pixel lengths such as `24px`; v1 does not accept `rem` or `calc()`
28
+ for these slots. Weights must have declared webfont faces; tracking uses `em` or `px`.
29
+ These names form complete treatments, not independent invitations to mix seven
30
+ sizes with seven line heights.
31
+
32
+ | Role / `data-type` | Semantic default | Family | Default color role |
33
+ | --- | --- | --- | --- |
34
+ | `fine` | Explicit hook only | `--kp-font-family` | `--kp-muted` |
35
+ | `caption` | `figcaption` | `--kp-font-family` | `--kp-muted` |
36
+ | `body` | Page text | `--kp-font-family` | `--kp-fg` |
37
+ | `lead` | `h3` | `--kp-font-family` | Inherited |
38
+ | `heading` | `h2` | `--kp-font-family-heading` | Inherited |
39
+ | `title` | `h1` | `--kp-font-family-heading` | `--kp-accent` |
40
+ | `display` | Explicit hook only | `--kp-font-family-display` | `--kp-accent` |
41
+
42
+ An explicit role replaces an element's semantic default: `h2 data-type="caption"`
43
+ uses the caption treatment, and remains an `h2`. An unmarked paragraph inherits
44
+ its context. `strong` uses `--kp-weight-strong`. Inline `code` keeps its context's
45
+ size and leading but uses `--kp-font-family-mono`, `--kp-weight-mono`, and the
46
+ accent. All four family slots are required; they may refer to the same face.
47
+
48
+ `inspect` reports the resolved `theme.type` for each page, including size, leading,
49
+ weight, tracking, and family. Use those measurements to plan line lengths without
50
+ opening CSS: Cobalt's link preset has body 32/44 and title 72/80 CSS px.
51
+
52
+ ## Color roles
53
+
54
+ Konpeki's only bundled theme is cobalt blue on a light canvas, independent of OS
55
+ preference. Define every token below when adapting it for a brief.
56
+
57
+ | Tokens | Meaning |
58
+ | --- | --- |
59
+ | `--kp-bg`, `--kp-fg`, `--kp-muted` | Canvas, primary ink, supporting ink |
60
+ | `--kp-surface` | Secondary surface |
61
+ | `--kp-line-subtle`, `--kp-line`, `--kp-line-strong` | Three boundary strengths |
62
+ | `--kp-contrast`, `--kp-on-contrast`, `--kp-on-contrast-muted` | Contrasting surface and its primary/supporting ink |
63
+ | `--kp-accent`, `--kp-wash`, `--kp-inverse` | Primary accent, tinted surface, ink for an accent field |
64
+ | `--kp-emphasis`, `--kp-emphasis-wash` | Secondary emphasis and its surface |
65
+ | `--kp-complete`, `--kp-attention`, `--kp-blocked` | Status, always paired with a non-color cue |
66
+ | `--kp-category-1` … `--kp-category-6` | Unordered identities; keep identity consistent across the document |
67
+ | `--kp-sequence-1` … `--kp-sequence-5` | Ordered amounts, from least to most emphasized |
68
+
69
+ Palette membership is not contrast approval. Check actual text/background pairs;
70
+ chart fills and subtle boundaries need not be suitable text colors. In particular,
71
+ `inverse` is ink for `accent`, not for every category or sequential fill. Pale
72
+ sequential steps and washes need direct labels or contrasting outlines when used
73
+ as chart marks. A contrasting surface owns its ink: set both its background and
74
+ its text colors in document CSS rather than inheriting the canvas's ink.
75
+
76
+ ## Rhythm, shape, and output geometry
77
+
78
+ - `--kp-unit` is the theme's spacing unit. The base derives `--kp-space-1`, `-2`, `-3`,
79
+ `-4`, `-5`, `-6`, `-8`, `-10`, `-12`, and `-16` as multiples on each page.
80
+ Choose gaps by relationship.
81
+ - `--kp-rule-width`, `--kp-stroke`, and `--kp-stroke-heavy` describe rules, diagram
82
+ strokes, and heavy marks. `--kp-radius-small` and `--kp-radius` describe corners.
83
+ `hr` uses the rule width and `--kp-line`; other boundaries, strokes, and corners
84
+ are applied by document CSS. These tokens do not mandate panels or rounded pages.
85
+ - `--kp-page-width`, `--kp-page-height`, and `--kp-page-margin` describe the output canvas.
86
+ The shared [base stylesheet](../theme-base.css) provides these presets:
87
+
88
+ | `data-size` | Width × height | Default margin |
89
+ | --- | --- | --- |
90
+ | `presentation` or omitted | 1920 × 1080 px | 72 px |
91
+ | `portrait` | 1080 × 1350 px | 60 px |
92
+ | `square` | 1080 × 1080 px | 60 px |
93
+ | `link` | 1200 × 630 px | 48 px |
94
+ | `article` | 1600 × 600 px | 48 px |
95
+ | `a4` | 210 × 297 mm | 56 px |
96
+ | `explainer` | 1200 × 1600 px | 60 px |
97
+ | `gallery` | 1600 × 1000 px | 64 px |
98
+
99
+ The base also supplies the existing seven-step type sizes and leading per
100
+ preset, box sizing, margin resets, and role application. It does not choose page
101
+ layout. Theme authors may tune type steps and margins on `[data-page]` or
102
+ `[data-size="…"]` after the import; changing them on `:root` will not override
103
+ the page's own values. Keep preset dimensions stable across themes.
104
+
105
+ ## Create a theme
106
+
107
+ When a brief brings its own look, adapt a copy of [`theme.css`](../theme.css).
108
+ Keep `theme-base.css` and any local font files and their licenses beside it.
109
+ Change token values and font declarations in the copy.
110
+ The HTML continues to link only `theme.css`; its first rule imports the local
111
+ base. Keep that import before font faces and other rules.
112
+
113
+ The default requests IBM Plex Sans normal 400/600/700, italic 400, and Plex Mono
114
+ normal 400 from Google Fonts. Preview and export need access to
115
+ `fonts.googleapis.com` and `fonts.gstatic.com`; remote updates may change font
116
+ metrics. Use licensed local files or embedded font data for offline or
117
+ reproducible rendering.
118
+
119
+ Supply additional subsets, weights, and styles when needed. Loading a font does
120
+ not prove it covers every glyph. Do not depend on a machine's installed fonts
121
+ or synthesized faces for required content. Font-load failures and installed-font
122
+ fallback remain export errors regardless of where the declared fonts are hosted.
123
+
124
+ ## Verify a theme
125
+
126
+ Define every listed token, directly or through the shared base. Run `konpeki check`
127
+ on a specimen for each supported page size. Use all font families and weights in
128
+ the specimen to exercise their loading. The existing inspector validates the
129
+ active definition on each page, including unused tokens.
130
+
131
+ For example, start a specimen beside the theme and its assets with:
132
+
133
+ ```html
134
+ <!doctype html>
135
+ <html lang="en">
136
+ <head><link rel="stylesheet" href="theme.css"></head>
137
+ <body>
138
+ <main id="specimen" data-page data-size="link">
139
+ <h1>Theme specimen</h1><p>Body, <strong>emphasis</strong>, and <code>code</code>.</p>
140
+ </main>
141
+ </body>
142
+ </html>
143
+ ```
144
+
145
+ Theme diagnostics require `--kp-theme: 1`. Other versions warn and skip these
146
+ checks; a stylesheet without the marker does not opt in.
147
+
148
+ - **Errors:** missing required tokens, invalid colors or type treatments,
149
+ non-increasing type sizes, leading smaller than its size, and text-role contrast
150
+ below 4.5:1. Text pairs are `fg`/`muted` on `bg`/`surface`/`wash`, the two
151
+ `on-contrast` inks on `contrast`, `accent` on `bg`, and `inverse` on `accent`.
152
+ Each role, strong and mono need a declared normal face at their requested weight.
153
+ Declared weight ranges are supported. Used text must have its requested style
154
+ and weight when its primary family has font-face declarations.
155
+ - **Warnings:** category marks below 3:1 against `bg`, exact duplicate category
156
+ colors, unknown `data-type` roles, root type/page values shadowed on a page,
157
+ and used colors, size/leading pairs, nonzero CSS corner radii, border widths or
158
+ painted SVG stroke widths outside the active theme. Zero corners and SVG circle
159
+ radii are allowed: tokens do not require every box to be rounded or forbid
160
+ circular chart marks. SVG text checks size only, because coordinates own its
161
+ line placement. SVG `line` fill is ignored because it does not paint.
162
+
163
+ The CLI also audits actual glyph fonts through Chromium. Installed-font use is
164
+ an error, including undeclared families and missing glyphs that fall back to a
165
+ system font. This audit runs on unthemed content too. Browser preview cannot run
166
+ CDP and only checks declarations and loading; run CLI inspection before delivery.
167
+ Face coverage checks declarations, not font-binary metadata: do not mislabel a
168
+ static font as a weight range. Include every family, weight, style and script
169
+ used in a theme specimen to exercise the actual files.
170
+
171
+ Contrast checks cover opaque sRGB only. Duplicate detection is not a test for
172
+ perceptual distinguishability or color-vision accessibility. Inspection cannot
173
+ prove a value was written with `var()` or establish good composition. Line-height
174
+ overlap boxes are estimates, not glyph-ink measurements. Render and review every page, including wrapping, chart labels,
175
+ and meaning. Compatible alternatives need no `data-theme="custom"` opt-out;
176
+ reserve that for deliberately unthemed content.
package/html/theme.md ADDED
@@ -0,0 +1,61 @@
1
+ # Use a theme
2
+
3
+ Link `theme.css` and keep `theme-base.css` beside it. The default loads Google
4
+ Fonts and needs network access. A document may instead use local or embedded
5
+ fonts; keep any local files and their licenses beside it.
6
+ Use the tokens below without reading their CSS values. `inspect` reports resolved
7
+ type sizes, leading, weights and families under each page's `theme.type`.
8
+ Theme construction and diagnostics are in [Author a theme](theme-authoring.md).
9
+
10
+ ## Type
11
+
12
+ Use semantic HTML. `data-type` changes appearance without changing reading order.
13
+ An explicit role replaces the semantic default; unmarked paragraphs inherit.
14
+
15
+ | `data-type` | Default | Purpose |
16
+ | --- | --- | --- |
17
+ | `fine` | Explicit only | Small supporting detail |
18
+ | `caption` | `figcaption` | Captions and sources |
19
+ | `body` | Page text | Main reading text |
20
+ | `lead` | `h3` | Lead or minor heading |
21
+ | `heading` | `h2` | Section heading |
22
+ | `title` | `h1` | Main headline |
23
+ | `display` | Explicit only | Large headline |
24
+
25
+ Custom SVG can use `--kp-font-ROLE`, `--kp-leading-ROLE`, `--kp-weight-ROLE` and
26
+ `--kp-tracking-ROLE`. Keep each size/leading pair together. Font slots are
27
+ `--kp-font-family`, `-heading`, `-display`, and `-mono`. `strong`, `code` and `em`
28
+ work directly; unsupported weights/styles and system-font fallback are errors.
29
+
30
+ ## Color and shape
31
+
32
+ All names below start with `--kp-`:
33
+
34
+ | Tokens | Use |
35
+ | --- | --- |
36
+ | `bg`, `fg`, `muted`, `surface` | Canvas, main/supporting ink, secondary surface |
37
+ | `accent`, `wash`, `inverse` | Accent, tint, ink on the accent |
38
+ | `contrast`, `on-contrast`, `on-contrast-muted` | Contrasting surface and its inks |
39
+ | `line-subtle`, `line`, `line-strong` | Boundary strengths |
40
+ | `emphasis`, `emphasis-wash` | Secondary emphasis and tint |
41
+ | `complete`, `attention`, `blocked` | Status, paired with a non-color cue |
42
+ | `category-1` … `-6`; `sequence-1` … `-5` | Identities; ordered amounts |
43
+ | `space-1`, `-2`, `-3`, `-4`, `-5`, `-6`, `-8`, `-10`, `-12`, `-16` | Gaps/padding by relationship |
44
+ | `rule-width`, `stroke`, `stroke-heavy`; `radius-small`, `radius` | Boundaries, strokes; corners |
45
+
46
+ Palette membership is not contrast approval. Set a contrasting surface's ink
47
+ explicitly. Pale chart marks need labels or outlines; `inverse` is not universal
48
+ ink for every fill. Document CSS owns layout and data geometry.
49
+
50
+ ## Page presets
51
+
52
+ Set `data-size` on each `[data-page]`: `presentation` (default, 1920×1080),
53
+ `portrait` (1080×1350), `square` (1080×1080), `link` (1200×630), `article`
54
+ (1600×600), `a4` (210×297 mm), `explainer` (1200×1600), or `gallery` (1600×1000).
55
+ Dimensions otherwise use CSS px. `--kp-page-width`, `-height`, and `-margin`
56
+ describe the canvas. Override page/type sizes on `[data-page]`, not `:root`.
57
+
58
+ Keep theme changes separate from composition. Read a theme's optional `NOTES.md`
59
+ for its taste; general judgment stays in AUTHORING.md. After a swap, inspect and
60
+ render every page. Reserve `data-theme="custom"` for deliberately unthemed
61
+ content; it does not disable layout or font checks.
package/index.html CHANGED
@@ -7,27 +7,27 @@
7
7
  <link rel="icon" type="image/png" sizes="128x128" href="/src/assets/konpeki-mark.png" />
8
8
  <meta
9
9
  name="description"
10
- content="Try Konpeki's editable canvas. Edit an example, create a page, export PNG or download editable JSON. No account needed; changes stay in your browser."
10
+ content="Review agent-authored HTML visuals. Select an element, leave a comment, and copy feedback for your agent. No account needed."
11
11
  />
12
12
  <meta property="og:type" content="website" />
13
13
  <meta property="og:site_name" content="Konpeki" />
14
- <meta property="og:title" content="Konpeki — Browser playground" />
15
- <meta property="og:description" content="Try Konpeki's editable canvas. Edit an example, create a page, export PNG or download editable JSON. No account needed; changes stay in your browser." />
16
- <meta property="og:url" content="https://vcfgdev.github.io/konpeki/?example=introducing-konpeki" />
14
+ <meta property="og:title" content="Konpeki — Visual review" />
15
+ <meta property="og:description" content="Review agent-authored HTML visuals. Select an element, leave a comment, and copy feedback for your agent. No account needed." />
16
+ <meta property="og:url" content="https://vcfgdev.github.io/konpeki/" />
17
17
  <meta property="og:image" content="https://vcfgdev.github.io/konpeki/og.png" />
18
18
  <meta property="og:image:type" content="image/png" />
19
19
  <meta property="og:image:width" content="1280" />
20
20
  <meta property="og:image:height" content="640" />
21
- <meta property="og:image:alt" content="Konpeki — Create clear visuals with your coding agent. Blue brush mark and editable canvas illustrations." />
21
+ <meta property="og:image:alt" content="Create clear visuals with your coding agent. Blue brush mark on warm paper, with the konpeki wordmark below." />
22
22
  <meta name="twitter:card" content="summary_large_image" />
23
- <meta name="twitter:title" content="Konpeki — Browser playground" />
24
- <meta name="twitter:description" content="Try Konpeki's editable canvas. Edit an example, create a page, export PNG or download editable JSON. No account needed; changes stay in your browser." />
23
+ <meta name="twitter:title" content="Konpeki — Visual review" />
24
+ <meta name="twitter:description" content="Review agent-authored HTML visuals. Select an element, leave a comment, and copy feedback for your agent. No account needed." />
25
25
  <meta name="twitter:image" content="https://vcfgdev.github.io/konpeki/og.png" />
26
- <meta name="twitter:image:alt" content="Konpeki — Create clear visuals with your coding agent. Blue brush mark and editable canvas illustrations." />
26
+ <meta name="twitter:image:alt" content="Create clear visuals with your coding agent. Blue brush mark on warm paper, with the konpeki wordmark below." />
27
27
  <title>Konpeki</title>
28
28
  </head>
29
29
  <body>
30
30
  <div id="root"></div>
31
- <script type="module" src="/src/main.tsx"></script>
31
+ <script type="module" src="/html/preview.tsx"></script>
32
32
  </body>
33
33
  </html>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "konpeki",
3
- "version": "0.3.0",
4
- "description": "A shared editable canvas for agent-made visuals",
3
+ "version": "0.4.0",
4
+ "description": "Agent-first HTML visual authoring and review",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -23,33 +23,27 @@
23
23
  "CONTRIBUTING.md",
24
24
  "SECURITY.md",
25
25
  "SETUP.md",
26
+ "README.md",
27
+ "LICENSE",
28
+ "theme.css",
29
+ "theme-base.css",
26
30
  "docs/workflow.md",
27
31
  "docs/development.md",
28
32
  "plugin.json",
29
33
  "skills/konpeki/SKILL.md",
34
+ "skills/konpeki/floor.md",
35
+ "skills/konpeki/references/*.md",
30
36
  "skills/konpeki/scripts/*.mjs",
31
- "skills/konpeki/assets/blank.json",
32
- "runtime/konpeki.mjs",
37
+ "skills/konpeki/assets/blank.html",
38
+ "html/*.{ts,tsx,css,html,md}",
39
+ "runtime/**/*.{mjs,js}",
33
40
  "index.html",
34
41
  "vite.config.ts",
35
42
  "public/og.png",
36
- "src/main.tsx",
37
- "src/app/*.tsx",
38
- "src/components/*.tsx",
39
- "src/lib/*.ts",
40
- "src/lib/examples/react-page-migration.json",
41
- "src/styles/*.css",
43
+ "src/components/PageBoard.tsx",
44
+ "src/lib/{page-board,alignment,review-position}.ts",
45
+ "src/styles/{base,shell,feedback}.css",
42
46
  "src/assets/konpeki-mark.png",
43
- "composition/{README.md,compile.ts,document.ts,schema.json,schema.ts,theme-tokens.ts,types.ts,validate.ts,vector.ts,visualizations.ts}",
44
- "design/*.md",
45
- "design/{palettes,themes}/*.{md,ts}",
46
- "design/review/*.md",
47
- "design/visual-languages/technical-product.md",
48
- "lib/{assets.d.ts,charts.ts,contrast.ts,layouts.ts,slide.tsx,taste.ts,text.tsx,typeface.ts}",
49
- "scripts/migrate-react-page.ts",
50
- "slides/README.md",
51
- "slides/{architecture,article-brief,bar-chart,comparison,decision-memo,delivery-plan,experiment,incident-workflow,line-chart,migration,og-images,product-introduction,research-brief,results-explanation,retrospective,sankey,teaching,vertical-bar-charts}/{PROMPT.md,index.tsx}",
52
- "slides/introducing-konpeki/{README.md,PROMPT.md,SOURCE.md,composition.json,author.ts}",
53
47
  "!**/*.test.ts"
54
48
  ],
55
49
  "engines": {
@@ -61,31 +55,23 @@
61
55
  "prepack": "node scripts/build-cli.mjs",
62
56
  "preview": "vite preview --host 0.0.0.0 --port 4318",
63
57
  "check": "tsc --noEmit",
64
- "test": "node --test lib/*.test.ts composition/*.test.ts src/lib/*.test.ts bin/*.test.ts",
58
+ "test": "node --test html/*.test.ts src/lib/*.test.ts scripts/*.test.mjs",
65
59
  "check:package": "node scripts/check-package.mjs",
66
- "konpeki": "node bin/konpeki.mjs",
67
- "composition:generate": "node composition/generate.ts",
68
- "example:migrate-page": "node scripts/migrate-react-page.ts"
60
+ "konpeki": "node bin/konpeki.mjs"
69
61
  },
70
62
  "dependencies": {
71
- "@fontsource/hanken-grotesk": "5.3.0",
72
63
  "@fontsource/ibm-plex-sans": "5.3.0",
73
- "@fontsource/ibm-plex-serif": "5.3.0",
74
- "@fontsource/newsreader": "5.3.0",
75
- "@fontsource/noto-sans": "5.3.0",
76
- "@nivo/bar": "0.99.0",
77
- "@nivo/line": "0.99.0",
78
- "@nivo/sankey": "0.99.0",
79
- "@xmldom/xmldom": "0.9.12",
80
- "ajv": "^8.20.0",
81
- "proper-lockfile": "4.1.2",
64
+ "parse5": "8.0.0",
65
+ "pdf-lib": "1.17.1",
66
+ "playwright": "1.63.0",
67
+ "postcss": "8.5.28",
82
68
  "react": "19.2.8",
83
69
  "react-dom": "19.2.8",
84
70
  "vite": "^8.3.0"
85
71
  },
86
72
  "devDependencies": {
73
+ "@fontsource/ibm-plex-mono": "5.3.0",
87
74
  "@types/node": "^24.0.0",
88
- "@types/proper-lockfile": "4.1.4",
89
75
  "@types/react": "^19.2.18",
90
76
  "@types/react-dom": "^19.2.5",
91
77
  "typescript": "^5.9.3"
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "konpeki",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "description": "Create editable visuals with your coding agent.",
6
6
  "repository": "https://github.com/vcfgdev/konpeki",
7
7
  "license": "Apache-2.0",
package/public/og.png CHANGED
Binary file