@kolkrabbi/kol-hardware 0.3.2 → 0.4.1
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 +7 -7
- package/src/frames/ChannelStrip.jsx +1 -1
- package/src/frames/FlipCard.jsx +1 -1
- package/src/frames/ModuleFrame.jsx +3 -2
- package/src/frames/Rack.jsx +99 -0
- package/src/frames/eurorack.js +19 -0
- package/src/index.js +10 -3
- package/src/indicators/JackSocket.jsx +1 -1
- package/src/indicators/LED.jsx +1 -1
- package/src/indicators/LabeledJack.jsx +1 -1
- package/src/indicators/SignalScope.jsx +1 -1
- package/src/panel/ModuleHeader.jsx +1 -1
- package/src/panel/PanelLabel.jsx +7 -25
- package/src/panel/SignalReference.jsx +2 -2
- package/src/switches/FlipToggle.jsx +1 -1
- package/src/switches/IconButton.jsx +1 -1
- package/src/switches/RockerSwitch.jsx +1 -1
- package/src/switches/Toggle.jsx +1 -1
- package/src/value/EnvelopeGenerator.jsx +15 -10
- package/src/value/Fader.jsx +7 -95
- package/src/value/Knob.jsx +8 -126
- package/src/value/ParamSheet.jsx +1 -1
- package/src/value/armLongPress.js +0 -17
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-hardware",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Panel controls for instruments",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.js",
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"./*": "./src/*"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"@kolkrabbi/kol-component": ">=0.
|
|
16
|
+
"@kolkrabbi/kol-component": ">=0.238.0",
|
|
17
17
|
"@kolkrabbi/kol-icons": ">=0.25.0",
|
|
18
|
-
"@kolkrabbi/kol-theme": ">=0.
|
|
18
|
+
"@kolkrabbi/kol-theme": ">=0.164.0",
|
|
19
19
|
"react": "^18.3.0 || ^19.0.0",
|
|
20
20
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@kolkrabbi/kol-component": "^0.
|
|
24
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
25
|
-
"@kolkrabbi/kol-theme": "^0.
|
|
23
|
+
"@kolkrabbi/kol-component": "^0.238.0",
|
|
24
|
+
"@kolkrabbi/kol-icons": "^0.33.0",
|
|
25
|
+
"@kolkrabbi/kol-theme": "^0.164.0"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ChannelStrip — a mixer channel's face (frames group, 2026-09-27). Lifted from kol-mirror's
|
|
2
|
+
* ChannelStrip — A mixer channel. a mixer channel's face (frames group, 2026-09-27). Lifted from kol-mirror's
|
|
3
3
|
* SymphonyMixer strip: a plate one rung below the desk, a top row of controls beside a column of
|
|
4
4
|
* actions, then the channel's sliders, then a footer. Slots only — which controls, which
|
|
5
5
|
* actions, what they do, all stay in the consumer.
|
package/src/frames/FlipCard.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FlipCard — a face that turns over in place to show its back (frames group, 2026-09-27). Lifted
|
|
2
|
+
* FlipCard — A face that turns to show its back. a face that turns over in place to show its back (frames group, 2026-09-27). Lifted
|
|
3
3
|
* from kol-mirror's channel flip (`styles/components.css` .mirror-flip-*), where a strip turns
|
|
4
4
|
* to its patch panel. The body turns; everything around it holds still, so the back is the same
|
|
5
5
|
* shaped object as the front.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ModuleHeader from '../panel/ModuleHeader.jsx'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* ModuleFrame — the front panel a module is built in: the header pinned at the top, the body
|
|
4
|
+
* ModuleFrame — The front panel of a module. the front panel a module is built in: the header pinned at the top, the body
|
|
5
5
|
* below it (frames group, 2026-09-27). Lifted from kol-monitor's `modules/utility/Module.jsx`,
|
|
6
6
|
* where every rack module sits in it; kol-mirror's modules and the controls reference compose
|
|
7
7
|
* the same shape. Presentational — the rack's edit context, power and routing stay in the
|
|
@@ -23,7 +23,8 @@ export default function ModuleFrame({
|
|
|
23
23
|
}) {
|
|
24
24
|
return (
|
|
25
25
|
<div
|
|
26
|
-
|
|
26
|
+
/* `kol-hw-panel`: the plate keeps its desk control sizes on touch (kol-components-controls.css) */
|
|
27
|
+
className={`kol-hw-panel ${className}`}
|
|
27
28
|
style={{
|
|
28
29
|
width: '100%', height: '100%', display: 'flex', flexDirection: 'column',
|
|
29
30
|
padding: `${PAD}px 0 ${u === 1 ? 8 : PAD}px`, userSelect: 'none', ...style,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createContext, memo, useContext } from 'react'
|
|
2
|
+
import { HP_PX, TOTAL_HP, ROW_HEIGHT, RAIL_HEIGHT, hpToPx } from './eurorack.js'
|
|
3
|
+
|
|
4
|
+
/* the case's width in HP, handed to its rows */
|
|
5
|
+
const RackHpContext = createContext(TOTAL_HP)
|
|
6
|
+
|
|
7
|
+
const Rail = memo(function Rail({ hp }) {
|
|
8
|
+
return (
|
|
9
|
+
<div style={{
|
|
10
|
+
height: RAIL_HEIGHT,
|
|
11
|
+
background: 'var(--kol-ctl-hw-rail)',
|
|
12
|
+
display: 'flex',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
boxShadow: 'inset 0 1px 0 var(--kol-ctl-hw-cap-edge), inset 0 -1px 0 rgba(0,0,0,0.2)',
|
|
15
|
+
}}>
|
|
16
|
+
{Array.from({ length: hp }, (_, i) => (
|
|
17
|
+
<div key={i} style={{ width: HP_PX, display: 'flex', justifyContent: 'center' }}>
|
|
18
|
+
<div style={{
|
|
19
|
+
width: 4,
|
|
20
|
+
height: 4,
|
|
21
|
+
borderRadius: '50%',
|
|
22
|
+
backgroundColor: 'var(--kol-ctl-hw-shade)',
|
|
23
|
+
boxShadow: 'inset 0 1px 1px rgba(0,0,0,0.6)',
|
|
24
|
+
}} />
|
|
25
|
+
</div>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* RackRow — One row of a rack. a 1U or 3U row: the case ground, a rail of mounting holes top and bottom, the
|
|
33
|
+
* modules over them (kol-monitor's `Case.jsx` `RackRow`, lifted 2026-10-01 — user ruling: the rack
|
|
34
|
+
* comes into the design system). A row is a definite pixel height — 3U 416, 1U 138.67 — and as
|
|
35
|
+
* wide as its case's HP. Lay `RackSlot`s in it.
|
|
36
|
+
*
|
|
37
|
+
* @param {'1u'|'3u'} height (default 3u)
|
|
38
|
+
* @param {ReactNode} children the row's content — normally a flex row of `RackSlot`
|
|
39
|
+
*/
|
|
40
|
+
export const RackRow = memo(function RackRow({ height = '3u', children }) {
|
|
41
|
+
const hp = useContext(RackHpContext)
|
|
42
|
+
return (
|
|
43
|
+
<div className="relative" style={{ width: hpToPx(hp), height: ROW_HEIGHT[height] || ROW_HEIGHT['3u'] }}>
|
|
44
|
+
{/* Layer 0: case background */}
|
|
45
|
+
<div style={{ position: 'absolute', inset: 0, backgroundColor: 'var(--kol-ctl-hw-case)', zIndex: 0 }} />
|
|
46
|
+
{/* Layer 1: rails — visible in empty HP space */}
|
|
47
|
+
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1 }}><Rail hp={hp} /></div>
|
|
48
|
+
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 1 }}><Rail hp={hp} /></div>
|
|
49
|
+
{/* Layer 2: modules */}
|
|
50
|
+
<div className="relative" style={{ width: '100%', height: '100%', zIndex: 2 }}>
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* RackSlot — A module's place in a rack row. the box a module sits in: `hp` wide (16px each), the row's height. It
|
|
59
|
+
* paints nothing — the module inside is the panel. Monitor's `ModuleSlot` minus its edit arrows
|
|
60
|
+
* and registry, which stay in the consumer.
|
|
61
|
+
*
|
|
62
|
+
* @param {number} hp width in HP
|
|
63
|
+
* @param {1|3} u the row it sits in (default 3)
|
|
64
|
+
* @param {boolean} overflow let the content run past the slot (edit-mode arrows on the edge)
|
|
65
|
+
* @param {ReactNode} children the module — a `ModuleFrame`
|
|
66
|
+
*/
|
|
67
|
+
export function RackSlot({ hp = 8, u = 3, overflow = false, style, children, ...props }) {
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
style={{ width: hpToPx(hp), height: ROW_HEIGHT[u === 1 ? '1u' : '3u'], flexShrink: 0, overflow: overflow ? 'visible' : 'hidden', position: 'relative', ...style }}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* RackCase — The case a rack is built in. the eurorack case: two side cheeks and the rows stacked between them
|
|
80
|
+
* (kol-monitor's `Case.jsx`, lifted 2026-10-01). A frame like `ModuleFrame` — slots and a shape.
|
|
81
|
+
* What is in the rows, where each module sits, routing, power and the edit mode stay in the
|
|
82
|
+
* consumer.
|
|
83
|
+
*
|
|
84
|
+
* @param {number} hp the case's width in HP (default 104); its rows read it
|
|
85
|
+
* @param {ReactNode} children `RackRow`s
|
|
86
|
+
*/
|
|
87
|
+
export default memo(function RackCase({ hp = TOTAL_HP, children }) {
|
|
88
|
+
return (
|
|
89
|
+
<RackHpContext.Provider value={hp}>
|
|
90
|
+
<div style={{ display: 'flex', flexDirection: 'row', padding: '3px 0' }}>
|
|
91
|
+
<div className="bg-oq-12" style={{ width: 24, flexShrink: 0, borderRadius: 4, margin: '-3px 0' }} />
|
|
92
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, padding: '0 2px' }}>
|
|
93
|
+
{children}
|
|
94
|
+
</div>
|
|
95
|
+
<div className="bg-oq-12" style={{ width: 24, flexShrink: 0, borderRadius: 4, margin: '-3px 0' }} />
|
|
96
|
+
</div>
|
|
97
|
+
</RackHpContext.Provider>
|
|
98
|
+
)
|
|
99
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The eurorack grid — 1HP = 16px, everything derives from it (kol-monitor's
|
|
2
|
+
// `modules/utility/eurorack.js`, lifted 2026-10-01 with the rack frames).
|
|
3
|
+
|
|
4
|
+
export const HP_PX = 16
|
|
5
|
+
export const TOTAL_HP = 104
|
|
6
|
+
export const MIN_HP = 2
|
|
7
|
+
|
|
8
|
+
// Row heights in px, by rack unit. Monitor resolves its row aspect (1U 12:1, 3U 4:1) against the
|
|
9
|
+
// 104hp row width, so a row is a definite height everywhere — WebKit does not treat an
|
|
10
|
+
// `aspect-ratio` box as a definite height for its percentage-height children (2026-09-02).
|
|
11
|
+
export const ROW_HEIGHT = {
|
|
12
|
+
'1u': (TOTAL_HP * HP_PX) / 12, // 138.67
|
|
13
|
+
'3u': (TOTAL_HP * HP_PX) / 4, // 416
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Rail height in px — the dead zone at the top and bottom of each module
|
|
17
|
+
export const RAIL_HEIGHT = 14
|
|
18
|
+
|
|
19
|
+
export const hpToPx = (hp) => hp * HP_PX
|
package/src/index.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// (mixers, synths, racks, decks). Renamed from kol-controls 2026-09-27 (user ruling: "hardware"
|
|
3
3
|
// says what it is — `controls` also meant the app atoms). Five groups:
|
|
4
4
|
//
|
|
5
|
-
// value things you turn or slide — Knob · Fader
|
|
5
|
+
// value things you turn or slide — the touch ParamSheet (Knob · Fader are kol-component's
|
|
6
|
+
// `Knob` / `Slider` at `variant="panel"`; the names here are aliases)
|
|
6
7
|
// switches hardware switches and buttons — Toggle · FlipToggle · RockerSwitch · IconButton
|
|
7
8
|
// indicators lights and patching — LED · JackSocket · LabeledJack
|
|
8
9
|
// panel panel furniture — PanelLabel · ModuleHeader
|
|
9
|
-
// frames the shells a module is built in — ModuleFrame · ChannelStrip · FlipCard
|
|
10
|
+
// frames the shells a module is built in — ModuleFrame · ChannelStrip · FlipCard — and the
|
|
11
|
+
// rack they mount in: RackCase · RackRow · RackSlot
|
|
10
12
|
//
|
|
11
13
|
// App-scale inputs are kol-component's (Dropdown · Input · Stepper at size="xs"); the panel copies
|
|
12
14
|
// retired in kol-controls 0.3.0 (ControlsXsRung).
|
|
@@ -15,7 +17,8 @@
|
|
|
15
17
|
export { default as Knob } from './value/Knob.jsx'
|
|
16
18
|
export { default as Fader } from './value/Fader.jsx'
|
|
17
19
|
export { default as ParamSheet } from './value/ParamSheet.jsx'
|
|
18
|
-
|
|
20
|
+
/* moved to kol-component with the controls that arm it (2026-10-01); re-exported so imports hold */
|
|
21
|
+
export { armLongPress } from '@kolkrabbi/kol-component'
|
|
19
22
|
// switches
|
|
20
23
|
export { default as Toggle } from './switches/Toggle.jsx'
|
|
21
24
|
export { default as FlipToggle } from './switches/FlipToggle.jsx'
|
|
@@ -32,6 +35,10 @@ export { default as ModuleHeader } from './panel/ModuleHeader.jsx'
|
|
|
32
35
|
export { default as ModuleFrame } from './frames/ModuleFrame.jsx'
|
|
33
36
|
export { default as ChannelStrip } from './frames/ChannelStrip.jsx'
|
|
34
37
|
export { default as FlipCard } from './frames/FlipCard.jsx'
|
|
38
|
+
/* the rack (2026-10-01, user ruling: it comes into the design system) — the case, its 1U/3U rows
|
|
39
|
+
* and the HP slot, lifted from kol-monitor; what mounts where and routing stay in the consumer */
|
|
40
|
+
export { default as RackCase, RackRow, RackSlot } from './frames/Rack.jsx'
|
|
41
|
+
export { HP_PX, TOTAL_HP, MIN_HP, ROW_HEIGHT, RAIL_HEIGHT, hpToPx } from './frames/eurorack.js'
|
|
35
42
|
// signal — the components over the signal engine (the engine itself, React-free, is `./signal`)
|
|
36
43
|
export { default as SignalScope } from './indicators/SignalScope.jsx'
|
|
37
44
|
export { default as SignalReference } from './panel/SignalReference.jsx'
|
|
@@ -28,7 +28,7 @@ function stopJackLoop() {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* JackSocket — the 3.5mm eurorack jack, PRESENTATIONAL (kol-monitor's rack,
|
|
31
|
+
* JackSocket — A patch jack. the 3.5mm eurorack jack, PRESENTATIONAL (kol-monitor's rack,
|
|
32
32
|
* lifted 2026-09-01): ring (well) + hole + label, and a rim that glows with the
|
|
33
33
|
* signal — `signalRef.current` is `{ type: 'scalar'|'color'|'points', value, … }`,
|
|
34
34
|
* read on a shared rAF at ~15fps, dirty-checked. Drag-to-patch, the registry and
|
package/src/indicators/LED.jsx
CHANGED
|
@@ -12,7 +12,7 @@ const SIZES = { sm: 6, md: 8 }
|
|
|
12
12
|
const HIT_PAD = 5
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* LED — the indicator lamp (kol-monitor's rack, lifted 2026-09-01). Sizes sm 6 ·
|
|
15
|
+
* LED — An indicator lamp. the indicator lamp (kol-monitor's rack, lifted 2026-09-01). Sizes sm 6 ·
|
|
16
16
|
* md 8; colors red · yellow · green · white · blue, or any CSS color. With
|
|
17
17
|
* `onClick` an invisible hit pad (+5px each side) overlays the lamp.
|
|
18
18
|
*
|
|
@@ -2,7 +2,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
2
2
|
import JackSocket from './JackSocket.jsx'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* LabeledJack — a `JackSocket` with an 8px label or a dim icon beside it
|
|
5
|
+
* LabeledJack — A jack with a label. a `JackSocket` with an 8px label or a dim icon beside it
|
|
6
6
|
* (kol-monitor's rack, lifted 2026-09-01). Use instead of the socket's own
|
|
7
7
|
* `label` when the label should sit left / right / above.
|
|
8
8
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* SignalScope — the oscilloscope every expression tool draws (signal engine, 2026-09-27): a
|
|
4
|
+
* SignalScope — An oscilloscope trace of a signal. the oscilloscope every expression tool draws (signal engine, 2026-09-27): a
|
|
5
5
|
* trace of `sample(t)` over a window, the knob range as dashed reference lines, a midline, and
|
|
6
6
|
* a playhead that sweeps the window live. kol-mirror's /expressions scope, kol-monitor's
|
|
7
7
|
* Scope / Scope+ and the design editor's Oscilloscope loop each drew their own.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Toggle from '../switches/Toggle.jsx'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* ModuleHeader — the enable dot + module name, left-aligned (kol-monitor's rack,
|
|
4
|
+
* ModuleHeader — A module's name and power dot. the enable dot + module name, left-aligned (kol-monitor's rack,
|
|
5
5
|
* lifted 2026-09-01). In edit mode the yellow dot removes the module; otherwise
|
|
6
6
|
* an optional bypass dot. The consumer's case-power context became `powered`:
|
|
7
7
|
* when the case is off the dot shows off whatever `enabled` says.
|
package/src/panel/PanelLabel.jsx
CHANGED
|
@@ -1,28 +1,10 @@
|
|
|
1
|
+
import { LabeledControl } from '@kolkrabbi/kol-component'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Toggle: `kol-helper-8`, `text-fg-32`, uppercase.
|
|
6
|
-
*
|
|
7
|
-
* @param {string} label
|
|
8
|
-
* @param {boolean} horizontal shorthand for a row
|
|
9
|
-
* @param {'top'|'bottom'|'left'|'right'} labelPosition (default bottom)
|
|
10
|
-
* @param {string} labelClass type class (default `kol-helper-8`)
|
|
11
|
-
* @param {number} gap
|
|
12
|
-
* @param {ReactNode} children
|
|
4
|
+
* @deprecated 2026-10-01 — use `<LabeledControl variant="panel">` from kol-component. Drops when nobody imports it (04-retirements.md).
|
|
5
|
+
* PanelLabel — A control with its panel label. the panel label is LabeledControl's panel variant now
|
|
6
|
+
* (user ruling 2026-10-01). Same props; `horizontal` is `labelPosition="right"`.
|
|
13
7
|
*/
|
|
14
|
-
export default function PanelLabel({
|
|
15
|
-
|
|
16
|
-
const labelEl = label && (
|
|
17
|
-
<span className={`${labelClass} text-fg-32`} style={{ textTransform: 'uppercase', lineHeight: 1 }}>
|
|
18
|
-
{label}
|
|
19
|
-
</span>
|
|
20
|
-
)
|
|
21
|
-
return (
|
|
22
|
-
<div style={{ display: 'inline-flex', flexDirection: isRow ? 'row' : 'column', alignItems: 'center', gap }}>
|
|
23
|
-
{(labelPosition === 'top' || labelPosition === 'left') && labelEl}
|
|
24
|
-
{children}
|
|
25
|
-
{(labelPosition === 'bottom' || labelPosition === 'right') && labelEl}
|
|
26
|
-
</div>
|
|
27
|
-
)
|
|
8
|
+
export default function PanelLabel({ horizontal = false, labelPosition = 'bottom', ...props }) {
|
|
9
|
+
return <LabeledControl variant="panel" labelPosition={horizontal && labelPosition === 'bottom' ? 'right' : labelPosition} {...props} />
|
|
28
10
|
}
|
|
@@ -2,7 +2,7 @@ import { useState } from 'react'
|
|
|
2
2
|
import { Button, TabsRow, usePopover, PopoverPanel } from '@kolkrabbi/kol-component'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* SignalReference — the reference an expression or envelope tool is explained with (signal
|
|
5
|
+
* SignalReference — The reference for a signal tool. the reference an expression or envelope tool is explained with (signal
|
|
6
6
|
* engine, 2026-09-27). ONE component over the reference DATA in `@kolkrabbi/kol-hardware/signal`,
|
|
7
7
|
* in the three shapes the estate already used:
|
|
8
8
|
*
|
|
@@ -107,7 +107,7 @@ function PopButton({ label, list, onPick, onAppend, size }) {
|
|
|
107
107
|
<>
|
|
108
108
|
{/* Button takes no ref — the wrapper is the anchor, and the click reaches it by bubbling */}
|
|
109
109
|
<span ref={popover.refs.setReference} {...popover.getReferenceProps()} className="inline-flex">
|
|
110
|
-
<Button
|
|
110
|
+
<Button tone="grey" size={size} pressed={open}>{label}</Button>
|
|
111
111
|
</span>
|
|
112
112
|
<PopoverPanel popover={popover} className="w-80 max-h-96 overflow-y-auto p-3">
|
|
113
113
|
<div className="flex flex-col gap-3">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FlipToggle — the eurorack flip switch, two or three positions (kol-monitor's
|
|
2
|
+
* FlipToggle — A eurorack flip switch. the eurorack flip switch, two or three positions (kol-monitor's
|
|
3
3
|
* rack, lifted 2026-09-01). `positions=2`: `value` is a boolean; `positions=3`:
|
|
4
4
|
* `value` is 0 · 1 · 2 (top, centre, bottom / left, centre, right).
|
|
5
5
|
*
|
|
@@ -4,7 +4,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
4
4
|
const PULSE_MS = 100
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* IconButton — the icon-only panel key (kol-monitor's rack, lifted 2026-09-01):
|
|
7
|
+
* IconButton — An icon-only panel key. the icon-only panel key (kol-monitor's rack, lifted 2026-09-01):
|
|
8
8
|
* 1px border, radius 3, `active` lights the LED-red border and a 15% fill;
|
|
9
9
|
* `momentary` flashes the active style for ~100ms on click — trigger-style
|
|
10
10
|
* actions. Use with `PanelLabel` for a text label.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RockerSwitch — the I/O rocker with a backlit paddle (kol-monitor's
|
|
2
|
+
* RockerSwitch — A lit rocker switch. the I/O rocker with a backlit paddle (kol-monitor's
|
|
3
3
|
* `PowerModule`, the switch alone, lifted 2026-09-01). 28 × 38 housing on the
|
|
4
4
|
* hardware cap; the paddle sits top when on (LED red, glowing) and bottom when
|
|
5
5
|
* off; static I / O legends on the housing show whichever half is uncovered.
|
package/src/switches/Toggle.jsx
CHANGED
|
@@ -5,7 +5,7 @@ const LONG_PRESS_MS = 500
|
|
|
5
5
|
const BLINK_MS_DEFAULT = 500
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Toggle — the LED-dot on/off button with a label (kol-monitor's rack, lifted
|
|
8
|
+
* Toggle — An LED on/off button. the LED-dot on/off button with a label (kol-monitor's rack, lifted
|
|
9
9
|
* 2026-09-01). `momentary` fires `onChange(true)` and the dot flashes 120ms;
|
|
10
10
|
* `onLongPress` (hold ≥500ms) fires instead of `onChange`; `blink` pulses the
|
|
11
11
|
* dot as an indicator; `forceLit` holds it on.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { Button, Divider, Input, SegmentedToggle, Slider } from '@kolkrabbi/kol-component'
|
|
3
|
-
import
|
|
2
|
+
import { Button, Divider, Input, Knob, SegmentedToggle, Slider } from '@kolkrabbi/kol-component'
|
|
3
|
+
import ParamSheet from './ParamSheet.jsx'
|
|
4
4
|
import Toggle from '../switches/Toggle.jsx'
|
|
5
5
|
import SignalScope from '../indicators/SignalScope.jsx'
|
|
6
6
|
import SignalReference from '../panel/SignalReference.jsx'
|
|
@@ -171,7 +171,7 @@ function EnvelopeHandles({ adsr, sec, onDragStart, onDrag, onDragEnd }) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
|
-
* EnvelopeGenerator — a value over time, typed as an equation or shaped as an ADSR envelope
|
|
174
|
+
* EnvelopeGenerator — A value over time. a value over time, typed as an equation or shaped as an ADSR envelope
|
|
175
175
|
* (signal engine, 2026-09-27). One tool for what kol-mirror's /expressions page, kol-monitor's
|
|
176
176
|
* Scope+ and Env modules and the design editor's Oscilloscope each built alone.
|
|
177
177
|
*
|
|
@@ -212,6 +212,8 @@ export default function EnvelopeGenerator({ generator, reference = 'panel', clas
|
|
|
212
212
|
const own = useEnvelopeGenerator(options)
|
|
213
213
|
const g = generator ?? own
|
|
214
214
|
const [collapsed, setCollapsed] = useState(false)
|
|
215
|
+
/* the holder opens the touch sheet, not the knob (2026-10-01): which ADSR stage is held */
|
|
216
|
+
const [sheet, setSheet] = useState(null)
|
|
215
217
|
const { mode, adsr, view } = g
|
|
216
218
|
|
|
217
219
|
const sample = mode === 'equation'
|
|
@@ -223,12 +225,12 @@ export default function EnvelopeGenerator({ generator, reference = 'panel', clas
|
|
|
223
225
|
|
|
224
226
|
const transport = (
|
|
225
227
|
<div className="flex h-6 shrink-0 items-center gap-3">
|
|
226
|
-
<Button
|
|
228
|
+
<Button tone="ghost" size="sm" iconOnly={g.playing ? 'pause' : 'play'} aria-label={g.playing ? 'Pause' : 'Play'} onClick={() => g.setPlaying(!g.playing)} />
|
|
227
229
|
<Field label="BPM" value={g.bpm} chars={3} onCommit={(v) => g.setBpm(Math.max(1, Math.min(999, num(v, g.bpm))))} />
|
|
228
230
|
{mode === 'adsr' && (
|
|
229
231
|
<>
|
|
230
232
|
<Toggle value={g.cycle} onChange={g.setCycle} label="cycle" />
|
|
231
|
-
<Button
|
|
233
|
+
<Button tone="ghost" size="sm" onClick={g.fire}>Trigger</Button>
|
|
232
234
|
</>
|
|
233
235
|
)}
|
|
234
236
|
<span className="ml-auto text-fg-48">{mode === 'adsr' ? `${passSec.toFixed(2)} s` : ''}</span>
|
|
@@ -276,10 +278,10 @@ export default function EnvelopeGenerator({ generator, reference = 'panel', clas
|
|
|
276
278
|
)}
|
|
277
279
|
</div>
|
|
278
280
|
<div className="flex min-h-6 shrink-0 flex-wrap items-center justify-between gap-x-2">
|
|
279
|
-
{mode === 'equation' && <Button
|
|
280
|
-
{panel && <Button
|
|
281
|
-
{g.save && <Button
|
|
282
|
-
<Button
|
|
281
|
+
{mode === 'equation' && <Button tone="ghost" size="sm" iconLeft="maximize" onClick={g.fit}>Fit</Button>}
|
|
282
|
+
{panel && <Button tone="ghost" size="sm" iconLeft="columns" onClick={() => setCollapsed(!collapsed)} className="hidden lg:inline-flex">{collapsed ? 'Expand' : 'Collapse'}</Button>}
|
|
283
|
+
{g.save && <Button tone="ghost" size="sm" iconLeft="save" onClick={g.save}>Save</Button>}
|
|
284
|
+
<Button tone="ghost" size="sm" iconLeft="refresh" onClick={mode === 'equation' ? g.reset : () => g.setAdsr(ADSR_DEFAULTS)}>Reset</Button>
|
|
283
285
|
</div>
|
|
284
286
|
<Divider className="shrink-0 py-1" />
|
|
285
287
|
{mode === 'equation' ? (
|
|
@@ -299,8 +301,11 @@ export default function EnvelopeGenerator({ generator, reference = 'panel', clas
|
|
|
299
301
|
) : (
|
|
300
302
|
<div className="flex shrink-0 flex-wrap items-end justify-between gap-4">
|
|
301
303
|
{[['attack', 'A'], ['decay', 'D'], ['sustain', 'S'], ['release', 'R']].map(([key, label]) => (
|
|
302
|
-
<Knob key={key} value={adsr[key]} onChange={(v) => g.setAdsr({ [key]: v })} label={label} size="md" defaultValue={ADSR_DEFAULTS[key]} />
|
|
304
|
+
<Knob variant="panel" key={key} value={adsr[key]} onChange={(v) => g.setAdsr({ [key]: v })} label={label} size="md" defaultValue={ADSR_DEFAULTS[key]} onHold={() => setSheet({ key, label })} />
|
|
303
305
|
))}
|
|
306
|
+
{sheet && (
|
|
307
|
+
<ParamSheet label={sheet.label} value={adsr[sheet.key]} min={0} max={100} defaultValue={ADSR_DEFAULTS[sheet.key]} onChange={(v) => g.setAdsr({ [sheet.key]: v })} onClose={() => setSheet(null)} />
|
|
308
|
+
)}
|
|
304
309
|
<Field label="Hold" value={r2(adsr.hold)} onCommit={(v) => g.setAdsr({ hold: Math.max(0, num(v, adsr.hold)) })} />
|
|
305
310
|
</div>
|
|
306
311
|
)}
|
package/src/value/Fader.jsx
CHANGED
|
@@ -1,99 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import ParamSheet from './ParamSheet.jsx'
|
|
3
|
-
import { armLongPress } from './armLongPress.js'
|
|
1
|
+
import { Slider } from '@kolkrabbi/kol-component'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* drag; on touch a 500ms hold opens `ParamSheet`.
|
|
11
|
-
*
|
|
12
|
-
* @param {number} value · onChange `(value) => void`
|
|
13
|
-
* @param {number} min (0) · max (100) · step (1)
|
|
14
|
-
* @param {string} label
|
|
15
|
-
* @param {'horizontal'|'vertical'} direction horizontal = flex-1 track + numeric readout; vertical = fixed `height`
|
|
16
|
-
* @param {number} height vertical only (default 60)
|
|
4
|
+
* @deprecated 2026-10-01 — use `<Slider variant="panel">` from kol-component. Drops when nobody imports it (04-retirements.md).
|
|
5
|
+
* Fader — A panel slider. the rack slider is Slider's panel variant now (user ruling 2026-10-01: one
|
|
6
|
+
* slider, not two that look alike). Same props: `value · onChange · min · max · step · label ·
|
|
7
|
+
* direction · height · onHold`.
|
|
17
8
|
*/
|
|
18
|
-
export default function Fader(
|
|
19
|
-
|
|
20
|
-
const vertical = direction === 'vertical'
|
|
21
|
-
const [sheet, setSheet] = useState(false)
|
|
22
|
-
|
|
23
|
-
const handlePointerDown = useCallback((e) => {
|
|
24
|
-
e.preventDefault()
|
|
25
|
-
const track = trackRef.current
|
|
26
|
-
if (!track) return
|
|
27
|
-
|
|
28
|
-
const update = (e) => {
|
|
29
|
-
const rect = track.getBoundingClientRect()
|
|
30
|
-
const ratio = vertical
|
|
31
|
-
? Math.max(0, Math.min(1, 1 - (e.clientY - rect.top) / rect.height))
|
|
32
|
-
: Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
|
33
|
-
const raw = min + ratio * (max - min)
|
|
34
|
-
onChange(Math.round(raw / step) * step)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
update(e)
|
|
38
|
-
const id = e.pointerId
|
|
39
|
-
const onMove = (e) => { if (e.pointerId === id) { hold.move(e); update(e) } } // a second finger is a pan
|
|
40
|
-
const onUp = () => {
|
|
41
|
-
hold.cancel()
|
|
42
|
-
window.removeEventListener('pointermove', onMove)
|
|
43
|
-
window.removeEventListener('pointerup', onUp)
|
|
44
|
-
}
|
|
45
|
-
// Touch: hold still 500ms → the big sheet instead of the drag
|
|
46
|
-
const hold = armLongPress(e, () => { onUp(); setSheet(true) })
|
|
47
|
-
window.addEventListener('pointermove', onMove)
|
|
48
|
-
window.addEventListener('pointerup', onUp)
|
|
49
|
-
}, [min, max, step, onChange, vertical])
|
|
50
|
-
|
|
51
|
-
const norm = max > min ? ((value ?? 0) - min) / (max - min) : 0
|
|
52
|
-
const sheetEl = sheet && (
|
|
53
|
-
<ParamSheet label={label} value={value} min={min} max={max} step={step} onChange={onChange} onClose={() => setSheet(false)} />
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
if (vertical) {
|
|
57
|
-
return (
|
|
58
|
-
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
|
|
59
|
-
{label && (
|
|
60
|
-
<span className="kol-helper-8 text-fg-32" style={{ textTransform: 'uppercase', lineHeight: 1 }}>
|
|
61
|
-
{label}
|
|
62
|
-
</span>
|
|
63
|
-
)}
|
|
64
|
-
<div
|
|
65
|
-
ref={trackRef}
|
|
66
|
-
onPointerDown={handlePointerDown}
|
|
67
|
-
className="bg-fg-16"
|
|
68
|
-
style={{ width: 2, height: height ?? 60, position: 'relative', cursor: 'ns-resize', touchAction: 'none' }}
|
|
69
|
-
>
|
|
70
|
-
<div className="bg-fg-16" style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: `${norm * 100}%` }} />
|
|
71
|
-
<div className="bg-fg-72" style={{ position: 'absolute', bottom: `calc(${norm * 100}% - 4px)`, left: -3, width: 8, height: 8, borderRadius: '50%' }} />
|
|
72
|
-
</div>
|
|
73
|
-
{sheetEl}
|
|
74
|
-
</div>
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flex: 1 }}>
|
|
80
|
-
{label && (
|
|
81
|
-
<span className="kol-helper-8 text-fg-32" style={{ textTransform: 'uppercase', lineHeight: 1, flexShrink: 0, minWidth: 20 }}>
|
|
82
|
-
{label}
|
|
83
|
-
</span>
|
|
84
|
-
)}
|
|
85
|
-
<div
|
|
86
|
-
ref={trackRef}
|
|
87
|
-
onPointerDown={handlePointerDown}
|
|
88
|
-
className="bg-fg-16"
|
|
89
|
-
style={{ flex: 1, height: 2, position: 'relative', cursor: 'pointer', touchAction: 'none' }}
|
|
90
|
-
>
|
|
91
|
-
<div className="bg-fg-72" style={{ position: 'absolute', left: `calc(${norm * 100}% - 4px)`, top: -3, width: 8, height: 8, borderRadius: '50%' }} />
|
|
92
|
-
</div>
|
|
93
|
-
<span className="kol-helper-8 text-fg-32" style={{ lineHeight: 1, flexShrink: 0, minWidth: 16, textAlign: 'right' }}>
|
|
94
|
-
{Math.round(value ?? 0)}
|
|
95
|
-
</span>
|
|
96
|
-
{sheetEl}
|
|
97
|
-
</div>
|
|
98
|
-
)
|
|
9
|
+
export default function Fader(props) {
|
|
10
|
+
return <Slider variant="panel" {...props} />
|
|
99
11
|
}
|
package/src/value/Knob.jsx
CHANGED
|
@@ -1,130 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import ParamSheet from './ParamSheet.jsx'
|
|
3
|
-
import { armLongPress } from './armLongPress.js'
|
|
4
|
-
|
|
5
|
-
const SIZES = { sm: 24, md: 32, lg: 40, xl: 64 }
|
|
1
|
+
import { Knob as KolKnob } from '@kolkrabbi/kol-component'
|
|
6
2
|
|
|
7
3
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @param {number} min · max default 0–100, or −100–100 when `bipolar`
|
|
14
|
-
* @param {string} label `kol-helper-8`, uppercase
|
|
15
|
-
* @param {'column'|'row'|'row-left'|'row-right'} variant label placement (default column)
|
|
16
|
-
* @param {boolean} bipolar −/+ legend and a centred default
|
|
17
|
-
* @param {number} labelMinWidth row-right only
|
|
18
|
-
* @param {'sm'|'md'|'lg'|'xl'} size 24 · 32 · 40 · 64 (default sm)
|
|
19
|
-
* @param {number} defaultValue ⌥-click / sheet reset target (default 50, or 0 bipolar)
|
|
4
|
+
* @deprecated 2026-10-01 — use `<Knob variant="panel">` from kol-component. Drops when nobody imports it (04-retirements.md).
|
|
5
|
+
* Knob — A rotary knob. there is one Knob and it is kol-component's (user rulings 2026-10-01 · 02);
|
|
6
|
+
* this is its panel variant under the import path the racks already use. One prop is renamed on the
|
|
7
|
+
* replacement: this `variant` (the label placement — `column · row · row-left · row-right`) is
|
|
8
|
+
* `labelPlacement` there.
|
|
20
9
|
*/
|
|
21
|
-
export default function Knob({
|
|
22
|
-
|
|
23
|
-
const effMin = min ?? (bipolar ? -100 : 0)
|
|
24
|
-
const effMax = max ?? 100
|
|
25
|
-
const effDefault = defaultValue ?? (bipolar ? 0 : 50)
|
|
26
|
-
const angle = ((value - effMin) / (effMax - effMin)) * 270 - 135
|
|
27
|
-
const r = size / 2
|
|
28
|
-
const ir = r * 0.56
|
|
29
|
-
const [sheet, setSheet] = useState(false)
|
|
30
|
-
|
|
31
|
-
const handlePointerDown = useCallback((e) => {
|
|
32
|
-
e.preventDefault()
|
|
33
|
-
// Alt/Option + click resets to default instead of starting a drag
|
|
34
|
-
if (e.altKey) {
|
|
35
|
-
onChange(effDefault)
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
const startY = e.clientY
|
|
39
|
-
const startVal = value
|
|
40
|
-
const range = effMax - effMin
|
|
41
|
-
|
|
42
|
-
const id = e.pointerId
|
|
43
|
-
const handleMove = (e) => {
|
|
44
|
-
if (e.pointerId !== id) return // a second finger is a pan, not this knob
|
|
45
|
-
hold.move(e)
|
|
46
|
-
const delta = (startY - e.clientY) * (range / 200)
|
|
47
|
-
const next = Math.round(Math.max(effMin, Math.min(effMax, startVal + delta)))
|
|
48
|
-
onChange(next)
|
|
49
|
-
}
|
|
50
|
-
const handleUp = () => {
|
|
51
|
-
hold.cancel()
|
|
52
|
-
window.removeEventListener('pointermove', handleMove)
|
|
53
|
-
window.removeEventListener('pointerup', handleUp)
|
|
54
|
-
}
|
|
55
|
-
// Touch: hold still 500ms → the big sheet instead of the drag
|
|
56
|
-
const hold = armLongPress(e, () => { handleUp(); setSheet(true) })
|
|
57
|
-
window.addEventListener('pointermove', handleMove)
|
|
58
|
-
window.addEventListener('pointerup', handleUp)
|
|
59
|
-
}, [value, effMin, effMax, effDefault, onChange])
|
|
60
|
-
|
|
61
|
-
const sheetEl = sheet && (
|
|
62
|
-
<ParamSheet label={label} value={value} min={effMin} max={effMax} defaultValue={effDefault} onChange={onChange} onClose={() => setSheet(false)} />
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
const knobSvg = (
|
|
66
|
-
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
|
67
|
-
<circle cx={r} cy={r} r={r * 0.75} style={{ fill: 'var(--kol-ctl-hw-cap)', stroke: 'var(--kol-fg-24)' }} strokeWidth="1" />
|
|
68
|
-
<line
|
|
69
|
-
x1={r} y1={r}
|
|
70
|
-
x2={r + ir * Math.cos((angle - 90) * Math.PI / 180)}
|
|
71
|
-
y2={r + ir * Math.sin((angle - 90) * Math.PI / 180)}
|
|
72
|
-
style={{ stroke: 'var(--kol-ctl-hw-on-cap)' }} strokeWidth="1.5" strokeLinecap="round"
|
|
73
|
-
/>
|
|
74
|
-
</svg>
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
if (variant === 'row-left' || variant === 'row') {
|
|
78
|
-
return (
|
|
79
|
-
<div
|
|
80
|
-
onPointerDown={handlePointerDown}
|
|
81
|
-
style={{ cursor: 'ns-resize', touchAction: 'none', display: 'flex', alignItems: 'center', gap: 4 }}
|
|
82
|
-
>
|
|
83
|
-
{label && (
|
|
84
|
-
<span className="kol-helper-8 text-fg-32" style={{ textTransform: 'uppercase', lineHeight: 1 }}>
|
|
85
|
-
{label}
|
|
86
|
-
</span>
|
|
87
|
-
)}
|
|
88
|
-
{knobSvg}
|
|
89
|
-
{sheetEl}
|
|
90
|
-
</div>
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (variant === 'row-right') {
|
|
95
|
-
return (
|
|
96
|
-
<div
|
|
97
|
-
onPointerDown={handlePointerDown}
|
|
98
|
-
style={{ cursor: 'ns-resize', touchAction: 'none', display: 'flex', alignItems: 'center', gap: 4 }}
|
|
99
|
-
>
|
|
100
|
-
{knobSvg}
|
|
101
|
-
{label && (
|
|
102
|
-
<span className="kol-helper-8 text-fg-32" style={{ textTransform: 'uppercase', lineHeight: 1, minWidth: labelMinWidth }}>
|
|
103
|
-
{label}
|
|
104
|
-
</span>
|
|
105
|
-
)}
|
|
106
|
-
{sheetEl}
|
|
107
|
-
</div>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<div
|
|
113
|
-
onPointerDown={handlePointerDown}
|
|
114
|
-
style={{ cursor: 'ns-resize', touchAction: 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}
|
|
115
|
-
>
|
|
116
|
-
{bipolar && (
|
|
117
|
-
<span className="kol-helper-8 text-fg-32" style={{ lineHeight: 1 }}>
|
|
118
|
-
-/+
|
|
119
|
-
</span>
|
|
120
|
-
)}
|
|
121
|
-
{knobSvg}
|
|
122
|
-
{label && (
|
|
123
|
-
<span className="kol-helper-8 text-fg-32" style={{ textTransform: 'uppercase', lineHeight: 1 }}>
|
|
124
|
-
{label}
|
|
125
|
-
</span>
|
|
126
|
-
)}
|
|
127
|
-
{sheetEl}
|
|
128
|
-
</div>
|
|
129
|
-
)
|
|
10
|
+
export default function Knob({ variant, size = 'sm', ...props }) {
|
|
11
|
+
return <KolKnob variant="panel" labelPlacement={variant} size={size} {...props} />
|
|
130
12
|
}
|
package/src/value/ParamSheet.jsx
CHANGED
|
@@ -3,7 +3,7 @@ import { useEffect } from 'react'
|
|
|
3
3
|
import { Slider } from '@kolkrabbi/kol-component'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* ParamSheet — a knob or fader, big, at the bottom of the screen. Opened by a
|
|
6
|
+
* ParamSheet — A big control at the screen's foot. a knob or fader, big, at the bottom of the screen. Opened by a
|
|
7
7
|
* long-press on the control (touch only): a rack knob is 24px and a finger is
|
|
8
8
|
* not, so precision on a phone comes from a full-width track, not from zoom
|
|
9
9
|
* (user, 2026-09-01: "if sometimes things are hard to reach"). Long-press, not
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// armLongPress — the hold that opens a ParamSheet (Knob + Fader; kol-monitor 2026-09-01).
|
|
2
|
-
|
|
3
|
-
const HOLD_MS = 500
|
|
4
|
-
const SLOP_PX = 6
|
|
5
|
-
|
|
6
|
-
/* Arm a long-press on a touch pointerdown. Returns `{ move, cancel }`: feed
|
|
7
|
-
* pointermoves to `move` (past SLOP it disarms — that is a drag, not a hold),
|
|
8
|
-
* call `cancel` on pointerup. `fire` runs once when the hold lands. A mouse
|
|
9
|
-
* never arms — desktop keeps drag + ⌥-click and nothing else. */
|
|
10
|
-
export function armLongPress(e, fire) {
|
|
11
|
-
if (e.pointerType !== 'touch') return { move: () => {}, cancel: () => {} }
|
|
12
|
-
const x = e.clientX, y = e.clientY
|
|
13
|
-
let t = setTimeout(() => { t = null; fire() }, HOLD_MS)
|
|
14
|
-
const cancel = () => { if (t) { clearTimeout(t); t = null } }
|
|
15
|
-
const move = (ev) => { if (t && Math.hypot(ev.clientX - x, ev.clientY - y) > SLOP_PX) cancel() }
|
|
16
|
-
return { move, cancel }
|
|
17
|
-
}
|