anentrypoint-design 0.0.211 → 0.0.213
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 +126 -66
- package/chat.css +136 -18
- package/colors_and_type.css +14 -9
- package/community.css +5 -1
- package/dist/247420.css +316 -103
- package/dist/247420.js +13 -13
- package/package.json +1 -1
- package/src/components/chat.js +15 -2
- package/src/components/content.js +7 -4
- package/src/components/files.js +93 -41
- package/src/components/interaction-primitives.js +7 -0
- package/src/components/sessions.js +10 -2
- package/src/components/shell.js +52 -18
- package/src/components.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.213",
|
|
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",
|
package/src/components/chat.js
CHANGED
|
@@ -215,6 +215,19 @@ function ToolCallNode(p) {
|
|
|
215
215
|
// collapse on success unless the caller explicitly overrides with open:true.
|
|
216
216
|
const defaultOpen = p.open != null ? !!p.open : (status === 'running' || status === 'error');
|
|
217
217
|
const iconName = status === 'running' ? 'refresh' : (status === 'error' ? 'warn' : 'check');
|
|
218
|
+
const copyText = (txt) => (e) => {
|
|
219
|
+
const b = e.currentTarget;
|
|
220
|
+
const done = () => {
|
|
221
|
+
b.textContent = 'copied';
|
|
222
|
+
b.classList.add('is-copied');
|
|
223
|
+
setTimeout(() => { b.textContent = 'copy'; b.classList.remove('is-copied'); }, 1600);
|
|
224
|
+
};
|
|
225
|
+
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(txt).then(done).catch(() => {});
|
|
226
|
+
else { try { const t = document.createElement('textarea'); t.value = txt; document.body.appendChild(t); t.select(); document.execCommand('copy'); document.body.removeChild(t); done(); } catch {} }
|
|
227
|
+
};
|
|
228
|
+
const sectionLabel = (text, txt) => h('div', { class: 'chat-tool-section-label' },
|
|
229
|
+
h('span', {}, text),
|
|
230
|
+
h('button', { type: 'button', class: 'chat-code-copy chat-tool-copy', 'aria-label': 'copy ' + text, onclick: copyText(txt) }, 'copy'));
|
|
218
231
|
return h('details', { class: 'chat-bubble chat-tool tool-' + status, open: defaultOpen },
|
|
219
232
|
h('summary', { class: 'chat-tool-head' },
|
|
220
233
|
h('span', { class: 'chat-tool-icon', 'aria-hidden': 'true' }, Icon(iconName, { size: 14 })),
|
|
@@ -224,10 +237,10 @@ function ToolCallNode(p) {
|
|
|
224
237
|
),
|
|
225
238
|
h('div', { class: 'chat-tool-body' },
|
|
226
239
|
h('div', { class: 'chat-tool-section' },
|
|
227
|
-
|
|
240
|
+
sectionLabel('args', argsText),
|
|
228
241
|
h('pre', { class: 'chat-tool-pre' }, h('code', {}, argsText))),
|
|
229
242
|
resultText ? h('div', { class: 'chat-tool-section' },
|
|
230
|
-
|
|
243
|
+
sectionLabel(p.error ? 'error' : 'result', resultText),
|
|
231
244
|
h('pre', { class: 'chat-tool-pre' + (p.error ? ' is-error' : '') }, h('code', {}, resultText)))
|
|
232
245
|
// A finished tool with no output would otherwise render no result
|
|
233
246
|
// section, reading identically to a still-running tool. Show an
|
|
@@ -6,8 +6,8 @@ import * as webjsx from '../../vendor/webjsx/index.js';
|
|
|
6
6
|
import { Btn, Heading, Lede, Dot, Icon } from './shell.js';
|
|
7
7
|
const h = webjsx.createElement;
|
|
8
8
|
|
|
9
|
-
export function Panel({ title, count, right, style = '', children, kind, id }) {
|
|
10
|
-
const cls = 'panel' + (kind ? ' panel-' + kind : '');
|
|
9
|
+
export function Panel({ title, count, right, style = '', class: className = '', children, kind, id }) {
|
|
10
|
+
const cls = 'panel' + (kind ? ' panel-' + kind : '') + (className ? ' ' + className : '');
|
|
11
11
|
return h('div', { class: cls, style, id: id || null },
|
|
12
12
|
title != null ? h('div', { class: 'panel-head' },
|
|
13
13
|
h('span', {}, title),
|
|
@@ -43,7 +43,7 @@ function highlightTitle(title, highlight) {
|
|
|
43
43
|
return segs;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export function Row({ code, rank, title, sub, meta, active, state = 'default', onClick, key, style, href, kind, cols, leading, trailing, target, selected, rail, expanded, highlight, actions }) {
|
|
46
|
+
export function Row({ code, rank, title, sub, meta, active, state = 'default', onClick, key, style, href, kind, cols, leading, trailing, target, selected, rail, expanded, highlight, actions, detail }) {
|
|
47
47
|
// `rank` is an alias for `code` (the leading monospace index); callers use
|
|
48
48
|
// either name. `rail` renders a thin colour bar at the row's leading edge as
|
|
49
49
|
// a status indicator (tone: green | purple | flame | <any token>).
|
|
@@ -105,12 +105,15 @@ export function Row({ code, rank, title, sub, meta, active, state = 'default', o
|
|
|
105
105
|
// state. green is the unremarkable default - announcing "ok" everywhere would
|
|
106
106
|
// be AT noise - so it emits nothing.
|
|
107
107
|
const railWord = rail === 'flame' ? 'error' : rail === 'purple' ? 'subagent' : null;
|
|
108
|
+
// `detail` renders as a sibling block AFTER the title/meta children (its own
|
|
109
|
+
// line via flex-basis:100% in .ds-row-detail), not inside the title span.
|
|
108
110
|
return h(isLink ? 'a' : 'div', props,
|
|
109
111
|
railWord ? h('span', { class: 'sr-only' }, railWord) : null,
|
|
110
112
|
leading != null ? leading : (codeVal != null ? h('span', { class: 'code' }, codeVal) : null),
|
|
111
113
|
h('span', { class: 'title' }, titleNode, sub ? h('span', { class: 'sub' }, sub) : null),
|
|
112
114
|
trailing != null ? trailing : (meta != null ? h('span', { class: 'meta' }, meta) : null),
|
|
113
|
-
actionRow
|
|
115
|
+
actionRow,
|
|
116
|
+
detail != null ? h('pre', { class: 'ds-row-detail' }, detail) : null);
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
export function RowLink({ code, title, sub, meta, href = '#', key, target }) {
|
package/src/components/files.js
CHANGED
|
@@ -4,10 +4,6 @@ import * as webjsx from '../../vendor/webjsx/index.js';
|
|
|
4
4
|
import { Btn, Icon } from './shell.js';
|
|
5
5
|
const h = webjsx.createElement;
|
|
6
6
|
|
|
7
|
-
// Minimum column width for the responsive file grid (minmax floor). Named so
|
|
8
|
-
// the magic 240px isn't buried in the gridTemplateColumns string.
|
|
9
|
-
const FILE_GRID_MIN_COL = '240px';
|
|
10
|
-
|
|
11
7
|
const FILE_TYPES = ['dir', 'image', 'video', 'audio', 'code', 'text', 'archive', 'document', 'symlink', 'other'];
|
|
12
8
|
const TYPE_ICON = {
|
|
13
9
|
dir: 'folder', image: 'file-image', video: 'file-video', audio: 'file-audio', code: 'file-code',
|
|
@@ -59,9 +55,12 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
59
55
|
const noAccess = locked || permissions === 'EACCES' || (Array.isArray(permissions) && permissions.length === 0);
|
|
60
56
|
const readOnly = !noAccess && Array.isArray(permissions) && permissions.indexOf('write') === -1 && permissions.indexOf('read') !== -1;
|
|
61
57
|
const permTag = noAccess ? 'no access' : (readOnly ? 'read-only' : null);
|
|
62
|
-
|
|
58
|
+
// permTag is rendered as its own chip (a SHAPE channel, not folded into the
|
|
59
|
+
// muted meta text) - so drop it from the meta join, but keep it in the
|
|
60
|
+
// accessible label so AT still announces the restriction.
|
|
61
|
+
const meta = [type === 'dir' ? null : fmtFileSize(size), modified || null].filter(Boolean).join(' · ');
|
|
63
62
|
const typeLabel = TYPE_LABELS[type] || 'file';
|
|
64
|
-
const accessibleLabel = `${typeLabel}: ${name}${meta ? ` (${meta})` : ''}`;
|
|
63
|
+
const accessibleLabel = `${typeLabel}: ${name}${meta ? ` (${meta})` : ''}${permTag ? ', ' + permTag : ''}`;
|
|
65
64
|
const canOpen = onOpen && !noAccess && !busy;
|
|
66
65
|
// Mutation actions on a read-only/no-access row render disabled (with a
|
|
67
66
|
// 'read-only' title) instead of vanishing, so the affordance reads honestly.
|
|
@@ -113,10 +112,13 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
113
112
|
'aria-pressed': active ? 'true' : 'false',
|
|
114
113
|
disabled: canOpen ? null : true,
|
|
115
114
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
...[
|
|
116
|
+
code != null ? h('span', { class: 'code', 'aria-label': `code: ${code}` }, code) : null,
|
|
117
|
+
FileIcon({ type }),
|
|
118
|
+
h('span', { class: 'title' }, name),
|
|
119
|
+
h('span', { class: 'ds-file-meta meta', 'aria-label': meta ? `metadata: ${meta}` : null }, meta || '—'),
|
|
120
|
+
permTag ? h('span', { class: 'ds-file-perm-tag' + (noAccess ? ' is-noaccess' : ''), 'aria-hidden': 'true' }, permTag) : null,
|
|
121
|
+
].filter(Boolean)
|
|
120
122
|
),
|
|
121
123
|
actionBtns.length ? h('span', { key: 'acts', class: 'ds-file-actions', role: 'group', 'aria-label': `actions for ${name}` },
|
|
122
124
|
...actionBtns
|
|
@@ -125,6 +127,7 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
125
127
|
return h('div', {
|
|
126
128
|
key,
|
|
127
129
|
class: 'ds-file-row row' + (active ? ' active' : '') + (noAccess ? ' is-locked' : '')
|
|
130
|
+
+ (readOnly ? ' is-restricted' : '')
|
|
128
131
|
+ (marked ? ' is-marked' : '') + (selectable ? ' is-selectable' : ''),
|
|
129
132
|
'data-file-type': type,
|
|
130
133
|
'aria-busy': busy ? 'true' : null,
|
|
@@ -134,7 +137,7 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
134
137
|
// FileSkeleton — placeholder shimmer rows shown while a directory loads, so the
|
|
135
138
|
// grid does not flash from a bare spinner to a full list (predictable perceived
|
|
136
139
|
// perf, the file-manager feel). `rows` controls how many ghost rows render.
|
|
137
|
-
export function FileSkeleton({ rows =
|
|
140
|
+
export function FileSkeleton({ rows = 12 } = {}) {
|
|
138
141
|
return h('div', { class: 'ds-file-grid ds-file-skeleton', 'aria-hidden': 'true' },
|
|
139
142
|
...Array.from({ length: Math.max(1, rows) }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-file-row ds-file-row-skeleton' },
|
|
140
143
|
h('span', { class: 'ds-skel ds-skel-icon' }),
|
|
@@ -177,8 +180,8 @@ export function sortFiles(files = [], sort = 'name', dir = 'asc') {
|
|
|
177
180
|
// CAP and a "show N more" row, mirroring the History tab's "load N older".
|
|
178
181
|
const FILE_GRID_CAP = 200;
|
|
179
182
|
|
|
180
|
-
export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No files here yet',
|
|
181
|
-
|
|
183
|
+
export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No files here yet', emptyAction,
|
|
184
|
+
sort, filter, loading = false,
|
|
182
185
|
shown, onShowMore, actions, busy,
|
|
183
186
|
// Canonical multi-select contract (shared with
|
|
184
187
|
// SessionDashboard): selected/onToggleSelect.
|
|
@@ -191,8 +194,14 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
191
194
|
// delete / upload round-trip) keeps the rows on screen and dims them -
|
|
192
195
|
// flashing the whole directory to shimmer rows on every mutation reads as
|
|
193
196
|
// data loss.
|
|
194
|
-
if (loading && !files.length) return FileSkeleton({});
|
|
195
|
-
|
|
197
|
+
if (loading && !files.length) return FileSkeleton({ rows: 12 });
|
|
198
|
+
// A filtered miss is NOT an empty directory: when the in-grid filter narrows
|
|
199
|
+
// to zero matches, the host still passes an empty `files` array - but we must
|
|
200
|
+
// keep the controls toolbar (the filter input that caused the miss) mounted so
|
|
201
|
+
// the user can clear/edit it to recover. Only a genuinely-empty directory (no
|
|
202
|
+
// active filter) gets the bare cold EmptyState early-return.
|
|
203
|
+
const hasFilter = !!(filter && (filter.value || '').length > 0);
|
|
204
|
+
if (!files.length && !hasFilter) return EmptyState({ text: emptyText, glyph: Icon('folder-open', { size: 28 }), action: emptyAction });
|
|
196
205
|
const refreshing = loading && files.length > 0;
|
|
197
206
|
// Cap the rendered rows. `shown` (host-controlled) overrides the default cap
|
|
198
207
|
// so "show more" can grow it; otherwise default to FILE_GRID_CAP.
|
|
@@ -200,16 +209,11 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
200
209
|
const capped = files.length > limit;
|
|
201
210
|
const visible = capped ? files.slice(0, limit) : files;
|
|
202
211
|
const isThumb = density === 'thumb';
|
|
212
|
+
// NOTE: the old `columns`-driven data-columns card-mode was removed - it placed
|
|
213
|
+
// flex list-rows into a 2-4 col grid (squashed rows, mis-sized actions) and was
|
|
214
|
+
// a half-wired third layout never exposed by the density radiogroup (list/
|
|
215
|
+
// compact/thumb). Thumb density is the canonical multi-column grid.
|
|
203
216
|
const gridAttrs = {};
|
|
204
|
-
if (!isThumb && columns !== 'auto' && columns > 0) {
|
|
205
|
-
const col = Math.max(1, Math.min(4, Math.floor(columns)));
|
|
206
|
-
gridAttrs['data-columns'] = String(col);
|
|
207
|
-
gridAttrs.style = {
|
|
208
|
-
display: 'grid',
|
|
209
|
-
gridTemplateColumns: `repeat(${col}, minmax(${FILE_GRID_MIN_COL}, 1fr))`,
|
|
210
|
-
gap: 'var(--space-3)'
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
217
|
// Multi-select bookkeeping. Entries are keyed by path (fallback name); a
|
|
214
218
|
// locked/EACCES entry is never selectable — bulk mutations would fail on it.
|
|
215
219
|
const entryKeyOf = (f) => f.path || f.name;
|
|
@@ -250,30 +254,49 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
250
254
|
// it switches presentation of the same content, not panels.
|
|
251
255
|
const densityCtl = onDensity
|
|
252
256
|
? h('div', { key: 'density', class: 'ds-density', role: 'radiogroup', 'aria-label': 'view density' },
|
|
253
|
-
...DENSITIES.map(([k, label]) => h('button', {
|
|
257
|
+
...DENSITIES.map(([k, label], idx) => h('button', {
|
|
254
258
|
key: 'd-' + k, type: 'button', role: 'radio',
|
|
255
259
|
class: 'ds-density-btn' + (density === k ? ' active' : ''),
|
|
256
260
|
'aria-checked': density === k ? 'true' : 'false',
|
|
261
|
+
// Icon-led, but the density name stays the accessible name
|
|
262
|
+
// (aria-label) + the native tooltip (title) so the control reads
|
|
263
|
+
// dense without losing its label.
|
|
264
|
+
'aria-label': label, title: label,
|
|
265
|
+
// Single tab stop: the checked radio is tabbable, the rest are
|
|
266
|
+
// roved. Arrow/Home/End move + select (selection follows focus).
|
|
267
|
+
tabindex: density === k ? '0' : '-1',
|
|
268
|
+
onkeydown: (e) => rovingRadio(e, idx, DENSITIES, (tk) => { if (density !== tk) onDensity(tk); }),
|
|
257
269
|
onclick: () => { if (density !== k) onDensity(k); },
|
|
258
|
-
},
|
|
270
|
+
}, Icon(DENSITY_ICONS[k], { size: 15 }))))
|
|
259
271
|
: null;
|
|
260
|
-
|
|
261
|
-
|
|
272
|
+
// One toolbar baseline: filter + select-all + sort sit left, density is
|
|
273
|
+
// pushed right by the spread. The filter used to be a separate right-aligned
|
|
274
|
+
// strip ABOVE controls, giving two strips with conflicting alignment.
|
|
275
|
+
const filterCtl = filter ? h('input', {
|
|
276
|
+
key: 'filter',
|
|
277
|
+
class: 'ds-file-filter-input', type: 'search',
|
|
278
|
+
value: filter.value || '', placeholder: filter.placeholder || 'Filter files',
|
|
279
|
+
'aria-label': filter.placeholder || 'Filter files in this directory',
|
|
280
|
+
oninput: (e) => filter.onInput && filter.onInput(e.target.value),
|
|
281
|
+
}) : null;
|
|
282
|
+
const leftKids = [filterCtl, selectAllCtl, head].filter(Boolean);
|
|
283
|
+
const controlsKids = [
|
|
284
|
+
...leftKids,
|
|
285
|
+
(leftKids.length && densityCtl) ? h('span', { key: 'spread', class: 'spread' }) : null,
|
|
262
286
|
densityCtl].filter(Boolean);
|
|
263
287
|
const controls = controlsKids.length
|
|
264
288
|
? h('div', { class: 'ds-file-controls' }, ...controlsKids)
|
|
265
289
|
: null;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
})) : null;
|
|
290
|
+
// A filtered miss (zero rows but an active filter) renders the EmptyState
|
|
291
|
+
// INSIDE the listing, below the controls toolbar, so the filter input stays
|
|
292
|
+
// mounted and editable - the user can clear/edit it to recover instead of
|
|
293
|
+
// being stranded with no toolbar (the early-return only fires for a genuinely
|
|
294
|
+
// empty directory). The host passes filter-aware copy via emptyText.
|
|
295
|
+
const filteredEmpty = !files.length && hasFilter;
|
|
273
296
|
// role=group not listbox: the rows contain real <button> action controls, so
|
|
274
297
|
// listbox/option semantics are invalid (an option can't host interactive
|
|
275
298
|
// children). Keyboard nav still works via roving focus over the open buttons.
|
|
276
|
-
const grid = h('div', {
|
|
299
|
+
const grid = filteredEmpty ? EmptyState({ text: emptyText, glyph: Icon('folder-open', { size: 28 }) }) : h('div', {
|
|
277
300
|
class: 'ds-file-grid' + (isThumb ? ' ds-file-grid-thumb' : '') + (refreshing ? ' is-refreshing' : ''),
|
|
278
301
|
role: 'group', 'aria-label': 'files', tabindex: '0',
|
|
279
302
|
'aria-busy': refreshing ? 'true' : 'false',
|
|
@@ -311,12 +334,31 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
311
334
|
onclick: () => onShowMore(Math.min(files.length, limit + FILE_GRID_CAP)) },
|
|
312
335
|
'show ' + Math.min(FILE_GRID_CAP, files.length - limit) + ' more') : null)
|
|
313
336
|
: null;
|
|
314
|
-
return (controls ||
|
|
315
|
-
? h('div', { class: 'ds-file-listing' },
|
|
337
|
+
return (controls || more)
|
|
338
|
+
? h('div', { class: 'ds-file-listing' }, controls, grid, more)
|
|
316
339
|
: grid;
|
|
317
340
|
}
|
|
318
341
|
|
|
319
342
|
const DENSITIES = [['list', 'list'], ['compact', 'compact'], ['thumb', 'thumbnails']];
|
|
343
|
+
const DENSITY_ICONS = { list: 'rows', compact: 'rows-tight', thumb: 'grid' };
|
|
344
|
+
|
|
345
|
+
// Roving-radiogroup keyboard helper (the WAI-ARIA radio pattern): a radiogroup
|
|
346
|
+
// is a SINGLE tab stop where Arrow/Home/End move AND select among options, with
|
|
347
|
+
// selection following focus. `items` is the ordered [[key, ...], ...] list;
|
|
348
|
+
// `onSelect(targetKey)` is the same handler the onclick fires. Mouse path is
|
|
349
|
+
// unchanged - this only adds keyboard navigation.
|
|
350
|
+
function rovingRadio(e, idx, items, onSelect) {
|
|
351
|
+
let target = -1;
|
|
352
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') target = (idx - 1 + items.length) % items.length;
|
|
353
|
+
else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') target = (idx + 1) % items.length;
|
|
354
|
+
else if (e.key === 'Home') target = 0;
|
|
355
|
+
else if (e.key === 'End') target = items.length - 1;
|
|
356
|
+
else return;
|
|
357
|
+
e.preventDefault();
|
|
358
|
+
onSelect(items[target][0]);
|
|
359
|
+
const sib = e.currentTarget.parentNode && e.currentTarget.parentNode.children[target];
|
|
360
|
+
sib && sib.focus();
|
|
361
|
+
}
|
|
320
362
|
|
|
321
363
|
// FileCell — the thumbnail-density tile. Image entries show a real (lazy)
|
|
322
364
|
// thumbnail through the host's confined thumbUrl; everything else keeps its
|
|
@@ -474,10 +516,20 @@ export function UploadProgress({ items = [], onDismiss } = {}) {
|
|
|
474
516
|
);
|
|
475
517
|
}
|
|
476
518
|
|
|
477
|
-
export function EmptyState({ text = 'nothing here', glyph = Icon('circle') } = {}) {
|
|
519
|
+
export function EmptyState({ text = 'nothing here', glyph = Icon('circle'), action } = {}) {
|
|
520
|
+
// action: { onClick, label } - an optional CTA (e.g. 'go up' / 'upload a
|
|
521
|
+
// file'), mirroring the SessionDashboard emptyAction contract so an empty
|
|
522
|
+
// directory is not a dead end. Children are built as an array + filtered so
|
|
523
|
+
// the keyed Btn never sits beside an unkeyed span (webjsx applyDiff 'key'
|
|
524
|
+
// crash on mixed keyed/unkeyed siblings).
|
|
478
525
|
return h('div', { class: 'ds-file-empty', role: 'status' },
|
|
479
|
-
|
|
480
|
-
|
|
526
|
+
...[
|
|
527
|
+
h('span', { key: 'glyph', class: 'ds-file-empty-glyph', 'aria-hidden': 'true' }, glyph),
|
|
528
|
+
h('span', { key: 'text', class: 'ds-file-empty-text' }, text),
|
|
529
|
+
(action && action.onClick)
|
|
530
|
+
? Btn({ key: 'ea', onClick: action.onClick, children: action.label || 'go up' })
|
|
531
|
+
: null,
|
|
532
|
+
].filter(Boolean)
|
|
481
533
|
);
|
|
482
534
|
}
|
|
483
535
|
|
|
@@ -296,6 +296,13 @@ export function useKeyboardShortcut(map = {}, { scope = 'global', enabled = true
|
|
|
296
296
|
|
|
297
297
|
export function ShortcutHint({ combo, kind = 'kbd' } = {}) { return h('kbd', { class: 'ds-kbd ds-kbd-' + kind }, formatShortcut(combo || '')); }
|
|
298
298
|
|
|
299
|
+
export function ShortcutList({ shortcuts = [] } = {}) {
|
|
300
|
+
return h('div', { class: 'ds-shortcuts-hint' },
|
|
301
|
+
...shortcuts.map(s => h('div', { class: 'ds-shortcut-row' },
|
|
302
|
+
h('kbd', { class: 'ds-kbd' }, s.keys || s.combo || ''),
|
|
303
|
+
h('span', { class: 'ds-kbd-label' }, s.desc || s.description || s.label || ''))));
|
|
304
|
+
}
|
|
305
|
+
|
|
299
306
|
export function useKeyboardShortcutHelp() { return { registry: Array.from(SHORTCUT_REGISTRY) }; }
|
|
300
307
|
export function ShortcutHelpDialog({ open = false, onClose, registry } = {}) {
|
|
301
308
|
if (!open) return null;
|
|
@@ -171,7 +171,9 @@ export function SessionCard({ session = {}, onStop, onOpen, onView, active = fal
|
|
|
171
171
|
// sessions with no cost source (external tally rows) simply omit the segment.
|
|
172
172
|
const tokText = s.tokens != null ? (typeof s.tokens === 'number' ? s.tokens.toLocaleString() : s.tokens) + ' tok' : null;
|
|
173
173
|
const costText = s.cost != null ? (typeof s.cost === 'number' ? '$' + s.cost.toFixed(4) : String(s.cost)) : null;
|
|
174
|
-
|
|
174
|
+
// Cost is rendered as its own emphasized segment (not buried in the mono run)
|
|
175
|
+
// so the command-center cost-at-a-glance signal is scannable.
|
|
176
|
+
const statBits = [elapsedText, s.counter != null ? s.counter : null, tokText].filter((x) => x != null && x !== '');
|
|
175
177
|
const activityBits = [
|
|
176
178
|
s.currentTool ? 'running: ' + s.currentTool : null,
|
|
177
179
|
s.lastActivity ? 'last ' + s.lastActivity : null,
|
|
@@ -195,7 +197,13 @@ export function SessionCard({ session = {}, onStop, onOpen, onView, active = fal
|
|
|
195
197
|
].filter(Boolean));
|
|
196
198
|
const meta = h('div', { class: 'ds-dash-meta' }, ...[
|
|
197
199
|
s.cwd ? h('span', { class: 'ds-dash-cwd', title: s.cwd }, s.cwd) : null,
|
|
198
|
-
statBits.length ? h('span', { class: 'ds-dash-stat' },
|
|
200
|
+
(statBits.length || costText) ? h('span', { class: 'ds-dash-stat' },
|
|
201
|
+
...[
|
|
202
|
+
statBits.length ? statBits.join(' · ') : null,
|
|
203
|
+
(statBits.length && costText) ? ' · ' : null,
|
|
204
|
+
costText ? h('span', { class: 'ds-dash-stat-cost' }, costText) : null,
|
|
205
|
+
].filter(Boolean)
|
|
206
|
+
) : null,
|
|
199
207
|
activityBits.length ? h('span', { class: 'ds-dash-activity' }, activityBits.join(' · ')) : null,
|
|
200
208
|
].filter(Boolean));
|
|
201
209
|
const actions = h('div', { class: 'ds-dash-actions', role: 'group', 'aria-label': 'session actions' }, ...[
|
package/src/components/shell.js
CHANGED
|
@@ -146,6 +146,10 @@ const ICON_PATHS = {
|
|
|
146
146
|
// file-browser icons (replace folder/file emoji + arrow glyphs in fs apps)
|
|
147
147
|
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"/>',
|
|
148
148
|
'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"/>',
|
|
149
|
+
// density-picker icons (list / compact / thumbnail view modes)
|
|
150
|
+
rows: '<path d="M4 6h16M4 12h16M4 18h16"/>',
|
|
151
|
+
'rows-tight': '<path d="M4 5h16M4 9h16M4 13h16M4 17h16"/>',
|
|
152
|
+
grid: '<rect x="4" y="4" width="7" height="7" rx="1"/><rect x="13" y="4" width="7" height="7" rx="1"/><rect x="4" y="13" width="7" height="7" rx="1"/><rect x="13" y="13" width="7" height="7" rx="1"/>',
|
|
149
153
|
'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"/>',
|
|
150
154
|
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"/>',
|
|
151
155
|
upload: '<path d="M12 16V4M7 9l5-5 5 5"/><path d="M5 20h14"/>',
|
|
@@ -318,8 +322,18 @@ function toggleWs(which) {
|
|
|
318
322
|
|
|
319
323
|
// Column resize: read the current rendered track width and write a clamped inline
|
|
320
324
|
// --ws-<col>-w on .ws-shell (inline overrides the fluid clamp base), persisted.
|
|
321
|
-
|
|
322
|
-
|
|
325
|
+
// Floors match the CSS fluid clamp() floors in app-shell.css (--ws-rail-w
|
|
326
|
+
// clamp(200,16vw,260); sessions clamp(248,22vw,360); pane clamp(288,24vw,420))
|
|
327
|
+
// so a drag/arrow can never shrink a column below its designed minimum (the
|
|
328
|
+
// collapsed rail is a SEPARATE class, not a resize target). The ceilings are
|
|
329
|
+
// INTENTIONALLY raised above the fluid clamp() mid-term ceilings: on wide
|
|
330
|
+
// viewports the 16/22/24vw mid term already pins each column to its clamp
|
|
331
|
+
// ceiling, so a ceiling-equals-clamp bound made the outward drag inert there.
|
|
332
|
+
// The higher resize ceilings let a deliberate drag/arrow grow a column past its
|
|
333
|
+
// auto-fluid width (the inline --ws-<col>-w override pins the chosen width past
|
|
334
|
+
// the clamp base).
|
|
335
|
+
const WS_RESIZE_CLAMP = { rail: [200, 320], sessions: [248, 520], pane: [288, 640] };
|
|
336
|
+
function wsResize(col, dx, persist = true) {
|
|
323
337
|
const shell = document.querySelector('.ws-shell');
|
|
324
338
|
if (!shell) return;
|
|
325
339
|
const track = shell.querySelector('.ws-' + col);
|
|
@@ -327,7 +341,11 @@ function wsResize(col, dx) {
|
|
|
327
341
|
const [lo, hi] = WS_RESIZE_CLAMP[col] || [120, 600];
|
|
328
342
|
const next = Math.max(lo, Math.min(hi, Math.round(cur + dx)));
|
|
329
343
|
shell.style.setProperty('--ws-' + col + '-w', next + 'px');
|
|
330
|
-
|
|
344
|
+
const handle = shell.querySelector('.ws-resizer-' + col);
|
|
345
|
+
if (handle) handle.setAttribute('aria-valuenow', String(next));
|
|
346
|
+
// Commit to storage only on a settled move (pointerup / keyboard), not on
|
|
347
|
+
// every pointermove frame (that fired dozens of synchronous writes per drag).
|
|
348
|
+
if (persist) { try { localStorage.setItem('ds.ws.w.' + col, String(next)); } catch (_) {} }
|
|
331
349
|
}
|
|
332
350
|
function seedWsWidths(el) {
|
|
333
351
|
if (!el) return;
|
|
@@ -340,22 +358,35 @@ function seedWsWidths(el) {
|
|
|
340
358
|
}
|
|
341
359
|
function WsResizer(col) {
|
|
342
360
|
const onKey = (e) => {
|
|
343
|
-
if (e.key === 'ArrowLeft') { e.preventDefault(); wsResize(col, -16); }
|
|
344
|
-
else if (e.key === 'ArrowRight') { e.preventDefault(); wsResize(col, 16); }
|
|
361
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); wsResize(col, -16, true); }
|
|
362
|
+
else if (e.key === 'ArrowRight') { e.preventDefault(); wsResize(col, 16, true); }
|
|
345
363
|
};
|
|
346
364
|
const onDown = (e) => {
|
|
347
365
|
e.preventDefault();
|
|
348
366
|
let lastX = e.clientX;
|
|
349
|
-
const move = (ev) => { const dx = ev.clientX - lastX; lastX = ev.clientX; wsResize(col, dx); };
|
|
350
|
-
const up = () => {
|
|
367
|
+
const move = (ev) => { const dx = ev.clientX - lastX; lastX = ev.clientX; wsResize(col, dx, false); };
|
|
368
|
+
const up = () => {
|
|
369
|
+
document.removeEventListener('pointermove', move);
|
|
370
|
+
document.removeEventListener('pointerup', up);
|
|
371
|
+
document.body.style.cursor = '';
|
|
372
|
+
wsResize(col, 0, true); // commit the settled width once
|
|
373
|
+
};
|
|
351
374
|
document.addEventListener('pointermove', move);
|
|
352
375
|
document.addEventListener('pointerup', up);
|
|
353
376
|
document.body.style.cursor = 'col-resize';
|
|
354
377
|
};
|
|
378
|
+
const [lo, hi] = WS_RESIZE_CLAMP[col] || [120, 600];
|
|
379
|
+
// Seed aria-valuenow from the rendered track width so AT announces real widths.
|
|
380
|
+
const seedNow = (el) => {
|
|
381
|
+
if (!el) return;
|
|
382
|
+
const track = el.closest('.ws-shell') && el.closest('.ws-shell').querySelector('.ws-' + col);
|
|
383
|
+
if (track) el.setAttribute('aria-valuenow', String(Math.round(track.getBoundingClientRect().width)));
|
|
384
|
+
};
|
|
355
385
|
return h('div', {
|
|
356
386
|
class: 'ws-resizer ws-resizer-' + col, role: 'separator', tabindex: '0',
|
|
357
387
|
'aria-orientation': 'vertical', 'aria-label': 'resize ' + col + ' column (arrow keys)',
|
|
358
|
-
|
|
388
|
+
'aria-valuemin': String(lo), 'aria-valuemax': String(hi),
|
|
389
|
+
onpointerdown: onDown, onkeydown: onKey, ref: seedNow,
|
|
359
390
|
});
|
|
360
391
|
}
|
|
361
392
|
|
|
@@ -483,6 +514,15 @@ export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narr
|
|
|
483
514
|
'aria-expanded': 'true', onclick: () => toggleWs('sessions'),
|
|
484
515
|
}, Icon('chevron-left')) : null,
|
|
485
516
|
h('div', { class: 'ws-crumb-main' }, crumb),
|
|
517
|
+
// Desktop-only context-pane collapse, on the same crumb-level
|
|
518
|
+
// chrome idiom as the sessions toggle. Hidden on mobile via CSS.
|
|
519
|
+
hasPane ? h('button', {
|
|
520
|
+
class: 'ws-desktop-toggle ws-pane-toggle', type: 'button',
|
|
521
|
+
'aria-label': paneIsCollapsed ? 'show context pane' : 'hide context pane',
|
|
522
|
+
title: paneIsCollapsed ? 'show context pane' : 'hide context pane',
|
|
523
|
+
'aria-expanded': paneIsCollapsed ? 'false' : 'true',
|
|
524
|
+
onclick: () => toggleWs('pane'),
|
|
525
|
+
}, Icon(paneIsCollapsed ? 'chevron-left' : 'chevron-right')) : null,
|
|
486
526
|
hasPane ? h('button', {
|
|
487
527
|
class: 'ws-drawer-toggle ws-pane-drawer-toggle', type: 'button',
|
|
488
528
|
'aria-label': 'toggle context pane', 'aria-expanded': 'false',
|
|
@@ -492,16 +532,10 @@ export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narr
|
|
|
492
532
|
h('main', { class: 'ws-main' + (narrow ? ' narrow' : '') + (mainFlush ? ' ws-main--flush' : ''), id: 'ws-main', tabindex: '-1' },
|
|
493
533
|
...(Array.isArray(main) ? main : [main])),
|
|
494
534
|
status || null),
|
|
495
|
-
// Optional right context pane
|
|
535
|
+
// Optional right context pane. Its desktop collapse toggle now lives in
|
|
536
|
+
// the crumb cluster, alongside the sessions toggle.
|
|
496
537
|
hasPane
|
|
497
538
|
? h('aside', { class: 'ws-pane', role: 'complementary', 'aria-label': paneLabel },
|
|
498
|
-
h('button', {
|
|
499
|
-
class: 'ws-pane-toggle', type: 'button',
|
|
500
|
-
'aria-label': paneIsCollapsed ? 'show context pane' : 'hide context pane',
|
|
501
|
-
title: paneIsCollapsed ? 'show context pane' : 'hide context pane',
|
|
502
|
-
'aria-expanded': paneIsCollapsed ? 'false' : 'true',
|
|
503
|
-
onclick: () => toggleWs('pane'),
|
|
504
|
-
}, Icon(paneIsCollapsed ? 'chevron-left' : 'chevron-right')),
|
|
505
539
|
pane)
|
|
506
540
|
: null,
|
|
507
541
|
// Keyboard/pointer column resize handles (desktop only).
|
|
@@ -550,8 +584,8 @@ export function WorkspaceRail({ brand = '247420', action, items = [], footer } =
|
|
|
550
584
|
);
|
|
551
585
|
}
|
|
552
586
|
|
|
553
|
-
export function Heading({ level = 1, children, style = '', 'aria-level': ariaLevel }) {
|
|
554
|
-
return h('h' + level, { style, 'aria-level': ariaLevel != null ? String(ariaLevel) : null }, children);
|
|
587
|
+
export function Heading({ level = 1, children, style = '', class: className = '', 'aria-level': ariaLevel }) {
|
|
588
|
+
return h('h' + level, { class: className || null, style, 'aria-level': ariaLevel != null ? String(ariaLevel) : null }, children);
|
|
555
589
|
}
|
|
556
590
|
|
|
557
591
|
export function Lede({ children }) {
|
package/src/components.js
CHANGED
|
@@ -66,7 +66,7 @@ export {
|
|
|
66
66
|
|
|
67
67
|
export {
|
|
68
68
|
useDraggable, useDropTarget, useNumberScrub, usePointerDrag, Reorderable,
|
|
69
|
-
useKeyboardShortcut, formatShortcut, ShortcutHint,
|
|
69
|
+
useKeyboardShortcut, formatShortcut, ShortcutHint, ShortcutList,
|
|
70
70
|
useKeyboardShortcutHelp, ShortcutHelpDialog
|
|
71
71
|
} from './components/interaction-primitives.js';
|
|
72
72
|
|