@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.
Files changed (61) hide show
  1. package/README.md +3 -0
  2. package/dist/styles.css +1 -1
  3. package/package.json +5 -1
  4. package/src/components/Accordion/Accordion.tsx +10 -1
  5. package/src/components/AddTile/AddTile.css.ts +17 -0
  6. package/src/components/AddTile/AddTile.tsx +15 -7
  7. package/src/components/Callout/Callout.css.ts +45 -0
  8. package/src/components/Callout/Callout.tsx +32 -0
  9. package/src/components/Card/Card.css.ts +18 -0
  10. package/src/components/Card/Card.tsx +25 -2
  11. package/src/components/Dialog/Dialog.css.ts +69 -2
  12. package/src/components/Dialog/Dialog.tsx +35 -7
  13. package/src/components/DropdownMenu/DropdownMenu.css.ts +22 -0
  14. package/src/components/DropdownMenu/DropdownMenu.tsx +67 -20
  15. package/src/components/EmptyState/EmptyState.css.ts +9 -0
  16. package/src/components/EmptyState/EmptyState.tsx +3 -1
  17. package/src/components/FrameStrip/FrameStrip.css.ts +6 -0
  18. package/src/components/FrameStrip/FrameStrip.tsx +30 -2
  19. package/src/components/Menubar/Menubar.css.ts +52 -0
  20. package/src/components/Menubar/Menubar.tsx +77 -0
  21. package/src/components/Mixer/Knob.tsx +188 -0
  22. package/src/components/Mixer/Mixer.css.ts +246 -0
  23. package/src/components/Mixer/MixerStrip.tsx +150 -0
  24. package/src/components/NumberField/NumberField.tsx +5 -1
  25. package/src/components/Panel/Panel.tsx +6 -5
  26. package/src/components/PianoRoll/PianoRoll.css.ts +224 -0
  27. package/src/components/PianoRoll/PianoRoll.tsx +353 -0
  28. package/src/components/PianoRoll/piano-roll-math.ts +89 -0
  29. package/src/components/Property/Property.css.ts +1 -1
  30. package/src/components/Property/Property.tsx +3 -0
  31. package/src/components/Sidebar/Sidebar.css.ts +8 -0
  32. package/src/components/Sidebar/Sidebar.tsx +10 -2
  33. package/src/components/Slider/Slider.tsx +6 -0
  34. package/src/components/SpriteSizePicker/SpriteSizePicker.tsx +15 -4
  35. package/src/components/Tabs/Tabs.css.ts +48 -0
  36. package/src/components/Tabs/Tabs.tsx +41 -3
  37. package/src/components/TextField/TextField.tsx +49 -5
  38. package/src/components/TimelineLanes/TimelineLanes.css.ts +365 -0
  39. package/src/components/TimelineLanes/TimelineLanes.tsx +509 -0
  40. package/src/components/TimelineLanes/lanes-math.ts +182 -0
  41. package/src/components/Toast/Toast.css.ts +48 -0
  42. package/src/components/Toast/Toast.tsx +37 -0
  43. package/src/components/TrackHeader/TrackHeader.css.ts +162 -0
  44. package/src/components/TrackHeader/TrackHeader.tsx +145 -0
  45. package/src/components/TransportBar/TransportBar.css.ts +65 -0
  46. package/src/components/TransportBar/TransportBar.tsx +154 -0
  47. package/src/components/Waveform/Waveform.css.ts +77 -0
  48. package/src/components/Waveform/Waveform.tsx +195 -0
  49. package/src/components/Waveform/peaks.ts +49 -0
  50. package/src/components/index.ts +72 -1
  51. package/src/global/accordion.css.ts +23 -0
  52. package/src/icons/create-icon.tsx +71 -0
  53. package/src/icons/icon-nodes.ts +75 -0
  54. package/src/icons/index.ts +54 -0
  55. package/src/icons/svg/camera-isometric.svg +16 -0
  56. package/src/icons/svg/free-perspective.svg +8 -0
  57. package/src/icons/svg/game-camera.svg +6 -0
  58. package/src/icons/svg/perspective.svg +5 -0
  59. package/src/icons/svg/sideview-perspective.svg +5 -0
  60. package/src/icons/svg/top-down-perspective.svg +5 -0
  61. package/src/styles.ts +11 -0
@@ -0,0 +1,195 @@
1
+ import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js';
2
+ import { clamp } from '../TimelineLanes/lanes-math';
3
+ import { computePeaks, peaksFromAudioBuffer } from './peaks';
4
+
5
+ export type { PeakSource } from './peaks';
6
+
7
+ export interface WaveformRegion {
8
+ /** Seconds. */
9
+ start: number;
10
+ end: number;
11
+ }
12
+
13
+ export interface WaveformProps {
14
+ /** Pre-computed peaks (0–1 per column); wins over `buffer`. */
15
+ peaks?: ArrayLike<number>;
16
+ /** Source audio; peaks are computed to match the drawn width. */
17
+ buffer?: { duration: number; numberOfChannels: number; getChannelData(channel: number): Float32Array };
18
+ /** Seconds the peaks span; defaults to `buffer.duration`. Needed for regions and the playhead. */
19
+ duration?: number;
20
+ /** Highlighted span with draggable edges. */
21
+ region?: WaveformRegion;
22
+ onRegionChange?: (region: WaveformRegion) => void;
23
+ onRegionCommit?: (region: WaveformRegion) => void;
24
+ /** Seconds. */
25
+ playhead?: number;
26
+ /** Any CSS color for the peaks. */
27
+ color?: string;
28
+ height?: number;
29
+ /** Click anywhere to seek. */
30
+ onSeek?: (seconds: number) => void;
31
+ disabled?: boolean;
32
+ class?: string;
33
+ }
34
+
35
+ /**
36
+ * Canvas waveform with an optional trim region and playhead. Peaks are
37
+ * redrawn on resize so the shape stays crisp at any width.
38
+ */
39
+ export function Waveform(props: WaveformProps) {
40
+ let canvas: HTMLCanvasElement | undefined;
41
+ let host: HTMLDivElement | undefined;
42
+ const [width, setWidth] = createSignal(0);
43
+ const height = () => props.height ?? 64;
44
+ const duration = () => props.duration ?? props.buffer?.duration ?? 0;
45
+
46
+ const peaks = createMemo(() => {
47
+ const bins = Math.max(1, Math.floor(width()));
48
+ if (props.peaks) {
49
+ const source = Float32Array.from(props.peaks as ArrayLike<number>);
50
+ return source.length === bins ? source : resample(source, bins);
51
+ }
52
+ if (props.buffer) return peaksFromAudioBuffer(props.buffer, bins);
53
+ return new Float32Array(bins);
54
+ });
55
+
56
+ onMount(() => {
57
+ if (!host) return;
58
+ setWidth(host.clientWidth);
59
+ if (typeof ResizeObserver === 'undefined') return;
60
+ const observer = new ResizeObserver((entries) => {
61
+ const entry = entries[0];
62
+ if (entry) setWidth(Math.floor(entry.contentRect.width));
63
+ });
64
+ observer.observe(host);
65
+ onCleanup(() => observer.disconnect());
66
+ });
67
+
68
+ createEffect(() => {
69
+ if (!canvas) return;
70
+ const w = width();
71
+ const h = height();
72
+ const data = peaks();
73
+ const color =
74
+ props.color ??
75
+ (getComputedStyle(canvas).getPropertyValue('--zylem-color-primary').trim() || '#61a6e8');
76
+ const ratio = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;
77
+ canvas.width = Math.max(1, Math.floor(w * ratio));
78
+ canvas.height = Math.max(1, Math.floor(h * ratio));
79
+ const context = canvas.getContext('2d');
80
+ if (!context) return;
81
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
82
+ context.clearRect(0, 0, w, h);
83
+ const mid = h / 2;
84
+ context.strokeStyle = 'rgba(205, 232, 255, 0.18)';
85
+ context.beginPath();
86
+ context.moveTo(0, mid + 0.5);
87
+ context.lineTo(w, mid + 0.5);
88
+ context.stroke();
89
+ context.fillStyle = color;
90
+ for (let x = 0; x < data.length; x += 1) {
91
+ const amplitude = Math.max(1, (data[x] ?? 0) * (mid - 2));
92
+ context.fillRect(x, mid - amplitude, 1, amplitude * 2);
93
+ }
94
+ });
95
+
96
+ const secondsAt = (clientX: number) => {
97
+ if (!host || duration() <= 0) return 0;
98
+ const rect = host.getBoundingClientRect();
99
+ return clamp(((clientX - rect.left) / rect.width) * duration(), 0, duration());
100
+ };
101
+ const percent = (seconds: number) => (duration() > 0 ? (seconds / duration()) * 100 : 0);
102
+
103
+ const beginRegionDrag = (edge: 'start' | 'end') => (event: PointerEvent) => {
104
+ const region = props.region;
105
+ if (props.disabled || !region || event.button !== 0) return;
106
+ event.preventDefault();
107
+ event.stopPropagation();
108
+ const target = event.currentTarget as HTMLElement;
109
+ target.setPointerCapture?.(event.pointerId);
110
+ let last = region;
111
+ const move = (moveEvent: PointerEvent) => {
112
+ const seconds = secondsAt(moveEvent.clientX);
113
+ last =
114
+ edge === 'start'
115
+ ? { start: Math.min(seconds, region.end - 0.01), end: region.end }
116
+ : { start: region.start, end: Math.max(seconds, region.start + 0.01) };
117
+ props.onRegionChange?.(last);
118
+ };
119
+ const stop = () => {
120
+ target.removeEventListener('pointermove', move);
121
+ target.removeEventListener('pointerup', stop);
122
+ target.removeEventListener('pointercancel', stop);
123
+ target.releasePointerCapture?.(event.pointerId);
124
+ props.onRegionCommit?.(last);
125
+ };
126
+ target.addEventListener('pointermove', move);
127
+ target.addEventListener('pointerup', stop);
128
+ target.addEventListener('pointercancel', stop);
129
+ };
130
+
131
+ return (
132
+ <div
133
+ ref={host}
134
+ class={props.class ? `zylem-waveform ${props.class}` : 'zylem-waveform'}
135
+ style={{ height: `${height()}px` }}
136
+ data-disabled={props.disabled ? '' : undefined}
137
+ onPointerDown={(event) => {
138
+ if (props.disabled || !props.onSeek || event.button !== 0) return;
139
+ props.onSeek(secondsAt(event.clientX));
140
+ }}
141
+ >
142
+ <canvas ref={canvas} class="zylem-waveform-canvas" aria-hidden="true" />
143
+ <Show when={props.region && duration() > 0}>
144
+ {(_) => {
145
+ const region = () => props.region!;
146
+ return (
147
+ <>
148
+ <div
149
+ class="zylem-waveform-shade"
150
+ style={{ left: 0, width: `${percent(region().start)}%` }}
151
+ />
152
+ <div
153
+ class="zylem-waveform-shade"
154
+ style={{ left: `${percent(region().end)}%`, right: 0 }}
155
+ />
156
+ <div
157
+ class="zylem-waveform-edge"
158
+ data-edge="start"
159
+ style={{ left: `${percent(region().start)}%` }}
160
+ onPointerDown={beginRegionDrag('start')}
161
+ role="slider"
162
+ aria-label="Trim start"
163
+ aria-valuenow={region().start}
164
+ aria-valuemin={0}
165
+ aria-valuemax={duration()}
166
+ />
167
+ <div
168
+ class="zylem-waveform-edge"
169
+ data-edge="end"
170
+ style={{ left: `${percent(region().end)}%` }}
171
+ onPointerDown={beginRegionDrag('end')}
172
+ role="slider"
173
+ aria-label="Trim end"
174
+ aria-valuenow={region().end}
175
+ aria-valuemin={0}
176
+ aria-valuemax={duration()}
177
+ />
178
+ </>
179
+ );
180
+ }}
181
+ </Show>
182
+ <Show when={props.playhead !== undefined && duration() > 0}>
183
+ <div
184
+ class="zylem-waveform-playhead"
185
+ style={{ left: `${percent(props.playhead ?? 0)}%` }}
186
+ aria-hidden="true"
187
+ />
188
+ </Show>
189
+ </div>
190
+ );
191
+ }
192
+
193
+ function resample(source: Float32Array, bins: number): Float32Array {
194
+ return computePeaks({ channels: [source] }, bins);
195
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Peak extraction for waveform drawing: folds PCM samples into `bins`
3
+ * columns, each the largest absolute sample in its slice, normalised so the
4
+ * loudest bin is 1 (silence stays 0).
5
+ */
6
+
7
+ export interface PeakSource {
8
+ /** One `Float32Array` per channel; channels are mixed by taking the max. */
9
+ channels: readonly Float32Array[];
10
+ }
11
+
12
+ export function computePeaks(source: PeakSource | Float32Array, bins: number): Float32Array {
13
+ const channels = source instanceof Float32Array ? [source] : source.channels;
14
+ const count = Math.max(1, Math.floor(bins));
15
+ const peaks = new Float32Array(count);
16
+ const length = channels[0]?.length ?? 0;
17
+ if (length === 0 || channels.length === 0) return peaks;
18
+ const perBin = length / count;
19
+ let loudest = 0;
20
+ for (let bin = 0; bin < count; bin += 1) {
21
+ const from = Math.floor(bin * perBin);
22
+ const to = Math.min(length, Math.max(from + 1, Math.floor((bin + 1) * perBin)));
23
+ let peak = 0;
24
+ for (const channel of channels) {
25
+ for (let i = from; i < to; i += 1) {
26
+ const value = Math.abs(channel[i] ?? 0);
27
+ if (value > peak) peak = value;
28
+ }
29
+ }
30
+ peaks[bin] = peak;
31
+ if (peak > loudest) loudest = peak;
32
+ }
33
+ if (loudest > 0 && loudest !== 1) {
34
+ for (let bin = 0; bin < count; bin += 1) peaks[bin] = peaks[bin]! / loudest;
35
+ }
36
+ return peaks;
37
+ }
38
+
39
+ /** Peaks from a Web Audio `AudioBuffer` without depending on its type here. */
40
+ export function peaksFromAudioBuffer(
41
+ buffer: { numberOfChannels: number; getChannelData(channel: number): Float32Array },
42
+ bins: number,
43
+ ): Float32Array {
44
+ const channels: Float32Array[] = [];
45
+ for (let channel = 0; channel < buffer.numberOfChannels; channel += 1) {
46
+ channels.push(buffer.getChannelData(channel));
47
+ }
48
+ return computePeaks({ channels }, bins);
49
+ }
@@ -38,6 +38,7 @@ export {
38
38
  TabsTrigger,
39
39
  TabsContent,
40
40
  type TabsProps,
41
+ type TabsTriggerProps,
41
42
  } from './Tabs/Tabs';
42
43
  export {
43
44
  Accordion,
@@ -47,17 +48,22 @@ export {
47
48
  export { Tooltip, type TooltipProps } from './Tooltip/Tooltip';
48
49
  export {
49
50
  DropdownMenu,
51
+ MenuItems,
50
52
  type DropdownMenuProps,
51
53
  type DropdownMenuItemConfig,
52
54
  } from './DropdownMenu/DropdownMenu';
55
+ export { Menubar, type MenubarProps, type MenubarMenuConfig } from './Menubar/Menubar';
53
56
  export {
54
57
  Dialog,
55
58
  ConfirmDialog,
56
59
  type DialogProps,
60
+ type DialogSize,
57
61
  type ConfirmDialogProps,
58
62
  } from './Dialog/Dialog';
59
63
  export { Progress, type ProgressProps } from './Progress/Progress';
60
64
  export { Badge, type BadgeProps, type BadgeTone } from './Badge/Badge';
65
+ export { Callout, type CalloutProps, type CalloutTone } from './Callout/Callout';
66
+ export { Toast, type ToastProps, type ToastTone } from './Toast/Toast';
61
67
  export { Separator, type SeparatorProps } from './Separator/Separator';
62
68
 
63
69
  // Composites
@@ -130,7 +136,7 @@ export {
130
136
  type ArtTool,
131
137
  type ToolButtonProps,
132
138
  } from './ToolButton/ToolButton';
133
- export { AddTile, type AddTileProps } from './AddTile/AddTile';
139
+ export { AddTile, type AddTileProps, type AddTileVariant } from './AddTile/AddTile';
134
140
  export {
135
141
  ColorPalette,
136
142
  Swatch,
@@ -163,3 +169,68 @@ export {
163
169
  type ToolboxGridProps,
164
170
  type ToolboxSize,
165
171
  } from './ToolboxButton/ToolboxButton';
172
+
173
+ // Timeline / Music (Creator director and music editors)
174
+ export {
175
+ TimelineLanes,
176
+ TimelineLane,
177
+ TimelineLaneGroup,
178
+ TimelineClip,
179
+ KeyframeMarker,
180
+ useTimeline,
181
+ type TimelineLanesProps,
182
+ type TimelineLaneProps,
183
+ type TimelineLaneGroupProps,
184
+ type TimelineClipProps,
185
+ type TimelineClipChange,
186
+ type TimelineContextValue,
187
+ type TimelineRange,
188
+ type KeyframeMarkerProps,
189
+ type TimeScale,
190
+ type TimeUnit,
191
+ type RulerTick,
192
+ type ClipBounds,
193
+ } from './TimelineLanes/TimelineLanes';
194
+ export {
195
+ snapTime,
196
+ resolveClipDrag,
197
+ rulerTicks,
198
+ majorTickTimes,
199
+ formatClock,
200
+ formatBeats,
201
+ } from './TimelineLanes/lanes-math';
202
+ export { TrackHeader, type TrackHeaderProps } from './TrackHeader/TrackHeader';
203
+ export { PianoRoll, type PianoRollProps, type PianoRollNote } from './PianoRoll/PianoRoll';
204
+ export {
205
+ pitchName,
206
+ isBlackKey,
207
+ resolveNoteDrag,
208
+ cellStart,
209
+ } from './PianoRoll/piano-roll-math';
210
+ export { Knob, arcPath, type KnobProps } from './Mixer/Knob';
211
+ export { MixerStrip, formatDb, formatPan, type MixerStripProps } from './Mixer/MixerStrip';
212
+ export { Waveform, type WaveformProps, type WaveformRegion } from './Waveform/Waveform';
213
+ export { computePeaks, peaksFromAudioBuffer, type PeakSource } from './Waveform/peaks';
214
+ export {
215
+ TransportBar,
216
+ formatTransportTime,
217
+ type TransportBarProps,
218
+ type TransportTimeSignature,
219
+ } from './TransportBar/TransportBar';
220
+
221
+ // Camera icons (Lucide-shaped originals; also `@zylem/ui/icons`)
222
+ export {
223
+ CAMERA_ICONS,
224
+ FreePerspective,
225
+ GameCamera,
226
+ IsometricPerspective,
227
+ Perspective,
228
+ SideviewPerspective,
229
+ TopDownPerspective,
230
+ cameraIconNodes,
231
+ createZylemIcon,
232
+ type CameraIcon,
233
+ type CameraIconName,
234
+ type IconNode,
235
+ type ZylemIconProps,
236
+ } from '../icons';
@@ -81,6 +81,29 @@ globalStyle('.accordion-content', {
81
81
  transition: 'height 0.3s ease',
82
82
  });
83
83
 
84
+ /**
85
+ * Stack layout: natural-height sections, no per-section scrolling. The
86
+ * accordion grows with its content and leaves scrolling to the parent.
87
+ */
88
+ globalStyle('.zylem-accordion--stack', {
89
+ height: 'auto',
90
+ flex: 'none',
91
+ });
92
+
93
+ globalStyle('.zylem-accordion--stack .accordion-item[data-expanded]', {
94
+ flex: 'none',
95
+ });
96
+
97
+ // The parent scrolls, so headers must not pin over the sections above them.
98
+ globalStyle('.zylem-accordion--stack .accordion-header', {
99
+ position: 'static',
100
+ });
101
+
102
+ globalStyle('.zylem-accordion--stack .accordion-content', {
103
+ flex: 'none',
104
+ overflow: 'visible',
105
+ });
106
+
84
107
  globalStyle('.accordion-trigger::after', {
85
108
  content: "'\\25BC'",
86
109
  position: 'relative',
@@ -0,0 +1,71 @@
1
+ import { For, splitProps, type Component, type JSX } from 'solid-js';
2
+ import { Dynamic } from 'solid-js/web';
3
+ import type { IconNode } from './icon-nodes';
4
+
5
+ export interface ZylemIconProps extends JSX.SvgSVGAttributes<SVGSVGElement> {
6
+ color?: string;
7
+ size?: string | number;
8
+ strokeWidth?: string | number;
9
+ absoluteStrokeWidth?: boolean;
10
+ }
11
+
12
+ const DEFAULTS = {
13
+ xmlns: 'http://www.w3.org/2000/svg',
14
+ viewBox: '0 0 24 24',
15
+ fill: 'none',
16
+ 'stroke-linecap': 'round',
17
+ 'stroke-linejoin': 'round',
18
+ } as const;
19
+
20
+ function kebab(name: string): string {
21
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
22
+ }
23
+
24
+ /**
25
+ * Solid icon factory matching lucide-solid's size / color / strokeWidth
26
+ * props so these glyphs drop into the same toolbar buttons and CSS.
27
+ */
28
+ export function createZylemIcon(
29
+ name: string,
30
+ iconNode: IconNode,
31
+ ): Component<ZylemIconProps> {
32
+ const Icon: Component<ZylemIconProps> = (props) => {
33
+ const [local, rest] = splitProps(props, [
34
+ 'color',
35
+ 'size',
36
+ 'strokeWidth',
37
+ 'class',
38
+ 'absoluteStrokeWidth',
39
+ ]);
40
+ const size = () => local.size ?? 24;
41
+ const strokeWidth = () => {
42
+ const width = Number(local.strokeWidth ?? 2);
43
+ if (local.absoluteStrokeWidth) {
44
+ return (width * 24) / Number(size());
45
+ }
46
+ return width;
47
+ };
48
+ const classes = () =>
49
+ ['zylem-icon', `zylem-icon-${kebab(name)}`, local.class]
50
+ .filter((value) => Boolean(value))
51
+ .join(' ');
52
+
53
+ return (
54
+ <svg
55
+ {...DEFAULTS}
56
+ width={size()}
57
+ height={size()}
58
+ stroke={local.color ?? 'currentColor'}
59
+ stroke-width={strokeWidth()}
60
+ class={classes()}
61
+ aria-hidden="true"
62
+ {...rest}
63
+ >
64
+ <For each={iconNode}>
65
+ {([element, attrs]) => <Dynamic component={element} {...attrs} />}
66
+ </For>
67
+ </svg>
68
+ );
69
+ };
70
+ return Icon;
71
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Original Zylem camera icons as Lucide-style SVG node lists.
3
+ * 24×24 viewBox, 2-unit stroke, no fill, rounded caps and joins.
4
+ */
5
+ export type IconNode = [
6
+ elementName: 'rect' | 'path',
7
+ attributes: Record<string, string>,
8
+ ][];
9
+
10
+ export const PerspectiveNode: IconNode = [
11
+ ['rect', { x: '2', y: '8', width: '8', height: '8', rx: '2' }],
12
+ ['path', { d: 'm10 10 4-2v8l-4-2' }],
13
+ ['path', { d: 'm18 7 4-3m-4 13 4 3' }],
14
+ ];
15
+
16
+ export const SideviewPerspectiveNode: IconNode = [
17
+ ['rect', { x: '2', y: '8', width: '8', height: '8', rx: '2' }],
18
+ ['path', { d: 'm10 10 4-2v8l-4-2' }],
19
+ ['path', { d: 'M18 6h4v12h-4' }],
20
+ ];
21
+
22
+ export const TopDownPerspectiveNode: IconNode = [
23
+ ['rect', { x: '8', y: '2', width: '8', height: '8', rx: '2' }],
24
+ ['path', { d: 'm10 10-2 4h8l-2-4' }],
25
+ ['path', { d: 'M6 18v4h12v-4' }],
26
+ ];
27
+
28
+ export const FreePerspectiveNode: IconNode = [
29
+ ['rect', { x: '7', y: '9', width: '7', height: '6', rx: '1' }],
30
+ ['path', { d: 'm14 11 3-2v6l-3-2' }],
31
+ ['path', { d: 'M3 8a9 9 0 0 1 15-3' }],
32
+ ['path', { d: 'M18 2v3h-3' }],
33
+ ['path', { d: 'M21 16a9 9 0 0 1-15 3' }],
34
+ ['path', { d: 'M6 22v-3h3' }],
35
+ ];
36
+
37
+ /** Source: `src/icons/svg/camera-isometric.svg` */
38
+ export const IsometricPerspectiveNode: IconNode = [
39
+ ['path', { d: 'M11.549 11.368 2 5.855' }],
40
+ ['path', { d: 'M11.549 19.944 11.549 17.252' }],
41
+ ['path', { d: 'M11.549 14.06 11.549 11.368' }],
42
+ ['path', { d: 'M11.549 19.944 2 14.431' }],
43
+ ['path', { d: 'M2 14.431 2 5.855' }],
44
+ ['path', { d: 'M2 5.855 6.775 3.098' }],
45
+ ['path', { d: 'M6.775 3.098 16.324 8.611' }],
46
+ ['path', { d: 'M11.549 11.368 16.324 8.611' }],
47
+ ['path', { d: 'M16.324 8.611 16.324 11.151' }],
48
+ ['path', { d: 'M13.749 18.674 11.549 19.944' }],
49
+ ['path', { d: 'M15.887 10.899 21.192 13.962' }],
50
+ ['path', { d: 'M11.986 17.656 17.291 20.719' }],
51
+ ['path', { d: 'M15.887 10.899A3.901 2.252 -60 0 0 11.986 17.656' }],
52
+ [
53
+ 'path',
54
+ {
55
+ d: 'M21.192 13.962A3.901 2.252 -60 1 1 17.291 20.719A3.901 2.252 -60 1 1 21.192 13.962Z',
56
+ },
57
+ ],
58
+ ];
59
+ export const GameCameraNode: IconNode = [
60
+ ['rect', { x: '2', y: '6', width: '16', height: '12', rx: '2' }],
61
+ ['path', { d: 'm18 10 4-3v10l-4-3' }],
62
+ ['path', { d: 'M6 12h4m-2-2v4' }],
63
+ ['path', { d: 'M14 10h.01M14 14h.01' }],
64
+ ];
65
+
66
+ export const cameraIconNodes = {
67
+ perspective: PerspectiveNode,
68
+ 'sideview-perspective': SideviewPerspectiveNode,
69
+ 'top-down-perspective': TopDownPerspectiveNode,
70
+ 'isometric-perspective': IsometricPerspectiveNode,
71
+ 'free-perspective': FreePerspectiveNode,
72
+ 'game-camera': GameCameraNode,
73
+ } as const;
74
+
75
+ export type CameraIconName = keyof typeof cameraIconNodes;
@@ -0,0 +1,54 @@
1
+ import type { Component } from 'solid-js';
2
+ import { createZylemIcon, type ZylemIconProps } from './create-icon';
3
+ import {
4
+ FreePerspectiveNode,
5
+ GameCameraNode,
6
+ IsometricPerspectiveNode,
7
+ PerspectiveNode,
8
+ SideviewPerspectiveNode,
9
+ TopDownPerspectiveNode,
10
+ } from './icon-nodes';
11
+
12
+ export type { IconNode, CameraIconName } from './icon-nodes';
13
+ export {
14
+ cameraIconNodes,
15
+ FreePerspectiveNode,
16
+ GameCameraNode,
17
+ IsometricPerspectiveNode,
18
+ PerspectiveNode,
19
+ SideviewPerspectiveNode,
20
+ TopDownPerspectiveNode,
21
+ } from './icon-nodes';
22
+ export { createZylemIcon, type ZylemIconProps } from './create-icon';
23
+
24
+ export type CameraIcon = Component<ZylemIconProps>;
25
+
26
+ /** Recorder with diverging FOV rays. */
27
+ export const Perspective = createZylemIcon('Perspective', PerspectiveNode);
28
+ /** Recorder facing a vertical view plane (side / ortho). */
29
+ export const SideviewPerspective = createZylemIcon(
30
+ 'SideviewPerspective',
31
+ SideviewPerspectiveNode,
32
+ );
33
+ /** Downward recorder facing a horizontal plane (top-down). */
34
+ export const TopDownPerspective = createZylemIcon(
35
+ 'TopDownPerspective',
36
+ TopDownPerspectiveNode,
37
+ );
38
+ /** Isometric camera body with lens (from camera-isometric.svg). */
39
+ export const IsometricPerspective = createZylemIcon(
40
+ 'IsometricPerspective',
41
+ IsometricPerspectiveNode,
42
+ );/** Recorder with two orbit arrows (free / orbit camera). */
43
+ export const FreePerspective = createZylemIcon('FreePerspective', FreePerspectiveNode);
44
+ /** Recorder with controller buttons (game camera). */
45
+ export const GameCamera = createZylemIcon('GameCamera', GameCameraNode);
46
+
47
+ export const CAMERA_ICONS = [
48
+ { name: 'perspective', label: 'Perspective', Icon: Perspective },
49
+ { name: 'sideview-perspective', label: 'Side view', Icon: SideviewPerspective },
50
+ { name: 'top-down-perspective', label: 'Top down', Icon: TopDownPerspective },
51
+ { name: 'isometric-perspective', label: 'Isometric', Icon: IsometricPerspective },
52
+ { name: 'free-perspective', label: 'Free / orbit', Icon: FreePerspective },
53
+ { name: 'game-camera', label: 'Game camera', Icon: GameCamera },
54
+ ] as const;
@@ -0,0 +1,16 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M11.549 11.368 2 5.855" />
3
+ <path d="M11.549 19.944 11.549 17.252" />
4
+ <path d="M11.549 14.06 11.549 11.368" />
5
+ <path d="M11.549 19.944 2 14.431" />
6
+ <path d="M2 14.431 2 5.855" />
7
+ <path d="M2 5.855 6.775 3.098" />
8
+ <path d="M6.775 3.098 16.324 8.611" />
9
+ <path d="M11.549 11.368 16.324 8.611" />
10
+ <path d="M16.324 8.611 16.324 11.151" />
11
+ <path d="M13.749 18.674 11.549 19.944" />
12
+ <path d="M15.887 10.899 21.192 13.962" />
13
+ <path d="M11.986 17.656 17.291 20.719" />
14
+ <path d="M15.887 10.899A3.901 2.252 -60 0 0 11.986 17.656" />
15
+ <path d="M21.192 13.962A3.901 2.252 -60 1 1 17.291 20.719A3.901 2.252 -60 1 1 21.192 13.962Z" />
16
+ </svg>
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <rect x="7" y="9" width="7" height="6" rx="1" />
3
+ <path d="m14 11 3-2v6l-3-2" />
4
+ <path d="M3 8a9 9 0 0 1 15-3" />
5
+ <path d="M18 2v3h-3" />
6
+ <path d="M21 16a9 9 0 0 1-15 3" />
7
+ <path d="M6 22v-3h3" />
8
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <rect x="2" y="6" width="16" height="12" rx="2" />
3
+ <path d="m18 10 4-3v10l-4-3" />
4
+ <path d="M6 12h4m-2-2v4" />
5
+ <path d="M14 10h.01M14 14h.01" />
6
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <rect x="2" y="8" width="8" height="8" rx="2" />
3
+ <path d="m10 10 4-2v8l-4-2" />
4
+ <path d="m18 7 4-3m-4 13 4 3" />
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <rect x="2" y="8" width="8" height="8" rx="2" />
3
+ <path d="m10 10 4-2v8l-4-2" />
4
+ <path d="M18 6h4v12h-4" />
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <rect x="8" y="2" width="8" height="8" rx="2" />
3
+ <path d="m10 10-2 4h8l-2-4" />
4
+ <path d="M6 18v4h12v-4" />
5
+ </svg>
package/src/styles.ts CHANGED
@@ -32,9 +32,12 @@ import './components/Slider/Slider.css';
32
32
  import './components/Tabs/Tabs.css';
33
33
  import './components/Tooltip/Tooltip.css';
34
34
  import './components/DropdownMenu/DropdownMenu.css';
35
+ import './components/Menubar/Menubar.css';
35
36
  import './components/Dialog/Dialog.css';
36
37
  import './components/Progress/Progress.css';
37
38
  import './components/Badge/Badge.css';
39
+ import './components/Callout/Callout.css';
40
+ import './components/Toast/Toast.css';
38
41
  import './components/Separator/Separator.css';
39
42
  import './components/WindowControls/WindowControls.css';
40
43
  import './components/TitleBar/TitleBar.css';
@@ -61,3 +64,11 @@ import './components/FrameStrip/FrameStrip.css';
61
64
  import './components/PlaybackControls/PlaybackControls.css';
62
65
  import './components/TimelineTrack/TimelineTrack.css';
63
66
  import './components/ToolboxButton/ToolboxButton.css';
67
+
68
+ // Timeline / Music surfaces (Creator director and music editors).
69
+ import './components/TimelineLanes/TimelineLanes.css';
70
+ import './components/TrackHeader/TrackHeader.css';
71
+ import './components/PianoRoll/PianoRoll.css';
72
+ import './components/Mixer/Mixer.css';
73
+ import './components/Waveform/Waveform.css';
74
+ import './components/TransportBar/TransportBar.css';