@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.203.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
- function Row({ icon, label, active, cursor = false, trailing, onClick, muted = false, indent = 0, meta }) {
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 px-4 py-3 cursor-pointer transition-colors${
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
- <span className="w-5 shrink-0 flex items-center justify-center text-fg-48">
154
- <Icon name={icon} size={14} />
155
- </span>
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
- {trailing}
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
- trailing={<Icon name={r.open ? 'chevron-down' : 'chevron-right'} size={12} className="text-fg-32" />}
540
- /* the same chevron opens and collapses tapping the open
541
- * folder returns to its parent level, which is the whole
542
- * inline-expand gesture */
543
- onClick={() => onPrefix(r.open ? r.level : r.level + r.name)}
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
  }