konpeki 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/AGENTS.md +21 -28
  2. package/AUTHORING.md +68 -229
  3. package/CONTRIBUTING.md +10 -23
  4. package/README.md +98 -106
  5. package/SETUP.md +49 -129
  6. package/docs/development.md +43 -256
  7. package/docs/workflow.md +6 -108
  8. package/html/README.md +140 -0
  9. package/html/browser.ts +103 -0
  10. package/html/document.ts +38 -0
  11. package/html/floor.ts +34 -0
  12. package/html/index.html +5 -0
  13. package/html/inspect.ts +313 -0
  14. package/html/preview.css +62 -0
  15. package/html/preview.tsx +463 -0
  16. package/html/review-hints.ts +63 -0
  17. package/html/server.ts +99 -0
  18. package/html/source.ts +104 -0
  19. package/html/starter.ts +6 -0
  20. package/html/theme-authoring.md +176 -0
  21. package/html/theme.md +61 -0
  22. package/index.html +9 -9
  23. package/package.json +21 -37
  24. package/plugin.json +1 -1
  25. package/public/og.png +0 -0
  26. package/runtime/browser-B-29TH1a.mjs +595 -0
  27. package/runtime/floor-Cmk7G3pU.mjs +41 -0
  28. package/runtime/konpeki.mjs +90 -1410
  29. package/runtime/server-BAqC_5WD.mjs +2 -0
  30. package/runtime/server-DfRpfcY9.mjs +205 -0
  31. package/runtime/source-B_Ui9tMp.mjs +141 -0
  32. package/runtime/source-CZQq9GUO.mjs +2 -0
  33. package/skills/konpeki/SKILL.md +88 -172
  34. package/skills/konpeki/assets/blank.html +17 -0
  35. package/skills/konpeki/floor.md +76 -0
  36. package/skills/konpeki/references/cover.md +27 -0
  37. package/skills/konpeki/references/long-document.md +40 -0
  38. package/skills/konpeki/references/one-pager.md +27 -0
  39. package/skills/konpeki/references/patterns.md +118 -0
  40. package/skills/konpeki/references/resume.md +28 -0
  41. package/skills/konpeki/references/slides.md +28 -0
  42. package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
  43. package/skills/konpeki/scripts/prepare-document.mjs +25 -14
  44. package/src/components/PageBoard.tsx +156 -0
  45. package/src/lib/alignment.ts +21 -0
  46. package/src/lib/page-board.ts +25 -0
  47. package/src/lib/review-position.ts +19 -0
  48. package/src/styles/base.css +1 -10
  49. package/src/styles/feedback.css +2 -1
  50. package/src/styles/shell.css +94 -354
  51. package/theme-base.css +90 -0
  52. package/theme.css +56 -0
  53. package/vite.config.ts +2 -5
  54. package/composition/README.md +0 -156
  55. package/composition/compile.ts +0 -227
  56. package/composition/document.ts +0 -600
  57. package/composition/schema.json +0 -3001
  58. package/composition/schema.ts +0 -437
  59. package/composition/theme-tokens.ts +0 -16
  60. package/composition/types.ts +0 -269
  61. package/composition/validate.ts +0 -226
  62. package/composition/vector.ts +0 -143
  63. package/composition/visualizations.ts +0 -319
  64. package/design/README.md +0 -17
  65. package/design/palettes/README.md +0 -14
  66. package/design/palettes/base.ts +0 -14
  67. package/design/palettes/candidates.ts +0 -19
  68. package/design/palettes/index.ts +0 -78
  69. package/design/review/color-theme.md +0 -44
  70. package/design/review/layout.md +0 -16
  71. package/design/review/text.md +0 -18
  72. package/design/review/typography.md +0 -15
  73. package/design/review/visuals.md +0 -31
  74. package/design/semantic-patterns.md +0 -43
  75. package/design/themes/README.md +0 -40
  76. package/design/themes/index.ts +0 -24
  77. package/design/visual-languages/technical-product.md +0 -17
  78. package/design/visual-review.md +0 -88
  79. package/lib/assets.d.ts +0 -8
  80. package/lib/charts.ts +0 -18
  81. package/lib/contrast.ts +0 -16
  82. package/lib/layouts.ts +0 -50
  83. package/lib/slide.tsx +0 -42
  84. package/lib/taste.ts +0 -17
  85. package/lib/text.tsx +0 -89
  86. package/lib/typeface.ts +0 -44
  87. package/scripts/migrate-react-page.ts +0 -120
  88. package/skills/konpeki/assets/blank.json +0 -23
  89. package/slides/README.md +0 -56
  90. package/slides/architecture/PROMPT.md +0 -31
  91. package/slides/architecture/index.tsx +0 -102
  92. package/slides/article-brief/PROMPT.md +0 -35
  93. package/slides/article-brief/index.tsx +0 -71
  94. package/slides/bar-chart/PROMPT.md +0 -39
  95. package/slides/bar-chart/index.tsx +0 -97
  96. package/slides/comparison/PROMPT.md +0 -29
  97. package/slides/comparison/index.tsx +0 -95
  98. package/slides/decision-memo/PROMPT.md +0 -34
  99. package/slides/decision-memo/index.tsx +0 -85
  100. package/slides/delivery-plan/PROMPT.md +0 -45
  101. package/slides/delivery-plan/index.tsx +0 -105
  102. package/slides/drawing-references.md +0 -32
  103. package/slides/experiment/PROMPT.md +0 -44
  104. package/slides/experiment/index.tsx +0 -127
  105. package/slides/github-cover/PROMPT.md +0 -21
  106. package/slides/github-cover/README.md +0 -21
  107. package/slides/github-cover/author.ts +0 -67
  108. package/slides/github-cover/composition.json +0 -595
  109. package/slides/incident-workflow/PROMPT.md +0 -57
  110. package/slides/incident-workflow/index.tsx +0 -78
  111. package/slides/introducing-konpeki/PROMPT.md +0 -30
  112. package/slides/introducing-konpeki/README.md +0 -44
  113. package/slides/introducing-konpeki/SOURCE.md +0 -19
  114. package/slides/introducing-konpeki/author.ts +0 -165
  115. package/slides/introducing-konpeki/composition.json +0 -3270
  116. package/slides/line-chart/PROMPT.md +0 -40
  117. package/slides/line-chart/index.tsx +0 -72
  118. package/slides/migration/PROMPT.md +0 -38
  119. package/slides/migration/index.tsx +0 -89
  120. package/slides/og-images/PROMPT.md +0 -21
  121. package/slides/og-images/index.tsx +0 -76
  122. package/slides/product-introduction/PROMPT.md +0 -24
  123. package/slides/product-introduction/index.tsx +0 -105
  124. package/slides/research-brief/PROMPT.md +0 -40
  125. package/slides/research-brief/index.tsx +0 -104
  126. package/slides/results-explanation/PROMPT.md +0 -32
  127. package/slides/results-explanation/index.tsx +0 -96
  128. package/slides/retrospective/PROMPT.md +0 -43
  129. package/slides/retrospective/index.tsx +0 -105
  130. package/slides/sankey/PROMPT.md +0 -11
  131. package/slides/sankey/index.tsx +0 -93
  132. package/slides/teaching/PROMPT.md +0 -45
  133. package/slides/teaching/index.tsx +0 -124
  134. package/slides/vertical-bar-charts/PROMPT.md +0 -13
  135. package/slides/vertical-bar-charts/index.tsx +0 -97
  136. package/src/app/App.tsx +0 -845
  137. package/src/components/BuildOrb.tsx +0 -40
  138. package/src/components/Canvas.tsx +0 -1191
  139. package/src/components/DiagramTypeIcon.tsx +0 -78
  140. package/src/components/InspectorPanel.tsx +0 -757
  141. package/src/components/LeftPanel.tsx +0 -120
  142. package/src/components/PageSizePicker.tsx +0 -30
  143. package/src/components/Presentation.tsx +0 -105
  144. package/src/components/RevisionNotes.tsx +0 -55
  145. package/src/components/RightPanel.tsx +0 -233
  146. package/src/components/VectorOverflowWarning.tsx +0 -73
  147. package/src/components/WorkspaceChrome.tsx +0 -316
  148. package/src/components/ui.tsx +0 -147
  149. package/src/lib/examples/react-page-migration.json +0 -1295
  150. package/src/lib/examples.ts +0 -42
  151. package/src/lib/export-png.ts +0 -104
  152. package/src/lib/file-session.ts +0 -87
  153. package/src/lib/history.ts +0 -53
  154. package/src/lib/model.ts +0 -188
  155. package/src/lib/page-size.ts +0 -24
  156. package/src/lib/presentation.ts +0 -17
  157. package/src/lib/review.ts +0 -26
  158. package/src/lib/storage.ts +0 -71
  159. package/src/lib/theme.ts +0 -25
  160. package/src/lib/use-file-session.ts +0 -249
  161. package/src/main.tsx +0 -29
  162. package/src/styles/canvas.css +0 -299
  163. package/src/styles/chrome.css +0 -475
  164. package/src/styles/component-previews.css +0 -386
  165. package/src/styles/left-panel.css +0 -187
  166. package/src/styles/presentation.css +0 -72
  167. package/src/styles/right-panel.css +0 -1298
@@ -1,120 +0,0 @@
1
- import type { CSSProperties, ReactNode } from "react";
2
- import type {
3
- CompositionSlide,
4
- ThemeMode,
5
- } from "../../composition/types.ts";
6
- import type { Draft } from "../lib/model.ts";
7
-
8
- export type LeftPanelView = "pages" | "notes";
9
-
10
- function ThumbnailCanvas({
11
- slide,
12
- themeMode,
13
- pageIndex,
14
- pageTotal,
15
- }: {
16
- slide: CompositionSlide;
17
- themeMode: ThemeMode;
18
- pageIndex: number;
19
- pageTotal: number;
20
- }) {
21
- return (
22
- <span className={`slide-thumbnail-canvas ${themeMode}`} style={{ aspectRatio: slide.canvas.width / slide.canvas.height }}>
23
- {slide.components.map((component) => {
24
- const rect = component.preferredRect;
25
- const style = {
26
- left: `${rect.x * 100 / slide.canvas.width}%`,
27
- top: `${rect.y * 100 / slide.canvas.height}%`,
28
- width: `${rect.width * 100 / slide.canvas.width}%`,
29
- height: `${rect.height * 100 / slide.canvas.height}%`,
30
- } as CSSProperties;
31
- return (
32
- <span
33
- key={component.id}
34
- className={`thumbnail-component kind-${component.kind} ${component.kind === "text-block" ? `role-${component.appearance.role}` : ""}`}
35
- style={style}
36
- />
37
- );
38
- })}
39
- {slide.pageNumber?.style !== "none" && (
40
- <span className="thumbnail-page-number">
41
- {String(pageIndex).padStart(2, "0")}
42
- {slide.pageNumber?.style === "01/02"
43
- ? `/${String(pageTotal).padStart(2, "0")}`
44
- : ""}
45
- </span>
46
- )}
47
- </span>
48
- );
49
- }
50
-
51
- export function LeftPanel({
52
- draft,
53
- activeSlideId,
54
- collapsed,
55
- view,
56
- onView,
57
- revisionNotes,
58
- onSelectSlide,
59
- onAddSlide,
60
- onRemoveSlide,
61
- }: {
62
- draft: Draft;
63
- activeSlideId: string;
64
- collapsed: boolean;
65
- view: LeftPanelView;
66
- onView: (view: LeftPanelView) => void;
67
- revisionNotes?: ReactNode;
68
- onSelectSlide: (id: string) => void;
69
- onAddSlide: () => void;
70
- onRemoveSlide: (id: string) => void;
71
- }) {
72
- return (
73
- <aside className={`left-panel${collapsed ? " collapsed" : ""}`} aria-label="Workspace panel" inert={collapsed}>
74
- <div className="panel-toolbar">
75
- {revisionNotes ? <div className="panel-tabs" aria-label="Left panel view">
76
- {(["pages", "notes"] as const).map(tab => <button key={tab} type="button"
77
- aria-pressed={view === tab} className={view === tab ? "active" : ""}
78
- onClick={() => onView(tab)}>{tab === "pages" ? "Pages" : "Notes"}</button>)}
79
- </div> : <span className="panel-heading">Pages</span>}
80
- </div>
81
- <div className="slide-list" hidden={!!revisionNotes && view !== "pages"}>
82
- {draft.slides.map((item, index) => (
83
- <div key={item.id} className="slide-thumbnail-item">
84
- <button
85
- type="button"
86
- className={`slide-thumbnail ${item.id === activeSlideId ? "selected" : ""}`}
87
- aria-current={item.id === activeSlideId ? "page" : undefined}
88
- onClick={() => onSelectSlide(item.id)}
89
- >
90
- <ThumbnailCanvas
91
- slide={item}
92
- themeMode={draft.theme?.mode ?? "paper"}
93
- pageIndex={index + 1}
94
- pageTotal={draft.slides.length}
95
- />
96
- <span>{item.name}</span>
97
- </button>
98
- <button
99
- type="button"
100
- className="remove-slide"
101
- aria-label={`Remove ${item.name}`}
102
- onClick={() => onRemoveSlide(item.id)}
103
- >
104
- <span className="panel-close-icon" aria-hidden="true" />
105
- </button>
106
- </div>
107
- ))}
108
- <button
109
- type="button"
110
- className="add-slide"
111
- aria-label="Add page"
112
- onClick={onAddSlide}
113
- >
114
- <span aria-hidden="true">+</span>
115
- </button>
116
- </div>
117
- {revisionNotes && <div className="left-notes" hidden={view !== "notes"}>{revisionNotes}</div>}
118
- </aside>
119
- );
120
- }
@@ -1,30 +0,0 @@
1
- import type { CompositionSlide } from "../../composition/types.ts";
2
- import { pagePresets, pageSizeIssue, resizePage } from "../lib/page-size.ts";
3
-
4
- export function PageSizePicker({ slide, onChange }: { slide: CompositionSlide; onChange: (slide: CompositionSlide) => void }) {
5
- const preset = pagePresets.find((item) => item.width === slide.canvas.width && item.height === slide.canvas.height);
6
- function apply(w: number, h: number, destination: CompositionSlide["intendedViewingSize"] = "custom") {
7
- const size = { width: w, height: h };
8
- if (!pageSizeIssue(slide, size)) {
9
- onChange(resizePage(slide, size, destination));
10
- }
11
- }
12
- return <fieldset>
13
- <legend>Size</legend>
14
- <label className="field">
15
- <span className="sr-only">Size</span>
16
- <select name="page-format" value={preset?.name ?? "Imported"} onChange={(event) => {
17
- const next = pagePresets.find((item) => item.name === event.target.value);
18
- if (next) apply(next.width, next.height, next.destination);
19
- }}>
20
- {!preset && <option value="Imported" disabled>{slide.canvas.width} × {slide.canvas.height} · Imported size</option>}
21
- {pagePresets.map((item) => {
22
- const unavailable = Boolean(pageSizeIssue(slide, item));
23
- return <option key={item.name} value={item.name} disabled={unavailable}>
24
- {item.name} · {item.width} × {item.height}{unavailable ? " · unavailable" : ""}
25
- </option>;
26
- })}
27
- </select>
28
- </label>
29
- </fieldset>;
30
- }
@@ -1,105 +0,0 @@
1
- import { useEffect, useRef, useState } from "react";
2
- import type { Draft } from "../lib/model.ts";
3
- import { presentationAction } from "../lib/presentation.ts";
4
- import { Canvas } from "./Canvas.tsx";
5
-
6
- const noop = () => {};
7
-
8
- export function Presentation({
9
- draft,
10
- initialSlideId,
11
- onExit,
12
- }: {
13
- draft: Draft;
14
- initialSlideId: string;
15
- onExit: () => void;
16
- }) {
17
- const [index, setIndex] = useState(() =>
18
- Math.max(0, draft.slides.findIndex((slide) => slide.id === initialSlideId)),
19
- );
20
- const overlay = useRef<HTMLDivElement>(null);
21
- const slide = draft.slides[index];
22
-
23
- useEffect(() => {
24
- overlay.current?.focus();
25
- }, []);
26
-
27
- useEffect(() => {
28
- function keydown(event: KeyboardEvent) {
29
- const interactiveTarget =
30
- event.target instanceof Element &&
31
- Boolean(event.target.closest("button,a,input,textarea,select,[contenteditable=true]"));
32
- const action = presentationAction(
33
- event.key,
34
- index,
35
- draft.slides.length,
36
- interactiveTarget,
37
- );
38
- if (action === undefined) return;
39
- event.preventDefault();
40
- if (action === "exit") onExit();
41
- else setIndex(action);
42
- }
43
- window.addEventListener("keydown", keydown);
44
- return () => window.removeEventListener("keydown", keydown);
45
- }, [draft.slides.length, index, onExit]);
46
-
47
- return (
48
- <div
49
- ref={overlay}
50
- className="presentation"
51
- role="dialog"
52
- aria-modal="true"
53
- aria-label="Presentation mode"
54
- tabIndex={-1}
55
- >
56
- <Canvas
57
- key={slide.id}
58
- mode="present"
59
- draft={draft}
60
- activeSlideId={slide.id}
61
- onSelect={noop}
62
- onSlideName={noop}
63
- onComponent={noop}
64
- onEditEnd={noop}
65
- onAdd={noop}
66
- onNotice={noop}
67
- />
68
- <nav className="presentation-controls" aria-label="Presentation controls">
69
- <button
70
- type="button"
71
- disabled={index === 0}
72
- aria-label="Previous page"
73
- onClick={() => setIndex((value) => Math.max(0, value - 1))}
74
- >
75
- ←
76
- </button>
77
- <span aria-live="polite">
78
- {index + 1} / {draft.slides.length}
79
- </span>
80
- <button
81
- type="button"
82
- disabled={index === draft.slides.length - 1}
83
- aria-label="Next page"
84
- onClick={() =>
85
- setIndex((value) => Math.min(draft.slides.length - 1, value + 1))
86
- }
87
- >
88
- →
89
- </button>
90
- <button
91
- type="button"
92
- onClick={() => {
93
- if (document.fullscreenElement) void document.exitFullscreen();
94
- else void overlay.current?.requestFullscreen();
95
- }}
96
- >
97
- Full screen
98
- </button>
99
- <button type="button" onClick={onExit}>
100
- Exit
101
- </button>
102
- </nav>
103
- </div>
104
- );
105
- }
@@ -1,55 +0,0 @@
1
- import { useState } from "react";
2
- import type { CompositionDocument } from "../../composition/types.ts";
3
- import { componentInstanceLabel } from "../lib/model.ts";
4
- import { activeRequest, type ReviewState, type ReviewTarget } from "../lib/review.ts";
5
-
6
- export function RevisionNotes({ document, target, review, disabled, onAdd, onRemove, onSelect, onNotice }: {
7
- document: CompositionDocument;
8
- target: ReviewTarget;
9
- review: ReviewState;
10
- disabled: boolean;
11
- onAdd: (target: ReviewTarget, text: string) => Promise<void>;
12
- onRemove: (id: string) => Promise<void>;
13
- onSelect: (target: ReviewTarget) => void;
14
- onNotice: (message: string) => void;
15
- }) {
16
- const [drafts, setDrafts] = useState<Record<string, string>>({});
17
- const [saving, setSaving] = useState(false);
18
- const key = JSON.stringify(target);
19
- const text = drafts[key] ?? "";
20
- function label(value: ReviewTarget) {
21
- const slide = document.slides.find(s => s.id === value.slideId);
22
- if (!slide) return "Deleted page";
23
- const component = slide.components.find(c => c.id === value.componentId);
24
- if (value.componentId && !component) return `${slide.name} · Deleted component`;
25
- const name = component ? componentInstanceLabel(slide.components, component.id) : "Page";
26
- const element = value.elementId && component?.customVisual?.format === "vector"
27
- ? component.customVisual.elements.find(e => e.id === value.elementId) : undefined;
28
- return `${slide.name} · ${name}${value.elementId ? ` · ${element ? value.elementId : "Deleted element"}` : ""}`;
29
- }
30
- async function run(action: () => Promise<void>) {
31
- setSaving(true);
32
- try { await action(); onNotice(""); }
33
- catch (error) { onNotice(error instanceof Error ? error.message : "Could not save revision note"); }
34
- finally { setSaving(false); }
35
- }
36
- const pending = review.notes.filter(n => !n.resolved);
37
- return <section className="revision-notes" aria-label="Revision notes">
38
- <form onSubmit={event => { event.preventDefault(); void run(async () => {
39
- await onAdd(target, text);
40
- setDrafts(current => ({ ...current, [key]: current[key] === text ? "" : current[key] }));
41
- }); }}>
42
- <label htmlFor="revision-note">Request a change</label>
43
- <p className="revision-note-scope">{label(target)}</p>
44
- <textarea id="revision-note" value={text} maxLength={4000} rows={3} placeholder="Describe what to change…" disabled={disabled || saving}
45
- onChange={event => setDrafts(current => ({ ...current, [key]: event.target.value }))} />
46
- <div className="revision-note-submit"><button type="submit" disabled={disabled || saving || !text.trim()}>{saving ? "Saving…" : "Add note"}</button></div>
47
- </form>
48
- <p className="revision-note-count">{pending.length} pending</p>
49
- {pending.length > 0 && <ol className="revision-note-list">{pending.map((note, i) => <li key={note.id} id={`note-${note.id}`}>
50
- <button type="button" className="revision-note-target" onClick={() => onSelect(note)}><span>{i + 1}</span>{label(note)}</button>
51
- <p>{note.text}</p>
52
- <button type="button" className="revision-note-remove" disabled={disabled || saving || activeRequest(review.request)} aria-label={`Remove note ${i + 1}`} onClick={() => { void run(() => onRemove(note.id)); }}>Remove</button>
53
- </li>)}</ol>}
54
- </section>;
55
- }
@@ -1,233 +0,0 @@
1
- import { useState } from "react";
2
- import type {
3
- CompositionComponent,
4
- CompositionSlide,
5
- } from "../../composition/types.ts";
6
- import { componentInstanceLabel, type Draft } from "../lib/model.ts";
7
- import { InspectorPanel } from "./InspectorPanel.tsx";
8
- import { VectorOverflowWarning } from "./VectorOverflowWarning.tsx";
9
-
10
- export type RightPanelView = "settings" | "layers";
11
-
12
- function LayersPanel({
13
- slide,
14
- selectedComponentId,
15
- onSelectComponent,
16
- onReorderPaintOrder,
17
- }: {
18
- slide: CompositionSlide;
19
- selectedComponentId?: string;
20
- onSelectComponent: (id: string) => void;
21
- onReorderPaintOrder: (ids: string[]) => void;
22
- }) {
23
- const [dragging, setDragging] = useState<string>();
24
- const [dropIndex, setDropIndex] = useState<number>();
25
- const layers = [...slide.paintOrder].reverse();
26
- function moveLayer(id: string, toIndex: number) {
27
- const next = layers.filter((item) => item !== id);
28
- next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, id);
29
- onReorderPaintOrder(next.reverse());
30
- }
31
- return (
32
- <section className="layers-panel" aria-labelledby="layers-heading">
33
- <div className="layers-heading-row">
34
- <h2 id="layers-heading">Components & layers</h2>
35
- <span>Front to back</span>
36
- </div>
37
- {layers.length ? (
38
- <div className="layer-list"
39
- onDragOver={(event) => {
40
- if (!dragging) return;
41
- event.preventDefault();
42
- const rows = [...event.currentTarget.querySelectorAll<HTMLElement>(".layer-row")];
43
- const next = rows.findIndex((row) => {
44
- const rect = row.getBoundingClientRect();
45
- return event.clientY < rect.top + rect.height / 2;
46
- });
47
- const boundary = next < 0 ? layers.length : next;
48
- const sourceIndex = layers.indexOf(dragging);
49
- const unchanged = boundary === sourceIndex || boundary === sourceIndex + 1;
50
- event.dataTransfer.dropEffect = unchanged ? "none" : "move";
51
- setDropIndex(unchanged ? undefined : boundary);
52
- }}
53
- onDragLeave={(event) => {
54
- if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
55
- setDropIndex(undefined);
56
- }
57
- }}
58
- onDrop={(event) => {
59
- if (!dragging) return;
60
- event.preventDefault();
61
- if (dropIndex !== undefined) {
62
- moveLayer(dragging, dropIndex > layers.indexOf(dragging) ? dropIndex - 1 : dropIndex);
63
- }
64
- setDragging(undefined);
65
- setDropIndex(undefined);
66
- }}
67
- >
68
- {layers.map((id, index) => {
69
- const component = slide.components.find((item) => item.id === id)!;
70
- const groups = slide.groups
71
- .filter((group) => group.childIds.includes(id))
72
- .map((group) => group.label?.trim() || group.id);
73
- const label = componentInstanceLabel(slide.components, id);
74
- return (
75
- <div
76
- key={id}
77
- data-layer={id}
78
- data-drop-edge={dropIndex === index ? "before" : dropIndex === layers.length && index === layers.length - 1 ? "after" : undefined}
79
- className={`layer-row ${selectedComponentId === id ? "selected" : ""} ${dragging === id ? "dragging" : ""}`}
80
- draggable
81
- onDragStart={(event) => {
82
- setDragging(id);
83
- setDropIndex(undefined);
84
- event.dataTransfer.effectAllowed = "move";
85
- event.dataTransfer.setData("text/plain", id);
86
- }}
87
- onDragEnd={() => {
88
- setDragging(undefined);
89
- setDropIndex(undefined);
90
- }}
91
- >
92
- <span className="layer-drag" aria-hidden="true">⠿</span>
93
- <button
94
- type="button"
95
- className="layer-select"
96
- onClick={() => onSelectComponent(id)}
97
- >
98
- <strong>{label}</strong>
99
- <span>{groups.length ? groups.join(", ") : "Page root"}</span>
100
- </button>
101
- <span className="layer-actions">
102
- <button
103
- type="button"
104
- aria-label={`Move ${label} forward`}
105
- disabled={index === 0}
106
- onClick={() => moveLayer(id, index - 1)}
107
- >↑</button>
108
- <button
109
- type="button"
110
- aria-label={`Move ${label} backward`}
111
- disabled={index === layers.length - 1}
112
- onClick={() => moveLayer(id, index + 1)}
113
- >↓</button>
114
- </span>
115
- </div>
116
- );
117
- })}
118
- </div>
119
- ) : (
120
- <p className="layers-empty">No components yet.</p>
121
- )}
122
- </section>
123
- );
124
- }
125
-
126
- export function RightPanel({
127
- view,
128
- collapsed,
129
- component,
130
- componentLabel,
131
- selectedComponentId,
132
- selectedVectorElementId,
133
- vectorEditRequest,
134
- slide,
135
- draft,
136
- onView,
137
- onCollapsedChange,
138
- onSelectSlide,
139
- onSelectComponent,
140
- onSelectVectorElement,
141
- onSelectOverflow,
142
- onReorderPaintOrder,
143
- onComponent,
144
- onSlide,
145
- onDraft,
146
- onEditEnd,
147
- }: {
148
- view: RightPanelView;
149
- collapsed: boolean;
150
- component?: CompositionComponent;
151
- componentLabel?: string;
152
- selectedComponentId?: string;
153
- selectedVectorElementId?: string;
154
- vectorEditRequest?: number;
155
- slide: CompositionSlide;
156
- draft: Draft;
157
- onView: (view: RightPanelView) => void;
158
- onCollapsedChange: (collapsed: boolean) => void;
159
- onSelectSlide: () => void;
160
- onSelectComponent: (id: string) => void;
161
- onSelectVectorElement: (id?: string) => void;
162
- onSelectOverflow: (componentId: string, elementId?: string) => void;
163
- onReorderPaintOrder: (ids: string[]) => void;
164
- onComponent: (component: CompositionComponent, mergeKey?: string) => void;
165
- onSlide: (slide: CompositionSlide, mergeKey?: string) => void;
166
- onDraft: (patch: Partial<Draft>, mergeKey?: string) => void;
167
- onEditEnd: () => void;
168
- }) {
169
- return (
170
- <aside className={`right-panel${collapsed ? " collapsed" : ""}`} aria-label="Inspector panel">
171
- <div className="panel-toolbar">
172
- <div className="panel-tabs" aria-label="Right panel view" inert={collapsed}>
173
- {(["settings", "layers"] as const).map((tab) => (
174
- <button
175
- key={tab}
176
- type="button"
177
- aria-pressed={view === tab}
178
- className={view === tab ? "active" : ""}
179
- onClick={() => onView(tab)}
180
- >
181
- {tab === "settings" ? "Settings" : "Layers"}
182
- </button>
183
- ))}
184
- </div>
185
- <div className="overflow-control" inert={collapsed}>
186
- <VectorOverflowWarning slide={slide} theme={draft.theme} onSelect={onSelectOverflow} />
187
- </div>
188
- <button
189
- type="button"
190
- className="panel-toggle"
191
- aria-label={collapsed ? "Expand right panel" : "Collapse right panel"}
192
- aria-expanded={!collapsed}
193
- onClick={() => onCollapsedChange(!collapsed)}
194
- >
195
- <svg
196
- className="panel-collapse-icon"
197
- viewBox="0 0 24 24"
198
- aria-hidden="true"
199
- >
200
- <rect x="3" y="4" width="18" height="16" rx="2" />
201
- <path d={collapsed ? "M15 4v16M11 9l-3 3 3 3" : "M15 4v16M8 9l3 3-3 3"} />
202
- </svg>
203
- </button>
204
- </div>
205
- <div className="right-panel-body" inert={collapsed}>
206
- {view === "settings" ? (
207
- <InspectorPanel
208
- key={component?.id ?? slide.id}
209
- component={component}
210
- componentLabel={componentLabel}
211
- slide={slide}
212
- draft={draft}
213
- selectedVectorElementId={selectedVectorElementId}
214
- vectorEditRequest={vectorEditRequest}
215
- onSelectSlide={onSelectSlide}
216
- onSelectVectorElement={onSelectVectorElement}
217
- onComponent={onComponent}
218
- onSlide={onSlide}
219
- onDraft={onDraft}
220
- onEditEnd={onEditEnd}
221
- />
222
- ) : (
223
- <LayersPanel
224
- slide={slide}
225
- selectedComponentId={selectedComponentId}
226
- onSelectComponent={onSelectComponent}
227
- onReorderPaintOrder={onReorderPaintOrder}
228
- />
229
- )}
230
- </div>
231
- </aside>
232
- );
233
- }
@@ -1,73 +0,0 @@
1
- import { useEffect, useId, useRef, useState } from "react";
2
- import type { CompositionSlide, CompositionDocument } from "../../composition/types.ts";
3
- import { componentInstanceLabel } from "../lib/model.ts";
4
-
5
- /** Measure rendered text, including nested SVG transforms, after fonts settle. */
6
- export function VectorOverflowWarning({ slide, theme, onSelect }: {
7
- slide: CompositionSlide;
8
- theme: CompositionDocument["theme"];
9
- onSelect: (componentId: string, elementId?: string) => void;
10
- }) {
11
- const [issues, setIssues] = useState<{ componentId: string; elementId?: string; text: string }[]>([]);
12
- const id = useId();
13
- const popover = useRef<HTMLDivElement>(null);
14
- useEffect(() => { popover.current?.hidePopover(); }, [slide.id]);
15
- useEffect(() => {
16
- let cancelled = false;
17
- let frame = 0;
18
- function measure() {
19
- frame = requestAnimationFrame(() => {
20
- if (cancelled) return;
21
- const overflow: typeof issues = [];
22
- function add(text: Element) {
23
- const componentId = text.closest<HTMLElement>("[data-component]")?.dataset.component;
24
- if (componentId) overflow.push({ componentId, elementId: text.getAttribute("data-vector-element") ?? undefined, text: text.textContent?.trim().slice(0, 80) ?? "Text" });
25
- }
26
- document.querySelectorAll<HTMLElement>(".stage .text-block-content").forEach((text) => {
27
- const surface = text.parentElement;
28
- if (surface && (text.scrollHeight > surface.clientHeight + 1 || text.scrollWidth > surface.clientWidth + 1)) add(text);
29
- });
30
- document.querySelectorAll<SVGSVGElement>(".stage .custom-vector-art").forEach((svg) => {
31
- const root = svg.getScreenCTM();
32
- if (!root) return;
33
- const view = svg.viewBox.baseVal;
34
- svg.querySelectorAll<SVGTextElement>("text").forEach((text) => {
35
- const matrix = text.getScreenCTM();
36
- if (!matrix) return;
37
- const box = text.getBBox();
38
- const transform = root.inverse().multiply(matrix);
39
- const corners = [[box.x, box.y], [box.x + box.width, box.y],
40
- [box.x, box.y + box.height], [box.x + box.width, box.y + box.height]];
41
- if (corners.some(([x, y]) => {
42
- const p = new DOMPoint(x, y).matrixTransform(transform);
43
- return p.x < view.x - 1 || p.y < view.y - 1 || p.x > view.x + view.width + 1 || p.y > view.y + view.height + 1;
44
- })) add(text);
45
- });
46
- });
47
- setIssues(overflow);
48
- if (!overflow.length) popover.current?.hidePopover();
49
- });
50
- }
51
- void document.fonts.ready.then(measure);
52
- document.fonts.addEventListener("loadingdone", measure);
53
- return () => { cancelled = true; cancelAnimationFrame(frame); document.fonts.removeEventListener("loadingdone", measure); };
54
- }, [slide, theme]);
55
- return <>
56
- <span className="sr-only" role="status">{issues.length ? `${issues.length} text overflow ${issues.length === 1 ? "warning" : "warnings"} on this page` : ""}</span>
57
- <button type="button" className="overflow-trigger" popoverTarget={id} disabled={!issues.length}
58
- aria-label={`${issues.length} text overflow warnings`} title="Text overflow warnings">
59
- <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 3 18 17H2ZM10 8v4m0 2v.5" /></svg>
60
- <span>{issues.length}</span>
61
- </button>
62
- <div id={id} ref={popover} popover="auto" className="overflow-popover">
63
- <strong>Text outside its frame</strong>
64
- <p>Select an item to revise its text or size. Nothing was resized automatically.</p>
65
- <ul>{issues.map(issue => <li key={`${issue.componentId}:${issue.elementId ?? "native"}`}>
66
- <button type="button" onClick={() => { popover.current?.hidePopover(); onSelect(issue.componentId, issue.elementId); }}>
67
- <strong>{componentInstanceLabel(slide.components, issue.componentId)}{issue.elementId ? ` · ${issue.elementId}` : ""}</strong>
68
- <span>{issue.text}</span>
69
- </button>
70
- </li>)}</ul>
71
- </div>
72
- </>;
73
- }