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 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
- For agent-led installation, give your agent [SETUP.md](SETUP.md) and your brief.
19
- For a manual quickstart:
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
- Use Konpeki to turn these notes into a three-slide explanation for engineers.
42
- Make the request flow and failure handling easy to follow. Preserve facts and
43
- caveats. Save editable composition JSON, then render, inspect and fix the result.
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.0
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "konpeki",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A shared editable canvas for agent-made visuals",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
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), 5000);
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, undoableFor?: Draft) {
213
+ }, [draft, presenting, selected, vectorSelection, fileSession.building]);
214
+ function showNotice(message: string) {
218
215
  setToastClosing(false);
219
- setNotice({ message, undoableFor });
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.", next);
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.", next);
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
- if (!await fileSession.build(instruction, slide.id, selected)) return;
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.", parsed.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={buildBusy}
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
- <p className="field-help">
527
- {component.customVisual.format === "vector"
528
- ? "Select the visual and press Enter or choose Edit elements. Double-click an element to edit its text or attributes here. Only top-level line endpoints have drag handles; paths use attribute controls."
529
- : "This legacy SVG is an opaque compatibility fallback. Ask an agent to convert it to editable vector elements."}
530
- </p>
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="primary"
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 || remote.revision === state.current.revision) return;
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 (!cancelled) setStatus("error");
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
- const revision = await save(draftRef.current);
151
- if (!revision) return false;
152
- await sendBuildRequest(token, {
153
- revision,
154
- instruction,
155
- slideId,
156
- ...(componentId ? { componentId } : {}),
157
- });
158
- return true;
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
  }
@@ -146,12 +146,12 @@
146
146
  }
147
147
 
148
148
  .action-island button.primary {
149
- background: #202020;
150
- color: white;
149
+ background: white;
150
+ color: var(--ui-text);
151
151
  }
152
152
 
153
153
  .action-island button.primary:hover:not(:disabled) {
154
- background: #383838;
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;
@@ -7,47 +7,49 @@
7
7
  transform-origin: center top;
8
8
  display: none;
9
9
  align-items: center;
10
- gap: 14px;
11
- background: #252525;
12
- color: white;
13
- padding: 10px 12px 10px 18px;
14
- border-radius: 6px;
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
- max-width: 80vw;
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-expand 360ms cubic-bezier(0.16, 1, 0.3, 1) both;
29
+ animation: toast-enter 180ms ease-out both;
22
30
  }
23
31
  .toast.visible.closing {
24
32
  pointer-events: none;
25
- animation: toast-expand 280ms cubic-bezier(0.4, 0, 0.8, 0.2) reverse both;
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
- .toast button:hover:not(:disabled) {
34
- background: #444;
35
- }
36
- @keyframes toast-expand {
37
- 0% {
35
+ @keyframes toast-enter {
36
+ from {
38
37
  opacity: 0;
39
- transform: translate(-50%, -22px) scale(0.72, 0.4);
38
+ transform: translate(-50%, -6px);
40
39
  }
41
- 62% {
40
+ to {
42
41
  opacity: 1;
43
- transform: translate(-50%, 5px) scale(1.04, 1.08);
44
- }
45
- 80% {
46
- transform: translate(-50%, -1px) scale(0.99, 0.97);
42
+ transform: translate(-50%, 0);
47
43
  }
48
- 100% {
44
+ }
45
+ @keyframes toast-exit {
46
+ from {
49
47
  opacity: 1;
50
- transform: translate(-50%, 0) scale(1);
48
+ transform: translate(-50%, 0);
49
+ }
50
+ to {
51
+ opacity: 0;
52
+ transform: translate(-50%, -6px);
51
53
  }
52
54
  }
53
55
  .recovery {
@@ -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)));