@yorozu/context-menu 0.5.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 +21 -0
- package/default.css +87 -0
- package/history.d.ts +11 -0
- package/index.d.ts +14 -0
- package/index.js +603 -0
- package/keyboard.d.ts +2 -0
- package/long-press.d.ts +11 -0
- package/package.json +28 -0
- package/place-above.d.ts +18 -0
- package/place-fixed.d.ts +29 -0
- package/popover.d.ts +7 -0
- package/session.d.ts +28 -0
- package/submenu.d.ts +27 -0
- package/tokens.css +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan
|
|
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/default.css
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
@import "./tokens.css";
|
|
2
|
+
|
|
3
|
+
/* Compact default layout for [data-yorozu-menu]. Host supplies item markup. */
|
|
4
|
+
[data-yorozu-menu] {
|
|
5
|
+
position: fixed;
|
|
6
|
+
z-index: var(--yorozu-menu-z);
|
|
7
|
+
min-width: var(--yorozu-menu-min-width);
|
|
8
|
+
padding-block: var(--yorozu-menu-pad-block);
|
|
9
|
+
border-radius: var(--yorozu-menu-radius);
|
|
10
|
+
color: var(--yorozu-menu-color);
|
|
11
|
+
font-size: var(--yorozu-menu-font-size);
|
|
12
|
+
font-weight: var(--yorozu-menu-font-weight);
|
|
13
|
+
font-family: var(--yorozu-menu-font-family);
|
|
14
|
+
background: var(--yorozu-menu-bg);
|
|
15
|
+
box-shadow: var(--yorozu-menu-shadow);
|
|
16
|
+
backdrop-filter: blur(var(--yorozu-menu-blur));
|
|
17
|
+
opacity: 0;
|
|
18
|
+
outline: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
[data-yorozu-menu] [role="menuitem"],
|
|
22
|
+
[data-yorozu-menu] [role="menuitemcheckbox"] {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: row;
|
|
25
|
+
align-items: center;
|
|
26
|
+
gap: var(--yorozu-menu-item-gap);
|
|
27
|
+
margin: var(--yorozu-menu-item-margin);
|
|
28
|
+
padding: var(--yorozu-menu-item-padding);
|
|
29
|
+
padding-inline-end: var(--yorozu-menu-item-padding-inline-end);
|
|
30
|
+
border-radius: var(--yorozu-menu-item-radius);
|
|
31
|
+
min-block-size: var(--yorozu-menu-item-min-block);
|
|
32
|
+
outline: none;
|
|
33
|
+
box-shadow: none;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[data-yorozu-menu] [role="menuitem"]:hover,
|
|
38
|
+
[data-yorozu-menu] [role="menuitem"]:focus,
|
|
39
|
+
[data-yorozu-menu] [role="menuitem"]:focus-visible,
|
|
40
|
+
[data-yorozu-menu] [role="menuitemcheckbox"]:hover,
|
|
41
|
+
[data-yorozu-menu] [role="menuitemcheckbox"]:focus,
|
|
42
|
+
[data-yorozu-menu] [role="menuitemcheckbox"]:focus-visible {
|
|
43
|
+
outline: none;
|
|
44
|
+
box-shadow: none;
|
|
45
|
+
background: var(--yorozu-menu-item-hover);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[data-yorozu-menu] [role="menuitem"]:active:not(.disabled):not([aria-disabled="true"]),
|
|
49
|
+
[data-yorozu-menu] [role="menuitemcheckbox"]:active:not(.disabled):not([aria-disabled="true"]) {
|
|
50
|
+
transform: scale(var(--yorozu-menu-item-active-scale));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
[data-yorozu-menu] [role="menuitem"].disabled,
|
|
54
|
+
[data-yorozu-menu] [role="menuitem"][aria-disabled="true"],
|
|
55
|
+
[data-yorozu-menu] [role="menuitemcheckbox"].disabled,
|
|
56
|
+
[data-yorozu-menu] [role="menuitemcheckbox"][aria-disabled="true"] {
|
|
57
|
+
opacity: var(--yorozu-menu-disabled-opacity);
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
[data-yorozu-menu] [role="menuitem"].disabled:hover,
|
|
62
|
+
[data-yorozu-menu] [role="menuitem"].disabled:focus,
|
|
63
|
+
[data-yorozu-menu] [role="menuitem"].disabled:focus-visible,
|
|
64
|
+
[data-yorozu-menu] [role="menuitem"][aria-disabled="true"]:hover,
|
|
65
|
+
[data-yorozu-menu] [role="menuitem"][aria-disabled="true"]:focus,
|
|
66
|
+
[data-yorozu-menu] [role="menuitem"][aria-disabled="true"]:focus-visible,
|
|
67
|
+
[data-yorozu-menu] [role="menuitemcheckbox"].disabled:hover,
|
|
68
|
+
[data-yorozu-menu] [role="menuitemcheckbox"].disabled:focus,
|
|
69
|
+
[data-yorozu-menu] [role="menuitemcheckbox"].disabled:focus-visible,
|
|
70
|
+
[data-yorozu-menu] [role="menuitemcheckbox"][aria-disabled="true"]:hover,
|
|
71
|
+
[data-yorozu-menu] [role="menuitemcheckbox"][aria-disabled="true"]:focus,
|
|
72
|
+
[data-yorozu-menu] [role="menuitemcheckbox"][aria-disabled="true"]:focus-visible {
|
|
73
|
+
background: transparent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
[data-yorozu-menu] .destructive,
|
|
77
|
+
[data-yorozu-menu] [role="menuitem"].destructive,
|
|
78
|
+
[data-yorozu-menu] [role="menuitemcheckbox"].destructive {
|
|
79
|
+
color: var(--yorozu-menu-destructive);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
[data-yorozu-menu] [role="separator"] {
|
|
83
|
+
height: 1px;
|
|
84
|
+
margin: var(--yorozu-menu-divider-margin);
|
|
85
|
+
background: var(--yorozu-menu-divider);
|
|
86
|
+
border: none;
|
|
87
|
+
}
|
package/history.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const MENU_HISTORY_STATE: {
|
|
2
|
+
yorozuMenu: 1;
|
|
3
|
+
};
|
|
4
|
+
export declare function isMenuHistoryState(state: unknown, marker?: Record<string, unknown>): boolean;
|
|
5
|
+
export type HistoryLayer = {
|
|
6
|
+
release: () => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function bindHistoryLayer(opts: {
|
|
9
|
+
onBack: () => void;
|
|
10
|
+
state?: Record<string, unknown>;
|
|
11
|
+
}): HistoryLayer;
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { MENU_VIEW_MARGIN_PX, MENU_POINTER_NUDGE_PX, MENU_MAX_HEIGHT_BOTTOM_MARGIN_PX, placeFixedMenu, } from './place-fixed';
|
|
2
|
+
export type { MenuPositionX, MenuPositionY, PlaceFixedMenuInput, PlaceFixedMenuResult } from './place-fixed';
|
|
3
|
+
export { COMPOSER_MENU_GAP_PX, placeAboveAnchor } from './place-above';
|
|
4
|
+
export type { MenuAlign, AnchorBox, ViewportBox, AboveAnchorPlacement } from './place-above';
|
|
5
|
+
export { MENU_FOCUSABLE_SELECTOR, moveMenuFocus } from './keyboard';
|
|
6
|
+
export { MENU_SUBMENU_DELAY_MS, createSubmenuOpenRegistry, createSubmenuHover } from './submenu';
|
|
7
|
+
export type { SubmenuAnchor, SubmenuOpenRegistry, SubmenuHover } from './submenu';
|
|
8
|
+
export { MENU_LONG_PRESS_MS, MENU_LONG_PRESS_SWALLOW_MS, bindLongPress } from './long-press';
|
|
9
|
+
export type { LongPressBinding } from './long-press';
|
|
10
|
+
export { MENU_HISTORY_STATE, isMenuHistoryState, bindHistoryLayer } from './history';
|
|
11
|
+
export type { HistoryLayer } from './history';
|
|
12
|
+
export { MENU_POPOVER_OPEN_MS, MENU_POPOVER_CLOSE_MS, MENU_POPOVER_OPEN_EASING, MENU_POPOVER_CLOSE_EASING, MENU_POPOVER_SCALE, createMenuPopover, } from './popover';
|
|
13
|
+
export { createMenuSession } from './session';
|
|
14
|
+
export type { PlacePointerOpts, MenuSessionOpts, MenuSession } from './session';
|
package/index.js
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
//#region src/place-fixed.ts
|
|
2
|
+
var MENU_VIEW_MARGIN_PX = 16;
|
|
3
|
+
var MENU_POINTER_NUDGE_PX = 3;
|
|
4
|
+
var MENU_MAX_HEIGHT_BOTTOM_MARGIN_PX = 12;
|
|
5
|
+
function clamp(n, min, max) {
|
|
6
|
+
if (max < min) return min;
|
|
7
|
+
return Math.min(Math.max(n, min), max);
|
|
8
|
+
}
|
|
9
|
+
function placeFixedMenu(input) {
|
|
10
|
+
let margin = input.margin ?? 16;
|
|
11
|
+
let extraMinWidth = input.extraMinWidth ?? 0;
|
|
12
|
+
let extraTopSpace = input.extraTopSpace ?? 0;
|
|
13
|
+
let extraPaddingX = input.extraPaddingX ?? 0;
|
|
14
|
+
let width = Math.max(input.menuWidth, extraMinWidth);
|
|
15
|
+
let height = input.menuHeight;
|
|
16
|
+
let positionX;
|
|
17
|
+
if (input.anchorX + width + extraPaddingX < input.viewportWidth) positionX = "left";
|
|
18
|
+
else if (input.anchorX - width > 0) positionX = "right";
|
|
19
|
+
else positionX = "left";
|
|
20
|
+
let positionY = input.anchorY + height < input.viewportHeight ? "top" : "bottom";
|
|
21
|
+
let left = positionX === "left" ? input.anchorX + 3 : input.anchorX - width;
|
|
22
|
+
let top = positionY === "top" ? input.anchorY : input.anchorY - height;
|
|
23
|
+
left = clamp(left, margin, input.viewportWidth - width - margin);
|
|
24
|
+
top = clamp(top, margin + extraTopSpace, input.viewportHeight - height - margin);
|
|
25
|
+
let originX = input.anchorX - left;
|
|
26
|
+
let originY = input.anchorY - top;
|
|
27
|
+
let origin = `${originX}px ${originY}px`;
|
|
28
|
+
let maxHeight = input.withMaxHeight ? Math.max(0, input.viewportHeight - 12) : void 0;
|
|
29
|
+
return {
|
|
30
|
+
left,
|
|
31
|
+
top,
|
|
32
|
+
positionX,
|
|
33
|
+
positionY,
|
|
34
|
+
originX,
|
|
35
|
+
originY,
|
|
36
|
+
origin,
|
|
37
|
+
maxHeight
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/place-above.ts
|
|
42
|
+
var COMPOSER_MENU_GAP_PX = 8;
|
|
43
|
+
function placeAboveAnchor(anchor, align, viewport, gapPx) {
|
|
44
|
+
let vp = viewport ?? {
|
|
45
|
+
width: window.innerWidth,
|
|
46
|
+
height: window.innerHeight
|
|
47
|
+
};
|
|
48
|
+
let gap = gapPx ?? 8;
|
|
49
|
+
let bottom = Math.max(0, vp.height - anchor.top + gap);
|
|
50
|
+
if (align === "end") return {
|
|
51
|
+
bottom,
|
|
52
|
+
left: void 0,
|
|
53
|
+
right: Math.max(0, vp.width - anchor.right),
|
|
54
|
+
origin: "bottom right"
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
bottom,
|
|
58
|
+
left: Math.max(0, anchor.left),
|
|
59
|
+
right: void 0,
|
|
60
|
+
origin: "bottom left"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/keyboard.ts
|
|
65
|
+
var MENU_FOCUSABLE_SELECTOR = "[role=\"menuitem\"]:not(.disabled):not([aria-disabled=\"true\"]), [role=\"menuitemcheckbox\"]:not(.disabled):not([aria-disabled=\"true\"])";
|
|
66
|
+
function moveMenuFocus(root, direction) {
|
|
67
|
+
let items = Array.from(root.querySelectorAll(MENU_FOCUSABLE_SELECTOR));
|
|
68
|
+
if (items.length === 0) return;
|
|
69
|
+
let activeIndex = items.indexOf(document.activeElement);
|
|
70
|
+
let nextIndex;
|
|
71
|
+
if (activeIndex < 0) nextIndex = direction === 1 ? 0 : items.length - 1;
|
|
72
|
+
else nextIndex = Math.min(Math.max(activeIndex + direction, 0), items.length - 1);
|
|
73
|
+
let item = items[nextIndex];
|
|
74
|
+
item.focus({ preventScroll: true });
|
|
75
|
+
item.scrollIntoView({ block: "nearest" });
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/submenu.ts
|
|
79
|
+
var MENU_SUBMENU_DELAY_MS = 150;
|
|
80
|
+
function createSubmenuOpenRegistry() {
|
|
81
|
+
let current = null;
|
|
82
|
+
return {
|
|
83
|
+
registerOpen(closeFn) {
|
|
84
|
+
if (current === closeFn) return;
|
|
85
|
+
current?.();
|
|
86
|
+
current = closeFn;
|
|
87
|
+
},
|
|
88
|
+
unregister(closeFn) {
|
|
89
|
+
if (current === closeFn) current = null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function createSubmenuHover(opts) {
|
|
94
|
+
let openTimer;
|
|
95
|
+
let closeTimer;
|
|
96
|
+
let stickyUntilEnter = false;
|
|
97
|
+
function clearOpenTimer() {
|
|
98
|
+
if (openTimer != null) {
|
|
99
|
+
clearTimeout(openTimer);
|
|
100
|
+
openTimer = void 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function clearCloseTimer() {
|
|
104
|
+
if (closeTimer != null) {
|
|
105
|
+
clearTimeout(closeTimer);
|
|
106
|
+
closeTimer = void 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function open() {
|
|
110
|
+
let rect = opts.getRect();
|
|
111
|
+
if (rect == null) return;
|
|
112
|
+
clearOpenTimer();
|
|
113
|
+
clearCloseTimer();
|
|
114
|
+
opts.setOpen({
|
|
115
|
+
x: rect.right - 16,
|
|
116
|
+
y: rect.top
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function close() {
|
|
120
|
+
stickyUntilEnter = false;
|
|
121
|
+
clearOpenTimer();
|
|
122
|
+
clearCloseTimer();
|
|
123
|
+
opts.setOpen(null);
|
|
124
|
+
}
|
|
125
|
+
function openFromClick() {
|
|
126
|
+
stickyUntilEnter = true;
|
|
127
|
+
open();
|
|
128
|
+
}
|
|
129
|
+
function scheduleOpen() {
|
|
130
|
+
clearCloseTimer();
|
|
131
|
+
if (opts.isOpen() || openTimer != null) return;
|
|
132
|
+
openTimer = setTimeout(() => {
|
|
133
|
+
openTimer = void 0;
|
|
134
|
+
open();
|
|
135
|
+
}, 150);
|
|
136
|
+
}
|
|
137
|
+
function scheduleClose() {
|
|
138
|
+
clearOpenTimer();
|
|
139
|
+
if (stickyUntilEnter) return;
|
|
140
|
+
if (closeTimer != null) return;
|
|
141
|
+
closeTimer = setTimeout(() => {
|
|
142
|
+
closeTimer = void 0;
|
|
143
|
+
stickyUntilEnter = false;
|
|
144
|
+
opts.setOpen(null);
|
|
145
|
+
}, 150);
|
|
146
|
+
}
|
|
147
|
+
function cancelClose() {
|
|
148
|
+
stickyUntilEnter = false;
|
|
149
|
+
clearCloseTimer();
|
|
150
|
+
}
|
|
151
|
+
function clear() {
|
|
152
|
+
stickyUntilEnter = false;
|
|
153
|
+
clearOpenTimer();
|
|
154
|
+
clearCloseTimer();
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
open,
|
|
158
|
+
openFromClick,
|
|
159
|
+
close,
|
|
160
|
+
scheduleOpen,
|
|
161
|
+
scheduleClose,
|
|
162
|
+
cancelClose,
|
|
163
|
+
clear
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/long-press.ts
|
|
168
|
+
var MENU_LONG_PRESS_MS = 200;
|
|
169
|
+
var MENU_LONG_PRESS_SWALLOW_MS = 400;
|
|
170
|
+
function bindLongPress(node, opts) {
|
|
171
|
+
let enabled = opts?.enabled !== false;
|
|
172
|
+
let pressTimer;
|
|
173
|
+
let swallowTimer;
|
|
174
|
+
let attached = false;
|
|
175
|
+
function clearPressTimer() {
|
|
176
|
+
if (pressTimer != null) {
|
|
177
|
+
clearTimeout(pressTimer);
|
|
178
|
+
pressTimer = void 0;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function removeSwallow() {
|
|
182
|
+
if (swallowTimer != null) {
|
|
183
|
+
clearTimeout(swallowTimer);
|
|
184
|
+
swallowTimer = void 0;
|
|
185
|
+
}
|
|
186
|
+
node.removeEventListener("touchend", swallow, true);
|
|
187
|
+
node.removeEventListener("click", swallow, true);
|
|
188
|
+
}
|
|
189
|
+
function swallow(event) {
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
event.stopPropagation();
|
|
192
|
+
}
|
|
193
|
+
function armSwallow() {
|
|
194
|
+
removeSwallow();
|
|
195
|
+
node.addEventListener("touchend", swallow, true);
|
|
196
|
+
node.addEventListener("click", swallow, true);
|
|
197
|
+
swallowTimer = setTimeout(() => {
|
|
198
|
+
swallowTimer = void 0;
|
|
199
|
+
removeSwallow();
|
|
200
|
+
}, 400);
|
|
201
|
+
}
|
|
202
|
+
function onTouchStart(event) {
|
|
203
|
+
if (!enabled) return;
|
|
204
|
+
clearPressTimer();
|
|
205
|
+
let touch = event.changedTouches[0] ?? event.touches[0];
|
|
206
|
+
if (touch == null) return;
|
|
207
|
+
let clientX = touch.clientX;
|
|
208
|
+
let clientY = touch.clientY;
|
|
209
|
+
pressTimer = setTimeout(() => {
|
|
210
|
+
pressTimer = void 0;
|
|
211
|
+
node.dispatchEvent(new MouseEvent("contextmenu", {
|
|
212
|
+
bubbles: true,
|
|
213
|
+
cancelable: true,
|
|
214
|
+
clientX,
|
|
215
|
+
clientY
|
|
216
|
+
}));
|
|
217
|
+
armSwallow();
|
|
218
|
+
}, 200);
|
|
219
|
+
}
|
|
220
|
+
function onMousePointerDown(event) {
|
|
221
|
+
if (event.pointerType === "mouse") clearPressTimer();
|
|
222
|
+
}
|
|
223
|
+
function attach() {
|
|
224
|
+
if (attached) return;
|
|
225
|
+
attached = true;
|
|
226
|
+
node.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
227
|
+
node.addEventListener("touchmove", clearPressTimer);
|
|
228
|
+
node.addEventListener("touchend", clearPressTimer);
|
|
229
|
+
node.addEventListener("touchcancel", clearPressTimer);
|
|
230
|
+
node.addEventListener("pointerdown", onMousePointerDown);
|
|
231
|
+
}
|
|
232
|
+
function detach() {
|
|
233
|
+
if (!attached) return;
|
|
234
|
+
attached = false;
|
|
235
|
+
clearPressTimer();
|
|
236
|
+
removeSwallow();
|
|
237
|
+
node.removeEventListener("touchstart", onTouchStart);
|
|
238
|
+
node.removeEventListener("touchmove", clearPressTimer);
|
|
239
|
+
node.removeEventListener("touchend", clearPressTimer);
|
|
240
|
+
node.removeEventListener("touchcancel", clearPressTimer);
|
|
241
|
+
node.removeEventListener("pointerdown", onMousePointerDown);
|
|
242
|
+
}
|
|
243
|
+
if (enabled) attach();
|
|
244
|
+
return {
|
|
245
|
+
update(o) {
|
|
246
|
+
let next = o?.enabled !== false;
|
|
247
|
+
if (next === enabled) return;
|
|
248
|
+
enabled = next;
|
|
249
|
+
if (enabled) attach();
|
|
250
|
+
else detach();
|
|
251
|
+
},
|
|
252
|
+
destroy() {
|
|
253
|
+
enabled = false;
|
|
254
|
+
detach();
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/history.ts
|
|
260
|
+
var MENU_HISTORY_STATE = { yorozuMenu: 1 };
|
|
261
|
+
function isMenuHistoryState(state, marker) {
|
|
262
|
+
if (state == null || typeof state !== "object") return false;
|
|
263
|
+
let expected = marker ?? MENU_HISTORY_STATE;
|
|
264
|
+
let record = state;
|
|
265
|
+
for (let key of Object.keys(expected)) if (record[key] !== expected[key]) return false;
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
function bindHistoryLayer(opts) {
|
|
269
|
+
let marker = opts.state ?? MENU_HISTORY_STATE;
|
|
270
|
+
let released = false;
|
|
271
|
+
history.pushState(marker, "");
|
|
272
|
+
function onPopState() {
|
|
273
|
+
if (released) return;
|
|
274
|
+
released = true;
|
|
275
|
+
window.removeEventListener("popstate", onPopState);
|
|
276
|
+
opts.onBack();
|
|
277
|
+
}
|
|
278
|
+
window.addEventListener("popstate", onPopState);
|
|
279
|
+
return { release() {
|
|
280
|
+
if (released) return;
|
|
281
|
+
released = true;
|
|
282
|
+
window.removeEventListener("popstate", onPopState);
|
|
283
|
+
if (isMenuHistoryState(history.state, marker)) history.go(-1);
|
|
284
|
+
} };
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region ../animations/src/core/playback.ts
|
|
288
|
+
function createPlayback() {
|
|
289
|
+
let cancelled = false;
|
|
290
|
+
let settled = false;
|
|
291
|
+
let resolveDone;
|
|
292
|
+
let done = new Promise((resolve) => {
|
|
293
|
+
resolveDone = resolve;
|
|
294
|
+
});
|
|
295
|
+
let resolve = (ran) => {
|
|
296
|
+
if (settled) return;
|
|
297
|
+
settled = true;
|
|
298
|
+
resolveDone(cancelled ? false : ran);
|
|
299
|
+
};
|
|
300
|
+
return {
|
|
301
|
+
playback: {
|
|
302
|
+
done,
|
|
303
|
+
cancel: () => {
|
|
304
|
+
cancelled = true;
|
|
305
|
+
resolve(false);
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
resolve,
|
|
309
|
+
isCancelled: () => cancelled
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function animateElement(el, keyframes, options) {
|
|
313
|
+
if (typeof el.animate !== "function") return null;
|
|
314
|
+
return el.animate(keyframes, options);
|
|
315
|
+
}
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region ../animations/src/core/styles.ts
|
|
318
|
+
function applyStyles(el, styles) {
|
|
319
|
+
for (let key in styles) el.style.setProperty(key, styles[key]);
|
|
320
|
+
}
|
|
321
|
+
function clearStyles(el, keys) {
|
|
322
|
+
for (let key of keys) el.style.removeProperty(key);
|
|
323
|
+
}
|
|
324
|
+
function closedFrame(scale) {
|
|
325
|
+
return {
|
|
326
|
+
transform: `scale(${scale})`,
|
|
327
|
+
opacity: "0"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
var OPEN = {
|
|
331
|
+
transform: "scale(1)",
|
|
332
|
+
opacity: "1"
|
|
333
|
+
};
|
|
334
|
+
var STYLE_KEYS = ["will-change"];
|
|
335
|
+
function createPopover(config) {
|
|
336
|
+
let lastOrigin = config?.origin ?? "center top";
|
|
337
|
+
let lastScale = config?.scale ?? .92;
|
|
338
|
+
let defaultDuration = config?.durationMs ?? 120;
|
|
339
|
+
let defaultEasing = config?.easing ?? "ease-out";
|
|
340
|
+
let current = null;
|
|
341
|
+
let stopCurrent = () => {
|
|
342
|
+
if (!current) return;
|
|
343
|
+
current.anim?.cancel();
|
|
344
|
+
current.resolve(false);
|
|
345
|
+
current = null;
|
|
346
|
+
};
|
|
347
|
+
let play = (el, opening, options) => {
|
|
348
|
+
stopCurrent();
|
|
349
|
+
let { playback, resolve, isCancelled } = createPlayback();
|
|
350
|
+
let durationMs = options?.durationMs ?? defaultDuration;
|
|
351
|
+
let easing = options?.easing ?? defaultEasing;
|
|
352
|
+
if (options?.origin != null) lastOrigin = options.origin;
|
|
353
|
+
if (options?.scale != null) lastScale = options.scale;
|
|
354
|
+
let origin = options?.origin ?? lastOrigin;
|
|
355
|
+
let closed = closedFrame(options?.scale ?? lastScale);
|
|
356
|
+
applyStyles(el, { "transform-origin": origin });
|
|
357
|
+
let from = opening ? closed : OPEN;
|
|
358
|
+
let to = opening ? OPEN : closed;
|
|
359
|
+
if (durationMs <= 0) {
|
|
360
|
+
applyStyles(el, {
|
|
361
|
+
transform: to.transform,
|
|
362
|
+
opacity: to.opacity
|
|
363
|
+
});
|
|
364
|
+
resolve(true);
|
|
365
|
+
return playback;
|
|
366
|
+
}
|
|
367
|
+
applyStyles(el, {
|
|
368
|
+
transform: from.transform,
|
|
369
|
+
opacity: from.opacity,
|
|
370
|
+
"will-change": "transform, opacity"
|
|
371
|
+
});
|
|
372
|
+
let anim = animateElement(el, [from, to], {
|
|
373
|
+
duration: durationMs,
|
|
374
|
+
easing,
|
|
375
|
+
fill: "forwards"
|
|
376
|
+
});
|
|
377
|
+
let run = {
|
|
378
|
+
anim,
|
|
379
|
+
resolve
|
|
380
|
+
};
|
|
381
|
+
current = run;
|
|
382
|
+
let finish = (ran) => {
|
|
383
|
+
if (current !== run) return;
|
|
384
|
+
current = null;
|
|
385
|
+
clearStyles(el, STYLE_KEYS);
|
|
386
|
+
applyStyles(el, {
|
|
387
|
+
transform: to.transform,
|
|
388
|
+
opacity: to.opacity
|
|
389
|
+
});
|
|
390
|
+
resolve(ran);
|
|
391
|
+
};
|
|
392
|
+
let cancel = playback.cancel;
|
|
393
|
+
playback.cancel = () => {
|
|
394
|
+
if (current === run) {
|
|
395
|
+
anim?.cancel();
|
|
396
|
+
current = null;
|
|
397
|
+
clearStyles(el, STYLE_KEYS);
|
|
398
|
+
}
|
|
399
|
+
cancel();
|
|
400
|
+
};
|
|
401
|
+
if (!anim) {
|
|
402
|
+
finish(true);
|
|
403
|
+
return playback;
|
|
404
|
+
}
|
|
405
|
+
anim.finished.then(() => finish(true), () => {
|
|
406
|
+
if (!isCancelled() && current === run) finish(false);
|
|
407
|
+
});
|
|
408
|
+
return playback;
|
|
409
|
+
};
|
|
410
|
+
return {
|
|
411
|
+
playOpen: (el, options) => play(el, true, options),
|
|
412
|
+
playClose: (el, options) => play(el, false, options)
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region src/popover.ts
|
|
417
|
+
var MENU_POPOVER_OPEN_MS = 150;
|
|
418
|
+
var MENU_POPOVER_CLOSE_MS = 200;
|
|
419
|
+
var MENU_POPOVER_OPEN_EASING = "cubic-bezier(0.2, 0, 0.2, 1)";
|
|
420
|
+
var MENU_POPOVER_CLOSE_EASING = "ease-in";
|
|
421
|
+
var MENU_POPOVER_SCALE = .85;
|
|
422
|
+
function createMenuPopover() {
|
|
423
|
+
let inner = createPopover({ scale: MENU_POPOVER_SCALE });
|
|
424
|
+
return {
|
|
425
|
+
playOpen: (el, options) => inner.playOpen(el, {
|
|
426
|
+
durationMs: 150,
|
|
427
|
+
easing: MENU_POPOVER_OPEN_EASING,
|
|
428
|
+
...options
|
|
429
|
+
}),
|
|
430
|
+
playClose: (el, options) => inner.playClose(el, {
|
|
431
|
+
durationMs: 200,
|
|
432
|
+
easing: MENU_POPOVER_CLOSE_EASING,
|
|
433
|
+
...options
|
|
434
|
+
})
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/session.ts
|
|
439
|
+
function createMenuSession(opts) {
|
|
440
|
+
let popover = createMenuPopover();
|
|
441
|
+
let el = null;
|
|
442
|
+
let closing = false;
|
|
443
|
+
let currentPlayback = null;
|
|
444
|
+
let rafId = null;
|
|
445
|
+
let historyLayer = null;
|
|
446
|
+
let insideSelector = opts.insideSelector ?? "[data-yorozu-menu]";
|
|
447
|
+
function durationFor(kind) {
|
|
448
|
+
let d = opts.getDurationMs?.(kind);
|
|
449
|
+
if (d === 0) return 0;
|
|
450
|
+
if (typeof d === "number" && d > 0) return d;
|
|
451
|
+
return kind === "open" ? 150 : 200;
|
|
452
|
+
}
|
|
453
|
+
function cancelRaf() {
|
|
454
|
+
if (rafId == null) return;
|
|
455
|
+
cancelAnimationFrame(rafId);
|
|
456
|
+
rafId = null;
|
|
457
|
+
}
|
|
458
|
+
function resetClosing() {
|
|
459
|
+
if (!closing) return;
|
|
460
|
+
currentPlayback?.cancel();
|
|
461
|
+
currentPlayback = null;
|
|
462
|
+
closing = false;
|
|
463
|
+
}
|
|
464
|
+
function finishClose() {
|
|
465
|
+
historyLayer?.release();
|
|
466
|
+
historyLayer = null;
|
|
467
|
+
}
|
|
468
|
+
function playOpen(origin) {
|
|
469
|
+
if (el == null) return;
|
|
470
|
+
currentPlayback = popover.playOpen(el, {
|
|
471
|
+
origin,
|
|
472
|
+
durationMs: durationFor("open"),
|
|
473
|
+
easing: MENU_POPOVER_OPEN_EASING
|
|
474
|
+
});
|
|
475
|
+
el.focus({ preventScroll: true });
|
|
476
|
+
}
|
|
477
|
+
function applyPointer(anchor, placeOpts) {
|
|
478
|
+
if (el == null) return;
|
|
479
|
+
let placed = placeFixedMenu({
|
|
480
|
+
anchorX: anchor.x,
|
|
481
|
+
anchorY: anchor.y,
|
|
482
|
+
menuWidth: el.offsetWidth,
|
|
483
|
+
menuHeight: el.offsetHeight,
|
|
484
|
+
viewportWidth: window.innerWidth,
|
|
485
|
+
viewportHeight: window.innerHeight,
|
|
486
|
+
margin: placeOpts?.margin,
|
|
487
|
+
extraMinWidth: placeOpts?.extraMinWidth,
|
|
488
|
+
extraTopSpace: placeOpts?.extraTopSpace,
|
|
489
|
+
extraPaddingX: placeOpts?.extraPaddingX,
|
|
490
|
+
withMaxHeight: placeOpts?.withMaxHeight
|
|
491
|
+
});
|
|
492
|
+
el.style.left = `${placed.left}px`;
|
|
493
|
+
el.style.top = `${placed.top}px`;
|
|
494
|
+
el.style.right = "unset";
|
|
495
|
+
el.style.bottom = "unset";
|
|
496
|
+
if (placed.maxHeight != null) {
|
|
497
|
+
el.style.setProperty("--yorozu-menu-max-height", `${placed.maxHeight}px`);
|
|
498
|
+
if (placeOpts?.applyMaxHeightStyle !== false) {
|
|
499
|
+
el.style.maxHeight = `${placed.maxHeight}px`;
|
|
500
|
+
el.style.overflow = "auto";
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
el.style.removeProperty("--yorozu-menu-max-height");
|
|
504
|
+
el.style.maxHeight = "";
|
|
505
|
+
el.style.overflow = "";
|
|
506
|
+
}
|
|
507
|
+
playOpen(placed.origin);
|
|
508
|
+
}
|
|
509
|
+
function close() {
|
|
510
|
+
if (closing) return;
|
|
511
|
+
closing = true;
|
|
512
|
+
cancelRaf();
|
|
513
|
+
if (el == null) {
|
|
514
|
+
finishClose();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
let playback = popover.playClose(el, {
|
|
518
|
+
durationMs: durationFor("close"),
|
|
519
|
+
easing: MENU_POPOVER_CLOSE_EASING
|
|
520
|
+
});
|
|
521
|
+
currentPlayback = playback;
|
|
522
|
+
playback.done.then((ran) => {
|
|
523
|
+
if (!ran || !closing || currentPlayback !== playback) return;
|
|
524
|
+
finishClose();
|
|
525
|
+
opts.onClose();
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function onPointerDown(event) {
|
|
529
|
+
if (closing) return;
|
|
530
|
+
if (event.target?.closest?.(insideSelector)) return;
|
|
531
|
+
close();
|
|
532
|
+
}
|
|
533
|
+
function onKeyDown(event) {
|
|
534
|
+
if (event.key === "Escape") close();
|
|
535
|
+
}
|
|
536
|
+
function attach(node) {
|
|
537
|
+
if (el === node) return;
|
|
538
|
+
if (el != null) {
|
|
539
|
+
document.removeEventListener("pointerdown", onPointerDown, false);
|
|
540
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
541
|
+
historyLayer?.release();
|
|
542
|
+
historyLayer = null;
|
|
543
|
+
}
|
|
544
|
+
el = node;
|
|
545
|
+
if (!opts.nested) historyLayer = bindHistoryLayer({
|
|
546
|
+
onBack: close,
|
|
547
|
+
state: opts.historyState
|
|
548
|
+
});
|
|
549
|
+
document.addEventListener("pointerdown", onPointerDown, false);
|
|
550
|
+
if (opts.listenEsc !== false) document.addEventListener("keydown", onKeyDown);
|
|
551
|
+
}
|
|
552
|
+
function placePointer(anchor, placeOpts) {
|
|
553
|
+
resetClosing();
|
|
554
|
+
cancelRaf();
|
|
555
|
+
if (el == null) return;
|
|
556
|
+
if (el.offsetWidth === 0 || el.offsetHeight === 0) {
|
|
557
|
+
rafId = requestAnimationFrame(() => {
|
|
558
|
+
rafId = null;
|
|
559
|
+
applyPointer(anchor, placeOpts);
|
|
560
|
+
});
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
applyPointer(anchor, placeOpts);
|
|
564
|
+
}
|
|
565
|
+
function placeAbove(placed) {
|
|
566
|
+
resetClosing();
|
|
567
|
+
cancelRaf();
|
|
568
|
+
if (el == null) return;
|
|
569
|
+
el.style.bottom = `${placed.bottom}px`;
|
|
570
|
+
el.style.top = "unset";
|
|
571
|
+
if (placed.left != null) {
|
|
572
|
+
el.style.left = `${placed.left}px`;
|
|
573
|
+
el.style.right = "unset";
|
|
574
|
+
} else {
|
|
575
|
+
el.style.right = `${placed.right ?? 0}px`;
|
|
576
|
+
el.style.left = "unset";
|
|
577
|
+
}
|
|
578
|
+
el.style.removeProperty("--yorozu-menu-max-height");
|
|
579
|
+
el.style.maxHeight = "";
|
|
580
|
+
el.style.overflow = "";
|
|
581
|
+
playOpen(placed.origin);
|
|
582
|
+
}
|
|
583
|
+
function destroy() {
|
|
584
|
+
currentPlayback?.cancel();
|
|
585
|
+
currentPlayback = null;
|
|
586
|
+
cancelRaf();
|
|
587
|
+
document.removeEventListener("pointerdown", onPointerDown, false);
|
|
588
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
589
|
+
historyLayer?.release();
|
|
590
|
+
historyLayer = null;
|
|
591
|
+
el = null;
|
|
592
|
+
closing = false;
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
attach,
|
|
596
|
+
placePointer,
|
|
597
|
+
placeAbove,
|
|
598
|
+
close,
|
|
599
|
+
destroy
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
//#endregion
|
|
603
|
+
export { COMPOSER_MENU_GAP_PX, MENU_FOCUSABLE_SELECTOR, MENU_HISTORY_STATE, MENU_LONG_PRESS_MS, MENU_LONG_PRESS_SWALLOW_MS, MENU_MAX_HEIGHT_BOTTOM_MARGIN_PX, MENU_POINTER_NUDGE_PX, MENU_POPOVER_CLOSE_EASING, MENU_POPOVER_CLOSE_MS, MENU_POPOVER_OPEN_EASING, MENU_POPOVER_OPEN_MS, MENU_POPOVER_SCALE, MENU_SUBMENU_DELAY_MS, MENU_VIEW_MARGIN_PX, bindHistoryLayer, bindLongPress, createMenuPopover, createMenuSession, createSubmenuHover, createSubmenuOpenRegistry, isMenuHistoryState, moveMenuFocus, placeAboveAnchor, placeFixedMenu };
|
package/keyboard.d.ts
ADDED
package/long-press.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const MENU_LONG_PRESS_MS: number;
|
|
2
|
+
export declare const MENU_LONG_PRESS_SWALLOW_MS: number;
|
|
3
|
+
export type LongPressBinding = {
|
|
4
|
+
update: (o?: {
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
}) => void;
|
|
7
|
+
destroy: () => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function bindLongPress(node: HTMLElement, opts?: {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
}): LongPressBinding;
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yorozu/context-menu",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.5.0",
|
|
5
|
+
"description": "framework-agnostic pointer-menu placement, motion, and session",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@yorozu/animations": "^0.5.0"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./index.d.ts",
|
|
14
|
+
"default": "./index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"./tokens.css": "./tokens.css",
|
|
18
|
+
"./default.css": "./default.css"
|
|
19
|
+
},
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"*.css"
|
|
22
|
+
],
|
|
23
|
+
"author": "inshinrei",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/inshinrei/yorozu.git"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/place-above.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const COMPOSER_MENU_GAP_PX: number;
|
|
2
|
+
export type MenuAlign = "start" | "end";
|
|
3
|
+
export type AnchorBox = {
|
|
4
|
+
top: number;
|
|
5
|
+
left: number;
|
|
6
|
+
right: number;
|
|
7
|
+
};
|
|
8
|
+
export type ViewportBox = {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
export type AboveAnchorPlacement = {
|
|
13
|
+
bottom: number;
|
|
14
|
+
left: number | undefined;
|
|
15
|
+
right: number | undefined;
|
|
16
|
+
origin: "bottom left" | "bottom right";
|
|
17
|
+
};
|
|
18
|
+
export declare function placeAboveAnchor(anchor: AnchorBox, align: MenuAlign, viewport?: ViewportBox, gapPx?: number): AboveAnchorPlacement;
|
package/place-fixed.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const MENU_VIEW_MARGIN_PX: number;
|
|
2
|
+
export declare const MENU_POINTER_NUDGE_PX: number;
|
|
3
|
+
export declare const MENU_MAX_HEIGHT_BOTTOM_MARGIN_PX: number;
|
|
4
|
+
export type MenuPositionX = "left" | "right";
|
|
5
|
+
export type MenuPositionY = "top" | "bottom";
|
|
6
|
+
export type PlaceFixedMenuInput = {
|
|
7
|
+
anchorX: number;
|
|
8
|
+
anchorY: number;
|
|
9
|
+
menuWidth: number;
|
|
10
|
+
menuHeight: number;
|
|
11
|
+
viewportWidth: number;
|
|
12
|
+
viewportHeight: number;
|
|
13
|
+
margin?: number;
|
|
14
|
+
extraMinWidth?: number;
|
|
15
|
+
extraTopSpace?: number;
|
|
16
|
+
extraPaddingX?: number;
|
|
17
|
+
withMaxHeight?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type PlaceFixedMenuResult = {
|
|
20
|
+
left: number;
|
|
21
|
+
top: number;
|
|
22
|
+
positionX: MenuPositionX;
|
|
23
|
+
positionY: MenuPositionY;
|
|
24
|
+
originX: number;
|
|
25
|
+
originY: number;
|
|
26
|
+
origin: string;
|
|
27
|
+
maxHeight: number | undefined;
|
|
28
|
+
};
|
|
29
|
+
export declare function placeFixedMenu(input: PlaceFixedMenuInput): PlaceFixedMenuResult;
|
package/popover.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Popover } from '@yorozu/animations';
|
|
2
|
+
export declare const MENU_POPOVER_OPEN_MS: number;
|
|
3
|
+
export declare const MENU_POPOVER_CLOSE_MS: number;
|
|
4
|
+
export declare const MENU_POPOVER_OPEN_EASING: string;
|
|
5
|
+
export declare const MENU_POPOVER_CLOSE_EASING: string;
|
|
6
|
+
export declare const MENU_POPOVER_SCALE: number;
|
|
7
|
+
export declare function createMenuPopover(): Popover;
|
package/session.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AboveAnchorPlacement } from './place-above';
|
|
2
|
+
export type PlacePointerOpts = {
|
|
3
|
+
extraMinWidth?: number;
|
|
4
|
+
extraTopSpace?: number;
|
|
5
|
+
extraPaddingX?: number;
|
|
6
|
+
withMaxHeight?: boolean;
|
|
7
|
+
margin?: number;
|
|
8
|
+
applyMaxHeightStyle?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type MenuSessionOpts = {
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
getDurationMs?: (kind: "open" | "close") => number;
|
|
13
|
+
nested?: boolean;
|
|
14
|
+
listenEsc?: boolean;
|
|
15
|
+
insideSelector?: string;
|
|
16
|
+
historyState?: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
export type MenuSession = {
|
|
19
|
+
attach: (el: HTMLElement) => void;
|
|
20
|
+
placePointer: (anchor: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
}, opts?: PlacePointerOpts) => void;
|
|
24
|
+
placeAbove: (placed: AboveAnchorPlacement) => void;
|
|
25
|
+
close: () => void;
|
|
26
|
+
destroy: () => void;
|
|
27
|
+
};
|
|
28
|
+
export declare function createMenuSession(opts: MenuSessionOpts): MenuSession;
|
package/submenu.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const MENU_SUBMENU_DELAY_MS: number;
|
|
2
|
+
export type SubmenuOpenRegistry = {
|
|
3
|
+
registerOpen: (closeFn: () => void) => void;
|
|
4
|
+
unregister: (closeFn: () => void) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function createSubmenuOpenRegistry(): SubmenuOpenRegistry;
|
|
7
|
+
export type SubmenuAnchor = {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
};
|
|
11
|
+
export type SubmenuHover = {
|
|
12
|
+
open: () => void;
|
|
13
|
+
openFromClick: () => void;
|
|
14
|
+
close: () => void;
|
|
15
|
+
scheduleOpen: () => void;
|
|
16
|
+
scheduleClose: () => void;
|
|
17
|
+
cancelClose: () => void;
|
|
18
|
+
clear: () => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function createSubmenuHover(opts: {
|
|
21
|
+
getRect: () => {
|
|
22
|
+
right: number;
|
|
23
|
+
top: number;
|
|
24
|
+
} | undefined;
|
|
25
|
+
isOpen: () => boolean;
|
|
26
|
+
setOpen: (next: SubmenuAnchor | null) => void;
|
|
27
|
+
}): SubmenuHover;
|
package/tokens.css
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* Compact default tokens for [data-yorozu-menu]. Override via CSS custom properties. */
|
|
2
|
+
[data-yorozu-menu] {
|
|
3
|
+
--yorozu-menu-min-width: 13.5rem;
|
|
4
|
+
--yorozu-menu-radius: 1rem;
|
|
5
|
+
--yorozu-menu-pad-block: 0.25rem;
|
|
6
|
+
--yorozu-menu-z: 1000;
|
|
7
|
+
--yorozu-menu-bg: #ffffffbb;
|
|
8
|
+
--yorozu-menu-shadow: 0 0.25rem 0.5rem 0.125rem #72727240;
|
|
9
|
+
--yorozu-menu-color: #000000;
|
|
10
|
+
--yorozu-menu-font-size: 0.875rem;
|
|
11
|
+
--yorozu-menu-font-weight: 500;
|
|
12
|
+
--yorozu-menu-font-family: system-ui, sans-serif;
|
|
13
|
+
--yorozu-menu-blur: 10px;
|
|
14
|
+
--yorozu-menu-item-margin: 0.125rem 0.25rem;
|
|
15
|
+
--yorozu-menu-item-padding: 0.25rem;
|
|
16
|
+
--yorozu-menu-item-padding-inline-end: 0.75rem;
|
|
17
|
+
--yorozu-menu-item-radius: 0.75rem;
|
|
18
|
+
--yorozu-menu-item-gap: 0.5rem;
|
|
19
|
+
--yorozu-menu-item-min-block: 2rem;
|
|
20
|
+
--yorozu-menu-item-hover: #000000b2;
|
|
21
|
+
--yorozu-menu-item-active-scale: 0.98;
|
|
22
|
+
--yorozu-menu-icon-size: 1.25rem;
|
|
23
|
+
--yorozu-menu-icon-color: #707579;
|
|
24
|
+
--yorozu-menu-destructive: #e53935;
|
|
25
|
+
--yorozu-menu-divider: #0000001a;
|
|
26
|
+
--yorozu-menu-disabled-opacity: 0.5;
|
|
27
|
+
--yorozu-menu-divider-margin: 0.25rem 0.5rem;
|
|
28
|
+
}
|