@zylem/ui 0.9.7 → 0.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zylem/ui",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "Design tokens, global styles, and Solid components for Zylem, built with vanilla-extract",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -50,18 +50,10 @@
50
50
  },
51
51
  "files": [
52
52
  "dist",
53
+ "etc",
53
54
  "src",
54
55
  "vite"
55
56
  ],
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
- },
65
57
  "peerDependencies": {
66
58
  "solid-js": "^1.9.9"
67
59
  },
@@ -72,6 +64,7 @@
72
64
  },
73
65
  "devDependencies": {
74
66
  "@biomejs/biome": "^2.3.7",
67
+ "@microsoft/api-extractor": "^7.55.2",
75
68
  "@clack/prompts": "^1.6.0",
76
69
  "@solidjs/testing-library": "^0.8.10",
77
70
  "@vanilla-extract/esbuild-plugin": "^2.3.22",
@@ -95,5 +88,13 @@
95
88
  "node": ">=22.12.0",
96
89
  "pnpm": ">=10.32.1"
97
90
  },
98
- "packageManager": "pnpm@10.33.0"
99
- }
91
+ "scripts": {
92
+ "build": "tsup",
93
+ "dev": "tsup --watch",
94
+ "showcase": "vite --host",
95
+ "test": "vitest run",
96
+ "test:watch": "vitest",
97
+ "lint": "biome check .",
98
+ "api:extract": "tsc -p tsconfig.api.json && api-extractor run"
99
+ }
100
+ }
@@ -0,0 +1,115 @@
1
+ import { For, Show, type Component } from 'solid-js';
2
+ import { Dynamic } from 'solid-js/web';
3
+ import {
4
+ FreePerspective,
5
+ GameCamera,
6
+ Perspective,
7
+ ViewBottom,
8
+ ViewIsoLeftBottom,
9
+ ViewIsoLeftTop,
10
+ ViewIsoRightBottom,
11
+ ViewIsoRightTop,
12
+ ViewLeft,
13
+ ViewRight,
14
+ ViewTop,
15
+ } from '../../icons';
16
+ import { MenuButton, type MenuButtonProps } from '../MenuButton/MenuButton';
17
+ import { ToolbarButton } from '../ToolbarButton/ToolbarButton';
18
+
19
+ /** Game and perspective sit above the cube. The square is row-major. */
20
+ const HEADER = ['game', 'perspective'] as const;
21
+ const SQUARE = [
22
+ 'iso-left-top',
23
+ 'top',
24
+ 'iso-right-top',
25
+ 'left',
26
+ 'free',
27
+ 'right',
28
+ 'iso-left-bottom',
29
+ 'bottom',
30
+ 'iso-right-bottom',
31
+ ] as const;
32
+
33
+ export const CAMERA_TOGGLE_IDS = [...HEADER, ...SQUARE] as const;
34
+
35
+ export type CameraToggleId = (typeof CAMERA_TOGGLE_IDS)[number];
36
+
37
+ const VIEWS: Record<CameraToggleId, { label: string; icon: Component<{ class?: string }> }> = {
38
+ game: { label: 'Game camera', icon: GameCamera },
39
+ perspective: { label: 'Perspective', icon: Perspective },
40
+ 'iso-left-top': { label: 'Iso left/top', icon: ViewIsoLeftTop },
41
+ top: { label: 'Top', icon: ViewTop },
42
+ 'iso-right-top': { label: 'Iso right/top', icon: ViewIsoRightTop },
43
+ left: { label: 'Left', icon: ViewLeft },
44
+ free: { label: 'Free camera', icon: FreePerspective },
45
+ right: { label: 'Right', icon: ViewRight },
46
+ 'iso-left-bottom': { label: 'Iso left/bottom', icon: ViewIsoLeftBottom },
47
+ bottom: { label: 'Bottom', icon: ViewBottom },
48
+ 'iso-right-bottom': { label: 'Iso right/bottom', icon: ViewIsoRightBottom },
49
+ };
50
+
51
+ export interface CameraToggleProps {
52
+ /** The view the face shows. */
53
+ value: CameraToggleId;
54
+ onChange: (id: CameraToggleId) => void;
55
+ /**
56
+ * Draws the face as an on/off toggle. Electric blue while an editor camera
57
+ * is showing; the game camera is the off state.
58
+ */
59
+ on?: boolean | undefined;
60
+ /** Splits the button: the face fires this and the chevron opens the cube. */
61
+ onAction?: (() => void) | undefined;
62
+ /** Where the cube opens. Defaults to dropping down from the face. */
63
+ placement?: MenuButtonProps['placement'];
64
+ }
65
+
66
+ /**
67
+ * One camera button. The face is the current view. The chevron opens a square:
68
+ * free camera in the middle, orthographic views on the edges, isometric views
69
+ * on the corners, with the game camera and perspective on the row above.
70
+ */
71
+ export function CameraToggle(props: CameraToggleProps) {
72
+ const current = () => VIEWS[props.value] ?? VIEWS.game;
73
+
74
+ const cell = (id: CameraToggleId, close: () => void) => {
75
+ const view = VIEWS[id];
76
+ return (
77
+ <ToolbarButton
78
+ label={view.label}
79
+ selected={props.value === id}
80
+ onClick={() => {
81
+ props.onChange(id);
82
+ close();
83
+ }}
84
+ >
85
+ <Dynamic component={view.icon} class="zylem-icon" />
86
+ </ToolbarButton>
87
+ );
88
+ };
89
+
90
+ return (
91
+ <Show when={current()} keyed>
92
+ {(view) => (
93
+ <MenuButton
94
+ label={view.label}
95
+ selected={props.on === undefined}
96
+ on={props.on}
97
+ onAction={props.onAction}
98
+ placement={props.placement ?? 'bottom-start'}
99
+ menu={(close) => (
100
+ <div class="zylem-menu-button__compass">
101
+ <div class="zylem-menu-button__compass-row">
102
+ <For each={HEADER}>{(id) => cell(id, close)}</For>
103
+ </div>
104
+ <div class="zylem-menu-button__compass-square">
105
+ <For each={SQUARE}>{(id) => cell(id, close)}</For>
106
+ </div>
107
+ </div>
108
+ )}
109
+ >
110
+ <Dynamic component={view.icon} class="zylem-icon" />
111
+ </MenuButton>
112
+ )}
113
+ </Show>
114
+ );
115
+ }
@@ -19,6 +19,11 @@ export interface ConsoleProps {
19
19
  /**
20
20
  * HyperGlass console: dark low-gloss terminal field with a floating Clear
21
21
  * action.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * <Console value={text()} onInput={setText} onClear={() => setText('')} />
26
+ * ```
22
27
  */
23
28
  export function Console(props: ConsoleProps) {
24
29
  return (
@@ -104,3 +104,28 @@ globalStyle('.zylem-menu-button__stack', {
104
104
  gap: vars.spacing.xs,
105
105
  padding: vars.spacing.sm,
106
106
  });
107
+
108
+ /**
109
+ * View cube. A short row (game camera, perspective) sits above a 3×3 square:
110
+ * isometric corners, orthographic edges, free camera in the middle.
111
+ */
112
+ globalStyle('.zylem-menu-button__compass', {
113
+ display: 'flex',
114
+ flexDirection: 'column',
115
+ alignItems: 'flex-start',
116
+ gap: vars.spacing.xs,
117
+ padding: vars.spacing.sm,
118
+ });
119
+
120
+ globalStyle('.zylem-menu-button__compass-row, .zylem-menu-button__compass-square', {
121
+ display: 'grid',
122
+ gap: vars.spacing.xs,
123
+ });
124
+
125
+ globalStyle('.zylem-menu-button__compass-row', {
126
+ gridTemplateColumns: 'repeat(2, auto)',
127
+ });
128
+
129
+ globalStyle('.zylem-menu-button__compass-square', {
130
+ gridTemplateColumns: 'repeat(3, auto)',
131
+ });
@@ -22,6 +22,12 @@ export interface MenuButtonProps {
22
22
  onAction?: (() => void) | undefined;
23
23
  /** Renders the face in its selected state. */
24
24
  selected?: boolean | undefined;
25
+ /**
26
+ * Checkbox-like toggle for the face. Electric blue while on. A split face
27
+ * exposes it as `aria-pressed`, so pass `false` for a toggle that is off and
28
+ * the face still announces itself.
29
+ */
30
+ on?: boolean | undefined;
25
31
  disabled?: boolean | undefined;
26
32
  /** Tooltip text; defaults to `label`. Pass `null` for no tooltip. */
27
33
  tooltip?: string | null | undefined;
@@ -108,6 +114,7 @@ export function MenuButton(props: MenuButtonProps) {
108
114
  aria-label={props.label}
109
115
  disabled={props.disabled}
110
116
  data-selected={props.selected ? '' : undefined}
117
+ data-on={props.on ? '' : undefined}
111
118
  >
112
119
  {props.children}
113
120
  {/*
@@ -129,8 +136,10 @@ export function MenuButton(props: MenuButtonProps) {
129
136
  ref={layer.anchorRef}
130
137
  class="zylem-toolbar-btn zylem-menu-button__face"
131
138
  aria-label={props.label}
139
+ aria-pressed={props.on}
132
140
  disabled={props.disabled}
133
141
  data-selected={props.selected ? '' : undefined}
142
+ data-on={props.on ? '' : undefined}
134
143
  onClick={() => props.onAction?.()}
135
144
  >
136
145
  {props.children}
@@ -1,7 +1,7 @@
1
1
  import { NumberField as KNumberField } from '@kobalte/core';
2
2
  import ChevronDown from 'lucide-solid/icons/chevron-down';
3
3
  import ChevronUp from 'lucide-solid/icons/chevron-up';
4
- import { Show } from 'solid-js';
4
+ import { createEffect, createSignal, on, onCleanup, Show } from 'solid-js';
5
5
 
6
6
  export interface NumberFieldProps {
7
7
  label?: string;
@@ -9,7 +9,15 @@ export interface NumberFieldProps {
9
9
  'aria-label'?: string;
10
10
  error?: string;
11
11
  value?: number;
12
+ /** Fires on every edit. */
12
13
  onChange?: (value: number) => void;
14
+ /**
15
+ * Fires on blur, Enter, and a stepper press. While set, typing stays in the
16
+ * field and only this reports it, so a parent that snaps or rounds the value
17
+ * does not fight the typing. A draft also outlasts changes to `value`, and
18
+ * Escape drops it; after each commit the field shows `value` again.
19
+ */
20
+ onCommit?: (value: number) => void;
13
21
  minValue?: number;
14
22
  maxValue?: number;
15
23
  step?: number;
@@ -23,15 +31,91 @@ export interface NumberFieldProps {
23
31
  * HyperGlass number field: inset glass input with square jewel steppers.
24
32
  */
25
33
  export function NumberField(props: NumberFieldProps) {
34
+ // A stepper press blurs the input before the click lands. Armed on pointer
35
+ // down, so that blur does not commit the pre-step value, and the click commits
36
+ // the stepped one.
37
+ let stepper = false;
38
+ // Enter commits, and the blur that follows it must not commit again.
39
+ let committed = false;
40
+
41
+ // Kobalte rewrites the input whenever `rawValue` changes, so a value that
42
+ // moves every frame would overwrite the typing. A draft hands it `held`.
43
+ const [drafting, setDrafting] = createSignal(false);
44
+ let held: number | undefined;
45
+ // A committed draft stays up until `value` changes, so the field does not
46
+ // flash the old value while the owner's answer is on its way. One frame is
47
+ // the limit, for an owner that rounds the commit back to the same value.
48
+ let settling = false;
49
+ let settleFrame = 0;
50
+
51
+ const hold = (value: number | undefined) => {
52
+ if (drafting() || value === undefined) return;
53
+ held = value;
54
+ setDrafting(true);
55
+ };
56
+
57
+ const stopSettling = () => {
58
+ settling = false;
59
+ if (settleFrame) cancelAnimationFrame(settleFrame);
60
+ settleFrame = 0;
61
+ };
62
+
63
+ const release = () => {
64
+ stopSettling();
65
+ setDrafting(false);
66
+ };
67
+
68
+ createEffect(
69
+ on(
70
+ () => props.value,
71
+ () => {
72
+ if (settling) release();
73
+ },
74
+ { defer: true },
75
+ ),
76
+ );
77
+ onCleanup(stopSettling);
78
+
79
+ const commit = (value: number) => {
80
+ if (!props.onCommit) return;
81
+ if (Number.isNaN(value) || value === props.value) {
82
+ release();
83
+ return;
84
+ }
85
+ // A stepper commit has no draft yet. Held at the stepped value, the field
86
+ // keeps showing it rather than the value it stepped from.
87
+ hold(value);
88
+ settling = true;
89
+ props.onCommit(value);
90
+ committed = true;
91
+ if (settling) settleFrame = requestAnimationFrame(release);
92
+ };
93
+
94
+ const commitInput = (input: HTMLInputElement) => {
95
+ const parsed = Number.parseFloat(input.value);
96
+ commit(Number.isFinite(parsed) ? parsed : Number.NaN);
97
+ };
98
+
99
+ const armStepper = () => {
100
+ stepper = true;
101
+ };
102
+
26
103
  return (
27
104
  <KNumberField.Root
28
105
  class={props.class ? `zylem-field-root ${props.class}` : 'zylem-field-root'}
29
- rawValue={props.value}
106
+ rawValue={drafting() ? held : props.value}
30
107
  // Kobalte reports the parsed value on mount and after each format pass;
31
108
  // only real edits reach the owner, so a controlled form is not marked
32
109
  // dirty just by rendering.
33
110
  onRawValueChange={(value) => {
34
- if (!props.onChange || Number.isNaN(value) || value === props.value) return;
111
+ if (Number.isNaN(value)) return;
112
+ if (props.onCommit) {
113
+ if (!stepper) return;
114
+ stepper = false;
115
+ commit(value);
116
+ return;
117
+ }
118
+ if (!props.onChange || value === props.value) return;
35
119
  props.onChange(value);
36
120
  }}
37
121
  minValue={props.minValue}
@@ -51,16 +135,45 @@ export function NumberField(props: NumberFieldProps) {
51
135
  class="zylem-field zylem-numberfield-input"
52
136
  aria-label={props['aria-label']}
53
137
  placeholder={props.placeholder}
138
+ onInput={() => {
139
+ committed = false;
140
+ if (!props.onCommit) return;
141
+ // Typing again while a commit settles carries on with the draft, so
142
+ // the value that lands does not overwrite the new keystrokes.
143
+ stopSettling();
144
+ hold(props.value);
145
+ }}
146
+ onBlur={(event) => {
147
+ if (!props.onCommit || stepper || committed) {
148
+ committed = false;
149
+ return;
150
+ }
151
+ commitInput(event.currentTarget);
152
+ }}
153
+ onKeyDown={(event) => {
154
+ if (!props.onCommit || event.defaultPrevented) return;
155
+ if (event.key === 'Enter') commitInput(event.currentTarget);
156
+ else if (event.key === 'Escape' && drafting()) {
157
+ // Claimed, so an accordion or dialog around the field leaves this
158
+ // Escape alone rather than collapsing or closing as well.
159
+ event.preventDefault();
160
+ release();
161
+ }
162
+ }}
54
163
  />
55
164
  <KNumberField.IncrementTrigger
56
165
  class="zylem-numberfield-stepper zylem-numberfield-stepper--up"
57
166
  aria-label="Increment"
167
+ onPointerDown={armStepper}
168
+ onClick={armStepper}
58
169
  >
59
170
  <ChevronUp aria-hidden="true" />
60
171
  </KNumberField.IncrementTrigger>
61
172
  <KNumberField.DecrementTrigger
62
173
  class="zylem-numberfield-stepper zylem-numberfield-stepper--down"
63
174
  aria-label="Decrement"
175
+ onPointerDown={armStepper}
176
+ onClick={armStepper}
64
177
  >
65
178
  <ChevronDown aria-hidden="true" />
66
179
  </KNumberField.DecrementTrigger>
@@ -102,9 +102,18 @@ export function PropertyRow(props: PropertyRowProps) {
102
102
  <Show
103
103
  when={display().isClickable}
104
104
  fallback={
105
- <span class={`zylem-property-value ${props.valueClass ?? ''}`}>
106
- {display().element}
107
- </span>
105
+ // A div when the value is elements. A NumberField root is a div, and a
106
+ // span would close before it, dropping the fields out of this cell.
107
+ <Show
108
+ when={props.children !== undefined}
109
+ fallback={
110
+ <span class={`zylem-property-value ${props.valueClass ?? ''}`}>
111
+ {display().element}
112
+ </span>
113
+ }
114
+ >
115
+ <div class={`zylem-property-value ${props.valueClass ?? ''}`}>{props.children}</div>
116
+ </Show>
108
117
  }
109
118
  >
110
119
  <button
@@ -1,6 +1,7 @@
1
1
  /**
2
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.
3
+ * track's range. A degenerate range, where max is less than or equal to min,
4
+ * pins everything to 0.
4
5
  */
5
6
  export function markerPercent(t: number, min: number, max: number): number {
6
7
  if (!Number.isFinite(t) || max <= min) {
@@ -34,6 +34,7 @@ export function TitleBar(props: TitleBarProps) {
34
34
  'closeLabel',
35
35
  'closeTestId',
36
36
  'dock',
37
+ 'dockOptions',
37
38
  'onDock',
38
39
  'class',
39
40
  ]);
@@ -63,6 +64,7 @@ export function TitleBar(props: TitleBarProps) {
63
64
  closeLabel={local.closeLabel}
64
65
  closeTestId={local.closeTestId}
65
66
  dock={local.dock}
67
+ dockOptions={local.dockOptions}
66
68
  onDock={local.onDock}
67
69
  />
68
70
  </div>
@@ -21,7 +21,7 @@ export type ArtTool =
21
21
  | 'eyedropper'
22
22
  | 'move';
23
23
 
24
- type IconComponent = Component<JSX.SvgSVGAttributes<SVGSVGElement>>;
24
+ export type IconComponent = Component<JSX.SvgSVGAttributes<SVGSVGElement>>;
25
25
 
26
26
  export const ART_TOOL_ICONS: Record<ArtTool, IconComponent> = {
27
27
  pencil: Pencil,
@@ -4,7 +4,8 @@ import { vars } from '../../theme.css';
4
4
  /**
5
5
  * Square glass icon button for toolbars — owns the `.zylem-toolbar-btn`
6
6
  * class the editor already uses. Selected state uses the live-green jewel
7
- * treatment.
7
+ * treatment. `data-danger` is that same selected state in the danger jewel,
8
+ * used by the Delete tool.
8
9
  */
9
10
  globalStyle('.zylem-toolbar-btn', {
10
11
  boxSizing: 'border-box',
@@ -67,6 +68,18 @@ globalStyle('.zylem-toolbar-btn[data-selected], .zylem-toolbar-btn.selected', {
67
68
  boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowActive}`,
68
69
  });
69
70
 
71
+ globalStyle('.zylem-toolbar-btn[data-danger]', {
72
+ borderColor: 'rgba(255, 132, 114, 0.78)',
73
+ background: vars.material.buttonGlassDanger,
74
+ boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowDanger}`,
75
+ });
76
+
77
+ globalStyle('.zylem-toolbar-btn[data-danger]:hover', {
78
+ background: vars.material.buttonGlassDanger,
79
+ boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowDanger}`,
80
+ filter: 'brightness(1.1)',
81
+ });
82
+
70
83
  globalStyle('.zylem-toolbar-btn[disabled]', {
71
84
  opacity: 0.45,
72
85
  cursor: 'not-allowed',
@@ -8,8 +8,14 @@ export interface ToolbarButtonProps {
8
8
  /**
9
9
  * The selected look alone, for a button whose label already says its state
10
10
  * (Play / Pause). Live green. For a toggle with a constant label, use `on`.
11
+ * With `danger`, the same state is the danger jewel instead.
11
12
  */
12
13
  selected?: boolean;
14
+ /**
15
+ * Draw `selected` as the danger jewel instead of live green. The button stays
16
+ * ordinary glass until it is selected. Only Delete uses this.
17
+ */
18
+ danger?: boolean;
13
19
  /**
14
20
  * Checkbox-like toggle. Electric blue while on, and exposed as `aria-pressed`.
15
21
  * Pass `false` for a toggle that is off so the button still announces itself.
@@ -36,7 +42,8 @@ function insidePopup(element: Element | undefined): boolean {
36
42
 
37
43
  /**
38
44
  * HyperGlass toolbar button: square glass icon button with a tooltip.
39
- * `selected` is the live-green jewel. `on` is the electric-blue toggle.
45
+ * `selected` is the live-green jewel. `danger` swaps that jewel for the
46
+ * danger one while selected. `on` is the electric-blue toggle.
40
47
  *
41
48
  * The tooltip portals into the layer container for its own tree, so it stays
42
49
  * styled when the toolbar lives inside a web component's shadow root.
@@ -74,7 +81,8 @@ export function ToolbarButton(props: ToolbarButtonProps) {
74
81
  ? `zylem-toolbar-btn ${props.class}`
75
82
  : 'zylem-toolbar-btn'
76
83
  }
77
- data-selected={props.selected ? '' : undefined}
84
+ data-selected={props.selected && !props.danger ? '' : undefined}
85
+ data-danger={props.selected && props.danger ? '' : undefined}
78
86
  data-on={props.on || (props.pressed && !props.selected) ? '' : undefined}
79
87
  >
80
88
  {props.children}
@@ -15,6 +15,13 @@ export interface ToolbarDropdownMenuProps {
15
15
  items: readonly ToolbarDropdownItem[];
16
16
  value: string;
17
17
  onChange: (id: string) => void;
18
+ /**
19
+ * Draws the face as an on/off toggle instead of always selected, for a picker
20
+ * where one choice means off, such as the game camera among editor cameras.
21
+ */
22
+ on?: boolean | undefined;
23
+ /** Splits the button: the face fires this and the chevron opens the menu. */
24
+ onAction?: (() => void) | undefined;
18
25
  /** Where the menu opens. Defaults to dropping down from the face. */
19
26
  placement?: MenuButtonProps['placement'];
20
27
  }
@@ -33,7 +40,9 @@ export function ToolbarDropdownMenu(props: ToolbarDropdownMenuProps) {
33
40
  {(item) => (
34
41
  <MenuButton
35
42
  label={item.label}
36
- selected
43
+ selected={props.on === undefined}
44
+ on={props.on}
45
+ onAction={props.onAction}
37
46
  placement={props.placement ?? 'bottom-start'}
38
47
  menu={(close) => (
39
48
  <div class="zylem-menu-button__stack">
@@ -5,10 +5,17 @@ import X from 'lucide-solid/icons/x';
5
5
  import { Dock } from '../../icons';
6
6
  import { DropdownMenu } from '../DropdownMenu/DropdownMenu';
7
7
 
8
- /** Where a panel sits. `float` leaves it free of the top and bottom edges. */
9
- export type DockPosition = 'top' | 'float' | 'bottom';
8
+ /** Where a panel sits. `float` leaves it free of a dock edge. */
9
+ export type DockPosition = 'left' | 'right' | 'top' | 'float' | 'bottom';
10
10
 
11
- const DOCK_OPTIONS: readonly { id: DockPosition; label: string }[] = [
11
+ /** One row in the dock menu. */
12
+ export interface DockOption {
13
+ id: DockPosition;
14
+ label: string;
15
+ }
16
+
17
+ /** Top, Float, and Bottom: the menu a strip uses when it docks to those edges. */
18
+ const DEFAULT_DOCK_OPTIONS: readonly DockOption[] = [
12
19
  { id: 'top', label: 'Top' },
13
20
  { id: 'float', label: 'Float' },
14
21
  { id: 'bottom', label: 'Bottom' },
@@ -27,6 +34,11 @@ export interface WindowControlsProps {
27
34
  closeTestId?: string | undefined;
28
35
  /** Current dock, checked in the menu. Omit for none checked. */
29
36
  dock?: DockPosition | undefined;
37
+ /**
38
+ * Rows in the dock menu. Defaults to Top, Float, and Bottom. Pass this when
39
+ * the window docks to other edges, such as Left and Right.
40
+ */
41
+ dockOptions?: readonly DockOption[] | undefined;
30
42
  /** Renders the dock control. Called with the position chosen from the menu. */
31
43
  onDock?: ((position: DockPosition) => void) | undefined;
32
44
  }
@@ -34,7 +46,8 @@ export interface WindowControlsProps {
34
46
  /**
35
47
  * Shared ghost window controls, used by floating and detached panels. Close
36
48
  * 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.
49
+ * where the panel sits: Top, Float, and Bottom, or `dockOptions` when the
50
+ * window uses other edges. Only controls with a handler are rendered.
38
51
  */
39
52
  export function WindowControls(props: WindowControlsProps) {
40
53
  return (
@@ -46,7 +59,7 @@ export function WindowControls(props: WindowControlsProps) {
46
59
  triggerClass="zylem-window-control zylem-window-control--dock"
47
60
  placement="bottom-end"
48
61
  trigger={<Dock size={11} aria-hidden="true" />}
49
- items={DOCK_OPTIONS.map((option) => ({
62
+ items={(props.dockOptions ?? DEFAULT_DOCK_OPTIONS).map((option) => ({
50
63
  id: option.id,
51
64
  label: option.label,
52
65
  checked: props.dock === option.id,
@@ -70,6 +70,7 @@ export { Separator, type SeparatorProps } from './Separator/Separator';
70
70
  // Composites
71
71
  export {
72
72
  WindowControls,
73
+ type DockOption,
73
74
  type DockPosition,
74
75
  type WindowControlsProps,
75
76
  } from './WindowControls/WindowControls';
@@ -99,6 +100,12 @@ export {
99
100
  type ToolbarDropdownItem,
100
101
  type ToolbarDropdownMenuProps,
101
102
  } from './ToolbarDropdownMenu/ToolbarDropdownMenu';
103
+ export {
104
+ CameraToggle,
105
+ CAMERA_TOGGLE_IDS,
106
+ type CameraToggleId,
107
+ type CameraToggleProps,
108
+ } from './CameraToggle/CameraToggle';
102
109
  export { ItemPicker, type ItemPickerProps } from './ItemPicker/ItemPicker';
103
110
  export {
104
111
  filterItems,
@@ -144,6 +151,7 @@ export {
144
151
  ART_TOOL_ICONS,
145
152
  ART_TOOL_LABELS,
146
153
  type ArtTool,
154
+ type IconComponent,
147
155
  type ToolButtonProps,
148
156
  } from './ToolButton/ToolButton';
149
157
  export { AddTile, type AddTileProps, type AddTileVariant } from './AddTile/AddTile';
@@ -208,6 +216,8 @@ export {
208
216
  majorTickTimes,
209
217
  formatClock,
210
218
  formatBeats,
219
+ type ResolveClipDragOptions,
220
+ type RulerTickOptions,
211
221
  } from './TimelineLanes/lanes-math';
212
222
  export { TrackHeader, type TrackHeaderProps } from './TrackHeader/TrackHeader';
213
223
  export { PianoRoll, type PianoRollProps, type PianoRollNote } from './PianoRoll/PianoRoll';
@@ -216,6 +226,7 @@ export {
216
226
  isBlackKey,
217
227
  resolveNoteDrag,
218
228
  cellStart,
229
+ type NoteDragOptions,
219
230
  } from './PianoRoll/piano-roll-math';
220
231
  export { Knob, arcPath, type KnobProps } from './Mixer/Knob';
221
232
  export { MixerStrip, formatDb, formatPan, type MixerStripProps } from './Mixer/MixerStrip';
@@ -231,12 +242,21 @@ export {
231
242
  // Camera icons (Lucide-shaped originals; also `@zylem/ui/icons`)
232
243
  export {
233
244
  CAMERA_ICONS,
245
+ CAMERA_VIEWS,
234
246
  FreePerspective,
235
247
  GameCamera,
236
248
  IsometricPerspective,
237
249
  Perspective,
238
250
  SideviewPerspective,
239
251
  TopDownPerspective,
252
+ ViewBottom,
253
+ ViewIsoLeftBottom,
254
+ ViewIsoLeftTop,
255
+ ViewIsoRightBottom,
256
+ ViewIsoRightTop,
257
+ ViewLeft,
258
+ ViewRight,
259
+ ViewTop,
240
260
  Dock,
241
261
  cameraIconNodes,
242
262
  createZylemIcon,