@kolkrabbi/kol-component 0.242.0 → 0.244.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.244.0",
|
|
4
4
|
"description": "The core component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"embla-carousel-react": "^8.6.0",
|
|
27
27
|
"pdfjs-dist": "^6.3.289",
|
|
28
28
|
"react-syntax-highlighter": "^16.1.1",
|
|
29
|
-
"@kolkrabbi/kol-
|
|
30
|
-
"@kolkrabbi/kol-
|
|
29
|
+
"@kolkrabbi/kol-search": "^0.3.0",
|
|
30
|
+
"@kolkrabbi/kol-markdown": "^0.1.3"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@kolkrabbi/kol-icons": ">=0.22.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react'
|
|
2
2
|
import { usePopover, PopoverPanel } from '../utilities/Popover.jsx'
|
|
3
|
+
import { pushLayer, popLayer } from '../utilities/layerStack.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* ContextMenu — a right-click menu, anchored at the pointer.
|
|
@@ -63,6 +64,13 @@ export function useContextMenu() {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export default function ContextMenu({ menu, children, className = '' }) {
|
|
67
|
+
/* ONE ESCAPE, ONE LEVEL (kol-fxr 2026-10-08: a menu opened inside a FullscreenOverlay took the
|
|
68
|
+
* dialog with it). Open, the menu is the top layer, so the overlay under it ignores that Escape. */
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!menu.open) return undefined
|
|
71
|
+
const layer = pushLayer()
|
|
72
|
+
return () => popLayer(layer)
|
|
73
|
+
}, [menu.open])
|
|
66
74
|
if (!menu.open) return null
|
|
67
75
|
return (
|
|
68
76
|
/* `focus={false}` — FloatingFocusManager reads attributes off the reference,
|
|
@@ -87,6 +87,7 @@ import { GRAB_COLUMN } from '../utilities/motion.js'
|
|
|
87
87
|
* `KOL-R2B2/<label>/…`) must strip its own prefix before it can look anything up or build a URL —
|
|
88
88
|
* the browser has no idea which part of a path is yours (kol-r2b2 2026-09-04).
|
|
89
89
|
* @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
|
|
90
|
+
* @param {boolean} tapSelects below the breakpoint a file tap selects only, no Quick Look (a picker)
|
|
90
91
|
* @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
|
|
91
92
|
*
|
|
92
93
|
* BELOW `md` (768) THIS IS A DIFFERENT TREE — one full-width inline-expanding
|
|
@@ -187,7 +188,7 @@ const defaultPartition = (objects, prefix) => {
|
|
|
187
188
|
const rel = prefix ? o.key.slice(prefix.length) : o.key
|
|
188
189
|
const slash = rel.indexOf('/')
|
|
189
190
|
if (slash !== -1) folderSet.add(rel.slice(0, slash + 1))
|
|
190
|
-
else files.push({ ...o, displayKey: o.displayKey ?? rel })
|
|
191
|
+
else files.push({ ...o, displayKey: o.displayKey ?? o.displayName ?? rel })
|
|
191
192
|
}
|
|
192
193
|
return { folders: [...folderSet].sort(), files }
|
|
193
194
|
}
|
|
@@ -573,6 +574,9 @@ export default function ColumnBrowser({
|
|
|
573
574
|
* media bucket the thumbnail IS the tile, so it carries counts and sizes
|
|
574
575
|
* rather than dates (§2). Above the breakpoint the columns are unaffected. */
|
|
575
576
|
stackView = 'list',
|
|
577
|
+
/* `tapSelects` — below the breakpoint a file tap SELECTS and opens nothing (kol-fxr 2026-10-08: a
|
|
578
|
+
* picker's tap must pick, and Quick Look covered the host's Open button). Off: a tap opens Quick Look. */
|
|
579
|
+
tapSelects = false,
|
|
576
580
|
autoFocus = false,
|
|
577
581
|
className = '',
|
|
578
582
|
}) {
|
|
@@ -899,6 +903,7 @@ export default function ColumnBrowser({
|
|
|
899
903
|
if (isFolder) { onPrefix(r.level + r.name); return }
|
|
900
904
|
const files = itemsAt(r.level ?? '').filter((it) => it.type === 'file').map((it) => it.o)
|
|
901
905
|
pick(o)
|
|
906
|
+
if (tapSelects) return
|
|
902
907
|
onQuickLook?.({ files: files.length ? files : [o], index: Math.max(0, files.findIndex((f) => f.key === o.key)) })
|
|
903
908
|
}}
|
|
904
909
|
>
|
|
@@ -966,6 +971,7 @@ export default function ColumnBrowser({
|
|
|
966
971
|
onClick={() => {
|
|
967
972
|
const files = itemsAt(r.level ?? '').filter((it) => it.type === 'file').map((it) => it.o)
|
|
968
973
|
pick(r.o)
|
|
974
|
+
if (tapSelects) return
|
|
969
975
|
onQuickLook?.({ files: files.length ? files : [r.o], index: Math.max(0, files.findIndex((f) => f.key === r.o.key)) })
|
|
970
976
|
}}
|
|
971
977
|
/>
|
|
@@ -247,7 +247,10 @@ function useBucketLibrary({ client, bucket, defaults, settings: controlled, onSe
|
|
|
247
247
|
const mediaUrl = (key) => client?.mediaUrl?.(key, bucketId ?? undefined) ?? key
|
|
248
248
|
const downloadUrl = (key) => client?.downloadUrl?.(key, bucketId ?? undefined) ?? mediaUrl(key)
|
|
249
249
|
const writable = !!bucketMeta.writable && !!(client?.deleteObject || client?.renameObject)
|
|
250
|
-
|
|
250
|
+
/* A CLIENT WITH NO `mediaUrl` HAS NO URLS (kol-fxr 2026-10-08: a stored preset is not a fetchable
|
|
251
|
+
* object) — Copy URL, Download and Quick Look's fetch all go, as write verbs go without a writer. */
|
|
252
|
+
const hasUrl = typeof client?.mediaUrl === 'function'
|
|
253
|
+
return { buckets, bucketMeta, bucketId, settings, setSettings, objects, setObjects, folderInfo, setFolderInfo, reload, error, ready, mediaUrl, downloadUrl, writable, hasUrl }
|
|
251
254
|
}
|
|
252
255
|
|
|
253
256
|
/* ── shared pieces, kol-r2b2's ─────────────────────────────────────────────── */
|
|
@@ -440,7 +443,7 @@ function FileRow({ o, onClick, onDoubleClick, depth = 0, formatDate, thumb, cols
|
|
|
440
443
|
{/* a file with no thumbnail draws its glyph at the FOLDER's size — one icon column (user 2026-09-25) */}
|
|
441
444
|
{thumb ?? <Icon name={kindOf(o) === 'image' ? 'image' : 'file'} size={glyphSize(size)} />}
|
|
442
445
|
</span>
|
|
443
|
-
<span className={`kol-item-name min-w-0 truncate${selected ? '' : ' text-fg-default'}`}>{o.displayKey ?? o.key.split('/').pop()}</span>
|
|
446
|
+
<span className={`kol-item-name min-w-0 truncate${selected ? '' : ' text-fg-default'}`}>{o.displayKey ?? o.displayName ?? o.key.split('/').pop()}</span>
|
|
444
447
|
</span>
|
|
445
448
|
<span className="flex-1" />
|
|
446
449
|
{cols && <>
|
|
@@ -588,7 +591,7 @@ function FileEditorHost({ o, client, bucket, onClose, onSaved, assets }) {
|
|
|
588
591
|
return () => { live = false }
|
|
589
592
|
}, [o.key]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
590
593
|
return (
|
|
591
|
-
<DocumentEditor name={o.displayKey ?? o.key.split('/').pop()} kind={editKindOf(o)} text={text}
|
|
594
|
+
<DocumentEditor name={o.displayKey ?? o.displayName ?? o.key.split('/').pop()} kind={editKindOf(o)} text={text}
|
|
592
595
|
savedAt={o.uploaded ? Date.parse(o.uploaded) : 0} draft={{ bucket, key: o.key }} assets={assets}
|
|
593
596
|
onSave={async (next) => { const r = await client.writeText(o.key, next, bucket); onSaved?.(o.key, r, next) }}
|
|
594
597
|
onClose={onClose} />
|
|
@@ -622,7 +625,7 @@ function FileEditorHost({ o, client, bucket, onClose, onSaved, assets }) {
|
|
|
622
625
|
* @param {boolean} showNav - ‹ › and n / N in the header (default on); off when not paging a selection
|
|
623
626
|
* @param {Function} onEdit - `(file) => void` — an Edit action in the header for a text file (markdown · json · yaml · text · code); absent, no Edit
|
|
624
627
|
*/
|
|
625
|
-
export function MediaInspector({ files, index, onClose, onPrev, onNext, mediaUrl, downloadUrl, keySet, showNav = true, onEdit }) {
|
|
628
|
+
export function MediaInspector({ files, index, onClose, onPrev, onNext, mediaUrl, downloadUrl, keySet, showNav = true, onEdit, hasUrl = true }) {
|
|
626
629
|
const o = files[index]
|
|
627
630
|
const [dims, setDims] = useState(null)
|
|
628
631
|
/* the window's size survives paging — Finder keeps it — so it lives here, not in the frame,
|
|
@@ -646,7 +649,7 @@ export function MediaInspector({ files, index, onClose, onPrev, onNext, mediaUrl
|
|
|
646
649
|
shownDims?.len && formatLength(shownDims.len),
|
|
647
650
|
].filter(Boolean).join(' · ')
|
|
648
651
|
const frame = {
|
|
649
|
-
title: o.displayKey ?? o.key.split('/').pop(),
|
|
652
|
+
title: o.displayKey ?? o.displayName ?? o.key.split('/').pop(),
|
|
650
653
|
meta: facts,
|
|
651
654
|
onClose,
|
|
652
655
|
/* ‹ › and n / N only when the window pages a SELECTION (user 2026-09-23: *"I only selected one
|
|
@@ -666,17 +669,26 @@ export function MediaInspector({ files, index, onClose, onPrev, onNext, mediaUrl
|
|
|
666
669
|
<Button variant="nav" size="sm" iconOnly="edit" onClick={() => onEdit(o)} aria-label="Edit" />
|
|
667
670
|
</Tooltip>
|
|
668
671
|
)}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
672
|
+
{hasUrl && (
|
|
673
|
+
<>
|
|
674
|
+
<CopyAction label="Copy URL" text={mediaUrl(o.key)} />
|
|
675
|
+
<Tooltip label="Download">
|
|
676
|
+
<Button variant="nav" size="sm" iconOnly="download" href={downloadUrl(o.key)} download={o.key.split('/').pop()} aria-label="Download" />
|
|
677
|
+
</Tooltip>
|
|
678
|
+
</>
|
|
679
|
+
)}
|
|
673
680
|
</span>
|
|
674
681
|
),
|
|
675
682
|
}
|
|
676
683
|
return (
|
|
677
684
|
/* QUICK LOOK DIMS, it does not black out — you keep the list you came from in view */
|
|
678
685
|
<FullscreenOverlay open onClose={onClose} closeButton={false} scrim>
|
|
679
|
-
{
|
|
686
|
+
{!hasUrl ? (
|
|
687
|
+
/* nothing to fetch, so nothing to draw — said, not spun on (kol-fxr 2026-10-08) */
|
|
688
|
+
<QuickLookFrame {...frame}>
|
|
689
|
+
<p className="kol-helper-12 text-meta p-8">No preview</p>
|
|
690
|
+
</QuickLookFrame>
|
|
691
|
+
) : isImage(o.contentType) ? (
|
|
680
692
|
<QuickLookFrame {...frame}>
|
|
681
693
|
{/* A VECTOR HAS NO PIXEL SIZE (user 2026-09-24: a 32px logo.svg opened as a 32px window). It
|
|
682
694
|
* is drawn in a 640 box — the window's max if that is smaller — and contained in it. */}
|
|
@@ -1081,6 +1093,10 @@ export function MediaLibraryBrowse({
|
|
|
1081
1093
|
* browse surface inside the modal card needs the selection for its Use button). Keys are
|
|
1082
1094
|
* bucket-relative. Absent, nothing. */
|
|
1083
1095
|
onPickFile,
|
|
1096
|
+
/* `searchPlaceholder` — the search field's words; a store that is not a bucket names itself (kol-fxr 2026-10-08) */
|
|
1097
|
+
searchPlaceholder = 'Search this bucket',
|
|
1098
|
+
/* `o.displayName` on a listed object is its LABEL in every view, Quick Look and search (kol-fxr
|
|
1099
|
+
* 2026-10-08: names not unique, so the key is an id). Absent, the label is the key, as before. */
|
|
1084
1100
|
}) {
|
|
1085
1101
|
const [ownPrefix, setOwnPrefix] = useState('')
|
|
1086
1102
|
const prefix = prefixProp ?? ownPrefix
|
|
@@ -1089,7 +1105,11 @@ export function MediaLibraryBrowse({
|
|
|
1089
1105
|
const [ownBucket, setOwnBucket] = useState(bucket)
|
|
1090
1106
|
const bucketId = bucket ?? ownBucket
|
|
1091
1107
|
const lib = useBucketLibrary({ client, bucket: bucketId, defaults, settings: settingsProp, onSettingsChange, refreshKey })
|
|
1092
|
-
const { buckets, bucketMeta, settings, setSettings, objects, error, mediaUrl, downloadUrl,
|
|
1108
|
+
const { buckets, bucketMeta, settings, setSettings, objects, error, mediaUrl, downloadUrl, hasUrl } = lib
|
|
1109
|
+
/* `fileActions` GIVEN IS A WRITER (kol-fxr 2026-10-08): the consumer brings its own verbs, so the
|
|
1110
|
+
* client need not carry a `deleteObject` stub to be treated as writable. The bucket still has to
|
|
1111
|
+
* say it is writable. */
|
|
1112
|
+
const writable = lib.writable || (!!fileActions && !!bucketMeta.writable)
|
|
1093
1113
|
const switchBucket = (id, pfx = '') => { if (bucket == null) setOwnBucket(id); onBucketChange?.(id); setPrefix(pfx) }
|
|
1094
1114
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
1095
1115
|
const [trashOpen, setTrashOpen] = useState(false)
|
|
@@ -1945,7 +1965,7 @@ export function MediaLibraryBrowse({
|
|
|
1945
1965
|
* settings the wall always had. */
|
|
1946
1966
|
const wallKinds = new Set(settings.kinds ?? SETTINGS_BASE.kinds)
|
|
1947
1967
|
const wallSource = settings.flat
|
|
1948
|
-
? scoped.map((o) => ({ ...o, displayKey: prefix ? o.key.slice(prefix.length) : o.key }))
|
|
1968
|
+
? scoped.map((o) => ({ ...o, displayKey: o.displayName ?? (prefix ? o.key.slice(prefix.length) : o.key) }))
|
|
1949
1969
|
: dirFiles
|
|
1950
1970
|
const wallVisible = wallKinds.has('system') ? wallSource : wallSource.filter((o) => !isSystemFile(o.key))
|
|
1951
1971
|
const wallGrouped = settings.groupVariants ? groupVariants(wallVisible) : wallVisible
|
|
@@ -1958,7 +1978,7 @@ export function MediaLibraryBrowse({
|
|
|
1958
1978
|
* bar at `brand/og/` and `logos`, `original`, `projects` vanished with no filter set). The
|
|
1959
1979
|
* columns draw every level up to the root, so a list scoped to `prefix` cut every sibling path
|
|
1960
1980
|
* the moment the bar mounted. */
|
|
1961
|
-
const filterItems = isWall ? wallFiles : sortedObjects.filter((o) => !isSystemFile(o.key)).map((o) => ({ ...o, kind: kindOf(o), poster: posterFor(o.key, keySet), displayKey: o.key }))
|
|
1981
|
+
const filterItems = isWall ? wallFiles : sortedObjects.filter((o) => !isSystemFile(o.key)).map((o) => ({ ...o, kind: kindOf(o), poster: posterFor(o.key, keySet), displayKey: o.displayName ?? o.key }))
|
|
1962
1982
|
const filterKinds = [...new Set(filterItems.map((o) => o.kind))].sort()
|
|
1963
1983
|
const filterTags = [...new Set(filterItems.flatMap((o) => o.tags ?? []))].sort()
|
|
1964
1984
|
/* favourites as a one-chip group: ContentFilters matches a value, so a starred file carries one */
|
|
@@ -2006,6 +2026,7 @@ export function MediaLibraryBrowse({
|
|
|
2006
2026
|
<ColumnBrowser
|
|
2007
2027
|
autoFocus={autoFocus}
|
|
2008
2028
|
className={atTitleRoot ? 'is-root' : ''}
|
|
2029
|
+
tapSelects={!!onPickFile}
|
|
2009
2030
|
height={colH}
|
|
2010
2031
|
onHeightChange={onColH}
|
|
2011
2032
|
rowSize={rowSize}
|
|
@@ -2266,7 +2287,7 @@ export function MediaLibraryBrowse({
|
|
|
2266
2287
|
* that means the top hit. Arrow-then-Enter and a click go through `onSelect`, same jump. */
|
|
2267
2288
|
const searchResults = rankMedia(objects, searchQuery, { limit: 50 }).map((o) => ({
|
|
2268
2289
|
id: o.key,
|
|
2269
|
-
label: o.key.split('/').pop(),
|
|
2290
|
+
label: o.displayName ?? o.key.split('/').pop(),
|
|
2270
2291
|
hint: o.key.slice(0, o.key.lastIndexOf('/') + 1) || 'the bucket root',
|
|
2271
2292
|
group: KIND_LABEL[kindOf(o)] || 'File',
|
|
2272
2293
|
}))
|
|
@@ -2302,7 +2323,7 @@ export function MediaLibraryBrowse({
|
|
|
2302
2323
|
if ((q.kinds ?? []).length && !q.kinds.includes(kindOf(o))) return false
|
|
2303
2324
|
if (q.text && !filterMedia([o], q.text).length) return false
|
|
2304
2325
|
return true
|
|
2305
|
-
}).map((o) => ({ ...o, kind: kindOf(o), poster: posterFor(o.key, keySet), displayKey: o.key })) : null
|
|
2326
|
+
}).map((o) => ({ ...o, kind: kindOf(o), poster: posterFor(o.key, keySet), displayKey: o.displayName ?? o.key })) : null
|
|
2306
2327
|
/* A TYPED QUERY IS A RESULTS LIST (user 2026-09-30, reversing "a flat results list nobody asked
|
|
2307
2328
|
* for"): pruning the tree to matching paths hid the files one level down and read as "search only
|
|
2308
2329
|
* finds folders". While the bar holds a query, the tree views show every hit flat, each with its
|
|
@@ -2433,7 +2454,7 @@ export function MediaLibraryBrowse({
|
|
|
2433
2454
|
value={query}
|
|
2434
2455
|
onChange={(e) => setQuery(e.target.value ?? '')}
|
|
2435
2456
|
onClear={() => setQuery('')}
|
|
2436
|
-
placeholder=
|
|
2457
|
+
placeholder={searchPlaceholder}
|
|
2437
2458
|
size="sm"
|
|
2438
2459
|
className="flex-1 min-w-0"
|
|
2439
2460
|
/>
|
|
@@ -2497,7 +2518,7 @@ export function MediaLibraryBrowse({
|
|
|
2497
2518
|
onClose={() => { setSearchOpen(false); setSearchQuery('') }}
|
|
2498
2519
|
query={searchQuery}
|
|
2499
2520
|
onQueryChange={setSearchQuery}
|
|
2500
|
-
placeholder=
|
|
2521
|
+
placeholder={searchPlaceholder}
|
|
2501
2522
|
results={searchResults}
|
|
2502
2523
|
onExpand={() => searchResults[0] && jumpTo(searchResults[0].id)}
|
|
2503
2524
|
onSelect={(item) => (item.run ? item.run() : jumpTo(item.id))}
|
|
@@ -2514,7 +2535,7 @@ export function MediaLibraryBrowse({
|
|
|
2514
2535
|
</FullscreenOverlay>
|
|
2515
2536
|
)}
|
|
2516
2537
|
{quickLook && (
|
|
2517
|
-
<MediaInspector files={quickLook.files} index={quickLook.index} onClose={closeQuickLook} mediaUrl={mediaUrl} downloadUrl={downloadUrl} keySet={keySet} showNav={rowSelection.size > 1}
|
|
2538
|
+
<MediaInspector files={quickLook.files} index={quickLook.index} onClose={closeQuickLook} mediaUrl={mediaUrl} downloadUrl={downloadUrl} hasUrl={hasUrl} keySet={keySet} showNav={rowSelection.size > 1}
|
|
2518
2539
|
onEdit={canEdit ? (f) => { closeQuickLook(); openEditor(f) } : undefined}
|
|
2519
2540
|
onPrev={() => stepQuickLook(-1)} onNext={() => stepQuickLook(1)} />
|
|
2520
2541
|
)}
|
|
@@ -2637,7 +2658,7 @@ export function MediaLibraryBrowse({
|
|
|
2637
2658
|
{fa.move && (
|
|
2638
2659
|
<MenuDropdownItem iconLeft={<Icon name="arrow-right" size={14} />} onClick={() => doBatch(many, 'move', 'Move')}>Move {many.length} to…</MenuDropdownItem>
|
|
2639
2660
|
)}
|
|
2640
|
-
{many.every((p) => !p.endsWith('/')) && (
|
|
2661
|
+
{hasUrl && many.every((p) => !p.endsWith('/')) && (
|
|
2641
2662
|
<>
|
|
2642
2663
|
<MenuDropdownItem iconLeft={<Icon name="copy" size={14} />} onClick={() => doCopyUrl(many)}>Copy {many.length} URLs</MenuDropdownItem>
|
|
2643
2664
|
<MenuDropdownItem iconLeft={<Icon name="download" size={14} />} onClick={() => doDownload(many)}>Download {many.length}</MenuDropdownItem>
|
|
@@ -2651,8 +2672,17 @@ export function MediaLibraryBrowse({
|
|
|
2651
2672
|
</>
|
|
2652
2673
|
)
|
|
2653
2674
|
}
|
|
2675
|
+
const qlFile = onPickFile && target.type === 'file' ? objects.find((o) => o.key === target.path) : null
|
|
2654
2676
|
return (
|
|
2655
2677
|
<>
|
|
2678
|
+
{/* A PICKER'S TAP SELECTS (`tapSelects`), so Quick Look needs a door of its own on a
|
|
2679
|
+
* phone — the menu (kol-fxr 2026-10-08). Only with `onPickFile`; others are unchanged. */}
|
|
2680
|
+
{qlFile && (
|
|
2681
|
+
<>
|
|
2682
|
+
<MenuDropdownItem iconLeft={<Icon name="eye-on" size={14} />} onClick={() => openQuickLook({ files: [qlFile], index: 0 })}>Quick Look</MenuDropdownItem>
|
|
2683
|
+
<MenuDropdownDivider />
|
|
2684
|
+
</>
|
|
2685
|
+
)}
|
|
2656
2686
|
{fa.createFolder && (
|
|
2657
2687
|
<MenuDropdownItem iconLeft={<Icon name="folder" size={14} />} onClick={() => doCreateFolder(isFolder ? target.path : (isLevel ? target.path : prefix))}>
|
|
2658
2688
|
New folder{isFolder ? ` in ${target.path.replace(/\/$/, '').split('/').pop()}` : ''}
|
|
@@ -2685,10 +2715,10 @@ export function MediaLibraryBrowse({
|
|
|
2685
2715
|
seam a file goes through, so what a folder URL means is the consumer's call
|
|
2686
2716
|
(a CDN prefix, a listing, a signed path) and never this page's guess. No
|
|
2687
2717
|
Download: a folder is not an object to fetch. */}
|
|
2688
|
-
{!isLevel && (
|
|
2718
|
+
{hasUrl && !isLevel && (
|
|
2689
2719
|
<MenuDropdownItem iconLeft={<Icon name="copy" size={14} />} onClick={() => doCopyUrl([target.path])}>Copy URL</MenuDropdownItem>
|
|
2690
2720
|
)}
|
|
2691
|
-
{target.type === 'file' && (
|
|
2721
|
+
{hasUrl && target.type === 'file' && (
|
|
2692
2722
|
<MenuDropdownItem iconLeft={<Icon name="download" size={14} />} onClick={() => doDownload([target.path])}>Download</MenuDropdownItem>
|
|
2693
2723
|
)}
|
|
2694
2724
|
{!isLevel && canTag && (
|
|
@@ -2971,7 +3001,7 @@ export function MediaLibraryLibrary({
|
|
|
2971
3001
|
|
|
2972
3002
|
const scoped = prefix ? objects.filter((o) => o.key.startsWith(prefix)) : objects
|
|
2973
3003
|
const { files: dirFiles } = partition(scoped, prefix)
|
|
2974
|
-
const flatFiles = scoped.map((o) => ({ ...o, displayKey: prefix ? o.key.slice(prefix.length) : o.key }))
|
|
3004
|
+
const flatFiles = scoped.map((o) => ({ ...o, displayKey: o.displayName ?? (prefix ? o.key.slice(prefix.length) : o.key) }))
|
|
2975
3005
|
const levelFiles = flat ? flatFiles : dirFiles
|
|
2976
3006
|
const systemCount = levelFiles.filter((o) => isSystemFile(o.key)).length
|
|
2977
3007
|
const visibleFiles = kinds.has('system') ? levelFiles : levelFiles.filter((o) => !isSystemFile(o.key))
|
|
@@ -127,7 +127,7 @@ export function groupVariants(files) {
|
|
|
127
127
|
for (const [base, list] of groups) {
|
|
128
128
|
if (list.length < 2) { out.push(list[0]); continue }
|
|
129
129
|
list.sort((a, b) => a.width - b.width)
|
|
130
|
-
out.push({ ...list[0], displayKey: base, variants: list, totalSize: list.reduce((n, f) => n + (f.size || 0), 0) })
|
|
130
|
+
out.push({ ...list[0], displayKey: list[0].displayName ?? base, variants: list, totalSize: list.reduce((n, f) => n + (f.size || 0), 0) })
|
|
131
131
|
}
|
|
132
132
|
return out
|
|
133
133
|
}
|
|
@@ -140,7 +140,7 @@ export function partition(objects, prefix) {
|
|
|
140
140
|
const rel = prefix ? o.key.slice(prefix.length) : o.key
|
|
141
141
|
const slash = rel.indexOf('/')
|
|
142
142
|
if (slash !== -1) folderSet.add(rel.slice(0, slash + 1))
|
|
143
|
-
else files.push({ ...o, displayKey: rel })
|
|
143
|
+
else files.push({ ...o, displayKey: o.displayName ?? rel })
|
|
144
144
|
}
|
|
145
145
|
return { folders: [...folderSet].sort(), files }
|
|
146
146
|
}
|
|
@@ -16,7 +16,7 @@ import { kindOf } from './mediaKinds.js'
|
|
|
16
16
|
|
|
17
17
|
const toItem = (o, nameOf) => ({
|
|
18
18
|
id: o.key,
|
|
19
|
-
title: nameOf ? nameOf(o) : o.key.slice(o.key.lastIndexOf('/') + 1) || o.key,
|
|
19
|
+
title: nameOf ? nameOf(o) : o.displayName ?? (o.key.slice(o.key.lastIndexOf('/') + 1) || o.key),
|
|
20
20
|
keywords: [o.key],
|
|
21
21
|
tags: o.tags ?? [],
|
|
22
22
|
kind: o.kind ?? kindOf(o),
|