@zylem/ui 0.2.0 → 0.8.8

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 (35) hide show
  1. package/dist/chunk-3TP2SNNW.js +1 -0
  2. package/dist/index.css +1 -1
  3. package/dist/index.d.ts +22 -2
  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 +11 -3
  8. package/src/components/Dialog/Dialog.css.ts +3 -2
  9. package/src/components/Dialog/Dialog.tsx +44 -20
  10. package/src/components/DropdownMenu/DropdownMenu.css.ts +1 -1
  11. package/src/components/DropdownMenu/DropdownMenu.tsx +47 -28
  12. package/src/components/ItemPicker/ItemPicker.css.ts +101 -0
  13. package/src/components/ItemPicker/ItemPicker.tsx +108 -0
  14. package/src/components/ItemPicker/filter-items.ts +75 -0
  15. package/src/components/MenuButton/MenuButton.css.ts +91 -0
  16. package/src/components/MenuButton/MenuButton.tsx +153 -0
  17. package/src/components/Property/Property.tsx +8 -6
  18. package/src/components/SearchInput/SearchInput.tsx +3 -0
  19. package/src/components/Select/Select.css.ts +1 -1
  20. package/src/components/Select/Select.tsx +17 -5
  21. package/src/components/ToolbarButton/ToolbarButton.tsx +17 -7
  22. package/src/components/Tooltip/Tooltip.css.ts +1 -1
  23. package/src/components/Tooltip/Tooltip.tsx +18 -6
  24. package/src/components/WindowControls/WindowControls.tsx +7 -5
  25. package/src/components/index.ts +27 -0
  26. package/src/global/detached-panel.css.ts +4 -1
  27. package/src/layers/LayerContext.tsx +132 -0
  28. package/src/layers/dismiss-guard.ts +81 -0
  29. package/src/layers/index.ts +27 -0
  30. package/src/layers/layer-tokens.css.ts +38 -0
  31. package/src/layers/resolve-mount.ts +48 -0
  32. package/src/layers/tiers.ts +53 -0
  33. package/src/styles.ts +3 -0
  34. package/src/theme.css.ts +7 -0
  35. package/dist/chunk-VGOOWBSO.js +0 -1
package/dist/styles.js CHANGED
@@ -1 +1 @@
1
- import"./chunk-VGOOWBSO.js";
1
+ import"./chunk-3TP2SNNW.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zylem/ui",
3
- "version": "0.2.0",
3
+ "version": "0.8.8",
4
4
  "description": "Design tokens, global styles, and Solid components for Zylem, built with vanilla-extract",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -51,13 +51,16 @@
51
51
  "devDependencies": {
52
52
  "@biomejs/biome": "^2.3.7",
53
53
  "@clack/prompts": "^1.6.0",
54
+ "@solidjs/testing-library": "^0.8.10",
54
55
  "@vanilla-extract/esbuild-plugin": "^2.3.22",
55
56
  "@vanilla-extract/vite-plugin": "^5.2.2",
57
+ "happy-dom": "^20.11.2",
56
58
  "solid-js": "^1.9.9",
57
59
  "tsup": "^8.5.1",
58
60
  "typescript": "6.0.3",
59
61
  "vite": "7.0.4",
60
- "vite-plugin-solid": "^2.11.8"
62
+ "vite-plugin-solid": "^2.11.8",
63
+ "vitest": "^4.1.10"
61
64
  },
62
65
  "dependencies": {
63
66
  "@kobalte/core": "^0.13.11",
@@ -66,11 +69,16 @@
66
69
  "@vanilla-extract/sprinkles": "^1.6.5",
67
70
  "lucide-solid": "^0.511.0"
68
71
  },
72
+ "engines": {
73
+ "node": ">=22.12.0",
74
+ "pnpm": ">=10.32.1"
75
+ },
69
76
  "scripts": {
70
- "zylem": "node --experimental-strip-types ./scripts/zylem-runner.ts",
71
77
  "build": "tsup",
72
78
  "dev": "tsup --watch",
73
79
  "showcase": "vite --host",
80
+ "test": "vitest run",
81
+ "test:watch": "vitest",
74
82
  "lint": "biome check ."
75
83
  }
76
84
  }
@@ -14,7 +14,7 @@ const contentEnter = keyframes({
14
14
  globalStyle('.zylem-dialog-overlay', {
15
15
  position: 'fixed',
16
16
  inset: 0,
17
- zIndex: 10000,
17
+ zIndex: vars.layers.modal,
18
18
  background: 'rgba(5, 10, 18, 0.6)',
19
19
  backdropFilter: 'blur(3px)',
20
20
  animation: `${overlayEnter} 0.15s ${vars.motion.easeOut}`,
@@ -23,7 +23,8 @@ globalStyle('.zylem-dialog-overlay', {
23
23
  globalStyle('.zylem-dialog-positioner', {
24
24
  position: 'fixed',
25
25
  inset: 0,
26
- zIndex: 10001,
26
+ // One above the backdrop it shares a tier with.
27
+ zIndex: `calc(${vars.layers.modal} + 1)`,
27
28
  display: 'flex',
28
29
  alignItems: 'center',
29
30
  justifyContent: 'center',
@@ -1,6 +1,7 @@
1
1
  import { Dialog as KDialog } from '@kobalte/core';
2
2
  import { Show } from 'solid-js';
3
3
  import type { JSX } from 'solid-js';
4
+ import { createDismissGuard, LayerScope, useLayer } from '../../layers';
4
5
  import { Button } from '../Button/Button';
5
6
  import { WindowControls } from '../WindowControls/WindowControls';
6
7
 
@@ -16,31 +17,54 @@ export interface DialogProps {
16
17
  /**
17
18
  * HyperGlass dialog: glass window with a glossy title bar and jewel close
18
19
  * control, centered over a dimmed backdrop.
20
+ *
21
+ * The body is a {@link LayerScope}, so overlays opened from inside the dialog
22
+ * stack against the dialog's own content rather than against the page. A tooltip
23
+ * on a dialog field floats over the dialog, but a menu belonging to the page
24
+ * behind still wins — which is what keeps a dialog from leaking overlays across
25
+ * the rest of the app.
19
26
  */
20
27
  export function Dialog(props: DialogProps) {
28
+ const layer = useLayer('modal');
29
+ const dismiss = createDismissGuard();
30
+
21
31
  return (
22
32
  <KDialog.Root open={props.open} onOpenChange={props.onOpenChange}>
23
- <KDialog.Portal>
24
- <KDialog.Overlay class="zylem-dialog-overlay" />
25
- <div class="zylem-dialog-positioner">
26
- <KDialog.Content class="zylem-dialog-content">
27
- <div class="zylem-dialog-header">
28
- <Show when={props.title}>
29
- <KDialog.Title class="zylem-dialog-title">
30
- {props.title}
31
- </KDialog.Title>
32
- </Show>
33
- <WindowControls onClose={() => props.onOpenChange?.(false)} />
33
+ {/*
34
+ A dialog is controlled and renders no trigger, so it has nothing in the
35
+ page to resolve its tree from. This marker sits where the dialog was
36
+ declared, which is enough to find an enclosing shadow root.
37
+ */}
38
+ <span ref={layer.anchorRef} style={{ display: 'none' }} aria-hidden="true" />
39
+ <Show when={layer.mount()} keyed>
40
+ {(mount) => (
41
+ <KDialog.Portal mount={mount}>
42
+ <KDialog.Overlay class="zylem-dialog-overlay" />
43
+ <div class="zylem-dialog-positioner">
44
+ <KDialog.Content
45
+ ref={dismiss.ref}
46
+ class="zylem-dialog-content"
47
+ onInteractOutside={dismiss.onInteractOutside}
48
+ >
49
+ <div class="zylem-dialog-header">
50
+ <Show when={props.title}>
51
+ <KDialog.Title class="zylem-dialog-title">
52
+ {props.title}
53
+ </KDialog.Title>
54
+ </Show>
55
+ <WindowControls onClose={() => props.onOpenChange?.(false)} />
56
+ </div>
57
+ <KDialog.Description as="div" class="zylem-dialog-body">
58
+ <LayerScope>{props.children}</LayerScope>
59
+ </KDialog.Description>
60
+ <Show when={props.footer}>
61
+ <div class="zylem-dialog-footer">{props.footer}</div>
62
+ </Show>
63
+ </KDialog.Content>
34
64
  </div>
35
- <KDialog.Description as="div" class="zylem-dialog-body">
36
- {props.children}
37
- </KDialog.Description>
38
- <Show when={props.footer}>
39
- <div class="zylem-dialog-footer">{props.footer}</div>
40
- </Show>
41
- </KDialog.Content>
42
- </div>
43
- </KDialog.Portal>
65
+ </KDialog.Portal>
66
+ )}
67
+ </Show>
44
68
  </KDialog.Root>
45
69
  );
46
70
  }
@@ -7,7 +7,7 @@ const enter = keyframes({
7
7
  });
8
8
 
9
9
  globalStyle('.zylem-dropdown-content', {
10
- zIndex: 10000,
10
+ zIndex: vars.layers.menu,
11
11
  minWidth: '180px',
12
12
  padding: vars.spacing.xs,
13
13
  borderRadius: vars.radii.control,
@@ -1,6 +1,7 @@
1
1
  import { DropdownMenu as KDropdownMenu } from '@kobalte/core';
2
2
  import { For, Show } from 'solid-js';
3
3
  import type { JSX } from 'solid-js';
4
+ import { createDismissGuard, useLayer } from '../../layers';
4
5
 
5
6
  export interface DropdownMenuItemConfig {
6
7
  id: string;
@@ -24,40 +25,58 @@ export interface DropdownMenuProps {
24
25
 
25
26
  /**
26
27
  * HyperGlass dropdown menu: floating glass sheet with highlight rows.
28
+ *
29
+ * Portals into the layer container for its tree at the `menu` tier, so it opens
30
+ * over panels and dialogs and stays styled inside a shadow root.
27
31
  */
28
32
  export function DropdownMenu(props: DropdownMenuProps) {
33
+ const layer = useLayer('menu');
34
+ const dismiss = createDismissGuard();
35
+
36
+ const items = () => (
37
+ <For each={props.items}>
38
+ {(item) => (
39
+ <>
40
+ <Show when={item.separatorAbove}>
41
+ <KDropdownMenu.Separator class="zylem-dropdown-separator" />
42
+ </Show>
43
+ <KDropdownMenu.Item
44
+ class={
45
+ item.danger
46
+ ? 'zylem-dropdown-item zylem-dropdown-item--danger'
47
+ : 'zylem-dropdown-item'
48
+ }
49
+ disabled={item.disabled}
50
+ onSelect={item.onSelect}
51
+ >
52
+ <span>{item.label}</span>
53
+ <Show when={item.shortcut}>
54
+ <span class="zylem-dropdown-shortcut">{item.shortcut}</span>
55
+ </Show>
56
+ </KDropdownMenu.Item>
57
+ </>
58
+ )}
59
+ </For>
60
+ );
61
+
29
62
  return (
30
63
  <KDropdownMenu.Root placement={props.placement ?? 'bottom-start'}>
31
- <KDropdownMenu.Trigger class={props.triggerClass}>
64
+ <KDropdownMenu.Trigger ref={layer.anchorRef} class={props.triggerClass}>
32
65
  {props.trigger}
33
66
  </KDropdownMenu.Trigger>
34
- <KDropdownMenu.Portal>
35
- <KDropdownMenu.Content class="zylem-dropdown-content">
36
- <For each={props.items}>
37
- {(item) => (
38
- <>
39
- <Show when={item.separatorAbove}>
40
- <KDropdownMenu.Separator class="zylem-dropdown-separator" />
41
- </Show>
42
- <KDropdownMenu.Item
43
- class={
44
- item.danger
45
- ? 'zylem-dropdown-item zylem-dropdown-item--danger'
46
- : 'zylem-dropdown-item'
47
- }
48
- disabled={item.disabled}
49
- onSelect={item.onSelect}
50
- >
51
- <span>{item.label}</span>
52
- <Show when={item.shortcut}>
53
- <span class="zylem-dropdown-shortcut">{item.shortcut}</span>
54
- </Show>
55
- </KDropdownMenu.Item>
56
- </>
57
- )}
58
- </For>
59
- </KDropdownMenu.Content>
60
- </KDropdownMenu.Portal>
67
+ <Show when={layer.mount()} keyed>
68
+ {(mount) => (
69
+ <KDropdownMenu.Portal mount={mount}>
70
+ <KDropdownMenu.Content
71
+ ref={dismiss.ref}
72
+ class="zylem-dropdown-content"
73
+ onInteractOutside={dismiss.onInteractOutside}
74
+ >
75
+ {items()}
76
+ </KDropdownMenu.Content>
77
+ </KDropdownMenu.Portal>
78
+ )}
79
+ </Show>
61
80
  </KDropdownMenu.Root>
62
81
  );
63
82
  }
@@ -0,0 +1,101 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ /**
5
+ * Searchable, grouped picker list. Sized to sit inside a MenuButton panel, but
6
+ * width and height are capped rather than fixed so it also works as a sidebar
7
+ * or dialog section.
8
+ */
9
+
10
+ globalStyle('.zylem-picker', {
11
+ display: 'flex',
12
+ flexDirection: 'column',
13
+ gap: vars.spacing.sm,
14
+ width: '260px',
15
+ maxHeight: '340px',
16
+ padding: vars.spacing.sm,
17
+ color: vars.colors.text,
18
+ fontFamily: vars.typography.fontFamily,
19
+ });
20
+
21
+ globalStyle('.zylem-picker__search', {
22
+ flexShrink: 0,
23
+ });
24
+
25
+ globalStyle('.zylem-picker__groups', {
26
+ display: 'flex',
27
+ flexDirection: 'column',
28
+ gap: vars.spacing.md,
29
+ overflowY: 'auto',
30
+ });
31
+
32
+ globalStyle('.zylem-picker__group-title', {
33
+ margin: `0 0 ${vars.spacing.xs}`,
34
+ fontSize: '9px',
35
+ fontWeight: 600,
36
+ letterSpacing: '0.08em',
37
+ textTransform: 'uppercase',
38
+ opacity: 0.5,
39
+ });
40
+
41
+ globalStyle('.zylem-picker__item', {
42
+ display: 'flex',
43
+ gap: vars.spacing.sm,
44
+ alignItems: 'center',
45
+ width: '100%',
46
+ padding: `${vars.spacing.xs} ${vars.spacing.xs}`,
47
+ fontFamily: 'inherit',
48
+ fontSize: `calc(${vars.typography.fontSize} - 2px)`,
49
+ color: 'inherit',
50
+ textAlign: 'left',
51
+ background: 'transparent',
52
+ border: '1px solid transparent',
53
+ borderRadius: vars.radii.control,
54
+ cursor: 'pointer',
55
+ transition: `background ${vars.motion.fast} ${vars.motion.easeOut}`,
56
+ });
57
+
58
+ globalStyle('.zylem-picker__item:hover:not([disabled])', {
59
+ background: 'rgba(255, 255, 255, 0.08)',
60
+ });
61
+
62
+ globalStyle('.zylem-picker__item:focus-visible', {
63
+ outline: 'none',
64
+ boxShadow: vars.effects.focusRing,
65
+ });
66
+
67
+ globalStyle('.zylem-picker__item[data-selected]', {
68
+ background: 'rgba(120, 180, 255, 0.16)',
69
+ borderColor: 'rgba(120, 180, 255, 0.45)',
70
+ });
71
+
72
+ globalStyle('.zylem-picker__item[disabled]', {
73
+ opacity: 0.45,
74
+ cursor: 'not-allowed',
75
+ });
76
+
77
+ globalStyle('.zylem-picker__item-glyph', {
78
+ display: 'inline-flex',
79
+ flexShrink: 0,
80
+ alignItems: 'center',
81
+ justifyContent: 'center',
82
+ color: 'currentColor',
83
+ });
84
+
85
+ globalStyle('.zylem-picker__item-glyph svg', {
86
+ width: vars.sizes.iconSm,
87
+ height: vars.sizes.iconSm,
88
+ });
89
+
90
+ globalStyle('.zylem-picker__item-label', {
91
+ overflow: 'hidden',
92
+ textOverflow: 'ellipsis',
93
+ whiteSpace: 'nowrap',
94
+ });
95
+
96
+ globalStyle('.zylem-picker__empty', {
97
+ margin: 0,
98
+ padding: `${vars.spacing.sm} ${vars.spacing.xs}`,
99
+ fontSize: `calc(${vars.typography.fontSize} - 3px)`,
100
+ opacity: 0.6,
101
+ });
@@ -0,0 +1,108 @@
1
+ import { For, Show, createMemo, createSignal, type JSX } from 'solid-js';
2
+ import { SearchInput } from '../SearchInput/SearchInput';
3
+ import { filterItems, groupItems, type PickerItem } from './filter-items';
4
+
5
+ export interface ItemPickerProps<T extends PickerItem = PickerItem> {
6
+ items: T[];
7
+ onSelect: (item: T) => void;
8
+ /** Marked as the current choice. */
9
+ selectedId?: string | null | undefined;
10
+ /** Hide the search field for a list short enough not to need one. */
11
+ searchable?: boolean | undefined;
12
+ searchPlaceholder?: string | undefined;
13
+ /**
14
+ * Focus the search field on mount. On by default: the picker is opened
15
+ * deliberately, so typing straight into it is the expected behaviour.
16
+ */
17
+ autoFocus?: boolean | undefined;
18
+ /** Shown when `items` is empty, as opposed to filtered down to nothing. */
19
+ emptyMessage?: JSX.Element | undefined;
20
+ class?: string | undefined;
21
+ }
22
+
23
+ /**
24
+ * Searchable, grouped list of choices — the picker half of a MenuButton, and
25
+ * usable on its own.
26
+ *
27
+ * Icons arrive as markup strings rather than components because the common case
28
+ * is a list supplied at runtime by something that does not share an icon library
29
+ * with the UI (the editor's entity catalog comes over a bridge from the running
30
+ * game, for instance).
31
+ */
32
+ export function ItemPicker<T extends PickerItem>(props: ItemPickerProps<T>) {
33
+ const [query, setQuery] = createSignal('');
34
+
35
+ const searchable = () => props.searchable !== false;
36
+ const groups = createMemo(() => groupItems(filterItems(props.items, query())));
37
+
38
+ return (
39
+ <div class={props.class ? `zylem-picker ${props.class}` : 'zylem-picker'}>
40
+ <Show when={searchable()}>
41
+ <SearchInput
42
+ class="zylem-picker__search"
43
+ value={query()}
44
+ onInput={setQuery}
45
+ placeholder={props.searchPlaceholder ?? 'Search…'}
46
+ ref={(element) => {
47
+ if (props.autoFocus === false) return;
48
+ // Deferred: the element is not focusable until it is in the document,
49
+ // which for a portaled panel is after this ref runs.
50
+ queueMicrotask(() => element.focus());
51
+ }}
52
+ />
53
+ </Show>
54
+
55
+ <Show
56
+ when={props.items.length}
57
+ fallback={
58
+ <p class="zylem-picker__empty">
59
+ {props.emptyMessage ?? 'Nothing to choose from.'}
60
+ </p>
61
+ }
62
+ >
63
+ <Show
64
+ when={groups().length}
65
+ fallback={
66
+ <p class="zylem-picker__empty">No matches for “{query()}”.</p>
67
+ }
68
+ >
69
+ <div class="zylem-picker__groups zylem-scroll">
70
+ <For each={groups()}>
71
+ {(group) => (
72
+ <div class="zylem-picker__group">
73
+ <h4 class="zylem-picker__group-title">{group.group}</h4>
74
+ <For each={group.items}>
75
+ {(item) => (
76
+ <button
77
+ type="button"
78
+ class="zylem-picker__item"
79
+ disabled={item.disabled}
80
+ data-selected={
81
+ props.selectedId === item.id ? '' : undefined
82
+ }
83
+ title={item.description}
84
+ onClick={() => props.onSelect(item)}
85
+ >
86
+ <Show when={item.icon}>
87
+ {(icon) => (
88
+ <span
89
+ class="zylem-picker__item-glyph"
90
+ // Markup from the caller's own registry, in the
91
+ // caller's own realm; not a new trust boundary.
92
+ innerHTML={icon()}
93
+ />
94
+ )}
95
+ </Show>
96
+ <span class="zylem-picker__item-label">{item.label}</span>
97
+ </button>
98
+ )}
99
+ </For>
100
+ </div>
101
+ )}
102
+ </For>
103
+ </div>
104
+ </Show>
105
+ </Show>
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Search and grouping for {@link ItemPicker}, kept pure and separate so the
3
+ * matching rules can be tested and reused without rendering anything.
4
+ */
5
+
6
+ /**
7
+ * Optional fields accept an explicit `undefined` so callers compiled with
8
+ * `exactOptionalPropertyTypes` can map their own optional data straight across
9
+ * without narrowing every field first.
10
+ */
11
+ export interface PickerItem {
12
+ id: string;
13
+ label: string;
14
+ /** Heading this item is filed under. Ungrouped items fall under `Other`. */
15
+ group?: string | undefined;
16
+ /**
17
+ * Leading glyph. A string is treated as inline SVG or HTML markup, which is
18
+ * what a host that does not share an icon library can realistically supply.
19
+ */
20
+ icon?: string | undefined;
21
+ /** Shown as the row's title attribute. */
22
+ description?: string | undefined;
23
+ /** Extra search terms that are not visible on the row. */
24
+ tags?: string[] | undefined;
25
+ disabled?: boolean | undefined;
26
+ }
27
+
28
+ export interface PickerGroup<T extends PickerItem = PickerItem> {
29
+ group: string;
30
+ items: T[];
31
+ }
32
+
33
+ /** Heading for items that declare no group. */
34
+ export const UNGROUPED_LABEL = 'Other';
35
+
36
+ /**
37
+ * Filter by a query matched against label, id, and tags. An empty or
38
+ * whitespace-only query returns everything, so an unopened search box is not a
39
+ * filter.
40
+ */
41
+ export function filterItems<T extends PickerItem>(items: T[], query: string): T[] {
42
+ const needle = query.trim().toLowerCase();
43
+ if (!needle) return items;
44
+
45
+ return items.filter((item) => {
46
+ if (item.label.toLowerCase().includes(needle)) return true;
47
+ if (item.id.toLowerCase().includes(needle)) return true;
48
+ return item.tags?.some((tag) => tag.toLowerCase().includes(needle)) ?? false;
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Bucket items under their group.
54
+ *
55
+ * Order comes from the input rather than the alphabet, for both the groups and
56
+ * the items within them: the caller has already decided what should be near the
57
+ * top, and sorting here would throw that away.
58
+ */
59
+ export function groupItems<T extends PickerItem>(items: T[]): PickerGroup<T>[] {
60
+ const groups: PickerGroup<T>[] = [];
61
+ const byName = new Map<string, T[]>();
62
+
63
+ for (const item of items) {
64
+ const name = item.group ?? UNGROUPED_LABEL;
65
+ let bucket = byName.get(name);
66
+ if (!bucket) {
67
+ bucket = [];
68
+ byName.set(name, bucket);
69
+ groups.push({ group: name, items: bucket });
70
+ }
71
+ bucket.push(item);
72
+ }
73
+
74
+ return groups;
75
+ }
@@ -0,0 +1,91 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ /**
5
+ * Split button with an attached menu. The face reuses `.zylem-toolbar-btn` for
6
+ * its look, so a MenuButton dropped into a toolbar matches its neighbours.
7
+ */
8
+
9
+ globalStyle('.zylem-menu-button', {
10
+ position: 'relative',
11
+ display: 'inline-flex',
12
+ alignItems: 'stretch',
13
+ });
14
+
15
+ globalStyle('.zylem-menu-button__face', {
16
+ display: 'inline-flex',
17
+ alignItems: 'center',
18
+ justifyContent: 'center',
19
+ });
20
+
21
+ /*
22
+ * The chevron overlaps the face's trailing corner rather than sitting beside it,
23
+ * so gaining a second affordance does not widen the button and break a toolbar's
24
+ * rhythm.
25
+ */
26
+ globalStyle('.zylem-menu-button__chevron', {
27
+ position: 'absolute',
28
+ right: '-2px',
29
+ bottom: '-2px',
30
+ display: 'flex',
31
+ alignItems: 'center',
32
+ justifyContent: 'center',
33
+ width: '14px',
34
+ height: '14px',
35
+ padding: 0,
36
+ fontSize: '9px',
37
+ lineHeight: 1,
38
+ color: 'rgba(255, 255, 255, 0.75)',
39
+ background: 'rgba(20, 24, 32, 0.92)',
40
+ border: '1px solid rgba(255, 255, 255, 0.16)',
41
+ borderRadius: vars.radii.control,
42
+ cursor: 'pointer',
43
+ transition: `color ${vars.motion.fast} ${vars.motion.easeOut}, border-color ${vars.motion.fast} ${vars.motion.easeOut}`,
44
+ });
45
+
46
+ /*
47
+ * The non-split chevron is decoration over a face that is already the trigger.
48
+ * Ignoring the pointer keeps it from hovering or hit-testing like its own
49
+ * control, and lets clicks through to the face beneath.
50
+ */
51
+ globalStyle('.zylem-menu-button__chevron--static', {
52
+ pointerEvents: 'none',
53
+ });
54
+
55
+ globalStyle('.zylem-menu-button__chevron:hover:not([disabled])', {
56
+ color: '#fff',
57
+ borderColor: 'rgba(255, 255, 255, 0.4)',
58
+ });
59
+
60
+ globalStyle('.zylem-menu-button__chevron:focus-visible', {
61
+ outline: 'none',
62
+ boxShadow: vars.effects.focusRing,
63
+ });
64
+
65
+ globalStyle('.zylem-menu-button__chevron[disabled]', {
66
+ opacity: 0.45,
67
+ cursor: 'not-allowed',
68
+ });
69
+
70
+ globalStyle('.zylem-menu-button__panel', {
71
+ zIndex: vars.layers.menu,
72
+ color: vars.colors.text,
73
+ fontFamily: vars.typography.fontFamily,
74
+ borderRadius: vars.radii.panel,
75
+ border: '1px solid rgba(97, 166, 232, 0.45)',
76
+ background: vars.material.glassPanelDark,
77
+ boxShadow: vars.effects.shadowPanel,
78
+ backdropFilter: `blur(${vars.effects.blurMd}) saturate(1.22)`,
79
+ outline: 'none',
80
+ '@supports': {
81
+ 'not (backdrop-filter: blur(1px))': {
82
+ background: vars.colors.surface,
83
+ },
84
+ },
85
+ });
86
+
87
+ /** Icon glyphs supplied as inline SVG markup rather than components. */
88
+ globalStyle('.zylem-menu-button__face svg', {
89
+ width: '18px',
90
+ height: '18px',
91
+ });