@zylem/ui 0.9.3 → 0.9.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zylem/ui",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Design tokens, global styles, and Solid components for Zylem, built with vanilla-extract",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,6 +53,15 @@
53
53
  "src",
54
54
  "vite"
55
55
  ],
56
+ "scripts": {
57
+ "build": "tsup",
58
+ "prepublishOnly": "NODE_ENV=production tsup",
59
+ "dev": "tsup --watch",
60
+ "showcase": "vite --host",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest",
63
+ "lint": "biome check ."
64
+ },
56
65
  "peerDependencies": {
57
66
  "solid-js": "^1.9.9"
58
67
  },
@@ -86,12 +95,5 @@
86
95
  "node": ">=22.12.0",
87
96
  "pnpm": ">=10.32.1"
88
97
  },
89
- "scripts": {
90
- "build": "tsup",
91
- "dev": "tsup --watch",
92
- "showcase": "vite --host",
93
- "test": "vitest run",
94
- "test:watch": "vitest",
95
- "lint": "biome check ."
96
- }
97
- }
98
+ "packageManager": "pnpm@10.33.0"
99
+ }
@@ -3,6 +3,11 @@ export interface BuildLabelModel {
3
3
  version?: string;
4
4
  /** Resolved `@zylem/*` package versions collected at build time. */
5
5
  packages?: Record<string, string>;
6
+ /**
7
+ * Package name to show when it is present in `packages`. Demo apps leave
8
+ * this unset and keep the engine-first default.
9
+ */
10
+ prefer?: string;
6
11
  }
7
12
 
8
13
  const PREFERRED_PACKAGES = [
@@ -21,6 +26,10 @@ const PREFERRED_PACKAGES = [
21
26
  */
22
27
  export function resolveBuildVersion(model: BuildLabelModel): string {
23
28
  const packages = model.packages;
29
+ if (packages && model.prefer) {
30
+ const preferred = packages[model.prefer];
31
+ if (preferred) return preferred;
32
+ }
24
33
  if (packages) {
25
34
  for (const name of PREFERRED_PACKAGES) {
26
35
  const version = packages[name];
@@ -8,7 +8,7 @@ const enter = keyframes({
8
8
 
9
9
  globalStyle('.zylem-dropdown-content', {
10
10
  zIndex: vars.layers.menu,
11
- minWidth: '180px',
11
+ width: 'max-content',
12
12
  padding: vars.spacing.xs,
13
13
  borderRadius: vars.radii.control,
14
14
  border: '1px solid rgba(97, 166, 232, 0.45)',
@@ -17,6 +17,11 @@ export interface DropdownMenuItemConfig {
17
17
  * one-of-many the way a stage picker works).
18
18
  */
19
19
  checked?: boolean;
20
+ /**
21
+ * Close the menu when this item is chosen. Checkable items stay open unless
22
+ * this is set, so a one-of-many picker can dismiss itself.
23
+ */
24
+ closeOnSelect?: boolean;
20
25
  /** Render a separator line above this item. */
21
26
  separatorAbove?: boolean;
22
27
  }
@@ -28,6 +33,8 @@ export interface DropdownMenuProps {
28
33
  placement?: 'bottom-start' | 'bottom-end' | 'bottom' | 'top-start' | 'top-end' | 'top' | 'right-start' | 'left-start';
29
34
  /** Class applied to the trigger button. */
30
35
  triggerClass?: string;
36
+ /** Accessible name when the trigger content is only an icon. */
37
+ triggerLabel?: string;
31
38
  }
32
39
 
33
40
  /**
@@ -85,6 +92,7 @@ export function MenuItems(props: { items: DropdownMenuItemConfig[] }) {
85
92
  class={itemClass(item)}
86
93
  checked={item.checked}
87
94
  disabled={item.disabled}
95
+ closeOnSelect={item.closeOnSelect}
88
96
  onSelect={item.onSelect}
89
97
  >
90
98
  {body(item)}
@@ -108,7 +116,12 @@ export function DropdownMenu(props: DropdownMenuProps) {
108
116
 
109
117
  return (
110
118
  <KDropdownMenu.Root placement={props.placement ?? 'bottom-start'}>
111
- <KDropdownMenu.Trigger ref={layer.anchorRef} class={props.triggerClass}>
119
+ <KDropdownMenu.Trigger
120
+ ref={layer.anchorRef}
121
+ class={props.triggerClass}
122
+ aria-label={props.triggerLabel}
123
+ title={props.triggerLabel}
124
+ >
112
125
  {props.trigger}
113
126
  </KDropdownMenu.Trigger>
114
127
  <Show when={layer.mount()} keyed>
@@ -92,3 +92,15 @@ globalStyle('.zylem-menu-button__face svg', {
92
92
  width: '18px',
93
93
  height: '18px',
94
94
  });
95
+
96
+ /**
97
+ * Vertical column of icon buttons inside the panel. The panel chrome stays on
98
+ * `.zylem-menu-button__panel`; this only stacks and pads its contents.
99
+ */
100
+ globalStyle('.zylem-menu-button__stack', {
101
+ display: 'flex',
102
+ flexDirection: 'column',
103
+ alignItems: 'center',
104
+ gap: vars.spacing.xs,
105
+ padding: vars.spacing.sm,
106
+ });
@@ -33,6 +33,8 @@ export function TitleBar(props: TitleBarProps) {
33
33
  'collapsed',
34
34
  'closeLabel',
35
35
  'closeTestId',
36
+ 'dock',
37
+ 'onDock',
36
38
  'class',
37
39
  ]);
38
40
 
@@ -60,6 +62,8 @@ export function TitleBar(props: TitleBarProps) {
60
62
  collapsed={local.collapsed}
61
63
  closeLabel={local.closeLabel}
62
64
  closeTestId={local.closeTestId}
65
+ dock={local.dock}
66
+ onDock={local.onDock}
63
67
  />
64
68
  </div>
65
69
  );
@@ -1,5 +1,5 @@
1
1
  import { Button as KButton, Tooltip as KTooltip } from '@kobalte/core';
2
- import { Show, type JSX } from 'solid-js';
2
+ import { Show, createSignal, onMount, type JSX } from 'solid-js';
3
3
  import { useLayer } from '../../layers';
4
4
 
5
5
  export interface ToolbarButtonProps {
@@ -17,6 +17,15 @@ export interface ToolbarButtonProps {
17
17
  class?: string;
18
18
  }
19
19
 
20
+ /**
21
+ * A floating popup (menu, popover) wraps its panel in a popper positioner.
22
+ * Tooltips inside one open beside the button, because a vertical stack has no
23
+ * free room above or below an item.
24
+ */
25
+ function insidePopup(element: Element | undefined): boolean {
26
+ return Boolean(element?.closest('[data-popper-positioner]'));
27
+ }
28
+
20
29
  /**
21
30
  * HyperGlass toolbar button: square glass icon button with a tooltip.
22
31
  * Selected and pressed states render as a live-green jewel.
@@ -26,10 +35,27 @@ export interface ToolbarButtonProps {
26
35
  */
27
36
  export function ToolbarButton(props: ToolbarButtonProps) {
28
37
  const layer = useLayer('tooltip');
38
+ const [beside, setBeside] = createSignal(false);
39
+ let trigger: HTMLElement | undefined;
40
+
41
+ const syncPlacement = () => setBeside(insidePopup(trigger));
42
+ onMount(syncPlacement);
29
43
 
30
44
  return (
31
- <KTooltip.Root openDelay={400}>
32
- <KTooltip.Trigger as="span" ref={layer.anchorRef}>
45
+ <KTooltip.Root
46
+ openDelay={400}
47
+ placement={beside() ? 'right' : undefined}
48
+ flip={beside() ? 'left' : undefined}
49
+ gutter={beside() ? 8 : undefined}
50
+ >
51
+ <KTooltip.Trigger
52
+ as="span"
53
+ ref={(element: HTMLElement) => {
54
+ trigger = element;
55
+ layer.anchorRef(element);
56
+ }}
57
+ onPointerEnter={syncPlacement}
58
+ >
33
59
  <KButton.Root
34
60
  aria-label={props.label}
35
61
  aria-pressed={props.pressed}
@@ -48,7 +74,9 @@ export function ToolbarButton(props: ToolbarButtonProps) {
48
74
  <Show when={layer.mount()} keyed>
49
75
  {(mount) => (
50
76
  <KTooltip.Portal mount={mount}>
51
- <KTooltip.Content class="zylem-tooltip">
77
+ <KTooltip.Content
78
+ class={beside() ? 'zylem-tooltip zylem-tooltip--front' : 'zylem-tooltip'}
79
+ >
52
80
  {props.label}
53
81
  </KTooltip.Content>
54
82
  </KTooltip.Portal>
@@ -0,0 +1,61 @@
1
+ import { For, Show, type Component } from 'solid-js';
2
+ import { Dynamic } from 'solid-js/web';
3
+ import { MenuButton, type MenuButtonProps } from '../MenuButton/MenuButton';
4
+ import { ToolbarButton } from '../ToolbarButton/ToolbarButton';
5
+
6
+ export interface ToolbarDropdownItem {
7
+ id: string;
8
+ label: string;
9
+ /** Glyph for the face and for this row in the menu. */
10
+ icon: Component<{ class?: string }>;
11
+ }
12
+
13
+ export interface ToolbarDropdownMenuProps {
14
+ /** Choices. The face shows whichever matches `value`. */
15
+ items: readonly ToolbarDropdownItem[];
16
+ value: string;
17
+ onChange: (id: string) => void;
18
+ /** Where the menu opens. Defaults to dropping down from the face. */
19
+ placement?: MenuButtonProps['placement'];
20
+ }
21
+
22
+ /**
23
+ * A toolbar button that shows the current choice and opens a vertical menu of
24
+ * the others. The face is the selected item; the rows are plain icon buttons,
25
+ * and picking one updates the face and closes the menu.
26
+ */
27
+ export function ToolbarDropdownMenu(props: ToolbarDropdownMenuProps) {
28
+ const current = () =>
29
+ props.items.find((item) => item.id === props.value) ?? props.items[0];
30
+
31
+ return (
32
+ <Show when={current()} keyed>
33
+ {(item) => (
34
+ <MenuButton
35
+ label={item.label}
36
+ selected
37
+ placement={props.placement ?? 'bottom-start'}
38
+ menu={(close) => (
39
+ <div class="zylem-menu-button__stack">
40
+ <For each={props.items}>
41
+ {(entry) => (
42
+ <ToolbarButton
43
+ label={entry.label}
44
+ onClick={() => {
45
+ props.onChange(entry.id);
46
+ close();
47
+ }}
48
+ >
49
+ <Dynamic component={entry.icon} class="zylem-icon" />
50
+ </ToolbarButton>
51
+ )}
52
+ </For>
53
+ </div>
54
+ )}
55
+ >
56
+ <Dynamic component={item.icon} class="zylem-icon" />
57
+ </MenuButton>
58
+ )}
59
+ </Show>
60
+ );
61
+ }
@@ -33,3 +33,12 @@ globalStyle('.zylem-tooltip', {
33
33
  globalStyle('.zylem-tooltip[data-enter]', {
34
34
  animation: `${enter} 0.15s ${vars.motion.easeOut}`,
35
35
  });
36
+
37
+ /**
38
+ * Opened from inside a popup. The menu tier sits above tooltips, so one step
39
+ * past it is what puts this label over the popup it came from and over
40
+ * everything else.
41
+ */
42
+ globalStyle('.zylem-tooltip.zylem-tooltip--front', {
43
+ zIndex: `calc(${vars.layers.menu} + 1)`,
44
+ });
@@ -1,7 +1,18 @@
1
1
  import { Show } from 'solid-js';
2
2
  import ChevronDown from 'lucide-solid/icons/chevron-down';
3
3
  import ChevronUp from 'lucide-solid/icons/chevron-up';
4
+ import Dock from 'lucide-solid/icons/dock';
4
5
  import X from 'lucide-solid/icons/x';
6
+ import { DropdownMenu } from '../DropdownMenu/DropdownMenu';
7
+
8
+ /** Where a panel sits. `float` leaves it free of the top and bottom edges. */
9
+ export type DockPosition = 'top' | 'float' | 'bottom';
10
+
11
+ const DOCK_OPTIONS: readonly { id: DockPosition; label: string }[] = [
12
+ { id: 'top', label: 'Top' },
13
+ { id: 'float', label: 'Float' },
14
+ { id: 'bottom', label: 'Bottom' },
15
+ ];
5
16
 
6
17
  export interface WindowControlsProps {
7
18
  // Optional props accept an explicit `undefined` so callers compiled with
@@ -14,17 +25,36 @@ export interface WindowControlsProps {
14
25
  closeLabel?: string | undefined;
15
26
  /** Forwarded as data-testid on the close button. */
16
27
  closeTestId?: string | undefined;
28
+ /** Current dock, checked in the menu. Omit for none checked. */
29
+ dock?: DockPosition | undefined;
30
+ /** Renders the dock control. Called with the position chosen from the menu. */
31
+ onDock?: ((position: DockPosition) => void) | undefined;
17
32
  }
18
33
 
19
34
  /**
20
35
  * Shared ghost window controls, used by floating and detached panels. Close
21
- * takes the danger color only while hovered or focused. Only controls with a
22
- * handler are rendered.
36
+ * takes the danger color only while hovered or focused. Dock opens a menu of
37
+ * where the panel sits. Only controls with a handler are rendered.
23
38
  */
24
39
  export function WindowControls(props: WindowControlsProps) {
25
40
  return (
26
- <Show when={props.onClose || props.onCollapse}>
41
+ <Show when={props.onClose || props.onCollapse || props.onDock}>
27
42
  <div class="zylem-window-controls" role="group" aria-label="Window controls">
43
+ <Show when={props.onDock}>
44
+ <DropdownMenu
45
+ triggerLabel="Dock panel"
46
+ triggerClass="zylem-window-control zylem-window-control--dock"
47
+ placement="bottom-end"
48
+ trigger={<Dock size={11} strokeWidth={2.5} aria-hidden="true" />}
49
+ items={DOCK_OPTIONS.map((option) => ({
50
+ id: option.id,
51
+ label: option.label,
52
+ checked: props.dock === option.id,
53
+ closeOnSelect: true,
54
+ onSelect: () => props.onDock?.(option.id),
55
+ }))}
56
+ />
57
+ </Show>
28
58
  <Show when={props.onCollapse}>
29
59
  <button
30
60
  type="button"
@@ -68,7 +68,11 @@ export { Toast, type ToastProps, type ToastTone } from './Toast/Toast';
68
68
  export { Separator, type SeparatorProps } from './Separator/Separator';
69
69
 
70
70
  // Composites
71
- export { WindowControls, type WindowControlsProps } from './WindowControls/WindowControls';
71
+ export {
72
+ WindowControls,
73
+ type DockPosition,
74
+ type WindowControlsProps,
75
+ } from './WindowControls/WindowControls';
72
76
  export { TitleBar, type TitleBarProps } from './TitleBar/TitleBar';
73
77
  export { Panel, type PanelProps } from './Panel/Panel';
74
78
  export { Toolbar, type ToolbarProps } from './Toolbar/Toolbar';
@@ -90,6 +94,11 @@ export {
90
94
  } from './Sidebar/Sidebar';
91
95
  export { Card, type CardProps } from './Card/Card';
92
96
  export { MenuButton, type MenuButtonProps } from './MenuButton/MenuButton';
97
+ export {
98
+ ToolbarDropdownMenu,
99
+ type ToolbarDropdownItem,
100
+ type ToolbarDropdownMenuProps,
101
+ } from './ToolbarDropdownMenu/ToolbarDropdownMenu';
93
102
  export { ItemPicker, type ItemPickerProps } from './ItemPicker/ItemPicker';
94
103
  export {
95
104
  filterItems,