@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.
|
|
4
|
-
"description": "KOL design-system components
|
|
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": "
|
|
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',
|
|
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)} />
|