@zylem/ui 0.8.8 → 0.8.11
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/dist/chunk-V47CGFWX.js +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +15 -0
- package/dist/index.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.js +1 -1
- package/package.json +22 -11
- package/src/components/Accordion/Accordion.tsx +10 -1
- package/src/components/AddTile/AddTile.css.ts +95 -0
- package/src/components/AddTile/AddTile.tsx +45 -0
- package/src/components/BuildLabel/BuildLabel.css.ts +31 -0
- package/src/components/BuildLabel/BuildLabel.tsx +37 -0
- package/src/components/BuildLabel/mount-build-label.ts +77 -0
- package/src/components/BuildLabel/resolve-build-version.ts +47 -0
- package/src/components/Callout/Callout.css.ts +45 -0
- package/src/components/Callout/Callout.tsx +32 -0
- package/src/components/Card/Card.css.ts +18 -0
- package/src/components/Card/Card.tsx +25 -2
- package/src/components/ColorPalette/ColorPalette.css.ts +51 -0
- package/src/components/ColorPalette/ColorPalette.tsx +78 -0
- package/src/components/Dialog/Dialog.css.ts +69 -2
- package/src/components/Dialog/Dialog.tsx +35 -7
- package/src/components/DropdownMenu/DropdownMenu.css.ts +22 -0
- package/src/components/DropdownMenu/DropdownMenu.tsx +67 -20
- package/src/components/EmptyState/EmptyState.css.ts +9 -0
- package/src/components/EmptyState/EmptyState.tsx +3 -1
- package/src/components/FrameStrip/FrameStrip.css.ts +138 -0
- package/src/components/FrameStrip/FrameStrip.tsx +146 -0
- package/src/components/Menubar/Menubar.css.ts +52 -0
- package/src/components/Menubar/Menubar.tsx +77 -0
- package/src/components/Mixer/Knob.tsx +188 -0
- package/src/components/Mixer/Mixer.css.ts +246 -0
- package/src/components/Mixer/MixerStrip.tsx +150 -0
- package/src/components/NumberField/NumberField.tsx +5 -1
- package/src/components/Panel/Panel.tsx +6 -5
- package/src/components/PianoRoll/PianoRoll.css.ts +224 -0
- package/src/components/PianoRoll/PianoRoll.tsx +353 -0
- package/src/components/PianoRoll/piano-roll-math.ts +89 -0
- package/src/components/PixelGrid/PixelGrid.css.ts +30 -0
- package/src/components/PixelGrid/PixelGrid.tsx +48 -0
- package/src/components/PlaybackControls/PlaybackControls.css.ts +53 -0
- package/src/components/PlaybackControls/PlaybackControls.tsx +80 -0
- package/src/components/Property/Property.css.ts +1 -1
- package/src/components/Property/Property.tsx +3 -0
- package/src/components/Sidebar/Sidebar.css.ts +8 -0
- package/src/components/Sidebar/Sidebar.tsx +10 -2
- package/src/components/Slider/Slider.tsx +6 -0
- package/src/components/SpriteSizePicker/SpriteSizePicker.css.ts +103 -0
- package/src/components/SpriteSizePicker/SpriteSizePicker.tsx +172 -0
- package/src/components/SpriteSizePicker/snap-size.ts +28 -0
- package/src/components/Tabs/Tabs.css.ts +48 -0
- package/src/components/Tabs/Tabs.tsx +41 -3
- package/src/components/TextField/TextField.tsx +49 -5
- package/src/components/TimelineLanes/TimelineLanes.css.ts +365 -0
- package/src/components/TimelineLanes/TimelineLanes.tsx +509 -0
- package/src/components/TimelineLanes/lanes-math.ts +182 -0
- package/src/components/TimelineTrack/TimelineTrack.css.ts +163 -0
- package/src/components/TimelineTrack/TimelineTrack.tsx +90 -0
- package/src/components/TimelineTrack/timeline-math.ts +43 -0
- package/src/components/Toast/Toast.css.ts +48 -0
- package/src/components/Toast/Toast.tsx +37 -0
- package/src/components/ToolButton/ToolButton.tsx +92 -0
- package/src/components/ToolGroup/ToolGroup.css.ts +35 -0
- package/src/components/ToolGroup/ToolGroup.tsx +29 -0
- package/src/components/ToolboxButton/ToolboxButton.css.ts +131 -0
- package/src/components/ToolboxButton/ToolboxButton.tsx +88 -0
- package/src/components/TrackHeader/TrackHeader.css.ts +162 -0
- package/src/components/TrackHeader/TrackHeader.tsx +145 -0
- package/src/components/TransportBar/TransportBar.css.ts +65 -0
- package/src/components/TransportBar/TransportBar.tsx +154 -0
- package/src/components/Waveform/Waveform.css.ts +77 -0
- package/src/components/Waveform/Waveform.tsx +195 -0
- package/src/components/Waveform/peaks.ts +49 -0
- package/src/components/index.ts +122 -0
- package/src/global/accordion.css.ts +23 -0
- package/src/global/hyperglass-base.css.ts +9 -6
- package/src/styles.ts +23 -0
- package/src/theme.css.ts +15 -0
- package/vite/collect-zylem-versions.ts +82 -0
- package/vite/index.ts +30 -0
- package/dist/chunk-3TP2SNNW.js +0 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Button as KButton } from '@kobalte/core';
|
|
2
|
+
import { type JSX, Show, splitProps } from 'solid-js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tile scale. `md` is the compact rail tile; `lg` and `xl` are the extra
|
|
6
|
+
* large tiles sized for three- and two-column toolboxes respectively.
|
|
7
|
+
*/
|
|
8
|
+
export type ToolboxSize = 'md' | 'lg' | 'xl';
|
|
9
|
+
|
|
10
|
+
export interface ToolboxButtonProps {
|
|
11
|
+
label: string;
|
|
12
|
+
/** Icon rendered above the label. */
|
|
13
|
+
children: JSX.Element;
|
|
14
|
+
selected?: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Overrides the size inherited from the enclosing `ToolboxGrid`. */
|
|
17
|
+
size?: ToolboxSize;
|
|
18
|
+
onClick?: (event: MouseEvent) => void;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Large toolbox tile: the toolbar button's glass jewel scaled up, with the
|
|
24
|
+
* icon stacked over a bold label. Selected renders as the live-green state.
|
|
25
|
+
*/
|
|
26
|
+
export function ToolboxButton(props: ToolboxButtonProps) {
|
|
27
|
+
return (
|
|
28
|
+
<KButton.Root
|
|
29
|
+
class={props.class ? `zylem-toolbox-btn ${props.class}` : 'zylem-toolbox-btn'}
|
|
30
|
+
data-size={props.size}
|
|
31
|
+
data-selected={props.selected ? '' : undefined}
|
|
32
|
+
aria-pressed={props.selected ? 'true' : undefined}
|
|
33
|
+
disabled={props.disabled}
|
|
34
|
+
onClick={props.onClick}
|
|
35
|
+
>
|
|
36
|
+
<span class="zylem-toolbox-btn-icon" aria-hidden="true">
|
|
37
|
+
{props.children}
|
|
38
|
+
</span>
|
|
39
|
+
<span class="zylem-toolbox-btn-label">{props.label}</span>
|
|
40
|
+
</KButton.Root>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ToolboxGridProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
45
|
+
/** Optional uppercase caption above the grid, e.g. `Current tools`. */
|
|
46
|
+
label?: string;
|
|
47
|
+
/** Columns in the grid. Defaults to 2. */
|
|
48
|
+
columns?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Tile size for every button in the grid that doesn't set its own. `xl`
|
|
51
|
+
* pairs with two columns, `lg` with three; defaults to `md`.
|
|
52
|
+
*/
|
|
53
|
+
size?: ToolboxSize;
|
|
54
|
+
children: JSX.Element;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Captioned grid of `ToolboxButton`s. */
|
|
58
|
+
export function ToolboxGrid(props: ToolboxGridProps) {
|
|
59
|
+
const [local, rest] = splitProps(props, [
|
|
60
|
+
'label',
|
|
61
|
+
'columns',
|
|
62
|
+
'size',
|
|
63
|
+
'children',
|
|
64
|
+
'class',
|
|
65
|
+
'style',
|
|
66
|
+
]);
|
|
67
|
+
const style = (): JSX.CSSProperties => ({
|
|
68
|
+
'--zylem-toolbox-columns': String(local.columns ?? 2),
|
|
69
|
+
...(typeof local.style === 'object' ? local.style : {}),
|
|
70
|
+
});
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
class={local.class ? `zylem-toolbox-grid ${local.class}` : 'zylem-toolbox-grid'}
|
|
74
|
+
role="group"
|
|
75
|
+
aria-label={local.label}
|
|
76
|
+
data-size={local.size}
|
|
77
|
+
style={style()}
|
|
78
|
+
{...rest}
|
|
79
|
+
>
|
|
80
|
+
<Show when={local.label}>
|
|
81
|
+
<span class="zylem-toolbox-grid-label" aria-hidden="true">
|
|
82
|
+
{local.label}
|
|
83
|
+
</span>
|
|
84
|
+
</Show>
|
|
85
|
+
<div class="zylem-toolbox-grid-items">{local.children}</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { globalStyle } from '@vanilla-extract/css';
|
|
2
|
+
import { vars } from '../../theme.css';
|
|
3
|
+
|
|
4
|
+
globalStyle('.zylem-track-header', {
|
|
5
|
+
display: 'flex',
|
|
6
|
+
alignItems: 'stretch',
|
|
7
|
+
gap: vars.spacing.xs,
|
|
8
|
+
width: '100%',
|
|
9
|
+
minWidth: 0,
|
|
10
|
+
boxSizing: 'border-box',
|
|
11
|
+
padding: `0 ${vars.spacing.xs} 0 0`,
|
|
12
|
+
fontFamily: vars.typography.fontFamily,
|
|
13
|
+
color: vars.colors.text,
|
|
14
|
+
cursor: 'default',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
globalStyle('.zylem-track-header[data-disabled]', {
|
|
18
|
+
opacity: 0.6,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
globalStyle('.zylem-track-header-swatch', {
|
|
22
|
+
flex: 'none',
|
|
23
|
+
width: '4px',
|
|
24
|
+
background: 'var(--zylem-track-color)',
|
|
25
|
+
boxShadow: '0 0 6px color-mix(in srgb, var(--zylem-track-color) 60%, transparent)',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
globalStyle('.zylem-track-header-main', {
|
|
29
|
+
flex: '1 1 auto',
|
|
30
|
+
minWidth: 0,
|
|
31
|
+
display: 'flex',
|
|
32
|
+
flexDirection: 'column',
|
|
33
|
+
justifyContent: 'center',
|
|
34
|
+
gap: '1px',
|
|
35
|
+
padding: `0 ${vars.spacing.xs}`,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
globalStyle('.zylem-track-header-title', {
|
|
39
|
+
display: 'flex',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
gap: vars.spacing.xs,
|
|
42
|
+
minWidth: 0,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
globalStyle('.zylem-track-header-icon', {
|
|
46
|
+
display: 'inline-flex',
|
|
47
|
+
flex: 'none',
|
|
48
|
+
color: vars.colors.textSecondary,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
globalStyle('.zylem-track-header-icon > svg', {
|
|
52
|
+
width: '13px',
|
|
53
|
+
height: '13px',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
globalStyle('.zylem-track-header-name', {
|
|
57
|
+
fontSize: `calc(${vars.typography.fontSize} - 1px)`,
|
|
58
|
+
fontWeight: 600,
|
|
59
|
+
whiteSpace: 'nowrap',
|
|
60
|
+
overflow: 'hidden',
|
|
61
|
+
textOverflow: 'ellipsis',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
globalStyle('.zylem-track-header[data-muted] .zylem-track-header-name', {
|
|
65
|
+
color: vars.colors.textSecondary,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
globalStyle('.zylem-track-header-input', {
|
|
69
|
+
width: '100%',
|
|
70
|
+
minWidth: 0,
|
|
71
|
+
boxSizing: 'border-box',
|
|
72
|
+
padding: '1px 4px',
|
|
73
|
+
font: 'inherit',
|
|
74
|
+
fontSize: `calc(${vars.typography.fontSize} - 1px)`,
|
|
75
|
+
color: vars.colors.text,
|
|
76
|
+
background: vars.material.fieldGlass,
|
|
77
|
+
border: '1px solid rgba(97, 166, 232, 0.5)',
|
|
78
|
+
borderRadius: '4px',
|
|
79
|
+
outline: 'none',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
globalStyle('.zylem-track-header-input:focus-visible', {
|
|
83
|
+
boxShadow: vars.effects.focusRing,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
globalStyle('.zylem-track-header-detail', {
|
|
87
|
+
fontSize: '10px',
|
|
88
|
+
color: vars.colors.textSecondary,
|
|
89
|
+
whiteSpace: 'nowrap',
|
|
90
|
+
overflow: 'hidden',
|
|
91
|
+
textOverflow: 'ellipsis',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
globalStyle('.zylem-track-header-toggles', {
|
|
95
|
+
flex: 'none',
|
|
96
|
+
display: 'flex',
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
gap: '3px',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
globalStyle('.zylem-track-toggle', {
|
|
102
|
+
display: 'inline-flex',
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
justifyContent: 'center',
|
|
105
|
+
width: '18px',
|
|
106
|
+
height: '18px',
|
|
107
|
+
padding: 0,
|
|
108
|
+
fontFamily: vars.typography.fontFamily,
|
|
109
|
+
fontSize: '10px',
|
|
110
|
+
fontWeight: 700,
|
|
111
|
+
lineHeight: 1,
|
|
112
|
+
color: vars.colors.textSecondary,
|
|
113
|
+
background: 'rgba(8, 19, 31, 0.6)',
|
|
114
|
+
border: '1px solid rgba(97, 166, 232, 0.28)',
|
|
115
|
+
borderRadius: '4px',
|
|
116
|
+
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.08)',
|
|
117
|
+
cursor: 'pointer',
|
|
118
|
+
outline: 'none',
|
|
119
|
+
transition: `background ${vars.motion.fast} ${vars.motion.easeOut}, color ${vars.motion.fast} ${vars.motion.easeOut}`,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
globalStyle('.zylem-track-toggle > svg', {
|
|
123
|
+
width: '11px',
|
|
124
|
+
height: '11px',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
globalStyle('.zylem-track-toggle:hover:not(:disabled)', {
|
|
128
|
+
color: vars.colors.text,
|
|
129
|
+
background: 'rgba(97, 166, 232, 0.22)',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
globalStyle('.zylem-track-toggle:focus-visible', {
|
|
133
|
+
boxShadow: vars.effects.focusRing,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
globalStyle('.zylem-track-toggle:disabled', {
|
|
137
|
+
cursor: 'default',
|
|
138
|
+
opacity: 0.5,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
globalStyle('.zylem-track-toggle[data-kind="mute"][aria-pressed="true"]', {
|
|
142
|
+
color: '#2b1a08',
|
|
143
|
+
background: 'linear-gradient(180deg, rgba(255, 220, 130, 0.95), rgba(230, 150, 40, 0.95))',
|
|
144
|
+
borderColor: 'rgba(255, 230, 170, 0.8)',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
globalStyle('.zylem-track-toggle[data-kind="solo"][aria-pressed="true"]', {
|
|
148
|
+
color: '#062010',
|
|
149
|
+
background: 'linear-gradient(180deg, rgba(170, 255, 190, 0.95), rgba(28, 174, 63, 0.95))',
|
|
150
|
+
borderColor: 'rgba(190, 255, 203, 0.8)',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
globalStyle('.zylem-track-toggle[data-kind="lock"][aria-pressed="true"]', {
|
|
154
|
+
color: vars.colors.text,
|
|
155
|
+
background: 'rgba(97, 166, 232, 0.35)',
|
|
156
|
+
borderColor: 'rgba(178, 219, 255, 0.7)',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
globalStyle('.zylem-track-header-actions', {
|
|
160
|
+
display: 'inline-flex',
|
|
161
|
+
alignItems: 'center',
|
|
162
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import Lock from 'lucide-solid/icons/lock';
|
|
2
|
+
import LockOpen from 'lucide-solid/icons/lock-open';
|
|
3
|
+
import { createSignal, type JSX, Show } from 'solid-js';
|
|
4
|
+
|
|
5
|
+
export interface TrackHeaderProps {
|
|
6
|
+
name: string;
|
|
7
|
+
/** Swatch color shown as a bar on the left edge. */
|
|
8
|
+
color?: string;
|
|
9
|
+
/** Small glyph before the name (an instrument or camera icon). */
|
|
10
|
+
icon?: JSX.Element;
|
|
11
|
+
/** Secondary line under the name (instrument preset, camera kind). */
|
|
12
|
+
detail?: string;
|
|
13
|
+
selected?: boolean;
|
|
14
|
+
onSelect?: () => void;
|
|
15
|
+
/** Double-click the name to rename; the new name arrives here on Enter/blur. */
|
|
16
|
+
onRename?: (name: string) => void;
|
|
17
|
+
/** Mute / solo / lock toggles render only when their handler is given. */
|
|
18
|
+
muted?: boolean;
|
|
19
|
+
onToggleMute?: () => void;
|
|
20
|
+
solo?: boolean;
|
|
21
|
+
onToggleSolo?: () => void;
|
|
22
|
+
locked?: boolean;
|
|
23
|
+
onToggleLock?: () => void;
|
|
24
|
+
/** Trailing controls (a menu button, an add button). */
|
|
25
|
+
actions?: JSX.Element;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Header cell for a timeline lane: color bar, name (rename on double-click),
|
|
32
|
+
* an optional detail line, and M / S / lock toggles.
|
|
33
|
+
*/
|
|
34
|
+
export function TrackHeader(props: TrackHeaderProps) {
|
|
35
|
+
const [editing, setEditing] = createSignal(false);
|
|
36
|
+
const [draft, setDraft] = createSignal('');
|
|
37
|
+
|
|
38
|
+
const startRename = () => {
|
|
39
|
+
if (!props.onRename || props.disabled) return;
|
|
40
|
+
setDraft(props.name);
|
|
41
|
+
setEditing(true);
|
|
42
|
+
};
|
|
43
|
+
const commitRename = () => {
|
|
44
|
+
if (!editing()) return;
|
|
45
|
+
setEditing(false);
|
|
46
|
+
const next = draft().trim();
|
|
47
|
+
if (next && next !== props.name) props.onRename?.(next);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
class={props.class ? `zylem-track-header ${props.class}` : 'zylem-track-header'}
|
|
53
|
+
style={{ '--zylem-track-color': props.color ?? 'var(--zylem-color-primary)' }}
|
|
54
|
+
data-selected={props.selected ? '' : undefined}
|
|
55
|
+
data-muted={props.muted ? '' : undefined}
|
|
56
|
+
data-disabled={props.disabled ? '' : undefined}
|
|
57
|
+
onPointerDown={() => props.onSelect?.()}
|
|
58
|
+
>
|
|
59
|
+
<span class="zylem-track-header-swatch" aria-hidden="true" />
|
|
60
|
+
<div class="zylem-track-header-main">
|
|
61
|
+
<div class="zylem-track-header-title">
|
|
62
|
+
<Show when={props.icon}>
|
|
63
|
+
<span class="zylem-track-header-icon">{props.icon}</span>
|
|
64
|
+
</Show>
|
|
65
|
+
<Show
|
|
66
|
+
when={editing()}
|
|
67
|
+
fallback={
|
|
68
|
+
<span class="zylem-track-header-name" onDblClick={startRename} title={props.name}>
|
|
69
|
+
{props.name}
|
|
70
|
+
</span>
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
<input
|
|
74
|
+
class="zylem-track-header-input"
|
|
75
|
+
value={draft()}
|
|
76
|
+
aria-label="Track name"
|
|
77
|
+
ref={(element) => queueMicrotask(() => element.select())}
|
|
78
|
+
onInput={(event) => setDraft(event.currentTarget.value)}
|
|
79
|
+
onBlur={commitRename}
|
|
80
|
+
onKeyDown={(event) => {
|
|
81
|
+
if (event.key === 'Enter') commitRename();
|
|
82
|
+
if (event.key === 'Escape') setEditing(false);
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
</Show>
|
|
86
|
+
</div>
|
|
87
|
+
<Show when={props.detail}>
|
|
88
|
+
<span class="zylem-track-header-detail">{props.detail}</span>
|
|
89
|
+
</Show>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="zylem-track-header-toggles">
|
|
92
|
+
<Show when={props.onToggleMute}>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
class="zylem-track-toggle"
|
|
96
|
+
data-kind="mute"
|
|
97
|
+
aria-pressed={props.muted ? 'true' : 'false'}
|
|
98
|
+
aria-label={props.muted ? 'Unmute' : 'Mute'}
|
|
99
|
+
title={props.muted ? 'Unmute' : 'Mute'}
|
|
100
|
+
disabled={props.disabled}
|
|
101
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
102
|
+
onClick={() => props.onToggleMute?.()}
|
|
103
|
+
>
|
|
104
|
+
M
|
|
105
|
+
</button>
|
|
106
|
+
</Show>
|
|
107
|
+
<Show when={props.onToggleSolo}>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
class="zylem-track-toggle"
|
|
111
|
+
data-kind="solo"
|
|
112
|
+
aria-pressed={props.solo ? 'true' : 'false'}
|
|
113
|
+
aria-label={props.solo ? 'Unsolo' : 'Solo'}
|
|
114
|
+
title={props.solo ? 'Unsolo' : 'Solo'}
|
|
115
|
+
disabled={props.disabled}
|
|
116
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
117
|
+
onClick={() => props.onToggleSolo?.()}
|
|
118
|
+
>
|
|
119
|
+
S
|
|
120
|
+
</button>
|
|
121
|
+
</Show>
|
|
122
|
+
<Show when={props.onToggleLock}>
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
class="zylem-track-toggle"
|
|
126
|
+
data-kind="lock"
|
|
127
|
+
aria-pressed={props.locked ? 'true' : 'false'}
|
|
128
|
+
aria-label={props.locked ? 'Unlock' : 'Lock'}
|
|
129
|
+
title={props.locked ? 'Unlock' : 'Lock'}
|
|
130
|
+
disabled={props.disabled}
|
|
131
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
132
|
+
onClick={() => props.onToggleLock?.()}
|
|
133
|
+
>
|
|
134
|
+
{props.locked ? <Lock class="zylem-icon" /> : <LockOpen class="zylem-icon" />}
|
|
135
|
+
</button>
|
|
136
|
+
</Show>
|
|
137
|
+
<Show when={props.actions}>
|
|
138
|
+
<span class="zylem-track-header-actions" onPointerDown={(event) => event.stopPropagation()}>
|
|
139
|
+
{props.actions}
|
|
140
|
+
</span>
|
|
141
|
+
</Show>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { globalStyle } from '@vanilla-extract/css';
|
|
2
|
+
import { vars } from '../../theme.css';
|
|
3
|
+
|
|
4
|
+
globalStyle('.zylem-transport', {
|
|
5
|
+
display: 'flex',
|
|
6
|
+
alignItems: 'center',
|
|
7
|
+
width: '100%',
|
|
8
|
+
minWidth: 0,
|
|
9
|
+
boxSizing: 'border-box',
|
|
10
|
+
fontFamily: vars.typography.fontFamily,
|
|
11
|
+
color: vars.colors.text,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
globalStyle('.zylem-transport > .zylem-playback', {
|
|
15
|
+
width: '100%',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
globalStyle('.zylem-transport .zylem-playback-extra', {
|
|
19
|
+
flex: '1 1 auto',
|
|
20
|
+
minWidth: 0,
|
|
21
|
+
gap: vars.spacing.sm,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
globalStyle('.zylem-transport-track', {
|
|
25
|
+
flex: '1 1 auto',
|
|
26
|
+
minWidth: '120px',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
globalStyle('.zylem-transport-time', {
|
|
30
|
+
flex: 'none',
|
|
31
|
+
minWidth: '9ch',
|
|
32
|
+
fontSize: `calc(${vars.typography.fontSize} - 1px)`,
|
|
33
|
+
fontWeight: 600,
|
|
34
|
+
fontVariantNumeric: 'tabular-nums',
|
|
35
|
+
whiteSpace: 'nowrap',
|
|
36
|
+
textShadow: '0 1px 1px rgba(0,0,0,0.6)',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
globalStyle('.zylem-transport-time-total', {
|
|
40
|
+
fontWeight: 400,
|
|
41
|
+
color: vars.colors.textSecondary,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
globalStyle('.zylem-transport-field', {
|
|
45
|
+
display: 'inline-flex',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
gap: vars.spacing.xs,
|
|
48
|
+
flex: 'none',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
globalStyle('.zylem-transport-field-label', {
|
|
52
|
+
fontSize: '10px',
|
|
53
|
+
letterSpacing: '0.08em',
|
|
54
|
+
textTransform: 'uppercase',
|
|
55
|
+
color: vars.colors.textSecondary,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
globalStyle('.zylem-transport-bpm', {
|
|
59
|
+
width: '84px',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
globalStyle('.zylem-transport-signature', {
|
|
63
|
+
width: '84px',
|
|
64
|
+
flex: 'none',
|
|
65
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import Square from 'lucide-solid/icons/square';
|
|
2
|
+
import { type JSX, Show } from 'solid-js';
|
|
3
|
+
import { NumberField } from '../NumberField/NumberField';
|
|
4
|
+
import { PlaybackControls } from '../PlaybackControls/PlaybackControls';
|
|
5
|
+
import { Select } from '../Select/Select';
|
|
6
|
+
import { formatBeats, formatClock, type TimeUnit } from '../TimelineLanes/lanes-math';
|
|
7
|
+
import { TimelineTrack } from '../TimelineTrack/TimelineTrack';
|
|
8
|
+
import { ToolbarButton } from '../ToolbarButton/ToolbarButton';
|
|
9
|
+
|
|
10
|
+
export interface TransportTimeSignature {
|
|
11
|
+
numerator: number;
|
|
12
|
+
denominator: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TransportBarProps {
|
|
16
|
+
playing: boolean;
|
|
17
|
+
onTogglePlay: () => void;
|
|
18
|
+
/** Adds a Stop button (pause and return to the start). */
|
|
19
|
+
onStop?: () => void;
|
|
20
|
+
onRewind?: () => void;
|
|
21
|
+
onFastForward?: () => void;
|
|
22
|
+
loop?: boolean;
|
|
23
|
+
onToggleLoop?: () => void;
|
|
24
|
+
/** Playhead, in `unit`s. */
|
|
25
|
+
time: number;
|
|
26
|
+
/** Timeline length, in `unit`s. */
|
|
27
|
+
length: number;
|
|
28
|
+
onSeek?: (time: number) => void;
|
|
29
|
+
unit?: TimeUnit;
|
|
30
|
+
/** Beats per bar, for the beat readout. */
|
|
31
|
+
beatsPerBar?: number;
|
|
32
|
+
/** Shows a BPM field when set. */
|
|
33
|
+
bpm?: number;
|
|
34
|
+
onBpmChange?: (bpm: number) => void;
|
|
35
|
+
/** Shows a time-signature select when set. */
|
|
36
|
+
timeSignature?: TransportTimeSignature;
|
|
37
|
+
onTimeSignatureChange?: (signature: TransportTimeSignature) => void;
|
|
38
|
+
/** Extra controls after the built-in ones. */
|
|
39
|
+
children?: JSX.Element;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
class?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SIGNATURES: TransportTimeSignature[] = [
|
|
45
|
+
{ numerator: 2, denominator: 4 },
|
|
46
|
+
{ numerator: 3, denominator: 4 },
|
|
47
|
+
{ numerator: 4, denominator: 4 },
|
|
48
|
+
{ numerator: 5, denominator: 4 },
|
|
49
|
+
{ numerator: 6, denominator: 8 },
|
|
50
|
+
{ numerator: 7, denominator: 8 },
|
|
51
|
+
{ numerator: 12, denominator: 8 },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const signatureKey = (signature: TransportTimeSignature) =>
|
|
55
|
+
`${signature.numerator}/${signature.denominator}`;
|
|
56
|
+
|
|
57
|
+
/** Readout for the current position: `m:ss.ff` for seconds, `bar.beat` for beats. */
|
|
58
|
+
export function formatTransportTime(time: number, unit: TimeUnit, beatsPerBar = 4): string {
|
|
59
|
+
return unit === 'beats' ? formatBeats(time, beatsPerBar) : formatClock(time);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Transport strip for the Director and Music editors: playback controls, a
|
|
64
|
+
* scrub track with a position readout, and optional BPM / time signature.
|
|
65
|
+
*/
|
|
66
|
+
export function TransportBar(props: TransportBarProps) {
|
|
67
|
+
const unit = () => props.unit ?? 'seconds';
|
|
68
|
+
const bpb = () => props.beatsPerBar ?? props.timeSignature?.numerator ?? 4;
|
|
69
|
+
const signatureOptions = () => {
|
|
70
|
+
const options = SIGNATURES.map((signature) => ({
|
|
71
|
+
value: signatureKey(signature),
|
|
72
|
+
label: signatureKey(signature),
|
|
73
|
+
}));
|
|
74
|
+
const current = props.timeSignature ? signatureKey(props.timeSignature) : null;
|
|
75
|
+
if (current && !options.some((option) => option.value === current)) {
|
|
76
|
+
options.push({ value: current, label: current });
|
|
77
|
+
}
|
|
78
|
+
return options;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
class={props.class ? `zylem-transport ${props.class}` : 'zylem-transport'}
|
|
84
|
+
data-unit={unit()}
|
|
85
|
+
>
|
|
86
|
+
<PlaybackControls
|
|
87
|
+
playing={props.playing}
|
|
88
|
+
onTogglePlay={props.onTogglePlay}
|
|
89
|
+
onRewind={props.onRewind}
|
|
90
|
+
onFastForward={props.onFastForward}
|
|
91
|
+
loop={props.loop}
|
|
92
|
+
onToggleLoop={props.onToggleLoop}
|
|
93
|
+
disabled={props.disabled}
|
|
94
|
+
>
|
|
95
|
+
<Show when={props.onStop}>
|
|
96
|
+
<ToolbarButton
|
|
97
|
+
label="Stop"
|
|
98
|
+
disabled={props.disabled}
|
|
99
|
+
onClick={() => props.onStop?.()}
|
|
100
|
+
class="zylem-transport-stop"
|
|
101
|
+
>
|
|
102
|
+
<Square class="zylem-icon" />
|
|
103
|
+
</ToolbarButton>
|
|
104
|
+
</Show>
|
|
105
|
+
<TimelineTrack
|
|
106
|
+
value={props.time}
|
|
107
|
+
max={props.length}
|
|
108
|
+
onChange={(value) => props.onSeek?.(value)}
|
|
109
|
+
disabled={props.disabled || !props.onSeek}
|
|
110
|
+
class="zylem-transport-track"
|
|
111
|
+
/>
|
|
112
|
+
<span class="zylem-transport-time" aria-live="off">
|
|
113
|
+
{formatTransportTime(props.time, unit(), bpb())}
|
|
114
|
+
<span class="zylem-transport-time-total">
|
|
115
|
+
{' / '}
|
|
116
|
+
{formatTransportTime(props.length, unit(), bpb())}
|
|
117
|
+
</span>
|
|
118
|
+
</span>
|
|
119
|
+
<Show when={props.bpm !== undefined}>
|
|
120
|
+
<label class="zylem-transport-field">
|
|
121
|
+
<span class="zylem-transport-field-label">BPM</span>
|
|
122
|
+
<NumberField
|
|
123
|
+
value={props.bpm}
|
|
124
|
+
minValue={20}
|
|
125
|
+
maxValue={300}
|
|
126
|
+
step={1}
|
|
127
|
+
disabled={props.disabled || !props.onBpmChange}
|
|
128
|
+
onChange={(value) => props.onBpmChange?.(value)}
|
|
129
|
+
class="zylem-transport-bpm"
|
|
130
|
+
/>
|
|
131
|
+
</label>
|
|
132
|
+
</Show>
|
|
133
|
+
<Show when={props.timeSignature}>
|
|
134
|
+
{(signature) => (
|
|
135
|
+
<Select
|
|
136
|
+
options={signatureOptions()}
|
|
137
|
+
value={signatureKey(signature())}
|
|
138
|
+
disabled={props.disabled || !props.onTimeSignatureChange}
|
|
139
|
+
onChange={(value) => {
|
|
140
|
+
if (!value) return;
|
|
141
|
+
const [numerator, denominator] = value.split('/').map(Number);
|
|
142
|
+
if (numerator && denominator) {
|
|
143
|
+
props.onTimeSignatureChange?.({ numerator, denominator });
|
|
144
|
+
}
|
|
145
|
+
}}
|
|
146
|
+
class="zylem-transport-signature"
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
</Show>
|
|
150
|
+
{props.children}
|
|
151
|
+
</PlaybackControls>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { globalStyle } from '@vanilla-extract/css';
|
|
2
|
+
import { vars } from '../../theme.css';
|
|
3
|
+
|
|
4
|
+
globalStyle('.zylem-waveform', {
|
|
5
|
+
position: 'relative',
|
|
6
|
+
width: '100%',
|
|
7
|
+
minWidth: 0,
|
|
8
|
+
boxSizing: 'border-box',
|
|
9
|
+
overflow: 'hidden',
|
|
10
|
+
borderRadius: vars.radii.control,
|
|
11
|
+
border: '1px solid rgba(97, 166, 232, 0.22)',
|
|
12
|
+
background: vars.material.fieldGlass,
|
|
13
|
+
boxShadow: vars.effects.shadowInset,
|
|
14
|
+
userSelect: 'none',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
globalStyle('.zylem-waveform[data-disabled]', {
|
|
18
|
+
opacity: 0.6,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
globalStyle('.zylem-waveform-canvas', {
|
|
22
|
+
display: 'block',
|
|
23
|
+
width: '100%',
|
|
24
|
+
height: '100%',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
globalStyle('.zylem-waveform-shade', {
|
|
28
|
+
position: 'absolute',
|
|
29
|
+
top: 0,
|
|
30
|
+
bottom: 0,
|
|
31
|
+
background: 'rgba(0, 0, 0, 0.45)',
|
|
32
|
+
pointerEvents: 'none',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
globalStyle('.zylem-waveform-edge', {
|
|
36
|
+
position: 'absolute',
|
|
37
|
+
top: 0,
|
|
38
|
+
bottom: 0,
|
|
39
|
+
width: '10px',
|
|
40
|
+
marginLeft: '-5px',
|
|
41
|
+
cursor: 'ew-resize',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
globalStyle('.zylem-waveform-edge::before', {
|
|
45
|
+
content: '""',
|
|
46
|
+
position: 'absolute',
|
|
47
|
+
top: 0,
|
|
48
|
+
bottom: 0,
|
|
49
|
+
left: '4px',
|
|
50
|
+
width: '2px',
|
|
51
|
+
background: 'rgba(255, 244, 200, 0.95)',
|
|
52
|
+
boxShadow: '0 0 6px rgba(240, 180, 60, 0.7)',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
globalStyle('.zylem-waveform-edge::after', {
|
|
56
|
+
content: '""',
|
|
57
|
+
position: 'absolute',
|
|
58
|
+
top: '50%',
|
|
59
|
+
left: '1px',
|
|
60
|
+
width: '8px',
|
|
61
|
+
height: '16px',
|
|
62
|
+
marginTop: '-8px',
|
|
63
|
+
borderRadius: '3px',
|
|
64
|
+
background: 'linear-gradient(180deg, rgba(255, 244, 200, 0.98), rgba(240, 180, 60, 0.95))',
|
|
65
|
+
border: '1px solid rgba(255,255,255,0.7)',
|
|
66
|
+
boxSizing: 'border-box',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
globalStyle('.zylem-waveform-playhead', {
|
|
70
|
+
position: 'absolute',
|
|
71
|
+
top: 0,
|
|
72
|
+
bottom: 0,
|
|
73
|
+
width: '1px',
|
|
74
|
+
background: 'rgba(255, 236, 170, 0.9)',
|
|
75
|
+
boxShadow: '0 0 6px rgba(240, 180, 60, 0.7)',
|
|
76
|
+
pointerEvents: 'none',
|
|
77
|
+
});
|