konpeki 0.1.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 (121) hide show
  1. package/.agents/skills/authoring-visuals/SKILL.md +82 -0
  2. package/AGENTS.md +33 -0
  3. package/AUTHORING.md +233 -0
  4. package/LICENSE +201 -0
  5. package/README.md +70 -0
  6. package/SETUP.md +86 -0
  7. package/composition/README.md +166 -0
  8. package/composition/compile.ts +227 -0
  9. package/composition/document.ts +709 -0
  10. package/composition/schema.json +2992 -0
  11. package/composition/schema.ts +435 -0
  12. package/composition/theme-tokens.ts +16 -0
  13. package/composition/types.ts +286 -0
  14. package/composition/validate.ts +684 -0
  15. package/composition/vector.ts +143 -0
  16. package/composition/visualizations.ts +335 -0
  17. package/design/README.md +17 -0
  18. package/design/palettes/README.md +14 -0
  19. package/design/palettes/base.ts +14 -0
  20. package/design/palettes/candidates.ts +19 -0
  21. package/design/palettes/index.ts +78 -0
  22. package/design/review/color-theme.md +44 -0
  23. package/design/review/layout.md +16 -0
  24. package/design/review/text.md +18 -0
  25. package/design/review/typography.md +15 -0
  26. package/design/review/visuals.md +31 -0
  27. package/design/semantic-patterns.md +43 -0
  28. package/design/themes/README.md +22 -0
  29. package/design/themes/index.ts +13 -0
  30. package/design/visual-languages/technical-product.md +17 -0
  31. package/design/visual-review.md +88 -0
  32. package/docs/development.md +140 -0
  33. package/docs/workflow.md +61 -0
  34. package/index.html +17 -0
  35. package/lib/assets.d.ts +8 -0
  36. package/lib/charts.ts +18 -0
  37. package/lib/contrast.ts +16 -0
  38. package/lib/layouts.ts +50 -0
  39. package/lib/slide.tsx +42 -0
  40. package/lib/taste.ts +17 -0
  41. package/lib/text.tsx +89 -0
  42. package/lib/typeface.ts +44 -0
  43. package/package.json +85 -0
  44. package/runtime/konpeki.mjs +1685 -0
  45. package/scripts/migrate-react-page.ts +120 -0
  46. package/slides/README.md +153 -0
  47. package/slides/architecture/PROMPT.md +31 -0
  48. package/slides/architecture/index.tsx +102 -0
  49. package/slides/article-brief/PROMPT.md +35 -0
  50. package/slides/article-brief/index.tsx +71 -0
  51. package/slides/bar-chart/PROMPT.md +39 -0
  52. package/slides/bar-chart/index.tsx +97 -0
  53. package/slides/comparison/PROMPT.md +29 -0
  54. package/slides/comparison/index.tsx +95 -0
  55. package/slides/decision-memo/PROMPT.md +34 -0
  56. package/slides/decision-memo/index.tsx +85 -0
  57. package/slides/delivery-plan/PROMPT.md +45 -0
  58. package/slides/delivery-plan/index.tsx +105 -0
  59. package/slides/experiment/PROMPT.md +44 -0
  60. package/slides/experiment/index.tsx +127 -0
  61. package/slides/incident-workflow/PROMPT.md +57 -0
  62. package/slides/incident-workflow/index.tsx +78 -0
  63. package/slides/introducing-konpeki/PROMPT.md +19 -0
  64. package/slides/introducing-konpeki/README.md +54 -0
  65. package/slides/introducing-konpeki/SOURCE.md +20 -0
  66. package/slides/introducing-konpeki/author.ts +163 -0
  67. package/slides/introducing-konpeki/composition.json +3265 -0
  68. package/slides/line-chart/PROMPT.md +40 -0
  69. package/slides/line-chart/index.tsx +72 -0
  70. package/slides/migration/PROMPT.md +38 -0
  71. package/slides/migration/index.tsx +89 -0
  72. package/slides/og-images/PROMPT.md +21 -0
  73. package/slides/og-images/index.tsx +76 -0
  74. package/slides/product-introduction/PROMPT.md +24 -0
  75. package/slides/product-introduction/index.tsx +105 -0
  76. package/slides/research-brief/PROMPT.md +40 -0
  77. package/slides/research-brief/index.tsx +104 -0
  78. package/slides/results-explanation/PROMPT.md +32 -0
  79. package/slides/results-explanation/index.tsx +96 -0
  80. package/slides/retrospective/PROMPT.md +43 -0
  81. package/slides/retrospective/index.tsx +105 -0
  82. package/slides/sankey/PROMPT.md +11 -0
  83. package/slides/sankey/index.tsx +93 -0
  84. package/slides/teaching/PROMPT.md +45 -0
  85. package/slides/teaching/index.tsx +124 -0
  86. package/slides/vertical-bar-charts/PROMPT.md +13 -0
  87. package/slides/vertical-bar-charts/index.tsx +97 -0
  88. package/src/app/App.tsx +694 -0
  89. package/src/assets/konpeki-mark.png +0 -0
  90. package/src/components/Canvas.tsx +1168 -0
  91. package/src/components/DiagramTypeIcon.tsx +78 -0
  92. package/src/components/InspectorPanel.tsx +687 -0
  93. package/src/components/LeftPanel.tsx +107 -0
  94. package/src/components/PageSizePicker.tsx +30 -0
  95. package/src/components/Presentation.tsx +105 -0
  96. package/src/components/RightPanel.tsx +201 -0
  97. package/src/components/VectorOverflowWarning.tsx +46 -0
  98. package/src/components/WorkspaceChrome.tsx +199 -0
  99. package/src/components/ui.tsx +53 -0
  100. package/src/lib/examples/react-page-migration.json +1295 -0
  101. package/src/lib/examples.ts +42 -0
  102. package/src/lib/export-png.ts +101 -0
  103. package/src/lib/file-session.ts +74 -0
  104. package/src/lib/history.ts +53 -0
  105. package/src/lib/model.ts +188 -0
  106. package/src/lib/page-size.ts +24 -0
  107. package/src/lib/presentation.ts +17 -0
  108. package/src/lib/storage.ts +43 -0
  109. package/src/lib/theme.ts +25 -0
  110. package/src/lib/use-file-session.ts +162 -0
  111. package/src/main.tsx +26 -0
  112. package/src/styles/base.css +105 -0
  113. package/src/styles/canvas.css +268 -0
  114. package/src/styles/chrome.css +214 -0
  115. package/src/styles/component-previews.css +386 -0
  116. package/src/styles/feedback.css +71 -0
  117. package/src/styles/left-panel.css +125 -0
  118. package/src/styles/presentation.css +72 -0
  119. package/src/styles/right-panel.css +1172 -0
  120. package/src/styles/shell.css +247 -0
  121. package/vite.config.ts +6 -0
package/lib/slide.tsx ADDED
@@ -0,0 +1,42 @@
1
+ import type { ReactNode } from 'react';
2
+ import { layout, palettes, typography as type, type Palette } from './taste.ts';
3
+ import type { Region, RelationshipSlot } from './layouts.ts';
4
+ import { Paragraphs, Text, type TextContent } from './text.tsx';
5
+
6
+ export function Panel({ region, title, body, palette: p }: { region: Region; title: string; body: TextContent; palette: Palette }) {
7
+ const padding = 20;
8
+ return <g data-panel="true" data-region={JSON.stringify(region)}>
9
+ <rect {...region} rx={layout.radius} fill={p.bg} stroke={p.line} />
10
+ <Paragraphs verticalAlign="top" region={{ x: region.x + padding, y: region.y + padding, width: region.width - 2 * padding, height: region.height - 2 * padding }}
11
+ items={[{ text: title, color: p.fg, weight: 500 }, { text: body, color: p.muted }]} />
12
+ </g>;
13
+ }
14
+
15
+ export function Relationship({ slot, kind = 'forward', reverse = false, color }: {
16
+ slot: RelationshipSlot; kind?: 'forward' | 'both' | 'cycle'; reverse?: boolean; color: string;
17
+ }) {
18
+ const angle = (slot.axis === 'vertical' ? 90 : 0) + (reverse ? 180 : 0);
19
+ const d = kind === 'cycle'
20
+ ? 'M-20 -5 A21 21 0 0 1 20 -5 M11 -10 L20 -5 L23 -15 M20 5 A21 21 0 0 1 -20 5 M-11 10 L-20 5 L-23 15'
21
+ : `M-24 0 H24 M12 -8 L24 0 L12 8${kind === 'both' ? ' M-12 -8 L-24 0 L-12 8' : ''}`;
22
+ return <g data-relationship={kind} role="img" aria-label={kind === 'cycle' ? 'Feedback cycle' : kind === 'both' ? 'Bidirectional relationship' : 'Directional relationship'}
23
+ transform={`translate(${slot.x} ${slot.y}) rotate(${angle})`}>
24
+ <path d={d} fill="none" stroke={color} strokeWidth={layout.stroke} strokeLinejoin="round" />
25
+ </g>;
26
+ }
27
+
28
+ export function Sheet({ id, number, title, caption, state, children, total = '06', palette }: {
29
+ id: string; number: string; title: string; caption: string; state: { theme: keyof typeof palettes };
30
+ children: (p: Palette) => ReactNode; total?: string; palette?: Palette;
31
+ }) {
32
+ const p = palette ?? palettes[state.theme];
33
+ return <svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080"
34
+ fontFamily={type.family} role="img" aria-label={title} data-visual={id}>
35
+ <rect width="1920" height="1080" fill={p.bg} />
36
+ <Text x={layout.inset} y={164} width={layout.contentWidth} size={type.display} weight={500} color={p.fg} maxLines={2} leading={1.12}>{title}</Text>
37
+ <g transform="translate(0 -56)" data-slide-content="true">{children(p)}</g>
38
+ <line x1={layout.inset} y1="982" x2="1808" y2="982" stroke={p.line} />
39
+ <text x={layout.inset} y="1026" fontSize={type.meta} fill={p.muted}>{caption}</text>
40
+ <text x="1808" y="1026" textAnchor="end" fontSize={type.meta} fill={p.muted} data-page-number="true">{number} / {total}</text>
41
+ </svg>;
42
+ }
package/lib/taste.ts ADDED
@@ -0,0 +1,17 @@
1
+ // Three independent choices: typography, surface/style, and relationship geometry.
2
+ // Layouts consume these roles; colors are never assigned by item index.
3
+ export const typography = {
4
+ family: '"IBM Plex Sans", sans-serif',
5
+ display: 72,
6
+ heading: 40,
7
+ body: 32,
8
+ caption: 24,
9
+ meta: 20,
10
+ leading: 1.3,
11
+ };
12
+
13
+ export const cornerRadii = { square: 0, soft: 16 };
14
+ export const layout = { inset: 112, contentWidth: 1696, gap: 32, stroke: 4, radius: cornerRadii.soft };
15
+
16
+ // Existing runtime consumers retain their imports; color definitions live in design/.
17
+ export { palettes, type Palette } from '../design/palettes/base.ts';
package/lib/text.tsx ADDED
@@ -0,0 +1,89 @@
1
+ import { typography as type } from './taste.ts';
2
+ import type { Region } from './layouts.ts';
3
+ import { useLayoutEffect, useRef, useState } from 'react';
4
+
5
+ export type TextRun = { text: string; bold?: boolean; italic?: boolean; color?: string };
6
+ export type TextContent = string | readonly TextRun[];
7
+
8
+ // Segment the complete copy, so a style boundary cannot introduce a word break.
9
+ function measureLines(copy: TextContent, width: number, size: number, weight: number, family = type.family) {
10
+ const ctx = document.createElement('canvas').getContext('2d')!;
11
+ const runs = typeof copy === 'string' ? [{ text: copy }] : copy;
12
+ const plain = runs.map(run => run.text).join('');
13
+ const slice = (start: number, end: number) => {
14
+ let position = 0;
15
+ return runs.flatMap(run => {
16
+ const text = run.text.slice(Math.max(0, start - position), Math.max(0, end - position));
17
+ position += run.text.length;
18
+ return text ? [{ ...run, text }] : [];
19
+ });
20
+ };
21
+ const measure = (line: readonly TextRun[]) => line.reduce((sum, run) => {
22
+ ctx.font = `${run.italic ? 'italic' : 'normal'} ${run.bold ? 600 : weight} ${size}px ${family}`;
23
+ return sum + ctx.measureText(run.text).width;
24
+ }, 0);
25
+ const lines: TextRun[][] = [];
26
+ let paragraphStart = 0;
27
+ for (const paragraph of plain.split('\n')) {
28
+ let start = paragraphStart, end = start;
29
+ const push = () => {
30
+ const raw = plain.slice(start, end);
31
+ lines.push(slice(start + raw.length - raw.trimStart().length, end - (raw.length - raw.trimEnd().length)));
32
+ };
33
+ const segments: string[] = [];
34
+ for (const { segment } of new Intl.Segmenter(undefined, { granularity: 'word' }).segment(paragraph)) {
35
+ if (segments.length && /^[.,!?;:%…\)\]}»”’]+$/u.test(segment)) segments[segments.length - 1] += segment;
36
+ else segments.push(segment);
37
+ }
38
+ for (const segment of segments) {
39
+ const next = end + segment.length;
40
+ if (plain.slice(start, end).trim() && measure(slice(start, next)) > width) {
41
+ push();
42
+ start = end + segment.length - segment.trimStart().length;
43
+ }
44
+ end = next;
45
+ }
46
+ push();
47
+ paragraphStart += paragraph.length + 1;
48
+ }
49
+ return { lines, tooWide: lines.some(line => measure(line) > width), plain };
50
+ }
51
+
52
+ export function Text({ children, x, y, width, size = type.body, weight = 400, color, maxLines = 3, leading = type.leading, align = 'start', family = type.family }: {
53
+ children: TextContent; x: number; y: number; width: number; size?: number; weight?: number; color: string; maxLines?: number; leading?: number; align?: 'start' | 'end'; family?: string;
54
+ }) {
55
+ const { lines, tooWide, plain } = measureLines(children, width, size, weight, family);
56
+ const overflow = lines.length > maxLines || tooWide;
57
+ return <text fill={color} fontFamily={family} fontSize={size} fontWeight={weight} textAnchor={align} data-copy={plain} data-overflow={overflow}>
58
+ {lines.map((line, i) => <tspan key={i} x={x} y={y + i * size * leading} data-line="true">
59
+ {typeof children === 'string' ? line.map(run => run.text).join('') : line.map((run, j) =>
60
+ <tspan key={j} data-run="true" fontWeight={run.bold ? 600 : weight} fontStyle={run.italic ? 'italic' : 'normal'} fill={run.color ?? color}>{run.text}</tspan>)}
61
+ </tspan>)}
62
+ </text>;
63
+ }
64
+
65
+ export function Paragraphs({ region, items, size = type.body, gap = size * type.leading / 2, verticalAlign = 'center' }: {
66
+ region: Region; items: { text: TextContent; color: string; weight?: number }[]; size?: number; gap?: number; verticalAlign?: 'top' | 'center';
67
+ }) {
68
+ const ref = useRef<SVGGElement>(null);
69
+ const [offset, setOffset] = useState(0);
70
+ useLayoutEffect(() => {
71
+ const box = ref.current!.getBBox();
72
+ // Upstream also mounts hidden slides whose SVG bounds are not measurable.
73
+ if (box.height === 0) return;
74
+ // Align rendered bounds, including wrapped lines and paragraph gaps.
75
+ // Overset copy stays top-aligned and flagged; centering must not hide it.
76
+ const delta = box.height <= region.height
77
+ ? verticalAlign === 'top' ? region.y - box.y : region.y + region.height / 2 - (box.y + box.height / 2)
78
+ : -offset;
79
+ if (Math.abs(delta) > 0.01) setOffset(offset + delta);
80
+ }, [items, region.y, region.height, region.width, size, gap, offset, verticalAlign]);
81
+ let baseline = region.y + size * 1.1 + offset;
82
+ return <g ref={ref} data-prose="true" data-vertical-align={verticalAlign} data-region={JSON.stringify(region)}>{items.map(({ text, color, weight = 400 }, i) => {
83
+ const y = baseline;
84
+ const { lines } = measureLines(text, region.width, size, weight);
85
+ baseline += lines.length * size * type.leading + gap;
86
+ const maxLines = Math.max(0, 1 + Math.floor((region.y + region.height - y - size * 0.3) / (size * type.leading)));
87
+ return <Text key={i} x={region.x} y={y} width={region.width} size={size} color={color} weight={weight} maxLines={maxLines}>{text}</Text>;
88
+ })}</g>;
89
+ }
@@ -0,0 +1,44 @@
1
+ import '@fontsource/ibm-plex-sans/latin-400.css';
2
+ import '@fontsource/ibm-plex-sans/latin-500.css';
3
+ import '@fontsource/ibm-plex-sans/latin-600.css';
4
+ import '@fontsource/ibm-plex-sans/latin-400-italic.css';
5
+ import '@fontsource/ibm-plex-sans/latin-500-italic.css';
6
+ import '@fontsource/ibm-plex-sans/latin-600-italic.css';
7
+ import regular from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-400-normal.woff2?url';
8
+ import medium from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-500-normal.woff2?url';
9
+ import semibold from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-600-normal.woff2?url';
10
+ import italic from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-400-italic.woff2?url';
11
+ import mediumItalic from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-500-italic.woff2?url';
12
+ import semiboldItalic from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-600-italic.woff2?url';
13
+ import license from '@fontsource/ibm-plex-sans/LICENSE?raw';
14
+
15
+ const faces = [
16
+ { url: regular, weight: 400, style: 'normal' },
17
+ { url: medium, weight: 500, style: 'normal' },
18
+ { url: semibold, weight: 600, style: 'normal' },
19
+ { url: italic, weight: 400, style: 'italic' },
20
+ { url: mediumItalic, weight: 500, style: 'italic' },
21
+ { url: semiboldItalic, weight: 600, style: 'italic' },
22
+ ];
23
+ export const fontsReady = Promise.all(faces.map(({ weight, style }) =>
24
+ document.fonts.load(`${style} ${weight} 32px "IBM Plex Sans"`),
25
+ ));
26
+
27
+ // Our existing SVG/PNG experiment must retain the newly selected typeface.
28
+ // Latin font files and their OFL notice travel with the SVG.
29
+ export async function embedTypeface(source: string) {
30
+ const css = await Promise.all(faces.map(async ({ url, weight, style }) => {
31
+ const response = await fetch(url);
32
+ if (!response.ok) throw new Error('Unable to load export typeface.');
33
+ const bytes = new Uint8Array(await response.arrayBuffer());
34
+ const data = btoa(Array.from(bytes, byte => String.fromCharCode(byte)).join(''));
35
+ return `@font-face{font-family:'IBM Plex Sans';font-style:${style};font-weight:${weight};src:url(data:font/woff2;base64,${data}) format('woff2')}`;
36
+ }));
37
+ const xml = new DOMParser().parseFromString(source, 'image/svg+xml');
38
+ const style = xml.createElementNS('http://www.w3.org/2000/svg', 'style');
39
+ style.textContent = css.join('\n');
40
+ const metadata = xml.createElementNS('http://www.w3.org/2000/svg', 'metadata');
41
+ metadata.textContent = license;
42
+ xml.documentElement.prepend(style, metadata);
43
+ return new XMLSerializer().serializeToString(xml);
44
+ }
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "konpeki",
3
+ "version": "0.1.0",
4
+ "description": "A shared editable canvas for agent-made visuals",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/vcfgdev/konpeki.git"
9
+ },
10
+ "homepage": "https://github.com/vcfgdev/konpeki#readme",
11
+ "bugs": "https://github.com/vcfgdev/konpeki/issues",
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "registry": "https://registry.npmjs.org/"
15
+ },
16
+ "type": "module",
17
+ "bin": {
18
+ "konpeki": "runtime/konpeki.mjs"
19
+ },
20
+ "files": [
21
+ "AGENTS.md",
22
+ "AUTHORING.md",
23
+ "SETUP.md",
24
+ "docs/workflow.md",
25
+ "docs/development.md",
26
+ ".agents/skills/authoring-visuals/SKILL.md",
27
+ "runtime/konpeki.mjs",
28
+ "index.html",
29
+ "vite.config.ts",
30
+ "src/main.tsx",
31
+ "src/app/*.tsx",
32
+ "src/components/*.tsx",
33
+ "src/lib/*.ts",
34
+ "src/lib/examples/react-page-migration.json",
35
+ "src/styles/*.css",
36
+ "src/assets/konpeki-mark.png",
37
+ "composition/{README.md,compile.ts,document.ts,schema.json,schema.ts,theme-tokens.ts,types.ts,validate.ts,vector.ts,visualizations.ts}",
38
+ "design/*.md",
39
+ "design/{palettes,themes}/*.{md,ts}",
40
+ "design/review/*.md",
41
+ "design/visual-languages/technical-product.md",
42
+ "lib/{assets.d.ts,charts.ts,contrast.ts,layouts.ts,slide.tsx,taste.ts,text.tsx,typeface.ts}",
43
+ "scripts/migrate-react-page.ts",
44
+ "slides/README.md",
45
+ "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}",
46
+ "slides/introducing-konpeki/{README.md,PROMPT.md,SOURCE.md,composition.json,author.ts}",
47
+ "!**/*.test.ts"
48
+ ],
49
+ "engines": {
50
+ "node": ">=24"
51
+ },
52
+ "scripts": {
53
+ "dev": "vite --host 0.0.0.0 --port 4318",
54
+ "build": "vite build",
55
+ "prepack": "node scripts/build-cli.mjs",
56
+ "preview": "vite preview --host 0.0.0.0 --port 4318",
57
+ "check": "tsc --noEmit",
58
+ "test": "node --test lib/*.test.ts composition/*.test.ts src/lib/*.test.ts bin/*.test.ts",
59
+ "check:package": "node scripts/check-package.mjs",
60
+ "konpeki": "node bin/konpeki.mjs",
61
+ "composition:generate": "node composition/generate.ts",
62
+ "example:migrate-page": "node scripts/migrate-react-page.ts"
63
+ },
64
+ "dependencies": {
65
+ "@fontsource/hanken-grotesk": "5.3.0",
66
+ "@fontsource/ibm-plex-sans": "5.3.0",
67
+ "@fontsource/ibm-plex-serif": "5.3.0",
68
+ "@fontsource/newsreader": "5.3.0",
69
+ "@fontsource/noto-sans": "5.3.0",
70
+ "@nivo/bar": "0.99.0",
71
+ "@nivo/line": "0.99.0",
72
+ "@nivo/sankey": "0.99.0",
73
+ "@xmldom/xmldom": "0.9.12",
74
+ "ajv": "^8.20.0",
75
+ "react": "19.2.8",
76
+ "react-dom": "19.2.8",
77
+ "vite": "^8.3.0"
78
+ },
79
+ "devDependencies": {
80
+ "@types/node": "^24.0.0",
81
+ "@types/react": "^19.2.18",
82
+ "@types/react-dom": "^19.2.5",
83
+ "typescript": "^5.9.3"
84
+ }
85
+ }