@kolkrabbi/kol-component 0.219.0 → 0.220.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.219.0",
3
+ "version": "0.220.0",
4
4
  "description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,7 +21,11 @@ export default function MediaTile({ preview, name, selected = false, markKey, on
21
21
  return (
22
22
  <div className={`kol-media-tile-item relative ${className}`.trim()} data-selected={selected || undefined} data-marquee-key={markKey}
23
23
  role="button" tabIndex={0} aria-pressed={selected}
24
- onClick={onClick} onDoubleClick={onDoubleClick} onContextMenu={onContextMenu}
24
+ /* THE SECOND CLICK OPENS (kol-client-olina 2026-09-23: double-click on a folder tile did nothing).
25
+ * The browser counted the double click (`detail: 2`) and never fired `dblclick` on a folder tile,
26
+ * so the open hung off an event that did not arrive. The click's own count is the signal. */
27
+ onClick={(e) => { if (e.detail === 2 && onDoubleClick) { onDoubleClick(e); return } onClick?.(e) }}
28
+ onContextMenu={onContextMenu}
25
29
  onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); onDoubleClick?.(e) } }}>
26
30
  <div className="kol-media-tile-thumb">{preview}</div>
27
31
  <span className="kol-media-tile-name">{name}</span>
@@ -325,9 +325,9 @@ function Row({
325
325
  </span>
326
326
  ) : (
327
327
  <span className="w-5 shrink-0 flex items-center justify-center text-oq-48">
328
- {/* a folder's mark is the only glyph a row carries now, so it is drawn at the box's
329
- size rather than floating in it (user 2026-09-23) */}
330
- <Icon name={icon} size={icon === 'folder' ? 18 : 14} />
328
+ {/* one glyph size for every row — a folder and a file side by side draw at the same size
329
+ (user 2026-09-25; the file glyph was 14 against the folder's 18) */}
330
+ <Icon name={icon} size={18} />
331
331
  </span>
332
332
  )}
333
333
 
@@ -209,7 +209,7 @@ function ImageFrame({ src }) {
209
209
  /* `depth` indents a row under the folder it was expanded from; `expanded`/`onToggle` draw the
210
210
  * disclosure twisty. A folder row without `onToggle` keeps the old behaviour exactly — one chevron
211
211
  * that navigates — so nothing that already renders these moves. */
212
- function FolderRow({ name, onClick, onDoubleClick, depth = 0, expanded, onToggle, meta, cols, onContextMenu, drag, dropFiles, selected, current, markKey }) {
212
+ function FolderRow({ name, icon = 'folder', onClick, onDoubleClick, depth = 0, expanded, onToggle, meta, cols, onContextMenu, drag, dropFiles, selected, current, markKey }) {
213
213
  const [over, setOver] = useState(false)
214
214
  /* A FOLDER IS A DROP TARGET. `drag` carries the page's move verb and the path this row is; the
215
215
  * row highlights only while something is actually over it, so an accidental hover reads as
@@ -246,7 +246,7 @@ function FolderRow({ name, onClick, onDoubleClick, depth = 0, expanded, onToggle
246
246
  {/* THE FOLDER GLYPH IS THE ONE ICON LEFT, so it fills its box (user 2026-09-23: *"only folder
247
247
  should use icon, and it should be bigger"*). Every other kind now renders its own bytes. */}
248
248
  <span data-hit {...hitDrag} className="kol-row-hit flex items-center gap-3 min-w-0">
249
- <span className="w-8 h-8 shrink-0 flex items-center justify-center text-oq-48"><Icon name="folder" size={26} /></span>
249
+ <span className="w-8 h-8 shrink-0 flex items-center justify-center text-oq-48"><Icon name={icon} size={26} /></span>
250
250
  {/* NO TRAILING SLASH (user 2026-09-22). `partition` hands folders back as `audio/` because a
251
251
  slash is what marks one in a flat key space; that is storage's spelling, not a label, and
252
252
  `ColumnBrowser` has always stripped it. The glyph says "folder". */}
@@ -326,7 +326,8 @@ function FileRow({ o, onClick, onDoubleClick, depth = 0, formatDate, thumb, cols
326
326
  {/* the hit — icon and name, as in the folder row above */}
327
327
  <span data-hit {...dragProps} className="kol-row-hit flex items-center gap-3 min-w-0">
328
328
  <span className="w-8 h-8 shrink-0 flex items-center justify-center text-oq-48 overflow-hidden rounded">
329
- {thumb ?? <Icon name={kindOf(o) === 'image' ? 'image' : 'file'} size={18} />}
329
+ {/* a file with no thumbnail draws its glyph at the FOLDER's size — one icon column (user 2026-09-25) */}
330
+ {thumb ?? <Icon name={kindOf(o) === 'image' ? 'image' : 'file'} size={26} />}
330
331
  </span>
331
332
  <span className="kol-mono-12 min-w-0 truncate text-fg-default">{o.displayKey ?? o.key.split('/').pop()}</span>
332
333
  </span>
@@ -354,12 +355,21 @@ function FolderPreview({ path, objects, width, formatDate }) {
354
355
  ['Size', formatSize(bytes)],
355
356
  ...(last ? [['Newest', formatDate(last) || '—']] : []),
356
357
  ]
358
+ return <ContainerPreview name={path.replace(/\/$/, '').split('/').pop()} icon="folder" facts={facts} width={width} />
359
+ }
360
+
361
+ /* ONE PREVIEW FOR EVERYTHING THAT HOLDS THINGS — a folder, a bucket, the title (user 2026-09-25:
362
+ * selecting a bucket or MEDIA left the pane empty). The glyph, the name, the facts; only the
363
+ * glyph and the facts differ. */
364
+ function ContainerPreview({ name, icon, facts, width }) {
357
365
  return (
358
366
  <div className="kol-column-browser-preview shrink-0 overflow-y-auto p-4 flex flex-col gap-4" style={{ width }}>
359
- <div className="w-full aspect-square bg-oq-04 rounded flex items-center justify-center">
360
- <Icon name="folder" size={64} className="text-oq-48" />
367
+ {/* the folder fills its box with the tile's inset (user 2026-09-24: a 64px glyph floated in
368
+ a 300px square while the grid's folder tile fills its own) */}
369
+ <div className="w-full aspect-square bg-oq-04 rounded flex items-center justify-center p-[5%]">
370
+ <Icon name={icon} size="100%" className="text-oq-48" />
361
371
  </div>
362
- <p className="kol-mono-12 text-fg-default break-all">{path.replace(/\/$/, '').split('/').pop()}</p>
372
+ <p className="kol-mono-12 text-fg-default break-all">{name}</p>
363
373
  <dl className="flex flex-col gap-1">
364
374
  {facts.map(([k, v]) => (
365
375
  <div key={k} className="flex justify-between gap-4 kol-mono-12">
@@ -474,7 +484,7 @@ export function MediaInspector({ files, index, onClose, onPrev, onNext, mediaUrl
474
484
  const [size, setSize] = useState(null)
475
485
  const shownDims = dims && dims.key === o?.key ? dims : null
476
486
  useEffect(() => {
477
- const onKey = (e) => { if (e.key === 'ArrowLeft') onPrev(); if (e.key === 'ArrowRight') onNext() }
487
+ const onKey = (e) => { if (e.shiftKey) return; if (e.key === 'ArrowLeft') onPrev(); if (e.key === 'ArrowRight') onNext() }
478
488
  window.addEventListener('keydown', onKey)
479
489
  return () => window.removeEventListener('keydown', onKey)
480
490
  }, [onPrev, onNext])
@@ -517,7 +527,12 @@ export function MediaInspector({ files, index, onClose, onPrev, onNext, mediaUrl
517
527
  <FullscreenOverlay open onClose={onClose} closeButton={false} scrim>
518
528
  {isImage(o.contentType) ? (
519
529
  <QuickLookFrame {...frame}>
520
- <img src={mediaUrl(o.key)} alt={o.displayKey} className="kol-quicklook-media" onLoad={(e) => setDims({ key: o.key, w: e.target.naturalWidth, h: e.target.naturalHeight })} />
530
+ {/* A VECTOR HAS NO PIXEL SIZE (user 2026-09-24: a 32px logo.svg opened as a 32px window). It
531
+ * is drawn in a 640 box — the window's max if that is smaller — and contained in it. */}
532
+ <img src={mediaUrl(o.key)} alt={o.displayKey}
533
+ className="kol-quicklook-media"
534
+ style={extOf(o.key) === 'svg' || o.contentType === 'image/svg+xml' ? { width: 'min(var(--kol-ql-max-w), 640px)', height: 'min(var(--kol-ql-media-h), 640px)' } : undefined}
535
+ onLoad={(e) => setDims({ key: o.key, w: e.target.naturalWidth, h: e.target.naturalHeight })} />
521
536
  </QuickLookFrame>
522
537
  ) : isVideo(o.contentType) ? (
523
538
  <VideoSheet key={o.key} src={mediaUrl(o.key)} poster={poster ? mediaUrl(poster) : undefined} onMeta={(m) => setDims({ key: o.key, ...m })} frame={frame} />
@@ -855,7 +870,7 @@ export function MediaLibraryBrowse({
855
870
  }) {
856
871
  const [ownPrefix, setOwnPrefix] = useState('')
857
872
  const prefix = prefixProp ?? ownPrefix
858
- const setPrefix = (v) => { if (prefixProp == null) setOwnPrefix(v); onPrefix?.(v) }
873
+ const setPrefix = (v) => { if (v) setAppRoot(false); if (prefixProp == null) setOwnPrefix(v); onPrefix?.(v) }
859
874
  const [appRoot, setAppRoot] = useState(false)
860
875
  const [ownBucket, setOwnBucket] = useState(bucket)
861
876
  const bucketId = bucket ?? ownBucket
@@ -869,8 +884,22 @@ export function MediaLibraryBrowse({
869
884
  * opens (user 2026-09-22, Finder's behaviour — "clicking shouldnt automatically open it"). The
870
885
  * chevron still expands in place, which is the third thing and always was. */
871
886
  const [pickedFolder, setPickedFolder] = useState(null)
872
- const pickFile = (o) => { setPickedFolder(null); setPickedFile(o) }
873
- const pickFolder = (path) => { setPickedFile(null); setPickedFolder(path) }
887
+ const pickFile = (o) => { setAppRoot(false); setPickedFolder(null); setPickedFile(o) }
888
+ const pickFolder = (path) => { setAppRoot(false); setPickedFile(null); setPickedFolder(path) }
889
+ /* AT THE TITLE ROOT IS DERIVED, NOT STORED (kol-client-olina 2026-09-23: rows and grid picked a
890
+ * file under `brand/logos/` and the crumb still read the title alone). `appRoot` was a second
891
+ * source of truth for the location, and only the columns ever cleared it. A path or a pick lives
892
+ * inside a bucket, so either one means you are not at the title — a stale flag can no longer
893
+ * hide where you are. Everything below reads this, never `appRoot`. */
894
+ const atTitleRoot = appRoot && !prefix && !pickedFile && !pickedFolder
895
+ /* ABOVE THE TITLE (user 2026-09-24: *"this is not the top root folder … in column view I can see
896
+ * the root MEDIA"*). Column 0 is a level whose one item is the title; rows and grid had no such
897
+ * level, so the bucket list was their top and the title never appeared as a thing. `appRoot` is
898
+ * `'top'` there — the same state, one level higher, derived the same way. */
899
+ const atTop = atTitleRoot && appRoot === 'top'
900
+ /* a BUCKET is a folder with another icon: a click highlights it, a double-click opens it */
901
+ const [pickedBucket, setPickedBucket] = useState(null)
902
+ const ROOT_KEY = '\u0000title'
874
903
  /* IN THE ROWS, WHAT YOU PICK SETS WHERE YOU ARE (user 2026-09-23: *"that files folder becomes the
875
904
  * default NOT the previous folder"*). The list is rooted at the top, so a pick can sit in any open
876
905
  * folder; the current folder follows it to the pick's parent. Whatever the old path had open
@@ -1126,7 +1155,14 @@ export function MediaLibraryBrowse({
1126
1155
  }
1127
1156
  /* stepping into a folder drops the pick — the preview would otherwise describe something no
1128
1157
  * longer in the list */
1129
- const goFolder = (path) => { setPickedFile(null); setPickedFolder(null); setPrefix(path) }
1158
+ /* GOING UP SELECTS THE FOLDER YOU CAME FROM (user 2026-09-24: back from `01-shoots` landed on
1159
+ * `img/` with nothing selected and an empty preview — every file manager lights the child you
1160
+ * left). One rule for every way up — ⌘↑ and the crumbs both come through here. */
1161
+ const goFolder = (path) => {
1162
+ const child = path !== prefix && prefix.startsWith(path) ? `${path}${prefix.slice(path.length).split('/')[0]}/` : null
1163
+ setPickedFile(null); setPickedFolder(null); setPrefix(path)
1164
+ if (child) { setRowSelection(new Set([child])); setPickedFolder(child); selectAnchorRef.current = child; selectCursorRef.current = null }
1165
+ }
1130
1166
  /* ONE "GO UP" FOR ALL THREE VIEWS (kol-client-olina 2026-09-23: *"3 different stops for 3
1131
1167
  * modes … they should all go all the way to the shared root folder"*): parent folder → … → the
1132
1168
  * bucket's top → the title root, which is what `onHome` does. With one bucket and no
@@ -1134,7 +1170,17 @@ export function MediaLibraryBrowse({
1134
1170
  * the same path with ArrowLeft. */
1135
1171
  const goUp = () => {
1136
1172
  if (prefix) { goFolder(dirOf(prefix)); return }
1137
- if (!single && !appRoot) { setAppRoot(true); setPickedFile(null); setPickedFolder(null) }
1173
+ if (!single && !atTitleRoot) goTitleRoot()
1174
+ else if (!single && !atTop) goTop()
1175
+ }
1176
+ /* THE TITLE ROOT CLEARS EVERYTHING that would say you are somewhere else — a pick, a selection —
1177
+ * or the derivation above reads you as still inside the bucket */
1178
+ const goTop = () => { setAppRoot('top'); setPickedFile(null); setPickedFolder(null); setRowSelection(new Set()); setPickedBucket(ROOT_KEY); setPrefix('') }
1179
+ const goTitleRoot = () => { setAppRoot(true); setPickedFile(null); setPickedFolder(null); setPickedBucket(atTitleRoot ? null : (bucketMeta.id ?? bucketMeta.label)); setRowSelection(new Set()); setPrefix('') }
1180
+ /* opening a bucket from the title root, the step the columns' column 1 is */
1181
+ const openBucket = (b) => {
1182
+ setAppRoot(false); setPickedFile(null); setPickedFolder(null); setPickedBucket(null); setRowSelection(new Set())
1183
+ if (b.id != null && b.id !== bucketMeta.id) switchBucket(b.id, ''); else setPrefix('')
1138
1184
  }
1139
1185
  /* SPACE IS QUICK LOOK IN EVERY VIEW (user 2026-09-23: *"spacebar preview only works in column,
1140
1186
  * fix that. CONSISTENCY"*). `ColumnBrowser` has owned that key since it shipped, so the columns
@@ -1185,6 +1231,8 @@ export function MediaLibraryBrowse({
1185
1231
  }
1186
1232
  const spaceQuickLook = () => {
1187
1233
  const key = pickedFile?.key ?? pickedFolder ?? (rowSelection.size === 1 ? [...rowSelection][0] : null)
1234
+ /* a picked bucket or the title Quick-Looks like any folder (user 2026-09-25) */
1235
+ if (!key && previewRoot) { setQuickLookFolder(ROOT_KEY); return }
1188
1236
  if (!key) return
1189
1237
  /* A FOLDER QUICK-LOOKS TOO (user 2026-09-23: *"anything can be quick viewed, also folders"*) —
1190
1238
  * the same pane the preview column draws, at overlay size. */
@@ -1212,7 +1260,14 @@ export function MediaLibraryBrowse({
1212
1260
  const gridOrderRef = useRef([])
1213
1261
  const arrowNav = (e) => {
1214
1262
  if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return false
1215
- if (view === 'columns' || quickLook || quickLookFolder || searchOpen) return false
1263
+ /* ⇧+ARROWS KEEP SELECTING UNDER QUICK LOOK (user 2026-09-24), Finder's: the selection grows
1264
+ * underneath and the window pages through it */
1265
+ if (view === 'columns' || (quickLook && !e.shiftKey) || quickLookFolder || searchOpen) return false
1266
+ if (atTitleRoot && !single) {
1267
+ if (e.metaKey && e.key === 'ArrowUp') goUp()
1268
+ else if (e.metaKey && e.key === 'ArrowDown') { if (atTop) goTitleRoot(); else openBucket(bucketList.find((b) => (b.id ?? b.label) === pickedBucket) ?? bucketMeta) }
1269
+ return true
1270
+ }
1216
1271
  const cur = selectCursorRef.current ?? selectAnchorRef.current ?? [...rowSelection].pop() ?? null
1217
1272
  /* ⇧ + ARROW EXTENDS (user 2026-09-23: *"shift up down or left right … should also make
1218
1273
  * selection"*): the anchor stays where the run began, the cursor moves, and the selection is
@@ -1239,18 +1294,29 @@ export function MediaLibraryBrowse({
1239
1294
  /* BEFORE the empty-list bail-out: a folder that holds only subfolders has an empty wall, and
1240
1295
  * ⌘↑ was swallowed there */
1241
1296
  if (e.metaKey && e.key === 'ArrowUp') { goUp(); return true }
1242
- const list = gridOrderRef.current
1243
- if (!list.length) return true
1244
- const i = list.findIndex((f) => f.key === cur)
1245
- if (e.metaKey && e.key === 'ArrowDown') { if (i !== -1) openQuickLook({ files: list, index: i }); return true }
1297
+ /* THE ARROWS WALK WHAT IS DRAWN (user 2026-09-24: they skipped every folder tile) — the
1298
+ * folders, then the files, in the order on screen. The list was the files alone, from before
1299
+ * the grid drew folders. */
1300
+ const files = gridOrderRef.current
1301
+ const items = [...(settings.flat ? [] : folders.map((f) => ({ key: prefix + f, folder: true }))), ...files.map((o) => ({ key: o.key, o }))]
1302
+ if (!items.length) return true
1303
+ const i = items.findIndex((it) => it.key === cur)
1304
+ if (e.metaKey && e.key === 'ArrowDown') {
1305
+ const it = items[i]
1306
+ if (it?.folder) goFolder(it.key)
1307
+ else if (it) openQuickLook({ files, index: Math.max(0, files.findIndex((f) => f.key === it.key)) })
1308
+ return true
1309
+ }
1246
1310
  const box = gridMarquee.ref.current
1247
- const cols = box ? getComputedStyle(box).gridTemplateColumns.split(' ').filter(Boolean).length : 1
1311
+ /* the band lives on the pane now; the column count is the tile grid's, inside it */
1312
+ const tiles = box?.querySelector('.grid')
1313
+ const cols = tiles ? getComputedStyle(tiles).gridTemplateColumns.split(' ').filter(Boolean).length : 1
1248
1314
  const step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -cols, ArrowDown: cols }[e.key]
1249
- const o = list[i === -1 ? 0 : Math.max(0, Math.min(list.length - 1, i + step))]
1250
- if (e.shiftKey && extend(list.map((f) => f.key), o.key)) { reveal(box, o.key); return true }
1251
- setRowSelection(new Set([o.key])); selectAnchorRef.current = o.key; selectCursorRef.current = null
1252
- setPickedFolder(null); setPickedFile(o)
1253
- reveal(box, o.key)
1315
+ const it = items[i === -1 ? 0 : Math.max(0, Math.min(items.length - 1, i + step))]
1316
+ if (e.shiftKey && extend(items.map((x) => x.key), it.key)) { reveal(box, it.key); return true }
1317
+ setRowSelection(new Set([it.key])); selectAnchorRef.current = it.key; selectCursorRef.current = null
1318
+ if (it.folder) { setPickedFile(null); setPickedFolder(it.key) } else { setPickedFolder(null); setPickedFile(it.o) }
1319
+ reveal(box, it.key)
1254
1320
  return true
1255
1321
  }
1256
1322
  if (view === 'rows') {
@@ -1298,6 +1364,18 @@ export function MediaLibraryBrowse({
1298
1364
  if (e.key === '/' && !e.metaKey && !e.ctrlKey) { e.preventDefault(); setSearchOpen(true) }
1299
1365
  if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setSearchOpen(true) }
1300
1366
  if (arrowNav(e)) { e.preventDefault(); return }
1367
+ /* ENTER OPENS THE SELECTED FOLDER, ⇧ENTER GOES TO THE PARENT (user 2026-09-24). The same
1368
+ * two moves as a double-click and ⌘↑, so the child you leave is still the one selected.
1369
+ * A focused tile has already opened itself on Enter (it calls preventDefault). */
1370
+ if (e.key === 'Enter' && !e.metaKey && !e.ctrlKey && !e.altKey && !e.defaultPrevented && !quickLook && !quickLookFolder && !searchOpen) {
1371
+ if (e.shiftKey) { e.preventDefault(); goUp(); return }
1372
+ if (view !== 'columns') {
1373
+ const root = rootItems.find((it) => it.key === pickedBucket)
1374
+ const sel = rowSelection.size === 1 ? [...rowSelection][0] : pickedFolder
1375
+ if (root) { e.preventDefault(); root.open(); return }
1376
+ if (sel?.endsWith('/')) { e.preventDefault(); goFolder(sel); return }
1377
+ }
1378
+ }
1301
1379
  /* the columns keep their own space handler — theirs knows the column's cursor */
1302
1380
  if (e.key === ' ' && view !== 'columns' && !searchOpen) {
1303
1381
  e.preventDefault()
@@ -1312,6 +1390,16 @@ export function MediaLibraryBrowse({
1312
1390
 
1313
1391
  const [quickLook, setQuickLook] = useState(null)
1314
1392
  const [quickLookFolder, setQuickLookFolder] = useState(null)
1393
+ /* …and the open window takes the new selection as its set, on the file the cursor reached */
1394
+ useEffect(() => {
1395
+ if (!quickLook || rowSelection.size < 2) return
1396
+ const order = view === 'grid' ? gridOrderRef.current : sortedObjects
1397
+ const files = order.filter((o) => rowSelection.has(o.key) && !o.key.endsWith('/'))
1398
+ if (files.length < 2) return
1399
+ if (files.length === quickLook.files.length && files.every((f, i) => f.key === quickLook.files[i].key)) return
1400
+ const at = files.findIndex((f) => f.key === selectCursorRef.current)
1401
+ setQuickLook({ files, index: at === -1 ? 0 : at })
1402
+ }, [rowSelection]) // eslint-disable-line react-hooks/exhaustive-deps
1315
1403
  const columnsRef = useRef(null)
1316
1404
  /* `flat` IS THE WALL'S, NOT THIS PAGE'S (user 2026-09-22: "there is no option to switch to flat
1317
1405
  * mode, bc its not in files view. this is row mode.. this makes no sense"). It reached here from
@@ -1346,6 +1434,14 @@ export function MediaLibraryBrowse({
1346
1434
  const previewWidth = settings.columnWidths?.preview ?? 320
1347
1435
  const previewFile = pickedFile && pickedFile.key.startsWith(prefix) ? pickedFile : null
1348
1436
  const previewFolder = pickedFolder && pickedFolder.startsWith(prefix) ? pickedFolder : null
1437
+ /* a bucket's totals come from `folderTree` when the consumer passes one; the open bucket can
1438
+ * always count its own listing */
1439
+ const bucketTotals = (id) => {
1440
+ const t = folderTree?.[id]
1441
+ if (t && t.files != null) return { files: t.files, bytes: t.bytes ?? 0 }
1442
+ if (id === bucketMeta.id) return { files: objects.length, bytes: objects.reduce((n, o) => n + (o.size || 0), 0) }
1443
+ return null
1444
+ }
1349
1445
  /* THE PREVIEW PANE, ONE FUNCTION FOR THE ROWS AND THE GRID (user 2026-09-23: *"why did you not
1350
1446
  * treat this like column and row, with container padding + preview pane? CONSISTENCY"*). Several
1351
1447
  * selected → the stack; a file → its preview; a folder → its summary; nothing → the reserved
@@ -1363,6 +1459,8 @@ export function MediaLibraryBrowse({
1363
1459
  }} />
1364
1460
  ) : previewFolder ? (
1365
1461
  <FolderPreview key={previewFolder} path={previewFolder} objects={objects} width={previewWidth} formatDate={formatDate} />
1462
+ ) : previewRoot ? (
1463
+ <ContainerPreview key={previewRoot.key} name={previewRoot.name} icon={previewRoot.icon} facts={previewRoot.facts} width={previewWidth} />
1366
1464
  ) : (
1367
1465
  <div className="kol-column-browser-preview shrink-0" style={{ width: previewWidth }} />
1368
1466
  )
@@ -1449,14 +1547,14 @@ export function MediaLibraryBrowse({
1449
1547
  if (!root) return undefined
1450
1548
  const id = requestAnimationFrame(() => root.scrollTo({ left: root.scrollWidth, behavior: 'smooth' }))
1451
1549
  return () => cancelAnimationFrame(id)
1452
- }, [folderView, prefix, appRoot, pickedFile, bucketMeta.id])
1550
+ }, [folderView, prefix, atTitleRoot, pickedFile, bucketMeta.id])
1453
1551
 
1454
1552
  if (error) return <p className="kol-mono-12 text-ui-error">Error: {error}</p>
1455
1553
  /* NOT SELECTABLE (user 2026-09-23, superseding 2026-09-22's selectable crumbs: *"instead of this
1456
1554
  * highlight, could we make a hover click to copy path?"*). A drag across the line painted the
1457
1555
  * browser's selection block over it; the path is one click on the current crumb now. */
1458
1556
  const crumbCls = (active) => `cursor-pointer select-none transition-colors ${active ? 'text-oq-96' : 'text-oq-48 hover:text-oq-64'}`
1459
- const fullPath = (tail = '') => [ROOT, ...(appRoot ? [] : [label, ...crumbs]), ...(tail ? [tail] : [])].join('/')
1557
+ const fullPath = (tail = '') => [ROOT, ...(atTitleRoot ? [] : [label, ...crumbs]), ...(tail ? [tail] : [])].join('/')
1460
1558
 
1461
1559
  /* ── THE WALL'S LIST, on the same surface (2026-09-22) ──────────────────────────────────────
1462
1560
  * `flat` belongs to the FILE views and not to the tree (that ruling stands): a wall shows the
@@ -1481,6 +1579,34 @@ export function MediaLibraryBrowse({
1481
1579
  * key space, or the subset the filter bar left standing. `wallPane` is the same two file views
1482
1580
  * the Files page rendered, in a pane of the browser's own height so the page does not jump
1483
1581
  * between views. */
1582
+ /* AT THE TITLE ROOT, ROWS AND GRID LIST THE BUCKETS (same ticket: they listed the bucket's
1583
+ * folders under a crumb that named the title). One row / tile per bucket; a click opens it. */
1584
+ const bucketList = buckets.length ? buckets : [bucketMeta]
1585
+ const titleLevel = atTitleRoot && !single
1586
+ /* what the root levels hold: above the title, the title as one folder; inside it, the buckets.
1587
+ * Each is a folder with its own icon — a click highlights it, a double-click opens it. */
1588
+ const previewRoot = (() => {
1589
+ if (!titleLevel || pickedBucket == null) return null
1590
+ if (atTop && pickedBucket === ROOT_KEY) {
1591
+ const all = bucketList.map((b) => bucketTotals(b.id)).filter(Boolean)
1592
+ return { key: ROOT_KEY, name: ROOT, icon: 'folder', facts: [
1593
+ ['Kind', 'folder'],
1594
+ ['Buckets', `${bucketList.length}`],
1595
+ ...(all.length === bucketList.length ? [['Items', `${all.reduce((n, x) => n + x.files, 0)}`], ['Size', formatSize(all.reduce((n, x) => n + x.bytes, 0))]] : []),
1596
+ ] }
1597
+ }
1598
+ const b = bucketList.find((x) => (x.id ?? x.label) === pickedBucket)
1599
+ if (!b) return null
1600
+ const tot = bucketTotals(b.id)
1601
+ return { key: pickedBucket, name: b.label || 'bucket', icon: 'database', facts: [
1602
+ ['Kind', 'bucket'],
1603
+ ...(tot ? [['Items', `${tot.files}`], ['Size', formatSize(tot.bytes)]] : []),
1604
+ ...(b.writable != null ? [['Access', b.writable ? 'read · write' : 'read-only']] : []),
1605
+ ] }
1606
+ })()
1607
+ const rootItems = !titleLevel ? [] : atTop
1608
+ ? [{ key: ROOT_KEY, name: ROOT, icon: 'folder', open: goTitleRoot }]
1609
+ : bucketList.map((b) => ({ key: b.id ?? b.label, name: b.label || 'bucket', icon: 'database', open: () => openBucket(b) }))
1484
1610
  const treeBody = (keep) => {
1485
1611
  const treeObjects = keep ? sortedObjects.filter((o) => keep.has(o.key)) : sortedObjects
1486
1612
  return (
@@ -1488,13 +1614,13 @@ export function MediaLibraryBrowse({
1488
1614
  <div ref={columnsRef} className="relative">
1489
1615
  <ColumnBrowser
1490
1616
  autoFocus={autoFocus}
1491
- className={appRoot ? 'is-root' : ''}
1617
+ className={atTitleRoot ? 'is-root' : ''}
1492
1618
  height={settings.columnHeight}
1493
1619
  onHeightChange={(px) => setSettings({ ...settings, columnHeight: px })}
1494
1620
  columnWidths={settings.columnWidths}
1495
1621
  onColumnResize={(i, px) => setSettings({ ...settings, columnWidths: { ...(settings.columnWidths ?? {}), [i]: px } })}
1496
1622
  objects={treeObjects.filter((o) => !isSystemFile(o.key)).map((o) => ({ ...o, key: `${VROOT}${o.key}` }))}
1497
- prefix={single ? prefix : (appRoot ? `${ROOT}/` : `${VROOT}${prefix}`)}
1623
+ prefix={single ? prefix : (atTitleRoot ? `${ROOT}/` : `${VROOT}${prefix}`)}
1498
1624
  onPrefix={(v) => {
1499
1625
  /* single: the browser's path IS the bucket path, no segment to strip */
1500
1626
  if (single) { setAppRoot(false); setPrefix(v); return }
@@ -1585,9 +1711,9 @@ export function MediaLibraryBrowse({
1585
1711
  <div className={`flex-1 min-w-0 flex flex-col${showRowPreview ? ' border-r' : ''}`} style={{ borderColor: 'var(--kol-oq-08)' }}>
1586
1712
  <ul ref={rowsMarquee.ref} {...rowsMarquee.props}
1587
1713
  /* the background deselects, here as in the columns */
1588
- onClick={(e) => { if (!e.target.closest('[data-marquee-key]')) { setRowSelection(new Set()); setPickedFile(null); setPickedFolder(null) } }}
1714
+ onClick={(e) => { if (!e.target.closest('[data-marquee-key]')) { setRowSelection(new Set()); setPickedFile(null); setPickedFolder(null); setPickedBucket(null) } }}
1589
1715
  className="relative kol-column-browser-column kol-row-browser flex-1 min-w-0 flex flex-col overflow-y-auto"
1590
- onContextMenu={(e) => menu.openAt(e, { type: 'level', path: prefix })}
1716
+ onContextMenu={titleLevel ? undefined : (e) => menu.openAt(e, { type: 'level', path: prefix })}
1591
1717
  onDragOver={(e) => {
1592
1718
  const files = dropFilesTo && isFileDrag(e)
1593
1719
  const d = !isFileDrag(e) && dragFor?.(prefix)
@@ -1609,6 +1735,11 @@ export function MediaLibraryBrowse({
1609
1735
  d.onDrop(prefix)
1610
1736
  }}>
1611
1737
  {rowsMarquee.rect && <div className="kol-marquee" style={rowsMarquee.rect} />}
1738
+ {titleLevel ? rootItems.map((it) => (
1739
+ <FolderRow key={it.key} name={it.name} icon={it.icon} cols={rowCols}
1740
+ selected={pickedBucket === it.key}
1741
+ onClick={(e) => { e.stopPropagation(); setPickedBucket(it.key) }} onDoubleClick={it.open} />
1742
+ )) : <>
1612
1743
  {rowLevel('', treeObjects).folders.map((f) => {
1613
1744
  const path = f
1614
1745
  const open = rowsExpanded.has(path)
@@ -1654,6 +1785,7 @@ export function MediaLibraryBrowse({
1654
1785
  if (!selectRow(o.key, e, visibleRowOrder(treeObjects))) rowPickFile(o)
1655
1786
  }} />
1656
1787
  ))}
1788
+ </>}
1657
1789
  </ul>
1658
1790
  </div>
1659
1791
  {showRowPreview && previewPane()}
@@ -1669,15 +1801,34 @@ export function MediaLibraryBrowse({
1669
1801
  * inside it, the same preview pane beside them, the same count line under it — with the size
1670
1802
  * slider at its right end, Finder's. */
1671
1803
  const tileSize = settings.tileSize ?? 180
1804
+ /* the tile size is the GRID's, not a level's (user 2026-09-24: the slider vanished at the root
1805
+ * levels) — one control, drawn wherever the grid is */
1806
+ const sizeSlider = (
1807
+ <input type="range" className="slider-black w-32 cursor-pointer" min={100} max={360} step={20} value={tileSize}
1808
+ onChange={(e) => setSettings({ ...settings, tileSize: Number(e.target.value) })} aria-label="Tile size" />
1809
+ )
1672
1810
  const wallPane = (files) => (
1673
1811
  <div className="flex flex-col gap-2" style={{ height: settings.columnHeight ?? SETTINGS_BASE.columnHeight }}>
1674
1812
  <div className="relative border rounded flex overflow-hidden flex-1 min-h-0" style={{ borderColor: 'var(--kol-oq-08)' }}>
1675
- <div className={`flex-1 min-w-0 overflow-y-auto p-4${showGridPreview ? ' border-r' : ''}`} style={{ borderColor: 'var(--kol-oq-08)' }}
1813
+ {/* THE MARQUEE IS THE PANE'S, not the tile grid's (user 2026-09-23: a drag only started where
1814
+ * there were tiles — the grid is as tall as its tiles, the pane is the whole frame). The rows'
1815
+ * band has always been on their full-height list; this is the grid catching up. */}
1816
+ <div ref={gridMarquee.ref} {...gridMarquee.props}
1817
+ className={`relative flex-1 min-w-0 overflow-y-auto p-4${showGridPreview ? ' border-r' : ''}`} style={{ borderColor: 'var(--kol-oq-08)' }}
1676
1818
  /* THE CONTAINER IS A LEVEL, like a column (kol-client-olina 2026-09-23: *"context menu anywhere
1677
1819
  * by right click"*). A tile's own menu stops the event first, so it still wins. */
1678
- onContextMenu={(e) => menu.openAt(e, { type: 'level', path: prefix })}
1679
- onClick={(e) => { if (!e.target.closest('[data-marquee-key]')) { setRowSelection(new Set()); setPickedFile(null); setPickedFolder(null) } }}>
1680
- <WallBody files={files} layout="grid" tiles="media" cardMin={tileSize} marquee={gridMarquee} onSorted={(list) => { gridOrderRef.current = list }}
1820
+ onContextMenu={titleLevel ? undefined : (e) => menu.openAt(e, { type: 'level', path: prefix })}
1821
+ onClick={(e) => { if (!e.target.closest('[data-marquee-key]')) { setRowSelection(new Set()); setPickedFile(null); setPickedFolder(null); setPickedBucket(null) } }}>
1822
+ {gridMarquee.rect && <div className="kol-marquee" style={gridMarquee.rect} />}
1823
+ {titleLevel ? (
1824
+ <div className="grid gap-x-3 gap-y-5" style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${tileSize}px, 1fr))` }}>
1825
+ {rootItems.map((it) => (
1826
+ <MediaTile key={it.key} preview={<Icon name={it.icon} size="100%" className="text-oq-48" />} name={it.name}
1827
+ selected={pickedBucket === it.key}
1828
+ onClick={(e) => { e.stopPropagation(); setPickedBucket(it.key) }} onDoubleClick={it.open} />
1829
+ ))}
1830
+ </div>
1831
+ ) : <WallBody files={files} layout="grid" tiles="media" cardMin={tileSize} onSorted={(list) => { gridOrderRef.current = list }}
1681
1832
  /* THE GRID DRAWS THE LEVEL'S SUBFOLDERS, ahead of the files (same source: kol-client-olina
1682
1833
  * 2026-09-23 — `projects/` read `0 files` over a blank pane). `flat` is the subtree's files
1683
1834
  * and no folders, as it was. */
@@ -1685,7 +1836,7 @@ export function MediaLibraryBrowse({
1685
1836
  onOpenFolder={goFolder}
1686
1837
  onPickFolder={(path, e) => { if (selectRow(path, e, [...folders.map((f) => prefix + f), ...files.map((f) => f.key)])) return; rowPickFolder(path) }}
1687
1838
  onFolderContextMenu={(e, path) => menu.openAt(e, { type: 'folder', path, targets: targetsFor(path) })}
1688
- onBackgroundClick={() => { setRowSelection(new Set()); setPickedFile(null); setPickedFolder(null) }}
1839
+ onBackgroundClick={() => { setRowSelection(new Set()); setPickedFile(null); setPickedFolder(null); setPickedBucket(null) }}
1689
1840
  sortBy={settings.sortBy} sortDir={settings.sortDir}
1690
1841
  pageSize={settings.pageSize} listId={`${prefix}|${view}|${settings.flat}|${refreshKey}|${bucketMeta.id}`}
1691
1842
  mediaUrl={mediaUrl} downloadUrl={downloadUrl} keySet={keySet} videoPreview={settings.videoPreview} formatDate={formatDate}
@@ -1698,7 +1849,7 @@ export function MediaLibraryBrowse({
1698
1849
  onOpen={(o, files) => openQuickLook({ files, index: Math.max(0, files.findIndex((f) => f.key === o.key)) })}
1699
1850
  /* COPY AND DOWNLOAD LIVE IN THE MENU AND QUICK LOOK (user 2026-09-23) — as in the rows and the
1700
1851
  * columns. The tile carries no buttons. */
1701
- onContextMenu={(e, o) => menu.openAt(e, { type: 'file', path: o.key, o, targets: targetsFor(o.key) })} />
1852
+ onContextMenu={(e, o) => menu.openAt(e, { type: 'file', path: o.key, o, targets: targetsFor(o.key) })} />}
1702
1853
  </div>
1703
1854
  {showGridPreview && previewPane()}
1704
1855
  </div>
@@ -1732,9 +1883,9 @@ export function MediaLibraryBrowse({
1732
1883
 
1733
1884
  return (
1734
1885
  <div {...press} className={`flex flex-col gap-6 ${className}`.trim()}>
1735
- <LibraryHeader title={title} buckets={buckets} bucketId={bucketMeta.id} appRoot={appRoot} bucketMeta={bucketMeta} writable={writable} headerActions={headerActions} headerTrailing={headerTrailing} onTrash={trash ? () => setTrashOpen(true) : undefined}
1736
- onHome={() => { setAppRoot(true); setPickedFile(null); setPrefix('') }}
1737
- onBucket={(v) => { if (v === 'all') { setAppRoot(true); setPrefix('') } else { setAppRoot(false); switchBucket(v) } }}
1886
+ <LibraryHeader title={title} buckets={buckets} bucketId={bucketMeta.id} appRoot={atTitleRoot} bucketMeta={bucketMeta} writable={writable} headerActions={headerActions} headerTrailing={headerTrailing} onTrash={trash ? () => setTrashOpen(true) : undefined}
1887
+ onHome={goTitleRoot}
1888
+ onBucket={(v) => { if (v === 'all') goTitleRoot(); else { setAppRoot(false); switchBucket(v) } }}
1738
1889
  onSettings={() => setSettingsOpen(true)} />
1739
1890
 
1740
1891
  {/* THE BANNER SLOT — anything that must sit directly under the header and above the body.
@@ -1760,16 +1911,16 @@ export function MediaLibraryBrowse({
1760
1911
  {/* the elided line — first segment, an ellipsis for anything between,
1761
1912
  * and the current one. Below `md` only. */}
1762
1913
  <div className="flex md:hidden items-center gap-2 kol-mono-12 min-w-0">
1763
- <button className={crumbCls(appRoot)} onClick={() => { setAppRoot(true); setPickedFile(null); setPrefix('') }}>{ROOT}</button>
1764
- {!appRoot && bucketMeta.id && crumbs.length > 1 && (
1914
+ <button className={crumbCls(atTitleRoot && !atTop)} onClick={single ? goTitleRoot : goTop}>{ROOT}</button>
1915
+ {!atTitleRoot && bucketMeta.id && crumbs.length > 1 && (
1765
1916
  <><span className="text-oq-32">/</span><span className="text-oq-32">…</span></>
1766
1917
  )}
1767
- {!appRoot && bucketMeta.id && (
1918
+ {!atTitleRoot && bucketMeta.id && (
1768
1919
  <span className="flex items-center gap-2 min-w-0">
1769
1920
  <span className="text-oq-32">/</span>
1770
1921
  <button
1771
1922
  className={`${crumbCls(true)} truncate`}
1772
- onClick={() => setPrefix(crumbs.length ? crumbs.join('/') + '/' : '')}
1923
+ onClick={() => goFolder(crumbs.length ? crumbs.join('/') + '/' : '')}
1773
1924
  >
1774
1925
  {(crumbs[crumbs.length - 1] ?? label).toUpperCase()}
1775
1926
  </button>
@@ -1777,33 +1928,48 @@ export function MediaLibraryBrowse({
1777
1928
  )}
1778
1929
  </div>
1779
1930
  <div className="hidden md:flex items-center gap-2 kol-mono-12">
1780
- <button className={crumbCls(appRoot)} onClick={() => { setAppRoot(true); setPickedFile(null); setPrefix('') }}>{ROOT}</button>
1781
- {!appRoot && bucketMeta.id && (
1931
+ <button className={crumbCls(atTitleRoot && (!atTop || pickedBucket === ROOT_KEY))} onClick={single ? goTitleRoot : goTop}>{ROOT}</button>
1932
+ {/* A PICKED BUCKET ENDS THE CRUMB at the title level (user 2026-09-24: a selected folder
1933
+ * must show in the crumb, as a selected file does — a bucket is a folder) */}
1934
+ {titleLevel && !atTop && pickedBucket && rootItems.some((it) => it.key === pickedBucket) && (
1935
+ <span className="flex items-center gap-2"><span className="text-oq-32 select-none">/</span>
1936
+ <span className={crumbCls(true)}>{rootItems.find((it) => it.key === pickedBucket).name.toUpperCase()}</span>
1937
+ </span>
1938
+ )}
1939
+ {!atTitleRoot && bucketMeta.id && (
1782
1940
  <span className="flex items-center gap-2">
1783
1941
  <span className="text-oq-32 select-none">/</span>
1784
- {crumbs.length === 0 && !(folderView === 'columns' && pickedFile)
1942
+ {crumbs.length === 0 && !pickedFile && !pickedFolder
1785
1943
  ? <CopyCrumb className={crumbCls(true)} label={label.toUpperCase()} path={fullPath()} />
1786
- : <button className={crumbCls(false)} onClick={() => setPrefix('')}>{label.toUpperCase()}</button>}
1944
+ : <button className={crumbCls(false)} onClick={() => goFolder('')}>{label.toUpperCase()}</button>}
1787
1945
  </span>
1788
1946
  )}
1789
- {!appRoot && crumbs.map((seg, i) => {
1947
+ {!atTitleRoot && crumbs.map((seg, i) => {
1790
1948
  const to = crumbs.slice(0, i + 1).join('/') + '/'
1791
1949
  const last = i === crumbs.length - 1
1792
- const current = last && !(folderView === 'columns' && pickedFile)
1950
+ const current = last && !pickedFile && !pickedFolder
1793
1951
  return (
1794
1952
  <span key={to} className="flex items-center gap-2">
1795
1953
  <span className="text-oq-32 select-none">/</span>
1796
1954
  {current
1797
1955
  ? <CopyCrumb className={crumbCls(true)} label={seg.toUpperCase()} path={fullPath()} />
1798
- : <button className={crumbCls(false)} onClick={() => setPrefix(to)}>{seg.toUpperCase()}</button>}
1956
+ : <button className={crumbCls(false)} onClick={() => goFolder(to)}>{seg.toUpperCase()}</button>}
1799
1957
  </span>
1800
1958
  )
1801
1959
  })}
1802
- {folderView === 'columns' && pickedFile && pickedFile.key.startsWith(prefix) && (
1960
+ {/* THE PICKED FILE ENDS THE CRUMB IN EVERY VIEW (kol-client-olina 2026-09-23 — it was the
1961
+ * columns' alone, so a file picked in rows showed its folder and never itself) */}
1962
+ {pickedFile && pickedFile.key.startsWith(prefix) && (
1803
1963
  <span className="flex items-center gap-2"><span className="text-oq-32 select-none">/</span>
1804
1964
  <CopyCrumb className={crumbCls(true)} label={pickedFile.key.slice(prefix.length).toUpperCase()} path={fullPath(pickedFile.key.slice(prefix.length))} />
1805
1965
  </span>
1806
1966
  )}
1967
+ {/* …and so does a picked folder */}
1968
+ {!pickedFile && pickedFolder && pickedFolder !== prefix && pickedFolder.startsWith(prefix) && (
1969
+ <span className="flex items-center gap-2"><span className="text-oq-32 select-none">/</span>
1970
+ <CopyCrumb className={crumbCls(true)} label={pickedFolder.slice(prefix.length).replace(/\/$/, '').toUpperCase()} path={fullPath(pickedFolder.slice(prefix.length).replace(/\/$/, ''))} />
1971
+ </span>
1972
+ )}
1807
1973
  </div>
1808
1974
  {/* the control cluster is desktop-only — below `md` this is the same
1809
1975
  * setting the drawer already carries, and nothing in this row survives 390.
@@ -1898,7 +2064,9 @@ export function MediaLibraryBrowse({
1898
2064
  )}
1899
2065
  {quickLookFolder && (
1900
2066
  <FullscreenOverlay open onClose={() => setQuickLookFolder(null)} scrim>
1901
- <FolderPreview path={quickLookFolder} objects={objects} width={420} formatDate={formatDate} />
2067
+ {quickLookFolder === ROOT_KEY
2068
+ ? previewRoot && <ContainerPreview name={previewRoot.name} icon={previewRoot.icon} facts={previewRoot.facts} width={420} />
2069
+ : <FolderPreview path={quickLookFolder} objects={objects} width={420} formatDate={formatDate} />}
1902
2070
  </FullscreenOverlay>
1903
2071
  )}
1904
2072
  {quickLook && (
@@ -1938,20 +2106,23 @@ export function MediaLibraryBrowse({
1938
2106
  * height again, and one shared `gap-3` puts both gaps level on its own. */}
1939
2107
  {/* THE COUNT LINE IS EVERY VIEW'S (user 2026-09-23) — and in the grid it carries the tile-size
1940
2108
  * slider at its right end, bare: no glyph, no readout (Finder's). */}
1941
- {isWall ? (
2109
+ {atTitleRoot && !single ? (
2110
+ <div className="flex items-center justify-between gap-4 h-4">
2111
+ <p className="kol-mono-12 text-fg-48">
2112
+ {atTop ? '1 folder' : `${bucketList.length} ${bucketList.length === 1 ? 'bucket' : 'buckets'}`}
2113
+ {folderTree && ` · ${Object.values(folderTree).reduce((n, t) => n + (t.files ?? 0), 0)} files · ${formatSize(Object.values(folderTree).reduce((n, t) => n + (t.bytes ?? 0), 0))}`}
2114
+ </p>
2115
+ {isWall && sizeSlider}
2116
+ </div>
2117
+ ) : isWall ? (
1942
2118
  /* ONE LINE'S HEIGHT, like the other views' count line — the slider is 24px tall and made
1943
2119
  * this row 8px taller than the fill budget, so the page scrolled by that much */
1944
2120
  <div className="flex items-center justify-between gap-4 h-4">
1945
2121
  <p className="kol-mono-12 text-fg-48">
1946
2122
  {!settings.flat && folders.length > 0 && `${folders.length} folder${folders.length > 1 ? 's' : ''} · `}{wallFiles.length} {wallFiles.length === 1 ? 'file' : 'files'} · {formatSize(wallFiles.reduce((n, o) => n + (o.size ?? 0), 0))}
1947
2123
  </p>
1948
- <input type="range" className="slider-black w-32 cursor-pointer" min={100} max={360} step={20} value={tileSize}
1949
- onChange={(e) => setSettings({ ...settings, tileSize: Number(e.target.value) })} aria-label="Tile size" />
2124
+ {sizeSlider}
1950
2125
  </div>
1951
- ) : appRoot && folderView === 'columns' && folderTree ? (
1952
- <p className="kol-mono-12 text-fg-48">
1953
- {buckets.length} buckets · {Object.values(folderTree).reduce((n, t) => n + (t.files ?? 0), 0)} files · {formatSize(Object.values(folderTree).reduce((n, t) => n + (t.bytes ?? 0), 0))}
1954
- </p>
1955
2126
  ) : (
1956
2127
  <p className="kol-mono-12 text-fg-48">
1957
2128
  {folders.length > 0 && `${folders.length} folder${folders.length > 1 ? 's' : ''} · `}
@@ -2211,7 +2382,7 @@ export function WallBody({
2211
2382
  onClick={(e) => { if (!e.target.closest('[data-marquee-key]')) onBackgroundClick?.() }}>
2212
2383
  {marquee?.rect && <div className="kol-marquee" style={marquee.rect} />}
2213
2384
  {folders.map((path) => (
2214
- <MediaTile key={path} markKey={path} preview={<Icon name="folder" size={56} className="text-oq-48" />}
2385
+ <MediaTile key={path} markKey={path} preview={<Icon name="folder" size="100%" className="text-oq-48" />}
2215
2386
  name={path.replace(/\/$/, '').split('/').pop()} selected={isSelected(path)}
2216
2387
  onContextMenu={onFolderContextMenu ? (e) => onFolderContextMenu(e, path) : undefined}
2217
2388
  onDoubleClick={() => onOpenFolder?.(path)}