@weasel-js/menu 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 orochi235
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,91 @@
1
- # Temporary Holding Version
1
+ # @weasel-js/menu
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
5
+ controller that owns the highlight, open submenus, keyboard and typeahead behavior, and placement
6
+ that keeps a menu on screen. No React, no DOM.
7
+
8
+ Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
9
+ scene-graph canvas kit for React. See the
10
+ [API reference](https://orochi235.github.io/weasel/#pkg/menu).
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm install @weasel-js/menu
16
+ ```
17
+
18
+ ## How it fits
19
+
20
+ | Package | Role |
21
+ |---|---|
22
+ | `@weasel-js/menu` | The model: entries, `createMenuController`, `placeMenu` / `placeSubmenu`. |
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>`. |
26
+ | `@weasel-js/hud` | `attachMenuHost`, the WebGL renderer. |
27
+
28
+ The DOM renderer does not use React Aria's `Menu`, which keeps its own state and key handling: a
29
+ second copy of the model's that would drift from the WebGL menu's.
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
+
58
+ ## Usage
59
+
60
+ Declare menus as data, mount one host, and open one from chrome:
61
+
62
+ ```tsx
63
+ import { ActionsProvider, useMenus, type MenuDef } from '@weasel-js/core';
64
+ import { pref } from '@weasel-js/prefs';
65
+ import { ContextMenuHost, useContextMenu } from '@weasel-js/ui';
66
+
67
+ const MENUS: MenuDef[] = [
68
+ { id: 'edit', menu: pref.group('', { duplicate: pref.action('duplicate'), delete: pref.action('delete') }) },
69
+ ];
70
+
71
+ function EditButton() {
72
+ const onContextMenu = useContextMenu('edit');
73
+ return <button onContextMenu={onContextMenu}>Edit</button>;
74
+ }
75
+
76
+ function Chrome() {
77
+ useMenus(MENUS);
78
+ return <ContextMenuHost />;
79
+ }
80
+
81
+ // <ActionsProvider><Chrome /><EditButton /></ActionsProvider>
82
+ ```
83
+
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
86
+ `EditButton` and `Chrome` must sit under an `<ActionsProvider>`. Any menu opens by id; one with a `when` (`{ bodyTarget, bodyKind }`) also
87
+ opens on a right-click of a matching canvas body.
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,232 @@
1
+ import { Display, Endless, InfinityText, UnitTable } from '@weasel-js/quantity';
2
+
3
+ /** A key chip: the key, and which modifiers are held with it. `@weasel-js/routing`
4
+ * derives these from action bindings as `ActionShortcut`; `@weasel-js/gestures`
5
+ * formats them as `ShortcutInput`. A tool's `keybinding` has this shape too. */
6
+ interface MenuShortcut {
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';
13
+ }
14
+ /** A row that runs something when chosen. */
15
+ interface MenuItemEntry {
16
+ kind: 'item';
17
+ key: string;
18
+ label: string;
19
+ /** Opaque to the model; each renderer knows what it can draw. */
20
+ icon?: unknown;
21
+ shortcut?: MenuShortcut;
22
+ disabled?: boolean;
23
+ /** `undefined` for a row with no on/off state. */
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;
31
+ run(): void;
32
+ }
33
+ /** A rule between groups of rows. Never highlighted. */
34
+ interface MenuSeparatorEntry {
35
+ kind: 'separator';
36
+ key: string;
37
+ }
38
+ /** A row that opens another level. */
39
+ interface SubmenuEntry {
40
+ kind: 'submenu';
41
+ key: string;
42
+ label: string;
43
+ icon?: unknown;
44
+ disabled?: boolean;
45
+ /** Letter that opens this row from the keyboard, compared ignoring case. */
46
+ access?: string;
47
+ entries: readonly MenuEntry[];
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
+ }
87
+ /** One line of a menu. */
88
+ type MenuEntry = MenuItemEntry | MenuSeparatorEntry | SubmenuEntry | MenuNumberEntry | MenuColorEntry;
89
+ /** An entry the highlight can land on. */
90
+ type MenuRowEntry = Exclude<MenuEntry, MenuSeparatorEntry>;
91
+ /** True for an entry the highlight can land on. */
92
+ declare function isRow(e: MenuEntry): e is MenuRowEntry;
93
+ /**
94
+ * The entries shown at `level`, following `stack` (the highlighted index at
95
+ * each open level) down from `root`. Empty when the path does not lead to a
96
+ * submenu.
97
+ */
98
+ declare function entriesAt(root: readonly MenuEntry[], stack: readonly (number | null)[], level: number): readonly MenuEntry[];
99
+
100
+ /** A position in viewport pixels (CSS pixels, as `getBoundingClientRect` reports). */
101
+ interface Point {
102
+ x: number;
103
+ y: number;
104
+ }
105
+ /** A width and height in pixels. */
106
+ interface Size {
107
+ w: number;
108
+ h: number;
109
+ }
110
+ /** A rectangle in viewport pixels: top-left corner plus width and height. */
111
+ interface Rect {
112
+ x: number;
113
+ y: number;
114
+ w: number;
115
+ h: number;
116
+ }
117
+ /** Where a menu of `size` opened at `anchor` goes: down and right of the
118
+ * anchor, flipped on an axis that does not fit, shifted inside `bounds`
119
+ * when neither side does. */
120
+ declare function placeMenu(size: Size, anchor: Point, bounds: Rect): Point;
121
+ /** Where a submenu goes: beside `row`, right first, left if that does not
122
+ * fit, top-aligned with the row and shifted up to stay inside `bounds`. */
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;
145
+
146
+ /** The highlighted index at each open level; its length is how many are open. */
147
+ type MenuStack = readonly (number | null)[];
148
+ /** What a key asks the owner of the menu to do beyond moving the highlight. */
149
+ type KeyEffect = {
150
+ kind: 'none';
151
+ } | {
152
+ kind: 'choose';
153
+ level: number;
154
+ index: number;
155
+ } | {
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;
165
+ };
166
+ /** Index of the first row, or null for a level with none. */
167
+ declare function firstRow(entries: readonly MenuEntry[]): number | null;
168
+ /** Apply one key to the menu: the new stack, any effect, and whether the key
169
+ * was the menu's (an unhandled key goes on to typeahead). */
170
+ declare function keyStep(root: readonly MenuEntry[], stack: MenuStack, key: string): {
171
+ stack: MenuStack;
172
+ effect: KeyEffect;
173
+ handled: boolean;
174
+ };
175
+
176
+ /** The row a typed `query` lands on, searching forward from `from` with
177
+ * wrap. A one-letter query starts after the highlight so repeating a letter
178
+ * cycles; a longer one starts at it so typing on keeps the match. */
179
+ declare function typeaheadMatch(entries: readonly MenuEntry[], from: number | null, query: string): number | null;
180
+ /** How long typing can pause before the typeahead buffer starts over, in milliseconds. */
181
+ declare const TYPEAHEAD_RESET_MS = 500;
182
+
183
+ /** How long the pointer rests on a submenu row before its panel opens, in milliseconds. */
184
+ declare const SUBMENU_OPEN_MS = 200;
185
+ /** How long a move toward an open submenu keeps it open while the pointer crosses other rows, in milliseconds. */
186
+ declare const DIAGONAL_GRACE_MS = 300;
187
+ /** Clock and timer, injected so tests and headless hosts control time. */
188
+ interface MenuScheduler {
189
+ now(): number;
190
+ /** Run `fn` after `ms`; returns a cancel. */
191
+ set(fn: () => void, ms: number): () => void;
192
+ }
193
+ /** The real-time scheduler: `Date.now` and `setTimeout`. */
194
+ declare const timerScheduler: MenuScheduler;
195
+ /** What {@link createMenuController} needs: the entries, how to close, and optionally a clock. */
196
+ interface MenuControllerOptions {
197
+ entries: readonly MenuEntry[];
198
+ /** Highlight the first row at open, as a keyboard open does. */
199
+ highlightFirst?: boolean;
200
+ /** The menu is done: an item was chosen, or it was dismissed. */
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;
204
+ scheduler?: MenuScheduler;
205
+ }
206
+ /** One open menu's behavior, independent of what draws it. */
207
+ interface MenuController {
208
+ stack(): MenuStack;
209
+ entriesAt(level: number): readonly MenuEntry[];
210
+ /** Returns whether the key was the menu's. */
211
+ key(key: string): boolean;
212
+ /** The pointer is over row `index` of `level`, or over none of its rows. */
213
+ hover(level: number, index: number | null, point?: Point): void;
214
+ press(level: number, index: number): void;
215
+ /** Where `level`'s panel is, for the diagonal allowance. */
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;
223
+ subscribe(fn: () => void): () => void;
224
+ dispose(): void;
225
+ }
226
+ /** Creates the controller that runs one open menu: highlight, keys, submenus and typeahead. */
227
+ declare function createMenuController(opts: MenuControllerOptions): MenuController;
228
+
229
+ /** Whether `p` lies in triangle `abc`, edges included. */
230
+ declare function inTriangle(p: Point, a: Point, b: Point, c: Point): boolean;
231
+
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 ADDED
@@ -0,0 +1,371 @@
1
+ import { decimal, endless, qty, spinKey, clampToBounds } from '@weasel-js/quantity';
2
+
3
+ // src/entries.ts
4
+ function isRow(e) {
5
+ return e.kind !== "separator";
6
+ }
7
+ function entriesAt(root, stack, level) {
8
+ let entries = root;
9
+ for (let i = 0; i < level; i++) {
10
+ const at = stack[i];
11
+ const e = at == null ? void 0 : entries[at];
12
+ if (e?.kind !== "submenu") return [];
13
+ entries = e.entries;
14
+ }
15
+ return entries;
16
+ }
17
+
18
+ // src/placement.ts
19
+ function flipOrShift(at, len, lo, span, flipAt = at) {
20
+ const hi = lo + span;
21
+ if (at + len <= hi) return Math.max(lo, at);
22
+ if (flipAt - len >= lo) return Math.max(lo, Math.min(flipAt - len, hi - len));
23
+ return Math.max(lo, hi - len);
24
+ }
25
+ function placeMenu(size, anchor, bounds2) {
26
+ return {
27
+ x: flipOrShift(anchor.x, size.w, bounds2.x, bounds2.w),
28
+ y: flipOrShift(anchor.y, size.h, bounds2.y, bounds2.h)
29
+ };
30
+ }
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 };
97
+ }
98
+
99
+ // src/keys.ts
100
+ var NONE = { kind: "none" };
101
+ function firstRow(entries) {
102
+ const i = entries.findIndex(isRow);
103
+ return i < 0 ? null : i;
104
+ }
105
+ function step(entries, from, dir) {
106
+ const n = entries.length;
107
+ let i = from ?? (dir === 1 ? -1 : n);
108
+ for (let k = 0; k < n; k++) {
109
+ i = (i + dir + n) % n;
110
+ if (isRow(entries[i])) return i;
111
+ }
112
+ return null;
113
+ }
114
+ function withTop(stack, index) {
115
+ return [...stack.slice(0, -1), index];
116
+ }
117
+ function openSub(root, stack) {
118
+ const level = stack.length - 1;
119
+ const at = stack[level];
120
+ const e = at == null ? void 0 : entriesAt(root, stack, level)[at];
121
+ if (e?.kind !== "submenu" || e.disabled) return null;
122
+ return [...stack, firstRow(e.entries)];
123
+ }
124
+ function keyStep(root, stack, key) {
125
+ const level = stack.length - 1;
126
+ const rows = entriesAt(root, stack, level);
127
+ const at = stack[level] ?? null;
128
+ const done = (s, effect = NONE) => ({ stack: s, effect, handled: true });
129
+ switch (key) {
130
+ case "ArrowDown":
131
+ return done(withTop(stack, step(rows, at, 1)));
132
+ case "ArrowUp":
133
+ return done(withTop(stack, step(rows, at, -1)));
134
+ case "Home":
135
+ return done(withTop(stack, step(rows, null, 1)));
136
+ case "End":
137
+ return done(withTop(stack, step(rows, null, -1)));
138
+ case "ArrowRight": {
139
+ const opened = openSub(root, stack);
140
+ return opened ? done(opened) : done(stack, { kind: "edge", dir: 1 });
141
+ }
142
+ case "ArrowLeft":
143
+ return level > 0 ? done(stack.slice(0, -1)) : done(stack, { kind: "edge", dir: -1 });
144
+ case "Escape":
145
+ return level > 0 ? done(stack.slice(0, -1)) : done(stack, { kind: "close" });
146
+ case "Tab":
147
+ return done(stack, { kind: "close" });
148
+ case "Enter":
149
+ case " ": {
150
+ const e = at == null ? void 0 : rows[at];
151
+ if (e?.kind === "submenu") return done(openSub(root, stack) ?? stack);
152
+ if (e?.kind === "item" && !e.disabled) return done(stack, { kind: "choose", level, index: at });
153
+ return done(stack);
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
+ }
162
+ default:
163
+ return { stack, effect: NONE, handled: false };
164
+ }
165
+ }
166
+
167
+ // src/typeahead.ts
168
+ function typeaheadMatch(entries, from, query) {
169
+ const q = query.toLowerCase();
170
+ const n = entries.length;
171
+ const start = from == null ? 0 : q.length === 1 ? from + 1 : from;
172
+ for (let k = 0; k < n; k++) {
173
+ const i = (start + k) % n;
174
+ const e = entries[i];
175
+ if (isRow(e) && e.label.toLowerCase().startsWith(q)) return i;
176
+ }
177
+ return null;
178
+ }
179
+ var TYPEAHEAD_RESET_MS = 500;
180
+ function typeaheadStep(entries, from, buffer, key, now) {
181
+ const prev = now - buffer.at < TYPEAHEAD_RESET_MS ? buffer.text : "";
182
+ const k = key.toLowerCase();
183
+ const cycling = prev !== "" && [...prev].every((c) => c.toLowerCase() === k);
184
+ const text = cycling ? key : prev + key;
185
+ return { buffer: { text, at: now }, hit: typeaheadMatch(entries, from, text) };
186
+ }
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
+
212
+ // src/triangle.ts
213
+ var cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
214
+ function inTriangle(p, a, b, c) {
215
+ const d1 = cross(p, a, b), d2 = cross(p, b, c), d3 = cross(p, c, a);
216
+ const neg = d1 < 0 || d2 < 0 || d3 < 0;
217
+ const pos = d1 > 0 || d2 > 0 || d3 > 0;
218
+ return !(neg && pos);
219
+ }
220
+
221
+ // src/controller.ts
222
+ var SUBMENU_OPEN_MS = 200;
223
+ var DIAGONAL_GRACE_MS = 300;
224
+ var timerScheduler = {
225
+ now: () => Date.now(),
226
+ set: (fn, ms) => {
227
+ const t = setTimeout(fn, ms);
228
+ return () => clearTimeout(t);
229
+ }
230
+ };
231
+ function createMenuController(opts) {
232
+ const { entries: root, onClose } = opts;
233
+ const sched = opts.scheduler ?? timerScheduler;
234
+ let stack = [opts.highlightFirst ? firstRow(root) : null];
235
+ const listeners = /* @__PURE__ */ new Set();
236
+ const panels = /* @__PURE__ */ new Map();
237
+ let pending = null;
238
+ let pendingKind = null;
239
+ let lastPoint = null;
240
+ let deferred = null;
241
+ let typed = EMPTY_TYPEAHEAD;
242
+ let closed = false;
243
+ const notify = () => {
244
+ for (const fn of listeners) fn();
245
+ };
246
+ const numbers = createNumberValues(notify, () => closed);
247
+ const set = (next) => {
248
+ stack = next;
249
+ for (const level of [...panels.keys()]) if (level >= next.length) panels.delete(level);
250
+ notify();
251
+ };
252
+ const cancel = () => {
253
+ pending?.();
254
+ pending = null;
255
+ pendingKind = null;
256
+ };
257
+ const later = (kind, fn, ms) => {
258
+ cancel();
259
+ pendingKind = kind;
260
+ pending = sched.set(() => {
261
+ pending = null;
262
+ pendingKind = null;
263
+ if (!closed) fn();
264
+ }, ms);
265
+ };
266
+ const entryAt = (level, index) => entriesAt(root, stack, level)[index];
267
+ const isRowAt = (level, index) => {
268
+ const e = entryAt(level, index);
269
+ return e !== void 0 && isRow(e);
270
+ };
271
+ const finish = () => {
272
+ closed = true;
273
+ cancel();
274
+ onClose();
275
+ };
276
+ const choose = (level, index) => {
277
+ const e = entryAt(level, index);
278
+ if (e?.kind !== "item" || e.disabled) return;
279
+ finish();
280
+ e.run();
281
+ };
282
+ const openAt = (level, index) => {
283
+ const e = entryAt(level, index);
284
+ const base = [...stack.slice(0, level), index];
285
+ set(e?.kind === "submenu" && !e.disabled ? [...base, null] : base);
286
+ };
287
+ const headingIntoChild = (level, p) => {
288
+ const child = panels.get(level + 1);
289
+ const parent = panels.get(level);
290
+ if (!child || !parent || !lastPoint || stack.length <= level + 1) return false;
291
+ const edge = child.x >= parent.x ? child.x : child.x + child.w;
292
+ return inTriangle(p, lastPoint, { x: edge, y: child.y }, { x: edge, y: child.y + child.h });
293
+ };
294
+ const hoverNow = (level, index) => {
295
+ cancel();
296
+ if (index === null && stack.length !== level + 1) return;
297
+ const next = [...stack.slice(0, level), index];
298
+ if (next.length !== stack.length || next[level] !== stack[level]) set(next);
299
+ if (index === null) return;
300
+ const e = entryAt(level, index);
301
+ if (e?.kind === "submenu" && !e.disabled) later("open", () => openAt(level, index), SUBMENU_OPEN_MS);
302
+ };
303
+ return {
304
+ stack: () => stack,
305
+ entriesAt: (level) => entriesAt(root, stack, level),
306
+ key(key) {
307
+ if (closed) return false;
308
+ const plan = planKey(root, stack, typed, key, sched.now());
309
+ if (!plan) return false;
310
+ cancel();
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
+ }
321
+ return true;
322
+ },
323
+ hover(level, index, point) {
324
+ if (closed || level >= stack.length) return;
325
+ if (index !== null && !isRowAt(level, index)) return;
326
+ const sameRow = index === stack[level];
327
+ if (point && !sameRow && headingIntoChild(level, point)) {
328
+ deferred = { level, index };
329
+ if (pendingKind !== "diagonal") {
330
+ later("diagonal", () => {
331
+ if (deferred) hoverNow(deferred.level, deferred.index);
332
+ }, DIAGONAL_GRACE_MS);
333
+ }
334
+ return;
335
+ }
336
+ if (point) lastPoint = point;
337
+ if (sameRow && stack.length > level + 1) {
338
+ cancel();
339
+ return;
340
+ }
341
+ if (sameRow && pendingKind === "open") return;
342
+ hoverNow(level, index);
343
+ },
344
+ press(level, index) {
345
+ if (closed || level >= stack.length || !isRowAt(level, index)) return;
346
+ if (stack[level] === index && stack.length > level + 1) return;
347
+ cancel();
348
+ if (entryAt(level, index)?.kind === "item") choose(level, index);
349
+ else openAt(level, index);
350
+ },
351
+ setPanel(level, rect) {
352
+ panels.set(level, rect);
353
+ },
354
+ ...numbers,
355
+ subscribe(fn) {
356
+ listeners.add(fn);
357
+ return () => {
358
+ listeners.delete(fn);
359
+ };
360
+ },
361
+ dispose() {
362
+ closed = true;
363
+ cancel();
364
+ listeners.clear();
365
+ }
366
+ };
367
+ }
368
+
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 };
370
+ //# sourceMappingURL=index.js.map
371
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
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,46 @@
1
1
  {
2
2
  "name": "@weasel-js/menu",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.11.0",
4
+ "description": "Headless menu model: entries, keyboard and pointer behavior, placement. No React, no DOM.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "author": "orochi235",
19
+ "homepage": "https://orochi235.github.io/weasel/",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/orochi235/weasel.git",
23
+ "directory": "packages/menu"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/orochi235/weasel/issues"
27
+ },
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsup"
38
+ },
39
+ "dependencies": {
40
+ "@weasel-js/quantity": "1.11.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public",
44
+ "provenance": true
45
+ }
46
+ }