@weasel-js/menu 1.10.1 → 1.11.0

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/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @weasel-js/menu
2
2
 
3
- The headless model behind weasel's context menus: entries (items, separators, submenus), a
3
+ The headless model behind weasel's context menus: entries (items, separators, submenus, number rows
4
+ edited in place, and read-only color rows), a
4
5
  controller that owns the highlight, open submenus, keyboard and typeahead behavior, and placement
5
6
  that keeps a menu on screen. No React, no DOM.
6
7
 
@@ -19,24 +20,52 @@ npm install @weasel-js/menu
19
20
  | Package | Role |
20
21
  |---|---|
21
22
  | `@weasel-js/menu` | The model: entries, `createMenuController`, `placeMenu` / `placeSubmenu`. |
22
- | `@weasel-js/routing` | Declared menus (`MenuDef`: sections of action references) and `MenuHost`, which holds the open menu. |
23
- | `@weasel-js/core` | The `menu.context` action, which opens a menu from a right-click or the ContextMenu key; re-exports `useMenus`. |
24
- | `@weasel-js/ui` | `<ContextMenuHost>`, the DOM renderer, and `useContextMenu` for opening a menu from chrome. |
23
+ | `@weasel-js/routing` | Declared menus (`MenuDef`: a prefs schema drawn as a menu, by `resolveSchemaMenu`) and `MenuHost`, which holds the open menu. |
24
+ | `@weasel-js/core` | The public `menu.context` action (`contextMenuAction`, `openContextMenu`), which opens a menu from a right-click or the ContextMenu key, with or without a scene; re-exports `useMenus`. |
25
+ | `@weasel-js/ui` | `<ContextMenuHost>`, the DOM renderer, and `useContextMenu` for opening a menu from chrome; `<MenuBar>`, a bar of menu groups across the top of an app; `<MenuButton>`. |
25
26
  | `@weasel-js/hud` | `attachMenuHost`, the WebGL renderer. |
26
27
 
27
28
  The DOM renderer does not use React Aria's `Menu`, which keeps its own state and key handling: a
28
29
  second copy of the model's that would drift from the WebGL menu's.
29
30
 
31
+ ## Decisions
32
+
33
+ These are not visible in the code; each was a choice between alternatives.
34
+
35
+ - A menu holds no code. A row names a registered action; `useCommand` turns a callback into one, so
36
+ every menu can be saved as data and edited in the asset editor.
37
+ - A menu bar is new surface. Nothing moves onto it out of the component that owns it — a trial's
38
+ title bar, a tool palette, a header — it holds what is declared for it.
39
+ - Access letters are supported, never required. The default chord is Alt; Chrome on Windows is said
40
+ to open its own menu on Alt+F, and nobody has checked it there.
41
+ - The `role="menubar"` element holds only the titles. Anything else at a bar's top level draws after
42
+ them in declared order, because ARIA allows only menu items in a menubar.
43
+ - After a row runs from a menu opened by pointer, focus returns to where it was before the bar.
44
+ Only Escape lands on the title.
45
+ - An action naming a parametric action lists every variant in a menu or an `actions` group, but a
46
+ bar `action` leaf naming the bare id draws a dead button: a button is one thing, so a bar names
47
+ the variant (`viewport.zoom:in`).
48
+ - An enum draws as radio rows between separators, not as a submenu. A number draws as a row edited
49
+ in place, stepped by `+` and `-` while highlighted, and the menu stays open. Menus sharing an id append
50
+ their children in order with a separator between (a later duplicate key gets `~n`).
51
+ - `useCommand`'s `variants` are `ActionVariant`s, so a command's variants carry `tags` like any action's.
52
+ - labkit's trials stay separate action roots. The lab's own dispatcher still hears only its own
53
+ node (`hears: 'own'`), as a guard in case trials ever nest. Lab-wide globals are values, drawn
54
+ as enum rows, not commands.
55
+ - The WebGL host neither underlines access letters nor marks the current row, though letters work
56
+ there through the shared controller.
57
+
30
58
  ## Usage
31
59
 
32
60
  Declare menus as data, mount one host, and open one from chrome:
33
61
 
34
62
  ```tsx
35
63
  import { ActionsProvider, useMenus, type MenuDef } from '@weasel-js/core';
64
+ import { pref } from '@weasel-js/prefs';
36
65
  import { ContextMenuHost, useContextMenu } from '@weasel-js/ui';
37
66
 
38
67
  const MENUS: MenuDef[] = [
39
- { id: 'edit', sections: [[{ action: 'duplicate' }, { action: 'delete' }]] },
68
+ { id: 'edit', menu: pref.group('', { duplicate: pref.action('duplicate'), delete: pref.action('delete') }) },
40
69
  ];
41
70
 
42
71
  function EditButton() {
@@ -52,7 +81,8 @@ function Chrome() {
52
81
  // <ActionsProvider><Chrome /><EditButton /></ActionsProvider>
53
82
  ```
54
83
 
55
- Rows name registered actions (`duplicate`, `delete`, which act on the selection), and
84
+ Action leaves name registered actions (`duplicate`, `delete`, which act on the selection); a
85
+ group drawn `as: 'menu'` is a submenu, and any other group sits inline between separators. And
56
86
  `EditButton` and `Chrome` must sit under an `<ActionsProvider>`. Any menu opens by id; one with a `when` (`{ bodyTarget, bodyKind }`) also
57
87
  opens on a right-click of a matching canvas body.
58
88
 
package/dist/index.d.ts CHANGED
@@ -1,10 +1,15 @@
1
+ import { Display, Endless, InfinityText, UnitTable } from '@weasel-js/quantity';
2
+
1
3
  /** A key chip: the key, and which modifiers are held with it. `@weasel-js/routing`
2
- * derives these from action bindings as `ActionShortcut`. */
4
+ * derives these from action bindings as `ActionShortcut`; `@weasel-js/gestures`
5
+ * formats them as `ShortcutInput`. A tool's `keybinding` has this shape too. */
3
6
  interface MenuShortcut {
4
- key: string;
5
- mod: boolean;
6
- alt: boolean;
7
- shift: boolean;
7
+ /** A list names spellings of one keycap (`['[', '{']`); the first is drawn. */
8
+ key: string | readonly string[];
9
+ mod?: boolean;
10
+ alt?: boolean;
11
+ /** `'optional'` matches held or not, and draws as an optional key. */
12
+ shift?: boolean | 'optional';
8
13
  }
9
14
  /** A row that runs something when chosen. */
10
15
  interface MenuItemEntry {
@@ -17,6 +22,12 @@ interface MenuItemEntry {
17
22
  disabled?: boolean;
18
23
  /** `undefined` for a row with no on/off state. */
19
24
  checked?: boolean;
25
+ /** Letter that chooses this row from the keyboard, compared ignoring case. */
26
+ access?: string;
27
+ /** Where choosing the row goes; see `withLinks`. */
28
+ href?: string;
29
+ /** The row for the page that is open. */
30
+ current?: boolean;
20
31
  run(): void;
21
32
  }
22
33
  /** A rule between groups of rows. Never highlighted. */
@@ -31,12 +42,54 @@ interface SubmenuEntry {
31
42
  label: string;
32
43
  icon?: unknown;
33
44
  disabled?: boolean;
45
+ /** Letter that opens this row from the keyboard, compared ignoring case. */
46
+ access?: string;
34
47
  entries: readonly MenuEntry[];
35
48
  }
49
+ /** A row holding a number that is edited in place: the menu stays open while it changes. All of it is in the
50
+ * units shown. Its controller's `numberValue` is what it holds now. */
51
+ interface MenuNumberEntry {
52
+ kind: 'number';
53
+ key: string;
54
+ label: string;
55
+ icon?: unknown;
56
+ disabled?: boolean;
57
+ access?: string;
58
+ /** What it held when the menu opened. `undefined` for none. */
59
+ value: number | undefined;
60
+ min?: number;
61
+ max?: number;
62
+ /** What one step adds. Default 1. */
63
+ step?: number;
64
+ /** Text after the value: `'°'`, `'px'`. */
65
+ suffix?: string;
66
+ /** How the value shows, and reads back where it is typed. Default: every decimal it has. */
67
+ display?: Display;
68
+ /** The ends that stand for infinity, one step past `min` or `max`. */
69
+ endless?: Endless;
70
+ /** The word infinity shows as. Default `∞`. */
71
+ infinity?: InfinityText;
72
+ /** Units a renderer with a field reads typed, each mapped to the factor that turns a number in it into the
73
+ * unit shown: `{ mm: 0.1, cm: 1 }`. */
74
+ accepts?: Readonly<UnitTable>;
75
+ set(value: number): void;
76
+ }
77
+ /** A row showing a color. Nothing edits it from the menu. */
78
+ interface MenuColorEntry {
79
+ kind: 'color';
80
+ key: string;
81
+ label: string;
82
+ disabled?: boolean;
83
+ access?: string;
84
+ /** A CSS color, `#rrggbb` or `#rrggbbaa`. `undefined` for none. */
85
+ value: string | undefined;
86
+ }
36
87
  /** One line of a menu. */
37
- type MenuEntry = MenuItemEntry | MenuSeparatorEntry | SubmenuEntry;
88
+ type MenuEntry = MenuItemEntry | MenuSeparatorEntry | SubmenuEntry | MenuNumberEntry | MenuColorEntry;
89
+ /** An entry the highlight can land on. */
90
+ type MenuRowEntry = Exclude<MenuEntry, MenuSeparatorEntry>;
38
91
  /** True for an entry the highlight can land on. */
39
- declare function isRow(e: MenuEntry): e is MenuItemEntry | SubmenuEntry;
92
+ declare function isRow(e: MenuEntry): e is MenuRowEntry;
40
93
  /**
41
94
  * The entries shown at `level`, following `stack` (the highlighted index at
42
95
  * each open level) down from `root`. Empty when the path does not lead to a
@@ -68,6 +121,27 @@ declare function placeMenu(size: Size, anchor: Point, bounds: Rect): Point;
68
121
  /** Where a submenu goes: beside `row`, right first, left if that does not
69
122
  * fit, top-aligned with the row and shifted up to stay inside `bounds`. */
70
123
  declare function placeSubmenu(size: Size, row: Rect, bounds: Rect): Point;
124
+ /** Where a menu opened from the button at `under` goes: below it and
125
+ * left-aligned, flipped above it or right-aligned to it on an axis that does
126
+ * not fit, shifted inside `bounds` when neither side does. */
127
+ declare function placeMenuUnder(size: Size, under: Rect, bounds: Rect): Point;
128
+
129
+ /** The one character a declared access letter stands for: its first, ignoring case. Undefined for none. */
130
+ declare function accessLetter(access: string | undefined): string | undefined;
131
+ /** Index of the first row declaring `key` as its access letter, ignoring case; disabled rows included. */
132
+ declare function accessMatch(entries: readonly MenuEntry[], key: string): number | null;
133
+
134
+ /** Entries where an item with `href` runs its own `run`, then `navigate(href)`. Recurses into submenus. */
135
+ declare function withLinks(entries: readonly MenuEntry[], navigate: (href: string) => void): MenuEntry[];
136
+
137
+ /** How a number row's value shows: its `display`, by default every decimal it has, with its word for infinity. */
138
+ declare function menuNumberDisplay(e: Pick<MenuNumberEntry, 'display' | 'infinity'>): Display;
139
+ /** A number row's value as a renderer with no field of its own prints it: in {@link menuNumberDisplay}, then its
140
+ * unit, which an infinity goes without. A dash for none. */
141
+ declare function menuNumberText(value: number | undefined, e?: Pick<MenuNumberEntry, 'display' | 'infinity' | 'suffix'>): string;
142
+
143
+ /** Whether a step up (`dir` 1) or down (-1) from `value` moves a number row anywhere. */
144
+ declare function menuNumberCanStep(e: MenuNumberEntry, value: number, dir: 1 | -1): boolean;
71
145
 
72
146
  /** The highlighted index at each open level; its length is how many are open. */
73
147
  type MenuStack = readonly (number | null)[];
@@ -80,6 +154,14 @@ type KeyEffect = {
80
154
  index: number;
81
155
  } | {
82
156
  kind: 'close';
157
+ } | {
158
+ kind: 'edge';
159
+ dir: -1 | 1;
160
+ } | {
161
+ kind: 'step';
162
+ level: number;
163
+ index: number;
164
+ dir: -1 | 1;
83
165
  };
84
166
  /** Index of the first row, or null for a level with none. */
85
167
  declare function firstRow(entries: readonly MenuEntry[]): number | null;
@@ -117,6 +199,8 @@ interface MenuControllerOptions {
117
199
  highlightFirst?: boolean;
118
200
  /** The menu is done: an item was chosen, or it was dismissed. */
119
201
  onClose(): void;
202
+ /** An arrow key ran off the menu: left from the first level (-1), or right from a row with no submenu (1). */
203
+ onEdge?(dir: -1 | 1): void;
120
204
  scheduler?: MenuScheduler;
121
205
  }
122
206
  /** One open menu's behavior, independent of what draws it. */
@@ -130,6 +214,12 @@ interface MenuController {
130
214
  press(level: number, index: number): void;
131
215
  /** Where `level`'s panel is, for the diagonal allowance. */
132
216
  setPanel(level: number, rect: Rect): void;
217
+ /** What a number row holds now: the last value set while the menu is open, else the one it opened with. */
218
+ numberValue(e: MenuNumberEntry): number | undefined;
219
+ /** Write a number row, held within its bounds. The menu stays open; subscribers hear it. */
220
+ setNumber(e: MenuNumberEntry, value: number): void;
221
+ /** Move a number row by a spin-button key: an arrow, Page Up or Down, Home or End. False for any other key. */
222
+ spin(e: MenuNumberEntry, key: string): boolean;
133
223
  subscribe(fn: () => void): () => void;
134
224
  dispose(): void;
135
225
  }
@@ -139,4 +229,4 @@ declare function createMenuController(opts: MenuControllerOptions): MenuControll
139
229
  /** Whether `p` lies in triangle `abc`, edges included. */
140
230
  declare function inTriangle(p: Point, a: Point, b: Point, c: Point): boolean;
141
231
 
142
- export { DIAGONAL_GRACE_MS, type KeyEffect, type MenuController, type MenuControllerOptions, type MenuEntry, type MenuItemEntry, type MenuScheduler, type MenuSeparatorEntry, type MenuShortcut, type MenuStack, type Point, type Rect, SUBMENU_OPEN_MS, type Size, type SubmenuEntry, TYPEAHEAD_RESET_MS, createMenuController, entriesAt, firstRow, inTriangle, isRow, keyStep, placeMenu, placeSubmenu, timerScheduler, typeaheadMatch };
232
+ export { DIAGONAL_GRACE_MS, type KeyEffect, type MenuColorEntry, type MenuController, type MenuControllerOptions, type MenuEntry, type MenuItemEntry, type MenuNumberEntry, type MenuRowEntry, type MenuScheduler, type MenuSeparatorEntry, type MenuShortcut, type MenuStack, type Point, type Rect, SUBMENU_OPEN_MS, type Size, type SubmenuEntry, TYPEAHEAD_RESET_MS, accessLetter, accessMatch, createMenuController, entriesAt, firstRow, inTriangle, isRow, keyStep, menuNumberCanStep, menuNumberDisplay, menuNumberText, placeMenu, placeMenuUnder, placeSubmenu, timerScheduler, typeaheadMatch, withLinks };
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { decimal, endless, qty, spinKey, clampToBounds } from '@weasel-js/quantity';
2
+
1
3
  // src/entries.ts
2
4
  function isRow(e) {
3
5
  return e.kind !== "separator";
@@ -14,24 +16,84 @@ function entriesAt(root, stack, level) {
14
16
  }
15
17
 
16
18
  // src/placement.ts
17
- function flipOrShift(at, len, lo, span) {
19
+ function flipOrShift(at, len, lo, span, flipAt = at) {
18
20
  const hi = lo + span;
19
21
  if (at + len <= hi) return Math.max(lo, at);
20
- if (at - len >= lo) return Math.max(lo, Math.min(at - len, hi - len));
22
+ if (flipAt - len >= lo) return Math.max(lo, Math.min(flipAt - len, hi - len));
21
23
  return Math.max(lo, hi - len);
22
24
  }
23
- function placeMenu(size, anchor, bounds) {
25
+ function placeMenu(size, anchor, bounds2) {
24
26
  return {
25
- x: flipOrShift(anchor.x, size.w, bounds.x, bounds.w),
26
- y: flipOrShift(anchor.y, size.h, bounds.y, bounds.h)
27
+ x: flipOrShift(anchor.x, size.w, bounds2.x, bounds2.w),
28
+ y: flipOrShift(anchor.y, size.h, bounds2.y, bounds2.h)
27
29
  };
28
30
  }
29
- function placeSubmenu(size, row, bounds) {
30
- const right = row.x + row.w;
31
- const hiX = bounds.x + bounds.w;
32
- const x = right + size.w <= hiX ? right : row.x - size.w >= bounds.x ? row.x - size.w : Math.max(bounds.x, hiX - size.w);
33
- const y = Math.max(bounds.y, Math.min(row.y, bounds.y + bounds.h - size.h));
34
- return { x, y };
31
+ function placeSubmenu(size, row, bounds2) {
32
+ return {
33
+ x: flipOrShift(row.x + row.w, size.w, bounds2.x, bounds2.w, row.x),
34
+ y: Math.max(bounds2.y, Math.min(row.y, bounds2.y + bounds2.h - size.h))
35
+ };
36
+ }
37
+ function placeMenuUnder(size, under, bounds2) {
38
+ return {
39
+ x: flipOrShift(under.x, size.w, bounds2.x, bounds2.w, under.x + under.w),
40
+ y: flipOrShift(under.y + under.h, size.h, bounds2.y, bounds2.h, under.y)
41
+ };
42
+ }
43
+
44
+ // src/access.ts
45
+ function accessLetter(access) {
46
+ return access ? [...access][0].toLowerCase() : void 0;
47
+ }
48
+ function accessMatch(entries, key) {
49
+ const k = key.toLowerCase();
50
+ const i = entries.findIndex((e) => isRow(e) && accessLetter(e.access) === k);
51
+ return i < 0 ? null : i;
52
+ }
53
+
54
+ // src/links.ts
55
+ function withLinks(entries, navigate) {
56
+ return entries.map((e) => {
57
+ if (e.kind === "submenu") return { ...e, entries: withLinks(e.entries, navigate) };
58
+ if (e.kind !== "item" || e.href === void 0) return e;
59
+ const { href } = e;
60
+ return { ...e, run: () => {
61
+ e.run();
62
+ navigate(href);
63
+ } };
64
+ });
65
+ }
66
+ var FULL_PRECISION = decimal({ maxPlaces: 20, grouping: false });
67
+ function menuNumberDisplay(e) {
68
+ const display = e.display ?? FULL_PRECISION;
69
+ return e.infinity === void 0 ? display : endless(display, e.infinity);
70
+ }
71
+ function menuNumberText(value, e = {}) {
72
+ if (value === void 0) return "\u2014";
73
+ const text = qty(value, menuNumberDisplay(e)).text;
74
+ return Number.isFinite(value) ? `${text}${e.suffix ?? ""}` : text;
75
+ }
76
+ var bounds = (e) => ({ step: e.step ?? 1, min: e.min, max: e.max, endless: e.endless });
77
+ function menuNumberCanStep(e, value, dir) {
78
+ return spinKey(dir > 0 ? "ArrowUp" : "ArrowDown", value, bounds(e)) !== value;
79
+ }
80
+ function createNumberValues(changed, closed) {
81
+ const held = /* @__PURE__ */ new Map();
82
+ const numberValue = (e) => held.has(e) ? held.get(e) : e.value;
83
+ const setNumber = (e, value) => {
84
+ const next = clampToBounds(value, bounds(e));
85
+ if (closed() || e.disabled || Number.isNaN(next) || next === numberValue(e)) return;
86
+ held.set(e, next);
87
+ e.set(next);
88
+ changed();
89
+ };
90
+ const spin = (e, key) => {
91
+ const next = spinKey(key, numberValue(e) ?? e.min ?? 0, bounds(e));
92
+ if (next === null) return false;
93
+ setNumber(e, next);
94
+ return true;
95
+ };
96
+ return { numberValue, setNumber, spin };
35
97
  }
36
98
 
37
99
  // src/keys.ts
@@ -73,10 +135,12 @@ function keyStep(root, stack, key) {
73
135
  return done(withTop(stack, step(rows, null, 1)));
74
136
  case "End":
75
137
  return done(withTop(stack, step(rows, null, -1)));
76
- case "ArrowRight":
77
- return done(openSub(root, stack) ?? stack);
138
+ case "ArrowRight": {
139
+ const opened = openSub(root, stack);
140
+ return opened ? done(opened) : done(stack, { kind: "edge", dir: 1 });
141
+ }
78
142
  case "ArrowLeft":
79
- return done(level > 0 ? stack.slice(0, -1) : stack);
143
+ return level > 0 ? done(stack.slice(0, -1)) : done(stack, { kind: "edge", dir: -1 });
80
144
  case "Escape":
81
145
  return level > 0 ? done(stack.slice(0, -1)) : done(stack, { kind: "close" });
82
146
  case "Tab":
@@ -88,6 +152,13 @@ function keyStep(root, stack, key) {
88
152
  if (e?.kind === "item" && !e.disabled) return done(stack, { kind: "choose", level, index: at });
89
153
  return done(stack);
90
154
  }
155
+ case "+":
156
+ case "=":
157
+ case "-": {
158
+ const e = at == null ? void 0 : rows[at];
159
+ if (e?.kind !== "number" || e.disabled) return { stack, effect: NONE, handled: false };
160
+ return done(stack, { kind: "step", level, index: at, dir: key === "-" ? -1 : 1 });
161
+ }
91
162
  default:
92
163
  return { stack, effect: NONE, handled: false };
93
164
  }
@@ -114,6 +185,30 @@ function typeaheadStep(entries, from, buffer, key, now) {
114
185
  return { buffer: { text, at: now }, hit: typeaheadMatch(entries, from, text) };
115
186
  }
116
187
 
188
+ // src/keyPlan.ts
189
+ var EMPTY_TYPEAHEAD = { text: "", at: -Infinity };
190
+ function planKey(root, stack, typed, key, now) {
191
+ const r = keyStep(root, stack, key);
192
+ if (r.handled) {
193
+ return { stack: r.stack, typed: r.stack !== stack ? EMPTY_TYPEAHEAD : typed, effect: r.effect };
194
+ }
195
+ if (key.length !== 1 || key === " ") return null;
196
+ const level = stack.length - 1;
197
+ const rows = entriesAt(root, stack, level);
198
+ const letter = accessMatch(rows, key);
199
+ if (letter !== null) {
200
+ const e = rows[letter];
201
+ if (e?.kind === "item" && !e.disabled) {
202
+ return { stack, typed: EMPTY_TYPEAHEAD, effect: { kind: "choose", level, index: letter } };
203
+ }
204
+ const opened = e?.kind === "submenu" && !e.disabled ? [...stack.slice(0, level), letter, firstRow(e.entries)] : stack;
205
+ return { stack: opened, typed: EMPTY_TYPEAHEAD, effect: { kind: "none" } };
206
+ }
207
+ const t = typeaheadStep(rows, stack[level] ?? null, typed, key, now);
208
+ const next = t.hit === null ? stack : [...stack.slice(0, level), t.hit];
209
+ return { stack: next, typed: t.buffer, effect: { kind: "none" } };
210
+ }
211
+
117
212
  // src/triangle.ts
118
213
  var cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
119
214
  function inTriangle(p, a, b, c) {
@@ -143,12 +238,16 @@ function createMenuController(opts) {
143
238
  let pendingKind = null;
144
239
  let lastPoint = null;
145
240
  let deferred = null;
146
- let typed = { text: "", at: -Infinity };
241
+ let typed = EMPTY_TYPEAHEAD;
147
242
  let closed = false;
243
+ const notify = () => {
244
+ for (const fn of listeners) fn();
245
+ };
246
+ const numbers = createNumberValues(notify, () => closed);
148
247
  const set = (next) => {
149
248
  stack = next;
150
249
  for (const level of [...panels.keys()]) if (level >= next.length) panels.delete(level);
151
- for (const fn of listeners) fn();
250
+ notify();
152
251
  };
153
252
  const cancel = () => {
154
253
  pending?.();
@@ -206,23 +305,19 @@ function createMenuController(opts) {
206
305
  entriesAt: (level) => entriesAt(root, stack, level),
207
306
  key(key) {
208
307
  if (closed) return false;
209
- const r = keyStep(root, stack, key);
210
- if (r.handled) {
211
- cancel();
212
- if (r.stack !== stack) {
213
- typed = { text: "", at: -Infinity };
214
- set(r.stack);
215
- }
216
- if (r.effect.kind === "choose") choose(r.effect.level, r.effect.index);
217
- else if (r.effect.kind === "close") finish();
218
- return true;
219
- }
220
- if (key.length !== 1 || key === " ") return false;
308
+ const plan = planKey(root, stack, typed, key, sched.now());
309
+ if (!plan) return false;
221
310
  cancel();
222
- const level = stack.length - 1;
223
- const t = typeaheadStep(entriesAt(root, stack, level), stack[level] ?? null, typed, key, sched.now());
224
- typed = t.buffer;
225
- if (t.hit !== null) set([...stack.slice(0, level), t.hit]);
311
+ typed = plan.typed;
312
+ if (plan.stack !== stack) set(plan.stack);
313
+ const { effect } = plan;
314
+ if (effect.kind === "choose") choose(effect.level, effect.index);
315
+ else if (effect.kind === "close") finish();
316
+ else if (effect.kind === "edge") opts.onEdge?.(effect.dir);
317
+ else if (effect.kind === "step") {
318
+ const e = entryAt(effect.level, effect.index);
319
+ if (e?.kind === "number") numbers.spin(e, effect.dir > 0 ? "ArrowUp" : "ArrowDown");
320
+ }
226
321
  return true;
227
322
  },
228
323
  hover(level, index, point) {
@@ -256,6 +351,7 @@ function createMenuController(opts) {
256
351
  setPanel(level, rect) {
257
352
  panels.set(level, rect);
258
353
  },
354
+ ...numbers,
259
355
  subscribe(fn) {
260
356
  listeners.add(fn);
261
357
  return () => {
@@ -270,6 +366,6 @@ function createMenuController(opts) {
270
366
  };
271
367
  }
272
368
 
273
- export { DIAGONAL_GRACE_MS, SUBMENU_OPEN_MS, TYPEAHEAD_RESET_MS, createMenuController, entriesAt, firstRow, inTriangle, isRow, keyStep, placeMenu, placeSubmenu, timerScheduler, typeaheadMatch };
369
+ export { DIAGONAL_GRACE_MS, SUBMENU_OPEN_MS, TYPEAHEAD_RESET_MS, accessLetter, accessMatch, createMenuController, entriesAt, firstRow, inTriangle, isRow, keyStep, menuNumberCanStep, menuNumberDisplay, menuNumberText, placeMenu, placeMenuUnder, placeSubmenu, timerScheduler, typeaheadMatch, withLinks };
274
370
  //# sourceMappingURL=index.js.map
275
371
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/entries.ts","../src/placement.ts","../src/keys.ts","../src/typeahead.ts","../src/triangle.ts","../src/controller.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** A key chip: the key, and which modifiers are held with it. `@weasel-js/routing`\n * derives these from action bindings as `ActionShortcut`. */\nexport interface MenuShortcut {\n key: string;\n mod: boolean;\n alt: boolean;\n shift: boolean;\n}\n\n/** A row that runs something when chosen. */\nexport interface MenuItemEntry {\n kind: 'item';\n key: string;\n label: string;\n /** Opaque to the model; each renderer knows what it can draw. */\n icon?: unknown;\n shortcut?: MenuShortcut;\n disabled?: boolean;\n /** `undefined` for a row with no on/off state. */\n checked?: boolean;\n run(): void;\n}\n\n/** A rule between groups of rows. Never highlighted. */\nexport interface MenuSeparatorEntry {\n kind: 'separator';\n key: string;\n}\n\n/** A row that opens another level. */\nexport interface SubmenuEntry {\n kind: 'submenu';\n key: string;\n label: string;\n icon?: unknown;\n disabled?: boolean;\n entries: readonly MenuEntry[];\n}\n\n/** One line of a menu. */\nexport type MenuEntry = MenuItemEntry | MenuSeparatorEntry | SubmenuEntry;\n\n/** True for an entry the highlight can land on. */\nexport function isRow(e: MenuEntry): e is MenuItemEntry | SubmenuEntry {\n return e.kind !== 'separator';\n}\n\n/**\n * The entries shown at `level`, following `stack` (the highlighted index at\n * each open level) down from `root`. Empty when the path does not lead to a\n * submenu.\n */\nexport function entriesAt(\n root: readonly MenuEntry[],\n stack: readonly (number | null)[],\n level: number,\n): readonly MenuEntry[] {\n let entries = root;\n for (let i = 0; i < level; i++) {\n const at = stack[i];\n const e = at == null ? undefined : entries[at];\n if (e?.kind !== 'submenu') return [];\n entries = e.entries;\n }\n return entries;\n}\n","/** A position in viewport pixels (CSS pixels, as `getBoundingClientRect` reports). */\nexport interface Point { x: number; y: number }\n/** A width and height in pixels. */\nexport interface Size { w: number; h: number }\n/** A rectangle in viewport pixels: top-left corner plus width and height. */\nexport interface Rect { x: number; y: number; w: number; h: number }\n\n/** Start at `at`, else end there (flip), else slide inside `[lo, lo + span]`. */\nfunction flipOrShift(at: number, len: number, lo: number, span: number): number {\n const hi = lo + span;\n if (at + len <= hi) return Math.max(lo, at);\n if (at - len >= lo) return Math.max(lo, Math.min(at - len, hi - len));\n return Math.max(lo, hi - len);\n}\n\n/** Where a menu of `size` opened at `anchor` goes: down and right of the\n * anchor, flipped on an axis that does not fit, shifted inside `bounds`\n * when neither side does. */\nexport function placeMenu(size: Size, anchor: Point, bounds: Rect): Point {\n return {\n x: flipOrShift(anchor.x, size.w, bounds.x, bounds.w),\n y: flipOrShift(anchor.y, size.h, bounds.y, bounds.h),\n };\n}\n\n/** Where a submenu goes: beside `row`, right first, left if that does not\n * fit, top-aligned with the row and shifted up to stay inside `bounds`. */\nexport function placeSubmenu(size: Size, row: Rect, bounds: Rect): Point {\n const right = row.x + row.w;\n const hiX = bounds.x + bounds.w;\n const x = right + size.w <= hiX\n ? right\n : row.x - size.w >= bounds.x ? row.x - size.w : Math.max(bounds.x, hiX - size.w);\n const y = Math.max(bounds.y, Math.min(row.y, bounds.y + bounds.h - size.h));\n return { x, y };\n}\n","import { entriesAt, isRow, type MenuEntry } from './entries';\n\n/** The highlighted index at each open level; its length is how many are open. */\nexport type MenuStack = readonly (number | null)[];\n\n/** What a key asks the owner of the menu to do beyond moving the highlight. */\nexport type KeyEffect =\n | { kind: 'none' }\n | { kind: 'choose'; level: number; index: number }\n | { kind: 'close' };\n\nconst NONE: KeyEffect = { kind: 'none' };\n\n/** Index of the first row, or null for a level with none. */\nexport function firstRow(entries: readonly MenuEntry[]): number | null {\n const i = entries.findIndex(isRow);\n return i < 0 ? null : i;\n}\n\nfunction step(entries: readonly MenuEntry[], from: number | null, dir: 1 | -1): number | null {\n const n = entries.length;\n let i = from ?? (dir === 1 ? -1 : n);\n for (let k = 0; k < n; k++) {\n i = (i + dir + n) % n;\n if (isRow(entries[i]!)) return i;\n }\n return null;\n}\n\nfunction withTop(stack: MenuStack, index: number | null): MenuStack {\n return [...stack.slice(0, -1), index];\n}\n\nfunction openSub(root: readonly MenuEntry[], stack: MenuStack): MenuStack | null {\n const level = stack.length - 1;\n const at = stack[level];\n const e = at == null ? undefined : entriesAt(root, stack, level)[at];\n if (e?.kind !== 'submenu' || e.disabled) return null;\n return [...stack, firstRow(e.entries)];\n}\n\n/** Apply one key to the menu: the new stack, any effect, and whether the key\n * was the menu's (an unhandled key goes on to typeahead). */\nexport function keyStep(\n root: readonly MenuEntry[],\n stack: MenuStack,\n key: string,\n): { stack: MenuStack; effect: KeyEffect; handled: boolean } {\n const level = stack.length - 1;\n const rows = entriesAt(root, stack, level);\n const at = stack[level] ?? null;\n const done = (s: MenuStack, effect: KeyEffect = NONE) => ({ stack: s, effect, handled: true });\n switch (key) {\n case 'ArrowDown': return done(withTop(stack, step(rows, at, 1)));\n case 'ArrowUp': return done(withTop(stack, step(rows, at, -1)));\n case 'Home': return done(withTop(stack, step(rows, null, 1)));\n case 'End': return done(withTop(stack, step(rows, null, -1)));\n case 'ArrowRight': return done(openSub(root, stack) ?? stack);\n case 'ArrowLeft': return done(level > 0 ? stack.slice(0, -1) : stack);\n case 'Escape': return level > 0 ? done(stack.slice(0, -1)) : done(stack, { kind: 'close' });\n case 'Tab': return done(stack, { kind: 'close' });\n case 'Enter':\n case ' ': {\n const e = at == null ? undefined : rows[at];\n if (e?.kind === 'submenu') return done(openSub(root, stack) ?? stack);\n if (e?.kind === 'item' && !e.disabled) return done(stack, { kind: 'choose', level, index: at! });\n return done(stack);\n }\n default: return { stack, effect: NONE, handled: false };\n }\n}\n","import { isRow, type MenuEntry } from './entries';\n\n/** The row a typed `query` lands on, searching forward from `from` with\n * wrap. A one-letter query starts after the highlight so repeating a letter\n * cycles; a longer one starts at it so typing on keeps the match. */\nexport function typeaheadMatch(\n entries: readonly MenuEntry[],\n from: number | null,\n query: string,\n): number | null {\n const q = query.toLowerCase();\n const n = entries.length;\n const start = from == null ? 0 : q.length === 1 ? from + 1 : from;\n for (let k = 0; k < n; k++) {\n const i = (start + k) % n;\n const e = entries[i]!;\n if (isRow(e) && e.label.toLowerCase().startsWith(q)) return i;\n }\n return null;\n}\n\n/** How long typing can pause before the typeahead buffer starts over, in milliseconds. */\nexport const TYPEAHEAD_RESET_MS = 500;\n\nexport interface TypeaheadBuffer {\n text: string;\n at: number;\n}\n\n/** One typed character: the new buffer and the row it lands on. A key that\n * repeats the whole buffer searches that one letter, so it cycles. */\nexport function typeaheadStep(\n entries: readonly MenuEntry[],\n from: number | null,\n buffer: TypeaheadBuffer,\n key: string,\n now: number,\n): { buffer: TypeaheadBuffer; hit: number | null } {\n const prev = now - buffer.at < TYPEAHEAD_RESET_MS ? buffer.text : '';\n const k = key.toLowerCase();\n const cycling = prev !== '' && [...prev].every((c) => c.toLowerCase() === k);\n const text = cycling ? key : prev + key;\n return { buffer: { text, at: now }, hit: typeaheadMatch(entries, from, text) };\n}\n","import type { Point } from './placement';\n\nconst cross = (o: Point, a: Point, b: Point) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);\n\n/** Whether `p` lies in triangle `abc`, edges included. */\nexport function inTriangle(p: Point, a: Point, b: Point, c: Point): boolean {\n const d1 = cross(p, a, b), d2 = cross(p, b, c), d3 = cross(p, c, a);\n const neg = d1 < 0 || d2 < 0 || d3 < 0;\n const pos = d1 > 0 || d2 > 0 || d3 > 0;\n return !(neg && pos);\n}\n","import { entriesAt, isRow, type MenuEntry } from './entries';\nimport { firstRow, keyStep, type MenuStack } from './keys';\nimport type { Point, Rect } from './placement';\nimport { inTriangle } from './triangle';\nimport { typeaheadStep, type TypeaheadBuffer } from './typeahead';\n\nexport { TYPEAHEAD_RESET_MS } from './typeahead';\n/** How long the pointer rests on a submenu row before its panel opens, in milliseconds. */\nexport const SUBMENU_OPEN_MS = 200;\n/** How long a move toward an open submenu keeps it open while the pointer crosses other rows, in milliseconds. */\nexport const DIAGONAL_GRACE_MS = 300;\n\n/** Clock and timer, injected so tests and headless hosts control time. */\nexport interface MenuScheduler {\n now(): number;\n /** Run `fn` after `ms`; returns a cancel. */\n set(fn: () => void, ms: number): () => void;\n}\n\n/** The real-time scheduler: `Date.now` and `setTimeout`. */\nexport const timerScheduler: MenuScheduler = {\n now: () => Date.now(),\n set: (fn, ms) => {\n const t = setTimeout(fn, ms);\n return () => clearTimeout(t);\n },\n};\n\n/** What {@link createMenuController} needs: the entries, how to close, and optionally a clock. */\nexport interface MenuControllerOptions {\n entries: readonly MenuEntry[];\n /** Highlight the first row at open, as a keyboard open does. */\n highlightFirst?: boolean;\n /** The menu is done: an item was chosen, or it was dismissed. */\n onClose(): void;\n scheduler?: MenuScheduler;\n}\n\n/** One open menu's behavior, independent of what draws it. */\nexport interface MenuController {\n stack(): MenuStack;\n entriesAt(level: number): readonly MenuEntry[];\n /** Returns whether the key was the menu's. */\n key(key: string): boolean;\n /** The pointer is over row `index` of `level`, or over none of its rows. */\n hover(level: number, index: number | null, point?: Point): void;\n press(level: number, index: number): void;\n /** Where `level`'s panel is, for the diagonal allowance. */\n setPanel(level: number, rect: Rect): void;\n subscribe(fn: () => void): () => void;\n dispose(): void;\n}\n\n/** Creates the controller that runs one open menu: highlight, keys, submenus and typeahead. */\nexport function createMenuController(opts: MenuControllerOptions): MenuController {\n const { entries: root, onClose } = opts;\n const sched = opts.scheduler ?? timerScheduler;\n let stack: MenuStack = [opts.highlightFirst ? firstRow(root) : null];\n const listeners = new Set<() => void>();\n const panels = new Map<number, Rect>();\n let pending: (() => void) | null = null;\n let pendingKind: 'open' | 'diagonal' | null = null;\n let lastPoint: Point | null = null;\n let deferred: { level: number; index: number | null } | null = null;\n let typed: TypeaheadBuffer = { text: '', at: -Infinity };\n let closed = false;\n\n const set = (next: MenuStack) => {\n stack = next;\n for (const level of [...panels.keys()]) if (level >= next.length) panels.delete(level);\n for (const fn of listeners) fn();\n };\n const cancel = () => { pending?.(); pending = null; pendingKind = null; };\n const later = (kind: 'open' | 'diagonal', fn: () => void, ms: number) => {\n cancel();\n pendingKind = kind;\n pending = sched.set(() => { pending = null; pendingKind = null; if (!closed) fn(); }, ms);\n };\n const entryAt = (level: number, index: number) => entriesAt(root, stack, level)[index];\n const isRowAt = (level: number, index: number) => {\n const e = entryAt(level, index);\n return e !== undefined && isRow(e);\n };\n\n const finish = () => { closed = true; cancel(); onClose(); };\n const choose = (level: number, index: number) => {\n const e = entryAt(level, index);\n if (e?.kind !== 'item' || e.disabled) return;\n finish();\n e.run();\n };\n\n const openAt = (level: number, index: number) => {\n const e = entryAt(level, index);\n const base = [...stack.slice(0, level), index];\n set(e?.kind === 'submenu' && !e.disabled ? [...base, null] : base);\n };\n\n /** Heading from the point the deferral began toward the open child's near edge. */\n const headingIntoChild = (level: number, p: Point): boolean => {\n const child = panels.get(level + 1);\n const parent = panels.get(level);\n if (!child || !parent || !lastPoint || stack.length <= level + 1) return false;\n const edge = child.x >= parent.x ? child.x : child.x + child.w;\n return inTriangle(p, lastPoint, { x: edge, y: child.y }, { x: edge, y: child.y + child.h });\n };\n\n const hoverNow = (level: number, index: number | null) => {\n cancel();\n if (index === null && stack.length !== level + 1) return;\n const next = [...stack.slice(0, level), index];\n if (next.length !== stack.length || next[level] !== stack[level]) set(next);\n if (index === null) return;\n const e = entryAt(level, index);\n if (e?.kind === 'submenu' && !e.disabled) later('open', () => openAt(level, index), SUBMENU_OPEN_MS);\n };\n\n return {\n stack: () => stack,\n entriesAt: (level) => entriesAt(root, stack, level),\n key(key) {\n if (closed) return false;\n const r = keyStep(root, stack, key);\n if (r.handled) {\n cancel();\n if (r.stack !== stack) { typed = { text: '', at: -Infinity }; set(r.stack); }\n if (r.effect.kind === 'choose') choose(r.effect.level, r.effect.index);\n else if (r.effect.kind === 'close') finish();\n return true;\n }\n if (key.length !== 1 || key === ' ') return false;\n cancel();\n const level = stack.length - 1;\n const t = typeaheadStep(entriesAt(root, stack, level), stack[level] ?? null, typed, key, sched.now());\n typed = t.buffer;\n if (t.hit !== null) set([...stack.slice(0, level), t.hit]);\n return true;\n },\n hover(level, index, point) {\n if (closed || level >= stack.length) return;\n if (index !== null && !isRowAt(level, index)) return;\n const sameRow = index === stack[level];\n if (point && !sameRow && headingIntoChild(level, point)) {\n deferred = { level, index };\n if (pendingKind !== 'diagonal') {\n later('diagonal', () => { if (deferred) hoverNow(deferred.level, deferred.index); }, DIAGONAL_GRACE_MS);\n }\n return;\n }\n if (point) lastPoint = point;\n if (sameRow && stack.length > level + 1) { cancel(); return; }\n if (sameRow && pendingKind === 'open') return;\n hoverNow(level, index);\n },\n press(level, index) {\n if (closed || level >= stack.length || !isRowAt(level, index)) return;\n if (stack[level] === index && stack.length > level + 1) return;\n cancel();\n if (entryAt(level, index)?.kind === 'item') choose(level, index);\n else openAt(level, index);\n },\n setPanel(level, rect) { panels.set(level, rect); },\n subscribe(fn) {\n listeners.add(fn);\n return () => { listeners.delete(fn); };\n },\n dispose() {\n closed = true;\n cancel();\n listeners.clear();\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/entries.ts","../src/placement.ts","../src/access.ts","../src/links.ts","../src/numberText.ts","../src/numberValues.ts","../src/keys.ts","../src/typeahead.ts","../src/keyPlan.ts","../src/triangle.ts","../src/controller.ts"],"names":["bounds"],"mappings":"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","file":"index.js","sourcesContent":["import type { Display, Endless, InfinityText, UnitTable } from '@weasel-js/quantity';\n\n/** A key chip: the key, and which modifiers are held with it. `@weasel-js/routing`\n * derives these from action bindings as `ActionShortcut`; `@weasel-js/gestures`\n * formats them as `ShortcutInput`. A tool's `keybinding` has this shape too. */\nexport interface MenuShortcut {\n /** A list names spellings of one keycap (`['[', '{']`); the first is drawn. */\n key: string | readonly string[];\n mod?: boolean;\n alt?: boolean;\n /** `'optional'` matches held or not, and draws as an optional key. */\n shift?: boolean | 'optional';\n}\n\n/** A row that runs something when chosen. */\nexport interface MenuItemEntry {\n kind: 'item';\n key: string;\n label: string;\n /** Opaque to the model; each renderer knows what it can draw. */\n icon?: unknown;\n shortcut?: MenuShortcut;\n disabled?: boolean;\n /** `undefined` for a row with no on/off state. */\n checked?: boolean;\n /** Letter that chooses this row from the keyboard, compared ignoring case. */\n access?: string;\n /** Where choosing the row goes; see `withLinks`. */\n href?: string;\n /** The row for the page that is open. */\n current?: boolean;\n run(): void;\n}\n\n/** A rule between groups of rows. Never highlighted. */\nexport interface MenuSeparatorEntry {\n kind: 'separator';\n key: string;\n}\n\n/** A row that opens another level. */\nexport interface SubmenuEntry {\n kind: 'submenu';\n key: string;\n label: string;\n icon?: unknown;\n disabled?: boolean;\n /** Letter that opens this row from the keyboard, compared ignoring case. */\n access?: string;\n entries: readonly MenuEntry[];\n}\n\n/** A row holding a number that is edited in place: the menu stays open while it changes. All of it is in the\n * units shown. Its controller's `numberValue` is what it holds now. */\nexport interface MenuNumberEntry {\n kind: 'number';\n key: string;\n label: string;\n icon?: unknown;\n disabled?: boolean;\n access?: string;\n /** What it held when the menu opened. `undefined` for none. */\n value: number | undefined;\n min?: number;\n max?: number;\n /** What one step adds. Default 1. */\n step?: number;\n /** Text after the value: `'°'`, `'px'`. */\n suffix?: string;\n /** How the value shows, and reads back where it is typed. Default: every decimal it has. */\n display?: Display;\n /** The ends that stand for infinity, one step past `min` or `max`. */\n endless?: Endless;\n /** The word infinity shows as. Default `∞`. */\n infinity?: InfinityText;\n /** Units a renderer with a field reads typed, each mapped to the factor that turns a number in it into the\n * unit shown: `{ mm: 0.1, cm: 1 }`. */\n accepts?: Readonly<UnitTable>;\n set(value: number): void;\n}\n\n/** A row showing a color. Nothing edits it from the menu. */\nexport interface MenuColorEntry {\n kind: 'color';\n key: string;\n label: string;\n disabled?: boolean;\n access?: string;\n /** A CSS color, `#rrggbb` or `#rrggbbaa`. `undefined` for none. */\n value: string | undefined;\n}\n\n/** One line of a menu. */\nexport type MenuEntry = MenuItemEntry | MenuSeparatorEntry | SubmenuEntry | MenuNumberEntry | MenuColorEntry;\n\n/** An entry the highlight can land on. */\nexport type MenuRowEntry = Exclude<MenuEntry, MenuSeparatorEntry>;\n\n/** True for an entry the highlight can land on. */\nexport function isRow(e: MenuEntry): e is MenuRowEntry {\n return e.kind !== 'separator';\n}\n\n/**\n * The entries shown at `level`, following `stack` (the highlighted index at\n * each open level) down from `root`. Empty when the path does not lead to a\n * submenu.\n */\nexport function entriesAt(\n root: readonly MenuEntry[],\n stack: readonly (number | null)[],\n level: number,\n): readonly MenuEntry[] {\n let entries = root;\n for (let i = 0; i < level; i++) {\n const at = stack[i];\n const e = at == null ? undefined : entries[at];\n if (e?.kind !== 'submenu') return [];\n entries = e.entries;\n }\n return entries;\n}\n","/** A position in viewport pixels (CSS pixels, as `getBoundingClientRect` reports). */\nexport interface Point { x: number; y: number }\n/** A width and height in pixels. */\nexport interface Size { w: number; h: number }\n/** A rectangle in viewport pixels: top-left corner plus width and height. */\nexport interface Rect { x: number; y: number; w: number; h: number }\n\n/** Start at `at`, else end at `flipAt` (default `at`), else slide inside `[lo, lo + span]`. */\nfunction flipOrShift(at: number, len: number, lo: number, span: number, flipAt = at): number {\n const hi = lo + span;\n if (at + len <= hi) return Math.max(lo, at);\n if (flipAt - len >= lo) return Math.max(lo, Math.min(flipAt - len, hi - len));\n return Math.max(lo, hi - len);\n}\n\n/** Where a menu of `size` opened at `anchor` goes: down and right of the\n * anchor, flipped on an axis that does not fit, shifted inside `bounds`\n * when neither side does. */\nexport function placeMenu(size: Size, anchor: Point, bounds: Rect): Point {\n return {\n x: flipOrShift(anchor.x, size.w, bounds.x, bounds.w),\n y: flipOrShift(anchor.y, size.h, bounds.y, bounds.h),\n };\n}\n\n/** Where a submenu goes: beside `row`, right first, left if that does not\n * fit, top-aligned with the row and shifted up to stay inside `bounds`. */\nexport function placeSubmenu(size: Size, row: Rect, bounds: Rect): Point {\n return {\n x: flipOrShift(row.x + row.w, size.w, bounds.x, bounds.w, row.x),\n y: Math.max(bounds.y, Math.min(row.y, bounds.y + bounds.h - size.h)),\n };\n}\n\n/** Where a menu opened from the button at `under` goes: below it and\n * left-aligned, flipped above it or right-aligned to it on an axis that does\n * not fit, shifted inside `bounds` when neither side does. */\nexport function placeMenuUnder(size: Size, under: Rect, bounds: Rect): Point {\n return {\n x: flipOrShift(under.x, size.w, bounds.x, bounds.w, under.x + under.w),\n y: flipOrShift(under.y + under.h, size.h, bounds.y, bounds.h, under.y),\n };\n}\n","import { isRow, type MenuEntry } from './entries';\n\n/** The one character a declared access letter stands for: its first, ignoring case. Undefined for none. */\nexport function accessLetter(access: string | undefined): string | undefined {\n return access ? [...access][0]!.toLowerCase() : undefined;\n}\n\n/** Index of the first row declaring `key` as its access letter, ignoring case; disabled rows included. */\nexport function accessMatch(entries: readonly MenuEntry[], key: string): number | null {\n const k = key.toLowerCase();\n const i = entries.findIndex((e) => isRow(e) && accessLetter(e.access) === k);\n return i < 0 ? null : i;\n}\n","import type { MenuEntry } from './entries';\n\n/** Entries where an item with `href` runs its own `run`, then `navigate(href)`. Recurses into submenus. */\nexport function withLinks(entries: readonly MenuEntry[], navigate: (href: string) => void): MenuEntry[] {\n return entries.map((e): MenuEntry => {\n if (e.kind === 'submenu') return { ...e, entries: withLinks(e.entries, navigate) };\n if (e.kind !== 'item' || e.href === undefined) return e;\n const { href } = e;\n return { ...e, run: () => { e.run(); navigate(href); } };\n });\n}\n","import { decimal, endless, qty, type Display } from '@weasel-js/quantity';\nimport type { MenuNumberEntry } from './entries';\n\nconst FULL_PRECISION = decimal({ maxPlaces: 20, grouping: false });\n\n/** How a number row's value shows: its `display`, by default every decimal it has, with its word for infinity. */\nexport function menuNumberDisplay(e: Pick<MenuNumberEntry, 'display' | 'infinity'>): Display {\n const display = e.display ?? FULL_PRECISION;\n return e.infinity === undefined ? display : endless(display, e.infinity);\n}\n\n/** A number row's value as a renderer with no field of its own prints it: in {@link menuNumberDisplay}, then its\n * unit, which an infinity goes without. A dash for none. */\nexport function menuNumberText(value: number | undefined, e: Pick<MenuNumberEntry, 'display' | 'infinity' | 'suffix'> = {}): string {\n if (value === undefined) return '—';\n const text = qty(value, menuNumberDisplay(e)).text;\n return Number.isFinite(value) ? `${text}${e.suffix ?? ''}` : text;\n}\n","import { clampToBounds, spinKey, type SpinBounds } from '@weasel-js/quantity';\nimport type { MenuNumberEntry } from './entries';\n\n/** The number rows of one open menu: what each holds now, and how it is written and stepped. */\nexport interface MenuNumberValues {\n numberValue(e: MenuNumberEntry): number | undefined;\n setNumber(e: MenuNumberEntry, value: number): void;\n spin(e: MenuNumberEntry, key: string): boolean;\n}\n\nconst bounds = (e: MenuNumberEntry): SpinBounds => ({ step: e.step ?? 1, min: e.min, max: e.max, endless: e.endless });\n\n/** Whether a step up (`dir` 1) or down (-1) from `value` moves a number row anywhere. */\nexport function menuNumberCanStep(e: MenuNumberEntry, value: number, dir: 1 | -1): boolean {\n return spinKey(dir > 0 ? 'ArrowUp' : 'ArrowDown', value, bounds(e)) !== value;\n}\n\n/** Number rows that remember what was set while the menu is open; `changed` hears each write, and nothing is\n * written once `closed` says so. */\nexport function createNumberValues(changed: () => void, closed: () => boolean): MenuNumberValues {\n const held = new Map<MenuNumberEntry, number>();\n const numberValue = (e: MenuNumberEntry) => (held.has(e) ? held.get(e) : e.value);\n const setNumber = (e: MenuNumberEntry, value: number) => {\n const next = clampToBounds(value, bounds(e));\n if (closed() || e.disabled || Number.isNaN(next) || next === numberValue(e)) return;\n held.set(e, next);\n e.set(next);\n changed();\n };\n const spin = (e: MenuNumberEntry, key: string): boolean => {\n const next = spinKey(key, numberValue(e) ?? e.min ?? 0, bounds(e));\n if (next === null) return false;\n setNumber(e, next);\n return true;\n };\n return { numberValue, setNumber, spin };\n}\n","import { entriesAt, isRow, type MenuEntry } from './entries';\n\n/** The highlighted index at each open level; its length is how many are open. */\nexport type MenuStack = readonly (number | null)[];\n\n/** What a key asks the owner of the menu to do beyond moving the highlight. */\nexport type KeyEffect =\n | { kind: 'none' }\n | { kind: 'choose'; level: number; index: number }\n | { kind: 'close' }\n | { kind: 'edge'; dir: -1 | 1 }\n | { kind: 'step'; level: number; index: number; dir: -1 | 1 };\n\nconst NONE: KeyEffect = { kind: 'none' };\n\n/** Index of the first row, or null for a level with none. */\nexport function firstRow(entries: readonly MenuEntry[]): number | null {\n const i = entries.findIndex(isRow);\n return i < 0 ? null : i;\n}\n\nfunction step(entries: readonly MenuEntry[], from: number | null, dir: 1 | -1): number | null {\n const n = entries.length;\n let i = from ?? (dir === 1 ? -1 : n);\n for (let k = 0; k < n; k++) {\n i = (i + dir + n) % n;\n if (isRow(entries[i]!)) return i;\n }\n return null;\n}\n\nfunction withTop(stack: MenuStack, index: number | null): MenuStack {\n return [...stack.slice(0, -1), index];\n}\n\nfunction openSub(root: readonly MenuEntry[], stack: MenuStack): MenuStack | null {\n const level = stack.length - 1;\n const at = stack[level];\n const e = at == null ? undefined : entriesAt(root, stack, level)[at];\n if (e?.kind !== 'submenu' || e.disabled) return null;\n return [...stack, firstRow(e.entries)];\n}\n\n/** Apply one key to the menu: the new stack, any effect, and whether the key\n * was the menu's (an unhandled key goes on to typeahead). */\nexport function keyStep(\n root: readonly MenuEntry[],\n stack: MenuStack,\n key: string,\n): { stack: MenuStack; effect: KeyEffect; handled: boolean } {\n const level = stack.length - 1;\n const rows = entriesAt(root, stack, level);\n const at = stack[level] ?? null;\n const done = (s: MenuStack, effect: KeyEffect = NONE) => ({ stack: s, effect, handled: true });\n switch (key) {\n case 'ArrowDown': return done(withTop(stack, step(rows, at, 1)));\n case 'ArrowUp': return done(withTop(stack, step(rows, at, -1)));\n case 'Home': return done(withTop(stack, step(rows, null, 1)));\n case 'End': return done(withTop(stack, step(rows, null, -1)));\n case 'ArrowRight': {\n const opened = openSub(root, stack);\n return opened ? done(opened) : done(stack, { kind: 'edge', dir: 1 });\n }\n case 'ArrowLeft': return level > 0 ? done(stack.slice(0, -1)) : done(stack, { kind: 'edge', dir: -1 });\n case 'Escape': return level > 0 ? done(stack.slice(0, -1)) : done(stack, { kind: 'close' });\n case 'Tab': return done(stack, { kind: 'close' });\n case 'Enter':\n case ' ': {\n const e = at == null ? undefined : rows[at];\n if (e?.kind === 'submenu') return done(openSub(root, stack) ?? stack);\n if (e?.kind === 'item' && !e.disabled) return done(stack, { kind: 'choose', level, index: at! });\n return done(stack);\n }\n case '+':\n case '=':\n case '-': {\n // A number row steps; anywhere else these are typed, as any other character is.\n const e = at == null ? undefined : rows[at];\n if (e?.kind !== 'number' || e.disabled) return { stack, effect: NONE, handled: false };\n return done(stack, { kind: 'step', level, index: at!, dir: key === '-' ? -1 : 1 });\n }\n default: return { stack, effect: NONE, handled: false };\n }\n}\n","import { isRow, type MenuEntry } from './entries';\n\n/** The row a typed `query` lands on, searching forward from `from` with\n * wrap. A one-letter query starts after the highlight so repeating a letter\n * cycles; a longer one starts at it so typing on keeps the match. */\nexport function typeaheadMatch(\n entries: readonly MenuEntry[],\n from: number | null,\n query: string,\n): number | null {\n const q = query.toLowerCase();\n const n = entries.length;\n const start = from == null ? 0 : q.length === 1 ? from + 1 : from;\n for (let k = 0; k < n; k++) {\n const i = (start + k) % n;\n const e = entries[i]!;\n if (isRow(e) && e.label.toLowerCase().startsWith(q)) return i;\n }\n return null;\n}\n\n/** How long typing can pause before the typeahead buffer starts over, in milliseconds. */\nexport const TYPEAHEAD_RESET_MS = 500;\n\nexport interface TypeaheadBuffer {\n text: string;\n at: number;\n}\n\n/** One typed character: the new buffer and the row it lands on. A key that\n * repeats the whole buffer searches that one letter, so it cycles. */\nexport function typeaheadStep(\n entries: readonly MenuEntry[],\n from: number | null,\n buffer: TypeaheadBuffer,\n key: string,\n now: number,\n): { buffer: TypeaheadBuffer; hit: number | null } {\n const prev = now - buffer.at < TYPEAHEAD_RESET_MS ? buffer.text : '';\n const k = key.toLowerCase();\n const cycling = prev !== '' && [...prev].every((c) => c.toLowerCase() === k);\n const text = cycling ? key : prev + key;\n return { buffer: { text, at: now }, hit: typeaheadMatch(entries, from, text) };\n}\n","import { accessMatch } from './access';\nimport { entriesAt } from './entries';\nimport type { MenuEntry } from './entries';\nimport { firstRow, keyStep, type KeyEffect, type MenuStack } from './keys';\nimport { typeaheadStep, type TypeaheadBuffer } from './typeahead';\n\nexport const EMPTY_TYPEAHEAD: TypeaheadBuffer = { text: '', at: -Infinity };\n\nexport interface KeyPlan {\n stack: MenuStack;\n typed: TypeaheadBuffer;\n effect: KeyEffect;\n}\n\n/** What one key does to an open menu: navigation, then an access letter, then typeahead.\n * `null` when the key is none of the menu's. `stack` is the same array when nothing moved. */\nexport function planKey(\n root: readonly MenuEntry[],\n stack: MenuStack,\n typed: TypeaheadBuffer,\n key: string,\n now: number,\n): KeyPlan | null {\n const r = keyStep(root, stack, key);\n if (r.handled) {\n return { stack: r.stack, typed: r.stack !== stack ? EMPTY_TYPEAHEAD : typed, effect: r.effect };\n }\n if (key.length !== 1 || key === ' ') return null;\n const level = stack.length - 1;\n const rows = entriesAt(root, stack, level);\n const letter = accessMatch(rows, key);\n if (letter !== null) {\n const e = rows[letter];\n if (e?.kind === 'item' && !e.disabled) {\n return { stack, typed: EMPTY_TYPEAHEAD, effect: { kind: 'choose', level, index: letter } };\n }\n const opened = e?.kind === 'submenu' && !e.disabled\n ? [...stack.slice(0, level), letter, firstRow(e.entries)]\n : stack;\n return { stack: opened, typed: EMPTY_TYPEAHEAD, effect: { kind: 'none' } };\n }\n const t = typeaheadStep(rows, stack[level] ?? null, typed, key, now);\n const next = t.hit === null ? stack : [...stack.slice(0, level), t.hit];\n return { stack: next, typed: t.buffer, effect: { kind: 'none' } };\n}\n","import type { Point } from './placement';\n\nconst cross = (o: Point, a: Point, b: Point) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);\n\n/** Whether `p` lies in triangle `abc`, edges included. */\nexport function inTriangle(p: Point, a: Point, b: Point, c: Point): boolean {\n const d1 = cross(p, a, b), d2 = cross(p, b, c), d3 = cross(p, c, a);\n const neg = d1 < 0 || d2 < 0 || d3 < 0;\n const pos = d1 > 0 || d2 > 0 || d3 > 0;\n return !(neg && pos);\n}\n","import { createNumberValues } from './numberValues';\nimport { entriesAt, isRow, type MenuEntry, type MenuNumberEntry } from './entries';\nimport { firstRow, type MenuStack } from './keys';\nimport { EMPTY_TYPEAHEAD, planKey } from './keyPlan';\nimport type { Point, Rect } from './placement';\nimport { inTriangle } from './triangle';\nimport type { TypeaheadBuffer } from './typeahead';\n\nexport { TYPEAHEAD_RESET_MS } from './typeahead';\n/** How long the pointer rests on a submenu row before its panel opens, in milliseconds. */\nexport const SUBMENU_OPEN_MS = 200;\n/** How long a move toward an open submenu keeps it open while the pointer crosses other rows, in milliseconds. */\nexport const DIAGONAL_GRACE_MS = 300;\n\n/** Clock and timer, injected so tests and headless hosts control time. */\nexport interface MenuScheduler {\n now(): number;\n /** Run `fn` after `ms`; returns a cancel. */\n set(fn: () => void, ms: number): () => void;\n}\n\n/** The real-time scheduler: `Date.now` and `setTimeout`. */\nexport const timerScheduler: MenuScheduler = {\n now: () => Date.now(),\n set: (fn, ms) => {\n const t = setTimeout(fn, ms);\n return () => clearTimeout(t);\n },\n};\n\n/** What {@link createMenuController} needs: the entries, how to close, and optionally a clock. */\nexport interface MenuControllerOptions {\n entries: readonly MenuEntry[];\n /** Highlight the first row at open, as a keyboard open does. */\n highlightFirst?: boolean;\n /** The menu is done: an item was chosen, or it was dismissed. */\n onClose(): void;\n /** An arrow key ran off the menu: left from the first level (-1), or right from a row with no submenu (1). */\n onEdge?(dir: -1 | 1): void;\n scheduler?: MenuScheduler;\n}\n\n/** One open menu's behavior, independent of what draws it. */\nexport interface MenuController {\n stack(): MenuStack;\n entriesAt(level: number): readonly MenuEntry[];\n /** Returns whether the key was the menu's. */\n key(key: string): boolean;\n /** The pointer is over row `index` of `level`, or over none of its rows. */\n hover(level: number, index: number | null, point?: Point): void;\n press(level: number, index: number): void;\n /** Where `level`'s panel is, for the diagonal allowance. */\n setPanel(level: number, rect: Rect): void;\n /** What a number row holds now: the last value set while the menu is open, else the one it opened with. */\n numberValue(e: MenuNumberEntry): number | undefined;\n /** Write a number row, held within its bounds. The menu stays open; subscribers hear it. */\n setNumber(e: MenuNumberEntry, value: number): void;\n /** Move a number row by a spin-button key: an arrow, Page Up or Down, Home or End. False for any other key. */\n spin(e: MenuNumberEntry, key: string): boolean;\n subscribe(fn: () => void): () => void;\n dispose(): void;\n}\n\n/** Creates the controller that runs one open menu: highlight, keys, submenus and typeahead. */\nexport function createMenuController(opts: MenuControllerOptions): MenuController {\n const { entries: root, onClose } = opts;\n const sched = opts.scheduler ?? timerScheduler;\n let stack: MenuStack = [opts.highlightFirst ? firstRow(root) : null];\n const listeners = new Set<() => void>();\n const panels = new Map<number, Rect>();\n let pending: (() => void) | null = null;\n let pendingKind: 'open' | 'diagonal' | null = null;\n let lastPoint: Point | null = null;\n let deferred: { level: number; index: number | null } | null = null;\n let typed: TypeaheadBuffer = EMPTY_TYPEAHEAD;\n let closed = false;\n const notify = () => { for (const fn of listeners) fn(); };\n const numbers = createNumberValues(notify, () => closed);\n const set = (next: MenuStack) => {\n stack = next;\n for (const level of [...panels.keys()]) if (level >= next.length) panels.delete(level);\n notify();\n };\n const cancel = () => { pending?.(); pending = null; pendingKind = null; };\n const later = (kind: 'open' | 'diagonal', fn: () => void, ms: number) => {\n cancel();\n pendingKind = kind;\n pending = sched.set(() => { pending = null; pendingKind = null; if (!closed) fn(); }, ms);\n };\n const entryAt = (level: number, index: number) => entriesAt(root, stack, level)[index];\n const isRowAt = (level: number, index: number) => {\n const e = entryAt(level, index);\n return e !== undefined && isRow(e);\n };\n\n const finish = () => { closed = true; cancel(); onClose(); };\n const choose = (level: number, index: number) => {\n const e = entryAt(level, index);\n if (e?.kind !== 'item' || e.disabled) return;\n finish();\n e.run();\n };\n\n const openAt = (level: number, index: number) => {\n const e = entryAt(level, index);\n const base = [...stack.slice(0, level), index];\n set(e?.kind === 'submenu' && !e.disabled ? [...base, null] : base);\n };\n\n /** Heading from the point the deferral began toward the open child's near edge. */\n const headingIntoChild = (level: number, p: Point): boolean => {\n const child = panels.get(level + 1);\n const parent = panels.get(level);\n if (!child || !parent || !lastPoint || stack.length <= level + 1) return false;\n const edge = child.x >= parent.x ? child.x : child.x + child.w;\n return inTriangle(p, lastPoint, { x: edge, y: child.y }, { x: edge, y: child.y + child.h });\n };\n\n const hoverNow = (level: number, index: number | null) => {\n cancel();\n if (index === null && stack.length !== level + 1) return;\n const next = [...stack.slice(0, level), index];\n if (next.length !== stack.length || next[level] !== stack[level]) set(next);\n if (index === null) return;\n const e = entryAt(level, index);\n if (e?.kind === 'submenu' && !e.disabled) later('open', () => openAt(level, index), SUBMENU_OPEN_MS);\n };\n\n return {\n stack: () => stack,\n entriesAt: (level) => entriesAt(root, stack, level),\n key(key) {\n if (closed) return false;\n const plan = planKey(root, stack, typed, key, sched.now());\n if (!plan) return false;\n cancel();\n typed = plan.typed;\n if (plan.stack !== stack) set(plan.stack);\n const { effect } = plan;\n if (effect.kind === 'choose') choose(effect.level, effect.index);\n else if (effect.kind === 'close') finish();\n else if (effect.kind === 'edge') opts.onEdge?.(effect.dir);\n else if (effect.kind === 'step') {\n const e = entryAt(effect.level, effect.index);\n if (e?.kind === 'number') numbers.spin(e, effect.dir > 0 ? 'ArrowUp' : 'ArrowDown');\n }\n return true;\n },\n hover(level, index, point) {\n if (closed || level >= stack.length) return;\n if (index !== null && !isRowAt(level, index)) return;\n const sameRow = index === stack[level];\n if (point && !sameRow && headingIntoChild(level, point)) {\n deferred = { level, index };\n if (pendingKind !== 'diagonal') {\n later('diagonal', () => { if (deferred) hoverNow(deferred.level, deferred.index); }, DIAGONAL_GRACE_MS);\n }\n return;\n }\n if (point) lastPoint = point;\n if (sameRow && stack.length > level + 1) { cancel(); return; }\n if (sameRow && pendingKind === 'open') return;\n hoverNow(level, index);\n },\n press(level, index) {\n if (closed || level >= stack.length || !isRowAt(level, index)) return;\n if (stack[level] === index && stack.length > level + 1) return;\n cancel();\n if (entryAt(level, index)?.kind === 'item') choose(level, index);\n else openAt(level, index);\n },\n setPanel(level, rect) { panels.set(level, rect); },\n ...numbers,\n subscribe(fn) {\n listeners.add(fn);\n return () => { listeners.delete(fn); };\n },\n dispose() {\n closed = true;\n cancel();\n listeners.clear();\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weasel-js/menu",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "description": "Headless menu model: entries, keyboard and pointer behavior, placement. No React, no DOM.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,6 +36,9 @@
36
36
  "scripts": {
37
37
  "build": "tsup"
38
38
  },
39
+ "dependencies": {
40
+ "@weasel-js/quantity": "1.11.0"
41
+ },
39
42
  "publishConfig": {
40
43
  "access": "public",
41
44
  "provenance": true