@mendylanda/ui 0.4.2 → 0.4.3-dev.68.e3417befc4d5
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/README.md +25 -3
- package/dist/class-names.js +1 -1
- package/dist/locales/en.d.ts +8 -0
- package/dist/locales/en.js +8 -0
- package/dist/locales/he.d.ts +8 -0
- package/dist/locales/he.js +8 -0
- package/dist/sheet/content.d.ts +7 -0
- package/dist/sheet/content.js +56 -0
- package/dist/sheet/context.d.ts +67 -0
- package/dist/sheet/context.js +11 -0
- package/dist/sheet/index.d.ts +19 -0
- package/dist/sheet/index.js +16 -0
- package/dist/sheet/layout.d.ts +19 -0
- package/dist/sheet/layout.js +48 -0
- package/dist/sheet/parts.d.ts +29 -0
- package/dist/sheet/parts.js +76 -0
- package/dist/sheet/persistence.d.ts +8 -0
- package/dist/sheet/persistence.js +33 -0
- package/dist/sheet/provider.d.ts +13 -0
- package/dist/sheet/provider.js +156 -0
- package/dist/sheet/root.d.ts +22 -0
- package/dist/sheet/root.js +132 -0
- package/dist/sheet/scroll-lock.d.ts +1 -0
- package/dist/sheet/scroll-lock.js +16 -0
- package/dist/sheet/sheet-guard.d.ts +10 -0
- package/dist/sheet/sheet-guard.js +25 -0
- package/dist/sheet/store.d.ts +33 -0
- package/dist/sheet/store.js +155 -0
- package/dist/sheet/types.d.ts +48 -0
- package/dist/sheet/types.js +1 -0
- package/dist/sheet/use-sheet-focus.d.ts +6 -0
- package/dist/sheet/use-sheet-focus.js +46 -0
- package/dist/styles.css +124 -0
- package/dist/styles.tailwind3.css +124 -0
- package/package.json +12 -4
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { SheetController, SheetPersistence } from "./types.js";
|
|
3
|
+
export interface SheetProviderProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
/** Supply once above the router outlet to retain managed sheets across navigation. */
|
|
6
|
+
persistence?: SheetPersistence;
|
|
7
|
+
/** Opening and layout motion is enabled by default, and respects reduced motion. */
|
|
8
|
+
animation?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function SheetProvider(props: SheetProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
/** Open persistent instances from events. Render functions run beneath the provider. */
|
|
13
|
+
export declare function useSheets(): SheetController;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { useMendyUI } from "../customization.js";
|
|
6
|
+
import { useMendyLocale } from "../locale-context.js";
|
|
7
|
+
import { cn } from "../utils.js";
|
|
8
|
+
import { SheetStackContext } from "./context.js";
|
|
9
|
+
import { createSheetStore } from "./store.js";
|
|
10
|
+
import { SheetRoot } from "./root.js";
|
|
11
|
+
import { lockSheetScroll } from "./scroll-lock.js";
|
|
12
|
+
import { readSheetRecords } from "./persistence.js";
|
|
13
|
+
export function SheetProvider(props) {
|
|
14
|
+
// A different account/workspace must never inherit another scope's open instances.
|
|
15
|
+
return _jsx(ProviderScope, { ...props }, props.persistence?.key ?? "default");
|
|
16
|
+
}
|
|
17
|
+
function ProviderScope({ children, persistence, animation = true, className }) {
|
|
18
|
+
const [store] = useState(createSheetStore);
|
|
19
|
+
const [mounted, setMounted] = useState(false);
|
|
20
|
+
useLayoutEffect(() => {
|
|
21
|
+
setMounted(true);
|
|
22
|
+
}, []);
|
|
23
|
+
const [container, setContainer] = useState(null);
|
|
24
|
+
const settings = useMendyUI();
|
|
25
|
+
const { direction } = useMendyLocale();
|
|
26
|
+
const layout = useSyncExternalStore(store.subscribe, store.getLayout, store.getLayout);
|
|
27
|
+
const managed = useSyncExternalStore(store.subscribe, store.getManaged, store.getManaged);
|
|
28
|
+
const pageRef = useRef(null);
|
|
29
|
+
const persistenceRef = useRef(persistence);
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
persistenceRef.current = persistence;
|
|
32
|
+
});
|
|
33
|
+
const value = useMemo(() => ({ store, container, animation, pageRef }), [store, container, animation]);
|
|
34
|
+
useLayoutEffect(() => {
|
|
35
|
+
let frame = 0;
|
|
36
|
+
const update = () => {
|
|
37
|
+
if (frame)
|
|
38
|
+
return;
|
|
39
|
+
frame = requestAnimationFrame(() => {
|
|
40
|
+
frame = 0;
|
|
41
|
+
store.setViewport(window.innerWidth);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
store.setViewport(window.innerWidth);
|
|
45
|
+
window.addEventListener("resize", update);
|
|
46
|
+
return () => {
|
|
47
|
+
window.removeEventListener("resize", update);
|
|
48
|
+
cancelAnimationFrame(frame);
|
|
49
|
+
};
|
|
50
|
+
}, [store]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const config = persistenceRef.current;
|
|
53
|
+
if (!config)
|
|
54
|
+
return;
|
|
55
|
+
let storage;
|
|
56
|
+
try {
|
|
57
|
+
storage = config.storage ?? window.sessionStorage;
|
|
58
|
+
for (const record of readSheetRecords(storage.getItem(config.key))) {
|
|
59
|
+
try {
|
|
60
|
+
const content = config.resolve(record);
|
|
61
|
+
if (content == null)
|
|
62
|
+
continue;
|
|
63
|
+
store.restore({
|
|
64
|
+
side: record.side,
|
|
65
|
+
width: record.width,
|
|
66
|
+
id: record.id,
|
|
67
|
+
persist: { type: record.type, payload: record.payload },
|
|
68
|
+
render: () => content,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
config.onError?.(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
config.onError?.(error);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let last = "";
|
|
81
|
+
// Coalesce mount and pin updates, avoiding writes for viewport/layout-only changes.
|
|
82
|
+
let queued = false;
|
|
83
|
+
let disposed = false;
|
|
84
|
+
const save = () => {
|
|
85
|
+
if (queued)
|
|
86
|
+
return;
|
|
87
|
+
queued = true;
|
|
88
|
+
queueMicrotask(() => {
|
|
89
|
+
queued = false;
|
|
90
|
+
if (disposed)
|
|
91
|
+
return;
|
|
92
|
+
try {
|
|
93
|
+
const next = JSON.stringify({ version: 1, sheets: store.records() });
|
|
94
|
+
if (next !== last) {
|
|
95
|
+
storage.setItem(config.key, next);
|
|
96
|
+
last = next;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
persistenceRef.current?.onError?.(error);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
const unsubscribe = store.subscribe(save);
|
|
105
|
+
return () => {
|
|
106
|
+
disposed = true;
|
|
107
|
+
unsubscribe();
|
|
108
|
+
};
|
|
109
|
+
}, [store]);
|
|
110
|
+
const hasOverlay = layout.hasOverlay;
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!hasOverlay)
|
|
113
|
+
return;
|
|
114
|
+
return lockSheetScroll(document);
|
|
115
|
+
}, [hasOverlay]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const onKeyDown = (event) => {
|
|
118
|
+
const target = event.target;
|
|
119
|
+
if (event.defaultPrevented ||
|
|
120
|
+
event.repeat ||
|
|
121
|
+
event.isComposing ||
|
|
122
|
+
event.ctrlKey ||
|
|
123
|
+
event.metaKey ||
|
|
124
|
+
event.altKey ||
|
|
125
|
+
event.key.toLowerCase() !== "p" ||
|
|
126
|
+
!(target instanceof Element) ||
|
|
127
|
+
!container?.contains(target) ||
|
|
128
|
+
target.closest("input,textarea,select,[contenteditable]:not([contenteditable=false]),[role=textbox],[role=combobox],[role=menu],[role=listbox],[role=alertdialog]"))
|
|
129
|
+
return;
|
|
130
|
+
if (store.pinTop())
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
};
|
|
133
|
+
document.addEventListener("keydown", onKeyDown);
|
|
134
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
135
|
+
}, [store, container]);
|
|
136
|
+
return (_jsxs(SheetStackContext.Provider, { value: value, children: [_jsx("div", { "data-mendy-sheet-page": "", ref: pageRef, tabIndex: -1, dir: direction, inert: hasOverlay || undefined, className: cn("mui-184ddc11e5f9", animation && "mui-55fc320dcf4a mui-f07df86364db mui-b2ed949e45a3", className), style: { paddingInlineStart: layout.startWidth, paddingInlineEnd: layout.endWidth }, children: children }), mounted &&
|
|
137
|
+
createPortal(_jsx("div", { "data-mendy-ui": "", "data-mendy-sheet-host": "", dir: direction, ref: setContainer, className: "mui-33228ff7d583 mui-992a93455c71 mui-ce13093ad8c9 mui-3eef1ce97dba", children: hasOverlay && (_jsx("div", { "data-mendy-sheet-overlay": "", "aria-hidden": "true", className: "mui-cb20bd519346 mui-747355bdc2a2 mui-ce13093ad8c9 mui-4a30330fe460", onPointerDown: (event) => {
|
|
138
|
+
if (event.button !== 0)
|
|
139
|
+
return;
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
store.closeTop("outside");
|
|
142
|
+
} })) }), settings.portalContainer ?? document.body), managed.map((entry) => (_jsx(ManagedSheet, { entry: entry }, entry.id)))] }));
|
|
143
|
+
}
|
|
144
|
+
const ManagedSheet = memo(function ManagedSheet({ entry }) {
|
|
145
|
+
return (_jsx(SheetRoot, { id: entry.id, defaultOpen: true, defaultPinned: entry.initialPinned, side: entry.side, width: entry.width, pinnable: entry.pinnable, children: _jsx(ManagedContent, { render: entry.render }) }));
|
|
146
|
+
});
|
|
147
|
+
const ManagedContent = memo(function ManagedContent({ render }) {
|
|
148
|
+
return render();
|
|
149
|
+
});
|
|
150
|
+
/** Open persistent instances from events. Render functions run beneath the provider. */
|
|
151
|
+
export function useSheets() {
|
|
152
|
+
const stack = useContext(SheetStackContext);
|
|
153
|
+
if (!stack)
|
|
154
|
+
throw new Error("useSheets requires a SheetProvider.");
|
|
155
|
+
return stack.store;
|
|
156
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { SheetCloseReason, SheetSide } from "./types.js";
|
|
3
|
+
export interface SheetProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
id?: string;
|
|
6
|
+
/** Opening sequence for route-driven sheets whose code may load out of order. */
|
|
7
|
+
order?: number;
|
|
8
|
+
open?: boolean;
|
|
9
|
+
defaultOpen?: boolean;
|
|
10
|
+
onOpenChange?: (open: boolean) => void;
|
|
11
|
+
pinned?: boolean;
|
|
12
|
+
defaultPinned?: boolean;
|
|
13
|
+
onPinnedChange?: (pinned: boolean) => void;
|
|
14
|
+
pinnable?: boolean;
|
|
15
|
+
/** Logical end by default: right in LTR, left in RTL. */
|
|
16
|
+
side?: SheetSide;
|
|
17
|
+
/** Desired width in CSS pixels. Clamped to the available viewport. */
|
|
18
|
+
width?: number;
|
|
19
|
+
/** Return false to prevent a user/API close request. Controlled prop changes remain authoritative. */
|
|
20
|
+
onBeforeClose?: (reason: SheetCloseReason) => boolean | void;
|
|
21
|
+
}
|
|
22
|
+
export declare function SheetRoot(props: SheetProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
|
+
import * as Dialog from "@radix-ui/react-dialog";
|
|
5
|
+
import { SheetControlsContext, SheetInstanceContext, SheetStackContext } from "./context.js";
|
|
6
|
+
import { SheetProvider } from "./provider.js";
|
|
7
|
+
import { CloseConfirmation } from "./sheet-guard.js";
|
|
8
|
+
export function SheetRoot(props) {
|
|
9
|
+
const stack = useContext(SheetStackContext);
|
|
10
|
+
return stack ? (_jsx(SheetInstance, { ...props })) : (_jsx(SheetProvider, { children: _jsx(SheetInstance, { ...props }) }));
|
|
11
|
+
}
|
|
12
|
+
function SheetInstance({ children, id: explicitId, order, open: controlledOpen, defaultOpen = false, onOpenChange, pinned: controlledPinned, defaultPinned = false, onPinnedChange, pinnable = true, side = "end", width: requestedWidth = 520, onBeforeClose, }) {
|
|
13
|
+
const stack = useContext(SheetStackContext);
|
|
14
|
+
const generatedId = useId();
|
|
15
|
+
const id = explicitId ?? generatedId;
|
|
16
|
+
const width = Number.isFinite(requestedWidth) ? Math.max(240, requestedWidth) : 520;
|
|
17
|
+
const [descriptionId, setDescriptionId] = useState();
|
|
18
|
+
const registerDescription = useCallback((id) => {
|
|
19
|
+
setDescriptionId(id);
|
|
20
|
+
return () => setDescriptionId((current) => (current === id ? undefined : current));
|
|
21
|
+
}, []);
|
|
22
|
+
const [ownOpen, setOwnOpen] = useState(defaultOpen);
|
|
23
|
+
const [ownPinned, setOwnPinned] = useState(defaultPinned);
|
|
24
|
+
const open = controlledOpen ?? ownOpen;
|
|
25
|
+
const pinned = controlledPinned ?? ownPinned;
|
|
26
|
+
const getLayout = useCallback(() => stack.store.getSheet(id), [stack.store, id]);
|
|
27
|
+
const layout = useSyncExternalStore(stack.store.subscribe, getLayout, getLayout);
|
|
28
|
+
const contentRef = useRef(null);
|
|
29
|
+
const triggerRef = useRef(null);
|
|
30
|
+
const guards = useRef(new Map());
|
|
31
|
+
const [confirmation, setConfirmation] = useState(null);
|
|
32
|
+
const registerGuard = useCallback((read) => {
|
|
33
|
+
const key = Symbol();
|
|
34
|
+
guards.current.set(key, read);
|
|
35
|
+
return () => {
|
|
36
|
+
guards.current.delete(key);
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
const setPinned = useCallback((next) => {
|
|
40
|
+
if (pinned === next)
|
|
41
|
+
return;
|
|
42
|
+
if (controlledPinned === undefined)
|
|
43
|
+
setOwnPinned(next);
|
|
44
|
+
onPinnedChange?.(next);
|
|
45
|
+
}, [pinned, controlledPinned, onPinnedChange]);
|
|
46
|
+
const setOpen = useCallback((next) => {
|
|
47
|
+
if (open === next)
|
|
48
|
+
return;
|
|
49
|
+
if (!next) {
|
|
50
|
+
setConfirmation(null);
|
|
51
|
+
setPinned(false);
|
|
52
|
+
}
|
|
53
|
+
if (controlledOpen === undefined)
|
|
54
|
+
setOwnOpen(next);
|
|
55
|
+
onOpenChange?.(next);
|
|
56
|
+
}, [open, controlledOpen, onOpenChange, setPinned]);
|
|
57
|
+
const requestClose = useCallback((reason = "programmatic") => {
|
|
58
|
+
if (onBeforeClose?.(reason) === false)
|
|
59
|
+
return;
|
|
60
|
+
for (const read of guards.current.values()) {
|
|
61
|
+
const guard = read();
|
|
62
|
+
if (guard.dirty) {
|
|
63
|
+
setConfirmation({ messages: guard.messages });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
setOpen(false);
|
|
68
|
+
}, [onBeforeClose, setOpen]);
|
|
69
|
+
// Stable handlers read the most recent committed props, without resubscribing the stack.
|
|
70
|
+
const actions = useRef({ requestClose, setPinned, setOpen });
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
actions.current = { requestClose, setPinned, setOpen };
|
|
73
|
+
});
|
|
74
|
+
useLayoutEffect(() => stack.store.register(id, {
|
|
75
|
+
reopen: () => actions.current.setOpen(true),
|
|
76
|
+
close: (reason) => actions.current.requestClose(reason),
|
|
77
|
+
pin: (next) => actions.current.setPinned(next),
|
|
78
|
+
focus: () => contentRef.current?.focus({ preventScroll: true }),
|
|
79
|
+
}), [stack.store, id]);
|
|
80
|
+
useLayoutEffect(() => {
|
|
81
|
+
if (open)
|
|
82
|
+
stack.store.update(id, { side, width, pinned, pinnable, requestedOrder: order });
|
|
83
|
+
else
|
|
84
|
+
stack.store.remove(id);
|
|
85
|
+
}, [stack.store, id, open, side, width, pinned, pinnable, order]);
|
|
86
|
+
const controls = useMemo(() => ({
|
|
87
|
+
open,
|
|
88
|
+
pinned,
|
|
89
|
+
docked: layout?.pinned ?? false,
|
|
90
|
+
close: (reason) => actions.current.requestClose(reason),
|
|
91
|
+
pin: () => stack.store.pin(id),
|
|
92
|
+
unpin: () => actions.current.setPinned(false),
|
|
93
|
+
registerGuard,
|
|
94
|
+
}), [open, pinned, layout?.pinned, stack.store, id, registerGuard]);
|
|
95
|
+
const value = useMemo(() => ({
|
|
96
|
+
id,
|
|
97
|
+
open,
|
|
98
|
+
pinned,
|
|
99
|
+
pinnable,
|
|
100
|
+
side,
|
|
101
|
+
width,
|
|
102
|
+
layout,
|
|
103
|
+
descriptionId,
|
|
104
|
+
registerDescription,
|
|
105
|
+
contentRef,
|
|
106
|
+
triggerRef,
|
|
107
|
+
requestClose,
|
|
108
|
+
setOpen,
|
|
109
|
+
setPinned,
|
|
110
|
+
registerGuard,
|
|
111
|
+
}), [
|
|
112
|
+
id,
|
|
113
|
+
open,
|
|
114
|
+
pinned,
|
|
115
|
+
pinnable,
|
|
116
|
+
side,
|
|
117
|
+
width,
|
|
118
|
+
layout,
|
|
119
|
+
requestClose,
|
|
120
|
+
setOpen,
|
|
121
|
+
setPinned,
|
|
122
|
+
registerGuard,
|
|
123
|
+
descriptionId,
|
|
124
|
+
registerDescription,
|
|
125
|
+
]);
|
|
126
|
+
return (_jsx(SheetControlsContext.Provider, { value: controls, children: _jsxs(SheetInstanceContext.Provider, { value: value, children: [_jsx(Dialog.Root, { open: open, modal: false, onOpenChange: (next) => (next ? setOpen(true) : requestClose()), children: children }), _jsx(CloseConfirmation, { open: open && confirmation !== null, messages: confirmation?.messages, onCancel: () => setConfirmation(null), onDiscard: () => setOpen(false), onPin: layout?.canPin && !pinned
|
|
127
|
+
? () => {
|
|
128
|
+
setPinned(true);
|
|
129
|
+
setConfirmation(null);
|
|
130
|
+
}
|
|
131
|
+
: undefined })] }) }));
|
|
132
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function lockSheetScroll(document: Document): () => void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Multiple independent providers may overlap. Closing one must not unlock another's page.
|
|
2
|
+
const locks = new WeakMap();
|
|
3
|
+
export function lockSheetScroll(document) {
|
|
4
|
+
const existing = locks.get(document);
|
|
5
|
+
const lock = existing ?? { count: 0, overflow: document.body.style.overflow };
|
|
6
|
+
lock.count += 1;
|
|
7
|
+
locks.set(document, lock);
|
|
8
|
+
document.body.style.overflow = "hidden";
|
|
9
|
+
return () => {
|
|
10
|
+
lock.count -= 1;
|
|
11
|
+
if (lock.count)
|
|
12
|
+
return;
|
|
13
|
+
document.body.style.overflow = lock.overflow;
|
|
14
|
+
locks.delete(document);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SheetGuardMessages } from "./context.js";
|
|
2
|
+
/** Inline messages are safe. Dirty form changes do not rerender the sheet stack. */
|
|
3
|
+
export declare function useSheetCloseGuard(dirty: boolean, messages?: SheetGuardMessages): void;
|
|
4
|
+
export declare function CloseConfirmation({ open, messages, onCancel, onDiscard, onPin, }: {
|
|
5
|
+
open: boolean;
|
|
6
|
+
messages?: SheetGuardMessages;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
onDiscard: () => void;
|
|
9
|
+
onPin?: () => void;
|
|
10
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useContext, useLayoutEffect, useRef } from "react";
|
|
4
|
+
import * as AlertDialog from "@radix-ui/react-alert-dialog";
|
|
5
|
+
import { Button } from "../customization.js";
|
|
6
|
+
import { useMendyLocale } from "../locale-context.js";
|
|
7
|
+
import { SheetControlsContext, SheetStackContext } from "./context.js";
|
|
8
|
+
/** Inline messages are safe. Dirty form changes do not rerender the sheet stack. */
|
|
9
|
+
export function useSheetCloseGuard(dirty, messages) {
|
|
10
|
+
const registerGuard = useContext(SheetControlsContext)?.registerGuard;
|
|
11
|
+
const current = useRef({ dirty, messages });
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
current.current = { dirty, messages };
|
|
14
|
+
});
|
|
15
|
+
useLayoutEffect(() => registerGuard?.(() => current.current), [registerGuard]);
|
|
16
|
+
}
|
|
17
|
+
export function CloseConfirmation({ open, messages, onCancel, onDiscard, onPin, }) {
|
|
18
|
+
const { t, direction } = useMendyLocale();
|
|
19
|
+
const stack = useContext(SheetStackContext);
|
|
20
|
+
return (_jsx(AlertDialog.Root, { open: open, onOpenChange: (next) => {
|
|
21
|
+
if (!next)
|
|
22
|
+
onCancel();
|
|
23
|
+
}, children: _jsxs(AlertDialog.Portal, { container: stack.container, children: [_jsx(AlertDialog.Overlay, { "data-mendy-ui": "", className: "mui-cb20bd519346 mui-992a93455c71 mui-ce13093ad8c9 mui-e749b7f89462 mui-8e5a84e37552" }), _jsxs(AlertDialog.Content, { "data-mendy-ui": "", dir: direction, className: "mui-cb20bd519346 mui-992a93455c71 mui-69b8e049976d mui-4e45f4841abb mui-d0957da54446 mui-0f2a693e93e2 mui-42be1194a64d mui-7b7574448a48 mui-90fc3ec35a37 mui-49abc543ea42 mui-4f1a55de40bc mui-e593c38256e0 mui-c432836760e3 mui-5bd2afe89f0b mui-0d4ca75de41c mui-b30fc56058b6", style: { transform: `translate(${direction === "rtl" ? "50%" : "-50%"}, -50%)` }, children: [_jsx(AlertDialog.Title, { className: "mui-035bd245837f mui-87e86ce38eaf", children: messages?.title ?? t("sheetUnsavedTitle") }), _jsx(AlertDialog.Description, { className: "mui-c74ab393b96d mui-35f35c41d134", children: messages?.description ??
|
|
24
|
+
t(onPin ? "sheetUnsavedPinDescription" : "sheetUnsavedDescription") }), _jsxs("div", { className: "mui-222f930b8752 mui-faa8a23c68f6 mui-307644c34db6 mui-074569488cca", children: [_jsx(AlertDialog.Cancel, { asChild: true, children: _jsx(Button, { variant: "outline", children: messages?.cancel ?? t("sheetKeepEditing") }) }), _jsx(AlertDialog.Action, { asChild: true, children: _jsx(Button, { variant: "destructive", onClick: onDiscard, children: messages?.confirm ?? t("sheetDiscard") }) }), onPin && (_jsx(AlertDialog.Action, { asChild: true, children: _jsx(Button, { onClick: onPin, children: t("sheetPin") }) }))] })] })] }) }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SheetEntry, StackLayout } from "./layout.js";
|
|
2
|
+
import type { OpenSheetOptions, SheetCloseReason, SheetLayout, SheetRecord } from "./types.js";
|
|
3
|
+
type Handlers = {
|
|
4
|
+
close: (reason: SheetCloseReason) => void;
|
|
5
|
+
reopen: () => void;
|
|
6
|
+
pin: (pinned: boolean) => void;
|
|
7
|
+
focus: () => void;
|
|
8
|
+
};
|
|
9
|
+
export interface ManagedSheetOptions extends OpenSheetOptions {
|
|
10
|
+
initialPinned: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function createSheetStore(): {
|
|
13
|
+
subscribe(listener: () => void): () => void;
|
|
14
|
+
getLayout: () => StackLayout;
|
|
15
|
+
getSheet: (id: string) => SheetLayout | undefined;
|
|
16
|
+
getManaged: () => readonly ManagedSheetOptions[];
|
|
17
|
+
setViewport(width: number): void;
|
|
18
|
+
register(id: string, callbacks: Handlers): () => void;
|
|
19
|
+
update(id: string, config: Omit<SheetEntry, "id" | "order" | "pinOrder">): void;
|
|
20
|
+
remove(id: string): void;
|
|
21
|
+
close(id: string, reason?: SheetCloseReason): void;
|
|
22
|
+
closeTop(reason?: SheetCloseReason): void;
|
|
23
|
+
pin(id: string): void;
|
|
24
|
+
unpin(id: string): void;
|
|
25
|
+
pinTop(): boolean;
|
|
26
|
+
focusTop(): boolean;
|
|
27
|
+
restore(options: OpenSheetOptions): void;
|
|
28
|
+
open(options: OpenSheetOptions): void;
|
|
29
|
+
removeManaged(id: string): void;
|
|
30
|
+
records(): SheetRecord[];
|
|
31
|
+
};
|
|
32
|
+
export type SheetStore = ReturnType<typeof createSheetStore>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { layoutSheets } from "./layout.js";
|
|
2
|
+
const EMPTY_MANAGED = [];
|
|
3
|
+
export function createSheetStore() {
|
|
4
|
+
const entries = new Map();
|
|
5
|
+
const handlers = new Map();
|
|
6
|
+
const listeners = new Set();
|
|
7
|
+
let managed = EMPTY_MANAGED;
|
|
8
|
+
let viewport = 0;
|
|
9
|
+
let order = 0;
|
|
10
|
+
let layout = layoutSheets([], viewport);
|
|
11
|
+
function emit() {
|
|
12
|
+
for (const listener of listeners)
|
|
13
|
+
listener();
|
|
14
|
+
}
|
|
15
|
+
function recalculate() {
|
|
16
|
+
const next = layoutSheets([...entries.values()], viewport);
|
|
17
|
+
// Keep individual snapshots referentially stable for useSyncExternalStore.
|
|
18
|
+
const stable = new Map();
|
|
19
|
+
for (const [id, value] of next.sheets) {
|
|
20
|
+
const previous = layout.sheets.get(id);
|
|
21
|
+
stable.set(id, previous &&
|
|
22
|
+
Object.keys(value).every((key) => value[key] === previous[key])
|
|
23
|
+
? previous
|
|
24
|
+
: value);
|
|
25
|
+
}
|
|
26
|
+
if (layout.startWidth === next.startWidth &&
|
|
27
|
+
layout.endWidth === next.endWidth &&
|
|
28
|
+
layout.hasOverlay === next.hasOverlay &&
|
|
29
|
+
layout.sheets.size === stable.size &&
|
|
30
|
+
[...stable].every(([id, value]) => layout.sheets.get(id) === value))
|
|
31
|
+
return;
|
|
32
|
+
layout = { ...next, sheets: stable };
|
|
33
|
+
emit();
|
|
34
|
+
}
|
|
35
|
+
const api = {
|
|
36
|
+
subscribe(listener) {
|
|
37
|
+
listeners.add(listener);
|
|
38
|
+
return () => {
|
|
39
|
+
listeners.delete(listener);
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
getLayout: () => layout,
|
|
43
|
+
getSheet: (id) => layout.sheets.get(id),
|
|
44
|
+
getManaged: () => managed,
|
|
45
|
+
setViewport(width) {
|
|
46
|
+
if (width === viewport)
|
|
47
|
+
return;
|
|
48
|
+
viewport = width;
|
|
49
|
+
recalculate();
|
|
50
|
+
},
|
|
51
|
+
register(id, callbacks) {
|
|
52
|
+
handlers.set(id, callbacks);
|
|
53
|
+
return () => {
|
|
54
|
+
if (handlers.get(id) === callbacks) {
|
|
55
|
+
handlers.delete(id);
|
|
56
|
+
entries.delete(id);
|
|
57
|
+
recalculate();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
update(id, config) {
|
|
62
|
+
const previous = entries.get(id);
|
|
63
|
+
if (previous &&
|
|
64
|
+
Object.keys(config).every((key) => previous[key] === config[key]))
|
|
65
|
+
return;
|
|
66
|
+
const nextOrder = config.requestedOrder ?? previous?.order ?? ++order;
|
|
67
|
+
entries.set(id, {
|
|
68
|
+
...config,
|
|
69
|
+
id,
|
|
70
|
+
order: nextOrder,
|
|
71
|
+
pinOrder: config.pinned ? (previous?.pinned ? previous.pinOrder : ++order) : 0,
|
|
72
|
+
});
|
|
73
|
+
order = Math.max(order, nextOrder);
|
|
74
|
+
recalculate();
|
|
75
|
+
},
|
|
76
|
+
remove(id) {
|
|
77
|
+
if (entries.delete(id))
|
|
78
|
+
recalculate();
|
|
79
|
+
},
|
|
80
|
+
close(id, reason = "programmatic") {
|
|
81
|
+
handlers.get(id)?.close(reason);
|
|
82
|
+
},
|
|
83
|
+
closeTop(reason = "programmatic") {
|
|
84
|
+
const top = [...layout.sheets].find(([, value]) => value.top);
|
|
85
|
+
if (top)
|
|
86
|
+
handlers.get(top[0])?.close(reason);
|
|
87
|
+
},
|
|
88
|
+
pin(id) {
|
|
89
|
+
if (layout.sheets.get(id)?.canPin)
|
|
90
|
+
handlers.get(id)?.pin(true);
|
|
91
|
+
},
|
|
92
|
+
unpin(id) {
|
|
93
|
+
handlers.get(id)?.pin(false);
|
|
94
|
+
},
|
|
95
|
+
pinTop() {
|
|
96
|
+
const top = [...layout.sheets].find(([, value]) => value.top && value.canPin && !value.pinned);
|
|
97
|
+
if (!top)
|
|
98
|
+
return false;
|
|
99
|
+
api.pin(top[0]);
|
|
100
|
+
return true;
|
|
101
|
+
},
|
|
102
|
+
focusTop() {
|
|
103
|
+
const top = [...layout.sheets].find(([, value]) => value.top);
|
|
104
|
+
if (top)
|
|
105
|
+
handlers.get(top[0])?.focus();
|
|
106
|
+
return Boolean(top);
|
|
107
|
+
},
|
|
108
|
+
restore(options) {
|
|
109
|
+
if (managed.some((entry) => entry.id === options.id))
|
|
110
|
+
return;
|
|
111
|
+
managed = [...managed, { ...options, initialPinned: true }];
|
|
112
|
+
emit();
|
|
113
|
+
},
|
|
114
|
+
open(options) {
|
|
115
|
+
if (entries.has(options.id)) {
|
|
116
|
+
const existing = entries.get(options.id);
|
|
117
|
+
entries.set(options.id, { ...existing, order: ++order });
|
|
118
|
+
recalculate();
|
|
119
|
+
handlers.get(options.id)?.focus();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (managed.some((entry) => entry.id === options.id)) {
|
|
123
|
+
handlers.get(options.id)?.reopen();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
managed = [...managed, { ...options, initialPinned: false }];
|
|
127
|
+
emit();
|
|
128
|
+
},
|
|
129
|
+
removeManaged(id) {
|
|
130
|
+
if (entries.has(id))
|
|
131
|
+
return;
|
|
132
|
+
if (!managed.some((entry) => entry.id === id))
|
|
133
|
+
return;
|
|
134
|
+
managed = managed.filter((entry) => entry.id !== id);
|
|
135
|
+
emit();
|
|
136
|
+
},
|
|
137
|
+
records() {
|
|
138
|
+
return managed
|
|
139
|
+
.flatMap((entry) => {
|
|
140
|
+
const state = entries.get(entry.id);
|
|
141
|
+
if (!entry.persist || !state?.pinned)
|
|
142
|
+
return [];
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
pinOrder: state.pinOrder,
|
|
146
|
+
record: { id: entry.id, ...entry.persist, side: state.side, width: state.width },
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
})
|
|
150
|
+
.sort((a, b) => a.pinOrder - b.pinOrder)
|
|
151
|
+
.map(({ record }) => record);
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
return api;
|
|
155
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type SheetSide = "start" | "end";
|
|
3
|
+
export type SheetCloseReason = "close-button" | "escape" | "outside" | "programmatic";
|
|
4
|
+
export type SheetJson = null | boolean | number | string | SheetJson[] | {
|
|
5
|
+
[key: string]: SheetJson;
|
|
6
|
+
};
|
|
7
|
+
/** Only identifiers needed to fetch a record again. Form drafts are never saved automatically. */
|
|
8
|
+
export interface SheetRecord {
|
|
9
|
+
id: string;
|
|
10
|
+
type: string;
|
|
11
|
+
payload?: SheetJson;
|
|
12
|
+
}
|
|
13
|
+
export interface SheetPersistence {
|
|
14
|
+
/** Scope this key to the signed-in user and workspace. Changing it resets managed sheets. */
|
|
15
|
+
key: string;
|
|
16
|
+
/** Defaults to sessionStorage. Access is deferred until mount. */
|
|
17
|
+
storage?: Pick<Storage, "getItem" | "setItem">;
|
|
18
|
+
/** Return sheet content, or null for records that no longer exist or are accessible. */
|
|
19
|
+
resolve: (record: SheetRecord) => ReactNode;
|
|
20
|
+
onError?: (error: unknown) => void;
|
|
21
|
+
}
|
|
22
|
+
export interface OpenSheetOptions {
|
|
23
|
+
/** Opening an existing id brings that instance forward without losing its state. */
|
|
24
|
+
id: string;
|
|
25
|
+
render: () => ReactNode;
|
|
26
|
+
side?: SheetSide;
|
|
27
|
+
width?: number;
|
|
28
|
+
pinnable?: boolean;
|
|
29
|
+
/** Opt this instance into reload restoration. */
|
|
30
|
+
persist?: Omit<SheetRecord, "id">;
|
|
31
|
+
}
|
|
32
|
+
export interface SheetController {
|
|
33
|
+
open: (options: OpenSheetOptions) => void;
|
|
34
|
+
/** Uses the same unsaved-changes guard as the close button. */
|
|
35
|
+
close: (id: string) => void;
|
|
36
|
+
closeTop: () => void;
|
|
37
|
+
pin: (id: string) => void;
|
|
38
|
+
unpin: (id: string) => void;
|
|
39
|
+
}
|
|
40
|
+
export interface SheetLayout {
|
|
41
|
+
offset: number;
|
|
42
|
+
width: number;
|
|
43
|
+
pinned: boolean;
|
|
44
|
+
covered: boolean;
|
|
45
|
+
top: boolean;
|
|
46
|
+
canPin: boolean;
|
|
47
|
+
modal: boolean;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import type * as Dialog from "@radix-ui/react-dialog";
|
|
3
|
+
export declare function useSheetFocus({ onOpenAutoFocus, onCloseAutoFocus, }: Pick<ComponentProps<typeof Dialog.Content>, "onOpenAutoFocus" | "onCloseAutoFocus">): {
|
|
4
|
+
onOpen(event: Event): void;
|
|
5
|
+
onClose(event: Event): void;
|
|
6
|
+
};
|