premium-ds 0.1.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/README.md +113 -0
- package/dist/alert.d.ts +31 -0
- package/dist/alert.js +6 -0
- package/dist/alert.js.map +1 -0
- package/dist/avatar-group.d.ts +13 -0
- package/dist/avatar-group.js +3 -0
- package/dist/avatar-group.js.map +1 -0
- package/dist/avatar.d.ts +25 -0
- package/dist/avatar.js +3 -0
- package/dist/avatar.js.map +1 -0
- package/dist/badge.d.ts +23 -0
- package/dist/badge.js +3 -0
- package/dist/badge.js.map +1 -0
- package/dist/button.d.ts +20 -0
- package/dist/button.js +3 -0
- package/dist/button.js.map +1 -0
- package/dist/checkbox.d.ts +25 -0
- package/dist/checkbox.js +3 -0
- package/dist/checkbox.js.map +1 -0
- package/dist/chunk-2OWHZ4JT.js +36 -0
- package/dist/chunk-2OWHZ4JT.js.map +1 -0
- package/dist/chunk-34SIXSYL.js +64 -0
- package/dist/chunk-34SIXSYL.js.map +1 -0
- package/dist/chunk-37O2ZXD6.js +55 -0
- package/dist/chunk-37O2ZXD6.js.map +1 -0
- package/dist/chunk-4AZL76UJ.js +89 -0
- package/dist/chunk-4AZL76UJ.js.map +1 -0
- package/dist/chunk-4HSCN5TZ.js +86 -0
- package/dist/chunk-4HSCN5TZ.js.map +1 -0
- package/dist/chunk-5DDOOT33.js +258 -0
- package/dist/chunk-5DDOOT33.js.map +1 -0
- package/dist/chunk-5FVHWIMY.js +117 -0
- package/dist/chunk-5FVHWIMY.js.map +1 -0
- package/dist/chunk-5K6KRJGX.js +147 -0
- package/dist/chunk-5K6KRJGX.js.map +1 -0
- package/dist/chunk-5PQMQBQC.js +74 -0
- package/dist/chunk-5PQMQBQC.js.map +1 -0
- package/dist/chunk-7OCTVQ7C.js +95 -0
- package/dist/chunk-7OCTVQ7C.js.map +1 -0
- package/dist/chunk-7OPMOET7.js +39 -0
- package/dist/chunk-7OPMOET7.js.map +1 -0
- package/dist/chunk-BXXS7YRC.js +270 -0
- package/dist/chunk-BXXS7YRC.js.map +1 -0
- package/dist/chunk-CV2Q4YXX.js +272 -0
- package/dist/chunk-CV2Q4YXX.js.map +1 -0
- package/dist/chunk-EIMMDWIW.js +282 -0
- package/dist/chunk-EIMMDWIW.js.map +1 -0
- package/dist/chunk-EZ2CWTBE.js +230 -0
- package/dist/chunk-EZ2CWTBE.js.map +1 -0
- package/dist/chunk-FGHDG3Y4.js +89 -0
- package/dist/chunk-FGHDG3Y4.js.map +1 -0
- package/dist/chunk-FPP2XLKX.js +127 -0
- package/dist/chunk-FPP2XLKX.js.map +1 -0
- package/dist/chunk-G6OY35DI.js +295 -0
- package/dist/chunk-G6OY35DI.js.map +1 -0
- package/dist/chunk-H6KWJNOE.js +65 -0
- package/dist/chunk-H6KWJNOE.js.map +1 -0
- package/dist/chunk-HGILYGY3.js +45 -0
- package/dist/chunk-HGILYGY3.js.map +1 -0
- package/dist/chunk-I3BCB4Z5.js +88 -0
- package/dist/chunk-I3BCB4Z5.js.map +1 -0
- package/dist/chunk-KBWNUUWM.js +582 -0
- package/dist/chunk-KBWNUUWM.js.map +1 -0
- package/dist/chunk-KN7JFAZ6.js +113 -0
- package/dist/chunk-KN7JFAZ6.js.map +1 -0
- package/dist/chunk-MEF7PI6U.js +16 -0
- package/dist/chunk-MEF7PI6U.js.map +1 -0
- package/dist/chunk-NKGMQL6I.js +310 -0
- package/dist/chunk-NKGMQL6I.js.map +1 -0
- package/dist/chunk-NMFQRGLL.js +127 -0
- package/dist/chunk-NMFQRGLL.js.map +1 -0
- package/dist/chunk-OUBWD6CX.js +433 -0
- package/dist/chunk-OUBWD6CX.js.map +1 -0
- package/dist/chunk-PFNXVBLU.js +96 -0
- package/dist/chunk-PFNXVBLU.js.map +1 -0
- package/dist/chunk-PUPZ4HME.js +165 -0
- package/dist/chunk-PUPZ4HME.js.map +1 -0
- package/dist/chunk-QFS52OK5.js +690 -0
- package/dist/chunk-QFS52OK5.js.map +1 -0
- package/dist/chunk-QNC6O3PG.js +45 -0
- package/dist/chunk-QNC6O3PG.js.map +1 -0
- package/dist/chunk-QUHOXWBK.js +82 -0
- package/dist/chunk-QUHOXWBK.js.map +1 -0
- package/dist/chunk-UIQGSTBJ.js +106 -0
- package/dist/chunk-UIQGSTBJ.js.map +1 -0
- package/dist/chunk-UJQKVP6V.js +193 -0
- package/dist/chunk-UJQKVP6V.js.map +1 -0
- package/dist/chunk-VVPGEAC6.js +11 -0
- package/dist/chunk-VVPGEAC6.js.map +1 -0
- package/dist/chunk-XA3T5KWA.js +58 -0
- package/dist/chunk-XA3T5KWA.js.map +1 -0
- package/dist/chunk-YSHJHSJM.js +19 -0
- package/dist/chunk-YSHJHSJM.js.map +1 -0
- package/dist/chunk-YVHOAVSM.js +182 -0
- package/dist/chunk-YVHOAVSM.js.map +1 -0
- package/dist/collapse.d.ts +16 -0
- package/dist/collapse.js +3 -0
- package/dist/collapse.js.map +1 -0
- package/dist/count-badge.d.ts +11 -0
- package/dist/count-badge.js +4 -0
- package/dist/count-badge.js.map +1 -0
- package/dist/date-field.d.ts +39 -0
- package/dist/date-field.js +8 -0
- package/dist/date-field.js.map +1 -0
- package/dist/date-range-field.d.ts +30 -0
- package/dist/date-range-field.js +8 -0
- package/dist/date-range-field.js.map +1 -0
- package/dist/datetime-field.d.ts +28 -0
- package/dist/datetime-field.js +10 -0
- package/dist/datetime-field.js.map +1 -0
- package/dist/dialog.d.ts +26 -0
- package/dist/dialog.js +7 -0
- package/dist/dialog.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/motion-tokens.d.ts +29 -0
- package/dist/motion-tokens.js +3 -0
- package/dist/motion-tokens.js.map +1 -0
- package/dist/multi-select.d.ts +25 -0
- package/dist/multi-select.js +7 -0
- package/dist/multi-select.js.map +1 -0
- package/dist/number-field.d.ts +24 -0
- package/dist/number-field.js +4 -0
- package/dist/number-field.js.map +1 -0
- package/dist/otp-field.d.ts +20 -0
- package/dist/otp-field.js +3 -0
- package/dist/otp-field.js.map +1 -0
- package/dist/overlay.d.ts +31 -0
- package/dist/overlay.js +4 -0
- package/dist/overlay.js.map +1 -0
- package/dist/pagination.d.ts +24 -0
- package/dist/pagination.js +5 -0
- package/dist/pagination.js.map +1 -0
- package/dist/radio-group.d.ts +46 -0
- package/dist/radio-group.js +6 -0
- package/dist/radio-group.js.map +1 -0
- package/dist/select-core-SAyS-8w0.d.ts +16 -0
- package/dist/select.d.ts +27 -0
- package/dist/select.js +7 -0
- package/dist/select.js.map +1 -0
- package/dist/status-badge.d.ts +17 -0
- package/dist/status-badge.js +5 -0
- package/dist/status-badge.js.map +1 -0
- package/dist/table.d.ts +65 -0
- package/dist/table.js +5 -0
- package/dist/table.js.map +1 -0
- package/dist/tabs.d.ts +44 -0
- package/dist/tabs.js +5 -0
- package/dist/tabs.js.map +1 -0
- package/dist/tag.d.ts +28 -0
- package/dist/tag.js +5 -0
- package/dist/tag.js.map +1 -0
- package/dist/text-field.d.ts +30 -0
- package/dist/text-field.js +6 -0
- package/dist/text-field.js.map +1 -0
- package/dist/textarea.d.ts +33 -0
- package/dist/textarea.js +5 -0
- package/dist/textarea.js.map +1 -0
- package/dist/time-field.d.ts +27 -0
- package/dist/time-field.js +6 -0
- package/dist/time-field.js.map +1 -0
- package/dist/toast-store.d.ts +75 -0
- package/dist/toast-store.js +3 -0
- package/dist/toast-store.js.map +1 -0
- package/dist/toast.d.ts +3 -0
- package/dist/toast.js +6 -0
- package/dist/toast.js.map +1 -0
- package/dist/toggle-tag.d.ts +24 -0
- package/dist/toggle-tag.js +4 -0
- package/dist/toggle-tag.js.map +1 -0
- package/dist/toggle.d.ts +21 -0
- package/dist/toggle.js +3 -0
- package/dist/toggle.js.map +1 -0
- package/dist/tooltip.d.ts +27 -0
- package/dist/tooltip.js +4 -0
- package/dist/tooltip.js.map +1 -0
- package/llms.txt +165 -0
- package/package.json +205 -0
- package/src/components/alert/Alert.tsx +118 -0
- package/src/components/alert/alert.css +136 -0
- package/src/components/avatar/Avatar.tsx +128 -0
- package/src/components/avatar/AvatarGroup.tsx +50 -0
- package/src/components/avatar/avatar.css +200 -0
- package/src/components/badge/Badge.tsx +66 -0
- package/src/components/badge/CountBadge.tsx +46 -0
- package/src/components/badge/StatusBadge.tsx +132 -0
- package/src/components/badge/badge.css +243 -0
- package/src/components/button/Button.tsx +68 -0
- package/src/components/button/button.css +222 -0
- package/src/components/checkbox/Checkbox.tsx +90 -0
- package/src/components/checkbox/checkbox.css +179 -0
- package/src/components/date-picker/DateField.tsx +362 -0
- package/src/components/date-picker/DateRangeField.tsx +533 -0
- package/src/components/date-picker/DateTimeField.tsx +177 -0
- package/src/components/date-picker/TimeField.tsx +100 -0
- package/src/components/date-picker/date-picker.css +591 -0
- package/src/components/date-picker/date-utils.ts +55 -0
- package/src/components/date-picker/field-shell.tsx +78 -0
- package/src/components/date-picker/glide-pill.tsx +81 -0
- package/src/components/date-picker/time-core.tsx +305 -0
- package/src/components/dialog/Dialog.tsx +181 -0
- package/src/components/dialog/dialog.css +170 -0
- package/src/components/glass/glass.css +100 -0
- package/src/components/icon/Icon.tsx +76 -0
- package/src/components/icon/IconSlot.tsx +11 -0
- package/src/components/icon/icon.css +33 -0
- package/src/components/input/NumberField.tsx +117 -0
- package/src/components/input/OtpField.tsx +118 -0
- package/src/components/input/TextField.tsx +123 -0
- package/src/components/input/input.css +335 -0
- package/src/components/motion/Collapse.tsx +33 -0
- package/src/components/motion/collapse.css +41 -0
- package/src/components/overlay/Overlay.tsx +239 -0
- package/src/components/overlay/overlay-core.tsx +565 -0
- package/src/components/overlay/overlay.css +119 -0
- package/src/components/overlay/sheet-drag.tsx +146 -0
- package/src/components/pagination/Pagination.tsx +140 -0
- package/src/components/pagination/pagination.css +48 -0
- package/src/components/radio-group/RadioGroup.tsx +182 -0
- package/src/components/radio-group/radio-group.css +277 -0
- package/src/components/select/MultiSelect.tsx +251 -0
- package/src/components/select/Select.tsx +235 -0
- package/src/components/select/select-core.tsx +417 -0
- package/src/components/select/select.css +386 -0
- package/src/components/table/Table.tsx +433 -0
- package/src/components/table/table.css +348 -0
- package/src/components/tabs/Tabs.tsx +371 -0
- package/src/components/tabs/tabs.css +228 -0
- package/src/components/tag/Tag.tsx +145 -0
- package/src/components/tag/ToggleTag.tsx +125 -0
- package/src/components/tag/tag.css +248 -0
- package/src/components/textarea/Textarea.tsx +197 -0
- package/src/components/textarea/textarea.css +219 -0
- package/src/components/toast/Toast.tsx +349 -0
- package/src/components/toast/toast-store.ts +266 -0
- package/src/components/toast/toast.css +233 -0
- package/src/components/toggle/Toggle.tsx +94 -0
- package/src/components/toggle/toggle.css +152 -0
- package/src/components/tooltip/Tooltip.tsx +365 -0
- package/src/components/tooltip/tooltip.css +86 -0
- package/src/index.ts +42 -0
- package/src/styles.css +39 -0
- package/src/tokens/avatar.css +20 -0
- package/src/tokens/color.css +56 -0
- package/src/tokens/elevation.css +20 -0
- package/src/tokens/fonts.css +3 -0
- package/src/tokens/glass.css +21 -0
- package/src/tokens/icons.css +7 -0
- package/src/tokens/layers.css +6 -0
- package/src/tokens/motion-tokens.ts +72 -0
- package/src/tokens/motion.css +49 -0
- package/src/tokens/radius.css +11 -0
- package/src/tokens/semantic.css +75 -0
- package/src/tokens/spacing.css +26 -0
- package/src/tokens/typography.css +54 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/* overlay-core - the headless mechanics under popover, dialog and sheet. */
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
6
|
+
import { motion as ovMotion, AnimatePresence as OvAnimatePresence } from 'motion/react';
|
|
7
|
+
import { UIMotion } from '../../tokens/motion-tokens';
|
|
8
|
+
|
|
9
|
+
const ovSM = UIMotion;
|
|
10
|
+
const {
|
|
11
|
+
useState: ovUseState,
|
|
12
|
+
useEffect: ovUseEffect,
|
|
13
|
+
useLayoutEffect: ovUseLayoutEffect,
|
|
14
|
+
useRef: ovUseRef,
|
|
15
|
+
} = React;
|
|
16
|
+
|
|
17
|
+
function useControllable<T>(
|
|
18
|
+
controlled: T | undefined,
|
|
19
|
+
initial: T,
|
|
20
|
+
onChange?: (next: T) => void,
|
|
21
|
+
): [T, (next: T) => void] {
|
|
22
|
+
const [internal, setInternal] = ovUseState(initial);
|
|
23
|
+
const isControlled = controlled !== undefined;
|
|
24
|
+
const value = isControlled ? controlled : internal;
|
|
25
|
+
const setValue = (next: T) => {
|
|
26
|
+
if (!isControlled) setInternal(next);
|
|
27
|
+
if (onChange) onChange(next);
|
|
28
|
+
};
|
|
29
|
+
return [value, setValue];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Token length - px; parseFloat alone reads "0.5rem" as 0.5px, so convert rem. */
|
|
33
|
+
function ovReadPx(token: string): number {
|
|
34
|
+
const root = document.documentElement;
|
|
35
|
+
const v = getComputedStyle(root).getPropertyValue(token).trim();
|
|
36
|
+
const n = parseFloat(v);
|
|
37
|
+
if (Number.isNaN(n)) return 0;
|
|
38
|
+
return v.endsWith('rem') ? n * parseFloat(getComputedStyle(root).fontSize) : n;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ovResolveChildren(
|
|
42
|
+
children: React.ReactNode | ((api: { close: () => void }) => React.ReactNode),
|
|
43
|
+
close: () => void,
|
|
44
|
+
): React.ReactNode {
|
|
45
|
+
return typeof children === 'function' ? children({ close }) : children;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ovCloneTrigger(
|
|
49
|
+
trigger: React.ReactElement | null,
|
|
50
|
+
{
|
|
51
|
+
open,
|
|
52
|
+
onPress,
|
|
53
|
+
panelId,
|
|
54
|
+
haspopup,
|
|
55
|
+
triggerRef,
|
|
56
|
+
}: {
|
|
57
|
+
open: boolean;
|
|
58
|
+
onPress: () => void;
|
|
59
|
+
panelId: string;
|
|
60
|
+
haspopup: string;
|
|
61
|
+
triggerRef: React.RefObject<HTMLElement>;
|
|
62
|
+
},
|
|
63
|
+
): React.ReactElement | null {
|
|
64
|
+
if (!trigger) return null;
|
|
65
|
+
const setRef = (node: HTMLElement | null) => {
|
|
66
|
+
if (triggerRef) triggerRef.current = node;
|
|
67
|
+
const r = (trigger as any).ref;
|
|
68
|
+
if (typeof r === 'function') r(node);
|
|
69
|
+
else if (r) r.current = node;
|
|
70
|
+
};
|
|
71
|
+
return React.cloneElement(trigger as React.ReactElement<any>, {
|
|
72
|
+
ref: setRef,
|
|
73
|
+
onClick: (e: React.MouseEvent) => {
|
|
74
|
+
const oc = (trigger.props as { onClick?: (e: React.MouseEvent) => void }).onClick;
|
|
75
|
+
if (oc) oc(e);
|
|
76
|
+
onPress();
|
|
77
|
+
},
|
|
78
|
+
'aria-haspopup': haspopup,
|
|
79
|
+
'aria-expanded': open,
|
|
80
|
+
'aria-controls': open ? panelId : undefined,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* The overlay stack: one Esc listener closes only the topmost dismissible, and defers to inner handlers that already consumed the key (defaultPrevented). */
|
|
85
|
+
interface OverlayEntry {
|
|
86
|
+
contains: (t: EventTarget | null) => boolean;
|
|
87
|
+
isDismissible: () => boolean;
|
|
88
|
+
requestClose: () => void;
|
|
89
|
+
_dismissible?: boolean;
|
|
90
|
+
_close?: () => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const ovStack: OverlayEntry[] = [];
|
|
94
|
+
|
|
95
|
+
function ovOnDocKeyDown(e: KeyboardEvent) {
|
|
96
|
+
if (e.key !== 'Escape' || e.defaultPrevented || ovStack.length === 0) return;
|
|
97
|
+
const top = ovStack[ovStack.length - 1];
|
|
98
|
+
if (!top.isDismissible()) return;
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
top.requestClose();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ovIsTop(entry: OverlayEntry) {
|
|
104
|
+
return ovStack[ovStack.length - 1] === entry;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* true if `target` sits in an overlay opened after `entry` (a nested child owns it). */
|
|
108
|
+
function ovInOverlayAbove(entry: OverlayEntry, target: EventTarget | null) {
|
|
109
|
+
const i = ovStack.indexOf(entry);
|
|
110
|
+
return i >= 0 && ovStack.slice(i + 1).some((e) => e.contains(target));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Join the stack for the panel's lifetime; set z = --layer-overlay + depth. */
|
|
114
|
+
function useOverlayEntry({
|
|
115
|
+
nodeRef,
|
|
116
|
+
dismissible,
|
|
117
|
+
requestClose,
|
|
118
|
+
}: {
|
|
119
|
+
nodeRef: React.RefObject<HTMLElement>;
|
|
120
|
+
dismissible: boolean;
|
|
121
|
+
requestClose: () => void;
|
|
122
|
+
}): OverlayEntry {
|
|
123
|
+
const ref = ovUseRef<OverlayEntry>(null);
|
|
124
|
+
if (!ref.current) {
|
|
125
|
+
ref.current = {
|
|
126
|
+
contains: (t) => Boolean(nodeRef.current && nodeRef.current.contains(t as Node)),
|
|
127
|
+
isDismissible: () => ref.current._dismissible,
|
|
128
|
+
requestClose: () => ref.current._close(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
ref.current._dismissible = dismissible;
|
|
132
|
+
ref.current._close = requestClose;
|
|
133
|
+
|
|
134
|
+
ovUseLayoutEffect(() => {
|
|
135
|
+
const entry = ref.current;
|
|
136
|
+
if (ovStack.length === 0) document.addEventListener('keydown', ovOnDocKeyDown);
|
|
137
|
+
ovStack.push(entry);
|
|
138
|
+
if (nodeRef.current)
|
|
139
|
+
nodeRef.current.style.zIndex = 'calc(var(--layer-overlay) + ' + (ovStack.length - 1) + ')';
|
|
140
|
+
return () => {
|
|
141
|
+
const i = ovStack.indexOf(entry);
|
|
142
|
+
if (i >= 0) ovStack.splice(i, 1);
|
|
143
|
+
if (ovStack.length === 0) document.removeEventListener('keydown', ovOnDocKeyDown);
|
|
144
|
+
};
|
|
145
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
146
|
+
|
|
147
|
+
return ref.current;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Light dismiss - close on a press outside the panel, trigger, and any overlay above. */
|
|
151
|
+
function useOutsidePress({
|
|
152
|
+
entry,
|
|
153
|
+
refs,
|
|
154
|
+
enabled,
|
|
155
|
+
onPress,
|
|
156
|
+
}: {
|
|
157
|
+
entry: OverlayEntry;
|
|
158
|
+
refs: React.RefObject<HTMLElement>[];
|
|
159
|
+
enabled: boolean;
|
|
160
|
+
onPress: () => void;
|
|
161
|
+
}) {
|
|
162
|
+
const latest = ovUseRef<{ enabled: boolean; onPress: () => void }>(null);
|
|
163
|
+
latest.current = { enabled, onPress };
|
|
164
|
+
ovUseEffect(() => {
|
|
165
|
+
const onDown = (e: PointerEvent) => {
|
|
166
|
+
if (!latest.current.enabled) return;
|
|
167
|
+
const t = e.target;
|
|
168
|
+
if (refs.some((r) => r.current && r.current.contains(t as Node))) return;
|
|
169
|
+
if (ovInOverlayAbove(entry, t)) return;
|
|
170
|
+
latest.current.onPress();
|
|
171
|
+
};
|
|
172
|
+
document.addEventListener('pointerdown', onDown, true);
|
|
173
|
+
return () => document.removeEventListener('pointerdown', onDown, true);
|
|
174
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Restore focus to the opener on unmount, but only if it would otherwise be lost; a microtask dodges the inert-release ordering race. */
|
|
178
|
+
function useReturnFocus(nodeRef: React.RefObject<HTMLElement>) {
|
|
179
|
+
ovUseEffect(() => {
|
|
180
|
+
const prev = document.activeElement as HTMLElement | null;
|
|
181
|
+
return () => {
|
|
182
|
+
const a = document.activeElement;
|
|
183
|
+
const orphaned =
|
|
184
|
+
!a || a === document.body || (nodeRef.current && nodeRef.current.contains(a));
|
|
185
|
+
if (orphaned && prev && prev.isConnected && typeof prev.focus === 'function') {
|
|
186
|
+
queueMicrotask(() => {
|
|
187
|
+
if (prev.isConnected) prev.focus({ preventScroll: true });
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const OV_FOCUSABLE =
|
|
195
|
+
'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), ' +
|
|
196
|
+
'select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
197
|
+
|
|
198
|
+
/* Hard focus trap: seed in, cycle Tab inside, recapture stray focus unless not topmost or it's in an overlay above. */
|
|
199
|
+
function useFocusTrap({
|
|
200
|
+
panelRef,
|
|
201
|
+
entry,
|
|
202
|
+
}: {
|
|
203
|
+
panelRef: React.RefObject<HTMLElement>;
|
|
204
|
+
entry: OverlayEntry;
|
|
205
|
+
}) {
|
|
206
|
+
ovUseEffect(() => {
|
|
207
|
+
const panel = panelRef.current;
|
|
208
|
+
if (!panel) return undefined;
|
|
209
|
+
const focusables = () =>
|
|
210
|
+
Array.from(panel.querySelectorAll<HTMLElement>(OV_FOCUSABLE)).filter(
|
|
211
|
+
(el) => el.offsetParent !== null || el === document.activeElement,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const seed =
|
|
215
|
+
panel.querySelector<HTMLElement>('[autofocus], [data-autofocus]') || focusables()[0] || panel;
|
|
216
|
+
seed.focus({ preventScroll: true });
|
|
217
|
+
|
|
218
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
219
|
+
if (e.key !== 'Tab') return;
|
|
220
|
+
const f = focusables();
|
|
221
|
+
if (f.length === 0) {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const cur = document.activeElement;
|
|
226
|
+
if (e.shiftKey) {
|
|
227
|
+
if (cur === f[0] || cur === panel) {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
f[f.length - 1].focus();
|
|
230
|
+
}
|
|
231
|
+
} else if (cur === f[f.length - 1]) {
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
f[0].focus();
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const onFocusIn = (e: FocusEvent) => {
|
|
237
|
+
if (panel.contains(e.target as Node)) return;
|
|
238
|
+
if (!ovIsTop(entry) || ovInOverlayAbove(entry, e.target)) return;
|
|
239
|
+
(focusables()[0] || panel).focus({ preventScroll: true });
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
panel.addEventListener('keydown', onKeyDown);
|
|
243
|
+
document.addEventListener('focusin', onFocusIn);
|
|
244
|
+
return () => {
|
|
245
|
+
panel.removeEventListener('keydown', onKeyDown);
|
|
246
|
+
document.removeEventListener('focusin', onFocusIn);
|
|
247
|
+
};
|
|
248
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let ovLocks = 0;
|
|
252
|
+
let ovSavedOverflow = '';
|
|
253
|
+
let ovSavedPad = '';
|
|
254
|
+
function useScrollLock() {
|
|
255
|
+
ovUseEffect(() => {
|
|
256
|
+
if (++ovLocks === 1) {
|
|
257
|
+
const body = document.body;
|
|
258
|
+
const gutter = window.innerWidth - document.documentElement.clientWidth;
|
|
259
|
+
ovSavedOverflow = body.style.overflow;
|
|
260
|
+
ovSavedPad = body.style.paddingRight;
|
|
261
|
+
body.style.overflow = 'hidden';
|
|
262
|
+
if (gutter > 0)
|
|
263
|
+
body.style.paddingRight =
|
|
264
|
+
(parseFloat(getComputedStyle(body).paddingRight) || 0) + gutter + 'px';
|
|
265
|
+
}
|
|
266
|
+
return () => {
|
|
267
|
+
if (--ovLocks === 0) {
|
|
268
|
+
document.body.style.overflow = ovSavedOverflow;
|
|
269
|
+
document.body.style.paddingRight = ovSavedPad;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}, []);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* `inert` on every non-overlay <body> child (refcounted); content mounted while a modal is already up is not swept. */
|
|
276
|
+
let ovInertCount = 0;
|
|
277
|
+
const ovInerted = new Set<HTMLElement>();
|
|
278
|
+
function useInertOutside() {
|
|
279
|
+
ovUseEffect(() => {
|
|
280
|
+
if (++ovInertCount === 1) {
|
|
281
|
+
for (const el of Array.from(document.body.children) as HTMLElement[]) {
|
|
282
|
+
if (
|
|
283
|
+
el.hasAttribute('data-overlay-root') ||
|
|
284
|
+
el.tagName === 'SCRIPT' ||
|
|
285
|
+
el.tagName === 'STYLE' ||
|
|
286
|
+
el.inert
|
|
287
|
+
)
|
|
288
|
+
continue;
|
|
289
|
+
el.inert = true;
|
|
290
|
+
ovInerted.add(el);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return () => {
|
|
294
|
+
if (--ovInertCount === 0) {
|
|
295
|
+
ovInerted.forEach((el) => {
|
|
296
|
+
el.inert = false;
|
|
297
|
+
});
|
|
298
|
+
ovInerted.clear();
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}, []);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* Anchored placement: measure, place on the side, flip when cramped, clamp the cross axis; writes data-side/-align + arrow vars in a layout effect. */
|
|
305
|
+
function useAnchorPosition({
|
|
306
|
+
side,
|
|
307
|
+
align,
|
|
308
|
+
arrow,
|
|
309
|
+
triggerRef,
|
|
310
|
+
panelRef,
|
|
311
|
+
}: {
|
|
312
|
+
side: 'top' | 'bottom' | 'left' | 'right';
|
|
313
|
+
align: 'start' | 'center' | 'end';
|
|
314
|
+
arrow: boolean;
|
|
315
|
+
triggerRef: React.RefObject<HTMLElement>;
|
|
316
|
+
panelRef: React.RefObject<HTMLElement>;
|
|
317
|
+
}) {
|
|
318
|
+
ovUseLayoutEffect(() => {
|
|
319
|
+
const place = () => {
|
|
320
|
+
const t = triggerRef.current;
|
|
321
|
+
const p = panelRef.current;
|
|
322
|
+
if (!t || !p) return;
|
|
323
|
+
const r = t.getBoundingClientRect();
|
|
324
|
+
const pw = p.offsetWidth,
|
|
325
|
+
ph = p.offsetHeight;
|
|
326
|
+
const edge = ovReadPx('--space-2');
|
|
327
|
+
const gap = edge + (arrow ? 3 : 0);
|
|
328
|
+
const vw = window.innerWidth,
|
|
329
|
+
vh = window.innerHeight;
|
|
330
|
+
|
|
331
|
+
const room: Record<string, number> = {
|
|
332
|
+
top: r.top,
|
|
333
|
+
bottom: vh - r.bottom,
|
|
334
|
+
left: r.left,
|
|
335
|
+
right: vw - r.right,
|
|
336
|
+
};
|
|
337
|
+
const opposite: Record<string, 'top' | 'bottom' | 'left' | 'right'> = {
|
|
338
|
+
top: 'bottom',
|
|
339
|
+
bottom: 'top',
|
|
340
|
+
left: 'right',
|
|
341
|
+
right: 'left',
|
|
342
|
+
};
|
|
343
|
+
const vertical = side === 'top' || side === 'bottom';
|
|
344
|
+
let s = side;
|
|
345
|
+
if (room[s] < (vertical ? ph : pw) + gap && room[opposite[s]] > room[s]) s = opposite[s];
|
|
346
|
+
|
|
347
|
+
let x, y;
|
|
348
|
+
if (vertical) {
|
|
349
|
+
y = s === 'top' ? r.top - ph - gap : r.bottom + gap;
|
|
350
|
+
x =
|
|
351
|
+
align === 'start' ? r.left : align === 'end' ? r.right - pw : r.left + (r.width - pw) / 2;
|
|
352
|
+
x = Math.min(Math.max(x, edge), vw - pw - edge);
|
|
353
|
+
y = Math.max(
|
|
354
|
+
Math.min(y, vh - ph - edge),
|
|
355
|
+
edge,
|
|
356
|
+
); /* main-axis clamp: keep the leading edge on-screen (max last - may cover the trigger) */
|
|
357
|
+
} else {
|
|
358
|
+
x = s === 'left' ? r.left - pw - gap : r.right + gap;
|
|
359
|
+
y =
|
|
360
|
+
align === 'start' ? r.top : align === 'end' ? r.bottom - ph : r.top + (r.height - ph) / 2;
|
|
361
|
+
y = Math.min(Math.max(y, edge), vh - ph - edge);
|
|
362
|
+
x = Math.max(Math.min(x, vw - pw - edge), edge);
|
|
363
|
+
}
|
|
364
|
+
p.style.left = Math.round(x) + 'px';
|
|
365
|
+
p.style.top = Math.round(y) + 'px';
|
|
366
|
+
p.setAttribute('data-side', s);
|
|
367
|
+
p.setAttribute('data-align', align);
|
|
368
|
+
if (arrow) {
|
|
369
|
+
if (vertical)
|
|
370
|
+
p.style.setProperty(
|
|
371
|
+
'--overlay-arrow-x',
|
|
372
|
+
Math.round(Math.min(Math.max(r.left + r.width / 2 - x, 12), pw - 12)) + 'px',
|
|
373
|
+
);
|
|
374
|
+
else
|
|
375
|
+
p.style.setProperty(
|
|
376
|
+
'--overlay-arrow-y',
|
|
377
|
+
Math.round(Math.min(Math.max(r.top + r.height / 2 - y, 12), ph - 12)) + 'px',
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
place();
|
|
382
|
+
window.addEventListener('scroll', place, true);
|
|
383
|
+
window.addEventListener('resize', place);
|
|
384
|
+
const ro = new ResizeObserver(place);
|
|
385
|
+
ro.observe(panelRef.current);
|
|
386
|
+
return () => {
|
|
387
|
+
window.removeEventListener('scroll', place, true);
|
|
388
|
+
window.removeEventListener('resize', place);
|
|
389
|
+
ro.disconnect();
|
|
390
|
+
};
|
|
391
|
+
}, [side, align, arrow]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* Per-instance <body> host (escapes ancestor transforms; skipped by inert); persists across open/close so AnimatePresence can play exits. */
|
|
395
|
+
function OverlayPortal({ children }: { children: React.ReactNode }) {
|
|
396
|
+
const hostRef = ovUseRef<HTMLDivElement | null>(null);
|
|
397
|
+
// SSR guard: reached during server render; hooks below stay unconditional.
|
|
398
|
+
if (typeof document !== 'undefined' && !hostRef.current) {
|
|
399
|
+
hostRef.current = document.createElement('div');
|
|
400
|
+
hostRef.current.setAttribute('data-overlay-root', '');
|
|
401
|
+
}
|
|
402
|
+
ovUseLayoutEffect(() => {
|
|
403
|
+
const el = hostRef.current;
|
|
404
|
+
if (!el) return;
|
|
405
|
+
document.body.appendChild(el);
|
|
406
|
+
return () => el.remove();
|
|
407
|
+
}, []);
|
|
408
|
+
if (!hostRef.current) return null;
|
|
409
|
+
return createPortal(children, hostRef.current);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* Scrim (real motion.div): dialogs fade via variants, sheets via a travel-derived MotionValue; a press must start AND end on it to dismiss. */
|
|
413
|
+
const ovScrimVariants = {
|
|
414
|
+
closed: { opacity: 0, transition: { duration: ovSM.dur.base, ease: ovSM.ease.standard } },
|
|
415
|
+
open: { opacity: 1, transition: { duration: ovSM.dur.slow, ease: ovSM.ease.entrance } },
|
|
416
|
+
};
|
|
417
|
+
function OverlayScrim({
|
|
418
|
+
dismissible,
|
|
419
|
+
onPress,
|
|
420
|
+
opacity = null,
|
|
421
|
+
}: {
|
|
422
|
+
dismissible: boolean;
|
|
423
|
+
onPress: () => void;
|
|
424
|
+
opacity?: any;
|
|
425
|
+
}) {
|
|
426
|
+
const down = ovUseRef(false);
|
|
427
|
+
return (
|
|
428
|
+
<ovMotion.div
|
|
429
|
+
className="overlay-scrim"
|
|
430
|
+
aria-hidden="true"
|
|
431
|
+
{...(opacity ? { style: { opacity } } : { variants: ovScrimVariants })}
|
|
432
|
+
onPointerDown={(e) => {
|
|
433
|
+
down.current = e.target === e.currentTarget;
|
|
434
|
+
}}
|
|
435
|
+
onClick={(e) => {
|
|
436
|
+
if (dismissible && down.current && e.target === e.currentTarget) onPress();
|
|
437
|
+
}}
|
|
438
|
+
></ovMotion.div>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* Render the panel: a motion.div wrapper, or with asChild the child's own tag (host element required - component children can't take a ref here). */
|
|
443
|
+
function ovPanelElement({
|
|
444
|
+
asChild,
|
|
445
|
+
children,
|
|
446
|
+
prepend = null,
|
|
447
|
+
nodeRef,
|
|
448
|
+
className,
|
|
449
|
+
motionProps,
|
|
450
|
+
}: {
|
|
451
|
+
asChild: boolean;
|
|
452
|
+
children: React.ReactNode;
|
|
453
|
+
prepend?: React.ReactNode;
|
|
454
|
+
nodeRef: React.RefObject<HTMLElement>;
|
|
455
|
+
className: string;
|
|
456
|
+
motionProps: Record<string, any>;
|
|
457
|
+
}): React.ReactElement {
|
|
458
|
+
if (asChild) {
|
|
459
|
+
const child = React.Children.only(children) as React.ReactElement<any>;
|
|
460
|
+
if (typeof child.type === 'string') {
|
|
461
|
+
const Tag = (ovMotion as any)[child.type];
|
|
462
|
+
const composedRef = (node: HTMLElement | null) => {
|
|
463
|
+
nodeRef.current = node;
|
|
464
|
+
const r = (child as any).ref;
|
|
465
|
+
if (typeof r === 'function') r(node);
|
|
466
|
+
else if (r) r.current = node;
|
|
467
|
+
};
|
|
468
|
+
return (
|
|
469
|
+
<Tag
|
|
470
|
+
{...child.props}
|
|
471
|
+
{...motionProps}
|
|
472
|
+
ref={composedRef}
|
|
473
|
+
className={child.props.className ? className + ' ' + child.props.className : className}
|
|
474
|
+
style={{ ...motionProps.style, ...child.props.style }}
|
|
475
|
+
>
|
|
476
|
+
{prepend}
|
|
477
|
+
{child.props.children}
|
|
478
|
+
</Tag>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
console.warn('[Overlay] asChild requires a DOM-element child - falling back to a wrapper');
|
|
482
|
+
}
|
|
483
|
+
return (
|
|
484
|
+
<ovMotion.div
|
|
485
|
+
{...motionProps}
|
|
486
|
+
ref={nodeRef as React.RefObject<HTMLDivElement>}
|
|
487
|
+
className={className}
|
|
488
|
+
>
|
|
489
|
+
{prepend}
|
|
490
|
+
{children}
|
|
491
|
+
</ovMotion.div>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* Shared modal shell (dialog + sheet): layer > scrim + slot; sets no role/label (the consumer's panel owns semantics); modality hooks live here so they hold until exit. */
|
|
496
|
+
function ModalShell({
|
|
497
|
+
layerClass,
|
|
498
|
+
slotClass,
|
|
499
|
+
slotVariants,
|
|
500
|
+
panelId,
|
|
501
|
+
dismissible,
|
|
502
|
+
requestClose,
|
|
503
|
+
asChild = false,
|
|
504
|
+
slotRef: externalSlotRef = null,
|
|
505
|
+
slotProps = {},
|
|
506
|
+
scrimOpacity = null,
|
|
507
|
+
children,
|
|
508
|
+
}: {
|
|
509
|
+
layerClass: string;
|
|
510
|
+
slotClass: string;
|
|
511
|
+
slotVariants: any;
|
|
512
|
+
panelId: string;
|
|
513
|
+
dismissible: boolean;
|
|
514
|
+
requestClose: () => void;
|
|
515
|
+
asChild?: boolean;
|
|
516
|
+
slotRef?: React.RefObject<HTMLElement> | null;
|
|
517
|
+
slotProps?: Record<string, any>;
|
|
518
|
+
scrimOpacity?: any;
|
|
519
|
+
children: React.ReactNode;
|
|
520
|
+
}) {
|
|
521
|
+
const layerRef = ovUseRef<HTMLDivElement>(null);
|
|
522
|
+
const internalSlotRef = ovUseRef<HTMLElement>(null);
|
|
523
|
+
const slotRef = externalSlotRef || internalSlotRef;
|
|
524
|
+
const entry = useOverlayEntry({ nodeRef: layerRef, dismissible, requestClose });
|
|
525
|
+
useScrollLock();
|
|
526
|
+
useInertOutside();
|
|
527
|
+
useReturnFocus(layerRef);
|
|
528
|
+
useFocusTrap({ panelRef: slotRef, entry });
|
|
529
|
+
return (
|
|
530
|
+
<ovMotion.div
|
|
531
|
+
ref={layerRef}
|
|
532
|
+
className={'overlay-layer ' + layerClass}
|
|
533
|
+
initial="closed"
|
|
534
|
+
animate="open"
|
|
535
|
+
exit="closed"
|
|
536
|
+
>
|
|
537
|
+
<OverlayScrim dismissible={dismissible} onPress={requestClose} opacity={scrimOpacity} />
|
|
538
|
+
{ovPanelElement({
|
|
539
|
+
asChild,
|
|
540
|
+
children,
|
|
541
|
+
nodeRef: slotRef,
|
|
542
|
+
className: 'overlay-slot ' + slotClass,
|
|
543
|
+
motionProps: { id: panelId, tabIndex: -1, variants: slotVariants, ...slotProps },
|
|
544
|
+
})}
|
|
545
|
+
</ovMotion.div>
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export {
|
|
550
|
+
ovMotion,
|
|
551
|
+
OvAnimatePresence,
|
|
552
|
+
ovSM,
|
|
553
|
+
useControllable,
|
|
554
|
+
ovResolveChildren,
|
|
555
|
+
ovCloneTrigger,
|
|
556
|
+
useOverlayEntry,
|
|
557
|
+
useOutsidePress,
|
|
558
|
+
useReturnFocus,
|
|
559
|
+
useAnchorPosition,
|
|
560
|
+
OverlayPortal,
|
|
561
|
+
OverlayScrim,
|
|
562
|
+
ovPanelElement,
|
|
563
|
+
ModalShell,
|
|
564
|
+
};
|
|
565
|
+
export type { OverlayEntry };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* overlay.css - headless paint for the overlay domain (popover - dialog - sheet). */
|
|
2
|
+
|
|
3
|
+
/* Modal geometry - fixed full-viewport layer; scrim fills it; slot is the panel's grid cell (min 0 + max 100% give a scroll constraint chain). */
|
|
4
|
+
.overlay-layer {
|
|
5
|
+
position: fixed;
|
|
6
|
+
inset: 0;
|
|
7
|
+
display: grid;
|
|
8
|
+
}
|
|
9
|
+
.overlay-scrim {
|
|
10
|
+
position: absolute;
|
|
11
|
+
inset: 0;
|
|
12
|
+
background: var(--bg-overlay);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.overlay-slot {
|
|
16
|
+
position: relative;
|
|
17
|
+
display: grid;
|
|
18
|
+
outline: none;
|
|
19
|
+
min-width: 0;
|
|
20
|
+
min-height: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.overlay-layer--dialog {
|
|
24
|
+
place-items: center;
|
|
25
|
+
padding: var(--space-6);
|
|
26
|
+
}
|
|
27
|
+
.overlay-slot--dialog {
|
|
28
|
+
max-width: 100%;
|
|
29
|
+
max-height: 100%;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* sheets dock flush - no layer padding; the slot spans the docked edge */
|
|
33
|
+
.overlay-layer--sheet-right {
|
|
34
|
+
align-items: stretch;
|
|
35
|
+
justify-items: end;
|
|
36
|
+
}
|
|
37
|
+
.overlay-slot--sheet-right {
|
|
38
|
+
height: 100%;
|
|
39
|
+
max-width: 100%;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.overlay-layer--sheet-bottom {
|
|
43
|
+
align-items: end;
|
|
44
|
+
justify-items: stretch;
|
|
45
|
+
}
|
|
46
|
+
.overlay-slot--sheet-bottom {
|
|
47
|
+
width: 100%;
|
|
48
|
+
max-height: 100%;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Anchored panel (popover) - fixed coords from useAnchorPosition; entrance origin = arrow tip, else the aligned edge (inset --space-4), else center. */
|
|
52
|
+
.overlay-popover {
|
|
53
|
+
position: fixed;
|
|
54
|
+
top: 0;
|
|
55
|
+
left: 0;
|
|
56
|
+
width: max-content;
|
|
57
|
+
outline: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.overlay-popover[data-align='start'] {
|
|
61
|
+
--ov-origin: var(--space-4);
|
|
62
|
+
}
|
|
63
|
+
.overlay-popover[data-align='center'] {
|
|
64
|
+
--ov-origin: 50%;
|
|
65
|
+
}
|
|
66
|
+
.overlay-popover[data-align='end'] {
|
|
67
|
+
--ov-origin: calc(100% - var(--space-4));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.overlay-popover[data-side='bottom'] {
|
|
71
|
+
transform-origin: var(--overlay-arrow-x, var(--ov-origin)) top;
|
|
72
|
+
}
|
|
73
|
+
.overlay-popover[data-side='top'] {
|
|
74
|
+
transform-origin: var(--overlay-arrow-x, var(--ov-origin)) bottom;
|
|
75
|
+
}
|
|
76
|
+
.overlay-popover[data-side='right'] {
|
|
77
|
+
transform-origin: left var(--overlay-arrow-y, var(--ov-origin));
|
|
78
|
+
}
|
|
79
|
+
.overlay-popover[data-side='left'] {
|
|
80
|
+
transform-origin: right var(--overlay-arrow-y, var(--ov-origin));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Arrow - a rotated square on the panel edge (--overlay-arrow-x/y); only its two outward edges keep the hairline, the panel covers the rest (must be opaque). */
|
|
84
|
+
.overlay-popover__arrow {
|
|
85
|
+
position: absolute;
|
|
86
|
+
width: 10px;
|
|
87
|
+
height: 10px;
|
|
88
|
+
background: var(--overlay-arrow-bg, var(--bg-surface-raised));
|
|
89
|
+
border: var(--border-hairline) solid var(--border-default);
|
|
90
|
+
pointer-events: none;
|
|
91
|
+
}
|
|
92
|
+
.overlay-popover[data-side='bottom'] .overlay-popover__arrow {
|
|
93
|
+
top: -5px;
|
|
94
|
+
left: var(--overlay-arrow-x, 50%);
|
|
95
|
+
transform: translateX(-50%) rotate(45deg);
|
|
96
|
+
border-right: none;
|
|
97
|
+
border-bottom: none;
|
|
98
|
+
}
|
|
99
|
+
.overlay-popover[data-side='top'] .overlay-popover__arrow {
|
|
100
|
+
bottom: -5px;
|
|
101
|
+
left: var(--overlay-arrow-x, 50%);
|
|
102
|
+
transform: translateX(-50%) rotate(45deg);
|
|
103
|
+
border-left: none;
|
|
104
|
+
border-top: none;
|
|
105
|
+
}
|
|
106
|
+
.overlay-popover[data-side='right'] .overlay-popover__arrow {
|
|
107
|
+
left: -5px;
|
|
108
|
+
top: var(--overlay-arrow-y, 50%);
|
|
109
|
+
transform: translateY(-50%) rotate(45deg);
|
|
110
|
+
border-right: none;
|
|
111
|
+
border-top: none;
|
|
112
|
+
}
|
|
113
|
+
.overlay-popover[data-side='left'] .overlay-popover__arrow {
|
|
114
|
+
right: -5px;
|
|
115
|
+
top: var(--overlay-arrow-y, 50%);
|
|
116
|
+
transform: translateY(-50%) rotate(45deg);
|
|
117
|
+
border-left: none;
|
|
118
|
+
border-bottom: none;
|
|
119
|
+
}
|