@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.
Files changed (81) hide show
  1. package/dist/chunk-V47CGFWX.js +1 -0
  2. package/dist/index.css +1 -1
  3. package/dist/index.d.ts +15 -0
  4. package/dist/index.js +1 -1
  5. package/dist/styles.css +1 -1
  6. package/dist/styles.js +1 -1
  7. package/package.json +22 -11
  8. package/src/components/Accordion/Accordion.tsx +10 -1
  9. package/src/components/AddTile/AddTile.css.ts +95 -0
  10. package/src/components/AddTile/AddTile.tsx +45 -0
  11. package/src/components/BuildLabel/BuildLabel.css.ts +31 -0
  12. package/src/components/BuildLabel/BuildLabel.tsx +37 -0
  13. package/src/components/BuildLabel/mount-build-label.ts +77 -0
  14. package/src/components/BuildLabel/resolve-build-version.ts +47 -0
  15. package/src/components/Callout/Callout.css.ts +45 -0
  16. package/src/components/Callout/Callout.tsx +32 -0
  17. package/src/components/Card/Card.css.ts +18 -0
  18. package/src/components/Card/Card.tsx +25 -2
  19. package/src/components/ColorPalette/ColorPalette.css.ts +51 -0
  20. package/src/components/ColorPalette/ColorPalette.tsx +78 -0
  21. package/src/components/Dialog/Dialog.css.ts +69 -2
  22. package/src/components/Dialog/Dialog.tsx +35 -7
  23. package/src/components/DropdownMenu/DropdownMenu.css.ts +22 -0
  24. package/src/components/DropdownMenu/DropdownMenu.tsx +67 -20
  25. package/src/components/EmptyState/EmptyState.css.ts +9 -0
  26. package/src/components/EmptyState/EmptyState.tsx +3 -1
  27. package/src/components/FrameStrip/FrameStrip.css.ts +138 -0
  28. package/src/components/FrameStrip/FrameStrip.tsx +146 -0
  29. package/src/components/Menubar/Menubar.css.ts +52 -0
  30. package/src/components/Menubar/Menubar.tsx +77 -0
  31. package/src/components/Mixer/Knob.tsx +188 -0
  32. package/src/components/Mixer/Mixer.css.ts +246 -0
  33. package/src/components/Mixer/MixerStrip.tsx +150 -0
  34. package/src/components/NumberField/NumberField.tsx +5 -1
  35. package/src/components/Panel/Panel.tsx +6 -5
  36. package/src/components/PianoRoll/PianoRoll.css.ts +224 -0
  37. package/src/components/PianoRoll/PianoRoll.tsx +353 -0
  38. package/src/components/PianoRoll/piano-roll-math.ts +89 -0
  39. package/src/components/PixelGrid/PixelGrid.css.ts +30 -0
  40. package/src/components/PixelGrid/PixelGrid.tsx +48 -0
  41. package/src/components/PlaybackControls/PlaybackControls.css.ts +53 -0
  42. package/src/components/PlaybackControls/PlaybackControls.tsx +80 -0
  43. package/src/components/Property/Property.css.ts +1 -1
  44. package/src/components/Property/Property.tsx +3 -0
  45. package/src/components/Sidebar/Sidebar.css.ts +8 -0
  46. package/src/components/Sidebar/Sidebar.tsx +10 -2
  47. package/src/components/Slider/Slider.tsx +6 -0
  48. package/src/components/SpriteSizePicker/SpriteSizePicker.css.ts +103 -0
  49. package/src/components/SpriteSizePicker/SpriteSizePicker.tsx +172 -0
  50. package/src/components/SpriteSizePicker/snap-size.ts +28 -0
  51. package/src/components/Tabs/Tabs.css.ts +48 -0
  52. package/src/components/Tabs/Tabs.tsx +41 -3
  53. package/src/components/TextField/TextField.tsx +49 -5
  54. package/src/components/TimelineLanes/TimelineLanes.css.ts +365 -0
  55. package/src/components/TimelineLanes/TimelineLanes.tsx +509 -0
  56. package/src/components/TimelineLanes/lanes-math.ts +182 -0
  57. package/src/components/TimelineTrack/TimelineTrack.css.ts +163 -0
  58. package/src/components/TimelineTrack/TimelineTrack.tsx +90 -0
  59. package/src/components/TimelineTrack/timeline-math.ts +43 -0
  60. package/src/components/Toast/Toast.css.ts +48 -0
  61. package/src/components/Toast/Toast.tsx +37 -0
  62. package/src/components/ToolButton/ToolButton.tsx +92 -0
  63. package/src/components/ToolGroup/ToolGroup.css.ts +35 -0
  64. package/src/components/ToolGroup/ToolGroup.tsx +29 -0
  65. package/src/components/ToolboxButton/ToolboxButton.css.ts +131 -0
  66. package/src/components/ToolboxButton/ToolboxButton.tsx +88 -0
  67. package/src/components/TrackHeader/TrackHeader.css.ts +162 -0
  68. package/src/components/TrackHeader/TrackHeader.tsx +145 -0
  69. package/src/components/TransportBar/TransportBar.css.ts +65 -0
  70. package/src/components/TransportBar/TransportBar.tsx +154 -0
  71. package/src/components/Waveform/Waveform.css.ts +77 -0
  72. package/src/components/Waveform/Waveform.tsx +195 -0
  73. package/src/components/Waveform/peaks.ts +49 -0
  74. package/src/components/index.ts +122 -0
  75. package/src/global/accordion.css.ts +23 -0
  76. package/src/global/hyperglass-base.css.ts +9 -6
  77. package/src/styles.ts +23 -0
  78. package/src/theme.css.ts +15 -0
  79. package/vite/collect-zylem-versions.ts +82 -0
  80. package/vite/index.ts +30 -0
  81. package/dist/chunk-3TP2SNNW.js +0 -1
@@ -0,0 +1,246 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ // Knob -----------------------------------------------------------------------
5
+
6
+ globalStyle('.zylem-knob', {
7
+ display: 'inline-flex',
8
+ flexDirection: 'column',
9
+ alignItems: 'center',
10
+ gap: '2px',
11
+ fontFamily: vars.typography.fontFamily,
12
+ color: vars.colors.text,
13
+ userSelect: 'none',
14
+ });
15
+
16
+ globalStyle('.zylem-knob[data-disabled]', {
17
+ opacity: 0.5,
18
+ });
19
+
20
+ globalStyle('.zylem-knob-dial', {
21
+ display: 'block',
22
+ borderRadius: '999px',
23
+ cursor: 'ns-resize',
24
+ outline: 'none',
25
+ touchAction: 'none',
26
+ });
27
+
28
+ globalStyle('.zylem-knob[data-dragging] .zylem-knob-dial', {
29
+ cursor: 'grabbing',
30
+ });
31
+
32
+ globalStyle('.zylem-knob-dial:focus-visible', {
33
+ boxShadow: vars.effects.focusRing,
34
+ });
35
+
36
+ globalStyle('.zylem-knob-dial svg', {
37
+ display: 'block',
38
+ overflow: 'visible',
39
+ });
40
+
41
+ globalStyle('.zylem-knob-track', {
42
+ fill: 'none',
43
+ stroke: 'rgba(8, 19, 31, 0.9)',
44
+ strokeWidth: 3,
45
+ strokeLinecap: 'round',
46
+ });
47
+
48
+ globalStyle('.zylem-knob-fill', {
49
+ fill: 'none',
50
+ stroke: 'rgba(120, 190, 255, 0.95)',
51
+ strokeWidth: 3,
52
+ strokeLinecap: 'round',
53
+ filter: 'drop-shadow(0 0 3px rgba(97, 166, 232, 0.7))',
54
+ });
55
+
56
+ globalStyle('.zylem-knob-cap', {
57
+ fill: 'rgba(40, 70, 105, 1)',
58
+ stroke: 'rgba(178, 219, 255, 0.5)',
59
+ strokeWidth: 1,
60
+ });
61
+
62
+ globalStyle('.zylem-knob-pointer', {
63
+ stroke: 'rgba(240, 250, 255, 0.98)',
64
+ strokeWidth: 2,
65
+ strokeLinecap: 'round',
66
+ });
67
+
68
+ globalStyle('.zylem-knob-label', {
69
+ fontSize: '9px',
70
+ letterSpacing: '0.08em',
71
+ textTransform: 'uppercase',
72
+ color: vars.colors.textSecondary,
73
+ });
74
+
75
+ globalStyle('.zylem-knob-value', {
76
+ fontSize: '10px',
77
+ fontVariantNumeric: 'tabular-nums',
78
+ color: vars.colors.text,
79
+ });
80
+
81
+ // Mixer strip ----------------------------------------------------------------
82
+
83
+ globalStyle('.zylem-mixer-strip', {
84
+ boxSizing: 'border-box',
85
+ display: 'flex',
86
+ flexDirection: 'column',
87
+ alignItems: 'center',
88
+ gap: vars.spacing.xs,
89
+ width: '64px',
90
+ padding: `${vars.spacing.sm} ${vars.spacing.xs}`,
91
+ fontFamily: vars.typography.fontFamily,
92
+ color: vars.colors.text,
93
+ background: vars.material.glassPanelDark,
94
+ border: '1px solid rgba(97, 166, 232, 0.2)',
95
+ borderTop: '3px solid var(--zylem-track-color)',
96
+ borderRadius: vars.radii.control,
97
+ boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.06)',
98
+ userSelect: 'none',
99
+ });
100
+
101
+ globalStyle('.zylem-mixer-strip[data-master]', {
102
+ width: '76px',
103
+ background: vars.material.glassPanel,
104
+ });
105
+
106
+ globalStyle('.zylem-mixer-strip[data-selected]', {
107
+ boxShadow: `inset 0 1px 0 rgba(255,255,255,0.06), 0 0 0 1px rgba(178, 219, 255, 0.6), ${vars.effects.glowPrimary}`,
108
+ });
109
+
110
+ globalStyle('.zylem-mixer-strip[data-disabled]', {
111
+ opacity: 0.6,
112
+ });
113
+
114
+ globalStyle('.zylem-mixer-fader-row', {
115
+ display: 'flex',
116
+ alignItems: 'stretch',
117
+ gap: '4px',
118
+ height: '132px',
119
+ });
120
+
121
+ globalStyle('.zylem-fader', {
122
+ position: 'relative',
123
+ display: 'flex',
124
+ flexDirection: 'column',
125
+ width: '24px',
126
+ height: '100%',
127
+ touchAction: 'none',
128
+ });
129
+
130
+ globalStyle('.zylem-fader[data-disabled]', {
131
+ opacity: 0.5,
132
+ });
133
+
134
+ globalStyle('.zylem-fader-sr', {
135
+ position: 'absolute',
136
+ width: '1px',
137
+ height: '1px',
138
+ padding: 0,
139
+ margin: '-1px',
140
+ overflow: 'hidden',
141
+ clip: 'rect(0, 0, 0, 0)',
142
+ whiteSpace: 'nowrap',
143
+ border: 0,
144
+ });
145
+
146
+ globalStyle('.zylem-fader-track', {
147
+ position: 'relative',
148
+ flex: '1 1 auto',
149
+ width: '8px',
150
+ margin: '0 auto',
151
+ borderRadius: '999px',
152
+ border: '1px solid rgba(97, 166, 232, 0.28)',
153
+ background: vars.material.fieldGlass,
154
+ boxShadow: vars.effects.shadowInset,
155
+ cursor: 'pointer',
156
+ });
157
+
158
+ globalStyle('.zylem-fader-fill', {
159
+ position: 'absolute',
160
+ bottom: 0,
161
+ width: '100%',
162
+ borderRadius: '999px',
163
+ background:
164
+ 'linear-gradient(0deg, rgba(48, 137, 220, 0.8), rgba(178, 219, 255, 0.55))',
165
+ });
166
+
167
+ globalStyle('.zylem-fader-zero', {
168
+ position: 'absolute',
169
+ left: '-6px',
170
+ right: '-6px',
171
+ bottom: 'calc(100% * 60 / 66)',
172
+ height: '1px',
173
+ background: 'rgba(205, 232, 255, 0.4)',
174
+ pointerEvents: 'none',
175
+ });
176
+
177
+ globalStyle('.zylem-fader-thumb', {
178
+ display: 'block',
179
+ left: '-7px',
180
+ width: '22px',
181
+ height: '12px',
182
+ borderRadius: '4px',
183
+ border: '1px solid rgba(255,255,255,0.55)',
184
+ background:
185
+ 'linear-gradient(180deg, rgba(240,250,255,0.96), rgba(140,190,235,0.92))',
186
+ boxShadow: `inset 0 1px 0 rgba(255,255,255,0.75), inset 0 -3px 0 rgba(0,0,0,0.18), 0 1px 4px rgba(0,0,0,0.5)`,
187
+ cursor: 'grab',
188
+ outline: 'none',
189
+ });
190
+
191
+ globalStyle('.zylem-fader-thumb:active', {
192
+ cursor: 'grabbing',
193
+ });
194
+
195
+ globalStyle('.zylem-fader-thumb:focus-visible', {
196
+ boxShadow: `inset 0 1px 0 rgba(255,255,255,0.75), ${vars.effects.focusRing}`,
197
+ });
198
+
199
+ globalStyle('.zylem-mixer-meter', {
200
+ position: 'relative',
201
+ width: '6px',
202
+ borderRadius: '3px',
203
+ background: 'rgba(8, 19, 31, 0.9)',
204
+ border: '1px solid rgba(97, 166, 232, 0.2)',
205
+ overflow: 'hidden',
206
+ });
207
+
208
+ globalStyle('.zylem-mixer-meter-fill', {
209
+ position: 'absolute',
210
+ bottom: 0,
211
+ left: 0,
212
+ right: 0,
213
+ background:
214
+ 'linear-gradient(0deg, rgba(28, 174, 63, 0.95) 0%, rgba(170, 255, 190, 0.95) 65%, rgba(255, 220, 130, 0.95) 85%, rgba(230, 69, 52, 0.95) 100%)',
215
+ transition: `height 60ms linear`,
216
+ });
217
+
218
+ globalStyle('.zylem-mixer-readout', {
219
+ fontSize: '10px',
220
+ fontVariantNumeric: 'tabular-nums',
221
+ color: vars.colors.textSecondary,
222
+ whiteSpace: 'nowrap',
223
+ });
224
+
225
+ globalStyle('.zylem-mixer-toggles', {
226
+ display: 'flex',
227
+ gap: '3px',
228
+ });
229
+
230
+ globalStyle('.zylem-mixer-name', {
231
+ maxWidth: '100%',
232
+ fontSize: '11px',
233
+ fontWeight: 600,
234
+ whiteSpace: 'nowrap',
235
+ overflow: 'hidden',
236
+ textOverflow: 'ellipsis',
237
+ });
238
+
239
+ globalStyle('.zylem-mixer-strip[data-muted] .zylem-mixer-name', {
240
+ color: vars.colors.textSecondary,
241
+ });
242
+
243
+ globalStyle('.zylem-mixer-strip[data-master] .zylem-mixer-name', {
244
+ letterSpacing: '0.08em',
245
+ textTransform: 'uppercase',
246
+ });
@@ -0,0 +1,150 @@
1
+ import { Slider as KSlider } from '@kobalte/core';
2
+ import { Show } from 'solid-js';
3
+ import { clamp } from '../TimelineLanes/lanes-math';
4
+ import { Knob } from './Knob';
5
+
6
+ export interface MixerStripProps {
7
+ name: string;
8
+ color?: string;
9
+ /** Fader position in dB. */
10
+ volume: number;
11
+ onVolumeChange?: (volume: number) => void;
12
+ onVolumeCommit?: (volume: number) => void;
13
+ minVolume?: number;
14
+ maxVolume?: number;
15
+ /** -1 (left) … 1 (right). Omit to hide the pan knob. */
16
+ pan?: number;
17
+ onPanChange?: (pan: number) => void;
18
+ onPanCommit?: (pan: number) => void;
19
+ /** Mute / solo render only when their handler is given. */
20
+ muted?: boolean;
21
+ onToggleMute?: () => void;
22
+ solo?: boolean;
23
+ onToggleSolo?: () => void;
24
+ /** Live output level, 0–1, drawn as a meter beside the fader. */
25
+ level?: number;
26
+ /** Wider strip with a stronger label; for the master bus. */
27
+ master?: boolean;
28
+ selected?: boolean;
29
+ onSelect?: () => void;
30
+ disabled?: boolean;
31
+ class?: string;
32
+ }
33
+
34
+ /** `-60` (the floor) reads as `-inf`; otherwise one decimal with a sign. */
35
+ export function formatDb(value: number, floor = -60): string {
36
+ if (!Number.isFinite(value) || value <= floor) return '-inf';
37
+ const rounded = Math.round(value * 10) / 10;
38
+ return `${rounded > 0 ? '+' : ''}${rounded.toFixed(1)} dB`;
39
+ }
40
+
41
+ /** `-1` → `L100`, `0` → `C`, `0.5` → `R50`. */
42
+ export function formatPan(value: number): string {
43
+ const percent = Math.round(Math.abs(value) * 100);
44
+ if (percent === 0) return 'C';
45
+ return `${value < 0 ? 'L' : 'R'}${percent}`;
46
+ }
47
+
48
+ /**
49
+ * Channel strip: pan knob, vertical fader with a level meter, mute/solo, and
50
+ * the track name with its color. Compose several into a mixer row.
51
+ */
52
+ export function MixerStrip(props: MixerStripProps) {
53
+ const min = () => props.minVolume ?? -60;
54
+ const max = () => props.maxVolume ?? 6;
55
+
56
+ return (
57
+ <div
58
+ class={props.class ? `zylem-mixer-strip ${props.class}` : 'zylem-mixer-strip'}
59
+ style={{ '--zylem-track-color': props.color ?? 'var(--zylem-color-primary)' }}
60
+ data-master={props.master ? '' : undefined}
61
+ data-selected={props.selected ? '' : undefined}
62
+ data-disabled={props.disabled ? '' : undefined}
63
+ data-muted={props.muted ? '' : undefined}
64
+ onPointerDown={() => props.onSelect?.()}
65
+ >
66
+ <Show when={props.pan !== undefined}>
67
+ <Knob
68
+ value={props.pan ?? 0}
69
+ min={-1}
70
+ max={1}
71
+ step={0.01}
72
+ defaultValue={0}
73
+ bipolar
74
+ size={30}
75
+ label="Pan"
76
+ format={formatPan}
77
+ disabled={props.disabled}
78
+ onChange={(value) => props.onPanChange?.(value)}
79
+ onCommit={(value) => props.onPanCommit?.(value)}
80
+ class="zylem-mixer-pan"
81
+ />
82
+ </Show>
83
+ <div class="zylem-mixer-fader-row">
84
+ <KSlider.Root
85
+ class="zylem-fader"
86
+ orientation="vertical"
87
+ value={[clamp(props.volume, min(), max())]}
88
+ minValue={min()}
89
+ maxValue={max()}
90
+ step={0.1}
91
+ disabled={props.disabled}
92
+ onChange={(values) => props.onVolumeChange?.(values[0] ?? min())}
93
+ onChangeEnd={(values) => props.onVolumeCommit?.(values[0] ?? min())}
94
+ getValueLabel={(params) => formatDb(params.values[0] ?? min(), min())}
95
+ >
96
+ <KSlider.Label class="zylem-fader-sr">{`${props.name} volume`}</KSlider.Label>
97
+ <KSlider.Track class="zylem-fader-track">
98
+ <KSlider.Fill class="zylem-fader-fill" />
99
+ <span class="zylem-fader-zero" aria-hidden="true" />
100
+ <KSlider.Thumb class="zylem-fader-thumb">
101
+ <KSlider.Input class="zylem-fader-sr" />
102
+ </KSlider.Thumb>
103
+ </KSlider.Track>
104
+ </KSlider.Root>
105
+ <div class="zylem-mixer-meter" aria-hidden="true">
106
+ <div
107
+ class="zylem-mixer-meter-fill"
108
+ style={{ height: `${Math.round(clamp(props.level ?? 0, 0, 1) * 100)}%` }}
109
+ />
110
+ </div>
111
+ </div>
112
+ <span class="zylem-mixer-readout">{formatDb(props.volume, min())}</span>
113
+ <div class="zylem-mixer-toggles">
114
+ <Show when={props.onToggleMute}>
115
+ <button
116
+ type="button"
117
+ class="zylem-track-toggle"
118
+ data-kind="mute"
119
+ aria-pressed={props.muted ? 'true' : 'false'}
120
+ aria-label={props.muted ? 'Unmute' : 'Mute'}
121
+ title={props.muted ? 'Unmute' : 'Mute'}
122
+ disabled={props.disabled}
123
+ onPointerDown={(event) => event.stopPropagation()}
124
+ onClick={() => props.onToggleMute?.()}
125
+ >
126
+ M
127
+ </button>
128
+ </Show>
129
+ <Show when={props.onToggleSolo}>
130
+ <button
131
+ type="button"
132
+ class="zylem-track-toggle"
133
+ data-kind="solo"
134
+ aria-pressed={props.solo ? 'true' : 'false'}
135
+ aria-label={props.solo ? 'Unsolo' : 'Solo'}
136
+ title={props.solo ? 'Unsolo' : 'Solo'}
137
+ disabled={props.disabled}
138
+ onPointerDown={(event) => event.stopPropagation()}
139
+ onClick={() => props.onToggleSolo?.()}
140
+ >
141
+ S
142
+ </button>
143
+ </Show>
144
+ </div>
145
+ <span class="zylem-mixer-name" title={props.name}>
146
+ {props.name}
147
+ </span>
148
+ </div>
149
+ );
150
+ }
@@ -23,8 +23,12 @@ export function NumberField(props: NumberFieldProps) {
23
23
  <KNumberField.Root
24
24
  class={props.class ? `zylem-field-root ${props.class}` : 'zylem-field-root'}
25
25
  rawValue={props.value}
26
+ // Kobalte reports the parsed value on mount and after each format pass;
27
+ // only real edits reach the owner, so a controlled form is not marked
28
+ // dirty just by rendering.
26
29
  onRawValueChange={(value) => {
27
- if (props.onChange && !Number.isNaN(value)) props.onChange(value);
30
+ if (!props.onChange || Number.isNaN(value) || value === props.value) return;
31
+ props.onChange(value);
28
32
  }}
29
33
  minValue={props.minValue}
30
34
  maxValue={props.maxValue}
@@ -16,6 +16,8 @@ export interface PanelProps
16
16
  * inside an already-outlined region.
17
17
  */
18
18
  borderless?: boolean;
19
+ /** Class for the body element, for layouts the body's children need (a form's column, a grid). */
20
+ bodyClass?: string;
19
21
  children?: JSX.Element;
20
22
  }
21
23
 
@@ -30,6 +32,7 @@ export function Panel(props: PanelProps) {
30
32
  'window',
31
33
  'flush',
32
34
  'borderless',
35
+ 'bodyClass',
33
36
  'children',
34
37
  'class',
35
38
  ]);
@@ -53,11 +56,9 @@ export function Panel(props: PanelProps) {
53
56
  </div>
54
57
  </Show>
55
58
  <div
56
- class={
57
- local.flush
58
- ? 'zylem-panel-body zylem-panel-body--flush'
59
- : 'zylem-panel-body'
60
- }
59
+ class={['zylem-panel-body', local.flush ? 'zylem-panel-body--flush' : '', local.bodyClass ?? '']
60
+ .filter(Boolean)
61
+ .join(' ')}
61
62
  >
62
63
  {local.children}
63
64
  </div>
@@ -0,0 +1,224 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ globalStyle('.zylem-piano-roll', {
5
+ position: 'relative',
6
+ display: 'flex',
7
+ flexDirection: 'column',
8
+ minHeight: 0,
9
+ minWidth: 0,
10
+ fontFamily: vars.typography.fontFamily,
11
+ color: vars.colors.text,
12
+ background: vars.material.fieldGlass,
13
+ border: '1px solid rgba(97, 166, 232, 0.22)',
14
+ borderRadius: vars.radii.control,
15
+ boxShadow: vars.effects.shadowInset,
16
+ outline: 'none',
17
+ userSelect: 'none',
18
+ });
19
+
20
+ globalStyle('.zylem-piano-roll:focus-visible', {
21
+ boxShadow: `${vars.effects.shadowInset}, ${vars.effects.focusRing}`,
22
+ });
23
+
24
+ globalStyle('.zylem-piano-roll[data-disabled]', {
25
+ opacity: 0.6,
26
+ });
27
+
28
+ globalStyle('.zylem-piano-roll-scroll', {
29
+ flex: '1 1 auto',
30
+ minHeight: 0,
31
+ overflow: 'auto',
32
+ });
33
+
34
+ globalStyle('.zylem-piano-roll-content', {
35
+ position: 'relative',
36
+ display: 'flex',
37
+ });
38
+
39
+ // Keys: sticky column of white/black rows, labelled at each C.
40
+ globalStyle('.zylem-piano-roll-keys', {
41
+ position: 'sticky',
42
+ left: 0,
43
+ zIndex: 2,
44
+ flex: 'none',
45
+ display: 'flex',
46
+ flexDirection: 'column',
47
+ borderRight: '1px solid rgba(97, 166, 232, 0.3)',
48
+ boxShadow: '2px 0 6px rgba(0,0,0,0.35)',
49
+ });
50
+
51
+ globalStyle('.zylem-piano-roll-key', {
52
+ boxSizing: 'border-box',
53
+ display: 'flex',
54
+ alignItems: 'center',
55
+ justifyContent: 'flex-end',
56
+ padding: '0 4px 0 0',
57
+ margin: 0,
58
+ border: 0,
59
+ borderBottom: '1px solid rgba(0,0,0,0.35)',
60
+ fontFamily: vars.typography.fontFamily,
61
+ fontSize: '9px',
62
+ lineHeight: 1,
63
+ color: '#1c2633',
64
+ background: 'linear-gradient(90deg, #dbe6f2, #f4f8fc)',
65
+ cursor: 'pointer',
66
+ outline: 'none',
67
+ });
68
+
69
+ globalStyle('.zylem-piano-roll-key[data-black]', {
70
+ color: '#cdd8e6',
71
+ background: 'linear-gradient(90deg, #0d151f, #263241)',
72
+ boxShadow: 'inset -6px 0 0 rgba(255,255,255,0.05)',
73
+ });
74
+
75
+ globalStyle('.zylem-piano-roll-key[data-c]', {
76
+ fontWeight: 700,
77
+ });
78
+
79
+ globalStyle('.zylem-piano-roll-key:hover:not(:disabled)', {
80
+ filter: 'brightness(1.1)',
81
+ });
82
+
83
+ globalStyle('.zylem-piano-roll-key:active:not(:disabled)', {
84
+ filter: 'brightness(0.92)',
85
+ });
86
+
87
+ // Grid: row stripes plus beat/bar/snap columns from gradients.
88
+ globalStyle('.zylem-piano-roll-grid', {
89
+ position: 'relative',
90
+ flex: 'none',
91
+ backgroundImage: [
92
+ 'repeating-linear-gradient(90deg, rgba(205, 232, 255, 0.28) 0 1px, transparent 1px var(--zylem-pr-bar))',
93
+ 'repeating-linear-gradient(90deg, rgba(205, 232, 255, 0.12) 0 1px, transparent 1px var(--zylem-pr-ppb))',
94
+ 'repeating-linear-gradient(90deg, rgba(205, 232, 255, 0.05) 0 1px, transparent 1px var(--zylem-pr-snap))',
95
+ ].join(', '),
96
+ });
97
+
98
+ globalStyle('.zylem-piano-roll-row', {
99
+ position: 'absolute',
100
+ left: 0,
101
+ right: 0,
102
+ boxSizing: 'border-box',
103
+ borderBottom: '1px solid rgba(255,255,255,0.04)',
104
+ pointerEvents: 'none',
105
+ });
106
+
107
+ globalStyle('.zylem-piano-roll-row[data-black]', {
108
+ background: 'rgba(0, 0, 0, 0.22)',
109
+ });
110
+
111
+ globalStyle('.zylem-piano-roll-row[data-c]', {
112
+ borderBottom: '1px solid rgba(205, 232, 255, 0.25)',
113
+ });
114
+
115
+ globalStyle('.zylem-piano-roll-end', {
116
+ position: 'absolute',
117
+ top: 0,
118
+ bottom: 0,
119
+ right: 0,
120
+ background: 'rgba(0, 0, 0, 0.35)',
121
+ pointerEvents: 'none',
122
+ });
123
+
124
+ globalStyle('.zylem-piano-roll-note', {
125
+ position: 'absolute',
126
+ boxSizing: 'border-box',
127
+ display: 'flex',
128
+ alignItems: 'center',
129
+ borderRadius: '3px',
130
+ border: '1px solid rgba(255, 255, 255, 0.55)',
131
+ background:
132
+ 'linear-gradient(180deg, rgba(178, 219, 255, 0.98), rgba(48, 137, 220, 0.98))',
133
+ boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.45), 0 1px 2px rgba(0,0,0,0.5)',
134
+ opacity: 'calc(0.45 + 0.55 * var(--zylem-pr-velocity, 0.8))',
135
+ color: '#08131f',
136
+ cursor: 'grab',
137
+ outline: 'none',
138
+ overflow: 'hidden',
139
+ });
140
+
141
+ globalStyle('.zylem-piano-roll-note[data-selected]', {
142
+ background:
143
+ 'linear-gradient(180deg, rgba(255, 244, 200, 0.98), rgba(240, 180, 60, 0.98))',
144
+ borderColor: 'white',
145
+ boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6), 0 0 8px rgba(240, 180, 60, 0.7)',
146
+ opacity: 1,
147
+ });
148
+
149
+ globalStyle('.zylem-piano-roll[data-dragging] .zylem-piano-roll-note', {
150
+ cursor: 'grabbing',
151
+ });
152
+
153
+ globalStyle('.zylem-piano-roll-note-label', {
154
+ padding: '0 4px',
155
+ fontSize: '9px',
156
+ fontWeight: 700,
157
+ lineHeight: 1,
158
+ whiteSpace: 'nowrap',
159
+ pointerEvents: 'none',
160
+ });
161
+
162
+ globalStyle('.zylem-piano-roll-note-handle', {
163
+ position: 'absolute',
164
+ top: 0,
165
+ bottom: 0,
166
+ right: 0,
167
+ width: '6px',
168
+ cursor: 'ew-resize',
169
+ });
170
+
171
+ globalStyle('.zylem-piano-roll-playhead', {
172
+ position: 'absolute',
173
+ top: 0,
174
+ bottom: 0,
175
+ width: '1px',
176
+ background: 'rgba(255, 236, 170, 0.9)',
177
+ boxShadow: '0 0 6px rgba(240, 180, 60, 0.7)',
178
+ pointerEvents: 'none',
179
+ });
180
+
181
+ // Velocity lane under the grid: one bar per note, drag vertically.
182
+ globalStyle('.zylem-piano-roll-velocity', {
183
+ display: 'flex',
184
+ flex: 'none',
185
+ borderTop: '1px solid rgba(97, 166, 232, 0.3)',
186
+ background: 'rgba(8, 19, 31, 0.6)',
187
+ });
188
+
189
+ globalStyle('.zylem-piano-roll-velocity-label', {
190
+ position: 'sticky',
191
+ left: 0,
192
+ zIndex: 2,
193
+ flex: 'none',
194
+ display: 'flex',
195
+ alignItems: 'flex-end',
196
+ justifyContent: 'flex-end',
197
+ padding: '0 4px 4px 0',
198
+ fontSize: '9px',
199
+ letterSpacing: '0.08em',
200
+ textTransform: 'uppercase',
201
+ color: vars.colors.textSecondary,
202
+ background: 'rgba(12, 22, 34, 0.98)',
203
+ borderRight: '1px solid rgba(97, 166, 232, 0.3)',
204
+ });
205
+
206
+ globalStyle('.zylem-piano-roll-velocity-lane', {
207
+ position: 'relative',
208
+ flex: 'none',
209
+ height: '100%',
210
+ });
211
+
212
+ globalStyle('.zylem-piano-roll-velocity-bar', {
213
+ position: 'absolute',
214
+ bottom: 0,
215
+ width: '5px',
216
+ marginLeft: '1px',
217
+ borderRadius: '2px 2px 0 0',
218
+ background: 'linear-gradient(180deg, rgba(178, 219, 255, 0.95), rgba(48, 137, 220, 0.85))',
219
+ cursor: 'ns-resize',
220
+ });
221
+
222
+ globalStyle('.zylem-piano-roll-velocity-bar[data-selected]', {
223
+ background: 'linear-gradient(180deg, rgba(255, 244, 200, 0.98), rgba(240, 180, 60, 0.95))',
224
+ });