@kolkrabbi/kol-component 0.224.0 → 0.226.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 +2 -2
- package/src/atoms/ColorSwatch.jsx +6 -2
- package/src/atoms/SelectionOverlay.jsx +3 -1
- package/src/atoms/XYPad.jsx +4 -4
- package/src/index.js +1 -1
- package/src/molecules/ColorInputRow.jsx +3 -2
- package/src/molecules/Dropdown.jsx +10 -5
- package/src/molecules/FieldRow.jsx +2 -2
- package/src/molecules/InspectorSection.jsx +14 -3
- package/src/molecules/MenuItem.jsx +5 -5
- package/src/molecules/SearchInput.jsx +1 -1
- package/src/molecules/SectionText.jsx +21 -4
- package/src/molecules/ShapeDropdown.jsx +3 -2
- package/src/molecules/SplitToolButton.jsx +3 -2
- package/src/molecules/Stepper.jsx +2 -2
- package/src/molecules/SwatchControls.jsx +3 -1
- package/src/molecules/TabsRow.jsx +1 -1
- package/src/organisms/Canvas.jsx +15 -7
- package/src/organisms/DocumentEditor.jsx +8 -5
- package/src/organisms/KeyframeEditor.jsx +4 -3
- package/src/organisms/LayerStack.jsx +11 -6
- package/src/organisms/MediaLibrary.jsx +1 -1
- package/src/organisms/MediaLibraryExplorer.jsx +67 -3
- package/src/organisms/MediaLibraryPages.jsx +113 -62
- package/src/organisms/RecordManager.jsx +2 -2
- package/src/organisms/SettingsPanel.jsx +5 -1
- package/src/organisms/SettingsSections.jsx +2 -1
- package/src/organisms/SpectrumGrid.jsx +3 -2
- package/src/organisms/TimelineDock.jsx +7 -4
- package/src/organisms/ToolPalette.jsx +6 -3
- package/src/utilities/Popover.jsx +61 -11
- package/src/utilities/QuadrantSync.jsx +3 -1
|
@@ -3,7 +3,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
3
3
|
import Button from '../atoms/Button.jsx'
|
|
4
4
|
import Input from '../atoms/Input.jsx'
|
|
5
5
|
import { MenuDropdownItem, MenuDropdownNest } from '../molecules/MenuItem.jsx'
|
|
6
|
-
import { usePopover, PopoverPanel } from '../utilities/Popover.jsx'
|
|
6
|
+
import { usePopover, PopoverPanel, Tooltip } from '../utilities/Popover.jsx'
|
|
7
7
|
import { rowLabelForLayer, findLayerDeep } from '../hooks/layerTree.js'
|
|
8
8
|
|
|
9
9
|
/* DS icon names for the engine's types — every one a shipped v1 glyph. A
|
|
@@ -51,11 +51,11 @@ function useShiftClickHandlers(onSelect, onShiftSelect) {
|
|
|
51
51
|
|
|
52
52
|
function Chevron({ IconC, collapsed, onToggle, title }) {
|
|
53
53
|
return (
|
|
54
|
+
<Tooltip label={title} asChild>
|
|
54
55
|
<button
|
|
55
56
|
type="button"
|
|
56
57
|
onClick={onToggle}
|
|
57
58
|
aria-expanded={!collapsed}
|
|
58
|
-
title={title}
|
|
59
59
|
className="kol-layer-stack-collapse"
|
|
60
60
|
>
|
|
61
61
|
<IconC
|
|
@@ -64,6 +64,7 @@ function Chevron({ IconC, collapsed, onToggle, title }) {
|
|
|
64
64
|
style={{ transform: collapsed ? 'rotate(-90deg)' : 'rotate(0deg)', transition: 'transform 150ms' }}
|
|
65
65
|
/>
|
|
66
66
|
</button>
|
|
67
|
+
</Tooltip>
|
|
67
68
|
)
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -160,24 +161,26 @@ function LayerRow({
|
|
|
160
161
|
</span>
|
|
161
162
|
</button>
|
|
162
163
|
)}
|
|
164
|
+
<Tooltip label={layer.visible ? 'Hide' : 'Show'} asChild>
|
|
163
165
|
<button
|
|
164
166
|
type="button"
|
|
165
167
|
onClick={onToggleVisibility}
|
|
166
|
-
title={layer.visible ? 'Hide' : 'Show'}
|
|
167
168
|
aria-pressed={!layer.visible}
|
|
168
169
|
className={`kol-layer-stack-toggle kol-layer-stack-toggle--eye${active || !layer.visible ? ' is-pinned' : ''}`}
|
|
169
170
|
>
|
|
170
171
|
<IconC name={layer.visible ? 'eye-on' : 'eye-off'} size={12} />
|
|
171
172
|
</button>
|
|
173
|
+
</Tooltip>
|
|
174
|
+
<Tooltip label={layer.locked ? 'Unlock' : 'Lock'} asChild>
|
|
172
175
|
<button
|
|
173
176
|
type="button"
|
|
174
177
|
onClick={onToggleLock}
|
|
175
|
-
title={layer.locked ? 'Unlock' : 'Lock'}
|
|
176
178
|
aria-pressed={!!layer.locked}
|
|
177
179
|
className={`kol-layer-stack-toggle kol-layer-stack-toggle--lock${active || layer.locked ? ' is-pinned' : ''}${layer.locked ? ' is-on' : ''}`}
|
|
178
180
|
>
|
|
179
181
|
<IconC name={layer.locked ? 'lock' : 'unlock'} size={12} />
|
|
180
182
|
</button>
|
|
183
|
+
</Tooltip>
|
|
181
184
|
</div>
|
|
182
185
|
</div>
|
|
183
186
|
)
|
|
@@ -419,16 +422,17 @@ export default function LayerStack({
|
|
|
419
422
|
* consumer's keymap. */}
|
|
420
423
|
{onGroup && layerSelectionCount >= 2 && (
|
|
421
424
|
<div className="mt-auto flex items-center gap-2 px-3 h-10 border-t border-fg-08">
|
|
425
|
+
<Tooltip label={`Group ${layerSelectionCount} selected layers`}>
|
|
422
426
|
<Button
|
|
423
427
|
iconComponent={IconC}
|
|
424
428
|
variant="primary"
|
|
425
429
|
size="sm"
|
|
426
430
|
iconLeft="layers"
|
|
427
431
|
onClick={() => onGroup(layerSelectedIds)}
|
|
428
|
-
title={`Group ${layerSelectionCount} selected layers`}
|
|
429
432
|
>
|
|
430
433
|
Group {layerSelectionCount}
|
|
431
434
|
</Button>
|
|
435
|
+
</Tooltip>
|
|
432
436
|
</div>
|
|
433
437
|
)}
|
|
434
438
|
</div>
|
|
@@ -469,6 +473,7 @@ export function AddLayerButton({
|
|
|
469
473
|
return (
|
|
470
474
|
<>
|
|
471
475
|
<span ref={popover.refs.setReference} {...popover.getReferenceProps()} className="inline-flex">
|
|
476
|
+
<Tooltip label="Add layer">
|
|
472
477
|
<Button
|
|
473
478
|
iconComponent={IconC}
|
|
474
479
|
variant="primary"
|
|
@@ -476,8 +481,8 @@ export function AddLayerButton({
|
|
|
476
481
|
quiet
|
|
477
482
|
iconOnly="plus"
|
|
478
483
|
aria-label="Add layer"
|
|
479
|
-
title="Add layer"
|
|
480
484
|
/>
|
|
485
|
+
</Tooltip>
|
|
481
486
|
</span>
|
|
482
487
|
<PopoverPanel popover={popover} className="py-1" style={{ width: menuWidth }}>
|
|
483
488
|
{types.map((t) => {
|
|
@@ -708,7 +708,7 @@ function FilesBody({ files, viewMode, onOpen, onPick }) {
|
|
|
708
708
|
)
|
|
709
709
|
/* the title voice is the family's ruled default (heading-04 card / heading-05
|
|
710
710
|
* row, truncated by ContentText); the full key rides as the tooltip, as before */
|
|
711
|
-
const nameFor = (row) => <
|
|
711
|
+
const nameFor = (row) => <Tooltip label={row.key} asChild><span>{row.displayKey}</span></Tooltip>
|
|
712
712
|
const date = (row) => (row.uploaded ? String(row.uploaded).slice(0, 10) : undefined)
|
|
713
713
|
const size = (row) => formatSize(row.size) || undefined
|
|
714
714
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { MediaLibraryBrowse } from './MediaLibraryPages.jsx'
|
|
3
|
+
import { useModal } from '../molecules/Modal.jsx'
|
|
4
|
+
import useMediaQuery from '../hooks/useMediaQuery.js'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* MediaLibraryExplorer — the media surface, and since 2026-09-22 a thin name for it.
|
|
@@ -17,15 +19,77 @@ import { MediaLibraryBrowse } from './MediaLibraryPages.jsx'
|
|
|
17
19
|
* @param {'columns'|'rows'|'grid'|'list'} view controlled view; omit and the page reads its own setting
|
|
18
20
|
* @param {Function} onViewChange (view) => void
|
|
19
21
|
* @param {'columns'|'rows'|'grid'|'list'} defaultView first view when uncontrolled and nothing is stored
|
|
22
|
+
*
|
|
23
|
+
* THE TOOL'S OWN BEHAVIOUR, opt-in (media parity, 2026-09-26 — user: "media in shell is just media in
|
|
24
|
+
* shell.. so there shouldnt really be a reason to diff"). apps/media carried these in its App.jsx, so
|
|
25
|
+
* the same tool one shell over had none of them. They are the product's, so they live with it:
|
|
26
|
+
*
|
|
27
|
+
* @param {boolean} keys the view keys — B browse · F files · R rows · C columns · G grid — and N
|
|
28
|
+
* (new folder, when `fileActions.createFolder` is given), plus K → `onKinds`.
|
|
29
|
+
* Ignored while typing and with ⌘ / Ctrl / ⌥ held
|
|
30
|
+
* @param {boolean} phoneTabs below 768 the surface is three tabs — Browse · Files · Kinds (Kinds only
|
|
31
|
+
* with `onKinds`); a tab picks the view, Kinds opens the overview and leaves
|
|
32
|
+
* the surface where it was
|
|
33
|
+
* @param {Function} onKinds opens the app's file-formats overview (K, the Kinds tab)
|
|
20
34
|
*/
|
|
21
|
-
|
|
35
|
+
const TABS = [
|
|
36
|
+
{ value: 'browse', label: 'Browse', icon: 'folder' },
|
|
37
|
+
{ value: 'files', label: 'Files', icon: 'view-list' },
|
|
38
|
+
{ value: 'kinds', label: 'Kinds', icon: 'grid' },
|
|
39
|
+
]
|
|
40
|
+
/* a key → the view it shows and the phone tab that view belongs to (apps/media's map, verbatim) */
|
|
41
|
+
const VIEW_KEYS = { b: ['columns', 'browse'], f: ['list', 'files'], r: ['rows', 'browse'], c: ['columns', 'browse'], g: ['grid', 'files'] }
|
|
42
|
+
|
|
43
|
+
export default function MediaLibraryExplorer({ view, onViewChange, defaultView, keys = false, phoneTabs = false, onKinds, ...pageProps }) {
|
|
22
44
|
const [ownView, setOwnView] = useState(view ?? defaultView)
|
|
23
45
|
const current = view ?? ownView
|
|
46
|
+
const setView = (v) => { setOwnView(v); onViewChange?.(v) }
|
|
47
|
+
const [tab, setTab] = useState('browse')
|
|
48
|
+
const phone = useMediaQuery('(max-width: 767px)')
|
|
49
|
+
const modal = useModal()
|
|
50
|
+
|
|
51
|
+
/* the latest props for the key handler, without re-binding it every render */
|
|
52
|
+
const live = useRef()
|
|
53
|
+
live.current = { onKinds, prefix: pageProps.prefix ?? '', createFolder: pageProps.fileActions?.createFolder, modal }
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!keys) return undefined
|
|
56
|
+
const onKey = (e) => {
|
|
57
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return
|
|
58
|
+
const el = e.target
|
|
59
|
+
if (el?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el?.tagName ?? '')) return
|
|
60
|
+
const { onKinds: kinds, prefix, createFolder, modal: m } = live.current
|
|
61
|
+
if (VIEW_KEYS[e.key]) {
|
|
62
|
+
e.preventDefault()
|
|
63
|
+
const [v, t] = VIEW_KEYS[e.key]
|
|
64
|
+
setOwnView(v); onViewChange?.(v); setTab(t)
|
|
65
|
+
} else if (e.key === 'k' && kinds) {
|
|
66
|
+
e.preventDefault(); kinds()
|
|
67
|
+
} else if (e.key === 'n' && createFolder) {
|
|
68
|
+
e.preventDefault()
|
|
69
|
+
m.prompt(`New folder in ${prefix || 'the bucket root'}:`, '', { okLabel: 'Create' }).then((name) => {
|
|
70
|
+
if (name?.trim()) createFolder(`${prefix}${name.trim().replace(/^\/+|\/+$/g, '')}/`)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
window.addEventListener('keydown', onKey)
|
|
75
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
76
|
+
}, [keys]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
77
|
+
|
|
78
|
+
const onTabChange = (v) => {
|
|
79
|
+
if (v === 'kinds') { onKinds?.(); return }
|
|
80
|
+
setTab(v)
|
|
81
|
+
setView(v === 'files' ? 'list' : 'columns')
|
|
82
|
+
}
|
|
83
|
+
const tabProps = phoneTabs && phone
|
|
84
|
+
? { tabs: onKinds ? TABS : TABS.filter((t) => t.value !== 'kinds'), activeTab: tab, onTabChange }
|
|
85
|
+
: {}
|
|
86
|
+
|
|
24
87
|
return (
|
|
25
88
|
<MediaLibraryBrowse
|
|
26
89
|
{...pageProps}
|
|
90
|
+
{...tabProps}
|
|
27
91
|
view={current}
|
|
28
|
-
onViewChange={
|
|
92
|
+
onViewChange={setView}
|
|
29
93
|
/>
|
|
30
94
|
)
|
|
31
95
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment, useEffect, useMemo, useRef, useState } from 'react'
|
|
1
|
+
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import formatSize from '../utilities/formatSize.js'
|
|
3
3
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
4
4
|
import Button from '../atoms/Button.jsx'
|
|
@@ -37,7 +37,8 @@ import { listDrafts, moveDrafts, DRAFTS_EVENT } from '../utilities/localDrafts.j
|
|
|
37
37
|
import { parseFrontmatter } from '../utilities/frontmatter.js'
|
|
38
38
|
import ShellSearchOverlay from './ShellSearchOverlay.jsx'
|
|
39
39
|
import ColumnBrowser, { isFileDrag, Preview as ColumnPreview, SelectionPreview } from './ColumnBrowser.jsx'
|
|
40
|
-
import SettingsPanel, {
|
|
40
|
+
import SettingsPanel, { SettingsSwitch, SettingsChoice, SettingsMulti, SettingsFooter } from './SettingsPanel.jsx'
|
|
41
|
+
import SettingsSections from './SettingsSections.jsx'
|
|
41
42
|
import { kindOf, extOf, KIND_LABEL, KINDS, DEFAULT_KINDS, isSystemFile, isSegment, groupSegments, groupVariants, posterFor, partition } from '../utilities/mediaKinds.js'
|
|
42
43
|
import { nearestRatio } from '../utilities/ratios.js'
|
|
43
44
|
|
|
@@ -108,7 +109,7 @@ export const SETTINGS_BASE = {
|
|
|
108
109
|
folderView: 'columns', // 'rows' | 'columns'
|
|
109
110
|
sortBy: 'name',
|
|
110
111
|
sortDir: 'asc',
|
|
111
|
-
columnHeight: 528, // px,
|
|
112
|
+
columnHeight: 528, // px, any CSS length (forwarded to ColumnBrowser `height` untouched), or 'fill' — what is left of the window
|
|
112
113
|
columnWidths: {},
|
|
113
114
|
rowPreview: true, // row view's preview pane beside the list (Finder's "Show preview")
|
|
114
115
|
view: 'columns', // 'columns' | 'rows' | 'grid' | 'list' — the ONE view state (2026-09-22)
|
|
@@ -333,7 +334,7 @@ function FolderRow({ name, icon = 'folder', onClick, onDoubleClick, depth = 0, e
|
|
|
333
334
|
{onToggle ? (
|
|
334
335
|
<button type="button" aria-label={expanded ? `Collapse ${name}` : `Expand ${name}`} aria-expanded={!!expanded}
|
|
335
336
|
onClick={(e) => { e.stopPropagation(); onToggle() }}
|
|
336
|
-
className="w-4 h-4 shrink-0 flex items-center justify-center text-
|
|
337
|
+
className="w-4 h-4 shrink-0 flex items-center justify-center text-oq-32 hover:text-fg-default transition-transform"
|
|
337
338
|
style={{ transform: expanded ? 'rotate(90deg)' : 'none' }}>
|
|
338
339
|
<Icon name="chevron-right" size={12} />
|
|
339
340
|
</button>
|
|
@@ -683,61 +684,62 @@ export function MediaInspector({ files, index, onClose, onPrev, onNext, mediaUrl
|
|
|
683
684
|
)
|
|
684
685
|
}
|
|
685
686
|
|
|
686
|
-
/*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
|
|
687
|
+
/* THE DISPLAY SETTINGS AS DATA (media-shell, user 2026-09-26: "missing settings sidebar? … or are
|
|
688
|
+
* we saying the shell has settings different from media?"). The rows were hard-wired JSX inside this
|
|
689
|
+
* drawer, so an app page could not show them and a second settings system grew beside it. Now ONE
|
|
690
|
+
* builder: this drawer renders it, and an app renders the same rows on its settings page and its own
|
|
691
|
+
* drawer (kol-shell `HubSettings sections`). `settings` is the page's per-bucket settings object,
|
|
692
|
+
* `onChange(next)` sets it, `profile` (what the bucket holds) drives the "nothing to fold" states —
|
|
693
|
+
* omit it where the listing is not at hand and those two switches stay enabled with plain hints.
|
|
694
|
+
* Every control sets a default, never a gate. */
|
|
695
|
+
export function mediaSettingsSections({ settings, onChange, profile }) {
|
|
690
696
|
const set = (patch) => onChange({ ...settings, ...patch })
|
|
691
|
-
const
|
|
692
|
-
const
|
|
693
|
-
const
|
|
697
|
+
const kinds = settings.kinds ?? []
|
|
698
|
+
const toggleKind = (k) => set({ kinds: kinds.includes(k) ? kinds.filter((x) => x !== k) : [...kinds, k] })
|
|
699
|
+
const noVariants = profile?.variantSets === 0
|
|
700
|
+
const noSegments = profile?.segments === 0
|
|
701
|
+
return [
|
|
702
|
+
{ label: 'Structure', rowGap: 1, rows: [
|
|
703
|
+
{ label: 'Columns', hint: 'Finder-style columns instead of folder rows',
|
|
704
|
+
render: () => <SettingsSwitch label="Columns" on={(settings.folderView ?? 'rows') === 'columns'} onChange={(v) => set({ folderView: v ? 'columns' : 'rows' })} /> },
|
|
705
|
+
{ label: 'Preview', hint: 'the picked file beside the rows',
|
|
706
|
+
render: () => <SettingsSwitch label="Preview" on={settings.rowPreview ?? true} onChange={(v) => set({ rowPreview: v })} /> },
|
|
707
|
+
/* NOT "Columns" — that switch above means Finder's column browser. These are the row view's
|
|
708
|
+
* date/size FIELDS, which is what the user calls them. */
|
|
709
|
+
{ label: 'Fields', hint: 'date and size beside each row, with a sort header',
|
|
710
|
+
render: () => <SettingsSwitch label="Fields" on={!!settings.rowColumns} onChange={(v) => set({ rowColumns: v })} /> },
|
|
711
|
+
{ label: 'Flat', hint: 'ignore folders, show the whole subtree',
|
|
712
|
+
render: () => <SettingsSwitch label="Flat" on={settings.flat} onChange={(v) => set({ flat: v })} /> },
|
|
713
|
+
{ label: 'Group resolutions', hint: noVariants ? 'no resolution sets in this bucket' : profile ? `${profile.variantSets} sets — previews the smallest file` : 'previews the smallest file of a set',
|
|
714
|
+
render: () => <SettingsSwitch label="Group resolutions" on={settings.groupVariants} onChange={(v) => set({ groupVariants: v })} disabled={noVariants} disabledHint="nothing to group here" /> },
|
|
715
|
+
{ label: 'Fold HLS segments', hint: noSegments ? 'no segments in this bucket' : profile ? `${profile.segments} segments into stream rows` : 'segments into stream rows',
|
|
716
|
+
render: () => <SettingsSwitch label="Fold HLS segments" on={settings.foldSegments} onChange={(v) => set({ foldSegments: v })} disabled={noSegments} disabledHint="nothing to fold here" /> },
|
|
717
|
+
] },
|
|
718
|
+
{ label: 'Loading', rows: [
|
|
719
|
+
{ label: 'Kinds', align: 'fill',
|
|
720
|
+
render: () => <SettingsMulti options={ALL_KINDS.map((k) => ({ value: k, label: KIND_LABEL[k] || k }))} selected={kinds} onToggle={toggleKind} noun="kinds" /> },
|
|
721
|
+
{ label: 'Page size', hint: 'entries mounted at once', align: 'fill',
|
|
722
|
+
render: () => <SettingsChoice options={[{ value: 100, label: '100' }, { value: 200, label: '200' }, { value: 500, label: '500' }, { value: 0, label: 'All' }]} value={settings.pageSize} onChange={(v) => set({ pageSize: v })} /> },
|
|
723
|
+
{ label: 'Video preview', hint: 'poster uses the sibling image; autoload fetches the file', align: 'fill',
|
|
724
|
+
render: () => <SettingsChoice options={[{ value: 'poster', label: 'Poster' }, { value: 'none', label: 'None' }, { value: 'autoload', label: 'Autoload' }]} value={settings.videoPreview} onChange={(v) => set({ videoPreview: v })} /> },
|
|
725
|
+
] },
|
|
726
|
+
{ label: 'Layout', rows: [
|
|
727
|
+
{ label: 'View', align: 'fill',
|
|
728
|
+
render: () => <SettingsChoice options={[{ value: 'off', label: 'Off' }, { value: 'grid', label: 'Grid' }, { value: 'list', label: 'List' }]} value={settings.layout} onChange={(v) => set({ layout: v })} /> },
|
|
729
|
+
{ label: 'Sort', align: 'fill',
|
|
730
|
+
render: () => <SettingsChoice options={SORT_OPTIONS} value={settings.sortBy} onChange={(v) => set({ sortBy: v })} /> },
|
|
731
|
+
{ label: 'Direction', align: 'fill',
|
|
732
|
+
render: () => <SettingsChoice options={[{ value: 'asc', label: '↓ Asc' }, { value: 'desc', label: '↑ Desc' }]} value={settings.sortDir} onChange={(v) => set({ sortDir: v })} /> },
|
|
733
|
+
] },
|
|
734
|
+
]
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/* Display settings for the ACTIVE bucket — kol-r2b2's drawer wiring on the DS organism (the
|
|
738
|
+
* composition the user locked 2026-08-27), drawn from `mediaSettingsSections`. */
|
|
739
|
+
function MediaSettings({ settings, onChange, onReset, onClose, profile, settingsFooter }) {
|
|
694
740
|
return (
|
|
695
741
|
<SettingsPanel variant="drawer" title="Display settings" onClose={onClose} footer={<SettingsFooter onReset={onReset} resetLabel="Reset preferences">{settingsFooter}</SettingsFooter>}>
|
|
696
|
-
<
|
|
697
|
-
<SettingsRow label="Columns" hint="Finder-style columns instead of folder rows">
|
|
698
|
-
<SettingsSwitch label="Columns" on={(settings.folderView ?? 'rows') === 'columns'} onChange={(v) => set({ folderView: v ? 'columns' : 'rows' })} />
|
|
699
|
-
</SettingsRow>
|
|
700
|
-
<SettingsRow label="Preview" hint="the picked file beside the rows">
|
|
701
|
-
<SettingsSwitch label="Preview" on={settings.rowPreview ?? true} onChange={(v) => set({ rowPreview: v })} />
|
|
702
|
-
</SettingsRow>
|
|
703
|
-
{/* NOT "Columns" — that switch three rows up means Finder's column browser. These are the
|
|
704
|
-
row view's date/size FIELDS, which is what the user calls them. */}
|
|
705
|
-
<SettingsRow label="Fields" hint="date and size beside each row, with a sort header">
|
|
706
|
-
<SettingsSwitch label="Fields" on={!!settings.rowColumns} onChange={(v) => set({ rowColumns: v })} />
|
|
707
|
-
</SettingsRow>
|
|
708
|
-
<SettingsRow label="Flat" hint="ignore folders, show the whole subtree">
|
|
709
|
-
<SettingsSwitch label="Flat" on={settings.flat} onChange={(v) => set({ flat: v })} />
|
|
710
|
-
</SettingsRow>
|
|
711
|
-
<SettingsRow label="Group resolutions" hint={noVariants ? 'no resolution sets in this bucket' : `${profile.variantSets} sets — previews the smallest file`}>
|
|
712
|
-
<SettingsSwitch label="Group resolutions" on={settings.groupVariants} onChange={(v) => set({ groupVariants: v })} disabled={noVariants} disabledHint="nothing to group here" />
|
|
713
|
-
</SettingsRow>
|
|
714
|
-
<SettingsRow label="Fold HLS segments" hint={noSegments ? 'no segments in this bucket' : `${profile.segments} segments into stream rows`}>
|
|
715
|
-
<SettingsSwitch label="Fold HLS segments" on={settings.foldSegments} onChange={(v) => set({ foldSegments: v })} disabled={noSegments} disabledHint="nothing to fold here" />
|
|
716
|
-
</SettingsRow>
|
|
717
|
-
</LabeledControlSection>
|
|
718
|
-
<LabeledControlSection label="Loading" divided>
|
|
719
|
-
<SettingsRow label="Kinds" align="fill">
|
|
720
|
-
<SettingsMulti options={ALL_KINDS.map((k) => ({ value: k, label: KIND_LABEL[k] || k }))} selected={settings.kinds} onToggle={toggleKind} noun="kinds" />
|
|
721
|
-
</SettingsRow>
|
|
722
|
-
<SettingsRow label="Page size" hint="entries mounted at once" align="fill">
|
|
723
|
-
<SettingsChoice options={[{ value: 100, label: '100' }, { value: 200, label: '200' }, { value: 500, label: '500' }, { value: 0, label: 'All' }]} value={settings.pageSize} onChange={(v) => set({ pageSize: v })} />
|
|
724
|
-
</SettingsRow>
|
|
725
|
-
<SettingsRow label="Video preview" hint="poster uses the sibling image; autoload fetches the file" align="fill">
|
|
726
|
-
<SettingsChoice options={[{ value: 'poster', label: 'Poster' }, { value: 'none', label: 'None' }, { value: 'autoload', label: 'Autoload' }]} value={settings.videoPreview} onChange={(v) => set({ videoPreview: v })} />
|
|
727
|
-
</SettingsRow>
|
|
728
|
-
</LabeledControlSection>
|
|
729
|
-
<LabeledControlSection label="Layout" divided>
|
|
730
|
-
<SettingsRow label="View" align="fill">
|
|
731
|
-
<SettingsChoice options={[{ value: 'off', label: 'Off' }, { value: 'grid', label: 'Grid' }, { value: 'list', label: 'List' }]} value={settings.layout} onChange={(v) => set({ layout: v })} />
|
|
732
|
-
</SettingsRow>
|
|
733
|
-
<SettingsRow label="Sort" align="fill">
|
|
734
|
-
<SettingsChoice options={SORT_OPTIONS} value={settings.sortBy} onChange={(v) => set({ sortBy: v })} />
|
|
735
|
-
</SettingsRow>
|
|
736
|
-
<SettingsRow label="Direction" align="fill">
|
|
737
|
-
<SettingsChoice options={[{ value: 'asc', label: '↓ Asc' }, { value: 'desc', label: '↑ Desc' }]} value={settings.sortDir} onChange={(v) => set({ sortDir: v })} />
|
|
738
|
-
</SettingsRow>
|
|
739
|
-
</LabeledControlSection>
|
|
740
|
-
{void bucketMeta}
|
|
742
|
+
<SettingsSections sections={mediaSettingsSections({ settings, onChange, profile })} divided />
|
|
741
743
|
</SettingsPanel>
|
|
742
744
|
)
|
|
743
745
|
}
|
|
@@ -916,6 +918,44 @@ function LibraryHeader({ title, buckets, bucketId, appRoot, onBucket, bucketMeta
|
|
|
916
918
|
)
|
|
917
919
|
}
|
|
918
920
|
|
|
921
|
+
/* `columnHeight: 'fill'` — THE BROWSER TAKES WHAT IS LEFT OF THE WINDOW (media-shell, user 2026-09-26:
|
|
922
|
+
* "why isnt the browser in media shell using available height like media does?"). apps/media had it
|
|
923
|
+
* as a hand-measured `calc(100dvh - 212px)` — its header, gaps and padding, counted once — so the same
|
|
924
|
+
* tool one shell over got the 528 base. Measured instead: the window, less the view's own top, less
|
|
925
|
+
* what the page draws under it (the count line), less the bottom padding / border / margin of every
|
|
926
|
+
* box around it. Follows a resize and any change above it. Floor 240 so a short window still browses. */
|
|
927
|
+
/* `view` is the ELEMENT (state, via a callback ref), not a ref object: the view mounts after the data
|
|
928
|
+
* loads, and in a fixed-height page (kol-shell PageShell) nothing else resizes to wake the observer —
|
|
929
|
+
* a ref read once at mount stayed null and the browser sat at the 528 base. */
|
|
930
|
+
function useFillHeight(on, view, pageEl) {
|
|
931
|
+
const [h, setH] = useState(null)
|
|
932
|
+
useLayoutEffect(() => {
|
|
933
|
+
if (!on || !view) return undefined
|
|
934
|
+
const measure = () => {
|
|
935
|
+
const page = pageEl.current
|
|
936
|
+
if (!page) return
|
|
937
|
+
const r = view.getBoundingClientRect()
|
|
938
|
+
/* what the page DRAWS under the view — its content's bottom, not its box's: a page stretched to
|
|
939
|
+
* fill its container (flex-1) would count its own empty space as "below" and freeze the height */
|
|
940
|
+
const contentBottom = Math.max(r.bottom, ...[...page.children].map((c) => c.getBoundingClientRect().bottom))
|
|
941
|
+
let below = contentBottom - r.bottom
|
|
942
|
+
for (let el = page; el && el !== document.body; el = el.parentElement) {
|
|
943
|
+
const cs = getComputedStyle(el)
|
|
944
|
+
below += parseFloat(cs.paddingBottom) + parseFloat(cs.borderBottomWidth) + (el === page ? 0 : parseFloat(cs.marginBottom))
|
|
945
|
+
}
|
|
946
|
+
const next = Math.max(240, Math.floor(window.innerHeight - r.top - below))
|
|
947
|
+
setH((p) => (p === next ? p : next))
|
|
948
|
+
}
|
|
949
|
+
measure()
|
|
950
|
+
const ro = new ResizeObserver(measure)
|
|
951
|
+
ro.observe(document.body)
|
|
952
|
+
if (pageEl.current) ro.observe(pageEl.current)
|
|
953
|
+
window.addEventListener('resize', measure)
|
|
954
|
+
return () => { ro.disconnect(); window.removeEventListener('resize', measure) }
|
|
955
|
+
}, [on, view]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
956
|
+
return h
|
|
957
|
+
}
|
|
958
|
+
|
|
919
959
|
/* the profile — what the bucket holds; drives the chips and the settings' "nothing to fold" states */
|
|
920
960
|
const profileOf = (objects, rawFiles, systemCount) => ({
|
|
921
961
|
variantSets: groupVariants(objects.map((o) => ({ ...o, displayKey: o.key }))).filter((g) => g.variants).length,
|
|
@@ -1689,6 +1729,17 @@ export function MediaLibraryBrowse({
|
|
|
1689
1729
|
setQuickLook({ files, index: at === -1 ? 0 : at })
|
|
1690
1730
|
}, [rowSelection]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
1691
1731
|
const columnsRef = useRef(null)
|
|
1732
|
+
/* the view box (whichever view is mounted) and the page root — what `fill` measures */
|
|
1733
|
+
const [viewEl, viewRef] = useState(null)
|
|
1734
|
+
/* stable, so the columns box is not re-attached (null, then itself) on every render */
|
|
1735
|
+
const columnsBox = useCallback((el) => { columnsRef.current = el; viewRef(el) }, [])
|
|
1736
|
+
const pageRef = useRef(null)
|
|
1737
|
+
const fill = settings.columnHeight === 'fill'
|
|
1738
|
+
const fillH = useFillHeight(fill, viewEl, pageRef)
|
|
1739
|
+
/* a drag in fill mode is the SESSION's, never saved — one height on every reload (apps/media's ruling) */
|
|
1740
|
+
const [dragH, setDragH] = useState(null)
|
|
1741
|
+
const colH = fill ? (dragH ?? fillH ?? SETTINGS_BASE.columnHeight) : (settings.columnHeight ?? SETTINGS_BASE.columnHeight)
|
|
1742
|
+
const onColH = (px) => (fill ? setDragH(px) : setSettings({ ...settings, columnHeight: px }))
|
|
1692
1743
|
/* `flat` IS THE WALL'S, NOT THIS PAGE'S (user 2026-09-22: "there is no option to switch to flat
|
|
1693
1744
|
* mode, bc its not in files view. this is row mode.. this makes no sense"). It reached here from
|
|
1694
1745
|
* kol-r2b2's single surface and did two things that cannot be defended in a TREE: it struck every
|
|
@@ -1904,12 +1955,12 @@ export function MediaLibraryBrowse({
|
|
|
1904
1955
|
const treeObjects = keep ? sortedObjects.filter((o) => keep.has(o.key)) : sortedObjects
|
|
1905
1956
|
return (
|
|
1906
1957
|
folderView === 'columns' || phone ? (
|
|
1907
|
-
<div ref={
|
|
1958
|
+
<div ref={columnsBox} className="relative">
|
|
1908
1959
|
<ColumnBrowser
|
|
1909
1960
|
autoFocus={autoFocus}
|
|
1910
1961
|
className={atTitleRoot ? 'is-root' : ''}
|
|
1911
|
-
height={
|
|
1912
|
-
onHeightChange={
|
|
1962
|
+
height={colH}
|
|
1963
|
+
onHeightChange={onColH}
|
|
1913
1964
|
columnWidths={settings.columnWidths}
|
|
1914
1965
|
onColumnResize={(i, px) => setSettings({ ...settings, columnWidths: { ...(settings.columnWidths ?? {}), [i]: px } })}
|
|
1915
1966
|
objects={treeObjects.filter((o) => !isSystemFile(o.key)).map((o) => ({ ...o, key: `${VROOT}${o.key}` }))}
|
|
@@ -1993,7 +2044,7 @@ export function MediaLibraryBrowse({
|
|
|
1993
2044
|
* the same line — the count line under them jumps otherwise, and a consumer's fill height
|
|
1994
2045
|
* is ONE value for both — so the header comes out of this view's budget rather than being
|
|
1995
2046
|
* added to it. The pane takes what is left. */
|
|
1996
|
-
<div className="flex flex-col gap-2" style={{ height:
|
|
2047
|
+
<div ref={viewRef} className="flex flex-col gap-2" style={{ height: colH }}>
|
|
1997
2048
|
{/* the header labels the LIST, so it spans the list and stops where the preview starts */}
|
|
1998
2049
|
{rowCols && (
|
|
1999
2050
|
<RowHeader sortBy={settings.sortBy} sortDir={settings.sortDir} width={showRowPreview ? `calc(100% - ${previewWidth}px)` : '100%'}
|
|
@@ -2103,7 +2154,7 @@ export function MediaLibraryBrowse({
|
|
|
2103
2154
|
onChange={(e) => setSettings({ ...settings, tileSize: Number(e.target.value) })} aria-label="Tile size" />
|
|
2104
2155
|
)
|
|
2105
2156
|
const wallPane = (files) => (
|
|
2106
|
-
<div className="flex flex-col gap-2" style={{ height:
|
|
2157
|
+
<div ref={viewRef} className="flex flex-col gap-2" style={{ height: colH }}>
|
|
2107
2158
|
<div className="relative border rounded flex overflow-hidden flex-1 min-h-0" style={{ borderColor: 'var(--kol-oq-08)' }}>
|
|
2108
2159
|
{/* THE MARQUEE IS THE PANE'S, not the tile grid's (user 2026-09-23: a drag only started where
|
|
2109
2160
|
* there were tiles — the grid is as tall as its tiles, the pane is the whole frame). The rows'
|
|
@@ -2189,7 +2240,7 @@ export function MediaLibraryBrowse({
|
|
|
2189
2240
|
: treeBody(filtered ? new Set(filtered.map((f) => f.key)) : null))
|
|
2190
2241
|
|
|
2191
2242
|
return (
|
|
2192
|
-
<div {...press} className={`flex flex-col gap-6 ${className}`.trim()}>
|
|
2243
|
+
<div ref={pageRef} {...press} className={`flex flex-col gap-6 ${className}`.trim()}>
|
|
2193
2244
|
<LibraryHeader title={title} buckets={buckets} bucketId={bucketMeta.id} appRoot={atTitleRoot} bucketMeta={bucketMeta} writable={writable} headerActions={headerActions} headerTrailing={headerTrailing} onTrash={trash ? () => setTrashOpen(true) : undefined}
|
|
2194
2245
|
onHome={goTitleRoot}
|
|
2195
2246
|
onBucket={(v) => { if (v === 'all') goTitleRoot(); else { setAppRoot(false); switchBucket(v) } }}
|
|
@@ -27,7 +27,7 @@ const ToolbarIcon = ({ name, label, onClick, active = false, triggerClassName })
|
|
|
27
27
|
* icon, and only when pressed". Rest is bare; hover is ink only. */
|
|
28
28
|
/* 20px glyph, half-step pad — "TOO SMALL, TOO MUCH PADDING" (user
|
|
29
29
|
* frames 2026-08-09): the reference icons nearly fill their box. */
|
|
30
|
-
className={`inline-flex cursor-pointer rounded-sm border-0 p-0.5 transition-colors duration-150 ${active ? 'bg-fg-08 text-emphasis' : 'bg-transparent text-
|
|
30
|
+
className={`inline-flex cursor-pointer rounded-sm border-0 p-0.5 transition-colors duration-150 ${active ? 'bg-fg-08 text-emphasis' : 'bg-transparent text-oq-64 hover:text-emphasis'}`}
|
|
31
31
|
>
|
|
32
32
|
<Icon name={name} size={20} />
|
|
33
33
|
</button>
|
|
@@ -255,7 +255,7 @@ export default function RecordManager({
|
|
|
255
255
|
<button
|
|
256
256
|
type="button"
|
|
257
257
|
aria-label={reorderLabel(i + 1)}
|
|
258
|
-
className="inline-flex w-4 cursor-grab touch-none justify-center border-0 bg-transparent p-0 text-
|
|
258
|
+
className="inline-flex w-4 cursor-grab touch-none justify-center border-0 bg-transparent p-0 text-oq-48 hover:text-emphasis"
|
|
259
259
|
onPointerDown={(e) => startDrag(e, i)}
|
|
260
260
|
>
|
|
261
261
|
<Icon name="drag-handle" size={16} />
|
|
@@ -171,7 +171,11 @@ export function LabeledControlSection({ label, divided = false, rowGap = 2, chil
|
|
|
171
171
|
* rebuilt this same Dropdown in a local `ChoiceRow`, which is the exact
|
|
172
172
|
* duplication this component exists to prevent. A component that wraps another
|
|
173
173
|
* has to forward the wrapped one's seams or it becomes a wall. */
|
|
174
|
-
|
|
174
|
+
/* `variant` UNSET by default (the Hub review, 2026-09-26): a forced `primary` blocked the
|
|
175
|
+
* tone of a `kol-tone-*` wrapper, so a sunken settings page drew a light well in every row
|
|
176
|
+
* unless the row said `tone="sunken"` itself. Unset, Dropdown takes the wrapper's tone and
|
|
177
|
+
* falls back to primary — the same render wherever there is no wrapper. */
|
|
178
|
+
export function SettingsChoice({ options = [], value, onChange, ariaLabel, tone = 'default', size = 'sm', variant, className = '' }) {
|
|
175
179
|
const opts = options.map((o) => (o != null && typeof o === 'object' ? o : { value: o, label: String(o) }))
|
|
176
180
|
return <Dropdown size={size} variant={variant} tone={tone} value={value} onChange={onChange} options={opts} className={`w-full ${className}`.trim()} aria-label={ariaLabel} />
|
|
177
181
|
}
|
|
@@ -35,7 +35,7 @@ import { LabeledControlSection, SettingsRow } from './SettingsPanel.jsx'
|
|
|
35
35
|
* first included: the rule is `.kol-section--divided + .kol-section--divided`,
|
|
36
36
|
* so the first draws nothing and skipping it just costs the next its line.
|
|
37
37
|
*
|
|
38
|
-
* @param {Array<{label?: string, rows: Array<{label: ReactNode, render?: Function, value?: ReactNode, align?: string, labelWidth?: number|'auto', id?: string}>, rowGap?: number}>} sections - The settings body, in order
|
|
38
|
+
* @param {Array<{label?: string, rows: Array<{label: ReactNode, render?: Function, value?: ReactNode, hint?: string, align?: string, labelWidth?: number|'auto', id?: string}>, rowGap?: number}>} sections - The settings body, in order (`hint` → SettingsRow's hint)
|
|
39
39
|
* @param {boolean} [divided=false] - Hairline above each section
|
|
40
40
|
* @param {number|'auto'} [labelWidth] - Default label column for every row — `'auto'` makes the label yield and the control hug, which is what a narrow column or a nowrap value needs
|
|
41
41
|
* @param {string} [align] - Default row alignment passed to `SettingsRow`
|
|
@@ -50,6 +50,7 @@ export default function SettingsSections({ sections = [], divided = false, label
|
|
|
50
50
|
<SettingsRow
|
|
51
51
|
key={row.id ?? row.label ?? j}
|
|
52
52
|
label={row.label}
|
|
53
|
+
hint={row.hint}
|
|
53
54
|
align={row.align ?? align}
|
|
54
55
|
labelWidth={row.labelWidth ?? labelWidth}
|
|
55
56
|
>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
2
|
import ColorSwatch from '../atoms/ColorSwatch.jsx'
|
|
3
3
|
import { resolveCssVar, resolveCssColor, isLight } from '../hooks/cssVar.js'
|
|
4
|
+
import { Tooltip } from '../utilities/Popover.jsx'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* SpectrumGrid — matrix view of the whole ramp system: rows = ramps, columns =
|
|
@@ -63,10 +64,9 @@ export default function SpectrumGrid({ ramps, stops = DEFAULT_STOPS, brandAnchor
|
|
|
63
64
|
const textColor = isLight(color) ? '#000' : '#fff'
|
|
64
65
|
const brand = isBrandAnchor(ramp, stop)
|
|
65
66
|
return (
|
|
67
|
+
<Tooltip key={stop} label={`${ramp}-${stop} ${hex}${brand ? ' · brand anchor' : ''}`} asChild>
|
|
66
68
|
<div
|
|
67
|
-
key={stop}
|
|
68
69
|
className="relative aspect-[5/3] rounded overflow-hidden transition-transform hover:scale-[1.04]"
|
|
69
|
-
title={`${ramp}-${stop} ${hex}${brand ? ' · brand anchor' : ''}`}
|
|
70
70
|
>
|
|
71
71
|
<ColorSwatch hex={`var(--${ramp}-${stop})`} size="stretch" radius="none" frame={false} className="absolute inset-0" />
|
|
72
72
|
<div className="pointer-events-none absolute inset-0 px-2 py-1.5" style={{ color: textColor }}>
|
|
@@ -81,6 +81,7 @@ export default function SpectrumGrid({ ramps, stops = DEFAULT_STOPS, brandAnchor
|
|
|
81
81
|
)}
|
|
82
82
|
</div>
|
|
83
83
|
</div>
|
|
84
|
+
</Tooltip>
|
|
84
85
|
)
|
|
85
86
|
})}
|
|
86
87
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useRef, useState } from 'react'
|
|
2
2
|
import Input from '../atoms/Input.jsx'
|
|
3
3
|
import Dropdown from '../molecules/Dropdown.jsx'
|
|
4
|
+
import { Tooltip } from '../utilities/Popover.jsx'
|
|
4
5
|
|
|
5
6
|
/* The six easings the key editor offers. The CURVES stay the consumer's
|
|
6
7
|
* (its interpolator resolves the name); this is the menu, not the math. */
|
|
@@ -123,9 +124,11 @@ function TrackRow({ track, t, selected, setSelected, writeKeys }) {
|
|
|
123
124
|
|
|
124
125
|
return (
|
|
125
126
|
<div className="flex items-center gap-3">
|
|
126
|
-
<
|
|
127
|
+
<Tooltip label={track.label} asChild>
|
|
128
|
+
<span className="kol-helper-10 text-meta truncate shrink-0 text-right" style={{ width: 120 }}>
|
|
127
129
|
{track.label}
|
|
128
130
|
</span>
|
|
131
|
+
</Tooltip>
|
|
129
132
|
<div
|
|
130
133
|
ref={laneRef}
|
|
131
134
|
className="relative flex-1 h-5 rounded cursor-copy"
|
|
@@ -136,13 +139,12 @@ function TrackRow({ track, t, selected, setSelected, writeKeys }) {
|
|
|
136
139
|
{track.keys.map((k, i) => {
|
|
137
140
|
const kt = drag.current?.index === i ? drag.current.t : k.t
|
|
138
141
|
return (
|
|
142
|
+
<Tooltip key={i} label={`t=${kt.toFixed(2)} v=${typeof k.v === 'number' ? Math.round(k.v * 100) / 100 : k.v} (alt-click deletes)`} asChild>
|
|
139
143
|
<span
|
|
140
|
-
key={i}
|
|
141
144
|
data-diamond=""
|
|
142
145
|
onPointerDown={onDiamondPointerDown(i)}
|
|
143
146
|
onPointerMove={onDiamondPointerMove(i)}
|
|
144
147
|
onPointerUp={onDiamondPointerUp(i)}
|
|
145
|
-
title={`t=${kt.toFixed(2)} v=${typeof k.v === 'number' ? Math.round(k.v * 100) / 100 : k.v} (alt-click deletes)`}
|
|
146
148
|
className="absolute top-1/2 cursor-grab"
|
|
147
149
|
style={{
|
|
148
150
|
left: `${kt * 100}%`,
|
|
@@ -152,6 +154,7 @@ function TrackRow({ track, t, selected, setSelected, writeKeys }) {
|
|
|
152
154
|
borderRadius: 1.5,
|
|
153
155
|
}}
|
|
154
156
|
/>
|
|
157
|
+
</Tooltip>
|
|
155
158
|
)
|
|
156
159
|
})}
|
|
157
160
|
</div>
|
|
@@ -247,7 +250,7 @@ export default function TimelineDock({ tracks = [], t = 0, onSeek, onChange, eas
|
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
return (
|
|
250
|
-
<div className={`kol-timeline-dock border-t border-
|
|
253
|
+
<div className={`kol-timeline-dock border-t border-oq-08 px-4 py-2 flex flex-col gap-1 select-none ${className}`.trim()} style={{ background: 'var(--kol-surface-primary)' }}>
|
|
251
254
|
<ScrubRuler t={t} onSeek={onSeek} />
|
|
252
255
|
{tracks.map((track) => (
|
|
253
256
|
<TrackRow key={track.id} track={track} t={t} selected={selected} setSelected={setSelected} writeKeys={writeKeys} />
|
|
@@ -2,6 +2,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
2
2
|
import Button from '../atoms/Button.jsx'
|
|
3
3
|
import Divider from '../atoms/Divider.jsx'
|
|
4
4
|
import SplitToolButton from '../molecules/SplitToolButton.jsx'
|
|
5
|
+
import { Tooltip } from '../utilities/Popover.jsx'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* ToolPalette — the editor's tool bar as one row (kol-fxr `shell/panels/ToolPalette.jsx`,
|
|
@@ -45,7 +46,6 @@ const ToolPalette = ({
|
|
|
45
46
|
<div className={`flex items-center gap-1 min-w-0 overflow-x-auto ${className}`.trim()}>
|
|
46
47
|
{items.map((it, i) => {
|
|
47
48
|
if (it.kind === 'divider') return <Divider key={`d${i}`} variant="vertical" height={20} className="mx-1.5 shrink-0" />
|
|
48
|
-
const tip = it.shortcut ? `${it.label} (${it.shortcut})` : it.label
|
|
49
49
|
if (it.kind === 'split') {
|
|
50
50
|
const variants = (it.variants ?? []).map((v) => (v.action && !it.action ? { ...v, onSelect: () => onAction?.(v.id) } : v))
|
|
51
51
|
const run = (id) => (it.action ? onAction : onSelect)?.(id)
|
|
@@ -58,10 +58,13 @@ const ToolPalette = ({
|
|
|
58
58
|
}
|
|
59
59
|
const isTool = it.kind === 'tool'
|
|
60
60
|
return (
|
|
61
|
-
|
|
61
|
+
/* the DS tooltip carries the key as its own chip (the native-title sweep, 2026-09-26) */
|
|
62
|
+
<Tooltip key={it.id} label={it.label} shortcut={it.shortcut}>
|
|
63
|
+
<Button variant="ghost" size={size} quiet iconOnly={it.icon} iconComponent={iconComponent}
|
|
62
64
|
pressed={isTool ? activeId === it.id : undefined} disabled={it.disabled}
|
|
63
|
-
aria-label={it.label}
|
|
65
|
+
aria-label={it.label}
|
|
64
66
|
onClick={(e) => { (isTool ? onSelect : onAction)?.(it.id); blur(e) }} />
|
|
67
|
+
</Tooltip>
|
|
65
68
|
)
|
|
66
69
|
})}
|
|
67
70
|
</div>
|