@zylem/ui 0.1.1 → 0.2.3

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 (40) 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 +14 -5
  8. package/src/components/Button/Button.css.ts +3 -0
  9. package/src/components/Console/Console.css.ts +4 -2
  10. package/src/components/Dialog/Dialog.css.ts +3 -2
  11. package/src/components/Dialog/Dialog.tsx +44 -20
  12. package/src/components/DropdownMenu/DropdownMenu.css.ts +1 -1
  13. package/src/components/DropdownMenu/DropdownMenu.tsx +47 -28
  14. package/src/components/ItemPicker/ItemPicker.css.ts +101 -0
  15. package/src/components/ItemPicker/ItemPicker.tsx +108 -0
  16. package/src/components/ItemPicker/filter-items.ts +75 -0
  17. package/src/components/MenuButton/MenuButton.css.ts +91 -0
  18. package/src/components/MenuButton/MenuButton.tsx +153 -0
  19. package/src/components/Property/Property.tsx +8 -6
  20. package/src/components/SearchInput/SearchInput.tsx +3 -0
  21. package/src/components/Select/Select.css.ts +1 -1
  22. package/src/components/Select/Select.tsx +17 -5
  23. package/src/components/ToolbarButton/ToolbarButton.css.ts +3 -0
  24. package/src/components/ToolbarButton/ToolbarButton.tsx +17 -7
  25. package/src/components/Tooltip/Tooltip.css.ts +4 -1
  26. package/src/components/Tooltip/Tooltip.tsx +26 -8
  27. package/src/components/WindowControls/WindowControls.tsx +7 -5
  28. package/src/components/index.ts +27 -0
  29. package/src/global/detached-panel.css.ts +4 -1
  30. package/src/global/editor-base.css.ts +32 -10
  31. package/src/global/hyperglass-base.css.ts +80 -5
  32. package/src/layers/LayerContext.tsx +132 -0
  33. package/src/layers/dismiss-guard.ts +81 -0
  34. package/src/layers/index.ts +27 -0
  35. package/src/layers/layer-tokens.css.ts +38 -0
  36. package/src/layers/resolve-mount.ts +48 -0
  37. package/src/layers/tiers.ts +53 -0
  38. package/src/styles.ts +3 -0
  39. package/src/theme.css.ts +7 -0
  40. package/dist/chunk-VGOOWBSO.js +0 -1
@@ -0,0 +1,153 @@
1
+ import { Popover as KPopover } from '@kobalte/core';
2
+ import { Show, createSignal, type JSX } from 'solid-js';
3
+ import { createDismissGuard, useLayer } from '../../layers';
4
+ import { Tooltip } from '../Tooltip/Tooltip';
5
+
6
+ export interface MenuButtonProps {
7
+ /** Accessible name, and the tooltip text unless `tooltip` overrides it. */
8
+ label: string;
9
+ /** Button face content, usually an icon. */
10
+ children: JSX.Element;
11
+ /** Panel content: a menu, a picker, anything. Rendered only while open. */
12
+ menu: (close: () => void) => JSX.Element;
13
+ /**
14
+ * Primary action for the face. Given one, the button splits: the face fires
15
+ * this and the chevron opens the menu. Without one, the whole button opens
16
+ * the menu and the chevron is only there to say so.
17
+ *
18
+ * Optional props here accept an explicit `undefined` so a caller compiled with
19
+ * `exactOptionalPropertyTypes` can decide the split at the call site.
20
+ */
21
+ onAction?: (() => void) | undefined;
22
+ /** Renders the face in its selected state. */
23
+ selected?: boolean | undefined;
24
+ disabled?: boolean | undefined;
25
+ /** Tooltip text; defaults to `label`. Pass `null` for no tooltip. */
26
+ tooltip?: string | null | undefined;
27
+ placement?:
28
+ | 'bottom-start'
29
+ | 'bottom-end'
30
+ | 'bottom'
31
+ | 'top-start'
32
+ | 'top-end'
33
+ | 'top'
34
+ | undefined;
35
+ /** Controlled open state. Omit to let the button own it. */
36
+ open?: boolean | undefined;
37
+ onOpenChange?: ((open: boolean) => void) | undefined;
38
+ class?: string | undefined;
39
+ }
40
+
41
+ /**
42
+ * Shown in both modes, because a chevron means "this has a menu" and that is
43
+ * true whether or not the face carries its own action.
44
+ */
45
+ const CHEVRON = '▾';
46
+
47
+ /**
48
+ * A toolbar button that owns a menu, optionally split.
49
+ *
50
+ * Split mode exists because choosing and doing are separate jobs: the face
51
+ * repeats the last choice and the chevron is there for picking a different one.
52
+ * Placing ten of the same thing is then one trip through the menu and ten
53
+ * clicks, rather than ten trips.
54
+ *
55
+ * The panel is layered at the `menu` tier and portaled out of the button's own
56
+ * subtree, so a toolbar that clips its overflow or sits in a lower stacking
57
+ * context cannot hide it.
58
+ */
59
+ export function MenuButton(props: MenuButtonProps) {
60
+ const [uncontrolledOpen, setUncontrolledOpen] = createSignal(false);
61
+ const layer = useLayer('menu');
62
+ const dismiss = createDismissGuard();
63
+
64
+ const isOpen = () => props.open ?? uncontrolledOpen();
65
+ const setOpen = (open: boolean) => {
66
+ setUncontrolledOpen(open);
67
+ props.onOpenChange?.(open);
68
+ };
69
+
70
+ const isSplit = () => props.onAction !== undefined;
71
+ const tooltipText = () =>
72
+ props.tooltip === null ? null : props.tooltip ?? props.label;
73
+
74
+ /** Wrap in a tooltip only when there is text for one. */
75
+ const described = (content: JSX.Element) => (
76
+ <Show when={tooltipText()} fallback={content} keyed>
77
+ {(text) => <Tooltip content={text}>{content}</Tooltip>}
78
+ </Show>
79
+ );
80
+
81
+ return (
82
+ <div
83
+ class={props.class ? `zylem-menu-button ${props.class}` : 'zylem-menu-button'}
84
+ data-split={isSplit() ? '' : undefined}
85
+ >
86
+ <KPopover.Root
87
+ open={isOpen()}
88
+ onOpenChange={setOpen}
89
+ placement={props.placement ?? 'bottom-start'}
90
+ >
91
+ <Show
92
+ when={isSplit()}
93
+ fallback={described(
94
+ <KPopover.Trigger
95
+ ref={layer.anchorRef}
96
+ class="zylem-toolbar-btn zylem-menu-button__face"
97
+ aria-label={props.label}
98
+ disabled={props.disabled}
99
+ data-selected={props.selected ? '' : undefined}
100
+ >
101
+ {props.children}
102
+ {/*
103
+ * Signage only: the button around it already opens the menu, so
104
+ * this must not read as a second control.
105
+ */}
106
+ <span
107
+ class="zylem-menu-button__chevron zylem-menu-button__chevron--static"
108
+ aria-hidden="true"
109
+ >
110
+ {CHEVRON}
111
+ </span>
112
+ </KPopover.Trigger>,
113
+ )}
114
+ >
115
+ {described(
116
+ <button
117
+ type="button"
118
+ ref={layer.anchorRef}
119
+ class="zylem-toolbar-btn zylem-menu-button__face"
120
+ aria-label={props.label}
121
+ disabled={props.disabled}
122
+ data-selected={props.selected ? '' : undefined}
123
+ onClick={() => props.onAction?.()}
124
+ >
125
+ {props.children}
126
+ </button>,
127
+ )}
128
+ <KPopover.Trigger
129
+ class="zylem-menu-button__chevron"
130
+ aria-label={`${props.label} options`}
131
+ disabled={props.disabled}
132
+ >
133
+ <span aria-hidden="true">{CHEVRON}</span>
134
+ </KPopover.Trigger>
135
+ </Show>
136
+
137
+ <Show when={layer.mount()} keyed>
138
+ {(mount) => (
139
+ <KPopover.Portal mount={mount}>
140
+ <KPopover.Content
141
+ ref={dismiss.ref}
142
+ class="zylem-menu-button__panel"
143
+ onInteractOutside={dismiss.onInteractOutside}
144
+ >
145
+ {props.menu(() => setOpen(false))}
146
+ </KPopover.Content>
147
+ </KPopover.Portal>
148
+ )}
149
+ </Show>
150
+ </KPopover.Root>
151
+ </div>
152
+ );
153
+ }
@@ -25,17 +25,19 @@ export function PropertyList(props: PropertyListProps) {
25
25
 
26
26
  export interface PropertyRowProps {
27
27
  label: string;
28
+ // Optional props accept an explicit `undefined` so callers compiled with
29
+ // `exactOptionalPropertyTypes` can forward their own optional props directly.
28
30
  /** The value to display. Can be a string, number, or JSX element. */
29
- value?: string | number | JSX.Element;
31
+ value?: string | number | JSX.Element | undefined;
30
32
  /** If true, treat string values as paths and truncate long ones. */
31
- isPath?: boolean;
33
+ isPath?: boolean | undefined;
32
34
  /** Maximum path segments to show (default: 3). */
33
- maxPathSegments?: number;
35
+ maxPathSegments?: number | undefined;
34
36
  /** Called with the full value when a truncated path is clicked. */
35
- onPathClick?: (fullValue: string) => void;
36
- valueClass?: string;
37
+ onPathClick?: ((fullValue: string) => void) | undefined;
38
+ valueClass?: string | undefined;
37
39
  /** Children can be used instead of value for complex content. */
38
- children?: JSX.Element;
40
+ children?: JSX.Element | undefined;
39
41
  }
40
42
 
41
43
  function truncatePath(
@@ -8,6 +8,8 @@ export interface SearchInputProps {
8
8
  class?: string;
9
9
  'aria-label'?: string;
10
10
  onKeyDown?: JSX.EventHandlerUnion<HTMLInputElement, KeyboardEvent>;
11
+ /** Handle on the input itself, for callers that need to focus it. */
12
+ ref?: (element: HTMLInputElement) => void;
11
13
  }
12
14
 
13
15
  /**
@@ -19,6 +21,7 @@ export function SearchInput(props: SearchInputProps) {
19
21
  <Search class="zylem-search-icon" />
20
22
  <input
21
23
  type="search"
24
+ ref={props.ref}
22
25
  class="zylem-field zylem-search-input"
23
26
  value={props.value ?? ''}
24
27
  placeholder={props.placeholder ?? 'Search...'}
@@ -31,7 +31,7 @@ globalStyle('.zylem-select-trigger[data-expanded] .zylem-select-icon', {
31
31
  });
32
32
 
33
33
  globalStyle('.zylem-select-content', {
34
- zIndex: 10000,
34
+ zIndex: vars.layers.menu,
35
35
  borderRadius: vars.radii.control,
36
36
  border: '1px solid rgba(97, 166, 232, 0.45)',
37
37
  background: vars.material.glassPanelDark,
@@ -1,5 +1,6 @@
1
1
  import { Select as KSelect } from '@kobalte/core';
2
2
  import { Show } from 'solid-js';
3
+ import { createDismissGuard, useLayer } from '../../layers';
3
4
 
4
5
  export interface SelectOption {
5
6
  value: string;
@@ -22,6 +23,8 @@ export interface SelectProps {
22
23
  * HyperGlass select: inset glass trigger opening a floating glass sheet.
23
24
  */
24
25
  export function Select(props: SelectProps) {
26
+ const layer = useLayer('menu');
27
+ const dismiss = createDismissGuard();
25
28
  const selected = () =>
26
29
  props.options.find((option) => option.value === props.value) ?? null;
27
30
 
@@ -50,6 +53,7 @@ export function Select(props: SelectProps) {
50
53
  <KSelect.Label class="zylem-field-label">{props.label}</KSelect.Label>
51
54
  </Show>
52
55
  <KSelect.Trigger
56
+ ref={layer.anchorRef}
53
57
  class="zylem-field zylem-select-trigger"
54
58
  aria-label={props.label}
55
59
  >
@@ -58,11 +62,19 @@ export function Select(props: SelectProps) {
58
62
  </KSelect.Value>
59
63
  <KSelect.Icon class="zylem-select-icon">▼</KSelect.Icon>
60
64
  </KSelect.Trigger>
61
- <KSelect.Portal>
62
- <KSelect.Content class="zylem-select-content">
63
- <KSelect.Listbox class="zylem-select-listbox zylem-scroll" />
64
- </KSelect.Content>
65
- </KSelect.Portal>
65
+ <Show when={layer.mount()} keyed>
66
+ {(mount) => (
67
+ <KSelect.Portal mount={mount}>
68
+ <KSelect.Content
69
+ ref={dismiss.ref}
70
+ class="zylem-select-content"
71
+ onInteractOutside={dismiss.onInteractOutside}
72
+ >
73
+ <KSelect.Listbox class="zylem-select-listbox zylem-scroll" />
74
+ </KSelect.Content>
75
+ </KSelect.Portal>
76
+ )}
77
+ </Show>
66
78
  </KSelect.Root>
67
79
  );
68
80
  }
@@ -38,6 +38,9 @@ globalStyle('.zylem-toolbar-btn:hover', {
38
38
  globalStyle('.zylem-toolbar-btn:active', {
39
39
  transform: 'translateY(1px)',
40
40
  filter: 'brightness(0.92)',
41
+ // CodePen-inspired press: compressed drop shadow + bright bottom rim flash.
42
+ boxShadow:
43
+ '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)',
41
44
  });
42
45
 
43
46
  globalStyle('.zylem-toolbar-btn:focus-visible', {
@@ -1,5 +1,6 @@
1
1
  import { Button as KButton, Tooltip as KTooltip } from '@kobalte/core';
2
- import type { JSX } from 'solid-js';
2
+ import { Show, type JSX } from 'solid-js';
3
+ import { useLayer } from '../../layers';
3
4
 
4
5
  export interface ToolbarButtonProps {
5
6
  label: string;
@@ -13,11 +14,16 @@ export interface ToolbarButtonProps {
13
14
  /**
14
15
  * HyperGlass toolbar button: square glass icon button with a tooltip.
15
16
  * Selected state renders as a live-green jewel.
17
+ *
18
+ * The tooltip portals into the layer container for its own tree, so it stays
19
+ * styled when the toolbar lives inside a web component's shadow root.
16
20
  */
17
21
  export function ToolbarButton(props: ToolbarButtonProps) {
22
+ const layer = useLayer('tooltip');
23
+
18
24
  return (
19
25
  <KTooltip.Root openDelay={400}>
20
- <KTooltip.Trigger as="span">
26
+ <KTooltip.Trigger as="span" ref={layer.anchorRef}>
21
27
  <KButton.Root
22
28
  aria-label={props.label}
23
29
  onClick={props.onClick}
@@ -32,11 +38,15 @@ export function ToolbarButton(props: ToolbarButtonProps) {
32
38
  {props.children}
33
39
  </KButton.Root>
34
40
  </KTooltip.Trigger>
35
- <KTooltip.Portal>
36
- <KTooltip.Content class="zylem-tooltip">
37
- {props.label}
38
- </KTooltip.Content>
39
- </KTooltip.Portal>
41
+ <Show when={layer.mount()} keyed>
42
+ {(mount) => (
43
+ <KTooltip.Portal mount={mount}>
44
+ <KTooltip.Content class="zylem-tooltip">
45
+ {props.label}
46
+ </KTooltip.Content>
47
+ </KTooltip.Portal>
48
+ )}
49
+ </Show>
40
50
  </KTooltip.Root>
41
51
  );
42
52
  }
@@ -7,7 +7,8 @@ const enter = keyframes({
7
7
  });
8
8
 
9
9
  globalStyle('.zylem-tooltip', {
10
- zIndex: 10000,
10
+ position: 'relative',
11
+ zIndex: vars.layers.tooltip,
11
12
  maxWidth: '260px',
12
13
  padding: `${vars.spacing.xs} ${vars.spacing.sm}`,
13
14
  borderRadius: vars.radii.control,
@@ -19,6 +20,8 @@ globalStyle('.zylem-tooltip', {
19
20
  color: vars.colors.text,
20
21
  fontFamily: vars.typography.fontFamily,
21
22
  fontSize: `calc(${vars.typography.fontSize} - 1px)`,
23
+ fontWeight: 600,
24
+ letterSpacing: '0.02em',
22
25
  pointerEvents: 'none',
23
26
  '@supports': {
24
27
  'not (backdrop-filter: blur(1px))': {
@@ -1,5 +1,6 @@
1
1
  import { Tooltip as KTooltip } from '@kobalte/core';
2
- import type { JSX } from 'solid-js';
2
+ import { Show, type JSX } from 'solid-js';
3
+ import { useLayer } from '../../layers';
3
4
 
4
5
  export interface TooltipProps {
5
6
  /** Tooltip body. */
@@ -11,17 +12,34 @@ export interface TooltipProps {
11
12
  }
12
13
 
13
14
  /**
14
- * HyperGlass tooltip: small floating glass sheet.
15
+ * HyperGlass tooltip: small floating glass sheet, portaled above page chrome
16
+ * so it is not trapped by local stacking contexts (grids, panels, etc.).
17
+ *
18
+ * The portal target is resolved from the trigger rather than assumed to be
19
+ * `document.body`, so a tooltip inside a web component's shadow root still
20
+ * picks up the styles injected there.
15
21
  */
16
22
  export function Tooltip(props: TooltipProps) {
23
+ const layer = useLayer('tooltip');
24
+
17
25
  return (
18
26
  <KTooltip.Root placement={props.placement} openDelay={props.openDelay ?? 400}>
19
- <KTooltip.Trigger as="span">{props.children}</KTooltip.Trigger>
20
- <KTooltip.Portal>
21
- <KTooltip.Content class="zylem-tooltip">
22
- {props.content}
23
- </KTooltip.Content>
24
- </KTooltip.Portal>
27
+ <KTooltip.Trigger
28
+ as="span"
29
+ ref={layer.anchorRef}
30
+ style={{ display: 'flex', width: '100%', height: '100%', 'min-width': 0 }}
31
+ >
32
+ {props.children}
33
+ </KTooltip.Trigger>
34
+ <Show when={layer.mount()}>
35
+ {(mount) => (
36
+ <KTooltip.Portal mount={mount()}>
37
+ <KTooltip.Content class="zylem-tooltip">
38
+ {props.content}
39
+ </KTooltip.Content>
40
+ </KTooltip.Portal>
41
+ )}
42
+ </Show>
25
43
  </KTooltip.Root>
26
44
  );
27
45
  }
@@ -4,14 +4,16 @@ import PanelBottomOpen from 'lucide-solid/icons/panel-bottom-open';
4
4
  import X from 'lucide-solid/icons/x';
5
5
 
6
6
  export interface WindowControlsProps {
7
- onClose?: () => void;
8
- onCollapse?: () => void;
7
+ // Optional props accept an explicit `undefined` so callers compiled with
8
+ // `exactOptionalPropertyTypes` can forward their own optional props directly.
9
+ onClose?: (() => void) | undefined;
10
+ onCollapse?: (() => void) | undefined;
9
11
  /** Show the "expand" collapse icon when the panel is collapsed. */
10
- collapsed?: boolean;
12
+ collapsed?: boolean | undefined;
11
13
  /** Accessible label/title for the close button (default "Close panel"). */
12
- closeLabel?: string;
14
+ closeLabel?: string | undefined;
13
15
  /** Forwarded as data-testid on the close button. */
14
- closeTestId?: string;
16
+ closeTestId?: string | undefined;
15
17
  }
16
18
 
17
19
  /**
@@ -5,6 +5,24 @@
5
5
  * vanilla-extract recipes/styles driven by the Zylem token system.
6
6
  */
7
7
 
8
+ // Layering. Overlays here use it by default; consumers need it only to scope a
9
+ // subtree (a modal) or to layer their own chrome on the same ladder.
10
+ export {
11
+ LayerProvider,
12
+ LayerScope,
13
+ useLayer,
14
+ resolveLayerRoot,
15
+ ensureLayerContainer,
16
+ createDismissGuard,
17
+ LAYER_PORTAL_CLASS,
18
+ type DismissalEvent,
19
+ type DismissGuard,
20
+ type LayerProviderProps,
21
+ type LayerScopeProps,
22
+ type LayerTier,
23
+ type ResolvedLayer,
24
+ } from '../layers';
25
+
8
26
  // Primitives
9
27
  export { Button, type ButtonProps, type ButtonVariant, type ButtonSize } from './Button/Button';
10
28
  export { ToolbarButton, type ToolbarButtonProps } from './ToolbarButton/ToolbarButton';
@@ -64,6 +82,15 @@ export {
64
82
  type SidebarItemProps,
65
83
  } from './Sidebar/Sidebar';
66
84
  export { Card, type CardProps } from './Card/Card';
85
+ export { MenuButton, type MenuButtonProps } from './MenuButton/MenuButton';
86
+ export { ItemPicker, type ItemPickerProps } from './ItemPicker/ItemPicker';
87
+ export {
88
+ filterItems,
89
+ groupItems,
90
+ UNGROUPED_LABEL,
91
+ type PickerItem,
92
+ type PickerGroup,
93
+ } from './ItemPicker/filter-items';
67
94
  export { SearchInput, type SearchInputProps } from './SearchInput/SearchInput';
68
95
  export { EmptyState, type EmptyStateProps } from './EmptyState/EmptyState';
69
96
  export { ScrollArea, type ScrollAreaProps } from './ScrollArea/ScrollArea';
@@ -74,7 +74,10 @@ globalStyle('.accordion-drag-ghost', {
74
74
  backdropFilter: `blur(${vars.effects.blurSm})`,
75
75
  boxShadow: `0 8px 24px rgba(0, 0, 0, 0.5), ${vars.effects.glowDanger}`,
76
76
  position: 'fixed',
77
- zIndex: 9999,
77
+ // Top of the panel tier: the ghost follows the cursor over every panel,
78
+ // including the one it was torn from, but is still page furniture rather than
79
+ // an overlay, so it stays under modals and menus.
80
+ zIndex: `calc(${vars.layers.panel} + 900)`,
78
81
  pointerEvents: 'none',
79
82
  });
80
83
 
@@ -155,42 +155,64 @@ globalStyle('.zylem-section-title', {
155
155
  textTransform: 'uppercase',
156
156
  });
157
157
 
158
- /** Entity grid: index-card jewel tiles. */
158
+ /** Entity grid: glossy glass icon tiles. Type names use portaled `.zylem-tooltip`. */
159
159
  globalStyle('.entity-grid', {
160
160
  display: 'grid',
161
161
  gridTemplateColumns: 'repeat(auto-fill, minmax(40px, 1fr))',
162
162
  gap: vars.spacing.xs,
163
163
  });
164
164
 
165
- globalStyle('.entity-grid-item', {
165
+ /** Tooltip/trigger wrappers must fill the cell so the icon button keeps a square. */
166
+ globalStyle('.entity-grid > *', {
167
+ display: 'flex',
168
+ minWidth: 0,
166
169
  aspectRatio: '1',
170
+ });
171
+
172
+ globalStyle('.entity-grid-item', {
173
+ position: 'relative',
174
+ flex: 1,
175
+ width: '100%',
176
+ height: '100%',
167
177
  display: 'flex',
168
178
  alignItems: 'center',
169
179
  justifyContent: 'center',
170
- background: vars.material.glassPanelDark,
171
- border: '1px solid rgba(97, 166, 232, 0.32)',
180
+ background: vars.material.buttonGlass,
181
+ border: '1px solid rgba(150, 210, 255, 0.5)',
172
182
  borderRadius: vars.radii.control,
173
- boxShadow:
174
- 'inset 0 1px 0 rgba(255,255,255,0.1), 0 2px 6px rgba(0,0,0,0.3)',
183
+ boxShadow: vars.effects.shadowButton,
175
184
  cursor: 'pointer',
176
- transition: `background ${vars.motion.fast} ${vars.motion.easeOut}, border-color ${vars.motion.fast} ${vars.motion.easeOut}, transform ${vars.motion.fast} ${vars.motion.easeOut}, box-shadow ${vars.motion.fast} ${vars.motion.easeOut}`,
185
+ color: vars.colors.textSecondary,
186
+ transition: `background ${vars.motion.fast} ${vars.motion.easeOut}, border-color ${vars.motion.fast} ${vars.motion.easeOut}, transform ${vars.motion.fast} ${vars.motion.easeOut}, box-shadow ${vars.motion.fast} ${vars.motion.easeOut}, filter ${vars.motion.fast} ${vars.motion.easeOut}`,
177
187
  });
178
188
 
179
189
  globalStyle('.entity-grid-item:hover', {
180
190
  background: vars.material.buttonGlassHover,
181
- borderColor: 'rgba(150, 210, 255, 0.6)',
182
- boxShadow: `inset 0 1px 0 rgba(255,255,255,0.3), ${vars.effects.glowPrimary}`,
191
+ borderColor: 'rgba(150, 210, 255, 0.75)',
192
+ boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowPrimary}`,
183
193
  transform: 'translateY(-1px)',
194
+ color: '#F4FAFF',
184
195
  });
185
196
 
186
197
  globalStyle('.entity-grid-item:active', {
187
198
  transform: 'translateY(1px)',
199
+ filter: 'brightness(0.92)',
200
+ boxShadow:
201
+ '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)',
202
+ });
203
+
204
+ globalStyle('.entity-grid-item:focus-visible', {
205
+ outline: 'none',
206
+ boxShadow: `${vars.effects.shadowButton}, ${vars.effects.focusRing}`,
188
207
  });
189
208
 
190
209
  globalStyle('.entity-icon', {
191
210
  width: '20px',
192
211
  height: '20px',
193
- color: vars.colors.textSecondary,
212
+ color: 'currentColor',
213
+ position: 'relative',
214
+ zIndex: 1,
215
+ filter: 'drop-shadow(0 1px 1px rgba(0,0,0,0.55))',
194
216
  });
195
217
 
196
218
  globalStyle('.entity-grid-item:hover .entity-icon', {
@@ -3,12 +3,14 @@ import { vars } from '../theme.css';
3
3
 
4
4
  /**
5
5
  * HyperGlass foundation styles: app root background treatment, visible
6
- * Aqua-inspired glass scrollbars, and shared focus rings. Component-level
7
- * glass treatments live in each component's recipe; this file only covers
8
- * page-level chrome.
6
+ * Aqua-inspired glass scrollbars, shared focus rings, and a light CodePen-
7
+ * inspired specular sheen on glossy controls. Component-level glass
8
+ * treatments live in each component's recipe; this file covers page-level
9
+ * chrome plus shared lighting primitives.
9
10
  */
10
11
 
11
- // Root surface: dark navy with a soft radial glow and a faint blueprint grid.
12
+ // Root surface: dark navy with a soft radial glow, angled light wash, and a
13
+ // faint blueprint grid.
12
14
  globalStyle('.zylem-hyperglass-root', {
13
15
  minHeight: '100vh',
14
16
  color: vars.colors.text,
@@ -16,10 +18,11 @@ globalStyle('.zylem-hyperglass-root', {
16
18
  backgroundColor: vars.colors.background,
17
19
  backgroundImage: `
18
20
  radial-gradient(circle at 30% 0%, rgba(97, 166, 232, 0.18), transparent 32%),
21
+ linear-gradient(-75deg, rgba(255, 255, 255, 0) 0%, rgba(97, 166, 232, 0.07) 42%, rgba(255, 255, 255, 0) 68%),
19
22
  linear-gradient(rgba(97, 166, 232, 0.045) 1px, transparent 1px),
20
23
  linear-gradient(90deg, rgba(97, 166, 232, 0.045) 1px, transparent 1px)
21
24
  `,
22
- backgroundSize: '100% 100%, 24px 24px, 24px 24px',
25
+ backgroundSize: '100% 100%, 100% 100%, 24px 24px, 24px 24px',
23
26
  });
24
27
 
25
28
  // Visible focus ring on any keyboard-focused interactive element.
@@ -31,6 +34,78 @@ globalStyle(
31
34
  },
32
35
  );
33
36
 
37
+ /**
38
+ * Specular sheen on glossy interactive hosts — a soft diagonal band that
39
+ * nudges on hover and shifts toward the top on press (CodePen glass lighting,
40
+ * dialed down for HyperGlass). Uses ::before so label/icon content paints above.
41
+ * Applied on the control classes themselves (not under `.zylem-hyperglass-root`)
42
+ * so portaled surfaces like Dialog keep the same look.
43
+ * Pseudo selectors are listed per-host (comma lists don't inherit a trailing ::before).
44
+ */
45
+ // Absolute-positioned hosts (e.g. `.zylem-console-clear`) already form a
46
+ // containing block for the sheen — don't override them to `relative` or they
47
+ // stretch in flex parents. `.entity-grid-item` already sets `relative` in
48
+ // editor-base.
49
+ globalStyle('.zylem-button:not(.zylem-console-clear), .zylem-toolbar-btn', {
50
+ position: 'relative',
51
+ });
52
+
53
+ globalStyle(
54
+ '.zylem-button::before, .zylem-toolbar-btn::before, .entity-grid-item::before',
55
+ {
56
+ content: '""',
57
+ position: 'absolute',
58
+ inset: 0,
59
+ borderRadius: 'inherit',
60
+ pointerEvents: 'none',
61
+ background: `linear-gradient(
62
+ -45deg,
63
+ rgba(255, 255, 255, 0) 0%,
64
+ rgba(255, 255, 255, 0.42) 40%,
65
+ rgba(255, 255, 255, 0.42) 50%,
66
+ rgba(255, 255, 255, 0) 55%
67
+ )`,
68
+ backgroundSize: '200% 200%',
69
+ backgroundPosition: '0% 50%',
70
+ backgroundRepeat: 'no-repeat',
71
+ mixBlendMode: 'screen',
72
+ opacity: 0.4,
73
+ transition: `background-position ${vars.motion.normal} ${vars.motion.easeOut}, opacity ${vars.motion.fast} ${vars.motion.easeOut}`,
74
+ '@media': {
75
+ '(prefers-reduced-motion: reduce)': {
76
+ transition: 'none',
77
+ backgroundPosition: '0% 50%',
78
+ },
79
+ },
80
+ },
81
+ );
82
+
83
+ globalStyle(
84
+ '.zylem-button:hover:not([disabled])::before, .zylem-toolbar-btn:hover:not([disabled])::before, .entity-grid-item:hover::before',
85
+ {
86
+ backgroundPosition: '25% 50%',
87
+ opacity: 0.5,
88
+ '@media': {
89
+ '(prefers-reduced-motion: reduce)': {
90
+ backgroundPosition: '0% 50%',
91
+ },
92
+ },
93
+ },
94
+ );
95
+
96
+ globalStyle(
97
+ '.zylem-button:active:not([disabled])::before, .zylem-toolbar-btn:active:not([disabled])::before, .entity-grid-item:active::before',
98
+ {
99
+ backgroundPosition: '50% 15%',
100
+ opacity: 0.58,
101
+ '@media': {
102
+ '(prefers-reduced-motion: reduce)': {
103
+ backgroundPosition: '0% 50%',
104
+ },
105
+ },
106
+ },
107
+ );
108
+
34
109
  /**
35
110
  * Glass scrollbars — apply `.zylem-scroll` (or rely on the root cascade for
36
111
  * `.zylem-hyperglass-root`) to get the visible Aqua-style rail.