@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
package/dist/styles.js CHANGED
@@ -1 +1 @@
1
- import"./chunk-3TP2SNNW.js";
1
+ import"./chunk-V47CGFWX.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zylem/ui",
3
- "version": "0.2.3",
3
+ "version": "0.8.9",
4
4
  "description": "Design tokens, global styles, and Solid components for Zylem, built with vanilla-extract",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,21 +34,21 @@
34
34
  "types": "./src/components/index.ts",
35
35
  "import": "./src/components/index.ts"
36
36
  },
37
+ "./build-label": {
38
+ "types": "./src/components/BuildLabel/mount-build-label.ts",
39
+ "import": "./src/components/BuildLabel/mount-build-label.ts"
40
+ },
41
+ "./vite": {
42
+ "types": "./vite/index.ts",
43
+ "import": "./vite/index.ts"
44
+ },
37
45
  "./styles.css": "./dist/styles.css"
38
46
  },
39
47
  "files": [
40
48
  "dist",
41
- "src"
49
+ "src",
50
+ "vite"
42
51
  ],
43
- "scripts": {
44
- "build": "tsup",
45
- "prepublishOnly": "NODE_ENV=production tsup",
46
- "dev": "tsup --watch",
47
- "showcase": "vite --host",
48
- "test": "vitest run",
49
- "test:watch": "vitest",
50
- "lint": "biome check ."
51
- },
52
52
  "peerDependencies": {
53
53
  "solid-js": "^1.9.9"
54
54
  },
@@ -82,5 +82,12 @@
82
82
  "node": ">=22.12.0",
83
83
  "pnpm": ">=10.32.1"
84
84
  },
85
- "packageManager": "pnpm@10.33.0"
86
- }
85
+ "scripts": {
86
+ "build": "tsup",
87
+ "dev": "tsup --watch",
88
+ "showcase": "vite --host",
89
+ "test": "vitest run",
90
+ "test:watch": "vitest",
91
+ "lint": "biome check ."
92
+ }
93
+ }
@@ -0,0 +1,78 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ /** Swatch / tile edge length shared by the palette and add tiles. */
5
+ export const TILE_SIZE = '26px';
6
+ /** Frame thumb edge length shared by the strip and the tall add tile. */
7
+ export const FRAME_SIZE = '56px';
8
+
9
+ globalStyle('.zylem-add-tile', {
10
+ boxSizing: 'border-box',
11
+ display: 'inline-flex',
12
+ flexDirection: 'column',
13
+ alignItems: 'center',
14
+ justifyContent: 'center',
15
+ gap: vars.spacing.xs,
16
+ flex: 'none',
17
+ padding: 0,
18
+ margin: 0,
19
+ cursor: 'pointer',
20
+ userSelect: 'none',
21
+ color: vars.colors.textSecondary,
22
+ fontFamily: vars.typography.fontFamily,
23
+ borderRadius: vars.radii.control,
24
+ border: '1px dashed rgba(150, 210, 255, 0.55)',
25
+ background: 'rgba(97, 166, 232, 0.06)',
26
+ transition: `color ${vars.motion.fast} ${vars.motion.easeOut}, border-color ${vars.motion.fast} ${vars.motion.easeOut}, background ${vars.motion.fast} ${vars.motion.easeOut}, box-shadow ${vars.motion.fast} ${vars.motion.easeOut}`,
27
+ });
28
+
29
+ globalStyle('.zylem-add-tile[data-variant="square"]', {
30
+ width: TILE_SIZE,
31
+ height: TILE_SIZE,
32
+ });
33
+
34
+ globalStyle('.zylem-add-tile[data-variant="tall"]', {
35
+ width: FRAME_SIZE,
36
+ height: `calc(${FRAME_SIZE} + 18px)`,
37
+ });
38
+
39
+ globalStyle('.zylem-add-tile:hover:not([disabled])', {
40
+ color: '#E8F4FF',
41
+ borderColor: 'rgba(178, 219, 255, 0.9)',
42
+ background: 'rgba(97, 166, 232, 0.16)',
43
+ boxShadow: vars.effects.glowPrimary,
44
+ });
45
+
46
+ globalStyle('.zylem-add-tile:active:not([disabled])', {
47
+ background: 'rgba(97, 166, 232, 0.24)',
48
+ });
49
+
50
+ globalStyle('.zylem-add-tile:focus-visible', {
51
+ outline: 'none',
52
+ boxShadow: vars.effects.focusRing,
53
+ });
54
+
55
+ globalStyle('.zylem-add-tile[disabled]', {
56
+ opacity: 0.45,
57
+ cursor: 'not-allowed',
58
+ });
59
+
60
+ globalStyle('.zylem-add-tile-icon', {
61
+ display: 'block',
62
+ width: '16px',
63
+ height: '16px',
64
+ stroke: 'currentColor',
65
+ flex: 'none',
66
+ });
67
+
68
+ globalStyle('.zylem-add-tile[data-variant="tall"] .zylem-add-tile-icon', {
69
+ width: '20px',
70
+ height: '20px',
71
+ });
72
+
73
+ globalStyle('.zylem-add-tile-label', {
74
+ fontSize: '11px',
75
+ fontWeight: 600,
76
+ lineHeight: 1,
77
+ whiteSpace: 'nowrap',
78
+ });
@@ -0,0 +1,37 @@
1
+ import Plus from 'lucide-solid/icons/plus';
2
+ import { Show } from 'solid-js';
3
+
4
+ export interface AddTileProps {
5
+ /** Text under the plus (tall variant) and the accessible name. */
6
+ label?: string;
7
+ /** `square` matches a swatch; `tall` matches a frame thumb with room for text. */
8
+ variant?: 'square' | 'tall';
9
+ onClick?: (event: MouseEvent) => void;
10
+ disabled?: boolean;
11
+ class?: string;
12
+ }
13
+
14
+ /**
15
+ * Dashed "add" tile that sits at the end of a swatch row or frame strip. The
16
+ * plus is centered on its own; the tall variant stacks a label beneath it.
17
+ */
18
+ export function AddTile(props: AddTileProps) {
19
+ const variant = () => props.variant ?? 'square';
20
+ const label = () => props.label ?? 'Add';
21
+ return (
22
+ <button
23
+ type="button"
24
+ class={props.class ? `zylem-add-tile ${props.class}` : 'zylem-add-tile'}
25
+ data-variant={variant()}
26
+ aria-label={variant() === 'square' || !props.label ? label() : undefined}
27
+ title={variant() === 'square' ? label() : undefined}
28
+ disabled={props.disabled}
29
+ onClick={props.onClick}
30
+ >
31
+ <Plus class="zylem-icon zylem-add-tile-icon" aria-hidden="true" />
32
+ <Show when={variant() === 'tall' && props.label}>
33
+ <span class="zylem-add-tile-label">{props.label}</span>
34
+ </Show>
35
+ </button>
36
+ );
37
+ }
@@ -0,0 +1,31 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ /**
5
+ * Tiny corner HUD for the resolved Zylem build number. Sits above game
6
+ * chrome (`tooltip` tier) and stays out of the way of the editor toggle
7
+ * (bottom-right).
8
+ */
9
+ globalStyle('.zylem-build-label', {
10
+ position: 'fixed',
11
+ top: '10px',
12
+ right: '12px',
13
+ zIndex: vars.layers.tooltip,
14
+ pointerEvents: 'auto',
15
+ padding: `2px ${vars.spacing.sm}`,
16
+ borderRadius: '5px',
17
+ border: '1px solid rgba(97, 166, 232, 0.35)',
18
+ background:
19
+ 'linear-gradient(180deg, rgba(60, 80, 105, 0.55), rgba(26, 31, 39, 0.78))',
20
+ boxShadow:
21
+ 'inset 0 1px 0 rgba(255,255,255,0.22), 0 2px 8px rgba(0,0,0,0.35)',
22
+ color: vars.colors.textSecondary,
23
+ fontFamily: vars.typography.fontFamily,
24
+ fontSize: '10px',
25
+ fontWeight: 700,
26
+ letterSpacing: '0.06em',
27
+ lineHeight: 1.4,
28
+ textShadow: '0 1px 1px rgba(0,0,0,0.55)',
29
+ userSelect: 'none',
30
+ whiteSpace: 'nowrap',
31
+ });
@@ -0,0 +1,37 @@
1
+ import { createMemo, onMount } from 'solid-js';
2
+ import {
3
+ formatBuildLabel,
4
+ packagesTitle,
5
+ resolveBuildVersion,
6
+ type BuildLabelModel,
7
+ } from './resolve-build-version';
8
+ import { BUILD_LABEL_ID, ensureFallbackStyles } from './mount-build-label';
9
+
10
+ export type BuildLabelProps = BuildLabelModel;
11
+
12
+ /**
13
+ * Fixed top-right HUD showing the resolved Zylem build number.
14
+ *
15
+ * Demo and showcase apps mount this (or `mountBuildLabel`) so the engine
16
+ * version is visible without opening DevTools. Creator is the exception:
17
+ * it shows only its own version in the menu bar.
18
+ */
19
+ export function BuildLabel(props: BuildLabelProps) {
20
+ const label = createMemo(() => formatBuildLabel(resolveBuildVersion(props)));
21
+ const title = createMemo(() => packagesTitle(props.packages));
22
+
23
+ onMount(() => {
24
+ ensureFallbackStyles();
25
+ });
26
+
27
+ return (
28
+ <div
29
+ id={BUILD_LABEL_ID}
30
+ class="zylem-build-label"
31
+ aria-label="Build version"
32
+ title={title()}
33
+ >
34
+ {label()}
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,77 @@
1
+ import {
2
+ formatBuildLabel,
3
+ packagesTitle,
4
+ resolveBuildVersion,
5
+ type BuildLabelModel,
6
+ } from './resolve-build-version';
7
+
8
+ export type { BuildLabelModel };
9
+
10
+ export const BUILD_LABEL_ID = 'zylem-build-label';
11
+ export const BUILD_LABEL_STYLE_ID = 'zylem-build-label-styles';
12
+
13
+ /**
14
+ * Fallback rules so the HUD still paints in apps that do not import
15
+ * `@zylem/ui/styles.css` (arena). Themed apps get the vanilla-extract class
16
+ * as well; both targets use the same selectors so they do not fight.
17
+ */
18
+ const FALLBACK_CSS = `
19
+ #${BUILD_LABEL_ID} {
20
+ position: fixed;
21
+ top: 10px;
22
+ right: 12px;
23
+ z-index: var(--zylem-layer-tooltip, 4000);
24
+ pointer-events: auto;
25
+ padding: 2px 8px;
26
+ border-radius: 5px;
27
+ border: 1px solid rgba(97, 166, 232, 0.35);
28
+ background: linear-gradient(180deg, rgba(60, 80, 105, 0.55), rgba(26, 31, 39, 0.78));
29
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), 0 2px 8px rgba(0, 0, 0, 0.35);
30
+ color: #c5d0da;
31
+ font-family: var(--zylem-font-family, 'Exo 2', sans-serif);
32
+ font-size: 10px;
33
+ font-weight: 700;
34
+ letter-spacing: 0.06em;
35
+ line-height: 1.4;
36
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.55);
37
+ user-select: none;
38
+ white-space: nowrap;
39
+ }
40
+ `;
41
+
42
+ export function ensureFallbackStyles(): void {
43
+ if (typeof document === 'undefined') return;
44
+ if (document.getElementById(BUILD_LABEL_STYLE_ID)) return;
45
+ const style = document.createElement('style');
46
+ style.id = BUILD_LABEL_STYLE_ID;
47
+ style.textContent = FALLBACK_CSS;
48
+ document.head.append(style);
49
+ }
50
+
51
+ /**
52
+ * Mount the build-number HUD on `parent` (defaults to `document.body`).
53
+ * Replaces a previous mount with the same id.
54
+ */
55
+ export function mountBuildLabel(
56
+ model: BuildLabelModel,
57
+ parent: ParentNode = document.body
58
+ ): HTMLElement {
59
+ ensureFallbackStyles();
60
+ parent.querySelector(`#${BUILD_LABEL_ID}`)?.remove();
61
+
62
+ const el = document.createElement('div');
63
+ el.id = BUILD_LABEL_ID;
64
+ el.className = 'zylem-build-label';
65
+ el.setAttribute('aria-label', 'Build version');
66
+ el.textContent = formatBuildLabel(resolveBuildVersion(model));
67
+ const title = packagesTitle(model.packages);
68
+ if (title) el.title = title;
69
+
70
+ if (model.packages) {
71
+ (globalThis as { __ZYLEM_PACKAGE_VERSIONS__?: Record<string, string> }).__ZYLEM_PACKAGE_VERSIONS__ =
72
+ model.packages;
73
+ }
74
+
75
+ parent.append(el);
76
+ return el;
77
+ }
@@ -0,0 +1,47 @@
1
+ export interface BuildLabelModel {
2
+ /** Host app version, used when `packages` does not resolve a preferred entry. */
3
+ version?: string;
4
+ /** Resolved `@zylem/*` package versions collected at build time. */
5
+ packages?: Record<string, string>;
6
+ }
7
+
8
+ const PREFERRED_PACKAGES = [
9
+ '@zylem/game-lib',
10
+ '@zylem/editor',
11
+ '@zylem/ui',
12
+ '@zylem/behaviors',
13
+ '@zylem/runtime',
14
+ ] as const;
15
+
16
+ /**
17
+ * Pick the version to show as the HUD build number.
18
+ *
19
+ * Demo/showcase apps prefer the engine (`@zylem/game-lib`) so the overlay
20
+ * reports the stack being exercised rather than the host app's own version.
21
+ */
22
+ export function resolveBuildVersion(model: BuildLabelModel): string {
23
+ const packages = model.packages;
24
+ if (packages) {
25
+ for (const name of PREFERRED_PACKAGES) {
26
+ const version = packages[name];
27
+ if (version) return version;
28
+ }
29
+ const first = Object.values(packages)[0];
30
+ if (first) return first;
31
+ }
32
+ return model.version ?? '';
33
+ }
34
+
35
+ /** Display form used in the HUD: the version prefixed with `v`. */
36
+ export function formatBuildLabel(version: string): string {
37
+ if (!version) return '';
38
+ return version.startsWith('v') ? version : `v${version}`;
39
+ }
40
+
41
+ /** Hover text listing every collected package. */
42
+ export function packagesTitle(packages: Record<string, string> | undefined): string | undefined {
43
+ if (!packages || Object.keys(packages).length === 0) return undefined;
44
+ return Object.entries(packages)
45
+ .map(([name, version]) => `${name} ${version}`)
46
+ .join('\n');
47
+ }
@@ -0,0 +1,51 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+ import { TILE_SIZE } from '../AddTile/AddTile.css';
4
+ import { checkerBackground } from '../PixelGrid/PixelGrid.css';
5
+
6
+ globalStyle('.zylem-palette', {
7
+ display: 'flex',
8
+ alignItems: 'center',
9
+ flexWrap: 'wrap',
10
+ gap: vars.spacing.sm,
11
+ });
12
+
13
+ globalStyle('.zylem-swatch', {
14
+ boxSizing: 'border-box',
15
+ width: TILE_SIZE,
16
+ height: TILE_SIZE,
17
+ flex: 'none',
18
+ padding: 0,
19
+ margin: 0,
20
+ cursor: 'pointer',
21
+ borderRadius: vars.radii.control,
22
+ border: '1px solid rgba(150, 210, 255, 0.4)',
23
+ background: 'var(--zylem-swatch-color, transparent)',
24
+ boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.22), 0 1px 3px rgba(0,0,0,0.4)',
25
+ transition: `transform ${vars.motion.fast} ${vars.motion.easeOut}, box-shadow ${vars.motion.fast} ${vars.motion.easeOut}`,
26
+ });
27
+
28
+ globalStyle('.zylem-swatch[data-transparent]', {
29
+ vars: { '--zylem-pixel-grid-cell': '5px' },
30
+ background: checkerBackground,
31
+ });
32
+
33
+ globalStyle('.zylem-swatch:hover:not([disabled])', {
34
+ transform: 'translateY(-1px)',
35
+ boxShadow: `inset 0 1px 0 rgba(255,255,255,0.3), 0 2px 6px rgba(0,0,0,0.45), ${vars.effects.glowPrimary}`,
36
+ });
37
+
38
+ globalStyle('.zylem-swatch:focus-visible', {
39
+ outline: 'none',
40
+ boxShadow: vars.effects.focusRing,
41
+ });
42
+
43
+ globalStyle('.zylem-swatch[data-selected]', {
44
+ borderColor: '#F4FAFF',
45
+ boxShadow: `0 0 0 2px rgba(10, 20, 30, 0.9), 0 0 0 3px rgba(244, 250, 255, 0.85), ${vars.effects.glowPrimary}`,
46
+ });
47
+
48
+ globalStyle('.zylem-swatch[disabled]', {
49
+ opacity: 0.45,
50
+ cursor: 'not-allowed',
51
+ });
@@ -0,0 +1,78 @@
1
+ import { For, Show } from 'solid-js';
2
+ import { AddTile } from '../AddTile/AddTile';
3
+
4
+ /** Sentinel colour for the eraser / transparent swatch. */
5
+ export const TRANSPARENT = 'transparent';
6
+
7
+ export interface SwatchProps {
8
+ /** Any CSS colour, or `'transparent'` for the checkered swatch. */
9
+ color: string;
10
+ selected?: boolean;
11
+ onClick?: (event: MouseEvent) => void;
12
+ /** Accessible name; defaults to the colour string. */
13
+ label?: string;
14
+ disabled?: boolean;
15
+ class?: string;
16
+ }
17
+
18
+ /** One palette colour: a small glass-edged square, ringed when selected. */
19
+ export function Swatch(props: SwatchProps) {
20
+ const transparent = () => props.color === TRANSPARENT;
21
+ return (
22
+ <button
23
+ type="button"
24
+ class={props.class ? `zylem-swatch ${props.class}` : 'zylem-swatch'}
25
+ style={transparent() ? undefined : { '--zylem-swatch-color': props.color }}
26
+ data-transparent={transparent() ? '' : undefined}
27
+ data-selected={props.selected ? '' : undefined}
28
+ aria-pressed={props.selected ? 'true' : 'false'}
29
+ aria-label={props.label ?? (transparent() ? 'Transparent' : props.color)}
30
+ title={props.label ?? (transparent() ? 'Transparent' : props.color)}
31
+ disabled={props.disabled}
32
+ onClick={props.onClick}
33
+ />
34
+ );
35
+ }
36
+
37
+ export interface ColorPaletteProps {
38
+ colors: readonly string[];
39
+ /** Currently selected colour (compare with `colors` entries or `'transparent'`). */
40
+ value?: string;
41
+ onSelect?: (color: string) => void;
42
+ /** When provided, an add tile follows the swatches. */
43
+ onAdd?: () => void;
44
+ /** Lead with the transparent swatch. Defaults to true. */
45
+ includeTransparent?: boolean;
46
+ class?: string;
47
+ }
48
+
49
+ /** Row of swatches with an optional transparent lead and add tile. */
50
+ export function ColorPalette(props: ColorPaletteProps) {
51
+ return (
52
+ <div
53
+ role="listbox"
54
+ aria-label="Palette"
55
+ class={props.class ? `zylem-palette ${props.class}` : 'zylem-palette'}
56
+ >
57
+ <Show when={props.includeTransparent ?? true}>
58
+ <Swatch
59
+ color={TRANSPARENT}
60
+ selected={props.value === TRANSPARENT}
61
+ onClick={() => props.onSelect?.(TRANSPARENT)}
62
+ />
63
+ </Show>
64
+ <For each={props.colors}>
65
+ {(color) => (
66
+ <Swatch
67
+ color={color}
68
+ selected={props.value === color}
69
+ onClick={() => props.onSelect?.(color)}
70
+ />
71
+ )}
72
+ </For>
73
+ <Show when={props.onAdd}>
74
+ <AddTile label="Add color" onClick={() => props.onAdd?.()} />
75
+ </Show>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,132 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+ import { FRAME_SIZE } from '../AddTile/AddTile.css';
4
+ import { checkerBackground } from '../PixelGrid/PixelGrid.css';
5
+ import { artCaption } from '../ToolGroup/ToolGroup.css';
6
+
7
+ globalStyle('.zylem-frame-strip', {
8
+ display: 'flex',
9
+ flexDirection: 'column',
10
+ gap: vars.spacing.sm,
11
+ minWidth: 0,
12
+ fontFamily: vars.typography.fontFamily,
13
+ });
14
+
15
+ globalStyle('.zylem-frame-strip-header', {
16
+ display: 'flex',
17
+ alignItems: 'baseline',
18
+ gap: vars.spacing.sm,
19
+ });
20
+
21
+ globalStyle('.zylem-frame-strip-label', artCaption);
22
+
23
+ globalStyle('.zylem-frame-strip-title', {
24
+ fontSize: `calc(${vars.typography.fontSize} - 1px)`,
25
+ fontWeight: 600,
26
+ color: vars.colors.text,
27
+ });
28
+
29
+ globalStyle('.zylem-frame-strip-row', {
30
+ display: 'flex',
31
+ alignItems: 'flex-start',
32
+ gap: vars.spacing.sm,
33
+ padding: `${vars.spacing.xs} ${vars.spacing.xs} ${vars.spacing.sm}`,
34
+ overflowX: 'auto',
35
+ overflowY: 'hidden',
36
+ });
37
+
38
+ // Thumb: checker surface with the frame number in a glass badge beneath.
39
+ globalStyle('.zylem-frame-thumb', {
40
+ boxSizing: 'border-box',
41
+ position: 'relative',
42
+ display: 'flex',
43
+ flexDirection: 'column',
44
+ flex: 'none',
45
+ width: FRAME_SIZE,
46
+ height: `calc(${FRAME_SIZE} + 18px)`,
47
+ padding: 0,
48
+ margin: 0,
49
+ cursor: 'pointer',
50
+ overflow: 'hidden',
51
+ borderRadius: vars.radii.control,
52
+ border: '1px solid rgba(150, 210, 255, 0.4)',
53
+ background: vars.material.fieldGlass,
54
+ boxShadow: vars.effects.shadowInset,
55
+ color: '#E8F4FF',
56
+ fontFamily: vars.typography.fontFamily,
57
+ transition: `border-color ${vars.motion.fast} ${vars.motion.easeOut}, box-shadow ${vars.motion.fast} ${vars.motion.easeOut}, transform ${vars.motion.fast} ${vars.motion.easeOut}`,
58
+ });
59
+
60
+ globalStyle('.zylem-frame-thumb:hover', {
61
+ borderColor: 'rgba(178, 219, 255, 0.8)',
62
+ transform: 'translateY(-1px)',
63
+ boxShadow: `${vars.effects.shadowInset}, ${vars.effects.glowPrimary}`,
64
+ });
65
+
66
+ globalStyle('.zylem-frame-thumb:focus-visible', {
67
+ outline: 'none',
68
+ boxShadow: `${vars.effects.shadowInset}, ${vars.effects.focusRing}`,
69
+ });
70
+
71
+ globalStyle('.zylem-frame-thumb[data-selected]', {
72
+ borderColor: 'rgba(178, 219, 255, 0.95)',
73
+ boxShadow: `${vars.effects.shadowInset}, 0 0 0 1px rgba(178, 219, 255, 0.6), ${vars.effects.glowPrimary}`,
74
+ });
75
+
76
+ globalStyle('.zylem-frame-thumb-surface', {
77
+ position: 'relative',
78
+ display: 'block',
79
+ flex: 'none',
80
+ width: '100%',
81
+ aspectRatio: '1 / 1',
82
+ vars: { '--zylem-pixel-grid-cell': '6px' },
83
+ background: checkerBackground,
84
+ imageRendering: 'pixelated',
85
+ });
86
+
87
+ globalStyle(
88
+ '.zylem-frame-thumb-surface > canvas, .zylem-frame-thumb-surface > img, .zylem-frame-thumb-surface > svg',
89
+ {
90
+ position: 'absolute',
91
+ inset: 0,
92
+ width: '100%',
93
+ height: '100%',
94
+ display: 'block',
95
+ imageRendering: 'pixelated',
96
+ }
97
+ );
98
+
99
+ globalStyle('.zylem-frame-thumb-index', {
100
+ display: 'flex',
101
+ alignItems: 'center',
102
+ justifyContent: 'center',
103
+ flex: '1 1 auto',
104
+ fontSize: '11px',
105
+ fontWeight: 600,
106
+ fontVariantNumeric: 'tabular-nums',
107
+ lineHeight: 1,
108
+ background: vars.material.panelHeaderGloss,
109
+ borderTop: '1px solid rgba(150, 210, 255, 0.25)',
110
+ });
111
+
112
+ // Actions row: commands left, step arrows right, all vertically centered.
113
+ globalStyle('.zylem-frame-actions', {
114
+ display: 'flex',
115
+ alignItems: 'center',
116
+ gap: vars.spacing.sm,
117
+ });
118
+
119
+ globalStyle('.zylem-frame-actions .zylem-button .zylem-icon', {
120
+ width: '14px',
121
+ height: '14px',
122
+ stroke: 'currentColor',
123
+ });
124
+
125
+ globalStyle('.zylem-frame-actions-spacer', {
126
+ flex: '1 1 auto',
127
+ });
128
+
129
+ globalStyle('.zylem-frame-actions-step', {
130
+ width: '34px',
131
+ padding: 0,
132
+ });