@miadi/stateloom-web 0.1.9 → 0.2.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/package.json +1 -1
- package/server/.next/BUILD_ID +1 -1
- package/server/.next/build-manifest.json +2 -2
- package/server/.next/server/app/_global-error.html +2 -2
- package/server/.next/server/app/_global-error.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/server/.next/server/app/_not-found.html +1 -1
- package/server/.next/server/app/_not-found.rsc +2 -2
- package/server/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/server/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/server/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/server/.next/server/app/api/docs/route.js.nft.json +1 -1
- package/server/.next/server/app/api/file/route.js.nft.json +1 -1
- package/server/.next/server/app/api/rispec/route.js.nft.json +1 -1
- package/server/.next/server/app/api/watch/route.js.nft.json +1 -1
- package/server/.next/server/app/index.html +1 -1
- package/server/.next/server/app/index.rsc +3 -3
- package/server/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/server/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/server/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/server/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/server/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/server/.next/server/app/page.js.nft.json +1 -1
- package/server/.next/server/app/page_client-reference-manifest.js +1 -1
- package/server/.next/server/chunks/[root-of-the-server]__13c3d2a8._.js +1 -1
- package/server/.next/server/chunks/[root-of-the-server]__2046b812._.js +2 -2
- package/server/.next/server/chunks/[root-of-the-server]__87bf4b91._.js +1 -1
- package/server/.next/server/chunks/[root-of-the-server]__95b05635._.js +3 -3
- package/server/.next/server/chunks/ssr/{[root-of-the-server]__d5879c0c._.js → [root-of-the-server]__b1b2bf74._.js} +2 -2
- package/server/.next/server/chunks/ssr/[root-of-the-server]__edbb995c._.js +4 -3
- package/server/.next/server/pages/404.html +1 -1
- package/server/.next/server/pages/500.html +2 -2
- package/server/.next/static/chunks/9390437c8b41a90e.js +11 -0
- package/server/.next/static/chunks/a8a37c487ef91a19.js +1 -0
- package/server/.next/static/chunks/c86e96e4d6eeb632.css +2 -0
- package/server/package-lock.json +2 -2
- package/server/package.json +1 -1
- package/server/src/app/api/file/route.ts +57 -12
- package/server/src/app/globals.css +15 -0
- package/server/src/app/page.tsx +52 -10
- package/server/src/components/BoardBoundary.tsx +46 -0
- package/server/src/components/PropertiesPanel.tsx +10 -0
- package/server/src/components/erd/ErdWorkspace.tsx +32 -3
- package/server/src/components/forms.tsx +129 -0
- package/server/src/components/sequence/SequenceWorkspace.tsx +1024 -0
- package/server/src/components/system/KindIcon.tsx +71 -0
- package/server/src/components/system/SystemStrip.tsx +285 -0
- package/server/src/components/system/SystemWorkspace.tsx +766 -0
- package/server/src/lib/docParam.ts +4 -1
- package/server/src/lib/machineFocus.ts +126 -0
- package/server/src/lib/projectFile.ts +180 -20
- package/server/src/lib/safely.ts +16 -0
- package/server/src/lib/systemLoad.ts +362 -0
- package/server/src/lib/systemParam.ts +110 -0
- package/server/src/types/definition.ts +2 -0
- package/server/stateloom-web.json +2 -2
- package/server/.next/static/chunks/636e7eb4aeda72e0.css +0 -2
- package/server/.next/static/chunks/92628504da59dcc8.js +0 -10
- package/server/.next/static/chunks/aea5b1716c68391c.js +0 -1
- /package/server/.next/static/{6dAVJYqnbe6oEAABQ1yMG → eIqAtQaBkU80e3XWuVwR8}/_buildManifest.js +0 -0
- /package/server/.next/static/{6dAVJYqnbe6oEAABQ1yMG → eIqAtQaBkU80e3XWuVwR8}/_clientMiddlewareManifest.json +0 -0
- /package/server/.next/static/{6dAVJYqnbe6oEAABQ1yMG → eIqAtQaBkU80e3XWuVwR8}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loading a system (Spec 82) in the browser: the `.sysdf.json` and each member
|
|
5
|
+
* it names, through the file API — which is what enforces the document
|
|
6
|
+
* allowlist. A member the API refuses, or cannot parse, arrives as a
|
|
7
|
+
* `LoadedMember` with `error`, and `checkSystem` reports it as Y003; it never
|
|
8
|
+
* stops the rest of the system from loading.
|
|
9
|
+
*
|
|
10
|
+
* Also here: writing the changes `reconcileSystem` proposes, because the strip,
|
|
11
|
+
* the system map and the sequence workspace all need the same write.
|
|
12
|
+
*/
|
|
13
|
+
import { useCallback, useEffect, useState } from "react";
|
|
14
|
+
import {
|
|
15
|
+
docKindOfPath,
|
|
16
|
+
hashDef,
|
|
17
|
+
isSystemDefinition,
|
|
18
|
+
machineName,
|
|
19
|
+
membersByKind,
|
|
20
|
+
reconcileSystem,
|
|
21
|
+
resolveMemberPath,
|
|
22
|
+
type LoadedMember,
|
|
23
|
+
type PatchOp,
|
|
24
|
+
type StateMachineDefinition,
|
|
25
|
+
type SystemDefinition,
|
|
26
|
+
type SystemReconcile,
|
|
27
|
+
} from "@miadi/stateloom-protocol";
|
|
28
|
+
import { createBridgeClient } from "@miadi/stateloom-client";
|
|
29
|
+
import { docQuery } from "./docParam";
|
|
30
|
+
import { loadRuntimeConfig } from "./runtimeConfig";
|
|
31
|
+
import { SYSTEM_CHANGED_EVENT } from "./systemParam";
|
|
32
|
+
|
|
33
|
+
export interface DocRead {
|
|
34
|
+
path: string;
|
|
35
|
+
content: string | null;
|
|
36
|
+
exists: boolean;
|
|
37
|
+
mtime?: number;
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readDoc(path: string | null): Promise<DocRead> {
|
|
42
|
+
try {
|
|
43
|
+
const r = await fetch(`/api/file${docQuery(path)}`, { cache: "no-store" });
|
|
44
|
+
const body = await r.json();
|
|
45
|
+
if (!r.ok || body.error) return { path: path ?? "", content: null, exists: false, error: body.error ?? `HTTP ${r.status}` };
|
|
46
|
+
return { path: body.path ?? path ?? "", content: body.content ?? null, exists: !!body.exists, mtime: body.mtime };
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return { path: path ?? "", content: null, exists: false, error: e instanceof Error ? e.message : String(e) };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The file changed on disk after it was read: the write was refused, nothing was lost. */
|
|
53
|
+
export class DocConflict extends Error {
|
|
54
|
+
constructor(path: string) {
|
|
55
|
+
super(`${path.split("/").pop()} changed on disk since it was read`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Write a whole document. Returns the new mtime; throws with the server's words
|
|
61
|
+
* on refusal. With `readMtime` (when the writer read it), the server refuses the
|
|
62
|
+
* write if the file changed since, and this throws `DocConflict`.
|
|
63
|
+
*/
|
|
64
|
+
export async function writeDoc(path: string, value: unknown, readMtime?: number): Promise<number | undefined> {
|
|
65
|
+
const r = await fetch(`/api/file${docQuery(path)}`, {
|
|
66
|
+
method: "PUT",
|
|
67
|
+
headers: { "content-type": "application/json" },
|
|
68
|
+
body: JSON.stringify({ content: JSON.stringify(value, null, 2) + "\n", ...(readMtime !== undefined ? { mtime: readMtime } : {}) }),
|
|
69
|
+
});
|
|
70
|
+
const body = await r.json().catch(() => ({}));
|
|
71
|
+
if (r.status === 409) throw new DocConflict(path);
|
|
72
|
+
if (!r.ok) throw new Error(body?.error ?? `HTTP ${r.status}`);
|
|
73
|
+
return body?.mtime;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface LoadedSystem {
|
|
77
|
+
/** The system file, as the server resolved it. */
|
|
78
|
+
path: string;
|
|
79
|
+
def: SystemDefinition | null;
|
|
80
|
+
members: LoadedMember[];
|
|
81
|
+
/** Member path as the system writes it → absolute path. */
|
|
82
|
+
absOf: Record<string, string>;
|
|
83
|
+
/** Machine members stored wrapped as `{ stateMachine: … }`, so a write keeps the file's shape. */
|
|
84
|
+
wrapped: Record<string, boolean>;
|
|
85
|
+
/** When each file was read (mtime), the system file's under its own path: what a write must still find. */
|
|
86
|
+
mtimes: Record<string, number>;
|
|
87
|
+
error?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function loadSystem(systemPath: string): Promise<LoadedSystem> {
|
|
91
|
+
const read = await readDoc(systemPath);
|
|
92
|
+
const empty = { path: read.path || systemPath, def: null, members: [], absOf: {}, wrapped: {}, mtimes: {} };
|
|
93
|
+
if (read.error) return { ...empty, error: read.error };
|
|
94
|
+
if (!read.exists || !read.content) return { ...empty, error: `${systemPath} does not exist` };
|
|
95
|
+
let def: SystemDefinition | null = null;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(read.content);
|
|
98
|
+
if (isSystemDefinition(parsed)) def = parsed;
|
|
99
|
+
} catch {
|
|
100
|
+
// Reported below.
|
|
101
|
+
}
|
|
102
|
+
if (!def) return { ...empty, error: `${read.path} is not a readable system definition` };
|
|
103
|
+
|
|
104
|
+
const absOf: Record<string, string> = {};
|
|
105
|
+
const wrapped: Record<string, boolean> = {};
|
|
106
|
+
const mtimes: Record<string, number> = { [read.path]: read.mtime ?? 0 };
|
|
107
|
+
const members = await Promise.all(
|
|
108
|
+
(def.members ?? [])
|
|
109
|
+
.filter((m) => m?.path)
|
|
110
|
+
.map(async (m): Promise<LoadedMember> => {
|
|
111
|
+
const abs = resolveMemberPath(read.path, m.path);
|
|
112
|
+
absOf[m.path] = abs;
|
|
113
|
+
const kind = docKindOfPath(m.path);
|
|
114
|
+
const doc = await readDoc(abs);
|
|
115
|
+
if (doc.error) return { path: m.path, kind, def: null, error: doc.error };
|
|
116
|
+
if (!doc.exists || !doc.content) return { path: m.path, kind, def: null, error: "the file does not exist" };
|
|
117
|
+
mtimes[m.path] = doc.mtime ?? 0;
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(doc.content);
|
|
120
|
+
if (kind === "machine" && parsed && (parsed.stateMachine || parsed.StateMachine)) {
|
|
121
|
+
wrapped[m.path] = true;
|
|
122
|
+
return { path: m.path, kind, def: parsed.stateMachine ?? parsed.StateMachine };
|
|
123
|
+
}
|
|
124
|
+
return { path: m.path, kind, def: parsed };
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return { path: m.path, kind, def: null, error: `not JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
127
|
+
}
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
return { path: read.path, def, members, absOf, wrapped, mtimes };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Tell every holder of a loaded system that one of its files was written. */
|
|
134
|
+
export function notifySystemChanged(): void {
|
|
135
|
+
window.dispatchEvent(new Event(SYSTEM_CHANGED_EVENT));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A system, loaded and kept current: it reloads whenever a member or the system file is written from this page. */
|
|
139
|
+
export function useSystem(systemPath: string | null): { system: LoadedSystem | null; reload: () => void } {
|
|
140
|
+
const [loaded, setLoaded] = useState<{ for: string; system: LoadedSystem } | null>(null);
|
|
141
|
+
const [version, setVersion] = useState(0);
|
|
142
|
+
const reload = useCallback(() => setVersion((v) => v + 1), []);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
window.addEventListener(SYSTEM_CHANGED_EVENT, reload);
|
|
145
|
+
return () => window.removeEventListener(SYSTEM_CHANGED_EVENT, reload);
|
|
146
|
+
}, [reload]);
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!systemPath) return;
|
|
149
|
+
let live = true;
|
|
150
|
+
loadSystem(systemPath).then((system) => live && setLoaded({ for: systemPath, system }));
|
|
151
|
+
return () => {
|
|
152
|
+
live = false;
|
|
153
|
+
};
|
|
154
|
+
}, [systemPath, version]);
|
|
155
|
+
// A system loaded for another path is not this one's; a reload of the same one keeps it on screen meanwhile.
|
|
156
|
+
return { system: systemPath && loaded?.for === systemPath ? loaded.system : null, reload };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The absolute path of a member, or null when `doc` is not one of the system's. */
|
|
160
|
+
export function memberOfDoc(system: LoadedSystem | null, doc: string | null): string | null {
|
|
161
|
+
if (!system || !doc) return null;
|
|
162
|
+
for (const [member, abs] of Object.entries(system.absOf)) if (abs === doc) return member;
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Tell a document's room what was just written, the way the MCP does
|
|
168
|
+
* (mcp/src/live.ts): join, and if the room already holds the written document
|
|
169
|
+
* — a cold room seeds itself from disk, and the file watch may have got there
|
|
170
|
+
* first — send nothing. If the room holds exactly what the ops were computed
|
|
171
|
+
* from, send the ops; if it holds anything else, send the whole document, so
|
|
172
|
+
* the room ends equal to the disk either way. Without a hub this is a no-op:
|
|
173
|
+
* the file is written, and the file watch carries it.
|
|
174
|
+
*/
|
|
175
|
+
async function sendToRoom(
|
|
176
|
+
docId: string,
|
|
177
|
+
written: unknown,
|
|
178
|
+
mtime: number | undefined,
|
|
179
|
+
patch?: { from: unknown; ops: PatchOp[] },
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
const { bridgeUrl } = await loadRuntimeConfig();
|
|
182
|
+
if (!bridgeUrl) return;
|
|
183
|
+
const client = createBridgeClient({ url: bridgeUrl, role: "web", docId, name: "web-reconcile", autoResync: false });
|
|
184
|
+
try {
|
|
185
|
+
const joined = await Promise.race([
|
|
186
|
+
client.join(),
|
|
187
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("join timeout")), 3000)),
|
|
188
|
+
]);
|
|
189
|
+
const held = joined.snapshot?.def as StateMachineDefinition | null | undefined;
|
|
190
|
+
const hash = (d: unknown) => hashDef(d as StateMachineDefinition);
|
|
191
|
+
if (held && hash(held) === hash(written)) return;
|
|
192
|
+
await new Promise<void>((resolve) => {
|
|
193
|
+
const off = client.on("ack", () => {
|
|
194
|
+
off();
|
|
195
|
+
resolve();
|
|
196
|
+
});
|
|
197
|
+
if (patch && held && hash(held) === hash(patch.from)) client.emitPatch(patch.ops, mtime);
|
|
198
|
+
else client.emitFull(written as StateMachineDefinition, mtime);
|
|
199
|
+
setTimeout(resolve, 800);
|
|
200
|
+
});
|
|
201
|
+
} catch {
|
|
202
|
+
// The write already happened; a hub that did not answer only costs the live repaint.
|
|
203
|
+
} finally {
|
|
204
|
+
client.disconnect();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Write what `reconcileSystem` proposed from `system`: each changed machine,
|
|
210
|
+
* ERD and the system file, disk first and only if the file is still the one
|
|
211
|
+
* `system` read (else it is a conflict, and nothing of it is written); then each
|
|
212
|
+
* room is told. Every holder of the system reloads after.
|
|
213
|
+
*/
|
|
214
|
+
export async function applyReconcile(
|
|
215
|
+
system: LoadedSystem,
|
|
216
|
+
result: SystemReconcile,
|
|
217
|
+
): Promise<{ written: string[]; failed: string[]; conflicts: string[] }> {
|
|
218
|
+
const written: string[] = [];
|
|
219
|
+
const failed: string[] = [];
|
|
220
|
+
const conflicts: string[] = [];
|
|
221
|
+
const attempt = async (label: string, run: () => Promise<void>) => {
|
|
222
|
+
try {
|
|
223
|
+
await run();
|
|
224
|
+
written.push(label);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
if (e instanceof DocConflict) conflicts.push(label);
|
|
227
|
+
else failed.push(`${label}: ${e instanceof Error ? e.message : String(e)}`);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
for (const m of result.machines) {
|
|
231
|
+
const abs = system.absOf[m.member];
|
|
232
|
+
if (!abs || !m.ops.length) continue;
|
|
233
|
+
await attempt(m.member, async () => {
|
|
234
|
+
const body = system.wrapped[m.member] ? { stateMachine: m.def } : m.def;
|
|
235
|
+
const mtime = await writeDoc(abs, body, system.mtimes[m.member]);
|
|
236
|
+
const before = system.members.find((x) => x.path === m.member)?.def;
|
|
237
|
+
await sendToRoom(abs, m.def, mtime, { from: before, ops: m.ops as PatchOp[] });
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
for (const e of result.erds) {
|
|
241
|
+
const abs = system.absOf[e.member];
|
|
242
|
+
if (!abs) continue;
|
|
243
|
+
await attempt(e.member, async () => {
|
|
244
|
+
const mtime = await writeDoc(abs, e.def, system.mtimes[e.member]);
|
|
245
|
+
await sendToRoom(abs, e.def, mtime);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (result.system) {
|
|
249
|
+
await attempt(system.path.split("/").pop() ?? system.path, async () => {
|
|
250
|
+
const mtime = await writeDoc(system.path, result.system!.def, system.mtimes[system.path]);
|
|
251
|
+
await sendToRoom(system.path, result.system!.def, mtime);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
notifySystemChanged();
|
|
255
|
+
return { written, failed, conflicts };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Reconcile a system as it is on disk NOW: load it fresh, compute, write. If a
|
|
260
|
+
* file changed between that load and its write (an agent's edit), reload and
|
|
261
|
+
* compute once more from what is there, so the edit is built on, never lost.
|
|
262
|
+
*/
|
|
263
|
+
export async function reconcileNow(systemPath: string): Promise<{
|
|
264
|
+
result: SystemReconcile | null;
|
|
265
|
+
written: string[];
|
|
266
|
+
failed: string[];
|
|
267
|
+
conflicts: string[];
|
|
268
|
+
retried: boolean;
|
|
269
|
+
}> {
|
|
270
|
+
let last = { result: null as SystemReconcile | null, written: [] as string[], failed: [] as string[], conflicts: [] as string[] };
|
|
271
|
+
for (let round = 0; round < 2; round += 1) {
|
|
272
|
+
const system = await loadSystem(systemPath);
|
|
273
|
+
if (!system.def) return { ...last, failed: [system.error ?? "the system could not be read"], retried: round > 0 };
|
|
274
|
+
const result = reconcileSystem(system.def, system.members);
|
|
275
|
+
if (!result.count) return { ...last, result, conflicts: round ? last.conflicts : [], retried: round > 0 };
|
|
276
|
+
const out = await applyReconcile(system, result);
|
|
277
|
+
last = { result, written: [...last.written, ...out.written], failed: out.failed, conflicts: out.conflicts };
|
|
278
|
+
if (!out.conflicts.length) return { ...last, retried: round > 0 };
|
|
279
|
+
}
|
|
280
|
+
return { ...last, retried: true };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The names a system's members define, for suggesting what a sequence may name. */
|
|
284
|
+
export interface SystemVocabulary {
|
|
285
|
+
events: string[];
|
|
286
|
+
states: string[];
|
|
287
|
+
machines: string[];
|
|
288
|
+
objects: string[];
|
|
289
|
+
entities: string[];
|
|
290
|
+
actors: string[];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function systemVocabulary(system: LoadedSystem | null): SystemVocabulary {
|
|
294
|
+
const out: Record<keyof SystemVocabulary, Set<string>> = {
|
|
295
|
+
events: new Set(),
|
|
296
|
+
states: new Set(),
|
|
297
|
+
machines: new Set(),
|
|
298
|
+
objects: new Set(),
|
|
299
|
+
entities: new Set(),
|
|
300
|
+
actors: new Set(),
|
|
301
|
+
};
|
|
302
|
+
if (!system) return { events: [], states: [], machines: [], objects: [], entities: [], actors: [] };
|
|
303
|
+
const { machines, erds } = membersByKind(system.members);
|
|
304
|
+
machines.forEach(({ def }, i) => {
|
|
305
|
+
out.machines.add(machineName(def, i));
|
|
306
|
+
for (const src of def.events ?? []) {
|
|
307
|
+
for (const e of [...(src?.events ?? []), ...(src?.timers ?? [])]) if (e?.id) out.events.add(e.id);
|
|
308
|
+
}
|
|
309
|
+
for (const o of def.settings?.objects ?? []) if (o?.instance) out.objects.add(o.instance);
|
|
310
|
+
const walk = (s: StateMachineDefinition["state"] | undefined): void => {
|
|
311
|
+
if (!s) return;
|
|
312
|
+
out.states.add(s.name);
|
|
313
|
+
(s.states ?? []).forEach(walk);
|
|
314
|
+
(s.parallel?.states ?? []).forEach(walk);
|
|
315
|
+
};
|
|
316
|
+
walk(def.state);
|
|
317
|
+
});
|
|
318
|
+
for (const { def } of erds) for (const e of def.entities ?? []) if (e?.name) out.entities.add(e.name);
|
|
319
|
+
for (const a of system.def?.actors ?? []) if (a?.name) out.actors.add(a.name);
|
|
320
|
+
const sorted = (s: Set<string>) => [...s].sort((a, b) => a.localeCompare(b));
|
|
321
|
+
return {
|
|
322
|
+
events: sorted(out.events),
|
|
323
|
+
states: sorted(out.states),
|
|
324
|
+
machines: sorted(out.machines),
|
|
325
|
+
objects: sorted(out.objects),
|
|
326
|
+
entities: sorted(out.entities),
|
|
327
|
+
actors: sorted(out.actors),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Join a room and stay in it: after the socket reconnects (a hub restart), the
|
|
333
|
+
* hub has forgotten the room, so join again. Returns a function that stops it.
|
|
334
|
+
*/
|
|
335
|
+
export function stayJoined(client: ReturnType<typeof createBridgeClient>, onJoined?: () => void): () => void {
|
|
336
|
+
let joinedOnce = false;
|
|
337
|
+
let dropped = false;
|
|
338
|
+
let stopped = false;
|
|
339
|
+
const join = () => {
|
|
340
|
+
client
|
|
341
|
+
.join()
|
|
342
|
+
.then(() => {
|
|
343
|
+
joinedOnce = true;
|
|
344
|
+
if (!stopped) onJoined?.();
|
|
345
|
+
})
|
|
346
|
+
.catch(() => {
|
|
347
|
+
// Refused or not reachable: the next reconnect tries again.
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
const off = client.on("status", (status) => {
|
|
351
|
+
if (status !== "connected") dropped = joinedOnce;
|
|
352
|
+
else if (dropped) {
|
|
353
|
+
dropped = false;
|
|
354
|
+
join();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
join();
|
|
358
|
+
return () => {
|
|
359
|
+
stopped = true;
|
|
360
|
+
off();
|
|
361
|
+
};
|
|
362
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The view of a system, kept in the URL (Spec 82): `?doc=<member>` is what is
|
|
5
|
+
* open, `system=<.sysdf.json>` says which system it is read as a member of, and
|
|
6
|
+
* `focus=<kind>:<name>` is the element in hand. None of it is ever written into
|
|
7
|
+
* a document — a view belongs to the browser that is looking.
|
|
8
|
+
*
|
|
9
|
+
* Four window events tie the pieces together without a shared store:
|
|
10
|
+
*
|
|
11
|
+
* - `stateloom:doc-change` (docParam.ts): the open document changed.
|
|
12
|
+
* - `stateloom:focus-request`: something outside the workspace (the strip, an
|
|
13
|
+
* agent's `view:show`) asks the open workspace to select and centre `focus`.
|
|
14
|
+
* - `stateloom:focus`: the workspace wrote the element the person selected, so
|
|
15
|
+
* the strip can list its links.
|
|
16
|
+
* - `stateloom:system-changed`: a member or the system file was written; every
|
|
17
|
+
* holder of a loaded system reloads it.
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useState } from "react";
|
|
20
|
+
import { DOC_CHANGE_EVENT } from "./docParam";
|
|
21
|
+
|
|
22
|
+
export const FOCUS_EVENT = "stateloom:focus";
|
|
23
|
+
export const FOCUS_REQUEST_EVENT = "stateloom:focus-request";
|
|
24
|
+
export const SYSTEM_CHANGED_EVENT = "stateloom:system-changed";
|
|
25
|
+
/** A one-line note for the system strip: `{ detail: { text, origin? } }`. */
|
|
26
|
+
export const STRIP_NOTE_EVENT = "stateloom:strip-note";
|
|
27
|
+
|
|
28
|
+
export function urlParam(name: string): string | null {
|
|
29
|
+
if (typeof window === "undefined") return null;
|
|
30
|
+
return new URLSearchParams(window.location.search).get(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A URL parameter as reactive state, following every way this app changes it. */
|
|
34
|
+
export function useUrlParam(name: string): string | null {
|
|
35
|
+
const [value, setValue] = useState<string | null>(null);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const update = () => setValue(urlParam(name));
|
|
38
|
+
update();
|
|
39
|
+
const events = ["popstate", DOC_CHANGE_EVENT, FOCUS_EVENT, FOCUS_REQUEST_EVENT];
|
|
40
|
+
for (const e of events) window.addEventListener(e, update);
|
|
41
|
+
return () => {
|
|
42
|
+
for (const e of events) window.removeEventListener(e, update);
|
|
43
|
+
};
|
|
44
|
+
}, [name]);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The query string for a member opened inside a system, focused on one element. */
|
|
49
|
+
export function systemQuery(target: { doc: string; system?: string | null; focus?: string | null }): string {
|
|
50
|
+
const params = new URLSearchParams();
|
|
51
|
+
params.set("doc", target.doc);
|
|
52
|
+
if (target.system) params.set("system", target.system);
|
|
53
|
+
if (target.focus) params.set("focus", target.focus);
|
|
54
|
+
return `?${params.toString()}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Open `doc` (inside `system`, when given; the current one otherwise) and ask
|
|
59
|
+
* whichever workspace shows it to focus `focus`. A new document is a history
|
|
60
|
+
* entry, so back walks it; a new focus on the same document is not.
|
|
61
|
+
*/
|
|
62
|
+
export function openInSystem(target: { doc: string; system?: string | null; focus?: string | null }): void {
|
|
63
|
+
const url = new URL(window.location.href);
|
|
64
|
+
const docChanged = url.searchParams.get("doc") !== target.doc;
|
|
65
|
+
url.searchParams.set("doc", target.doc);
|
|
66
|
+
const system = target.system === undefined ? url.searchParams.get("system") : target.system;
|
|
67
|
+
if (system) url.searchParams.set("system", system);
|
|
68
|
+
else url.searchParams.delete("system");
|
|
69
|
+
if (target.focus) url.searchParams.set("focus", target.focus);
|
|
70
|
+
else url.searchParams.delete("focus");
|
|
71
|
+
if (docChanged) window.history.pushState({}, "", url);
|
|
72
|
+
else window.history.replaceState(window.history.state, "", url);
|
|
73
|
+
if (docChanged) window.dispatchEvent(new Event(DOC_CHANGE_EVENT));
|
|
74
|
+
window.dispatchEvent(new Event(FOCUS_REQUEST_EVENT));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Record the element a person selected, without a history entry. */
|
|
78
|
+
export function writeFocus(focus: string | null): void {
|
|
79
|
+
if (typeof window === "undefined") return;
|
|
80
|
+
const url = new URL(window.location.href);
|
|
81
|
+
if ((url.searchParams.get("focus") ?? null) === focus) return;
|
|
82
|
+
if (focus) url.searchParams.set("focus", focus);
|
|
83
|
+
else url.searchParams.delete("focus");
|
|
84
|
+
window.history.replaceState(window.history.state, "", url);
|
|
85
|
+
window.dispatchEvent(new CustomEvent(FOCUS_EVENT));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The focus a workspace is asked to show, with a counter that moves on every
|
|
90
|
+
* request — on mount, on back/forward, on a document change and on an explicit
|
|
91
|
+
* request — so the same focus asked twice is shown twice.
|
|
92
|
+
*/
|
|
93
|
+
export function useFocusRequest(): { focus: string | null; nonce: number } {
|
|
94
|
+
const [request, setRequest] = useState<{ focus: string | null; nonce: number }>({ focus: null, nonce: 0 });
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const bump = () => setRequest((r) => ({ focus: urlParam("focus"), nonce: r.nonce + 1 }));
|
|
97
|
+
bump();
|
|
98
|
+
const events = ["popstate", DOC_CHANGE_EVENT, FOCUS_REQUEST_EVENT];
|
|
99
|
+
for (const e of events) window.addEventListener(e, bump);
|
|
100
|
+
return () => {
|
|
101
|
+
for (const e of events) window.removeEventListener(e, bump);
|
|
102
|
+
};
|
|
103
|
+
}, []);
|
|
104
|
+
return request;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Say one short line in the system strip. */
|
|
108
|
+
export function stripNote(text: string, origin?: string): void {
|
|
109
|
+
window.dispatchEvent(new CustomEvent(STRIP_NOTE_EVENT, { detail: { text, origin } }));
|
|
110
|
+
}
|
|
@@ -101,6 +101,8 @@ export interface StateDef {
|
|
|
101
101
|
description?: string;
|
|
102
102
|
/** Working notes about this state: the conversation about it, where `description` says what it is. */
|
|
103
103
|
notes?: string;
|
|
104
|
+
/** What an agent is asked to resolve while the machine is in this state. Ignored by engines and codegen. */
|
|
105
|
+
prompt?: string;
|
|
104
106
|
onEntry?: { actions: ActionDef[] };
|
|
105
107
|
onExit?: { actions: ActionDef[] };
|
|
106
108
|
transitions?: TransitionDef[];
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--color-red-200:#ffcaca;--color-red-300:#ffa3a3;--color-red-400:#ff6568;--color-red-500:#fb2c36;--color-red-600:#e40014;--color-red-900:#82181a;--color-red-950:#460809;--color-amber-300:#ffd236;--color-amber-400:#fcbb00;--color-amber-700:#b75000;--color-yellow-400:#fac800;--color-yellow-500:#edb200;--color-yellow-600:#cd8900;--color-green-400:#05df72;--color-green-500:#00c758;--color-emerald-500:#00bb7f;--color-blue-200:#bedbff;--color-blue-300:#90c5ff;--color-blue-400:#54a2ff;--color-blue-500:#3080ff;--color-blue-600:#155dfc;--color-blue-900:#1c398e;--color-blue-950:#162456;--color-gray-200:#e5e7eb;--color-gray-300:#d1d5dc;--color-gray-400:#99a1af;--color-gray-500:#6a7282;--color-gray-600:#4a5565;--color-gray-700:#364153;--color-gray-800:#1e2939;--color-gray-900:#101828;--color-gray-950:#030712;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--font-weight-medium:500;--font-weight-semibold:600;--tracking-normal:0em;--tracking-wide:.025em;--leading-snug:1.375;--leading-relaxed:1.625;--radius-lg:.5rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0,0,.2,1);--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-geist-sans);--default-mono-font-family:var(--font-geist-mono)}@supports (color:lab(0% 0 0)){:root,:host{--color-red-200:lab(86.017% 19.8815 7.75869);--color-red-300:lab(76.5514% 36.422 15.5335);--color-red-400:lab(63.7053% 60.745 31.3109);--color-red-500:lab(55.4814% 75.0732 48.8528);--color-red-600:lab(48.4493% 77.4328 61.5452);--color-red-900:lab(28.5139% 44.5539 29.0463);--color-red-950:lab(13.003% 29.04 16.7519);--color-amber-300:lab(86.4156% 6.13147 78.3961);--color-amber-400:lab(80.1641% 16.6016 99.2089);--color-amber-700:lab(47.2709% 42.9082 69.2966);--color-yellow-400:lab(83.2664% 8.65132 106.895);--color-yellow-500:lab(76.3898% 14.5258 98.4589);--color-yellow-600:lab(62.7799% 22.4197 86.1544);--color-green-400:lab(78.503% -64.9265 39.7492);--color-green-500:lab(70.5521% -66.5147 45.8073);--color-emerald-500:lab(66.9756% -58.27 19.5419);--color-blue-200:lab(86.15% -4.04379 -21.0797);--color-blue-300:lab(77.5052% -6.4629 -36.42);--color-blue-400:lab(65.0361% -1.42065 -56.9802);--color-blue-500:lab(54.1736% 13.3369 -74.6839);--color-blue-600:lab(44.0605% 29.0279 -86.0352);--color-blue-900:lab(26.1542% 15.7545 -51.5504);--color-blue-950:lab(15.6723% 8.86232 -32.2945);--color-gray-200:lab(91.6229% -.159115 -2.26791);--color-gray-300:lab(85.1236% -.612259 -3.7138);--color-gray-400:lab(65.9269% -.832707 -8.17473);--color-gray-500:lab(47.7841% -.393182 -10.0268);--color-gray-600:lab(35.6337% -1.58697 -10.8425);--color-gray-700:lab(27.1134% -.956401 -12.3224);--color-gray-800:lab(16.1051% -1.18239 -11.7533);--color-gray-900:lab(8.11897% .811279 -12.254);--color-gray-950:lab(1.90334% .278696 -5.48866)}}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.inset-x-4{inset-inline:calc(var(--spacing)*4)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing)*0)}.top-1{top:calc(var(--spacing)*1)}.top-2{top:calc(var(--spacing)*2)}.top-full{top:100%}.right-0{right:calc(var(--spacing)*0)}.right-1{right:calc(var(--spacing)*1)}.right-1\/2{right:50%}.right-3{right:calc(var(--spacing)*3)}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-3{bottom:calc(var(--spacing)*3)}.left-0{left:calc(var(--spacing)*0)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing)*2)}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.col-span-2{grid-column:span 2/span 2}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.-mr-2{margin-right:calc(var(--spacing)*-2)}.mr-1{margin-right:calc(var(--spacing)*1)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.ml-0\.5{margin-left:calc(var(--spacing)*.5)}.ml-1{margin-left:calc(var(--spacing)*1)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-0\.5{height:calc(var(--spacing)*.5)}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-2{height:calc(var(--spacing)*2)}.h-4{height:calc(var(--spacing)*4)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-14{height:calc(var(--spacing)*14)}.h-16{height:calc(var(--spacing)*16)}.h-20{height:calc(var(--spacing)*20)}.h-24{height:calc(var(--spacing)*24)}.h-\[var\(--sheet-h\)\]{height:var(--sheet-h)}.h-full{height:100%}.max-h-64{max-height:calc(var(--spacing)*64)}.max-h-\[85dvh\]{max-height:85dvh}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-11{min-height:calc(var(--spacing)*11)}.w-1\.5{width:calc(var(--spacing)*1.5)}.w-2{width:calc(var(--spacing)*2)}.w-6{width:calc(var(--spacing)*6)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-24{width:calc(var(--spacing)*24)}.w-32{width:calc(var(--spacing)*32)}.w-56{width:calc(var(--spacing)*56)}.w-80{width:calc(var(--spacing)*80)}.w-full{width:100%}.w-px{width:1px}.max-w-\[40vw\]{max-width:40vw}.max-w-\[50vw\]{max-width:50vw}.max-w-\[60\%\]{max-width:60%}.max-w-\[75\%\]{max-width:75%}.max-w-\[80px\]{max-width:80px}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-11{min-width:calc(var(--spacing)*11)}.min-w-\[14px\]{min-width:14px}.min-w-\[16px\]{min-width:16px}.min-w-\[180px\]{min-width:180px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-4{--tw-translate-x:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-5{--tw-translate-x:calc(var(--spacing)*5);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-0{--tw-translate-y:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-full{--tw-translate-y:100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-y{resize:vertical}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.-space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*-1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*-1)*calc(1 - var(--tw-space-x-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-blue-400{border-color:var(--color-blue-400)}.border-blue-500{border-color:var(--color-blue-500)}.border-blue-500\/60{border-color:#3080ff99}@supports (color:color-mix(in lab, red, red)){.border-blue-500\/60{border-color:color-mix(in oklab,var(--color-blue-500)60%,transparent)}}.border-gray-600{border-color:var(--color-gray-600)}.border-gray-700{border-color:var(--color-gray-700)}.border-gray-800{border-color:var(--color-gray-800)}.border-gray-800\/50{border-color:#1e293980}@supports (color:color-mix(in lab, red, red)){.border-gray-800\/50{border-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}.border-red-900\/80{border-color:#82181acc}@supports (color:color-mix(in lab, red, red)){.border-red-900\/80{border-color:color-mix(in oklab,var(--color-red-900)80%,transparent)}}.\!bg-amber-700{background-color:var(--color-amber-700)!important}.bg-amber-400{background-color:var(--color-amber-400)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black)60%,transparent)}}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-blue-900\/30{background-color:#1c398e4d}@supports (color:color-mix(in lab, red, red)){.bg-blue-900\/30{background-color:color-mix(in oklab,var(--color-blue-900)30%,transparent)}}.bg-blue-900\/60{background-color:#1c398e99}@supports (color:color-mix(in lab, red, red)){.bg-blue-900\/60{background-color:color-mix(in oklab,var(--color-blue-900)60%,transparent)}}.bg-blue-950\/60{background-color:#16245699}@supports (color:color-mix(in lab, red, red)){.bg-blue-950\/60{background-color:color-mix(in oklab,var(--color-blue-950)60%,transparent)}}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-gray-600{background-color:var(--color-gray-600)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-800\/50{background-color:#1e293980}@supports (color:color-mix(in lab, red, red)){.bg-gray-800\/50{background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}.bg-gray-900{background-color:var(--color-gray-900)}.bg-gray-900\/50{background-color:#10182880}@supports (color:color-mix(in lab, red, red)){.bg-gray-900\/50{background-color:color-mix(in oklab,var(--color-gray-900)50%,transparent)}}.bg-gray-900\/90{background-color:#101828e6}@supports (color:color-mix(in lab, red, red)){.bg-gray-900\/90{background-color:color-mix(in oklab,var(--color-gray-900)90%,transparent)}}.bg-gray-900\/95{background-color:#101828f2}@supports (color:color-mix(in lab, red, red)){.bg-gray-900\/95{background-color:color-mix(in oklab,var(--color-gray-900)95%,transparent)}}.bg-gray-950{background-color:var(--color-gray-950)}.bg-gray-950\/70{background-color:#030712b3}@supports (color:color-mix(in lab, red, red)){.bg-gray-950\/70{background-color:color-mix(in oklab,var(--color-gray-950)70%,transparent)}}.bg-red-600{background-color:var(--color-red-600)}.bg-red-950\/80{background-color:#460809cc}@supports (color:color-mix(in lab, red, red)){.bg-red-950\/80{background-color:color-mix(in oklab,var(--color-red-950)80%,transparent)}}.bg-transparent{background-color:#0000}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.pt-1{padding-top:calc(var(--spacing)*1)}.pt-2{padding-top:calc(var(--spacing)*2)}.pr-1{padding-right:calc(var(--spacing)*1)}.pb-0\.5{padding-bottom:calc(var(--spacing)*.5)}.pb-1{padding-bottom:calc(var(--spacing)*1)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-geist-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.leading-4{--tw-leading:calc(var(--spacing)*4);line-height:calc(var(--spacing)*4)}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.\!text-red-400{color:var(--color-red-400)!important}.\!text-white{color:var(--color-white)!important}.text-amber-300{color:var(--color-amber-300)}.text-amber-400{color:var(--color-amber-400)}.text-blue-200{color:var(--color-blue-200)}.text-blue-300{color:var(--color-blue-300)}.text-blue-400{color:var(--color-blue-400)}.text-blue-500{color:var(--color-blue-500)}.text-emerald-500{color:var(--color-emerald-500)}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-red-200{color:var(--color-red-200)}.text-red-300{color:var(--color-red-300)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-white{color:var(--color-white)}.text-yellow-400{color:var(--color-yellow-400)}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-600{color:var(--color-yellow-600)}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-gray-900{--tw-ring-color:var(--color-gray-900)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[transform\,height\]{transition-property:transform,height;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.placeholder\:text-gray-600::placeholder{color:var(--color-gray-600)}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.hover\:bg-gray-800\/30:hover{background-color:#1e29394d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-gray-800\/30:hover{background-color:color-mix(in oklab,var(--color-gray-800)30%,transparent)}}.hover\:text-blue-400:hover{color:var(--color-blue-400)}.hover\:text-gray-300:hover{color:var(--color-gray-300)}.hover\:text-gray-400:hover{color:var(--color-gray-400)}.hover\:text-red-400:hover{color:var(--color-red-400)}}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}@media not all and (min-width:48rem){.max-md\:mt-0{margin-top:calc(var(--spacing)*0)}}@media (min-width:48rem){.md\:pointer-events-auto{pointer-events:auto}.md\:static{position:static}.md\:z-auto{z-index:auto}.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-8{height:calc(var(--spacing)*8)}.md\:h-auto{height:auto}.md\:max-h-\[80dvh\]{max-height:80dvh}.md\:max-h-\[calc\(100vh-160px\)\]{max-height:calc(100vh - 160px)}.md\:min-h-0{min-height:calc(var(--spacing)*0)}.md\:w-8{width:calc(var(--spacing)*8)}.md\:w-24{width:calc(var(--spacing)*24)}.md\:w-80{width:calc(var(--spacing)*80)}.md\:w-\[80\%\]{width:80%}.md\:max-w-3xl{max-width:var(--container-3xl)}.md\:max-w-\[16rem\]{max-width:16rem}.md\:max-w-none{max-width:none}.md\:min-w-0{min-width:calc(var(--spacing)*0)}.md\:min-w-\[140px\]{min-width:140px}.md\:translate-y-0{--tw-translate-y:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.md\:flex-row{flex-direction:row}.md\:flex-wrap{flex-wrap:wrap}.md\:items-center{align-items:center}.md\:overflow-y-auto{overflow-y:auto}.md\:rounded-none{border-radius:0}.md\:border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.md\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.md\:p-0{padding:calc(var(--spacing)*0)}.md\:px-2{padding-inline:calc(var(--spacing)*2)}.md\:px-3{padding-inline:calc(var(--spacing)*3)}.md\:py-1{padding-block:calc(var(--spacing)*1)}.md\:py-1\.5{padding-block:calc(var(--spacing)*1.5)}.md\:py-2{padding-block:calc(var(--spacing)*2)}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.md\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.md\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.md\:transition-none{transition-property:none}}@media (min-width:64rem){.lg\:inline{display:inline}}}:root{--background:#030712;--foreground:#e2e8f0}body{background:var(--background);color:var(--foreground);font-family:Arial,Helvetica,sans-serif}.toolbar-btn{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-700);background-color:var(--color-gray-800);padding-inline:calc(var(--spacing)*2.5);padding-block:calc(var(--spacing)*1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-300);border-radius:.25rem}@media (hover:hover){.toolbar-btn:hover{background-color:var(--color-gray-700)}}.toolbar-btn-sm{background-color:var(--color-gray-700);padding-inline:calc(var(--spacing)*1.5);padding-block:calc(var(--spacing)*.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-300);border-radius:.25rem}@media (hover:hover){.toolbar-btn-sm:hover{background-color:var(--color-gray-600)}}:root{--sm-dock-h:3.5rem}html,body{height:100%}html,body,.app-shell{touch-action:pan-x pan-y}body{overscroll-behavior:none;overflow:hidden}.app-shell{height:100dvh;padding-top:env(safe-area-inset-top);padding-left:env(safe-area-inset-left);padding-right:env(safe-area-inset-right)}.dock-offset{padding-bottom:calc(var(--sm-dock-h) + env(safe-area-inset-bottom))}.dock-safe{padding-bottom:env(safe-area-inset-bottom)}.safe-x{padding-left:env(safe-area-inset-left);padding-right:env(safe-area-inset-right)}@media (min-width:768px){.dock-offset{padding-bottom:0}}.ui-scale-pill{z-index:45;right:calc(env(safe-area-inset-right) + 12px);bottom:calc(var(--sm-dock-h) + env(safe-area-inset-bottom) + 8px);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);color:#9ca3af;white-space:nowrap;background:#111827eb;border:1px solid #374151;border-radius:9999px;align-items:center;gap:8px;padding:6px 8px 6px 12px;font-size:12px;line-height:1;display:flex;position:fixed}.ui-scale-pill button{color:#93c5fd;background:#1f2937;border-radius:9999px;min-height:32px;padding:0 12px;font-size:12px;line-height:1}@media (min-width:768px){.ui-scale-pill{bottom:calc(env(safe-area-inset-bottom) + 12px)}}.toolbar-ident,.toolbar-strip{align-items:center;gap:.375rem;min-width:0;display:flex}.toolbar-strip{scrollbar-width:thin;-webkit-overflow-scrolling:touch;flex-wrap:nowrap;overflow:auto hidden}.toolbar-strip>*{flex:none}@media (min-width:768px){.toolbar-ident,.toolbar-strip{display:contents}}@media (pointer:coarse){.toolbar-btn,.toolbar-btn-sm{justify-content:center;align-items:center;min-width:2.75rem;min-height:2.75rem;padding-inline:.75rem;font-size:.8125rem;display:inline-flex}.toolbar-strip select,.toolbar-strip input{min-height:2.75rem}input:not([type=checkbox]),select,textarea{font-size:16px}input[type=checkbox]{width:1.15rem;height:1.15rem}.touch-targets button{min-width:2.75rem;min-height:2.75rem}.touch-targets select,.touch-targets input:not([type=checkbox]){min-height:2.75rem}.touch-targets textarea{min-height:3.5rem}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
|
|
2
|
-
.slc-pane{--slc-surface:#030712;--slc-node-fill:#0f172a;--slc-node-stroke:#475569;--slc-composite-fill:#0c1020;--slc-final-fill:#1e293b;--slc-accent:#3b82f6;--slc-accent-fill:#1e3a5f;--slc-accent-ink:#93c5fd;--slc-danger:#ef4444;--slc-draw-source:#f59e0b;--slc-active:#22c55e;--slc-edge:#94a3b8;--slc-chip-fill:#1e293b;--slc-chip-stroke:#334155;--slc-label:#e2e8f0;--slc-muted:#94a3b8;--slc-faint:#64748b;--slc-entry:#22c55e;--slc-exit:#f97316;--slc-composite:#6366f1;--slc-panel-bg:#111827e6;--slc-panel-border:#374151;--slc-panel-hover:#1f2937;touch-action:none;width:100%;height:100%;position:relative}.slc-surface{background:var(--slc-surface);width:100%;height:100%;display:block}.slc-node{fill:var(--slc-node-fill);stroke:var(--slc-node-stroke);stroke-width:1.5px}.slc-node--final{fill:var(--slc-final-fill)}.slc-node--composite{fill:var(--slc-composite-fill)}.slc-node--source{fill:var(--slc-accent-fill);stroke:var(--slc-draw-source)}.slc-node--error{stroke:var(--slc-danger);stroke-width:2px}.slc-node--selected{stroke:var(--slc-accent);stroke-width:2.5px}.slc-node-name{fill:var(--slc-label);pointer-events:none;-webkit-user-select:none;user-select:none;font-size:14px;font-weight:600}.slc-node-kind{fill:var(--slc-faint);pointer-events:none;-webkit-user-select:none;user-select:none;font-size:10px}.slc-node-entry{fill:var(--slc-entry);pointer-events:none;-webkit-user-select:none;user-select:none;font-size:9px}.slc-node-exit{fill:var(--slc-exit);pointer-events:none;-webkit-user-select:none;user-select:none;font-size:9px}.slc-node-warn{fill:var(--slc-danger);pointer-events:none;-webkit-user-select:none;user-select:none;font-size:12px}.slc-note-mark{fill:var(--slc-accent-ink);opacity:.75}.slc-node-warn-mark{fill:none;stroke:var(--slc-danger);stroke-width:1.4px;stroke-linejoin:round;stroke-linecap:round;pointer-events:none}.slc-node-drill{fill:var(--slc-composite);pointer-events:none;-webkit-user-select:none;user-select:none;font-size:9px}.slc-node--drawing{cursor:crosshair}.slc-node--drillable{cursor:pointer}.slc-node--draggable{cursor:grab}.slc-node--draggable:active{cursor:grabbing}.slc-node--static{cursor:default}.slc-edge{fill:none;stroke:var(--slc-edge);stroke-width:1.5px;cursor:pointer}.slc-edge:hover{stroke:var(--slc-accent-ink)}.slc-edge--selected{stroke:var(--slc-accent);stroke-width:2.5px}.slc-arrowhead{fill:var(--slc-edge)}.slc-arrowhead--selected{fill:var(--slc-accent)}.slc-chip{cursor:pointer}.slc-chip-plate{fill:var(--slc-chip-fill);stroke:var(--slc-chip-stroke);stroke-width:1px}.slc-chip-plate--selected{fill:var(--slc-accent-fill);stroke:var(--slc-accent)}.slc-chip-text{fill:var(--slc-muted);pointer-events:none;-webkit-user-select:none;user-select:none;font-size:11px;font-weight:500}.slc-chip-text--selected{fill:var(--slc-accent-ink)}.slc-chip-guard{fill:var(--slc-faint);-webkit-user-select:none;user-select:none;font-size:9px}.slc-chip-leader{stroke:var(--slc-chip-stroke);stroke-width:1px;stroke-dasharray:2 3;pointer-events:none}.slc-chip-leader--selected{stroke:var(--slc-accent)}.slc-glyph{fill:none;stroke:#7c93b0;stroke-linecap:round;stroke-linejoin:round;pointer-events:none}.slc-glyph--selected{stroke:var(--slc-accent-ink)}.slc-glyph .slc-glyph-dot{fill:#7c93b0}.slc-glyph--selected .slc-glyph-dot{fill:var(--slc-accent-ink)}.slc-draw-hint{fill:var(--slc-draw-source);pointer-events:none;-webkit-user-select:none;user-select:none;font-size:13px}.slc-empty{fill:var(--slc-faint);font-size:16px}.slc-crumbs{z-index:10;border:1px solid var(--slc-panel-border);background:var(--slc-panel-bg);border-radius:.5rem;align-items:center;gap:.25rem;padding:.375rem .75rem;font-size:.8125rem;line-height:1.25;display:flex;position:absolute;top:.5rem;left:.5rem}.slc-crumb-item{align-items:center;gap:.25rem;display:flex}.slc-crumb-sep{color:var(--slc-faint)}.slc-crumb{cursor:pointer;color:var(--slc-muted);font:inherit;background:0 0;border:0;padding:0}.slc-crumb:hover{color:var(--slc-accent-ink)}.slc-crumb--current{color:var(--slc-accent-ink);font-weight:600}.slc-hud{z-index:10;border:1px solid var(--slc-panel-border);background:var(--slc-panel-bg);color:var(--slc-muted);-webkit-user-select:none;user-select:none;border-radius:.5rem;align-items:center;gap:.125rem;padding:.25rem .375rem;font-size:.75rem;display:flex;position:absolute;bottom:.75rem;left:.75rem}.slc-hud-btn{color:inherit;font:inherit;cursor:pointer;background:0 0;border:0;border-radius:.25rem;padding:.125rem .375rem}.slc-hud-btn:hover:not(:disabled){background:var(--slc-panel-hover);color:var(--slc-label)}.slc-hud-btn:disabled{opacity:.4;cursor:default}.slc-hud-btn--scale{font-variant-numeric:tabular-nums;min-width:3.25rem}.slc-hud--coarse .slc-hud-btn{padding:.5rem .75rem}.slc-hud-sep{color:var(--slc-panel-border);padding:0 .125rem}.slc-hud-hint{color:var(--slc-faint);padding:0 .25rem;font-size:.625rem}.slc-picker{z-index:50;border:1px solid var(--slc-panel-border);background:var(--slc-panel-bg);border-radius:.5rem;min-width:160px;padding:.5rem;position:absolute;box-shadow:0 10px 25px #00000073}.slc-picker-title{color:var(--slc-muted);padding:0 .25rem .25rem;font-size:.75rem}.slc-picker-sub{color:var(--slc-faint);padding:0 .25rem .25rem;font-size:.75rem}.slc-picker-empty{color:var(--slc-faint);padding:.5rem .25rem;font-size:.75rem}.slc-picker-item{text-align:left;width:100%;color:var(--slc-label);font:inherit;cursor:pointer;background:0 0;border:0;border-radius:.25rem;padding:.25rem .5rem;font-size:.875rem;display:block}.slc-picker-item:hover{background:var(--slc-panel-hover)}.slc-picker-cancel{text-align:left;border:0;border-top:1px solid var(--slc-panel-border);width:100%;color:var(--slc-faint);font:inherit;cursor:pointer;background:0 0;margin-top:.25rem;padding:.25rem .5rem;font-size:.75rem;display:block}.slc-picker-cancel:hover{color:var(--slc-label)}@keyframes slc-node-in{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}@keyframes slc-node-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.9)}}@keyframes slc-edge-draw{0%{stroke-dashoffset:1px;opacity:0}to{stroke-dashoffset:0;opacity:1}}@keyframes slc-pulse{0%,to{filter:drop-shadow(0 0 1px #22c55e8c)}50%{filter:drop-shadow(0 0 6px #22c55ef2)}}.slc-node-enter{transform-box:fill-box;transform-origin:50%;animation:.22s ease-out both slc-node-in}.slc-node-exit-group{transform-box:fill-box;transform-origin:50%;pointer-events:none;animation:.2s ease-in forwards slc-node-out}.slc-edge-enter{stroke-dasharray:1;animation:.26s ease-out both slc-edge-draw}.slc-node--active{stroke:var(--slc-active);animation:1.2s ease-in-out infinite slc-pulse}@media (prefers-reduced-motion:reduce){.slc-node-enter,.slc-edge-enter{animation:none}.slc-node-exit-group{animation-duration:1ms}.slc-node--active{filter:drop-shadow(0 0 4px #22c55ed9);animation:none}}.slc-erd-line{fill:none;stroke:var(--slc-edge);stroke-width:1.5px}.slc-erd-mark{fill:none;stroke:var(--slc-edge);stroke-width:1.5px;stroke-linecap:round}.slc-erd-rel-label{fill:var(--slc-muted);pointer-events:none;-webkit-user-select:none;user-select:none;font-size:11px;font-weight:500}.slc-erd-rule{stroke:var(--slc-node-stroke);stroke-width:1px;pointer-events:none}.slc-erd-row{fill:var(--slc-muted);-webkit-user-select:none;user-select:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}.slc-erd-row--pk{fill:var(--slc-label);font-weight:600}.slc-erd-row--state{fill:var(--slc-accent-ink)}.slc-erd-row--empty{fill:var(--slc-faint);font-style:italic}.slc-erd-state{cursor:pointer}.slc-erd-state:hover .slc-erd-row--state{text-decoration:underline}.slc-erd-badge{fill:var(--slc-faint);letter-spacing:.04em;pointer-events:none;-webkit-user-select:none;user-select:none;font-size:9px;font-weight:600}.slc-erd-oval{fill:var(--slc-node-fill);stroke:var(--slc-node-stroke);stroke-width:1.2px}.slc-erd-oval-text{fill:var(--slc-muted);pointer-events:none;-webkit-user-select:none;user-select:none;font-size:11px}.slc-erd-oval-text--pk{fill:var(--slc-label);font-weight:600;text-decoration:underline}.slc-erd-attr-line{fill:none;stroke:var(--slc-chip-stroke);stroke-width:1px}.slc-erd-diamond{fill:var(--slc-chip-fill);stroke:var(--slc-edge);stroke-width:1.5px}.slc-erd-weak{fill:none;stroke:var(--slc-node-stroke);stroke-width:1.2px;pointer-events:none}.slc-erd-card{fill:var(--slc-label);pointer-events:none;-webkit-user-select:none;user-select:none;font-size:12px;font-weight:700}.slc-erd-state:hover .slc-erd-oval{stroke:var(--slc-accent)}.slc-erd-empty{color:var(--slc-faint);pointer-events:none;justify-content:center;align-items:center;font-size:16px;display:flex;position:absolute;inset:0}
|