anentrypoint-design 0.0.206 → 0.0.208
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/app-shell.css +124 -12
- package/chat.css +139 -2
- package/dist/247420.css +263 -14
- package/dist/247420.js +15 -13
- 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 +164 -29
- package/src/components/sessions.js +131 -50
- package/src/components/shell.js +14 -6
- package/src/components.js +1 -1
|
@@ -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.
|
package/src/components.js
CHANGED
|
@@ -36,7 +36,7 @@ export { ContextPane } from './components/context-pane.js';
|
|
|
36
36
|
export {
|
|
37
37
|
fileGlyph, fmtFileSize,
|
|
38
38
|
FileIcon, FileRow, FileGrid, FileSkeleton, sortFiles, FileToolbar, RootsPicker,
|
|
39
|
-
DropZone, UploadProgress, EmptyState, BreadcrumbPath
|
|
39
|
+
DropZone, UploadProgress, EmptyState, BreadcrumbPath, BulkBar
|
|
40
40
|
} from './components/files.js';
|
|
41
41
|
|
|
42
42
|
export {
|