editor-shell 0.30.0 → 0.31.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/dist/menu/index.d.ts +176 -0
- package/dist/menu/index.js +202 -0
- package/dist/menu/index.js.map +1 -0
- package/dist/menu/menu.css +232 -0
- package/package.json +10 -3
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/** One row of the menu.
|
|
5
|
+
*
|
|
6
|
+
* Carried over unchanged from `efficient-admin-portal`'s `RowContextMenuItem`
|
|
7
|
+
* (card 66039), because the campaign designer's registry is typed against it and
|
|
8
|
+
* what the menus OFFER is not this card's to move. A `label` of `'—'` renders a
|
|
9
|
+
* separator; `header: true` renders a caption, and the FIRST such caption names
|
|
10
|
+
* the menu for assistive tech. */
|
|
11
|
+
interface EditorContextMenuItem {
|
|
12
|
+
label: string;
|
|
13
|
+
onClick: () => void;
|
|
14
|
+
danger?: boolean;
|
|
15
|
+
header?: boolean;
|
|
16
|
+
/** A SUBMENU: this item opens a flyout of these instead of acting itself.
|
|
17
|
+
* One level only — a child carrying its own `children` is drawn as a plain
|
|
18
|
+
* item, which is all any caller has needed and keeps the flyout from becoming
|
|
19
|
+
* a tree nobody can navigate with a mouse. */
|
|
20
|
+
children?: EditorContextMenuItem[];
|
|
21
|
+
/** Draw a tick beside this item — for a submenu whose entries are STATES
|
|
22
|
+
* rather than actions, so it can say which one you are already on. */
|
|
23
|
+
checked?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* A glyph for the row, drawn before the label.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ A TYPE SLOT, NOT A LOOKUP. The box renders what it is handed and does
|
|
28
|
+
* not know what a glyph means — it must never import the icon set, or the
|
|
29
|
+
* leaf stops being a leaf (`tests/menu-leaf-safety.test.ts` (a) fails if it
|
|
30
|
+
* tries). Hosts pass an element from `editor-shell/icons`, whose four
|
|
31
|
+
* menu glyphs — copy, paste, move-up, move-down — shipped in 0.30.0 for
|
|
32
|
+
* exactly this row.
|
|
33
|
+
*
|
|
34
|
+
* OMIT IT AND NOTHING IS DRAWN — not an empty span. That is what keeps the
|
|
35
|
+
* admin portal's three shipped surfaces byte-identical, and it is pinned by
|
|
36
|
+
* `tests/menu-layer.test.tsx` (g).
|
|
37
|
+
*/
|
|
38
|
+
icon?: ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* The keyboard shortcut, drawn right-aligned after the label (the `.k`
|
|
41
|
+
* column of the approved drawing).
|
|
42
|
+
*
|
|
43
|
+
* Display text, not a binding — the box neither registers nor honours it;
|
|
44
|
+
* the host owns the key handling. `aria-hidden`, like the tick and the
|
|
45
|
+
* chevron: "Duplicate ⌘D" read aloud is worse than "Duplicate".
|
|
46
|
+
*
|
|
47
|
+
* OMIT IT AND NOTHING IS DRAWN. Same guard as `icon`.
|
|
48
|
+
*/
|
|
49
|
+
shortcut?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What the box is dressed in.
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ A SKIN IS TOTAL. Pass one and it replaces {@link ES_MENU_SKIN} outright —
|
|
55
|
+
* nothing from the default is merged in underneath. That is not tidiness, it is
|
|
56
|
+
* the only arrangement that keeps the admin portal's three shipped render sites
|
|
57
|
+
* looking the way they do:
|
|
58
|
+
*
|
|
59
|
+
* · their look is Tailwind classes, and Tailwind v4 scans the project root and
|
|
60
|
+
* NOT `node_modules` (`efficient-admin-portal/src/index.css:13-22` says so in
|
|
61
|
+
* its own words, and adds `@source` lines for react-os-shell because of it);
|
|
62
|
+
* · so those class strings have to keep living in that project's own `src/`,
|
|
63
|
+
* where its build still sees them — they cannot move into this package;
|
|
64
|
+
* · and a merge would append a class of ours to a caller's string, which is
|
|
65
|
+
* the one thing that stops the rendered markup being byte-identical.
|
|
66
|
+
*
|
|
67
|
+
* A slot left empty is left empty. The default fills all of them.
|
|
68
|
+
*/
|
|
69
|
+
interface EditorContextMenuSkin {
|
|
70
|
+
/** The box. */
|
|
71
|
+
menu?: string;
|
|
72
|
+
/** The FLYOUT box. Falls back to {@link menu} when a skin gives only one —
|
|
73
|
+
* they are the same surface in the default look, but not everywhere: the
|
|
74
|
+
* admin portal's box carries `overflow-y-auto` (it takes a `maxHeight`) and
|
|
75
|
+
* its flyout does not. Collapsing the two would quietly change the markup of
|
|
76
|
+
* a surface already in front of users. */
|
|
77
|
+
menuSub?: string;
|
|
78
|
+
/** A plain row. */
|
|
79
|
+
item?: string;
|
|
80
|
+
/** A row that destroys something. */
|
|
81
|
+
itemDanger?: string;
|
|
82
|
+
/** The `'—'` separator. */
|
|
83
|
+
divider?: string;
|
|
84
|
+
/** A `header: true` caption. */
|
|
85
|
+
header?: string;
|
|
86
|
+
/** The label span of a row that has something beside it — a chevron, a tick.
|
|
87
|
+
* A row that is only a label renders bare text and takes no class at all,
|
|
88
|
+
* which is what the two grid callers have always drawn. */
|
|
89
|
+
label?: string;
|
|
90
|
+
/** The label span inside a FLYOUT. Falls back to {@link label} when a skin
|
|
91
|
+
* gives only one — the admin portal wants them different, because a state
|
|
92
|
+
* name in a flyout can be long enough to need truncating and a row in the box
|
|
93
|
+
* never is. */
|
|
94
|
+
labelSub?: string;
|
|
95
|
+
/** The glyph column, when a row carries an `icon`. */
|
|
96
|
+
icon?: string;
|
|
97
|
+
/** The right-aligned shortcut column, when a row carries a `shortcut`. */
|
|
98
|
+
shortcut?: string;
|
|
99
|
+
/** The `›` on a row that opens a flyout. */
|
|
100
|
+
chevron?: string;
|
|
101
|
+
/** The tick column of a flyout whose entries are states. */
|
|
102
|
+
tick?: string;
|
|
103
|
+
/** Merged into the box's own inline style, under the placement this component
|
|
104
|
+
* computes — so a caller can hand it a surface (the admin passes
|
|
105
|
+
* `glassStyle()` from react-os-shell) without being able to move it. */
|
|
106
|
+
surface?: CSSProperties;
|
|
107
|
+
/** Merged into each row's inline style. */
|
|
108
|
+
itemStyle?: CSSProperties;
|
|
109
|
+
}
|
|
110
|
+
interface EditorContextMenuProps {
|
|
111
|
+
/** Where the pointer was. The box is placed from here and then kept on
|
|
112
|
+
* screen — see the note on the component. */
|
|
113
|
+
x: number;
|
|
114
|
+
y: number;
|
|
115
|
+
items: EditorContextMenuItem[];
|
|
116
|
+
onClose: () => void;
|
|
117
|
+
/** Cap the box's height; it scrolls past that. */
|
|
118
|
+
maxHeight?: number;
|
|
119
|
+
/** Defaults to {@link ES_MENU_SKIN}, the storefront editor's approved look. */
|
|
120
|
+
skin?: EditorContextMenuSkin;
|
|
121
|
+
/**
|
|
122
|
+
* Where to portal to. Defaults to `document.body`.
|
|
123
|
+
*
|
|
124
|
+
* A host that OVERRIDES `--es-*` on its own editor root should pass that root
|
|
125
|
+
* instead: the default skin re-declares the token defaults on the box (see the
|
|
126
|
+
* component's note), which is right for a box hanging off `<body>` and wrong
|
|
127
|
+
* for one that should inherit a re-themed editor's values.
|
|
128
|
+
*/
|
|
129
|
+
portalTo?: HTMLElement | null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The right-click menu box, shared by both editors (card 66400.d).
|
|
134
|
+
*
|
|
135
|
+
* MOVED HERE, NOT WRITTEN HERE. This is `efficient-admin-portal`'s
|
|
136
|
+
* `src/components/RowContextMenu.tsx` — portal placement, viewport edge-flip,
|
|
137
|
+
* a one-level submenu with ticks, focus move-in and restore, Escape and
|
|
138
|
+
* outside-click — which had been in front of real users on three surfaces
|
|
139
|
+
* (price sheets, the goods-issue dialog, the campaign canvas) before the
|
|
140
|
+
* storefront editor needed the same affordance. The alternative was those same
|
|
141
|
+
* 230 lines written a second time.
|
|
142
|
+
*
|
|
143
|
+
* ONE THING CHANGED IN THE MOVE, and it is the reason the move is worth doing:
|
|
144
|
+
* the box no longer knows what it looks like. The admin version hard-coded ONE
|
|
145
|
+
* editor's surface — `glassStyle()` from react-os-shell, plus that portal's
|
|
146
|
+
* Tailwind palette — into the only box both editors were ever going to share.
|
|
147
|
+
* Here the look is a {@link EditorContextMenuSkin}, the default is the
|
|
148
|
+
* storefront's approved one, and the admin passes its own strings in. So this
|
|
149
|
+
* module imports nothing but React (`tests/menu-leaf-safety.test.ts`), and the
|
|
150
|
+
* admin's rendered markup is byte-identical to what shipped
|
|
151
|
+
* (`tests/menu-layer.test.tsx` (e)).
|
|
152
|
+
*
|
|
153
|
+
* A CLIENT LEAF, like the gold layer and unlike the rail: it portals, it
|
|
154
|
+
* measures and it moves focus, so it has hooks and touches the DOM. A Next-16
|
|
155
|
+
* server component may import the module and must render it inside the host's
|
|
156
|
+
* client boundary.
|
|
157
|
+
*/
|
|
158
|
+
declare function EditorContextMenu({ x, y, items, onClose, maxHeight, skin, portalTo, }: EditorContextMenuProps): react.ReactPortal | null;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The default skin — the storefront editor's approved look, in classes that
|
|
162
|
+
* `editor-shell/menu.css` dresses.
|
|
163
|
+
*
|
|
164
|
+
* ⚠️ `es-editor` IS LOAD-BEARING AND IS NOT DECORATION. `shell.css` declares
|
|
165
|
+
* every `--es-*` on `.es-editor`, deliberately never on `:root`, so importing it
|
|
166
|
+
* themes an editor root and leaks nothing into the host page — the campaign
|
|
167
|
+
* designer renders INSIDE the admin DOM. The box is portalled to
|
|
168
|
+
* `document.body`, which is OUTSIDE that root, so without the class here every
|
|
169
|
+
* `var(--es-*)` in `menu.css` resolves to nothing: the background collapses to
|
|
170
|
+
* transparent and the border to `currentColor`. The result is markup that is
|
|
171
|
+
* present, answers every query a test can ask, and cannot be seen. Pinned by
|
|
172
|
+
* `tests/menu-layer.test.tsx` (f).
|
|
173
|
+
*/
|
|
174
|
+
declare const ES_MENU_SKIN: EditorContextMenuSkin;
|
|
175
|
+
|
|
176
|
+
export { ES_MENU_SKIN, EditorContextMenu, type EditorContextMenuItem, type EditorContextMenuProps, type EditorContextMenuSkin };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useRef, useState, useLayoutEffect, useEffect } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/menu/EditorContextMenu.tsx
|
|
6
|
+
|
|
7
|
+
// src/menu/skin.ts
|
|
8
|
+
var ES_MENU_SKIN = {
|
|
9
|
+
menu: "es-editor es-menu",
|
|
10
|
+
menuSub: "es-editor es-menu",
|
|
11
|
+
item: "es-menu-item",
|
|
12
|
+
itemDanger: "es-menu-item is-danger",
|
|
13
|
+
divider: "es-menu-divider",
|
|
14
|
+
header: "es-menu-header",
|
|
15
|
+
icon: "es-menu-icon",
|
|
16
|
+
shortcut: "es-menu-shortcut",
|
|
17
|
+
label: "es-menu-label",
|
|
18
|
+
labelSub: "es-menu-label es-menu-label-sub",
|
|
19
|
+
chevron: "es-menu-chevron",
|
|
20
|
+
tick: "es-menu-tick"
|
|
21
|
+
};
|
|
22
|
+
function EditorContextMenu({
|
|
23
|
+
x,
|
|
24
|
+
y,
|
|
25
|
+
items,
|
|
26
|
+
onClose,
|
|
27
|
+
maxHeight,
|
|
28
|
+
skin,
|
|
29
|
+
portalTo
|
|
30
|
+
}) {
|
|
31
|
+
const menuRef = useRef(null);
|
|
32
|
+
const subRef = useRef(null);
|
|
33
|
+
const [sub, setSub] = useState(null);
|
|
34
|
+
const [at, setAt] = useState({ x, y });
|
|
35
|
+
const [openedAt, setOpenedAt] = useState({ x, y });
|
|
36
|
+
if (openedAt.x !== x || openedAt.y !== y) {
|
|
37
|
+
setOpenedAt({ x, y });
|
|
38
|
+
setAt({ x, y });
|
|
39
|
+
}
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
const el = menuRef.current;
|
|
42
|
+
if (!el) return;
|
|
43
|
+
const { width, height } = el.getBoundingClientRect();
|
|
44
|
+
const margin = 8;
|
|
45
|
+
const room = { w: window.innerWidth, h: window.innerHeight };
|
|
46
|
+
let nextY = y;
|
|
47
|
+
if (y + height > room.h - margin) {
|
|
48
|
+
nextY = y - height >= margin ? y - height : Math.max(margin, room.h - height - margin);
|
|
49
|
+
}
|
|
50
|
+
let nextX = x;
|
|
51
|
+
if (x + width > room.w - margin) {
|
|
52
|
+
nextX = x - width >= margin ? x - width : Math.max(margin, room.w - width - margin);
|
|
53
|
+
}
|
|
54
|
+
if (nextX !== at.x || nextY !== at.y) setAt({ x: nextX, y: nextY });
|
|
55
|
+
}, [x, y, items, at.x, at.y]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const returnTo = document.activeElement;
|
|
58
|
+
menuRef.current?.querySelector('[role="menuitem"]')?.focus();
|
|
59
|
+
return () => returnTo?.focus?.();
|
|
60
|
+
}, []);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const handler = (e) => {
|
|
63
|
+
const target = e.target;
|
|
64
|
+
if (subRef.current?.contains(target)) return;
|
|
65
|
+
if (menuRef.current && !menuRef.current.contains(target)) onClose();
|
|
66
|
+
};
|
|
67
|
+
const escHandler = (e) => {
|
|
68
|
+
if (e.key === "Escape") onClose();
|
|
69
|
+
};
|
|
70
|
+
const armed = setTimeout(() => {
|
|
71
|
+
window.addEventListener("mousedown", handler);
|
|
72
|
+
window.addEventListener("contextmenu", handler);
|
|
73
|
+
window.addEventListener("keydown", escHandler);
|
|
74
|
+
}, 0);
|
|
75
|
+
return () => {
|
|
76
|
+
clearTimeout(armed);
|
|
77
|
+
window.removeEventListener("mousedown", handler);
|
|
78
|
+
window.removeEventListener("contextmenu", handler);
|
|
79
|
+
window.removeEventListener("keydown", escHandler);
|
|
80
|
+
};
|
|
81
|
+
}, [onClose]);
|
|
82
|
+
if (items.length === 0) return null;
|
|
83
|
+
const s = skin ?? ES_MENU_SKIN;
|
|
84
|
+
const itemClass = (item) => item.danger ? s.itemDanger : s.item;
|
|
85
|
+
const Tick = ({ on }) => /* @__PURE__ */ jsx("span", { className: s.tick, "aria-hidden": "true", children: on ? "\u2713" : "" });
|
|
86
|
+
const Icon = ({ of: item }) => item.icon == null ? null : /* @__PURE__ */ jsx("span", { className: s.icon, "aria-hidden": "true", children: item.icon });
|
|
87
|
+
const Shortcut = ({ of: item }) => item.shortcut == null ? null : /* @__PURE__ */ jsx("span", { className: s.shortcut, "aria-hidden": "true", children: item.shortcut });
|
|
88
|
+
const decorated = (item) => item.icon != null || item.shortcut != null;
|
|
89
|
+
const openSub = (item, i, el) => {
|
|
90
|
+
if (!item.children?.length) {
|
|
91
|
+
setSub(null);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const r = el.getBoundingClientRect();
|
|
95
|
+
const box = menuRef.current;
|
|
96
|
+
const floor = box ? parseFloat(getComputedStyle(box).minWidth) : NaN;
|
|
97
|
+
const width = Number.isFinite(floor) && floor > 0 ? floor : box?.getBoundingClientRect().width ?? 0;
|
|
98
|
+
const left = r.right + width > window.innerWidth - 8 ? r.left - width : r.right;
|
|
99
|
+
setSub({ i, x: Math.max(8, left), y: r.top });
|
|
100
|
+
};
|
|
101
|
+
const open = sub ? items[sub.i] : null;
|
|
102
|
+
const surface = (extra) => ({
|
|
103
|
+
...s.surface,
|
|
104
|
+
...extra
|
|
105
|
+
});
|
|
106
|
+
return createPortal(
|
|
107
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
108
|
+
/* @__PURE__ */ jsx(
|
|
109
|
+
"div",
|
|
110
|
+
{
|
|
111
|
+
ref: menuRef,
|
|
112
|
+
"data-es-menu": "",
|
|
113
|
+
role: "menu",
|
|
114
|
+
"aria-label": items.find((i) => i.header)?.label,
|
|
115
|
+
className: s.menu,
|
|
116
|
+
style: surface({ left: at.x, top: at.y, maxHeight: maxHeight || void 0 }),
|
|
117
|
+
children: items.map((item, i) => item.label === "\u2014" ? (
|
|
118
|
+
// `role="separator"`, because a bare <div> inside a `menu` is a
|
|
119
|
+
// generic child that assistive tech reads as part of the list.
|
|
120
|
+
/* @__PURE__ */ jsx("div", { role: "separator", className: s.divider }, i)
|
|
121
|
+
) : item.header ? /* @__PURE__ */ jsx("div", { className: s.header, children: item.label }, i) : item.children?.length ? /* @__PURE__ */ jsxs(
|
|
122
|
+
"button",
|
|
123
|
+
{
|
|
124
|
+
type: "button",
|
|
125
|
+
role: "menuitem",
|
|
126
|
+
"aria-haspopup": "menu",
|
|
127
|
+
"aria-expanded": sub?.i === i,
|
|
128
|
+
onMouseEnter: (e) => openSub(item, i, e.currentTarget),
|
|
129
|
+
onFocus: (e) => openSub(item, i, e.currentTarget),
|
|
130
|
+
onClick: (e) => openSub(item, i, e.currentTarget),
|
|
131
|
+
className: itemClass(item),
|
|
132
|
+
style: s.itemStyle,
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ jsx(Icon, { of: item }),
|
|
135
|
+
/* @__PURE__ */ jsx("span", { className: s.label, children: item.label }),
|
|
136
|
+
/* @__PURE__ */ jsx(Shortcut, { of: item }),
|
|
137
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: s.chevron, children: "\u203A" })
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
i
|
|
141
|
+
) : /* @__PURE__ */ jsx(
|
|
142
|
+
"button",
|
|
143
|
+
{
|
|
144
|
+
type: "button",
|
|
145
|
+
role: "menuitem",
|
|
146
|
+
onMouseEnter: () => setSub(null),
|
|
147
|
+
onClick: () => {
|
|
148
|
+
item.onClick();
|
|
149
|
+
onClose();
|
|
150
|
+
},
|
|
151
|
+
className: itemClass(item),
|
|
152
|
+
style: s.itemStyle,
|
|
153
|
+
children: decorated(item) ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
154
|
+
/* @__PURE__ */ jsx(Icon, { of: item }),
|
|
155
|
+
/* @__PURE__ */ jsx("span", { className: s.label, children: item.label }),
|
|
156
|
+
/* @__PURE__ */ jsx(Shortcut, { of: item })
|
|
157
|
+
] }) : item.label
|
|
158
|
+
},
|
|
159
|
+
i
|
|
160
|
+
))
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
open?.children?.length && sub ? /* @__PURE__ */ jsx(
|
|
164
|
+
"div",
|
|
165
|
+
{
|
|
166
|
+
ref: subRef,
|
|
167
|
+
"data-es-menu": "",
|
|
168
|
+
role: "menu",
|
|
169
|
+
"aria-label": open.label,
|
|
170
|
+
className: s.menuSub ?? s.menu,
|
|
171
|
+
style: surface({ left: sub.x, top: sub.y }),
|
|
172
|
+
children: open.children.map((child, ci) => /* @__PURE__ */ jsxs(
|
|
173
|
+
"button",
|
|
174
|
+
{
|
|
175
|
+
type: "button",
|
|
176
|
+
role: "menuitemradio",
|
|
177
|
+
"aria-checked": Boolean(child.checked),
|
|
178
|
+
onClick: () => {
|
|
179
|
+
child.onClick();
|
|
180
|
+
onClose();
|
|
181
|
+
},
|
|
182
|
+
className: itemClass(child),
|
|
183
|
+
style: s.itemStyle,
|
|
184
|
+
children: [
|
|
185
|
+
/* @__PURE__ */ jsx(Tick, { on: Boolean(child.checked) }),
|
|
186
|
+
/* @__PURE__ */ jsx(Icon, { of: child }),
|
|
187
|
+
/* @__PURE__ */ jsx("span", { className: s.labelSub ?? s.label, children: child.label }),
|
|
188
|
+
/* @__PURE__ */ jsx(Shortcut, { of: child })
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
ci
|
|
192
|
+
))
|
|
193
|
+
}
|
|
194
|
+
) : null
|
|
195
|
+
] }),
|
|
196
|
+
portalTo ?? document.body
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export { ES_MENU_SKIN, EditorContextMenu };
|
|
201
|
+
//# sourceMappingURL=index.js.map
|
|
202
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/menu/skin.ts","../../src/menu/EditorContextMenu.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { EditorContextMenuSkin } from './types';\n\n/**\n * The default skin — the storefront editor's approved look, in classes that\n * `editor-shell/menu.css` dresses.\n *\n * ⚠️ `es-editor` IS LOAD-BEARING AND IS NOT DECORATION. `shell.css` declares\n * every `--es-*` on `.es-editor`, deliberately never on `:root`, so importing it\n * themes an editor root and leaks nothing into the host page — the campaign\n * designer renders INSIDE the admin DOM. The box is portalled to\n * `document.body`, which is OUTSIDE that root, so without the class here every\n * `var(--es-*)` in `menu.css` resolves to nothing: the background collapses to\n * transparent and the border to `currentColor`. The result is markup that is\n * present, answers every query a test can ask, and cannot be seen. Pinned by\n * `tests/menu-layer.test.tsx` (f).\n */\nexport const ES_MENU_SKIN: EditorContextMenuSkin = {\n menu: 'es-editor es-menu',\n menuSub: 'es-editor es-menu',\n item: 'es-menu-item',\n itemDanger: 'es-menu-item is-danger',\n divider: 'es-menu-divider',\n header: 'es-menu-header',\n icon: 'es-menu-icon',\n shortcut: 'es-menu-shortcut',\n label: 'es-menu-label',\n labelSub: 'es-menu-label es-menu-label-sub',\n chevron: 'es-menu-chevron',\n tick: 'es-menu-tick',\n};\n","import { useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { ES_MENU_SKIN } from './skin';\nimport type { EditorContextMenuItem, EditorContextMenuProps } from './types';\n\n/**\n * The right-click menu box, shared by both editors (card 66400.d).\n *\n * MOVED HERE, NOT WRITTEN HERE. This is `efficient-admin-portal`'s\n * `src/components/RowContextMenu.tsx` — portal placement, viewport edge-flip,\n * a one-level submenu with ticks, focus move-in and restore, Escape and\n * outside-click — which had been in front of real users on three surfaces\n * (price sheets, the goods-issue dialog, the campaign canvas) before the\n * storefront editor needed the same affordance. The alternative was those same\n * 230 lines written a second time.\n *\n * ONE THING CHANGED IN THE MOVE, and it is the reason the move is worth doing:\n * the box no longer knows what it looks like. The admin version hard-coded ONE\n * editor's surface — `glassStyle()` from react-os-shell, plus that portal's\n * Tailwind palette — into the only box both editors were ever going to share.\n * Here the look is a {@link EditorContextMenuSkin}, the default is the\n * storefront's approved one, and the admin passes its own strings in. So this\n * module imports nothing but React (`tests/menu-leaf-safety.test.ts`), and the\n * admin's rendered markup is byte-identical to what shipped\n * (`tests/menu-layer.test.tsx` (e)).\n *\n * A CLIENT LEAF, like the gold layer and unlike the rail: it portals, it\n * measures and it moves focus, so it has hooks and touches the DOM. A Next-16\n * server component may import the module and must render it inside the host's\n * client boundary.\n */\nexport function EditorContextMenu({\n x, y, items, onClose, maxHeight, skin, portalTo,\n}: EditorContextMenuProps) {\n const menuRef = useRef<HTMLDivElement>(null);\n // The open submenu — which item it belongs to, and where its flyout sits.\n const subRef = useRef<HTMLDivElement>(null);\n const [sub, setSub] = useState<{ i: number; x: number; y: number } | null>(null);\n // Where the box ACTUALLY lands, once it has been measured — see `useLayoutEffect`.\n const [at, setAt] = useState({ x, y });\n // Re-seed from the pointer whenever the menu is opened somewhere new, so the\n // measurement below starts from the real gesture rather than from where the\n // last one ended up. Done as a render-time adjustment keyed on the transition\n // (React's sanctioned alternative to a setState-in-effect).\n const [openedAt, setOpenedAt] = useState({ x, y });\n if (openedAt.x !== x || openedAt.y !== y) {\n setOpenedAt({ x, y });\n setAt({ x, y });\n }\n\n /**\n * Keep the box on screen.\n *\n * `x`/`y` are where the pointer was, and a box drawn straight from there runs\n * off the bottom whenever the click was within its own height of the viewport\n * edge — measured in the campaign designer at a 560px-tall window: it\n * overflowed by 131px and its LAST item, `Delete`, was entirely off screen\n * with no way to reach it.\n *\n * Flip rather than merely clamp: sliding the box up until it fits would park\n * it under the pointer, so the item beneath the cursor is no longer the one\n * the gesture aimed at. Opening UPWARDS keeps that relationship, which is what\n * every native menu does. Only then clamp, for the case where the box is\n * taller than the viewport on both sides.\n *\n * Measured in a layout effect (before paint) so it is never seen in the wrong\n * place, and re-run per open because the item list changes its height.\n */\n useLayoutEffect(() => {\n const el = menuRef.current;\n if (!el) return;\n const { width, height } = el.getBoundingClientRect();\n const margin = 8;\n const room = { w: window.innerWidth, h: window.innerHeight };\n let nextY = y;\n if (y + height > room.h - margin) {\n // Above the pointer if it fits there, else pinned to the bottom edge.\n nextY = y - height >= margin ? y - height : Math.max(margin, room.h - height - margin);\n }\n // The same for the right edge, which a box opened near it would otherwise\n // overflow — cheap to do here and the identical class of bug.\n let nextX = x;\n if (x + width > room.w - margin) {\n nextX = x - width >= margin ? x - width : Math.max(margin, room.w - width - margin);\n }\n if (nextX !== at.x || nextY !== at.y) setAt({ x: nextX, y: nextY });\n }, [x, y, items, at.x, at.y]);\n\n /**\n * Move focus INTO the box when it opens, and put it back when it closes.\n *\n * Without this the menu is openable from the keyboard and then unusable: the\n * box is portalled to the end of its container, so focus is left behind on\n * whatever was right-clicked and Tab walks the rest of the page before\n * reaching the items. Restoring on close matters just as much — Escape from a\n * menu that dropped focus on the floor leaves a keyboard user at the top of\n * the document, having lost the row they were working on.\n */\n useEffect(() => {\n const returnTo = document.activeElement as HTMLElement | null;\n menuRef.current?.querySelector<HTMLElement>('[role=\"menuitem\"]')?.focus();\n return () => returnTo?.focus?.();\n }, []);\n\n useEffect(() => {\n const handler = (e: MouseEvent) => {\n const target = e.target as Node;\n // The flyout is a SEPARATE portal, so \"outside the menu box\" would other-\n // wise include it — and this fires on mousedown, a beat before the child's\n // own click, which would unmount the flyout out from under the gesture.\n if (subRef.current?.contains(target)) return;\n if (menuRef.current && !menuRef.current.contains(target)) onClose();\n };\n const escHandler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };\n // Use a timeout so the opening right-click doesn't immediately close it.\n const armed = setTimeout(() => {\n window.addEventListener('mousedown', handler);\n window.addEventListener('contextmenu', handler);\n window.addEventListener('keydown', escHandler);\n }, 0);\n return () => {\n clearTimeout(armed);\n window.removeEventListener('mousedown', handler);\n window.removeEventListener('contextmenu', handler);\n window.removeEventListener('keydown', escHandler);\n };\n }, [onClose]);\n\n if (items.length === 0) return null;\n\n // TOTAL, not merged — see the note on EditorContextMenuSkin. A caller's string\n // is what renders, with nothing of ours appended to it.\n const s = skin ?? ES_MENU_SKIN;\n const itemClass = (item: EditorContextMenuItem) => (item.danger ? s.itemDanger : s.item);\n\n /** A tick column, present on every row of a flyout that uses one, so the\n * labels line up whether or not the row is the current state. */\n const Tick = ({ on }: { on: boolean }) => (\n <span className={s.tick} aria-hidden=\"true\">{on ? '✓' : ''}</span>\n );\n\n /** The glyph before the label, and the shortcut after it — the `.ci` and `.k`\n * columns of the approved drawing.\n *\n * ⚠️ DRAWN ONLY WHEN GIVEN. An item carrying neither renders the bare label\n * it always did — not a pair of empty spans. That is the whole reason the\n * admin portal's three shipped surfaces come out byte-identical, and it is\n * what `tests/menu-layer.test.tsx` (g) pins. Both are `aria-hidden`, like the\n * tick and the chevron: the label already carries the meaning, and\n * \"Duplicate ⌘D\" read aloud is worse than \"Duplicate\". */\n const Icon = ({ of: item }: { of: EditorContextMenuItem }) =>\n item.icon == null ? null : <span className={s.icon} aria-hidden=\"true\">{item.icon}</span>;\n\n const Shortcut = ({ of: item }: { of: EditorContextMenuItem }) =>\n item.shortcut == null ? null : <span className={s.shortcut} aria-hidden=\"true\">{item.shortcut}</span>;\n\n /** Whether a row needs a label SPAN at all. A plain row with no decoration is\n * bare text inside its button, exactly as it has always been. */\n const decorated = (item: EditorContextMenuItem) =>\n item.icon != null || item.shortcut != null;\n\n /** Open `item`'s flyout beside its row. Measured now rather than positioned in\n * flow, because the flyout is portalled separately — the box scrolls\n * (`overflow-y: auto` for `maxHeight`) and a CSS overflow that is not\n * `visible` on one axis clips the other too, so an in-flow flyout would be\n * cut off at the box's edge. */\n const openSub = (item: EditorContextMenuItem, i: number, el: HTMLElement) => {\n if (!item.children?.length) {\n setSub(null);\n return;\n }\n const r = el.getBoundingClientRect();\n /* How wide the flyout will be, MEASURED rather than typed.\n *\n * It wears the same skin as the box, so its floor is the box's floor — read\n * off the live box instead of restated here. This was the literal `220` for\n * one release, with a comment claiming it tracked `--es-menu-min-w`; when\n * the fork ruled that floor down to 186 the stylesheet followed and this did\n * not, and a comment that says \"derived\" over a number that is typed is\n * exactly the line nobody re-reads. Measuring removes the second copy\n * instead of guarding it.\n *\n * It is also the only version that is right for BOTH skins: the default's\n * floor comes from `--es-menu-min-w`, and the admin portal's from its own\n * `min-w-[220px]`. A constant would have to be wrong for one of them.\n *\n * Falls back to what the box actually measures when a skin sets no floor at\n * all — `min-width` is then `auto`/`0px` and parses to nothing useful. */\n const box = menuRef.current;\n const floor = box ? parseFloat(getComputedStyle(box).minWidth) : NaN;\n const width = Number.isFinite(floor) && floor > 0\n ? floor\n : box?.getBoundingClientRect().width ?? 0;\n // Flip to the left when there is no room to the right, the same rule the box\n // itself follows for the viewport edge.\n const left = r.right + width > window.innerWidth - 8 ? r.left - width : r.right;\n setSub({ i, x: Math.max(8, left), y: r.top });\n };\n\n const open = sub ? items[sub.i] : null;\n const surface = (extra: { left: number; top: number; maxHeight?: number }) => ({\n ...s.surface,\n ...extra,\n });\n\n return createPortal(\n <>\n {/* A real `menu` role, named by its own caption row. Assistive tech was\n given a bare <div> of buttons before — announced one at a time with no\n hint they belonged together, and nothing saying WHAT they act on. The\n name comes from the first `header` item rather than a new prop so there\n is still exactly one place a caller writes that name. */}\n <div\n ref={menuRef}\n data-es-menu=\"\"\n role=\"menu\"\n aria-label={items.find((i) => i.header)?.label}\n className={s.menu}\n style={surface({ left: at.x, top: at.y, maxHeight: maxHeight || undefined })}\n >\n {items.map((item, i) => item.label === '—' ? (\n // `role=\"separator\"`, because a bare <div> inside a `menu` is a\n // generic child that assistive tech reads as part of the list.\n <div key={i} role=\"separator\" className={s.divider} />\n ) : item.header ? (\n <div key={i} className={s.header}>{item.label}</div>\n ) : item.children?.length ? (\n <button\n key={i}\n type=\"button\"\n role=\"menuitem\"\n aria-haspopup=\"menu\"\n aria-expanded={sub?.i === i}\n onMouseEnter={(e) => openSub(item, i, e.currentTarget)}\n onFocus={(e) => openSub(item, i, e.currentTarget)}\n onClick={(e) => openSub(item, i, e.currentTarget)}\n className={itemClass(item)}\n style={s.itemStyle}\n >\n <Icon of={item} />\n <span className={s.label}>{item.label}</span>\n <Shortcut of={item} />\n <span aria-hidden=\"true\" className={s.chevron}>›</span>\n </button>\n ) : (\n <button\n key={i}\n type=\"button\"\n role=\"menuitem\"\n onMouseEnter={() => setSub(null)}\n onClick={() => { item.onClick(); onClose(); }}\n className={itemClass(item)}\n style={s.itemStyle}\n >\n {decorated(item) ? (\n <>\n <Icon of={item} />\n <span className={s.label}>{item.label}</span>\n <Shortcut of={item} />\n </>\n ) : item.label}\n </button>\n ))}\n </div>\n {open?.children?.length && sub ? (\n <div\n ref={subRef}\n data-es-menu=\"\"\n role=\"menu\"\n aria-label={open.label}\n className={s.menuSub ?? s.menu}\n style={surface({ left: sub.x, top: sub.y })}\n >\n {open.children.map((child, ci) => (\n <button\n key={ci}\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={Boolean(child.checked)}\n onClick={() => { child.onClick(); onClose(); }}\n className={itemClass(child)}\n style={s.itemStyle}\n >\n <Tick on={Boolean(child.checked)} />\n <Icon of={child} />\n <span className={s.labelSub ?? s.label}>{child.label}</span>\n <Shortcut of={child} />\n </button>\n ))}\n </div>\n ) : null}\n </>,\n portalTo ?? document.body,\n );\n}\n"]}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* editor-shell/menu.css — the right-click menu box (card 66400.d).
|
|
3
|
+
*
|
|
4
|
+
* THE VALUES ARE NOT OURS. They are the storefront editor's shipped
|
|
5
|
+
* `.sf-blockmenu` / `.sf-blockmenu-item` — `efficient-shop/components/storefront-editor.css`,
|
|
6
|
+
* read at line 1165 — which is the look card 66400.d names for the menu Lewis
|
|
7
|
+
* approved on 2026-08-31. Each `--sf-*` there is already an alias of the
|
|
8
|
+
* matching `--es-*` here (card 66024), so the look is expressed with no new
|
|
9
|
+
* value except the three noted below. Change one only to track a deliberate
|
|
10
|
+
* change THERE. Pinned by `tests/menu-box-css.test.ts`.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ THREE VALUES ARE UNDER A LIVE QUESTION, and each is behind ONE var so
|
|
13
|
+
* settling it is a three-line change. The drawing Lewis approved that same day
|
|
14
|
+
* (`mockups/editor-add-and-context-v1.html`, its `.ctx` rule) is a FLOATING
|
|
15
|
+
* menu, where `.sf-blockmenu` is an in-flow picker, and it holds different
|
|
16
|
+
* numbers for exactly the three things that difference would move:
|
|
17
|
+
*
|
|
18
|
+
* .sf-blockmenu (shipped) .ctx (the drawing)
|
|
19
|
+
* radius 8px 9px
|
|
20
|
+
* shadow 0 6px 18px rgba(0,0,0,.08) 0 10px 28px rgba(0,0,0,.16)
|
|
21
|
+
* min-width 220px (the admin box's own) 186px
|
|
22
|
+
*
|
|
23
|
+
* This sheet ships `.sf-blockmenu`'s, per the card's own constraint. The
|
|
24
|
+
* question is with the fork that owns the design.
|
|
25
|
+
*
|
|
26
|
+
* LIGHT only — neither editing surface has a dark mode (owner, 2026-08-06), so
|
|
27
|
+
* there is no dark counterpart block here. Do not add one.
|
|
28
|
+
*
|
|
29
|
+
* Import once from a client entry / global stylesheet:
|
|
30
|
+
* import 'editor-shell/menu.css';
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/* ── this sheet's own vars ──────────────────────────────────────────────────
|
|
34
|
+
ON `.es-editor`, the chrome root, following `rail.css` (which owns
|
|
35
|
+
`--es-rail-*`) and `panel.css` (`--es-row-gap`): the sheet that owns the
|
|
36
|
+
rules owns its tokens, and `shell.css` stays a verbatim mirror of the
|
|
37
|
+
storefront editor's `--sf-*` block. None of these is a shell token.
|
|
38
|
+
|
|
39
|
+
⚠️ The box itself names `es-editor` (see `skin.ts`) — it is portalled to
|
|
40
|
+
`document.body`, outside the editor tree, so this is also where it picks these
|
|
41
|
+
up. Read that note before moving them. */
|
|
42
|
+
.es-editor {
|
|
43
|
+
/* HOW FAR THE BOX FLOATS OFF THE PAGE BEHIND IT.
|
|
44
|
+
The drawing's value. `.sf-blockmenu` — the shop's shipped Add-block picker
|
|
45
|
+
— carries `0 6px 18px rgba(0,0,0,.08)`, and this is deliberately heavier:
|
|
46
|
+
that picker sits IN FLOW inside a panel, while this box FLOATS over
|
|
47
|
+
content, and a floating box has to separate itself from whatever is under
|
|
48
|
+
it. The drawing's own header cites `.sf-blockmenu`, so its author had that
|
|
49
|
+
value open and moved off it on purpose.
|
|
50
|
+
This is the CONTEXT half of the split described on `--es-menu-hover-bg`:
|
|
51
|
+
it follows the situation, not the neighbour. */
|
|
52
|
+
--es-menu-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
|
|
53
|
+
|
|
54
|
+
/* HOW SOFT ITS CORNERS ARE. The drawing's 9px, one more than the in-flow
|
|
55
|
+
picker's 8 — the same floating-vs-in-flow distinction. */
|
|
56
|
+
--es-menu-radius: 9px;
|
|
57
|
+
|
|
58
|
+
/* THE NARROWEST THE BOX MAY GET — a floor, not a width.
|
|
59
|
+
A menu that shrank to its longest label would change width with its
|
|
60
|
+
contents, so a merchant's pointer would land on a different row depending
|
|
61
|
+
on which label happened to be longest. The drawing's 186px. It is also the
|
|
62
|
+
width the flyout measures against when it decides to flip left. */
|
|
63
|
+
--es-menu-min-w: 186px;
|
|
64
|
+
|
|
65
|
+
/* WHAT A ROW DOES UNDER THE POINTER — the one thing a merchant watches move.
|
|
66
|
+
⚠️ THIS ONE DOES NOT FOLLOW THE DRAWING, AND THAT IS DELIBERATE. The
|
|
67
|
+
drawing's `.ctx` washes the row GREY and leaves the text alone. This washes
|
|
68
|
+
the accent tint and turns the text blue, which is `.sf-blockmenu` — the Add
|
|
69
|
+
menu, one keystroke away in the same editor.
|
|
70
|
+
|
|
71
|
+
Owner ruling, 2026-09-02, recorded as a dated amendment in the
|
|
72
|
+
`owner-design-rulings` skill ("the right-click menu hovers BLUE, not the
|
|
73
|
+
drawing's grey"): it was put to him rather than decided, because two menus
|
|
74
|
+
in one editor highlighting differently is a rule-zero failure. His word was
|
|
75
|
+
blue.
|
|
76
|
+
|
|
77
|
+
⛔ DO NOT "RESTORE" THE DRAWING'S GREY. The split from the three values
|
|
78
|
+
above is not an inconsistency, it is the rule:
|
|
79
|
+
|
|
80
|
+
hover interaction grammar -> follows the NEIGHBOUR
|
|
81
|
+
shadow / radius / context -> follows the SITUATION
|
|
82
|
+
min-width (this box floats; the Add
|
|
83
|
+
menu sits in flow)
|
|
84
|
+
|
|
85
|
+
Still behind a var, because that is what made asking him cost nothing. */
|
|
86
|
+
--es-menu-hover-bg: var(--es-accent-tint);
|
|
87
|
+
--es-menu-hover-fg: var(--es-accent);
|
|
88
|
+
|
|
89
|
+
/* A ROW THAT DESTROYS SOMETHING.
|
|
90
|
+
⚠️ The drawing is the ONLY reference for this one — `.sf-blockmenu` has no
|
|
91
|
+
danger row at all, so nothing is being overridden here. Its `#b91c1c` is
|
|
92
|
+
exactly `--es-danger-strong`, so it is read from the token layer rather
|
|
93
|
+
than copied as a literal. */
|
|
94
|
+
--es-menu-danger-fg: var(--es-danger-strong);
|
|
95
|
+
|
|
96
|
+
/* THE SHORTCUT COLUMN'S INK — quieter than a label, quieter than `--es-muted`,
|
|
97
|
+
because a shortcut is a reminder and never the thing being read.
|
|
98
|
+
⚠️ The one value here with no shell token behind it: the drawing's own
|
|
99
|
+
`#b6bcc4`, which is lighter than `--es-disabled` (#9ca3af). Named here
|
|
100
|
+
rather than reaching for a token that is not this colour. */
|
|
101
|
+
--es-menu-shortcut-fg: #b6bcc4;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/* ── the box ────────────────────────────────────────────────────────────────
|
|
106
|
+
FIXED and lifted on purpose: it hangs OVER the panel it was opened from
|
|
107
|
+
rather than inside it, so a left panel dragged to its 220px minimum — which
|
|
108
|
+
clips its own overflow — cannot cut it off. Measured in Chromium at both
|
|
109
|
+
panel widths; see `tests/fixtures/menu-clipping.html`. */
|
|
110
|
+
.es-menu {
|
|
111
|
+
position: fixed;
|
|
112
|
+
z-index: 9999;
|
|
113
|
+
box-sizing: border-box;
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
gap: 1px;
|
|
117
|
+
min-width: var(--es-menu-min-w);
|
|
118
|
+
/* A box that could grow past the viewport would be unreachable on its far
|
|
119
|
+
side whatever the edge-flip did. */
|
|
120
|
+
max-width: calc(100vw - 16px);
|
|
121
|
+
padding: 4px;
|
|
122
|
+
border: 1px solid var(--es-line);
|
|
123
|
+
border-radius: var(--es-menu-radius);
|
|
124
|
+
background: var(--es-panel);
|
|
125
|
+
box-shadow: var(--es-menu-shadow);
|
|
126
|
+
font-family: var(--es-font);
|
|
127
|
+
/* For `maxHeight`. The flyout is portalled separately BECAUSE of this: a CSS
|
|
128
|
+
overflow that is not `visible` on one axis clips the other too, so an
|
|
129
|
+
in-flow flyout would be cut off at this box's edge. */
|
|
130
|
+
overflow-y: auto;
|
|
131
|
+
overscroll-behavior: contain;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ── a row ──────────────────────────────────────────────────────────────── */
|
|
135
|
+
.es-menu-item {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: 8px;
|
|
139
|
+
width: 100%;
|
|
140
|
+
padding: 7px 9px;
|
|
141
|
+
border: none;
|
|
142
|
+
border-radius: 6px;
|
|
143
|
+
background: transparent;
|
|
144
|
+
color: var(--es-text);
|
|
145
|
+
font: inherit;
|
|
146
|
+
font-size: 12.5px;
|
|
147
|
+
text-align: left;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* The one thing a merchant watches move — see `--es-menu-hover-*` above for
|
|
152
|
+
which reference this follows and why it is behind a var. */
|
|
153
|
+
.es-menu-item:hover {
|
|
154
|
+
background: var(--es-menu-hover-bg);
|
|
155
|
+
color: var(--es-menu-hover-fg);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.es-menu-item:focus-visible {
|
|
159
|
+
outline: 2px solid var(--es-accent-ring);
|
|
160
|
+
outline-offset: -2px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.es-menu-item.is-danger {
|
|
164
|
+
color: var(--es-menu-danger-fg);
|
|
165
|
+
}
|
|
166
|
+
/* The drawing gives a danger row no hover of its own: it takes the same grey as
|
|
167
|
+
every other row and KEEPS its red, rather than washing red as well. Stated as
|
|
168
|
+
its own rule because `.es-menu-item:hover` would otherwise repaint the text. */
|
|
169
|
+
.es-menu-item.is-danger:hover {
|
|
170
|
+
background: var(--es-menu-hover-bg);
|
|
171
|
+
color: var(--es-menu-danger-fg);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ── the parts of a row ─────────────────────────────────────────────────── */
|
|
175
|
+
.es-menu-label {
|
|
176
|
+
flex: 1;
|
|
177
|
+
}
|
|
178
|
+
/* A state name in a flyout can be long; a row in the box never is. */
|
|
179
|
+
.es-menu-label-sub {
|
|
180
|
+
overflow: hidden;
|
|
181
|
+
text-overflow: ellipsis;
|
|
182
|
+
white-space: nowrap;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.es-menu-chevron {
|
|
186
|
+
color: var(--es-icon);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* The drawing's `.ci` — a fixed 16px column so labels line up whether or not a
|
|
190
|
+
row has a glyph, exactly as the tick column does in a flyout. */
|
|
191
|
+
.es-menu-icon {
|
|
192
|
+
flex: none;
|
|
193
|
+
width: 16px;
|
|
194
|
+
text-align: center;
|
|
195
|
+
color: var(--es-muted);
|
|
196
|
+
font-size: 11px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* The drawing's `.k`. `margin-left: auto` is what right-aligns it, so it works
|
|
200
|
+
whether or not the row also carries a chevron. */
|
|
201
|
+
.es-menu-shortcut {
|
|
202
|
+
margin-left: auto;
|
|
203
|
+
color: var(--es-menu-shortcut-fg);
|
|
204
|
+
font-size: 11px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Present on every row of a flyout that uses one, so the labels line up whether
|
|
208
|
+
or not the row is the state you are already on. */
|
|
209
|
+
.es-menu-tick {
|
|
210
|
+
flex: none;
|
|
211
|
+
width: 12px;
|
|
212
|
+
color: var(--es-accent);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* ── a caption, and a separator ─────────────────────────────────────────── */
|
|
216
|
+
.es-menu-header {
|
|
217
|
+
padding: 6px 9px 4px;
|
|
218
|
+
color: var(--es-muted);
|
|
219
|
+
font-size: 10px;
|
|
220
|
+
font-weight: 500;
|
|
221
|
+
letter-spacing: 0.06em;
|
|
222
|
+
text-transform: uppercase;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* AIR, NOT A LINE (card 66350). The owner has asked for divider hairlines to
|
|
226
|
+
go, more than once, and `tests/panel-no-divider-lines.test.ts` sweeps every
|
|
227
|
+
stylesheet this package ships for a one-sided border — this one included. A
|
|
228
|
+
`'—'` item still separates; it does it with a gap. With the box's own 1px
|
|
229
|
+
`gap` either side, that is 7px between groups against 1px between rows. */
|
|
230
|
+
.es-menu-divider {
|
|
231
|
+
height: 5px;
|
|
232
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "editor-shell",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared editor-chrome primitives for the EFFICIENT editors: EditorRail (a Next-16-safe left icon-rail leaf), the shell token layer, and GoldTextInput
|
|
3
|
+
"version": "0.31.0",
|
|
4
|
+
"description": "Shared editor-chrome primitives for the EFFICIENT editors: EditorRail (a Next-16-safe left icon-rail leaf), the shell token layer, and GoldTextInput — type in place and watch the markup formatting appear as you type.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Lewis Liu",
|
|
7
7
|
"homepage": "https://github.com/Lewislhy/editor-shell#readme",
|
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
"import": "./dist/icons/index.js",
|
|
36
36
|
"default": "./dist/icons/index.js"
|
|
37
37
|
},
|
|
38
|
+
"./menu": {
|
|
39
|
+
"types": "./dist/menu/index.d.ts",
|
|
40
|
+
"import": "./dist/menu/index.js",
|
|
41
|
+
"default": "./dist/menu/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./menu.css": "./dist/menu/menu.css",
|
|
38
44
|
"./panel": {
|
|
39
45
|
"types": "./dist/panel/index.d.ts",
|
|
40
46
|
"import": "./dist/panel/index.js",
|
|
@@ -83,7 +89,7 @@
|
|
|
83
89
|
"typescript": "^5.3.0"
|
|
84
90
|
},
|
|
85
91
|
"scripts": {
|
|
86
|
-
"build": "tsup && cp src/rail/rail.css dist/rail/rail.css && cp src/shell/shell.css dist/shell/shell.css && cp src/gold/gold.css dist/gold/gold.css && cp src/panel/panel.css dist/panel/panel.css",
|
|
92
|
+
"build": "tsup && cp src/rail/rail.css dist/rail/rail.css && cp src/shell/shell.css dist/shell/shell.css && cp src/gold/gold.css dist/gold/gold.css && cp src/panel/panel.css dist/panel/panel.css && cp src/menu/menu.css dist/menu/menu.css",
|
|
87
93
|
"dev": "tsup --watch",
|
|
88
94
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
89
95
|
"test": "node scripts/test.mjs",
|
|
@@ -95,6 +101,7 @@
|
|
|
95
101
|
"editor",
|
|
96
102
|
"ui-shell",
|
|
97
103
|
"rail",
|
|
104
|
+
"menu",
|
|
98
105
|
"toolbar",
|
|
99
106
|
"next",
|
|
100
107
|
"rsc",
|