konpeki 0.3.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/README.md +56 -39
- package/SETUP.md +35 -19
- package/docs/development.md +11 -3
- package/package.json +3 -1
- package/plugin.json +1 -1
- package/skills/konpeki/scripts/ensure-runtime.mjs +1 -1
- 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 +20 -30
- package/slides/introducing-konpeki/README.md +28 -60
- package/slides/introducing-konpeki/SOURCE.md +13 -20
- package/src/app/App.tsx +63 -38
- package/src/components/Canvas.tsx +8 -2
- package/src/components/InspectorPanel.tsx +11 -27
- 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 +57 -29
- package/src/components/ui.tsx +95 -1
- package/src/lib/use-file-session.ts +60 -38
- package/src/styles/base.css +7 -0
- package/src/styles/chrome.css +103 -12
- package/src/styles/feedback.css +96 -61
- package/src/styles/left-panel.css +27 -6
- package/src/styles/right-panel.css +86 -3
- package/src/styles/shell.css +43 -11
|
@@ -1,40 +1,30 @@
|
|
|
1
1
|
# Public brief excerpt
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
is not the complete conversation record.
|
|
3
|
+
Public requests and authoring decisions; debugging and product-development
|
|
4
|
+
follow-ups are omitted. This is not the complete conversation.
|
|
6
5
|
|
|
7
6
|
> build a pre to introduce konpeki
|
|
8
7
|
|
|
9
|
-
##
|
|
8
|
+
## Creation
|
|
10
9
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
clear hierarchy, no invented metrics or unsupported export claims.
|
|
17
|
-
- Sources: the repository's documented capabilities;
|
|
18
|
-
see SOURCE.md for concrete references and qualifications.
|
|
19
|
-
- Deliverable: composition JSON, browser presentation, reviewed captures.
|
|
10
|
+
- Interpreted “pre” as a six-page English introduction for first-time users.
|
|
11
|
+
- No mode or visual preferences supplied; chose default mode, white, IBM Plex
|
|
12
|
+
Sans, dark ink and Konpeki blue, with restrained diagrams and clear hierarchy.
|
|
13
|
+
- Use documented capabilities, not invented metrics or export claims.
|
|
14
|
+
Deliver editable JSON, browser presentation and reviewed captures.
|
|
20
15
|
|
|
21
|
-
##
|
|
16
|
+
## Revision
|
|
22
17
|
|
|
23
18
|
> review the example composition and revise if needed
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
prerequisites. Do not imply that every agent uses the same slash command.
|
|
37
|
-
- Preserve the six pages, existing Precision/Paper appearance, stable IDs and
|
|
38
|
-
unrelated geometry. Precision uses Noto Sans; the IBM Plex direction above
|
|
39
|
-
records the original creation, not the current theme.
|
|
40
|
-
- Refresh the editable JSON, authoring source, source notes and reviewed captures.
|
|
20
|
+
- Show creation from a brief, with init optional. Canvas-first intent / Build it
|
|
21
|
+
remains an alternative; Konpeki is not primarily a slide explainer.
|
|
22
|
+
- Separate browser-local storage from agent-opened files. Notes and Build it
|
|
23
|
+
require a file session; Copy prompt resumes an idle agent.
|
|
24
|
+
- Use skill installation and user prerequisites, not contributor setup or
|
|
25
|
+
assumptions that every agent supports the same slash command.
|
|
26
|
+
- Retain default mode, six pages, stable IDs, unrelated geometry and the existing
|
|
27
|
+
Precision Paper appearance. Its Noto Sans differs from the original font choice.
|
|
28
|
+
- Refresh JSON, authoring source, source notes and captures.
|
|
29
|
+
|
|
30
|
+
See [SOURCE.md](SOURCE.md) for capability evidence and artwork provenance.
|
|
@@ -1,76 +1,44 @@
|
|
|
1
1
|
# Introducing Konpeki
|
|
2
2
|
|
|
3
|
-
A six-page
|
|
4
|
-
|
|
5
|
-
Paper
|
|
6
|
-
|
|
7
|
-
1. **Meet Konpeki** — create editable visuals with your coding agent.
|
|
8
|
-
2. **Create and revise together** — brief, generate, edit and revise the same file.
|
|
9
|
-
3. **Five semantic components** — Text block, Diagram, Chart, Image and Table.
|
|
10
|
-
4. **Keep the detail editable** — direct editing and targeted agent revision notes.
|
|
11
|
-
5. **Browser or agent editor** — local playground versus file-backed editing.
|
|
12
|
-
6. **Try a real brief** — install the skill and give a one-sentence creation request.
|
|
3
|
+
A six-page introduction covering creation, shared editing, components, revision
|
|
4
|
+
notes, browser/file sessions and a first brief. Uses default authoring mode and
|
|
5
|
+
Precision Paper with theme-linked colors and fonts.
|
|
13
6
|
|
|
14
7
|
## Open and revise
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
Open the [playground example](https://vcfgdev.github.io/konpeki/?example=introducing-konpeki)
|
|
10
|
+
and select **Present**. Edits save to a separate browser-local copy; use
|
|
11
|
+
**Browser → Download JSON** to keep them. **Reset example** restores the bundled
|
|
12
|
+
version, so download your work first. Browser storage is not a backup or agent sync.
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
and **
|
|
23
|
-
|
|
24
|
-
loading an updated example. Browser storage is not a backup and does not sync
|
|
25
|
-
with an agent's file. The playground has no connected agent, Notes or Build it.
|
|
14
|
+
For agent revisions, follow [setup](../../SETUP.md) and open a copy of
|
|
15
|
+
[composition.json](composition.json) in a file-backed preview. Notes, **Build it**
|
|
16
|
+
and **Copy prompt** work there, not in the standalone playground. See the
|
|
17
|
+
[canvas workflow](../../docs/workflow.md) for handoff details.
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Browser and agent revisions use that same file in its preview. Continue feedback
|
|
30
|
-
in agent chat, or use Notes / Build it and Copy prompt as described in the
|
|
31
|
-
[canvas workflow](../../docs/workflow.md). A creation brief selects generate;
|
|
32
|
-
init is optional, not a required first step.
|
|
19
|
+
The JSON is the editable source: double-click text to edit it; diagrams contain
|
|
20
|
+
editable vector elements. [author.ts](author.ts) reconstructs the original deck:
|
|
33
21
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
ordinary text to edit it directly; Content intent remains separate agent guidance.
|
|
38
|
-
This is not a second React deck runtime or a collection of page images.
|
|
22
|
+
```sh
|
|
23
|
+
mise exec -- node slides/introducing-konpeki/author.ts
|
|
24
|
+
```
|
|
39
25
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
it **replaces manual canvas revisions**, so do not rerun it over revised work.
|
|
43
|
-
See [the public brief excerpt](PROMPT.md) and [source facts / provenance](SOURCE.md).
|
|
26
|
+
Run from the repository root. **This overwrites canvas edits.**
|
|
27
|
+
[Brief](PROMPT.md) · [Sources](SOURCE.md) · [Reviewed screenshots](screenshots/)
|
|
44
28
|
|
|
45
29
|
## Review
|
|
46
30
|
|
|
47
|
-
|
|
48
|
-
`mise exec -- pnpm build`, composition validation and `git diff --check`: passed.
|
|
49
|
-
The build retains its existing large-chunk advisory.
|
|
50
|
-
- Inspected all six canvas pages in Precision Paper, Precision Night and Editorial
|
|
51
|
-
Paper at 1920×1080 and 1024×768, DPR2 (36 states). No clipped text or overlapping
|
|
52
|
-
labels. Revised the file/playground wording and kept the revision arrow returning
|
|
53
|
-
from Revise to Edit, not to a new brief.
|
|
54
|
-
- `review.mjs` checked text bounds after font loading and each authored vector
|
|
55
|
-
viewport against its component rectangle within 0.5 screen pixels at both sizes.
|
|
56
|
-
With a dev or preview server and `agent-browser` available, run:
|
|
31
|
+
With a dev/preview server and `agent-browser`, run from the repository root:
|
|
57
32
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
33
|
+
```sh
|
|
34
|
+
mise exec -- node slides/introducing-konpeki/review.mjs <preview-url> /tmp/konpeki-intro Precision paper
|
|
35
|
+
```
|
|
61
36
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
and the chart's illustrative values. Home / ArrowRight navigation reached all
|
|
69
|
-
six pages; Noto Sans weights 400, 500 and 600 loaded. Text on the white canvas
|
|
70
|
-
measures 15.59:1 (ink), 6.91:1 (muted) and 9.54:1 (accent).
|
|
71
|
-
- Final reviewed presentation captures: [screenshots](screenshots/).
|
|
72
|
-
- Chart values are illustrative; diagrams are explanatory artwork, not screenshots.
|
|
73
|
-
- Native agent-client installation, PDF/PPTX, mobile, reduced motion, focus styling
|
|
74
|
-
and cross-application fidelity were not verified by this content review.
|
|
37
|
+
The script checks text bounds and vector alignment at 1920×1080 and 1024×768,
|
|
38
|
+
DPR2. Repeat with `Precision night` and `Editorial paper`, then inspect captures.
|
|
39
|
+
An optional final font ID overrides the palette's default, e.g. `Green paper plex-serif`.
|
|
40
|
+
These 36 states passed the original visual review, alongside validation, typecheck,
|
|
41
|
+
tests and build. Browser review does not verify agent-client installation,
|
|
42
|
+
PDF/PPTX, mobile, reduced motion, focus styling or cross-application fidelity.
|
|
75
43
|
|
|
76
44
|

|
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
# Source facts and provenance
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Product claims come from repository documentation, not a customer case study.
|
|
4
4
|
|
|
5
5
|
| Claim | Repository evidence |
|
|
6
6
|
| --- | --- |
|
|
7
|
-
| Covers, social graphics, charts, diagrams and presentations share
|
|
8
|
-
|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
| Direct text, shape and layout editing with undo/redo | [
|
|
12
|
-
| Diagram, Chart and Table
|
|
13
|
-
|
|
|
14
|
-
| Playground examples save independent browser-local copies; import/download JSON, start blank, reset, PNG and Present are available | [Demo hosting](../../docs/development.md#development-server-and-demo-hosting); [storage implementation](../../src/lib/storage.ts); [browser controls](../../src/components/WorkspaceChrome.tsx) |
|
|
15
|
-
| Playground files do not sync with an agent session; there is no connected AI or cloud storage | [Demo hosting](../../docs/development.md#development-server-and-demo-hosting) |
|
|
16
|
-
| Agent workflow requires Node.js 24+, npm, file/command access and a browser; contributor pnpm setup is separate | [User setup](../../SETUP.md); [development setup](../../docs/development.md) |
|
|
7
|
+
| Covers, social graphics, charts, diagrams and presentations share one canvas | [Product](../../README.md); [authoring](../../AUTHORING.md) |
|
|
8
|
+
| Skill-led creation; init optional; Node.js 24+, npm, file/command access and a browser required | [User setup](../../SETUP.md) |
|
|
9
|
+
| Browser and agent edit the same JSON; file sessions support notes, Build it and Copy prompt | [File-backed workflow](../../docs/workflow.md#file-backed-editing-with-an-agent) |
|
|
10
|
+
| Text block, Diagram, Chart, Image and Table; editable vectors with stable IDs | [Contract](../../composition/README.md); [types](../../composition/types.ts) |
|
|
11
|
+
| Direct text, shape and layout editing with undo/redo | [Operations](../../composition/document.ts) |
|
|
12
|
+
| Diagram, Chart and Table defaults are structural drafts; finished artwork needs source data | [Exports](../../docs/workflow.md#documents-and-exports); [fidelity rules](../../AUTHORING.md#requirements) |
|
|
13
|
+
| Playground: independent local copies, JSON import/download, blank/reset, PNG and Present; no agent sync, connected AI or cloud storage | [Hosting](../../docs/development.md#development-server-and-demo-hosting); [storage](../../src/lib/storage.ts); [controls](../../src/components/WorkspaceChrome.tsx) |
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
theme uses Noto Sans from the repository's Fontsource dependency; theme-linked
|
|
24
|
-
text follows the selected palette's font roles. No PDF/PPTX or cross-application
|
|
25
|
-
fidelity is claimed. This deck does not establish native agent-client installation
|
|
26
|
-
compatibility or an official hosted playground URL.
|
|
15
|
+
Drawings are original explanatory artwork, not screenshots. Chart values and
|
|
16
|
+
the announcement brief are illustrative, not product measurements or customer
|
|
17
|
+
results. No third-party images are used. Precision defaults to bundled Noto Sans;
|
|
18
|
+
text follows the selected font roles. See the [review scope](README.md#review)
|
|
19
|
+
for verification limits.
|
package/src/app/App.tsx
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
type RightPanelView,
|
|
45
45
|
} from "../components/RightPanel.tsx";
|
|
46
46
|
import { WorkspaceChrome } from "../components/WorkspaceChrome.tsx";
|
|
47
|
+
import { FeedbackNotice } from "../components/ui.tsx";
|
|
47
48
|
import { exampleDraft } from "../lib/examples.ts";
|
|
48
49
|
import { exportPagePNG } from "../lib/export-png.ts";
|
|
49
50
|
import { fileSessionToken } from "../lib/file-session.ts";
|
|
@@ -85,12 +86,13 @@ export function App() {
|
|
|
85
86
|
const validation = useMemo(() => validateComposition(draft), [draft]);
|
|
86
87
|
const [savedDraft, setSavedDraft] = useState<Draft>();
|
|
87
88
|
const [blocked, setBlocked] = useState(loaded.storageBlocked);
|
|
89
|
+
const [requiresReset, setRequiresReset] = useState(loaded.storageBlocked);
|
|
88
90
|
const [error, setError] = useState(loaded.error ?? "");
|
|
89
91
|
const [notice, setNotice] = useState<{
|
|
90
92
|
message: string;
|
|
91
93
|
tone: "neutral" | "error";
|
|
92
94
|
}>();
|
|
93
|
-
const [
|
|
95
|
+
const [toastPaused, setToastPaused] = useState(false);
|
|
94
96
|
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
|
95
97
|
const [leftView, setLeftView] = useState<LeftPanelView>("pages");
|
|
96
98
|
const [rightCollapsed, setRightCollapsed] = useState(false);
|
|
@@ -125,6 +127,7 @@ export function App() {
|
|
|
125
127
|
onNotice: showNotice,
|
|
126
128
|
onError: setError,
|
|
127
129
|
});
|
|
130
|
+
const fileLocked = loaded.fileSession && (!fileSession.ready || fileSession.opening);
|
|
128
131
|
useEffect(() => {
|
|
129
132
|
if (!focusAfterHistory.current) return;
|
|
130
133
|
focusAfterHistory.current = false;
|
|
@@ -160,18 +163,10 @@ export function App() {
|
|
|
160
163
|
return () => clearTimeout(timer);
|
|
161
164
|
}, [draft, validation, blocked, loaded.exampleName, loaded.fileSession]);
|
|
162
165
|
useEffect(() => {
|
|
163
|
-
if (!notice) return;
|
|
164
|
-
const timer = window.setTimeout(() =>
|
|
166
|
+
if (!notice || notice.tone === "error" || toastPaused) return;
|
|
167
|
+
const timer = window.setTimeout(() => setNotice(undefined), 3000);
|
|
165
168
|
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]);
|
|
169
|
+
}, [notice, toastPaused]);
|
|
175
170
|
useEffect(() => {
|
|
176
171
|
const narrow = window.matchMedia("(max-width: 1200px)");
|
|
177
172
|
const compact = window.matchMedia("(max-width: 900px)");
|
|
@@ -189,7 +184,7 @@ export function App() {
|
|
|
189
184
|
}, []);
|
|
190
185
|
useEffect(() => {
|
|
191
186
|
function keydown(event: KeyboardEvent) {
|
|
192
|
-
if (presenting || fileSession.building) return;
|
|
187
|
+
if (presenting || fileLocked || fileSession.building) return;
|
|
193
188
|
const modifier = event.metaKey || event.ctrlKey;
|
|
194
189
|
const target = event.target;
|
|
195
190
|
const editable =
|
|
@@ -246,10 +241,18 @@ export function App() {
|
|
|
246
241
|
}
|
|
247
242
|
window.addEventListener("keydown", keydown);
|
|
248
243
|
return () => window.removeEventListener("keydown", keydown);
|
|
249
|
-
}, [draft, presenting, selected, vectorSelection, fileSession.building]);
|
|
244
|
+
}, [draft, presenting, selected, vectorSelection, fileSession.building, fileLocked]);
|
|
250
245
|
function showNotice(message: string, tone: "neutral" | "error" = "neutral") {
|
|
251
|
-
|
|
252
|
-
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);
|
|
253
256
|
}
|
|
254
257
|
function updateDraft(
|
|
255
258
|
next: Draft,
|
|
@@ -454,7 +457,7 @@ export function App() {
|
|
|
454
457
|
const page = draft.slides.find(s => s.id === target.slideId);
|
|
455
458
|
if (!page) return;
|
|
456
459
|
selectSlide(page.id);
|
|
457
|
-
|
|
460
|
+
changeLeftPanel(false);
|
|
458
461
|
setLeftView("notes");
|
|
459
462
|
const component = page.components.find(c => c.id === target.componentId);
|
|
460
463
|
if (component) {
|
|
@@ -474,7 +477,7 @@ export function App() {
|
|
|
474
477
|
const requested = await fileSession.build(instruction, slide.id, selected, vectorSelection?.componentId === selected ? vectorSelection?.elementId : undefined);
|
|
475
478
|
if (requested) {
|
|
476
479
|
setNotice(undefined);
|
|
477
|
-
|
|
480
|
+
setToastPaused(false);
|
|
478
481
|
}
|
|
479
482
|
} catch (requestError) {
|
|
480
483
|
showNotice(requestError instanceof Error ? requestError.message : "Could not send the build request.", "error");
|
|
@@ -582,6 +585,7 @@ export function App() {
|
|
|
582
585
|
createHistory({ draft: next, activeSlideId: next.slides[0].id }),
|
|
583
586
|
);
|
|
584
587
|
setBlocked(false);
|
|
588
|
+
setRequiresReset(false);
|
|
585
589
|
setError("");
|
|
586
590
|
setTitleError(false);
|
|
587
591
|
showNotice(loaded.exampleName ? "Example reset." : "Saved draft reset.");
|
|
@@ -597,6 +601,11 @@ export function App() {
|
|
|
597
601
|
: savedDraft !== draft
|
|
598
602
|
? "Saving changes in this browser…"
|
|
599
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;
|
|
600
609
|
const selectedComponent = slide.components.find((c) => c.id === selected);
|
|
601
610
|
const buildState = fileSession.building
|
|
602
611
|
? fileSession.review.request?.status === "working" ? "working" : "ready"
|
|
@@ -609,25 +618,36 @@ export function App() {
|
|
|
609
618
|
<a className="skip-link" href="#canvas-stage">
|
|
610
619
|
Skip to canvas
|
|
611
620
|
</a>
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
<span>{
|
|
615
|
-
<
|
|
616
|
-
{
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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>
|
|
620
639
|
<div
|
|
621
640
|
className={`workspace ${fileSession.building ? "is-building" : ""} ${leftCollapsed ? "left-collapsed" : ""} ${rightCollapsed ? "right-collapsed" : ""}`}
|
|
622
641
|
aria-hidden={presenting || undefined}
|
|
623
|
-
inert={presenting || undefined}
|
|
642
|
+
inert={presenting || fileLocked || undefined}
|
|
643
|
+
aria-busy={fileSession.opening || undefined}
|
|
624
644
|
onDragOver={(event) => {
|
|
625
645
|
if (!event.dataTransfer.types.includes("Files")) return;
|
|
626
646
|
event.preventDefault();
|
|
627
647
|
event.dataTransfer.dropEffect = "copy";
|
|
628
648
|
}}
|
|
629
649
|
onDrop={(event) => {
|
|
630
|
-
if (fileSession.building) {
|
|
650
|
+
if (fileLocked || fileSession.building) {
|
|
631
651
|
event.preventDefault();
|
|
632
652
|
return;
|
|
633
653
|
}
|
|
@@ -639,7 +659,7 @@ export function App() {
|
|
|
639
659
|
>
|
|
640
660
|
<WorkspaceChrome
|
|
641
661
|
leftCollapsed={leftCollapsed}
|
|
642
|
-
onToggleLeftPanel={() =>
|
|
662
|
+
onToggleLeftPanel={() => changeLeftPanel(!leftCollapsed)}
|
|
643
663
|
title={draft.title}
|
|
644
664
|
titleRef={title}
|
|
645
665
|
titleError={titleError}
|
|
@@ -679,6 +699,7 @@ export function App() {
|
|
|
679
699
|
onAdd={fileSession.addNote}
|
|
680
700
|
onRemove={fileSession.removeNote}
|
|
681
701
|
onSelect={selectNoteTarget}
|
|
702
|
+
onNotice={(message) => showNotice(message, "error")}
|
|
682
703
|
/>}
|
|
683
704
|
activeSlideId={slide.id}
|
|
684
705
|
collapsed={leftCollapsed}
|
|
@@ -703,7 +724,7 @@ export function App() {
|
|
|
703
724
|
setVectorSelection(selection);
|
|
704
725
|
setRightView("settings");
|
|
705
726
|
if (edit) {
|
|
706
|
-
|
|
727
|
+
changeRightPanel(false);
|
|
707
728
|
setVectorEditRequest((request) => request + 1);
|
|
708
729
|
}
|
|
709
730
|
} else setVectorSelection(undefined);
|
|
@@ -752,12 +773,19 @@ export function App() {
|
|
|
752
773
|
slide={slide}
|
|
753
774
|
draft={draft}
|
|
754
775
|
onView={setRightView}
|
|
755
|
-
onCollapsedChange={
|
|
776
|
+
onCollapsedChange={changeRightPanel}
|
|
756
777
|
onSelectSlide={() => selectComponent()}
|
|
757
778
|
onSelectComponent={selectComponent}
|
|
758
779
|
onSelectVectorElement={(elementId) =>
|
|
759
780
|
selected && setVectorSelection({ componentId: selected, elementId })
|
|
760
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
|
+
}}
|
|
761
789
|
onReorderPaintOrder={updatePaintOrder}
|
|
762
790
|
onComponent={updateComponent}
|
|
763
791
|
onSlide={(nextSlide, mergeKey) =>
|
|
@@ -808,13 +836,10 @@ export function App() {
|
|
|
808
836
|
onExit={() => setPresenting(false)}
|
|
809
837
|
/>
|
|
810
838
|
)}
|
|
811
|
-
<
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
{notice?.message}
|
|
816
|
-
</span>
|
|
817
|
-
</div>
|
|
839
|
+
<FeedbackNotice kind="toast" tone={notice?.tone} onPauseChange={setToastPaused}
|
|
840
|
+
onDismiss={() => { setNotice(undefined); setToastPaused(false); }}>
|
|
841
|
+
{notice?.message}
|
|
842
|
+
</FeedbackNotice>
|
|
818
843
|
</>
|
|
819
844
|
);
|
|
820
845
|
}
|
|
@@ -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}
|
|
@@ -34,10 +34,9 @@ import {
|
|
|
34
34
|
import { composerPalette, themeLabel } from "../lib/theme.ts";
|
|
35
35
|
import { getTheme, typographyLabels } from "../../design/themes/index.ts";
|
|
36
36
|
import { DiagramTypeIcon } from "./DiagramTypeIcon.tsx";
|
|
37
|
-
import { Field, Select, humanize } from "./ui.tsx";
|
|
37
|
+
import { Field, NumberField, Select, humanize } from "./ui.tsx";
|
|
38
38
|
import { moveVectorElement, removeVectorElement } from "../../composition/vector.ts";
|
|
39
39
|
import { tokensForAttribute } from "../../composition/theme-tokens.ts";
|
|
40
|
-
import { VectorOverflowWarning } from "./VectorOverflowWarning.tsx";
|
|
41
40
|
import { PageSizePicker } from "./PageSizePicker.tsx";
|
|
42
41
|
|
|
43
42
|
const appearanceLabels: Record<string, string> = {
|
|
@@ -410,7 +409,6 @@ export function InspectorPanel({
|
|
|
410
409
|
})
|
|
411
410
|
}
|
|
412
411
|
/>
|
|
413
|
-
<VectorOverflowWarning slide={slide} theme={draft.theme} />
|
|
414
412
|
<h2 className="slide-settings-heading">Page numbering</h2>
|
|
415
413
|
<fieldset className="page-number-picker">
|
|
416
414
|
<SegmentedControl
|
|
@@ -477,9 +475,9 @@ export function InspectorPanel({
|
|
|
477
475
|
if (key === "x") next.x = clamp(value, 0, slide.canvas.width - rect.width);
|
|
478
476
|
if (key === "y") next.y = clamp(value, 0, slide.canvas.height - rect.height);
|
|
479
477
|
if (key === "width")
|
|
480
|
-
next.width = clamp(value, Math.min(180, slide.canvas.width - rect.x), slide.canvas.width - rect.x);
|
|
478
|
+
next.width = clamp(value, Math.min(180, rect.width, slide.canvas.width - rect.x), slide.canvas.width - rect.x);
|
|
481
479
|
if (key === "height")
|
|
482
|
-
next.height = clamp(value, Math.min(72, slide.canvas.height - rect.y), slide.canvas.height - rect.y);
|
|
480
|
+
next.height = clamp(value, Math.min(72, rect.height, slide.canvas.height - rect.y), slide.canvas.height - rect.y);
|
|
483
481
|
onComponent(
|
|
484
482
|
{ ...component!, preferredRect: next },
|
|
485
483
|
`geometry:${component!.id}`,
|
|
@@ -504,18 +502,17 @@ export function InspectorPanel({
|
|
|
504
502
|
<span>{componentLabel ?? componentLabels[component.kind]}</span>
|
|
505
503
|
</h2>
|
|
506
504
|
{component.kind === "text-block" && !component.customVisual && <>
|
|
507
|
-
<VectorOverflowWarning slide={slide} theme={draft.theme} />
|
|
508
505
|
<Field label="Text"><textarea name="text-content" value={component.content ?? ""}
|
|
509
506
|
onChange={(event) => onComponent({ ...component, content: event.target.value }, `content:${component.id}`)} onBlur={onEditEnd} /></Field>
|
|
510
507
|
<div className="text-typography-fields">
|
|
511
|
-
<
|
|
512
|
-
|
|
508
|
+
<NumberField label="Font size" min={8} max={240} value={component.textStyle?.size ?? 36}
|
|
509
|
+
onCommit={size => onComponent({ ...component, textStyle: { ...component.textStyle, size } }, `font:${component.id}`)} onEditEnd={onEditEnd} />
|
|
513
510
|
<Select label="Text color" value={component.textStyle?.color ?? "ink"} options={["ink", "muted", "accent"]}
|
|
514
511
|
onChange={(color) => onComponent({ ...component, textStyle: { ...component.textStyle, color: color as "ink" | "muted" | "accent" } })} />
|
|
515
512
|
<Select label="Font weight" value={String(component.textStyle?.weight ?? 400)} options={["400", "500", "600"]}
|
|
516
513
|
onChange={(weight) => onComponent({ ...component, textStyle: { ...component.textStyle, weight: Number(weight) as 400 | 500 | 600 } })} />
|
|
517
|
-
<
|
|
518
|
-
|
|
514
|
+
<NumberField label="Line height" min={1} max={3} step={0.1} value={component.textStyle?.lineHeight ?? 1.4}
|
|
515
|
+
onCommit={lineHeight => onComponent({ ...component, textStyle: { ...component.textStyle, lineHeight } }, `leading:${component.id}`)} onEditEnd={onEditEnd} />
|
|
519
516
|
</div>
|
|
520
517
|
</>}
|
|
521
518
|
<Field label={component.kind === "diagram" || component.kind === "chart" ? "What should this explain?" : "Content intent"}>
|
|
@@ -555,23 +552,10 @@ export function InspectorPanel({
|
|
|
555
552
|
<legend>Position & size</legend>
|
|
556
553
|
<div className="geometry">
|
|
557
554
|
{(["x", "y", "width", "height"] as const).map((key) => (
|
|
558
|
-
<
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
autoComplete="off"
|
|
563
|
-
type="number"
|
|
564
|
-
step="1"
|
|
565
|
-
value={Math.round(rect[key])}
|
|
566
|
-
onChange={(e) => {
|
|
567
|
-
if (e.target.value !== "")
|
|
568
|
-
geometry(key, e.target.valueAsNumber);
|
|
569
|
-
}}
|
|
570
|
-
onBlur={onEditEnd}
|
|
571
|
-
/>
|
|
572
|
-
<span className="parameter-unit" aria-hidden="true">px</span>
|
|
573
|
-
</span>
|
|
574
|
-
</Field>
|
|
555
|
+
<NumberField key={key} name={key} label={humanize(key)} unit="px" value={rect[key]}
|
|
556
|
+
min={key === "width" ? Math.min(180, rect.width, slide.canvas.width - rect.x) : key === "height" ? Math.min(72, rect.height, slide.canvas.height - rect.y) : 0}
|
|
557
|
+
max={key === "x" ? slide.canvas.width - rect.width : key === "y" ? slide.canvas.height - rect.height : key === "width" ? slide.canvas.width - rect.x : slide.canvas.height - rect.y}
|
|
558
|
+
onCommit={value => geometry(key, value)} onEditEnd={onEditEnd} />
|
|
575
559
|
))}
|
|
576
560
|
</div>
|
|
577
561
|
</fieldset>
|
|
@@ -3,7 +3,7 @@ import type { CompositionDocument } from "../../composition/types.ts";
|
|
|
3
3
|
import { componentInstanceLabel } from "../lib/model.ts";
|
|
4
4
|
import { activeRequest, type ReviewState, type ReviewTarget } from "../lib/review.ts";
|
|
5
5
|
|
|
6
|
-
export function RevisionNotes({ document, target, review, disabled, onAdd, onRemove, onSelect }: {
|
|
6
|
+
export function RevisionNotes({ document, target, review, disabled, onAdd, onRemove, onSelect, onNotice }: {
|
|
7
7
|
document: CompositionDocument;
|
|
8
8
|
target: ReviewTarget;
|
|
9
9
|
review: ReviewState;
|
|
@@ -11,10 +11,10 @@ export function RevisionNotes({ document, target, review, disabled, onAdd, onRem
|
|
|
11
11
|
onAdd: (target: ReviewTarget, text: string) => Promise<void>;
|
|
12
12
|
onRemove: (id: string) => Promise<void>;
|
|
13
13
|
onSelect: (target: ReviewTarget) => void;
|
|
14
|
+
onNotice: (message: string) => void;
|
|
14
15
|
}) {
|
|
15
16
|
const [drafts, setDrafts] = useState<Record<string, string>>({});
|
|
16
17
|
const [saving, setSaving] = useState(false);
|
|
17
|
-
const [error, setError] = useState("");
|
|
18
18
|
const key = JSON.stringify(target);
|
|
19
19
|
const text = drafts[key] ?? "";
|
|
20
20
|
function label(value: ReviewTarget) {
|
|
@@ -28,16 +28,16 @@ export function RevisionNotes({ document, target, review, disabled, onAdd, onRem
|
|
|
28
28
|
return `${slide.name} · ${name}${value.elementId ? ` · ${element ? value.elementId : "Deleted element"}` : ""}`;
|
|
29
29
|
}
|
|
30
30
|
async function run(action: () => Promise<void>) {
|
|
31
|
-
setSaving(true);
|
|
32
|
-
try { await action(); }
|
|
33
|
-
catch (error) {
|
|
31
|
+
setSaving(true);
|
|
32
|
+
try { await action(); onNotice(""); }
|
|
33
|
+
catch (error) { onNotice(error instanceof Error ? error.message : "Could not save revision note"); }
|
|
34
34
|
finally { setSaving(false); }
|
|
35
35
|
}
|
|
36
36
|
const pending = review.notes.filter(n => !n.resolved);
|
|
37
37
|
return <section className="revision-notes" aria-label="Revision notes">
|
|
38
38
|
<form onSubmit={event => { event.preventDefault(); void run(async () => {
|
|
39
39
|
await onAdd(target, text);
|
|
40
|
-
setDrafts(current => ({ ...current, [key]: "" }));
|
|
40
|
+
setDrafts(current => ({ ...current, [key]: current[key] === text ? "" : current[key] }));
|
|
41
41
|
}); }}>
|
|
42
42
|
<label htmlFor="revision-note">Request a change</label>
|
|
43
43
|
<p className="revision-note-scope">{label(target)}</p>
|
|
@@ -45,7 +45,6 @@ export function RevisionNotes({ document, target, review, disabled, onAdd, onRem
|
|
|
45
45
|
onChange={event => setDrafts(current => ({ ...current, [key]: event.target.value }))} />
|
|
46
46
|
<div className="revision-note-submit"><button type="submit" disabled={disabled || saving || !text.trim()}>{saving ? "Saving…" : "Add note"}</button></div>
|
|
47
47
|
</form>
|
|
48
|
-
{error && <p role="alert" className="revision-note-error">{error}</p>}
|
|
49
48
|
<p className="revision-note-count">{pending.length} pending</p>
|
|
50
49
|
{pending.length > 0 && <ol className="revision-note-list">{pending.map((note, i) => <li key={note.id} id={`note-${note.id}`}>
|
|
51
50
|
<button type="button" className="revision-note-target" onClick={() => onSelect(note)}><span>{i + 1}</span>{label(note)}</button>
|