anentrypoint-design 0.0.212 → 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 +87 -39
- package/chat.css +75 -17
- package/colors_and_type.css +14 -9
- package/dist/247420.css +185 -73
- package/dist/247420.js +13 -13
- package/package.json +1 -1
- package/src/components/chat.js +15 -2
- package/src/components/content.js +5 -2
- package/src/components/files.js +72 -16
- package/src/components/interaction-primitives.js +7 -0
- package/src/components/sessions.js +5 -2
- package/src/components/shell.js +26 -14
- 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
|
|
@@ -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
|
@@ -55,9 +55,12 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
55
55
|
const noAccess = locked || permissions === 'EACCES' || (Array.isArray(permissions) && permissions.length === 0);
|
|
56
56
|
const readOnly = !noAccess && Array.isArray(permissions) && permissions.indexOf('write') === -1 && permissions.indexOf('read') !== -1;
|
|
57
57
|
const permTag = noAccess ? 'no access' : (readOnly ? 'read-only' : null);
|
|
58
|
-
|
|
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(' · ');
|
|
59
62
|
const typeLabel = TYPE_LABELS[type] || 'file';
|
|
60
|
-
const accessibleLabel = `${typeLabel}: ${name}${meta ? ` (${meta})` : ''}`;
|
|
63
|
+
const accessibleLabel = `${typeLabel}: ${name}${meta ? ` (${meta})` : ''}${permTag ? ', ' + permTag : ''}`;
|
|
61
64
|
const canOpen = onOpen && !noAccess && !busy;
|
|
62
65
|
// Mutation actions on a read-only/no-access row render disabled (with a
|
|
63
66
|
// 'read-only' title) instead of vanishing, so the affordance reads honestly.
|
|
@@ -109,10 +112,13 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
109
112
|
'aria-pressed': active ? 'true' : 'false',
|
|
110
113
|
disabled: canOpen ? null : true,
|
|
111
114
|
},
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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)
|
|
116
122
|
),
|
|
117
123
|
actionBtns.length ? h('span', { key: 'acts', class: 'ds-file-actions', role: 'group', 'aria-label': `actions for ${name}` },
|
|
118
124
|
...actionBtns
|
|
@@ -121,6 +127,7 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
121
127
|
return h('div', {
|
|
122
128
|
key,
|
|
123
129
|
class: 'ds-file-row row' + (active ? ' active' : '') + (noAccess ? ' is-locked' : '')
|
|
130
|
+
+ (readOnly ? ' is-restricted' : '')
|
|
124
131
|
+ (marked ? ' is-marked' : '') + (selectable ? ' is-selectable' : ''),
|
|
125
132
|
'data-file-type': type,
|
|
126
133
|
'aria-busy': busy ? 'true' : null,
|
|
@@ -130,7 +137,7 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
130
137
|
// FileSkeleton — placeholder shimmer rows shown while a directory loads, so the
|
|
131
138
|
// grid does not flash from a bare spinner to a full list (predictable perceived
|
|
132
139
|
// perf, the file-manager feel). `rows` controls how many ghost rows render.
|
|
133
|
-
export function FileSkeleton({ rows =
|
|
140
|
+
export function FileSkeleton({ rows = 12 } = {}) {
|
|
134
141
|
return h('div', { class: 'ds-file-grid ds-file-skeleton', 'aria-hidden': 'true' },
|
|
135
142
|
...Array.from({ length: Math.max(1, rows) }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-file-row ds-file-row-skeleton' },
|
|
136
143
|
h('span', { class: 'ds-skel ds-skel-icon' }),
|
|
@@ -173,7 +180,7 @@ export function sortFiles(files = [], sort = 'name', dir = 'asc') {
|
|
|
173
180
|
// CAP and a "show N more" row, mirroring the History tab's "load N older".
|
|
174
181
|
const FILE_GRID_CAP = 200;
|
|
175
182
|
|
|
176
|
-
export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No files here yet',
|
|
183
|
+
export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No files here yet', emptyAction,
|
|
177
184
|
sort, filter, loading = false,
|
|
178
185
|
shown, onShowMore, actions, busy,
|
|
179
186
|
// Canonical multi-select contract (shared with
|
|
@@ -187,8 +194,14 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
187
194
|
// delete / upload round-trip) keeps the rows on screen and dims them -
|
|
188
195
|
// flashing the whole directory to shimmer rows on every mutation reads as
|
|
189
196
|
// data loss.
|
|
190
|
-
if (loading && !files.length) return FileSkeleton({});
|
|
191
|
-
|
|
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 });
|
|
192
205
|
const refreshing = loading && files.length > 0;
|
|
193
206
|
// Cap the rendered rows. `shown` (host-controlled) overrides the default cap
|
|
194
207
|
// so "show more" can grow it; otherwise default to FILE_GRID_CAP.
|
|
@@ -241,12 +254,20 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
241
254
|
// it switches presentation of the same content, not panels.
|
|
242
255
|
const densityCtl = onDensity
|
|
243
256
|
? h('div', { key: 'density', class: 'ds-density', role: 'radiogroup', 'aria-label': 'view density' },
|
|
244
|
-
...DENSITIES.map(([k, label]) => h('button', {
|
|
257
|
+
...DENSITIES.map(([k, label], idx) => h('button', {
|
|
245
258
|
key: 'd-' + k, type: 'button', role: 'radio',
|
|
246
259
|
class: 'ds-density-btn' + (density === k ? ' active' : ''),
|
|
247
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); }),
|
|
248
269
|
onclick: () => { if (density !== k) onDensity(k); },
|
|
249
|
-
},
|
|
270
|
+
}, Icon(DENSITY_ICONS[k], { size: 15 }))))
|
|
250
271
|
: null;
|
|
251
272
|
// One toolbar baseline: filter + select-all + sort sit left, density is
|
|
252
273
|
// pushed right by the spread. The filter used to be a separate right-aligned
|
|
@@ -266,10 +287,16 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
266
287
|
const controls = controlsKids.length
|
|
267
288
|
? h('div', { class: 'ds-file-controls' }, ...controlsKids)
|
|
268
289
|
: 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;
|
|
269
296
|
// role=group not listbox: the rows contain real <button> action controls, so
|
|
270
297
|
// listbox/option semantics are invalid (an option can't host interactive
|
|
271
298
|
// children). Keyboard nav still works via roving focus over the open buttons.
|
|
272
|
-
const grid = h('div', {
|
|
299
|
+
const grid = filteredEmpty ? EmptyState({ text: emptyText, glyph: Icon('folder-open', { size: 28 }) }) : h('div', {
|
|
273
300
|
class: 'ds-file-grid' + (isThumb ? ' ds-file-grid-thumb' : '') + (refreshing ? ' is-refreshing' : ''),
|
|
274
301
|
role: 'group', 'aria-label': 'files', tabindex: '0',
|
|
275
302
|
'aria-busy': refreshing ? 'true' : 'false',
|
|
@@ -313,6 +340,25 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
313
340
|
}
|
|
314
341
|
|
|
315
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
|
+
}
|
|
316
362
|
|
|
317
363
|
// FileCell — the thumbnail-density tile. Image entries show a real (lazy)
|
|
318
364
|
// thumbnail through the host's confined thumbUrl; everything else keeps its
|
|
@@ -470,10 +516,20 @@ export function UploadProgress({ items = [], onDismiss } = {}) {
|
|
|
470
516
|
);
|
|
471
517
|
}
|
|
472
518
|
|
|
473
|
-
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).
|
|
474
525
|
return h('div', { class: 'ds-file-empty', role: 'status' },
|
|
475
|
-
|
|
476
|
-
|
|
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)
|
|
477
533
|
);
|
|
478
534
|
}
|
|
479
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;
|
|
@@ -198,8 +198,11 @@ export function SessionCard({ session = {}, onStop, onOpen, onView, active = fal
|
|
|
198
198
|
const meta = h('div', { class: 'ds-dash-meta' }, ...[
|
|
199
199
|
s.cwd ? h('span', { class: 'ds-dash-cwd', title: s.cwd }, s.cwd) : null,
|
|
200
200
|
(statBits.length || costText) ? h('span', { class: 'ds-dash-stat' },
|
|
201
|
-
|
|
202
|
-
|
|
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)
|
|
203
206
|
) : null,
|
|
204
207
|
activityBits.length ? h('span', { class: 'ds-dash-activity' }, activityBits.join(' · ')) : null,
|
|
205
208
|
].filter(Boolean));
|
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,12 +322,17 @@ 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
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
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] };
|
|
327
336
|
function wsResize(col, dx, persist = true) {
|
|
328
337
|
const shell = document.querySelector('.ws-shell');
|
|
329
338
|
if (!shell) return;
|
|
@@ -505,6 +514,15 @@ export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narr
|
|
|
505
514
|
'aria-expanded': 'true', onclick: () => toggleWs('sessions'),
|
|
506
515
|
}, Icon('chevron-left')) : null,
|
|
507
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,
|
|
508
526
|
hasPane ? h('button', {
|
|
509
527
|
class: 'ws-drawer-toggle ws-pane-drawer-toggle', type: 'button',
|
|
510
528
|
'aria-label': 'toggle context pane', 'aria-expanded': 'false',
|
|
@@ -514,16 +532,10 @@ export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narr
|
|
|
514
532
|
h('main', { class: 'ws-main' + (narrow ? ' narrow' : '') + (mainFlush ? ' ws-main--flush' : ''), id: 'ws-main', tabindex: '-1' },
|
|
515
533
|
...(Array.isArray(main) ? main : [main])),
|
|
516
534
|
status || null),
|
|
517
|
-
// Optional right context pane
|
|
535
|
+
// Optional right context pane. Its desktop collapse toggle now lives in
|
|
536
|
+
// the crumb cluster, alongside the sessions toggle.
|
|
518
537
|
hasPane
|
|
519
538
|
? h('aside', { class: 'ws-pane', role: 'complementary', 'aria-label': paneLabel },
|
|
520
|
-
h('button', {
|
|
521
|
-
class: 'ws-pane-toggle', type: 'button',
|
|
522
|
-
'aria-label': paneIsCollapsed ? 'show context pane' : 'hide context pane',
|
|
523
|
-
title: paneIsCollapsed ? 'show context pane' : 'hide context pane',
|
|
524
|
-
'aria-expanded': paneIsCollapsed ? 'false' : 'true',
|
|
525
|
-
onclick: () => toggleWs('pane'),
|
|
526
|
-
}, Icon(paneIsCollapsed ? 'chevron-left' : 'chevron-right')),
|
|
527
539
|
pane)
|
|
528
540
|
: null,
|
|
529
541
|
// Keyboard/pointer column resize handles (desktop only).
|
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
|
|