@kolkrabbi/kol-component 0.210.0 → 0.211.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.
|
|
3
|
+
"version": "0.211.0",
|
|
4
4
|
"description": "KOL design-system components \u2014 atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -103,38 +103,58 @@ const defaultFormatSize = (bytes) => {
|
|
|
103
103
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/* THE STACK'S ROW
|
|
107
|
-
* so it is reachable by a check (the defect below is invisible to every
|
|
108
|
-
* structural assertion: the rows are all present and each carries the right
|
|
109
|
-
* `depth`, they are merely in the wrong ORDER).
|
|
106
|
+
/* THE STACK'S ROW LIST — the CURRENT LEVEL'S CONTENTS, and nothing above it.
|
|
110
107
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* folder's children surfaced below its last SIBLING and below that level's
|
|
115
|
-
* files (D1, kol-r2b2 2026-09-04) — which read as an indent bug rather than a
|
|
116
|
-
* sequence bug, and is how a comment claiming "the loop IS the path" survived.
|
|
108
|
+
* Lifted out of the component so the order is reachable by a check: the two
|
|
109
|
+
* defects this function has had were both invisible to a structural assertion —
|
|
110
|
+
* every row present, every `depth` right, and the LIST still wrong.
|
|
117
111
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
112
|
+
* D1 (2026-09-04) was order: a flat loop over the open path appended each level
|
|
113
|
+
* after the whole level above it, so an open folder's children landed after its
|
|
114
|
+
* last sibling and after that level's files. The walk is recursive now and a
|
|
115
|
+
* subtree is contiguous under its parent.
|
|
116
|
+
*
|
|
117
|
+
* `StackModeChromeAndAncestors` (kol-r2b2, 2026-09-04) was MEMBERSHIP, and the
|
|
118
|
+
* correction is theirs against their own spec: the browser walked from the root
|
|
119
|
+
* down the open path, so every ANCESTOR of the current level rendered as a row —
|
|
120
|
+
* at a bucket root, two rows and 120px of a 844px viewport restating the
|
|
121
|
+
* breadcrumb directly above them, with rows indented to 56px because depth
|
|
122
|
+
* counted from the root.
|
|
123
|
+
*
|
|
124
|
+
* The original spec held two rules that fight: "ancestors are reached by back,
|
|
125
|
+
* not by a scroll nobody can see" and "inline expand, capped at three levels of
|
|
126
|
+
* indent" — back means they are off screen, an indent cap implies they are on
|
|
127
|
+
* it. The resolution is that NAVIGATING and EXPANDING were fused into one piece
|
|
128
|
+
* of state and had to come apart:
|
|
129
|
+
*
|
|
130
|
+
* the ROW opens the folder → `onPrefix`, and the list re-bases to it
|
|
131
|
+
* the CHEVRON expands in place → local state, children spliced under it
|
|
132
|
+
*
|
|
133
|
+
* which is what item 11 already said the two targets were for. So `base` is the
|
|
134
|
+
* level the consumer navigated to, depth restarts there, and nothing above it is
|
|
135
|
+
* ever a row.
|
|
136
|
+
*
|
|
137
|
+
* @param {object[]} objects the flat key space
|
|
138
|
+
* @param {string} base the current level — its contents are the list
|
|
139
|
+
* @param {Function} isExpanded (folderPath) => boolean; `() => false` is a flat list
|
|
140
|
+
* @param {Function} partition (objects, level) => { folders, files }
|
|
141
|
+
* @param {number} cap deepest expansion below `base` (default INDENT_CAP)
|
|
121
142
|
* @returns {object[]} rows — `folder` | `file` | `empty`, each with its `depth`
|
|
122
143
|
*/
|
|
123
|
-
export function stackRows(objects,
|
|
144
|
+
export function stackRows(objects, base, isExpanded, partition, cap = INDENT_CAP) {
|
|
124
145
|
const rows = []
|
|
125
|
-
const walk = (depth) => {
|
|
126
|
-
const level = openPath[depth]
|
|
146
|
+
const walk = (level, depth) => {
|
|
127
147
|
const { folders, files } = partition(objects.filter((o) => o.key.startsWith(level)), level)
|
|
128
|
-
const openFolder = openPath[depth + 1]?.slice(level.length) ?? null
|
|
129
148
|
folders.forEach((f) => {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
const path = level + f
|
|
150
|
+
const open = depth < cap && isExpanded(path)
|
|
151
|
+
rows.push({ kind: 'folder', key: path, level, name: f, depth, open })
|
|
152
|
+
if (open) walk(path, depth + 1)
|
|
133
153
|
})
|
|
134
154
|
files.forEach((o) => rows.push({ kind: 'file', key: o.key, o, depth, level }))
|
|
135
155
|
if (!folders.length && !files.length) rows.push({ kind: 'empty', key: level + '·empty', depth })
|
|
136
156
|
}
|
|
137
|
-
walk(0)
|
|
157
|
+
walk(base, 0)
|
|
138
158
|
return rows
|
|
139
159
|
}
|
|
140
160
|
|
|
@@ -186,11 +206,17 @@ const COL_ICON = { image: 'image', video: 'video', audio: 'file', playlist: 'vid
|
|
|
186
206
|
* the deepest column that holds one is full strength, every column on the way there is the trail. */
|
|
187
207
|
/* `indent` and `meta` serve the STACK mode and are inert without it: the
|
|
188
208
|
* columns pass neither, so a desktop row is byte-identical to what it was. */
|
|
189
|
-
/* Three levels of
|
|
190
|
-
*
|
|
191
|
-
* path indents until the name has no room.
|
|
209
|
+
/* Three levels of inline expansion below the current level, then the chevron
|
|
210
|
+
* stops expanding — the user's cap (ColumnBrowserStackMode, 2026-09-03): a deep
|
|
211
|
+
* path indents until the name has no room. Past it the ROW is the way down; it
|
|
212
|
+
* re-bases the list and the back control names what it left. */
|
|
192
213
|
const INDENT_CAP = 3
|
|
193
214
|
|
|
215
|
+
/* The four-zone row's icon/thumbnail box. ONE constant because the glyph that
|
|
216
|
+
* stands in for a missing thumbnail is sized off it — two numbers here is how
|
|
217
|
+
* the box and its contents drifted apart in the first place. */
|
|
218
|
+
const ZONE_BOX = 44
|
|
219
|
+
|
|
194
220
|
/* THE FOUR-ZONE ROW (ColumnBrowserMobileViews §1, kol-r2b2 2026-09-03) is the
|
|
195
221
|
* `zones` form. The first stack build inherited the DS Table's `12px 16px` with
|
|
196
222
|
* a 14px glyph in a 20px slot, and the user's read was *"kinda underwhelming…
|
|
@@ -250,13 +276,23 @@ function Row({
|
|
|
250
276
|
<span aria-hidden="true" className="shrink-0" style={{ width: 14 }} />
|
|
251
277
|
))}
|
|
252
278
|
|
|
253
|
-
{/* ZONE 2 — the icon box, or the thumbnail the consumer supplies
|
|
279
|
+
{/* ZONE 2 — the icon box, or the thumbnail the consumer supplies.
|
|
280
|
+
THE GLYPH FILLS THE BOX (StackModeChromeAndAncestors §3, kol-r2b2
|
|
281
|
+
2026-09-04). It sat at 20 in a 44 box while a file's thumbnail filled
|
|
282
|
+
its 44, so the icon column read ragged — thumbnails filling their
|
|
283
|
+
squares, folder marks floating in the middle of theirs. The grid had
|
|
284
|
+
it right already (tile-box 103, thumb 103); this is the list catching
|
|
285
|
+
up, and it is what makes the column read as ONE RAIL.
|
|
286
|
+
Not a glyph-ladder rung and not a contradiction of one: SOLO pairs
|
|
287
|
+
12/16/20/24 with the CONTROL squares 22/26/32/40. This is not a
|
|
288
|
+
control square — it is a MEDIA slot that a glyph stands in for when
|
|
289
|
+
the consumer has no thumbnail, which is why an <img> fills it. */}
|
|
254
290
|
{zones ? (
|
|
255
291
|
<span
|
|
256
292
|
className="kol-column-browser-thumb shrink-0 inline-flex items-center justify-center overflow-hidden"
|
|
257
|
-
style={{ width:
|
|
293
|
+
style={{ width: ZONE_BOX, height: ZONE_BOX, borderRadius: 5, background: thumb ? 'var(--kol-oq-04)' : 'transparent' }}
|
|
258
294
|
>
|
|
259
|
-
{thumb ?? <Icon name={icon} size={
|
|
295
|
+
{thumb ?? <Icon name={icon} size={ZONE_BOX} className="text-fg-48" />}
|
|
260
296
|
</span>
|
|
261
297
|
) : (
|
|
262
298
|
<span className="w-5 shrink-0 flex items-center justify-center text-fg-48">
|
|
@@ -465,6 +501,7 @@ export default function ColumnBrowser({
|
|
|
465
501
|
}
|
|
466
502
|
const pickedByCollapse = useRef(false)
|
|
467
503
|
useEffect(() => { if (pickedByCollapse.current) { pickedByCollapse.current = false; return } if (picked) pick(null) }, [prefix]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
504
|
+
useEffect(() => { setExpanded((prev) => (prev.size ? new Set() : prev)) }, [prefix])
|
|
468
505
|
const levelsOf = (pfx) => {
|
|
469
506
|
const out = ['']
|
|
470
507
|
if (pfx) {
|
|
@@ -499,6 +536,19 @@ export default function ColumnBrowser({
|
|
|
499
536
|
* kol-r2b2 2026-08-27): at rest it sat on row 0 beside the open folder and
|
|
500
537
|
* read as a second selection. Arrows arm it; a click seeds it. */
|
|
501
538
|
const [cursorActive, setCursorActive] = useState(false)
|
|
539
|
+
/* STACK EXPANSION IS NOT NAVIGATION (StackModeChromeAndAncestors, kol-r2b2
|
|
540
|
+
* 2026-09-04). The chevron expands a folder where it stands; the ROW opens it
|
|
541
|
+
* and re-bases the list. Fusing both into `prefix` is what put the whole
|
|
542
|
+
* ancestor chain on screen as rows. Local because it is a VIEW state below a
|
|
543
|
+
* breakpoint — a consumer persisting it would be storing a phone gesture, and
|
|
544
|
+
* a desktop never reads it. Cleared whenever the level changes: expansions
|
|
545
|
+
* belong to the list they were made in. */
|
|
546
|
+
const [expanded, setExpanded] = useState(() => new Set())
|
|
547
|
+
const toggleExpanded = (path) => setExpanded((prev) => {
|
|
548
|
+
const next = new Set(prev)
|
|
549
|
+
if (!next.delete(path)) next.add(path)
|
|
550
|
+
return next
|
|
551
|
+
})
|
|
502
552
|
const rootRef = useRef(null)
|
|
503
553
|
/* `autoFocus` (ColumnBrowserSeams, kol-r2b2 2026-08-28): the arrow keys were dead until a row was
|
|
504
554
|
* clicked — nothing focused the root on mount, and the consumer reached into the DOM for it. Re-run
|
|
@@ -607,29 +657,33 @@ export default function ColumnBrowser({
|
|
|
607
657
|
const stack = useMediaQuery('(max-width: 767px)')
|
|
608
658
|
|
|
609
659
|
if (stack) {
|
|
610
|
-
/*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
|
|
614
|
-
const base =
|
|
615
|
-
const parent =
|
|
616
|
-
const openPath = levels.slice(baseIdx)
|
|
660
|
+
/* THE CURRENT LEVEL IS THE LIST (StackModeChromeAndAncestors, kol-r2b2
|
|
661
|
+
* 2026-09-04). `base` is where the consumer navigated to and depth restarts
|
|
662
|
+
* there; nothing above it is ever a row. The back control is the only place
|
|
663
|
+
* an ancestor appears, and it NAMES the one it returns to. */
|
|
664
|
+
const base = prefix
|
|
665
|
+
const parent = levels.length > 1 ? levels[levels.length - 2] : null
|
|
617
666
|
|
|
618
|
-
/*
|
|
619
|
-
* the
|
|
620
|
-
|
|
667
|
+
/* The grid is FLAT — a tile has nowhere to put a child list, so expansion
|
|
668
|
+
* is not offered there and the list is one level, as both references draw
|
|
669
|
+
* it. */
|
|
670
|
+
const rows = stackRows(objects, base, stackView === 'grid' ? () => false : (path) => expanded.has(path), partition)
|
|
621
671
|
|
|
622
672
|
const metaOf = (o) => [o.size != null && formatSize(o.size), formatDate(o.uploaded)].filter(Boolean).join(' · ')
|
|
623
673
|
|
|
624
674
|
return (
|
|
625
675
|
<div
|
|
626
|
-
|
|
676
|
+
/* NO FRAME BELOW `md` (StackModeChromeAndAncestors §2). On desktop this
|
|
677
|
+
is a PANE — a thing with edges sitting in a page — so it is bordered
|
|
678
|
+
and rounded. In stack mode it IS the page's content: rows run to the
|
|
679
|
+
page's own padding and the only line is the divider between them.
|
|
680
|
+
Neither reference frames it. */
|
|
681
|
+
className={`kol-column-browser kol-column-browser--stack flex flex-col ${className}`.trim()}
|
|
627
682
|
/* THE VIEWPORT IS THE HEIGHT (item 4). `height` is a value someone
|
|
628
683
|
* dragged on a desktop; applied literally to a phone it painted a
|
|
629
684
|
* black void the length of the viewport under two near-empty columns.
|
|
630
685
|
* A desktop drag is not a phone measurement, so below the breakpoint
|
|
631
686
|
* the stored one is ignored outright rather than clamped. */
|
|
632
|
-
style={{ borderColor: 'var(--kol-oq-08)' }}
|
|
633
687
|
>
|
|
634
688
|
{parent != null && (
|
|
635
689
|
<button
|
|
@@ -697,7 +751,10 @@ export default function ColumnBrowser({
|
|
|
697
751
|
* in place, the ROW opens the folder. One control doing both is
|
|
698
752
|
* what left no way to peek without leaving where you are. */
|
|
699
753
|
disclosed={r.open}
|
|
700
|
-
|
|
754
|
+
/* the chevron expands where it stands and does NOT move the
|
|
755
|
+
* level; past the indent cap it has nothing left to offer, so
|
|
756
|
+
* the row is the only way down */
|
|
757
|
+
onDisclose={r.depth < INDENT_CAP ? () => toggleExpanded(r.key) : undefined}
|
|
701
758
|
onClick={() => onPrefix(r.level + r.name)}
|
|
702
759
|
/>
|
|
703
760
|
) : (
|
|
@@ -285,14 +285,31 @@ function MediaSettings({ bucketMeta, settings, onChange, onReset, onClose, profi
|
|
|
285
285
|
)
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
/* ── the header — bucket Dropdown · lock / the app's actions · settings ────
|
|
288
|
+
/* ── the header — bucket Dropdown · lock / the app's actions · settings ────
|
|
289
|
+
*
|
|
290
|
+
* THE ROW HAS TO GIVE AT 390 (StackModeChromeAndAncestors §4, kol-r2b2
|
|
291
|
+
* 2026-09-04). The title was `white-space: normal` in a box the fixed `w-48`
|
|
292
|
+
* dropdown had squeezed to 48px, so `KOL-R2B2` wrapped MID-TOKEN into two
|
|
293
|
+
* 36px lines — 72px of header spent breaking a word in half. Neither reference
|
|
294
|
+
* wraps a title.
|
|
295
|
+
*
|
|
296
|
+
* The dropdown's 192px was the actual cost: 192 of 390 before the title, the
|
|
297
|
+
* lock and the gear have had any. It keeps that width from `md` up, where the
|
|
298
|
+
* room exists, and shrinks below it. The title never wraps — it takes the room
|
|
299
|
+
* left and ellipsises, which is honest where a mid-token break is not.
|
|
300
|
+
*
|
|
301
|
+
* `headerActions` is a REAL slot and it is narrow here: at 390 the row holds
|
|
302
|
+
* the dropdown, the gear, and about ONE consumer icon. kol-r2b2 added a second
|
|
303
|
+
* and pushed the gear off the right edge — they resolved it by SWAPPING a
|
|
304
|
+
* control below `md` rather than adding one, which is the pattern to copy. If a
|
|
305
|
+
* consumer needs more than one at this width, it belongs in a sheet, not here. */
|
|
289
306
|
function LibraryHeader({ title, buckets, bucketId, appRoot, onBucket, bucketMeta, writable, headerActions, onSettings }) {
|
|
290
307
|
const options = buckets.length ? [{ value: 'all', label: `${title} · all` }, ...buckets.map((b) => ({ value: b.id, label: b.label }))] : []
|
|
291
308
|
return (
|
|
292
309
|
<header className="flex items-baseline justify-between gap-4">
|
|
293
|
-
<h1 className="kol-sans-display-03">{title}</h1>
|
|
294
|
-
<div className="flex items-center gap-2">
|
|
295
|
-
{options.length > 0 && <Dropdown className="w-48" value={appRoot ? 'all' : bucketId} onChange={onBucket} options={options} />}
|
|
310
|
+
<h1 className="kol-sans-display-03 min-w-0 truncate">{title}</h1>
|
|
311
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
312
|
+
{options.length > 0 && <Dropdown className="min-w-0 max-w-[45vw] md:w-48 md:max-w-none" value={appRoot ? 'all' : bucketId} onChange={onBucket} options={options} />}
|
|
296
313
|
{headerActions}
|
|
297
314
|
{!writable && bucketMeta.id && (
|
|
298
315
|
<IconFrame name="lock" variant="primary" size="sm" title={`${bucketMeta.label} is read-only here`} aria-label="Read-only" />
|