@zylem/ui 0.2.3 → 0.8.9

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 (39) 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 +20 -13
  8. package/src/components/AddTile/AddTile.css.ts +78 -0
  9. package/src/components/AddTile/AddTile.tsx +37 -0
  10. package/src/components/BuildLabel/BuildLabel.css.ts +31 -0
  11. package/src/components/BuildLabel/BuildLabel.tsx +37 -0
  12. package/src/components/BuildLabel/mount-build-label.ts +77 -0
  13. package/src/components/BuildLabel/resolve-build-version.ts +47 -0
  14. package/src/components/ColorPalette/ColorPalette.css.ts +51 -0
  15. package/src/components/ColorPalette/ColorPalette.tsx +78 -0
  16. package/src/components/FrameStrip/FrameStrip.css.ts +132 -0
  17. package/src/components/FrameStrip/FrameStrip.tsx +118 -0
  18. package/src/components/PixelGrid/PixelGrid.css.ts +30 -0
  19. package/src/components/PixelGrid/PixelGrid.tsx +48 -0
  20. package/src/components/PlaybackControls/PlaybackControls.css.ts +53 -0
  21. package/src/components/PlaybackControls/PlaybackControls.tsx +80 -0
  22. package/src/components/SpriteSizePicker/SpriteSizePicker.css.ts +103 -0
  23. package/src/components/SpriteSizePicker/SpriteSizePicker.tsx +161 -0
  24. package/src/components/SpriteSizePicker/snap-size.ts +28 -0
  25. package/src/components/TimelineTrack/TimelineTrack.css.ts +163 -0
  26. package/src/components/TimelineTrack/TimelineTrack.tsx +90 -0
  27. package/src/components/TimelineTrack/timeline-math.ts +43 -0
  28. package/src/components/ToolButton/ToolButton.tsx +92 -0
  29. package/src/components/ToolGroup/ToolGroup.css.ts +35 -0
  30. package/src/components/ToolGroup/ToolGroup.tsx +29 -0
  31. package/src/components/ToolboxButton/ToolboxButton.css.ts +131 -0
  32. package/src/components/ToolboxButton/ToolboxButton.tsx +88 -0
  33. package/src/components/index.ts +68 -0
  34. package/src/global/hyperglass-base.css.ts +9 -6
  35. package/src/styles.ts +12 -0
  36. package/src/theme.css.ts +15 -0
  37. package/vite/collect-zylem-versions.ts +82 -0
  38. package/vite/index.ts +30 -0
  39. 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,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
+ });
@@ -0,0 +1,88 @@
1
+ import { Button as KButton } from '@kobalte/core';
2
+ import { type JSX, Show, splitProps } from 'solid-js';
3
+
4
+ /**
5
+ * Tile scale. `md` is the compact rail tile; `lg` and `xl` are the extra
6
+ * large tiles sized for three- and two-column toolboxes respectively.
7
+ */
8
+ export type ToolboxSize = 'md' | 'lg' | 'xl';
9
+
10
+ export interface ToolboxButtonProps {
11
+ label: string;
12
+ /** Icon rendered above the label. */
13
+ children: JSX.Element;
14
+ selected?: boolean;
15
+ disabled?: boolean;
16
+ /** Overrides the size inherited from the enclosing `ToolboxGrid`. */
17
+ size?: ToolboxSize;
18
+ onClick?: (event: MouseEvent) => void;
19
+ class?: string;
20
+ }
21
+
22
+ /**
23
+ * Large toolbox tile: the toolbar button's glass jewel scaled up, with the
24
+ * icon stacked over a bold label. Selected renders as the live-green state.
25
+ */
26
+ export function ToolboxButton(props: ToolboxButtonProps) {
27
+ return (
28
+ <KButton.Root
29
+ class={props.class ? `zylem-toolbox-btn ${props.class}` : 'zylem-toolbox-btn'}
30
+ data-size={props.size}
31
+ data-selected={props.selected ? '' : undefined}
32
+ aria-pressed={props.selected ? 'true' : undefined}
33
+ disabled={props.disabled}
34
+ onClick={props.onClick}
35
+ >
36
+ <span class="zylem-toolbox-btn-icon" aria-hidden="true">
37
+ {props.children}
38
+ </span>
39
+ <span class="zylem-toolbox-btn-label">{props.label}</span>
40
+ </KButton.Root>
41
+ );
42
+ }
43
+
44
+ export interface ToolboxGridProps extends JSX.HTMLAttributes<HTMLDivElement> {
45
+ /** Optional uppercase caption above the grid, e.g. `Current tools`. */
46
+ label?: string;
47
+ /** Columns in the grid. Defaults to 2. */
48
+ columns?: number;
49
+ /**
50
+ * Tile size for every button in the grid that doesn't set its own. `xl`
51
+ * pairs with two columns, `lg` with three; defaults to `md`.
52
+ */
53
+ size?: ToolboxSize;
54
+ children: JSX.Element;
55
+ }
56
+
57
+ /** Captioned grid of `ToolboxButton`s. */
58
+ export function ToolboxGrid(props: ToolboxGridProps) {
59
+ const [local, rest] = splitProps(props, [
60
+ 'label',
61
+ 'columns',
62
+ 'size',
63
+ 'children',
64
+ 'class',
65
+ 'style',
66
+ ]);
67
+ const style = (): JSX.CSSProperties => ({
68
+ '--zylem-toolbox-columns': String(local.columns ?? 2),
69
+ ...(typeof local.style === 'object' ? local.style : {}),
70
+ });
71
+ return (
72
+ <div
73
+ class={local.class ? `zylem-toolbox-grid ${local.class}` : 'zylem-toolbox-grid'}
74
+ role="group"
75
+ aria-label={local.label}
76
+ data-size={local.size}
77
+ style={style()}
78
+ {...rest}
79
+ >
80
+ <Show when={local.label}>
81
+ <span class="zylem-toolbox-grid-label" aria-hidden="true">
82
+ {local.label}
83
+ </span>
84
+ </Show>
85
+ <div class="zylem-toolbox-grid-items">{local.children}</div>
86
+ </div>
87
+ );
88
+ }