konpeki 0.1.0 → 0.1.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 +25 -24
- package/SETUP.md +1 -1
- package/docs/development.md +53 -0
- package/package.json +1 -1
- package/src/app/App.tsx +26 -42
- package/src/components/InspectorPanel.tsx +5 -6
- package/src/components/WorkspaceChrome.tsx +7 -5
- package/src/lib/use-file-session.ts +43 -14
- package/src/styles/chrome.css +51 -3
- package/src/styles/feedback.css +29 -27
- package/src/styles/shell.css +39 -0
package/README.md
CHANGED
|
@@ -15,32 +15,15 @@ editable JSON, or use **Present** for a chrome-free presentation.
|
|
|
15
15
|
Requires **Node.js 24+**, npm, a coding agent that can edit files and run commands,
|
|
16
16
|
and a browser.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```sh
|
|
22
|
-
mkdir konpeki-workspace
|
|
23
|
-
cd konpeki-workspace
|
|
24
|
-
npm init -y
|
|
25
|
-
npm install --save-dev konpeki@0.1.0
|
|
26
|
-
cp node_modules/konpeki/slides/introducing-konpeki/composition.json introduction.json
|
|
27
|
-
npm exec --no -- konpeki preview introduction.json
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Open the exact URL printed by `preview`. Browser edits save to the composition
|
|
31
|
-
file; valid agent edits appear on the same canvas. In a remote environment, use
|
|
32
|
-
its authenticated preview mechanism rather than sharing a local address.
|
|
33
|
-
|
|
34
|
-
Open the workspace in your coding agent and ask it to read
|
|
35
|
-
`node_modules/konpeki/SETUP.md`. The installed package includes the authoring skill,
|
|
36
|
-
design guidance and examples; skills inside dependencies may need to be read
|
|
37
|
-
explicitly. Keep your documents outside `node_modules`. Give your brief in the
|
|
38
|
-
agent's prompt field:
|
|
18
|
+
Give your coding agent the public setup URL and your brief. The guide covers
|
|
19
|
+
installation; no repository clone or manual package installation is needed first:
|
|
39
20
|
|
|
40
21
|
```text
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
22
|
+
Read https://raw.githubusercontent.com/vcfgdev/konpeki/main/SETUP.md
|
|
23
|
+
and set up Konpeki in this workspace.
|
|
24
|
+
Turn these notes into a three-slide explanation for engineers. Make the request
|
|
25
|
+
flow and failure handling easy to follow. Preserve facts and caveats. Save editable composition
|
|
26
|
+
JSON, open the preview, then render, inspect and fix the result.
|
|
44
27
|
|
|
45
28
|
[Paste notes or provide source files.]
|
|
46
29
|
```
|
|
@@ -50,6 +33,24 @@ approval” when you want a checkpoint. Supply a visual direction or leave it op
|
|
|
50
33
|
[authoring modes](AUTHORING.md#authoring-mode) provide defaults without requiring
|
|
51
34
|
you to choose fonts, colors or layouts first.
|
|
52
35
|
|
|
36
|
+
The package includes the authoring skill, design guidance and examples—no GitHub
|
|
37
|
+
clone is required. Keep your documents outside `node_modules`.
|
|
38
|
+
|
|
39
|
+
## Try an editable example
|
|
40
|
+
|
|
41
|
+
For a manual start, install [Konpeki from npm](https://www.npmjs.com/package/konpeki)
|
|
42
|
+
in your workspace (run `npm init -y` first in a new, empty directory):
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npm install --save-dev konpeki@0.1.1
|
|
46
|
+
cp node_modules/konpeki/slides/introducing-konpeki/composition.json introduction.json
|
|
47
|
+
npm exec --no -- konpeki preview introduction.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Open the exact URL printed by `preview`. Browser edits save to your copied file;
|
|
51
|
+
valid agent edits appear on the same canvas. In a remote environment, use its
|
|
52
|
+
authenticated preview mechanism rather than sharing a local address.
|
|
53
|
+
|
|
53
54
|
## Examples and guides
|
|
54
55
|
|
|
55
56
|
- [Example gallery](slides/README.md): 18 fictional examples and an editable
|
package/SETUP.md
CHANGED
|
@@ -25,7 +25,7 @@ agent. An in-app browser is convenient but not required.
|
|
|
25
25
|
reuse the same CLI version. Do not edit files inside `node_modules`.
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
npm install --save-dev konpeki@0.1.
|
|
28
|
+
npm install --save-dev konpeki@0.1.1
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
In a new empty directory, run `npm init -y` first. Run subsequent commands from
|
package/docs/development.md
CHANGED
|
@@ -101,6 +101,54 @@ commands against a composition outside the installed package. Do not publish
|
|
|
101
101
|
until that isolated preview works. Publish the tested tarball rather than
|
|
102
102
|
rebuilding during publication. Packing locally does not publish anything.
|
|
103
103
|
|
|
104
|
+
## Tag releases
|
|
105
|
+
|
|
106
|
+
`.github/workflows/publish.yml` stages releases on bare version tags such as `0.1.1`
|
|
107
|
+
(no `v` prefix). The tag must equal `package.json`'s version.
|
|
108
|
+
The workflow installs the mise toolchain and frozen dependencies, runs typecheck,
|
|
109
|
+
tests, build and package checks, then installs a tarball in an isolated directory
|
|
110
|
+
to validate a composition and build the packaged canvas. It stages that same
|
|
111
|
+
tarball for maintainer approval; it does not publish directly. Browser review
|
|
112
|
+
remains a pre-release responsibility.
|
|
113
|
+
|
|
114
|
+
Before the first tag release, configure **Trusted publishing → GitHub Actions**
|
|
115
|
+
in the `konpeki` package settings on npmjs.com:
|
|
116
|
+
|
|
117
|
+
- Organization or user: `vcfgdev`
|
|
118
|
+
- Repository: `konpeki`
|
|
119
|
+
- Workflow filename: `publish.yml`
|
|
120
|
+
- Environment name: leave empty
|
|
121
|
+
- Leave **Allow npm publish** unchecked (staged publishing only)
|
|
122
|
+
|
|
123
|
+
The workflow uses GitHub-hosted runners and OIDC (`id-token: write`); no npm
|
|
124
|
+
token secret is needed. Staged publishing requires npm 11.15.0 or newer and
|
|
125
|
+
Node 22.14.0 or newer; the pinned toolchain meets both requirements.
|
|
126
|
+
npm generates provenance automatically for public repositories;
|
|
127
|
+
private repositories do not receive provenance.
|
|
128
|
+
|
|
129
|
+
After updating the package version, completing release checks and pushing the
|
|
130
|
+
release commit, explicitly create and push its matching tag:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
git tag 0.1.1
|
|
134
|
+
git push origin 0.1.1
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Replace `0.1.1` with the new version. `0.1.0` is already published and cannot be
|
|
138
|
+
republished. Pushing a matching tag submits the tested package to npm's staging
|
|
139
|
+
area. After the workflow succeeds, review the release in npmjs.com's **Staged
|
|
140
|
+
Packages** tab and click **Approve**, completing 2FA to publish it. Alternatively,
|
|
141
|
+
use an authenticated local CLI:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
mise exec -- npm stage list konpeki
|
|
145
|
+
mise exec -- npm stage view <stage-id>
|
|
146
|
+
mise exec -- npm stage approve <stage-id>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Approval makes the version public. Reject an incorrect staged release instead
|
|
150
|
+
of approving it (`npm stage reject <stage-id>`).
|
|
151
|
+
|
|
104
152
|
## Verification
|
|
105
153
|
|
|
106
154
|
```sh
|
|
@@ -117,6 +165,11 @@ node scripts/check-canvas.mjs http://localhost:4318 .amp/in/artifacts
|
|
|
117
165
|
node scripts/check-pages.mjs http://localhost:4318 .amp/in/artifacts/pages
|
|
118
166
|
```
|
|
119
167
|
|
|
168
|
+
For the Build it lifecycle, `node scripts/check-build.mjs` starts its own
|
|
169
|
+
disposable file session. It checks pending requests, agent refresh, failure
|
|
170
|
+
recovery and reduced motion, and captures the affected states. Add an output
|
|
171
|
+
directory and `--record` to also record the animation.
|
|
172
|
+
|
|
120
173
|
These checks exercise all five component kinds, empty slides, JSON round trips,
|
|
121
174
|
vector editing/history and fitted line dragging. They capture editor and
|
|
122
175
|
presentation states at two sizes; inspect the images because assertions alone
|
package/package.json
CHANGED
package/src/app/App.tsx
CHANGED
|
@@ -72,7 +72,6 @@ export function App() {
|
|
|
72
72
|
const [error, setError] = useState(loaded.error ?? "");
|
|
73
73
|
const [notice, setNotice] = useState<{
|
|
74
74
|
message: string;
|
|
75
|
-
undoableFor?: Draft;
|
|
76
75
|
}>();
|
|
77
76
|
const [toastClosing, setToastClosing] = useState(false);
|
|
78
77
|
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
|
@@ -80,7 +79,6 @@ export function App() {
|
|
|
80
79
|
const [rightView, setRightView] = useState<RightPanelView>("settings");
|
|
81
80
|
const [vectorEditRequest, setVectorEditRequest] = useState(0);
|
|
82
81
|
const [presenting, setPresenting] = useState(false);
|
|
83
|
-
const [buildBusy, setBuildBusy] = useState(false);
|
|
84
82
|
const [exportBusy, setExportBusy] = useState(false);
|
|
85
83
|
const [vectorSelection, setVectorSelection] = useState<{
|
|
86
84
|
componentId: string;
|
|
@@ -105,7 +103,6 @@ export function App() {
|
|
|
105
103
|
) ? current.present.activeSlideId : document.slides[0].id,
|
|
106
104
|
selected: undefined,
|
|
107
105
|
}));
|
|
108
|
-
showNotice("Agent changes loaded. Undo restores the previous composition.");
|
|
109
106
|
},
|
|
110
107
|
onNotice: showNotice,
|
|
111
108
|
onError: setError,
|
|
@@ -144,7 +141,7 @@ export function App() {
|
|
|
144
141
|
}, [draft, blocked, loaded.example, loaded.fileSession]);
|
|
145
142
|
useEffect(() => {
|
|
146
143
|
if (!notice) return;
|
|
147
|
-
const timer = window.setTimeout(() => setToastClosing(true),
|
|
144
|
+
const timer = window.setTimeout(() => setToastClosing(true), 3000);
|
|
148
145
|
return () => window.clearTimeout(timer);
|
|
149
146
|
}, [notice]);
|
|
150
147
|
useEffect(() => {
|
|
@@ -157,7 +154,7 @@ export function App() {
|
|
|
157
154
|
}, [toastClosing]);
|
|
158
155
|
useEffect(() => {
|
|
159
156
|
function keydown(event: KeyboardEvent) {
|
|
160
|
-
if (presenting) return;
|
|
157
|
+
if (presenting || fileSession.building) return;
|
|
161
158
|
const modifier = event.metaKey || event.ctrlKey;
|
|
162
159
|
const target = event.target;
|
|
163
160
|
const editable =
|
|
@@ -213,10 +210,10 @@ export function App() {
|
|
|
213
210
|
}
|
|
214
211
|
window.addEventListener("keydown", keydown);
|
|
215
212
|
return () => window.removeEventListener("keydown", keydown);
|
|
216
|
-
}, [draft, presenting, selected, vectorSelection]);
|
|
217
|
-
function showNotice(message: string
|
|
213
|
+
}, [draft, presenting, selected, vectorSelection, fileSession.building]);
|
|
214
|
+
function showNotice(message: string) {
|
|
218
215
|
setToastClosing(false);
|
|
219
|
-
setNotice({ message
|
|
216
|
+
setNotice({ message: message.replace(/\.$/, "") });
|
|
220
217
|
}
|
|
221
218
|
function updateDraft(
|
|
222
219
|
next: Draft,
|
|
@@ -283,15 +280,11 @@ export function App() {
|
|
|
283
280
|
activeSlideId: nextActiveSlideId,
|
|
284
281
|
selected: id === slide.id ? undefined : selected,
|
|
285
282
|
});
|
|
286
|
-
showNotice("Page deleted."
|
|
283
|
+
showNotice("Page deleted.");
|
|
287
284
|
}
|
|
288
285
|
function finishEdit() {
|
|
289
286
|
setHistory(finishHistoryEdit);
|
|
290
287
|
}
|
|
291
|
-
function performUndo() {
|
|
292
|
-
focusAfterHistory.current = true;
|
|
293
|
-
setHistory(undoHistory);
|
|
294
|
-
}
|
|
295
288
|
function performRedo() {
|
|
296
289
|
focusAfterHistory.current = true;
|
|
297
290
|
setHistory(redoHistory);
|
|
@@ -419,20 +412,16 @@ export function App() {
|
|
|
419
412
|
: selected;
|
|
420
413
|
focusAfterHistory.current = true;
|
|
421
414
|
updateDraft(next, { selected: nextSelected });
|
|
422
|
-
showNotice("Component deleted."
|
|
415
|
+
showNotice("Component deleted.");
|
|
423
416
|
}
|
|
424
417
|
async function requestBuild() {
|
|
425
|
-
setBuildBusy(true);
|
|
426
418
|
try {
|
|
427
419
|
const instruction = selected
|
|
428
420
|
? "Review the selected component in context and build it from the current saved composition."
|
|
429
421
|
: "Review the current slides and build them from the current saved composition.";
|
|
430
|
-
|
|
431
|
-
showNotice("Build request sent. The agent will reread this saved revision.");
|
|
422
|
+
await fileSession.build(instruction, slide.id, selected);
|
|
432
423
|
} catch (requestError) {
|
|
433
424
|
showNotice(requestError instanceof Error ? requestError.message : "Could not send the build request.");
|
|
434
|
-
} finally {
|
|
435
|
-
setBuildBusy(false);
|
|
436
425
|
}
|
|
437
426
|
}
|
|
438
427
|
async function openComposition(file: File) {
|
|
@@ -454,7 +443,7 @@ export function App() {
|
|
|
454
443
|
selected: undefined,
|
|
455
444
|
});
|
|
456
445
|
setTitleError(false);
|
|
457
|
-
showNotice("Editable composition opened. Undo restores the previous document."
|
|
446
|
+
showNotice("Editable composition opened. Undo restores the previous document.");
|
|
458
447
|
}
|
|
459
448
|
function present() {
|
|
460
449
|
const validation = validateComposition(draft);
|
|
@@ -524,7 +513,7 @@ export function App() {
|
|
|
524
513
|
</div>
|
|
525
514
|
)}
|
|
526
515
|
<div
|
|
527
|
-
className={`workspace ${leftCollapsed ? "left-collapsed" : ""} ${rightCollapsed ? "right-collapsed" : ""}`}
|
|
516
|
+
className={`workspace ${fileSession.building ? "is-building" : ""} ${leftCollapsed ? "left-collapsed" : ""} ${rightCollapsed ? "right-collapsed" : ""}`}
|
|
528
517
|
aria-hidden={presenting || undefined}
|
|
529
518
|
inert={presenting || undefined}
|
|
530
519
|
onDragOver={(event) => {
|
|
@@ -533,6 +522,10 @@ export function App() {
|
|
|
533
522
|
event.dataTransfer.dropEffect = "copy";
|
|
534
523
|
}}
|
|
535
524
|
onDrop={(event) => {
|
|
525
|
+
if (fileSession.building) {
|
|
526
|
+
event.preventDefault();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
536
529
|
const file = event.dataTransfer.files[0];
|
|
537
530
|
if (!file) return;
|
|
538
531
|
event.preventDefault();
|
|
@@ -555,11 +548,12 @@ export function App() {
|
|
|
555
548
|
exporting={exportBusy}
|
|
556
549
|
onPresent={present}
|
|
557
550
|
fileStatus={loaded.fileSession ? fileSession.status : undefined}
|
|
558
|
-
building={
|
|
551
|
+
building={fileSession.building}
|
|
559
552
|
onBuild={loaded.fileSession ? () => { void requestBuild(); } : undefined}
|
|
560
553
|
onSelectTool={() => selectComponent()}
|
|
561
554
|
onComponentTool={useComponentTool}
|
|
562
555
|
/>
|
|
556
|
+
<div className="editor-content" inert={fileSession.building} aria-busy={fileSession.building}>
|
|
563
557
|
<LeftPanel
|
|
564
558
|
draft={draft}
|
|
565
559
|
activeSlideId={slide.id}
|
|
@@ -654,6 +648,16 @@ export function App() {
|
|
|
654
648
|
}
|
|
655
649
|
onEditEnd={finishEdit}
|
|
656
650
|
/>
|
|
651
|
+
</div>
|
|
652
|
+
{fileSession.building && (
|
|
653
|
+
<div className="stage build-loading" role="status" aria-live="polite">
|
|
654
|
+
<div className="build-loading-card">
|
|
655
|
+
<div className="build-loading-track" aria-hidden="true" />
|
|
656
|
+
<strong>Waiting for agent</strong>
|
|
657
|
+
<span>Your canvas will refresh when changes arrive.</span>
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
)}
|
|
657
661
|
</div>
|
|
658
662
|
{presenting && (
|
|
659
663
|
<Presentation
|
|
@@ -668,26 +672,6 @@ export function App() {
|
|
|
668
672
|
<span role="status" aria-live="polite">
|
|
669
673
|
{notice?.message}
|
|
670
674
|
</span>
|
|
671
|
-
{notice?.undoableFor === draft && (
|
|
672
|
-
<button
|
|
673
|
-
type="button"
|
|
674
|
-
onClick={() => {
|
|
675
|
-
performUndo();
|
|
676
|
-
showNotice("Change undone.");
|
|
677
|
-
}}
|
|
678
|
-
>
|
|
679
|
-
Undo
|
|
680
|
-
</button>
|
|
681
|
-
)}
|
|
682
|
-
{notice && (
|
|
683
|
-
<button
|
|
684
|
-
type="button"
|
|
685
|
-
aria-label="Dismiss notification"
|
|
686
|
-
onClick={() => setToastClosing(true)}
|
|
687
|
-
>
|
|
688
|
-
×
|
|
689
|
-
</button>
|
|
690
|
-
)}
|
|
691
675
|
</div>
|
|
692
676
|
</>
|
|
693
677
|
);
|
|
@@ -502,7 +502,6 @@ export function InspectorPanel({
|
|
|
502
502
|
{component.customVisual && (
|
|
503
503
|
<fieldset className="custom-visual-summary">
|
|
504
504
|
<legend>Agent visual</legend>
|
|
505
|
-
<p>{component.customVisual.description}</p>
|
|
506
505
|
<dl>
|
|
507
506
|
<div>
|
|
508
507
|
<dt>Source</dt>
|
|
@@ -523,11 +522,11 @@ export function InspectorPanel({
|
|
|
523
522
|
<dd>{component.customVisual.fit ?? "contain"}</dd>
|
|
524
523
|
</div>
|
|
525
524
|
</dl>
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
525
|
+
{component.customVisual.format !== "vector" && (
|
|
526
|
+
<p className="field-help">
|
|
527
|
+
This legacy SVG is an opaque compatibility fallback. Ask an agent to convert it to editable vector elements.
|
|
528
|
+
</p>
|
|
529
|
+
)}
|
|
531
530
|
{vectorVisual && (
|
|
532
531
|
<>
|
|
533
532
|
<Field label="Vector element">
|
|
@@ -100,6 +100,7 @@ export function WorkspaceChrome({
|
|
|
100
100
|
aria-describedby={titleError ? "title-error" : undefined}
|
|
101
101
|
name="composition-title"
|
|
102
102
|
autoComplete="off"
|
|
103
|
+
disabled={building}
|
|
103
104
|
value={title}
|
|
104
105
|
onChange={(event) => onTitle(event.target.value, "title")}
|
|
105
106
|
onBlur={onEditEnd}
|
|
@@ -140,7 +141,7 @@ export function WorkspaceChrome({
|
|
|
140
141
|
className="icon-only"
|
|
141
142
|
aria-label="Export PNG"
|
|
142
143
|
title="Export active page as PNG"
|
|
143
|
-
disabled={exporting}
|
|
144
|
+
disabled={exporting || building}
|
|
144
145
|
onClick={onExportPNG}
|
|
145
146
|
>
|
|
146
147
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
@@ -148,7 +149,7 @@ export function WorkspaceChrome({
|
|
|
148
149
|
<path d="m7 14 3.5-4 3 3 2-2 2.5 3M12 16v5m-3-3 3 3 3-3" />
|
|
149
150
|
</svg>
|
|
150
151
|
</button>
|
|
151
|
-
<button type="button" onClick={onPresent}>
|
|
152
|
+
<button type="button" onClick={onPresent} disabled={building}>
|
|
152
153
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
153
154
|
<rect x="3" y="4" width="18" height="12" rx="2" />
|
|
154
155
|
<path d="M12 16v5m-4 0h8M10 7l5 3-5 3Z" />
|
|
@@ -158,19 +159,20 @@ export function WorkspaceChrome({
|
|
|
158
159
|
{onBuild && (
|
|
159
160
|
<button
|
|
160
161
|
type="button"
|
|
161
|
-
className=
|
|
162
|
+
className={`primary build-button ${building ? "building" : ""}`}
|
|
162
163
|
disabled={fileStatus !== "saved" || building}
|
|
164
|
+
aria-busy={building}
|
|
163
165
|
onClick={onBuild}
|
|
164
166
|
>
|
|
165
167
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
166
168
|
<path d="m5 19 10-10 3 3L8 22Zm10-10 2-2 3 3-2 2M5 3v6M2 6h6m10-5v4m-2-2h4" />
|
|
167
169
|
</svg>
|
|
168
|
-
{building ? "Building…" : "Build it"}
|
|
170
|
+
<span>{building ? "Building…" : "Build it"}</span>
|
|
169
171
|
</button>
|
|
170
172
|
)}
|
|
171
173
|
</div>
|
|
172
174
|
|
|
173
|
-
<nav className="component-dock" aria-label="Canvas tools">
|
|
175
|
+
<nav className="component-dock" aria-label="Canvas tools" inert={building}>
|
|
174
176
|
<button
|
|
175
177
|
type="button"
|
|
176
178
|
className={!selectedKind ? "active" : ""}
|
|
@@ -26,6 +26,9 @@ export function useFileSession(
|
|
|
26
26
|
) {
|
|
27
27
|
const token = fileSessionToken();
|
|
28
28
|
const [status, setStatus] = useState<FileStatus>(enabled ? "loading" : "saved");
|
|
29
|
+
const [building, setBuilding] = useState(false);
|
|
30
|
+
const buildPending = useRef(false);
|
|
31
|
+
const submittingBuild = useRef(false);
|
|
29
32
|
const callbacksRef = useRef(callbacks);
|
|
30
33
|
callbacksRef.current = callbacks;
|
|
31
34
|
const draftRef = useRef(draft);
|
|
@@ -72,10 +75,12 @@ export function useFileSession(
|
|
|
72
75
|
if (!enabled || !token) return;
|
|
73
76
|
let cancelled = false;
|
|
74
77
|
const timer = window.setInterval(() => {
|
|
75
|
-
if (!state.current.ready || state.current.drain) return;
|
|
78
|
+
if (!state.current.ready || state.current.drain || submittingBuild.current) return;
|
|
79
|
+
const revision = state.current.revision;
|
|
76
80
|
void loadFileSession(token).then((remote) => {
|
|
77
|
-
if (cancelled ||
|
|
81
|
+
if (cancelled || state.current.drain || submittingBuild.current || revision !== state.current.revision || remote.revision === revision) return;
|
|
78
82
|
if (canonicalJSON(draftRef.current) !== state.current.savedJSON) {
|
|
83
|
+
finishBuild();
|
|
79
84
|
setStatus("conflict");
|
|
80
85
|
callbacksRef.current.onNotice(
|
|
81
86
|
"The file changed elsewhere. Neither version was overwritten.",
|
|
@@ -89,8 +94,14 @@ export function useFileSession(
|
|
|
89
94
|
};
|
|
90
95
|
setStatus("saved");
|
|
91
96
|
callbacksRef.current.onExternalChange(remote.document);
|
|
97
|
+
finishBuild();
|
|
92
98
|
}).catch(() => {
|
|
93
|
-
if (
|
|
99
|
+
if (cancelled || submittingBuild.current || revision !== state.current.revision) return;
|
|
100
|
+
setStatus("error");
|
|
101
|
+
if (buildPending.current) {
|
|
102
|
+
finishBuild();
|
|
103
|
+
callbacksRef.current.onNotice("Could not load the agent result. Check the local file service.");
|
|
104
|
+
}
|
|
94
105
|
});
|
|
95
106
|
}, 1000);
|
|
96
107
|
return () => {
|
|
@@ -99,6 +110,11 @@ export function useFileSession(
|
|
|
99
110
|
};
|
|
100
111
|
}, [enabled, token]);
|
|
101
112
|
|
|
113
|
+
function finishBuild() {
|
|
114
|
+
buildPending.current = false;
|
|
115
|
+
setBuilding(false);
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
function save(next: Draft): Promise<string | undefined> {
|
|
103
119
|
if (!token || !state.current.ready) return Promise.resolve(undefined);
|
|
104
120
|
const serialized = canonicalJSON(next);
|
|
@@ -146,17 +162,30 @@ export function useFileSession(
|
|
|
146
162
|
slideId: string,
|
|
147
163
|
componentId?: string,
|
|
148
164
|
) {
|
|
149
|
-
if (!token) return false;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
if (!token || buildPending.current) return false;
|
|
166
|
+
buildPending.current = true;
|
|
167
|
+
submittingBuild.current = true;
|
|
168
|
+
setBuilding(true);
|
|
169
|
+
try {
|
|
170
|
+
const revision = await save(draftRef.current);
|
|
171
|
+
if (!revision) {
|
|
172
|
+
finishBuild();
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
await sendBuildRequest(token, {
|
|
176
|
+
revision,
|
|
177
|
+
instruction,
|
|
178
|
+
slideId,
|
|
179
|
+
...(componentId ? { componentId } : {}),
|
|
180
|
+
});
|
|
181
|
+
return true;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
finishBuild();
|
|
184
|
+
throw error;
|
|
185
|
+
} finally {
|
|
186
|
+
submittingBuild.current = false;
|
|
187
|
+
}
|
|
159
188
|
}
|
|
160
189
|
|
|
161
|
-
return { status, build };
|
|
190
|
+
return { status, build, building };
|
|
162
191
|
}
|
package/src/styles/chrome.css
CHANGED
|
@@ -146,12 +146,12 @@
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
.action-island button.primary {
|
|
149
|
-
background:
|
|
150
|
-
color:
|
|
149
|
+
background: white;
|
|
150
|
+
color: var(--ui-text);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
.action-island button.primary:hover:not(:disabled) {
|
|
154
|
-
background:
|
|
154
|
+
background: var(--ui-hover);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
.action-island button.primary:disabled {
|
|
@@ -159,6 +159,54 @@
|
|
|
159
159
|
color: var(--ui-muted);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
.action-island .build-button {
|
|
163
|
+
position: relative;
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
justify-content: center;
|
|
166
|
+
flex: 1;
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.action-island button.build-button.building {
|
|
171
|
+
background: linear-gradient(110deg, #155e75, #2449aa, #7535a5, #a82d62, #155e75);
|
|
172
|
+
background-size: 300% 100%;
|
|
173
|
+
color: white;
|
|
174
|
+
opacity: 1;
|
|
175
|
+
cursor: wait;
|
|
176
|
+
animation: build-colors 4s linear infinite;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.build-button.building::after,
|
|
180
|
+
.build-loading-track::after {
|
|
181
|
+
position: absolute;
|
|
182
|
+
inset: 0;
|
|
183
|
+
background: linear-gradient(90deg, transparent, #ffffff50, transparent);
|
|
184
|
+
transform: translateX(-100%);
|
|
185
|
+
animation: build-sweep 1.8s ease-in-out infinite;
|
|
186
|
+
content: "";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.build-button.building::after {
|
|
190
|
+
top: auto;
|
|
191
|
+
height: 3px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@keyframes build-colors {
|
|
195
|
+
to { background-position: 150% 0; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@keyframes build-sweep {
|
|
199
|
+
to { transform: translateX(100%); }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@media (prefers-reduced-motion: reduce) {
|
|
203
|
+
.action-island button.build-button.building,
|
|
204
|
+
.build-button.building::after,
|
|
205
|
+
.build-loading-track::after {
|
|
206
|
+
animation: none;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
162
210
|
.action-island button svg {
|
|
163
211
|
flex: none;
|
|
164
212
|
width: 18px;
|
package/src/styles/feedback.css
CHANGED
|
@@ -7,47 +7,49 @@
|
|
|
7
7
|
transform-origin: center top;
|
|
8
8
|
display: none;
|
|
9
9
|
align-items: center;
|
|
10
|
-
gap:
|
|
11
|
-
background:
|
|
12
|
-
color:
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
gap: 12px;
|
|
11
|
+
background: var(--ui-surface);
|
|
12
|
+
color: var(--ui-text);
|
|
13
|
+
border: 1px solid var(--ui-border);
|
|
14
|
+
box-shadow: 0 4px 16px #0000000a, 0 1px 3px #00000008;
|
|
15
|
+
padding: 10px 14px;
|
|
16
|
+
border-radius: 12px;
|
|
15
17
|
font-size: 13px;
|
|
16
|
-
|
|
18
|
+
line-height: 1.5;
|
|
19
|
+
width: max-content;
|
|
20
|
+
max-width: min(520px, max(240px, calc(100vw - 744px)), calc(100vw - 40px));
|
|
17
21
|
z-index: 50;
|
|
18
22
|
}
|
|
23
|
+
.toast > [role="status"] {
|
|
24
|
+
min-width: 0;
|
|
25
|
+
overflow-wrap: anywhere;
|
|
26
|
+
}
|
|
19
27
|
.toast.visible {
|
|
20
28
|
display: flex;
|
|
21
|
-
animation: toast-
|
|
29
|
+
animation: toast-enter 180ms ease-out both;
|
|
22
30
|
}
|
|
23
31
|
.toast.visible.closing {
|
|
24
32
|
pointer-events: none;
|
|
25
|
-
animation: toast-
|
|
26
|
-
}
|
|
27
|
-
.toast button {
|
|
28
|
-
background: transparent;
|
|
29
|
-
border-color: #707070;
|
|
30
|
-
color: white;
|
|
31
|
-
padding: 5px 10px;
|
|
33
|
+
animation: toast-exit 180ms ease-in both;
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
@keyframes toast-expand {
|
|
37
|
-
0% {
|
|
35
|
+
@keyframes toast-enter {
|
|
36
|
+
from {
|
|
38
37
|
opacity: 0;
|
|
39
|
-
transform: translate(-50%, -
|
|
38
|
+
transform: translate(-50%, -6px);
|
|
40
39
|
}
|
|
41
|
-
|
|
40
|
+
to {
|
|
42
41
|
opacity: 1;
|
|
43
|
-
transform: translate(-50%,
|
|
44
|
-
}
|
|
45
|
-
80% {
|
|
46
|
-
transform: translate(-50%, -1px) scale(0.99, 0.97);
|
|
42
|
+
transform: translate(-50%, 0);
|
|
47
43
|
}
|
|
48
|
-
|
|
44
|
+
}
|
|
45
|
+
@keyframes toast-exit {
|
|
46
|
+
from {
|
|
49
47
|
opacity: 1;
|
|
50
|
-
transform: translate(-50%, 0)
|
|
48
|
+
transform: translate(-50%, 0);
|
|
49
|
+
}
|
|
50
|
+
to {
|
|
51
|
+
opacity: 0;
|
|
52
|
+
transform: translate(-50%, -6px);
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
.recovery {
|
package/src/styles/shell.css
CHANGED
|
@@ -208,6 +208,45 @@
|
|
|
208
208
|
.workspace.right-collapsed .stage {
|
|
209
209
|
padding-right: 96px;
|
|
210
210
|
}
|
|
211
|
+
.editor-content {
|
|
212
|
+
display: contents;
|
|
213
|
+
}
|
|
214
|
+
.is-building .canvas {
|
|
215
|
+
opacity: 0.25;
|
|
216
|
+
filter: blur(2px);
|
|
217
|
+
}
|
|
218
|
+
.build-loading {
|
|
219
|
+
z-index: 20;
|
|
220
|
+
pointer-events: none;
|
|
221
|
+
}
|
|
222
|
+
.build-loading-card {
|
|
223
|
+
display: grid;
|
|
224
|
+
gap: 10px;
|
|
225
|
+
width: min(320px, 100%);
|
|
226
|
+
padding: 24px;
|
|
227
|
+
border: 1px solid var(--ui-border);
|
|
228
|
+
border-radius: 14px;
|
|
229
|
+
background: var(--ui-surface);
|
|
230
|
+
box-shadow: var(--ui-shadow);
|
|
231
|
+
text-align: center;
|
|
232
|
+
}
|
|
233
|
+
.build-loading-card strong {
|
|
234
|
+
font-size: 14px;
|
|
235
|
+
font-weight: 500;
|
|
236
|
+
}
|
|
237
|
+
.build-loading-card span {
|
|
238
|
+
font-size: 12px;
|
|
239
|
+
line-height: 1.5;
|
|
240
|
+
color: var(--ui-muted);
|
|
241
|
+
}
|
|
242
|
+
.build-loading-track {
|
|
243
|
+
position: relative;
|
|
244
|
+
overflow: hidden;
|
|
245
|
+
height: 3px;
|
|
246
|
+
margin-bottom: 8px;
|
|
247
|
+
border-radius: 2px;
|
|
248
|
+
background: linear-gradient(90deg, #155e75, #2449aa, #7535a5, #a82d62);
|
|
249
|
+
}
|
|
211
250
|
.slide-wrap {
|
|
212
251
|
width: 100%;
|
|
213
252
|
max-width: min(1200px, calc((100vh - 270px) * var(--page-ratio)));
|