konpeki 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +21 -28
- package/AUTHORING.md +68 -229
- package/CONTRIBUTING.md +10 -23
- package/README.md +96 -87
- package/SETUP.md +50 -114
- package/docs/development.md +43 -248
- package/docs/workflow.md +6 -108
- package/html/README.md +140 -0
- package/html/browser.ts +103 -0
- package/html/document.ts +38 -0
- package/html/floor.ts +34 -0
- package/html/index.html +5 -0
- package/html/inspect.ts +313 -0
- package/html/preview.css +62 -0
- package/html/preview.tsx +463 -0
- package/html/review-hints.ts +63 -0
- package/html/server.ts +99 -0
- package/html/source.ts +104 -0
- package/html/starter.ts +6 -0
- package/html/theme-authoring.md +176 -0
- package/html/theme.md +61 -0
- package/index.html +9 -9
- package/package.json +21 -35
- package/plugin.json +1 -1
- package/public/og.png +0 -0
- package/runtime/browser-B-29TH1a.mjs +595 -0
- package/runtime/floor-Cmk7G3pU.mjs +41 -0
- package/runtime/konpeki.mjs +90 -1410
- package/runtime/server-BAqC_5WD.mjs +2 -0
- package/runtime/server-DfRpfcY9.mjs +205 -0
- package/runtime/source-B_Ui9tMp.mjs +141 -0
- package/runtime/source-CZQq9GUO.mjs +2 -0
- package/skills/konpeki/SKILL.md +88 -172
- package/skills/konpeki/assets/blank.html +17 -0
- package/skills/konpeki/floor.md +76 -0
- package/skills/konpeki/references/cover.md +27 -0
- package/skills/konpeki/references/long-document.md +40 -0
- package/skills/konpeki/references/one-pager.md +27 -0
- package/skills/konpeki/references/patterns.md +118 -0
- package/skills/konpeki/references/resume.md +28 -0
- package/skills/konpeki/references/slides.md +28 -0
- package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
- package/skills/konpeki/scripts/prepare-document.mjs +25 -14
- package/src/components/PageBoard.tsx +156 -0
- package/src/lib/alignment.ts +21 -0
- package/src/lib/page-board.ts +25 -0
- package/src/lib/review-position.ts +19 -0
- package/src/styles/base.css +4 -6
- package/src/styles/feedback.css +97 -61
- package/src/styles/shell.css +95 -323
- package/theme-base.css +90 -0
- package/theme.css +56 -0
- package/vite.config.ts +2 -5
- package/composition/README.md +0 -156
- package/composition/compile.ts +0 -227
- package/composition/document.ts +0 -600
- package/composition/schema.json +0 -3001
- package/composition/schema.ts +0 -437
- package/composition/theme-tokens.ts +0 -16
- package/composition/types.ts +0 -269
- package/composition/validate.ts +0 -226
- package/composition/vector.ts +0 -143
- package/composition/visualizations.ts +0 -319
- package/design/README.md +0 -17
- package/design/palettes/README.md +0 -14
- package/design/palettes/base.ts +0 -14
- package/design/palettes/candidates.ts +0 -19
- package/design/palettes/index.ts +0 -78
- package/design/review/color-theme.md +0 -44
- package/design/review/layout.md +0 -16
- package/design/review/text.md +0 -18
- package/design/review/typography.md +0 -15
- package/design/review/visuals.md +0 -31
- package/design/semantic-patterns.md +0 -43
- package/design/themes/README.md +0 -40
- package/design/themes/index.ts +0 -24
- package/design/visual-languages/technical-product.md +0 -17
- package/design/visual-review.md +0 -88
- package/lib/assets.d.ts +0 -8
- package/lib/charts.ts +0 -18
- package/lib/contrast.ts +0 -16
- package/lib/layouts.ts +0 -50
- package/lib/slide.tsx +0 -42
- package/lib/taste.ts +0 -17
- package/lib/text.tsx +0 -89
- package/lib/typeface.ts +0 -44
- package/scripts/migrate-react-page.ts +0 -120
- package/skills/konpeki/assets/blank.json +0 -23
- package/slides/README.md +0 -153
- package/slides/architecture/PROMPT.md +0 -31
- package/slides/architecture/index.tsx +0 -102
- package/slides/article-brief/PROMPT.md +0 -35
- package/slides/article-brief/index.tsx +0 -71
- package/slides/bar-chart/PROMPT.md +0 -39
- package/slides/bar-chart/index.tsx +0 -97
- package/slides/comparison/PROMPT.md +0 -29
- package/slides/comparison/index.tsx +0 -95
- package/slides/decision-memo/PROMPT.md +0 -34
- package/slides/decision-memo/index.tsx +0 -85
- package/slides/delivery-plan/PROMPT.md +0 -45
- package/slides/delivery-plan/index.tsx +0 -105
- package/slides/experiment/PROMPT.md +0 -44
- package/slides/experiment/index.tsx +0 -127
- package/slides/incident-workflow/PROMPT.md +0 -57
- package/slides/incident-workflow/index.tsx +0 -78
- package/slides/introducing-konpeki/PROMPT.md +0 -40
- package/slides/introducing-konpeki/README.md +0 -76
- package/slides/introducing-konpeki/SOURCE.md +0 -26
- package/slides/introducing-konpeki/author.ts +0 -165
- package/slides/introducing-konpeki/composition.json +0 -3270
- package/slides/line-chart/PROMPT.md +0 -40
- package/slides/line-chart/index.tsx +0 -72
- package/slides/migration/PROMPT.md +0 -38
- package/slides/migration/index.tsx +0 -89
- package/slides/og-images/PROMPT.md +0 -21
- package/slides/og-images/index.tsx +0 -76
- package/slides/product-introduction/PROMPT.md +0 -24
- package/slides/product-introduction/index.tsx +0 -105
- package/slides/research-brief/PROMPT.md +0 -40
- package/slides/research-brief/index.tsx +0 -104
- package/slides/results-explanation/PROMPT.md +0 -32
- package/slides/results-explanation/index.tsx +0 -96
- package/slides/retrospective/PROMPT.md +0 -43
- package/slides/retrospective/index.tsx +0 -105
- package/slides/sankey/PROMPT.md +0 -11
- package/slides/sankey/index.tsx +0 -93
- package/slides/teaching/PROMPT.md +0 -45
- package/slides/teaching/index.tsx +0 -124
- package/slides/vertical-bar-charts/PROMPT.md +0 -13
- package/slides/vertical-bar-charts/index.tsx +0 -97
- package/src/app/App.tsx +0 -820
- package/src/components/BuildOrb.tsx +0 -40
- package/src/components/Canvas.tsx +0 -1185
- package/src/components/DiagramTypeIcon.tsx +0 -78
- package/src/components/InspectorPanel.tsx +0 -773
- package/src/components/LeftPanel.tsx +0 -120
- package/src/components/PageSizePicker.tsx +0 -30
- package/src/components/Presentation.tsx +0 -105
- package/src/components/RevisionNotes.tsx +0 -56
- package/src/components/RightPanel.tsx +0 -201
- package/src/components/VectorOverflowWarning.tsx +0 -46
- package/src/components/WorkspaceChrome.tsx +0 -288
- package/src/components/ui.tsx +0 -53
- package/src/lib/examples/react-page-migration.json +0 -1295
- package/src/lib/examples.ts +0 -42
- package/src/lib/export-png.ts +0 -104
- package/src/lib/file-session.ts +0 -87
- package/src/lib/history.ts +0 -53
- package/src/lib/model.ts +0 -188
- package/src/lib/page-size.ts +0 -24
- package/src/lib/presentation.ts +0 -17
- package/src/lib/review.ts +0 -26
- package/src/lib/storage.ts +0 -71
- package/src/lib/theme.ts +0 -25
- package/src/lib/use-file-session.ts +0 -227
- package/src/main.tsx +0 -29
- package/src/styles/canvas.css +0 -299
- package/src/styles/chrome.css +0 -384
- package/src/styles/component-previews.css +0 -386
- package/src/styles/left-panel.css +0 -166
- package/src/styles/presentation.css +0 -72
- package/src/styles/right-panel.css +0 -1215
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Authoring floor
|
|
2
|
+
|
|
3
|
+
Read this immediately before writing or revising HTML. These are the rules
|
|
4
|
+
agents break when working from habit. Each has an ID. `inspect` reports the
|
|
5
|
+
`(inspect)` rules as diagnostics under the same code, and `render` prints the
|
|
6
|
+
`(review)` rules as the checklist for looking at the page.
|
|
7
|
+
|
|
8
|
+
Fix every floor warning or justify keeping it in your final report. An opt-out
|
|
9
|
+
attribute such as `data-kp-allow="label-above-headline"` needs that
|
|
10
|
+
justification too.
|
|
11
|
+
|
|
12
|
+
## Bans
|
|
13
|
+
|
|
14
|
+
No brief, theme or habit earns these back.
|
|
15
|
+
|
|
16
|
+
- `label-above-headline` (inspect): No label, kicker, eyebrow or metadata line
|
|
17
|
+
above a headline or section heading. Put the product, date, series or source
|
|
18
|
+
in the headline, the supporting line, a caption or the footer. Running heads
|
|
19
|
+
repeated across multi-page print are exempt. A label the brief requires, such
|
|
20
|
+
as Before/After, sits outside the headline's column.
|
|
21
|
+
- `stranded-word` (inspect): No headline line holds one stranded word after a
|
|
22
|
+
fuller line, including the line before a `<br>`. Reword it or change its
|
|
23
|
+
measure; break with `<br>` only where the meaning divides. Never shrink type
|
|
24
|
+
to fit.
|
|
25
|
+
- `restated-takeaway` (review): State the takeaway once: a subtitle, caption,
|
|
26
|
+
callout or note adds evidence, a qualification or a next step, never the
|
|
27
|
+
headline in other words.
|
|
28
|
+
- `slogan-copy` (review): No slogan contrasts ("Not a feature. A platform.",
|
|
29
|
+
"a signal, not proof") and no chains of em dashes. Keep a "not X" clause only
|
|
30
|
+
where it corrects a likely misreading, such as concentrations, not emissions.
|
|
31
|
+
- `decorative-container` (review): No panel, card, badge or pill unless it marks
|
|
32
|
+
a distinct object, state or contrasting field, and never a panel inside a
|
|
33
|
+
panel. Use space, alignment and rules first.
|
|
34
|
+
- `honest-encoding` (review): Amount bars start at zero, arrows point the true
|
|
35
|
+
direction, and nothing implies causal, temporal or quantitative meaning the
|
|
36
|
+
source lacks. Colour never carries meaning alone; pair it with a label, shape
|
|
37
|
+
or position.
|
|
38
|
+
|
|
39
|
+
## Defaults
|
|
40
|
+
|
|
41
|
+
The brief or a theme's `NOTES.md` may override these, explicitly.
|
|
42
|
+
|
|
43
|
+
- `headline-first` (review): Reading starts with the headline, and the headline
|
|
44
|
+
states the claim. "Cold starts fell 62%" is a claim; "Q3 results" is a topic.
|
|
45
|
+
- `one-focus` (review): Accent marks the headline and at most one other element:
|
|
46
|
+
the main claim, path or data point. Everything else uses ink, muted ink or
|
|
47
|
+
categorical colour.
|
|
48
|
+
- `format-budget` (review): Fit content to the viewing size: a link card holds a
|
|
49
|
+
headline, one supporting line and a date, and a slide holds one idea. Full
|
|
50
|
+
detail belongs on the one-pager or report.
|
|
51
|
+
- `grouping` (review): Space shows grouping: an item sits closer to what it
|
|
52
|
+
qualifies than to the next subject, and true peers share size, weight and
|
|
53
|
+
treatment. New edges align with existing ones.
|
|
54
|
+
- `page-footer` (review): Keep page-level source notes and page numbers together
|
|
55
|
+
in a reserved footer, clear of body content. Reserve that space in the first
|
|
56
|
+
layout, not after filling the page. Do not pin the page number independently
|
|
57
|
+
while leaving its source note in body flow. Citations that explain a specific
|
|
58
|
+
figure or claim still belong beside that content.
|
|
59
|
+
- `diagram-economy` (review): A diagram uses at most three box styles, each with
|
|
60
|
+
a meaning. Arrowheads scale with the stroke (`markerUnits="strokeWidth"`) and
|
|
61
|
+
stay clear of labels.
|
|
62
|
+
|
|
63
|
+
## Verify
|
|
64
|
+
|
|
65
|
+
`inspect` enforces these as errors or warnings; read the diagnostic message.
|
|
66
|
+
|
|
67
|
+
- `small-text` (inspect): Text is at least 24 CSS px on screen presets and 11 on
|
|
68
|
+
A4. These are floors for small print, not sizes for body text.
|
|
69
|
+
- `low-contrast` (inspect): Text contrast is at least 4.5:1, or 3:1 for large
|
|
70
|
+
text, including nested roles and code on contrasting fields.
|
|
71
|
+
- `text-overflow`, `clipped-text`, `text-overlap`, `page-overflow` (inspect):
|
|
72
|
+
Nothing leaves the page, hides behind a clip or collides. Recompose; do not
|
|
73
|
+
hide, truncate or shrink required copy.
|
|
74
|
+
- `font-fallback` (inspect): Every glyph comes from a loaded webfont, whether
|
|
75
|
+
Google-hosted, local, or embedded. Supply the required subsets, weights, and
|
|
76
|
+
styles; do not rely on installed-font fallback or synthesized faces.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Cover or social graphic
|
|
2
|
+
|
|
3
|
+
Use for a cover, thumbnail, link preview, announcement image, or social graphic.
|
|
4
|
+
Apply the [shared guidance](../SKILL.md#before-authoring) and the
|
|
5
|
+
[authoring floor](../floor.md); this is not a miniature document.
|
|
6
|
+
|
|
7
|
+
- Use the requested destination dimensions and crop. If none are supplied for
|
|
8
|
+
a general link-preview cover, start with the theme's `link` preset. Square,
|
|
9
|
+
portrait, and other ratios are equally valid for their intended channels.
|
|
10
|
+
- Make the main subject or message recognizable at thumbnail size. Let imagery
|
|
11
|
+
lead when it communicates the subject; do not require a large slogan on every
|
|
12
|
+
cover. Keep supporting copy short without stripping essential qualifications.
|
|
13
|
+
- A deliberate headline break may improve composition. Check the actual words
|
|
14
|
+
and font rather than fixing a line count or inserting breaks into every label.
|
|
15
|
+
- Keep essential text, faces, logos, and marks clear of the expected crop and
|
|
16
|
+
platform overlays. Do not invent universal safe-area numbers; use the brief's
|
|
17
|
+
placement requirements or verify the destination when placement is critical.
|
|
18
|
+
- Use supplied or approved imagery and preserve its meaning. Do not stretch
|
|
19
|
+
images, crop away evidence, or imply an unverified product interface is real.
|
|
20
|
+
- For mixed compositions, use [text with visuals](patterns.md#text-with-visuals)
|
|
21
|
+
to choose the image role, crop, text region, and any edge or contrast treatment.
|
|
22
|
+
Do not add badges, eyebrows, metadata, or a call to action simply because other
|
|
23
|
+
covers contain them.
|
|
24
|
+
|
|
25
|
+
Inspect both the full-resolution export and a reduced preview at the intended
|
|
26
|
+
viewing size. Verify the headline remains legible and essential details survive
|
|
27
|
+
the expected crop. Export PNG at the requested size; PDF is optional if requested.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Multi-page long document
|
|
2
|
+
|
|
3
|
+
Use for reports, essays, field guides, proposals, and other sustained reading.
|
|
4
|
+
Apply the [shared guidance](../SKILL.md#before-authoring) and the
|
|
5
|
+
[authoring floor](../floor.md), not slide-density conventions.
|
|
6
|
+
|
|
7
|
+
- Start with the theme's portrait `a4` preset for an unspecified printable
|
|
8
|
+
document; honor Letter or other destination sizes. Keep margins, body
|
|
9
|
+
typography, and heading levels consistent across pages.
|
|
10
|
+
- Test the page structure before filling the document: a short page and a dense
|
|
11
|
+
page, both with multiline source notes. Apply the floor's `page-footer` rule.
|
|
12
|
+
A flex-column page with non-shrinking body and footer, and `margin-top: auto`
|
|
13
|
+
on the footer, puts spare space above it rather than between its parts.
|
|
14
|
+
Check the same bottom margin and body clearance on both pages; neither may
|
|
15
|
+
rely on hiding overflow or shrinking text. Then reuse that structure.
|
|
16
|
+
- Organize the argument into sections before dividing it into pages. Use normal
|
|
17
|
+
HTML text flow within each page, not separately positioned lines or sentences.
|
|
18
|
+
Choose line measure and leading for continuous reading at the delivered size.
|
|
19
|
+
- Author every page explicitly. Konpeki will not flow an overflowing paragraph
|
|
20
|
+
into a new page; CSS print-break hints do not replace `data-page` boundaries.
|
|
21
|
+
Measure real text with the loaded fonts before deciding where to split it.
|
|
22
|
+
- Keep headings with their opening paragraph and captions with their figures.
|
|
23
|
+
Avoid isolated first or last lines and stranded short list items. Split a long
|
|
24
|
+
paragraph only at a readable boundary, preserving its wording and punctuation.
|
|
25
|
+
- Keep complete table rows together. When a table spans pages, repeat its
|
|
26
|
+
column headings and make units and continuation clear. Do not silently drop
|
|
27
|
+
rows, references, or qualifications to meet a page count.
|
|
28
|
+
- Use page numbers for navigation. Add running titles or a contents page only
|
|
29
|
+
when the length and structure warrant them. Keep footnotes and citations with
|
|
30
|
+
the claims they qualify; essential evidence is not decorative footer clutter.
|
|
31
|
+
- On revision, recheck the following pages: a changed paragraph can alter the
|
|
32
|
+
best page breaks even when the edited page no longer overflows.
|
|
33
|
+
|
|
34
|
+
Inspect every PDF page, including the final page, for continuity, uneven density,
|
|
35
|
+
awkward breaks, missing text, and footer grouping and clearance. Compare page
|
|
36
|
+
endings across the document; a detached footer can pass overflow checks. Review
|
|
37
|
+
text extraction order when searchable or accessible reading is part of delivery.
|
|
38
|
+
|
|
39
|
+
The footer check comes from the VulcanBench SWE-v4 report: its source note floated
|
|
40
|
+
above an independently pinned page number despite passing overflow inspection.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# One-pager or card
|
|
2
|
+
|
|
3
|
+
Use for a bounded brief, summary, reference sheet, invitation, or information
|
|
4
|
+
card. Apply the [shared guidance](../SKILL.md#before-authoring) and the
|
|
5
|
+
[authoring floor](../floor.md). A résumé or presentation slide uses its own guide
|
|
6
|
+
even when it contains only one page.
|
|
7
|
+
|
|
8
|
+
- Deliver one explicit page. Use the `a4` preset for an unspecified printable
|
|
9
|
+
one-pager; for a card, choose dimensions for its destination. Landscape, portrait,
|
|
10
|
+
square, and custom sizes are all valid; do not force cards into a slide ratio.
|
|
11
|
+
- Establish the page's reader task: understand a proposal, compare options,
|
|
12
|
+
recall information, or act on an invitation. Let that task determine emphasis
|
|
13
|
+
and section order instead of imposing a fixed number of content blocks.
|
|
14
|
+
- Retain the evidence needed to interpret the main message. A headline, a
|
|
15
|
+
prominent number, and a call to action are not sufficient if scope, units,
|
|
16
|
+
conditions, or supporting facts have disappeared.
|
|
17
|
+
- Use columns for genuinely parallel or scannable material. Use flowing prose
|
|
18
|
+
for an argument. Do not turn every paragraph into an equally prominent card.
|
|
19
|
+
- Add contact details or a next action only when the brief calls for them.
|
|
20
|
+
Keep them findable without giving boilerplate the same weight as the content.
|
|
21
|
+
- Treat the one-page limit as an editorial constraint, not permission to use
|
|
22
|
+
tiny type. Condense repetition and recompose; if required content still cannot
|
|
23
|
+
fit legibly, surface that conflict rather than adding a second page silently.
|
|
24
|
+
|
|
25
|
+
Check the whole page at its intended print or display size, including small cards.
|
|
26
|
+
Whitespace should reveal hierarchy, not leave essential copy crowded into one
|
|
27
|
+
corner while a decorative region consumes the page.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Content and visual patterns
|
|
2
|
+
|
|
3
|
+
Read only the sections that occur in the work, after the
|
|
4
|
+
[shared guidance](../SKILL.md#before-authoring) and [authoring floor](../floor.md).
|
|
5
|
+
Choose by the audience's question, not a favorite layout. Plain text or an image
|
|
6
|
+
may be enough. Infer the form without asking the user to choose a diagram taxonomy;
|
|
7
|
+
preserve an explicitly requested form unless a change is agreed.
|
|
8
|
+
|
|
9
|
+
## Text with visuals
|
|
10
|
+
|
|
11
|
+
Use when text shares a composition with photography, screenshots, illustrations,
|
|
12
|
+
charts, diagrams, or other graphic elements, in any output format.
|
|
13
|
+
|
|
14
|
+
- Decide the visual's role: evidence, explanation, main subject, or atmosphere.
|
|
15
|
+
A chart remains evidence even when it looks decorative. A screenshot may
|
|
16
|
+
contain text the reader needs to read; budget for that at delivery size.
|
|
17
|
+
Keep supporting text that adds meaning, not a caption just to fill a region.
|
|
18
|
+
- Plan the crop, focal subject, and text region together before filling the
|
|
19
|
+
page. Keep text clear of faces, subjects, meaningful details, and data marks.
|
|
20
|
+
Do not finish the visual and squeeze required copy into the remaining space.
|
|
21
|
+
- Use side-by-side regions when text and visual explain each other and both
|
|
22
|
+
remain readable. Stack them when the format is narrow or the visual needs
|
|
23
|
+
full width. Overlay text when the image has a suitable text region or its
|
|
24
|
+
atmosphere should fill the composition. These are choices, not fixed templates.
|
|
25
|
+
- For two columns, give text the claim and interpretation, and the visual the
|
|
26
|
+
evidence or explanation; avoid repetition. Allocate width for readable visual
|
|
27
|
+
details and a useful text measure, not an automatic 50/50 split. Align meaningful
|
|
28
|
+
anchors such as the headline and plot area, accounting for whitespace inside
|
|
29
|
+
an image. Keep captions, units, legends, and sources with their visual. Recompose
|
|
30
|
+
into stacked regions rather than squeezing both columns in a narrow format.
|
|
31
|
+
- First find a readable text region through placement: beside the visual or
|
|
32
|
+
in quiet image space. Add an opaque field over an image only when needed for
|
|
33
|
+
required text, not to demonstrate a treatment. Separate regions need not be
|
|
34
|
+
cards, and white is only one field color. Set its ink explicitly, including
|
|
35
|
+
nested type roles; a contrast field should not obscure meaningful content.
|
|
36
|
+
- Use local darkening or lightening to support text when the image can retain
|
|
37
|
+
its meaning. Use an edge gradient or mask when an atmospheric image needs to
|
|
38
|
+
blend into the surrounding field; a clean edge is equally valid. Apply the
|
|
39
|
+
effect to the image or a separate overlay, never to the text or a shared
|
|
40
|
+
parent. Do not use text shadows as a substitute for a readable region.
|
|
41
|
+
- Keep informational visuals intact. Do not fade chart marks or axes, hide
|
|
42
|
+
screenshot details, or turn a meaningful diagram into background texture.
|
|
43
|
+
Place explanations near what they qualify, with clearance from marks and
|
|
44
|
+
labels. Preserve aspect ratio and meaningful relative scale; identify crops
|
|
45
|
+
and alterations when they could change how evidence is interpreted.
|
|
46
|
+
- Review the final crop at delivery size and reduced size, not just enlarged.
|
|
47
|
+
Include captions, sources, and qualifications in that review. Check local
|
|
48
|
+
contrast under every line, subject visibility, effect boundaries, and whether
|
|
49
|
+
the visual still communicates its purpose. Solid-color contrast checks cannot
|
|
50
|
+
establish readability over images, gradients, or masks.
|
|
51
|
+
|
|
52
|
+
## Charts and quantities
|
|
53
|
+
|
|
54
|
+
- Choose the encoding for the question: comparison, change over time,
|
|
55
|
+
distribution, or parts of a whole. Preserve units, denominators, time windows,
|
|
56
|
+
uncertainty, missing values, and sources.
|
|
57
|
+
- Apply the floor's `honest-encoding` rule. Make scales explicit; distinguish
|
|
58
|
+
a truncated axis from a complete magnitude. Keep scales consistent across
|
|
59
|
+
charts meant to be compared.
|
|
60
|
+
- Derive mark lengths, areas, and positions from the supplied values, not visual
|
|
61
|
+
guesses. For parts or branches, reconcile totals and explain any remainder.
|
|
62
|
+
- Keep labels near their marks and give annotations clearance from marks and
|
|
63
|
+
axes. Do not turn uncertain or illustrative data into apparently measured
|
|
64
|
+
precision.
|
|
65
|
+
|
|
66
|
+
## Tables
|
|
67
|
+
|
|
68
|
+
- Use a table for exact lookup or repeated attributes, not merely to position
|
|
69
|
+
unrelated paragraphs. Name columns and units; align comparable numbers and
|
|
70
|
+
use consistent precision. Distinguish zero, unavailable, and not applicable.
|
|
71
|
+
- Align short row labels with the first line of multiline descriptions. Let
|
|
72
|
+
rows grow with content; do not hide a long cell to maintain equal heights.
|
|
73
|
+
- Use rules or fills when they clarify rows, groups, or totals. A table-ending
|
|
74
|
+
rule belongs to the table, not the page footer. Keep notes clear of that rule.
|
|
75
|
+
- On continued tables, preserve complete rows and repeat the information needed
|
|
76
|
+
to interpret the next page.
|
|
77
|
+
|
|
78
|
+
## Comparisons and decisions
|
|
79
|
+
|
|
80
|
+
- Compare options against the same criteria, units, and scope. Preserve material
|
|
81
|
+
drawbacks and unknowns; do not invent scores to create a neat ranking.
|
|
82
|
+
- Align corresponding rows or measures so the reader need not reconstruct the
|
|
83
|
+
mapping. Different evidence lengths do not require identical box heights.
|
|
84
|
+
- Make a recommendation distinguishable from the evidence. Keep its conditions
|
|
85
|
+
near it rather than presenting a conditional choice as universally best.
|
|
86
|
+
|
|
87
|
+
## Processes and sequences
|
|
88
|
+
|
|
89
|
+
- Show inputs, actors or steps, outcomes, and meaningful blockers. Label branches
|
|
90
|
+
and return paths; pending is not approved and retrying is not completed.
|
|
91
|
+
- Use arrows only for supported direction, dependency, or order. Preserve message
|
|
92
|
+
order and named participants when time is the story. Label asynchronous work,
|
|
93
|
+
failure paths, and guarded transitions when those distinctions matter.
|
|
94
|
+
- Give every connector an identifiable source and destination. Route it clear
|
|
95
|
+
of labels and unrelated nodes; near-touches must not imply junctions. Useful
|
|
96
|
+
elbows are preferable to diagonal fans or detached endpoints. Check arrowheads
|
|
97
|
+
and shafts at delivery size, not just coordinate bounds.
|
|
98
|
+
|
|
99
|
+
## Systems and hierarchies
|
|
100
|
+
|
|
101
|
+
- Name entities, connections, and meaningful boundaries. Use enclosure only for
|
|
102
|
+
actual containment, ownership, or scope; proximity alone must not invent one.
|
|
103
|
+
- Keep parent-child meaning consistent. Use a system map rather than a tree
|
|
104
|
+
when shared ownership or cross-links are central to the explanation.
|
|
105
|
+
- Label connection meaning and direction. A static dependency map is not an
|
|
106
|
+
execution trace; use a sequence when event order is the reader's question.
|
|
107
|
+
- Preserve specialized notation when requested. Draw it directly in HTML/SVG;
|
|
108
|
+
do not invent a component schema or convert every relationship to one box type.
|
|
109
|
+
|
|
110
|
+
## Annotated details
|
|
111
|
+
|
|
112
|
+
- Retain enough context to locate the detail. Put labels near the relevant
|
|
113
|
+
evidence and make crops or mockups explicit.
|
|
114
|
+
- Verify what the artifact actually demonstrates; do not add unsupported
|
|
115
|
+
specifications or measurements. An annotation should clarify, not obscure.
|
|
116
|
+
|
|
117
|
+
Inspect the rendered pattern: are relationships, quantities, and labels clear
|
|
118
|
+
without narration? Browser overflow checks cannot establish their truth.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Résumé
|
|
2
|
+
|
|
3
|
+
Use for a résumé or CV, even when it is a single page. Apply the
|
|
4
|
+
[shared guidance](../SKILL.md#before-authoring) and the
|
|
5
|
+
[authoring floor](../floor.md); do not treat it as a marketing slide or a generic card.
|
|
6
|
+
|
|
7
|
+
- Start with the theme's portrait `a4` preset; use Letter or another size when
|
|
8
|
+
the destination calls for it. Let relevant supplied experience determine
|
|
9
|
+
length rather than assuming every résumé must fit one page.
|
|
10
|
+
- Make the person's name, contact details, and relevant experience easy to scan.
|
|
11
|
+
Choose section order for the candidate and role; do not manufacture a summary,
|
|
12
|
+
project, award, or skills section merely to fill a template.
|
|
13
|
+
- Keep organization, role, dates, and location unambiguous. Use consistent date
|
|
14
|
+
formatting and clear chronology; align dates without detaching them from the
|
|
15
|
+
role they describe.
|
|
16
|
+
- Write concise evidence-led accomplishments from supplied facts. Never invent
|
|
17
|
+
metrics, seniority, qualifications, or causal claims. Preserve materially
|
|
18
|
+
different roles rather than merging them to make the layout cleaner.
|
|
19
|
+
- Start with a simple reading order and flowing text. Use additional columns
|
|
20
|
+
only when they remain readable and extract in the intended order. Avoid
|
|
21
|
+
skill-rating bars and icon-only contact information.
|
|
22
|
+
- Keep a role heading with its first accomplishment. Prefer breaks between
|
|
23
|
+
roles or sections; if an entry must continue, make that continuation clear.
|
|
24
|
+
Remove low-value repetition before reducing type to meet a page limit.
|
|
25
|
+
|
|
26
|
+
Inspect the PDF at normal reading size. Check contact text, links, dates,
|
|
27
|
+
selectable text, and extraction order. Do not claim ATS compatibility from
|
|
28
|
+
appearance or a clean overflow report alone.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Landscape slides
|
|
2
|
+
|
|
3
|
+
Use for presentation pages or a deck, including a single slide. Apply the
|
|
4
|
+
[shared guidance](../SKILL.md#before-authoring) and the
|
|
5
|
+
[authoring floor](../floor.md); the brief overrides format defaults below.
|
|
6
|
+
|
|
7
|
+
- Start with the theme's 16:9 `presentation` preset when the destination
|
|
8
|
+
supplies no size. Honor a requested ratio and keep deck dimensions consistent
|
|
9
|
+
unless a mixed-size deliverable is intentional.
|
|
10
|
+
- Build a reading sequence: each page advances the explanation rather than
|
|
11
|
+
repeating the previous page's conclusion. Do not impose a fixed opening,
|
|
12
|
+
agenda, or closing-page formula on every brief.
|
|
13
|
+
- Give a page one main point and enough evidence to support it. A takeaway
|
|
14
|
+
headline is useful; a giant headline that displaces the explanation is not.
|
|
15
|
+
- Compose for viewing at a distance. Start with the preset's resolved type
|
|
16
|
+
roles, then check the intended view; the small-text floor is not a body-copy
|
|
17
|
+
target. Keep sources, units, and caveats legible too.
|
|
18
|
+
- Prefer short, coherent text groups over document-length paragraphs. Use a
|
|
19
|
+
diagram, chart, image, or comparison when it carries the explanation; do not
|
|
20
|
+
require a visual on every page or flatten every idea into matching cards.
|
|
21
|
+
- When content exceeds a page, redistribute it into a meaningful sequence
|
|
22
|
+
rather than shrinking everything. Respect an explicit page-count constraint.
|
|
23
|
+
- Keep recurring type, margins, and semantic colors consistent while varying
|
|
24
|
+
composition when the content changes. A title page need not dictate every page.
|
|
25
|
+
|
|
26
|
+
Review the deck at fitted size and in reading order. Check that comparisons use
|
|
27
|
+
the same criteria and scales, and that each page remains understandable without
|
|
28
|
+
presenter notes. Konpeki delivers static pages, not a presentation player.
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import {
|
|
5
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
6
|
|
|
8
7
|
// A copied skill must resolve a known runtime, not follow repo-relative links.
|
|
9
|
-
const version = "0.
|
|
10
|
-
const probeDocument = fileURLToPath(new URL("../assets/blank.
|
|
8
|
+
const version = "0.4.0";
|
|
9
|
+
const probeDocument = fileURLToPath(new URL("../assets/blank.html", import.meta.url));
|
|
11
10
|
function runtime(root) {
|
|
12
11
|
try {
|
|
13
12
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
14
13
|
if (pkg.name !== "konpeki" || pkg.version !== version) return;
|
|
15
14
|
const cli = join(root, pkg.bin.konpeki);
|
|
16
15
|
const sourceCLI = join(root, "bin/konpeki.mjs");
|
|
17
|
-
if (!["
|
|
16
|
+
if (!["AUTHORING.md", "html/README.md", "theme.css", "theme-base.css"]
|
|
18
17
|
.every(path => existsSync(join(root, path)))) return;
|
|
19
18
|
// The development checkout runs TypeScript directly on the pinned Node.
|
|
20
|
-
for (const candidate of
|
|
19
|
+
for (const candidate of [sourceCLI, cli]) {
|
|
21
20
|
if (!existsSync(candidate)) continue;
|
|
22
21
|
const probe = spawnSync(process.execPath, [candidate, "validate", probeDocument], {
|
|
23
22
|
stdio: "ignore",
|
|
@@ -33,35 +32,15 @@ function runtime(root) {
|
|
|
33
32
|
try {
|
|
34
33
|
if (Number(process.versions.node.split(".")[0]) < 24)
|
|
35
34
|
throw new Error("Konpeki needs Node.js 24+. Use your host's approved toolchain setup, then retry.");
|
|
36
|
-
if (process.argv.
|
|
37
|
-
throw new Error("Usage: node ensure-runtime.mjs [--install]");
|
|
35
|
+
if (process.argv.length > 2) throw new Error("Usage: node ensure-runtime.mjs");
|
|
38
36
|
let workspace;
|
|
39
37
|
try {
|
|
40
38
|
workspace = dirname(createRequire(join(process.cwd(), "package.json")).resolve("konpeki/package.json"));
|
|
41
39
|
} catch {}
|
|
42
40
|
const bundled = fileURLToPath(new URL("../../../", import.meta.url));
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const cache = resolve(cacheBase, "konpeki", version);
|
|
47
|
-
const cachedRoot = join(cache, "node_modules", "konpeki");
|
|
48
|
-
let found = (workspace && runtime(workspace)) || runtime(bundled) || runtime(cachedRoot);
|
|
49
|
-
if (!found) {
|
|
50
|
-
if (!process.argv.includes("--install"))
|
|
51
|
-
throw new Error(`Konpeki ${version} is not installed. After the host approves installation, rerun with --install. This uses a user cache and leaves project dependencies unchanged.`);
|
|
52
|
-
mkdirSync(cache, { recursive: true });
|
|
53
|
-
// Explicit local prefix prevents npm from walking up into an ancestor project.
|
|
54
|
-
// Resolve "." from the absolute cwd; no user path enters Windows shell text.
|
|
55
|
-
const result = spawnSync("npm", ["install", "--prefix=.", "--global=false", "--save-exact", "--no-audit", "--no-fund", `konpeki@${version}`], {
|
|
56
|
-
cwd: cache,
|
|
57
|
-
stdio: ["inherit", 2, 2],
|
|
58
|
-
shell: process.platform === "win32",
|
|
59
|
-
});
|
|
60
|
-
if (result.error || result.status !== 0)
|
|
61
|
-
throw new Error("Konpeki installation failed. Check npm/network access and retry; the runtime is not ready.");
|
|
62
|
-
found = runtime(cachedRoot);
|
|
63
|
-
if (!found) throw new Error("The installed Konpeki runtime is incomplete or incompatible.");
|
|
64
|
-
}
|
|
41
|
+
const found = (workspace && runtime(workspace)) || runtime(bundled);
|
|
42
|
+
if (!found)
|
|
43
|
+
throw new Error("Konpeki 0.4.0 is required. Check availability with npm view konpeki@0.4.0 version, then install that exact version in this workspace. If unavailable, use a locally packed tarball or prepared checkout. The 0.3.x runtime is incompatible. See SETUP.md.");
|
|
65
44
|
console.log(JSON.stringify(found));
|
|
66
45
|
} catch (error) {
|
|
67
46
|
console.error(error.message);
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { constants, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
try {
|
|
7
7
|
const [cli, input, ...extra] = process.argv.slice(2);
|
|
8
|
-
if (!cli || !input || extra.length)
|
|
9
|
-
|
|
10
|
-
const
|
|
8
|
+
if (!cli || !input || extra.length) throw new Error("Usage: node prepare-document.mjs <cli> <document.html>");
|
|
9
|
+
if (!/\.html?$/i.test(input)) throw new Error("Use an .html document path.");
|
|
10
|
+
const documentPath = resolve(input);
|
|
11
11
|
function validate(path) {
|
|
12
|
-
// Use the
|
|
12
|
+
// Use the supplied CLI, not internal TypeScript imports from node_modules.
|
|
13
13
|
const result = spawnSync(process.execPath, [cli, "validate", path], {
|
|
14
14
|
stdio: ["ignore", 2, 2],
|
|
15
15
|
});
|
|
@@ -17,23 +17,34 @@ try {
|
|
|
17
17
|
throw new Error("Document validation failed; existing files were not changed.");
|
|
18
18
|
}
|
|
19
19
|
let created = false;
|
|
20
|
-
if (!existsSync(
|
|
21
|
-
|
|
22
|
-
throw new Error("Review data exists without its composition. Restore the document or choose a new path.");
|
|
23
|
-
const template = fileURLToPath(new URL("../assets/blank.json", import.meta.url));
|
|
20
|
+
if (!existsSync(documentPath)) {
|
|
21
|
+
const template = fileURLToPath(new URL("../assets/blank.html", import.meta.url));
|
|
24
22
|
validate(template);
|
|
25
23
|
const blank = readFileSync(template);
|
|
26
|
-
|
|
24
|
+
const root = resolve(dirname(cli), "..");
|
|
25
|
+
const assets = new Map([["theme.css", join(root, "theme.css")], ["theme-base.css", join(root, "theme-base.css")]]);
|
|
26
|
+
// Preflight every asset before writing any: never replace an author's
|
|
27
|
+
// adapted stylesheet in a shared folder.
|
|
28
|
+
for (const [asset, source] of assets) {
|
|
29
|
+
const destination = join(dirname(documentPath), asset);
|
|
30
|
+
if (existsSync(destination) && !readFileSync(destination).equals(readFileSync(source)))
|
|
31
|
+
throw new Error(`Existing asset differs: ${destination}. Use a new directory; existing files were not changed.`);
|
|
32
|
+
}
|
|
33
|
+
mkdirSync(dirname(documentPath), { recursive: true });
|
|
34
|
+
for (const [asset, source] of assets) {
|
|
35
|
+
try { copyFileSync(source, join(dirname(documentPath), asset), constants.COPYFILE_EXCL); }
|
|
36
|
+
catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
37
|
+
}
|
|
27
38
|
try {
|
|
28
|
-
writeFileSync(
|
|
39
|
+
writeFileSync(documentPath, blank, { flag: "wx" });
|
|
29
40
|
created = true;
|
|
30
41
|
} catch (error) {
|
|
31
42
|
// Another initializer may have created it; reopen, never replace it.
|
|
32
43
|
if (error.code !== "EEXIST") throw error;
|
|
33
44
|
}
|
|
34
45
|
}
|
|
35
|
-
validate(
|
|
36
|
-
console.log(JSON.stringify({
|
|
46
|
+
validate(documentPath);
|
|
47
|
+
console.log(JSON.stringify({ documentPath, created }));
|
|
37
48
|
} catch (error) {
|
|
38
49
|
console.error(error.message);
|
|
39
50
|
process.exitCode = 1;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, useState, type ReactNode, type RefObject } from "react";
|
|
2
|
+
import { layoutPages } from "../lib/page-board.ts";
|
|
3
|
+
|
|
4
|
+
export type PageBoardControls = {
|
|
5
|
+
goTo: (id: string, fit?: boolean) => void;
|
|
6
|
+
pan: (x: number, y: number) => void;
|
|
7
|
+
zoom: (factor: number) => void;
|
|
8
|
+
stopZoom: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function PageBoard<Page extends { id: string; name: string; canvas: { width: number; height: number } }>({ draft, children, controls, onActivePage }: {
|
|
12
|
+
draft: { pages: Page[] }; children: (page: Page, index: number) => ReactNode;
|
|
13
|
+
controls: RefObject<PageBoardControls | null>; onActivePage: (id: string) => void;
|
|
14
|
+
}) {
|
|
15
|
+
const root = useRef<HTMLDivElement>(null);
|
|
16
|
+
const content = useRef<HTMLDivElement>(null);
|
|
17
|
+
const [space, setSpace] = useState({ width: 0, height: 0 });
|
|
18
|
+
const marker = useId();
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const viewport = root.current!.closest<HTMLElement>(".workspace")!;
|
|
21
|
+
const observer = new ResizeObserver(() => {
|
|
22
|
+
const help = viewport.querySelector<HTMLElement>(".board-help");
|
|
23
|
+
const footer = help ? help.offsetHeight + parseFloat(getComputedStyle(help).marginTop) : 0;
|
|
24
|
+
const top = root.current!.getBoundingClientRect().top - viewport.getBoundingClientRect().top + viewport.scrollTop;
|
|
25
|
+
const padding = getComputedStyle(viewport);
|
|
26
|
+
const width = viewport.clientWidth - parseFloat(padding.paddingLeft) - parseFloat(padding.paddingRight);
|
|
27
|
+
const height = Math.max(0, viewport.clientHeight - top - parseFloat(padding.paddingBottom) - footer);
|
|
28
|
+
setSpace(current => current.width === width && current.height === height ? current : { width, height });
|
|
29
|
+
});
|
|
30
|
+
observer.observe(root.current!);
|
|
31
|
+
observer.observe(viewport);
|
|
32
|
+
return () => observer.disconnect();
|
|
33
|
+
}, []);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const viewport = root.current!.closest<HTMLElement>(".workspace")!;
|
|
36
|
+
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
37
|
+
let scale = 1, goal = 1, gestureScale: number | undefined;
|
|
38
|
+
let frame = 0, lastTime = 0, x = 0, y = 0, smooth = true;
|
|
39
|
+
let anchorX = 0, anchorY = 0;
|
|
40
|
+
function activePage() {
|
|
41
|
+
const center = viewport.getBoundingClientRect().left + viewport.clientWidth / 2;
|
|
42
|
+
const pages = Array.from(root.current!.querySelectorAll<HTMLElement>(".board-page"));
|
|
43
|
+
const page = pages.reduce<HTMLElement | undefined>((nearest, page) => {
|
|
44
|
+
const rect = page.getBoundingClientRect(), previous = nearest?.getBoundingClientRect();
|
|
45
|
+
return !previous || Math.abs(rect.left + rect.width / 2 - center) < Math.abs(previous.left + previous.width / 2 - center) ? page : nearest;
|
|
46
|
+
}, undefined);
|
|
47
|
+
if (page?.dataset.page) onActivePage(page.dataset.page);
|
|
48
|
+
}
|
|
49
|
+
function stopZoom() {
|
|
50
|
+
cancelAnimationFrame(frame); frame = 0; goal = scale;
|
|
51
|
+
if (content.current) delete content.current.dataset.zooming;
|
|
52
|
+
}
|
|
53
|
+
function animate(time: number) {
|
|
54
|
+
frame = 0;
|
|
55
|
+
const board = content.current;
|
|
56
|
+
if (!board) return;
|
|
57
|
+
const distance = Math.log(goal / scale);
|
|
58
|
+
const fraction = smooth && !reducedMotion.matches ? 1 - Math.exp(-Math.min(time - lastTime, 64) / 40) : 1;
|
|
59
|
+
const next = Math.abs(distance) < .002 ? goal : scale * Math.exp(distance * fraction);
|
|
60
|
+
lastTime = time;
|
|
61
|
+
// Keep view-only zoom out of React. Retain the original anchor across
|
|
62
|
+
// frames so integer scroll rounding cannot accumulate into pointer drift.
|
|
63
|
+
board.style.zoom = String(next);
|
|
64
|
+
const after = board.getBoundingClientRect();
|
|
65
|
+
viewport.scrollLeft += after.left + anchorX * next - x;
|
|
66
|
+
viewport.scrollTop += after.top + anchorY * next - y;
|
|
67
|
+
scale = next;
|
|
68
|
+
activePage();
|
|
69
|
+
if (scale !== goal) frame = requestAnimationFrame(animate);
|
|
70
|
+
else delete board.dataset.zooming;
|
|
71
|
+
}
|
|
72
|
+
function zoomAt(next: number, clientX: number, clientY: number, ease: boolean) {
|
|
73
|
+
if (!content.current) return;
|
|
74
|
+
const bounds = content.current.getBoundingClientRect();
|
|
75
|
+
anchorX = (clientX - bounds.left) / scale;
|
|
76
|
+
anchorY = (clientY - bounds.top) / scale;
|
|
77
|
+
goal = Math.min(4, Math.max(.1, next));
|
|
78
|
+
x = clientX; y = clientY; smooth = ease;
|
|
79
|
+
content.current.dataset.zooming = "true";
|
|
80
|
+
if (!frame) { lastTime = performance.now(); frame = requestAnimationFrame(animate); }
|
|
81
|
+
}
|
|
82
|
+
controls.current = {
|
|
83
|
+
stopZoom,
|
|
84
|
+
goTo(id, fit = false) {
|
|
85
|
+
const page = root.current!.querySelector<HTMLElement>(`.board-page[data-page="${CSS.escape(id)}"]`);
|
|
86
|
+
if (!page || !content.current) return;
|
|
87
|
+
stopZoom();
|
|
88
|
+
if (fit) { scale = goal = 1; content.current.style.zoom = "1"; }
|
|
89
|
+
const rect = page.getBoundingClientRect(), bounds = viewport.getBoundingClientRect();
|
|
90
|
+
viewport.scrollLeft += rect.left + rect.width / 2 - bounds.left - viewport.clientWidth / 2;
|
|
91
|
+
viewport.scrollTop += rect.top + rect.height / 2 - bounds.top - viewport.clientHeight / 2;
|
|
92
|
+
onActivePage(id);
|
|
93
|
+
},
|
|
94
|
+
pan(dx, dy) { stopZoom(); viewport.scrollLeft += dx; viewport.scrollTop += dy; activePage(); },
|
|
95
|
+
zoom(factor) {
|
|
96
|
+
const bounds = viewport.getBoundingClientRect();
|
|
97
|
+
zoomAt(goal * factor, bounds.left + viewport.clientWidth / 2, bounds.top + viewport.clientHeight / 2, true);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
function wheel(event: WheelEvent) {
|
|
101
|
+
// Floating review panels own their scrolling, not the canvas beneath them.
|
|
102
|
+
if ((event.target as Element).closest(".revision-notes,dialog")) return;
|
|
103
|
+
if (!event.ctrlKey && !event.metaKey) {
|
|
104
|
+
// A fitted row has no vertical travel. Let a plain mouse wheel navigate
|
|
105
|
+
// it, while preserving native two-axis panning when zoomed in.
|
|
106
|
+
if (!event.deltaX && viewport.scrollHeight <= viewport.clientHeight + 1 && viewport.scrollWidth > viewport.clientWidth) {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? viewport.clientWidth : 1;
|
|
109
|
+
viewport.scrollLeft += event.deltaY * unit;
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
if (gestureScale !== undefined) return; // Safari can emit both event families.
|
|
115
|
+
const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? viewport.clientHeight : 1;
|
|
116
|
+
zoomAt(goal * Math.exp(-event.deltaY * unit * .004), event.clientX, event.clientY, true);
|
|
117
|
+
}
|
|
118
|
+
function gesture(event: Event) {
|
|
119
|
+
const pinch = event as Event & { scale: number; clientX: number; clientY: number };
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
if (event.type === "gesturestart") gestureScale = goal;
|
|
122
|
+
else if (event.type === "gestureend") gestureScale = undefined;
|
|
123
|
+
else if (gestureScale !== undefined) zoomAt(gestureScale * pinch.scale, pinch.clientX, pinch.clientY, false);
|
|
124
|
+
}
|
|
125
|
+
const observer = new ResizeObserver(activePage);
|
|
126
|
+
observer.observe(root.current!);
|
|
127
|
+
observer.observe(viewport);
|
|
128
|
+
viewport.addEventListener("scroll", activePage);
|
|
129
|
+
viewport.addEventListener("wheel", wheel, { passive: false });
|
|
130
|
+
for (const type of ["gesturestart", "gesturechange", "gestureend"]) viewport.addEventListener(type, gesture, { passive: false });
|
|
131
|
+
return () => {
|
|
132
|
+
cancelAnimationFrame(frame);
|
|
133
|
+
controls.current = null;
|
|
134
|
+
observer.disconnect();
|
|
135
|
+
viewport.removeEventListener("scroll", activePage);
|
|
136
|
+
viewport.removeEventListener("wheel", wheel);
|
|
137
|
+
for (const type of ["gesturestart", "gesturechange", "gestureend"]) viewport.removeEventListener(type, gesture);
|
|
138
|
+
};
|
|
139
|
+
}, [controls, onActivePage]);
|
|
140
|
+
const layout = layoutPages(draft.pages.map(page => page.canvas), space.width, space.height);
|
|
141
|
+
return <div ref={root} className="page-board" style={{ minHeight: space.height }} aria-label="All pages in reading order">
|
|
142
|
+
{space.width > 0 && <div ref={content} className="page-board-content" style={{ width: layout.width, height: layout.height, zoom: 1 }}>
|
|
143
|
+
<svg className="page-order" width={layout.width} height={layout.height} aria-hidden="true">
|
|
144
|
+
<defs><marker id={marker} markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M1 1 7 4 1 7" /></marker></defs>
|
|
145
|
+
{layout.arrows.map((path, index) => <path key={draft.pages[index].id} data-from={draft.pages[index].id} data-to={draft.pages[index + 1].id} d={path} markerEnd={`url(#${marker})`} />)}
|
|
146
|
+
</svg>
|
|
147
|
+
{draft.pages.map((page, index) => {
|
|
148
|
+
const box = layout.pages[index];
|
|
149
|
+
return <article key={page.id} className="board-page" data-page={page.id} aria-label={`Page ${index + 1}: ${page.name}`}
|
|
150
|
+
style={{ left: box.x, top: box.y - 32, width: box.width }}>
|
|
151
|
+
{children(page, index)}
|
|
152
|
+
</article>;
|
|
153
|
+
})}
|
|
154
|
+
</div>}
|
|
155
|
+
</div>;
|
|
156
|
+
}
|