@kolkrabbi/kol-component 0.236.0 → 0.238.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 +4 -3
- package/src/atoms/Button.jsx +10 -2
- package/src/atoms/Input.jsx +5 -2
- package/src/atoms/Kbd.jsx +6 -3
- package/src/index.js +15 -11
- package/src/molecules/ColorInputRow.jsx +1 -1
- package/src/molecules/ColorRamp.jsx +1 -1
- package/src/molecules/CopyButton.jsx +1 -1
- package/src/molecules/Dropdown.jsx +48 -7
- package/src/molecules/KindPreview.jsx +6 -6
- package/src/{atoms/RotaryDial.jsx → molecules/Knob.jsx} +20 -2
- package/src/molecules/KnobPanel.jsx +116 -0
- package/src/molecules/LabeledControl.jsx +22 -0
- package/src/molecules/MediaPlayer.jsx +107 -0
- package/src/molecules/MenuItem.jsx +5 -2
- package/src/molecules/Modal.jsx +1 -1
- package/src/molecules/MultiSelect.jsx +70 -0
- package/src/molecules/OptionRow.jsx +1 -1
- package/src/molecules/PlayTile.jsx +70 -0
- package/src/molecules/PlaybackBar.jsx +7 -16
- package/src/molecules/RowMenuButton.jsx +1 -1
- package/src/molecules/ShapeDropdown.jsx +3 -3
- package/src/molecules/Slider.jsx +93 -56
- package/src/molecules/SliderPanel.jsx +88 -0
- package/src/molecules/SplitToolButton.jsx +1 -1
- package/src/molecules/SwatchControls.jsx +1 -1
- package/src/molecules/TiltBento.jsx +5 -5
- package/src/organisms/ColumnBrowser.jsx +1 -1
- package/src/organisms/ContentFilters.jsx +1 -1
- package/src/organisms/CurveEditor.jsx +2 -2
- package/src/organisms/FeaturedCarousel.jsx +4 -4
- package/src/organisms/KeyframeEditor.jsx +3 -3
- package/src/organisms/LayerStack.jsx +2 -2
- package/src/organisms/MediaLibrary.jsx +1 -1
- package/src/organisms/MediaLibraryPages.jsx +20 -20
- package/src/organisms/RecordManager.jsx +1 -1
- package/src/organisms/SectionHero.jsx +3 -3
- package/src/organisms/SectionNewsletter.jsx +2 -1
- package/src/organisms/ShellSearchOverlay.jsx +13 -9
- package/src/organisms/SpectrumGrid.jsx +1 -1
- package/src/organisms/ToolPalette.jsx +1 -1
- package/src/{atoms/HlsVideo.jsx → utilities/BackgroundVideo.jsx} +6 -2
- package/src/utilities/ErrorBoundary.jsx +1 -1
- package/src/utilities/Popover.jsx +3 -1
- package/src/utilities/QuadrantSync.jsx +4 -1
- package/src/utilities/armLongPress.js +18 -0
- package/src/utilities/mediaSearch.js +2 -2
- package/src/utilities/tone.js +6 -0
- package/src/atoms/AudioPlayer.jsx +0 -51
- package/src/molecules/AudioPreview.jsx +0 -140
- package/src/molecules/AudioSheet.jsx +0 -48
- package/src/molecules/VideoSheet.jsx +0 -25
- /package/src/{atoms → molecules}/ActionButton.jsx +0 -0
- /package/src/{atoms → molecules}/ColorSwatch.jsx +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// armLongPress — the touch hold a value control reports (Slider/Knob `variant="panel"`; kol-monitor 2026-09-01,
|
|
2
|
+
// moved here from kol-hardware 2026-10-01 with the controls that arm it).
|
|
3
|
+
|
|
4
|
+
const HOLD_MS = 500
|
|
5
|
+
const SLOP_PX = 6
|
|
6
|
+
|
|
7
|
+
/* Arm a long-press on a touch pointerdown. Returns `{ move, cancel }`: feed
|
|
8
|
+
* pointermoves to `move` (past SLOP it disarms — that is a drag, not a hold),
|
|
9
|
+
* call `cancel` on pointerup. `fire` runs once when the hold lands. A mouse
|
|
10
|
+
* never arms — desktop keeps drag + ⌥-click and nothing else. */
|
|
11
|
+
export function armLongPress(e, fire) {
|
|
12
|
+
if (e.pointerType !== 'touch') return { move: () => {}, cancel: () => {} }
|
|
13
|
+
const x = e.clientX, y = e.clientY
|
|
14
|
+
let t = setTimeout(() => { t = null; fire() }, HOLD_MS)
|
|
15
|
+
const cancel = () => { if (t) { clearTimeout(t); t = null } }
|
|
16
|
+
const move = (ev) => { if (t && Math.hypot(ev.clientX - x, ev.clientY - y) > SLOP_PX) cancel() }
|
|
17
|
+
return { move, cancel }
|
|
18
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* mediaSearch — the media library's search, on the KOL engine (`@kolkrabbi/kol-search`).
|
|
3
3
|
*
|
|
4
4
|
* Four places in the media pages each ran their own `key.toLowerCase().includes(q)` — the browse
|
|
5
|
-
* filter, the ⌘K
|
|
5
|
+
* filter, the ⌘K search modal, the smart-folder text match, the picker's filter (apps review,
|
|
6
6
|
* 2026-09-29). One helper now, so the media search speaks the same language as every other KOL
|
|
7
7
|
* search: several words must ALL match, `-word` must not, `"a phrase"`, `tag:x` / `#x`,
|
|
8
8
|
* `kind:image` / `is:video`, `after:2026-09-01`. A plain substring still finds what it found.
|
|
@@ -24,7 +24,7 @@ const toItem = (o, nameOf) => ({
|
|
|
24
24
|
o,
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
/** ranked hits, best first — the
|
|
27
|
+
/** ranked hits, best first — the search modal's order. `nameOf(o)` overrides the title (a display key). */
|
|
28
28
|
export function rankMedia(objects, query, { nameOf, limit } = {}) {
|
|
29
29
|
const q = String(query ?? '').trim()
|
|
30
30
|
if (!q) return limit ? objects.slice(0, limit) : objects
|
package/src/utilities/tone.js
CHANGED
|
@@ -26,6 +26,12 @@ export const TONES = ['primary', 'secondary', 'inverted', 'outline', 'ghost', 'g
|
|
|
26
26
|
export const toneClass = (tone) =>
|
|
27
27
|
tone === 'inverse' ? 'kol-tone-sunken' : TONES.includes(tone) ? `kol-tone-${tone}` : ''
|
|
28
28
|
|
|
29
|
+
/* THE SIX VARIANTS THAT WERE TONES (user ruling 2026-10-01). Button's `variant` named five grounds
|
|
30
|
+
* and a legacy alias of one; each is a tone under a second name — the theme's variant classes are
|
|
31
|
+
* aliases of the tone bundles. `variant` keeps `accent · danger · nav`; these six are deprecated
|
|
32
|
+
* aliases on the retirement ledger. `secondary` is the misleading one: it paints `inverted`. */
|
|
33
|
+
export const VARIANT_TONE = { primary: 'primary', secondary: 'inverted', outline: 'outline', ghost: 'ghost', grey: 'grey', control: 'ghost' }
|
|
34
|
+
|
|
29
35
|
/* the properties a bundle sets — Dropdown copies them from its trigger onto
|
|
30
36
|
* the PORTALLED panel, which the cascade cannot reach */
|
|
31
37
|
export const TONE_VARS = [
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AudioPlayer — A native audio player with a label. the interactive audio atom: one native <audio> with the UA's
|
|
3
|
-
* own control strip, and an optional label line above it.
|
|
4
|
-
*
|
|
5
|
-
* WHY IT EXISTS. Audio was the one media kind the design system had nothing for,
|
|
6
|
-
* so every consumer hand-rolled a bare `<audio controls>` and inherited whatever
|
|
7
|
-
* the browser painted. The MediaLibrary widening (kol-component 0.39.0) made
|
|
8
|
-
* audio objects arrive — 116 sound files in `kol-vault-media` alone — and left
|
|
9
|
-
* minting this as the open taxonomy call; this is that call made: an ATOM,
|
|
10
|
-
* beside HlsVideo.
|
|
11
|
-
*
|
|
12
|
-
* THE CONTRAST WITH HlsVideo, which is the thing to read before touching either.
|
|
13
|
-
* Same tier, same shape — one native media element, all layout via `className`,
|
|
14
|
-
* no composition — and **inverted intent**. HlsVideo is deliberately inert:
|
|
15
|
-
* `pointer-events: none`, `controls={false}`, plus the hardening set (no PiP, no
|
|
16
|
-
* download, no fullscreen, no remote playback, no context menu). It is
|
|
17
|
-
* decorative. This atom exists **to be operated**, so none of that hardening
|
|
18
|
-
* crosses over, and `controls` is not a prop — an audio player without controls
|
|
19
|
-
* is not a variant, it is a different component.
|
|
20
|
-
*
|
|
21
|
-
* The native strip is UA-painted and not themeable past `color-scheme`. Do not
|
|
22
|
-
* reach for pseudo-element hacks; a branded transport would be a separate
|
|
23
|
-
* `AudioTransport` molecule built on this atom.
|
|
24
|
-
*
|
|
25
|
-
* Layout is the call site's: the padding, the centring and the width that lived
|
|
26
|
-
* in the consumer source are all call-site concerns and none of them are baked
|
|
27
|
-
* in here.
|
|
28
|
-
*
|
|
29
|
-
* @param {string} src audio URL; renders nothing when absent (the guard
|
|
30
|
-
* style the other media atoms use)
|
|
31
|
-
* @param {string} [label] line above the player; the element is omitted entirely
|
|
32
|
-
* when absent. Authored casing — no text-transform
|
|
33
|
-
* @param {'none'|'metadata'|'auto'} [preload='metadata'] native preload hint
|
|
34
|
-
* @param {string} [className] all layout/sizing (consumer-supplied)
|
|
35
|
-
*/
|
|
36
|
-
export default function AudioPlayer({
|
|
37
|
-
src,
|
|
38
|
-
label,
|
|
39
|
-
preload = 'metadata',
|
|
40
|
-
className = '',
|
|
41
|
-
...props
|
|
42
|
-
}) {
|
|
43
|
-
if (!src) return null
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<figure className={`kol-audio-player flex flex-col gap-3 ${className}`.trim()}>
|
|
47
|
-
{label && <figcaption className="kol-mono-12 text-fg-48">{label}</figcaption>}
|
|
48
|
-
<audio src={src} preload={preload} controls className="w-full" {...props} />
|
|
49
|
-
</figure>
|
|
50
|
-
)
|
|
51
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/* eslint-disable react-hooks/refs -- floating-ui's `refs.setReference` is a callback ref, not a ref read; the DS Tooltip wires it the same way */
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import IconFrame from '../atoms/IconFrame.jsx';
|
|
4
|
-
import Slider from './Slider.jsx';
|
|
5
|
-
import { usePopover, PopoverPanel } from '../utilities/Popover.jsx';
|
|
6
|
-
import { readCover } from '../utilities/id3.js';
|
|
7
|
-
|
|
8
|
-
/* taxonomy-ok: molecule — nests IconFrame (atom) + Slider / PopoverPanel (relative). */
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* AudioPreview · AudioTile · VideoTile — kol-r2b2's players, promoted verbatim
|
|
12
|
-
* (ColumnBrowserMediaFacts 2026-08-27; the tiles re-ruled the same day,
|
|
13
|
-
* PlayDiscAndVideoBar): players composed from DS parts (IconFrame · Slider ·
|
|
14
|
-
* Popover) — the DS AudioPlayer is a native <audio controls>, whose one-row
|
|
15
|
-
* layout can't be reshaped. Finder model (user ruling 2026-08-27): the COLUMN
|
|
16
|
-
* gets a square tile over full-bleed artwork with ONE control — the Finder
|
|
17
|
-
* play/pause disc (`.kol-play-disc`: round, hidden at rest, shown on hover of
|
|
18
|
-
* `.kol-media-tile`) — `AudioTile` (artwork = the file's embedded ID3 cover,
|
|
19
|
-
* `readCover`) / `VideoTile` (artwork = the poster; the media element is hidden
|
|
20
|
-
* — the tile never shows a decoded video frame). Timeline + volume belong to
|
|
21
|
-
* the OVERLAY / Quick Look — `AudioPreview` (play/pause · Slider seek with an
|
|
22
|
-
* m:ss readout · volume behind `slider-01`, a vertical `slider-black` range in
|
|
23
|
-
* a PopoverPanel, placement top; no title line — the name sits in the facts)
|
|
24
|
-
* and `VideoSheet` (the QuickTime bar).
|
|
25
|
-
*
|
|
26
|
-
* @param {string} src the media URL
|
|
27
|
-
* @param {string} poster VideoTile — the poster URL
|
|
28
|
-
* @param {Function} onDuration AudioPreview — (seconds) => void once metadata lands
|
|
29
|
-
* @param {string} className AudioPreview — wrapper classes (the width is the consumer's)
|
|
30
|
-
* @param {ReactNode} fallback AudioTile — shown when the file carries no cover art (2026-09-23:
|
|
31
|
-
* it was an empty square, so a WAV read as a missing file)
|
|
32
|
-
*/
|
|
33
|
-
export const formatLength = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
|
|
34
|
-
const fmt = formatLength;
|
|
35
|
-
|
|
36
|
-
function useCover(src) {
|
|
37
|
-
const [cover, setCover] = useState(null);
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
let live = true;
|
|
40
|
-
readCover(src).then((url) => { if (live) setCover(url); });
|
|
41
|
-
return () => { live = false; };
|
|
42
|
-
}, [src]);
|
|
43
|
-
return cover;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/* One disc for both tiles — the ruled Finder play/pause. The theme's
|
|
47
|
-
* `.kol-play-disc` paints it after the IconFrame variants, so nothing is restated here. */
|
|
48
|
-
function PlayDisc({ playing, onClick }) {
|
|
49
|
-
return (
|
|
50
|
-
<IconFrame name={playing ? 'pause' : 'play'} variant="secondary" size="lg" radius="full" onClick={onClick} aria-label={playing ? 'Pause' : 'Play'} className="relative kol-play-disc" />
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function AudioTile({ src, fallback = null }) {
|
|
55
|
-
const ref = useRef(null);
|
|
56
|
-
const [playing, setPlaying] = useState(false);
|
|
57
|
-
const cover = useCover(src);
|
|
58
|
-
return (
|
|
59
|
-
<div className="kol-media-tile relative w-full aspect-square rounded overflow-hidden flex items-center justify-center">
|
|
60
|
-
<audio ref={ref} src={src} preload="metadata" onPlay={() => setPlaying(true)} onPause={() => setPlaying(false)} onEnded={() => setPlaying(false)} />
|
|
61
|
-
{cover ? <img src={cover} alt="" className="absolute inset-0 w-full h-full object-cover" /> : fallback && <div className="absolute inset-0 flex items-center justify-center">{fallback}</div>}
|
|
62
|
-
<PlayDisc playing={playing} onClick={() => (playing ? ref.current.pause() : ref.current.play())} />
|
|
63
|
-
</div>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function VideoTile({ src, poster }) {
|
|
68
|
-
const ref = useRef(null);
|
|
69
|
-
const [playing, setPlaying] = useState(false);
|
|
70
|
-
return (
|
|
71
|
-
<div className="kol-media-tile relative w-full aspect-square rounded overflow-hidden flex items-center justify-center">
|
|
72
|
-
{/* Same as audio: the media element is hidden, the artwork (the poster) is the tile. */}
|
|
73
|
-
<video ref={ref} src={src} playsInline preload="metadata" className={poster ? 'hidden' : 'absolute inset-0 w-full h-full object-cover'} onPlay={() => setPlaying(true)} onPause={() => setPlaying(false)} onEnded={() => setPlaying(false)} />
|
|
74
|
-
{poster && <img src={poster} alt="" className="absolute inset-0 w-full h-full object-cover" />}
|
|
75
|
-
<PlayDisc playing={playing} onClick={() => (playing ? ref.current.pause() : ref.current.play())} />
|
|
76
|
-
</div>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function useAudio(onDuration) {
|
|
81
|
-
const ref = useRef(null);
|
|
82
|
-
const [playing, setPlaying] = useState(false);
|
|
83
|
-
const [time, setTime] = useState(0);
|
|
84
|
-
const [duration, setDuration] = useState(0);
|
|
85
|
-
const toggle = () => (playing ? ref.current.pause() : ref.current.play());
|
|
86
|
-
const element = (src) => (
|
|
87
|
-
<audio
|
|
88
|
-
ref={ref}
|
|
89
|
-
src={src}
|
|
90
|
-
preload="metadata"
|
|
91
|
-
onPlay={() => setPlaying(true)}
|
|
92
|
-
onPause={() => setPlaying(false)}
|
|
93
|
-
onEnded={() => setPlaying(false)}
|
|
94
|
-
onTimeUpdate={(e) => setTime(e.target.currentTime)}
|
|
95
|
-
onLoadedMetadata={(e) => { setDuration(e.target.duration); onDuration?.(e.target.duration); }}
|
|
96
|
-
/>
|
|
97
|
-
);
|
|
98
|
-
return { ref, playing, time, duration, toggle, element, setTime };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export default function AudioPreview({ src, className = '', onDuration }) {
|
|
102
|
-
const { ref, playing, time, duration, toggle, element, setTime } = useAudio(onDuration);
|
|
103
|
-
const [volume, setVolume] = useState(100);
|
|
104
|
-
const [open, setOpen] = useState(false);
|
|
105
|
-
const pop = usePopover({ open, onOpenChange: setOpen, placement: 'top' });
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<div className={`flex items-center gap-2 ${className}`}>
|
|
109
|
-
{element(src)}
|
|
110
|
-
<IconFrame name={playing ? 'pause' : 'play'} variant="ghost" size="sm" onClick={toggle} />
|
|
111
|
-
<Slider
|
|
112
|
-
className="flex-1"
|
|
113
|
-
min={0}
|
|
114
|
-
max={duration || 0}
|
|
115
|
-
step={0.1}
|
|
116
|
-
value={time}
|
|
117
|
-
onChange={(v) => { ref.current.currentTime = v; setTime(v); }}
|
|
118
|
-
formatValue={fmt}
|
|
119
|
-
displayWidth={5}
|
|
120
|
-
/>
|
|
121
|
-
<span ref={pop.refs.setReference} {...pop.getReferenceProps()} className="inline-flex">
|
|
122
|
-
<IconFrame name="slider-01" variant={open ? 'secondary' : 'ghost'} size="sm" onClick={() => {}} />
|
|
123
|
-
</span>
|
|
124
|
-
<PopoverPanel popover={pop} className="p-2">
|
|
125
|
-
{/* DS track class on a native range, turned upright: the one thing the DS Slider can't do. */}
|
|
126
|
-
<div className="w-6 h-24 flex items-center justify-center">
|
|
127
|
-
<input
|
|
128
|
-
type="range"
|
|
129
|
-
min={0}
|
|
130
|
-
max={100}
|
|
131
|
-
value={volume}
|
|
132
|
-
onChange={(e) => { const v = Number(e.target.value); ref.current.volume = v / 100; setVolume(v); }}
|
|
133
|
-
className="slider-black cursor-pointer w-24 -rotate-90"
|
|
134
|
-
aria-label="Volume"
|
|
135
|
-
/>
|
|
136
|
-
</div>
|
|
137
|
-
</PopoverPanel>
|
|
138
|
-
</div>
|
|
139
|
-
);
|
|
140
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import usePlayback from '../hooks/usePlayback.js'
|
|
3
|
-
import PlaybackBar, { clock } from './PlaybackBar.jsx'
|
|
4
|
-
import QuickLookFrame from './QuickLookFrame.jsx'
|
|
5
|
-
import FileIcon from '../atoms/FileIcon.jsx'
|
|
6
|
-
import { readCover } from '../utilities/id3.js'
|
|
7
|
-
|
|
8
|
-
/* taxonomy-ok: molecule — nests PlaybackBar + QuickLookFrame (relative) + FileIcon (atom). */
|
|
9
|
-
|
|
10
|
-
function useCover(src) {
|
|
11
|
-
const [cover, setCover] = useState(null)
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
let live = true
|
|
14
|
-
readCover(src).then((url) => { if (live) setCover(url) }).catch(() => {})
|
|
15
|
-
return () => { live = false }
|
|
16
|
-
}, [src])
|
|
17
|
-
return cover
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* AudioSheet — Audio in the preview window. audio in the Quick Look window, Finder's layout (user 2026-09-23, the reference
|
|
22
|
-
* shot): the artwork square left, `Time: mm:ss` beside it, the QuickTime bar docked in the
|
|
23
|
-
* window's footer. Artwork = the file's embedded ID3 `APIC` cover (`readCover`); no cover →
|
|
24
|
-
* `FileIcon`, the same page the tile shows. The `cover` / `sheet` variants are gone — there is
|
|
25
|
-
* one window now, and it is the sheet.
|
|
26
|
-
*
|
|
27
|
-
* @param {string} src the audio URL
|
|
28
|
-
* @param {string} ext the extension, for the no-cover icon
|
|
29
|
-
* @param {Function} onDuration (seconds) => void once metadata lands
|
|
30
|
-
* @param {Object} frame the window's header — `QuickLookFrame`'s title · meta · actions · onClose
|
|
31
|
-
*/
|
|
32
|
-
export default function AudioSheet({ src, ext, onDuration, frame }) {
|
|
33
|
-
const { ref, handlers, bar } = usePlayback((el) => onDuration?.(el.duration))
|
|
34
|
-
const cover = useCover(src)
|
|
35
|
-
return (
|
|
36
|
-
<QuickLookFrame {...frame} footer={<PlaybackBar {...bar} docked />}>
|
|
37
|
-
<audio ref={ref} src={src} preload="metadata" {...handlers} />
|
|
38
|
-
<div className="kol-quicklook-audio">
|
|
39
|
-
<div className="kol-quicklook-art">
|
|
40
|
-
{cover
|
|
41
|
-
? <img src={cover} alt="" className="w-full h-full object-cover" />
|
|
42
|
-
: <FileIcon ext={ext} glyph="music-note" className="w-[52%]" />}
|
|
43
|
-
</div>
|
|
44
|
-
<span className="kol-mono-16 text-meta">Time: <b className="text-strong">{clock(bar.duration)}</b></span>
|
|
45
|
-
</div>
|
|
46
|
-
</QuickLookFrame>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import usePlayback from '../hooks/usePlayback.js'
|
|
2
|
-
import PlaybackBar from './PlaybackBar.jsx'
|
|
3
|
-
import QuickLookFrame from './QuickLookFrame.jsx'
|
|
4
|
-
|
|
5
|
-
/* taxonomy-ok: molecule — nests PlaybackBar + QuickLookFrame (relative). */
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* VideoSheet — A video in the preview window. video in the Quick Look window: the frame at the video's own aspect ratio, the
|
|
9
|
-
* QuickTime bar docked in the window's footer (2026-09-23 — it floated over the picture, which
|
|
10
|
-
* made video the one kind whose controls sat on the media). No native controls; click on the
|
|
11
|
-
* video toggles play; no autoplay (user 2026-09-23 — Quick Look opens paused), `playsInline`, `preload="metadata"`.
|
|
12
|
-
*
|
|
13
|
-
* @param {string} src the video URL
|
|
14
|
-
* @param {string} poster the poster URL
|
|
15
|
-
* @param {Function} onMeta ({ w, h, len }) => void once metadata lands — the window's facts
|
|
16
|
-
* @param {Object} frame the window's header — `QuickLookFrame`'s title · meta · actions · onClose
|
|
17
|
-
*/
|
|
18
|
-
export default function VideoSheet({ src, poster, onMeta, frame }) {
|
|
19
|
-
const { ref, handlers, bar } = usePlayback((el) => onMeta?.({ w: el.videoWidth, h: el.videoHeight, len: el.duration }))
|
|
20
|
-
return (
|
|
21
|
-
<QuickLookFrame {...frame} footer={<PlaybackBar {...bar} docked />}>
|
|
22
|
-
<video ref={ref} src={src} poster={poster} playsInline preload="metadata" className="kol-quicklook-media" onClick={bar.onToggle} {...handlers} />
|
|
23
|
-
</QuickLookFrame>
|
|
24
|
-
)
|
|
25
|
-
}
|
|
File without changes
|
|
File without changes
|