@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.237.0",
|
|
4
4
|
"description": "The core component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"embla-carousel-react": "^8.6.0",
|
|
26
26
|
"pdfjs-dist": "^6.3.289",
|
|
27
27
|
"react-syntax-highlighter": "^16.1.1",
|
|
28
|
-
"@kolkrabbi/kol-
|
|
29
|
-
"@kolkrabbi/kol-
|
|
28
|
+
"@kolkrabbi/kol-search": "^0.3.0",
|
|
29
|
+
"@kolkrabbi/kol-markdown": "^0.1.2"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@kolkrabbi/kol-icons": ">=0.22.0",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
40
|
+
"@kolkrabbi/kol-icons": "^0.33.0"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"src",
|
package/src/atoms/Button.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isValidElement } from 'react'
|
|
2
|
-
import { toneClass } from '../utilities/tone.js'
|
|
2
|
+
import { toneClass, VARIANT_TONE } from '../utilities/tone.js'
|
|
3
3
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
4
4
|
import { glyphSize } from '../hooks/glyphLadders.js'
|
|
5
5
|
|
|
@@ -12,7 +12,7 @@ import { glyphSize } from '../hooks/glyphLadders.js'
|
|
|
12
12
|
*
|
|
13
13
|
* @param {Object} props
|
|
14
14
|
* @param {ReactNode} props.children - Button content
|
|
15
|
-
* @param {'
|
|
15
|
+
* @param {'accent'|'danger'|'nav'} props.variant - The intent; the GROUND is `tone`. `primary · secondary · outline · ghost · grey · control` are deprecated aliases of tones (2026-10-01, on the retirement ledger): pass `tone` — `secondary` is `tone="inverted"`, `control` is `tone="ghost"`, the rest keep their name. Visual variant. UNSET (the default) inherits the tone of the nearest `kol-tone-*` wrapper, else renders primary; the five ground variants are aliases of `tone`. `danger` is the destructive treatment (--ui-error fill); `nav` is the chrome rung — transparent, oq-64 ink, one step brighter than `ghost`; `control` is an alias for `ghost` (legacy call-sites).
|
|
16
16
|
* @param {'xs'|'sm'|'md'|'lg'} props.size - Button size (default: 'md'); xs is the panel rung (ControlsXsRung, 2026-09-01) — kol-mono-8 in a 22px shell (20 icon-only), opt-in by prop
|
|
17
17
|
* @param {string} props.iconLeft - Icon name to display on the left
|
|
18
18
|
* @param {string} props.iconRight - Icon name to display on the right
|
|
@@ -36,6 +36,8 @@ import { glyphSize } from '../hooks/glyphLadders.js'
|
|
|
36
36
|
* @param {ElementType|ReactNode} props.iconComponent - Icon renderer seam — a component that receives `{ name, size, className, style }` in place of the DS Icon (custom icon registries plug in here), or a pre-rendered node dropped in verbatim where the glyph would go. Defaults to DS Icon.
|
|
37
37
|
* @param {boolean} props.pressed - Toggle state — sets `aria-pressed` (true/false) and `kol-btn-pressed`; a `quiet` button drops its dimming while pressed. Leave undefined for non-toggle buttons.
|
|
38
38
|
*/
|
|
39
|
+
const warned = new Set()
|
|
40
|
+
|
|
39
41
|
const Button = ({
|
|
40
42
|
children,
|
|
41
43
|
variant,
|
|
@@ -85,6 +87,12 @@ const Button = ({
|
|
|
85
87
|
* `kol-btn-secondary` (the text color as fill) until 2026-09-29, so a misspelt variant
|
|
86
88
|
* shipped inverted. It now stamps nothing, like an unset variant: the wrapper's tone,
|
|
87
89
|
* else primary — and says so in dev. */
|
|
90
|
+
/* the six tone-duplicates still render exactly as they did — an alias gains nothing and loses
|
|
91
|
+
* nothing — and say once in dev what replaces them */
|
|
92
|
+
if (import.meta.env.DEV && VARIANT_TONE[variant] && !warned.has(variant)) {
|
|
93
|
+
warned.add(variant)
|
|
94
|
+
console.warn(`Button: variant="${variant}" is deprecated — use tone="${VARIANT_TONE[variant]}"`)
|
|
95
|
+
}
|
|
88
96
|
const KNOWN = ['primary', 'secondary', 'accent', 'outline', 'ghost', 'nav', 'danger', 'grey']
|
|
89
97
|
const known = !resolvedVariant || KNOWN.includes(resolvedVariant)
|
|
90
98
|
if (!known && import.meta.env.DEV) console.warn(`Button: unknown variant "${resolvedVariant}" — rendering the inherited tone (primary)`)
|
package/src/atoms/Kbd.jsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Kbd — A key cap for a shortcut. a key cap: one key or chord (↵, ⌘K, Esc) shown as an affordance.
|
|
3
3
|
* Replaces the two hand-rolled <kbd> chips (SearchInput's shortcut hint and
|
|
4
|
-
* the
|
|
4
|
+
* the search modal footer) that had drifted apart (2026-09-30). A plate, so it
|
|
5
5
|
* takes oq — never fg (icons-use-oq law).
|
|
6
6
|
*
|
|
7
7
|
* aria-hidden by default: a cap is a hint beside a label, not the label.
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* md = beside helper-12 text (20px)
|
|
11
11
|
* @param {string} icon kol-icons name drawn before the children — a key
|
|
12
12
|
* with a symbol (↵ `corner-down-left`, ⌘ `command`)
|
|
13
|
-
* is a GLYPH, never a typed character in the mono face
|
|
13
|
+
* is a GLYPH, never a typed character in the mono face.
|
|
14
|
+
* It follows the cap: 10 in `sm`, 12 in `md` (2026-10-02 —
|
|
15
|
+
* it was 12 in both, so in the small ⌘K cap the ⌘ outgrew the K)
|
|
14
16
|
*/
|
|
15
17
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
16
18
|
|
|
@@ -18,6 +20,7 @@ const SIZE = {
|
|
|
18
20
|
sm: 'h-4 min-w-4 px-1 kol-helper-10',
|
|
19
21
|
md: 'h-5 min-w-5 px-1.5 kol-helper-12',
|
|
20
22
|
}
|
|
23
|
+
const GLYPH = { sm: 10, md: 12 }
|
|
21
24
|
|
|
22
25
|
export default function Kbd({ size = 'md', icon, className = '', children, ...rest }) {
|
|
23
26
|
return (
|
|
@@ -26,7 +29,7 @@ export default function Kbd({ size = 'md', icon, className = '', children, ...re
|
|
|
26
29
|
className={`inline-flex items-center justify-center gap-0.5 shrink-0 rounded-[var(--kol-radius-xs)] bg-oq-08 text-oq-64 ${SIZE[size] ?? SIZE.md} ${className}`.trim()}
|
|
27
30
|
{...rest}
|
|
28
31
|
>
|
|
29
|
-
{icon && <Icon name={icon} size={
|
|
32
|
+
{icon && <Icon name={icon} size={GLYPH[size] ?? GLYPH.md} />}
|
|
30
33
|
{children}
|
|
31
34
|
</kbd>
|
|
32
35
|
)
|
package/src/index.js
CHANGED
|
@@ -23,7 +23,7 @@ export { default as Avatar } from './atoms/Avatar.jsx'
|
|
|
23
23
|
export { default as Badge } from './atoms/Badge.jsx'
|
|
24
24
|
export { default as Button } from './atoms/Button.jsx'
|
|
25
25
|
export { default as CloseButton } from './atoms/CloseButton.jsx'
|
|
26
|
-
export { default as ActionButton } from './
|
|
26
|
+
export { default as ActionButton } from './molecules/ActionButton.jsx'
|
|
27
27
|
export { default as FileIcon } from './atoms/FileIcon.jsx'
|
|
28
28
|
export { default as SizeOrDownload } from './atoms/SizeOrDownload.jsx'
|
|
29
29
|
export { default as SortHeader } from './atoms/SortHeader.jsx'
|
|
@@ -38,21 +38,22 @@ export { default as IconFrame } from './atoms/IconFrame.jsx'
|
|
|
38
38
|
export { default as ExitPreview } from './utilities/ExitPreview.jsx'
|
|
39
39
|
export { default as Figure } from './atoms/Figure.jsx'
|
|
40
40
|
export { default as FullscreenOverlay } from './utilities/FullscreenOverlay.jsx'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* either one. */
|
|
45
|
-
export { default as AudioPlayer } from './atoms/AudioPlayer.jsx'
|
|
41
|
+
/* BackgroundVideo — renamed from HlsVideo and moved to utilities 2026-10-01 (user ruling: no UI,
|
|
42
|
+
* and the name said how it streams, not what it is for). `HlsVideo` is the alias, on the ledger. */
|
|
43
|
+
export { default as BackgroundVideo, default as HlsVideo } from './utilities/BackgroundVideo.jsx'
|
|
46
44
|
export { default as Input } from './atoms/Input.jsx'
|
|
47
45
|
export { default as Label } from './atoms/Label.jsx'
|
|
48
46
|
export { default as LabeledControl } from './molecules/LabeledControl.jsx'
|
|
49
47
|
export { default as OverlayGlassPanel } from './utilities/OverlayGlassPanel.jsx'
|
|
50
48
|
export { default as Pill } from './atoms/Pill.jsx'
|
|
51
49
|
export { usePopover, PopoverPanel, Tooltip } from './utilities/Popover.jsx'
|
|
50
|
+
/* armLongPress — the touch hold the panel value controls report (moved from kol-hardware 2026-10-01) */
|
|
51
|
+
export { armLongPress } from './utilities/armLongPress.js'
|
|
52
52
|
export { default as ContextMenu, useContextMenu } from './molecules/ContextMenu.jsx'
|
|
53
53
|
export { default as ProsePreview } from './utilities/ProsePreview.jsx'
|
|
54
54
|
export { default as QuantityInput } from './molecules/QuantityInput.jsx'
|
|
55
|
-
|
|
55
|
+
/* Knob — the survivor's name (2026-10-02): `variant` dial · panel. `RotaryDial` is the alias, on the ledger. */
|
|
56
|
+
export { default as Knob, default as RotaryDial } from './molecules/Knob.jsx'
|
|
56
57
|
export { default as SearchInput } from './molecules/SearchInput.jsx'
|
|
57
58
|
export { default as InspectorSection, default as Section } from './molecules/InspectorSection.jsx'
|
|
58
59
|
export { default as SectionText } from './molecules/SectionText.jsx'
|
|
@@ -96,8 +97,10 @@ export { default as SectionCardItem, default as CardFeatureItem } from './molecu
|
|
|
96
97
|
export { default as CodeBlock } from './molecules/CodeBlock.jsx'
|
|
97
98
|
export { default as ColorInputRow } from './molecules/ColorInputRow.jsx'
|
|
98
99
|
export { default as ColorRamp } from './molecules/ColorRamp.jsx'
|
|
99
|
-
export { default as ColorSwatch } from './
|
|
100
|
+
export { default as ColorSwatch } from './molecules/ColorSwatch.jsx'
|
|
100
101
|
export { default as Dropdown } from './molecules/Dropdown.jsx'
|
|
102
|
+
/* MultiSelect — several pick-one settings in one dropdown, a column each (2026-10-01, first cut) */
|
|
103
|
+
export { default as MultiSelect } from './molecules/MultiSelect.jsx'
|
|
101
104
|
export { default as FieldRow, StatusChip } from './molecules/FieldRow.jsx'
|
|
102
105
|
export { default as FramedMediaBand } from './organisms/FramedMediaBand.jsx'
|
|
103
106
|
export { default as Image } from './atoms/Image.jsx'
|
|
@@ -128,6 +131,8 @@ export { default as ToolPalette } from './organisms/ToolPalette.jsx'
|
|
|
128
131
|
export { default as DocumentEditor } from './organisms/DocumentEditor.jsx'
|
|
129
132
|
export { default as SwatchControls, SwatchStack, EyedropPick } from './molecules/SwatchControls.jsx'
|
|
130
133
|
export { default as TabsRow } from './molecules/TabsRow.jsx'
|
|
134
|
+
/* TabChips — tabs as chips, for inside a toolbar (2026-10-01; was the showcase's local DocTabs) */
|
|
135
|
+
export { default as TabChips } from './molecules/TabChips.jsx'
|
|
131
136
|
|
|
132
137
|
// organisms
|
|
133
138
|
/* monorepo sets (P6–P10) — organism members. Foundry members live in the
|
|
@@ -183,10 +188,11 @@ export { default as SectionNewsletter } from './organisms/SectionNewsletter.jsx'
|
|
|
183
188
|
export { default as NewsletterBand } from './organisms/NewsletterBand.jsx'
|
|
184
189
|
export { default as ColumnBrowser } from './organisms/ColumnBrowser.jsx'
|
|
185
190
|
export { default as KindPreview } from './molecules/KindPreview.jsx'
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
/* MediaPlayer — the one player (2026-10-02): `variant` video · audio, `frame` for the Quick Look
|
|
192
|
+
* window. VideoSheet · AudioSheet · AudioPreview · AudioPlayer are its aliases, on the ledger. */
|
|
193
|
+
export { default as MediaPlayer, VideoSheet, AudioSheet, AudioPreview, AudioPlayer } from './molecules/MediaPlayer.jsx'
|
|
194
|
+
export { AudioTile, VideoTile, formatLength } from './molecules/PlayTile.jsx'
|
|
188
195
|
export { default as PlaybackBar } from './molecules/PlaybackBar.jsx'
|
|
189
|
-
export { default as AudioSheet } from './molecules/AudioSheet.jsx'
|
|
190
196
|
export { default as QuickLookFrame } from './molecules/QuickLookFrame.jsx'
|
|
191
197
|
export { default as MediaTile } from './molecules/MediaTile.jsx'
|
|
192
198
|
export { default as RowMenuButton } from './molecules/RowMenuButton.jsx'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
3
|
-
import ColorSwatch from '
|
|
3
|
+
import ColorSwatch from './ColorSwatch.jsx'
|
|
4
4
|
import Input from '../atoms/Input'
|
|
5
5
|
import { usePopover, PopoverPanel, Tooltip } from '../utilities/Popover'
|
|
6
6
|
|
|
@@ -163,7 +163,9 @@ const Dropdown = ({
|
|
|
163
163
|
/* `{ divider: true }` in `options` draws a hairline between groups (FoundationsTones, 2026-09-26 —
|
|
164
164
|
* user: none on top, the filled tones in order, outline and ghost apart). It is never a choice:
|
|
165
165
|
* the lookup, the fallback and the trigger's width all read the choices only. */
|
|
166
|
-
|
|
166
|
+
/* `{ heading: 'Variant' }` names the list (2026-10-01 — user: "first item 'variant' then divider
|
|
167
|
+
* then the list"): a row that says what the dropdown sets, never a choice. */
|
|
168
|
+
const choices = options.filter((opt) => !opt?.divider && !opt?.heading)
|
|
167
169
|
const currentOption = choices.find((opt) => opt.value === value) || choices[0]
|
|
168
170
|
|
|
169
171
|
/* A clamped list (Popover caps the panel to the viewport) can open with the
|
|
@@ -172,7 +174,7 @@ const Dropdown = ({
|
|
|
172
174
|
const listRef = useRef(null)
|
|
173
175
|
useEffect(() => {
|
|
174
176
|
if (!isOpen) return
|
|
175
|
-
const idx = options.findIndex((opt) => !opt?.divider && opt.value === currentOption?.value)
|
|
177
|
+
const idx = options.findIndex((opt) => !opt?.divider && !opt?.heading && opt.value === currentOption?.value)
|
|
176
178
|
listRef.current?.children[idx]?.scrollIntoView({ block: 'nearest' })
|
|
177
179
|
}, [isOpen]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
178
180
|
|
|
@@ -253,6 +255,7 @@ const Dropdown = ({
|
|
|
253
255
|
<div ref={listRef} className="kol-dd-list" role="listbox">
|
|
254
256
|
{options.map((option, i) => {
|
|
255
257
|
if (option?.divider) return <MenuDropdownDivider key={`divider-${i}`} />
|
|
258
|
+
if (option?.heading) return <div key={`heading-${i}`} className={`${resolvedSize === 'xs' ? 'kol-mono-8 px-2 h-5' : 'kol-helper-12 px-3 h-8'} flex shrink-0 items-center text-meta`}>{option.heading}</div>
|
|
256
259
|
const isActive = option.value === currentOption?.value
|
|
257
260
|
return (
|
|
258
261
|
<MenuDropdownItem
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
|
-
import { AudioTile, VideoTile } from './
|
|
3
|
-
import
|
|
2
|
+
import { AudioTile, VideoTile } from './PlayTile.jsx'
|
|
3
|
+
import BackgroundVideo from '../utilities/BackgroundVideo.jsx'
|
|
4
4
|
import CodeBlock from './CodeBlock.jsx'
|
|
5
5
|
import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
|
|
6
6
|
import FileIcon from '../atoms/FileIcon.jsx'
|
|
@@ -16,11 +16,11 @@ import DocPage from './DocPage.jsx'
|
|
|
16
16
|
* KindPreview — A preview for any kind of file. a preview for any kind of file: kol-r2b2's `KindPreview.jsx`,
|
|
17
17
|
* promoted 2026-08-27 (SettingsPanelChromeAndColumnPreview). Before it, anything
|
|
18
18
|
* that was not an image or a video showed a grey box with the word "text".
|
|
19
|
-
* HLS → `
|
|
19
|
+
* HLS → `BackgroundVideo` (inert — the DS's background-video atom, preview only),
|
|
20
20
|
* video → `VideoTile` and audio → `AudioTile` (ColumnBrowserMediaFacts
|
|
21
21
|
* 2026-08-27 — the square column tile with one play/pause control; no Figure, no
|
|
22
22
|
* border — `VideoBlock`'s Figure border is why kol-r2b2 bypassed it; the overlay
|
|
23
|
-
* player is `
|
|
23
|
+
* player is `MediaPlayer`), markdown → rendered prose in `.kol-prose`
|
|
24
24
|
* (markdownToHtml, KindPreviewMarkdown 2026-08-27), json / yaml / text / code →
|
|
25
25
|
* `CodeBlock` (language by kind or extension) — every document on ONE plate,
|
|
26
26
|
* `DocPage` (DocPageAndKindShowcase 2026-08-27: the overlay's A-series page, the
|
|
@@ -171,10 +171,10 @@ export default function KindPreview({
|
|
|
171
171
|
|
|
172
172
|
if (kind === 'playlist') {
|
|
173
173
|
/* the caption is pane chrome — in a tile it is a line of prose over the picture */
|
|
174
|
-
if (fit !== 'pane') return <
|
|
174
|
+
if (fit !== 'pane') return <BackgroundVideo src={url} poster={poster} className="w-full h-full object-cover" />
|
|
175
175
|
return (
|
|
176
176
|
<div className="flex flex-col items-center gap-2">
|
|
177
|
-
<
|
|
177
|
+
<BackgroundVideo src={url} poster={poster} className="max-w-full max-h-[70vh] rounded" />
|
|
178
178
|
<span className="kol-mono-12 text-meta">HLS stream · playback is preview-only, no controls</span>
|
|
179
179
|
</div>
|
|
180
180
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import KnobPanel from './KnobPanel.jsx'
|
|
2
3
|
|
|
3
4
|
/** Clamp to min–max, snapped to the nearest step from min. */
|
|
4
5
|
const snapTo = (v, min, max, step) => {
|
|
@@ -7,12 +8,17 @@ const snapTo = (v, min, max, step) => {
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* Knob — drag-to-set rotary knob numeric input. The ROTARY TWIN of
|
|
11
12
|
* Slider (molecules/Slider.jsx): both implement the shared value-control
|
|
12
13
|
* contract — `value` / `min` / `max` / `step` / `onChange(next: number)` /
|
|
13
14
|
* `label` / `size` / `disabled` / `formatValue`. Controlled; onChange always
|
|
14
15
|
* fires with the plain number.
|
|
15
16
|
*
|
|
17
|
+
* ONE KNOB, TWO VARIANTS (user ruling 2026-10-02: *"knob being the more simple form, rotary dial
|
|
18
|
+
* being more of a variant"*). It was `RotaryDial` with kol-hardware's `Knob` wrapped over it; the
|
|
19
|
+
* name is `Knob` now, `dial` is the ring-and-disc drawing that name used to mean, and `RotaryDial`
|
|
20
|
+
* is the alias on the retirement ledger.
|
|
21
|
+
*
|
|
16
22
|
* Vertical pointer drag rotates the whole dial over a 270° sweep (−135° to
|
|
17
23
|
* +135°) mapped onto min–max; a full sweep is ~200px of drag, movement
|
|
18
24
|
* measured absolute from drag start. A local visual buffer keeps rotation
|
|
@@ -30,9 +36,12 @@ const snapTo = (v, min, max, step) => {
|
|
|
30
36
|
* @param {number} size dial px size (default 80); derives ring + disc radii
|
|
31
37
|
* @param {boolean} disabled blocks drag + keyboard and dims the control (default false)
|
|
32
38
|
* @param {Function} formatValue optional readout formatter (value: number) => string; default `${value}%`
|
|
39
|
+
* @param {'dial'|'panel'} variant `dial` (default) = the ring and disc · `panel` = the rack knob (kol-hardware's, merged 2026-10-01): a cap in the hardware colors;
|
|
40
|
+
* there `size` is `sm · md · lg · xl` (24 · 32 · 40 · 64) and it also takes `labelPlacement`
|
|
41
|
+
* (`column · row · row-left · row-right`), `bipolar`, `labelMinWidth` and `onHold`
|
|
33
42
|
* @param {number} defaultValue alt-click the dial resets to this (falls back to `min`) — Slider carries the same gesture
|
|
34
43
|
*/
|
|
35
|
-
export default function
|
|
44
|
+
export default function Knob({
|
|
36
45
|
label,
|
|
37
46
|
value = 0,
|
|
38
47
|
onChange,
|
|
@@ -43,6 +52,11 @@ export default function RotaryDial({
|
|
|
43
52
|
disabled = false,
|
|
44
53
|
formatValue,
|
|
45
54
|
defaultValue,
|
|
55
|
+
variant = 'dial',
|
|
56
|
+
labelPlacement,
|
|
57
|
+
bipolar,
|
|
58
|
+
labelMinWidth,
|
|
59
|
+
onHold,
|
|
46
60
|
}) {
|
|
47
61
|
const [isDragging, setIsDragging] = useState(false)
|
|
48
62
|
const [localValue, setLocalValue] = useState(value) // visual buffer — updates every move
|
|
@@ -119,6 +133,10 @@ export default function RotaryDial({
|
|
|
119
133
|
handlePointerDown(e)
|
|
120
134
|
}
|
|
121
135
|
|
|
136
|
+
if (variant === 'panel') {
|
|
137
|
+
return <KnobPanel value={value} onChange={onChange} min={bipolar && min === 0 ? undefined : min} max={max} label={label} variant={labelPlacement} bipolar={bipolar} labelMinWidth={labelMinWidth} size={typeof size === 'string' ? size : undefined} defaultValue={defaultValue} onHold={onHold} />
|
|
138
|
+
}
|
|
139
|
+
|
|
122
140
|
const outerRadius = size / 2
|
|
123
141
|
const innerRadius = (size * 0.7) / 2
|
|
124
142
|
const strokeWidth = 2
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
import { armLongPress } from '../utilities/armLongPress.js'
|
|
3
|
+
|
|
4
|
+
const NO_HOLD = { move() {}, cancel() {} }
|
|
5
|
+
|
|
6
|
+
const SIZES = { sm: 24, md: 32, lg: 40, xl: 64 }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* KnobPanel — Knob's `variant="panel"`: the rack knob. A cap and a pointer line in the
|
|
10
|
+
* hardware cap colors, sized 24 · 32 · 40 · 64 by name, with the four label placements and the
|
|
11
|
+
* bipolar legend. It was kol-hardware's own `Knob` (kol-monitor's rack, lifted 2026-09-01) and is
|
|
12
|
+
* carried here class for class — merged in 2026-10-01 (user ruling). Not exported:
|
|
13
|
+
* reach it through `<Knob variant="panel">`.
|
|
14
|
+
*/
|
|
15
|
+
export default function KnobPanel({ value, onChange, min, max, label, variant = 'column', bipolar = false, labelMinWidth, size: sizeProp = 'sm', defaultValue, onHold }) {
|
|
16
|
+
const size = SIZES[sizeProp] || SIZES.sm
|
|
17
|
+
const effMin = min ?? (bipolar ? -100 : 0)
|
|
18
|
+
const effMax = max ?? 100
|
|
19
|
+
const effDefault = defaultValue ?? (bipolar ? 0 : 50)
|
|
20
|
+
const angle = ((value - effMin) / (effMax - effMin)) * 270 - 135
|
|
21
|
+
const r = size / 2
|
|
22
|
+
const ir = r * 0.56
|
|
23
|
+
|
|
24
|
+
const handlePointerDown = useCallback((e) => {
|
|
25
|
+
e.preventDefault()
|
|
26
|
+
// Alt/Option + click resets to default instead of starting a drag
|
|
27
|
+
if (e.altKey) {
|
|
28
|
+
onChange(effDefault)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
const startY = e.clientY
|
|
32
|
+
const startVal = value
|
|
33
|
+
const range = effMax - effMin
|
|
34
|
+
|
|
35
|
+
const id = e.pointerId
|
|
36
|
+
const handleMove = (e) => {
|
|
37
|
+
if (e.pointerId !== id) return // a second finger is a pan, not this knob
|
|
38
|
+
hold.move(e)
|
|
39
|
+
const delta = (startY - e.clientY) * (range / 200)
|
|
40
|
+
const next = Math.round(Math.max(effMin, Math.min(effMax, startVal + delta)))
|
|
41
|
+
onChange(next)
|
|
42
|
+
}
|
|
43
|
+
const handleUp = () => {
|
|
44
|
+
hold.cancel()
|
|
45
|
+
window.removeEventListener('pointermove', handleMove)
|
|
46
|
+
window.removeEventListener('pointerup', handleUp)
|
|
47
|
+
}
|
|
48
|
+
// Touch: hold still 500ms → the holder's sheet instead of the drag
|
|
49
|
+
const hold = onHold ? armLongPress(e, () => { handleUp(); onHold({ label, value, min: effMin, max: effMax, defaultValue: effDefault }) }) : NO_HOLD
|
|
50
|
+
window.addEventListener('pointermove', handleMove)
|
|
51
|
+
window.addEventListener('pointerup', handleUp)
|
|
52
|
+
}, [value, effMin, effMax, effDefault, onChange, onHold, label])
|
|
53
|
+
|
|
54
|
+
const knobSvg = (
|
|
55
|
+
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
|
56
|
+
<circle cx={r} cy={r} r={r * 0.75} style={{ fill: 'var(--kol-ctl-hw-cap)', stroke: 'var(--kol-fg-24)' }} strokeWidth="1" />
|
|
57
|
+
<line
|
|
58
|
+
x1={r} y1={r}
|
|
59
|
+
x2={r + ir * Math.cos((angle - 90) * Math.PI / 180)}
|
|
60
|
+
y2={r + ir * Math.sin((angle - 90) * Math.PI / 180)}
|
|
61
|
+
style={{ stroke: 'var(--kol-ctl-hw-on-cap)' }} strokeWidth="1.5" strokeLinecap="round"
|
|
62
|
+
/>
|
|
63
|
+
</svg>
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if (variant === 'row-left' || variant === 'row') {
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
onPointerDown={handlePointerDown}
|
|
70
|
+
style={{ cursor: 'ns-resize', touchAction: 'none', display: 'flex', alignItems: 'center', gap: 4 }}
|
|
71
|
+
>
|
|
72
|
+
{label && (
|
|
73
|
+
<span className="kol-helper-8 text-fg-32" style={{ textTransform: 'uppercase', lineHeight: 1 }}>
|
|
74
|
+
{label}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
{knobSvg}
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (variant === 'row-right') {
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
onPointerDown={handlePointerDown}
|
|
86
|
+
style={{ cursor: 'ns-resize', touchAction: 'none', display: 'flex', alignItems: 'center', gap: 4 }}
|
|
87
|
+
>
|
|
88
|
+
{knobSvg}
|
|
89
|
+
{label && (
|
|
90
|
+
<span className="kol-helper-8 text-fg-32" style={{ textTransform: 'uppercase', lineHeight: 1, minWidth: labelMinWidth }}>
|
|
91
|
+
{label}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
onPointerDown={handlePointerDown}
|
|
101
|
+
style={{ cursor: 'ns-resize', touchAction: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}
|
|
102
|
+
>
|
|
103
|
+
{bipolar && (
|
|
104
|
+
<span className="kol-helper-8 text-fg-32" style={{ lineHeight: 1 }}>
|
|
105
|
+
-/+
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
{knobSvg}
|
|
109
|
+
{label && (
|
|
110
|
+
<span className="kol-helper-8 text-fg-32" style={{ textTransform: 'uppercase', lineHeight: 1 }}>
|
|
111
|
+
{label}
|
|
112
|
+
</span>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
* kol-monitor 2026-09-01 — in a narrow multi-column grid a fixed
|
|
24
24
|
* 160 label left the control 4px, and a nowrap combo painted into
|
|
25
25
|
* the neighbour column; the 48px column gap had been hiding it).
|
|
26
|
+
* variant — `'panel'` is the hardware panel label (was kol-hardware `PanelLabel`, merged
|
|
27
|
+
* 2026-10-01): `kol-helper-8`, uppercase, hugging the control; it takes
|
|
28
|
+
* `labelPosition` (`top · bottom · left · right`, default bottom), `labelClass` and `gap`.
|
|
26
29
|
* children — the control body.
|
|
27
30
|
* className — additional classes on the wrapper.
|
|
28
31
|
*/
|
|
@@ -33,7 +36,26 @@ export default function LabeledControl({
|
|
|
33
36
|
labelWidth = 48,
|
|
34
37
|
children,
|
|
35
38
|
className = '',
|
|
39
|
+
variant,
|
|
40
|
+
labelPosition = 'bottom',
|
|
41
|
+
labelClass = 'kol-helper-8',
|
|
42
|
+
gap = 0,
|
|
36
43
|
}) {
|
|
44
|
+
if (variant === 'panel') {
|
|
45
|
+
const isRow = labelPosition === 'left' || labelPosition === 'right'
|
|
46
|
+
const labelEl = label && (
|
|
47
|
+
<span className={`${labelClass} text-fg-32`} style={{ textTransform: 'uppercase', lineHeight: 1 }}>
|
|
48
|
+
{label}
|
|
49
|
+
</span>
|
|
50
|
+
)
|
|
51
|
+
return (
|
|
52
|
+
<div style={{ display: 'inline-flex', flexDirection: isRow ? 'row' : 'column', alignItems: 'center', gap }}>
|
|
53
|
+
{(labelPosition === 'top' || labelPosition === 'left') && labelEl}
|
|
54
|
+
{children}
|
|
55
|
+
{(labelPosition === 'bottom' || labelPosition === 'right') && labelEl}
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
37
59
|
const showLabel = !!label
|
|
38
60
|
const labelInner = (
|
|
39
61
|
<>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import usePlayback from '../hooks/usePlayback.js'
|
|
2
|
+
import PlaybackBar, { clock } from './PlaybackBar.jsx'
|
|
3
|
+
import QuickLookFrame from './QuickLookFrame.jsx'
|
|
4
|
+
import FileIcon from '../atoms/FileIcon.jsx'
|
|
5
|
+
import { useCover } from './PlayTile.jsx'
|
|
6
|
+
|
|
7
|
+
/* taxonomy-ok: molecule — nests PlaybackBar + QuickLookFrame (relative) + FileIcon (atom). */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* MediaPlayer — Play a video or an audio file. the media, `PlaybackBar` and the `usePlayback` hook
|
|
11
|
+
* as ONE player (user ruling 2026-10-02: *"I was referring to the two players in the quicklook in
|
|
12
|
+
* media"*). It was four components — `VideoSheet` and `AudioSheet` (the Quick Look pair),
|
|
13
|
+
* `AudioPreview` (its own transport) and `AudioPlayer` (the browser's strip) — and each is an
|
|
14
|
+
* alias of this one now, on the retirement ledger.
|
|
15
|
+
*
|
|
16
|
+
* `variant` is what plays. `frame` is where: given, the player draws inside `QuickLookFrame` (the
|
|
17
|
+
* Quick Look window — the bar docked in its footer); absent, it plays inline (*"why would media
|
|
18
|
+
* player not just prop the window, its not called WINDOWmediaplayer"*).
|
|
19
|
+
*
|
|
20
|
+
* video the picture at its own ratio; click toggles play. Inline, the bar floats over
|
|
21
|
+
* its bottom edge (inset 16, radius 12)
|
|
22
|
+
* audio inline, the bar alone. In the window, Finder's layout: the artwork square (the
|
|
23
|
+
* file's embedded ID3 cover; none → `FileIcon`) beside `Time: mm:ss`
|
|
24
|
+
*
|
|
25
|
+
* No native controls, no autoplay (Quick Look opens paused), `playsInline`.
|
|
26
|
+
*
|
|
27
|
+
* @param {'video'|'audio'} variant what plays (default `video`)
|
|
28
|
+
* @param {string} src the media URL
|
|
29
|
+
* @param {string} poster video — the poster URL
|
|
30
|
+
* @param {string} ext audio — the extension, for the no-cover icon
|
|
31
|
+
* @param {Function} onMeta ({ w, h, len }) => void once metadata lands (audio reports `len` only)
|
|
32
|
+
* @param {Object} frame the window's header — `QuickLookFrame`'s title · meta · actions · onClose · nav · size
|
|
33
|
+
* @param {'none'|'metadata'|'auto'} preload the native preload hint (default `metadata`)
|
|
34
|
+
* @param {string} className inline only — the player's box (its width is the caller's)
|
|
35
|
+
*/
|
|
36
|
+
export default function MediaPlayer({ variant = 'video', src, poster, ext, onMeta, frame, preload = 'metadata', className = '' }) {
|
|
37
|
+
const audio = variant === 'audio'
|
|
38
|
+
const { ref, handlers, bar } = usePlayback((el) => onMeta?.(audio ? { len: el.duration } : { w: el.videoWidth, h: el.videoHeight, len: el.duration }))
|
|
39
|
+
const cover = useCover(audio && frame ? src : null)
|
|
40
|
+
const media = audio
|
|
41
|
+
? <audio ref={ref} src={src} preload={preload} {...handlers} />
|
|
42
|
+
: <video ref={ref} src={src} poster={poster} playsInline preload={preload} className={frame ? 'kol-quicklook-media' : 'block w-full'} onClick={bar.onToggle} {...handlers} />
|
|
43
|
+
|
|
44
|
+
if (frame) {
|
|
45
|
+
return (
|
|
46
|
+
<QuickLookFrame {...frame} footer={<PlaybackBar {...bar} docked />}>
|
|
47
|
+
{media}
|
|
48
|
+
{audio && (
|
|
49
|
+
<div className="kol-quicklook-audio">
|
|
50
|
+
<div className="kol-quicklook-art">
|
|
51
|
+
{cover
|
|
52
|
+
? <img src={cover} alt="" className="w-full h-full object-cover" />
|
|
53
|
+
: <FileIcon ext={ext} glyph="music-note" className="w-[52%]" />}
|
|
54
|
+
</div>
|
|
55
|
+
<span className="kol-mono-16 text-meta">Time: <b className="text-strong">{clock(bar.duration)}</b></span>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</QuickLookFrame>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className={`relative ${className}`.trim()}>
|
|
64
|
+
{media}
|
|
65
|
+
<PlaybackBar {...bar} docked={audio} />
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @deprecated 2026-10-02 — use `<MediaPlayer variant="video" frame={…}>`. Drops when nobody imports it (04-retirements.md).
|
|
72
|
+
* VideoSheet — the video in the Quick Look window. Same props.
|
|
73
|
+
*/
|
|
74
|
+
export function VideoSheet({ frame, ...props }) {
|
|
75
|
+
return <MediaPlayer variant="video" frame={frame ?? {}} {...props} />
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @deprecated 2026-10-02 — use `<MediaPlayer variant="audio" frame={…}>`; `onDuration(seconds)` is `onMeta({ len })`. Drops when nobody imports it (04-retirements.md).
|
|
80
|
+
* AudioSheet — audio in the Quick Look window.
|
|
81
|
+
*/
|
|
82
|
+
export function AudioSheet({ frame, onDuration, ...props }) {
|
|
83
|
+
return <MediaPlayer variant="audio" frame={frame ?? {}} onMeta={onDuration && ((m) => onDuration(m.len))} {...props} />
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @deprecated 2026-10-02 — use `<MediaPlayer variant="audio">`; `onDuration(seconds)` is `onMeta({ len })`. Drops when nobody imports it (04-retirements.md).
|
|
88
|
+
* AudioPreview — the inline audio player. Its own transport is the `PlaybackBar` now.
|
|
89
|
+
*/
|
|
90
|
+
export function AudioPreview({ onDuration, ...props }) {
|
|
91
|
+
return <MediaPlayer variant="audio" onMeta={onDuration && ((m) => onDuration(m.len))} {...props} />
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @deprecated 2026-10-02 — use `<MediaPlayer variant="audio">`. Drops when nobody imports it (04-retirements.md).
|
|
96
|
+
* AudioPlayer — the native audio strip with a label. The strip is the
|
|
97
|
+
* `PlaybackBar` now; the label is the caller's to draw.
|
|
98
|
+
*/
|
|
99
|
+
export function AudioPlayer({ src, label, preload, className = '' }) {
|
|
100
|
+
if (!src) return null
|
|
101
|
+
return (
|
|
102
|
+
<figure className={`kol-audio-player flex flex-col gap-3 ${className}`.trim()}>
|
|
103
|
+
{label && <figcaption className="kol-mono-12 text-fg-48">{label}</figcaption>}
|
|
104
|
+
<MediaPlayer variant="audio" src={src} preload={preload} />
|
|
105
|
+
</figure>
|
|
106
|
+
)
|
|
107
|
+
}
|
package/src/molecules/Modal.jsx
CHANGED
|
@@ -120,7 +120,7 @@ function ModalView({ state, closeWith }) {
|
|
|
120
120
|
)}
|
|
121
121
|
<div className="flex gap-2 justify-end">
|
|
122
122
|
{state.kind !== 'alert' && <Button size="sm" onClick={cancel}>{state.cancelLabel ?? 'Cancel'}</Button>}
|
|
123
|
-
<Button
|
|
123
|
+
<Button tone="primary" size="sm" onClick={submit}>{state.okLabel ?? 'OK'}</Button>
|
|
124
124
|
</div>
|
|
125
125
|
</div>
|
|
126
126
|
</div>
|