@kolkrabbi/kol-component 0.317.2 → 0.317.4
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 +2 -2
- package/src/index.js +2 -2
- package/src/molecules/MobileTabBar.jsx +2 -2
- package/src/molecules/PageControls.jsx +2 -2
- package/src/molecules/TabChips.jsx +3 -3
- package/src/molecules/TabsRow.jsx +22 -16
- package/src/organisms/ColorPickerPopover.jsx +4 -3
- package/src/organisms/ColumnBrowser.jsx +10 -2
- package/src/organisms/MediaLibraryPages.jsx +83 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.317.
|
|
3
|
+
"version": "0.317.4",
|
|
4
4
|
"description": "The core component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@kolkrabbi/kol-icons": "^0.53.
|
|
41
|
+
"@kolkrabbi/kol-icons": "^0.53.5"
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"src",
|
package/src/index.js
CHANGED
|
@@ -150,7 +150,7 @@ export { default as ToolPalette } from './organisms/ToolPalette.jsx'
|
|
|
150
150
|
export { default as DocumentEditor } from './organisms/DocumentEditor.jsx'
|
|
151
151
|
export { default as SwatchControls, SwatchStack, EyedropPick } from './molecules/SwatchControls.jsx'
|
|
152
152
|
export { default as TabsRow, TABBAR_H, TABBAR_PILL_INSET, TABBAR_PILL_TAB, tabbarPillWidth } from './molecules/TabsRow.jsx'
|
|
153
|
-
/** @deprecated 2026-10-17 — alias of `TabsRow
|
|
153
|
+
/** @deprecated 2026-10-17 — alias of `TabsRow variant="chip"` */
|
|
154
154
|
export { default as TabChips } from './molecules/TabChips.jsx'
|
|
155
155
|
|
|
156
156
|
// organisms
|
|
@@ -180,7 +180,7 @@ export { default as Carousel } from './molecules/Carousel.jsx'
|
|
|
180
180
|
* stage reaches for it instead of re-typing the button markup, which is how the
|
|
181
181
|
* three in-package copies (and kol-website's CarouselNavigation) happened. */
|
|
182
182
|
export { default as EmblaNav } from './molecules/EmblaNav.jsx'
|
|
183
|
-
/** @deprecated 2026-10-17 — alias of `TabsRow
|
|
183
|
+
/** @deprecated 2026-10-17 — alias of `TabsRow variant="stacked" placement="phone"` */
|
|
184
184
|
export { default as MobileTabBar } from './molecules/MobileTabBar.jsx'
|
|
185
185
|
export { default as ContentFilters } from './organisms/ContentFilters.jsx'
|
|
186
186
|
export { default as PageHeader, PAGE_HEADER_ROLES } from './molecules/PageHeader.jsx'
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import TabsRow, { TABBAR_H } from './TabsRow.jsx'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @deprecated 2026-10-17 — alias of `TabsRow
|
|
4
|
+
* @deprecated 2026-10-17 — alias of `TabsRow variant="stacked" placement="phone"` (TabsStackedLayout). Drops when nobody imports it (04-retirements.md).
|
|
5
5
|
* MobileTabBar — Old name for the stacked phone bar. Same props: `tabs` (`{ value, label, icon }`) · `value` · `onChange` · `className`.
|
|
6
6
|
*/
|
|
7
7
|
export { TABBAR_H }
|
|
8
8
|
export default function MobileTabBar(props) {
|
|
9
|
-
return <TabsRow
|
|
9
|
+
return <TabsRow variant="stacked" placement="phone" {...props} />
|
|
10
10
|
}
|
|
@@ -13,7 +13,7 @@ import TabsRow from './TabsRow.jsx'
|
|
|
13
13
|
*
|
|
14
14
|
* The slots, in the order they are drawn — an empty slot is not there:
|
|
15
15
|
* row 1 toggles · views · count the series chips (Tag), the view switches (SegmentedToggle), the readout
|
|
16
|
-
* row 2 jump the section tabs (TabsRow
|
|
16
|
+
* row 2 jump the section tabs (TabsRow variant="chip"), scrolling sideways when long
|
|
17
17
|
* row 3 legend a dot and a name per series, as many columns as series
|
|
18
18
|
*
|
|
19
19
|
* STICKY by default: `sticky top-0` on the page's z-sticky rung, a hairline under it, the surface painted so the
|
|
@@ -51,7 +51,7 @@ export default function PageControls({ toggles, views = [], count, jump, legend,
|
|
|
51
51
|
)}
|
|
52
52
|
{jump && (
|
|
53
53
|
<div className="overflow-x-auto">
|
|
54
|
-
<TabsRow
|
|
54
|
+
<TabsRow variant="chip" size="sm" ariaLabel={jump.label} value={jump.value} onChange={jump.onChange} tabs={jump.tabs} />
|
|
55
55
|
</div>
|
|
56
56
|
)}
|
|
57
57
|
{legend && legend.length > 0 && (
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import TabsRow from './TabsRow.jsx'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @deprecated 2026-10-17 — alias of `TabsRow
|
|
5
|
-
* TabChips — Old name for TabsRow's chip
|
|
4
|
+
* @deprecated 2026-10-17 — alias of `TabsRow variant="chip"` (TabsStackedLayout: one tab component). Drops when nobody imports it (04-retirements.md).
|
|
5
|
+
* TabChips — Old name for TabsRow's chip variant. Same props: `tabs` · `value` · `onChange` · `size` · `ariaLabel` · `className`.
|
|
6
6
|
*/
|
|
7
7
|
export default function TabChips(props) {
|
|
8
|
-
return <TabsRow
|
|
8
|
+
return <TabsRow variant="chip" {...props} />
|
|
9
9
|
}
|
|
@@ -24,16 +24,21 @@ import { RAIL_ROW } from '../hooks/railRow.js'
|
|
|
24
24
|
* @param {Function} onClose () => void — show + wire the leading close button
|
|
25
25
|
* @param {Function} onMinimise () => void — show + wire the trailing minimise chevron
|
|
26
26
|
*
|
|
27
|
-
* ONE TAB COMPONENT, FOUR
|
|
27
|
+
* ONE TAB COMPONENT, FOUR VARIANTS (TabsStackedLayout, kol-monitor 2026-10-17 — four components drew one control). The prop
|
|
28
|
+
* is `variant`, the one word the DS uses for a look (control-chrome: "variant is the look"); `look` was an agent's coinage
|
|
29
|
+
* on the merge and is read as a silent alias until no published consumer passes it (the user, 2026-10-30: *"USE VARIANT"*).
|
|
28
30
|
* @param {'scroll'|'wrap'} [overflow='scroll'] A ROW WIDER THAN ITS COLUMN (TabsRowOverflow, kol-website 2026-10-29 — six
|
|
29
31
|
* underline tabs are 394px, so at 390 the page scrolled sideways and record wrapped the row in its own div): `scroll`
|
|
30
32
|
* keeps one line that scrolls sideways with no scrollbar, `.kol-scroll-fade--x` fading the clipped edge(s), and the
|
|
31
33
|
* active tab scrolled into view when it changes — arrow keys included; `wrap` breaks the tabs onto a second line.
|
|
32
|
-
* Every
|
|
33
|
-
* @param {'underline'|'chip'|'text'|'stacked'} [
|
|
34
|
+
* Every variant but `stacked`, which has its phone bar
|
|
35
|
+
* @param {'underline'|'chip'|'text'|'stacked'} [variant='underline']
|
|
36
|
+
* @param {string} [look] @deprecated 2026-10-30 — the old spelling of `variant`; `variant` wins when both are passed
|
|
37
|
+
* @param {'mono'|'sans'|'mono-light'} [type='mono'] `underline` only: the tab's type — `mono-light` is `sans`'s row on JetBrains Mono at 300 (his word, 2026-10-30)
|
|
34
38
|
* @param {boolean} [filled=false] - `chip` only: every chip shows at rest (Button `tab-filled`), so the row reads as choices
|
|
35
|
-
* underline — this row, as it always was; a tab's `icon` sits before its label. `
|
|
36
|
-
*
|
|
39
|
+
* underline — this row, as it always was; a tab's `icon` sits before its label. `type` picks its type: `mono`
|
|
40
|
+
* (default, the mono-12 row) or `sans` (the neutral sans at 300, a 20px glyph in a 40px row — the
|
|
41
|
+
* user's ref, Adobe's tab strip, 2026-10-29; named `type` by him, 2026-10-30)
|
|
37
42
|
* chip — the active tab a filled chip, the rest quiet (was `TabChips`); `size` is the control rung
|
|
38
43
|
* text — flat helper-type tabs, the active one bright (was kol-shell's `TabStrip`); `size` is the
|
|
39
44
|
* helper size (default 14), `tracked` adds 1px tracking
|
|
@@ -71,13 +76,14 @@ export const tabbarPillWidth = (count) => count * TABBAR_PILL_TAB + 14
|
|
|
71
76
|
/* the collapse's motion — one duration for the width and the glyph swap (300, user 2026-10-23: 180 read as abrupt) */
|
|
72
77
|
const COLLAPSE_MS = 300
|
|
73
78
|
|
|
74
|
-
export default function TabsRow({ tabs: raw = [], value, onChange, onClose, onMinimise, look = '
|
|
79
|
+
export default function TabsRow({ tabs: raw = [], value, onChange, onClose, onMinimise, variant, look: lookLegacy, type = 'mono', filled = false, placement = 'inline', shape = 'bar', collapsed = false, onExpand, edge, border, shadow, dock, active, foldable, folded, onFoldChange, foldSide, size, tracked = false, overflow = 'scroll', ariaLabel, className, style }) {
|
|
75
80
|
const tabs = raw.map((t) => ({ ...t, id: t.id ?? t.value }))
|
|
81
|
+
const look = variant ?? lookLegacy ?? 'underline'
|
|
76
82
|
const wrap = overflow === 'wrap'
|
|
77
83
|
if (look === 'chip') return <Overflow mode={overflow} value={value}><ChipTabs tabs={tabs} value={value} onChange={onChange} size={size ?? 'sm'} filled={filled} ariaLabel={ariaLabel} className={`${wrap ? 'flex-wrap ' : ''}${className ?? ''}`} /></Overflow>
|
|
78
84
|
if (look === 'text') return <Overflow mode={overflow} value={value}><TextTabs tabs={tabs} value={value} onChange={onChange} size={size ?? 14} tracked={tracked} className={`${wrap ? 'flex-wrap gap-y-2 ' : ''}${className ?? 'gap-6'}`} style={style} /></Overflow>
|
|
79
85
|
if (look === 'stacked') return <StackedTabs tabs={tabs} value={value} onChange={onChange} placement={placement} shape={shape} collapsed={collapsed} onExpand={onExpand} edge={edge} border={border} shadow={shadow} dock={dock} active={active} foldable={foldable} folded={folded} onFoldChange={onFoldChange} foldSide={foldSide} ariaLabel={ariaLabel} className={className ?? ''} />
|
|
80
|
-
return <Overflow mode={overflow} value={value}><UnderlineTabs tabs={tabs} value={value} onChange={onChange} onClose={onClose} onMinimise={onMinimise} wrap={wrap}
|
|
86
|
+
return <Overflow mode={overflow} value={value}><UnderlineTabs tabs={tabs} value={value} onChange={onChange} onClose={onClose} onMinimise={onMinimise} wrap={wrap} type={type} /></Overflow>
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
/* `overflow="scroll"`: the row in a sideways scroller that fades its clipped edges and keeps the active tab (`data-active`)
|
|
@@ -265,10 +271,10 @@ function StackedTabs({ tabs, value, onChange, placement, shape = 'bar', collapse
|
|
|
265
271
|
)
|
|
266
272
|
}
|
|
267
273
|
|
|
268
|
-
function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap,
|
|
269
|
-
/* `
|
|
270
|
-
*
|
|
271
|
-
const md =
|
|
274
|
+
function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap, type }) {
|
|
275
|
+
/* `type="sans"` (2026-10-29, the user's ref — Adobe's tab strip): the neutral sans at 300 (his word, 2026-10-30), a 20px glyph at the
|
|
276
|
+
* set's 1.5 stroke and a 1px underline (his word, 2026-10-30), a 40px row (his word, 2026-10-30), 24 between tabs — review 8: *"the problem … was thickness and stroke"* */
|
|
277
|
+
const md = type === 'sans' || type === 'mono-light'
|
|
272
278
|
const listRef = useRef(null)
|
|
273
279
|
const activeIdx = tabs.findIndex((t) => t.id === value)
|
|
274
280
|
|
|
@@ -282,7 +288,7 @@ function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap, size
|
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
return (
|
|
285
|
-
<div className={`flex items-stretch gap-4 ${wrap ?
|
|
291
|
+
<div className={`flex items-stretch gap-4 ${wrap ? 'min-h-10' : 'h-10'}`}>
|
|
286
292
|
{onClose && (
|
|
287
293
|
/* the one close, at the rung whose glyph is the 12 this hand-rolled
|
|
288
294
|
* (2026-09-03) — xs SOLO is 12, so the drawing does not move */
|
|
@@ -302,15 +308,15 @@ function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap, size
|
|
|
302
308
|
tabIndex={isActive || (activeIdx === -1 && i === 0) ? 0 : -1}
|
|
303
309
|
onClick={() => onChange?.(t.id)}
|
|
304
310
|
className={[
|
|
305
|
-
`group ${md ? 'kol-tab-
|
|
311
|
+
`group ${md ? `${type === 'sans' ? 'kol-tab-sans' : 'kol-tab-mono-light'} kol-sans-body-02 font-light gap-2.5 border-b` : 'kol-mono-12 gap-2 border-b-2'} flex items-center cursor-pointer`,
|
|
306
312
|
isActive
|
|
307
|
-
? 'text-
|
|
308
|
-
: 'text-
|
|
313
|
+
? 'text-oq-96 border-fg'
|
|
314
|
+
: 'text-oq-48 hover:text-oq-80 border-transparent',
|
|
309
315
|
].join(' ')}
|
|
310
316
|
>
|
|
311
317
|
{/* a tab may carry its glyph before the label (2026-10-29, Adobe's Base color · Image · Color wheel) */}
|
|
312
318
|
{/* the glyph in OPAQUE ink (icons take oq, never fg) — full when active, meta otherwise, full under the pointer */}
|
|
313
|
-
{t.icon && <Icon name={t.icon} size={md ? 20 : 16} className={isActive ? 'text-oq-96' : 'text-oq-48 group-hover:text-oq-
|
|
319
|
+
{t.icon && <Icon name={t.icon} size={md ? 20 : 16} className={isActive ? 'text-oq-96' : 'text-oq-48 group-hover:text-oq-80'} />}
|
|
314
320
|
{t.label}
|
|
315
321
|
</button>
|
|
316
322
|
)
|
|
@@ -74,8 +74,9 @@ function ChannelStrip({ ch, hex, hsva, onHsva }) {
|
|
|
74
74
|
onKeyDown={(e) => { const d = { ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1 }[e.key]; if (d) { e.preventDefault(); set(value + d * (e.shiftKey ? 10 : 1)) } }}>
|
|
75
75
|
<span className="absolute top-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full" style={{ left: `${pct}%`, boxShadow: '0 0 0 2px #fff, 0 0 0 3px rgba(0,0,0,0.35)', background: hex.slice(0, 7) }} />
|
|
76
76
|
</div>
|
|
77
|
-
{/* the box
|
|
78
|
-
|
|
77
|
+
{/* the box is 56 wide by the Input's own `width` — a utility width was not honoured in a consumer (ref 81), and `chars`
|
|
78
|
+
* alone did not hold against the row's flex (0.317.2, seen in the review-8 pass) */}
|
|
79
|
+
<Input size="sm" type="number" chars={3} width="56px" value={Math.round(value)} aria-label={`${ch.label ?? ch.key} value`} onChange={(e) => { const v = Number(e.target.value); if (!Number.isNaN(v)) set(v) }} />
|
|
79
80
|
</div>
|
|
80
81
|
)
|
|
81
82
|
}
|
|
@@ -212,7 +213,7 @@ export default function ColorPickerPopover({ color, onChange, defaultValue, size
|
|
|
212
213
|
<div className={`flex w-full flex-col gap-3 ${className}`.trim()}>
|
|
213
214
|
<div className="flex items-center justify-between gap-3">
|
|
214
215
|
<span className="kol-eyebrow text-fg-80">{title ?? label}</span>
|
|
215
|
-
{modes && <Dropdown
|
|
216
|
+
{modes && <Dropdown size="sm" value={mode} onChange={setMode} options={PICKER_MODES} ariaLabel={`${label} mode`} />}
|
|
216
217
|
</div>
|
|
217
218
|
{hexField}
|
|
218
219
|
<div className="w-full overflow-hidden rounded-[var(--kol-radius-xs)]" style={{ height: 200 }}>
|
|
@@ -521,6 +521,10 @@ export default function ColumnBrowser({
|
|
|
521
521
|
/* `renderDetails(file)` — extra rows under the preview column's facts (tags, an Edit action);
|
|
522
522
|
* absent, the column is exactly what it was */
|
|
523
523
|
renderDetails,
|
|
524
|
+
/* `renderFolderPreview(level, width)` — the preview column when NO file is shown: the open
|
|
525
|
+
* folder's own pane (MediaLibraryInfoKey, kol-website 2026-10-30 — the media page's folder facts,
|
|
526
|
+
* as its rows draw them). Null for a level with nothing to say; absent, the column is empty. */
|
|
527
|
+
renderFolderPreview,
|
|
524
528
|
height,
|
|
525
529
|
defaultHeight = 528,
|
|
526
530
|
onHeightChange,
|
|
@@ -798,6 +802,8 @@ export default function ColumnBrowser({
|
|
|
798
802
|
const levels = levelsOf(prefix) // '' → ['']; 'a/b/' → ['', 'a/', 'a/b/']
|
|
799
803
|
// While Quick Look is open the overlay's index is the truth for the highlight.
|
|
800
804
|
const shown = quickLook ? quickLook.files[quickLook.index] : picked
|
|
805
|
+
/* no file shown: the open folder's pane, when the caller has one for this level */
|
|
806
|
+
const folderPane = !shown && renderFolderPreview ? renderFolderPreview(prefix, widthOf('preview')) : null
|
|
801
807
|
|
|
802
808
|
/* ── THE STACK MODE (ColumnBrowserStackMode, kol-r2b2 2026-09-03) ─────────
|
|
803
809
|
*
|
|
@@ -1093,9 +1099,11 @@ export default function ColumnBrowser({
|
|
|
1093
1099
|
<SelectionPreview keys={[...selectedKeys]} objects={objects} urlOf={urlOf} kindOf={kindOf} formatSize={formatSize} formatDate={formatDate} width={widthOf('preview')} />
|
|
1094
1100
|
<ResizeHandle axis="x" onDrag={resizeCol('preview')} onEnd={endDrag} />
|
|
1095
1101
|
</>
|
|
1096
|
-
) : shown && (
|
|
1102
|
+
) : (shown || folderPane) && (
|
|
1097
1103
|
<>
|
|
1098
|
-
|
|
1104
|
+
{shown
|
|
1105
|
+
? <Preview key={shown.key} o={shown} urlOf={urlOf} kindOf={kindOf} kindLabel={kindLabel} formatSize={formatSize} formatDate={formatDate} renderPreview={renderPreview} renderDetails={renderDetails} width={widthOf('preview')} />
|
|
1106
|
+
: folderPane}
|
|
1099
1107
|
<ResizeHandle axis="x" onDrag={resizeCol('preview')} onEnd={endDrag} />
|
|
1100
1108
|
</>
|
|
1101
1109
|
)}
|
|
@@ -262,23 +262,53 @@ function useBucketLibrary({ client, bucket, defaults, settings: controlled, onSe
|
|
|
262
262
|
|
|
263
263
|
/* ── shared pieces, kol-r2b2's ─────────────────────────────────────────────── */
|
|
264
264
|
|
|
265
|
-
/* The frame is square until the file reports its size
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
|
|
265
|
+
/* The frame is square until the file reports its size. `mode="native"` (the default since
|
|
266
|
+
* MediaPreviewNativeRatio, kol-website 2026-10-30 — a certificate lost its top and bottom) then takes
|
|
267
|
+
* the file's own ratio and shows it whole, a very tall one capped at the media box's height (the pane
|
|
268
|
+
* shrinks that box to what is left under the facts) so it never runs off. `mode="preset"` is the older look: snap to
|
|
269
|
+
* the nearest preset; a photo near one fills it (`cover`, a few percent cropped); a vector or anything
|
|
270
|
+
* past the ladder's ends — a 2.8:1 wordmark, an 11:1 nav logo — shows whole (`contain`,
|
|
271
|
+
* kol-client-olina 2026-09-25). */
|
|
272
|
+
function ImageFrame({ src, vector, mode = 'native' }) {
|
|
269
273
|
const [frame, setFrame] = useState({ ratio: '1 / 1', contain: !!vector })
|
|
274
|
+
const native = mode !== 'preset'
|
|
270
275
|
const onLoad = (e) => {
|
|
271
276
|
const { naturalWidth: w, naturalHeight: h } = e.target
|
|
272
277
|
const r = w && h ? w / h : 1
|
|
273
|
-
setFrame({ ratio:
|
|
278
|
+
if (native) setFrame({ ratio: w && h ? `${w} / ${h}` : '1 / 1', contain: true })
|
|
279
|
+
else setFrame({ ratio: nearestRatio(w, h), contain: vector || r > 16 / 9 || r < 9 / 16 })
|
|
274
280
|
}
|
|
275
281
|
return (
|
|
276
|
-
<div className=
|
|
282
|
+
<div className={`w-full rounded overflow-hidden flex items-center justify-center${native ? ' max-h-full' : ''}`} style={{ aspectRatio: frame.ratio }}>
|
|
277
283
|
<img src={src} alt="" className={`w-full h-full ${frame.contain ? 'object-contain' : 'object-cover'}`} loading="lazy" onLoad={onLoad} />
|
|
278
284
|
</div>
|
|
279
285
|
)
|
|
280
286
|
}
|
|
281
287
|
|
|
288
|
+
/* THE PAGE'S OWN KEYS, ONE GUARD. `,` toggles the settings drawer (MediaPreviewNativeRatio,
|
|
289
|
+
* kol-website 2026-10-30: the media app is not on AppHub, which binds its own); `mod+i` opens Get
|
|
290
|
+
* Info (MediaLibraryInfoKey, kol-website 2026-10-30). AppHub's guard — no other modifiers, no
|
|
291
|
+
* repeat, not while typing. A bare key takes no modifier at all; `mod` is ⌘ or Ctrl. `on` false
|
|
292
|
+
* hands the key to the host (the gear's `onOpenSettings`), or holds it while an overlay is up. */
|
|
293
|
+
function useHotkey(combo, on, run) {
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (!combo || !on) return undefined
|
|
296
|
+
const parts = combo.toLowerCase().split('+')
|
|
297
|
+
const key = parts.pop()
|
|
298
|
+
const mod = parts.includes('mod')
|
|
299
|
+
const onKey = (e) => {
|
|
300
|
+
if (!(e.key?.toLowerCase() === key || (key === ',' && e.code === 'Comma')) || e.altKey || e.repeat) return
|
|
301
|
+
if (mod ? !(e.metaKey || e.ctrlKey) : (e.metaKey || e.ctrlKey)) return
|
|
302
|
+
const el = e.target
|
|
303
|
+
if (el?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el?.tagName ?? '')) return
|
|
304
|
+
e.preventDefault()
|
|
305
|
+
run()
|
|
306
|
+
}
|
|
307
|
+
window.addEventListener('keydown', onKey)
|
|
308
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
309
|
+
}, [combo, on, run])
|
|
310
|
+
}
|
|
311
|
+
|
|
282
312
|
|
|
283
313
|
/* TAGS IN THE PREVIEW PANE (the media D1 pass, 2026-09-25). The file's tags as removable chips and
|
|
284
314
|
* one field that adds on Enter — comma-separated adds several. Tags are the CLIENT's (`setTags`);
|
|
@@ -1127,6 +1157,21 @@ export function MediaLibraryBrowse({
|
|
|
1127
1157
|
searchPlaceholder = 'Search this bucket',
|
|
1128
1158
|
/* `o.displayName` on a listed object is its LABEL in every view, Quick Look and search (kol-fxr
|
|
1129
1159
|
* 2026-10-08: names not unique, so the key is an id). Absent, the label is the key, as before. */
|
|
1160
|
+
/* `previewFrame` — how the preview pane and the column / Quick Look frame an image
|
|
1161
|
+
* (MediaPreviewNativeRatio, kol-website 2026-10-30): `'native'` the file's own ratio, shown whole;
|
|
1162
|
+
* `'preset'` snapped to the nearest preset and filled. Grid tiles are not part of it. */
|
|
1163
|
+
previewFrame = 'native',
|
|
1164
|
+
/* `settingsKey` — the key that toggles the settings drawer (`','`, AppHub's; `null` for none).
|
|
1165
|
+
* `settingsOpen` / `onSettingsOpenChange` — the drawer, controlled, so a host can drive it. */
|
|
1166
|
+
settingsKey = ',',
|
|
1167
|
+
settingsOpen: settingsOpenProp,
|
|
1168
|
+
onSettingsOpenChange,
|
|
1169
|
+
/* `infoKey` — Get Info on the pick, file or folder, in every view (MediaLibraryInfoKey, kol-website
|
|
1170
|
+
* 2026-10-30; Finder's ⌘I): the same overlay Space opens, Esc closes. `'mod+i'`; `false` for none.
|
|
1171
|
+
* `columnFolderInfo` — Columns: the open folder's facts pane in the preview column, as the rows
|
|
1172
|
+
* draw a picked folder's; `false` leaves the column empty, as before. */
|
|
1173
|
+
infoKey = 'mod+i',
|
|
1174
|
+
columnFolderInfo = true,
|
|
1130
1175
|
}) {
|
|
1131
1176
|
const [ownPrefix, setOwnPrefix] = useState('')
|
|
1132
1177
|
const prefix = prefixProp ?? ownPrefix
|
|
@@ -1141,7 +1186,10 @@ export function MediaLibraryBrowse({
|
|
|
1141
1186
|
* say it is writable. */
|
|
1142
1187
|
const writable = lib.writable || (!!fileActions && !!bucketMeta.writable)
|
|
1143
1188
|
const switchBucket = (id, pfx = '') => { if (bucket == null) setOwnBucket(id); onBucketChange?.(id); setPrefix(pfx) }
|
|
1144
|
-
const [
|
|
1189
|
+
const [ownSettingsOpen, setOwnSettingsOpen] = useState(false)
|
|
1190
|
+
const settingsOpen = settingsOpenProp ?? ownSettingsOpen
|
|
1191
|
+
const setSettingsOpen = (v) => { if (settingsOpenProp == null) setOwnSettingsOpen(v); onSettingsOpenChange?.(v) }
|
|
1192
|
+
useHotkey(settingsKey, !onOpenSettings, () => setSettingsOpen(!settingsOpen))
|
|
1145
1193
|
const [trashOpen, setTrashOpen] = useState(false)
|
|
1146
1194
|
const [pickedFile, setPickedFile] = useState(null)
|
|
1147
1195
|
useEffect(() => { onPickFile?.(pickedFile) }, [pickedFile]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
@@ -1644,7 +1692,9 @@ export function MediaLibraryBrowse({
|
|
|
1644
1692
|
}
|
|
1645
1693
|
}
|
|
1646
1694
|
const spaceQuickLook = () => {
|
|
1647
|
-
|
|
1695
|
+
/* in the columns a folder is picked by being OPEN — the deepest level (MediaLibraryInfoKey: ⌘I
|
|
1696
|
+
* is the only caller there; the columns keep their own Space) */
|
|
1697
|
+
const key = pickedFile?.key ?? (view === 'columns' ? (prefix || null) : pickedFolder ?? (rowSelection.size === 1 ? [...rowSelection][0] : null))
|
|
1648
1698
|
/* a picked bucket or the title Quick-Looks like any folder (user 2026-09-25) */
|
|
1649
1699
|
if (!key && previewRoot) { setQuickLookFolder(ROOT_KEY); return }
|
|
1650
1700
|
if (!key) return
|
|
@@ -1807,6 +1857,9 @@ export function MediaLibraryBrowse({
|
|
|
1807
1857
|
|
|
1808
1858
|
const [quickLook, setQuickLook] = useState(null)
|
|
1809
1859
|
const [quickLookFolder, setQuickLookFolder] = useState(null)
|
|
1860
|
+
/* ⌘I — GET INFO (MediaLibraryInfoKey): what Space opens, from every view; inert over an open
|
|
1861
|
+
* overlay (Esc closes it) and over the search modal */
|
|
1862
|
+
useHotkey(infoKey, !quickLook && !quickLookFolder && !searchOpen, spaceQuickLook)
|
|
1810
1863
|
/* …and the open window takes the new selection as its set, on the file the cursor reached */
|
|
1811
1864
|
useEffect(() => {
|
|
1812
1865
|
if (!quickLook || rowSelection.size < 2) return
|
|
@@ -1886,7 +1939,7 @@ export function MediaLibraryBrowse({
|
|
|
1886
1939
|
formatSize={formatSize} formatDate={formatDate} width={previewWidth}
|
|
1887
1940
|
renderDetails={detailsFor}
|
|
1888
1941
|
renderPreview={(o) => {
|
|
1889
|
-
if (isImage(o.contentType)) return <ImageFrame src={mediaUrl(o.key)} vector={o.contentType === 'image/svg+xml'} />
|
|
1942
|
+
if (isImage(o.contentType)) return <ImageFrame src={mediaUrl(o.key)} vector={o.contentType === 'image/svg+xml'} mode={previewFrame} />
|
|
1890
1943
|
const poster = posterFor(o.key, keySet)
|
|
1891
1944
|
return <KindPreview o={o} urlOf={(x) => mediaUrl(x.key)} poster={poster ? mediaUrl(poster) : undefined} kindOf={kindOf} kindLabel={KIND_LABEL} />
|
|
1892
1945
|
}} />
|
|
@@ -2100,9 +2153,18 @@ export function MediaLibraryBrowse({
|
|
|
2100
2153
|
owns it through settings (item 15) */
|
|
2101
2154
|
stackView={stackView ?? settings.stackView ?? 'list'}
|
|
2102
2155
|
renderDetails={(o) => detailsFor({ ...o, key: o.key.slice(VROOT.length) })}
|
|
2156
|
+
/* THE OPEN FOLDER'S FACTS IN THE PREVIEW COLUMN (MediaLibraryInfoKey, kol-website
|
|
2157
|
+
* 2026-10-30: picking a folder showed nothing beside the list) — the rows' pane, drawn
|
|
2158
|
+
* where the columns put a file's. A level above the bucket (the title, a bucket) has
|
|
2159
|
+
* no pane here, as before. */
|
|
2160
|
+
renderFolderPreview={columnFolderInfo ? (level, width) => {
|
|
2161
|
+
const real = unroot(level)
|
|
2162
|
+
if (!real || (VROOT && real === level)) return null
|
|
2163
|
+
return <FolderPreview key={real} path={real} objects={objects} width={width} formatDate={formatDate} details={detailsFor(real)} />
|
|
2164
|
+
} : undefined}
|
|
2103
2165
|
renderPreview={(o) => {
|
|
2104
2166
|
const real = { ...o, key: o.key.slice(VROOT.length) }
|
|
2105
|
-
if (isImage(real.contentType)) return <ImageFrame src={mediaUrl(real.key)} vector={real.contentType === 'image/svg+xml'} />
|
|
2167
|
+
if (isImage(real.contentType)) return <ImageFrame src={mediaUrl(real.key)} vector={real.contentType === 'image/svg+xml'} mode={previewFrame} />
|
|
2106
2168
|
const poster = posterFor(real.key, keySet)
|
|
2107
2169
|
return <KindPreview o={real} urlOf={(x) => mediaUrl(x.key)} poster={poster ? mediaUrl(poster) : undefined} kindOf={kindOf} kindLabel={KIND_LABEL} />
|
|
2108
2170
|
}}
|
|
@@ -2808,7 +2870,7 @@ export function MediaLibraryBrowse({
|
|
|
2808
2870
|
{tabs?.length > 0 && (
|
|
2809
2871
|
<>
|
|
2810
2872
|
<div className="md:hidden" aria-hidden="true" style={{ height: TABBAR_H }} />
|
|
2811
|
-
<TabsRow
|
|
2873
|
+
<TabsRow variant="stacked" placement="phone" tabs={tabs} value={activeTab} onChange={onTabChange} />
|
|
2812
2874
|
</>
|
|
2813
2875
|
)}
|
|
2814
2876
|
</div>
|
|
@@ -3014,6 +3076,11 @@ export function MediaLibraryLibrary({
|
|
|
3014
3076
|
* concept only one page has stay put — `thumbnailFor`, `folderMeta` and
|
|
3015
3077
|
* `stackView` are folder and stack concepts and are correctly absent here. */
|
|
3016
3078
|
formatDate = defaultFormatDate,
|
|
3079
|
+
/* the settings drawer's key and its controlled state — the same seam as the browse page (the
|
|
3080
|
+
* drawer is on both; MediaPreviewNativeRatio, kol-website 2026-10-30) */
|
|
3081
|
+
settingsKey = ',',
|
|
3082
|
+
settingsOpen: settingsOpenProp,
|
|
3083
|
+
onSettingsOpenChange,
|
|
3017
3084
|
}) {
|
|
3018
3085
|
const [ownBucket, setOwnBucket] = useState(bucket)
|
|
3019
3086
|
const bucketId = bucket ?? ownBucket
|
|
@@ -3021,7 +3088,10 @@ export function MediaLibraryLibrary({
|
|
|
3021
3088
|
const { buckets, bucketMeta, settings, setSettings, objects, setObjects, error, mediaUrl, downloadUrl, writable } = lib
|
|
3022
3089
|
/* the same one dialog idiom as the browse page — see its note on `useModal()` */
|
|
3023
3090
|
const modal = useModal()
|
|
3024
|
-
const [
|
|
3091
|
+
const [ownSettingsOpen, setOwnSettingsOpen] = useState(false)
|
|
3092
|
+
const settingsOpen = settingsOpenProp ?? ownSettingsOpen
|
|
3093
|
+
const setSettingsOpen = (v) => { if (settingsOpenProp == null) setOwnSettingsOpen(v); onSettingsOpenChange?.(v) }
|
|
3094
|
+
useHotkey(settingsKey, header, () => setSettingsOpen(!settingsOpen))
|
|
3025
3095
|
const [editingKey, setEditingKey] = useState(null)
|
|
3026
3096
|
const [editingValue, setEditingValue] = useState('')
|
|
3027
3097
|
const [renaming, setRenaming] = useState(false)
|
|
@@ -3238,7 +3308,7 @@ export function MediaLibraryLibrary({
|
|
|
3238
3308
|
{tabs?.length > 0 && (
|
|
3239
3309
|
<>
|
|
3240
3310
|
<div className="md:hidden" aria-hidden="true" style={{ height: TABBAR_H }} />
|
|
3241
|
-
<TabsRow
|
|
3311
|
+
<TabsRow variant="stacked" placement="phone" tabs={tabs} value={activeTab} onChange={onTabChange} />
|
|
3242
3312
|
</>
|
|
3243
3313
|
)}
|
|
3244
3314
|
</div>
|