@bakapiano/ccsm 0.22.5 → 0.22.7
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/CLAUDE.md +538 -538
- package/README.md +189 -189
- package/bin/ccsm.js +235 -235
- package/lib/cliActivity.js +139 -139
- package/lib/codexSeed.js +183 -183
- package/lib/config.js +279 -274
- package/lib/devices.js +229 -229
- package/lib/folders.js +124 -124
- package/lib/localCliSessions.js +519 -519
- package/lib/persistedSessions.js +129 -129
- package/lib/tunnel.js +621 -621
- package/lib/webTerminal.js +225 -225
- package/lib/workspace.js +233 -233
- package/package.json +57 -57
- package/public/css/base.css +99 -99
- package/public/css/cards.css +183 -183
- package/public/css/feedback.css +504 -504
- package/public/css/forms.css +453 -453
- package/public/css/layout.css +177 -176
- package/public/css/modal.css +190 -190
- package/public/css/responsive.css +176 -176
- package/public/css/sidebar.css +707 -707
- package/public/css/terminals.css +547 -553
- package/public/css/tokens.css +81 -81
- package/public/css/wco.css +196 -196
- package/public/css/widgets.css +2725 -2725
- package/public/index.html +152 -152
- package/public/js/api.js +371 -371
- package/public/js/backend.js +149 -149
- package/public/js/components/App.js +73 -73
- package/public/js/components/DirectoryPicker.js +203 -203
- package/public/js/components/EntityFormModal.js +153 -153
- package/public/js/components/Modal.js +57 -57
- package/public/js/components/OfflineBanner.js +67 -67
- package/public/js/components/PageTitleBar.js +13 -13
- package/public/js/components/PendingApprovalOverlay.js +128 -128
- package/public/js/components/Picker.js +179 -179
- package/public/js/components/Popover.js +55 -55
- package/public/js/components/RestartOverlay.js +36 -36
- package/public/js/components/Sidebar.js +380 -380
- package/public/js/components/TerminalInstance.js +28 -9
- package/public/js/components/XtermTerminal.js +62 -2
- package/public/js/components/useDragSort.js +67 -67
- package/public/js/dialog.js +67 -67
- package/public/js/icons.js +212 -212
- package/public/js/main.js +296 -296
- package/public/js/pages/AboutPage.js +90 -90
- package/public/js/pages/ConfigurePage.js +728 -713
- package/public/js/pages/LaunchPage.js +421 -421
- package/public/js/pages/RemotePage.js +743 -743
- package/public/js/pages/SessionsPage.js +73 -80
- package/public/js/state.js +335 -335
- package/scripts/dev.js +149 -149
- package/scripts/install.js +153 -153
- package/scripts/restart-helper.js +96 -96
- package/scripts/upgrade-helper.js +687 -687
- package/server.js +1820 -1807
- package/public/manifest.webmanifest +0 -25
- package/public/setup/index.html +0 -567
|
@@ -24,6 +24,8 @@ export class TerminalInstance {
|
|
|
24
24
|
this.lastLayoutDimensions = null;
|
|
25
25
|
this.lastSentDimensions = null;
|
|
26
26
|
this.pendingLayoutFrame = null;
|
|
27
|
+
this.themeRefreshTimer = null;
|
|
28
|
+
this.pendingThemeRefresh = false;
|
|
27
29
|
this.layoutRetryTimers = new Set();
|
|
28
30
|
this.disposables = [];
|
|
29
31
|
this.helperTextarea = null;
|
|
@@ -61,6 +63,8 @@ export class TerminalInstance {
|
|
|
61
63
|
|
|
62
64
|
applyTheme() {
|
|
63
65
|
this.xterm.applyResolvedTheme();
|
|
66
|
+
this.xterm.forceRedraw();
|
|
67
|
+
this._scheduleThemeRefreshForCli();
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
focus() {
|
|
@@ -116,6 +120,10 @@ export class TerminalInstance {
|
|
|
116
120
|
if (nextVisible) {
|
|
117
121
|
this.resizeDebouncer.flush();
|
|
118
122
|
this.scheduleLayout({ immediate: true, retries: true, forceRedraw: true });
|
|
123
|
+
if (this.pendingThemeRefresh) {
|
|
124
|
+
this.pendingThemeRefresh = false;
|
|
125
|
+
this._scheduleThemeRefreshForCli();
|
|
126
|
+
}
|
|
119
127
|
}
|
|
120
128
|
return didChange;
|
|
121
129
|
}
|
|
@@ -123,6 +131,9 @@ export class TerminalInstance {
|
|
|
123
131
|
dispose() {
|
|
124
132
|
this.closedByUs = true;
|
|
125
133
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
134
|
+
if (this.themeRefreshTimer) clearTimeout(this.themeRefreshTimer);
|
|
135
|
+
this.themeRefreshTimer = null;
|
|
136
|
+
this.pendingThemeRefresh = false;
|
|
126
137
|
this._cancelScheduledLayout();
|
|
127
138
|
this.resizeDebouncer.dispose();
|
|
128
139
|
for (const dispose of this.disposables.splice(0)) {
|
|
@@ -205,15 +216,6 @@ export class TerminalInstance {
|
|
|
205
216
|
ro.observe(host);
|
|
206
217
|
this.disposables.push(() => ro.disconnect());
|
|
207
218
|
|
|
208
|
-
const vv = window.visualViewport;
|
|
209
|
-
const onVisualResize = () => this.scheduleLayout({ retries: true });
|
|
210
|
-
vv?.addEventListener?.('resize', onVisualResize);
|
|
211
|
-
vv?.addEventListener?.('scroll', onVisualResize);
|
|
212
|
-
this.disposables.push(() => {
|
|
213
|
-
vv?.removeEventListener?.('resize', onVisualResize);
|
|
214
|
-
vv?.removeEventListener?.('scroll', onVisualResize);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
219
|
const onHostClick = () => this.xterm.focus();
|
|
218
220
|
if (this.xterm.isMobile) {
|
|
219
221
|
host.addEventListener('click', onHostClick);
|
|
@@ -349,6 +351,23 @@ export class TerminalInstance {
|
|
|
349
351
|
}
|
|
350
352
|
}
|
|
351
353
|
|
|
354
|
+
_scheduleThemeRefreshForCli() {
|
|
355
|
+
if (this.cliType !== 'codex') return;
|
|
356
|
+
if (!this.isVisible) {
|
|
357
|
+
this.pendingThemeRefresh = true;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (this.themeRefreshTimer) clearTimeout(this.themeRefreshTimer);
|
|
361
|
+
// Codex caches terminal default colours for its composer. A focus-in
|
|
362
|
+
// event makes it re-query OSC 10/11, which our handlers answer from the
|
|
363
|
+
// just-applied xterm theme.
|
|
364
|
+
this.themeRefreshTimer = setTimeout(() => {
|
|
365
|
+
this.themeRefreshTimer = null;
|
|
366
|
+
if (this.closedByUs || this.cliType !== 'codex' || !this.isVisible) return;
|
|
367
|
+
this._sendFrame({ type: 'input', data: '\x1b[I' });
|
|
368
|
+
}, 40);
|
|
369
|
+
}
|
|
370
|
+
|
|
352
371
|
_sendResize(cols, rows, force = false) {
|
|
353
372
|
if (!(cols > 0 && rows > 0)) return;
|
|
354
373
|
if (!force
|
|
@@ -61,6 +61,8 @@ export class XtermTerminal {
|
|
|
61
61
|
this.webglAddon = null;
|
|
62
62
|
this.webglContextLossDisposable = null;
|
|
63
63
|
this.refreshDimensionListeners = new Set();
|
|
64
|
+
this.resizeScrollState = null;
|
|
65
|
+
this.resizeScrollStateTimer = null;
|
|
64
66
|
this.host = null;
|
|
65
67
|
|
|
66
68
|
this.raw = new Terminal({
|
|
@@ -151,7 +153,7 @@ export class XtermTerminal {
|
|
|
151
153
|
if (!proposed) return null;
|
|
152
154
|
|
|
153
155
|
if (proposed.cols !== this.raw.cols || proposed.rows !== this.raw.rows) {
|
|
154
|
-
|
|
156
|
+
this._resizeRaw(proposed.cols, proposed.rows);
|
|
155
157
|
}
|
|
156
158
|
lastKnownGridDimensions = proposed;
|
|
157
159
|
return proposed;
|
|
@@ -163,7 +165,7 @@ export class XtermTerminal {
|
|
|
163
165
|
|
|
164
166
|
resize(cols, rows) {
|
|
165
167
|
if (!(cols > 0 && rows > 0)) return;
|
|
166
|
-
|
|
168
|
+
this._resizeRaw(cols, rows);
|
|
167
169
|
lastKnownGridDimensions = { cols: this.raw.cols, rows: this.raw.rows };
|
|
168
170
|
}
|
|
169
171
|
|
|
@@ -217,6 +219,9 @@ export class XtermTerminal {
|
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
dispose() {
|
|
222
|
+
if (this.resizeScrollStateTimer) clearTimeout(this.resizeScrollStateTimer);
|
|
223
|
+
this.resizeScrollState = null;
|
|
224
|
+
this.resizeScrollStateTimer = null;
|
|
220
225
|
if (this.host?.xterm === this.raw) {
|
|
221
226
|
try { delete this.host.xterm; } catch { this.host.xterm = undefined; }
|
|
222
227
|
}
|
|
@@ -268,6 +273,61 @@ export class XtermTerminal {
|
|
|
268
273
|
}
|
|
269
274
|
}
|
|
270
275
|
|
|
276
|
+
_resizeRaw(cols, rows) {
|
|
277
|
+
const scrollState = this._scrollStateForResize();
|
|
278
|
+
try { this.raw.resize(cols, rows); } catch {}
|
|
279
|
+
this._restoreScrollStateIfNeeded(scrollState);
|
|
280
|
+
requestAnimationFrame(() => this._restoreScrollStateIfNeeded(scrollState));
|
|
281
|
+
setTimeout(() => this._restoreScrollStateIfNeeded(scrollState), 150);
|
|
282
|
+
setTimeout(() => this._restoreScrollStateIfNeeded(scrollState), 350);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_scrollStateForResize() {
|
|
286
|
+
const state = this._captureScrollState();
|
|
287
|
+
if (state?.viewportY > 0) {
|
|
288
|
+
this._rememberResizeScrollState(state);
|
|
289
|
+
return state;
|
|
290
|
+
}
|
|
291
|
+
return this.resizeScrollState;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_rememberResizeScrollState(state) {
|
|
295
|
+
this.resizeScrollState = state;
|
|
296
|
+
if (this.resizeScrollStateTimer) clearTimeout(this.resizeScrollStateTimer);
|
|
297
|
+
this.resizeScrollStateTimer = setTimeout(() => {
|
|
298
|
+
this.resizeScrollState = null;
|
|
299
|
+
this.resizeScrollStateTimer = null;
|
|
300
|
+
}, 500);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_captureScrollState() {
|
|
304
|
+
const buffer = this.raw?.buffer?.active;
|
|
305
|
+
if (!buffer) return null;
|
|
306
|
+
return {
|
|
307
|
+
viewportY: buffer.viewportY,
|
|
308
|
+
baseY: buffer.baseY,
|
|
309
|
+
atBottom: buffer.viewportY >= buffer.baseY,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_restoreScrollStateIfNeeded(state) {
|
|
314
|
+
if (!state || !(state.viewportY > 0)) return;
|
|
315
|
+
const buffer = this.raw?.buffer?.active;
|
|
316
|
+
if (!buffer) return;
|
|
317
|
+
|
|
318
|
+
if (state.atBottom) {
|
|
319
|
+
if (buffer.viewportY < buffer.baseY) {
|
|
320
|
+
try { this.raw.scrollToBottom(); } catch {}
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const target = Math.min(state.viewportY, buffer.baseY);
|
|
326
|
+
if (target > 0 && buffer.viewportY !== target) {
|
|
327
|
+
try { this.raw.scrollToLine(target); } catch {}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
271
331
|
_installSelectionCopyGuard() {
|
|
272
332
|
this.raw.attachCustomKeyEventHandler((ev) => {
|
|
273
333
|
if (ev.type === 'keydown'
|
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
// Lightweight HTML5 drag-reorder helper.
|
|
2
|
-
//
|
|
3
|
-
// Usage:
|
|
4
|
-
// const dnd = useDragSort(items.map((i) => i.id), async (nextIds) => {
|
|
5
|
-
// await reorderFolders(nextIds);
|
|
6
|
-
// });
|
|
7
|
-
// items.map((it) => html`
|
|
8
|
-
// <div ...${dnd.rowProps(it.id)}>
|
|
9
|
-
// <span ...${dnd.handleProps(it.id)}>⋮⋮</span>
|
|
10
|
-
// ...
|
|
11
|
-
// </div>`);
|
|
12
|
-
//
|
|
13
|
-
// rowProps spreads onDragOver / onDrop / data-* on the row container.
|
|
14
|
-
// handleProps spreads draggable + onDragStart on the drag handle. We
|
|
15
|
-
// gate "draggable" on the handle so clicks inside the row don't start a
|
|
16
|
-
// drag and the user can still click rows normally.
|
|
17
|
-
|
|
18
|
-
import { useRef, useState } from 'preact/hooks';
|
|
19
|
-
|
|
20
|
-
export function useDragSort(ids, onCommit) {
|
|
21
|
-
const dragging = useRef(null);
|
|
22
|
-
const [overId, setOverId] = useState(null);
|
|
23
|
-
|
|
24
|
-
const handleProps = (id) => ({
|
|
25
|
-
draggable: true,
|
|
26
|
-
onDragStart: (ev) => {
|
|
27
|
-
dragging.current = id;
|
|
28
|
-
ev.dataTransfer.effectAllowed = 'move';
|
|
29
|
-
// Setting some data is required for Firefox to actually start a drag.
|
|
30
|
-
try { ev.dataTransfer.setData('text/plain', id); } catch {}
|
|
31
|
-
},
|
|
32
|
-
onDragEnd: () => { dragging.current = null; setOverId(null); },
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const rowProps = (id) => ({
|
|
36
|
-
'data-dnd-id': id,
|
|
37
|
-
'data-dnd-over': overId === id ? 'true' : undefined,
|
|
38
|
-
onDragOver: (ev) => {
|
|
39
|
-
if (dragging.current == null || dragging.current === id) return;
|
|
40
|
-
ev.preventDefault();
|
|
41
|
-
ev.dataTransfer.dropEffect = 'move';
|
|
42
|
-
if (overId !== id) setOverId(id);
|
|
43
|
-
},
|
|
44
|
-
onDragLeave: (ev) => {
|
|
45
|
-
// Only clear if the pointer leaves the row entirely (not when entering a child).
|
|
46
|
-
const rt = ev.relatedTarget;
|
|
47
|
-
if (rt && ev.currentTarget.contains(rt)) return;
|
|
48
|
-
if (overId === id) setOverId(null);
|
|
49
|
-
},
|
|
50
|
-
onDrop: (ev) => {
|
|
51
|
-
ev.preventDefault();
|
|
52
|
-
const src = dragging.current;
|
|
53
|
-
dragging.current = null;
|
|
54
|
-
setOverId(null);
|
|
55
|
-
if (src == null || src === id) return;
|
|
56
|
-
const cur = [...ids];
|
|
57
|
-
const from = cur.indexOf(src);
|
|
58
|
-
const to = cur.indexOf(id);
|
|
59
|
-
if (from < 0 || to < 0) return;
|
|
60
|
-
cur.splice(from, 1);
|
|
61
|
-
cur.splice(to, 0, src);
|
|
62
|
-
onCommit?.(cur);
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return { handleProps, rowProps, draggingId: dragging.current, overId };
|
|
67
|
-
}
|
|
1
|
+
// Lightweight HTML5 drag-reorder helper.
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// const dnd = useDragSort(items.map((i) => i.id), async (nextIds) => {
|
|
5
|
+
// await reorderFolders(nextIds);
|
|
6
|
+
// });
|
|
7
|
+
// items.map((it) => html`
|
|
8
|
+
// <div ...${dnd.rowProps(it.id)}>
|
|
9
|
+
// <span ...${dnd.handleProps(it.id)}>⋮⋮</span>
|
|
10
|
+
// ...
|
|
11
|
+
// </div>`);
|
|
12
|
+
//
|
|
13
|
+
// rowProps spreads onDragOver / onDrop / data-* on the row container.
|
|
14
|
+
// handleProps spreads draggable + onDragStart on the drag handle. We
|
|
15
|
+
// gate "draggable" on the handle so clicks inside the row don't start a
|
|
16
|
+
// drag and the user can still click rows normally.
|
|
17
|
+
|
|
18
|
+
import { useRef, useState } from 'preact/hooks';
|
|
19
|
+
|
|
20
|
+
export function useDragSort(ids, onCommit) {
|
|
21
|
+
const dragging = useRef(null);
|
|
22
|
+
const [overId, setOverId] = useState(null);
|
|
23
|
+
|
|
24
|
+
const handleProps = (id) => ({
|
|
25
|
+
draggable: true,
|
|
26
|
+
onDragStart: (ev) => {
|
|
27
|
+
dragging.current = id;
|
|
28
|
+
ev.dataTransfer.effectAllowed = 'move';
|
|
29
|
+
// Setting some data is required for Firefox to actually start a drag.
|
|
30
|
+
try { ev.dataTransfer.setData('text/plain', id); } catch {}
|
|
31
|
+
},
|
|
32
|
+
onDragEnd: () => { dragging.current = null; setOverId(null); },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const rowProps = (id) => ({
|
|
36
|
+
'data-dnd-id': id,
|
|
37
|
+
'data-dnd-over': overId === id ? 'true' : undefined,
|
|
38
|
+
onDragOver: (ev) => {
|
|
39
|
+
if (dragging.current == null || dragging.current === id) return;
|
|
40
|
+
ev.preventDefault();
|
|
41
|
+
ev.dataTransfer.dropEffect = 'move';
|
|
42
|
+
if (overId !== id) setOverId(id);
|
|
43
|
+
},
|
|
44
|
+
onDragLeave: (ev) => {
|
|
45
|
+
// Only clear if the pointer leaves the row entirely (not when entering a child).
|
|
46
|
+
const rt = ev.relatedTarget;
|
|
47
|
+
if (rt && ev.currentTarget.contains(rt)) return;
|
|
48
|
+
if (overId === id) setOverId(null);
|
|
49
|
+
},
|
|
50
|
+
onDrop: (ev) => {
|
|
51
|
+
ev.preventDefault();
|
|
52
|
+
const src = dragging.current;
|
|
53
|
+
dragging.current = null;
|
|
54
|
+
setOverId(null);
|
|
55
|
+
if (src == null || src === id) return;
|
|
56
|
+
const cur = [...ids];
|
|
57
|
+
const from = cur.indexOf(src);
|
|
58
|
+
const to = cur.indexOf(id);
|
|
59
|
+
if (from < 0 || to < 0) return;
|
|
60
|
+
cur.splice(from, 1);
|
|
61
|
+
cur.splice(to, 0, src);
|
|
62
|
+
onCommit?.(cur);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return { handleProps, rowProps, draggingId: dragging.current, overId };
|
|
67
|
+
}
|
package/public/js/dialog.js
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
// Promise-based confirm/prompt rendered through DialogHost. The stack
|
|
2
|
-
// signal lets us nest dialogs if ever needed; .close() pops by id.
|
|
3
|
-
|
|
4
|
-
import { signal } from '@preact/signals';
|
|
5
|
-
import { html } from './html.js';
|
|
6
|
-
|
|
7
|
-
export const dialogs = signal([]);
|
|
8
|
-
let nextId = 1;
|
|
9
|
-
|
|
10
|
-
function push(entry) {
|
|
11
|
-
return new Promise((resolve) => {
|
|
12
|
-
const id = nextId++;
|
|
13
|
-
const close = (action, host) => {
|
|
14
|
-
dialogs.value = dialogs.value.filter((d) => d.id !== id);
|
|
15
|
-
resolve(entry.onResolve(action, host));
|
|
16
|
-
};
|
|
17
|
-
dialogs.value = [...dialogs.value, { id, ...entry, close }];
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const CLOSE_X = html`
|
|
22
|
-
<button class="modal-close" type="button" aria-label="Close" data-action="cancel">
|
|
23
|
-
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
24
|
-
<line x1="3" y1="3" x2="13" y2="13"/>
|
|
25
|
-
<line x1="13" y1="3" x2="3" y2="13"/>
|
|
26
|
-
</svg>
|
|
27
|
-
</button>`;
|
|
28
|
-
|
|
29
|
-
export function ccsmConfirm(message, opts = {}) {
|
|
30
|
-
const { title = 'Confirm', okLabel = 'Confirm', cancelLabel = 'Cancel', danger = false } = opts;
|
|
31
|
-
return push({
|
|
32
|
-
render: () => html`<div class="modal modal-dialog">
|
|
33
|
-
<header class="modal-head"><h2>${title}</h2>${CLOSE_X}</header>
|
|
34
|
-
<div class="modal-body"><p class="dialog-msg">${message}</p></div>
|
|
35
|
-
<footer class="modal-foot">
|
|
36
|
-
<button class="action" data-action="cancel">${cancelLabel}</button>
|
|
37
|
-
<button class=${`action ${danger ? 'danger' : 'primary'}`} data-action="ok">${okLabel}</button>
|
|
38
|
-
</footer>
|
|
39
|
-
</div>`,
|
|
40
|
-
onResolve: (action) => action === 'ok' || action === 'enter',
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function ccsmPrompt(message, defaultValue = '', opts = {}) {
|
|
45
|
-
const { title, okLabel = 'Save', cancelLabel = 'Cancel', placeholder = '' } = opts;
|
|
46
|
-
return push({
|
|
47
|
-
render: () => html`<div class="modal modal-dialog">
|
|
48
|
-
<header class="modal-head"><h2>${title || message}</h2>${CLOSE_X}</header>
|
|
49
|
-
<div class="modal-body">
|
|
50
|
-
${title ? html`<p class="dialog-msg">${message}</p>` : null}
|
|
51
|
-
<input type="text" class="input" placeholder=${placeholder} value=${defaultValue} />
|
|
52
|
-
</div>
|
|
53
|
-
<footer class="modal-foot">
|
|
54
|
-
<button class="action" data-action="cancel">${cancelLabel}</button>
|
|
55
|
-
<button class="action primary" data-action="ok">${okLabel}</button>
|
|
56
|
-
</footer>
|
|
57
|
-
</div>`,
|
|
58
|
-
initialFocus: (host) => {
|
|
59
|
-
const inp = host.querySelector('input');
|
|
60
|
-
if (inp) { inp.focus(); inp.select(); }
|
|
61
|
-
},
|
|
62
|
-
onResolve: (action, host) => {
|
|
63
|
-
const inp = host?.querySelector('input');
|
|
64
|
-
return (action === 'ok' || action === 'enter') ? (inp?.value ?? '') : null;
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
}
|
|
1
|
+
// Promise-based confirm/prompt rendered through DialogHost. The stack
|
|
2
|
+
// signal lets us nest dialogs if ever needed; .close() pops by id.
|
|
3
|
+
|
|
4
|
+
import { signal } from '@preact/signals';
|
|
5
|
+
import { html } from './html.js';
|
|
6
|
+
|
|
7
|
+
export const dialogs = signal([]);
|
|
8
|
+
let nextId = 1;
|
|
9
|
+
|
|
10
|
+
function push(entry) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const id = nextId++;
|
|
13
|
+
const close = (action, host) => {
|
|
14
|
+
dialogs.value = dialogs.value.filter((d) => d.id !== id);
|
|
15
|
+
resolve(entry.onResolve(action, host));
|
|
16
|
+
};
|
|
17
|
+
dialogs.value = [...dialogs.value, { id, ...entry, close }];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CLOSE_X = html`
|
|
22
|
+
<button class="modal-close" type="button" aria-label="Close" data-action="cancel">
|
|
23
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
24
|
+
<line x1="3" y1="3" x2="13" y2="13"/>
|
|
25
|
+
<line x1="13" y1="3" x2="3" y2="13"/>
|
|
26
|
+
</svg>
|
|
27
|
+
</button>`;
|
|
28
|
+
|
|
29
|
+
export function ccsmConfirm(message, opts = {}) {
|
|
30
|
+
const { title = 'Confirm', okLabel = 'Confirm', cancelLabel = 'Cancel', danger = false } = opts;
|
|
31
|
+
return push({
|
|
32
|
+
render: () => html`<div class="modal modal-dialog">
|
|
33
|
+
<header class="modal-head"><h2>${title}</h2>${CLOSE_X}</header>
|
|
34
|
+
<div class="modal-body"><p class="dialog-msg">${message}</p></div>
|
|
35
|
+
<footer class="modal-foot">
|
|
36
|
+
<button class="action" data-action="cancel">${cancelLabel}</button>
|
|
37
|
+
<button class=${`action ${danger ? 'danger' : 'primary'}`} data-action="ok">${okLabel}</button>
|
|
38
|
+
</footer>
|
|
39
|
+
</div>`,
|
|
40
|
+
onResolve: (action) => action === 'ok' || action === 'enter',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ccsmPrompt(message, defaultValue = '', opts = {}) {
|
|
45
|
+
const { title, okLabel = 'Save', cancelLabel = 'Cancel', placeholder = '' } = opts;
|
|
46
|
+
return push({
|
|
47
|
+
render: () => html`<div class="modal modal-dialog">
|
|
48
|
+
<header class="modal-head"><h2>${title || message}</h2>${CLOSE_X}</header>
|
|
49
|
+
<div class="modal-body">
|
|
50
|
+
${title ? html`<p class="dialog-msg">${message}</p>` : null}
|
|
51
|
+
<input type="text" class="input" placeholder=${placeholder} value=${defaultValue} />
|
|
52
|
+
</div>
|
|
53
|
+
<footer class="modal-foot">
|
|
54
|
+
<button class="action" data-action="cancel">${cancelLabel}</button>
|
|
55
|
+
<button class="action primary" data-action="ok">${okLabel}</button>
|
|
56
|
+
</footer>
|
|
57
|
+
</div>`,
|
|
58
|
+
initialFocus: (host) => {
|
|
59
|
+
const inp = host.querySelector('input');
|
|
60
|
+
if (inp) { inp.focus(); inp.select(); }
|
|
61
|
+
},
|
|
62
|
+
onResolve: (action, host) => {
|
|
63
|
+
const inp = host?.querySelector('input');
|
|
64
|
+
return (action === 'ok' || action === 'enter') ? (inp?.value ?? '') : null;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|