@kolkrabbi/kol-component 0.235.0 → 0.237.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 -4
- package/src/atoms/Button.jsx +10 -2
- package/src/atoms/Kbd.jsx +6 -3
- package/src/index.js +17 -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 +5 -2
- 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/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/TabChips.jsx +39 -0
- 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 +8 -8
- 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/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
|
@@ -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
|
|
@@ -96,8 +96,8 @@ export default function ShellSearchOverlay({
|
|
|
96
96
|
onClose,
|
|
97
97
|
results: rawResults = [],
|
|
98
98
|
suggestions = [],
|
|
99
|
-
/* EXPANDED — the
|
|
100
|
-
* commits the query and opens `children` as the results body; the
|
|
99
|
+
/* EXPANDED — the search modal's second state (user ruling 2026-08-01). Enter
|
|
100
|
+
* commits the query and opens `children` as the results body; the search modal
|
|
101
101
|
* and the old tag overlay are one surface with two states, not two
|
|
102
102
|
* components. `chips` are the committed tag facets of the same query. */
|
|
103
103
|
expanded = false,
|
|
@@ -176,7 +176,7 @@ export default function ShellSearchOverlay({
|
|
|
176
176
|
if (active >= 0) select(results[active])
|
|
177
177
|
else onExpand?.()
|
|
178
178
|
} else if (e.key === 'Tab') {
|
|
179
|
-
/* Focus trap — the input is the
|
|
179
|
+
/* Focus trap — the input is the search modal's only tab stop. */
|
|
180
180
|
e.preventDefault()
|
|
181
181
|
}
|
|
182
182
|
}
|
|
@@ -214,7 +214,7 @@ export default function ShellSearchOverlay({
|
|
|
214
214
|
))}
|
|
215
215
|
</div>
|
|
216
216
|
)}
|
|
217
|
-
{/* A REAL FIELD, inset in the panel (2026-09-30, shadcn's
|
|
217
|
+
{/* A REAL FIELD, inset in the panel (2026-09-30, shadcn's search modal as
|
|
218
218
|
* the aim) — the flush `bare` strip read as a hole, not a control. */}
|
|
219
219
|
<div className="kol-tone-grey p-2">
|
|
220
220
|
<SearchInput
|
|
@@ -243,8 +243,8 @@ export default function ShellSearchOverlay({
|
|
|
243
243
|
* group section heading the row files under — 'Atoms', 'Documentation', 'Tags'
|
|
244
244
|
* icon optional leading glyph (kol-icons name)
|
|
245
245
|
* hint subtext shown when the LABEL was not what matched
|
|
246
|
-
* href a destination; dismisses the
|
|
247
|
-
* action a closure; runs and KEEPS the
|
|
246
|
+
* href a destination; dismisses the search modal
|
|
247
|
+
* action a closure; runs and KEEPS the search modal open (tag rows)
|
|
248
248
|
* Built by `buildShellSearchItems` (showcase/src/nav/shell-nav.js). */}
|
|
249
249
|
{expanded ? (
|
|
250
250
|
<div className="border-t border-fg-08 max-h-[70vh] overflow-y-auto">{children}</div>
|
|
@@ -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>
|
|
@@ -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
|
|
|
@@ -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
|
-
}
|