@weasel-js/menu 0.0.0-stage → 1.10.1
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 +21 -0
- package/README.md +60 -2
- package/dist/index.d.ts +142 -0
- package/dist/index.js +275 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -4
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,61 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weasel-js/menu
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The headless model behind weasel's context menus: entries (items, separators, submenus), a
|
|
4
|
+
controller that owns the highlight, open submenus, keyboard and typeahead behavior, and placement
|
|
5
|
+
that keeps a menu on screen. No React, no DOM.
|
|
6
|
+
|
|
7
|
+
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
8
|
+
scene-graph canvas kit for React. See the
|
|
9
|
+
[API reference](https://orochi235.github.io/weasel/#pkg/menu).
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @weasel-js/menu
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## How it fits
|
|
18
|
+
|
|
19
|
+
| Package | Role |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `@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. |
|
|
25
|
+
| `@weasel-js/hud` | `attachMenuHost`, the WebGL renderer. |
|
|
26
|
+
|
|
27
|
+
The DOM renderer does not use React Aria's `Menu`, which keeps its own state and key handling: a
|
|
28
|
+
second copy of the model's that would drift from the WebGL menu's.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Declare menus as data, mount one host, and open one from chrome:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { ActionsProvider, useMenus, type MenuDef } from '@weasel-js/core';
|
|
36
|
+
import { ContextMenuHost, useContextMenu } from '@weasel-js/ui';
|
|
37
|
+
|
|
38
|
+
const MENUS: MenuDef[] = [
|
|
39
|
+
{ id: 'edit', sections: [[{ action: 'duplicate' }, { action: 'delete' }]] },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function EditButton() {
|
|
43
|
+
const onContextMenu = useContextMenu('edit');
|
|
44
|
+
return <button onContextMenu={onContextMenu}>Edit</button>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function Chrome() {
|
|
48
|
+
useMenus(MENUS);
|
|
49
|
+
return <ContextMenuHost />;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// <ActionsProvider><Chrome /><EditButton /></ActionsProvider>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Rows name registered actions (`duplicate`, `delete`, which act on the selection), and
|
|
56
|
+
`EditButton` and `Chrome` must sit under an `<ActionsProvider>`. Any menu opens by id; one with a `when` (`{ bodyTarget, bodyKind }`) also
|
|
57
|
+
opens on a right-click of a matching canvas body.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/** A key chip: the key, and which modifiers are held with it. `@weasel-js/routing`
|
|
2
|
+
* derives these from action bindings as `ActionShortcut`. */
|
|
3
|
+
interface MenuShortcut {
|
|
4
|
+
key: string;
|
|
5
|
+
mod: boolean;
|
|
6
|
+
alt: boolean;
|
|
7
|
+
shift: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** A row that runs something when chosen. */
|
|
10
|
+
interface MenuItemEntry {
|
|
11
|
+
kind: 'item';
|
|
12
|
+
key: string;
|
|
13
|
+
label: string;
|
|
14
|
+
/** Opaque to the model; each renderer knows what it can draw. */
|
|
15
|
+
icon?: unknown;
|
|
16
|
+
shortcut?: MenuShortcut;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/** `undefined` for a row with no on/off state. */
|
|
19
|
+
checked?: boolean;
|
|
20
|
+
run(): void;
|
|
21
|
+
}
|
|
22
|
+
/** A rule between groups of rows. Never highlighted. */
|
|
23
|
+
interface MenuSeparatorEntry {
|
|
24
|
+
kind: 'separator';
|
|
25
|
+
key: string;
|
|
26
|
+
}
|
|
27
|
+
/** A row that opens another level. */
|
|
28
|
+
interface SubmenuEntry {
|
|
29
|
+
kind: 'submenu';
|
|
30
|
+
key: string;
|
|
31
|
+
label: string;
|
|
32
|
+
icon?: unknown;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
entries: readonly MenuEntry[];
|
|
35
|
+
}
|
|
36
|
+
/** One line of a menu. */
|
|
37
|
+
type MenuEntry = MenuItemEntry | MenuSeparatorEntry | SubmenuEntry;
|
|
38
|
+
/** True for an entry the highlight can land on. */
|
|
39
|
+
declare function isRow(e: MenuEntry): e is MenuItemEntry | SubmenuEntry;
|
|
40
|
+
/**
|
|
41
|
+
* The entries shown at `level`, following `stack` (the highlighted index at
|
|
42
|
+
* each open level) down from `root`. Empty when the path does not lead to a
|
|
43
|
+
* submenu.
|
|
44
|
+
*/
|
|
45
|
+
declare function entriesAt(root: readonly MenuEntry[], stack: readonly (number | null)[], level: number): readonly MenuEntry[];
|
|
46
|
+
|
|
47
|
+
/** A position in viewport pixels (CSS pixels, as `getBoundingClientRect` reports). */
|
|
48
|
+
interface Point {
|
|
49
|
+
x: number;
|
|
50
|
+
y: number;
|
|
51
|
+
}
|
|
52
|
+
/** A width and height in pixels. */
|
|
53
|
+
interface Size {
|
|
54
|
+
w: number;
|
|
55
|
+
h: number;
|
|
56
|
+
}
|
|
57
|
+
/** A rectangle in viewport pixels: top-left corner plus width and height. */
|
|
58
|
+
interface Rect {
|
|
59
|
+
x: number;
|
|
60
|
+
y: number;
|
|
61
|
+
w: number;
|
|
62
|
+
h: number;
|
|
63
|
+
}
|
|
64
|
+
/** Where a menu of `size` opened at `anchor` goes: down and right of the
|
|
65
|
+
* anchor, flipped on an axis that does not fit, shifted inside `bounds`
|
|
66
|
+
* when neither side does. */
|
|
67
|
+
declare function placeMenu(size: Size, anchor: Point, bounds: Rect): Point;
|
|
68
|
+
/** Where a submenu goes: beside `row`, right first, left if that does not
|
|
69
|
+
* fit, top-aligned with the row and shifted up to stay inside `bounds`. */
|
|
70
|
+
declare function placeSubmenu(size: Size, row: Rect, bounds: Rect): Point;
|
|
71
|
+
|
|
72
|
+
/** The highlighted index at each open level; its length is how many are open. */
|
|
73
|
+
type MenuStack = readonly (number | null)[];
|
|
74
|
+
/** What a key asks the owner of the menu to do beyond moving the highlight. */
|
|
75
|
+
type KeyEffect = {
|
|
76
|
+
kind: 'none';
|
|
77
|
+
} | {
|
|
78
|
+
kind: 'choose';
|
|
79
|
+
level: number;
|
|
80
|
+
index: number;
|
|
81
|
+
} | {
|
|
82
|
+
kind: 'close';
|
|
83
|
+
};
|
|
84
|
+
/** Index of the first row, or null for a level with none. */
|
|
85
|
+
declare function firstRow(entries: readonly MenuEntry[]): number | null;
|
|
86
|
+
/** Apply one key to the menu: the new stack, any effect, and whether the key
|
|
87
|
+
* was the menu's (an unhandled key goes on to typeahead). */
|
|
88
|
+
declare function keyStep(root: readonly MenuEntry[], stack: MenuStack, key: string): {
|
|
89
|
+
stack: MenuStack;
|
|
90
|
+
effect: KeyEffect;
|
|
91
|
+
handled: boolean;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** The row a typed `query` lands on, searching forward from `from` with
|
|
95
|
+
* wrap. A one-letter query starts after the highlight so repeating a letter
|
|
96
|
+
* cycles; a longer one starts at it so typing on keeps the match. */
|
|
97
|
+
declare function typeaheadMatch(entries: readonly MenuEntry[], from: number | null, query: string): number | null;
|
|
98
|
+
/** How long typing can pause before the typeahead buffer starts over, in milliseconds. */
|
|
99
|
+
declare const TYPEAHEAD_RESET_MS = 500;
|
|
100
|
+
|
|
101
|
+
/** How long the pointer rests on a submenu row before its panel opens, in milliseconds. */
|
|
102
|
+
declare const SUBMENU_OPEN_MS = 200;
|
|
103
|
+
/** How long a move toward an open submenu keeps it open while the pointer crosses other rows, in milliseconds. */
|
|
104
|
+
declare const DIAGONAL_GRACE_MS = 300;
|
|
105
|
+
/** Clock and timer, injected so tests and headless hosts control time. */
|
|
106
|
+
interface MenuScheduler {
|
|
107
|
+
now(): number;
|
|
108
|
+
/** Run `fn` after `ms`; returns a cancel. */
|
|
109
|
+
set(fn: () => void, ms: number): () => void;
|
|
110
|
+
}
|
|
111
|
+
/** The real-time scheduler: `Date.now` and `setTimeout`. */
|
|
112
|
+
declare const timerScheduler: MenuScheduler;
|
|
113
|
+
/** What {@link createMenuController} needs: the entries, how to close, and optionally a clock. */
|
|
114
|
+
interface MenuControllerOptions {
|
|
115
|
+
entries: readonly MenuEntry[];
|
|
116
|
+
/** Highlight the first row at open, as a keyboard open does. */
|
|
117
|
+
highlightFirst?: boolean;
|
|
118
|
+
/** The menu is done: an item was chosen, or it was dismissed. */
|
|
119
|
+
onClose(): void;
|
|
120
|
+
scheduler?: MenuScheduler;
|
|
121
|
+
}
|
|
122
|
+
/** One open menu's behavior, independent of what draws it. */
|
|
123
|
+
interface MenuController {
|
|
124
|
+
stack(): MenuStack;
|
|
125
|
+
entriesAt(level: number): readonly MenuEntry[];
|
|
126
|
+
/** Returns whether the key was the menu's. */
|
|
127
|
+
key(key: string): boolean;
|
|
128
|
+
/** The pointer is over row `index` of `level`, or over none of its rows. */
|
|
129
|
+
hover(level: number, index: number | null, point?: Point): void;
|
|
130
|
+
press(level: number, index: number): void;
|
|
131
|
+
/** Where `level`'s panel is, for the diagonal allowance. */
|
|
132
|
+
setPanel(level: number, rect: Rect): void;
|
|
133
|
+
subscribe(fn: () => void): () => void;
|
|
134
|
+
dispose(): void;
|
|
135
|
+
}
|
|
136
|
+
/** Creates the controller that runs one open menu: highlight, keys, submenus and typeahead. */
|
|
137
|
+
declare function createMenuController(opts: MenuControllerOptions): MenuController;
|
|
138
|
+
|
|
139
|
+
/** Whether `p` lies in triangle `abc`, edges included. */
|
|
140
|
+
declare function inTriangle(p: Point, a: Point, b: Point, c: Point): boolean;
|
|
141
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// src/entries.ts
|
|
2
|
+
function isRow(e) {
|
|
3
|
+
return e.kind !== "separator";
|
|
4
|
+
}
|
|
5
|
+
function entriesAt(root, stack, level) {
|
|
6
|
+
let entries = root;
|
|
7
|
+
for (let i = 0; i < level; i++) {
|
|
8
|
+
const at = stack[i];
|
|
9
|
+
const e = at == null ? void 0 : entries[at];
|
|
10
|
+
if (e?.kind !== "submenu") return [];
|
|
11
|
+
entries = e.entries;
|
|
12
|
+
}
|
|
13
|
+
return entries;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/placement.ts
|
|
17
|
+
function flipOrShift(at, len, lo, span) {
|
|
18
|
+
const hi = lo + span;
|
|
19
|
+
if (at + len <= hi) return Math.max(lo, at);
|
|
20
|
+
if (at - len >= lo) return Math.max(lo, Math.min(at - len, hi - len));
|
|
21
|
+
return Math.max(lo, hi - len);
|
|
22
|
+
}
|
|
23
|
+
function placeMenu(size, anchor, bounds) {
|
|
24
|
+
return {
|
|
25
|
+
x: flipOrShift(anchor.x, size.w, bounds.x, bounds.w),
|
|
26
|
+
y: flipOrShift(anchor.y, size.h, bounds.y, bounds.h)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
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 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/keys.ts
|
|
38
|
+
var NONE = { kind: "none" };
|
|
39
|
+
function firstRow(entries) {
|
|
40
|
+
const i = entries.findIndex(isRow);
|
|
41
|
+
return i < 0 ? null : i;
|
|
42
|
+
}
|
|
43
|
+
function step(entries, from, dir) {
|
|
44
|
+
const n = entries.length;
|
|
45
|
+
let i = from ?? (dir === 1 ? -1 : n);
|
|
46
|
+
for (let k = 0; k < n; k++) {
|
|
47
|
+
i = (i + dir + n) % n;
|
|
48
|
+
if (isRow(entries[i])) return i;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function withTop(stack, index) {
|
|
53
|
+
return [...stack.slice(0, -1), index];
|
|
54
|
+
}
|
|
55
|
+
function openSub(root, stack) {
|
|
56
|
+
const level = stack.length - 1;
|
|
57
|
+
const at = stack[level];
|
|
58
|
+
const e = at == null ? void 0 : entriesAt(root, stack, level)[at];
|
|
59
|
+
if (e?.kind !== "submenu" || e.disabled) return null;
|
|
60
|
+
return [...stack, firstRow(e.entries)];
|
|
61
|
+
}
|
|
62
|
+
function keyStep(root, stack, key) {
|
|
63
|
+
const level = stack.length - 1;
|
|
64
|
+
const rows = entriesAt(root, stack, level);
|
|
65
|
+
const at = stack[level] ?? null;
|
|
66
|
+
const done = (s, effect = NONE) => ({ stack: s, effect, handled: true });
|
|
67
|
+
switch (key) {
|
|
68
|
+
case "ArrowDown":
|
|
69
|
+
return done(withTop(stack, step(rows, at, 1)));
|
|
70
|
+
case "ArrowUp":
|
|
71
|
+
return done(withTop(stack, step(rows, at, -1)));
|
|
72
|
+
case "Home":
|
|
73
|
+
return done(withTop(stack, step(rows, null, 1)));
|
|
74
|
+
case "End":
|
|
75
|
+
return done(withTop(stack, step(rows, null, -1)));
|
|
76
|
+
case "ArrowRight":
|
|
77
|
+
return done(openSub(root, stack) ?? stack);
|
|
78
|
+
case "ArrowLeft":
|
|
79
|
+
return done(level > 0 ? stack.slice(0, -1) : stack);
|
|
80
|
+
case "Escape":
|
|
81
|
+
return level > 0 ? done(stack.slice(0, -1)) : done(stack, { kind: "close" });
|
|
82
|
+
case "Tab":
|
|
83
|
+
return done(stack, { kind: "close" });
|
|
84
|
+
case "Enter":
|
|
85
|
+
case " ": {
|
|
86
|
+
const e = at == null ? void 0 : rows[at];
|
|
87
|
+
if (e?.kind === "submenu") return done(openSub(root, stack) ?? stack);
|
|
88
|
+
if (e?.kind === "item" && !e.disabled) return done(stack, { kind: "choose", level, index: at });
|
|
89
|
+
return done(stack);
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
return { stack, effect: NONE, handled: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/typeahead.ts
|
|
97
|
+
function typeaheadMatch(entries, from, query) {
|
|
98
|
+
const q = query.toLowerCase();
|
|
99
|
+
const n = entries.length;
|
|
100
|
+
const start = from == null ? 0 : q.length === 1 ? from + 1 : from;
|
|
101
|
+
for (let k = 0; k < n; k++) {
|
|
102
|
+
const i = (start + k) % n;
|
|
103
|
+
const e = entries[i];
|
|
104
|
+
if (isRow(e) && e.label.toLowerCase().startsWith(q)) return i;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
var TYPEAHEAD_RESET_MS = 500;
|
|
109
|
+
function typeaheadStep(entries, from, buffer, key, now) {
|
|
110
|
+
const prev = now - buffer.at < TYPEAHEAD_RESET_MS ? buffer.text : "";
|
|
111
|
+
const k = key.toLowerCase();
|
|
112
|
+
const cycling = prev !== "" && [...prev].every((c) => c.toLowerCase() === k);
|
|
113
|
+
const text = cycling ? key : prev + key;
|
|
114
|
+
return { buffer: { text, at: now }, hit: typeaheadMatch(entries, from, text) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/triangle.ts
|
|
118
|
+
var cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
119
|
+
function inTriangle(p, a, b, c) {
|
|
120
|
+
const d1 = cross(p, a, b), d2 = cross(p, b, c), d3 = cross(p, c, a);
|
|
121
|
+
const neg = d1 < 0 || d2 < 0 || d3 < 0;
|
|
122
|
+
const pos = d1 > 0 || d2 > 0 || d3 > 0;
|
|
123
|
+
return !(neg && pos);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/controller.ts
|
|
127
|
+
var SUBMENU_OPEN_MS = 200;
|
|
128
|
+
var DIAGONAL_GRACE_MS = 300;
|
|
129
|
+
var timerScheduler = {
|
|
130
|
+
now: () => Date.now(),
|
|
131
|
+
set: (fn, ms) => {
|
|
132
|
+
const t = setTimeout(fn, ms);
|
|
133
|
+
return () => clearTimeout(t);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
function createMenuController(opts) {
|
|
137
|
+
const { entries: root, onClose } = opts;
|
|
138
|
+
const sched = opts.scheduler ?? timerScheduler;
|
|
139
|
+
let stack = [opts.highlightFirst ? firstRow(root) : null];
|
|
140
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
141
|
+
const panels = /* @__PURE__ */ new Map();
|
|
142
|
+
let pending = null;
|
|
143
|
+
let pendingKind = null;
|
|
144
|
+
let lastPoint = null;
|
|
145
|
+
let deferred = null;
|
|
146
|
+
let typed = { text: "", at: -Infinity };
|
|
147
|
+
let closed = false;
|
|
148
|
+
const set = (next) => {
|
|
149
|
+
stack = next;
|
|
150
|
+
for (const level of [...panels.keys()]) if (level >= next.length) panels.delete(level);
|
|
151
|
+
for (const fn of listeners) fn();
|
|
152
|
+
};
|
|
153
|
+
const cancel = () => {
|
|
154
|
+
pending?.();
|
|
155
|
+
pending = null;
|
|
156
|
+
pendingKind = null;
|
|
157
|
+
};
|
|
158
|
+
const later = (kind, fn, ms) => {
|
|
159
|
+
cancel();
|
|
160
|
+
pendingKind = kind;
|
|
161
|
+
pending = sched.set(() => {
|
|
162
|
+
pending = null;
|
|
163
|
+
pendingKind = null;
|
|
164
|
+
if (!closed) fn();
|
|
165
|
+
}, ms);
|
|
166
|
+
};
|
|
167
|
+
const entryAt = (level, index) => entriesAt(root, stack, level)[index];
|
|
168
|
+
const isRowAt = (level, index) => {
|
|
169
|
+
const e = entryAt(level, index);
|
|
170
|
+
return e !== void 0 && isRow(e);
|
|
171
|
+
};
|
|
172
|
+
const finish = () => {
|
|
173
|
+
closed = true;
|
|
174
|
+
cancel();
|
|
175
|
+
onClose();
|
|
176
|
+
};
|
|
177
|
+
const choose = (level, index) => {
|
|
178
|
+
const e = entryAt(level, index);
|
|
179
|
+
if (e?.kind !== "item" || e.disabled) return;
|
|
180
|
+
finish();
|
|
181
|
+
e.run();
|
|
182
|
+
};
|
|
183
|
+
const openAt = (level, index) => {
|
|
184
|
+
const e = entryAt(level, index);
|
|
185
|
+
const base = [...stack.slice(0, level), index];
|
|
186
|
+
set(e?.kind === "submenu" && !e.disabled ? [...base, null] : base);
|
|
187
|
+
};
|
|
188
|
+
const headingIntoChild = (level, p) => {
|
|
189
|
+
const child = panels.get(level + 1);
|
|
190
|
+
const parent = panels.get(level);
|
|
191
|
+
if (!child || !parent || !lastPoint || stack.length <= level + 1) return false;
|
|
192
|
+
const edge = child.x >= parent.x ? child.x : child.x + child.w;
|
|
193
|
+
return inTriangle(p, lastPoint, { x: edge, y: child.y }, { x: edge, y: child.y + child.h });
|
|
194
|
+
};
|
|
195
|
+
const hoverNow = (level, index) => {
|
|
196
|
+
cancel();
|
|
197
|
+
if (index === null && stack.length !== level + 1) return;
|
|
198
|
+
const next = [...stack.slice(0, level), index];
|
|
199
|
+
if (next.length !== stack.length || next[level] !== stack[level]) set(next);
|
|
200
|
+
if (index === null) return;
|
|
201
|
+
const e = entryAt(level, index);
|
|
202
|
+
if (e?.kind === "submenu" && !e.disabled) later("open", () => openAt(level, index), SUBMENU_OPEN_MS);
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
stack: () => stack,
|
|
206
|
+
entriesAt: (level) => entriesAt(root, stack, level),
|
|
207
|
+
key(key) {
|
|
208
|
+
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;
|
|
221
|
+
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]);
|
|
226
|
+
return true;
|
|
227
|
+
},
|
|
228
|
+
hover(level, index, point) {
|
|
229
|
+
if (closed || level >= stack.length) return;
|
|
230
|
+
if (index !== null && !isRowAt(level, index)) return;
|
|
231
|
+
const sameRow = index === stack[level];
|
|
232
|
+
if (point && !sameRow && headingIntoChild(level, point)) {
|
|
233
|
+
deferred = { level, index };
|
|
234
|
+
if (pendingKind !== "diagonal") {
|
|
235
|
+
later("diagonal", () => {
|
|
236
|
+
if (deferred) hoverNow(deferred.level, deferred.index);
|
|
237
|
+
}, DIAGONAL_GRACE_MS);
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (point) lastPoint = point;
|
|
242
|
+
if (sameRow && stack.length > level + 1) {
|
|
243
|
+
cancel();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (sameRow && pendingKind === "open") return;
|
|
247
|
+
hoverNow(level, index);
|
|
248
|
+
},
|
|
249
|
+
press(level, index) {
|
|
250
|
+
if (closed || level >= stack.length || !isRowAt(level, index)) return;
|
|
251
|
+
if (stack[level] === index && stack.length > level + 1) return;
|
|
252
|
+
cancel();
|
|
253
|
+
if (entryAt(level, index)?.kind === "item") choose(level, index);
|
|
254
|
+
else openAt(level, index);
|
|
255
|
+
},
|
|
256
|
+
setPanel(level, rect) {
|
|
257
|
+
panels.set(level, rect);
|
|
258
|
+
},
|
|
259
|
+
subscribe(fn) {
|
|
260
|
+
listeners.add(fn);
|
|
261
|
+
return () => {
|
|
262
|
+
listeners.delete(fn);
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
dispose() {
|
|
266
|
+
closed = true;
|
|
267
|
+
cancel();
|
|
268
|
+
listeners.clear();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export { DIAGONAL_GRACE_MS, SUBMENU_OPEN_MS, TYPEAHEAD_RESET_MS, createMenuController, entriesAt, firstRow, inTriangle, isRow, keyStep, placeMenu, placeSubmenu, timerScheduler, typeaheadMatch };
|
|
274
|
+
//# sourceMappingURL=index.js.map
|
|
275
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/menu",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.10.1",
|
|
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
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"provenance": true
|
|
42
|
+
}
|
|
43
|
+
}
|