@crisp-ui-kit/crisp 0.19.1 → 0.20.1
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/appshell.css +2 -2
- package/dist/cjs/components/menu/menu.js +8 -9
- package/dist/cjs/components/multiselect/multiselect.js +11 -8
- package/dist/cjs/components/popover/popover.js +8 -10
- package/dist/cjs/components/select/select.js +11 -8
- package/dist/cjs/core/direction.js +25 -0
- package/dist/cjs/core/index.js +5 -1
- package/dist/cjs/core/place-floating.js +38 -0
- package/dist/cjs/product/appshell/appshell.js +7 -1
- package/dist/cjs/product/appshell/appshell.recipe.js +2 -2
- package/dist/cjs/product/recordpage/recordpage.js +9 -2
- package/dist/cjs/product/recordpage/recordpage.recipe.js +2 -2
- package/dist/esm/components/menu/menu.js +8 -9
- package/dist/esm/components/multiselect/multiselect.js +11 -8
- package/dist/esm/components/popover/popover.js +8 -10
- package/dist/esm/components/select/select.js +11 -8
- package/dist/esm/core/direction.js +22 -0
- package/dist/esm/core/index.js +2 -0
- package/dist/esm/core/place-floating.js +35 -0
- package/dist/esm/product/appshell/appshell.js +7 -1
- package/dist/esm/product/appshell/appshell.recipe.js +2 -2
- package/dist/esm/product/recordpage/recordpage.js +9 -2
- package/dist/esm/product/recordpage/recordpage.recipe.js +2 -2
- package/dist/recordpage.css +2 -2
- package/dist/styles.css +4 -4
- package/dist/types/components/menu/menu.d.ts +2 -1
- package/dist/types/components/popover/popover.d.ts +2 -1
- package/dist/types/core/direction.d.ts +10 -0
- package/dist/types/core/index.d.ts +2 -0
- package/dist/types/core/place-floating.d.ts +40 -0
- package/package.json +1 -1
package/dist/appshell.css
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
display: flex; height: 100%; width: 100%; overflow: hidden; background: var(--bg-surface); font-family: var(--font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
4
4
|
& .appshell-side { position: relative; flex-shrink: 0; overflow: hidden; }
|
|
5
5
|
& .appshell-side-inner { height: 100%; }
|
|
6
|
-
& .appshell-resize { position: absolute; top: 0;
|
|
7
|
-
& .appshell-resize::after { content: ""; position: absolute; top: 0;
|
|
6
|
+
& .appshell-resize { position: absolute; top: 0; inset-inline-end: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
|
|
7
|
+
& .appshell-resize::after { content: ""; position: absolute; top: 0; inset-inline-end: 0; width: 1px; height: 100%; background: var(--border-subtle); transition: background-color .16s, height .16s; }
|
|
8
8
|
& .appshell-resize:hover::after, & .appshell-resize[data-dragging]::after { width: 2px; background: var(--bg-brand-solid); }
|
|
9
9
|
& .appshell-main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
10
10
|
& .appshell-expand { position: absolute; top: var(--space-2); inset-inline-start: var(--space-2); z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: var(--size-row); height: var(--size-row); padding: 0; border: none; border-radius: var(--radius-md); box-shadow: inset 0 0 0 1px var(--border-subtle); background: var(--bg-surface); color: var(--fg-muted); font-family: inherit; cursor: pointer; transition: background-color .12s; }
|
|
@@ -41,6 +41,7 @@ exports.MenuLabel = MenuLabel;
|
|
|
41
41
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
42
42
|
const lucide_react_1 = require("lucide-react");
|
|
43
43
|
const React = __importStar(require("react"));
|
|
44
|
+
const core_1 = require("../../core");
|
|
44
45
|
const menu_recipe_1 = require("./menu.recipe");
|
|
45
46
|
const menu_keyboard_1 = require("./menu-keyboard");
|
|
46
47
|
const MenuContext = React.createContext(null);
|
|
@@ -71,15 +72,13 @@ function Menu({ trigger, children, align = "start", open: openProp, defaultOpen,
|
|
|
71
72
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
72
73
|
if (!rect)
|
|
73
74
|
return;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
else
|
|
82
|
-
style.left = rect.left;
|
|
75
|
+
const { style, up: flipUp } = (0, core_1.placeFloating)({
|
|
76
|
+
rect,
|
|
77
|
+
align,
|
|
78
|
+
gap: 4,
|
|
79
|
+
flipBelow: 280,
|
|
80
|
+
anchor: triggerRef.current,
|
|
81
|
+
});
|
|
83
82
|
setUp(flipUp);
|
|
84
83
|
setPos(style);
|
|
85
84
|
};
|
|
@@ -37,6 +37,7 @@ exports.MultiSelect = MultiSelect;
|
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const lucide_react_1 = require("lucide-react");
|
|
39
39
|
const React = __importStar(require("react"));
|
|
40
|
+
const core_1 = require("../../core");
|
|
40
41
|
const select_1 = require("../select");
|
|
41
42
|
const multiselect_recipe_1 = require("./multiselect.recipe");
|
|
42
43
|
const TONES = [
|
|
@@ -92,15 +93,17 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
|
|
|
92
93
|
return;
|
|
93
94
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
94
95
|
if (rect) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
// the popup can grow past the trigger (minWidth, not width), so which
|
|
97
|
+
// edge it is pinned to decides which way it grows — start, in whichever
|
|
98
|
+
// direction that is (#210)
|
|
99
|
+
const { style, up: flipUp } = (0, core_1.placeFloating)({
|
|
100
|
+
rect,
|
|
101
|
+
gap: 4,
|
|
102
|
+
flipBelow: 300,
|
|
103
|
+
anchor: triggerRef.current,
|
|
103
104
|
});
|
|
105
|
+
setUp(flipUp);
|
|
106
|
+
setPos({ ...style, minWidth: rect.width });
|
|
104
107
|
}
|
|
105
108
|
setQuery("");
|
|
106
109
|
setHighlight(0);
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.Popover = Popover;
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const React = __importStar(require("react"));
|
|
39
|
+
const core_1 = require("../../core");
|
|
39
40
|
const popover_recipe_1 = require("./popover.recipe");
|
|
40
41
|
/**
|
|
41
42
|
* A generic anchored popover — the Menu/Select surface holding arbitrary
|
|
@@ -58,16 +59,13 @@ function Popover({ trigger, children, align = "start", open: openProp, defaultOp
|
|
|
58
59
|
const el = triggerRef.current;
|
|
59
60
|
if (!el)
|
|
60
61
|
return;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
:
|
|
67
|
-
|
|
68
|
-
style.right = window.innerWidth - rect.right;
|
|
69
|
-
else
|
|
70
|
-
style.left = rect.left;
|
|
62
|
+
const { style, up: flipUp } = (0, core_1.placeFloating)({
|
|
63
|
+
rect: el.getBoundingClientRect(),
|
|
64
|
+
align,
|
|
65
|
+
gap: 6,
|
|
66
|
+
flipBelow: 280,
|
|
67
|
+
anchor: el,
|
|
68
|
+
});
|
|
71
69
|
setUp(flipUp);
|
|
72
70
|
setPos(style);
|
|
73
71
|
};
|
|
@@ -37,6 +37,7 @@ exports.Select = Select;
|
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const lucide_react_1 = require("lucide-react");
|
|
39
39
|
const React = __importStar(require("react"));
|
|
40
|
+
const core_1 = require("../../core");
|
|
40
41
|
const listbox_keyboard_1 = require("./listbox-keyboard");
|
|
41
42
|
const select_recipe_1 = require("./select.recipe");
|
|
42
43
|
function optionText(o) {
|
|
@@ -87,15 +88,17 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
|
|
|
87
88
|
return;
|
|
88
89
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
89
90
|
if (rect) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
// the popup can grow past the trigger (minWidth, not width), so which
|
|
92
|
+
// edge it is pinned to decides which way it grows — start, in whichever
|
|
93
|
+
// direction that is (#210)
|
|
94
|
+
const { style, up: flipUp } = (0, core_1.placeFloating)({
|
|
95
|
+
rect,
|
|
96
|
+
gap: 4,
|
|
97
|
+
flipBelow: 260,
|
|
98
|
+
anchor: triggerRef.current,
|
|
98
99
|
});
|
|
100
|
+
setUp(flipUp);
|
|
101
|
+
setPos({ ...style, minWidth: rect.width });
|
|
99
102
|
}
|
|
100
103
|
setQuery("");
|
|
101
104
|
const sel = options.findIndex((o) => o.value === selected);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isRtlElement = isRtlElement;
|
|
4
|
+
/**
|
|
5
|
+
* The nearest declared direction. The `dir` attribute is checked first because
|
|
6
|
+
* it is how a document says this in practice and it is readable without a
|
|
7
|
+
* layout, then computed style for a direction set purely in CSS.
|
|
8
|
+
*
|
|
9
|
+
* Shared so that a floating layer's edge choice and a drag handler's sign come
|
|
10
|
+
* from the same answer. They have to agree: mirroring one without the other is
|
|
11
|
+
* the half-conversion lesson 54 is about.
|
|
12
|
+
*/
|
|
13
|
+
function isRtlElement(anchor) {
|
|
14
|
+
if (!anchor || typeof window === "undefined")
|
|
15
|
+
return false;
|
|
16
|
+
const declared = anchor.closest("[dir]")?.getAttribute("dir");
|
|
17
|
+
if (declared)
|
|
18
|
+
return declared.toLowerCase() === "rtl";
|
|
19
|
+
try {
|
|
20
|
+
return window.getComputedStyle(anchor).direction === "rtl";
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/cjs/core/index.js
CHANGED
|
@@ -14,11 +14,15 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.usePrefersReducedMotion = exports.resolveRecipe = exports.Primitive = exports.defineRecipe = exports.cx = void 0;
|
|
17
|
+
exports.usePrefersReducedMotion = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.isRtlElement = exports.defineRecipe = exports.cx = void 0;
|
|
18
18
|
var cx_1 = require("./cx");
|
|
19
19
|
Object.defineProperty(exports, "cx", { enumerable: true, get: function () { return cx_1.cx; } });
|
|
20
20
|
var define_1 = require("./define");
|
|
21
21
|
Object.defineProperty(exports, "defineRecipe", { enumerable: true, get: function () { return define_1.defineRecipe; } });
|
|
22
|
+
var direction_1 = require("./direction");
|
|
23
|
+
Object.defineProperty(exports, "isRtlElement", { enumerable: true, get: function () { return direction_1.isRtlElement; } });
|
|
24
|
+
var place_floating_1 = require("./place-floating");
|
|
25
|
+
Object.defineProperty(exports, "placeFloating", { enumerable: true, get: function () { return place_floating_1.placeFloating; } });
|
|
22
26
|
var primitive_1 = require("./primitive");
|
|
23
27
|
Object.defineProperty(exports, "Primitive", { enumerable: true, get: function () { return primitive_1.Primitive; } });
|
|
24
28
|
var resolve_1 = require("./resolve");
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.placeFloating = placeFloating;
|
|
4
|
+
const direction_1 = require("./direction");
|
|
5
|
+
/**
|
|
6
|
+
* Where a floating layer goes, in logical terms.
|
|
7
|
+
*
|
|
8
|
+
* `start` and `end` are inline edges, not left and right. `Menu` and `Popover`
|
|
9
|
+
* both took an `align` prop named that way and then wrote
|
|
10
|
+
* `align === "end" ? right : left` with no direction anywhere, so in an RTL
|
|
11
|
+
* document both were backwards and a layer near the viewport edge overflowed on
|
|
12
|
+
* the wrong side (#210).
|
|
13
|
+
*
|
|
14
|
+
* The direction is read from the anchor's computed style rather than from a
|
|
15
|
+
* prop, so an inherited `dir` on any ancestor is honoured without every call
|
|
16
|
+
* site having to thread it down.
|
|
17
|
+
*
|
|
18
|
+
* Physical `left`/`right` are deliberate in the OUTPUT: this positions a
|
|
19
|
+
* `position: fixed` layer against viewport coordinates, which are physical in
|
|
20
|
+
* both directions. What must be direction-aware is the CHOICE of edge, and
|
|
21
|
+
* that happens here, once. Converting the output to `inset-inline-start` would
|
|
22
|
+
* be the half-conversion #146 exists to prevent — the rect it is measured
|
|
23
|
+
* against would still be physical.
|
|
24
|
+
*/
|
|
25
|
+
function placeFloating({ rect, align = "start", gap, flipBelow, anchor, }) {
|
|
26
|
+
const rtl = (0, direction_1.isRtlElement)(anchor);
|
|
27
|
+
const up = window.innerHeight - rect.bottom < flipBelow;
|
|
28
|
+
const style = up
|
|
29
|
+
? { bottom: window.innerHeight - rect.top + gap }
|
|
30
|
+
: { top: rect.bottom + gap };
|
|
31
|
+
// start is the left edge in LTR and the right edge in RTL; end is the other
|
|
32
|
+
const toRight = align === "end" ? !rtl : rtl;
|
|
33
|
+
if (toRight)
|
|
34
|
+
style.right = window.innerWidth - rect.right;
|
|
35
|
+
else
|
|
36
|
+
style.left = rect.left;
|
|
37
|
+
return { style, up };
|
|
38
|
+
}
|
|
@@ -37,6 +37,7 @@ exports.AppShell = AppShell;
|
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const lucide_react_1 = require("lucide-react");
|
|
39
39
|
const React = __importStar(require("react"));
|
|
40
|
+
const core_1 = require("../../core");
|
|
40
41
|
const appshell_recipe_1 = require("./appshell.recipe");
|
|
41
42
|
/**
|
|
42
43
|
* App-shell layout with a collapsible + resizable sidebar, mirroring Attio's
|
|
@@ -62,6 +63,11 @@ function AppShell({ sidebar, children, width: widthProp, defaultWidth = 275, onW
|
|
|
62
63
|
e.preventDefault();
|
|
63
64
|
const startX = e.clientX;
|
|
64
65
|
const startW = width;
|
|
66
|
+
// The sidebar is the flex row's first child, so under RTL it renders on the
|
|
67
|
+
// RIGHT — measured. `clientX` stays physical, so pulling right must SHRINK
|
|
68
|
+
// it there. Mirroring the handle's CSS without this is the half-conversion
|
|
69
|
+
// lesson 54 is about, which is why both change in one commit (#212).
|
|
70
|
+
const dir = (0, core_1.isRtlElement)(e.currentTarget) ? -1 : 1;
|
|
65
71
|
// Attio only enters the drag state once the pointer has actually moved — a
|
|
66
72
|
// click on the handle is not a drag, so the blue bar must not flash on press.
|
|
67
73
|
let moved = false;
|
|
@@ -75,7 +81,7 @@ function AppShell({ sidebar, children, width: widthProp, defaultWidth = 275, onW
|
|
|
75
81
|
moved = true;
|
|
76
82
|
setDragging(true);
|
|
77
83
|
}
|
|
78
|
-
const w = Math.round(Math.min(maxWidth, Math.max(minWidth, startW + (ev.clientX - startX))));
|
|
84
|
+
const w = Math.round(Math.min(maxWidth, Math.max(minWidth, startW + dir * (ev.clientX - startX))));
|
|
79
85
|
setDragWidth(w);
|
|
80
86
|
last = w;
|
|
81
87
|
};
|
|
@@ -12,8 +12,8 @@ const config = (0, core_1.defineRecipe)({
|
|
|
12
12
|
display: flex; height: 100%; width: 100%; overflow: hidden; background: ${tokens_1.contract.bgSurface}; font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
13
13
|
& .appshell-side { position: relative; flex-shrink: 0; overflow: hidden; }
|
|
14
14
|
& .appshell-side-inner { height: 100%; }
|
|
15
|
-
& .appshell-resize { position: absolute; top: 0;
|
|
16
|
-
& .appshell-resize::after { content: ""; position: absolute; top: 0;
|
|
15
|
+
& .appshell-resize { position: absolute; top: 0; inset-inline-end: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
|
|
16
|
+
& .appshell-resize::after { content: ""; position: absolute; top: 0; inset-inline-end: 0; width: 1px; height: 100%; background: ${tokens_1.contract.borderSubtle}; transition: background-color .16s, height .16s; }
|
|
17
17
|
& .appshell-resize:hover::after, & .appshell-resize[data-dragging]::after { width: 2px; background: ${tokens_1.contract.bgBrandSolid}; }
|
|
18
18
|
& .appshell-main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
19
19
|
& .appshell-expand { position: absolute; top: ${tokens_1.contract.space["2"]}; inset-inline-start: ${tokens_1.contract.space["2"]}; z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: ${tokens_1.contract.size.row}; height: ${tokens_1.contract.size.row}; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; background: ${tokens_1.contract.bgSurface}; color: ${tokens_1.contract.fgMuted}; font-family: inherit; cursor: pointer; transition: background-color .12s; }
|
|
@@ -42,6 +42,7 @@ const avatar_1 = require("../../components/avatar");
|
|
|
42
42
|
const checkbox_1 = require("../../components/checkbox");
|
|
43
43
|
const menu_1 = require("../../components/menu");
|
|
44
44
|
const tabs_1 = require("../../components/tabs");
|
|
45
|
+
const core_1 = require("../../core");
|
|
45
46
|
const recordstable_1 = require("../recordstable");
|
|
46
47
|
const recordpage_recipe_1 = require("./recordpage.recipe");
|
|
47
48
|
function valueEmpty(v) {
|
|
@@ -215,14 +216,20 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
215
216
|
const [resizing, setResizing] = React.useState(false);
|
|
216
217
|
const dragRef = React.useRef(null);
|
|
217
218
|
const onResizeDown = (e) => {
|
|
218
|
-
|
|
219
|
+
// details is the flex row's first child, so it sits on the right under RTL
|
|
220
|
+
// and a rightward pull must shrink it (#212)
|
|
221
|
+
dragRef.current = {
|
|
222
|
+
x: e.clientX,
|
|
223
|
+
w: detailsW,
|
|
224
|
+
dir: (0, core_1.isRtlElement)(e.currentTarget) ? -1 : 1,
|
|
225
|
+
};
|
|
219
226
|
setResizing(true);
|
|
220
227
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
221
228
|
};
|
|
222
229
|
const onResizeMove = (e) => {
|
|
223
230
|
if (!dragRef.current)
|
|
224
231
|
return;
|
|
225
|
-
const next = dragRef.current.w + (e.clientX - dragRef.current.x);
|
|
232
|
+
const next = dragRef.current.w + dragRef.current.dir * (e.clientX - dragRef.current.x);
|
|
226
233
|
setDetailsW(Math.max(300, Math.min(560, next)));
|
|
227
234
|
};
|
|
228
235
|
const onResizeUp = (e) => {
|
|
@@ -34,8 +34,8 @@ const config = (0, core_1.defineRecipe)({
|
|
|
34
34
|
|
|
35
35
|
& .recordpage-details { width: 320px; flex-shrink: 0; overflow-y: auto; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; padding: ${tokens_1.contract.space["3"]}; }
|
|
36
36
|
& .recordpage-resizer { position: relative; flex: 0 0 auto; width: 0; align-self: stretch; cursor: col-resize; z-index: 5; touch-action: none; }
|
|
37
|
-
& .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0;
|
|
38
|
-
& .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0;
|
|
37
|
+
& .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -4px; width: 9px; }
|
|
38
|
+
& .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
|
|
39
39
|
& .recordpage-resizer:hover::after, & .recordpage-resizer[data-active]::after { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 55%, transparent); }
|
|
40
40
|
& .recordpage-entity { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["2"]}; }
|
|
41
41
|
& .recordpage-entity-logo { width: 32px; height: 32px; border-radius: 30%; flex-shrink: 0; object-fit: cover; background: ${tokens_1.contract.bgRaised}; }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { placeFloating } from "../../core/index.js";
|
|
4
5
|
import { menuRecipe } from "./menu.recipe.js";
|
|
5
6
|
import { useMenuKeyboard } from "./menu-keyboard.js";
|
|
6
7
|
const MenuContext = React.createContext(null);
|
|
@@ -31,15 +32,13 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
31
32
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
32
33
|
if (!rect)
|
|
33
34
|
return;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
else
|
|
42
|
-
style.left = rect.left;
|
|
35
|
+
const { style, up: flipUp } = placeFloating({
|
|
36
|
+
rect,
|
|
37
|
+
align,
|
|
38
|
+
gap: 4,
|
|
39
|
+
flipBelow: 280,
|
|
40
|
+
anchor: triggerRef.current,
|
|
41
|
+
});
|
|
43
42
|
setUp(flipUp);
|
|
44
43
|
setPos(style);
|
|
45
44
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { placeFloating } from "../../core/index.js";
|
|
4
5
|
import { useListboxKeyboard } from "../select/index.js";
|
|
5
6
|
import { multiselectRecipe } from "./multiselect.recipe.js";
|
|
6
7
|
const TONES = [
|
|
@@ -56,15 +57,17 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
56
57
|
return;
|
|
57
58
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
58
59
|
if (rect) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
// the popup can grow past the trigger (minWidth, not width), so which
|
|
61
|
+
// edge it is pinned to decides which way it grows — start, in whichever
|
|
62
|
+
// direction that is (#210)
|
|
63
|
+
const { style, up: flipUp } = placeFloating({
|
|
64
|
+
rect,
|
|
65
|
+
gap: 4,
|
|
66
|
+
flipBelow: 300,
|
|
67
|
+
anchor: triggerRef.current,
|
|
67
68
|
});
|
|
69
|
+
setUp(flipUp);
|
|
70
|
+
setPos({ ...style, minWidth: rect.width });
|
|
68
71
|
}
|
|
69
72
|
setQuery("");
|
|
70
73
|
setHighlight(0);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { placeFloating } from "../../core/index.js";
|
|
3
4
|
import { popoverRecipe } from "./popover.recipe.js";
|
|
4
5
|
/**
|
|
5
6
|
* A generic anchored popover — the Menu/Select surface holding arbitrary
|
|
@@ -22,16 +23,13 @@ export function Popover({ trigger, children, align = "start", open: openProp, de
|
|
|
22
23
|
const el = triggerRef.current;
|
|
23
24
|
if (!el)
|
|
24
25
|
return;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
:
|
|
31
|
-
|
|
32
|
-
style.right = window.innerWidth - rect.right;
|
|
33
|
-
else
|
|
34
|
-
style.left = rect.left;
|
|
26
|
+
const { style, up: flipUp } = placeFloating({
|
|
27
|
+
rect: el.getBoundingClientRect(),
|
|
28
|
+
align,
|
|
29
|
+
gap: 6,
|
|
30
|
+
flipBelow: 280,
|
|
31
|
+
anchor: el,
|
|
32
|
+
});
|
|
35
33
|
setUp(flipUp);
|
|
36
34
|
setPos(style);
|
|
37
35
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { placeFloating } from "../../core/index.js";
|
|
4
5
|
import { useListboxKeyboard } from "./listbox-keyboard.js";
|
|
5
6
|
import { selectRecipe } from "./select.recipe.js";
|
|
6
7
|
function optionText(o) {
|
|
@@ -51,15 +52,17 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
|
|
|
51
52
|
return;
|
|
52
53
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
53
54
|
if (rect) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
// the popup can grow past the trigger (minWidth, not width), so which
|
|
56
|
+
// edge it is pinned to decides which way it grows — start, in whichever
|
|
57
|
+
// direction that is (#210)
|
|
58
|
+
const { style, up: flipUp } = placeFloating({
|
|
59
|
+
rect,
|
|
60
|
+
gap: 4,
|
|
61
|
+
flipBelow: 260,
|
|
62
|
+
anchor: triggerRef.current,
|
|
62
63
|
});
|
|
64
|
+
setUp(flipUp);
|
|
65
|
+
setPos({ ...style, minWidth: rect.width });
|
|
63
66
|
}
|
|
64
67
|
setQuery("");
|
|
65
68
|
const sel = options.findIndex((o) => o.value === selected);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The nearest declared direction. The `dir` attribute is checked first because
|
|
3
|
+
* it is how a document says this in practice and it is readable without a
|
|
4
|
+
* layout, then computed style for a direction set purely in CSS.
|
|
5
|
+
*
|
|
6
|
+
* Shared so that a floating layer's edge choice and a drag handler's sign come
|
|
7
|
+
* from the same answer. They have to agree: mirroring one without the other is
|
|
8
|
+
* the half-conversion lesson 54 is about.
|
|
9
|
+
*/
|
|
10
|
+
export function isRtlElement(anchor) {
|
|
11
|
+
if (!anchor || typeof window === "undefined")
|
|
12
|
+
return false;
|
|
13
|
+
const declared = anchor.closest("[dir]")?.getAttribute("dir");
|
|
14
|
+
if (declared)
|
|
15
|
+
return declared.toLowerCase() === "rtl";
|
|
16
|
+
try {
|
|
17
|
+
return window.getComputedStyle(anchor).direction === "rtl";
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/esm/core/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { cx } from "./cx.js";
|
|
2
2
|
export { defineRecipe } from "./define.js";
|
|
3
|
+
export { isRtlElement } from "./direction.js";
|
|
4
|
+
export { placeFloating, } from "./place-floating.js";
|
|
3
5
|
export { Primitive } from "./primitive.js";
|
|
4
6
|
export { resolveRecipe } from "./resolve.js";
|
|
5
7
|
export * from "./rtl.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isRtlElement } from "./direction.js";
|
|
2
|
+
/**
|
|
3
|
+
* Where a floating layer goes, in logical terms.
|
|
4
|
+
*
|
|
5
|
+
* `start` and `end` are inline edges, not left and right. `Menu` and `Popover`
|
|
6
|
+
* both took an `align` prop named that way and then wrote
|
|
7
|
+
* `align === "end" ? right : left` with no direction anywhere, so in an RTL
|
|
8
|
+
* document both were backwards and a layer near the viewport edge overflowed on
|
|
9
|
+
* the wrong side (#210).
|
|
10
|
+
*
|
|
11
|
+
* The direction is read from the anchor's computed style rather than from a
|
|
12
|
+
* prop, so an inherited `dir` on any ancestor is honoured without every call
|
|
13
|
+
* site having to thread it down.
|
|
14
|
+
*
|
|
15
|
+
* Physical `left`/`right` are deliberate in the OUTPUT: this positions a
|
|
16
|
+
* `position: fixed` layer against viewport coordinates, which are physical in
|
|
17
|
+
* both directions. What must be direction-aware is the CHOICE of edge, and
|
|
18
|
+
* that happens here, once. Converting the output to `inset-inline-start` would
|
|
19
|
+
* be the half-conversion #146 exists to prevent — the rect it is measured
|
|
20
|
+
* against would still be physical.
|
|
21
|
+
*/
|
|
22
|
+
export function placeFloating({ rect, align = "start", gap, flipBelow, anchor, }) {
|
|
23
|
+
const rtl = isRtlElement(anchor);
|
|
24
|
+
const up = window.innerHeight - rect.bottom < flipBelow;
|
|
25
|
+
const style = up
|
|
26
|
+
? { bottom: window.innerHeight - rect.top + gap }
|
|
27
|
+
: { top: rect.bottom + gap };
|
|
28
|
+
// start is the left edge in LTR and the right edge in RTL; end is the other
|
|
29
|
+
const toRight = align === "end" ? !rtl : rtl;
|
|
30
|
+
if (toRight)
|
|
31
|
+
style.right = window.innerWidth - rect.right;
|
|
32
|
+
else
|
|
33
|
+
style.left = rect.left;
|
|
34
|
+
return { style, up };
|
|
35
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { PanelLeft } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { isRtlElement } from "../../core/index.js";
|
|
4
5
|
import { appshellRecipe } from "./appshell.recipe.js";
|
|
5
6
|
/**
|
|
6
7
|
* App-shell layout with a collapsible + resizable sidebar, mirroring Attio's
|
|
@@ -26,6 +27,11 @@ export function AppShell({ sidebar, children, width: widthProp, defaultWidth = 2
|
|
|
26
27
|
e.preventDefault();
|
|
27
28
|
const startX = e.clientX;
|
|
28
29
|
const startW = width;
|
|
30
|
+
// The sidebar is the flex row's first child, so under RTL it renders on the
|
|
31
|
+
// RIGHT — measured. `clientX` stays physical, so pulling right must SHRINK
|
|
32
|
+
// it there. Mirroring the handle's CSS without this is the half-conversion
|
|
33
|
+
// lesson 54 is about, which is why both change in one commit (#212).
|
|
34
|
+
const dir = isRtlElement(e.currentTarget) ? -1 : 1;
|
|
29
35
|
// Attio only enters the drag state once the pointer has actually moved — a
|
|
30
36
|
// click on the handle is not a drag, so the blue bar must not flash on press.
|
|
31
37
|
let moved = false;
|
|
@@ -39,7 +45,7 @@ export function AppShell({ sidebar, children, width: widthProp, defaultWidth = 2
|
|
|
39
45
|
moved = true;
|
|
40
46
|
setDragging(true);
|
|
41
47
|
}
|
|
42
|
-
const w = Math.round(Math.min(maxWidth, Math.max(minWidth, startW + (ev.clientX - startX))));
|
|
48
|
+
const w = Math.round(Math.min(maxWidth, Math.max(minWidth, startW + dir * (ev.clientX - startX))));
|
|
43
49
|
setDragWidth(w);
|
|
44
50
|
last = w;
|
|
45
51
|
};
|
|
@@ -9,8 +9,8 @@ const config = defineRecipe({
|
|
|
9
9
|
display: flex; height: 100%; width: 100%; overflow: hidden; background: ${c.bgSurface}; font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
10
10
|
& .appshell-side { position: relative; flex-shrink: 0; overflow: hidden; }
|
|
11
11
|
& .appshell-side-inner { height: 100%; }
|
|
12
|
-
& .appshell-resize { position: absolute; top: 0;
|
|
13
|
-
& .appshell-resize::after { content: ""; position: absolute; top: 0;
|
|
12
|
+
& .appshell-resize { position: absolute; top: 0; inset-inline-end: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
|
|
13
|
+
& .appshell-resize::after { content: ""; position: absolute; top: 0; inset-inline-end: 0; width: 1px; height: 100%; background: ${c.borderSubtle}; transition: background-color .16s, height .16s; }
|
|
14
14
|
& .appshell-resize:hover::after, & .appshell-resize[data-dragging]::after { width: 2px; background: ${c.bgBrandSolid}; }
|
|
15
15
|
& .appshell-main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
16
16
|
& .appshell-expand { position: absolute; top: ${c.space["2"]}; inset-inline-start: ${c.space["2"]}; z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: ${c.size.row}; height: ${c.size.row}; padding: 0; border: none; border-radius: ${c.radius.md}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; background: ${c.bgSurface}; color: ${c.fgMuted}; font-family: inherit; cursor: pointer; transition: background-color .12s; }
|
|
@@ -5,6 +5,7 @@ import { Avatar } from "../../components/avatar/index.js";
|
|
|
5
5
|
import { Checkbox } from "../../components/checkbox/index.js";
|
|
6
6
|
import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
|
|
7
7
|
import { Tabs } from "../../components/tabs/index.js";
|
|
8
|
+
import { isRtlElement } from "../../core/index.js";
|
|
8
9
|
import { tagTone, } from "../recordstable/index.js";
|
|
9
10
|
import { recordpageRecipe } from "./recordpage.recipe.js";
|
|
10
11
|
function valueEmpty(v) {
|
|
@@ -178,14 +179,20 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
178
179
|
const [resizing, setResizing] = React.useState(false);
|
|
179
180
|
const dragRef = React.useRef(null);
|
|
180
181
|
const onResizeDown = (e) => {
|
|
181
|
-
|
|
182
|
+
// details is the flex row's first child, so it sits on the right under RTL
|
|
183
|
+
// and a rightward pull must shrink it (#212)
|
|
184
|
+
dragRef.current = {
|
|
185
|
+
x: e.clientX,
|
|
186
|
+
w: detailsW,
|
|
187
|
+
dir: isRtlElement(e.currentTarget) ? -1 : 1,
|
|
188
|
+
};
|
|
182
189
|
setResizing(true);
|
|
183
190
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
184
191
|
};
|
|
185
192
|
const onResizeMove = (e) => {
|
|
186
193
|
if (!dragRef.current)
|
|
187
194
|
return;
|
|
188
|
-
const next = dragRef.current.w + (e.clientX - dragRef.current.x);
|
|
195
|
+
const next = dragRef.current.w + dragRef.current.dir * (e.clientX - dragRef.current.x);
|
|
189
196
|
setDetailsW(Math.max(300, Math.min(560, next)));
|
|
190
197
|
};
|
|
191
198
|
const onResizeUp = (e) => {
|
|
@@ -31,8 +31,8 @@ const config = defineRecipe({
|
|
|
31
31
|
|
|
32
32
|
& .recordpage-details { width: 320px; flex-shrink: 0; overflow-y: auto; border-inline-end: 1px solid ${c.borderSubtle}; padding: ${c.space["3"]}; }
|
|
33
33
|
& .recordpage-resizer { position: relative; flex: 0 0 auto; width: 0; align-self: stretch; cursor: col-resize; z-index: 5; touch-action: none; }
|
|
34
|
-
& .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0;
|
|
35
|
-
& .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0;
|
|
34
|
+
& .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -4px; width: 9px; }
|
|
35
|
+
& .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
|
|
36
36
|
& .recordpage-resizer:hover::after, & .recordpage-resizer[data-active]::after { background: color-mix(in srgb, ${c.bgBrandSolid} 55%, transparent); }
|
|
37
37
|
& .recordpage-entity { display: flex; align-items: center; gap: ${c.space["2"]}; padding: ${c.space["1"]} ${c.space["1"]} ${c.space["2"]}; }
|
|
38
38
|
& .recordpage-entity-logo { width: 32px; height: 32px; border-radius: 30%; flex-shrink: 0; object-fit: cover; background: ${c.bgRaised}; }
|
package/dist/recordpage.css
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
& .recordpage-details { width: 320px; flex-shrink: 0; overflow-y: auto; border-inline-end: 1px solid var(--border-subtle); padding: var(--space-3); }
|
|
24
24
|
& .recordpage-resizer { position: relative; flex: 0 0 auto; width: 0; align-self: stretch; cursor: col-resize; z-index: 5; touch-action: none; }
|
|
25
|
-
& .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0;
|
|
26
|
-
& .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0;
|
|
25
|
+
& .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -4px; width: 9px; }
|
|
26
|
+
& .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
|
|
27
27
|
& .recordpage-resizer:hover::after, & .recordpage-resizer[data-active]::after { background: color-mix(in srgb, var(--bg-brand-solid) 55%, transparent); }
|
|
28
28
|
& .recordpage-entity { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-1) var(--space-2); }
|
|
29
29
|
& .recordpage-entity-logo { width: 32px; height: 32px; border-radius: 30%; flex-shrink: 0; object-fit: cover; background: var(--bg-raised); }
|
package/dist/styles.css
CHANGED
|
@@ -1857,8 +1857,8 @@
|
|
|
1857
1857
|
display: flex; height: 100%; width: 100%; overflow: hidden; background: var(--bg-surface); font-family: var(--font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
1858
1858
|
& .appshell-side { position: relative; flex-shrink: 0; overflow: hidden; }
|
|
1859
1859
|
& .appshell-side-inner { height: 100%; }
|
|
1860
|
-
& .appshell-resize { position: absolute; top: 0;
|
|
1861
|
-
& .appshell-resize::after { content: ""; position: absolute; top: 0;
|
|
1860
|
+
& .appshell-resize { position: absolute; top: 0; inset-inline-end: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
|
|
1861
|
+
& .appshell-resize::after { content: ""; position: absolute; top: 0; inset-inline-end: 0; width: 1px; height: 100%; background: var(--border-subtle); transition: background-color .16s, height .16s; }
|
|
1862
1862
|
& .appshell-resize:hover::after, & .appshell-resize[data-dragging]::after { width: 2px; background: var(--bg-brand-solid); }
|
|
1863
1863
|
& .appshell-main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
1864
1864
|
& .appshell-expand { position: absolute; top: var(--space-2); inset-inline-start: var(--space-2); z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: var(--size-row); height: var(--size-row); padding: 0; border: none; border-radius: var(--radius-md); box-shadow: inset 0 0 0 1px var(--border-subtle); background: var(--bg-surface); color: var(--fg-muted); font-family: inherit; cursor: pointer; transition: background-color .12s; }
|
|
@@ -2213,8 +2213,8 @@
|
|
|
2213
2213
|
|
|
2214
2214
|
& .recordpage-details { width: 320px; flex-shrink: 0; overflow-y: auto; border-inline-end: 1px solid var(--border-subtle); padding: var(--space-3); }
|
|
2215
2215
|
& .recordpage-resizer { position: relative; flex: 0 0 auto; width: 0; align-self: stretch; cursor: col-resize; z-index: 5; touch-action: none; }
|
|
2216
|
-
& .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0;
|
|
2217
|
-
& .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0;
|
|
2216
|
+
& .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -4px; width: 9px; }
|
|
2217
|
+
& .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
|
|
2218
2218
|
& .recordpage-resizer:hover::after, & .recordpage-resizer[data-active]::after { background: color-mix(in srgb, var(--bg-brand-solid) 55%, transparent); }
|
|
2219
2219
|
& .recordpage-entity { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-1) var(--space-2); }
|
|
2220
2220
|
& .recordpage-entity-logo { width: 32px; height: 32px; border-radius: 30%; flex-shrink: 0; object-fit: cover; background: var(--bg-raised); }
|
|
@@ -3,7 +3,8 @@ export type MenuProps = {
|
|
|
3
3
|
/** The element that opens the menu (an icon button, etc.). */
|
|
4
4
|
trigger: React.ReactElement;
|
|
5
5
|
children: React.ReactNode;
|
|
6
|
-
/** Align the menu to the start
|
|
6
|
+
/** Align the menu to the trigger's inline `start` or `end` edge — the left
|
|
7
|
+
* edge in an LTR document, the right one in RTL. */
|
|
7
8
|
align?: "start" | "end";
|
|
8
9
|
/** Controlled open state. Omit to let Menu manage it (see defaultOpen). */
|
|
9
10
|
open?: boolean;
|
|
@@ -4,7 +4,8 @@ export type PopoverProps = {
|
|
|
4
4
|
trigger: React.ReactElement;
|
|
5
5
|
/** Popover body. */
|
|
6
6
|
children: React.ReactNode;
|
|
7
|
-
/** Align the popover to the trigger's
|
|
7
|
+
/** Align the popover to the trigger's inline `start` or `end` edge — the
|
|
8
|
+
* left edge in an LTR document, the right one in RTL. */
|
|
8
9
|
align?: "start" | "end";
|
|
9
10
|
/** Controlled open state. Omit to let the popover manage it. */
|
|
10
11
|
/** Controlled open state. Omit to let Popover manage it (see defaultOpen). */
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The nearest declared direction. The `dir` attribute is checked first because
|
|
3
|
+
* it is how a document says this in practice and it is readable without a
|
|
4
|
+
* layout, then computed style for a direction set purely in CSS.
|
|
5
|
+
*
|
|
6
|
+
* Shared so that a floating layer's edge choice and a drag handler's sign come
|
|
7
|
+
* from the same answer. They have to agree: mirroring one without the other is
|
|
8
|
+
* the half-conversion lesson 54 is about.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isRtlElement(anchor: Element | null | undefined): boolean;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { cx } from "./cx";
|
|
2
2
|
export { defineRecipe } from "./define";
|
|
3
|
+
export { isRtlElement } from "./direction";
|
|
4
|
+
export { type FloatingAlign, type PlaceFloatingOptions, placeFloating, } from "./place-floating";
|
|
3
5
|
export { Primitive } from "./primitive";
|
|
4
6
|
export { resolveRecipe } from "./resolve";
|
|
5
7
|
export * from "./rtl";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
export type FloatingAlign = "start" | "end";
|
|
3
|
+
export type PlaceFloatingOptions = {
|
|
4
|
+
/** The trigger's box, from `getBoundingClientRect()`. */
|
|
5
|
+
rect: DOMRect;
|
|
6
|
+
/** Which of the trigger's INLINE edges the layer lines up with. */
|
|
7
|
+
align?: FloatingAlign;
|
|
8
|
+
/** Distance between the trigger and the layer. */
|
|
9
|
+
gap: number;
|
|
10
|
+
/** Flip above the trigger when there is less room below than this. */
|
|
11
|
+
flipBelow: number;
|
|
12
|
+
/** The element the layer belongs to; its computed direction decides sides. */
|
|
13
|
+
anchor?: Element | null;
|
|
14
|
+
};
|
|
15
|
+
export type PlacedFloating = {
|
|
16
|
+
style: React.CSSProperties;
|
|
17
|
+
/** True when the layer was flipped above the trigger. */
|
|
18
|
+
up: boolean;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Where a floating layer goes, in logical terms.
|
|
22
|
+
*
|
|
23
|
+
* `start` and `end` are inline edges, not left and right. `Menu` and `Popover`
|
|
24
|
+
* both took an `align` prop named that way and then wrote
|
|
25
|
+
* `align === "end" ? right : left` with no direction anywhere, so in an RTL
|
|
26
|
+
* document both were backwards and a layer near the viewport edge overflowed on
|
|
27
|
+
* the wrong side (#210).
|
|
28
|
+
*
|
|
29
|
+
* The direction is read from the anchor's computed style rather than from a
|
|
30
|
+
* prop, so an inherited `dir` on any ancestor is honoured without every call
|
|
31
|
+
* site having to thread it down.
|
|
32
|
+
*
|
|
33
|
+
* Physical `left`/`right` are deliberate in the OUTPUT: this positions a
|
|
34
|
+
* `position: fixed` layer against viewport coordinates, which are physical in
|
|
35
|
+
* both directions. What must be direction-aware is the CHOICE of edge, and
|
|
36
|
+
* that happens here, once. Converting the output to `inset-inline-start` would
|
|
37
|
+
* be the half-conversion #146 exists to prevent — the rect it is measured
|
|
38
|
+
* against would still be physical.
|
|
39
|
+
*/
|
|
40
|
+
export declare function placeFloating({ rect, align, gap, flipBelow, anchor, }: PlaceFloatingOptions): PlacedFloating;
|