konpeki 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/authoring-visuals/SKILL.md +12 -1
- package/README.md +26 -25
- package/SETUP.md +1 -1
- package/composition/README.md +26 -49
- package/composition/compile.ts +1 -1
- package/composition/document.ts +2 -111
- package/composition/schema.json +3 -3
- package/composition/schema.ts +1 -1
- package/composition/types.ts +1 -20
- package/composition/validate.ts +3 -461
- package/composition/visualizations.ts +0 -16
- package/docs/development.md +57 -0
- package/docs/workflow.md +49 -7
- package/package.json +3 -1
- package/runtime/konpeki.mjs +182 -460
- package/slides/introducing-konpeki/composition.json +1740 -1735
- package/src/app/App.tsx +112 -54
- package/src/components/BuildOrb.tsx +40 -0
- package/src/components/Canvas.tsx +17 -0
- package/src/components/InspectorPanel.tsx +41 -8
- package/src/components/LeftPanel.tsx +16 -3
- package/src/components/RevisionNotes.tsx +56 -0
- package/src/components/WorkspaceChrome.tsx +11 -9
- package/src/lib/examples/react-page-migration.json +1 -1
- package/src/lib/export-png.ts +4 -1
- package/src/lib/file-session.ts +14 -1
- package/src/lib/review.ts +26 -0
- package/src/lib/use-file-session.ts +84 -19
- package/src/styles/base.css +16 -0
- package/src/styles/canvas.css +43 -12
- package/src/styles/chrome.css +106 -8
- package/src/styles/feedback.css +45 -25
- package/src/styles/left-panel.css +48 -7
- package/src/styles/right-panel.css +62 -20
- package/src/styles/shell.css +70 -16
|
@@ -2,6 +2,7 @@ import type { RefObject } from "react";
|
|
|
2
2
|
import type { CompositionComponent } from "../../composition/types.ts";
|
|
3
3
|
import { componentLabels } from "../lib/model.ts";
|
|
4
4
|
import mark from "../assets/konpeki-mark.png";
|
|
5
|
+
import { BuildOrb } from "./BuildOrb.tsx";
|
|
5
6
|
|
|
6
7
|
const dockItems: {
|
|
7
8
|
kind: CompositionComponent["kind"];
|
|
@@ -65,6 +66,7 @@ export function WorkspaceChrome({
|
|
|
65
66
|
onPresent,
|
|
66
67
|
fileStatus,
|
|
67
68
|
building,
|
|
69
|
+
buildState,
|
|
68
70
|
onBuild,
|
|
69
71
|
onSelectTool,
|
|
70
72
|
onComponentTool,
|
|
@@ -84,6 +86,7 @@ export function WorkspaceChrome({
|
|
|
84
86
|
onPresent: () => void;
|
|
85
87
|
fileStatus?: "loading" | "saved" | "saving" | "conflict" | "error";
|
|
86
88
|
building: boolean;
|
|
89
|
+
buildState?: "ready" | "working";
|
|
87
90
|
onBuild?: () => void;
|
|
88
91
|
onSelectTool: () => void;
|
|
89
92
|
onComponentTool: (kind: CompositionComponent["kind"]) => void;
|
|
@@ -100,6 +103,7 @@ export function WorkspaceChrome({
|
|
|
100
103
|
aria-describedby={titleError ? "title-error" : undefined}
|
|
101
104
|
name="composition-title"
|
|
102
105
|
autoComplete="off"
|
|
106
|
+
disabled={building}
|
|
103
107
|
value={title}
|
|
104
108
|
onChange={(event) => onTitle(event.target.value, "title")}
|
|
105
109
|
onBlur={onEditEnd}
|
|
@@ -140,7 +144,7 @@ export function WorkspaceChrome({
|
|
|
140
144
|
className="icon-only"
|
|
141
145
|
aria-label="Export PNG"
|
|
142
146
|
title="Export active page as PNG"
|
|
143
|
-
disabled={exporting}
|
|
147
|
+
disabled={exporting || building}
|
|
144
148
|
onClick={onExportPNG}
|
|
145
149
|
>
|
|
146
150
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
@@ -148,29 +152,27 @@ export function WorkspaceChrome({
|
|
|
148
152
|
<path d="m7 14 3.5-4 3 3 2-2 2.5 3M12 16v5m-3-3 3 3 3-3" />
|
|
149
153
|
</svg>
|
|
150
154
|
</button>
|
|
151
|
-
<button type="button" onClick={onPresent}>
|
|
155
|
+
<button type="button" className="icon-only" aria-label="Present" title="Present" onClick={onPresent} disabled={building}>
|
|
152
156
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
153
157
|
<rect x="3" y="4" width="18" height="12" rx="2" />
|
|
154
158
|
<path d="M12 16v5m-4 0h8M10 7l5 3-5 3Z" />
|
|
155
159
|
</svg>
|
|
156
|
-
Present
|
|
157
160
|
</button>
|
|
158
161
|
{onBuild && (
|
|
159
162
|
<button
|
|
160
163
|
type="button"
|
|
161
|
-
className=
|
|
164
|
+
className={`primary build-button ${building ? "building" : ""}`}
|
|
162
165
|
disabled={fileStatus !== "saved" || building}
|
|
166
|
+
aria-busy={building}
|
|
163
167
|
onClick={onBuild}
|
|
164
168
|
>
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
</svg>
|
|
168
|
-
{building ? "Building…" : "Build it"}
|
|
169
|
+
<BuildOrb active={building} />
|
|
170
|
+
<span className="build-label">{buildState === "ready" ? "Request ready" : buildState === "working" ? "Agent working" : "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" : ""}
|
package/src/lib/export-png.ts
CHANGED
|
@@ -48,7 +48,7 @@ export async function exportPagePNG(
|
|
|
48
48
|
});
|
|
49
49
|
clone
|
|
50
50
|
.querySelectorAll(
|
|
51
|
-
".resize-handle, .guide, .vector-handles, .inline-intent-editor",
|
|
51
|
+
".resize-handle, .guide, .vector-handles, .inline-intent-editor, .revision-pin",
|
|
52
52
|
)
|
|
53
53
|
.forEach((node) => node.remove());
|
|
54
54
|
const rect = source.getBoundingClientRect();
|
|
@@ -56,6 +56,9 @@ export async function exportPagePNG(
|
|
|
56
56
|
clone.style.height = `${rect.height}px`;
|
|
57
57
|
clone.style.margin = "0";
|
|
58
58
|
clone.style.overflow = "hidden";
|
|
59
|
+
// Loading feedback belongs to the editor, never to the exported artwork.
|
|
60
|
+
clone.style.filter = "none";
|
|
61
|
+
clone.style.opacity = "1";
|
|
59
62
|
const fontRules = Array.from(document.styleSheets).flatMap((sheet) =>
|
|
60
63
|
Array.from(sheet.cssRules).filter(
|
|
61
64
|
(rule): rule is CSSFontFaceRule => rule instanceof CSSFontFaceRule,
|
package/src/lib/file-session.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { CompositionDocument } from "../../composition/types.ts";
|
|
2
|
+
import type { ReviewState, ReviewTarget } from "./review.ts";
|
|
2
3
|
|
|
3
4
|
export type FileSessionDocument = {
|
|
4
5
|
document: CompositionDocument;
|
|
5
6
|
revision: string;
|
|
6
7
|
name: string;
|
|
8
|
+
review: ReviewState;
|
|
7
9
|
};
|
|
8
10
|
|
|
9
11
|
export class FileSessionError extends Error {
|
|
@@ -65,10 +67,21 @@ export function sendBuildRequest(
|
|
|
65
67
|
instruction: string;
|
|
66
68
|
slideId: string;
|
|
67
69
|
componentId?: string;
|
|
70
|
+
elementId?: string;
|
|
68
71
|
},
|
|
69
72
|
) {
|
|
70
|
-
return request<
|
|
73
|
+
return request<ReviewState>(token, "/build", {
|
|
71
74
|
method: "POST",
|
|
72
75
|
body: JSON.stringify(requestBody),
|
|
73
76
|
});
|
|
74
77
|
}
|
|
78
|
+
|
|
79
|
+
export function addNote(token: string, target: ReviewTarget, text: string) {
|
|
80
|
+
return request<ReviewState>(token, "/notes", { method: "POST", body: JSON.stringify({ ...target, text }) });
|
|
81
|
+
}
|
|
82
|
+
export function removeNote(token: string, id: string) {
|
|
83
|
+
return request<ReviewState>(token, "/notes", { method: "DELETE", body: JSON.stringify({ id }) });
|
|
84
|
+
}
|
|
85
|
+
export function cancelRequest(token: string, id: string) {
|
|
86
|
+
return request<ReviewState>(token, "/cancel", { method: "POST", body: JSON.stringify({ id }) });
|
|
87
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ReviewTarget = { slideId: string; componentId?: string; elementId?: string };
|
|
2
|
+
export type RevisionNote = ReviewTarget & {
|
|
3
|
+
id: string;
|
|
4
|
+
text: string;
|
|
5
|
+
resolved: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type BuildRequest = ReviewTarget & {
|
|
8
|
+
schema: "konpeki-build-request/v2";
|
|
9
|
+
id: string;
|
|
10
|
+
compositionPath: string;
|
|
11
|
+
revision: string;
|
|
12
|
+
instruction: string;
|
|
13
|
+
notes: RevisionNote[];
|
|
14
|
+
status: "submitted" | "working" | "done" | "needs-clarification" | "failed";
|
|
15
|
+
message?: string;
|
|
16
|
+
resultRevision?: string;
|
|
17
|
+
};
|
|
18
|
+
export type ReviewState = {
|
|
19
|
+
schema: "konpeki-review/v1";
|
|
20
|
+
version: number;
|
|
21
|
+
notes: RevisionNote[];
|
|
22
|
+
request?: BuildRequest;
|
|
23
|
+
};
|
|
24
|
+
export const emptyReview = (): ReviewState => ({ schema: "konpeki-review/v1", version: 0, notes: [] });
|
|
25
|
+
export const activeRequest = (request?: BuildRequest) =>
|
|
26
|
+
request?.status === "submitted" || request?.status === "working";
|
|
@@ -2,7 +2,11 @@ import { useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { canonicalJSON } from "../../composition/compile.ts";
|
|
3
3
|
import { validateComposition } from "../../composition/validate.ts";
|
|
4
4
|
import type { Draft } from "./model.ts";
|
|
5
|
+
import { activeRequest, emptyReview, type ReviewState, type ReviewTarget } from "./review.ts";
|
|
5
6
|
import {
|
|
7
|
+
addNote,
|
|
8
|
+
removeNote,
|
|
9
|
+
cancelRequest,
|
|
6
10
|
FileSessionError,
|
|
7
11
|
fileSessionToken,
|
|
8
12
|
loadFileSession,
|
|
@@ -15,7 +19,7 @@ export type FileStatus = "loading" | "saved" | "saving" | "conflict" | "error";
|
|
|
15
19
|
type Callbacks = {
|
|
16
20
|
onOpen: (draft: Draft, name: string) => void;
|
|
17
21
|
onExternalChange: (draft: Draft) => void;
|
|
18
|
-
onNotice: (message: string) => void;
|
|
22
|
+
onNotice: (message: string, tone?: "neutral" | "error") => void;
|
|
19
23
|
onError: (message: string) => void;
|
|
20
24
|
};
|
|
21
25
|
|
|
@@ -26,6 +30,11 @@ export function useFileSession(
|
|
|
26
30
|
) {
|
|
27
31
|
const token = fileSessionToken();
|
|
28
32
|
const [status, setStatus] = useState<FileStatus>(enabled ? "loading" : "saved");
|
|
33
|
+
const [building, setBuilding] = useState(false);
|
|
34
|
+
const [review, setReview] = useState<ReviewState>(emptyReview);
|
|
35
|
+
const reviewRef = useRef(review);
|
|
36
|
+
const buildPending = useRef(false);
|
|
37
|
+
const submittingBuild = useRef(false);
|
|
29
38
|
const callbacksRef = useRef(callbacks);
|
|
30
39
|
callbacksRef.current = callbacks;
|
|
31
40
|
const draftRef = useRef(draft);
|
|
@@ -49,6 +58,7 @@ export function useFileSession(
|
|
|
49
58
|
savedJSON: canonicalJSON(session.document),
|
|
50
59
|
};
|
|
51
60
|
setStatus("saved");
|
|
61
|
+
receiveReview(session.review);
|
|
52
62
|
callbacksRef.current.onOpen(session.document, session.name);
|
|
53
63
|
}).catch((error) => {
|
|
54
64
|
if (cancelled) return;
|
|
@@ -72,13 +82,18 @@ export function useFileSession(
|
|
|
72
82
|
if (!enabled || !token) return;
|
|
73
83
|
let cancelled = false;
|
|
74
84
|
const timer = window.setInterval(() => {
|
|
75
|
-
if (!state.current.ready || state.current.drain) return;
|
|
85
|
+
if (!state.current.ready || state.current.drain || submittingBuild.current) return;
|
|
86
|
+
const revision = state.current.revision;
|
|
76
87
|
void loadFileSession(token).then((remote) => {
|
|
77
|
-
if (cancelled ||
|
|
88
|
+
if (cancelled || state.current.drain || submittingBuild.current || revision !== state.current.revision) return;
|
|
89
|
+
receiveReview(remote.review);
|
|
90
|
+
setStatus(current => current === "error" ? "saved" : current);
|
|
91
|
+
if (remote.revision === revision) return;
|
|
78
92
|
if (canonicalJSON(draftRef.current) !== state.current.savedJSON) {
|
|
79
93
|
setStatus("conflict");
|
|
80
94
|
callbacksRef.current.onNotice(
|
|
81
95
|
"The file changed elsewhere. Neither version was overwritten.",
|
|
96
|
+
"error",
|
|
82
97
|
);
|
|
83
98
|
return;
|
|
84
99
|
}
|
|
@@ -90,7 +105,11 @@ export function useFileSession(
|
|
|
90
105
|
setStatus("saved");
|
|
91
106
|
callbacksRef.current.onExternalChange(remote.document);
|
|
92
107
|
}).catch(() => {
|
|
93
|
-
if (
|
|
108
|
+
if (cancelled || submittingBuild.current || revision !== state.current.revision) return;
|
|
109
|
+
setStatus("error");
|
|
110
|
+
if (buildPending.current) {
|
|
111
|
+
callbacksRef.current.onNotice("Could not load the agent result. Check the local file service.", "error");
|
|
112
|
+
}
|
|
94
113
|
});
|
|
95
114
|
}, 1000);
|
|
96
115
|
return () => {
|
|
@@ -99,8 +118,30 @@ export function useFileSession(
|
|
|
99
118
|
};
|
|
100
119
|
}, [enabled, token]);
|
|
101
120
|
|
|
121
|
+
function finishBuild() {
|
|
122
|
+
buildPending.current = false;
|
|
123
|
+
setBuilding(false);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function receiveReview(next: ReviewState) {
|
|
127
|
+
if (next.version < reviewRef.current.version) return;
|
|
128
|
+
reviewRef.current = next;
|
|
129
|
+
setReview(next);
|
|
130
|
+
buildPending.current = activeRequest(next.request);
|
|
131
|
+
setBuilding(buildPending.current);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function noteAction(action: () => Promise<ReviewState>, saveFirst = false) {
|
|
135
|
+
if (submittingBuild.current) throw new Error("Wait for the current save before submitting.");
|
|
136
|
+
submittingBuild.current = true;
|
|
137
|
+
try {
|
|
138
|
+
if (saveFirst && !await save(draftRef.current)) throw new Error("Save the composition before adding a note.");
|
|
139
|
+
receiveReview(await action());
|
|
140
|
+
} finally { submittingBuild.current = false; }
|
|
141
|
+
}
|
|
142
|
+
|
|
102
143
|
function save(next: Draft): Promise<string | undefined> {
|
|
103
|
-
if (!token || !state.current.ready) return Promise.resolve(undefined);
|
|
144
|
+
if (!token || !state.current.ready || activeRequest(reviewRef.current.request)) return Promise.resolve(undefined);
|
|
104
145
|
const serialized = canonicalJSON(next);
|
|
105
146
|
if (serialized === state.current.savedJSON && !state.current.drain)
|
|
106
147
|
return Promise.resolve(state.current.revision);
|
|
@@ -133,9 +174,12 @@ export function useFileSession(
|
|
|
133
174
|
state.current.pending = undefined;
|
|
134
175
|
const conflict = error instanceof FileSessionError && error.status === 409;
|
|
135
176
|
setStatus(conflict ? "conflict" : "error");
|
|
136
|
-
callbacksRef.current.onNotice(
|
|
137
|
-
|
|
138
|
-
|
|
177
|
+
callbacksRef.current.onNotice(
|
|
178
|
+
conflict
|
|
179
|
+
? "The composition changed outside this browser. Your edits were not overwritten."
|
|
180
|
+
: error instanceof Error ? error.message : "Could not save the composition file.",
|
|
181
|
+
"error",
|
|
182
|
+
);
|
|
139
183
|
return;
|
|
140
184
|
}
|
|
141
185
|
}
|
|
@@ -145,18 +189,39 @@ export function useFileSession(
|
|
|
145
189
|
instruction: string,
|
|
146
190
|
slideId: string,
|
|
147
191
|
componentId?: string,
|
|
192
|
+
elementId?: string,
|
|
148
193
|
) {
|
|
149
|
-
if (!token) return false;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
194
|
+
if (!token || buildPending.current) return false;
|
|
195
|
+
if (submittingBuild.current) throw new Error("Wait for your note to finish saving before building.");
|
|
196
|
+
buildPending.current = true;
|
|
197
|
+
submittingBuild.current = true;
|
|
198
|
+
setBuilding(true);
|
|
199
|
+
try {
|
|
200
|
+
const revision = await save(draftRef.current);
|
|
201
|
+
if (!revision) {
|
|
202
|
+
finishBuild();
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
receiveReview(await sendBuildRequest(token, {
|
|
206
|
+
revision,
|
|
207
|
+
instruction,
|
|
208
|
+
slideId,
|
|
209
|
+
...(componentId ? { componentId } : {}),
|
|
210
|
+
...(elementId ? { elementId } : {}),
|
|
211
|
+
}));
|
|
212
|
+
return true;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
finishBuild();
|
|
215
|
+
throw error;
|
|
216
|
+
} finally {
|
|
217
|
+
submittingBuild.current = false;
|
|
218
|
+
}
|
|
159
219
|
}
|
|
160
220
|
|
|
161
|
-
return {
|
|
221
|
+
return {
|
|
222
|
+
status, build, building, review,
|
|
223
|
+
addNote: (target: ReviewTarget, text: string) => noteAction(() => addNote(token!, target, text), true),
|
|
224
|
+
removeNote: (id: string) => noteAction(() => removeNote(token!, id)),
|
|
225
|
+
cancel: () => noteAction(() => cancelRequest(token!, reviewRef.current.request!.id)),
|
|
226
|
+
};
|
|
162
227
|
}
|
package/src/styles/base.css
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
color: #202020;
|
|
4
4
|
background: #f4f4f4;
|
|
5
5
|
font-synthesis: none;
|
|
6
|
+
-webkit-font-smoothing: antialiased;
|
|
7
|
+
-moz-osx-font-smoothing: grayscale;
|
|
6
8
|
--accent: #007bbb;
|
|
7
9
|
--muted: #626262;
|
|
8
10
|
--ui-bg: #f4f4f4;
|
|
@@ -39,6 +41,8 @@ button {
|
|
|
39
41
|
color: inherit;
|
|
40
42
|
background: #fff;
|
|
41
43
|
padding: 7px 10px;
|
|
44
|
+
transition: background-color 120ms ease-out, border-color 120ms ease-out,
|
|
45
|
+
color 120ms ease-out, box-shadow 120ms ease-out, transform 120ms ease-out;
|
|
42
46
|
}
|
|
43
47
|
button:hover:not(:disabled) {
|
|
44
48
|
background: #f1f1f1;
|
|
@@ -62,6 +66,8 @@ select {
|
|
|
62
66
|
background: #fff;
|
|
63
67
|
color: var(--ui-text);
|
|
64
68
|
padding: 7px 9px;
|
|
69
|
+
transition: background-color 120ms ease-out, border-color 120ms ease-out,
|
|
70
|
+
box-shadow 120ms ease-out;
|
|
65
71
|
}
|
|
66
72
|
input:hover,
|
|
67
73
|
textarea:hover,
|
|
@@ -92,6 +98,16 @@ h2 {
|
|
|
92
98
|
background: #383838;
|
|
93
99
|
border-color: #383838;
|
|
94
100
|
}
|
|
101
|
+
.panel-toggle:active:not(:disabled),
|
|
102
|
+
.action-island button:active:not(:disabled),
|
|
103
|
+
.component-dock button:active:not(:disabled),
|
|
104
|
+
.segmented-options button:active:not(:disabled),
|
|
105
|
+
.theme-options button:active:not(:disabled),
|
|
106
|
+
.appearance-options button:active:not(:disabled),
|
|
107
|
+
.revision-note-submit button:active:not(:disabled),
|
|
108
|
+
.revision-note-remove:active:not(:disabled) {
|
|
109
|
+
transform: scale(0.96);
|
|
110
|
+
}
|
|
95
111
|
.sr-only {
|
|
96
112
|
position: absolute;
|
|
97
113
|
width: 1px;
|
package/src/styles/canvas.css
CHANGED
|
@@ -192,8 +192,8 @@
|
|
|
192
192
|
}
|
|
193
193
|
.resize-handle {
|
|
194
194
|
position: absolute;
|
|
195
|
-
width:
|
|
196
|
-
height:
|
|
195
|
+
width: 40px;
|
|
196
|
+
height: 40px;
|
|
197
197
|
padding: 0;
|
|
198
198
|
border: 0;
|
|
199
199
|
background: transparent;
|
|
@@ -202,8 +202,8 @@
|
|
|
202
202
|
}
|
|
203
203
|
.resize-handle::after {
|
|
204
204
|
position: absolute;
|
|
205
|
-
top:
|
|
206
|
-
left:
|
|
205
|
+
top: 17px;
|
|
206
|
+
left: 17px;
|
|
207
207
|
width: 6px;
|
|
208
208
|
height: 6px;
|
|
209
209
|
border: 1px solid var(--accent);
|
|
@@ -215,23 +215,23 @@
|
|
|
215
215
|
background: transparent;
|
|
216
216
|
}
|
|
217
217
|
.resize-nw {
|
|
218
|
-
top: -
|
|
219
|
-
left: -
|
|
218
|
+
top: -20px;
|
|
219
|
+
left: -20px;
|
|
220
220
|
cursor: nwse-resize;
|
|
221
221
|
}
|
|
222
222
|
.resize-ne {
|
|
223
|
-
top: -
|
|
224
|
-
right: -
|
|
223
|
+
top: -20px;
|
|
224
|
+
right: -20px;
|
|
225
225
|
cursor: nesw-resize;
|
|
226
226
|
}
|
|
227
227
|
.resize-sw {
|
|
228
|
-
bottom: -
|
|
229
|
-
left: -
|
|
228
|
+
bottom: -20px;
|
|
229
|
+
left: -20px;
|
|
230
230
|
cursor: nesw-resize;
|
|
231
231
|
}
|
|
232
232
|
.resize-se {
|
|
233
|
-
right: -
|
|
234
|
-
bottom: -
|
|
233
|
+
right: -20px;
|
|
234
|
+
bottom: -20px;
|
|
235
235
|
cursor: nwse-resize;
|
|
236
236
|
}
|
|
237
237
|
.guide {
|
|
@@ -266,3 +266,34 @@
|
|
|
266
266
|
min-height: 100%;
|
|
267
267
|
height: 100%;
|
|
268
268
|
}
|
|
269
|
+
.revision-pin {
|
|
270
|
+
position: absolute;
|
|
271
|
+
display: grid;
|
|
272
|
+
place-items: center;
|
|
273
|
+
width: 40px;
|
|
274
|
+
height: 40px;
|
|
275
|
+
min-height: 0;
|
|
276
|
+
padding: 0;
|
|
277
|
+
border: 0;
|
|
278
|
+
border-radius: 50%;
|
|
279
|
+
background: radial-gradient(
|
|
280
|
+
circle,
|
|
281
|
+
var(--accent) 0 10px,
|
|
282
|
+
var(--ui-surface) 10.5px 11.5px,
|
|
283
|
+
transparent 12px
|
|
284
|
+
);
|
|
285
|
+
color: white;
|
|
286
|
+
font: 500 11px "IBM Plex Sans", sans-serif;
|
|
287
|
+
box-shadow: none;
|
|
288
|
+
filter: drop-shadow(0 1px 2px #0003);
|
|
289
|
+
}
|
|
290
|
+
.revision-pin:hover:not(:disabled),
|
|
291
|
+
.revision-pin:active:not(:disabled) {
|
|
292
|
+
border-color: transparent;
|
|
293
|
+
background: radial-gradient(
|
|
294
|
+
circle,
|
|
295
|
+
#006aa1 0 10px,
|
|
296
|
+
var(--ui-surface) 10.5px 11.5px,
|
|
297
|
+
transparent 12px
|
|
298
|
+
);
|
|
299
|
+
}
|
package/src/styles/chrome.css
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
top: 20px;
|
|
15
15
|
left: 20px;
|
|
16
16
|
width: 252px;
|
|
17
|
-
height:
|
|
17
|
+
height: 60px;
|
|
18
18
|
gap: 10px;
|
|
19
19
|
padding: 8px 10px;
|
|
20
20
|
border-radius: 14px;
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
.document-title input {
|
|
45
|
-
height:
|
|
45
|
+
height: 40px;
|
|
46
46
|
border-color: transparent;
|
|
47
47
|
background: transparent;
|
|
48
48
|
font-size: 13px;
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
display: inline-flex;
|
|
126
126
|
align-items: center;
|
|
127
127
|
gap: 6px;
|
|
128
|
-
height:
|
|
128
|
+
height: 40px;
|
|
129
129
|
padding: 0 12px;
|
|
130
130
|
border-radius: 7px;
|
|
131
131
|
border-color: transparent;
|
|
@@ -141,17 +141,18 @@
|
|
|
141
141
|
|
|
142
142
|
.action-island button.icon-only {
|
|
143
143
|
flex: none;
|
|
144
|
-
width:
|
|
145
|
-
padding: 0
|
|
144
|
+
width: 40px;
|
|
145
|
+
padding: 0;
|
|
146
|
+
justify-content: center;
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
.action-island button.primary {
|
|
149
|
-
background:
|
|
150
|
-
color:
|
|
150
|
+
background: white;
|
|
151
|
+
color: var(--ui-text);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
.action-island button.primary:hover:not(:disabled) {
|
|
154
|
-
background:
|
|
155
|
+
background: var(--ui-hover);
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
.action-island button.primary:disabled {
|
|
@@ -159,6 +160,86 @@
|
|
|
159
160
|
color: var(--ui-muted);
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
.action-island .build-button {
|
|
164
|
+
position: relative;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
flex: none;
|
|
168
|
+
padding-inline: 9px;
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.build-button .build-label {
|
|
173
|
+
min-width: 11ch;
|
|
174
|
+
text-align: left;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.action-island button.build-button.building {
|
|
178
|
+
background: white;
|
|
179
|
+
color: #2449aa;
|
|
180
|
+
opacity: 1;
|
|
181
|
+
cursor: wait;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.build-button.building .build-label {
|
|
185
|
+
background: linear-gradient(110deg, #155e75 10%, #2449aa 30%, #a82d62 45%, #87bdeb 50%, #7535a5 55%, #155e75 80%);
|
|
186
|
+
background-size: 250% 100%;
|
|
187
|
+
background-clip: text;
|
|
188
|
+
color: transparent;
|
|
189
|
+
animation: build-colors 3s linear infinite;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.build-orb circle {
|
|
193
|
+
fill: currentColor;
|
|
194
|
+
stroke: none;
|
|
195
|
+
transform: translate(var(--x), var(--y));
|
|
196
|
+
}
|
|
197
|
+
.build-orb.active circle {
|
|
198
|
+
animation: orb-shape 8s ease-in-out infinite;
|
|
199
|
+
}
|
|
200
|
+
.build-orb.nebula {
|
|
201
|
+
width: 100%;
|
|
202
|
+
height: 100%;
|
|
203
|
+
color: #4265ad;
|
|
204
|
+
transform: rotate(-18deg);
|
|
205
|
+
}
|
|
206
|
+
.build-orb.nebula circle {
|
|
207
|
+
animation: orb-sphere 12s ease-in-out infinite;
|
|
208
|
+
animation-delay: var(--phase);
|
|
209
|
+
}
|
|
210
|
+
.build-orb.nebula circle:nth-child(3n) { fill: #9772bb; }
|
|
211
|
+
.build-orb.nebula circle:nth-child(3n + 1) { fill: #198faa; }
|
|
212
|
+
|
|
213
|
+
@keyframes orb-shape {
|
|
214
|
+
0%, 10%, 100% { transform: translate(var(--x), var(--y)); }
|
|
215
|
+
25%, 35% { transform: translate(var(--tx), var(--ty)); }
|
|
216
|
+
50%, 60% { transform: translate(var(--sx), var(--sy)); }
|
|
217
|
+
75%, 85% { transform: translate(var(--hx), var(--hy)); }
|
|
218
|
+
}
|
|
219
|
+
@keyframes orb-sphere {
|
|
220
|
+
0%, 100% { transform: translate(var(--radius), var(--y)); opacity: 0.5; }
|
|
221
|
+
25% { transform: translate(0, var(--y)); opacity: 1; }
|
|
222
|
+
50% { transform: translate(calc(-1 * var(--radius)), var(--y)); opacity: 0.5; }
|
|
223
|
+
75% { transform: translate(0, var(--y)); opacity: 0.15; }
|
|
224
|
+
}
|
|
225
|
+
@keyframes orb-turn {
|
|
226
|
+
to { transform: rotate(360deg); }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@keyframes build-colors {
|
|
230
|
+
to { background-position: 150% 0; }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@media (prefers-reduced-motion: reduce) {
|
|
234
|
+
.build-button.building .build-label,
|
|
235
|
+
.build-orb.active circle,
|
|
236
|
+
.build-orb.nebula circle,
|
|
237
|
+
.build-orb.nebula,
|
|
238
|
+
.build-nebula::before {
|
|
239
|
+
animation: none;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
162
243
|
.action-island button svg {
|
|
163
244
|
flex: none;
|
|
164
245
|
width: 18px;
|
|
@@ -170,6 +251,11 @@
|
|
|
170
251
|
stroke-width: 1.7;
|
|
171
252
|
}
|
|
172
253
|
|
|
254
|
+
.action-island .build-button .build-orb {
|
|
255
|
+
width: 20px;
|
|
256
|
+
height: 20px;
|
|
257
|
+
}
|
|
258
|
+
|
|
173
259
|
.component-dock {
|
|
174
260
|
bottom: 20px;
|
|
175
261
|
left: 50%;
|
|
@@ -188,6 +274,7 @@
|
|
|
188
274
|
gap: 1px;
|
|
189
275
|
padding: 4px 7px;
|
|
190
276
|
border-color: transparent;
|
|
277
|
+
border-radius: 10px;
|
|
191
278
|
color: var(--ui-text);
|
|
192
279
|
font-size: 11px;
|
|
193
280
|
}
|
|
@@ -212,3 +299,14 @@
|
|
|
212
299
|
min-width: 55px;
|
|
213
300
|
}
|
|
214
301
|
}
|
|
302
|
+
@media (max-width: 600px) {
|
|
303
|
+
.component-dock {
|
|
304
|
+
width: calc(100% - 24px);
|
|
305
|
+
}
|
|
306
|
+
.component-dock button {
|
|
307
|
+
flex: 1;
|
|
308
|
+
min-width: 0;
|
|
309
|
+
padding-inline: 3px;
|
|
310
|
+
font-size: 10px;
|
|
311
|
+
}
|
|
312
|
+
}
|