konpeki 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +21 -28
- package/AUTHORING.md +68 -229
- package/CONTRIBUTING.md +10 -23
- package/README.md +96 -87
- package/SETUP.md +50 -114
- package/docs/development.md +43 -248
- package/docs/workflow.md +6 -108
- package/html/README.md +140 -0
- package/html/browser.ts +103 -0
- package/html/document.ts +38 -0
- package/html/floor.ts +34 -0
- package/html/index.html +5 -0
- package/html/inspect.ts +313 -0
- package/html/preview.css +62 -0
- package/html/preview.tsx +463 -0
- package/html/review-hints.ts +63 -0
- package/html/server.ts +99 -0
- package/html/source.ts +104 -0
- package/html/starter.ts +6 -0
- package/html/theme-authoring.md +176 -0
- package/html/theme.md +61 -0
- package/index.html +9 -9
- package/package.json +21 -35
- package/plugin.json +1 -1
- package/public/og.png +0 -0
- package/runtime/browser-B-29TH1a.mjs +595 -0
- package/runtime/floor-Cmk7G3pU.mjs +41 -0
- package/runtime/konpeki.mjs +90 -1410
- package/runtime/server-BAqC_5WD.mjs +2 -0
- package/runtime/server-DfRpfcY9.mjs +205 -0
- package/runtime/source-B_Ui9tMp.mjs +141 -0
- package/runtime/source-CZQq9GUO.mjs +2 -0
- package/skills/konpeki/SKILL.md +88 -172
- package/skills/konpeki/assets/blank.html +17 -0
- package/skills/konpeki/floor.md +76 -0
- package/skills/konpeki/references/cover.md +27 -0
- package/skills/konpeki/references/long-document.md +40 -0
- package/skills/konpeki/references/one-pager.md +27 -0
- package/skills/konpeki/references/patterns.md +118 -0
- package/skills/konpeki/references/resume.md +28 -0
- package/skills/konpeki/references/slides.md +28 -0
- package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
- package/skills/konpeki/scripts/prepare-document.mjs +25 -14
- package/src/components/PageBoard.tsx +156 -0
- package/src/lib/alignment.ts +21 -0
- package/src/lib/page-board.ts +25 -0
- package/src/lib/review-position.ts +19 -0
- package/src/styles/base.css +4 -6
- package/src/styles/feedback.css +97 -61
- package/src/styles/shell.css +95 -323
- package/theme-base.css +90 -0
- package/theme.css +56 -0
- package/vite.config.ts +2 -5
- package/composition/README.md +0 -156
- package/composition/compile.ts +0 -227
- package/composition/document.ts +0 -600
- package/composition/schema.json +0 -3001
- package/composition/schema.ts +0 -437
- package/composition/theme-tokens.ts +0 -16
- package/composition/types.ts +0 -269
- package/composition/validate.ts +0 -226
- package/composition/vector.ts +0 -143
- package/composition/visualizations.ts +0 -319
- package/design/README.md +0 -17
- package/design/palettes/README.md +0 -14
- package/design/palettes/base.ts +0 -14
- package/design/palettes/candidates.ts +0 -19
- package/design/palettes/index.ts +0 -78
- package/design/review/color-theme.md +0 -44
- package/design/review/layout.md +0 -16
- package/design/review/text.md +0 -18
- package/design/review/typography.md +0 -15
- package/design/review/visuals.md +0 -31
- package/design/semantic-patterns.md +0 -43
- package/design/themes/README.md +0 -40
- package/design/themes/index.ts +0 -24
- package/design/visual-languages/technical-product.md +0 -17
- package/design/visual-review.md +0 -88
- package/lib/assets.d.ts +0 -8
- package/lib/charts.ts +0 -18
- package/lib/contrast.ts +0 -16
- package/lib/layouts.ts +0 -50
- package/lib/slide.tsx +0 -42
- package/lib/taste.ts +0 -17
- package/lib/text.tsx +0 -89
- package/lib/typeface.ts +0 -44
- package/scripts/migrate-react-page.ts +0 -120
- package/skills/konpeki/assets/blank.json +0 -23
- package/slides/README.md +0 -153
- package/slides/architecture/PROMPT.md +0 -31
- package/slides/architecture/index.tsx +0 -102
- package/slides/article-brief/PROMPT.md +0 -35
- package/slides/article-brief/index.tsx +0 -71
- package/slides/bar-chart/PROMPT.md +0 -39
- package/slides/bar-chart/index.tsx +0 -97
- package/slides/comparison/PROMPT.md +0 -29
- package/slides/comparison/index.tsx +0 -95
- package/slides/decision-memo/PROMPT.md +0 -34
- package/slides/decision-memo/index.tsx +0 -85
- package/slides/delivery-plan/PROMPT.md +0 -45
- package/slides/delivery-plan/index.tsx +0 -105
- package/slides/experiment/PROMPT.md +0 -44
- package/slides/experiment/index.tsx +0 -127
- package/slides/incident-workflow/PROMPT.md +0 -57
- package/slides/incident-workflow/index.tsx +0 -78
- package/slides/introducing-konpeki/PROMPT.md +0 -40
- package/slides/introducing-konpeki/README.md +0 -76
- package/slides/introducing-konpeki/SOURCE.md +0 -26
- package/slides/introducing-konpeki/author.ts +0 -165
- package/slides/introducing-konpeki/composition.json +0 -3270
- package/slides/line-chart/PROMPT.md +0 -40
- package/slides/line-chart/index.tsx +0 -72
- package/slides/migration/PROMPT.md +0 -38
- package/slides/migration/index.tsx +0 -89
- package/slides/og-images/PROMPT.md +0 -21
- package/slides/og-images/index.tsx +0 -76
- package/slides/product-introduction/PROMPT.md +0 -24
- package/slides/product-introduction/index.tsx +0 -105
- package/slides/research-brief/PROMPT.md +0 -40
- package/slides/research-brief/index.tsx +0 -104
- package/slides/results-explanation/PROMPT.md +0 -32
- package/slides/results-explanation/index.tsx +0 -96
- package/slides/retrospective/PROMPT.md +0 -43
- package/slides/retrospective/index.tsx +0 -105
- package/slides/sankey/PROMPT.md +0 -11
- package/slides/sankey/index.tsx +0 -93
- package/slides/teaching/PROMPT.md +0 -45
- package/slides/teaching/index.tsx +0 -124
- package/slides/vertical-bar-charts/PROMPT.md +0 -13
- package/slides/vertical-bar-charts/index.tsx +0 -97
- package/src/app/App.tsx +0 -820
- package/src/components/BuildOrb.tsx +0 -40
- package/src/components/Canvas.tsx +0 -1185
- package/src/components/DiagramTypeIcon.tsx +0 -78
- package/src/components/InspectorPanel.tsx +0 -773
- package/src/components/LeftPanel.tsx +0 -120
- package/src/components/PageSizePicker.tsx +0 -30
- package/src/components/Presentation.tsx +0 -105
- package/src/components/RevisionNotes.tsx +0 -56
- package/src/components/RightPanel.tsx +0 -201
- package/src/components/VectorOverflowWarning.tsx +0 -46
- package/src/components/WorkspaceChrome.tsx +0 -288
- package/src/components/ui.tsx +0 -53
- package/src/lib/examples/react-page-migration.json +0 -1295
- package/src/lib/examples.ts +0 -42
- package/src/lib/export-png.ts +0 -104
- package/src/lib/file-session.ts +0 -87
- package/src/lib/history.ts +0 -53
- package/src/lib/model.ts +0 -188
- package/src/lib/page-size.ts +0 -24
- package/src/lib/presentation.ts +0 -17
- package/src/lib/review.ts +0 -26
- package/src/lib/storage.ts +0 -71
- package/src/lib/theme.ts +0 -25
- package/src/lib/use-file-session.ts +0 -227
- package/src/main.tsx +0 -29
- package/src/styles/canvas.css +0 -299
- package/src/styles/chrome.css +0 -384
- package/src/styles/component-previews.css +0 -386
- package/src/styles/left-panel.css +0 -166
- package/src/styles/presentation.css +0 -72
- package/src/styles/right-panel.css +0 -1215
package/design/themes/README.md
DELETED
|
@@ -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.
|
package/design/themes/index.ts
DELETED
|
@@ -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).
|
package/design/visual-review.md
DELETED
|
@@ -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
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
|
-
}
|