@kolkrabbi/kol-component 0.203.0 → 0.204.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 +1 -1
- package/src/organisms/ColumnBrowser.jsx +135 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.204.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",
|
|
@@ -71,6 +71,10 @@ import { GRAB_COLUMN } from '../utilities/motion.js'
|
|
|
71
71
|
* @param {Function} onColumnResize (index, px) => void — the column's index, or `'preview'`
|
|
72
72
|
* @param {string} className extra classes on the browser
|
|
73
73
|
*
|
|
74
|
+
* @param {Function} thumbnailFor (o) => node — the 44px tile's content below the breakpoint; null falls back to the kind glyph. WHERE a thumbnail comes from is the consumer's: R2 and B2 serve originals, so a 44px tile can mean a 2 MB download
|
|
75
|
+
* @param {Function} folderMeta (prefix, view) => string — a folder's own meta line (`date · N items` in list, `N items` in grid). A SEAM, not a computation: counting by prefix is O(n) per folder, and a consumer that already holds a folder tree answers it for free
|
|
76
|
+
* @param {'list'|'grid'} stackView the mobile view below the breakpoint (default 'list'); `grid` is 3-up tiles, flat — a grid has nowhere to put an inline child list
|
|
77
|
+
*
|
|
74
78
|
* BELOW `md` (768) THIS IS A DIFFERENT TREE — one full-width inline-expanding
|
|
75
79
|
* list, not columns (`ColumnBrowserStackMode`, kol-r2b2 2026-09-03, user-ruled
|
|
76
80
|
* inline expand over push). `height` / `defaultHeight` / `onHeightChange`,
|
|
@@ -134,13 +138,39 @@ const COL_ICON = { image: 'image', video: 'video', audio: 'file', playlist: 'vid
|
|
|
134
138
|
* path indents until the name has no room. */
|
|
135
139
|
const INDENT_CAP = 3
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
/* THE FOUR-ZONE ROW (ColumnBrowserMobileViews §1, kol-r2b2 2026-09-03) is the
|
|
142
|
+
* `zones` form. The first stack build inherited the DS Table's `12px 16px` with
|
|
143
|
+
* a 14px glyph in a 20px slot, and the user's read was *"kinda underwhelming…
|
|
144
|
+
* did you even look at the refs?"* — correctly, and the fault was the spec's.
|
|
145
|
+
* iOS Files and Dropbox both draw four:
|
|
146
|
+
*
|
|
147
|
+
* 1 · disclosure 14px, FOLDERS ONLY, its own tap target — tapping it
|
|
148
|
+
* expands in place, tapping the row opens. Without the split
|
|
149
|
+
* there is no way to peek into a folder without leaving the
|
|
150
|
+
* one you are in
|
|
151
|
+
* 2 · icon 44×44 at radius 5 — a folder glyph, or a real THUMBNAIL
|
|
152
|
+
* 3 · text name, then meta beneath
|
|
153
|
+
* 4 · trailing 20px, the row's own affordance
|
|
154
|
+
*
|
|
155
|
+
* Row height follows zone 2: 60px, not the table padding. The divider starts at
|
|
156
|
+
* zone 3's left edge, not the row's — both references.
|
|
157
|
+
*
|
|
158
|
+
* `zones`, `thumb`, `onDisclose`, `disclosed`, `indent` and `meta` are all inert
|
|
159
|
+
* without the stack: the columns pass none, so a desktop row is byte-identical
|
|
160
|
+
* to what it was. */
|
|
161
|
+
function Row({
|
|
162
|
+
icon, label, active, cursor = false, trailing, onClick, muted = false,
|
|
163
|
+
indent = 0, meta, zones = false, thumb, onDisclose, disclosed,
|
|
164
|
+
}) {
|
|
138
165
|
return (
|
|
139
166
|
<li
|
|
140
167
|
/* Row metrics are the DS Table's (kol-components-organisms.css .kol-table-cell-*):
|
|
141
|
-
* 12px 16px padding, mono 12, an oq-08 hairline between rows, none after the last.
|
|
168
|
+
* 12px 16px padding, mono 12, an oq-08 hairline between rows, none after the last.
|
|
169
|
+
* The `zones` row sets its own 60px instead — a 44px thumbnail in a
|
|
170
|
+
* 12px-padded row is three times too airy. */
|
|
142
171
|
/* `cursor` = the keyboard row, drawn with the hover fill so ↑/↓ always shows where you are. */
|
|
143
|
-
className={`kol-column-browser-row flex items-center gap-2
|
|
172
|
+
className={`kol-column-browser-row flex items-center gap-2 cursor-pointer transition-colors${
|
|
173
|
+
zones ? ' kol-column-browser-row--zones px-4' : ' px-4 py-3'}${
|
|
144
174
|
active ? ' is-selected' : ''}${cursor ? ' is-cursor' : ''} ${
|
|
145
175
|
active || cursor || !muted ? 'text-fg-default' : 'text-fg-48'
|
|
146
176
|
}`}
|
|
@@ -150,12 +180,39 @@ function Row({ icon, label, active, cursor = false, trailing, onClick, muted = f
|
|
|
150
180
|
* scrollers does not */
|
|
151
181
|
style={indent ? { paddingLeft: `calc(var(--kol-spacing-4) + ${indent} * var(--kol-spacing-5))` } : undefined}
|
|
152
182
|
>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
183
|
+
{/* ZONE 1 — disclosure, its own tap target. A bare span holds the column
|
|
184
|
+
for files so every icon in the list lands on one x. */}
|
|
185
|
+
{zones && (onDisclose ? (
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
onClick={(e) => { e.stopPropagation(); onDisclose() }}
|
|
189
|
+
aria-expanded={!!disclosed}
|
|
190
|
+
aria-label={disclosed ? 'Collapse folder' : 'Expand folder'}
|
|
191
|
+
className="kol-column-browser-disclose shrink-0 inline-flex items-center justify-center"
|
|
192
|
+
style={{ width: 14, alignSelf: 'stretch', color: 'var(--kol-accent-primary)' }}
|
|
193
|
+
>
|
|
194
|
+
<Icon name={disclosed ? 'chevron-down' : 'chevron-right'} size={12} />
|
|
195
|
+
</button>
|
|
196
|
+
) : (
|
|
197
|
+
<span aria-hidden="true" className="shrink-0" style={{ width: 14 }} />
|
|
198
|
+
))}
|
|
199
|
+
|
|
200
|
+
{/* ZONE 2 — the icon box, or the thumbnail the consumer supplies */}
|
|
201
|
+
{zones ? (
|
|
202
|
+
<span
|
|
203
|
+
className="kol-column-browser-thumb shrink-0 inline-flex items-center justify-center overflow-hidden"
|
|
204
|
+
style={{ width: 44, height: 44, borderRadius: 5, background: thumb ? 'var(--kol-oq-04)' : 'transparent' }}
|
|
205
|
+
>
|
|
206
|
+
{thumb ?? <Icon name={icon} size={20} className="text-fg-48" />}
|
|
207
|
+
</span>
|
|
208
|
+
) : (
|
|
209
|
+
<span className="w-5 shrink-0 flex items-center justify-center text-fg-48">
|
|
210
|
+
<Icon name={icon} size={14} />
|
|
211
|
+
</span>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{/* ZONE 3 — name over meta */}
|
|
156
215
|
{meta ? (
|
|
157
|
-
/* size · date under the name — there is no preview column at this
|
|
158
|
-
* width to carry the facts (ColumnBrowserStackMode item 5) */
|
|
159
216
|
<span className="flex-1 min-w-0 flex flex-col gap-0.5">
|
|
160
217
|
<span className="kol-mono-12 truncate">{label}</span>
|
|
161
218
|
<span className="kol-helper-10 text-fg-48 truncate">{meta}</span>
|
|
@@ -163,7 +220,9 @@ function Row({ icon, label, active, cursor = false, trailing, onClick, muted = f
|
|
|
163
220
|
) : (
|
|
164
221
|
<span className="kol-mono-12 flex-1 truncate">{label}</span>
|
|
165
222
|
)}
|
|
166
|
-
|
|
223
|
+
|
|
224
|
+
{/* ZONE 4 */}
|
|
225
|
+
{zones ? <span className="shrink-0 inline-flex justify-end" style={{ width: 20 }}>{trailing}</span> : trailing}
|
|
167
226
|
</li>
|
|
168
227
|
)
|
|
169
228
|
}
|
|
@@ -292,6 +351,27 @@ export default function ColumnBrowser({
|
|
|
292
351
|
columnWidth = 260,
|
|
293
352
|
columnWidths,
|
|
294
353
|
onColumnResize,
|
|
354
|
+
/* THE TWO SEAMS THE MOBILE TICKET'S RULINGS POINT AT (ColumnBrowserMobileViews,
|
|
355
|
+
* kol-r2b2 2026-09-03). Both are questions the DS must not answer for a
|
|
356
|
+
* consumer, so neither is computed here:
|
|
357
|
+
*
|
|
358
|
+
* `thumbnailFor(o)` — a node for the 44px box, or null for the kind glyph.
|
|
359
|
+
* WHERE a thumbnail comes from is the consumer's and it is not free: R2 and
|
|
360
|
+
* B2 serve originals, so a 44px tile can mean downloading a 2 MB JPEG. That
|
|
361
|
+
* repo's own mitigations (lazy originals, a resolution-set that picks the
|
|
362
|
+
* smallest variant) and whether to buy image resizing are its calls.
|
|
363
|
+
*
|
|
364
|
+
* `folderMeta(prefix)` — the folder's own meta line, `date · N items` in the
|
|
365
|
+
* references. Counting `objects` by prefix is O(n) per folder against a
|
|
366
|
+
* 3443-object bucket, and that repo already passes a baked folder tree
|
|
367
|
+
* carrying files and bytes. A seam, not a computation — the ticket says so
|
|
368
|
+
* and it is right. */
|
|
369
|
+
thumbnailFor,
|
|
370
|
+
folderMeta,
|
|
371
|
+
/* `list` (default) or `grid` below the breakpoint — item 14. 3-up, and for a
|
|
372
|
+
* media bucket the thumbnail IS the tile, so it carries counts and sizes
|
|
373
|
+
* rather than dates (§2). Above the breakpoint the columns are unaffected. */
|
|
374
|
+
stackView = 'list',
|
|
295
375
|
autoFocus = false,
|
|
296
376
|
className = '',
|
|
297
377
|
}) {
|
|
@@ -525,6 +605,41 @@ export default function ColumnBrowser({
|
|
|
525
605
|
</span>
|
|
526
606
|
</button>
|
|
527
607
|
)}
|
|
608
|
+
{stackView === 'grid' ? (
|
|
609
|
+
/* THE GRID (item 14). Flat — one level at a time, no inline expand:
|
|
610
|
+
a grid of tiles has nowhere to put a child list, which is why both
|
|
611
|
+
references drop disclosure in this view and navigate by tap. */
|
|
612
|
+
<div className="kol-column-browser-grid flex-1 overflow-y-auto">
|
|
613
|
+
{rows.filter((r) => r.kind !== 'empty').map((r) => {
|
|
614
|
+
const isFolder = r.kind === 'folder'
|
|
615
|
+
const o = r.o
|
|
616
|
+
return (
|
|
617
|
+
<button
|
|
618
|
+
key={r.key}
|
|
619
|
+
type="button"
|
|
620
|
+
className={`kol-column-browser-tile${!isFolder && shown?.key === o.key ? ' is-selected' : ''}`}
|
|
621
|
+
onClick={() => {
|
|
622
|
+
if (isFolder) { onPrefix(r.level + r.name); return }
|
|
623
|
+
const files = itemsAt(r.level ?? '').filter((it) => it.type === 'file').map((it) => it.o)
|
|
624
|
+
pick(o)
|
|
625
|
+
onQuickLook?.({ files: files.length ? files : [o], index: Math.max(0, files.findIndex((f) => f.key === o.key)) })
|
|
626
|
+
}}
|
|
627
|
+
>
|
|
628
|
+
<span className="kol-column-browser-tile-box">
|
|
629
|
+
{isFolder
|
|
630
|
+
? <Icon name="folder" size={28} className="text-fg-48" />
|
|
631
|
+
: (thumbnailFor?.(o) ?? <Icon name={COL_ICON[kindOf(o)] || 'file'} size={28} className="text-fg-48" />)}
|
|
632
|
+
</span>
|
|
633
|
+
<span className="kol-mono-12 truncate">{isFolder ? r.name.replace(/\/$/, '') : (o.displayKey ?? o.key)}</span>
|
|
634
|
+
{/* counts for a folder, size for a file — never dates (§2) */}
|
|
635
|
+
<span className="kol-helper-10 text-fg-48 truncate">
|
|
636
|
+
{isFolder ? folderMeta?.(r.level + r.name, 'grid') : (o.size != null ? formatSize(o.size) : '')}
|
|
637
|
+
</span>
|
|
638
|
+
</button>
|
|
639
|
+
)
|
|
640
|
+
})}
|
|
641
|
+
</div>
|
|
642
|
+
) : (
|
|
528
643
|
<ul className="kol-column-browser-column flex-1 overflow-y-auto">
|
|
529
644
|
{rows.map((r) =>
|
|
530
645
|
r.kind === 'empty' ? (
|
|
@@ -532,20 +647,25 @@ export default function ColumnBrowser({
|
|
|
532
647
|
) : r.kind === 'folder' ? (
|
|
533
648
|
<Row
|
|
534
649
|
key={r.key}
|
|
650
|
+
zones
|
|
535
651
|
icon="folder"
|
|
536
652
|
label={r.name.replace(/\/$/, '')}
|
|
537
653
|
indent={r.depth}
|
|
538
654
|
active={r.open}
|
|
539
|
-
|
|
540
|
-
/*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
|
|
655
|
+
meta={folderMeta?.(r.level + r.name)}
|
|
656
|
+
/* DISCLOSURE IS ITS OWN TARGET (item 11): the chevron expands
|
|
657
|
+
* in place, the ROW opens the folder. One control doing both is
|
|
658
|
+
* what left no way to peek without leaving where you are. */
|
|
659
|
+
disclosed={r.open}
|
|
660
|
+
onDisclose={() => onPrefix(r.open ? r.level : r.level + r.name)}
|
|
661
|
+
onClick={() => onPrefix(r.level + r.name)}
|
|
544
662
|
/>
|
|
545
663
|
) : (
|
|
546
664
|
<Row
|
|
547
665
|
key={r.key}
|
|
666
|
+
zones
|
|
548
667
|
icon={COL_ICON[kindOf(r.o)] || 'file'}
|
|
668
|
+
thumb={thumbnailFor?.(r.o)}
|
|
549
669
|
label={r.o.displayKey ?? r.o.key}
|
|
550
670
|
indent={r.depth}
|
|
551
671
|
meta={metaOf(r.o)}
|
|
@@ -563,6 +683,7 @@ export default function ColumnBrowser({
|
|
|
563
683
|
),
|
|
564
684
|
)}
|
|
565
685
|
</ul>
|
|
686
|
+
)}
|
|
566
687
|
</div>
|
|
567
688
|
)
|
|
568
689
|
}
|