anentrypoint-design 0.0.206 → 0.0.207
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/chat.css +139 -2
- package/dist/247420.css +139 -2
- package/dist/247420.js +14 -12
- package/package.json +1 -1
- package/src/components/agent-chat.js +5 -1
- package/src/components/chat.js +26 -3
- package/src/components/files-modals.js +45 -3
- package/src/components/files.js +2 -2
- package/src/components/sessions.js +131 -50
- package/src/components/shell.js +14 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.207",
|
|
4
4
|
"description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/247420.js",
|
|
@@ -252,7 +252,11 @@ export function AgentChat(props = {}) {
|
|
|
252
252
|
return ChatMessage({
|
|
253
253
|
key: m.id || String(i),
|
|
254
254
|
who: isAssistant ? 'them' : 'you',
|
|
255
|
-
|
|
255
|
+
// Claude-Code-web layout: flat full-width turns (no avatar disc, no colored
|
|
256
|
+
// bubble), distinguished by a role label + a faint assistant background.
|
|
257
|
+
// aicat is left OFF so the mascot tint never reaches the agent surface.
|
|
258
|
+
flat: true,
|
|
259
|
+
aicat: false,
|
|
256
260
|
// A stable per-agent product mark (host passes a small line-SVG via
|
|
257
261
|
// `avatar`) instead of a per-agent letter initial that shifts identity.
|
|
258
262
|
avatar: isAssistant ? (m.avatar != null ? m.avatar : avatar) : undefined,
|
package/src/components/chat.js
CHANGED
|
@@ -128,6 +128,18 @@ export function injectCodeCopy(container) {
|
|
|
128
128
|
shell.className = 'chat-code-block';
|
|
129
129
|
pre.parentNode.insertBefore(shell, pre);
|
|
130
130
|
shell.appendChild(pre);
|
|
131
|
+
// Surface the fenced language as a small header tab (claude.ai/code
|
|
132
|
+
// shows the language on every block, not just the structured CodeNode).
|
|
133
|
+
// The highlighter sets language-xx / lang-xx on the inner <code>.
|
|
134
|
+
const codeEl = pre.querySelector('code');
|
|
135
|
+
const langCls = codeEl && (codeEl.className || '').match(/(?:language|lang)-([a-z0-9+#]+)/i);
|
|
136
|
+
if (langCls && langCls[1]) {
|
|
137
|
+
const lang = document.createElement('span');
|
|
138
|
+
lang.className = 'chat-code-lang';
|
|
139
|
+
lang.setAttribute('aria-hidden', 'true');
|
|
140
|
+
lang.textContent = langCls[1];
|
|
141
|
+
shell.appendChild(lang);
|
|
142
|
+
}
|
|
131
143
|
const btn = document.createElement('button');
|
|
132
144
|
btn.type = 'button';
|
|
133
145
|
btn.className = 'chat-code-copy';
|
|
@@ -289,7 +301,7 @@ function renderPart(p, key) {
|
|
|
289
301
|
return node;
|
|
290
302
|
}
|
|
291
303
|
|
|
292
|
-
export function ChatMessage({ role, who = 'them', avatar, text, parts, time, typing, key, aicat, reactions, receipt, name, streaming, actions, incomplete, stopped }) {
|
|
304
|
+
export function ChatMessage({ role, who = 'them', avatar, text, parts, time, typing, key, aicat, reactions, receipt, name, streaming, actions, incomplete, stopped, flat }) {
|
|
293
305
|
_stats.messages += 1;
|
|
294
306
|
// Support legacy 'who' prop, prefer 'role' with mapping:
|
|
295
307
|
// 'user' -> 'you' (right-aligned, accent bubble)
|
|
@@ -304,7 +316,11 @@ export function ChatMessage({ role, who = 'them', avatar, text, parts, time, typ
|
|
|
304
316
|
: role)
|
|
305
317
|
: who;
|
|
306
318
|
const isCentered = resolvedWho === 'system' || resolvedWho === 'tool' || resolvedWho === 'thinking';
|
|
307
|
-
|
|
319
|
+
// Flat layout (Claude-Code-web): full-width, avatar-less turns with a role
|
|
320
|
+
// label above the content and a faint assistant background, instead of the
|
|
321
|
+
// messenger avatar-disc + colored-bubble layout (kept for the chat demo).
|
|
322
|
+
const isFlat = flat && !isCentered;
|
|
323
|
+
const cls = 'chat-msg ' + resolvedWho + (aicat && resolvedWho === 'them' ? ' aicat' : '') + (isCentered ? ' centered' : '') + (isFlat ? ' chat-msg-flat' : '');
|
|
308
324
|
const fallbackAvatar = avatar != null
|
|
309
325
|
? avatar
|
|
310
326
|
: (resolvedWho === 'you' ? 'u' : (name ? String(name).trim().charAt(0).toUpperCase() || '?' : '?'));
|
|
@@ -355,11 +371,18 @@ export function ChatMessage({ role, who = 'them', avatar, text, parts, time, typ
|
|
|
355
371
|
}, a.icon ? Icon(a.icon, { size: 14 }) : null,
|
|
356
372
|
a.label ? h('span', { class: 'chat-msg-action-label' }, a.label) : null)))
|
|
357
373
|
: null;
|
|
358
|
-
|
|
374
|
+
// Flat layout leads the turn with a small role label (You / agent name)
|
|
375
|
+
// above the content, the way claude.ai/code titles each turn.
|
|
376
|
+
const roleLabel = isFlat
|
|
377
|
+
? h('div', { class: 'chat-role', key: '_role' }, resolvedWho === 'you' ? 'You' : (name || 'Assistant'))
|
|
378
|
+
: null;
|
|
379
|
+
const stack = h('div', { class: 'chat-stack' }, roleLabel, ...bodyNodes, reactionRow, actionRow, meta);
|
|
359
380
|
// Centered roles (system/tool/thinking) skip the avatar column entirely so
|
|
360
381
|
// the bubble owns the full row — the chrome reads as out-of-band signal,
|
|
361
382
|
// not a participant turn.
|
|
362
383
|
if (isCentered) return h('div', { key, class: cls }, stack);
|
|
384
|
+
// Flat turns drop the avatar column entirely (full-width content).
|
|
385
|
+
if (isFlat) return h('div', { key, class: cls }, stack);
|
|
363
386
|
return h('div', { key, class: cls }, resolvedWho === 'you' ? stack : av, resolvedWho === 'you' ? av : stack);
|
|
364
387
|
}
|
|
365
388
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
4
4
|
import { Btn, Icon } from './shell.js';
|
|
5
5
|
import { fileGlyph, fmtFileSize } from './files.js';
|
|
6
|
+
import { highlightAllUnder } from '../highlight.js';
|
|
6
7
|
const h = webjsx.createElement;
|
|
7
8
|
|
|
8
9
|
// Monotonic id source for aria-labelledby wiring between a modal and its head.
|
|
@@ -205,7 +206,29 @@ export function PromptDialog({ title = 'name', value = '', placeholder = '', con
|
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
export function FilePreviewMedia({ src, type = 'other', name } = {}) {
|
|
208
|
-
if (type === 'image')
|
|
209
|
+
if (type === 'image') {
|
|
210
|
+
// Fit-to-pane (default) vs actual-size (1:1) toggle + a checkerboard so
|
|
211
|
+
// transparency reads. The toggle flips a class on the img in-place and
|
|
212
|
+
// reports the natural pixel dimensions into its own caption on load.
|
|
213
|
+
const onToggle = (e) => {
|
|
214
|
+
const wrap = e.currentTarget.closest('.ds-preview-media-wrap');
|
|
215
|
+
const img = wrap && wrap.querySelector('.ds-preview-media');
|
|
216
|
+
if (!img) return;
|
|
217
|
+
const actual = img.classList.toggle('is-actual');
|
|
218
|
+
e.currentTarget.textContent = actual ? 'fit to pane' : 'actual size';
|
|
219
|
+
};
|
|
220
|
+
const onLoad = (e) => {
|
|
221
|
+
const img = e.currentTarget;
|
|
222
|
+
const cap = img.closest('.ds-preview-media-wrap');
|
|
223
|
+
const dim = cap && cap.querySelector('.ds-preview-media-dim');
|
|
224
|
+
if (dim && img.naturalWidth) dim.textContent = img.naturalWidth + ' x ' + img.naturalHeight + ' px';
|
|
225
|
+
};
|
|
226
|
+
return h('div', { class: 'ds-preview-media-wrap' },
|
|
227
|
+
h('img', { class: 'ds-preview-media ds-preview-media-alpha', src, alt: name || '', onload: onLoad }),
|
|
228
|
+
h('div', { class: 'ds-preview-media-controls' },
|
|
229
|
+
h('span', { class: 'ds-preview-media-dim', 'aria-live': 'polite' }, ''),
|
|
230
|
+
h('button', { type: 'button', class: 'chat-code-copy', onclick: onToggle }, 'actual size')));
|
|
231
|
+
}
|
|
209
232
|
if (type === 'video') return h('video', { class: 'ds-preview-media', src, controls: true });
|
|
210
233
|
if (type === 'audio') return h('audio', { class: 'ds-preview-audio', src, controls: true });
|
|
211
234
|
return h('div', { class: 'ds-preview-fallback' },
|
|
@@ -230,11 +253,30 @@ export function FilePreviewCode({ content = '', lang, filename } = {}) {
|
|
|
230
253
|
filename ? h('span', { class: 'name' }, filename) : null,
|
|
231
254
|
h('span', { class: 'spread' }),
|
|
232
255
|
h('button', { type: 'button', class: 'chat-code-copy chat-code-copy-head', 'aria-label': 'copy code', onclick: onCopy }, 'copy')),
|
|
233
|
-
|
|
234
|
-
h('code', { class: lang ? 'language-' + lang : '' }, content))
|
|
256
|
+
codeBody({ content, lang })
|
|
235
257
|
);
|
|
236
258
|
}
|
|
237
259
|
|
|
260
|
+
// The code body: a non-selectable line-number gutter + the highlighted code.
|
|
261
|
+
// A ref triggers Prism over the <code> after mount (the bundle only auto-runs
|
|
262
|
+
// Prism in the chat path), so the file preview is token-colored like Claude
|
|
263
|
+
// Code's file pane. lineNumbers defaults on for code, off for plaintext.
|
|
264
|
+
function codeBody({ content = '', lang } = {}) {
|
|
265
|
+
const wantGutter = !!lang;
|
|
266
|
+
const lineCount = content ? content.split('\n').length : 1;
|
|
267
|
+
const gutter = wantGutter
|
|
268
|
+
? h('div', { class: 'ds-preview-gutter', 'aria-hidden': 'true' },
|
|
269
|
+
Array.from({ length: lineCount }, (_, i) => String(i + 1)).join('\n'))
|
|
270
|
+
: null;
|
|
271
|
+
const highlightRef = (el) => {
|
|
272
|
+
if (!el) return;
|
|
273
|
+
try { highlightAllUnder(el); } catch {}
|
|
274
|
+
};
|
|
275
|
+
return h('pre', { class: 'ds-preview-code' + (lang ? ' lang-' + lang : '') + (wantGutter ? ' has-gutter' : ''), ref: highlightRef },
|
|
276
|
+
gutter,
|
|
277
|
+
h('code', { class: lang ? 'language-' + lang : '' }, content));
|
|
278
|
+
}
|
|
279
|
+
|
|
238
280
|
export function FilePreviewText({ content = '', truncated } = {}) {
|
|
239
281
|
return h('pre', { class: 'ds-preview-text' },
|
|
240
282
|
h('code', {}, content),
|
package/src/components/files.js
CHANGED
|
@@ -10,8 +10,8 @@ const FILE_GRID_MIN_COL = '240px';
|
|
|
10
10
|
|
|
11
11
|
const FILE_TYPES = ['dir', 'image', 'video', 'audio', 'code', 'text', 'archive', 'document', 'symlink', 'other'];
|
|
12
12
|
const TYPE_ICON = {
|
|
13
|
-
dir: '
|
|
14
|
-
text: 'file-text', archive: 'file-zip', document: 'file-text', symlink: '
|
|
13
|
+
dir: 'folder', image: 'file-image', video: 'file-video', audio: 'file-audio', code: 'file-code',
|
|
14
|
+
text: 'file-text', archive: 'file-zip', document: 'file-text', symlink: 'link', other: 'file'
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
const TYPE_LABELS = {
|
|
@@ -69,7 +69,13 @@ export function ConversationList({ sessions = [], selected, groups, search, capt
|
|
|
69
69
|
// are uniformly keyed; non-row states render a single unkeyed status line.
|
|
70
70
|
let inner;
|
|
71
71
|
if (loading) {
|
|
72
|
-
|
|
72
|
+
// Shape-matched skeleton rows during the cold ccsniff index walk (the rail
|
|
73
|
+
// showed a bare line before) - Claude-Desktop skeletons its sidebar on load.
|
|
74
|
+
inner = [
|
|
75
|
+
h('div', { key: 'st', class: 'ds-session-state', role: 'status', 'aria-live': 'polite' }, loadingText),
|
|
76
|
+
...Array.from({ length: 5 }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-session-row-skeleton', 'aria-hidden': 'true' },
|
|
77
|
+
h('div', { class: 'ds-skel ds-skel-title' }), h('div', { class: 'ds-skel ds-skel-meta' }))),
|
|
78
|
+
];
|
|
73
79
|
} else if (error) {
|
|
74
80
|
inner = [h('div', { key: 'st', class: 'ds-session-state ds-session-state-error', role: 'status' }, String(error))];
|
|
75
81
|
} else if (!sessions.length) {
|
|
@@ -161,41 +167,51 @@ export function SessionCard({ session = {}, onStop, onOpen, onView, active = fal
|
|
|
161
167
|
// last-activity time and the current tool so a card shows MOTION, not just a
|
|
162
168
|
// start offset. Both are middot-joined (kept product separator).
|
|
163
169
|
const elapsedText = s.elapsedMs != null ? fmtDuration(s.elapsedMs) : (s.elapsed != null ? s.elapsed : null);
|
|
164
|
-
|
|
170
|
+
// At-a-glance cost/usage (the prompt's named command-center signal). Null-safe:
|
|
171
|
+
// sessions with no cost source (external tally rows) simply omit the segment.
|
|
172
|
+
const tokText = s.tokens != null ? (typeof s.tokens === 'number' ? s.tokens.toLocaleString() : s.tokens) + ' tok' : null;
|
|
173
|
+
const costText = s.cost != null ? (typeof s.cost === 'number' ? '$' + s.cost.toFixed(4) : String(s.cost)) : null;
|
|
174
|
+
const statBits = [elapsedText, s.counter != null ? s.counter : null, tokText, costText].filter((x) => x != null && x !== '');
|
|
165
175
|
const activityBits = [
|
|
166
176
|
s.currentTool ? 'running: ' + s.currentTool : null,
|
|
167
177
|
s.lastActivity ? 'last ' + s.lastActivity : null,
|
|
168
178
|
].filter(Boolean);
|
|
169
|
-
const cls = 'ds-dash-card is-' + st + (active ? ' is-active' : '') + (selected ? ' is-selected' : '') + (s.external ? ' is-external' : '');
|
|
179
|
+
const cls = 'ds-dash-card is-' + st + (active ? ' is-active' : '') + (selected ? ' is-selected' : '') + (s.external ? ' is-external' : '') + (s.isNew ? ' is-new' : '');
|
|
180
|
+
// EVERY children array is filter(Boolean)'d: webjsx applyDiff crashes
|
|
181
|
+
// (reading 'key') on a bare null among VElement siblings, so a null cwd /
|
|
182
|
+
// model / external flag must never reach a positional child slot.
|
|
183
|
+
const head = h('div', { class: 'ds-dash-card-head' }, ...[
|
|
184
|
+
selectable ? h('button', {
|
|
185
|
+
type: 'button', class: 'ds-dash-select', role: 'checkbox',
|
|
186
|
+
'aria-checked': selected ? 'true' : 'false',
|
|
187
|
+
'aria-label': (selected ? 'deselect' : 'select') + ' session ' + (s.title || s.agent || s.sid),
|
|
188
|
+
onclick: () => onToggleSelect && onToggleSelect(s),
|
|
189
|
+
}, selected ? '[x]' : '[ ]') : null,
|
|
190
|
+
h('span', { class: 'status-dot-disc ' + STATUS_DISC[st], 'aria-hidden': 'true' }),
|
|
191
|
+
h('span', { class: 'ds-dash-status is-' + st }, STATUS_WORD[st]),
|
|
192
|
+
s.external ? h('span', { class: 'ds-dash-external' }, 'external') : null,
|
|
193
|
+
h('span', { class: 'ds-dash-agent', title: s.agent || null }, s.agent || 'agent'),
|
|
194
|
+
s.model ? h('span', { class: 'ds-dash-model', title: s.model }, s.model) : null,
|
|
195
|
+
].filter(Boolean));
|
|
196
|
+
const meta = h('div', { class: 'ds-dash-meta' }, ...[
|
|
197
|
+
s.cwd ? h('span', { class: 'ds-dash-cwd', title: s.cwd }, s.cwd) : null,
|
|
198
|
+
statBits.length ? h('span', { class: 'ds-dash-stat' }, statBits.join(' · ')) : null,
|
|
199
|
+
activityBits.length ? h('span', { class: 'ds-dash-activity' }, activityBits.join(' · ')) : null,
|
|
200
|
+
].filter(Boolean));
|
|
201
|
+
const actions = h('div', { class: 'ds-dash-actions', role: 'group', 'aria-label': 'session actions' }, ...[
|
|
202
|
+
onOpen ? Btn({ key: 'open', primary: true, 'aria-label': 'open session', onClick: () => onOpen(s),
|
|
203
|
+
children: [Icon('external-link', { size: 14 }), h('span', {}, 'open')] }) : null,
|
|
204
|
+
onView ? Btn({ key: 'view', 'aria-label': s.external ? 'open in history' : 'view events', onClick: () => onView(s),
|
|
205
|
+
children: [Icon('file-text', { size: 14 }), h('span', {}, s.external ? 'history' : 'events')] }) : null,
|
|
206
|
+
(onStop && !s.external) ? Btn({ key: 'stop', danger: true, disabled: !!s.stopping, 'aria-label': 'stop session',
|
|
207
|
+
onClick: () => !s.stopping && onStop(s),
|
|
208
|
+
children: [Icon('square', { size: 14 }), h('span', {}, s.stopping ? 'stopping…' : 'stop')] }) : null,
|
|
209
|
+
].filter(Boolean));
|
|
170
210
|
return h('div', { class: cls, role: 'group', 'aria-label': 'session ' + (s.title || s.agent || s.sid), 'aria-current': active ? 'true' : null },
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
type: 'button', class: 'ds-dash-select', role: 'checkbox',
|
|
176
|
-
'aria-checked': selected ? 'true' : 'false',
|
|
177
|
-
'aria-label': (selected ? 'deselect' : 'select') + ' session ' + (s.title || s.agent || s.sid),
|
|
178
|
-
onclick: () => onToggleSelect && onToggleSelect(s),
|
|
179
|
-
}, selected ? '[x]' : '[ ]') : null,
|
|
180
|
-
h('span', { class: 'status-dot-disc ' + STATUS_DISC[st], 'aria-hidden': 'true' }),
|
|
181
|
-
// Status is words + the disc, never colour alone (WCAG 1.4.1): the disc is
|
|
182
|
-
// aria-hidden, so the visible/AT status word carries the state.
|
|
183
|
-
h('span', { class: 'ds-dash-status is-' + st }, STATUS_WORD[st]),
|
|
184
|
-
s.external ? h('span', { class: 'ds-dash-external' }, 'external') : null,
|
|
185
|
-
h('span', { class: 'ds-dash-agent', title: s.agent || null }, s.agent || 'agent'),
|
|
186
|
-
s.model ? h('span', { class: 'ds-dash-model', title: s.model }, s.model) : null),
|
|
187
|
-
h('div', { class: 'ds-dash-meta' },
|
|
188
|
-
s.cwd ? h('span', { class: 'ds-dash-cwd', title: s.cwd }, s.cwd) : null,
|
|
189
|
-
statBits.length ? h('span', { class: 'ds-dash-stat' }, statBits.join(' · ')) : null,
|
|
190
|
-
activityBits.length ? h('span', { class: 'ds-dash-activity' }, activityBits.join(' · ')) : null),
|
|
191
|
-
h('div', { class: 'ds-dash-actions', role: 'group', 'aria-label': 'session actions' },
|
|
192
|
-
// open and resume collapsed into one 'open' action (they both just reopen
|
|
193
|
-
// the session in chat); 'events' kept for the read-only event view.
|
|
194
|
-
onOpen ? Btn({ key: 'open', onClick: () => onOpen(s), children: 'open' }) : null,
|
|
195
|
-
onView ? Btn({ key: 'view', onClick: () => onView(s), children: s.external ? 'open in history' : 'events' }) : null,
|
|
196
|
-
// External sessions get no stop control: we own no process to kill.
|
|
197
|
-
(onStop && !s.external) ? Btn({ key: 'stop', danger: true, disabled: !!s.stopping,
|
|
198
|
-
onClick: () => !s.stopping && onStop(s), children: s.stopping ? 'stopping…' : 'stop' }) : null));
|
|
211
|
+
...[
|
|
212
|
+
s.title ? h('div', { class: 'ds-dash-title', title: s.title }, s.title) : null,
|
|
213
|
+
head, meta, actions,
|
|
214
|
+
].filter(Boolean));
|
|
199
215
|
}
|
|
200
216
|
|
|
201
217
|
// SessionDashboard — grid of SessionCards for ALL live sessions, managed at once.
|
|
@@ -226,7 +242,7 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
|
|
|
226
242
|
confirmingStopAll = false, confirmingStopSelected = false,
|
|
227
243
|
onArmStopAll, onArmStopSelected,
|
|
228
244
|
sort, filter, errorsOnly = false, onErrorsOnly,
|
|
229
|
-
selectable = false, selected, onToggleSelect,
|
|
245
|
+
selectable = false, selected, onToggleSelect, onSelectAll, onClearSelection,
|
|
230
246
|
activeSid, streamState,
|
|
231
247
|
emptyText = 'No live sessions', offline = false } = {}) {
|
|
232
248
|
if (offline) {
|
|
@@ -239,11 +255,37 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
|
|
|
239
255
|
const stoppingCount = sessions.filter((s) => s.stopping).length;
|
|
240
256
|
// The stream-state line always renders (even with zero sessions) so a
|
|
241
257
|
// connected-but-idle dashboard reads differently from an offline one.
|
|
258
|
+
// The stream line leads with a status disc so a connected dashboard visibly
|
|
259
|
+
// PULSES that it is listening (the command-center heartbeat), connecting/offline
|
|
260
|
+
// show a static disc. The disc is aria-hidden; the word carries the state.
|
|
261
|
+
const streamDisc = streamState
|
|
262
|
+
? 'status-dot-disc ' + (streamState === 'connected' ? 'status-dot-live'
|
|
263
|
+
: streamState === 'connecting' ? 'status-dot-connecting' : 'status-dot-error')
|
|
264
|
+
: null;
|
|
242
265
|
const streamLine = streamState
|
|
243
|
-
? h('span', {
|
|
266
|
+
? h('span', { key: 'stream', class: 'ds-dash-stream-disc' },
|
|
267
|
+
h('span', { class: streamDisc, 'aria-hidden': 'true' }),
|
|
268
|
+
h('span', { class: 'ds-dash-stream is-' + streamState, role: 'status', 'aria-live': 'polite' }, STREAM_WORD[streamState] || streamState))
|
|
269
|
+
: null;
|
|
270
|
+
// At-a-glance status breakdown for the command-center header.
|
|
271
|
+
const counts = sessions.reduce((a, s) => {
|
|
272
|
+
const k = s.status === 'error' ? 'error' : (s.status === 'stale' ? 'idle' : 'running');
|
|
273
|
+
a[k] = (a[k] || 0) + 1; return a;
|
|
274
|
+
}, {});
|
|
275
|
+
const breakdownSegs = [
|
|
276
|
+
counts.running ? { k: 'running', t: counts.running + ' running' } : null,
|
|
277
|
+
counts.idle ? { k: 'idle', t: counts.idle + ' idle' } : null,
|
|
278
|
+
counts.error ? { k: 'error', t: counts.error + ' error' + (counts.error === 1 ? '' : 's') } : null,
|
|
279
|
+
].filter(Boolean);
|
|
280
|
+
const breakdown = breakdownSegs.length
|
|
281
|
+
? h('span', { key: 'bd', class: 'ds-dash-breakdown', role: 'status', 'aria-live': 'polite' },
|
|
282
|
+
...breakdownSegs.flatMap((seg, i) => [
|
|
283
|
+
i ? h('span', { key: 'bsep' + i, class: 'ds-dash-breakdown-sep', 'aria-hidden': 'true' }, ' · ') : null,
|
|
284
|
+
h('span', { key: 'bseg' + i, class: 'seg is-' + seg.k }, seg.t),
|
|
285
|
+
].filter(Boolean)))
|
|
244
286
|
: null;
|
|
245
287
|
const toolbar = (sort || filter || onErrorsOnly)
|
|
246
|
-
? h('div', { class: 'ds-dash-toolbar', role: 'group', 'aria-label': 'sort and filter sessions' },
|
|
288
|
+
? h('div', { key: 'tb', class: 'ds-dash-toolbar', role: 'group', 'aria-label': 'sort and filter sessions' },
|
|
247
289
|
filter ? SearchInput({ key: 'filt', value: filter.value || '', label: filter.placeholder || 'Filter sessions', placeholder: filter.placeholder || 'Filter sessions', onInput: (v) => filter.onInput && filter.onInput(v) }) : null,
|
|
248
290
|
sort ? Select({ key: 'sort', value: sort.value || 'status', title: 'Sort sessions',
|
|
249
291
|
options: [
|
|
@@ -258,31 +300,70 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
|
|
|
258
300
|
if (!sessions.length) {
|
|
259
301
|
return h('div', { class: 'ds-dash' },
|
|
260
302
|
h('div', { class: 'ds-dash-header', role: 'group', 'aria-label': 'live session controls' },
|
|
261
|
-
h('span', { class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, '0 running'), streamLine),
|
|
303
|
+
...[h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, '0 running'), streamLine].filter(Boolean)),
|
|
262
304
|
h('div', { class: 'ds-dash-state', role: 'status' }, emptyText));
|
|
263
305
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
306
|
+
// Tri-state select-all over the selectable (non-external) sessions.
|
|
307
|
+
const selectableSids = sessions.filter((s) => !s.external).map((s) => s.sid);
|
|
308
|
+
const selOfVisible = selectableSids.filter((sid) => selSet.has(sid)).length;
|
|
309
|
+
const allState = selOfVisible === 0 ? 'false' : (selOfVisible === selectableSids.length ? 'true' : 'mixed');
|
|
310
|
+
const selectAllCtl = (selectable && onSelectAll && selectableSids.length)
|
|
311
|
+
? h('button', { key: 'selall', type: 'button', class: 'ds-dash-selectall', role: 'checkbox',
|
|
312
|
+
'aria-checked': allState, 'aria-label': allState === 'true' ? 'clear selection' : 'select all sessions',
|
|
313
|
+
onclick: () => (allState === 'true' && onClearSelection) ? onClearSelection() : onSelectAll(selectableSids) },
|
|
314
|
+
h('span', { 'aria-hidden': 'true' }, allState === 'true' ? '[x]' : allState === 'mixed' ? '[-]' : '[ ]'),
|
|
315
|
+
h('span', {}, 'all'))
|
|
316
|
+
: null;
|
|
317
|
+
const clearCtl = (selectable && selCount && onClearSelection)
|
|
318
|
+
? h('button', { key: 'selclr', type: 'button', class: 'ds-dash-clear', onclick: () => onClearSelection() }, 'clear')
|
|
319
|
+
: null;
|
|
320
|
+
const stopBtn = stoppingCount > 0 && (onStopSelected || onStopAll)
|
|
270
321
|
? Btn({ key: 'stopbusy', danger: true, disabled: true, children: 'stopping ' + stoppingCount + '…' })
|
|
271
322
|
: (selectable && selCount && onStopSelected
|
|
272
323
|
? (onArmStopSelected && !confirmingStopSelected
|
|
273
324
|
? Btn({ key: 'stopsel', danger: true, onClick: () => onArmStopSelected([...selSet]), children: 'stop selected' })
|
|
274
|
-
: Btn({ key: 'stopsel', danger: true, onClick: () => onStopSelected([...selSet]),
|
|
325
|
+
: Btn({ key: 'stopsel', danger: true, className: confirmingStopSelected ? 'is-armed' : null, onClick: () => onStopSelected([...selSet]),
|
|
275
326
|
children: confirmingStopSelected ? 'stop ' + selCount + ' sessions - press again' : 'stop selected' }))
|
|
276
327
|
: (onStopAll
|
|
277
328
|
? (onArmStopAll && !confirmingStopAll
|
|
278
329
|
? Btn({ key: 'stopall', danger: true, onClick: () => onArmStopAll(sessions), children: 'stop all' })
|
|
279
|
-
: Btn({ key: 'stopall', danger: true, onClick: () => onStopAll(sessions),
|
|
330
|
+
: Btn({ key: 'stopall', danger: true, className: confirmingStopAll ? 'is-armed' : null, onClick: () => onStopAll(sessions),
|
|
280
331
|
children: confirmingStopAll ? 'stop ' + sessions.length + ' sessions - press again' : 'stop all' }))
|
|
281
|
-
: null))
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
332
|
+
: null));
|
|
333
|
+
// Build header children as a filtered array: webjsx applyDiff crashes
|
|
334
|
+
// (reading 'key') when a bare null sits among keyed siblings, so never pass
|
|
335
|
+
// a conditional child positionally - filter it out first.
|
|
336
|
+
const headerKids = [
|
|
337
|
+
selectable && selCount
|
|
338
|
+
? h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, selCount + ' selected')
|
|
339
|
+
: (breakdown || h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, sessions.length + ' running')),
|
|
340
|
+
selectAllCtl, clearCtl, streamLine,
|
|
341
|
+
h('span', { key: 'spread', class: 'spread' }),
|
|
342
|
+
stopBtn, toolbar,
|
|
343
|
+
].filter(Boolean);
|
|
344
|
+
const header = h('div', { class: 'ds-dash-header', role: 'group', 'aria-label': 'live session controls' }, ...headerKids);
|
|
345
|
+
// Status-bucketed command center: when sorting by status (the default), the
|
|
346
|
+
// grid renders labelled sections (Errored / Running / Idle / External) so a
|
|
347
|
+
// pile of sessions reads as scannable groups. Other sorts collapse to one
|
|
348
|
+
// flat grid (the sort already orders them).
|
|
349
|
+
const grouped = !sort || !sort.value || sort.value === 'status';
|
|
350
|
+
const cardOf = (s) => h('div', { key: s.sid, role: 'listitem' },
|
|
351
|
+
SessionCard({ session: s, onStop, onOpen, onView, active: s.sid === activeSid,
|
|
352
|
+
selectable, selected: selSet.has(s.sid), onToggleSelect }));
|
|
353
|
+
let body;
|
|
354
|
+
if (grouped) {
|
|
355
|
+
const buckets = [
|
|
356
|
+
{ key: 'error', label: 'Errored', rows: sessions.filter((s) => !s.external && s.status === 'error') },
|
|
357
|
+
{ key: 'running', label: 'Running', rows: sessions.filter((s) => !s.external && s.status !== 'error' && s.status !== 'stale') },
|
|
358
|
+
{ key: 'idle', label: 'Idle', rows: sessions.filter((s) => !s.external && s.status === 'stale') },
|
|
359
|
+
{ key: 'external', label: 'External', rows: sessions.filter((s) => s.external) },
|
|
360
|
+
].filter((b) => b.rows.length);
|
|
361
|
+
body = h('div', { class: 'ds-dash-groups' },
|
|
362
|
+
...buckets.map((b) => h('div', { key: 'grp' + b.key, class: 'ds-dash-group', role: 'group', 'aria-label': b.label + ' sessions' },
|
|
363
|
+
h('div', { class: 'ds-dash-group-label' }, b.label + ' · ' + b.rows.length),
|
|
364
|
+
h('div', { class: 'ds-dash-grid', role: 'list', 'aria-label': b.label + ' sessions' }, ...b.rows.map(cardOf)))));
|
|
365
|
+
} else {
|
|
366
|
+
body = h('div', { class: 'ds-dash-grid', role: 'list', 'aria-label': 'live sessions' }, ...sessions.map(cardOf));
|
|
367
|
+
}
|
|
368
|
+
return h('div', { class: 'ds-dash' }, header, body);
|
|
288
369
|
}
|
package/src/components/shell.js
CHANGED
|
@@ -15,11 +15,12 @@ export function Chip({ tone = '', children }) {
|
|
|
15
15
|
return h('span', { class: 'chip' + (tone ? ' tone-' + tone : '') }, children);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function Btn({ href, variant = 'default', children, onClick, 'aria-label': ariaLabel, primary, ghost, danger, disabled }) {
|
|
18
|
+
export function Btn({ href, variant = 'default', children, onClick, 'aria-label': ariaLabel, primary, ghost, danger, disabled, className }) {
|
|
19
19
|
// Support legacy primary/ghost props for backward compatibility, but prefer variant
|
|
20
20
|
const resolvedVariant = variant !== 'default' ? variant : (primary ? 'primary' : (ghost ? 'ghost' : (danger ? 'danger' : 'default')));
|
|
21
21
|
const cls = (resolvedVariant === 'primary' ? 'btn-primary' : (resolvedVariant === 'ghost' ? 'btn-ghost' : (resolvedVariant === 'danger' ? 'btn-primary danger' : 'btn')))
|
|
22
|
-
+ (disabled ? ' is-disabled' : '')
|
|
22
|
+
+ (disabled ? ' is-disabled' : '')
|
|
23
|
+
+ (className ? ' ' + className : '');
|
|
23
24
|
const onclick = (e) => {
|
|
24
25
|
if (disabled) { e.preventDefault(); return; }
|
|
25
26
|
if (onClick) onClick(e);
|
|
@@ -29,6 +30,10 @@ export function Btn({ href, variant = 'default', children, onClick, 'aria-label'
|
|
|
29
30
|
// A real navigational href renders an anchor; everything else is an action
|
|
30
31
|
// button and renders a native <button> (correct semantics + keyboard
|
|
31
32
|
// activation for free, no role=button / href="#" scroll-jump hack).
|
|
33
|
+
// children may be a string OR an array of vnodes (e.g. icon + label); spread
|
|
34
|
+
// arrays so each vnode is a real child - passing the array as a single child
|
|
35
|
+
// produces a nested array webjsx applyDiff cannot key-diff (reading 'key').
|
|
36
|
+
const kids = Array.isArray(children) ? children : [children];
|
|
32
37
|
const isLink = href != null && href !== '' && href !== '#';
|
|
33
38
|
if (isLink) {
|
|
34
39
|
return h('a', {
|
|
@@ -37,14 +42,14 @@ export function Btn({ href, variant = 'default', children, onClick, 'aria-label'
|
|
|
37
42
|
'aria-disabled': disabled ? 'true' : null,
|
|
38
43
|
tabindex: disabled ? '-1' : null,
|
|
39
44
|
onclick
|
|
40
|
-
},
|
|
45
|
+
}, ...kids);
|
|
41
46
|
}
|
|
42
47
|
return h('button', {
|
|
43
48
|
type: 'button', class: cls,
|
|
44
49
|
disabled: disabled ? true : null,
|
|
45
50
|
'aria-label': ariaName,
|
|
46
51
|
onclick
|
|
47
|
-
},
|
|
52
|
+
}, ...kids);
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
export function IconButton({ icon, onClick, title, size = 'base', variant = 'ghost', disabled = false }) {
|
|
@@ -138,6 +143,9 @@ const ICON_PATHS = {
|
|
|
138
143
|
contrast: '<circle cx="12" cy="12" r="9"/><path d="M12 3v18a9 9 0 0 0 0-18z" fill="currentColor"/>',
|
|
139
144
|
// file-browser icons (replace folder/file emoji + arrow glyphs in fs apps)
|
|
140
145
|
folder: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
|
146
|
+
'folder-open': '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2H5l-2 9z"/><path d="M3 18l2-9h17l-2 9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
|
147
|
+
'file-image': '<path d="M6 3h8l5 5v13a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><path d="M14 3v5h5"/><circle cx="9.5" cy="12.5" r="1.5"/><path d="M18 19l-4-4-3 3-2-2-3 3"/>',
|
|
148
|
+
link: '<path d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1"/>',
|
|
141
149
|
upload: '<path d="M12 16V4M7 9l5-5 5 5"/><path d="M5 20h14"/>',
|
|
142
150
|
download: '<path d="M12 4v12M7 11l5 5 5-5"/><path d="M5 20h14"/>',
|
|
143
151
|
'corner-up-left': '<path d="M9 14 4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 5 5v6"/>',
|
|
@@ -363,7 +371,7 @@ function wsCollapsed(which, fallback) {
|
|
|
363
371
|
export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narrow,
|
|
364
372
|
railCollapsed = false, paneCollapsed = false,
|
|
365
373
|
railLabel = 'workspace navigation',
|
|
366
|
-
paneLabel = 'context', stableFrame = false } = {}) {
|
|
374
|
+
paneLabel = 'context', stableFrame = false, mainFlush = false } = {}) {
|
|
367
375
|
const hasSessions = Boolean(sessions);
|
|
368
376
|
const hasPane = Boolean(pane);
|
|
369
377
|
// Stable frame: keep the pane grid TRACK present even when this tab has no
|
|
@@ -423,7 +431,7 @@ export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narr
|
|
|
423
431
|
onclick: () => toggleWsDrawer('pane'),
|
|
424
432
|
}, Icon('page')) : null)
|
|
425
433
|
: null,
|
|
426
|
-
h('main', { class: 'ws-main' + (narrow ? ' narrow' : ''), id: 'ws-main', tabindex: '-1' },
|
|
434
|
+
h('main', { class: 'ws-main' + (narrow ? ' narrow' : '') + (mainFlush ? ' ws-main--flush' : ''), id: 'ws-main', tabindex: '-1' },
|
|
427
435
|
...(Array.isArray(main) ? main : [main])),
|
|
428
436
|
status || null),
|
|
429
437
|
// Optional right context pane with its own collapse toggle.
|