@parallelworks/ui 0.5.1 → 0.5.3

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.
@@ -1,319 +0,0 @@
1
- import { CheckIcon, ChevronRightIcon, ClipboardIcon, CopyIcon, IdCardIcon, KeyIcon, LinkIcon, MailIcon, MoreIcon, UserIcon } from "../icons.js";
2
- import { c as useStrings, i as useNotify, n as useLink } from "./Provider-wC9G01Ou.js";
3
- import cx from "classnames";
4
- import { jsx, jsxs } from "react/jsx-runtime";
5
- import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
6
- import { createPortal } from "react-dom";
7
- //#region src/list/RowContextMenu.tsx
8
- /** Converts a single row action to a menu item: a `to` makes it a link, otherwise an action. */
9
- function toRowMenuItem(action) {
10
- if (action.to) return {
11
- kind: "link",
12
- label: action.label,
13
- icon: action.icon,
14
- to: action.to,
15
- destructive: action.destructive
16
- };
17
- return {
18
- kind: "action",
19
- label: action.label,
20
- icon: action.icon,
21
- onSelect: action.onSelect ?? (() => {}),
22
- disabled: action.disabled,
23
- destructive: action.destructive
24
- };
25
- }
26
- /** Bridges a toolbar action (icon component, `onClick`/`href`) onto the shared
27
- * `RowAction` shape. Typed structurally so any toolbar action shape matching
28
- * these fields is assignable. */
29
- function toolbarActionToRowAction(action) {
30
- const Icon = action.icon;
31
- return {
32
- key: action.key,
33
- label: action.label,
34
- icon: /* @__PURE__ */ jsx(Icon, {}),
35
- onSelect: action.onClick,
36
- to: action.href,
37
- disabled: action.disabled,
38
- destructive: action.key === "delete" || action.key === "destroy"
39
- };
40
- }
41
- /** Assembles a row's context menu in the shared order: leading link(s) → middle
42
- * actions → extra items → Copy submenu → trailing (destructive) actions last. */
43
- function assembleRowMenu({ lead = [], actions, exclude, order, trailing = ["delete"], extra = [], copy }) {
44
- const trailingSet = new Set(trailing);
45
- const excludeSet = new Set(exclude);
46
- let middle = actions.filter((a) => !trailingSet.has(a.key) && !excludeSet.has(a.key));
47
- if (order) {
48
- const byKey = new Map(middle.map((a) => [a.key, a]));
49
- middle = order.map((key) => byKey.get(key)).filter((a) => a !== void 0);
50
- }
51
- const trailingItems = trailing.map((key) => actions.find((a) => a.key === key)).filter((a) => a !== void 0);
52
- return [
53
- ...lead,
54
- ...middle.map(toRowMenuItem),
55
- ...extra,
56
- ...copy ? [copy] : [],
57
- ...trailingItems.map(toRowMenuItem)
58
- ];
59
- }
60
- function useRowMenu() {
61
- const [menu, setMenu] = useState(null);
62
- const onCloseRef = useRef(void 0);
63
- return {
64
- openMenu: useCallback((x, y, items, onClose) => {
65
- if (items.length > 0) {
66
- onCloseRef.current?.();
67
- onCloseRef.current = onClose;
68
- setMenu({
69
- x,
70
- y,
71
- items
72
- });
73
- }
74
- }, []),
75
- contextMenu: /* @__PURE__ */ jsx(RowContextMenu, {
76
- state: menu,
77
- onClose: useCallback(() => {
78
- onCloseRef.current?.();
79
- onCloseRef.current = void 0;
80
- setMenu(null);
81
- }, [])
82
- })
83
- };
84
- }
85
- /** The full-screen menu overlay drops CSS :hover on the triggering row; wrap openMenu here to substitute a JS-driven `active` highlight. */
86
- function useRowMenuActive(openMenu) {
87
- const [active, setActive] = useState(false);
88
- return {
89
- active,
90
- openMenu: useCallback((x, y, items) => {
91
- setActive(true);
92
- openMenu(x, y, items, () => setActive(false));
93
- }, [openMenu])
94
- };
95
- }
96
- function rowContextMenuProps(openMenu, items) {
97
- return { onContextMenu: (e) => {
98
- e.preventDefault();
99
- if (items.length > 0) e.stopPropagation();
100
- openMenu(e.clientX, e.clientY, items);
101
- } };
102
- }
103
- function RowContextMenu({ state, onClose }) {
104
- const ref = useRef(null);
105
- const [pos, setPos] = useState(null);
106
- useLayoutEffect(() => {
107
- if (!state || !ref.current) {
108
- setPos(null);
109
- return;
110
- }
111
- const { width, height } = ref.current.getBoundingClientRect();
112
- const pad = 8;
113
- const top = height > window.innerHeight - pad - state.y && state.y - height - pad >= 0 ? state.y - height : Math.min(state.y, window.innerHeight - height - pad);
114
- setPos({
115
- left: Math.max(pad, Math.min(state.x, window.innerWidth - width - pad)),
116
- top: Math.max(pad, top)
117
- });
118
- }, [state]);
119
- useEffect(() => {
120
- if (!state) return;
121
- const close = () => onClose();
122
- const onKey = (e) => {
123
- if (e.key === "Escape") onClose();
124
- };
125
- window.addEventListener("scroll", close, true);
126
- window.addEventListener("resize", close);
127
- window.addEventListener("keydown", onKey);
128
- return () => {
129
- window.removeEventListener("scroll", close, true);
130
- window.removeEventListener("resize", close);
131
- window.removeEventListener("keydown", onKey);
132
- };
133
- }, [state, onClose]);
134
- if (!state) return null;
135
- const side = (pos?.left ?? state.x) > window.innerWidth * .6 ? "left" : "right";
136
- return createPortal(/* @__PURE__ */ jsx("div", {
137
- className: "fixed inset-0 z-50",
138
- onClick: onClose,
139
- onContextMenu: (e) => {
140
- e.preventDefault();
141
- if (ref.current?.contains(e.target)) {
142
- onClose();
143
- return;
144
- }
145
- const { clientX, clientY } = e;
146
- const overlay = e.currentTarget;
147
- overlay.style.pointerEvents = "none";
148
- const under = document.elementFromPoint(clientX, clientY);
149
- overlay.style.pointerEvents = "";
150
- onClose();
151
- under?.dispatchEvent(new MouseEvent("contextmenu", {
152
- bubbles: true,
153
- cancelable: true,
154
- clientX,
155
- clientY
156
- }));
157
- },
158
- children: /* @__PURE__ */ jsx("div", {
159
- ref,
160
- role: "menu",
161
- className: "absolute min-w-44 rounded-lg border border-(--theme-border) bg-(--theme-panel-bg) py-1 shadow-lg backdrop-blur-xl",
162
- style: {
163
- left: pos?.left ?? state.x,
164
- top: pos?.top ?? state.y,
165
- visibility: pos ? "visible" : "hidden"
166
- },
167
- onClick: (e) => e.stopPropagation(),
168
- children: /* @__PURE__ */ jsx(MenuItemList, {
169
- items: state.items,
170
- onClose,
171
- side
172
- })
173
- })
174
- }), document.body);
175
- }
176
- var MENU_ITEM_CLASSES = "flex w-full items-center gap-2.5 px-3 py-1.5 text-left text-[13px] hover:bg-(--theme-muted-panel-bg) transition-colors cursor-pointer [&>svg]:h-4 [&>svg]:w-4 [&>svg]:shrink-0";
177
- function MenuItemList({ items, onClose, side }) {
178
- return items.map((item, i) => item.kind === "divider" ? /* @__PURE__ */ jsx("div", {
179
- "aria-hidden": "true",
180
- className: "my-1 h-px bg-(--theme-border)"
181
- }, `divider-${i}`) : /* @__PURE__ */ jsx(MenuRow, {
182
- item,
183
- onClose,
184
- side
185
- }, `${item.label}-${i}`));
186
- }
187
- function MenuRow({ item, onClose, side }) {
188
- const Link = useLink();
189
- const notify = useNotify();
190
- const { list: t } = useStrings();
191
- const [openSub, setOpenSub] = useState(false);
192
- const closeTimer = useRef(void 0);
193
- if (item.kind === "submenu") {
194
- const open = () => {
195
- clearTimeout(closeTimer.current);
196
- setOpenSub(true);
197
- };
198
- const close = () => {
199
- closeTimer.current = setTimeout(() => setOpenSub(false), 160);
200
- };
201
- return /* @__PURE__ */ jsxs("div", {
202
- className: "relative",
203
- onMouseEnter: open,
204
- onMouseLeave: close,
205
- children: [/* @__PURE__ */ jsxs("div", {
206
- className: cx(MENU_ITEM_CLASSES, "justify-between text-(--theme-app)"),
207
- children: [/* @__PURE__ */ jsxs("span", {
208
- className: "flex items-center gap-2.5",
209
- children: [item.icon, item.label]
210
- }), /* @__PURE__ */ jsx(ChevronRightIcon, { className: "h-3 w-3 shrink-0 opacity-60" })]
211
- }), openSub && /* @__PURE__ */ jsx("div", {
212
- className: cx("absolute -top-1 z-10 min-w-44 rounded-lg border border-(--theme-border) bg-(--theme-panel-bg) py-1 shadow-lg", side === "left" ? "right-full" : "left-full"),
213
- children: /* @__PURE__ */ jsx(MenuItemList, {
214
- items: item.items,
215
- onClose,
216
- side
217
- })
218
- })]
219
- });
220
- }
221
- if (item.kind === "link") return /* @__PURE__ */ jsxs(Link, {
222
- to: item.to,
223
- onClick: onClose,
224
- className: cx(MENU_ITEM_CLASSES, item.destructive ? "text-red-500" : "text-(--theme-app)"),
225
- children: [item.icon, item.label]
226
- });
227
- return /* @__PURE__ */ jsxs("button", {
228
- type: "button",
229
- disabled: item.disabled,
230
- onClick: () => {
231
- if (item.copy) {
232
- const { text, label } = item.copy;
233
- navigator.clipboard?.writeText(text).then(() => notify.success(t.copied(label)), () => notify.error(t.couldntCopy(label)));
234
- }
235
- item.onSelect?.();
236
- onClose();
237
- },
238
- className: cx(MENU_ITEM_CLASSES, item.destructive ? "text-red-500" : "text-(--theme-app)", item.disabled && "opacity-40 cursor-not-allowed hover:bg-transparent"),
239
- children: [
240
- item.icon,
241
- item.label,
242
- item.selected && /* @__PURE__ */ jsx(CheckIcon, { className: "ml-auto opacity-80" })
243
- ]
244
- });
245
- }
246
- /** Builds the Copy submenu entries for whichever fields a row has, in stable
247
- * order. The menu itself performs the copy and notifies; `t` is an optional
248
- * host translator for the field labels (falls back to the provider defaults'
249
- * English). */
250
- function copySubmenu({ name, nameLabel, email, username, uid, id, url, extra, t }) {
251
- const items = [
252
- {
253
- label: nameLabel ?? (t ? t("rowContextMenu.labelName") : "name"),
254
- value: name,
255
- icon: /* @__PURE__ */ jsx(CopyIcon, {})
256
- },
257
- {
258
- label: t ? t("rowContextMenu.labelEmail") : "email",
259
- value: email,
260
- icon: /* @__PURE__ */ jsx(MailIcon, {})
261
- },
262
- {
263
- label: t ? t("rowContextMenu.labelUsername") : "username",
264
- value: username,
265
- icon: /* @__PURE__ */ jsx(UserIcon, {})
266
- },
267
- {
268
- label: t ? t("rowContextMenu.labelUid") : "UID",
269
- value: uid === null || uid === void 0 ? null : String(uid),
270
- icon: /* @__PURE__ */ jsx(KeyIcon, {})
271
- },
272
- {
273
- label: t ? t("rowContextMenu.labelId") : "ID",
274
- value: id,
275
- icon: /* @__PURE__ */ jsx(IdCardIcon, {})
276
- },
277
- {
278
- label: t ? t("rowContextMenu.labelUrl") : "URL",
279
- value: url,
280
- icon: /* @__PURE__ */ jsx(LinkIcon, {})
281
- },
282
- ...(extra ?? []).map((e) => ({
283
- label: e.label,
284
- value: e.value,
285
- icon: e.icon ?? /* @__PURE__ */ jsx(CopyIcon, {})
286
- }))
287
- ].filter((f) => f.value).map((f) => ({
288
- kind: "action",
289
- label: t ? t("rowContextMenu.copy", { label: f.label }) : `Copy ${f.label}`,
290
- icon: f.icon,
291
- copy: {
292
- text: f.value,
293
- label: f.label
294
- }
295
- }));
296
- return {
297
- kind: "submenu",
298
- label: t ? t("common.copy") : "Copy",
299
- icon: /* @__PURE__ */ jsx(ClipboardIcon, {}),
300
- items
301
- };
302
- }
303
- /** Stays visible while active (menu open) so it doesn't vanish behind the overlay. */
304
- function MoreButton({ onOpen, active, className }) {
305
- const { list: t } = useStrings();
306
- return /* @__PURE__ */ jsx("button", {
307
- type: "button",
308
- "aria-label": t.moreActions,
309
- onClick: (e) => {
310
- e.stopPropagation();
311
- const r = e.currentTarget.getBoundingClientRect();
312
- onOpen(r.right, r.bottom);
313
- },
314
- className: cx("rounded p-1 cursor-pointer text-(--theme-muted-text-color) transition-colors hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app) focus-visible:opacity-100", active ? "opacity-100" : "opacity-0 group-hover:opacity-100", className),
315
- children: /* @__PURE__ */ jsx(MoreIcon, { className: "h-4 w-4" })
316
- });
317
- }
318
- //#endregion
319
- export { toRowMenuItem as a, useRowMenuActive as c, rowContextMenuProps as i, assembleRowMenu as n, toolbarActionToRowAction as o, copySubmenu as r, useRowMenu as s, MoreButton as t };