konpeki 0.3.1 → 0.4.0

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