@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,163 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ const TRACK_HEIGHT = '18px';
5
+
6
+ globalStyle('.zylem-timeline', {
7
+ display: 'flex',
8
+ alignItems: 'center',
9
+ gap: vars.spacing.md,
10
+ width: '100%',
11
+ minWidth: 0,
12
+ fontFamily: vars.typography.fontFamily,
13
+ });
14
+
15
+ globalStyle('.zylem-timeline-root', {
16
+ display: 'flex',
17
+ flexDirection: 'column',
18
+ gap: vars.spacing.xxs,
19
+ flex: '1 1 auto',
20
+ minWidth: 0,
21
+ userSelect: 'none',
22
+ touchAction: 'none',
23
+ });
24
+
25
+ globalStyle('.zylem-timeline-root[data-disabled]', {
26
+ opacity: 0.5,
27
+ });
28
+
29
+ globalStyle('.zylem-timeline-label', {
30
+ fontSize: '11px',
31
+ letterSpacing: '0.08em',
32
+ textTransform: 'uppercase',
33
+ color: vars.colors.textSecondary,
34
+ });
35
+
36
+ globalStyle('.zylem-timeline-track', {
37
+ position: 'relative',
38
+ height: TRACK_HEIGHT,
39
+ width: '100%',
40
+ borderRadius: '999px',
41
+ border: '1px solid rgba(97, 166, 232, 0.28)',
42
+ background: vars.material.fieldGlass,
43
+ boxShadow: vars.effects.shadowInset,
44
+ cursor: 'pointer',
45
+ });
46
+
47
+ globalStyle('.zylem-timeline-fill', {
48
+ position: 'absolute',
49
+ top: 0,
50
+ height: '100%',
51
+ borderRadius: '999px',
52
+ background:
53
+ 'linear-gradient(180deg, rgba(178, 219, 255, 0.55), rgba(48, 137, 220, 0.8))',
54
+ boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.4)',
55
+ });
56
+
57
+ globalStyle('.zylem-timeline-markers, .zylem-timeline-keyframes', {
58
+ position: 'absolute',
59
+ inset: 0,
60
+ pointerEvents: 'none',
61
+ });
62
+
63
+ // Interval dots: small, dim, centered on the rail.
64
+ globalStyle('.zylem-timeline-tick', {
65
+ position: 'absolute',
66
+ top: '50%',
67
+ width: '4px',
68
+ height: '4px',
69
+ marginLeft: '-2px',
70
+ borderRadius: '999px',
71
+ background: 'rgba(205, 232, 255, 0.55)',
72
+ boxShadow: '0 0 0 1px rgba(0, 0, 0, 0.35)',
73
+ transform: 'translateY(-50%)',
74
+ });
75
+
76
+ // Keyframes: rotated squares that catch the pointer over the rail.
77
+ globalStyle('.zylem-timeline-keyframe', {
78
+ position: 'absolute',
79
+ top: '50%',
80
+ width: '10px',
81
+ height: '10px',
82
+ padding: 0,
83
+ margin: '0 0 0 -5px',
84
+ cursor: 'pointer',
85
+ pointerEvents: 'auto',
86
+ border: '1px solid rgba(255, 255, 255, 0.7)',
87
+ borderRadius: '2px',
88
+ background:
89
+ 'linear-gradient(135deg, rgba(255, 244, 200, 0.98), rgba(240, 180, 60, 0.95))',
90
+ boxShadow: '0 1px 3px rgba(0,0,0,0.55)',
91
+ transform: 'translateY(-50%) rotate(45deg)',
92
+ transition: `box-shadow ${vars.motion.fast} ${vars.motion.easeOut}, filter ${vars.motion.fast} ${vars.motion.easeOut}`,
93
+ });
94
+
95
+ globalStyle('.zylem-timeline-keyframe:hover:not([disabled])', {
96
+ filter: 'brightness(1.12)',
97
+ boxShadow: '0 1px 3px rgba(0,0,0,0.55), 0 0 10px rgba(240, 180, 60, 0.6)',
98
+ });
99
+
100
+ globalStyle('.zylem-timeline-keyframe[data-active]', {
101
+ boxShadow: '0 1px 3px rgba(0,0,0,0.55), 0 0 12px rgba(240, 180, 60, 0.8)',
102
+ });
103
+
104
+ globalStyle('.zylem-timeline-keyframe:focus-visible', {
105
+ outline: 'none',
106
+ boxShadow: vars.effects.focusRing,
107
+ });
108
+
109
+ globalStyle('.zylem-timeline-thumb', {
110
+ display: 'block',
111
+ width: '14px',
112
+ height: `calc(${TRACK_HEIGHT} + 6px)`,
113
+ top: '-4px',
114
+ borderRadius: '5px',
115
+ border: '1px solid rgba(255,255,255,0.55)',
116
+ background:
117
+ 'linear-gradient(180deg, rgba(240,250,255,0.96), rgba(140,190,235,0.92))',
118
+ boxShadow: `inset 0 1px 0 rgba(255,255,255,0.75), 0 1px 4px rgba(0,0,0,0.5), ${vars.effects.glowPrimary}`,
119
+ cursor: 'grab',
120
+ outline: 'none',
121
+ transition: `box-shadow ${vars.motion.fast} ${vars.motion.easeOut}`,
122
+ });
123
+
124
+ globalStyle('.zylem-timeline-thumb:active', {
125
+ cursor: 'grabbing',
126
+ });
127
+
128
+ globalStyle('.zylem-timeline-thumb:focus-visible', {
129
+ boxShadow: `inset 0 1px 0 rgba(255,255,255,0.75), ${vars.effects.focusRing}`,
130
+ });
131
+
132
+ globalStyle('.zylem-timeline-thumb-input', {
133
+ position: 'absolute',
134
+ width: '1px',
135
+ height: '1px',
136
+ padding: 0,
137
+ margin: '-1px',
138
+ overflow: 'hidden',
139
+ clip: 'rect(0, 0, 0, 0)',
140
+ whiteSpace: 'nowrap',
141
+ border: 0,
142
+ });
143
+
144
+ // Fixed box so the readout doesn't reflow the track as digits change:
145
+ // wide enough for `59.99s / 120s`, one track-height tall, right-aligned.
146
+ globalStyle('.zylem-timeline-time', {
147
+ boxSizing: 'border-box',
148
+ flex: 'none',
149
+ display: 'inline-flex',
150
+ alignItems: 'center',
151
+ justifyContent: 'flex-end',
152
+ width: '12ch',
153
+ height: TRACK_HEIGHT,
154
+ lineHeight: TRACK_HEIGHT,
155
+ fontSize: `calc(${vars.typography.fontSize} - 1px)`,
156
+ fontWeight: 600,
157
+ fontVariantNumeric: 'tabular-nums',
158
+ color: vars.colors.text,
159
+ textShadow: '0 1px 1px rgba(0,0,0,0.6)',
160
+ textAlign: 'right',
161
+ whiteSpace: 'nowrap',
162
+ overflow: 'hidden',
163
+ });
@@ -0,0 +1,90 @@
1
+ import { Slider as KSlider } from '@kobalte/core';
2
+ import { For, Show, createMemo } from 'solid-js';
3
+ import { formatSeconds, intervalTicks, markerPercent } from './timeline-math';
4
+
5
+ export interface TimelineTrackProps {
6
+ value: number;
7
+ onChange?: (value: number) => void;
8
+ min?: number;
9
+ max: number;
10
+ step?: number;
11
+ /** Times to mark with a diamond. */
12
+ keyframes?: readonly number[];
13
+ /** Explicit tick times to mark with a dot. */
14
+ ticks?: readonly number[];
15
+ /** Or split the range into this many equal intervals and dot the boundaries. */
16
+ intervals?: number;
17
+ onKeyframeClick?: (time: number) => void;
18
+ /** Show `value / max` as seconds after the track. */
19
+ showTime?: boolean;
20
+ disabled?: boolean;
21
+ label?: string;
22
+ class?: string;
23
+ }
24
+
25
+ /**
26
+ * Director scrub track: a taller inset glass rail with the playhead as a
27
+ * glossy thumb, keyframes as diamonds, and interval boundaries as dots.
28
+ */
29
+ export function TimelineTrack(props: TimelineTrackProps) {
30
+ const min = () => props.min ?? 0;
31
+ const ticks = createMemo(() => {
32
+ if (props.ticks) return [...props.ticks];
33
+ if (props.intervals) return intervalTicks(props.intervals, min(), props.max);
34
+ return [];
35
+ });
36
+ const pct = (t: number) => `${markerPercent(t, min(), props.max)}%`;
37
+
38
+ return (
39
+ <div class={props.class ? `zylem-timeline ${props.class}` : 'zylem-timeline'}>
40
+ <KSlider.Root
41
+ class="zylem-timeline-root"
42
+ value={[props.value]}
43
+ onChange={(values) => props.onChange?.(values[0] ?? min())}
44
+ minValue={min()}
45
+ maxValue={props.max}
46
+ step={props.step ?? 0.01}
47
+ disabled={props.disabled}
48
+ getValueLabel={(params) => formatSeconds(params.values[0] ?? 0)}
49
+ >
50
+ <Show when={props.label}>
51
+ <KSlider.Label class="zylem-timeline-label">{props.label}</KSlider.Label>
52
+ </Show>
53
+ <KSlider.Track class="zylem-timeline-track">
54
+ <KSlider.Fill class="zylem-timeline-fill" />
55
+ <div class="zylem-timeline-markers" aria-hidden="true">
56
+ <For each={ticks()}>
57
+ {(t) => <span class="zylem-timeline-tick" style={{ left: pct(t) }} />}
58
+ </For>
59
+ </div>
60
+ <Show when={props.keyframes?.length}>
61
+ <div class="zylem-timeline-keyframes">
62
+ <For each={props.keyframes}>
63
+ {(t) => (
64
+ <button
65
+ type="button"
66
+ class="zylem-timeline-keyframe"
67
+ style={{ left: pct(t) }}
68
+ aria-label={`Keyframe at ${formatSeconds(t)}`}
69
+ data-active={Math.abs(t - props.value) < (props.step ?? 0.01) ? '' : undefined}
70
+ disabled={props.disabled}
71
+ onPointerDown={(event) => event.stopPropagation()}
72
+ onClick={() => props.onKeyframeClick?.(t)}
73
+ />
74
+ )}
75
+ </For>
76
+ </div>
77
+ </Show>
78
+ <KSlider.Thumb class="zylem-timeline-thumb">
79
+ <KSlider.Input class="zylem-timeline-thumb-input" />
80
+ </KSlider.Thumb>
81
+ </KSlider.Track>
82
+ </KSlider.Root>
83
+ <Show when={props.showTime}>
84
+ <span class="zylem-timeline-time">
85
+ {formatSeconds(props.value)} / {formatSeconds(props.max)}
86
+ </span>
87
+ </Show>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Position of `t` along a track, as a 0–100 percentage clamped to the
3
+ * track's range. A degenerate range (max <= min) pins everything to 0.
4
+ */
5
+ export function markerPercent(t: number, min: number, max: number): number {
6
+ if (!Number.isFinite(t) || max <= min) {
7
+ return 0;
8
+ }
9
+ const ratio = (t - min) / (max - min);
10
+ return Math.min(100, Math.max(0, ratio * 100));
11
+ }
12
+
13
+ /**
14
+ * Evenly spaced tick times between `min` and `max` (exclusive of both ends),
15
+ * one per interval boundary: `intervals = 4` yields three ticks.
16
+ */
17
+ export function intervalTicks(intervals: number, min: number, max: number): number[] {
18
+ if (!Number.isFinite(intervals) || intervals < 2 || max <= min) {
19
+ return [];
20
+ }
21
+ const count = Math.floor(intervals);
22
+ const span = max - min;
23
+ const ticks: number[] = [];
24
+ for (let i = 1; i < count; i += 1) {
25
+ ticks.push(min + (span * i) / count);
26
+ }
27
+ return ticks;
28
+ }
29
+
30
+ /**
31
+ * Seconds as a short readout: whole seconds bare (`10s`), fractions to two
32
+ * places with trailing zeros trimmed (`5.62s`, `2.5s`).
33
+ */
34
+ export function formatSeconds(seconds: number): string {
35
+ if (!Number.isFinite(seconds)) {
36
+ return '0s';
37
+ }
38
+ const rounded = Math.round(seconds * 100) / 100;
39
+ if (Number.isInteger(rounded)) {
40
+ return `${rounded}s`;
41
+ }
42
+ return `${rounded.toFixed(2).replace(/0+$/, '')}s`;
43
+ }
@@ -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,92 @@
1
+ import Circle from 'lucide-solid/icons/circle';
2
+ import Eraser from 'lucide-solid/icons/eraser';
3
+ import Hand from 'lucide-solid/icons/hand';
4
+ import PaintBucket from 'lucide-solid/icons/paint-bucket';
5
+ import Pencil from 'lucide-solid/icons/pencil';
6
+ import Pipette from 'lucide-solid/icons/pipette';
7
+ import Square from 'lucide-solid/icons/square';
8
+ import SquareDashed from 'lucide-solid/icons/square-dashed';
9
+ import { type Component, type JSX } from 'solid-js';
10
+ import { Dynamic } from 'solid-js/web';
11
+ import { ToolbarButton } from '../ToolbarButton/ToolbarButton';
12
+
13
+ /** Pixel-editor tools with a built-in icon and label. */
14
+ export type ArtTool =
15
+ | 'pencil'
16
+ | 'fill'
17
+ | 'eraser'
18
+ | 'rectangle'
19
+ | 'ellipse'
20
+ | 'select'
21
+ | 'eyedropper'
22
+ | 'move';
23
+
24
+ type IconComponent = Component<JSX.SvgSVGAttributes<SVGSVGElement>>;
25
+
26
+ export const ART_TOOL_ICONS: Record<ArtTool, IconComponent> = {
27
+ pencil: Pencil,
28
+ fill: PaintBucket,
29
+ eraser: Eraser,
30
+ rectangle: Square,
31
+ ellipse: Circle,
32
+ select: SquareDashed,
33
+ eyedropper: Pipette,
34
+ move: Hand,
35
+ };
36
+
37
+ export const ART_TOOL_LABELS: Record<ArtTool, string> = {
38
+ pencil: 'Pencil',
39
+ fill: 'Fill',
40
+ eraser: 'Eraser',
41
+ rectangle: 'Rectangle',
42
+ ellipse: 'Ellipse',
43
+ select: 'Select',
44
+ eyedropper: 'Eyedropper',
45
+ move: 'Move',
46
+ };
47
+
48
+ /** Every tool in the order the editor's strip shows them. */
49
+ export const ART_TOOLS: readonly ArtTool[] = [
50
+ 'pencil',
51
+ 'fill',
52
+ 'eraser',
53
+ 'rectangle',
54
+ 'ellipse',
55
+ 'select',
56
+ 'eyedropper',
57
+ 'move',
58
+ ];
59
+
60
+ export interface ToolButtonProps {
61
+ tool: ArtTool;
62
+ /** Tooltip / accessible name; defaults to the tool's label. */
63
+ label?: string;
64
+ /** Keyboard shortcut shown after the label in the tooltip, e.g. `B`. */
65
+ shortcut?: string;
66
+ selected?: boolean;
67
+ disabled?: boolean;
68
+ onClick?: (event: MouseEvent) => void;
69
+ class?: string;
70
+ }
71
+
72
+ /**
73
+ * Toolbar button preset for one pixel-editor tool: same glass jewel as
74
+ * `ToolbarButton`, with the tool's lucide icon and label filled in.
75
+ */
76
+ export function ToolButton(props: ToolButtonProps) {
77
+ const label = () => {
78
+ const base = props.label ?? ART_TOOL_LABELS[props.tool];
79
+ return props.shortcut ? `${base} (${props.shortcut})` : base;
80
+ };
81
+ return (
82
+ <ToolbarButton
83
+ label={label()}
84
+ selected={props.selected}
85
+ disabled={props.disabled}
86
+ onClick={props.onClick}
87
+ class={props.class ? `zylem-tool-btn ${props.class}` : 'zylem-tool-btn'}
88
+ >
89
+ <Dynamic component={ART_TOOL_ICONS[props.tool]} class="zylem-icon" data-tool={props.tool} />
90
+ </ToolbarButton>
91
+ );
92
+ }
@@ -0,0 +1,35 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ /** Shared uppercase caption used by the art-tool groups and strips. */
5
+ export const artCaption = {
6
+ fontFamily: vars.typography.fontFamily,
7
+ fontSize: '11px',
8
+ fontWeight: 600,
9
+ letterSpacing: '0.08em',
10
+ textTransform: 'uppercase',
11
+ color: vars.colors.textSecondary,
12
+ whiteSpace: 'nowrap',
13
+ } as const;
14
+
15
+ globalStyle('.zylem-tool-group', {
16
+ display: 'inline-flex',
17
+ alignItems: 'center',
18
+ gap: vars.spacing.md,
19
+ minWidth: 0,
20
+ });
21
+
22
+ globalStyle('.zylem-tool-group-label', artCaption);
23
+
24
+ globalStyle('.zylem-tool-group-items', {
25
+ display: 'flex',
26
+ alignItems: 'center',
27
+ flexWrap: 'wrap',
28
+ gap: vars.spacing.sm,
29
+ });
30
+
31
+ // Tool buttons are square; the label lives in the tooltip.
32
+ globalStyle('.zylem-tool-btn', {
33
+ width: '34px',
34
+ padding: 0,
35
+ });
@@ -0,0 +1,29 @@
1
+ import { type JSX, splitProps } from 'solid-js';
2
+
3
+ export interface ToolGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {
4
+ /** Caption shown before the controls, e.g. `Tools` or `Palette`. */
5
+ label: string;
6
+ children: JSX.Element;
7
+ }
8
+
9
+ /**
10
+ * Captioned strip for a row of related controls: `TOOLS` followed by tool
11
+ * buttons, `PALETTE` followed by swatches. Several groups sit side by side
12
+ * on one rail.
13
+ */
14
+ export function ToolGroup(props: ToolGroupProps) {
15
+ const [local, rest] = splitProps(props, ['label', 'children', 'class']);
16
+ return (
17
+ <div
18
+ role="group"
19
+ aria-label={local.label}
20
+ class={local.class ? `zylem-tool-group ${local.class}` : 'zylem-tool-group'}
21
+ {...rest}
22
+ >
23
+ <span class="zylem-tool-group-label" aria-hidden="true">
24
+ {local.label}
25
+ </span>
26
+ <div class="zylem-tool-group-items">{local.children}</div>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,131 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+ import { artCaption } from '../ToolGroup/ToolGroup.css';
4
+
5
+ /**
6
+ * Toolbox tile: `.zylem-toolbar-btn` at panel scale. Same border, glass,
7
+ * press and selected treatment; icon stacked over a label.
8
+ */
9
+ globalStyle('.zylem-toolbox-btn', {
10
+ boxSizing: 'border-box',
11
+ display: 'inline-flex',
12
+ flexDirection: 'column',
13
+ alignItems: 'center',
14
+ justifyContent: 'center',
15
+ gap: vars.spacing.sm,
16
+ width: '100%',
17
+ minHeight: '72px',
18
+ padding: `${vars.spacing.md} ${vars.spacing.sm}`,
19
+ fontFamily: vars.typography.fontFamily,
20
+ fontSize: vars.typography.fontSize,
21
+ fontWeight: 600,
22
+ lineHeight: 1,
23
+ whiteSpace: 'nowrap',
24
+ textShadow: '0 1px 1px rgba(0,0,0,0.6)',
25
+ cursor: 'pointer',
26
+ userSelect: 'none',
27
+ borderRadius: vars.radii.button,
28
+ border: '1px solid rgba(150, 210, 255, 0.5)',
29
+ background: vars.material.buttonGlass,
30
+ boxShadow: vars.effects.shadowButton,
31
+ color: '#E8F4FF',
32
+ transition: `transform ${vars.motion.fast} ${vars.motion.easeOut}, box-shadow ${vars.motion.fast} ${vars.motion.easeOut}, filter ${vars.motion.fast} ${vars.motion.easeOut}, background ${vars.motion.fast} ${vars.motion.easeOut}`,
33
+ });
34
+
35
+ globalStyle('.zylem-toolbox-btn:hover:not([disabled])', {
36
+ background: vars.material.buttonGlassHover,
37
+ boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowPrimary}`,
38
+ transform: 'translateY(-1px)',
39
+ });
40
+
41
+ globalStyle('.zylem-toolbox-btn:active:not([disabled])', {
42
+ transform: 'translateY(1px)',
43
+ filter: 'brightness(0.92)',
44
+ boxShadow:
45
+ 'inset 0 1px 0 rgba(255,255,255,0.22), inset 0 -1px 0 rgba(255,255,255,0.55), inset 0 2px 3px rgba(0,0,0,0.28), 0 1px 3px rgba(0,0,0,0.28)',
46
+ });
47
+
48
+ globalStyle('.zylem-toolbox-btn:focus-visible', {
49
+ outline: 'none',
50
+ boxShadow: `${vars.effects.shadowButton}, ${vars.effects.focusRing}`,
51
+ });
52
+
53
+ globalStyle('.zylem-toolbox-btn[data-selected]', {
54
+ borderColor: 'rgba(20, 255, 60, 0.78)',
55
+ background: vars.material.buttonGlassActive,
56
+ boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowActive}`,
57
+ });
58
+
59
+ globalStyle('.zylem-toolbox-btn[disabled]', {
60
+ opacity: 0.45,
61
+ cursor: 'not-allowed',
62
+ });
63
+
64
+ globalStyle('.zylem-toolbox-btn-icon', {
65
+ display: 'inline-flex',
66
+ alignItems: 'center',
67
+ justifyContent: 'center',
68
+ width: '24px',
69
+ height: '24px',
70
+ });
71
+
72
+ globalStyle('.zylem-toolbox-btn-icon svg', {
73
+ width: '24px',
74
+ height: '24px',
75
+ stroke: 'currentColor',
76
+ filter: 'drop-shadow(0 1px 1px rgba(0,0,0,0.55))',
77
+ });
78
+
79
+ globalStyle('.zylem-toolbox-btn-label', {
80
+ display: 'block',
81
+ });
82
+
83
+ /**
84
+ * Extra large tiles. A button's own `data-size` wins; otherwise it inherits
85
+ * the enclosing grid's. `lg` suits three columns, `xl` two.
86
+ */
87
+ const lg = (suffix = '') =>
88
+ `.zylem-toolbox-btn[data-size="lg"]${suffix}, .zylem-toolbox-grid[data-size="lg"] .zylem-toolbox-btn:not([data-size])${suffix}`;
89
+ const xl = (suffix = '') =>
90
+ `.zylem-toolbox-btn[data-size="xl"]${suffix}, .zylem-toolbox-grid[data-size="xl"] .zylem-toolbox-btn:not([data-size])${suffix}`;
91
+
92
+ globalStyle(lg(), {
93
+ minHeight: '96px',
94
+ gap: vars.spacing.md,
95
+ padding: `${vars.spacing.lg} ${vars.spacing.sm}`,
96
+ fontSize: `calc(${vars.typography.fontSize} + 1px)`,
97
+ borderRadius: vars.radii.card,
98
+ });
99
+
100
+ globalStyle(`${lg(' .zylem-toolbox-btn-icon')}, ${lg(' .zylem-toolbox-btn-icon svg')}`, {
101
+ width: '32px',
102
+ height: '32px',
103
+ });
104
+
105
+ globalStyle(xl(), {
106
+ minHeight: '120px',
107
+ gap: vars.spacing.md,
108
+ padding: `${vars.spacing.xl} ${vars.spacing.md}`,
109
+ fontSize: `calc(${vars.typography.fontSize} + 2px)`,
110
+ borderRadius: vars.radii.card,
111
+ });
112
+
113
+ globalStyle(`${xl(' .zylem-toolbox-btn-icon')}, ${xl(' .zylem-toolbox-btn-icon svg')}`, {
114
+ width: '40px',
115
+ height: '40px',
116
+ });
117
+
118
+ globalStyle('.zylem-toolbox-grid', {
119
+ display: 'flex',
120
+ flexDirection: 'column',
121
+ gap: vars.spacing.sm,
122
+ minWidth: 0,
123
+ });
124
+
125
+ globalStyle('.zylem-toolbox-grid-label', artCaption);
126
+
127
+ globalStyle('.zylem-toolbox-grid-items', {
128
+ display: 'grid',
129
+ gridTemplateColumns: 'repeat(var(--zylem-toolbox-columns, 2), minmax(0, 1fr))',
130
+ gap: vars.spacing.sm,
131
+ });