@kolkrabbi/kol-component 0.94.0 → 0.96.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.94.0",
3
+ "version": "0.96.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",
@@ -30,7 +30,7 @@
30
30
  "react-router-dom": "^6.0.0 || ^7.0.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@kolkrabbi/kol-icons": "^0.18.0"
33
+ "@kolkrabbi/kol-icons": "^0.19.0"
34
34
  },
35
35
  "files": [
36
36
  "src",
package/src/index.js CHANGED
@@ -140,6 +140,7 @@ export { default as MediaViewer } from './organisms/MediaViewer.jsx'
140
140
  export { default as SettingsPanel, SettingsRow, SettingsSwitch, SettingsChoice, SettingsChipRow, SettingsFooter } from './organisms/SettingsPanel.jsx'
141
141
  export { default as SectionNewsletter } from './organisms/SectionNewsletter.jsx'
142
142
  export { default as NewsletterBand } from './organisms/NewsletterBand.jsx'
143
+ export { default as ColumnBrowser } from './organisms/ColumnBrowser.jsx'
143
144
  export { default as RecordManager } from './organisms/RecordManager.jsx'
144
145
  export { default as SpectrumGrid } from './organisms/SpectrumGrid.jsx'
145
146
  export { default as Table } from './organisms/Table.jsx'
@@ -40,7 +40,10 @@ const RATIOS = {
40
40
  /* ruled box values per variant, verbatim from the §3 reference cards —
41
41
  * paddings in tokens: pad-card-sm 12 · pad-card-md 16 · pad-card-lg 24 */
42
42
  const BOX = {
43
- default: { layout: 'stack', border: 'var(--kol-fg-12)', bg: 'var(--kol-fg-02)', pad: 'var(--kol-pad-card-sm)' },
43
+ /* no border (ColumnBrowser round, user 2026-08-27: "I don't like the border, I feel
44
+ * like I've said that before" — the ListingCardThumbBorder ruling): the selected
45
+ * state reads from the checked ToggleCheckbox, not a fg-64 border */
46
+ default: { layout: 'stack', border: null, bg: 'var(--kol-fg-02)', pad: 'var(--kol-pad-card-sm)' },
44
47
  catalog: { layout: 'fill-card', border: 'var(--kol-fg-04)', bg: 'var(--kol-fg-04)', pad: 'var(--kol-pad-card-sm) var(--kol-pad-card-md)', plateTop: true, plateBg: 'var(--kol-surface-primary)' },
45
48
  /* flip: PrintGridCard's 3D turn on `isFlipped` → `selected` (ContentRowsAndPrintCard, 2026-08-27) */
46
49
  print: { layout: 'fill-card', border: null, bg: 'var(--kol-surface-secondary)', pad: 'var(--kol-pad-card-sm) var(--kol-pad-card-md)', plateTop: true, flip: true },
@@ -104,6 +107,7 @@ export default function ContentCard({
104
107
  zoom,
105
108
  control,
106
109
  controlStart,
110
+ reveal,
107
111
  actions,
108
112
  expanded = false,
109
113
  expandedContent,
@@ -260,14 +264,21 @@ export default function ContentCard({
260
264
  {textNode && <div className="kol-card-drawer">{textNode}</div>}
261
265
  </>
262
266
  ) : (
263
- /* canvas — media fills the frame, plate floats on top */
267
+ /* canvas — media fills the frame, plate floats on top. `reveal`
268
+ * (TypefaceCardAndRow, 2026-08-27): on hover the plate and the media
269
+ * fade out and the reveal node fades in — the card owns the
270
+ * choreography (kol-theme `.kol-card.has-reveal`), the consumer owns the
271
+ * node (what it says and which face it wears are never the family's). */
264
272
  <>
265
- <div className="absolute" style={{ inset: 0 }}>
273
+ <div className="kol-card-canvas-media absolute" style={{ inset: 0 }}>
266
274
  <ContentMedia ratio={null} {...mediaProps}>{media}</ContentMedia>
267
275
  {controlNode}
268
276
  {controlStartNode}
269
277
  </div>
270
278
  {textNode}
279
+ {reveal != null && (
280
+ <div className="kol-card-reveal absolute inset-0 flex items-center justify-center p-8 pointer-events-none" style={{ zIndex: 2 }}>{reveal}</div>
281
+ )}
271
282
  </>
272
283
  )
273
284
 
@@ -290,7 +301,7 @@ export default function ContentCard({
290
301
 
291
302
  const common = {
292
303
  'data-tags': isHero && Array.isArray(text.tags) && text.tags.length ? text.tags.join(' ') : undefined,
293
- className: `kol-card group flex ${expanded ? 'flex-row-reverse' : 'flex-col'} ${box.layout === 'drawer' ? 'relative overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${framed ? 'overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${box.border ? 'border' : ''} ${box.layout === 'canvas' ? 'relative' : ''} ${interactive ? 'cursor-pointer select-none' : ''} ${hoverBg && interactive ? 'kol-content-hover' : ''} ${className}`.trim(),
304
+ className: `kol-card group flex ${box.layout === 'canvas' && reveal != null ? 'has-reveal' : ''} ${expanded ? 'flex-row-reverse' : 'flex-col'} ${box.layout === 'drawer' ? 'relative overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${framed ? 'overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${box.border ? 'border' : ''} ${box.layout === 'canvas' ? 'relative' : ''} ${interactive ? 'cursor-pointer select-none' : ''} ${hoverBg && interactive ? 'kol-content-hover' : ''} ${className}`.trim(),
294
305
  style: {
295
306
  /* same reason as ContentRow: rest colours are PROPERTIES, because an
296
307
  * inline background/borderColor outranks the hover class and the step
@@ -70,8 +70,9 @@ import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
70
70
  * @param {ReactNode} children the real media
71
71
  */
72
72
  const FIT = {
73
- cover: '[&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>video]:h-full [&>video]:w-full [&>video]:object-cover',
74
- natural: '[&>img]:h-full [&>img]:w-full [&>img]:object-contain [&>video]:h-full [&>video]:w-full [&>video]:object-contain',
73
+ /* a consumer WRAPPER div fills the frame as an img/video does (ColumnBrowser round, 2026-08-27 — kol-r2b2's wrapped thumb fell back to the image's intrinsic size) */
74
+ cover: '[&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>video]:h-full [&>video]:w-full [&>video]:object-cover [&>div]:h-full [&>div]:w-full',
75
+ natural: '[&>img]:h-full [&>img]:w-full [&>img]:object-contain [&>video]:h-full [&>video]:w-full [&>video]:object-contain [&>div]:h-full [&>div]:w-full',
75
76
  compact: 'grid place-items-center [&>img]:max-h-[70%] [&>img]:max-w-[70%] [&>img]:object-contain',
76
77
  }
77
78
 
@@ -88,13 +88,16 @@ const RAMP = {
88
88
  row: { title: 'kol-mono-12 text-body uppercase truncate', body: 'kol-sans-heading-03 leading-tight text-emphasis truncate', meta: 'kol-mono-12 text-body', date: 'kol-mono-12 text-meta', tags: 'flex flex-wrap items-center gap-1.5' },
89
89
  },
90
90
  typeface: {
91
+ /* RULED ON SCREEN (TypefaceCardAndRow, kol-website 2026-08-27): name and
92
+ * classification are FULL ink, the year steps to 64; the card's title is the
93
+ * row's title string — one title voice for the typeface family. Ink only. */
91
94
  /* same as the row: the SPECIMEN carries the emphasis on a typeface card,
92
95
  * so the name steps down. The glyph is what you came to look at. */
93
- card: { title: 'kol-mono-16 text-body', body: 'kol-mono-12 text-meta', date: 'kol-mono-12 text-meta' },
96
+ card: { title: 'kol-mono-14 uppercase text-emphasis', body: 'kol-mono-12 text-meta', date: 'kol-mono-12 text-meta' },
94
97
  /* the name steps DOWN to `body`: on a typeface row the SPECIMEN is the
95
98
  * thing you came for and it carries the one emphasis. Two full inks in one
96
99
  * block is the ladder broken, and the measurement caught it. */
97
- row: { title: 'kol-mono-14 uppercase text-body', body: 'kol-mono-12 text-meta', detail: 'kol-mono-14 text-body', date: 'kol-mono-12 text-meta' },
100
+ row: { title: 'kol-mono-14 uppercase text-emphasis', body: 'kol-mono-12 text-meta', detail: 'kol-mono-14 text-emphasis', date: 'kol-mono-12 text-fg-64' },
98
101
  },
99
102
  }
100
103
 
@@ -0,0 +1,288 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { Icon } from '@kolkrabbi/kol-icons'
3
+
4
+ /**
5
+ * ColumnBrowser — Finder-style Miller columns over a flat key space (kol-r2b2's
6
+ * `src/ColumnBrowser.jsx`, built and ruled there first — user 2026-08-27: "make
7
+ * it locally first, then ship it" — and shipped verbatim; ticket ColumnBrowser).
8
+ * The listing surface's FOLDER view: it replaces the folder rows above the
9
+ * files, it is not a card layout.
10
+ *
11
+ * One column per path segment of `prefix`: column 0 is the root level, column
12
+ * k the contents of the k-th folder on the path. Picking a folder sets the
13
+ * prefix to it (and so truncates deeper columns); picking a file highlights it
14
+ * and opens a PREVIEW column on the right — the image and its facts, as Finder
15
+ * does.
16
+ *
17
+ * Keyboard, Finder's: ↑/↓ move within the column (a folder opens its column as
18
+ * you land on it, a file previews), → steps into the open folder's column,
19
+ * ← steps back to the parent column. Space = Quick Look: `onQuickLook({ files,
20
+ * index })` over the column's files; while it is open ↑/↓ step the file off
21
+ * `window` (the overlay holds focus) and space closes — the opening keystroke
22
+ * bubbles from here and is ignored.
23
+ *
24
+ * Rows wear the DS Table's cell metrics (12px 16px, mono 12, an oq-08 hairline
25
+ * between rows, none after the last). Selected and cursor rows draw the HOVER
26
+ * fill (`bg-fg-04`) — user ruling: "make hover state the selected state".
27
+ * Height is fixed at 528px = 12 rows × 44px (user ruling); columns scroll.
28
+ *
29
+ * The app-owned bits are SEAMS with working defaults: `urlOf(o)` (the preview
30
+ * image — no URL, no image), `kindOf(o)` (image / video / audio / file from
31
+ * `contentType`), `kindLabel`, `formatSize`, `partition(objects, level)`.
32
+ *
33
+ * @param {Array<{key: string, size?: number, uploaded?: string, contentType?: string}>} objects the flat key space
34
+ * @param {string} prefix the open folder path ('' = root, 'a/b/' = two deep)
35
+ * @param {Function} onPrefix (prefix) => void
36
+ * @param {{files: object[], index: number}|null} quickLook the open Quick Look, or null
37
+ * @param {Function} onQuickLook (quickLook|null) => void — space opens / closes, ↑/↓ step while open
38
+ * @param {Function} urlOf (o) => string — the object's public URL for the preview image
39
+ * @param {Function} kindOf (o) => 'image' | 'video' | 'audio' | string
40
+ * @param {Object} kindLabel kind → label shown when there is no visual preview
41
+ * @param {Function} formatSize (bytes) => string
42
+ * @param {Function} partition (objects, level) => { folders: string[], files: object[] }
43
+ * @param {string} className extra classes on the browser
44
+ */
45
+
46
+ const defaultKindOf = (o) => {
47
+ const ct = o.contentType || ''
48
+ if (ct.startsWith('image/')) return 'image'
49
+ if (ct.startsWith('video/')) return 'video'
50
+ if (ct.startsWith('audio/')) return 'audio'
51
+ return 'file'
52
+ }
53
+ const DEFAULT_KIND_LABEL = { image: 'image', video: 'video', audio: 'audio', file: 'file' }
54
+ const defaultFormatSize = (bytes) => {
55
+ if (bytes == null) return '—'
56
+ if (bytes < 1024) return `${bytes} B`
57
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
58
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
59
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
60
+ }
61
+ /* kol-r2b2's partition: the next path segment is a folder, the rest are files */
62
+ const defaultPartition = (objects, prefix) => {
63
+ const folderSet = new Set()
64
+ const files = []
65
+ for (const o of objects) {
66
+ const rel = prefix ? o.key.slice(prefix.length) : o.key
67
+ const slash = rel.indexOf('/')
68
+ if (slash !== -1) folderSet.add(rel.slice(0, slash + 1))
69
+ else files.push({ ...o, displayKey: o.displayKey ?? rel })
70
+ }
71
+ return { folders: [...folderSet].sort(), files }
72
+ }
73
+ const isImage = (o) => (o.contentType || '').startsWith('image/')
74
+
75
+ /* kol-icons has no `audio` glyph yet — audio rows wear `file` until it does */
76
+ const COL_ICON = { image: 'image', video: 'video', audio: 'file' }
77
+
78
+ function Row({ icon, label, active, cursor = false, trailing, onClick, muted = false }) {
79
+ return (
80
+ <li
81
+ /* Row metrics are the DS Table's (kol-components-organisms.css .kol-table-cell-*):
82
+ * 12px 16px padding, mono 12, an oq-08 hairline between rows, none after the last. */
83
+ /* `cursor` = the keyboard row, drawn with the hover fill so ↑/↓ always shows where you are. */
84
+ className={`kol-column-browser-row flex items-center gap-2 px-4 py-3 border-b last:border-b-0 cursor-pointer transition-colors ${
85
+ active || cursor ? 'bg-fg-04 text-fg-default' : muted ? 'text-fg-48 hover:bg-fg-04' : 'text-fg-default hover:bg-fg-04'
86
+ }`}
87
+ style={{ borderColor: 'var(--kol-oq-08)' }}
88
+ onClick={onClick}
89
+ >
90
+ <span className="w-5 shrink-0 flex items-center justify-center text-fg-48">
91
+ <Icon name={icon} size={14} />
92
+ </span>
93
+ <span className="kol-mono-12 flex-1 truncate">{label}</span>
94
+ {trailing}
95
+ </li>
96
+ )
97
+ }
98
+
99
+ function Preview({ o, urlOf, kindOf, kindLabel, formatSize }) {
100
+ // Pixel size comes from the loaded image itself — the bucket stores none.
101
+ const [dims, setDims] = useState(null)
102
+ const src = isImage(o) ? urlOf?.(o) : null
103
+ const facts = [
104
+ ['Kind', kindLabel[kindOf(o)] || 'file'],
105
+ ['Type', o.contentType || '—'],
106
+ ['Size', formatSize(o.size)],
107
+ ...(src ? [['Dimensions', dims ? `${dims.w} × ${dims.h} px` : '…']] : []),
108
+ ['Date', o.uploaded ? new Date(o.uploaded).toISOString().slice(0, 10) : '—'],
109
+ ]
110
+ return (
111
+ <div className="kol-column-browser-preview w-[320px] shrink-0 overflow-y-auto p-4 flex flex-col gap-4">
112
+ <div className="w-full aspect-square bg-fg-04 rounded flex items-center justify-center overflow-hidden">
113
+ {src ? (
114
+ <img
115
+ src={src}
116
+ alt=""
117
+ className="max-w-full max-h-full object-contain"
118
+ loading="lazy"
119
+ onLoad={(e) => setDims({ w: e.target.naturalWidth, h: e.target.naturalHeight })}
120
+ />
121
+ ) : (
122
+ <span className="kol-mono-12 text-fg-48">{kindLabel[kindOf(o)] || 'file'}</span>
123
+ )}
124
+ </div>
125
+ <p className="kol-mono-12 text-fg-default break-all">{o.displayKey ?? o.key}</p>
126
+ <dl className="flex flex-col gap-1">
127
+ {facts.map(([k, v]) => (
128
+ <div key={k} className="flex justify-between gap-4 kol-mono-12">
129
+ <dt className="text-fg-48">{k}</dt>
130
+ <dd className="text-fg-default text-right break-all">{v}</dd>
131
+ </div>
132
+ ))}
133
+ </dl>
134
+ </div>
135
+ )
136
+ }
137
+
138
+ export default function ColumnBrowser({
139
+ objects = [],
140
+ prefix = '',
141
+ onPrefix = () => {},
142
+ quickLook = null,
143
+ onQuickLook,
144
+ urlOf,
145
+ kindOf = defaultKindOf,
146
+ kindLabel = DEFAULT_KIND_LABEL,
147
+ formatSize = defaultFormatSize,
148
+ partition = defaultPartition,
149
+ className = '',
150
+ }) {
151
+ const [picked, setPicked] = useState(null)
152
+ // Keyboard cursor: which column, which row. Clicks keep it in sync.
153
+ const [cursor, setCursor] = useState({ col: 0, idx: 0 })
154
+ const rootRef = useRef(null)
155
+
156
+ const levelsOf = (pfx) => {
157
+ const out = ['']
158
+ if (pfx) {
159
+ const segs = pfx.replace(/\/$/, '').split('/')
160
+ segs.forEach((_, i) => out.push(segs.slice(0, i + 1).join('/') + '/'))
161
+ }
162
+ return out
163
+ }
164
+ const itemsAt = (level) => {
165
+ const { folders, files } = partition(objects.filter((o) => o.key.startsWith(level)), level)
166
+ return [...folders.map((f) => ({ type: 'folder', name: f })), ...files.map((o) => ({ type: 'file', o }))]
167
+ }
168
+ const land = (level, item) => {
169
+ if (!item) return
170
+ if (item.type === 'folder') { setPicked(null); onPrefix(level + item.name) }
171
+ else setPicked(item.o)
172
+ }
173
+
174
+ const onKeyDown = (e) => {
175
+ // Space = Quick Look over the picked column's files, starting at the picked one.
176
+ if (e.key === ' ' && picked) {
177
+ e.preventDefault()
178
+ const siblings = itemsAt(levelsOf(prefix)[cursor.col] ?? '').filter((it) => it.type === 'file').map((it) => it.o)
179
+ onQuickLook?.({ files: siblings, index: Math.max(0, siblings.findIndex((o) => o.key === picked.key)) })
180
+ return
181
+ }
182
+ if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return
183
+ e.preventDefault()
184
+ const lv = levelsOf(prefix)
185
+ const col = Math.min(cursor.col, lv.length - 1)
186
+ const items = itemsAt(lv[col])
187
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
188
+ const idx = Math.max(0, Math.min(items.length - 1, cursor.idx + (e.key === 'ArrowDown' ? 1 : -1)))
189
+ setCursor({ col, idx })
190
+ land(lv[col], items[idx])
191
+ } else if (e.key === 'ArrowRight') {
192
+ // Into the open folder's column, first row.
193
+ if (lv[col + 1]) {
194
+ const next = itemsAt(lv[col + 1])
195
+ setCursor({ col: col + 1, idx: 0 })
196
+ land(lv[col + 1], next[0])
197
+ }
198
+ } else if (col > 0) {
199
+ // Back to the parent column, on the folder we came out of.
200
+ const parentItems = itemsAt(lv[col - 1])
201
+ const opened = lv[col].slice(lv[col - 1].length)
202
+ const idx = Math.max(0, parentItems.findIndex((it) => it.type === 'folder' && it.name === opened))
203
+ setCursor({ col: col - 1, idx })
204
+ setPicked(null)
205
+ onPrefix(lv[col])
206
+ }
207
+ }
208
+ // While Quick Look is open the overlay holds focus, so the same keys are read
209
+ // off window: ↑/↓ step the picked file (overlay follows), space closes. The
210
+ // opening keystroke bubbles from this component and is ignored.
211
+ useEffect(() => {
212
+ if (!quickLook) return
213
+ const onKey = (e) => {
214
+ if (rootRef.current?.contains(e.target)) return
215
+ if (e.key === ' ') { e.preventDefault(); onQuickLook?.(null); return }
216
+ if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return
217
+ e.preventDefault()
218
+ const lv = levelsOf(prefix)
219
+ const col = Math.min(cursor.col, lv.length - 1)
220
+ const items = itemsAt(lv[col])
221
+ let idx = cursor.idx
222
+ const step = e.key === 'ArrowDown' ? 1 : -1
223
+ do { idx += step } while (items[idx] && items[idx].type !== 'file')
224
+ if (!items[idx]) return
225
+ setCursor({ col, idx })
226
+ setPicked(items[idx].o)
227
+ const siblings = items.filter((it) => it.type === 'file').map((it) => it.o)
228
+ onQuickLook?.({ files: siblings, index: siblings.findIndex((o) => o.key === items[idx].o.key) })
229
+ }
230
+ window.addEventListener('keydown', onKey)
231
+ return () => window.removeEventListener('keydown', onKey)
232
+ })
233
+
234
+ const levels = levelsOf(prefix) // '' → ['']; 'a/b/' → ['', 'a/', 'a/b/']
235
+ // While Quick Look is open the overlay's index is the truth for the highlight.
236
+ const shown = quickLook ? quickLook.files[quickLook.index] : picked
237
+
238
+ return (
239
+ <div
240
+ ref={rootRef}
241
+ tabIndex={0}
242
+ onKeyDown={onKeyDown}
243
+ // Fixed height — 12 rows of 44px (user ruling 2026-08-27), so the browser never jumps as columns change; each column scrolls.
244
+ className={`kol-column-browser flex overflow-x-auto h-[528px] border rounded outline-none ${className}`.trim()}
245
+ style={{ borderColor: 'var(--kol-oq-08)' }}
246
+ >
247
+ {levels.map((level, k) => {
248
+ const { folders, files } = partition(objects.filter((o) => o.key.startsWith(level)), level)
249
+ const next = levels[k + 1]
250
+ const activeFolder = next ? next.slice(level.length) : null
251
+ return (
252
+ <ul
253
+ key={level || '/'}
254
+ className="kol-column-browser-column w-[260px] shrink-0 overflow-y-auto border-r last:border-r-0"
255
+ style={{ borderColor: 'var(--kol-oq-08)' }}
256
+ >
257
+ {folders.map((f, i) => (
258
+ <Row
259
+ key={f}
260
+ icon="folder"
261
+ label={f.replace(/\/$/, '')}
262
+ active={f === activeFolder}
263
+ cursor={cursor.col === k && cursor.idx === i}
264
+ trailing={<Icon name="chevron-right" size={12} className="text-fg-32" />}
265
+ onClick={() => { setCursor({ col: k, idx: i }); rootRef.current?.focus(); setPicked(null); onPrefix(level + f) }}
266
+ />
267
+ ))}
268
+ {files.map((o, i) => (
269
+ <Row
270
+ key={o.key}
271
+ icon={COL_ICON[kindOf(o)] || 'file'}
272
+ label={o.displayKey ?? o.key}
273
+ muted={shown?.key !== o.key}
274
+ active={shown?.key === o.key}
275
+ cursor={cursor.col === k && cursor.idx === folders.length + i}
276
+ onClick={() => { setCursor({ col: k, idx: folders.length + i }); rootRef.current?.focus(); setPicked(o) }}
277
+ />
278
+ ))}
279
+ {folders.length === 0 && files.length === 0 && (
280
+ <li className="kol-mono-12 text-fg-32 px-4 py-3">empty</li>
281
+ )}
282
+ </ul>
283
+ )
284
+ })}
285
+ {shown && <Preview key={shown.key} o={shown} urlOf={urlOf} kindOf={kindOf} kindLabel={kindLabel} formatSize={formatSize} />}
286
+ </div>
287
+ )
288
+ }