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
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
- }