@kolkrabbi/kol-component 0.208.0 → 0.209.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,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.208.0",
4
- "description": "KOL design-system components atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
3
+ "version": "0.209.0",
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",
7
7
  "main": "./src/index.js",
@@ -34,7 +34,7 @@
34
34
  "react-dom": "^18.3.0 || ^19.0.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@kolkrabbi/kol-icons": "^0.27.0"
37
+ "@kolkrabbi/kol-icons": "workspace:^"
38
38
  },
39
39
  "files": [
40
40
  "src",
@@ -56,4 +56,4 @@
56
56
  "access": "public"
57
57
  },
58
58
  "sideEffects": false
59
- }
59
+ }
@@ -57,6 +57,7 @@ import { GRAB_COLUMN } from '../utilities/motion.js'
57
57
  * @param {Function} kindOf (o) => 'image' | 'video' | 'audio' | string
58
58
  * @param {Object} kindLabel kind → label shown when there is no visual preview
59
59
  * @param {Function} formatSize (bytes) => string
60
+ * @param {Function} formatDate (isoString) => string — the row/preview date, ISO date-only by default
60
61
  * @param {Function} partition (objects, level) => { folders: string[], files: object[] }
61
62
  * @param {Function} renderPreview (file) => ReactNode — replaces the preview column's media frame (the facts stay — Dimensions and Length are read off whatever <img> / <video> / <audio> the node loads); without it images render the organism's <img>, everything else the DS KindPreview
62
63
  * @param {number} height controlled height in px (omit for uncontrolled)
@@ -97,6 +98,54 @@ const defaultFormatSize = (bytes) => {
97
98
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
98
99
  return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
99
100
  }
101
+
102
+ /* THE STACK'S ROW ORDER — walked, not iterated, and lifted out of the component
103
+ * so it is reachable by a check (the defect below is invisible to every
104
+ * structural assertion: the rows are all present and each carries the right
105
+ * `depth`, they are merely in the wrong ORDER).
106
+ *
107
+ * The open folder's children have to land DIRECTLY under its own row. That
108
+ * adjacency IS inline expand, and inline expand is the whole ruling. A flat
109
+ * loop over `openPath` appends each level after the one before it, so an open
110
+ * folder's children surfaced below its last SIBLING and below that level's
111
+ * files (D1, kol-r2b2 2026-09-04) — which read as an indent bug rather than a
112
+ * sequence bug, and is how a comment claiming "the loop IS the path" survived.
113
+ *
114
+ * @param {object[]} objects the flat key space
115
+ * @param {string[]} openPath the open levels, shallowest first
116
+ * @param {Function} partition (objects, level) => { folders, files }
117
+ * @returns {object[]} rows — `folder` | `file` | `empty`, each with its `depth`
118
+ */
119
+ export function stackRows(objects, openPath, partition) {
120
+ const rows = []
121
+ const walk = (depth) => {
122
+ const level = openPath[depth]
123
+ const { folders, files } = partition(objects.filter((o) => o.key.startsWith(level)), level)
124
+ const openFolder = openPath[depth + 1]?.slice(level.length) ?? null
125
+ folders.forEach((f) => {
126
+ const isOpen = f === openFolder
127
+ rows.push({ kind: 'folder', key: level + f, level, name: f, depth, open: isOpen })
128
+ if (isOpen) walk(depth + 1)
129
+ })
130
+ files.forEach((o) => rows.push({ kind: 'file', key: o.key, o, depth, level }))
131
+ if (!folders.length && !files.length) rows.push({ kind: 'empty', key: level + '·empty', depth })
132
+ }
133
+ walk(0)
134
+ return rows
135
+ }
136
+
137
+ /* A ROW SHOWS A DATE, NOT A TIMESTAMP. `2026-06-19T02:00:14.629Z` is storage
138
+ * answering "when" in its own voice, and it shipped into the stack meta line
139
+ * because the value arrives as a string and a string renders (D2, kol-r2b2
140
+ * 2026-09-04). A seam beside `formatSize` for the same reason that one is a
141
+ * seam: how a date reads is the consumer's call, not the DS's. The default is
142
+ * ISO-8601 date-only — unambiguous everywhere, which no locale format is.
143
+ * Anything unparseable comes through verbatim rather than as `Invalid Date`. */
144
+ const defaultFormatDate = (v) => {
145
+ if (!v) return ''
146
+ const d = new Date(v)
147
+ return Number.isNaN(d.getTime()) ? String(v) : d.toISOString().slice(0, 10)
148
+ }
100
149
  /* kol-r2b2's partition: the next path segment is a folder, the rest are files */
101
150
  const defaultPartition = (objects, prefix) => {
102
151
  const folderSet = new Set()
@@ -227,7 +276,7 @@ function Row({
227
276
  )
228
277
  }
229
278
 
230
- function Preview({ o, urlOf, kindOf, kindLabel, formatSize, renderPreview, width }) {
279
+ function Preview({ o, urlOf, kindOf, kindLabel, formatSize, formatDate, renderPreview, width }) {
231
280
  // Pixel size and length come from the loaded media itself — the bucket stores
232
281
  // none. `{ w, h }` off an <img> load, `{ w, h, len }` off a <video>'s and
233
282
  // `{ len }` off an <audio>'s loadedmetadata (ColumnBrowserMediaFacts, kol-r2b2
@@ -243,7 +292,7 @@ function Preview({ o, urlOf, kindOf, kindLabel, formatSize, renderPreview, width
243
292
  ['Size', formatSize(o.size)],
244
293
  ...(sized ? [['Dimensions', dims?.w ? `${dims.w} × ${dims.h} px` : '…']] : []),
245
294
  ...(timed ? [['Length', dims?.len != null ? formatLength(dims.len) : '…']] : []),
246
- ['Date', o.uploaded ? new Date(o.uploaded).toISOString().slice(0, 10) : '—'],
295
+ ['Date', formatDate(o.uploaded) || '—'],
247
296
  ]
248
297
  return (
249
298
  <div className="kol-column-browser-preview shrink-0 overflow-y-auto p-4 flex flex-col gap-4" style={{ width }}>
@@ -343,6 +392,7 @@ export default function ColumnBrowser({
343
392
  kindOf = defaultKindOf,
344
393
  kindLabel = DEFAULT_KIND_LABEL,
345
394
  formatSize = defaultFormatSize,
395
+ formatDate = defaultFormatDate,
346
396
  partition = defaultPartition,
347
397
  renderPreview,
348
398
  height,
@@ -563,23 +613,9 @@ export default function ColumnBrowser({
563
613
 
564
614
  /* One flat row list, walked down the open path: each level's items, with
565
615
  * the open folder's children spliced in directly under it. */
566
- const rows = []
567
- openPath.forEach((level, depth) => {
568
- const { folders, files } = partition(objects.filter((o) => o.key.startsWith(level)), level)
569
- const openFolder = openPath[depth + 1]?.slice(level.length) ?? null
570
- folders.forEach((f) => {
571
- const isOpen = f === openFolder
572
- rows.push({
573
- kind: 'folder', key: level + f, level, name: f, depth, open: isOpen,
574
- })
575
- /* the open folder's own children are pushed by the next iteration —
576
- * nothing recursive here, the loop IS the path */
577
- })
578
- files.forEach((o) => rows.push({ kind: 'file', key: o.key, o, depth, level }))
579
- if (!folders.length && !files.length) rows.push({ kind: 'empty', key: level + '·empty', depth })
580
- })
616
+ const rows = stackRows(objects, openPath, partition)
581
617
 
582
- const metaOf = (o) => [o.size != null && formatSize(o.size), o.uploaded].filter(Boolean).join(' · ')
618
+ const metaOf = (o) => [o.size != null && formatSize(o.size), formatDate(o.uploaded)].filter(Boolean).join(' · ')
583
619
 
584
620
  return (
585
621
  <div
@@ -741,7 +777,7 @@ export default function ColumnBrowser({
741
777
  })}
742
778
  {shown && (
743
779
  <>
744
- <Preview key={shown.key} o={shown} urlOf={urlOf} kindOf={kindOf} kindLabel={kindLabel} formatSize={formatSize} renderPreview={renderPreview} width={widthOf('preview')} />
780
+ <Preview key={shown.key} o={shown} urlOf={urlOf} kindOf={kindOf} kindLabel={kindLabel} formatSize={formatSize} formatDate={formatDate} renderPreview={renderPreview} width={widthOf('preview')} />
745
781
  <ResizeHandle axis="x" onDrag={resizeCol('preview')} onEnd={endDrag} />
746
782
  </>
747
783
  )}
@@ -314,6 +314,17 @@ const profileOf = (objects, rawFiles, systemCount) => ({
314
314
  export function MediaLibraryBrowse({
315
315
  client, title = 'MEDIA', bucket, onBucketChange, prefix: prefixProp, onPrefix, defaults, settings: settingsProp, onSettingsChange,
316
316
  folderTree, headerActions, refreshKey, onOpen, autoFocus = false, settingsFooter, className = '',
317
+ /* PASS-THROUGHS TO `ColumnBrowser`. A documented prop this page does not
318
+ * forward is a prop no consumer of the PAGE can reach, which makes the seam
319
+ * fictional — `thumbnailFor` and `folderMeta` shipped, were announced, and
320
+ * rendered as if they never existed because this signature ended before them
321
+ * (ColumnBrowserMobileViews items 10 + 13, kol-r2b2 2026-09-04). Second time
322
+ * this shape bit: `SettingsPanel` documented a `footer` slot this file
323
+ * hardcoded past. The rule the sweep leaves behind — WHEN A PROP IS ADDED TO
324
+ * `ColumnBrowser`, IT IS ADDED HERE IN THE SAME EDIT, unless the page holds a
325
+ * real opinion about it (it owns `height`, the widths and `partition`, so
326
+ * those are deliberately absent from this list). */
327
+ thumbnailFor, folderMeta, formatDate, stackView,
317
328
  }) {
318
329
  const [ownPrefix, setOwnPrefix] = useState('')
319
330
  const prefix = prefixProp ?? ownPrefix
@@ -483,6 +494,10 @@ export function MediaLibraryBrowse({
483
494
  kindOf={kindOf}
484
495
  kindLabel={KIND_LABEL}
485
496
  formatSize={formatSize}
497
+ formatDate={formatDate}
498
+ thumbnailFor={thumbnailFor}
499
+ folderMeta={folderMeta}
500
+ stackView={stackView}
486
501
  renderPreview={(o) => {
487
502
  const real = { ...o, key: o.key.slice(VROOT.length) }
488
503
  if (isImage(real.contentType)) return <ImageFrame src={mediaUrl(real.key)} />