anentrypoint-design 0.0.207 → 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/dist/247420.css +124 -12
- package/dist/247420.js +14 -14
- package/package.json +1 -1
- package/src/components/files.js +162 -27
- 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.208",
|
|
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/files.js
CHANGED
|
@@ -51,7 +51,7 @@ export function FileIcon({ type = 'other' } = {}) {
|
|
|
51
51
|
const FILE_ROW_ACTIONS = ['download', 'rename', 'delete'];
|
|
52
52
|
|
|
53
53
|
export function FileRow({ name, type = 'other', size, modified, code, onOpen, onAction, active, key, permissions, locked,
|
|
54
|
-
actions = FILE_ROW_ACTIONS, busy = false } = {}) {
|
|
54
|
+
actions = FILE_ROW_ACTIONS, busy = false, selectable = false, marked = false, onMark } = {}) {
|
|
55
55
|
// permissions: ['read','write'] | ['read'] | 'EACCES'. A no-access entry can
|
|
56
56
|
// be listed (the dir stat saw it) but not opened — show an ASCII tag and
|
|
57
57
|
// disable the open button so the row reads honestly instead of silently
|
|
@@ -85,17 +85,27 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
85
85
|
actions.indexOf('delete') !== -1
|
|
86
86
|
? actBtn('delete', 'delete', `delete ${name}`, 'x', true) : null,
|
|
87
87
|
].filter(Boolean) : [];
|
|
88
|
+
// Multi-select checkbox — a sibling control before the open button so the
|
|
89
|
+
// row stays valid HTML (no interactive nesting). A no-access entry cannot
|
|
90
|
+
// be marked (bulk mutations would fail on it anyway).
|
|
91
|
+
const checkCtl = selectable ? h('button', {
|
|
92
|
+
key: 'mark',
|
|
93
|
+
type: 'button',
|
|
94
|
+
class: 'ds-file-check' + (marked ? ' is-marked' : ''),
|
|
95
|
+
role: 'checkbox',
|
|
96
|
+
'aria-checked': marked ? 'true' : 'false',
|
|
97
|
+
'aria-label': (marked ? 'unselect ' : 'select ') + name,
|
|
98
|
+
disabled: (noAccess || busy) ? true : null,
|
|
99
|
+
onclick: onMark ? (e) => onMark({ range: !!e.shiftKey }) : null,
|
|
100
|
+
}, h('span', { 'aria-hidden': 'true' }, marked ? '[x]' : '[ ]')) : null;
|
|
88
101
|
// A role=button row containing real <button> action controls is invalid
|
|
89
102
|
// HTML (interactive nesting). Instead the row is a plain container and the
|
|
90
103
|
// primary "open" affordance is itself a real <button> (native keyboard +
|
|
91
104
|
// semantics); the per-file action buttons sit alongside it as siblings.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
class: 'ds-file-row row' + (active ? ' active' : '') + (noAccess ? ' is-locked' : ''),
|
|
95
|
-
'data-file-type': type,
|
|
96
|
-
'aria-busy': busy ? 'true' : null,
|
|
97
|
-
},
|
|
105
|
+
const rowKids = [
|
|
106
|
+
checkCtl,
|
|
98
107
|
h('button', {
|
|
108
|
+
key: 'open',
|
|
99
109
|
type: 'button',
|
|
100
110
|
class: 'ds-file-open',
|
|
101
111
|
onclick: canOpen ? onOpen : null,
|
|
@@ -108,10 +118,17 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
|
|
|
108
118
|
h('span', { class: 'title' }, name),
|
|
109
119
|
h('span', { class: 'ds-file-meta meta', 'aria-label': meta ? `metadata: ${meta}` : null }, meta || '—')
|
|
110
120
|
),
|
|
111
|
-
actionBtns.length ? h('span', { class: 'ds-file-actions', role: 'group', 'aria-label': `actions for ${name}` },
|
|
121
|
+
actionBtns.length ? h('span', { key: 'acts', class: 'ds-file-actions', role: 'group', 'aria-label': `actions for ${name}` },
|
|
112
122
|
...actionBtns
|
|
113
|
-
) : null
|
|
114
|
-
);
|
|
123
|
+
) : null,
|
|
124
|
+
].filter(Boolean);
|
|
125
|
+
return h('div', {
|
|
126
|
+
key,
|
|
127
|
+
class: 'ds-file-row row' + (active ? ' active' : '') + (noAccess ? ' is-locked' : '')
|
|
128
|
+
+ (marked ? ' is-marked' : '') + (selectable ? ' is-selectable' : ''),
|
|
129
|
+
'data-file-type': type,
|
|
130
|
+
'aria-busy': busy ? 'true' : null,
|
|
131
|
+
}, ...rowKids);
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
// FileSkeleton — placeholder shimmer rows shown while a directory loads, so the
|
|
@@ -162,7 +179,9 @@ const FILE_GRID_CAP = 200;
|
|
|
162
179
|
|
|
163
180
|
export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No files here yet',
|
|
164
181
|
columns = 'auto', sort, filter, loading = false,
|
|
165
|
-
shown, onShowMore, actions, busy
|
|
182
|
+
shown, onShowMore, actions, busy,
|
|
183
|
+
selectable = false, marked, onMark, onSelectAll, onClearSelection,
|
|
184
|
+
density = 'list', onDensity, thumbUrl } = {}) {
|
|
166
185
|
if (loading) return FileSkeleton({});
|
|
167
186
|
if (!files.length) return EmptyState({ text: emptyText });
|
|
168
187
|
// Cap the rendered rows. `shown` (host-controlled) overrides the default cap
|
|
@@ -170,8 +189,9 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
170
189
|
const limit = shown != null ? shown : FILE_GRID_CAP;
|
|
171
190
|
const capped = files.length > limit;
|
|
172
191
|
const visible = capped ? files.slice(0, limit) : files;
|
|
192
|
+
const isThumb = density === 'thumb';
|
|
173
193
|
const gridAttrs = {};
|
|
174
|
-
if (columns !== 'auto' && columns > 0) {
|
|
194
|
+
if (!isThumb && columns !== 'auto' && columns > 0) {
|
|
175
195
|
const col = Math.max(1, Math.min(4, Math.floor(columns)));
|
|
176
196
|
gridAttrs['data-columns'] = String(col);
|
|
177
197
|
gridAttrs.style = {
|
|
@@ -180,19 +200,59 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
180
200
|
gap: 'var(--space-3)'
|
|
181
201
|
};
|
|
182
202
|
}
|
|
183
|
-
//
|
|
203
|
+
// Multi-select bookkeeping. Entries are keyed by path (fallback name); a
|
|
204
|
+
// locked/EACCES entry is never selectable — bulk mutations would fail on it.
|
|
205
|
+
const entryKeyOf = (f) => f.path || f.name;
|
|
206
|
+
const isLockedEntry = (f) => f.locked || f.permissions === 'EACCES'
|
|
207
|
+
|| (Array.isArray(f.permissions) && f.permissions.length === 0);
|
|
208
|
+
const selSet = marked instanceof Set ? marked : new Set(marked || []);
|
|
209
|
+
const selectableKeys = selectable ? visible.filter((f) => !isLockedEntry(f)).map(entryKeyOf) : [];
|
|
210
|
+
// Keyboard: roving focus over the open buttons inside the grid (rows and
|
|
211
|
+
// thumbnail cells share the pattern). Ctrl/Cmd+A selects all SHOWN rows.
|
|
184
212
|
const onKeyDown = (e) => {
|
|
185
213
|
const grid = e.currentTarget;
|
|
186
|
-
const opens = Array.from(grid.querySelectorAll('.ds-file-open:not([disabled])'));
|
|
187
|
-
if (!opens.length) return;
|
|
214
|
+
const opens = Array.from(grid.querySelectorAll('.ds-file-open:not([disabled]), .ds-file-cell-open:not([disabled])'));
|
|
188
215
|
const cur = opens.indexOf(document.activeElement);
|
|
189
216
|
if (e.key === 'ArrowDown') { e.preventDefault(); opens[Math.min(opens.length - 1, cur + 1)]?.focus(); }
|
|
190
217
|
else if (e.key === 'ArrowUp') { e.preventDefault(); (cur <= 0 ? opens[0] : opens[cur - 1])?.focus(); }
|
|
191
218
|
else if (e.key === 'Home') { e.preventDefault(); opens[0]?.focus(); }
|
|
192
219
|
else if (e.key === 'End') { e.preventDefault(); opens[opens.length - 1]?.focus(); }
|
|
193
220
|
else if (e.key === 'Backspace') { e.preventDefault(); onUp && onUp(); }
|
|
221
|
+
else if ((e.key === 'a' || e.key === 'A') && (e.ctrlKey || e.metaKey)
|
|
222
|
+
&& selectable && onSelectAll && selectableKeys.length) {
|
|
223
|
+
e.preventDefault(); onSelectAll(selectableKeys);
|
|
224
|
+
}
|
|
194
225
|
};
|
|
195
226
|
const head = sort ? FileSortHeader(sort) : null;
|
|
227
|
+
// Tri-state select-all over the selectable SHOWN rows (the cap label below
|
|
228
|
+
// already tells the user more rows exist beyond the window).
|
|
229
|
+
const selOfVisible = selectableKeys.filter((k) => selSet.has(k)).length;
|
|
230
|
+
const allState = selOfVisible === 0 ? 'false' : (selOfVisible === selectableKeys.length ? 'true' : 'mixed');
|
|
231
|
+
const selectAllCtl = (selectable && onSelectAll && selectableKeys.length)
|
|
232
|
+
? h('button', { key: 'selall', type: 'button', class: 'ds-file-selectall', role: 'checkbox',
|
|
233
|
+
'aria-checked': allState,
|
|
234
|
+
'aria-label': allState === 'true' ? 'clear selection' : 'select all ' + selectableKeys.length + ' shown files',
|
|
235
|
+
onclick: () => (allState === 'true' && onClearSelection) ? onClearSelection() : onSelectAll(selectableKeys) },
|
|
236
|
+
h('span', { 'aria-hidden': 'true' }, allState === 'true' ? '[x]' : allState === 'mixed' ? '[-]' : '[ ]'),
|
|
237
|
+
h('span', {}, 'all'))
|
|
238
|
+
: null;
|
|
239
|
+
// Density picker — list / compact / thumbnails. A radiogroup, not tabs:
|
|
240
|
+
// it switches presentation of the same content, not panels.
|
|
241
|
+
const densityCtl = onDensity
|
|
242
|
+
? h('div', { key: 'density', class: 'ds-density', role: 'radiogroup', 'aria-label': 'view density' },
|
|
243
|
+
...DENSITIES.map(([k, label]) => h('button', {
|
|
244
|
+
key: 'd-' + k, type: 'button', role: 'radio',
|
|
245
|
+
class: 'ds-density-btn' + (density === k ? ' active' : ''),
|
|
246
|
+
'aria-checked': density === k ? 'true' : 'false',
|
|
247
|
+
onclick: () => { if (density !== k) onDensity(k); },
|
|
248
|
+
}, label)))
|
|
249
|
+
: null;
|
|
250
|
+
const controlsKids = [selectAllCtl, head,
|
|
251
|
+
(selectAllCtl || head) && densityCtl ? h('span', { key: 'spread', class: 'spread' }) : null,
|
|
252
|
+
densityCtl].filter(Boolean);
|
|
253
|
+
const controls = controlsKids.length
|
|
254
|
+
? h('div', { class: 'ds-file-controls' }, ...controlsKids)
|
|
255
|
+
: null;
|
|
196
256
|
const filterBar = filter ? h('div', { class: 'ds-file-filter' },
|
|
197
257
|
h('input', {
|
|
198
258
|
class: 'ds-file-filter-input', type: 'search',
|
|
@@ -203,16 +263,32 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
203
263
|
// role=group not listbox: the rows contain real <button> action controls, so
|
|
204
264
|
// listbox/option semantics are invalid (an option can't host interactive
|
|
205
265
|
// children). Keyboard nav still works via roving focus over the open buttons.
|
|
206
|
-
const grid = h('div', {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
266
|
+
const grid = h('div', {
|
|
267
|
+
class: 'ds-file-grid' + (isThumb ? ' ds-file-grid-thumb' : ''),
|
|
268
|
+
role: 'group', 'aria-label': 'files', tabindex: '0',
|
|
269
|
+
// Always concrete (webjsx's attribute diff can leave a null-valued
|
|
270
|
+
// attribute unset when toggling away from the default).
|
|
271
|
+
'data-density': density || 'list',
|
|
272
|
+
onkeydown: onKeyDown, ...gridAttrs },
|
|
273
|
+
...visible.map((f, i) => isThumb
|
|
274
|
+
? FileCell({
|
|
275
|
+
key: f.path || f.name + i, f,
|
|
276
|
+
selectable, marked: selSet.has(entryKeyOf(f)),
|
|
277
|
+
onMark: onMark ? (opts) => onMark(f, opts) : null,
|
|
278
|
+
onOpen,
|
|
279
|
+
thumb: (thumbUrl && f.type === 'image') ? thumbUrl(f) : null,
|
|
280
|
+
})
|
|
281
|
+
: FileRow({
|
|
282
|
+
key: f.path || f.name + i,
|
|
283
|
+
name: f.name, type: f.type, size: f.size, modified: f.modified, code: f.code, active: f.active,
|
|
284
|
+
permissions: f.permissions, locked: f.locked,
|
|
285
|
+
actions: actions != null ? actions : undefined,
|
|
286
|
+
busy: busy != null ? !!busy : !!f.busy,
|
|
287
|
+
selectable, marked: selSet.has(entryKeyOf(f)),
|
|
288
|
+
onMark: onMark ? (opts) => onMark(f, opts) : null,
|
|
289
|
+
onOpen: onOpen ? () => onOpen(f) : null,
|
|
290
|
+
onAction: onAction ? (act) => onAction(act, f) : null
|
|
291
|
+
}))
|
|
216
292
|
);
|
|
217
293
|
// A count + "show more" affordance so a capped large dir reads as "more
|
|
218
294
|
// exist", not "this is everything". aria-live announces the shown/total.
|
|
@@ -224,11 +300,70 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
|
|
|
224
300
|
onclick: () => onShowMore(Math.min(files.length, limit + FILE_GRID_CAP)) },
|
|
225
301
|
'show ' + Math.min(FILE_GRID_CAP, files.length - limit) + ' more') : null)
|
|
226
302
|
: null;
|
|
227
|
-
return (
|
|
228
|
-
? h('div', { class: 'ds-file-listing' }, filterBar,
|
|
303
|
+
return (controls || filterBar || more)
|
|
304
|
+
? h('div', { class: 'ds-file-listing' }, filterBar, controls, grid, more)
|
|
229
305
|
: grid;
|
|
230
306
|
}
|
|
231
307
|
|
|
308
|
+
const DENSITIES = [['list', 'list'], ['compact', 'compact'], ['thumb', 'thumbnails']];
|
|
309
|
+
|
|
310
|
+
// FileCell — the thumbnail-density tile. Image entries show a real (lazy)
|
|
311
|
+
// thumbnail through the host's confined thumbUrl; everything else keeps its
|
|
312
|
+
// type icon. Same open/mark semantics as FileRow, same no-nesting rule.
|
|
313
|
+
function FileCell({ key, f = {}, selectable = false, marked = false, onMark, onOpen, thumb } = {}) {
|
|
314
|
+
const noAccess = f.locked || f.permissions === 'EACCES'
|
|
315
|
+
|| (Array.isArray(f.permissions) && f.permissions.length === 0);
|
|
316
|
+
const canOpen = onOpen && !noAccess;
|
|
317
|
+
const typeLabel = TYPE_LABELS[f.type] || 'file';
|
|
318
|
+
const kids = [
|
|
319
|
+
selectable ? h('button', {
|
|
320
|
+
key: 'mark', type: 'button',
|
|
321
|
+
class: 'ds-file-check ds-file-cell-check' + (marked ? ' is-marked' : ''),
|
|
322
|
+
role: 'checkbox', 'aria-checked': marked ? 'true' : 'false',
|
|
323
|
+
'aria-label': (marked ? 'unselect ' : 'select ') + f.name,
|
|
324
|
+
disabled: noAccess ? true : null,
|
|
325
|
+
onclick: onMark ? (e) => onMark({ range: !!e.shiftKey }) : null,
|
|
326
|
+
}, h('span', { 'aria-hidden': 'true' }, marked ? '[x]' : '[ ]')) : null,
|
|
327
|
+
h('button', {
|
|
328
|
+
key: 'open', type: 'button', class: 'ds-file-cell-open',
|
|
329
|
+
onclick: canOpen ? () => onOpen(f) : null,
|
|
330
|
+
disabled: canOpen ? null : true,
|
|
331
|
+
'aria-label': typeLabel + ': ' + f.name + (noAccess ? ' (no access)' : ''),
|
|
332
|
+
},
|
|
333
|
+
h('span', { class: 'ds-file-cell-media' },
|
|
334
|
+
thumb
|
|
335
|
+
? h('img', { class: 'ds-file-cell-thumb', src: thumb, alt: '', loading: 'lazy' })
|
|
336
|
+
: FileIcon({ type: f.type })),
|
|
337
|
+
h('span', { class: 'ds-file-cell-name', title: f.name }, f.name),
|
|
338
|
+
h('span', { class: 'ds-file-cell-meta' }, f.type === 'dir' ? 'folder' : fmtFileSize(f.size))),
|
|
339
|
+
].filter(Boolean);
|
|
340
|
+
return h('div', {
|
|
341
|
+
key,
|
|
342
|
+
class: 'ds-file-cell' + (marked ? ' is-marked' : '') + (f.active ? ' active' : '') + (noAccess ? ' is-locked' : ''),
|
|
343
|
+
'data-file-type': f.type,
|
|
344
|
+
}, ...kids);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// BulkBar — the act-on-selection strip shown while a multi-select is active.
|
|
348
|
+
// Host renders it above the grid; `actions` are [{ label, onClick, danger,
|
|
349
|
+
// disabled }]; `busy` disables everything while a bulk operation is in flight.
|
|
350
|
+
export function BulkBar({ count = 0, noun = 'file', nounPlural, actions = [], onClear, busy = false } = {}) {
|
|
351
|
+
if (!count) return null;
|
|
352
|
+
// 'entry' pluralizes to 'entries', not 'entrys' - handle the -y noun class
|
|
353
|
+
// unless the host passes an explicit plural.
|
|
354
|
+
const plural = nounPlural || (/[^aeiou]y$/.test(noun) ? noun.slice(0, -1) + 'ies' : noun + 's');
|
|
355
|
+
const kids = [
|
|
356
|
+
h('span', { key: 'count', class: 'ds-bulkbar-count', role: 'status', 'aria-live': 'polite' },
|
|
357
|
+
count + ' ' + (count === 1 ? noun : plural) + ' selected'),
|
|
358
|
+
...actions.map((a, i) => Btn({
|
|
359
|
+
key: 'bba' + i, danger: !!a.danger, disabled: busy || a.disabled,
|
|
360
|
+
onClick: a.onClick, children: a.label,
|
|
361
|
+
})),
|
|
362
|
+
onClear ? Btn({ key: 'bbclear', disabled: busy, onClick: onClear, children: 'clear selection' }) : null,
|
|
363
|
+
].filter(Boolean);
|
|
364
|
+
return h('div', { class: 'ds-bulkbar', role: 'toolbar', 'aria-label': 'bulk file actions', 'aria-busy': busy ? 'true' : null }, ...kids);
|
|
365
|
+
}
|
|
366
|
+
|
|
232
367
|
// Clickable column headers for FileGrid sort. Active column shows its direction
|
|
233
368
|
// as an ASCII caret word (asc/desc) - never a glyph arrow.
|
|
234
369
|
function FileSortHeader({ key: active = 'name', dir = 'asc', onSort } = {}) {
|
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 {
|