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
@@ -1,40 +0,0 @@
1
- # Themes
2
-
3
- Document appearance has independent **Typography**, **Color palette**, and
4
- **Background** controls. These do not choose a layout or change font sizes.
5
-
6
- Use [authoring guidance](../../AUTHORING.md#taste-and-creative-freedom) for theme
7
- selection, font defaults and deck-wide consistency.
8
-
9
- ```ts
10
- import { getTheme } from './design/themes/index.ts';
11
- const theme = getTheme('Green', 'paper', 'noto-sans');
12
- // theme.body, theme.headline, theme.typography, theme.palette
13
- ```
14
-
15
- Eight palettes retain their existing names: Plex, Precision, Editorial,
16
- Blue–cyan, Orange–coral, Yellow, Green and Graphite. Each supports Paper and Night.
17
- Choose any palette with any of these typography presets:
18
-
19
- | Typography ID | Headings | Body |
20
- | --- | --- | --- |
21
- | `plex-sans` | IBM Plex Sans | IBM Plex Sans |
22
- | `noto-sans` | Noto Sans | Noto Sans |
23
- | `plex-serif` | IBM Plex Serif | IBM Plex Sans |
24
- | `hanken-grotesk` | Hanken Grotesk | Hanken Grotesk |
25
-
26
- Composition JSON stores the optional choice in `theme.typography`, independently
27
- of `theme.id` (palette) and `theme.mode` (background). Omitted typography preserves
28
- legacy appearance: Precision uses Noto Sans, Editorial uses Plex Serif headings
29
- with Plex Sans body, and all other palettes use Plex Sans. Changing the palette
30
- in the editor records the current typography so colors no longer change fonts.
31
- Two-argument `getTheme` callers retain those legacy pairings too.
32
-
33
- Font loading remains the author's responsibility; load the required Fontsource
34
- Latin weights before measuring or rendering text. Theme-linked native text and
35
- vectors follow the selection; literal font/color overrides remain fixed.
36
- Different fonts can change wrapping or overflow, so inspect the result.
37
-
38
- Mineral and Botanical are light-only palette candidates in
39
- [palettes/candidates.ts](../palettes/candidates.ts).
40
- A theme does not impose geometry, density or supporting-text sizes.
@@ -1,24 +0,0 @@
1
- import { typography } from '../../lib/taste.ts';
2
- import { paletteNames, resolvePalette, type PaletteName, type PaletteMode } from '../palettes/index.ts';
3
- import type { TypographyId } from '../../composition/types.ts';
4
-
5
- export const themeNames = paletteNames;
6
- export type ThemeName = PaletteName;
7
- export type ThemeMode = PaletteMode;
8
- export const typographyLabels: Record<TypographyId, string> = {
9
- 'plex-sans': 'IBM Plex Sans',
10
- 'noto-sans': 'Noto Sans',
11
- 'plex-serif': 'Plex Serif + Plex Sans',
12
- 'hanken-grotesk': 'Hanken Grotesk',
13
- };
14
-
15
- // Themes combine typography with a palette; they do not choose a composition.
16
- // Omitted typography retains the original pairing for older documents/callers.
17
- export function getTheme(name: ThemeName, mode: ThemeMode,
18
- typographyId: TypographyId = name === 'Precision' ? 'noto-sans' : name === 'Editorial' ? 'plex-serif' : 'plex-sans',
19
- ) {
20
- const body = typographyId === 'noto-sans' ? '"Noto Sans", sans-serif'
21
- : typographyId === 'hanken-grotesk' ? '"Hanken Grotesk", sans-serif' : typography.family;
22
- const headline = typographyId === 'plex-serif' ? '"IBM Plex Serif", serif' : body;
23
- return { name, typographyId, body, headline, typography: { ...typography, family: body }, palette: resolvePalette(name, mode) };
24
- }
@@ -1,17 +0,0 @@
1
- # Technical product
2
-
3
- Use for product explanations, processes and comparisons where a concrete visual
4
- can carry the argument. This is art direction, not a palette or a fixed template.
5
-
6
- - Give the product/interface/output enough space to explain the claim.
7
- - Apply the selected mode's framing and emphasis rules; do not flatten
8
- unrelated content into equally prominent boxes.
9
- - Connect a process to its tangible output. Keep labels near their connectors.
10
- - Make selection/recommendation distinct without obscuring alternatives.
11
- - Keep diagrams and annotations readable at the intended viewing size. Shared
12
- card headers round outer top corners only, not the internal bottom seam.
13
- - Avoid importing website density, tiny gray labels, ubiquitous registration
14
- marks, or scroll effects into slides.
15
-
16
- Works with Blue–cyan light or Graphite dark; these are examples, not limits.
17
- Reference: [user-supplied website video](https://video.twimg.com/amplify_video/2097350430993072128/vid/avc1/2306x2160/IgjnX7Duukk4W54G.mp4).
@@ -1,88 +0,0 @@
1
- # Focused visual review
2
-
3
- Choose the relevant categories for the work. Each owns its checks; shared
4
- requirements remain in [AUTHORING](../AUTHORING.md). Apply judgment to the
5
- content; no automatic bans or aesthetic score.
6
-
7
- All categories apply the selected [authoring mode](../AUTHORING.md#authoring-mode)
8
- and explicit brief. Default uses the original visual restrictions; dynamic
9
- relaxes only the rules named in the mode table. That table takes precedence over
10
- general suggestions below and in category guides. Do not impose default-only
11
- restrictions on dynamic work or require dynamic work to add effects. Judge
12
- explanation structure and depth against the source, audience and brief.
13
- Correctness, readability and source fidelity remain non-negotiable in both modes.
14
- Report a mode mismatch separately from a factual/access defect or taste preference.
15
-
16
- | Category | Scope |
17
- | --- | --- |
18
- | [Text](review/text.md) | Meaning, claims, repetition and writing tone |
19
- | [Typography](review/typography.md) | Font rendering, text hierarchy and reading comfort |
20
- | [Layout](review/layout.md) | Composition, grouping, spacing and comparisons |
21
- | [Color / theme](review/color-theme.md) | Deck-wide direction, contrast and semantic color roles |
22
- | [Visuals](review/visuals.md) | Diagrams, charts, imagery and meaningful boundaries |
23
-
24
- ## Independent review
25
-
26
- For a small change, review directly. When useful and subagents are available,
27
- assign independent categories concurrently. Give each reviewer the same brief,
28
- chosen direction, source and full/half-size renders, plus its category file.
29
- Reviewers inspect their scope without editing files or reading each other's findings.
30
-
31
- Return only meaningful findings: **priority · page/element · evidence · reader
32
- impact · suggested fix**. Distinguish observed defects from design suggestions;
33
- list checks not performed as **Not verified**, not as defects or passes.
34
- An empty finding list is valid.
35
- The coordinator deduplicates cross-category issues, resolves conflicting fixes
36
- against the brief, and checks narrative flow and deck coherence. Report a shared
37
- root cause once, naming affected pages. Apply fixes in one editing pass,
38
- then re-render affected pages and repeat relevant checks.
39
-
40
- ## Repair order
41
-
42
- 1. **Accuracy:** factual errors, misleading scales, missing qualifications and
43
- incorrect relationships.
44
- 2. **Access to content:** unreadable, missing or clipped essential content,
45
- color-only meaning and inaccessible interactions when present.
46
- 3. **Understanding:** grouping, reading order, hierarchy and deck coherence.
47
- 4. **Polish:** optical alignment, decorative details and surface consistency.
48
-
49
- Judge whether the treatment helps the audience, not whether it could be removed.
50
- A repair may add a diagram, color field or annotation, or remove competing
51
- material. Reuse existing primitives where useful; never delete required evidence to improve fit.
52
- Repair upstream causes first: a cramped region may need recomposition rather
53
- than separate font-size, line-break and padding changes.
54
-
55
- ### Repair examples
56
-
57
- These are diagnostic examples, not measured findings or mandatory layouts.
58
- Render the repair with real content to judge whether it helps.
59
-
60
- | Before | After | Why |
61
- | --- | --- | --- |
62
- | A heading sits equally far from the preceding section and its own paragraph. | Move it nearer its paragraph and increase separation from the preceding section. | Proximity makes ownership clear without another box. |
63
- | A wide diagram leaves its evidence and caveat in tiny side text. | Give the evidence a larger region or split the page while retaining the qualification. | Essential context becomes readable without shrinking the diagram's labels. |
64
- | A large value dominates a faint unit and denominator. | Keep unit and denominator adjacent and readable; reduce the value's dominance if needed. | The reader can interpret the number rather than just notice it. |
65
- | Every sentence sits in an equally prominent nested panel. | Reduce competing layers; retain surfaces that support grouping, emphasis or the chosen direction. | The audience can distinguish the main point from supporting material. |
66
- | A prose list makes readers reconstruct a request journey. | Draw the named participants and labeled connections, retaining useful explanatory copy. | Relationships become easier to follow even though words already describe them. |
67
-
68
- ## Accessibility and delivery
69
-
70
- Apply checks to the requested format, at its actual viewing size. These are
71
- delivery checks, not a claim of full accessibility conformance.
72
- The author (or coordinator when review is delegated) owns this checklist and
73
- records evidence or **Not verified** for every applicable check.
74
-
75
- - Check captions, sources, units and caveats as carefully as the headline.
76
- Keep category and state meanings available through labels, shapes or patterns,
77
- not color alone. Measure contrast on actual surfaces; see [color review](review/color-theme.md).
78
- - For browser slides, provide meaningful accessible descriptions or a text
79
- equivalent for visual content. Check reading order and that chart alternatives
80
- convey the conclusion and relevant values, not merely “chart.” Confirm the
81
- content is exposed in the accessibility tree; source markup alone is insufficient.
82
- - When controls or motion are included, exercise keyboard navigation, visible
83
- focus and accessible control names. Check the reduced-motion path and ensure
84
- state changes remain understandable without animation. Keep runtime fixes
85
- upstream rather than building a second navigation or export system.
86
- - Inspect each requested export for text, fonts, reading order and alternatives
87
- as applicable. Browser/PNG success does not establish accessible PDF/PPTX or
88
- cross-application fidelity. Report unsupported or untested delivery properties.
package/lib/assets.d.ts DELETED
@@ -1,8 +0,0 @@
1
- declare module '*?url' {
2
- const url: string;
3
- export default url;
4
- }
5
- declare module '*?raw' {
6
- const text: string;
7
- export default text;
8
- }
package/lib/charts.ts DELETED
@@ -1,18 +0,0 @@
1
- import { typography, type Palette } from './taste.ts';
2
-
3
- // Shared presentation defaults for Nivo SVG components, not a chart wrapper.
4
- // Decks still own data, dimensions, scales, labels and semantic color assignments.
5
- export function chartDefaults(p: Palette) {
6
- return {
7
- animate: false,
8
- isInteractive: false,
9
- renderWrapper: false,
10
- colors: [p.accent, p.emphasis],
11
- theme: {
12
- text: { fontFamily: typography.family, fontSize: typography.caption, fill: p.fg },
13
- axis: { ticks: { text: { fill: p.fg, fontSize: typography.caption } }, domain: { line: { stroke: p.line } } },
14
- grid: { line: { stroke: p.line, strokeWidth: 1 } },
15
- labels: { text: { fontSize: typography.caption, fill: p.fg } },
16
- },
17
- };
18
- }
package/lib/contrast.ts DELETED
@@ -1,16 +0,0 @@
1
- // WCAG relative luminance for opaque, six-digit sRGB hex colors only.
2
- // Callers must establish the actual rendered pair; this does not inspect a DOM.
3
- export function contrastRatio(foreground: string, background: string): number {
4
- const luminance = (color: string) => {
5
- if (!/^#[0-9a-f]{6}$/i.test(color)) {
6
- throw new Error('Contrast requires opaque six-digit sRGB hex colors (#RRGGBB).');
7
- }
8
- const channels = [1, 3, 5].map(offset => {
9
- const value = Number.parseInt(color.slice(offset, offset + 2), 16) / 255;
10
- return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
11
- });
12
- return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
13
- };
14
- const fg = luminance(foreground), bg = luminance(background);
15
- return (Math.max(fg, bg) + 0.05) / (Math.min(fg, bg) + 0.05);
16
- }
package/lib/layouts.ts DELETED
@@ -1,50 +0,0 @@
1
- export type Region = { x: number; y: number; width: number; height: number };
2
- export type RelationshipSlot = { x: number; y: number; axis: 'horizontal' | 'vertical' };
3
- export const layoutKinds = ['two-columns', 'main-aside', 'three-columns', 'four-grid', 'one-left', 'one-right', 'one-top', 'one-bottom'] as const;
4
- export type LayoutKind = typeof layoutKinds[number];
5
- export const contentRegion: Region = { x: 112, y: 404, width: 1696, height: 520 };
6
-
7
- function split(area: Region, axis: 'horizontal' | 'vertical', weights: number[], gap: number): Region[] {
8
- const length = axis === 'horizontal' ? area.width : area.height;
9
- const available = length - gap * (weights.length - 1);
10
- const total = weights.reduce((a, b) => a + b, 0);
11
- let offset = 0;
12
- return weights.map(weight => {
13
- const size = available * weight / total;
14
- const region = axis === 'horizontal'
15
- ? { ...area, x: area.x + offset, width: size }
16
- : { ...area, y: area.y + offset, height: size };
17
- offset += size + gap;
18
- return region;
19
- });
20
- }
21
-
22
- function between(a: Region, b: Region, axis: RelationshipSlot['axis']): RelationshipSlot {
23
- return axis === 'horizontal'
24
- ? { x: (a.x + a.width + b.x) / 2, y: a.y + a.height / 2, axis }
25
- : { x: a.x + a.width / 2, y: (a.y + a.height + b.y) / 2, axis };
26
- }
27
-
28
- // Regions and relationship gutters, not an automatic layout/diagram solver.
29
- // A slot is available space; the author decides whether an arrow belongs there.
30
- export function arrange(kind: LayoutKind, area: Region = contentRegion, gap = 80): { panels: Region[]; slots: RelationshipSlot[] } {
31
- if (kind === 'two-columns' || kind === 'main-aside' || kind === 'three-columns') {
32
- const weights = kind === 'main-aside' ? [2, 1] : kind === 'three-columns' ? [1, 1, 1] : [1, 1];
33
- const panels = split(area, 'horizontal', weights, gap);
34
- return { panels, slots: panels.slice(1).map((panel, i) => between(panels[i], panel, 'horizontal')) };
35
- }
36
- if (kind === 'four-grid') {
37
- const rows = split(area, 'vertical', [1, 1], gap);
38
- const panels = rows.flatMap(row => split(row, 'horizontal', [1, 1], gap));
39
- return { panels, slots: [between(panels[0], panels[1], 'horizontal'), between(panels[2], panels[3], 'horizontal'), between(panels[0], panels[2], 'vertical'), between(panels[1], panels[3], 'vertical')] };
40
- }
41
- const axis = kind === 'one-top' || kind === 'one-bottom' ? 'vertical' : 'horizontal';
42
- const halves = split(area, axis, [1, 1], gap);
43
- const reversed = kind === 'one-right' || kind === 'one-bottom';
44
- const single = halves[reversed ? 1 : 0], group = halves[reversed ? 0 : 1];
45
- // Single panel is always index 0; the three siblings follow in reading order.
46
- return {
47
- panels: [single, ...split(group, axis === 'horizontal' ? 'vertical' : 'horizontal', [1, 1, 1], 32)],
48
- slots: [between(halves[0], halves[1], axis)],
49
- };
50
- }
package/lib/slide.tsx DELETED
@@ -1,42 +0,0 @@
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 DELETED
@@ -1,17 +0,0 @@
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 DELETED
@@ -1,89 +0,0 @@
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
- }
package/lib/typeface.ts DELETED
@@ -1,44 +0,0 @@
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
- }
@@ -1,120 +0,0 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, relative, resolve } from "node:path";
3
- import { createElement, type ComponentType } from "react";
4
- import { renderToStaticMarkup } from "react-dom/server";
5
- import { createServer } from "vite";
6
- import { compositionSchema } from "../composition/types.ts";
7
- import { validateComposition } from "../composition/validate.ts";
8
- import { parseEditableSvg } from "../composition/vector.ts";
9
-
10
- if (process.argv.includes("--help")) {
11
- console.log("Usage: pnpm example:migrate-page [trusted-react-file] [all|zero-based-page] [output.json]\nExecutes trusted local React source; imports its supported SVG primitives as editable composition vectors.");
12
- process.exit(0);
13
- }
14
-
15
- const root = process.cwd();
16
- const input = resolve(root, process.argv[2] ?? "slides/architecture/index.tsx");
17
- const pageSelection = process.argv[3] ?? "all";
18
- const output = resolve(
19
- root,
20
- process.argv[4] ?? "src/lib/examples/react-page-migration.json",
21
- );
22
- const moduleId = `/${relative(root, input).replaceAll("\\", "/")}`;
23
-
24
- if (
25
- pageSelection !== "all" &&
26
- (!Number.isInteger(Number(pageSelection)) || Number(pageSelection) < 0)
27
- )
28
- throw new Error('Page selection must be "all" or a non-negative index.');
29
-
30
- const server = await createServer({
31
- root,
32
- appType: "custom",
33
- server: { middlewareMode: true },
34
- plugins: [
35
- {
36
- name: "konpeki-react-page-migration",
37
- enforce: "pre",
38
- resolveId(source) {
39
- if (source.endsWith("/lib/typeface.ts")) return "\0konpeki-typeface";
40
- },
41
- load(id) {
42
- if (id === "\0konpeki-typeface")
43
- return "export const fontsReady = Promise.resolve([]);";
44
- },
45
- },
46
- ],
47
- });
48
-
49
- try {
50
- const loaded = await server.ssrLoadModule(moduleId) as {
51
- default?: ComponentType[];
52
- meta?: { title?: string };
53
- };
54
- const pages = loaded.default ?? [];
55
- const indexes = pageSelection === "all"
56
- ? pages.map((_, index) => index)
57
- : [Number(pageSelection)];
58
- if (!indexes.length || indexes.some((index) => !pages[index]))
59
- throw new Error(`No selected page exported by ${moduleId}.`);
60
- const title = loaded.meta?.title ?? "Migrated React page";
61
- const document = {
62
- schema: compositionSchema,
63
- title: `${title} — migrated React deck`,
64
- theme: { id: "plex", mode: "paper" },
65
- slides: indexes.map((pageIndex, outputIndex) => {
66
- const source = renderToStaticMarkup(createElement(pages[pageIndex]));
67
- const parsed = parseEditableSvg(source);
68
- const id = `migrated-page-${outputIndex + 1}`;
69
- const slotId = `${id}-content`;
70
- return {
71
- id: `slide-${outputIndex + 1}`,
72
- name: `Migrated page ${pageIndex + 1}`,
73
- canvas: { width: 1920, height: 1080 },
74
- innerPadding: { top: 72, right: 112, bottom: 0, left: 112 },
75
- pageNumber: { style: "none", color: "muted" },
76
- audience: "Reviewers of the migrated slide",
77
- question: "Does the former React page survive in the shared canvas?",
78
- intendedViewingSize: "presentation",
79
- contentSlots: [
80
- {
81
- id: slotId,
82
- label: "Migrated page",
83
- required: true,
84
- instruction: "Preserve the trusted React page as editable vector elements.",
85
- role: "entity",
86
- },
87
- ],
88
- components: [
89
- {
90
- id,
91
- kind: "diagram",
92
- preferredRect: { x: 0, y: 0, width: 1920, height: 1080 },
93
- slotIds: [slotId],
94
- intent: "A former React page converted to editable vector elements.",
95
- appearance: { type: "architecture", border: "none" },
96
- customVisual: {
97
- format: "vector",
98
- elements: parsed.elements,
99
- viewBox: parsed.viewBox,
100
- description: `Migrated page ${pageIndex + 1} from ${title}`,
101
- fit: "stretch",
102
- },
103
- },
104
- ],
105
- groups: [],
106
- readingOrder: [{ kind: "component", id }],
107
- paintOrder: [id],
108
- relationships: [],
109
- };
110
- }),
111
- };
112
- const validation = validateComposition(document);
113
- if (!validation.ok)
114
- throw new Error(`Generated composition is invalid: ${JSON.stringify(validation.issues)}`);
115
- await mkdir(dirname(output), { recursive: true });
116
- await writeFile(output, `${JSON.stringify(validation.document, null, 2)}\n`);
117
- console.log(`${relative(root, input)} (${indexes.length} pages) → ${relative(root, output)}`);
118
- } finally {
119
- await server.close();
120
- }
@@ -1,23 +0,0 @@
1
- {
2
- "schema": "konpeki-composition/v1",
3
- "title": "Untitled composition",
4
- "theme": { "id": "plex", "mode": "paper" },
5
- "slides": [
6
- {
7
- "id": "slide-1",
8
- "name": "Page 01",
9
- "canvas": { "width": 1920, "height": 1080 },
10
- "innerPadding": { "top": 72, "right": 112, "bottom": 0, "left": 112 },
11
- "pageNumber": { "style": "none", "color": "muted" },
12
- "audience": "Decision makers",
13
- "question": "What should the audience understand or decide?",
14
- "intendedViewingSize": "presentation",
15
- "contentSlots": [],
16
- "components": [],
17
- "groups": [],
18
- "readingOrder": [],
19
- "paintOrder": [],
20
- "relationships": []
21
- }
22
- ]
23
- }