@kolkrabbi/kol-component 0.317.1 → 0.317.3
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 +19 -12
- package/src/organisms/ColorPickerPopover.jsx +5 -5
- package/src/organisms/MediaLibraryPages.jsx +56 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.317.
|
|
3
|
+
"version": "0.317.3",
|
|
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.3"
|
|
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'} [type='mono'] `underline` only: the tab's type
|
|
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,9 +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
|
-
|
|
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'
|
|
271
278
|
const listRef = useRef(null)
|
|
272
279
|
const activeIdx = tabs.findIndex((t) => t.id === value)
|
|
273
280
|
|
|
@@ -281,7 +288,7 @@ function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap, size
|
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
return (
|
|
284
|
-
<div className={`flex items-stretch gap-4 ${wrap ?
|
|
291
|
+
<div className={`flex items-stretch gap-4 ${wrap ? 'min-h-10' : 'h-10'}`}>
|
|
285
292
|
{onClose && (
|
|
286
293
|
/* the one close, at the rung whose glyph is the 12 this hand-rolled
|
|
287
294
|
* (2026-09-03) — xs SOLO is 12, so the drawing does not move */
|
|
@@ -301,7 +308,7 @@ function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap, size
|
|
|
301
308
|
tabIndex={isActive || (activeIdx === -1 && i === 0) ? 0 : -1}
|
|
302
309
|
onClick={() => onChange?.(t.id)}
|
|
303
310
|
className={[
|
|
304
|
-
`group ${md ? 'kol-sans-body-02 gap-2.5' : 'kol-mono-12 gap-2'} flex items-center cursor-pointer
|
|
311
|
+
`group ${md ? 'kol-tab-sans kol-sans-body-02 font-light gap-2.5 border-b' : 'kol-mono-12 gap-2 border-b-2'} flex items-center cursor-pointer`,
|
|
305
312
|
isActive
|
|
306
313
|
? 'text-emphasis border-fg'
|
|
307
314
|
: 'text-meta hover:text-emphasis border-transparent',
|
|
@@ -74,9 +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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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) }} />
|
|
80
80
|
</div>
|
|
81
81
|
)
|
|
82
82
|
}
|
|
@@ -212,8 +212,8 @@ export default function ColorPickerPopover({ color, onChange, defaultValue, size
|
|
|
212
212
|
return hsva && (
|
|
213
213
|
<div className={`flex w-full flex-col gap-3 ${className}`.trim()}>
|
|
214
214
|
<div className="flex items-center justify-between gap-3">
|
|
215
|
-
<span className="kol-
|
|
216
|
-
{modes && <Dropdown
|
|
215
|
+
<span className="kol-eyebrow text-fg-80">{title ?? label}</span>
|
|
216
|
+
{modes && <Dropdown size="sm" value={mode} onChange={setMode} options={PICKER_MODES} ariaLabel={`${label} mode`} />}
|
|
217
217
|
</div>
|
|
218
218
|
{hexField}
|
|
219
219
|
<div className="w-full overflow-hidden rounded-[var(--kol-radius-xs)]" style={{ height: 200 }}>
|
|
@@ -262,23 +262,47 @@ 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
|
+
/* `,` TOGGLES THE SETTINGS DRAWER (MediaPreviewNativeRatio, kol-website 2026-10-30: the media app is
|
|
289
|
+
* not on AppHub, which binds its own). AppHub's guard — no modifiers, no repeat, not while typing.
|
|
290
|
+
* `on` is false when the gear hands over (`onOpenSettings`): the host's key owns it then. */
|
|
291
|
+
function useSettingsKey(key, on, toggle) {
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (!key || !on) return undefined
|
|
294
|
+
const onKey = (e) => {
|
|
295
|
+
if (!(e.key === key || (key === ',' && e.code === 'Comma')) || e.metaKey || e.ctrlKey || e.altKey || e.repeat) return
|
|
296
|
+
const el = e.target
|
|
297
|
+
if (el?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el?.tagName ?? '')) return
|
|
298
|
+
e.preventDefault()
|
|
299
|
+
toggle()
|
|
300
|
+
}
|
|
301
|
+
window.addEventListener('keydown', onKey)
|
|
302
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
303
|
+
}, [key, on, toggle])
|
|
304
|
+
}
|
|
305
|
+
|
|
282
306
|
|
|
283
307
|
/* TAGS IN THE PREVIEW PANE (the media D1 pass, 2026-09-25). The file's tags as removable chips and
|
|
284
308
|
* one field that adds on Enter — comma-separated adds several. Tags are the CLIENT's (`setTags`);
|
|
@@ -1127,6 +1151,15 @@ export function MediaLibraryBrowse({
|
|
|
1127
1151
|
searchPlaceholder = 'Search this bucket',
|
|
1128
1152
|
/* `o.displayName` on a listed object is its LABEL in every view, Quick Look and search (kol-fxr
|
|
1129
1153
|
* 2026-10-08: names not unique, so the key is an id). Absent, the label is the key, as before. */
|
|
1154
|
+
/* `previewFrame` — how the preview pane and the column / Quick Look frame an image
|
|
1155
|
+
* (MediaPreviewNativeRatio, kol-website 2026-10-30): `'native'` the file's own ratio, shown whole;
|
|
1156
|
+
* `'preset'` snapped to the nearest preset and filled. Grid tiles are not part of it. */
|
|
1157
|
+
previewFrame = 'native',
|
|
1158
|
+
/* `settingsKey` — the key that toggles the settings drawer (`','`, AppHub's; `null` for none).
|
|
1159
|
+
* `settingsOpen` / `onSettingsOpenChange` — the drawer, controlled, so a host can drive it. */
|
|
1160
|
+
settingsKey = ',',
|
|
1161
|
+
settingsOpen: settingsOpenProp,
|
|
1162
|
+
onSettingsOpenChange,
|
|
1130
1163
|
}) {
|
|
1131
1164
|
const [ownPrefix, setOwnPrefix] = useState('')
|
|
1132
1165
|
const prefix = prefixProp ?? ownPrefix
|
|
@@ -1141,7 +1174,10 @@ export function MediaLibraryBrowse({
|
|
|
1141
1174
|
* say it is writable. */
|
|
1142
1175
|
const writable = lib.writable || (!!fileActions && !!bucketMeta.writable)
|
|
1143
1176
|
const switchBucket = (id, pfx = '') => { if (bucket == null) setOwnBucket(id); onBucketChange?.(id); setPrefix(pfx) }
|
|
1144
|
-
const [
|
|
1177
|
+
const [ownSettingsOpen, setOwnSettingsOpen] = useState(false)
|
|
1178
|
+
const settingsOpen = settingsOpenProp ?? ownSettingsOpen
|
|
1179
|
+
const setSettingsOpen = (v) => { if (settingsOpenProp == null) setOwnSettingsOpen(v); onSettingsOpenChange?.(v) }
|
|
1180
|
+
useSettingsKey(settingsKey, !onOpenSettings, () => setSettingsOpen(!settingsOpen))
|
|
1145
1181
|
const [trashOpen, setTrashOpen] = useState(false)
|
|
1146
1182
|
const [pickedFile, setPickedFile] = useState(null)
|
|
1147
1183
|
useEffect(() => { onPickFile?.(pickedFile) }, [pickedFile]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
@@ -1886,7 +1922,7 @@ export function MediaLibraryBrowse({
|
|
|
1886
1922
|
formatSize={formatSize} formatDate={formatDate} width={previewWidth}
|
|
1887
1923
|
renderDetails={detailsFor}
|
|
1888
1924
|
renderPreview={(o) => {
|
|
1889
|
-
if (isImage(o.contentType)) return <ImageFrame src={mediaUrl(o.key)} vector={o.contentType === 'image/svg+xml'} />
|
|
1925
|
+
if (isImage(o.contentType)) return <ImageFrame src={mediaUrl(o.key)} vector={o.contentType === 'image/svg+xml'} mode={previewFrame} />
|
|
1890
1926
|
const poster = posterFor(o.key, keySet)
|
|
1891
1927
|
return <KindPreview o={o} urlOf={(x) => mediaUrl(x.key)} poster={poster ? mediaUrl(poster) : undefined} kindOf={kindOf} kindLabel={KIND_LABEL} />
|
|
1892
1928
|
}} />
|
|
@@ -2102,7 +2138,7 @@ export function MediaLibraryBrowse({
|
|
|
2102
2138
|
renderDetails={(o) => detailsFor({ ...o, key: o.key.slice(VROOT.length) })}
|
|
2103
2139
|
renderPreview={(o) => {
|
|
2104
2140
|
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'} />
|
|
2141
|
+
if (isImage(real.contentType)) return <ImageFrame src={mediaUrl(real.key)} vector={real.contentType === 'image/svg+xml'} mode={previewFrame} />
|
|
2106
2142
|
const poster = posterFor(real.key, keySet)
|
|
2107
2143
|
return <KindPreview o={real} urlOf={(x) => mediaUrl(x.key)} poster={poster ? mediaUrl(poster) : undefined} kindOf={kindOf} kindLabel={KIND_LABEL} />
|
|
2108
2144
|
}}
|
|
@@ -2808,7 +2844,7 @@ export function MediaLibraryBrowse({
|
|
|
2808
2844
|
{tabs?.length > 0 && (
|
|
2809
2845
|
<>
|
|
2810
2846
|
<div className="md:hidden" aria-hidden="true" style={{ height: TABBAR_H }} />
|
|
2811
|
-
<TabsRow
|
|
2847
|
+
<TabsRow variant="stacked" placement="phone" tabs={tabs} value={activeTab} onChange={onTabChange} />
|
|
2812
2848
|
</>
|
|
2813
2849
|
)}
|
|
2814
2850
|
</div>
|
|
@@ -3014,6 +3050,11 @@ export function MediaLibraryLibrary({
|
|
|
3014
3050
|
* concept only one page has stay put — `thumbnailFor`, `folderMeta` and
|
|
3015
3051
|
* `stackView` are folder and stack concepts and are correctly absent here. */
|
|
3016
3052
|
formatDate = defaultFormatDate,
|
|
3053
|
+
/* the settings drawer's key and its controlled state — the same seam as the browse page (the
|
|
3054
|
+
* drawer is on both; MediaPreviewNativeRatio, kol-website 2026-10-30) */
|
|
3055
|
+
settingsKey = ',',
|
|
3056
|
+
settingsOpen: settingsOpenProp,
|
|
3057
|
+
onSettingsOpenChange,
|
|
3017
3058
|
}) {
|
|
3018
3059
|
const [ownBucket, setOwnBucket] = useState(bucket)
|
|
3019
3060
|
const bucketId = bucket ?? ownBucket
|
|
@@ -3021,7 +3062,10 @@ export function MediaLibraryLibrary({
|
|
|
3021
3062
|
const { buckets, bucketMeta, settings, setSettings, objects, setObjects, error, mediaUrl, downloadUrl, writable } = lib
|
|
3022
3063
|
/* the same one dialog idiom as the browse page — see its note on `useModal()` */
|
|
3023
3064
|
const modal = useModal()
|
|
3024
|
-
const [
|
|
3065
|
+
const [ownSettingsOpen, setOwnSettingsOpen] = useState(false)
|
|
3066
|
+
const settingsOpen = settingsOpenProp ?? ownSettingsOpen
|
|
3067
|
+
const setSettingsOpen = (v) => { if (settingsOpenProp == null) setOwnSettingsOpen(v); onSettingsOpenChange?.(v) }
|
|
3068
|
+
useSettingsKey(settingsKey, header, () => setSettingsOpen(!settingsOpen))
|
|
3025
3069
|
const [editingKey, setEditingKey] = useState(null)
|
|
3026
3070
|
const [editingValue, setEditingValue] = useState('')
|
|
3027
3071
|
const [renaming, setRenaming] = useState(false)
|
|
@@ -3238,7 +3282,7 @@ export function MediaLibraryLibrary({
|
|
|
3238
3282
|
{tabs?.length > 0 && (
|
|
3239
3283
|
<>
|
|
3240
3284
|
<div className="md:hidden" aria-hidden="true" style={{ height: TABBAR_H }} />
|
|
3241
|
-
<TabsRow
|
|
3285
|
+
<TabsRow variant="stacked" placement="phone" tabs={tabs} value={activeTab} onChange={onTabChange} />
|
|
3242
3286
|
</>
|
|
3243
3287
|
)}
|
|
3244
3288
|
</div>
|