konpeki 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/AGENTS.md +21 -28
  2. package/AUTHORING.md +68 -229
  3. package/CONTRIBUTING.md +10 -23
  4. package/README.md +96 -87
  5. package/SETUP.md +50 -114
  6. package/docs/development.md +43 -248
  7. package/docs/workflow.md +6 -108
  8. package/html/README.md +140 -0
  9. package/html/browser.ts +103 -0
  10. package/html/document.ts +38 -0
  11. package/html/floor.ts +34 -0
  12. package/html/index.html +5 -0
  13. package/html/inspect.ts +313 -0
  14. package/html/preview.css +62 -0
  15. package/html/preview.tsx +463 -0
  16. package/html/review-hints.ts +63 -0
  17. package/html/server.ts +99 -0
  18. package/html/source.ts +104 -0
  19. package/html/starter.ts +6 -0
  20. package/html/theme-authoring.md +176 -0
  21. package/html/theme.md +61 -0
  22. package/index.html +9 -9
  23. package/package.json +21 -35
  24. package/plugin.json +1 -1
  25. package/public/og.png +0 -0
  26. package/runtime/browser-B-29TH1a.mjs +595 -0
  27. package/runtime/floor-Cmk7G3pU.mjs +41 -0
  28. package/runtime/konpeki.mjs +90 -1410
  29. package/runtime/server-BAqC_5WD.mjs +2 -0
  30. package/runtime/server-DfRpfcY9.mjs +205 -0
  31. package/runtime/source-B_Ui9tMp.mjs +141 -0
  32. package/runtime/source-CZQq9GUO.mjs +2 -0
  33. package/skills/konpeki/SKILL.md +88 -172
  34. package/skills/konpeki/assets/blank.html +17 -0
  35. package/skills/konpeki/floor.md +76 -0
  36. package/skills/konpeki/references/cover.md +27 -0
  37. package/skills/konpeki/references/long-document.md +40 -0
  38. package/skills/konpeki/references/one-pager.md +27 -0
  39. package/skills/konpeki/references/patterns.md +118 -0
  40. package/skills/konpeki/references/resume.md +28 -0
  41. package/skills/konpeki/references/slides.md +28 -0
  42. package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
  43. package/skills/konpeki/scripts/prepare-document.mjs +25 -14
  44. package/src/components/PageBoard.tsx +156 -0
  45. package/src/lib/alignment.ts +21 -0
  46. package/src/lib/page-board.ts +25 -0
  47. package/src/lib/review-position.ts +19 -0
  48. package/src/styles/base.css +4 -6
  49. package/src/styles/feedback.css +97 -61
  50. package/src/styles/shell.css +95 -323
  51. package/theme-base.css +90 -0
  52. package/theme.css +56 -0
  53. package/vite.config.ts +2 -5
  54. package/composition/README.md +0 -156
  55. package/composition/compile.ts +0 -227
  56. package/composition/document.ts +0 -600
  57. package/composition/schema.json +0 -3001
  58. package/composition/schema.ts +0 -437
  59. package/composition/theme-tokens.ts +0 -16
  60. package/composition/types.ts +0 -269
  61. package/composition/validate.ts +0 -226
  62. package/composition/vector.ts +0 -143
  63. package/composition/visualizations.ts +0 -319
  64. package/design/README.md +0 -17
  65. package/design/palettes/README.md +0 -14
  66. package/design/palettes/base.ts +0 -14
  67. package/design/palettes/candidates.ts +0 -19
  68. package/design/palettes/index.ts +0 -78
  69. package/design/review/color-theme.md +0 -44
  70. package/design/review/layout.md +0 -16
  71. package/design/review/text.md +0 -18
  72. package/design/review/typography.md +0 -15
  73. package/design/review/visuals.md +0 -31
  74. package/design/semantic-patterns.md +0 -43
  75. package/design/themes/README.md +0 -40
  76. package/design/themes/index.ts +0 -24
  77. package/design/visual-languages/technical-product.md +0 -17
  78. package/design/visual-review.md +0 -88
  79. package/lib/assets.d.ts +0 -8
  80. package/lib/charts.ts +0 -18
  81. package/lib/contrast.ts +0 -16
  82. package/lib/layouts.ts +0 -50
  83. package/lib/slide.tsx +0 -42
  84. package/lib/taste.ts +0 -17
  85. package/lib/text.tsx +0 -89
  86. package/lib/typeface.ts +0 -44
  87. package/scripts/migrate-react-page.ts +0 -120
  88. package/skills/konpeki/assets/blank.json +0 -23
  89. package/slides/README.md +0 -153
  90. package/slides/architecture/PROMPT.md +0 -31
  91. package/slides/architecture/index.tsx +0 -102
  92. package/slides/article-brief/PROMPT.md +0 -35
  93. package/slides/article-brief/index.tsx +0 -71
  94. package/slides/bar-chart/PROMPT.md +0 -39
  95. package/slides/bar-chart/index.tsx +0 -97
  96. package/slides/comparison/PROMPT.md +0 -29
  97. package/slides/comparison/index.tsx +0 -95
  98. package/slides/decision-memo/PROMPT.md +0 -34
  99. package/slides/decision-memo/index.tsx +0 -85
  100. package/slides/delivery-plan/PROMPT.md +0 -45
  101. package/slides/delivery-plan/index.tsx +0 -105
  102. package/slides/experiment/PROMPT.md +0 -44
  103. package/slides/experiment/index.tsx +0 -127
  104. package/slides/incident-workflow/PROMPT.md +0 -57
  105. package/slides/incident-workflow/index.tsx +0 -78
  106. package/slides/introducing-konpeki/PROMPT.md +0 -40
  107. package/slides/introducing-konpeki/README.md +0 -76
  108. package/slides/introducing-konpeki/SOURCE.md +0 -26
  109. package/slides/introducing-konpeki/author.ts +0 -165
  110. package/slides/introducing-konpeki/composition.json +0 -3270
  111. package/slides/line-chart/PROMPT.md +0 -40
  112. package/slides/line-chart/index.tsx +0 -72
  113. package/slides/migration/PROMPT.md +0 -38
  114. package/slides/migration/index.tsx +0 -89
  115. package/slides/og-images/PROMPT.md +0 -21
  116. package/slides/og-images/index.tsx +0 -76
  117. package/slides/product-introduction/PROMPT.md +0 -24
  118. package/slides/product-introduction/index.tsx +0 -105
  119. package/slides/research-brief/PROMPT.md +0 -40
  120. package/slides/research-brief/index.tsx +0 -104
  121. package/slides/results-explanation/PROMPT.md +0 -32
  122. package/slides/results-explanation/index.tsx +0 -96
  123. package/slides/retrospective/PROMPT.md +0 -43
  124. package/slides/retrospective/index.tsx +0 -105
  125. package/slides/sankey/PROMPT.md +0 -11
  126. package/slides/sankey/index.tsx +0 -93
  127. package/slides/teaching/PROMPT.md +0 -45
  128. package/slides/teaching/index.tsx +0 -124
  129. package/slides/vertical-bar-charts/PROMPT.md +0 -13
  130. package/slides/vertical-bar-charts/index.tsx +0 -97
  131. package/src/app/App.tsx +0 -820
  132. package/src/components/BuildOrb.tsx +0 -40
  133. package/src/components/Canvas.tsx +0 -1185
  134. package/src/components/DiagramTypeIcon.tsx +0 -78
  135. package/src/components/InspectorPanel.tsx +0 -773
  136. package/src/components/LeftPanel.tsx +0 -120
  137. package/src/components/PageSizePicker.tsx +0 -30
  138. package/src/components/Presentation.tsx +0 -105
  139. package/src/components/RevisionNotes.tsx +0 -56
  140. package/src/components/RightPanel.tsx +0 -201
  141. package/src/components/VectorOverflowWarning.tsx +0 -46
  142. package/src/components/WorkspaceChrome.tsx +0 -288
  143. package/src/components/ui.tsx +0 -53
  144. package/src/lib/examples/react-page-migration.json +0 -1295
  145. package/src/lib/examples.ts +0 -42
  146. package/src/lib/export-png.ts +0 -104
  147. package/src/lib/file-session.ts +0 -87
  148. package/src/lib/history.ts +0 -53
  149. package/src/lib/model.ts +0 -188
  150. package/src/lib/page-size.ts +0 -24
  151. package/src/lib/presentation.ts +0 -17
  152. package/src/lib/review.ts +0 -26
  153. package/src/lib/storage.ts +0 -71
  154. package/src/lib/theme.ts +0 -25
  155. package/src/lib/use-file-session.ts +0 -227
  156. package/src/main.tsx +0 -29
  157. package/src/styles/canvas.css +0 -299
  158. package/src/styles/chrome.css +0 -384
  159. package/src/styles/component-previews.css +0 -386
  160. package/src/styles/left-panel.css +0 -166
  161. package/src/styles/presentation.css +0 -72
  162. package/src/styles/right-panel.css +0 -1215
@@ -1,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,56 +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 }: {
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
- }) {
15
- const [drafts, setDrafts] = useState<Record<string, string>>({});
16
- const [saving, setSaving] = useState(false);
17
- const [error, setError] = useState("");
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); setError("");
32
- try { await action(); }
33
- catch (error) { setError(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]: "" }));
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
- {error && <p role="alert" className="revision-note-error">{error}</p>}
49
- <p className="revision-note-count">{pending.length} pending</p>
50
- {pending.length > 0 && <ol className="revision-note-list">{pending.map((note, i) => <li key={note.id} id={`note-${note.id}`}>
51
- <button type="button" className="revision-note-target" onClick={() => onSelect(note)}><span>{i + 1}</span>{label(note)}</button>
52
- <p>{note.text}</p>
53
- <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>
54
- </li>)}</ol>}
55
- </section>;
56
- }
@@ -1,201 +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
-
9
- export type RightPanelView = "settings" | "layers";
10
-
11
- function LayersPanel({
12
- slide,
13
- selectedComponentId,
14
- onSelectComponent,
15
- onReorderPaintOrder,
16
- }: {
17
- slide: CompositionSlide;
18
- selectedComponentId?: string;
19
- onSelectComponent: (id: string) => void;
20
- onReorderPaintOrder: (ids: string[]) => void;
21
- }) {
22
- const [dragging, setDragging] = useState<string>();
23
- const layers = [...slide.paintOrder].reverse();
24
- function moveLayer(id: string, toIndex: number) {
25
- const next = layers.filter((item) => item !== id);
26
- next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, id);
27
- onReorderPaintOrder(next.reverse());
28
- }
29
- return (
30
- <section className="layers-panel" aria-labelledby="layers-heading">
31
- <div className="layers-heading-row">
32
- <h2 id="layers-heading">Components & layers</h2>
33
- <span>Front to back</span>
34
- </div>
35
- {layers.length ? (
36
- <div className="layer-list">
37
- {layers.map((id, index) => {
38
- const component = slide.components.find((item) => item.id === id)!;
39
- const groups = slide.groups
40
- .filter((group) => group.childIds.includes(id))
41
- .map((group) => group.label?.trim() || group.id);
42
- const label = componentInstanceLabel(slide.components, id);
43
- return (
44
- <div
45
- key={id}
46
- data-layer={id}
47
- className={`layer-row ${selectedComponentId === id ? "selected" : ""} ${dragging === id ? "dragging" : ""}`}
48
- draggable
49
- onDragStart={(event) => {
50
- setDragging(id);
51
- event.dataTransfer.effectAllowed = "move";
52
- event.dataTransfer.setData("text/plain", id);
53
- }}
54
- onDragEnd={() => setDragging(undefined)}
55
- onDragOver={(event) => {
56
- event.preventDefault();
57
- event.dataTransfer.dropEffect = "move";
58
- }}
59
- onDrop={(event) => {
60
- event.preventDefault();
61
- const source = dragging || event.dataTransfer.getData("text/plain");
62
- if (source && source !== id) moveLayer(source, index);
63
- setDragging(undefined);
64
- }}
65
- >
66
- <span className="layer-drag" aria-hidden="true">⠿</span>
67
- <button
68
- type="button"
69
- className="layer-select"
70
- onClick={() => onSelectComponent(id)}
71
- >
72
- <strong>{label}</strong>
73
- <span>{groups.length ? groups.join(", ") : "Page root"}</span>
74
- </button>
75
- <span className="layer-actions">
76
- <button
77
- type="button"
78
- aria-label={`Move ${label} forward`}
79
- disabled={index === 0}
80
- onClick={() => moveLayer(id, index - 1)}
81
- >↑</button>
82
- <button
83
- type="button"
84
- aria-label={`Move ${label} backward`}
85
- disabled={index === layers.length - 1}
86
- onClick={() => moveLayer(id, index + 1)}
87
- >↓</button>
88
- </span>
89
- </div>
90
- );
91
- })}
92
- </div>
93
- ) : (
94
- <p className="layers-empty">No components yet.</p>
95
- )}
96
- </section>
97
- );
98
- }
99
-
100
- export function RightPanel({
101
- view,
102
- collapsed,
103
- component,
104
- componentLabel,
105
- selectedComponentId,
106
- selectedVectorElementId,
107
- vectorEditRequest,
108
- slide,
109
- draft,
110
- onView,
111
- onCollapsedChange,
112
- onSelectSlide,
113
- onSelectComponent,
114
- onSelectVectorElement,
115
- onReorderPaintOrder,
116
- onComponent,
117
- onSlide,
118
- onDraft,
119
- onEditEnd,
120
- }: {
121
- view: RightPanelView;
122
- collapsed: boolean;
123
- component?: CompositionComponent;
124
- componentLabel?: string;
125
- selectedComponentId?: string;
126
- selectedVectorElementId?: string;
127
- vectorEditRequest?: number;
128
- slide: CompositionSlide;
129
- draft: Draft;
130
- onView: (view: RightPanelView) => void;
131
- onCollapsedChange: (collapsed: boolean) => void;
132
- onSelectSlide: () => void;
133
- onSelectComponent: (id: string) => void;
134
- onSelectVectorElement: (id?: string) => void;
135
- onReorderPaintOrder: (ids: string[]) => void;
136
- onComponent: (component: CompositionComponent, mergeKey?: string) => void;
137
- onSlide: (slide: CompositionSlide, mergeKey?: string) => void;
138
- onDraft: (patch: Partial<Draft>, mergeKey?: string) => void;
139
- onEditEnd: () => void;
140
- }) {
141
- return (
142
- <aside className={`right-panel${collapsed ? " collapsed" : ""}`} aria-label="Inspector panel">
143
- <div className="panel-toolbar">
144
- <div className="panel-tabs" aria-label="Right panel view" inert={collapsed}>
145
- {(["settings", "layers"] as const).map((tab) => (
146
- <button
147
- key={tab}
148
- type="button"
149
- aria-pressed={view === tab}
150
- className={view === tab ? "active" : ""}
151
- onClick={() => onView(tab)}
152
- >
153
- {tab === "settings" ? "Settings" : "Layers"}
154
- </button>
155
- ))}
156
- </div>
157
- <button
158
- type="button"
159
- className="panel-toggle"
160
- aria-label={collapsed ? "Expand right panel" : "Collapse right panel"}
161
- aria-expanded={!collapsed}
162
- onClick={() => onCollapsedChange(!collapsed)}
163
- >
164
- <svg
165
- className="panel-collapse-icon"
166
- viewBox="0 0 24 24"
167
- aria-hidden="true"
168
- >
169
- <rect x="3" y="4" width="18" height="16" rx="2" />
170
- <path d={collapsed ? "M15 4v16M11 9l-3 3 3 3" : "M15 4v16M8 9l3 3-3 3"} />
171
- </svg>
172
- </button>
173
- </div>
174
- <div className="right-panel-body" inert={collapsed}>
175
- {view === "settings" ? (
176
- <InspectorPanel
177
- component={component}
178
- componentLabel={componentLabel}
179
- slide={slide}
180
- draft={draft}
181
- selectedVectorElementId={selectedVectorElementId}
182
- vectorEditRequest={vectorEditRequest}
183
- onSelectSlide={onSelectSlide}
184
- onSelectVectorElement={onSelectVectorElement}
185
- onComponent={onComponent}
186
- onSlide={onSlide}
187
- onDraft={onDraft}
188
- onEditEnd={onEditEnd}
189
- />
190
- ) : (
191
- <LayersPanel
192
- slide={slide}
193
- selectedComponentId={selectedComponentId}
194
- onSelectComponent={onSelectComponent}
195
- onReorderPaintOrder={onReorderPaintOrder}
196
- />
197
- )}
198
- </div>
199
- </aside>
200
- );
201
- }
@@ -1,46 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import type { CompositionSlide, CompositionDocument } from "../../composition/types.ts";
3
-
4
- /** Measure rendered text, including nested SVG transforms, after fonts settle. */
5
- export function VectorOverflowWarning({ slide, theme }: {
6
- slide: CompositionSlide;
7
- theme: CompositionDocument["theme"];
8
- }) {
9
- const [count, setCount] = useState(0);
10
- useEffect(() => {
11
- let cancelled = false;
12
- let frame = 0;
13
- function measure() {
14
- frame = requestAnimationFrame(() => {
15
- if (cancelled) return;
16
- let overflow = 0;
17
- document.querySelectorAll<HTMLElement>(".stage .text-block-content").forEach((text) => {
18
- const surface = text.parentElement;
19
- if (surface && (text.scrollHeight > surface.clientHeight + 1 || text.scrollWidth > surface.clientWidth + 1)) overflow++;
20
- });
21
- document.querySelectorAll<SVGSVGElement>(".stage .custom-vector-art").forEach((svg) => {
22
- const root = svg.getScreenCTM();
23
- if (!root) return;
24
- const view = svg.viewBox.baseVal;
25
- svg.querySelectorAll<SVGTextElement>("text").forEach((text) => {
26
- const matrix = text.getScreenCTM();
27
- if (!matrix) return;
28
- const box = text.getBBox();
29
- const transform = root.inverse().multiply(matrix);
30
- const corners = [[box.x, box.y], [box.x + box.width, box.y],
31
- [box.x, box.y + box.height], [box.x + box.width, box.y + box.height]];
32
- if (corners.some(([x, y]) => {
33
- const p = new DOMPoint(x, y).matrixTransform(transform);
34
- 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;
35
- })) overflow++;
36
- });
37
- });
38
- setCount(overflow);
39
- });
40
- }
41
- void document.fonts.ready.then(measure);
42
- document.fonts.addEventListener("loadingdone", measure);
43
- return () => { cancelled = true; cancelAnimationFrame(frame); document.fonts.removeEventListener("loadingdone", measure); };
44
- }, [slide, theme]);
45
- return count ? <p role="status" className="field-error">{count} text {count === 1 ? "element exceeds" : "elements exceed"} its component viewport on this slide. Revise the text or geometry; nothing was resized.</p> : null;
46
- }