@kolkrabbi/kol-hardware 0.3.1 → 0.4.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/README.md CHANGED
@@ -47,7 +47,7 @@ Hardware is theme-INVARIANT — a knob cap is black on a white panel too — so
47
47
  --kol-ctl-signal-input · --kol-ctl-cv-attenuate
48
48
  ```
49
49
 
50
- `JackSocket` reads its colour role as a HEX off `:root` (it appends a hex alpha for the glow), so a consumer overriding a role binds a hex, never a `var()`.
50
+ `JackSocket` reads its color role as a HEX off `:root` (it appends a hex alpha for the glow), so a consumer overriding a role binds a hex, never a `var()`.
51
51
 
52
52
  ## Seams
53
53
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-hardware",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
- "description": "KOL hardware — panel controls for instruments (mixers, synths, racks, decks), in five groups: value (knob, fader, the touch ParamSheet), switches (toggle, flip, rocker, icon button), indicators (LED, jacks), panel (label, module header), frames (module frame, channel strip, flip card). Plus the signal engine (./signal): one expression compiler and ADSR for every scope and envelope in the estate. Renamed from @kolkrabbi/kol-controls. Sits above @kolkrabbi/kol-{theme,icons,component}.",
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.156.0",
16
+ "@kolkrabbi/kol-component": ">=0.237.0",
17
17
  "@kolkrabbi/kol-icons": ">=0.25.0",
18
- "@kolkrabbi/kol-theme": ">=0.124.0",
18
+ "@kolkrabbi/kol-theme": ">=0.163.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.229.0",
24
- "@kolkrabbi/kol-theme": "^0.156.0",
25
- "@kolkrabbi/kol-icons": "^0.29.0"
23
+ "@kolkrabbi/kol-component": "^0.237.0",
24
+ "@kolkrabbi/kol-icons": "^0.33.0",
25
+ "@kolkrabbi/kol-theme": "^0.163.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.
@@ -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
@@ -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 · the touch ParamSheet
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
- export { armLongPress } from './value/armLongPress.js'
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'
@@ -1,6 +1,6 @@
1
1
  import { useRef, useEffect } from 'react'
2
2
 
3
- /* Read a colour binding once, lazily — the roles live in kol-theme's
3
+ /* Read a color binding once, lazily — the roles live in kol-theme's
4
4
  * kol-components-controls.css. HEX is required here: the ring appends a hex
5
5
  * alpha (`${color}${aa}`), so these must resolve to hex on :root, never var(). */
6
6
  const _hex = {}
@@ -28,12 +28,12 @@ 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
35
35
  * the pending cable are the CONSUMER'S: they arrive as the props below and
36
- * `onPointerDown`. Colour role: `color` is a HEX (the glow appends a hex alpha);
36
+ * `onPointerDown`. Color role: `color` is a HEX (the glow appends a hex alpha);
37
37
  * default is the LED red; the consumer passes `--kol-ctl-signal-input` /
38
38
  * `--kol-ctl-cv-attenuate` for its primary / attenuate inputs.
39
39
  *
@@ -45,7 +45,7 @@ function stopJackLoop() {
45
45
  * @param {boolean} pending this output is the pending cable's source
46
46
  * @param {boolean} dimPending a cable is pending elsewhere (an input shows a dim ring)
47
47
  * @param {boolean} cablesHidden the consumer hides cables — a connected hole fills solid
48
- * @param {string} color HEX role colour (default `--kol-ctl-led-red`)
48
+ * @param {string} color HEX role color (default `--kol-ctl-led-red`)
49
49
  * @param {object} signalRef `{ current }` — the live signal for the glow
50
50
  * @param {boolean} bg the fg-04 plate behind the ring (default: outputs only)
51
51
  * @param {Function} onPointerDown the consumer's routing gesture
@@ -1,4 +1,4 @@
1
- /* The set's own LED colours (`--kol-ctl-led-*`) — a module that needs a
1
+ /* The set's own LED colors (`--kol-ctl-led-*`) — a module that needs a
2
2
  * red/green/yellow uses <LED color="…"> or the same token, never a hex. */
3
3
  const COLORS = {
4
4
  red: 'var(--kol-ctl-led-red)',
@@ -12,12 +12,12 @@ 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 ·
16
- * md 8; colours red · yellow · green · white · blue, or any CSS colour. With
15
+ * LED — An indicator lamp. the indicator lamp (kol-monitor's rack, lifted 2026-09-01). Sizes sm 6 ·
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
  *
19
19
  * @param {boolean} active
20
- * @param {string} color a name above or a CSS colour (default red)
20
+ * @param {string} color a name above or a CSS color (default red)
21
21
  * @param {'sm'|'md'|number} size
22
22
  * @param {Function} onClick
23
23
  */
@@ -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.
@@ -1,28 +1,10 @@
1
+ import { LabeledControl } from '@kolkrabbi/kol-component'
2
+
1
3
  /**
2
- * PanelLabel — wraps any control with the panel's text label (kol-monitor's
3
- * rack `LabeledControl`, lifted 2026-09-01; named `PanelLabel` because
4
- * `LabeledControl` is kol-component's settings-row control). Matches Knob /
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({ label, horizontal = false, labelPosition = 'bottom', labelClass = 'kol-helper-8', gap = 0, children }) {
15
- const isRow = horizontal || labelPosition === 'left' || labelPosition === 'right'
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 variant="grey" size={size} pressed={open}>{label}</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.
@@ -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.
@@ -16,7 +16,7 @@ const BLINK_MS_DEFAULT = 500
16
16
  * @param {'sm'|'md'} size 8 · 12 (default md)
17
17
  * @param {number} padding hit padding (default 4)
18
18
  * @param {boolean} momentary
19
- * @param {string} color CSS colour (default `var(--kol-ctl-led-red)`)
19
+ * @param {string} color CSS color (default `var(--kol-ctl-led-red)`)
20
20
  * @param {Function} onLongPress
21
21
  * @param {boolean} blink · {number} blinkPeriodMs (500)
22
22
  * @param {boolean} forceLit
@@ -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 Knob from './Knob.jsx'
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 variant="ghost" size="sm" iconOnly={g.playing ? 'pause' : 'play'} aria-label={g.playing ? 'Pause' : 'Play'} onClick={() => g.setPlaying(!g.playing)} />
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 variant="ghost" size="sm" onClick={g.fire}>Trigger</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 variant="ghost" size="sm" iconLeft="maximize" onClick={g.fit}>Fit</Button>}
280
- {panel && <Button variant="ghost" size="sm" iconLeft="columns" onClick={() => setCollapsed(!collapsed)} className="hidden lg:inline-flex">{collapsed ? 'Expand' : 'Collapse'}</Button>}
281
- {g.save && <Button variant="ghost" size="sm" iconLeft="save" onClick={g.save}>Save</Button>}
282
- <Button variant="ghost" size="sm" iconLeft="refresh" onClick={mode === 'equation' ? g.reset : () => g.setAdsr(ADSR_DEFAULTS)}>Reset</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
  )}
@@ -1,99 +1,11 @@
1
- import { useRef, useCallback, useState } from 'react'
2
- import ParamSheet from './ParamSheet.jsx'
3
- import { armLongPress } from './armLongPress.js'
1
+ import { Slider } from '@kolkrabbi/kol-component'
4
2
 
5
3
  /**
6
- * Fader — the rack slider (kol-monitor's panel `Slider`, lifted 2026-09-01;
7
- * named `Fader` because `Slider` is kol-component's app control and the two are
8
- * different tiers on purpose — a 2px track on a 24px eurorack panel is not app
9
- * chrome). Same conventions as Knob: `kol-helper-8` label, inline styles, pointer
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({ value, onChange, min = 0, max = 100, step = 1, label, direction = 'horizontal', height }) {
19
- const trackRef = useRef(null)
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
  }
@@ -1,130 +1,12 @@
1
- import { useCallback, useState } from 'react'
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
- * Knob — SVG rotary knob with drag-to-change (kol-monitor's rack, lifted
9
- * 2026-09-01). 270° sweep; drag ns, 200px of travel = the range; ⌥-click resets
10
- * to `defaultValue`; on touch a 500ms hold opens `ParamSheet` instead of the drag.
11
- *
12
- * @param {number} value · onChange `(value) => void`
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({ value, onChange, min, max, label, variant = 'column', bipolar = false, labelMinWidth, size: sizeProp = 'sm', defaultValue }) {
22
- const size = SIZES[sizeProp] || SIZES.sm
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
  }
@@ -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
- }