plotlink-ows 0.1.18 → 1.0.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/README.md +167 -67
- package/app/lib/publish.ts +134 -32
- package/app/routes/dashboard.ts +64 -13
- package/app/routes/publish.ts +52 -1
- package/app/routes/settings.ts +194 -0
- package/app/routes/stories.ts +75 -8
- package/app/routes/terminal.ts +167 -63
- package/app/server.ts +7 -1
- package/app/web/components/Dashboard.tsx +83 -32
- package/app/web/components/PreviewPanel.tsx +280 -41
- package/app/web/components/Settings.tsx +227 -3
- package/app/web/components/StoriesPage.tsx +121 -8
- package/app/web/components/StoryBrowser.tsx +32 -8
- package/app/web/components/TerminalPanel.tsx +384 -78
- package/app/web/dist/assets/index-BuOxhUWG.css +32 -0
- package/app/web/dist/assets/index-De8CpT47.js +129 -0
- package/app/web/dist/index.html +2 -2
- package/app/web/styles.css +18 -0
- package/bin/plotlink-ows.js +16 -61
- package/package.json +7 -2
- package/scripts/fix-index-status.ts +93 -0
- package/app/web/dist/assets/index-D5gfwaEX.css +0 -32
- package/app/web/dist/assets/index-pBt5Q_bN.js +0 -117
|
@@ -1,122 +1,428 @@
|
|
|
1
|
-
import { useRef, useEffect, useCallback } from "react";
|
|
1
|
+
import { useRef, useEffect, useCallback, useState } from "react";
|
|
2
2
|
import { Terminal } from "@xterm/xterm";
|
|
3
3
|
import { FitAddon } from "@xterm/addon-fit";
|
|
4
|
+
import { SerializeAddon } from "@xterm/addon-serialize";
|
|
4
5
|
import "@xterm/xterm/css/xterm.css";
|
|
5
6
|
|
|
6
7
|
interface TerminalPanelProps {
|
|
7
8
|
token: string;
|
|
9
|
+
storyName: string | null;
|
|
10
|
+
authFetch: (url: string, opts?: RequestInit) => Promise<Response>;
|
|
11
|
+
onSelectStory?: (storyName: string) => void;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
interface TerminalSession {
|
|
15
|
+
term: Terminal;
|
|
16
|
+
fit: FitAddon;
|
|
17
|
+
serialize: SerializeAddon;
|
|
18
|
+
ws: WebSocket | null;
|
|
19
|
+
container: HTMLDivElement;
|
|
20
|
+
observer: ResizeObserver;
|
|
21
|
+
connected: boolean;
|
|
22
|
+
_retried?: boolean;
|
|
23
|
+
}
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
const THEME = {
|
|
26
|
+
background: "#F0EBE1",
|
|
27
|
+
foreground: "#2C1810",
|
|
28
|
+
cursor: "#8B4513",
|
|
29
|
+
cursorAccent: "#F0EBE1",
|
|
30
|
+
selectionBackground: "#D4C5B0",
|
|
31
|
+
selectionForeground: "#2C1810",
|
|
32
|
+
black: "#2C1810",
|
|
33
|
+
red: "#A63D40",
|
|
34
|
+
green: "#4A7A4A",
|
|
35
|
+
yellow: "#8B6914",
|
|
36
|
+
blue: "#4A6FA5",
|
|
37
|
+
magenta: "#7B4B8A",
|
|
38
|
+
cyan: "#3D7A7A",
|
|
39
|
+
white: "#3A2A1E",
|
|
40
|
+
brightBlack: "#8B7355",
|
|
41
|
+
brightRed: "#B85C5C", // muted red — readable as text, soft as diff bg
|
|
42
|
+
brightGreen: "#5A8A5A", // muted green — readable as text, soft as diff bg
|
|
43
|
+
brightYellow: "#A07D1C",
|
|
44
|
+
brightBlue: "#5A82BA",
|
|
45
|
+
brightMagenta: "#8E5D9F",
|
|
46
|
+
brightCyan: "#5A8F8F",
|
|
47
|
+
brightWhite: "#4A3728",
|
|
48
|
+
};
|
|
18
49
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
fontFamily: '"Geist Mono", ui-monospace, monospace',
|
|
24
|
-
lineHeight: 1.4,
|
|
25
|
-
letterSpacing: 0.5,
|
|
26
|
-
cursorBlink: true,
|
|
27
|
-
cursorStyle: "block",
|
|
28
|
-
theme: {
|
|
29
|
-
background: "#F0EBE1",
|
|
30
|
-
foreground: "#2C1810",
|
|
31
|
-
cursor: "#8B4513",
|
|
32
|
-
cursorAccent: "#F0EBE1",
|
|
33
|
-
selectionBackground: "#D4C5B0",
|
|
34
|
-
selectionForeground: "#2C1810",
|
|
35
|
-
black: "#2C1810",
|
|
36
|
-
red: "#CC3333",
|
|
37
|
-
green: "#5B7A2E",
|
|
38
|
-
yellow: "#8B6914",
|
|
39
|
-
blue: "#4A6FA5",
|
|
40
|
-
magenta: "#7B4B8A",
|
|
41
|
-
cyan: "#4A8B8B",
|
|
42
|
-
white: "#2C1810",
|
|
43
|
-
brightBlack: "#8B7355",
|
|
44
|
-
brightRed: "#E04040",
|
|
45
|
-
brightGreen: "#6B8F38",
|
|
46
|
-
brightYellow: "#A07D1C",
|
|
47
|
-
brightBlue: "#5A82BA",
|
|
48
|
-
brightMagenta: "#8E5D9F",
|
|
49
|
-
brightCyan: "#5A9F9F",
|
|
50
|
-
brightWhite: "#4A3728",
|
|
51
|
-
},
|
|
52
|
-
allowTransparency: false,
|
|
53
|
-
drawBoldTextInBrightColors: true,
|
|
54
|
-
});
|
|
50
|
+
const DB_NAME = "plotlink-terminal";
|
|
51
|
+
const DB_VERSION = 1;
|
|
52
|
+
const STORE_NAME = "scrollback";
|
|
53
|
+
const MAX_SCROLLBACK_BYTES = 10 * 1024 * 1024; // 10MB per story
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
// ---- IndexedDB helpers ----
|
|
56
|
+
function openDb(): Promise<IDBDatabase> {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
59
|
+
req.onupgradeneeded = () => {
|
|
60
|
+
const db = req.result;
|
|
61
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
62
|
+
db.createObjectStore(STORE_NAME);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
req.onsuccess = () => resolve(req.result);
|
|
66
|
+
req.onerror = () => reject(req.error);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
async function saveScrollback(storyName: string, data: string): Promise<void> {
|
|
71
|
+
// Enforce size limit
|
|
72
|
+
const trimmed = data.length > MAX_SCROLLBACK_BYTES ? data.slice(-MAX_SCROLLBACK_BYTES) : data;
|
|
73
|
+
const db = await openDb();
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
76
|
+
tx.objectStore(STORE_NAME).put(trimmed, storyName);
|
|
77
|
+
tx.oncomplete = () => { db.close(); resolve(); };
|
|
78
|
+
tx.onerror = () => { db.close(); reject(tx.error); };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function loadScrollback(storyName: string): Promise<string | null> {
|
|
83
|
+
const db = await openDb();
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
86
|
+
const req = tx.objectStore(STORE_NAME).get(storyName);
|
|
87
|
+
req.onsuccess = () => { db.close(); resolve(req.result ?? null); };
|
|
88
|
+
req.onerror = () => { db.close(); reject(req.error); };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Sessions live outside React state to avoid ref-in-effect lint issues
|
|
93
|
+
const sessions = new Map<string, TerminalSession>();
|
|
94
|
+
|
|
95
|
+
export function TerminalPanel({ token, storyName, authFetch, onSelectStory }: TerminalPanelProps) {
|
|
96
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
97
|
+
const authFetchRef = useRef(authFetch);
|
|
98
|
+
const [sessionList, setSessionList] = useState<string[]>([]);
|
|
99
|
+
const [disconnected, setDisconnected] = useState<Set<string>>(new Set());
|
|
66
100
|
|
|
67
|
-
|
|
101
|
+
const connectWsRef = useRef<(name: string, session: TerminalSession, resume: boolean) => void>(() => {});
|
|
102
|
+
|
|
103
|
+
useEffect(() => { authFetchRef.current = authFetch; }, [authFetch]);
|
|
104
|
+
|
|
105
|
+
const safeFit = useCallback((session: TerminalSession) => {
|
|
106
|
+
const { width } = session.container.getBoundingClientRect();
|
|
107
|
+
if (width < 50) return; // Skip fit if container has no real dimensions
|
|
108
|
+
try {
|
|
109
|
+
session.fit.fit();
|
|
110
|
+
if (session.ws?.readyState === WebSocket.OPEN) {
|
|
111
|
+
session.ws.send(JSON.stringify({ type: "resize", cols: session.term.cols, rows: session.term.rows }));
|
|
112
|
+
}
|
|
113
|
+
} catch { /* ignore */ }
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
const showSession = useCallback((name: string | null) => {
|
|
117
|
+
for (const [key, session] of sessions) {
|
|
118
|
+
session.container.style.display = key === name ? "block" : "none";
|
|
119
|
+
}
|
|
120
|
+
if (name) {
|
|
121
|
+
const active = sessions.get(name);
|
|
122
|
+
if (active) {
|
|
123
|
+
// setTimeout gives browser time to compute layout after display change
|
|
124
|
+
setTimeout(() => safeFit(active), 50);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}, [safeFit]);
|
|
128
|
+
|
|
129
|
+
const connectWs = useCallback((name: string, session: TerminalSession, resume: boolean) => {
|
|
68
130
|
const wsProto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
69
|
-
const ws = new WebSocket(
|
|
70
|
-
|
|
131
|
+
const ws = new WebSocket(
|
|
132
|
+
`${wsProto}//${window.location.host}/ws/terminal?story=${encodeURIComponent(name)}&token=${token}&resume=${resume}`
|
|
133
|
+
);
|
|
71
134
|
|
|
72
135
|
ws.onopen = () => {
|
|
73
|
-
|
|
136
|
+
session.connected = true;
|
|
137
|
+
session._retried = false;
|
|
138
|
+
setDisconnected((prev) => { const next = new Set(prev); next.delete(name); return next; });
|
|
139
|
+
ws.send(JSON.stringify({ type: "resize", cols: session.term.cols, rows: session.term.rows }));
|
|
74
140
|
};
|
|
75
141
|
|
|
76
142
|
ws.onmessage = (e) => {
|
|
77
|
-
term.write(e.data);
|
|
143
|
+
session.term.write(e.data);
|
|
78
144
|
};
|
|
79
145
|
|
|
80
|
-
ws.onclose = () => {
|
|
81
|
-
|
|
146
|
+
ws.onclose = (event) => {
|
|
147
|
+
session.connected = false;
|
|
148
|
+
if (session.ws === ws) {
|
|
149
|
+
session.ws = null;
|
|
150
|
+
// Save scrollback before marking disconnected
|
|
151
|
+
try {
|
|
152
|
+
const data = session.serialize.serialize();
|
|
153
|
+
saveScrollback(name, data).catch(() => {});
|
|
154
|
+
} catch { /* ignore */ }
|
|
155
|
+
|
|
156
|
+
// Code 4000 = resume failed, auto-reconnect fresh (once only)
|
|
157
|
+
if (event.code === 4000 && !session._retried) {
|
|
158
|
+
session._retried = true;
|
|
159
|
+
session.term.write("\r\n\x1b[33m[Resume failed — starting fresh session...]\x1b[0m\r\n");
|
|
160
|
+
connectWsRef.current(name, session, false);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setDisconnected((prev) => new Set(prev).add(name));
|
|
165
|
+
}
|
|
82
166
|
};
|
|
83
167
|
|
|
84
|
-
term.onData((data) => {
|
|
168
|
+
session.term.onData((data) => {
|
|
85
169
|
if (ws.readyState === WebSocket.OPEN) {
|
|
86
170
|
ws.send(data);
|
|
87
171
|
}
|
|
88
172
|
});
|
|
89
173
|
|
|
90
|
-
|
|
174
|
+
session.ws = ws;
|
|
175
|
+
}, [token]);
|
|
176
|
+
|
|
177
|
+
useEffect(() => { connectWsRef.current = connectWs; }, [connectWs]);
|
|
178
|
+
|
|
179
|
+
const createSession = useCallback(async (name: string, opts?: { resume?: boolean; autoConnect?: boolean }) => {
|
|
180
|
+
if (!wrapperRef.current || sessions.has(name)) return;
|
|
181
|
+
const { resume = false, autoConnect = true } = opts ?? {};
|
|
182
|
+
|
|
183
|
+
const container = document.createElement("div");
|
|
184
|
+
container.style.width = "100%";
|
|
185
|
+
container.style.height = "100%";
|
|
186
|
+
container.style.display = "none";
|
|
187
|
+
wrapperRef.current.appendChild(container);
|
|
188
|
+
|
|
189
|
+
const term = new Terminal({
|
|
190
|
+
cols: 80, // Fallback minimum until FitAddon computes actual size
|
|
191
|
+
scrollback: 5000,
|
|
192
|
+
fontSize: 13,
|
|
193
|
+
fontFamily: '"Geist Mono", ui-monospace, monospace',
|
|
194
|
+
lineHeight: 1.05,
|
|
195
|
+
letterSpacing: 0,
|
|
196
|
+
cursorBlink: true,
|
|
197
|
+
cursorStyle: "block",
|
|
198
|
+
theme: THEME,
|
|
199
|
+
allowTransparency: false,
|
|
200
|
+
drawBoldTextInBrightColors: false,
|
|
201
|
+
minimumContrastRatio: 7, // High contrast — compensates for dim text halving
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const fit = new FitAddon();
|
|
205
|
+
const serialize = new SerializeAddon();
|
|
206
|
+
term.loadAddon(fit);
|
|
207
|
+
term.loadAddon(serialize);
|
|
208
|
+
term.open(container);
|
|
209
|
+
|
|
210
|
+
// Apply padding to term.element so FitAddon measures correctly
|
|
211
|
+
if (term.element) {
|
|
212
|
+
term.element.style.paddingLeft = "10px";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const session: TerminalSession = { term, fit, serialize, ws: null, container, observer: null as unknown as ResizeObserver, connected: false };
|
|
216
|
+
|
|
91
217
|
const observer = new ResizeObserver(() => {
|
|
218
|
+
const { width } = container.getBoundingClientRect();
|
|
219
|
+
if (width < 50) return; // Skip if container not yet laid out
|
|
92
220
|
try {
|
|
93
|
-
|
|
94
|
-
if (ws
|
|
95
|
-
ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
|
|
221
|
+
fit.fit();
|
|
222
|
+
if (session.ws?.readyState === WebSocket.OPEN) {
|
|
223
|
+
session.ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
|
|
96
224
|
}
|
|
97
225
|
} catch { /* ignore */ }
|
|
98
226
|
});
|
|
99
|
-
observer.observe(
|
|
227
|
+
observer.observe(container);
|
|
228
|
+
session.observer = observer;
|
|
229
|
+
sessions.set(name, session);
|
|
230
|
+
setSessionList((prev) => [...prev, name]);
|
|
231
|
+
|
|
232
|
+
// Restore scrollback from IndexedDB
|
|
233
|
+
try {
|
|
234
|
+
const saved = await loadScrollback(name);
|
|
235
|
+
if (saved) {
|
|
236
|
+
term.write(saved);
|
|
237
|
+
}
|
|
238
|
+
} catch { /* ignore */ }
|
|
239
|
+
|
|
240
|
+
if (autoConnect) {
|
|
241
|
+
connectWs(name, session, resume);
|
|
242
|
+
} else {
|
|
243
|
+
// Show as disconnected so overlay appears
|
|
244
|
+
setDisconnected((prev) => new Set(prev).add(name));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Defer initial fit — container may still be display:none
|
|
248
|
+
setTimeout(() => safeFit(session), 50);
|
|
249
|
+
}, [connectWs, safeFit]);
|
|
250
|
+
|
|
251
|
+
const reconnectSession = useCallback(async (name: string, resume: boolean) => {
|
|
252
|
+
const session = sessions.get(name);
|
|
253
|
+
if (!session) return;
|
|
254
|
+
|
|
255
|
+
// Close existing WS if any
|
|
256
|
+
if (session.ws) {
|
|
257
|
+
session.ws.close();
|
|
258
|
+
session.ws = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!resume) {
|
|
262
|
+
// Kill old server PTY so a fresh one spawns on reconnect
|
|
263
|
+
await authFetchRef.current(`/api/terminal/${encodeURIComponent(name)}`, { method: "DELETE" }).catch(() => {});
|
|
264
|
+
session.term.clear();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
connectWs(name, session, resume);
|
|
268
|
+
}, [connectWs]);
|
|
269
|
+
|
|
270
|
+
const destroySession = useCallback((name: string) => {
|
|
271
|
+
const session = sessions.get(name);
|
|
272
|
+
if (!session) return;
|
|
100
273
|
|
|
274
|
+
// Save scrollback before destroying
|
|
275
|
+
try {
|
|
276
|
+
const data = session.serialize.serialize();
|
|
277
|
+
saveScrollback(name, data).catch(() => {});
|
|
278
|
+
} catch { /* ignore */ }
|
|
279
|
+
|
|
280
|
+
session.observer.disconnect();
|
|
281
|
+
if (session.ws) session.ws.close();
|
|
282
|
+
session.term.dispose();
|
|
283
|
+
session.container.remove();
|
|
284
|
+
sessions.delete(name);
|
|
285
|
+
setSessionList((prev) => prev.filter((s) => s !== name));
|
|
286
|
+
setDisconnected((prev) => { const next = new Set(prev); next.delete(name); return next; });
|
|
287
|
+
|
|
288
|
+
authFetch(`/api/terminal/${encodeURIComponent(name)}`, { method: "DELETE" }).catch(() => {});
|
|
289
|
+
}, [authFetch]);
|
|
290
|
+
|
|
291
|
+
// Auto-spawn + show/hide when story changes
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (!storyName) return;
|
|
294
|
+
if (!sessions.has(storyName)) {
|
|
295
|
+
// Check if a previous session exists — if so, show overlay instead of auto-connecting
|
|
296
|
+
authFetchRef.current(`/api/terminal/session/${encodeURIComponent(storyName)}`)
|
|
297
|
+
.then((res) => res.ok ? res.json() : null)
|
|
298
|
+
.then((data) => {
|
|
299
|
+
if (!sessions.has(storyName)) { // guard against race
|
|
300
|
+
const hasStoredSession = data?.sessionId && !data?.running;
|
|
301
|
+
createSession(storyName, { autoConnect: !hasStoredSession });
|
|
302
|
+
showSession(storyName);
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
.catch(() => {
|
|
306
|
+
if (!sessions.has(storyName)) {
|
|
307
|
+
createSession(storyName);
|
|
308
|
+
showSession(storyName);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
showSession(storyName);
|
|
313
|
+
}
|
|
314
|
+
}, [storyName, createSession, showSession]);
|
|
315
|
+
|
|
316
|
+
// Periodic scrollback save (every 30s for active session)
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
const interval = setInterval(() => {
|
|
319
|
+
for (const [name, session] of sessions) {
|
|
320
|
+
if (session.connected) {
|
|
321
|
+
try {
|
|
322
|
+
const data = session.serialize.serialize();
|
|
323
|
+
saveScrollback(name, data).catch(() => {});
|
|
324
|
+
} catch { /* ignore */ }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}, 30000);
|
|
328
|
+
return () => clearInterval(interval);
|
|
329
|
+
}, []);
|
|
330
|
+
|
|
331
|
+
// Cleanup all sessions on unmount
|
|
332
|
+
useEffect(() => {
|
|
101
333
|
return () => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
334
|
+
for (const [name, session] of sessions) {
|
|
335
|
+
// Save scrollback before cleanup
|
|
336
|
+
try {
|
|
337
|
+
const data = session.serialize.serialize();
|
|
338
|
+
saveScrollback(name, data).catch(() => {});
|
|
339
|
+
} catch { /* ignore */ }
|
|
340
|
+
session.observer.disconnect();
|
|
341
|
+
if (session.ws) session.ws.close();
|
|
342
|
+
session.term.dispose();
|
|
343
|
+
session.container.remove();
|
|
344
|
+
authFetchRef.current(`/api/terminal/${encodeURIComponent(name)}`, { method: "DELETE" }).catch(() => {});
|
|
345
|
+
}
|
|
346
|
+
sessions.clear();
|
|
105
347
|
};
|
|
106
|
-
}, [
|
|
348
|
+
}, []);
|
|
107
349
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return cleanup;
|
|
111
|
-
}, [connect]);
|
|
350
|
+
const isDisconnected = storyName ? disconnected.has(storyName) : false;
|
|
351
|
+
const isEmpty = sessionList.length === 0;
|
|
112
352
|
|
|
113
353
|
return (
|
|
114
354
|
<div className="h-full flex flex-col">
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
355
|
+
{/* Session tabs — hidden when no sessions */}
|
|
356
|
+
{!isEmpty && (
|
|
357
|
+
<div className="px-2 py-1 border-b border-border flex items-center gap-1 overflow-x-auto">
|
|
358
|
+
{sessionList.map((name) => (
|
|
359
|
+
<div
|
|
360
|
+
key={name}
|
|
361
|
+
onClick={() => onSelectStory?.(name)}
|
|
362
|
+
className={`flex items-center gap-1 px-2 py-0.5 rounded text-xs font-mono cursor-pointer ${
|
|
363
|
+
name === storyName
|
|
364
|
+
? "bg-accent/10 text-accent"
|
|
365
|
+
: "text-muted hover:text-foreground"
|
|
366
|
+
}`}
|
|
367
|
+
>
|
|
368
|
+
<span className={`w-1.5 h-1.5 rounded-full ${
|
|
369
|
+
disconnected.has(name) ? "bg-amber-500" : name === storyName ? "bg-green-600" : "bg-muted/50"
|
|
370
|
+
}`} />
|
|
371
|
+
<span className="truncate max-w-[120px]">{name}</span>
|
|
372
|
+
<button
|
|
373
|
+
onClick={(e) => {
|
|
374
|
+
e.stopPropagation();
|
|
375
|
+
destroySession(name);
|
|
376
|
+
}}
|
|
377
|
+
className="ml-0.5 text-muted hover:text-error text-[10px] leading-none"
|
|
378
|
+
title="Close terminal"
|
|
379
|
+
>
|
|
380
|
+
×
|
|
381
|
+
</button>
|
|
382
|
+
</div>
|
|
383
|
+
))
|
|
384
|
+
}
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
387
|
+
|
|
388
|
+
{/* Terminal containers — always rendered so wrapperRef is available */}
|
|
389
|
+
<div className="relative flex-1 min-h-0">
|
|
390
|
+
<div ref={wrapperRef} className="h-full" />
|
|
391
|
+
|
|
392
|
+
{/* Empty state overlay */}
|
|
393
|
+
{isEmpty && (
|
|
394
|
+
<div className="absolute inset-0 flex items-center justify-center text-muted">
|
|
395
|
+
<div className="text-center">
|
|
396
|
+
<p className="text-lg font-serif">Select a story on the left menu</p>
|
|
397
|
+
<p className="text-sm mt-1">to start an AI Writer session</p>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
|
|
402
|
+
{/* Reconnect overlay */}
|
|
403
|
+
{isDisconnected && storyName && (
|
|
404
|
+
<div className="absolute inset-0 flex items-center justify-center" style={{ background: "rgba(240, 235, 225, 0.9)" }}>
|
|
405
|
+
<div className="text-center space-y-3">
|
|
406
|
+
<p className="text-sm font-serif text-foreground">Terminal disconnected</p>
|
|
407
|
+
<div className="flex items-center gap-2">
|
|
408
|
+
<button
|
|
409
|
+
onClick={() => reconnectSession(storyName, true)}
|
|
410
|
+
className="px-4 py-1.5 bg-accent text-white text-sm rounded hover:bg-accent-dim"
|
|
411
|
+
>
|
|
412
|
+
Resume Session
|
|
413
|
+
</button>
|
|
414
|
+
<button
|
|
415
|
+
onClick={() => reconnectSession(storyName, false)}
|
|
416
|
+
className="px-4 py-1.5 border border-border text-sm rounded hover:bg-surface"
|
|
417
|
+
>
|
|
418
|
+
Start Fresh
|
|
419
|
+
</button>
|
|
420
|
+
</div>
|
|
421
|
+
<p className="text-xs text-muted">Resume continues your previous Claude conversation</p>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
118
425
|
</div>
|
|
119
|
-
<div ref={containerRef} className="flex-1 min-h-0" />
|
|
120
426
|
</div>
|
|
121
427
|
);
|
|
122
428
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
3
|
+
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
4
|
+
* https://github.com/chjj/term.js
|
|
5
|
+
* @license MIT
|
|
6
|
+
*
|
|
7
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
* in the Software without restriction, including without limitation the rights
|
|
10
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
* furnished to do so, subject to the following conditions:
|
|
13
|
+
*
|
|
14
|
+
* The above copyright notice and this permission notice shall be included in
|
|
15
|
+
* all copies or substantial portions of the Software.
|
|
16
|
+
*
|
|
17
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
* THE SOFTWARE.
|
|
24
|
+
*
|
|
25
|
+
* Originally forked from (with the author's permission):
|
|
26
|
+
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
27
|
+
* http://bellard.org/jslinux/
|
|
28
|
+
* Copyright (c) 2011 Fabrice Bellard
|
|
29
|
+
* The original design remains. The terminal itself
|
|
30
|
+
* has been extended to include xterm CSI codes, among
|
|
31
|
+
* other features.
|
|
32
|
+
*/.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{font-family:monospace;-webkit-user-select:text;user-select:text;white-space:pre}.xterm .xterm-accessibility-tree>div{transform-origin:left;width:fit-content}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;background:#0000;transition:opacity .1s linear;z-index:11}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{position:absolute;display:none}.xterm .xterm-scrollable-element>.shadow.top{display:block;top:0;left:3px;height:3px;width:100%;box-shadow:var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.left{display:block;top:3px;left:0;height:100%;width:3px;box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.top-left-corner{display:block;top:0;left:0;height:3px;width:3px}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */@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-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-outline-style:solid}}}@layer theme{:root,:host{--font-serif:ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-700:oklch(50.5% .213 27.518);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-lg:32rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--leading-relaxed:1.625;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-mono-font-family:var(--font-mono)}}@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%;-moz-tab-size:4;tab-size:4;font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent;font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.5}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;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]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance: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{.absolute{position:absolute}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.top-1{top:calc(var(--spacing) * 1)}.right-1{right:calc(var(--spacing) * 1)}.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-auto{margin-inline:auto}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows),0 3px 0 var(--tw-prose-kbd-shadows);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:oklab(21% -.00316127 -.0338527/.1);--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.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)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-0\.5{height:calc(var(--spacing) * .5)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-14{height:calc(var(--spacing) * 14)}.h-\[calc\(100vh-3\.5rem\)\]{height:calc(100vh - 3.5rem)}.h-full{height:100%}.h-screen{height:100vh}.min-h-0{min-height:calc(var(--spacing) * 0)}.w-0\.5{width:calc(var(--spacing) * .5)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-56{width:calc(var(--spacing) * 56)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-\[120px\]{max-width:120px}.max-w-lg{max-width:var(--container-lg)}.max-w-none{max-width:none}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.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-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-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-accent{border-color:var(--accent)}.border-accent-dim\/30{border-color:var(--accent-dim)}@supports (color:color-mix(in lab,red,red)){.border-accent-dim\/30{border-color:color-mix(in oklab,var(--accent-dim) 30%,transparent)}}.border-accent\/30{border-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.border-accent\/30{border-color:color-mix(in oklab,var(--accent) 30%,transparent)}}.border-amber-600\/30{border-color:#dd74004d}@supports (color:color-mix(in lab,red,red)){.border-amber-600\/30{border-color:color-mix(in oklab,var(--color-amber-600) 30%,transparent)}}.border-border{border-color:var(--border)}.border-green-700\/30{border-color:#0081384d}@supports (color:color-mix(in lab,red,red)){.border-green-700\/30{border-color:color-mix(in oklab,var(--color-green-700) 30%,transparent)}}.border-red-700\/30{border-color:#bf000f4d}@supports (color:color-mix(in lab,red,red)){.border-red-700\/30{border-color:color-mix(in oklab,var(--color-red-700) 30%,transparent)}}.border-transparent{border-color:#0000}.bg-accent,.bg-accent\/10{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.bg-accent\/10{background-color:color-mix(in oklab,var(--accent) 10%,transparent)}}.bg-amber-500{background-color:var(--color-amber-500)}.bg-background{background-color:var(--bg)}.bg-green-600{background-color:var(--color-green-600)}.bg-muted\/50{background-color:var(--text-muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/50{background-color:color-mix(in oklab,var(--text-muted) 50%,transparent)}}.bg-surface{background-color:var(--bg-surface)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.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)}.py-4{padding-block:calc(var(--spacing) * 4)}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pr-16{padding-right:calc(var(--spacing) * 16)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.font-serif{font-family:var(--font-serif)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--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}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-all{word-break:break-all}.text-accent{color:var(--accent)}.text-accent-dim{color:var(--accent-dim)}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-error{color:var(--error)}.text-foreground{color:var(--text)}.text-green-700{color:var(--color-green-700)}.text-muted{color:var(--text-muted)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.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))}.outline-none{--tw-outline-style:none;outline-style:none}.placeholder\:text-muted\/50::placeholder{color:var(--text-muted)}@supports (color:color-mix(in lab,red,red)){.placeholder\:text-muted\/50::placeholder{color:color-mix(in oklab,var(--text-muted) 50%,transparent)}}@media(hover:hover){.hover\:border-accent:hover{border-color:var(--accent)}.hover\:border-error:hover{border-color:var(--error)}.hover\:bg-accent-dim:hover{background-color:var(--accent-dim)}.hover\:bg-accent\/10:hover{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-accent\/10:hover{background-color:color-mix(in oklab,var(--accent) 10%,transparent)}}.hover\:bg-border\/50:hover{background-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-border\/50:hover{background-color:color-mix(in oklab,var(--border) 50%,transparent)}}.hover\:bg-surface:hover{background-color:var(--bg-surface)}.hover\:text-accent:hover{color:var(--accent)}.hover\:text-error:hover{color:var(--error)}.hover\:text-foreground:hover{color:var(--text)}.hover\:opacity-80:hover{opacity:.8}}.focus\:border-accent:focus{border-color:var(--accent)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}}:root{--bg:#e8dfd0;--bg-surface:#f0ebe1;--bg-shelf:#ddd3c2;--text:#2c1810;--text-muted:#8b7355;--accent:#8b4513;--accent-dim:#6b3410;--border:#d4c5b0;--error:#c33;--paper-bg:#f5f0e8}body{background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:Inter,system-ui,-apple-system,sans-serif}::selection{background:var(--accent);color:#fff}h1,h2,h3,h4{font-family:Lora,Georgia,Times New Roman,serif}.prose{--tw-prose-body:var(--text);--tw-prose-headings:var(--text);--tw-prose-links:var(--accent);--tw-prose-bold:var(--text);--tw-prose-quotes:var(--text-muted);--tw-prose-quote-borders:var(--border);--tw-prose-code:var(--text);--tw-prose-hr:var(--border)}.prose,.prose p,.prose li,.prose blockquote{font-family:Lora,Georgia,Times New Roman,serif}code,pre{font-family:Geist Mono,ui-monospace,monospace}.xterm .xterm-dim{opacity:1!important;color:#8b7355!important}@property --tw-space-y-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-outline-style{syntax:"*";inherits:false;initial-value:solid}
|