konpeki 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +21 -28
- package/AUTHORING.md +68 -229
- package/CONTRIBUTING.md +10 -23
- package/README.md +96 -87
- package/SETUP.md +50 -114
- package/docs/development.md +43 -248
- package/docs/workflow.md +6 -108
- package/html/README.md +140 -0
- package/html/browser.ts +103 -0
- package/html/document.ts +38 -0
- package/html/floor.ts +34 -0
- package/html/index.html +5 -0
- package/html/inspect.ts +313 -0
- package/html/preview.css +62 -0
- package/html/preview.tsx +463 -0
- package/html/review-hints.ts +63 -0
- package/html/server.ts +99 -0
- package/html/source.ts +104 -0
- package/html/starter.ts +6 -0
- package/html/theme-authoring.md +176 -0
- package/html/theme.md +61 -0
- package/index.html +9 -9
- package/package.json +21 -35
- package/plugin.json +1 -1
- package/public/og.png +0 -0
- package/runtime/browser-B-29TH1a.mjs +595 -0
- package/runtime/floor-Cmk7G3pU.mjs +41 -0
- package/runtime/konpeki.mjs +90 -1410
- package/runtime/server-BAqC_5WD.mjs +2 -0
- package/runtime/server-DfRpfcY9.mjs +205 -0
- package/runtime/source-B_Ui9tMp.mjs +141 -0
- package/runtime/source-CZQq9GUO.mjs +2 -0
- package/skills/konpeki/SKILL.md +88 -172
- package/skills/konpeki/assets/blank.html +17 -0
- package/skills/konpeki/floor.md +76 -0
- package/skills/konpeki/references/cover.md +27 -0
- package/skills/konpeki/references/long-document.md +40 -0
- package/skills/konpeki/references/one-pager.md +27 -0
- package/skills/konpeki/references/patterns.md +118 -0
- package/skills/konpeki/references/resume.md +28 -0
- package/skills/konpeki/references/slides.md +28 -0
- package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
- package/skills/konpeki/scripts/prepare-document.mjs +25 -14
- package/src/components/PageBoard.tsx +156 -0
- package/src/lib/alignment.ts +21 -0
- package/src/lib/page-board.ts +25 -0
- package/src/lib/review-position.ts +19 -0
- package/src/styles/base.css +4 -6
- package/src/styles/feedback.css +97 -61
- package/src/styles/shell.css +95 -323
- package/theme-base.css +90 -0
- package/theme.css +56 -0
- package/vite.config.ts +2 -5
- package/composition/README.md +0 -156
- package/composition/compile.ts +0 -227
- package/composition/document.ts +0 -600
- package/composition/schema.json +0 -3001
- package/composition/schema.ts +0 -437
- package/composition/theme-tokens.ts +0 -16
- package/composition/types.ts +0 -269
- package/composition/validate.ts +0 -226
- package/composition/vector.ts +0 -143
- package/composition/visualizations.ts +0 -319
- package/design/README.md +0 -17
- package/design/palettes/README.md +0 -14
- package/design/palettes/base.ts +0 -14
- package/design/palettes/candidates.ts +0 -19
- package/design/palettes/index.ts +0 -78
- package/design/review/color-theme.md +0 -44
- package/design/review/layout.md +0 -16
- package/design/review/text.md +0 -18
- package/design/review/typography.md +0 -15
- package/design/review/visuals.md +0 -31
- package/design/semantic-patterns.md +0 -43
- package/design/themes/README.md +0 -40
- package/design/themes/index.ts +0 -24
- package/design/visual-languages/technical-product.md +0 -17
- package/design/visual-review.md +0 -88
- package/lib/assets.d.ts +0 -8
- package/lib/charts.ts +0 -18
- package/lib/contrast.ts +0 -16
- package/lib/layouts.ts +0 -50
- package/lib/slide.tsx +0 -42
- package/lib/taste.ts +0 -17
- package/lib/text.tsx +0 -89
- package/lib/typeface.ts +0 -44
- package/scripts/migrate-react-page.ts +0 -120
- package/skills/konpeki/assets/blank.json +0 -23
- package/slides/README.md +0 -153
- package/slides/architecture/PROMPT.md +0 -31
- package/slides/architecture/index.tsx +0 -102
- package/slides/article-brief/PROMPT.md +0 -35
- package/slides/article-brief/index.tsx +0 -71
- package/slides/bar-chart/PROMPT.md +0 -39
- package/slides/bar-chart/index.tsx +0 -97
- package/slides/comparison/PROMPT.md +0 -29
- package/slides/comparison/index.tsx +0 -95
- package/slides/decision-memo/PROMPT.md +0 -34
- package/slides/decision-memo/index.tsx +0 -85
- package/slides/delivery-plan/PROMPT.md +0 -45
- package/slides/delivery-plan/index.tsx +0 -105
- package/slides/experiment/PROMPT.md +0 -44
- package/slides/experiment/index.tsx +0 -127
- package/slides/incident-workflow/PROMPT.md +0 -57
- package/slides/incident-workflow/index.tsx +0 -78
- package/slides/introducing-konpeki/PROMPT.md +0 -40
- package/slides/introducing-konpeki/README.md +0 -76
- package/slides/introducing-konpeki/SOURCE.md +0 -26
- package/slides/introducing-konpeki/author.ts +0 -165
- package/slides/introducing-konpeki/composition.json +0 -3270
- package/slides/line-chart/PROMPT.md +0 -40
- package/slides/line-chart/index.tsx +0 -72
- package/slides/migration/PROMPT.md +0 -38
- package/slides/migration/index.tsx +0 -89
- package/slides/og-images/PROMPT.md +0 -21
- package/slides/og-images/index.tsx +0 -76
- package/slides/product-introduction/PROMPT.md +0 -24
- package/slides/product-introduction/index.tsx +0 -105
- package/slides/research-brief/PROMPT.md +0 -40
- package/slides/research-brief/index.tsx +0 -104
- package/slides/results-explanation/PROMPT.md +0 -32
- package/slides/results-explanation/index.tsx +0 -96
- package/slides/retrospective/PROMPT.md +0 -43
- package/slides/retrospective/index.tsx +0 -105
- package/slides/sankey/PROMPT.md +0 -11
- package/slides/sankey/index.tsx +0 -93
- package/slides/teaching/PROMPT.md +0 -45
- package/slides/teaching/index.tsx +0 -124
- package/slides/vertical-bar-charts/PROMPT.md +0 -13
- package/slides/vertical-bar-charts/index.tsx +0 -97
- package/src/app/App.tsx +0 -820
- package/src/components/BuildOrb.tsx +0 -40
- package/src/components/Canvas.tsx +0 -1185
- package/src/components/DiagramTypeIcon.tsx +0 -78
- package/src/components/InspectorPanel.tsx +0 -773
- package/src/components/LeftPanel.tsx +0 -120
- package/src/components/PageSizePicker.tsx +0 -30
- package/src/components/Presentation.tsx +0 -105
- package/src/components/RevisionNotes.tsx +0 -56
- package/src/components/RightPanel.tsx +0 -201
- package/src/components/VectorOverflowWarning.tsx +0 -46
- package/src/components/WorkspaceChrome.tsx +0 -288
- package/src/components/ui.tsx +0 -53
- package/src/lib/examples/react-page-migration.json +0 -1295
- package/src/lib/examples.ts +0 -42
- package/src/lib/export-png.ts +0 -104
- package/src/lib/file-session.ts +0 -87
- package/src/lib/history.ts +0 -53
- package/src/lib/model.ts +0 -188
- package/src/lib/page-size.ts +0 -24
- package/src/lib/presentation.ts +0 -17
- package/src/lib/review.ts +0 -26
- package/src/lib/storage.ts +0 -71
- package/src/lib/theme.ts +0 -25
- package/src/lib/use-file-session.ts +0 -227
- package/src/main.tsx +0 -29
- package/src/styles/canvas.css +0 -299
- package/src/styles/chrome.css +0 -384
- package/src/styles/component-previews.css +0 -386
- package/src/styles/left-panel.css +0 -166
- package/src/styles/presentation.css +0 -72
- package/src/styles/right-panel.css +0 -1215
package/src/app/App.tsx
DELETED
|
@@ -1,820 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import type { CompositionComponent } from "../../composition/types.ts";
|
|
3
|
-
import { validateComposition } from "../../composition/validate.ts";
|
|
4
|
-
import { removeVectorElement } from "../../composition/vector.ts";
|
|
5
|
-
import {
|
|
6
|
-
addComponent,
|
|
7
|
-
addSlide as appendSlide,
|
|
8
|
-
componentRemovalIssue,
|
|
9
|
-
componentInstanceLabel,
|
|
10
|
-
componentLabels,
|
|
11
|
-
duplicateComponent,
|
|
12
|
-
getSlide,
|
|
13
|
-
initialDraft,
|
|
14
|
-
parseCompositionJSON,
|
|
15
|
-
removeComponent,
|
|
16
|
-
removeSlide as deleteSlide,
|
|
17
|
-
reorderPaintOrder,
|
|
18
|
-
transformComponentRect,
|
|
19
|
-
type Draft,
|
|
20
|
-
} from "../lib/model.ts";
|
|
21
|
-
import {
|
|
22
|
-
commitHistory,
|
|
23
|
-
createHistory,
|
|
24
|
-
finishHistoryEdit,
|
|
25
|
-
redoHistory,
|
|
26
|
-
undoHistory,
|
|
27
|
-
} from "../lib/history.ts";
|
|
28
|
-
import {
|
|
29
|
-
clearStoredDraft,
|
|
30
|
-
clearStoredExampleDraft,
|
|
31
|
-
loadDraft,
|
|
32
|
-
loadExampleDraft,
|
|
33
|
-
persistDraft,
|
|
34
|
-
persistExampleDraft,
|
|
35
|
-
} from "../lib/storage.ts";
|
|
36
|
-
import { canonicalJSON } from "../../composition/compile.ts";
|
|
37
|
-
import { diagramDefinition } from "../../composition/visualizations.ts";
|
|
38
|
-
import { Canvas } from "../components/Canvas.tsx";
|
|
39
|
-
import { BuildOrb } from "../components/BuildOrb.tsx";
|
|
40
|
-
import { LeftPanel, type LeftPanelView } from "../components/LeftPanel.tsx";
|
|
41
|
-
import { Presentation } from "../components/Presentation.tsx";
|
|
42
|
-
import {
|
|
43
|
-
RightPanel,
|
|
44
|
-
type RightPanelView,
|
|
45
|
-
} from "../components/RightPanel.tsx";
|
|
46
|
-
import { WorkspaceChrome } from "../components/WorkspaceChrome.tsx";
|
|
47
|
-
import { exampleDraft } from "../lib/examples.ts";
|
|
48
|
-
import { exportPagePNG } from "../lib/export-png.ts";
|
|
49
|
-
import { fileSessionToken } from "../lib/file-session.ts";
|
|
50
|
-
import { useFileSession } from "../lib/use-file-session.ts";
|
|
51
|
-
import { RevisionNotes } from "../components/RevisionNotes.tsx";
|
|
52
|
-
import type { ReviewTarget } from "../lib/review.ts";
|
|
53
|
-
|
|
54
|
-
const sessionToken = fileSessionToken();
|
|
55
|
-
|
|
56
|
-
function loadInitialDraft() {
|
|
57
|
-
if (sessionToken)
|
|
58
|
-
return {
|
|
59
|
-
draft: initialDraft(true),
|
|
60
|
-
storageBlocked: false,
|
|
61
|
-
exampleName: undefined,
|
|
62
|
-
fileSession: true,
|
|
63
|
-
};
|
|
64
|
-
const exampleName = new URLSearchParams(window.location.search).get("example");
|
|
65
|
-
const example = exampleDraft(exampleName);
|
|
66
|
-
return example
|
|
67
|
-
? {
|
|
68
|
-
...loadExampleDraft(exampleName!, example),
|
|
69
|
-
exampleName: exampleName!,
|
|
70
|
-
fileSession: false,
|
|
71
|
-
}
|
|
72
|
-
: { ...loadDraft(), exampleName: undefined, fileSession: false };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function App() {
|
|
76
|
-
const [loaded] = useState(loadInitialDraft);
|
|
77
|
-
const [history, setHistory] = useState(() =>
|
|
78
|
-
createHistory<{ draft: Draft; selected?: string; activeSlideId: string }>({
|
|
79
|
-
draft: loaded.draft,
|
|
80
|
-
activeSlideId: loaded.draft.slides[0].id,
|
|
81
|
-
}),
|
|
82
|
-
);
|
|
83
|
-
const { draft, selected, activeSlideId } = history.present;
|
|
84
|
-
const slide = getSlide(draft, activeSlideId);
|
|
85
|
-
const validation = useMemo(() => validateComposition(draft), [draft]);
|
|
86
|
-
const [savedDraft, setSavedDraft] = useState<Draft>();
|
|
87
|
-
const [blocked, setBlocked] = useState(loaded.storageBlocked);
|
|
88
|
-
const [error, setError] = useState(loaded.error ?? "");
|
|
89
|
-
const [notice, setNotice] = useState<{
|
|
90
|
-
message: string;
|
|
91
|
-
tone: "neutral" | "error";
|
|
92
|
-
}>();
|
|
93
|
-
const [toastClosing, setToastClosing] = useState(false);
|
|
94
|
-
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
|
95
|
-
const [leftView, setLeftView] = useState<LeftPanelView>("pages");
|
|
96
|
-
const [rightCollapsed, setRightCollapsed] = useState(false);
|
|
97
|
-
const [rightView, setRightView] = useState<RightPanelView>("settings");
|
|
98
|
-
const [vectorEditRequest, setVectorEditRequest] = useState(0);
|
|
99
|
-
const [presenting, setPresenting] = useState(false);
|
|
100
|
-
const [exportBusy, setExportBusy] = useState(false);
|
|
101
|
-
const [vectorSelection, setVectorSelection] = useState<{
|
|
102
|
-
componentId: string;
|
|
103
|
-
elementId?: string;
|
|
104
|
-
}>();
|
|
105
|
-
const [titleError, setTitleError] = useState(false);
|
|
106
|
-
const title = useRef<HTMLInputElement>(null);
|
|
107
|
-
const focusAfterHistory = useRef(false);
|
|
108
|
-
const fileSession = useFileSession(loaded.fileSession, draft, {
|
|
109
|
-
onOpen: (document, name) => {
|
|
110
|
-
setHistory(createHistory({
|
|
111
|
-
draft: document,
|
|
112
|
-
activeSlideId: document.slides[0].id,
|
|
113
|
-
}));
|
|
114
|
-
showNotice(`${name} opened from the local service.`);
|
|
115
|
-
},
|
|
116
|
-
onExternalChange: (document) => {
|
|
117
|
-
setHistory((current) => commitHistory(current, {
|
|
118
|
-
draft: document,
|
|
119
|
-
activeSlideId: document.slides.some(
|
|
120
|
-
(candidate) => candidate.id === current.present.activeSlideId,
|
|
121
|
-
) ? current.present.activeSlideId : document.slides[0].id,
|
|
122
|
-
selected: undefined,
|
|
123
|
-
}));
|
|
124
|
-
},
|
|
125
|
-
onNotice: showNotice,
|
|
126
|
-
onError: setError,
|
|
127
|
-
});
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
if (!focusAfterHistory.current) return;
|
|
130
|
-
focusAfterHistory.current = false;
|
|
131
|
-
const frame = requestAnimationFrame(() => {
|
|
132
|
-
const target = history.present.selected
|
|
133
|
-
? [...document.querySelectorAll<HTMLElement>("[data-component]")]
|
|
134
|
-
.find(
|
|
135
|
-
(element) =>
|
|
136
|
-
element.dataset.component === history.present.selected,
|
|
137
|
-
)
|
|
138
|
-
?.querySelector<HTMLElement>(".component-surface")
|
|
139
|
-
: document.querySelector<HTMLElement>("#canvas-stage");
|
|
140
|
-
target?.focus();
|
|
141
|
-
});
|
|
142
|
-
return () => cancelAnimationFrame(frame);
|
|
143
|
-
}, [history.present]);
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
if (loaded.fileSession) return;
|
|
146
|
-
if (blocked) return;
|
|
147
|
-
if (!validation.ok) return;
|
|
148
|
-
const timer = setTimeout(() => {
|
|
149
|
-
try {
|
|
150
|
-
if (loaded.exampleName) persistExampleDraft(loaded.exampleName, draft);
|
|
151
|
-
else persistDraft(draft);
|
|
152
|
-
setSavedDraft(draft);
|
|
153
|
-
} catch {
|
|
154
|
-
setError(
|
|
155
|
-
"Unable to save this browser-local draft. Download JSON or reset to retry.",
|
|
156
|
-
);
|
|
157
|
-
setBlocked(true);
|
|
158
|
-
}
|
|
159
|
-
}, 250);
|
|
160
|
-
return () => clearTimeout(timer);
|
|
161
|
-
}, [draft, validation, blocked, loaded.exampleName, loaded.fileSession]);
|
|
162
|
-
useEffect(() => {
|
|
163
|
-
if (!notice) return;
|
|
164
|
-
const timer = window.setTimeout(() => setToastClosing(true), 3000);
|
|
165
|
-
return () => window.clearTimeout(timer);
|
|
166
|
-
}, [notice]);
|
|
167
|
-
useEffect(() => {
|
|
168
|
-
if (!toastClosing) return;
|
|
169
|
-
const timer = window.setTimeout(() => {
|
|
170
|
-
setNotice(undefined);
|
|
171
|
-
setToastClosing(false);
|
|
172
|
-
}, 280);
|
|
173
|
-
return () => window.clearTimeout(timer);
|
|
174
|
-
}, [toastClosing]);
|
|
175
|
-
useEffect(() => {
|
|
176
|
-
const narrow = window.matchMedia("(max-width: 1200px)");
|
|
177
|
-
const compact = window.matchMedia("(max-width: 900px)");
|
|
178
|
-
const adaptPanels = () => {
|
|
179
|
-
setRightCollapsed(narrow.matches);
|
|
180
|
-
setLeftCollapsed(compact.matches);
|
|
181
|
-
};
|
|
182
|
-
adaptPanels();
|
|
183
|
-
narrow.addEventListener("change", adaptPanels);
|
|
184
|
-
compact.addEventListener("change", adaptPanels);
|
|
185
|
-
return () => {
|
|
186
|
-
narrow.removeEventListener("change", adaptPanels);
|
|
187
|
-
compact.removeEventListener("change", adaptPanels);
|
|
188
|
-
};
|
|
189
|
-
}, []);
|
|
190
|
-
useEffect(() => {
|
|
191
|
-
function keydown(event: KeyboardEvent) {
|
|
192
|
-
if (presenting || fileSession.building) return;
|
|
193
|
-
const modifier = event.metaKey || event.ctrlKey;
|
|
194
|
-
const target = event.target;
|
|
195
|
-
const editable =
|
|
196
|
-
target instanceof HTMLElement &&
|
|
197
|
-
(target.closest("input,textarea,select") || target.isContentEditable);
|
|
198
|
-
const slideRename =
|
|
199
|
-
target instanceof HTMLInputElement &&
|
|
200
|
-
target.getAttribute("aria-label") === "Page name";
|
|
201
|
-
const noteEditing = target instanceof HTMLTextAreaElement && target.id === "revision-note";
|
|
202
|
-
if (event.key === "Escape" && !editable && vectorSelection) {
|
|
203
|
-
setVectorSelection(undefined);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
if (modifier && !event.altKey && event.key.toLowerCase() === "z") {
|
|
207
|
-
if (slideRename || noteEditing) return;
|
|
208
|
-
event.preventDefault();
|
|
209
|
-
setHistory((current) =>
|
|
210
|
-
event.shiftKey ? redoHistory(current) : undoHistory(current),
|
|
211
|
-
);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (
|
|
215
|
-
modifier &&
|
|
216
|
-
!event.altKey &&
|
|
217
|
-
event.key.toLowerCase() === "y"
|
|
218
|
-
) {
|
|
219
|
-
if (slideRename || noteEditing) return;
|
|
220
|
-
event.preventDefault();
|
|
221
|
-
setHistory(redoHistory);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (
|
|
225
|
-
modifier &&
|
|
226
|
-
!event.altKey &&
|
|
227
|
-
event.key.toLowerCase() === "d" &&
|
|
228
|
-
selected &&
|
|
229
|
-
!editable
|
|
230
|
-
) {
|
|
231
|
-
event.preventDefault();
|
|
232
|
-
duplicateSelected();
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
if (
|
|
236
|
-
!["Delete", "Backspace"].includes(event.key) ||
|
|
237
|
-
event.ctrlKey ||
|
|
238
|
-
event.metaKey ||
|
|
239
|
-
event.altKey ||
|
|
240
|
-
!selected
|
|
241
|
-
)
|
|
242
|
-
return;
|
|
243
|
-
if (editable) return;
|
|
244
|
-
event.preventDefault();
|
|
245
|
-
deleteSelected();
|
|
246
|
-
}
|
|
247
|
-
window.addEventListener("keydown", keydown);
|
|
248
|
-
return () => window.removeEventListener("keydown", keydown);
|
|
249
|
-
}, [draft, presenting, selected, vectorSelection, fileSession.building]);
|
|
250
|
-
function showNotice(message: string, tone: "neutral" | "error" = "neutral") {
|
|
251
|
-
setToastClosing(false);
|
|
252
|
-
setNotice({ message: message.replace(/\.$/, ""), tone });
|
|
253
|
-
}
|
|
254
|
-
function updateDraft(
|
|
255
|
-
next: Draft,
|
|
256
|
-
options: { mergeKey?: string; selected?: string; activeSlideId?: string } = {},
|
|
257
|
-
) {
|
|
258
|
-
setHistory((current) =>
|
|
259
|
-
commitHistory(
|
|
260
|
-
current,
|
|
261
|
-
{
|
|
262
|
-
draft: next,
|
|
263
|
-
selected: Object.hasOwn(options, "selected")
|
|
264
|
-
? options.selected
|
|
265
|
-
: current.present.selected,
|
|
266
|
-
activeSlideId: options.activeSlideId ?? current.present.activeSlideId,
|
|
267
|
-
},
|
|
268
|
-
options.mergeKey,
|
|
269
|
-
),
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
function selectComponent(id?: string) {
|
|
273
|
-
if (!id || vectorSelection?.componentId !== id) setVectorSelection(undefined);
|
|
274
|
-
setHistory((current) => {
|
|
275
|
-
const next = finishHistoryEdit(current);
|
|
276
|
-
return { ...next, present: { ...next.present, selected: id } };
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
function selectSlide(id: string) {
|
|
280
|
-
setVectorSelection(undefined);
|
|
281
|
-
setHistory((current) => {
|
|
282
|
-
const next = finishHistoryEdit(current);
|
|
283
|
-
return {
|
|
284
|
-
...next,
|
|
285
|
-
present: { ...next.present, activeSlideId: id, selected: undefined },
|
|
286
|
-
};
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
function addNewSlide() {
|
|
290
|
-
const result = appendSlide(draft);
|
|
291
|
-
if (!result.slideId) {
|
|
292
|
-
showNotice("Could not add a page until the composition is valid.", "error");
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
focusAfterHistory.current = true;
|
|
296
|
-
updateDraft(result.draft, {
|
|
297
|
-
activeSlideId: result.slideId,
|
|
298
|
-
selected: undefined,
|
|
299
|
-
});
|
|
300
|
-
showNotice("Page added.");
|
|
301
|
-
}
|
|
302
|
-
function removeExistingSlide(id: string) {
|
|
303
|
-
if (draft.slides.length === 1) {
|
|
304
|
-
showNotice("A document needs at least one page.", "error");
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
const index = draft.slides.findIndex((item) => item.id === id);
|
|
308
|
-
const next = deleteSlide(draft, id);
|
|
309
|
-
if (next === draft) return;
|
|
310
|
-
const nextActiveSlideId =
|
|
311
|
-
id === slide.id
|
|
312
|
-
? next.slides[Math.min(index, next.slides.length - 1)].id
|
|
313
|
-
: slide.id;
|
|
314
|
-
focusAfterHistory.current = true;
|
|
315
|
-
updateDraft(next, {
|
|
316
|
-
activeSlideId: nextActiveSlideId,
|
|
317
|
-
selected: id === slide.id ? undefined : selected,
|
|
318
|
-
});
|
|
319
|
-
showNotice("Page deleted.");
|
|
320
|
-
}
|
|
321
|
-
function finishEdit() {
|
|
322
|
-
setHistory(finishHistoryEdit);
|
|
323
|
-
}
|
|
324
|
-
function performRedo() {
|
|
325
|
-
focusAfterHistory.current = true;
|
|
326
|
-
setHistory(redoHistory);
|
|
327
|
-
}
|
|
328
|
-
function updateComponent(
|
|
329
|
-
component: CompositionComponent,
|
|
330
|
-
mergeKey?: string,
|
|
331
|
-
) {
|
|
332
|
-
const previous = slide.components.find(
|
|
333
|
-
(candidate) => candidate.id === component.id,
|
|
334
|
-
);
|
|
335
|
-
const nextComponent = previous && (
|
|
336
|
-
previous.preferredRect.x !== component.preferredRect.x ||
|
|
337
|
-
previous.preferredRect.y !== component.preferredRect.y ||
|
|
338
|
-
previous.preferredRect.width !== component.preferredRect.width ||
|
|
339
|
-
previous.preferredRect.height !== component.preferredRect.height
|
|
340
|
-
)
|
|
341
|
-
? transformComponentRect(
|
|
342
|
-
component,
|
|
343
|
-
previous.preferredRect,
|
|
344
|
-
component.preferredRect,
|
|
345
|
-
)
|
|
346
|
-
: component;
|
|
347
|
-
const diagramTypeChanged =
|
|
348
|
-
previous?.kind === "diagram" &&
|
|
349
|
-
nextComponent.kind === "diagram" &&
|
|
350
|
-
previous.appearance.type !== nextComponent.appearance.type;
|
|
351
|
-
updateDraft(
|
|
352
|
-
{
|
|
353
|
-
...draft,
|
|
354
|
-
slides: draft.slides.map((item) =>
|
|
355
|
-
item.id === slide.id
|
|
356
|
-
? {
|
|
357
|
-
...item,
|
|
358
|
-
components: item.components.map((candidate) =>
|
|
359
|
-
candidate.id === nextComponent.id ? nextComponent : candidate,
|
|
360
|
-
),
|
|
361
|
-
contentSlots: item.contentSlots.map((slot) => {
|
|
362
|
-
if (
|
|
363
|
-
!nextComponent.slotIds.includes(slot.id) ||
|
|
364
|
-
"targets" in slot
|
|
365
|
-
)
|
|
366
|
-
return slot;
|
|
367
|
-
if (
|
|
368
|
-
diagramTypeChanged &&
|
|
369
|
-
nextComponent.kind === "diagram" &&
|
|
370
|
-
["process-step", "entity"].includes(slot.role)
|
|
371
|
-
)
|
|
372
|
-
return {
|
|
373
|
-
...slot,
|
|
374
|
-
role: diagramDefinition(nextComponent.appearance.type).slotRole,
|
|
375
|
-
};
|
|
376
|
-
return slot;
|
|
377
|
-
}),
|
|
378
|
-
}
|
|
379
|
-
: item,
|
|
380
|
-
),
|
|
381
|
-
},
|
|
382
|
-
{ mergeKey },
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
function add(
|
|
386
|
-
kind: CompositionComponent["kind"],
|
|
387
|
-
at?: { x: number; y: number },
|
|
388
|
-
) {
|
|
389
|
-
const next = addComponent(draft, kind, at, slide.id);
|
|
390
|
-
if (next === draft) return;
|
|
391
|
-
const nextSlide = getSlide(next, slide.id);
|
|
392
|
-
const id = nextSlide.components.find(
|
|
393
|
-
(component) =>
|
|
394
|
-
!slide.components.some((item) => item.id === component.id),
|
|
395
|
-
)!.id;
|
|
396
|
-
focusAfterHistory.current = true;
|
|
397
|
-
updateDraft(next, { selected: id });
|
|
398
|
-
showNotice(`${componentLabels[kind]} added.`);
|
|
399
|
-
}
|
|
400
|
-
function duplicateSelected() {
|
|
401
|
-
if (!selected) return;
|
|
402
|
-
const next = duplicateComponent(draft, selected, slide.id);
|
|
403
|
-
if (next === draft) return;
|
|
404
|
-
const nextSlide = getSlide(next, slide.id);
|
|
405
|
-
const id = nextSlide.components.find(
|
|
406
|
-
(component) =>
|
|
407
|
-
!slide.components.some((item) => item.id === component.id),
|
|
408
|
-
)!.id;
|
|
409
|
-
focusAfterHistory.current = true;
|
|
410
|
-
updateDraft(next, { selected: id });
|
|
411
|
-
showNotice("Component duplicated.");
|
|
412
|
-
}
|
|
413
|
-
function updatePaintOrder(ids: string[]) {
|
|
414
|
-
const next = reorderPaintOrder(draft, ids, slide.id);
|
|
415
|
-
if (next === draft) return;
|
|
416
|
-
updateDraft(next);
|
|
417
|
-
}
|
|
418
|
-
function deleteSelected() {
|
|
419
|
-
if (!selected) return;
|
|
420
|
-
if (vectorSelection?.componentId === selected) {
|
|
421
|
-
const component = slide.components.find((item) => item.id === selected);
|
|
422
|
-
if (component?.customVisual?.format === "vector" && vectorSelection.elementId) {
|
|
423
|
-
updateComponent({
|
|
424
|
-
...component,
|
|
425
|
-
customVisual: {
|
|
426
|
-
...component.customVisual,
|
|
427
|
-
elements: removeVectorElement(component.customVisual.elements, vectorSelection.elementId),
|
|
428
|
-
},
|
|
429
|
-
});
|
|
430
|
-
setVectorSelection({ componentId: selected });
|
|
431
|
-
}
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
remove(selected);
|
|
435
|
-
}
|
|
436
|
-
function remove(id: string) {
|
|
437
|
-
const issue = componentRemovalIssue(draft, id, slide.id);
|
|
438
|
-
if (issue) {
|
|
439
|
-
showNotice(issue, "error");
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
const index = slide.components.findIndex((component) => component.id === id);
|
|
443
|
-
const next = removeComponent(draft, id, slide.id);
|
|
444
|
-
if (next === draft) return;
|
|
445
|
-
const nextSelected =
|
|
446
|
-
selected === id
|
|
447
|
-
? slide.components[index - 1]?.id ?? slide.components[index + 1]?.id
|
|
448
|
-
: selected;
|
|
449
|
-
focusAfterHistory.current = true;
|
|
450
|
-
updateDraft(next, { selected: nextSelected });
|
|
451
|
-
showNotice("Component deleted.");
|
|
452
|
-
}
|
|
453
|
-
function selectNoteTarget(target: ReviewTarget & { id?: string }) {
|
|
454
|
-
const page = draft.slides.find(s => s.id === target.slideId);
|
|
455
|
-
if (!page) return;
|
|
456
|
-
selectSlide(page.id);
|
|
457
|
-
setLeftCollapsed(false);
|
|
458
|
-
setLeftView("notes");
|
|
459
|
-
const component = page.components.find(c => c.id === target.componentId);
|
|
460
|
-
if (component) {
|
|
461
|
-
selectComponent(component.id);
|
|
462
|
-
if (target.elementId && component.customVisual?.format === "vector" && component.customVisual.elements.some(e => e.id === target.elementId))
|
|
463
|
-
setVectorSelection({ componentId: component.id, elementId: target.elementId });
|
|
464
|
-
}
|
|
465
|
-
if (target.id) requestAnimationFrame(() => document.getElementById(`note-${target.id}`)?.scrollIntoView({ block: "nearest" }));
|
|
466
|
-
}
|
|
467
|
-
async function requestBuild() {
|
|
468
|
-
try {
|
|
469
|
-
const instruction = fileSession.review.notes.some(note => !note.resolved)
|
|
470
|
-
? "Apply the attached revision notes to their named targets. Preserve unrelated human edits. Validate and inspect the result, then acknowledge this request with konpeki finish."
|
|
471
|
-
: selected
|
|
472
|
-
? "Review the selected component in context and build it from the current saved composition."
|
|
473
|
-
: "Review the current slides and build them from the current saved composition.";
|
|
474
|
-
const requested = await fileSession.build(instruction, slide.id, selected, vectorSelection?.componentId === selected ? vectorSelection?.elementId : undefined);
|
|
475
|
-
if (requested) {
|
|
476
|
-
setNotice(undefined);
|
|
477
|
-
setToastClosing(false);
|
|
478
|
-
}
|
|
479
|
-
} catch (requestError) {
|
|
480
|
-
showNotice(requestError instanceof Error ? requestError.message : "Could not send the build request.", "error");
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
async function openComposition(file: File) {
|
|
484
|
-
let raw = "";
|
|
485
|
-
try {
|
|
486
|
-
raw = await file.text();
|
|
487
|
-
} catch {
|
|
488
|
-
showNotice("Could not read that composition file.", "error");
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
const parsed = parseCompositionJSON(raw);
|
|
492
|
-
if (!parsed.ok) {
|
|
493
|
-
showNotice(`Could not open: ${parsed.message}`, "error");
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
focusAfterHistory.current = true;
|
|
497
|
-
updateDraft(parsed.document, {
|
|
498
|
-
activeSlideId: parsed.document.slides[0].id,
|
|
499
|
-
selected: undefined,
|
|
500
|
-
});
|
|
501
|
-
setTitleError(false);
|
|
502
|
-
showNotice("Editable composition opened. Undo restores the previous document.");
|
|
503
|
-
}
|
|
504
|
-
function present() {
|
|
505
|
-
const validation = validateComposition(draft);
|
|
506
|
-
if (!validation.ok) {
|
|
507
|
-
const issue = validation.issues[0];
|
|
508
|
-
showNotice(
|
|
509
|
-
`Cannot present: ${issue?.path || "document"} ${issue?.message || "is invalid"}.`,
|
|
510
|
-
"error",
|
|
511
|
-
);
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
selectComponent();
|
|
515
|
-
setPresenting(true);
|
|
516
|
-
}
|
|
517
|
-
async function downloadPNG() {
|
|
518
|
-
const source = document.querySelector<HTMLElement>(".workspace .canvas");
|
|
519
|
-
if (!source) return;
|
|
520
|
-
setExportBusy(true);
|
|
521
|
-
showNotice("Exporting page…");
|
|
522
|
-
try {
|
|
523
|
-
const blob = await exportPagePNG(source, slide.canvas);
|
|
524
|
-
const url = URL.createObjectURL(blob);
|
|
525
|
-
const link = document.createElement("a");
|
|
526
|
-
link.href = url;
|
|
527
|
-
link.download = `${slide.name.replace(/[^a-z0-9_-]+/gi, "-") || "konpeki-page"}.png`;
|
|
528
|
-
link.click();
|
|
529
|
-
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
530
|
-
showNotice(
|
|
531
|
-
`PNG exported at ${slide.canvas.width} × ${slide.canvas.height}.`,
|
|
532
|
-
);
|
|
533
|
-
} catch (exportError) {
|
|
534
|
-
showNotice(
|
|
535
|
-
`Could not export PNG: ${exportError instanceof Error ? exportError.message : "Unknown error"}`,
|
|
536
|
-
"error",
|
|
537
|
-
);
|
|
538
|
-
} finally {
|
|
539
|
-
setExportBusy(false);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
function downloadJSON() {
|
|
543
|
-
const validation = validateComposition(draft);
|
|
544
|
-
if (!validation.ok) {
|
|
545
|
-
showNotice("Fix the composition before downloading JSON.", "error");
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
const blob = new Blob([canonicalJSON(draft)], { type: "application/json" });
|
|
549
|
-
const url = URL.createObjectURL(blob);
|
|
550
|
-
const link = document.createElement("a");
|
|
551
|
-
link.href = url;
|
|
552
|
-
link.download = `${draft.title.replace(/[^a-z0-9_-]+/gi, "-") || "konpeki-composition"}.json`;
|
|
553
|
-
link.click();
|
|
554
|
-
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
555
|
-
showNotice("Editable composition JSON downloaded.");
|
|
556
|
-
}
|
|
557
|
-
function startBlank() {
|
|
558
|
-
if (!window.confirm(
|
|
559
|
-
"Start with a blank composition? You can undo this replacement until you reload.",
|
|
560
|
-
)) return;
|
|
561
|
-
const next = initialDraft(true);
|
|
562
|
-
focusAfterHistory.current = true;
|
|
563
|
-
updateDraft(next, {
|
|
564
|
-
activeSlideId: next.slides[0].id,
|
|
565
|
-
selected: undefined,
|
|
566
|
-
});
|
|
567
|
-
setTitleError(false);
|
|
568
|
-
showNotice("Blank composition started. Undo restores the previous document.");
|
|
569
|
-
}
|
|
570
|
-
function reset() {
|
|
571
|
-
const target = loaded.exampleName ? "example" : "saved local draft";
|
|
572
|
-
if (!window.confirm(
|
|
573
|
-
`Reset this ${target}? This removes its browser-local working copy.`,
|
|
574
|
-
)) return;
|
|
575
|
-
try {
|
|
576
|
-
if (loaded.exampleName) clearStoredExampleDraft(loaded.exampleName);
|
|
577
|
-
else clearStoredDraft();
|
|
578
|
-
const next = loaded.exampleName
|
|
579
|
-
? structuredClone(exampleDraft(loaded.exampleName)!)
|
|
580
|
-
: initialDraft(true);
|
|
581
|
-
setHistory(
|
|
582
|
-
createHistory({ draft: next, activeSlideId: next.slides[0].id }),
|
|
583
|
-
);
|
|
584
|
-
setBlocked(false);
|
|
585
|
-
setError("");
|
|
586
|
-
setTitleError(false);
|
|
587
|
-
showNotice(loaded.exampleName ? "Example reset." : "Saved draft reset.");
|
|
588
|
-
} catch {
|
|
589
|
-
setBlocked(true);
|
|
590
|
-
setError("Storage remains unavailable. Export your work to keep it.");
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
const browserSaveMessage = !validation.ok
|
|
594
|
-
? `Autosave paused: ${validation.issues[0]?.path || "document"} ${validation.issues[0]?.message || "is invalid"}. Fix this to resume saving.`
|
|
595
|
-
: blocked
|
|
596
|
-
? "Changes are not being saved. Download JSON to keep your work."
|
|
597
|
-
: savedDraft !== draft
|
|
598
|
-
? "Saving changes in this browser…"
|
|
599
|
-
: "Your editable composition is saved only in this browser.";
|
|
600
|
-
const selectedComponent = slide.components.find((c) => c.id === selected);
|
|
601
|
-
const buildState = fileSession.building
|
|
602
|
-
? fileSession.review.request?.status === "working" ? "working" : "ready"
|
|
603
|
-
: undefined;
|
|
604
|
-
function useComponentTool(kind: CompositionComponent["kind"]) {
|
|
605
|
-
add(kind);
|
|
606
|
-
}
|
|
607
|
-
return (
|
|
608
|
-
<>
|
|
609
|
-
<a className="skip-link" href="#canvas-stage">
|
|
610
|
-
Skip to canvas
|
|
611
|
-
</a>
|
|
612
|
-
{error && (
|
|
613
|
-
<div className="recovery" role="alert">
|
|
614
|
-
<span>{error}</span>
|
|
615
|
-
<button type="button" onClick={reset}>
|
|
616
|
-
{loaded.exampleName ? "Reset example" : "Reset saved draft"}
|
|
617
|
-
</button>
|
|
618
|
-
</div>
|
|
619
|
-
)}
|
|
620
|
-
<div
|
|
621
|
-
className={`workspace ${fileSession.building ? "is-building" : ""} ${leftCollapsed ? "left-collapsed" : ""} ${rightCollapsed ? "right-collapsed" : ""}`}
|
|
622
|
-
aria-hidden={presenting || undefined}
|
|
623
|
-
inert={presenting || undefined}
|
|
624
|
-
onDragOver={(event) => {
|
|
625
|
-
if (!event.dataTransfer.types.includes("Files")) return;
|
|
626
|
-
event.preventDefault();
|
|
627
|
-
event.dataTransfer.dropEffect = "copy";
|
|
628
|
-
}}
|
|
629
|
-
onDrop={(event) => {
|
|
630
|
-
if (fileSession.building) {
|
|
631
|
-
event.preventDefault();
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
const file = event.dataTransfer.files[0];
|
|
635
|
-
if (!file) return;
|
|
636
|
-
event.preventDefault();
|
|
637
|
-
void openComposition(file);
|
|
638
|
-
}}
|
|
639
|
-
>
|
|
640
|
-
<WorkspaceChrome
|
|
641
|
-
leftCollapsed={leftCollapsed}
|
|
642
|
-
onToggleLeftPanel={() => setLeftCollapsed(!leftCollapsed)}
|
|
643
|
-
title={draft.title}
|
|
644
|
-
titleRef={title}
|
|
645
|
-
titleError={titleError}
|
|
646
|
-
selectedKind={selectedComponent?.kind}
|
|
647
|
-
onTitle={(value, mergeKey) => {
|
|
648
|
-
updateDraft({ ...draft, title: value }, { mergeKey });
|
|
649
|
-
setTitleError(false);
|
|
650
|
-
}}
|
|
651
|
-
onEditEnd={finishEdit}
|
|
652
|
-
onExportPNG={() => { void downloadPNG(); }}
|
|
653
|
-
exporting={exportBusy}
|
|
654
|
-
onPresent={present}
|
|
655
|
-
fileStatus={loaded.fileSession ? fileSession.status : undefined}
|
|
656
|
-
building={fileSession.building}
|
|
657
|
-
buildState={buildState}
|
|
658
|
-
onBuild={loaded.fileSession ? () => { void requestBuild(); } : undefined}
|
|
659
|
-
browserTools={loaded.fileSession ? undefined : {
|
|
660
|
-
example: Boolean(loaded.exampleName),
|
|
661
|
-
saveMessage: browserSaveMessage,
|
|
662
|
-
onImportJSON: (file) => { void openComposition(file); },
|
|
663
|
-
onDownloadJSON: downloadJSON,
|
|
664
|
-
onStartBlank: startBlank,
|
|
665
|
-
onReset: reset,
|
|
666
|
-
}}
|
|
667
|
-
onSelectTool={() => selectComponent()}
|
|
668
|
-
onComponentTool={useComponentTool}
|
|
669
|
-
>
|
|
670
|
-
<LeftPanel
|
|
671
|
-
draft={draft}
|
|
672
|
-
view={leftView}
|
|
673
|
-
onView={setLeftView}
|
|
674
|
-
revisionNotes={loaded.fileSession && <RevisionNotes
|
|
675
|
-
document={draft}
|
|
676
|
-
target={{ slideId: slide.id, ...(selected ? { componentId: selected } : {}), ...(vectorSelection?.componentId === selected && vectorSelection?.elementId ? { elementId: vectorSelection.elementId } : {}) }}
|
|
677
|
-
review={fileSession.review}
|
|
678
|
-
disabled={fileSession.building || fileSession.status !== "saved"}
|
|
679
|
-
onAdd={fileSession.addNote}
|
|
680
|
-
onRemove={fileSession.removeNote}
|
|
681
|
-
onSelect={selectNoteTarget}
|
|
682
|
-
/>}
|
|
683
|
-
activeSlideId={slide.id}
|
|
684
|
-
collapsed={leftCollapsed}
|
|
685
|
-
onSelectSlide={selectSlide}
|
|
686
|
-
onAddSlide={addNewSlide}
|
|
687
|
-
onRemoveSlide={removeExistingSlide}
|
|
688
|
-
/>
|
|
689
|
-
</WorkspaceChrome>
|
|
690
|
-
<div className="editor-content" inert={fileSession.building} aria-busy={fileSession.building}>
|
|
691
|
-
<Canvas
|
|
692
|
-
key={slide.id}
|
|
693
|
-
revisionNotes={fileSession.review.notes.filter(note => !note.resolved)}
|
|
694
|
-
onSelectNote={selectNoteTarget}
|
|
695
|
-
draft={draft}
|
|
696
|
-
activeSlideId={slide.id}
|
|
697
|
-
selected={selected}
|
|
698
|
-
vectorSelection={vectorSelection}
|
|
699
|
-
onSelect={selectComponent}
|
|
700
|
-
onVectorSelect={(selection, edit) => {
|
|
701
|
-
if (selection) {
|
|
702
|
-
selectComponent(selection.componentId);
|
|
703
|
-
setVectorSelection(selection);
|
|
704
|
-
setRightView("settings");
|
|
705
|
-
if (edit) {
|
|
706
|
-
setRightCollapsed(false);
|
|
707
|
-
setVectorEditRequest((request) => request + 1);
|
|
708
|
-
}
|
|
709
|
-
} else setVectorSelection(undefined);
|
|
710
|
-
}}
|
|
711
|
-
onSlideName={(name) =>
|
|
712
|
-
updateDraft({
|
|
713
|
-
...draft,
|
|
714
|
-
slides: draft.slides.map((item) =>
|
|
715
|
-
item.id === slide.id ? { ...item, name } : item,
|
|
716
|
-
),
|
|
717
|
-
})
|
|
718
|
-
}
|
|
719
|
-
onComponent={updateComponent}
|
|
720
|
-
onDuplicate={(sourceId, rect) => {
|
|
721
|
-
const next = duplicateComponent(draft, sourceId, slide.id);
|
|
722
|
-
if (next === draft) return;
|
|
723
|
-
const nextSlide = getSlide(next, slide.id);
|
|
724
|
-
const copy = nextSlide.components.find(candidate => !slide.components.some(existing => existing.id === candidate.id));
|
|
725
|
-
if (!copy) return;
|
|
726
|
-
const moved = transformComponentRect(copy, copy.preferredRect, rect);
|
|
727
|
-
nextSlide.components = nextSlide.components.map(candidate => candidate.id === copy.id ? moved : candidate);
|
|
728
|
-
setVectorSelection(undefined);
|
|
729
|
-
updateDraft(next, { selected: moved.id, mergeKey: `geometry:${moved.id}` });
|
|
730
|
-
return moved;
|
|
731
|
-
}}
|
|
732
|
-
onEditEnd={finishEdit}
|
|
733
|
-
onAdd={add}
|
|
734
|
-
onNotice={showNotice}
|
|
735
|
-
/>
|
|
736
|
-
<RightPanel
|
|
737
|
-
view={rightView}
|
|
738
|
-
collapsed={rightCollapsed}
|
|
739
|
-
vectorEditRequest={vectorEditRequest}
|
|
740
|
-
component={selectedComponent}
|
|
741
|
-
selectedComponentId={selected}
|
|
742
|
-
selectedVectorElementId={
|
|
743
|
-
vectorSelection && vectorSelection.componentId === selected
|
|
744
|
-
? vectorSelection.elementId
|
|
745
|
-
: undefined
|
|
746
|
-
}
|
|
747
|
-
componentLabel={
|
|
748
|
-
selected
|
|
749
|
-
? componentInstanceLabel(slide.components, selected)
|
|
750
|
-
: undefined
|
|
751
|
-
}
|
|
752
|
-
slide={slide}
|
|
753
|
-
draft={draft}
|
|
754
|
-
onView={setRightView}
|
|
755
|
-
onCollapsedChange={setRightCollapsed}
|
|
756
|
-
onSelectSlide={() => selectComponent()}
|
|
757
|
-
onSelectComponent={selectComponent}
|
|
758
|
-
onSelectVectorElement={(elementId) =>
|
|
759
|
-
selected && setVectorSelection({ componentId: selected, elementId })
|
|
760
|
-
}
|
|
761
|
-
onReorderPaintOrder={updatePaintOrder}
|
|
762
|
-
onComponent={updateComponent}
|
|
763
|
-
onSlide={(nextSlide, mergeKey) =>
|
|
764
|
-
updateDraft(
|
|
765
|
-
{
|
|
766
|
-
...draft,
|
|
767
|
-
slides: draft.slides.map((item) =>
|
|
768
|
-
item.id === nextSlide.id ? nextSlide : item,
|
|
769
|
-
),
|
|
770
|
-
},
|
|
771
|
-
{ mergeKey },
|
|
772
|
-
)
|
|
773
|
-
}
|
|
774
|
-
onDraft={(patch, mergeKey) =>
|
|
775
|
-
updateDraft({ ...draft, ...patch }, { mergeKey })
|
|
776
|
-
}
|
|
777
|
-
onEditEnd={finishEdit}
|
|
778
|
-
/>
|
|
779
|
-
</div>
|
|
780
|
-
{fileSession.building && (
|
|
781
|
-
<div className="stage build-loading" role="status" aria-live="polite">
|
|
782
|
-
<div className="build-loading-card">
|
|
783
|
-
<div className="build-nebula"><BuildOrb active nebula /></div>
|
|
784
|
-
{buildState === "working" ? <>
|
|
785
|
-
<strong>Agent working</strong>
|
|
786
|
-
<span>Updating this composition. Editing resumes when the request finishes.</span>
|
|
787
|
-
</> : <>
|
|
788
|
-
<strong>Request ready</strong>
|
|
789
|
-
<span>Copy the handoff prompt to ask your coding agent to apply these changes.</span>
|
|
790
|
-
<button type="button" className="primary" onClick={async () => {
|
|
791
|
-
try {
|
|
792
|
-
await navigator.clipboard.writeText("Pick up my pending Konpeki request and apply the changes.");
|
|
793
|
-
showNotice("Prompt copied");
|
|
794
|
-
} catch {
|
|
795
|
-
showNotice("Could not copy. Ask your coding agent to pick up your pending Konpeki request", "error");
|
|
796
|
-
}
|
|
797
|
-
}}>Copy prompt</button>
|
|
798
|
-
</>}
|
|
799
|
-
<button type="button" onClick={() => { void fileSession.cancel().catch(error => showNotice(error.message, "error")); }}>Cancel request</button>
|
|
800
|
-
</div>
|
|
801
|
-
</div>
|
|
802
|
-
)}
|
|
803
|
-
</div>
|
|
804
|
-
{presenting && (
|
|
805
|
-
<Presentation
|
|
806
|
-
draft={draft}
|
|
807
|
-
initialSlideId={slide.id}
|
|
808
|
-
onExit={() => setPresenting(false)}
|
|
809
|
-
/>
|
|
810
|
-
)}
|
|
811
|
-
<div
|
|
812
|
-
className={`toast ${notice ? "visible" : ""} ${notice?.tone === "error" ? "error" : ""} ${toastClosing ? "closing" : ""}`}
|
|
813
|
-
>
|
|
814
|
-
<span role={notice?.tone === "error" ? "alert" : "status"} aria-live={notice?.tone === "error" ? "assertive" : "polite"}>
|
|
815
|
-
{notice?.message}
|
|
816
|
-
</span>
|
|
817
|
-
</div>
|
|
818
|
-
</>
|
|
819
|
-
);
|
|
820
|
-
}
|