anentrypoint-design 0.0.385 → 0.0.387
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/dist/247420.css +33 -0
- package/dist/247420.js +53 -51
- package/package.json +1 -1
- package/src/components/agent-chat/controls.js +143 -0
- package/src/components/agent-chat/empty-state.js +63 -0
- package/src/components/agent-chat/message-rows.js +141 -0
- package/src/components/agent-chat/surface.js +206 -0
- package/src/components/agent-chat/thread-behaviour.js +50 -0
- package/src/components/agent-chat.js +6 -546
- package/src/components/chat/composer-affordances.js +109 -0
- package/src/components/chat/composer.js +256 -0
- package/src/components/chat/threads.js +105 -0
- package/src/components/chat-message-parts/agent-nodes.js +103 -0
- package/src/components/chat-message-parts/inline.js +106 -0
- package/src/components/chat-message-parts/prose-nodes.js +133 -0
- package/src/components/chat-message-parts/renderers.js +89 -0
- package/src/components/chat-message-parts.js +12 -408
- package/src/components/chat-minimap/minimap.js +167 -0
- package/src/components/chat-minimap/paint.js +73 -0
- package/src/components/chat-minimap/preview.js +41 -0
- package/src/components/chat-minimap.js +7 -263
- package/src/components/chat.js +20 -628
- package/src/components/community/chrome.js +63 -0
- package/src/components/community/navigation.js +167 -0
- package/src/components/community/presence.js +106 -0
- package/src/components/community/shell.js +17 -0
- package/src/components/community/views.js +131 -0
- package/src/components/community.js +18 -447
- package/src/components/files/chrome.js +140 -0
- package/src/components/files/entries.js +156 -0
- package/src/components/files/grid-controls.js +63 -0
- package/src/components/files/grid.js +177 -0
- package/src/components/files/types.js +69 -0
- package/src/components/files-modals/dialogs.js +118 -0
- package/src/components/files-modals/modal-shell.js +153 -0
- package/src/components/files-modals/preview-bodies.js +129 -0
- package/src/components/files-modals/preview-containers.js +96 -0
- package/src/components/files-modals.js +14 -475
- package/src/components/files.js +15 -564
- package/src/components/freddie/pages-config.js +3 -94
- package/src/components/freddie/pages-models.js +97 -0
- package/src/components/freddie.js +2 -1
- package/src/components/interaction-primitives/mobile.js +25 -0
- package/src/components/interaction-primitives/pointer.js +214 -0
- package/src/components/interaction-primitives/reorderable.js +47 -0
- package/src/components/interaction-primitives/shortcuts.js +119 -0
- package/src/components/interaction-primitives.js +16 -388
- package/src/components/sessions/conversation-list.js +230 -0
- package/src/components/sessions/dashboard.js +202 -0
- package/src/components/sessions/detail-bits.js +49 -0
- package/src/components/sessions/format.js +42 -0
- package/src/components/sessions/session-card.js +92 -0
- package/src/components/sessions.js +16 -579
- package/src/components/voice/audio-cue.js +39 -0
- package/src/components/voice/capture.js +90 -0
- package/src/components/voice/playback.js +75 -0
- package/src/components/voice/settings-modal.js +104 -0
- package/src/components/voice.js +16 -293
- package/src/css/app-shell/base.css +23 -0
- package/src/css/app-shell/states-interactions.css +10 -0
- package/src/kits/os/freddie/chat-protocol.js +32 -0
- package/src/kits/os/freddie/chat-transport.js +178 -0
- package/src/kits/os/freddie/pages-chat.js +10 -180
- package/src/kits/os/shell-chrome.js +163 -0
- package/src/kits/os/shell-geometry.js +59 -0
- package/src/kits/os/shell.js +178 -335
- package/src/page-html/client-script.js +151 -0
- package/src/page-html/head-tags.js +59 -0
- package/src/page-html/markdown.js +68 -0
- package/src/page-html/page-styles.js +44 -0
- package/src/page-html.js +14 -291
|
@@ -1,476 +1,15 @@
|
|
|
1
1
|
// File modals — matches upstream signatures + class names.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// counter that advances on every render and leaves the old aria-labelledby
|
|
17
|
-
// reference dangling. A WeakMap keyed on the options object would not survive
|
|
18
|
-
// re-renders, so we use a short random suffix minted ONCE per Modal() call
|
|
19
|
-
// inside the function body — the closure keeps it stable for that render tree.
|
|
20
|
-
// _modalSeq is retained only for external callers that may import it; it is no
|
|
21
|
-
// longer used internally.
|
|
22
|
-
let _modalSeq = 0;
|
|
23
|
-
|
|
24
|
-
function Backdrop({ onClose, children, kind = '', labelledBy, busy = false } = {}) {
|
|
25
|
-
// webjsx invokes a ref callback with the element on mount and with null on
|
|
26
|
-
// unmount. We stash the per-element keydown teardown on the node itself so
|
|
27
|
-
// the null branch can run it — otherwise the document/element listener leaks
|
|
28
|
-
// once the modal is removed.
|
|
29
|
-
const backdropRef = (el) => {
|
|
30
|
-
if (!el) return; // unmount (ref(null)) handled by wrapper below
|
|
31
|
-
const modal = el.querySelector('.ds-modal');
|
|
32
|
-
if (!modal) return;
|
|
33
|
-
|
|
34
|
-
const handleKeydown = (e) => {
|
|
35
|
-
// Escape closes the modal — unless a mutation is in flight (the live
|
|
36
|
-
// busy state is read off the data-busy attribute, which re-renders;
|
|
37
|
-
// this handler's closure is bound once at mount).
|
|
38
|
-
if (e.key === 'Escape') {
|
|
39
|
-
e.preventDefault();
|
|
40
|
-
if (el.dataset.busy === '1') return;
|
|
41
|
-
if (onClose) onClose();
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
// Focus trap: re-query focusables on each Tab press so that buttons
|
|
45
|
-
// disabled mid-flight (busy state) are excluded from the cycle and
|
|
46
|
-
// do not break tab navigation.
|
|
47
|
-
if (e.key === 'Tab') {
|
|
48
|
-
const focusables = modal.querySelectorAll(FOCUSABLE_SEL);
|
|
49
|
-
if (focusables.length === 0) {
|
|
50
|
-
e.preventDefault();
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const firstFocusable = focusables[0];
|
|
54
|
-
const lastFocusable = focusables[focusables.length - 1];
|
|
55
|
-
if (e.shiftKey) {
|
|
56
|
-
if (document.activeElement === firstFocusable) {
|
|
57
|
-
e.preventDefault();
|
|
58
|
-
lastFocusable.focus();
|
|
59
|
-
}
|
|
60
|
-
} else {
|
|
61
|
-
if (document.activeElement === lastFocusable) {
|
|
62
|
-
e.preventDefault();
|
|
63
|
-
firstFocusable.focus();
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// Escape must close the modal no matter where focus sits (re-renders
|
|
70
|
-
// can bounce focus out of the dialog), so listen at document level
|
|
71
|
-
// for the modal's lifetime.
|
|
72
|
-
document.addEventListener('keydown', handleKeydown, true);
|
|
73
|
-
// Record the invoker BEFORE the modal steals focus, so close (confirm,
|
|
74
|
-
// cancel, Escape, backdrop click) restores keyboard/AT focus to where
|
|
75
|
-
// the user was (e.g. the FileGrid row button) instead of <body>.
|
|
76
|
-
// Re-mounts mid-lifetime (every app render re-runs this ref) keep the
|
|
77
|
-
// ORIGINAL invoker and never re-steal focus from the user.
|
|
78
|
-
const invoker = el.contains(document.activeElement) ? (Backdrop._invoker || document.activeElement) : document.activeElement;
|
|
79
|
-
if (!Backdrop._invoker) Backdrop._invoker = invoker;
|
|
80
|
-
el._dsModalTeardown = (removed) => {
|
|
81
|
-
document.removeEventListener('keydown', handleKeydown, true);
|
|
82
|
-
// Only restore focus when the modal is genuinely going away (not a
|
|
83
|
-
// re-render remount) and focus is not already somewhere useful.
|
|
84
|
-
if (removed && Backdrop._invoker && Backdrop._invoker.focus && Backdrop._invoker.isConnected) {
|
|
85
|
-
try { Backdrop._invoker.focus(); } catch { /* swallow: restoring focus on close is best-effort, teardown still completes */ }
|
|
86
|
-
}
|
|
87
|
-
if (removed) Backdrop._invoker = null;
|
|
88
|
-
};
|
|
89
|
-
// Auto-focus on open - only when focus is not already inside the modal
|
|
90
|
-
// (re-renders must not yank the caret around).
|
|
91
|
-
if (!el.contains(document.activeElement)) {
|
|
92
|
-
const preferred = modal.querySelector('[autofocus]') || modal.querySelector(FOCUSABLE_SEL);
|
|
93
|
-
if (preferred) preferred.focus();
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
return h('div', {
|
|
98
|
-
class: 'ds-modal-backdrop',
|
|
99
|
-
// Live busy flag read by the mount-bound Escape handler + backdrop click.
|
|
100
|
-
'data-busy': busy ? '1' : '0',
|
|
101
|
-
ref: (el) => {
|
|
102
|
-
if (el) {
|
|
103
|
-
// A remount in the same tick (render churn) is not a close:
|
|
104
|
-
// cancel the pending removal teardown before re-binding.
|
|
105
|
-
Backdrop._pendingRemoval = false;
|
|
106
|
-
backdropRef(el);
|
|
107
|
-
Backdrop._last = el;
|
|
108
|
-
} else if (Backdrop._last && Backdrop._last._dsModalTeardown) {
|
|
109
|
-
const t = Backdrop._last._dsModalTeardown;
|
|
110
|
-
Backdrop._last = null;
|
|
111
|
-
Backdrop._pendingRemoval = true;
|
|
112
|
-
t(false); // always unhook the document listener now
|
|
113
|
-
queueMicrotask(() => {
|
|
114
|
-
// Still gone next microtask -> genuine close: restore focus.
|
|
115
|
-
if (Backdrop._pendingRemoval) { t(true); Backdrop._pendingRemoval = false; }
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
onclick: (e) => {
|
|
120
|
-
if (e.target !== e.currentTarget) return;
|
|
121
|
-
if (e.currentTarget.dataset.busy === '1') return; // no mid-flight close
|
|
122
|
-
if (onClose) onClose();
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
h('div', {
|
|
126
|
-
class: 'ds-modal' + (kind ? ' ds-modal-' + kind : ''),
|
|
127
|
-
role: 'dialog', 'aria-modal': 'true',
|
|
128
|
-
...(labelledBy ? { 'aria-labelledby': labelledBy } : {})
|
|
129
|
-
}, ...(Array.isArray(children) ? children : [children]))
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Shared modal shell: head + body + actions row. ConfirmDialog/PromptDialog/
|
|
134
|
-
// FileViewer all funnel through this so the ds-modal markup is authored once.
|
|
135
|
-
// `actions` is an array of vnodes (already using the Btn primitive). Any of the
|
|
136
|
-
// slots may be omitted.
|
|
137
|
-
function Modal({ onClose, kind = '', head, headClass = '', headAttrs = {}, body, bodyClass = 'ds-modal-body', bodyAttrs = {}, actions, busy = false } = {}) {
|
|
138
|
-
// Give the head a stable id so the dialog can point aria-labelledby at it,
|
|
139
|
-
// exposing the title as the dialog's accessible name to screen readers.
|
|
140
|
-
// The id is minted once per Modal() call with a short random suffix so it
|
|
141
|
-
// stays constant across re-renders of the same dialog instance — an
|
|
142
|
-
// incrementing counter advances on every render, leaving the previous
|
|
143
|
-
// aria-labelledby reference pointing at a now-absent element.
|
|
144
|
-
const headId = head != null ? ('ds-modal-head-' + shortUid(6)) : null;
|
|
145
|
-
return Backdrop({
|
|
146
|
-
onClose,
|
|
147
|
-
kind,
|
|
148
|
-
busy,
|
|
149
|
-
labelledBy: headId,
|
|
150
|
-
children: [
|
|
151
|
-
head != null ? h('div', { id: headId, class: ('ds-modal-head' + (headClass ? ' ' + headClass : '')), ...headAttrs }, ...(Array.isArray(head) ? head : [head])) : null,
|
|
152
|
-
body != null ? h('div', { class: bodyClass, ...bodyAttrs }, ...(Array.isArray(body) ? body : [body])) : null,
|
|
153
|
-
actions != null ? h('div', { class: 'ds-modal-actions' }, ...(Array.isArray(actions) ? actions : [actions])) : null,
|
|
154
|
-
].filter(Boolean)
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// A role=alert error line rendered INSIDE the modal body (so a 409/403 from a
|
|
159
|
-
// mutation is visible at the point of action, inside the focus trap — not a
|
|
160
|
-
// sibling stuck in page flow behind the fixed backdrop).
|
|
161
|
-
function modalError(error) {
|
|
162
|
-
return error ? h('p', { class: 'ds-modal-error', role: 'alert' }, String(error)) : null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// `error` renders inside .ds-modal-body (role=alert, error tone). `busy`
|
|
166
|
-
// disables both action buttons AND the Escape/backdrop close paths; the confirm
|
|
167
|
-
// label flips to `busyLabel` (default 'working…') so the in-flight state reads.
|
|
168
|
-
export function ConfirmDialog({ title = 'Are you sure?', message, confirmLabel = 'confirm', cancelLabel = 'cancel', destructive, onConfirm, onCancel, error, busy = false, busyLabel = 'working…' } = {}) {
|
|
169
|
-
return Modal({
|
|
170
|
-
onClose: onCancel,
|
|
171
|
-
kind: 'small',
|
|
172
|
-
busy,
|
|
173
|
-
head: title,
|
|
174
|
-
body: [message || '', modalError(error)].filter(Boolean),
|
|
175
|
-
actions: [
|
|
176
|
-
Btn({ onClick: onCancel, disabled: busy, children: cancelLabel }),
|
|
177
|
-
Btn({ variant: destructive ? 'danger' : 'primary', disabled: busy, onClick: onConfirm, children: busy ? busyLabel : confirmLabel })
|
|
178
|
-
]
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function PromptDialog({ title = 'Enter a name', value = '', placeholder = '', confirmLabel = 'ok', cancelLabel = 'cancel', onConfirm, onCancel, onInput, error, busy = false, busyLabel = 'working…', roots, onPickRoot } = {}) {
|
|
183
|
-
// Optional one-click starting-point chips (e.g. a destination-path prompt
|
|
184
|
-
// for a filesystem with more than one allowed root) - a user typing a
|
|
185
|
-
// path has no way to discover what a second disjoint root even looks
|
|
186
|
-
// like otherwise. Each { path, label } fills the input via the same
|
|
187
|
-
// onInput callback a manual keystroke would.
|
|
188
|
-
const rootsRow = (roots && roots.length)
|
|
189
|
-
? h('div', { class: 'ds-prompt-roots', role: 'group', 'aria-label': 'accessible folders' },
|
|
190
|
-
...roots.map((r, i) => h('button', {
|
|
191
|
-
key: 'pr' + i, type: 'button', class: 'ds-prompt-root-chip',
|
|
192
|
-
onclick: () => { const p = r.path || r; if (onPickRoot) onPickRoot(p); else if (onInput) onInput(p); },
|
|
193
|
-
}, r.label || r.path || r)))
|
|
194
|
-
: null;
|
|
195
|
-
return Modal({
|
|
196
|
-
onClose: onCancel,
|
|
197
|
-
kind: 'small',
|
|
198
|
-
busy,
|
|
199
|
-
head: title,
|
|
200
|
-
body: [h('input', {
|
|
201
|
-
class: 'input ds-modal-input',
|
|
202
|
-
type: 'text',
|
|
203
|
-
value,
|
|
204
|
-
placeholder,
|
|
205
|
-
autofocus: true,
|
|
206
|
-
disabled: busy ? true : null,
|
|
207
|
-
'aria-invalid': error ? 'true' : null,
|
|
208
|
-
oninput: (e) => onInput && onInput(e.target.value),
|
|
209
|
-
onkeydown: (e) => {
|
|
210
|
-
// IME guard: the Enter that commits a CJK composition must not confirm.
|
|
211
|
-
if (e.key === 'Enter' && !e.isComposing && e.keyCode !== 229) { e.preventDefault(); if (!busy) onConfirm && onConfirm(e.target.value); }
|
|
212
|
-
if (e.key === 'Escape') { e.preventDefault(); if (!busy) onCancel && onCancel(); }
|
|
213
|
-
}
|
|
214
|
-
}), rootsRow, modalError(error)].filter(Boolean),
|
|
215
|
-
actions: [
|
|
216
|
-
Btn({ onClick: onCancel, disabled: busy, children: cancelLabel }),
|
|
217
|
-
Btn({
|
|
218
|
-
primary: true,
|
|
219
|
-
disabled: busy,
|
|
220
|
-
// Read the live input value, not the closed-over `value` prop:
|
|
221
|
-
// consumers update their state in oninput without re-rendering
|
|
222
|
-
// (to avoid caret jump), so the prop is stale at click time.
|
|
223
|
-
onClick: (e) => {
|
|
224
|
-
if (!onConfirm) return;
|
|
225
|
-
const inp = e.currentTarget.closest('.ds-modal')?.querySelector('.ds-modal-input');
|
|
226
|
-
onConfirm(inp ? inp.value : value);
|
|
227
|
-
},
|
|
228
|
-
children: busy ? busyLabel : confirmLabel
|
|
229
|
-
})
|
|
230
|
-
]
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// CountdownDialog — a modal with a role=status line ticking down from
|
|
235
|
-
// `seconds` to 0 once per second, auto-firing onExpire at zero. Composes on
|
|
236
|
-
// top of the same Modal() shell ConfirmDialog/PromptDialog use, so it
|
|
237
|
-
// inherits Backdrop's focus-trap + Escape/backdrop-dismiss handling for free
|
|
238
|
-
// rather than reimplementing dialog plumbing.
|
|
239
|
-
export function CountdownDialog({ title = 'Are you sure?', message, seconds = 10, onExpire, actions } = {}) {
|
|
240
|
-
const startSeconds = Math.max(0, Math.floor(seconds));
|
|
241
|
-
return Modal({
|
|
242
|
-
onClose: undefined, // no implicit dismiss path unless the caller supplies one via `actions`
|
|
243
|
-
kind: 'small',
|
|
244
|
-
head: title,
|
|
245
|
-
body: [
|
|
246
|
-
message || '',
|
|
247
|
-
h('p', {
|
|
248
|
-
class: 'ds-countdown-status', role: 'status', 'aria-live': 'polite',
|
|
249
|
-
ref: (el) => {
|
|
250
|
-
if (!el || el._dsCountdownTimer) return;
|
|
251
|
-
let remaining = startSeconds;
|
|
252
|
-
const render = () => { el.textContent = remaining + (remaining === 1 ? ' second' : ' seconds') + ' remaining'; };
|
|
253
|
-
render();
|
|
254
|
-
el._dsCountdownTimer = setInterval(() => {
|
|
255
|
-
remaining -= 1;
|
|
256
|
-
if (remaining <= 0) {
|
|
257
|
-
clearInterval(el._dsCountdownTimer);
|
|
258
|
-
el._dsCountdownTimer = null;
|
|
259
|
-
remaining = 0;
|
|
260
|
-
render();
|
|
261
|
-
if (onExpire) onExpire();
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
render();
|
|
265
|
-
}, 1000);
|
|
266
|
-
},
|
|
267
|
-
}),
|
|
268
|
-
modalError(null),
|
|
269
|
-
].filter(Boolean),
|
|
270
|
-
actions: actions || [],
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export function FilePreviewMedia({ src, type = 'other', name } = {}) {
|
|
275
|
-
if (type === 'image') {
|
|
276
|
-
// Fit-to-pane (default) vs actual-size (1:1) toggle + a checkerboard so
|
|
277
|
-
// transparency reads. The toggle flips a class on the img in-place and
|
|
278
|
-
// reports the natural pixel dimensions into its own caption on load.
|
|
279
|
-
const onToggle = (e) => {
|
|
280
|
-
const wrap = e.currentTarget.closest('.ds-preview-media-wrap');
|
|
281
|
-
const img = wrap && wrap.querySelector('.ds-preview-media');
|
|
282
|
-
if (!img) return;
|
|
283
|
-
const actual = img.classList.toggle('is-actual');
|
|
284
|
-
e.currentTarget.textContent = actual ? 'fit to pane' : 'actual size';
|
|
285
|
-
};
|
|
286
|
-
const onLoad = (e) => {
|
|
287
|
-
const img = e.currentTarget;
|
|
288
|
-
const cap = img.closest('.ds-preview-media-wrap');
|
|
289
|
-
const dim = cap && cap.querySelector('.ds-preview-media-dim');
|
|
290
|
-
if (dim && img.naturalWidth) dim.textContent = img.naturalWidth + ' x ' + img.naturalHeight + ' px';
|
|
291
|
-
};
|
|
292
|
-
return h('div', { class: 'ds-preview-media-wrap' },
|
|
293
|
-
h('img', { class: 'ds-preview-media ds-preview-media-alpha', src, alt: name || '', onload: onLoad }),
|
|
294
|
-
h('div', { class: 'ds-preview-media-controls' },
|
|
295
|
-
h('span', { class: 'ds-preview-media-dim', 'aria-live': 'polite' }, ''),
|
|
296
|
-
h('button', { type: 'button', class: 'chat-code-copy', onclick: onToggle }, 'actual size')));
|
|
297
|
-
}
|
|
298
|
-
if (type === 'video') return h('video', { class: 'ds-preview-media', src, controls: true });
|
|
299
|
-
if (type === 'audio') return h('audio', { class: 'ds-preview-audio', src, controls: true });
|
|
300
|
-
return h('div', { class: 'ds-preview-fallback' },
|
|
301
|
-
h('span', { class: 'ds-preview-glyph', 'aria-hidden': 'true' }, Icon(fileGlyph(type))),
|
|
302
|
-
h('span', {}, 'no inline preview for ' + (type || 'this file'))
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// FilePreviewCode — the code/source pane. Two additive, opt-in affordances
|
|
307
|
-
// ported from pi-web's FileViewer (behavior only, not its React/SSE plumbing):
|
|
308
|
-
// wrap : host-controlled wrap-lines toggle. Pass `wrap` (current
|
|
309
|
-
// state) + `onWrapToggle` to show the control; omitted host
|
|
310
|
-
// keeps the old always-'pre' behavior (no regression).
|
|
311
|
-
// previewHtml : when the host has already rendered markdown/HTML to a safe
|
|
312
|
-
// HTML string (e.g. via markdown-cache.js's
|
|
313
|
-
// renderMarkdownCached, or a sanitized srcDoc for raw HTML
|
|
314
|
-
// files) it passes it here + `previewLabel` (defaults
|
|
315
|
-
// 'preview') to get a source/preview mode switcher, mirroring
|
|
316
|
-
// pi-web's DisplayMode tabs. This component never renders
|
|
317
|
-
// unsanitized markdown itself — that stays the host's job
|
|
318
|
-
// (chat.js already owns the sanitize+render pipeline).
|
|
319
|
-
export function FilePreviewCode({ content = '', lang, filename, wrap, onWrapToggle, previewHtml, previewLabel = 'preview', mode, onModeChange } = {}) {
|
|
320
|
-
// A filename/lang header matching the chat CodeNode's .chat-code-head, plus
|
|
321
|
-
// the same copy control (chat A1/A2 ship this run, so preview matches for
|
|
322
|
-
// full cross-surface consistency).
|
|
323
|
-
const onCopy = (e) => {
|
|
324
|
-
const btn = e.currentTarget;
|
|
325
|
-
const done = () => { btn.textContent = 'copied'; btn.classList.add('is-copied'); setTimeout(() => { btn.textContent = 'copy'; btn.classList.remove('is-copied'); }, 1600); };
|
|
326
|
-
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(content).then(done).catch(() => {});
|
|
327
|
-
else { try { const t = document.createElement('textarea'); t.value = content; document.body.appendChild(t); t.select(); document.execCommand('copy'); document.body.removeChild(t); done(); } catch { /* swallow: legacy execCommand copy fallback unsupported, nothing more to try */ } }
|
|
328
|
-
};
|
|
329
|
-
const hasPreview = previewHtml != null && onModeChange;
|
|
330
|
-
const activeMode = hasPreview ? (mode || 'source') : 'source';
|
|
331
|
-
const modeSwitch = hasPreview ? h('div', { class: 'ds-preview-mode-switch', role: 'group', 'aria-label': 'file view mode' },
|
|
332
|
-
h('button', { type: 'button', class: 'ds-preview-mode-btn' + (activeMode === 'source' ? ' active' : ''),
|
|
333
|
-
'aria-pressed': activeMode === 'source' ? 'true' : 'false', onclick: () => onModeChange('source') }, 'source'),
|
|
334
|
-
h('button', { type: 'button', class: 'ds-preview-mode-btn' + (activeMode === 'preview' ? ' active' : ''),
|
|
335
|
-
'aria-pressed': activeMode === 'preview' ? 'true' : 'false', onclick: () => onModeChange('preview') }, previewLabel)
|
|
336
|
-
) : null;
|
|
337
|
-
const wrapCtl = (onWrapToggle && activeMode === 'source') ? h('button', {
|
|
338
|
-
type: 'button', class: 'chat-code-copy ds-preview-wrap-toggle' + (wrap ? ' active' : ''),
|
|
339
|
-
title: wrap ? 'disable word wrap' : 'enable word wrap',
|
|
340
|
-
'aria-label': wrap ? 'disable word wrap' : 'enable word wrap',
|
|
341
|
-
'aria-pressed': wrap ? 'true' : 'false',
|
|
342
|
-
onclick: () => onWrapToggle(!wrap),
|
|
343
|
-
}, 'wrap') : null;
|
|
344
|
-
return h('div', { class: 'ds-preview-code-wrap' },
|
|
345
|
-
h('div', { class: 'chat-code-head ds-preview-code-head' },
|
|
346
|
-
h('span', { class: 'lang' }, lang || 'text'),
|
|
347
|
-
filename ? h('span', { class: 'name' }, filename) : null,
|
|
348
|
-
h('span', { class: 'spread' }),
|
|
349
|
-
modeSwitch,
|
|
350
|
-
wrapCtl,
|
|
351
|
-
h('button', { type: 'button', class: 'chat-code-copy chat-code-copy-head', 'aria-label': 'copy code', onclick: onCopy }, 'copy')),
|
|
352
|
-
activeMode === 'preview'
|
|
353
|
-
// webjsx has no innerHTML prop — set it imperatively via ref, same
|
|
354
|
-
// pattern as chat-message-parts.js/community.js. `previewHtml` is
|
|
355
|
-
// the HOST's already-sanitized HTML (e.g. via markdown-cache.js's
|
|
356
|
-
// renderMarkdownCached, which owns its own sanitize step); this
|
|
357
|
-
// component never sanitizes or fetches on its own.
|
|
358
|
-
? h('div', { class: 'ds-preview-html', ref: (el) => { if (el) el.innerHTML = previewHtml; } })
|
|
359
|
-
: codeBody({ content, lang, wrap })
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// The code body: a non-selectable line-number gutter + the highlighted code.
|
|
364
|
-
// A ref triggers Prism over the <code> after mount (the bundle only auto-runs
|
|
365
|
-
// Prism in the chat path), so the file preview is token-colored like Claude
|
|
366
|
-
// Code's file pane. lineNumbers defaults on for code, off for plaintext.
|
|
367
|
-
function codeBody({ content = '', lang, wrap = false } = {}) {
|
|
368
|
-
const wantGutter = !!lang;
|
|
369
|
-
const lineCount = content ? content.split('\n').length : 1;
|
|
370
|
-
const gutter = wantGutter
|
|
371
|
-
? h('div', { class: 'ds-preview-gutter', 'aria-hidden': 'true' },
|
|
372
|
-
Array.from({ length: lineCount }, (_, i) => String(i + 1)).join('\n'))
|
|
373
|
-
: null;
|
|
374
|
-
const highlightRef = (el) => {
|
|
375
|
-
if (!el) return;
|
|
376
|
-
try { highlightAllUnder(el); } catch { /* swallow: syntax highlighting is a progressive enhancement, plain code still renders */ }
|
|
377
|
-
};
|
|
378
|
-
// wrap: ported from pi-web's wrapLines toggle — pre-wrap + anywhere-break
|
|
379
|
-
// instead of the default horizontal-scroll 'pre', for long unbroken lines
|
|
380
|
-
// (minified JS, long log lines) that are easier to read wrapped.
|
|
381
|
-
return h('pre', { class: 'ds-preview-code' + (lang ? ' lang-' + lang : '') + (wantGutter ? ' has-gutter' : '') + (wrap ? ' is-wrapped' : ''), ref: highlightRef },
|
|
382
|
-
gutter,
|
|
383
|
-
h('code', { class: lang ? 'language-' + lang : '' }, content));
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export function FilePreviewText({ content = '', truncated } = {}) {
|
|
387
|
-
return h('pre', { class: 'ds-preview-text' },
|
|
388
|
-
h('code', {}, content),
|
|
389
|
-
truncated ? h('div', { class: 'ds-preview-truncated' }, '… (truncated)') : null
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Shared preview-head children for both the modal FileViewer and the inline
|
|
394
|
-
// FilePreviewPane: name + meta + prev/next stepper + download + close. ASCII
|
|
395
|
-
// prev/next words (no glyph arrows). onPrev/onNext are omitted when there is no
|
|
396
|
-
// previewable neighbour in that direction.
|
|
397
|
-
function previewHead({ file, onClose, onAction, onPrev, onNext } = {}) {
|
|
398
|
-
const meta = [file.type, file.size != null ? fmtFileSize(file.size) : null, file.modified || null]
|
|
399
|
-
.filter(Boolean).join(' · ');
|
|
400
|
-
return [
|
|
401
|
-
h('span', { class: 'ds-preview-name', title: file.path || file.name || '' }, file.name || ''),
|
|
402
|
-
h('span', { class: 'ds-preview-meta' }, meta),
|
|
403
|
-
h('span', { class: 'ds-preview-actions' },
|
|
404
|
-
(onPrev || onNext) ? h('span', { class: 'ds-preview-step', role: 'group', 'aria-label': 'step files' },
|
|
405
|
-
h('button', { class: 'ds-file-act', title: 'previous file', 'aria-label': 'previous file', disabled: onPrev ? null : true, onclick: () => onPrev && onPrev() }, 'prev'),
|
|
406
|
-
h('button', { class: 'ds-file-act', title: 'next file', 'aria-label': 'next file', disabled: onNext ? null : true, onclick: () => onNext && onNext() }, 'next')) : null,
|
|
407
|
-
onAction ? h('button', { class: 'ds-file-act', title: 'download', 'aria-label': 'download', onclick: () => onAction('download') }, Icon('arrow-down')) : null,
|
|
408
|
-
onClose ? h('button', { class: 'ds-file-act', title: 'close', 'aria-label': 'close', onclick: onClose }, Icon('x')) : null
|
|
409
|
-
)
|
|
410
|
-
];
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// ArrowLeft/Right step the preview when focus is inside it (both pane + modal).
|
|
414
|
-
function previewKeyNav(onPrev, onNext) {
|
|
415
|
-
return (e) => {
|
|
416
|
-
if (e.key === 'ArrowLeft' && onPrev) { e.preventDefault(); onPrev(); }
|
|
417
|
-
else if (e.key === 'ArrowRight' && onNext) { e.preventDefault(); onNext(); }
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Touch stepping: horizontal swipe on the preview body steps prev/next. Skips
|
|
422
|
-
// when the gesture starts inside a horizontally-scrollable child (code <pre>)
|
|
423
|
-
// so panning wide code never flips files.
|
|
424
|
-
function previewSwipe(onPrev, onNext) {
|
|
425
|
-
if (!onPrev && !onNext) return {};
|
|
426
|
-
let sx = null, sy = null;
|
|
427
|
-
return {
|
|
428
|
-
onpointerdown: (e) => {
|
|
429
|
-
const scroller = e.target.closest && e.target.closest('pre');
|
|
430
|
-
if (scroller && scroller.scrollWidth > scroller.clientWidth) { sx = null; return; }
|
|
431
|
-
sx = e.clientX; sy = e.clientY;
|
|
432
|
-
},
|
|
433
|
-
onpointerup: (e) => {
|
|
434
|
-
if (sx == null) return;
|
|
435
|
-
const dx = e.clientX - sx, dy = e.clientY - sy;
|
|
436
|
-
sx = null;
|
|
437
|
-
if (Math.abs(dx) < 48 || Math.abs(dy) > Math.abs(dx)) return;
|
|
438
|
-
if (dx < 0 && onNext) onNext();
|
|
439
|
-
else if (dx > 0 && onPrev) onPrev();
|
|
440
|
-
},
|
|
441
|
-
onpointercancel: () => { sx = null; },
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
export function FileViewer({ file, body, onClose, onAction, onPrev, onNext } = {}) {
|
|
446
|
-
if (!file) return null;
|
|
447
|
-
const keyNav = previewKeyNav(onPrev, onNext);
|
|
448
|
-
return Modal({
|
|
449
|
-
onClose,
|
|
450
|
-
kind: 'preview',
|
|
451
|
-
headClass: 'ds-preview-head',
|
|
452
|
-
headAttrs: { 'data-file-type': file.type || 'other', onkeydown: keyNav },
|
|
453
|
-
head: previewHead({ file, onClose, onAction, onPrev, onNext }),
|
|
454
|
-
bodyClass: 'ds-preview-body',
|
|
455
|
-
bodyAttrs: { 'data-file-type': file.type || 'other', onkeydown: keyNav, ...previewSwipe(onPrev, onNext) },
|
|
456
|
-
body: Array.isArray(body) ? body : [body],
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// FilePreviewPane — the SAME preview, but as a persistent, non-modal side pane
|
|
461
|
-
// for the WorkspaceShell's pane slot (the split-view, claude-Desktop file-pane
|
|
462
|
-
// feel). Distinct from the overlay FileViewer (kept as the <900px fallback).
|
|
463
|
-
// Not focus-trapped (it is not modal); ArrowLeft/Right step files when focused.
|
|
464
|
-
export function FilePreviewPane({ file, body, onClose, onAction, onPrev, onNext } = {}) {
|
|
465
|
-
if (!file) {
|
|
466
|
-
return h('div', { class: 'ds-preview-pane ds-preview-pane-empty', role: 'status' },
|
|
467
|
-
h('span', {}, 'Select a file to preview'));
|
|
468
|
-
}
|
|
469
|
-
return h('div', { class: 'ds-preview-pane', role: 'region', 'aria-label': 'file preview: ' + (file.name || ''),
|
|
470
|
-
tabindex: '0', onkeydown: previewKeyNav(onPrev, onNext) },
|
|
471
|
-
h('div', { class: 'ds-preview-head', 'data-file-type': file.type || 'other' },
|
|
472
|
-
...previewHead({ file, onClose, onAction, onPrev, onNext })),
|
|
473
|
-
h('div', { class: 'ds-preview-body', 'data-file-type': file.type || 'other' },
|
|
474
|
-
...(Array.isArray(body) ? body : [body]))
|
|
475
|
-
);
|
|
476
|
-
}
|
|
2
|
+
//
|
|
3
|
+
// This module is a barrel: every component lives in a single-responsibility
|
|
4
|
+
// submodule under ./files-modals/, and the public export surface here is
|
|
5
|
+
// unchanged — no consumer import needs to move.
|
|
6
|
+
|
|
7
|
+
import { ConfirmDialog, PromptDialog, CountdownDialog } from './files-modals/dialogs.js';
|
|
8
|
+
import { FilePreviewMedia, FilePreviewCode, FilePreviewText } from './files-modals/preview-bodies.js';
|
|
9
|
+
import { FileViewer, FilePreviewPane } from './files-modals/preview-containers.js';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
ConfirmDialog, PromptDialog, CountdownDialog,
|
|
13
|
+
FilePreviewMedia, FilePreviewCode, FilePreviewText,
|
|
14
|
+
FileViewer, FilePreviewPane,
|
|
15
|
+
};
|