konpeki 0.2.0 → 0.3.1
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 +1 -1
- package/CONTRIBUTING.md +36 -0
- package/README.md +81 -28
- package/SECURITY.md +22 -0
- package/SETUP.md +128 -51
- package/composition/README.md +16 -3
- package/composition/compile.ts +1 -1
- package/composition/schema.json +9 -0
- package/composition/schema.ts +3 -1
- package/composition/types.ts +3 -1
- package/design/themes/README.md +25 -7
- package/design/themes/index.ts +15 -4
- package/docs/development.md +79 -11
- package/docs/workflow.md +11 -4
- package/index.html +17 -1
- package/package.json +10 -2
- package/plugin.json +22 -0
- package/public/og.png +0 -0
- package/runtime/konpeki.mjs +41 -13
- package/skills/konpeki/SKILL.md +186 -0
- package/skills/konpeki/assets/blank.json +23 -0
- package/skills/konpeki/scripts/ensure-runtime.mjs +69 -0
- package/skills/konpeki/scripts/prepare-document.mjs +40 -0
- package/slides/README.md +35 -132
- package/slides/drawing-references.md +32 -0
- package/slides/github-cover/PROMPT.md +21 -0
- package/slides/github-cover/README.md +21 -0
- package/slides/github-cover/author.ts +67 -0
- package/slides/github-cover/composition.json +595 -0
- package/slides/introducing-konpeki/PROMPT.md +25 -14
- package/slides/introducing-konpeki/README.md +32 -42
- package/slides/introducing-konpeki/SOURCE.md +13 -14
- package/slides/introducing-konpeki/author.ts +44 -42
- package/slides/introducing-konpeki/composition.json +135 -135
- package/src/app/App.tsx +149 -56
- package/src/components/Canvas.tsx +9 -3
- package/src/components/InspectorPanel.tsx +104 -67
- package/src/components/RevisionNotes.tsx +6 -7
- package/src/components/RightPanel.tsx +42 -10
- package/src/components/VectorOverflowWarning.tsx +35 -8
- package/src/components/WorkspaceChrome.tsx +125 -10
- package/src/components/ui.tsx +95 -1
- package/src/lib/storage.ts +38 -10
- package/src/lib/use-file-session.ts +60 -38
- package/src/main.tsx +3 -0
- package/src/styles/base.css +7 -0
- package/src/styles/chrome.css +188 -25
- package/src/styles/feedback.css +101 -49
- package/src/styles/left-panel.css +27 -6
- package/src/styles/right-panel.css +114 -30
- package/src/styles/shell.css +96 -33
- package/.agents/skills/authoring-visuals/SKILL.md +0 -93
package/src/app/App.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { CompositionComponent } from "../../composition/types.ts";
|
|
3
3
|
import { validateComposition } from "../../composition/validate.ts";
|
|
4
4
|
import { removeVectorElement } from "../../composition/vector.ts";
|
|
@@ -25,7 +25,15 @@ import {
|
|
|
25
25
|
redoHistory,
|
|
26
26
|
undoHistory,
|
|
27
27
|
} from "../lib/history.ts";
|
|
28
|
-
import {
|
|
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";
|
|
29
37
|
import { diagramDefinition } from "../../composition/visualizations.ts";
|
|
30
38
|
import { Canvas } from "../components/Canvas.tsx";
|
|
31
39
|
import { BuildOrb } from "../components/BuildOrb.tsx";
|
|
@@ -36,6 +44,7 @@ import {
|
|
|
36
44
|
type RightPanelView,
|
|
37
45
|
} from "../components/RightPanel.tsx";
|
|
38
46
|
import { WorkspaceChrome } from "../components/WorkspaceChrome.tsx";
|
|
47
|
+
import { FeedbackNotice } from "../components/ui.tsx";
|
|
39
48
|
import { exampleDraft } from "../lib/examples.ts";
|
|
40
49
|
import { exportPagePNG } from "../lib/export-png.ts";
|
|
41
50
|
import { fileSessionToken } from "../lib/file-session.ts";
|
|
@@ -50,15 +59,18 @@ function loadInitialDraft() {
|
|
|
50
59
|
return {
|
|
51
60
|
draft: initialDraft(true),
|
|
52
61
|
storageBlocked: false,
|
|
53
|
-
|
|
62
|
+
exampleName: undefined,
|
|
54
63
|
fileSession: true,
|
|
55
64
|
};
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
);
|
|
65
|
+
const exampleName = new URLSearchParams(window.location.search).get("example");
|
|
66
|
+
const example = exampleDraft(exampleName);
|
|
59
67
|
return example
|
|
60
|
-
? {
|
|
61
|
-
|
|
68
|
+
? {
|
|
69
|
+
...loadExampleDraft(exampleName!, example),
|
|
70
|
+
exampleName: exampleName!,
|
|
71
|
+
fileSession: false,
|
|
72
|
+
}
|
|
73
|
+
: { ...loadDraft(), exampleName: undefined, fileSession: false };
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
export function App() {
|
|
@@ -71,13 +83,16 @@ export function App() {
|
|
|
71
83
|
);
|
|
72
84
|
const { draft, selected, activeSlideId } = history.present;
|
|
73
85
|
const slide = getSlide(draft, activeSlideId);
|
|
86
|
+
const validation = useMemo(() => validateComposition(draft), [draft]);
|
|
87
|
+
const [savedDraft, setSavedDraft] = useState<Draft>();
|
|
74
88
|
const [blocked, setBlocked] = useState(loaded.storageBlocked);
|
|
89
|
+
const [requiresReset, setRequiresReset] = useState(loaded.storageBlocked);
|
|
75
90
|
const [error, setError] = useState(loaded.error ?? "");
|
|
76
91
|
const [notice, setNotice] = useState<{
|
|
77
92
|
message: string;
|
|
78
93
|
tone: "neutral" | "error";
|
|
79
94
|
}>();
|
|
80
|
-
const [
|
|
95
|
+
const [toastPaused, setToastPaused] = useState(false);
|
|
81
96
|
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
|
82
97
|
const [leftView, setLeftView] = useState<LeftPanelView>("pages");
|
|
83
98
|
const [rightCollapsed, setRightCollapsed] = useState(false);
|
|
@@ -112,6 +127,7 @@ export function App() {
|
|
|
112
127
|
onNotice: showNotice,
|
|
113
128
|
onError: setError,
|
|
114
129
|
});
|
|
130
|
+
const fileLocked = loaded.fileSession && (!fileSession.ready || fileSession.opening);
|
|
115
131
|
useEffect(() => {
|
|
116
132
|
if (!focusAfterHistory.current) return;
|
|
117
133
|
focusAfterHistory.current = false;
|
|
@@ -130,33 +146,27 @@ export function App() {
|
|
|
130
146
|
}, [history.present]);
|
|
131
147
|
useEffect(() => {
|
|
132
148
|
if (loaded.fileSession) return;
|
|
133
|
-
if (blocked
|
|
134
|
-
if (!
|
|
149
|
+
if (blocked) return;
|
|
150
|
+
if (!validation.ok) return;
|
|
135
151
|
const timer = setTimeout(() => {
|
|
136
152
|
try {
|
|
137
|
-
|
|
153
|
+
if (loaded.exampleName) persistExampleDraft(loaded.exampleName, draft);
|
|
154
|
+
else persistDraft(draft);
|
|
155
|
+
setSavedDraft(draft);
|
|
138
156
|
} catch {
|
|
139
157
|
setError(
|
|
140
|
-
"Unable to save this draft.
|
|
158
|
+
"Unable to save this browser-local draft. Download JSON or reset to retry.",
|
|
141
159
|
);
|
|
142
160
|
setBlocked(true);
|
|
143
161
|
}
|
|
144
162
|
}, 250);
|
|
145
163
|
return () => clearTimeout(timer);
|
|
146
|
-
}, [draft, blocked, loaded.
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
if (!notice) return;
|
|
149
|
-
const timer = window.setTimeout(() => setToastClosing(true), 3000);
|
|
150
|
-
return () => window.clearTimeout(timer);
|
|
151
|
-
}, [notice]);
|
|
164
|
+
}, [draft, validation, blocked, loaded.exampleName, loaded.fileSession]);
|
|
152
165
|
useEffect(() => {
|
|
153
|
-
if (!
|
|
154
|
-
const timer = window.setTimeout(() =>
|
|
155
|
-
setNotice(undefined);
|
|
156
|
-
setToastClosing(false);
|
|
157
|
-
}, 280);
|
|
166
|
+
if (!notice || notice.tone === "error" || toastPaused) return;
|
|
167
|
+
const timer = window.setTimeout(() => setNotice(undefined), 3000);
|
|
158
168
|
return () => window.clearTimeout(timer);
|
|
159
|
-
}, [
|
|
169
|
+
}, [notice, toastPaused]);
|
|
160
170
|
useEffect(() => {
|
|
161
171
|
const narrow = window.matchMedia("(max-width: 1200px)");
|
|
162
172
|
const compact = window.matchMedia("(max-width: 900px)");
|
|
@@ -174,7 +184,7 @@ export function App() {
|
|
|
174
184
|
}, []);
|
|
175
185
|
useEffect(() => {
|
|
176
186
|
function keydown(event: KeyboardEvent) {
|
|
177
|
-
if (presenting || fileSession.building) return;
|
|
187
|
+
if (presenting || fileLocked || fileSession.building) return;
|
|
178
188
|
const modifier = event.metaKey || event.ctrlKey;
|
|
179
189
|
const target = event.target;
|
|
180
190
|
const editable =
|
|
@@ -231,10 +241,18 @@ export function App() {
|
|
|
231
241
|
}
|
|
232
242
|
window.addEventListener("keydown", keydown);
|
|
233
243
|
return () => window.removeEventListener("keydown", keydown);
|
|
234
|
-
}, [draft, presenting, selected, vectorSelection, fileSession.building]);
|
|
244
|
+
}, [draft, presenting, selected, vectorSelection, fileSession.building, fileLocked]);
|
|
235
245
|
function showNotice(message: string, tone: "neutral" | "error" = "neutral") {
|
|
236
|
-
|
|
237
|
-
setNotice({ message: message.replace(/\.$/, ""), tone });
|
|
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);
|
|
238
256
|
}
|
|
239
257
|
function updateDraft(
|
|
240
258
|
next: Draft,
|
|
@@ -439,7 +457,7 @@ export function App() {
|
|
|
439
457
|
const page = draft.slides.find(s => s.id === target.slideId);
|
|
440
458
|
if (!page) return;
|
|
441
459
|
selectSlide(page.id);
|
|
442
|
-
|
|
460
|
+
changeLeftPanel(false);
|
|
443
461
|
setLeftView("notes");
|
|
444
462
|
const component = page.components.find(c => c.id === target.componentId);
|
|
445
463
|
if (component) {
|
|
@@ -459,7 +477,7 @@ export function App() {
|
|
|
459
477
|
const requested = await fileSession.build(instruction, slide.id, selected, vectorSelection?.componentId === selected ? vectorSelection?.elementId : undefined);
|
|
460
478
|
if (requested) {
|
|
461
479
|
setNotice(undefined);
|
|
462
|
-
|
|
480
|
+
setToastPaused(false);
|
|
463
481
|
}
|
|
464
482
|
} catch (requestError) {
|
|
465
483
|
showNotice(requestError instanceof Error ? requestError.message : "Could not send the build request.", "error");
|
|
@@ -524,20 +542,70 @@ export function App() {
|
|
|
524
542
|
setExportBusy(false);
|
|
525
543
|
}
|
|
526
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
|
+
}
|
|
527
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;
|
|
528
578
|
try {
|
|
529
|
-
|
|
530
|
-
|
|
579
|
+
if (loaded.exampleName) clearStoredExampleDraft(loaded.exampleName);
|
|
580
|
+
else clearStoredDraft();
|
|
581
|
+
const next = loaded.exampleName
|
|
582
|
+
? structuredClone(exampleDraft(loaded.exampleName)!)
|
|
583
|
+
: initialDraft(true);
|
|
531
584
|
setHistory(
|
|
532
585
|
createHistory({ draft: next, activeSlideId: next.slides[0].id }),
|
|
533
586
|
);
|
|
534
587
|
setBlocked(false);
|
|
588
|
+
setRequiresReset(false);
|
|
535
589
|
setError("");
|
|
536
|
-
|
|
590
|
+
setTitleError(false);
|
|
591
|
+
showNotice(loaded.exampleName ? "Example reset." : "Saved draft reset.");
|
|
537
592
|
} catch {
|
|
593
|
+
setBlocked(true);
|
|
538
594
|
setError("Storage remains unavailable. Export your work to keep it.");
|
|
539
595
|
}
|
|
540
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;
|
|
541
609
|
const selectedComponent = slide.components.find((c) => c.id === selected);
|
|
542
610
|
const buildState = fileSession.building
|
|
543
611
|
? fileSession.review.request?.status === "working" ? "working" : "ready"
|
|
@@ -550,25 +618,36 @@ export function App() {
|
|
|
550
618
|
<a className="skip-link" href="#canvas-stage">
|
|
551
619
|
Skip to canvas
|
|
552
620
|
</a>
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
<span>{
|
|
556
|
-
<
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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>
|
|
561
639
|
<div
|
|
562
640
|
className={`workspace ${fileSession.building ? "is-building" : ""} ${leftCollapsed ? "left-collapsed" : ""} ${rightCollapsed ? "right-collapsed" : ""}`}
|
|
563
641
|
aria-hidden={presenting || undefined}
|
|
564
|
-
inert={presenting || undefined}
|
|
642
|
+
inert={presenting || fileLocked || undefined}
|
|
643
|
+
aria-busy={fileSession.opening || undefined}
|
|
565
644
|
onDragOver={(event) => {
|
|
566
645
|
if (!event.dataTransfer.types.includes("Files")) return;
|
|
567
646
|
event.preventDefault();
|
|
568
647
|
event.dataTransfer.dropEffect = "copy";
|
|
569
648
|
}}
|
|
570
649
|
onDrop={(event) => {
|
|
571
|
-
if (fileSession.building) {
|
|
650
|
+
if (fileLocked || fileSession.building) {
|
|
572
651
|
event.preventDefault();
|
|
573
652
|
return;
|
|
574
653
|
}
|
|
@@ -580,7 +659,7 @@ export function App() {
|
|
|
580
659
|
>
|
|
581
660
|
<WorkspaceChrome
|
|
582
661
|
leftCollapsed={leftCollapsed}
|
|
583
|
-
onToggleLeftPanel={() =>
|
|
662
|
+
onToggleLeftPanel={() => changeLeftPanel(!leftCollapsed)}
|
|
584
663
|
title={draft.title}
|
|
585
664
|
titleRef={title}
|
|
586
665
|
titleError={titleError}
|
|
@@ -597,10 +676,17 @@ export function App() {
|
|
|
597
676
|
building={fileSession.building}
|
|
598
677
|
buildState={buildState}
|
|
599
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
|
+
}}
|
|
600
687
|
onSelectTool={() => selectComponent()}
|
|
601
688
|
onComponentTool={useComponentTool}
|
|
602
|
-
|
|
603
|
-
<div className="editor-content" inert={fileSession.building} aria-busy={fileSession.building}>
|
|
689
|
+
>
|
|
604
690
|
<LeftPanel
|
|
605
691
|
draft={draft}
|
|
606
692
|
view={leftView}
|
|
@@ -613,6 +699,7 @@ export function App() {
|
|
|
613
699
|
onAdd={fileSession.addNote}
|
|
614
700
|
onRemove={fileSession.removeNote}
|
|
615
701
|
onSelect={selectNoteTarget}
|
|
702
|
+
onNotice={(message) => showNotice(message, "error")}
|
|
616
703
|
/>}
|
|
617
704
|
activeSlideId={slide.id}
|
|
618
705
|
collapsed={leftCollapsed}
|
|
@@ -620,6 +707,8 @@ export function App() {
|
|
|
620
707
|
onAddSlide={addNewSlide}
|
|
621
708
|
onRemoveSlide={removeExistingSlide}
|
|
622
709
|
/>
|
|
710
|
+
</WorkspaceChrome>
|
|
711
|
+
<div className="editor-content" inert={fileSession.building} aria-busy={fileSession.building}>
|
|
623
712
|
<Canvas
|
|
624
713
|
key={slide.id}
|
|
625
714
|
revisionNotes={fileSession.review.notes.filter(note => !note.resolved)}
|
|
@@ -635,7 +724,7 @@ export function App() {
|
|
|
635
724
|
setVectorSelection(selection);
|
|
636
725
|
setRightView("settings");
|
|
637
726
|
if (edit) {
|
|
638
|
-
|
|
727
|
+
changeRightPanel(false);
|
|
639
728
|
setVectorEditRequest((request) => request + 1);
|
|
640
729
|
}
|
|
641
730
|
} else setVectorSelection(undefined);
|
|
@@ -684,12 +773,19 @@ export function App() {
|
|
|
684
773
|
slide={slide}
|
|
685
774
|
draft={draft}
|
|
686
775
|
onView={setRightView}
|
|
687
|
-
onCollapsedChange={
|
|
776
|
+
onCollapsedChange={changeRightPanel}
|
|
688
777
|
onSelectSlide={() => selectComponent()}
|
|
689
778
|
onSelectComponent={selectComponent}
|
|
690
779
|
onSelectVectorElement={(elementId) =>
|
|
691
780
|
selected && setVectorSelection({ componentId: selected, elementId })
|
|
692
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
|
+
}}
|
|
693
789
|
onReorderPaintOrder={updatePaintOrder}
|
|
694
790
|
onComponent={updateComponent}
|
|
695
791
|
onSlide={(nextSlide, mergeKey) =>
|
|
@@ -740,13 +836,10 @@ export function App() {
|
|
|
740
836
|
onExit={() => setPresenting(false)}
|
|
741
837
|
/>
|
|
742
838
|
)}
|
|
743
|
-
<
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
{notice?.message}
|
|
748
|
-
</span>
|
|
749
|
-
</div>
|
|
839
|
+
<FeedbackNotice kind="toast" tone={notice?.tone} onPauseChange={setToastPaused}
|
|
840
|
+
onDismiss={() => { setNotice(undefined); setToastPaused(false); }}>
|
|
841
|
+
{notice?.message}
|
|
842
|
+
</FeedbackNotice>
|
|
750
843
|
</>
|
|
751
844
|
);
|
|
752
845
|
}
|
|
@@ -760,7 +760,7 @@ export function Canvas({
|
|
|
760
760
|
const themeId = draft.theme?.id ?? "plex";
|
|
761
761
|
const themeMode = draft.theme?.mode ?? "paper";
|
|
762
762
|
const palette = composerPalette(themeId, themeMode);
|
|
763
|
-
const typography = getTheme(themeLabel(themeId), themeMode);
|
|
763
|
+
const typography = getTheme(themeLabel(themeId), themeMode, draft.theme?.typography);
|
|
764
764
|
const canvasStyle = {
|
|
765
765
|
"--component-layers": slide.paintOrder.length,
|
|
766
766
|
"--slide-bg": palette.bg,
|
|
@@ -791,8 +791,10 @@ export function Canvas({
|
|
|
791
791
|
if (
|
|
792
792
|
event.target instanceof Element &&
|
|
793
793
|
!event.target.closest(".canvas")
|
|
794
|
-
)
|
|
794
|
+
) {
|
|
795
|
+
event.currentTarget.focus({ preventScroll: true });
|
|
795
796
|
onSelect();
|
|
797
|
+
}
|
|
796
798
|
}}
|
|
797
799
|
>
|
|
798
800
|
<div className="slide-wrap">
|
|
@@ -802,6 +804,7 @@ export function Canvas({
|
|
|
802
804
|
aria-label="Page name"
|
|
803
805
|
autoFocus
|
|
804
806
|
value={nextSlideName}
|
|
807
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
805
808
|
onChange={(event) => setNextSlideName(event.target.value)}
|
|
806
809
|
onBlur={() => {
|
|
807
810
|
const name = nextSlideName.trim();
|
|
@@ -850,7 +853,10 @@ export function Canvas({
|
|
|
850
853
|
style={canvasStyle}
|
|
851
854
|
aria-label="Page canvas"
|
|
852
855
|
onPointerDown={(e) => {
|
|
853
|
-
if (interactive && e.target === e.currentTarget)
|
|
856
|
+
if (interactive && e.target === e.currentTarget) {
|
|
857
|
+
e.currentTarget.closest<HTMLElement>(".stage")?.focus({ preventScroll: true });
|
|
858
|
+
onSelect();
|
|
859
|
+
}
|
|
854
860
|
}}
|
|
855
861
|
onPointerMove={interactive ? move : undefined}
|
|
856
862
|
onPointerUp={interactive ? () => end() : undefined}
|