@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.238.0",
|
|
4
4
|
"description": "The core component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"module": "./src/index.js",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./src/index.js",
|
|
11
|
+
"./atoms/RotaryDial": "./src/molecules/Knob.jsx",
|
|
11
12
|
"./atoms/*": "./src/atoms/*.jsx",
|
|
12
13
|
"./molecules/*": "./src/molecules/*.jsx",
|
|
13
14
|
"./organisms/*": "./src/organisms/*.jsx",
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"pdfjs-dist": "^6.3.289",
|
|
27
28
|
"react-syntax-highlighter": "^16.1.1",
|
|
28
29
|
"@kolkrabbi/kol-markdown": "^0.1.2",
|
|
29
|
-
"@kolkrabbi/kol-search": "^0.
|
|
30
|
+
"@kolkrabbi/kol-search": "^0.3.0"
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
32
33
|
"@kolkrabbi/kol-icons": ">=0.22.0",
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
41
|
+
"@kolkrabbi/kol-icons": "^0.33.0"
|
|
41
42
|
},
|
|
42
43
|
"files": [
|
|
43
44
|
"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/Input.jsx
CHANGED
|
@@ -159,8 +159,11 @@ export default function Input({
|
|
|
159
159
|
* from the font's ascender+descender (font-metric), not strictly from
|
|
160
160
|
* CSS line-height. Result: kol-control-sm ends up 26.5px instead of 26.
|
|
161
161
|
* h-4 / h-[18px] / h-[22px] match the kol-mono-12 / -14 / -16 line-heights; on touch every size is
|
|
162
|
-
* the 16 / 22 rung (D5, 2026-09-29 — see SearchInput)
|
|
163
|
-
|
|
162
|
+
* the 16 / 22 rung (D5, 2026-09-29 — see SearchInput) — except inside a hardware panel
|
|
163
|
+
* (`.kol-hw-panel`, 2026-10-02): a module's field keeps its desk height on touch, as the rest of
|
|
164
|
+
* the plate does (kol-components-controls.css). The type still goes to 16px there, so iOS does
|
|
165
|
+
* not zoom into it. */
|
|
166
|
+
const heightCls = `${size === 'xs' ? 'h-3' : size === 'sm' ? 'h-4' : size === 'md' ? 'h-[18px]' : 'h-[22px]'} pointer-coarse:not-[.kol-hw-panel_*]:h-[22px]`
|
|
164
167
|
|
|
165
168
|
const inputCls = [
|
|
166
169
|
'min-w-0 bg-transparent border-none outline-none text-auto',
|
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'
|
|
@@ -185,10 +188,11 @@ export { default as SectionNewsletter } from './organisms/SectionNewsletter.jsx'
|
|
|
185
188
|
export { default as NewsletterBand } from './organisms/NewsletterBand.jsx'
|
|
186
189
|
export { default as ColumnBrowser } from './organisms/ColumnBrowser.jsx'
|
|
187
190
|
export { default as KindPreview } from './molecules/KindPreview.jsx'
|
|
188
|
-
|
|
189
|
-
|
|
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'
|
|
190
195
|
export { default as PlaybackBar } from './molecules/PlaybackBar.jsx'
|
|
191
|
-
export { default as AudioSheet } from './molecules/AudioSheet.jsx'
|
|
192
196
|
export { default as QuickLookFrame } from './molecules/QuickLookFrame.jsx'
|
|
193
197
|
export { default as MediaTile } from './molecules/MediaTile.jsx'
|
|
194
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
|
|
|
@@ -36,6 +36,19 @@ import { glyphSize, indicatorSize } from '../hooks/glyphLadders.js'
|
|
|
36
36
|
|
|
37
37
|
/* xs (ControlsXsRung, 2026-09-01): the panel rung, opt-in by prop — the default stays sm (2026-07-28 law) */
|
|
38
38
|
const SIZE_TYPE = { xs: 'kol-mono-8', sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }
|
|
39
|
+
/* THE LIST IS THE TRIGGER'S, CONTINUED (2026-10-02 — user: *"the list has different padding then the
|
|
40
|
+
* button"*). Two things were off at every size but one. The rows were set in `kol-helper-12`
|
|
41
|
+
* whatever the trigger wore, and the helper is wider — "Newest first" fitted its trigger and was
|
|
42
|
+
* cut off in its own row. And the rows sat 4px in from the trigger's text, because the list pads
|
|
43
|
+
* itself by 4 on top of the row's padding. So: the row wears the trigger's face, and its side
|
|
44
|
+
* padding is the trigger's (8 · 12 · 16 · 20) less the list's 4. */
|
|
45
|
+
const ROW_PAD = { xs: 'px-1', sm: 'px-2', md: 'px-3', lg: 'px-4' }
|
|
46
|
+
/* …AND THE TRIGGER'S HEIGHT (same day — user: *"is it always the same regardless of size?"*). It was:
|
|
47
|
+
* 32px rows at sm · md · lg whatever the trigger stood (26 · 32 · 40), 20 at xs. A row is the
|
|
48
|
+
* trigger's box now, on the control ramp (22 · 26 · 32 · 40 — `--kol-ctl-*`, so the touch rung and
|
|
49
|
+
* a hardware panel move rows and trigger together). */
|
|
50
|
+
const ROW_H = { xs: 'h-[var(--kol-ctl-xs)]', sm: 'h-[var(--kol-ctl-sm)]', md: 'h-[var(--kol-ctl-md)]', lg: 'h-[var(--kol-ctl-lg)]' }
|
|
51
|
+
const ROW = { xs: `kol-mono-8 ${ROW_PAD.xs} ${ROW_H.xs}`, sm: `kol-mono-12 ${ROW_PAD.sm} ${ROW_H.sm}`, md: `kol-mono-14 ${ROW_PAD.md} ${ROW_H.md}`, lg: `kol-mono-16 ${ROW_PAD.lg} ${ROW_H.lg}` }
|
|
39
52
|
/* Caret size comes from the INDICATOR ladder (glyphLadders.js) — the private
|
|
40
53
|
* map that lived here was a transcription of ADJACENT, which is the wrong
|
|
41
54
|
* ladder for a decoration: it put a caret one rung HEAVIER than the label
|
|
@@ -95,6 +108,11 @@ const Dropdown = ({
|
|
|
95
108
|
* has one implementation and the drawing stays the caller's. */
|
|
96
109
|
triggerAdornment,
|
|
97
110
|
defaultOpen = false,
|
|
111
|
+
/* `stayOpen` — a click outside does not close the list (the trigger and Esc still do). For a
|
|
112
|
+
* surface where the open list is the thing being looked at while other controls are worked:
|
|
113
|
+
* the showcase's preview, with its toolbar pickers beside it (user 2026-10-02: *"can we
|
|
114
|
+
* disclude the preview from that behaviour?"*). */
|
|
115
|
+
stayOpen = false,
|
|
98
116
|
className = ''
|
|
99
117
|
}) => {
|
|
100
118
|
const [isOpen, setIsOpen] = useState(defaultOpen)
|
|
@@ -107,12 +125,26 @@ const Dropdown = ({
|
|
|
107
125
|
* ambient case is the one only this can serve. */
|
|
108
126
|
const triggerRef = useRef(null)
|
|
109
127
|
const [ambient, setAmbient] = useState(null)
|
|
128
|
+
/* …AND SO IS A ZOOM (2026-10-02, kol-monitor's rack — user: *"what about the dropdown in general?
|
|
129
|
+
* its kinda wrong no"*). A trigger inside a zoomed surface (the rack at 80%) is drawn smaller,
|
|
130
|
+
* but the portalled list was not: it kept its full-size type inside a panel pinned to the
|
|
131
|
+
* trigger's SHRUNKEN width, so the names were cut off — 18 of monitor's 40 presets at 80%, all
|
|
132
|
+
* of them at 50%. The trigger's scale is its drawn width over its layout width; the list's
|
|
133
|
+
* content takes the same zoom, so trigger and list stay one piece at any scale. */
|
|
134
|
+
const [scale, setScale] = useState(1)
|
|
135
|
+
const [inPanel, setInPanel] = useState(false)
|
|
110
136
|
useLayoutEffect(() => {
|
|
111
137
|
if (!isOpen || !triggerRef.current) return
|
|
112
138
|
const cs = getComputedStyle(triggerRef.current)
|
|
113
139
|
const vars = {}
|
|
114
140
|
for (const v of TONE_VARS) { const val = cs.getPropertyValue(v).trim(); if (val) vars[v] = val }
|
|
115
141
|
setAmbient(vars)
|
|
142
|
+
const laid = triggerRef.current.offsetWidth
|
|
143
|
+
const s = laid ? triggerRef.current.getBoundingClientRect().width / laid : 1
|
|
144
|
+
setScale(Math.abs(s - 1) > 0.01 ? s : 1)
|
|
145
|
+
/* and the panel it opened from: a hardware plate keeps its desk sizes on touch, and the
|
|
146
|
+
* portalled list would otherwise take the touch rung its trigger skipped */
|
|
147
|
+
setInPanel(!!triggerRef.current.closest('.kol-hw-panel'))
|
|
116
148
|
}, [isOpen])
|
|
117
149
|
|
|
118
150
|
// sm everywhere unless explicitly overridden (see docblock size law).
|
|
@@ -132,6 +164,7 @@ const Dropdown = ({
|
|
|
132
164
|
flip: false,
|
|
133
165
|
matchReferenceWidth: true,
|
|
134
166
|
role: 'listbox',
|
|
167
|
+
dismiss: stayOpen ? { outsidePress: false } : true,
|
|
135
168
|
})
|
|
136
169
|
|
|
137
170
|
/* Width belongs to the CALL SITE (2026-08-09 user call — "width without any
|
|
@@ -163,7 +196,9 @@ const Dropdown = ({
|
|
|
163
196
|
/* `{ divider: true }` in `options` draws a hairline between groups (FoundationsTones, 2026-09-26 —
|
|
164
197
|
* user: none on top, the filled tones in order, outline and ghost apart). It is never a choice:
|
|
165
198
|
* the lookup, the fallback and the trigger's width all read the choices only. */
|
|
166
|
-
|
|
199
|
+
/* `{ heading: 'Variant' }` names the list (2026-10-01 — user: "first item 'variant' then divider
|
|
200
|
+
* then the list"): a row that says what the dropdown sets, never a choice. */
|
|
201
|
+
const choices = options.filter((opt) => !opt?.divider && !opt?.heading)
|
|
167
202
|
const currentOption = choices.find((opt) => opt.value === value) || choices[0]
|
|
168
203
|
|
|
169
204
|
/* A clamped list (Popover caps the panel to the viewport) can open with the
|
|
@@ -172,7 +207,7 @@ const Dropdown = ({
|
|
|
172
207
|
const listRef = useRef(null)
|
|
173
208
|
useEffect(() => {
|
|
174
209
|
if (!isOpen) return
|
|
175
|
-
const idx = options.findIndex((opt) => !opt?.divider && opt.value === currentOption?.value)
|
|
210
|
+
const idx = options.findIndex((opt) => !opt?.divider && !opt?.heading && opt.value === currentOption?.value)
|
|
176
211
|
listRef.current?.children[idx]?.scrollIntoView({ block: 'nearest' })
|
|
177
212
|
}, [isOpen]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
178
213
|
|
|
@@ -239,20 +274,25 @@ const Dropdown = ({
|
|
|
239
274
|
popover={popover}
|
|
240
275
|
panel={false}
|
|
241
276
|
focus={false}
|
|
242
|
-
className={`kol-dd-panel ${resolvedVariant ? `kol-dd-panel--${resolvedVariant}` : ''} ${toneClass(tone)}`.replace(/\s+/g, ' ').trim()}
|
|
277
|
+
className={`kol-dd-panel ${resolvedVariant ? `kol-dd-panel--${resolvedVariant}` : ''} ${toneClass(tone)} ${inPanel ? 'kol-hw-panel' : ''}`.replace(/\s+/g, ' ').trim()}
|
|
243
278
|
style={{
|
|
244
279
|
...ambient,
|
|
245
280
|
'--kol-dd-max-rows': maxRows ?? 10,
|
|
246
|
-
/*
|
|
247
|
-
...(
|
|
281
|
+
/* a row is the trigger's height; the panel's max-height reads it */
|
|
282
|
+
...(rowHeight == null ? { '--kol-dd-row-h': `var(--kol-ctl-${resolvedSize})` } : null),
|
|
248
283
|
...(rowHeight != null ? { '--kol-dd-row-h': typeof rowHeight === 'number' ? `${rowHeight}px` : rowHeight } : null),
|
|
249
284
|
}}
|
|
250
285
|
>
|
|
251
|
-
{
|
|
286
|
+
{/* THE HAIRLINE UNDER THE TRIGGER, ALWAYS (2026-10-02 — user: *"I see it sometimes and sometimes
|
|
287
|
+
* not"*). It was drawn only for `variant="primary" | "grey"`: a dropdown toned by `tone=`
|
|
288
|
+
* got none, and an outline one lost its divider too — the panel sits 1px over the trigger's
|
|
289
|
+
* bottom border, which was meant to be it. One rule: an open dropdown has the line. */}
|
|
290
|
+
<div className="kol-dd-div" />
|
|
252
291
|
|
|
253
|
-
<div ref={listRef} className="kol-dd-list" role="listbox">
|
|
292
|
+
<div ref={listRef} className="kol-dd-list" role="listbox" style={scale !== 1 ? { zoom: scale } : undefined}>
|
|
254
293
|
{options.map((option, i) => {
|
|
255
294
|
if (option?.divider) return <MenuDropdownDivider key={`divider-${i}`} />
|
|
295
|
+
if (option?.heading) return <div key={`heading-${i}`} className={`${resolvedSize === 'xs' ? 'kol-mono-8' : 'kol-helper-12'} ${ROW_H[resolvedSize] ?? ROW_H.sm} ${ROW_PAD[resolvedSize] ?? ROW_PAD.sm} flex shrink-0 items-center text-meta`}>{option.heading}</div>
|
|
256
296
|
const isActive = option.value === currentOption?.value
|
|
257
297
|
return (
|
|
258
298
|
<MenuDropdownItem
|
|
@@ -265,6 +305,7 @@ const Dropdown = ({
|
|
|
265
305
|
* brighten. The check mark is what marks the current value. */
|
|
266
306
|
hover={false}
|
|
267
307
|
size={resolvedSize}
|
|
308
|
+
rowClass={ROW[resolvedSize] ?? ROW.sm}
|
|
268
309
|
height={rowHeight}
|
|
269
310
|
onPointerEnter={onOptionHover ? () => reportHover(option.value) : undefined}
|
|
270
311
|
onPointerLeave={onOptionHover ? () => reportHover(null) : undefined}
|
|
@@ -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
|
<>
|