@kolkrabbi/kol-hardware 0.8.0 → 0.9.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 +1 -1
- package/src/panel/TimeStrip.jsx +84 -38
package/package.json
CHANGED
package/src/panel/TimeStrip.jsx
CHANGED
|
@@ -23,24 +23,31 @@ import { useTransport, useTransportNow } from './useTransport.js'
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
/* The visible span: the loop window's far edge, at least four seconds, and on a
|
|
26
|
-
free-running clock it grows in four-second steps to keep the head on screen.
|
|
27
|
-
|
|
26
|
+
free-running clock it grows in four-second steps to keep the head on screen.
|
|
27
|
+
`span="loop"` (TimeStripInADock, kol-fxr 2026-10-28): the loop window exactly, so a lane's `t` 0..1 is the full width;
|
|
28
|
+
`span={seconds}`: that many seconds from 0. Returns [origin, span] — origin is where the left edge sits, in seconds. */
|
|
29
|
+
function viewOf(t, now, mode = 'auto') {
|
|
30
|
+
if (mode === 'loop') {
|
|
31
|
+
const a = Math.min(t.loopIn, t.loopOut)
|
|
32
|
+
return [a, Math.max(Math.max(t.loopIn, t.loopOut) - a, 1e-6)]
|
|
33
|
+
}
|
|
34
|
+
if (typeof mode === 'number' && mode > 0) return [0, mode]
|
|
28
35
|
const far = Math.max(t.loopIn, t.loopOut, 4)
|
|
29
36
|
const free = t.mode === 'forward' && !t.looping
|
|
30
|
-
return free && now > far ? Math.ceil(now / 4) * 4 : far
|
|
37
|
+
return [0, free && now > far ? Math.ceil(now / 4) * 4 : far]
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
/* The bar itself — the loop window shaded, IN marked in the accent, the head. */
|
|
34
|
-
function Bar({ transport, t, now, span, height = 14, marker = false }) {
|
|
41
|
+
function Bar({ transport, t, now, span, origin = 0, height = 14, marker = false }) {
|
|
35
42
|
const ref = useRef(null)
|
|
36
43
|
const drag = useRef(false)
|
|
37
44
|
const at = (e) => {
|
|
38
45
|
const box = ref.current.getBoundingClientRect()
|
|
39
|
-
seekToSeconds(transport, Math.max(0, Math.min(1, (e.clientX - box.left) / box.width)) * span)
|
|
46
|
+
seekToSeconds(transport, origin + Math.max(0, Math.min(1, (e.clientX - box.left) / box.width)) * span)
|
|
40
47
|
}
|
|
41
48
|
const a = Math.min(t.loopIn, t.loopOut)
|
|
42
49
|
const b = Math.max(t.loopIn, t.loopOut)
|
|
43
|
-
const pct = (x) => `${Math.max(0, Math.min(100, (x / span) * 100))}%`
|
|
50
|
+
const pct = (x) => `${Math.max(0, Math.min(100, ((x - origin) / span) * 100))}%`
|
|
44
51
|
return (
|
|
45
52
|
<div
|
|
46
53
|
ref={ref}
|
|
@@ -54,7 +61,7 @@ function Bar({ transport, t, now, span, height = 14, marker = false }) {
|
|
|
54
61
|
{/* the loop window — lit when the clock wraps inside it */}
|
|
55
62
|
<div
|
|
56
63
|
className="absolute top-0 bottom-0"
|
|
57
|
-
style={{ left: pct(a), width:
|
|
64
|
+
style={{ left: pct(a), width: `${Math.max(0, Math.min(100, ((b - a) / span) * 100))}%`, background: t.looping || t.mode !== 'forward' ? 'var(--kol-fg-24)' : 'var(--kol-fg-12)' }}
|
|
58
65
|
/>
|
|
59
66
|
{marker && (
|
|
60
67
|
<div className="absolute top-0 bottom-0" style={{ left: pct(a), width: 1, background: 'var(--kol-accent-primary)' }}>
|
|
@@ -74,48 +81,87 @@ export function Scrubber({ transport, width = 232 }) {
|
|
|
74
81
|
const now = useTransportNow(transport)
|
|
75
82
|
return (
|
|
76
83
|
<div style={{ width }}>
|
|
77
|
-
<Bar transport={transport} t={t} now={now} span={
|
|
84
|
+
<Bar transport={transport} t={t} now={now} span={viewOf(t, now)[1]} height={10} />
|
|
78
85
|
</div>
|
|
79
86
|
)
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
/** The studio's bottom strip — hidden by default, `B` shows it. `children` render under the bar, on its span (lanes)
|
|
83
|
-
|
|
89
|
+
/** The studio's bottom strip — hidden by default, `B` shows it. `children` render under the bar, on its span (lanes); as a
|
|
90
|
+
* function they get `{ now, span, origin, state }`.
|
|
91
|
+
*
|
|
92
|
+
* For a host that frames it (TimeStripInADock, kol-fxr 2026-10-28 — fxr's TimelineDock is an in-flow panel):
|
|
93
|
+
* framed={false} in flow — no absolute position, border, padding, background or z; the host frames it
|
|
94
|
+
* span="loop" the loop window exactly (or `span={seconds}`); default `"auto"` is mirror's
|
|
95
|
+
* gutter={px} a left column that wide: the readout sits in it, the ruler and bar start after it, and `children`
|
|
96
|
+
* render full width beneath — so a lane's own label column lines up with the ruler
|
|
97
|
+
*/
|
|
98
|
+
export default function TimeStrip({ transport, onClose, children, framed = true, span: spanMode = 'auto', gutter }) {
|
|
84
99
|
const t = useTransport(transport)
|
|
85
100
|
const now = useTransportNow(transport)
|
|
86
|
-
const span =
|
|
87
|
-
const
|
|
101
|
+
const [origin, span] = viewOf(t, now, spanMode)
|
|
102
|
+
const first = Math.ceil(origin)
|
|
103
|
+
const ticks = Array.from({ length: Math.max(0, Math.floor(origin + span) - first + 1) }, (_, i) => first + i)
|
|
104
|
+
const x = (s) => ((s - origin) / span) * 100
|
|
105
|
+
const loopOn = t.looping || t.mode !== 'forward'
|
|
106
|
+
const lanes = typeof children === 'function' ? children({ now, span, origin, state: t }) : children
|
|
107
|
+
const ruler = (
|
|
108
|
+
<div className="relative w-full" style={{ height: 12 }}>
|
|
109
|
+
{ticks.map((s) => (
|
|
110
|
+
<span key={s} className="absolute text-fg-32" style={{ left: `${x(s)}%`, transform: s > origin ? 'translateX(-50%)' : undefined }}>
|
|
111
|
+
{span <= 16 || s % 4 === 0 ? s : ''}
|
|
112
|
+
</span>
|
|
113
|
+
))}
|
|
114
|
+
<div className="absolute" style={{ left: `${Math.max(0, Math.min(100, x(now)))}%`, top: 0, bottom: -4, width: 1, background: 'var(--kol-fg-96)' }} />
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
const bar = <Bar transport={transport} t={t} now={now} span={span} origin={origin} height={16} marker />
|
|
118
|
+
const readout = <span>{now.toFixed(2)} / {span.toFixed(2)} s</span>
|
|
119
|
+
const loopLabel = <span className="text-fg-96">Loop {loopOn ? 'on' : 'off'}</span>
|
|
120
|
+
const frame = framed
|
|
121
|
+
? { className: 'absolute left-0 right-0 bottom-0 flex items-stretch gap-4 px-4 py-3 border-t border-fg-08 kol-helper-10 group', style: { background: 'var(--kol-surface-primary)', zIndex: 'var(--kol-z-overlay)' } }
|
|
122
|
+
: { className: 'relative flex items-stretch gap-4 kol-helper-10 group', style: undefined }
|
|
123
|
+
const close = onClose && <button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={onClose}
|
|
126
|
+
className="self-start text-fg-32 hover:text-fg-96 opacity-0 group-hover:opacity-100 cursor-pointer"
|
|
127
|
+
aria-label="Hide timeline"
|
|
128
|
+
title="Hide timeline (B)"
|
|
129
|
+
>
|
|
130
|
+
×
|
|
131
|
+
</button>
|
|
132
|
+
|
|
133
|
+
if (gutter != null) {
|
|
134
|
+
const cell = { width: gutter, fontVariantNumeric: 'tabular-nums' }
|
|
135
|
+
return (
|
|
136
|
+
<div className={frame.className} style={frame.style}>
|
|
137
|
+
<div className="flex flex-col flex-1 gap-2" style={{ minWidth: 0 }}>
|
|
138
|
+
<div className="flex items-end">
|
|
139
|
+
<div className="shrink-0 text-fg-64" style={cell}>{readout}</div>
|
|
140
|
+
<div className="flex-1" style={{ minWidth: 0 }}>{ruler}</div>
|
|
141
|
+
</div>
|
|
142
|
+
<div className="flex items-center">
|
|
143
|
+
<div className="shrink-0" style={cell}>{loopLabel}</div>
|
|
144
|
+
<div className="flex-1" style={{ minWidth: 0 }}>{bar}</div>
|
|
145
|
+
</div>
|
|
146
|
+
{lanes}
|
|
147
|
+
</div>
|
|
148
|
+
{close}
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
88
153
|
return (
|
|
89
|
-
<div
|
|
90
|
-
className="absolute left-0 right-0 bottom-0 flex items-stretch gap-4 px-4 py-3 border-t border-fg-08 kol-helper-10 group"
|
|
91
|
-
style={{ background: 'var(--kol-surface-primary)', zIndex: 'var(--kol-z-overlay)' }}
|
|
92
|
-
>
|
|
154
|
+
<div className={frame.className} style={frame.style}>
|
|
93
155
|
<div className="flex flex-col justify-between shrink-0 text-fg-64" style={{ width: 112, fontVariantNumeric: 'tabular-nums' }}>
|
|
94
|
-
|
|
95
|
-
|
|
156
|
+
{readout}
|
|
157
|
+
{loopLabel}
|
|
96
158
|
</div>
|
|
97
159
|
<div className="flex flex-col flex-1 gap-2" style={{ minWidth: 0 }}>
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<span key={s} className="absolute text-fg-32" style={{ left: `${(s / span) * 100}%`, transform: s ? 'translateX(-50%)' : undefined }}>
|
|
102
|
-
{span <= 16 || s % 4 === 0 ? s : ''}
|
|
103
|
-
</span>
|
|
104
|
-
))}
|
|
105
|
-
<div className="absolute" style={{ left: `${Math.min(100, (now / span) * 100)}%`, top: 0, bottom: -4, width: 1, background: 'var(--kol-fg-96)' }} />
|
|
106
|
-
</div>
|
|
107
|
-
<Bar transport={transport} t={t} now={now} span={span} height={16} marker />
|
|
108
|
-
{typeof children === 'function' ? children({ now, span, state: t }) : children}
|
|
160
|
+
{ruler}
|
|
161
|
+
{bar}
|
|
162
|
+
{lanes}
|
|
109
163
|
</div>
|
|
110
|
-
{
|
|
111
|
-
type="button"
|
|
112
|
-
onClick={onClose}
|
|
113
|
-
className="self-start text-fg-32 hover:text-fg-96 opacity-0 group-hover:opacity-100 cursor-pointer"
|
|
114
|
-
aria-label="Hide timeline"
|
|
115
|
-
title="Hide timeline (B)"
|
|
116
|
-
>
|
|
117
|
-
×
|
|
118
|
-
</button>}
|
|
164
|
+
{close}
|
|
119
165
|
</div>
|
|
120
166
|
)
|
|
121
167
|
}
|