@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,88 @@
|
|
|
1
|
+
import { useRef, useCallback } from 'react'
|
|
2
|
+
import { armLongPress } from '../utilities/armLongPress.js'
|
|
3
|
+
|
|
4
|
+
const NO_HOLD = { move() {}, cancel() {} }
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SliderPanel — Slider's `variant="panel"`: the rack slider. A 2px track and an 8px thumb for a
|
|
8
|
+
* 24px hardware panel, drawn rather than a native range, with a vertical direction. It was
|
|
9
|
+
* kol-hardware's `Fader` (kol-monitor's rack, lifted 2026-09-01) and is carried here class for
|
|
10
|
+
* class — merged into Slider 2026-10-01 (user ruling: one slider, not two that look alike).
|
|
11
|
+
* Not exported: reach it through `<Slider variant="panel">`.
|
|
12
|
+
*/
|
|
13
|
+
export default function SliderPanel({ value, onChange, min = 0, max = 100, step = 1, label, direction = 'horizontal', height, onHold }) {
|
|
14
|
+
const trackRef = useRef(null)
|
|
15
|
+
const vertical = direction === 'vertical'
|
|
16
|
+
|
|
17
|
+
const handlePointerDown = useCallback((e) => {
|
|
18
|
+
e.preventDefault()
|
|
19
|
+
const track = trackRef.current
|
|
20
|
+
if (!track) return
|
|
21
|
+
|
|
22
|
+
const update = (e) => {
|
|
23
|
+
const rect = track.getBoundingClientRect()
|
|
24
|
+
const ratio = vertical
|
|
25
|
+
? Math.max(0, Math.min(1, 1 - (e.clientY - rect.top) / rect.height))
|
|
26
|
+
: Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
|
27
|
+
const raw = min + ratio * (max - min)
|
|
28
|
+
onChange(Math.round(raw / step) * step)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
update(e)
|
|
32
|
+
const id = e.pointerId
|
|
33
|
+
const onMove = (e) => { if (e.pointerId === id) { hold.move(e); update(e) } } // a second finger is a pan
|
|
34
|
+
const onUp = () => {
|
|
35
|
+
hold.cancel()
|
|
36
|
+
window.removeEventListener('pointermove', onMove)
|
|
37
|
+
window.removeEventListener('pointerup', onUp)
|
|
38
|
+
}
|
|
39
|
+
// Touch: hold still 500ms → the holder's sheet instead of the drag
|
|
40
|
+
const hold = onHold ? armLongPress(e, () => { onUp(); onHold({ label, value, min, max, step }) }) : NO_HOLD
|
|
41
|
+
window.addEventListener('pointermove', onMove)
|
|
42
|
+
window.addEventListener('pointerup', onUp)
|
|
43
|
+
}, [min, max, step, onChange, vertical, onHold, label, value])
|
|
44
|
+
|
|
45
|
+
const norm = max > min ? ((value ?? 0) - min) / (max - min) : 0
|
|
46
|
+
|
|
47
|
+
if (vertical) {
|
|
48
|
+
return (
|
|
49
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
|
|
50
|
+
{label && (
|
|
51
|
+
<span className="kol-helper-8 text-fg-32" style={{ textTransform: 'uppercase', lineHeight: 1 }}>
|
|
52
|
+
{label}
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
<div
|
|
56
|
+
ref={trackRef}
|
|
57
|
+
onPointerDown={handlePointerDown}
|
|
58
|
+
className="bg-fg-16"
|
|
59
|
+
style={{ width: 2, height: height ?? 60, position: 'relative', cursor: 'ns-resize', touchAction: 'none' }}
|
|
60
|
+
>
|
|
61
|
+
<div className="bg-fg-16" style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: `${norm * 100}%` }} />
|
|
62
|
+
<div className="bg-fg-72" style={{ position: 'absolute', bottom: `calc(${norm * 100}% - 4px)`, left: -3, width: 8, height: 8, borderRadius: '50%' }} />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flex: 1 }}>
|
|
70
|
+
{label && (
|
|
71
|
+
<span className="kol-helper-8 text-fg-32" style={{ textTransform: 'uppercase', lineHeight: 1, flexShrink: 0, minWidth: 20 }}>
|
|
72
|
+
{label}
|
|
73
|
+
</span>
|
|
74
|
+
)}
|
|
75
|
+
<div
|
|
76
|
+
ref={trackRef}
|
|
77
|
+
onPointerDown={handlePointerDown}
|
|
78
|
+
className="bg-fg-16"
|
|
79
|
+
style={{ flex: 1, height: 2, position: 'relative', cursor: 'pointer', touchAction: 'none' }}
|
|
80
|
+
>
|
|
81
|
+
<div className="bg-fg-72" style={{ position: 'absolute', left: `calc(${norm * 100}% - 4px)`, top: -3, width: 8, height: 8, borderRadius: '50%' }} />
|
|
82
|
+
</div>
|
|
83
|
+
<span className="kol-helper-8 text-fg-32" style={{ lineHeight: 1, flexShrink: 0, minWidth: 16, textAlign: 'right' }}>
|
|
84
|
+
{Math.round(value ?? 0)}
|
|
85
|
+
</span>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -17,7 +17,7 @@ import { glyphSize } from '../hooks/glyphLadders.js'
|
|
|
17
17
|
* corner fold inside its own box, and `blurOnClick` must blur the button
|
|
18
18
|
* itself. The class output — kol-btn kol-btn-ghost kol-btn-icon kol-btn-{size}
|
|
19
19
|
* + kol-btn-quiet / kol-btn-pressed — is exactly what
|
|
20
|
-
* `<Button
|
|
20
|
+
* `<Button tone="ghost" quiet pressed iconOnly>` emits, so the box and the
|
|
21
21
|
* visual contract stay Button's.
|
|
22
22
|
*
|
|
23
23
|
* `size` was a raw px number (28) with a transcribed 14px glyph until 0.182.0:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
2
|
import useInViewAttention from '../hooks/useInViewAttention.js'
|
|
3
3
|
import { motion } from 'framer-motion'
|
|
4
|
-
import
|
|
4
|
+
import BackgroundVideo from '../utilities/BackgroundVideo.jsx'
|
|
5
5
|
import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
|
|
6
6
|
import Button from '../atoms/Button.jsx'
|
|
7
7
|
import Image from '../atoms/Image.jsx'
|
|
@@ -9,7 +9,7 @@ import useTilt from '../hooks/useTilt.js'
|
|
|
9
9
|
import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
|
|
10
10
|
|
|
11
11
|
/* taxonomy-ok: nests the same-file Media helper plus DS atoms/molecules
|
|
12
|
-
* (
|
|
12
|
+
* (BackgroundVideo, AssetPlaceholder, Button, Image) it composes — an organism. */
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* True on coarse-pointer (touch/no-hover) devices. Local to TiltBento, mirrors
|
|
@@ -37,7 +37,7 @@ function useCoarsePointer() {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Media — internal, NOT exported. Sniffs `src` by extension and renders the
|
|
40
|
-
* right full-bleed media element: `.m3u8` →
|
|
40
|
+
* right full-bleed media element: `.m3u8` → BackgroundVideo, `.mov|.mp4|.webm` →
|
|
41
41
|
* autoplay <video>, any other src → the Image molecule (its own broken-asset
|
|
42
42
|
* fallback), and no src → AssetPlaceholder. Positioning/fit arrive via
|
|
43
43
|
* `className`; Image gets an inline height so its base `h-auto` can't beat the
|
|
@@ -45,7 +45,7 @@ function useCoarsePointer() {
|
|
|
45
45
|
*/
|
|
46
46
|
function Media({ src, poster, className }) {
|
|
47
47
|
if (!src) return <AssetPlaceholder className={className} />
|
|
48
|
-
if (/\.m3u8$/i.test(src)) return <
|
|
48
|
+
if (/\.m3u8$/i.test(src)) return <BackgroundVideo src={src} poster={poster} className={className} />
|
|
49
49
|
if (/\.(mov|mp4|webm)$/i.test(src)) {
|
|
50
50
|
return (
|
|
51
51
|
<video
|
|
@@ -198,7 +198,7 @@ export default function TiltBento({
|
|
|
198
198
|
<div className={revealClass}>
|
|
199
199
|
<Button
|
|
200
200
|
href={href}
|
|
201
|
-
|
|
201
|
+
tone="primary"
|
|
202
202
|
size="sm"
|
|
203
203
|
{...(isExternal
|
|
204
204
|
? { target: '_blank', rel: 'noreferrer noopener' }
|
|
@@ -4,7 +4,7 @@ import KindPreview, { KIND_GLYPH } from '../molecules/KindPreview.jsx'
|
|
|
4
4
|
import FileIcon from '../atoms/FileIcon.jsx'
|
|
5
5
|
import RowMenuButton from '../molecules/RowMenuButton.jsx'
|
|
6
6
|
import OptionRow from '../molecules/OptionRow.jsx'
|
|
7
|
-
import { formatLength } from '../molecules/
|
|
7
|
+
import { formatLength } from '../molecules/PlayTile.jsx'
|
|
8
8
|
import { kindOf as dsKindOf, KIND_LABEL as DS_KIND_LABEL } from '../utilities/mediaKinds.js'
|
|
9
9
|
import useGrabEdge from '../hooks/useGrabEdge.js'
|
|
10
10
|
import useMediaQuery from '../hooks/useMediaQuery.js'
|
|
@@ -117,7 +117,7 @@ const ContentFilters = ({
|
|
|
117
117
|
searchKeys = ['label', 'name', 'title', 'type'],
|
|
118
118
|
/* CONTROLLED SEARCH (2026-09-30) — same seam as `filtersOpen`: a consumer that must know the
|
|
119
119
|
* query (media swaps the tree for a flat list while one is typed) or hand one in (⌘Enter from its
|
|
120
|
-
*
|
|
120
|
+
* search modal) owns it. Absent, the text stays private, exactly as before. */
|
|
121
121
|
searchValue,
|
|
122
122
|
onSearchChange,
|
|
123
123
|
headerActions,
|
|
@@ -79,7 +79,7 @@ function TermsEditor({ def, commit }) {
|
|
|
79
79
|
<div className="flex flex-col gap-2">
|
|
80
80
|
<div className="flex items-center justify-between">
|
|
81
81
|
<span className="kol-helper-10 text-meta">Vectors</span>
|
|
82
|
-
<Button
|
|
82
|
+
<Button tone="primary" size="sm" onClick={addTerm}>Add</Button>
|
|
83
83
|
</div>
|
|
84
84
|
<div className="grid grid-cols-[1fr_1fr_1fr_auto] gap-1.5 items-center min-w-0 [&>*]:min-w-0">
|
|
85
85
|
<span className="kol-helper-10 text-fg-48 text-center">amp</span>
|
|
@@ -103,7 +103,7 @@ function TermRow({ term, onSet, onRemove, canRemove }) {
|
|
|
103
103
|
{cell('amp', 1)}
|
|
104
104
|
{cell('freq', 1)}
|
|
105
105
|
{cell('phase', 0)}
|
|
106
|
-
<Button
|
|
106
|
+
<Button tone="ghost" size="sm" onClick={onRemove} disabled={!canRemove} aria-label="Remove vector">×</Button>
|
|
107
107
|
</>
|
|
108
108
|
)
|
|
109
109
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { isValidElement, useCallback, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import useEmblaCarousel from 'embla-carousel-react'
|
|
3
3
|
import Image from '../atoms/Image.jsx'
|
|
4
|
-
import
|
|
4
|
+
import BackgroundVideo from '../utilities/BackgroundVideo.jsx'
|
|
5
5
|
import EmblaNav from '../molecules/EmblaNav.jsx'
|
|
6
6
|
import OverlayGlassPanel from '../utilities/OverlayGlassPanel.jsx'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Per-slide media layer: a `{ src, kind }` descriptor becomes a cover-fit
|
|
10
|
-
* Image /
|
|
10
|
+
* Image / BackgroundVideo / inert <video> pinned over the frame
|
|
11
11
|
* (`.kol-featured-carousel-media` — absolute inset-0 object-cover, CSS in
|
|
12
12
|
* @kol/theme, out-cascades Image's baked `h-auto`). A ready ReactNode renders
|
|
13
13
|
* as-is and must position itself (give it that same class). `onEnded` /
|
|
@@ -36,7 +36,7 @@ function SlideMedia({ media, active = true, onEnded, onTimeUpdate }) {
|
|
|
36
36
|
|
|
37
37
|
if (kind === 'video') {
|
|
38
38
|
// Non-HLS file (mp4/webm): a plain inert video. HLS manifests and the
|
|
39
|
-
// poster-only case go through
|
|
39
|
+
// poster-only case go through BackgroundVideo, which owns hls.js attachment and
|
|
40
40
|
// the non-interactive hardening set. `loop` is dropped once `onEnded` is
|
|
41
41
|
// wired so the active slide plays once, then advances.
|
|
42
42
|
if (src && !src.endsWith('.m3u8')) {
|
|
@@ -57,7 +57,7 @@ function SlideMedia({ media, active = true, onEnded, onTimeUpdate }) {
|
|
|
57
57
|
)
|
|
58
58
|
}
|
|
59
59
|
return (
|
|
60
|
-
<
|
|
60
|
+
<BackgroundVideo
|
|
61
61
|
src={src}
|
|
62
62
|
poster={poster}
|
|
63
63
|
active={active}
|
|
@@ -105,7 +105,7 @@ export default function KeyframeEditor({
|
|
|
105
105
|
{kfs.map((kf, i) => (
|
|
106
106
|
<Button
|
|
107
107
|
key={i}
|
|
108
|
-
|
|
108
|
+
tone={i === sel ? 'primary' : 'inverted'}
|
|
109
109
|
size="sm"
|
|
110
110
|
className="w-full"
|
|
111
111
|
style={{ justifyContent: 'space-between' }}
|
|
@@ -117,8 +117,8 @@ export default function KeyframeEditor({
|
|
|
117
117
|
))}
|
|
118
118
|
</div>
|
|
119
119
|
<div className="flex gap-2">
|
|
120
|
-
<Button
|
|
121
|
-
<Tooltip label="Delete keyframe"><Button
|
|
120
|
+
<Button tone="primary" size="sm" className="flex-1" onClick={onAdd}>Add @ playhead</Button>
|
|
121
|
+
<Tooltip label="Delete keyframe"><Button tone="ghost" size="sm" onClick={onDelete} disabled={kfs.length <= 1}>Delete</Button></Tooltip>
|
|
122
122
|
</div>
|
|
123
123
|
|
|
124
124
|
<span className="kol-helper-10 text-meta">Pose</span>
|
|
@@ -425,7 +425,7 @@ export default function LayerStack({
|
|
|
425
425
|
<Tooltip label={`Group ${layerSelectionCount} selected layers`}>
|
|
426
426
|
<Button
|
|
427
427
|
iconComponent={IconC}
|
|
428
|
-
|
|
428
|
+
tone="primary"
|
|
429
429
|
size="sm"
|
|
430
430
|
iconLeft="layers"
|
|
431
431
|
onClick={() => onGroup(layerSelectedIds)}
|
|
@@ -476,7 +476,7 @@ export function AddLayerButton({
|
|
|
476
476
|
<Tooltip label="Add layer">
|
|
477
477
|
<Button
|
|
478
478
|
iconComponent={IconC}
|
|
479
|
-
|
|
479
|
+
tone="primary"
|
|
480
480
|
size="sm"
|
|
481
481
|
quiet
|
|
482
482
|
iconOnly="plus"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
|
2
2
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
3
|
-
import ActionButton from '../
|
|
3
|
+
import ActionButton from '../molecules/ActionButton.jsx'
|
|
4
4
|
import Button from '../atoms/Button.jsx'
|
|
5
5
|
import Divider from '../atoms/Divider.jsx'
|
|
6
6
|
import Input from '../atoms/Input.jsx'
|
|
@@ -8,7 +8,7 @@ import Divider from '../atoms/Divider.jsx'
|
|
|
8
8
|
import Input from '../atoms/Input.jsx'
|
|
9
9
|
import Tag from '../atoms/Tag.jsx'
|
|
10
10
|
import IconFrame from '../atoms/IconFrame.jsx'
|
|
11
|
-
import ActionButton from '../
|
|
11
|
+
import ActionButton from '../molecules/ActionButton.jsx'
|
|
12
12
|
import SizeOrDownload from '../atoms/SizeOrDownload.jsx'
|
|
13
13
|
import ToggleCheckbox from '../atoms/ToggleCheckbox.jsx'
|
|
14
14
|
import ViewToggle from '../atoms/ViewToggle.jsx'
|
|
@@ -29,10 +29,10 @@ import useMarquee from '../hooks/useMarquee.js'
|
|
|
29
29
|
import useLongPress from '../hooks/useLongPress.js'
|
|
30
30
|
import useMediaQuery from '../hooks/useMediaQuery.js'
|
|
31
31
|
import KindPreview from '../molecules/KindPreview.jsx'
|
|
32
|
-
import
|
|
33
|
-
import
|
|
32
|
+
import MediaPlayer from '../molecules/MediaPlayer.jsx'
|
|
33
|
+
import Slider from '../molecules/Slider.jsx'
|
|
34
34
|
import QuickLookFrame from '../molecules/QuickLookFrame.jsx'
|
|
35
|
-
import { formatLength } from '../molecules/
|
|
35
|
+
import { formatLength } from '../molecules/PlayTile.jsx'
|
|
36
36
|
import FullscreenOverlay from '../utilities/FullscreenOverlay.jsx'
|
|
37
37
|
import { useModal } from '../molecules/Modal.jsx'
|
|
38
38
|
import ContentFilters from './ContentFilters.jsx'
|
|
@@ -65,7 +65,7 @@ import { nearestRatio } from '../utilities/ratios.js'
|
|
|
65
65
|
* SortControls · the selection bar while selecting), the
|
|
66
66
|
* wall on ContentCard / ContentRow default, paging, the
|
|
67
67
|
* per-bucket list cache, the stats line, the inspector
|
|
68
|
-
* lightbox (image ·
|
|
68
|
+
* lightbox (image · MediaPlayer · DocPage via
|
|
69
69
|
* KindPreview)
|
|
70
70
|
*
|
|
71
71
|
* Both take the injected `client` (`{ listMedia, mediaUrl, proxied?, buckets? }`
|
|
@@ -686,9 +686,9 @@ export function MediaInspector({ files, index, onClose, onPrev, onNext, mediaUrl
|
|
|
686
686
|
onLoad={(e) => setDims({ key: o.key, w: e.target.naturalWidth, h: e.target.naturalHeight })} />
|
|
687
687
|
</QuickLookFrame>
|
|
688
688
|
) : isVideo(o.contentType) ? (
|
|
689
|
-
<
|
|
689
|
+
<MediaPlayer key={o.key} variant="video" src={mediaUrl(o.key)} poster={poster ? mediaUrl(poster) : undefined} onMeta={(m) => setDims({ key: o.key, ...m })} frame={frame} />
|
|
690
690
|
) : kind === 'audio' ? (
|
|
691
|
-
<
|
|
691
|
+
<MediaPlayer key={o.key} variant="audio" src={mediaUrl(o.key)} ext={extOf(o.key)} onMeta={(m) => setDims({ key: o.key, ...m })} frame={frame} />
|
|
692
692
|
) : (
|
|
693
693
|
<QuickLookFrame {...frame}>
|
|
694
694
|
<KindPreview o={o} fit="sheet" urlOf={(x) => mediaUrl(x.key)} poster={poster ? mediaUrl(poster) : undefined} />
|
|
@@ -849,7 +849,7 @@ function TrashPanel({ trash, onClose, run, confirm }) {
|
|
|
849
849
|
<SettingsPanel variant="drawer" title="Trash" onClose={onClose}
|
|
850
850
|
footer={items.length > 0 && trash.empty ? (
|
|
851
851
|
<SettingsFooter>
|
|
852
|
-
<Button
|
|
852
|
+
<Button tone="primary" size="sm" onClick={async () => {
|
|
853
853
|
if (confirm && !(await confirm(`Empty the trash? ${items.length} item${items.length === 1 ? '' : 's'} will be gone for good.`, { okLabel: 'Empty trash' }))) return
|
|
854
854
|
run(async () => { await trash.empty(); setSel(new Set()) })
|
|
855
855
|
}}>Empty trash</Button>
|
|
@@ -1069,9 +1069,9 @@ export function MediaLibraryBrowse({
|
|
|
1069
1069
|
* media-hub, "two too many"). Given, the gear calls it and this page opens no drawer of its own —
|
|
1070
1070
|
* inside a Hub the Hub's Settings is the one place. Absent, the gear opens the drawer as always. */
|
|
1071
1071
|
onOpenSettings,
|
|
1072
|
-
/* `searchSuggestions` — what the
|
|
1072
|
+
/* `searchSuggestions` — what the search modal lists before you type. Absent: favourites, smart
|
|
1073
1073
|
* folders, then the bucket's top-level folders. An array replaces them; a function receives them
|
|
1074
|
-
* and returns the list. Rows are the
|
|
1074
|
+
* and returns the list. Rows are the search modal's shape; a row with `run` calls it instead of
|
|
1075
1075
|
* jumping to `id` as a file key. */
|
|
1076
1076
|
searchSuggestions,
|
|
1077
1077
|
/* `bucketLevel` — keep title → bucket → folders with ONE bucket (kol-client-olina 2026-09-23).
|
|
@@ -1604,13 +1604,13 @@ export function MediaLibraryBrowse({
|
|
|
1604
1604
|
openQuickLook({ files, index })
|
|
1605
1605
|
}
|
|
1606
1606
|
|
|
1607
|
-
/* SEARCH IS THE DS'S OWN
|
|
1607
|
+
/* SEARCH IS THE DS'S OWN MODAL (2026-09-22). `/` is the file-manager habit and ⌘K the app one,
|
|
1608
1608
|
* so both open it; it searches the bucket's keys and JUMPS — it does not filter the list under
|
|
1609
1609
|
* you, which is what the filter bar is for. */
|
|
1610
1610
|
const [searchOpen, setSearchOpen] = useState(false)
|
|
1611
1611
|
const [searchQuery, setSearchQuery] = useState('')
|
|
1612
1612
|
/* the filter bar's own query — lifted so a typed one swaps the tree for a flat list, and so the
|
|
1613
|
-
*
|
|
1613
|
+
* search modal's ⌘Enter can hand its query over */
|
|
1614
1614
|
const [barQuery, setBarQuery] = useState('')
|
|
1615
1615
|
/* THE ARROWS IN THE GRID AND THE ROWS (user 2026-09-23: *"arrow navigation doesnt work in grid
|
|
1616
1616
|
* mode? nor in row mode"*). The columns have always had theirs (`ColumnBrowser`); these are the
|
|
@@ -2197,12 +2197,12 @@ export function MediaLibraryBrowse({
|
|
|
2197
2197
|
* do in grid view"*) — in steps of the control ramp, not pixels, so a row is always a control height */
|
|
2198
2198
|
const RAMP = ['xs', 'sm', 'md', 'lg']
|
|
2199
2199
|
const rowSlider = (
|
|
2200
|
-
<
|
|
2201
|
-
onChange={(
|
|
2200
|
+
<Slider readout="none" size={128} min={0} max={RAMP.length - 1} step={1} value={Math.max(0, RAMP.indexOf(rowSize))}
|
|
2201
|
+
onChange={(v) => setSettings({ ...settings, rowSize: RAMP[v] })} aria-label="Row size" />
|
|
2202
2202
|
)
|
|
2203
2203
|
const sizeSlider = (
|
|
2204
|
-
<
|
|
2205
|
-
onChange={(
|
|
2204
|
+
<Slider readout="none" size={128} min={100} max={360} step={20} value={tileSize}
|
|
2205
|
+
onChange={(v) => setSettings({ ...settings, tileSize: v })} aria-label="Tile size" />
|
|
2206
2206
|
)
|
|
2207
2207
|
/* `hits` = a query's results: no folders, no root level, each file named by its whole path —
|
|
2208
2208
|
* in the tree views as a list, the rows' shape; the wall keeps its tiles */
|
|
@@ -2256,7 +2256,7 @@ export function MediaLibraryBrowse({
|
|
|
2256
2256
|
</div>
|
|
2257
2257
|
)
|
|
2258
2258
|
|
|
2259
|
-
/* The
|
|
2259
|
+
/* The search modal's own ruling (2026-08-01) is that Enter COMMITS the query rather than navigating
|
|
2260
2260
|
* to whatever happened to be first — so `onExpand` is where a committed query lands, and here
|
|
2261
2261
|
* that means the top hit. Arrow-then-Enter and a click go through `onSelect`, same jump. */
|
|
2262
2262
|
const searchResults = rankMedia(objects, searchQuery, { limit: 50 }).map((o) => ({
|
|
@@ -2265,7 +2265,7 @@ export function MediaLibraryBrowse({
|
|
|
2265
2265
|
hint: o.key.slice(0, o.key.lastIndexOf('/') + 1) || 'the bucket root',
|
|
2266
2266
|
group: KIND_LABEL[kindOf(o)] || 'File',
|
|
2267
2267
|
}))
|
|
2268
|
-
/* THE
|
|
2268
|
+
/* THE SEARCH MODAL OPENS ON SOMEWHERE TO GO (user 2026-09-30: it opened blank in olina). */
|
|
2269
2269
|
const openFolder = (path) => { setAppRoot(false); goFolder(path) }
|
|
2270
2270
|
const leafOf = (path) => path.replace(/\/$/, '').split('/').pop()
|
|
2271
2271
|
const hintOf = (path) => dirOf(path) || 'the bucket root'
|
|
@@ -3034,8 +3034,8 @@ export function MediaLibraryLibrary({
|
|
|
3034
3034
|
if (editingKey === o.key) {
|
|
3035
3035
|
return (
|
|
3036
3036
|
<div className="flex gap-2">
|
|
3037
|
-
<Button
|
|
3038
|
-
<Button
|
|
3037
|
+
<Button tone="primary" size="sm" onClick={commitRename} disabled={renaming}>{renaming ? 'Saving…' : 'Save'}</Button>
|
|
3038
|
+
<Button tone="ghost" size="sm" onClick={cancelRename} disabled={renaming}>Cancel</Button>
|
|
3039
3039
|
</div>
|
|
3040
3040
|
)
|
|
3041
3041
|
}
|
|
@@ -355,7 +355,7 @@ export default function RecordManager({
|
|
|
355
355
|
)}
|
|
356
356
|
{saveState != null && <span className="kol-helper-10 text-meta truncate leading-normal">{saveState}</span>}
|
|
357
357
|
{onPublish && (
|
|
358
|
-
<Button
|
|
358
|
+
<Button tone="primary" size="sm" onClick={() => onPublish(record)}>
|
|
359
359
|
{publishLabel}
|
|
360
360
|
</Button>
|
|
361
361
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isValidElement } from 'react'
|
|
2
2
|
import { FULL_BLEED, bleedClass } from './sectionBleed.js'
|
|
3
3
|
import { surfaceClass } from '../utilities/sectionSurface.js'
|
|
4
|
-
import
|
|
4
|
+
import BackgroundVideo from '../utilities/BackgroundVideo.jsx'
|
|
5
5
|
import Image from '../atoms/Image.jsx'
|
|
6
6
|
import OverlayGlassPanel from '../utilities/OverlayGlassPanel.jsx'
|
|
7
7
|
import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
|
|
@@ -42,7 +42,7 @@ const SPLIT_HEIGHTS = {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Background layer: a media descriptor becomes a cover-fit Image /
|
|
45
|
+
* Background layer: a media descriptor becomes a cover-fit Image / BackgroundVideo /
|
|
46
46
|
* inert <video> (`.kol-full-bleed-hero-media` pins it absolute + object-cover
|
|
47
47
|
* — CSS in @kol/theme); a ready ReactNode renders as-is and must position
|
|
48
48
|
* itself (give it that same class).
|
|
@@ -69,7 +69,7 @@ function MediaLayer({ media }) {
|
|
|
69
69
|
/>
|
|
70
70
|
)
|
|
71
71
|
}
|
|
72
|
-
return <
|
|
72
|
+
return <BackgroundVideo src={src} poster={poster} className="kol-full-bleed-hero-media" />
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useId, useState } from 'react'
|
|
2
|
+
import { VARIANT_TONE } from '../utilities/tone.js'
|
|
2
3
|
import { bleedClass } from './sectionBleed.js'
|
|
3
4
|
import { surfaceClass } from '../utilities/sectionSurface.js'
|
|
4
5
|
import Input from '../atoms/Input.jsx'
|
|
@@ -185,7 +186,7 @@ export default function SectionNewsletter({
|
|
|
185
186
|
/>
|
|
186
187
|
<Button
|
|
187
188
|
type="submit"
|
|
188
|
-
|
|
189
|
+
{...(VARIANT_TONE[submitVariant] ? { tone: VARIANT_TONE[submitVariant] } : { variant: submitVariant })}
|
|
189
190
|
size={controlSize}
|
|
190
191
|
disabled={status === 'submitting'}
|
|
191
192
|
className="w-full sm:w-auto"
|
|
@@ -59,7 +59,7 @@ export function HighlightMatch({ label, query, ranges }) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* ShellSearchOverlay — The ⌘K search
|
|
62
|
+
* ShellSearchOverlay — The ⌘K search modal. the ⌘K search modal: fullscreen dim + centered
|
|
63
63
|
* panel, a bare SearchInput on top, result rows beneath (HighlightMatch
|
|
64
64
|
* label, dim hint line, right-aligned group label). Distinct from Modal
|
|
65
65
|
* (prompt/confirm only) — this is the search/command primitive.
|
|
@@ -78,7 +78,7 @@ export function HighlightMatch({ label, query, ranges }) {
|
|
|
78
78
|
* @param {Function} onClose () => void — backdrop click, Escape, post-select
|
|
79
79
|
* @param {Array} results pre-filtered rows: { id, label, group?, hint?, icon? }
|
|
80
80
|
* @param {Array} [suggestions] rows shown while the query is empty (same shape) —
|
|
81
|
-
* the
|
|
81
|
+
* the search modal opens on somewhere to go, not a blank box
|
|
82
82
|
* @param {string} query controlled query (drives the highlight slice)
|
|
83
83
|
* @param {Function} onQueryChange (string) => void — input change
|
|
84
84
|
* @param {Function} onSelect (item) => void — row click / Enter; consumer navigates
|
|
@@ -90,14 +90,17 @@ export function HighlightMatch({ label, query, ranges }) {
|
|
|
90
90
|
* @param {Function} [onOpenResults] (query) => void — ⌘/Ctrl+Enter: every hit, on the consumer's
|
|
91
91
|
* own results surface. Plain Enter keeps its meaning.
|
|
92
92
|
* @param {string} [resultsLabel] the footer's second line for ⌘Enter (e.g. `All results`)
|
|
93
|
+
* @param {string} [selectLabel] the footer's Enter line while a row is highlighted (default
|
|
94
|
+
* `Go to page`) — a modal whose rows are not pages says what
|
|
95
|
+
* Enter does instead (the rack's module search: `Add module`)
|
|
93
96
|
*/
|
|
94
97
|
export default function ShellSearchOverlay({
|
|
95
98
|
open,
|
|
96
99
|
onClose,
|
|
97
100
|
results: rawResults = [],
|
|
98
101
|
suggestions = [],
|
|
99
|
-
/* EXPANDED — the
|
|
100
|
-
* commits the query and opens `children` as the results body; the
|
|
102
|
+
/* EXPANDED — the search modal's second state (user ruling 2026-08-01). Enter
|
|
103
|
+
* commits the query and opens `children` as the results body; the search modal
|
|
101
104
|
* and the old tag overlay are one surface with two states, not two
|
|
102
105
|
* components. `chips` are the committed tag facets of the same query. */
|
|
103
106
|
expanded = false,
|
|
@@ -112,6 +115,7 @@ export default function ShellSearchOverlay({
|
|
|
112
115
|
enterLabel,
|
|
113
116
|
onOpenResults,
|
|
114
117
|
resultsLabel = 'All results',
|
|
118
|
+
selectLabel = 'Go to page',
|
|
115
119
|
}) {
|
|
116
120
|
const panelRef = useRef(null)
|
|
117
121
|
const listRef = useRef(null)
|
|
@@ -176,7 +180,7 @@ export default function ShellSearchOverlay({
|
|
|
176
180
|
if (active >= 0) select(results[active])
|
|
177
181
|
else onExpand?.()
|
|
178
182
|
} else if (e.key === 'Tab') {
|
|
179
|
-
/* Focus trap — the input is the
|
|
183
|
+
/* Focus trap — the input is the search modal's only tab stop. */
|
|
180
184
|
e.preventDefault()
|
|
181
185
|
}
|
|
182
186
|
}
|
|
@@ -214,7 +218,7 @@ export default function ShellSearchOverlay({
|
|
|
214
218
|
))}
|
|
215
219
|
</div>
|
|
216
220
|
)}
|
|
217
|
-
{/* A REAL FIELD, inset in the panel (2026-09-30, shadcn's
|
|
221
|
+
{/* A REAL FIELD, inset in the panel (2026-09-30, shadcn's search modal as
|
|
218
222
|
* the aim) — the flush `bare` strip read as a hole, not a control. */}
|
|
219
223
|
<div className="kol-tone-grey p-2">
|
|
220
224
|
<SearchInput
|
|
@@ -243,8 +247,8 @@ export default function ShellSearchOverlay({
|
|
|
243
247
|
* group section heading the row files under — 'Atoms', 'Documentation', 'Tags'
|
|
244
248
|
* icon optional leading glyph (kol-icons name)
|
|
245
249
|
* hint subtext shown when the LABEL was not what matched
|
|
246
|
-
* href a destination; dismisses the
|
|
247
|
-
* action a closure; runs and KEEPS the
|
|
250
|
+
* href a destination; dismisses the search modal
|
|
251
|
+
* action a closure; runs and KEEPS the search modal open (tag rows)
|
|
248
252
|
* Built by `buildShellSearchItems` (showcase/src/nav/shell-nav.js). */}
|
|
249
253
|
{expanded ? (
|
|
250
254
|
<div className="border-t border-fg-08 max-h-[70vh] overflow-y-auto">{children}</div>
|
|
@@ -289,7 +293,7 @@ export default function ShellSearchOverlay({
|
|
|
289
293
|
{(results.length > 0 || enterLabel) && (
|
|
290
294
|
<p className="flex items-center gap-2 kol-helper-12 text-fg-48 mx-2 px-4 border-x border-transparent">
|
|
291
295
|
<Kbd icon="corner-down-left" />
|
|
292
|
-
{results.length > 0 ?
|
|
296
|
+
{results.length > 0 ? selectLabel : enterLabel}
|
|
293
297
|
</p>
|
|
294
298
|
)}
|
|
295
299
|
{/* THE RESULTS LINE IS A LINK (user 2026-10-01: *"make 'all results...' at the bottom
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
|
-
import ColorSwatch from '../
|
|
2
|
+
import ColorSwatch from '../molecules/ColorSwatch.jsx'
|
|
3
3
|
import { resolveCssVar, resolveCssColor, isLight } from '../hooks/cssVar.js'
|
|
4
4
|
import { Tooltip } from '../utilities/Popover.jsx'
|
|
5
5
|
|
|
@@ -60,7 +60,7 @@ const ToolPalette = ({
|
|
|
60
60
|
return (
|
|
61
61
|
/* the DS tooltip carries the key as its own chip (the native-title sweep, 2026-09-26) */
|
|
62
62
|
<Tooltip key={it.id} label={it.label} shortcut={it.shortcut}>
|
|
63
|
-
<Button
|
|
63
|
+
<Button tone="ghost" size={size} quiet iconOnly={it.icon} iconComponent={iconComponent}
|
|
64
64
|
pressed={isTool ? activeId === it.id : undefined} disabled={it.disabled}
|
|
65
65
|
aria-label={it.label}
|
|
66
66
|
onClick={(e) => { (isTool ? onSelect : onAction)?.(it.id); blur(e) }} />
|
|
@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react'
|
|
|
2
2
|
import Hls from 'hls.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* BackgroundVideo — A background video with no controls. the background/decorative HLS-streaming video. A single native
|
|
6
6
|
* <video>: the effect attaches an .m3u8 stream via hls.js (Safari-native HLS
|
|
7
7
|
* when hls.js isn't supported) and destroys the instance on unmount/src
|
|
8
8
|
* change. Deliberately inert — pointer-events none plus the full hardening
|
|
@@ -11,6 +11,10 @@ import Hls from 'hls.js'
|
|
|
11
11
|
* these without explicit approval. All layout/sizing arrives via `className`
|
|
12
12
|
* (consumer decides `absolute inset-0 object-cover`, etc.).
|
|
13
13
|
*
|
|
14
|
+
* NAMED FOR WHAT IT IS FOR (user ruling 2026-10-01): it was `HlsVideo`, which says how it streams,
|
|
15
|
+
* and it sat in atoms. It has no UI — nothing to operate — so it is a utility; `HlsVideo` is the
|
|
16
|
+
* alias on the retirement ledger.
|
|
17
|
+
*
|
|
14
18
|
* @param {string} src HLS manifest URL (.m3u8); attach effect re-runs on change
|
|
15
19
|
* @param {string} poster native poster frame shown before playback
|
|
16
20
|
* @param {string} className all layout/sizing/positioning (consumer-supplied)
|
|
@@ -19,7 +23,7 @@ import Hls from 'hls.js'
|
|
|
19
23
|
* off-stage carousel slide pauses instead of autoplaying beside the
|
|
20
24
|
* active one (FeaturedCarouselFullWidth, 2026-08-26)
|
|
21
25
|
*/
|
|
22
|
-
export default function
|
|
26
|
+
export default function BackgroundVideo({ src, poster, className, onEnded, active = true, ...props }) {
|
|
23
27
|
const videoRef = useRef(null)
|
|
24
28
|
|
|
25
29
|
useEffect(() => {
|
|
@@ -60,7 +60,7 @@ export default class ErrorBoundary extends Component {
|
|
|
60
60
|
)}
|
|
61
61
|
|
|
62
62
|
<div className="flex gap-4 justify-center flex-wrap">
|
|
63
|
-
<Button
|
|
63
|
+
<Button tone="primary" onClick={this.handleReset}>Try again</Button>
|
|
64
64
|
<Button href={this.props.homeHref ?? '/'}>Go home</Button>
|
|
65
65
|
</div>
|
|
66
66
|
</div>
|
|
@@ -123,7 +123,9 @@ export function usePopover({
|
|
|
123
123
|
useClick(data.context, { enabled: click }),
|
|
124
124
|
useHover(data.context, { enabled: hover, delay: hoverDelay, move: false }),
|
|
125
125
|
useFocus(data.context, { enabled: focus }),
|
|
126
|
-
|
|
126
|
+
/* `dismiss` may be an object of floating-ui's own options — `{ outsidePress: false }` keeps Esc
|
|
127
|
+
* and drops the outside click (Dropdown `stayOpen`) */
|
|
128
|
+
useDismiss(data.context, typeof dismiss === 'object' && dismiss ? { enabled: true, ...dismiss } : { enabled: dismiss }),
|
|
127
129
|
useRole(data.context, { role }),
|
|
128
130
|
])
|
|
129
131
|
|
|
@@ -469,7 +469,10 @@ export default function QuadrantSync ({
|
|
|
469
469
|
<input
|
|
470
470
|
id="kq-fade" type="range" min={15} max={100} value={Math.round(fade * 100)}
|
|
471
471
|
onChange={(e) => setFade(Number(e.target.value) / 100)}
|
|
472
|
-
|
|
472
|
+
/* Slider's own track, not the browser's range (2026-10-02): a utility cannot import the
|
|
473
|
+
* Slider molecule, so the range wears its class and its `--kol-slider-track` seam */
|
|
474
|
+
className="slider-black w-full block cursor-pointer"
|
|
475
|
+
style={{ '--kol-slider-track': handleColor }}
|
|
473
476
|
/>
|
|
474
477
|
</div>
|
|
475
478
|
|