@zylem/ui 0.8.9 → 0.8.12
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 +3 -0
- package/dist/styles.css +1 -1
- package/package.json +5 -1
- package/src/components/Accordion/Accordion.tsx +10 -1
- package/src/components/AddTile/AddTile.css.ts +17 -0
- package/src/components/AddTile/AddTile.tsx +15 -7
- 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/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 +6 -0
- package/src/components/FrameStrip/FrameStrip.tsx +30 -2
- 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/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.tsx +15 -4
- 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/Toast/Toast.css.ts +48 -0
- package/src/components/Toast/Toast.tsx +37 -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 +72 -1
- package/src/global/accordion.css.ts +23 -0
- package/src/icons/create-icon.tsx +71 -0
- package/src/icons/icon-nodes.ts +75 -0
- package/src/icons/index.ts +54 -0
- package/src/icons/svg/camera-isometric.svg +16 -0
- package/src/icons/svg/free-perspective.svg +8 -0
- package/src/icons/svg/game-camera.svg +6 -0
- package/src/icons/svg/perspective.svg +5 -0
- package/src/icons/svg/sideview-perspective.svg +5 -0
- package/src/icons/svg/top-down-perspective.svg +5 -0
- package/src/styles.ts +11 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { globalStyle, keyframes } from '@vanilla-extract/css';
|
|
2
|
+
import { vars } from '../../theme.css';
|
|
3
|
+
|
|
4
|
+
const enter = keyframes({
|
|
5
|
+
from: { opacity: 0, transform: 'translate(-50%, 6px)' },
|
|
6
|
+
to: { opacity: 1, transform: 'translate(-50%, 0)' },
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
globalStyle('.zylem-toast', {
|
|
10
|
+
position: 'absolute',
|
|
11
|
+
left: '50%',
|
|
12
|
+
bottom: vars.spacing.lg,
|
|
13
|
+
transform: 'translateX(-50%)',
|
|
14
|
+
zIndex: vars.layers.panel,
|
|
15
|
+
maxWidth: 'calc(100% - 32px)',
|
|
16
|
+
padding: `${vars.spacing.sm} ${vars.spacing.md}`,
|
|
17
|
+
borderRadius: vars.radii.control,
|
|
18
|
+
border: '1px solid rgba(150, 210, 255, 0.45)',
|
|
19
|
+
background: vars.material.glassPanelDark,
|
|
20
|
+
boxShadow: vars.effects.shadowPanel,
|
|
21
|
+
backdropFilter: `blur(${vars.effects.blurSm})`,
|
|
22
|
+
color: vars.colors.text,
|
|
23
|
+
fontFamily: vars.typography.fontFamily,
|
|
24
|
+
fontSize: `calc(${vars.typography.fontSize} - 1px)`,
|
|
25
|
+
lineHeight: 1.4,
|
|
26
|
+
textAlign: 'center',
|
|
27
|
+
pointerEvents: 'none',
|
|
28
|
+
animation: `${enter} 0.16s ${vars.motion.easeOut}`,
|
|
29
|
+
'@supports': {
|
|
30
|
+
'not (backdrop-filter: blur(1px))': {
|
|
31
|
+
background: vars.colors.surface,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
globalStyle('.zylem-toast[data-position="top"]', {
|
|
37
|
+
top: vars.spacing.lg,
|
|
38
|
+
bottom: 'auto',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
globalStyle('.zylem-toast[data-tone="success"]', {
|
|
42
|
+
borderColor: 'rgba(120, 255, 150, 0.55)',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
globalStyle('.zylem-toast[data-tone="error"]', {
|
|
46
|
+
borderColor: 'rgba(255, 132, 114, 0.65)',
|
|
47
|
+
color: '#FFD9D3',
|
|
48
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Show } from 'solid-js';
|
|
2
|
+
import type { JSX } from 'solid-js';
|
|
3
|
+
|
|
4
|
+
export type ToastTone = 'info' | 'success' | 'error';
|
|
5
|
+
|
|
6
|
+
export interface ToastProps {
|
|
7
|
+
/** The message. Nothing renders while this is empty. */
|
|
8
|
+
message?: JSX.Element | null | false;
|
|
9
|
+
tone?: ToastTone;
|
|
10
|
+
/** Edge of the positioned parent to float against. Defaults to `bottom`. */
|
|
11
|
+
position?: 'top' | 'bottom';
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Transient glass message floated over a stage (a game view, an editor
|
|
17
|
+
* surface). It positions itself against the nearest positioned ancestor and
|
|
18
|
+
* ignores the pointer, so it never gets in the way of what it reports on.
|
|
19
|
+
*
|
|
20
|
+
* Timing is the caller's: show it by passing a message and hide it by passing
|
|
21
|
+
* none, which is how a store's `flashHint()` already works.
|
|
22
|
+
*/
|
|
23
|
+
export function Toast(props: ToastProps) {
|
|
24
|
+
return (
|
|
25
|
+
<Show when={props.message}>
|
|
26
|
+
<div
|
|
27
|
+
class={props.class ? `zylem-toast ${props.class}` : 'zylem-toast'}
|
|
28
|
+
role="status"
|
|
29
|
+
aria-live="polite"
|
|
30
|
+
data-tone={props.tone}
|
|
31
|
+
data-position={props.position}
|
|
32
|
+
>
|
|
33
|
+
{props.message}
|
|
34
|
+
</div>
|
|
35
|
+
</Show>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -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
|
+
});
|