@crisp-ui-kit/crisp 0.35.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bulkbar.css +3 -0
- package/dist/bulkbar.layered.css +3 -0
- package/dist/cjs/components/bulkbar/bulkbar.js +1 -1
- package/dist/cjs/components/bulkbar/bulkbar.recipe.js +6 -0
- package/dist/cjs/components/contextmenu/contextmenu.js +25 -8
- package/dist/cjs/components/contextmenu/contextmenu.recipe.js +5 -2
- package/dist/cjs/components/dialog/dialog.js +1 -1
- package/dist/cjs/components/dialog/dialog.recipe.js +1 -1
- package/dist/cjs/components/menu/menu.js +20 -3
- package/dist/cjs/components/menu/menu.recipe.js +4 -3
- package/dist/cjs/components/multiselect/multiselect.js +26 -9
- package/dist/cjs/components/multiselect/multiselect.recipe.js +4 -3
- package/dist/cjs/components/popover/popover.js +25 -8
- package/dist/cjs/components/popover/popover.recipe.js +5 -3
- package/dist/cjs/components/select/select.js +30 -9
- package/dist/cjs/components/select/select.recipe.js +5 -3
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +50 -6
- package/dist/cjs/core/floating-portal.js +28 -0
- package/dist/cjs/core/floating-stack.js +54 -0
- package/dist/cjs/core/index.js +6 -1
- package/dist/cjs/product/recordpanel/recordpanel.js +4 -2
- package/dist/cjs/product/recordpanel/recordpanel.recipe.js +8 -0
- package/dist/cjs/product/recordstable/recordstable.js +15 -4
- package/dist/cjs/product/sidebar/sidebar.js +16 -3
- package/dist/contextmenu.css +1 -1
- package/dist/contextmenu.layered.css +1 -1
- package/dist/dialog.css +1 -1
- package/dist/dialog.layered.css +1 -1
- package/dist/esm/components/bulkbar/bulkbar.js +1 -1
- package/dist/esm/components/bulkbar/bulkbar.recipe.js +6 -0
- package/dist/esm/components/contextmenu/contextmenu.js +25 -8
- package/dist/esm/components/contextmenu/contextmenu.recipe.js +5 -2
- package/dist/esm/components/dialog/dialog.js +1 -1
- package/dist/esm/components/dialog/dialog.recipe.js +1 -1
- package/dist/esm/components/menu/menu.js +21 -4
- package/dist/esm/components/menu/menu.recipe.js +4 -3
- package/dist/esm/components/multiselect/multiselect.js +27 -10
- package/dist/esm/components/multiselect/multiselect.recipe.js +4 -3
- package/dist/esm/components/popover/popover.js +26 -9
- package/dist/esm/components/popover/popover.recipe.js +5 -3
- package/dist/esm/components/select/select.js +31 -10
- package/dist/esm/components/select/select.recipe.js +5 -3
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +51 -7
- package/dist/esm/core/floating-portal.js +25 -0
- package/dist/esm/core/floating-stack.js +50 -0
- package/dist/esm/core/index.js +2 -0
- package/dist/esm/product/recordpanel/recordpanel.js +4 -2
- package/dist/esm/product/recordpanel/recordpanel.recipe.js +8 -0
- package/dist/esm/product/recordstable/recordstable.js +16 -5
- package/dist/esm/product/sidebar/sidebar.js +17 -4
- package/dist/menu.css +1 -1
- package/dist/menu.layered.css +1 -1
- package/dist/multiselect.css +1 -1
- package/dist/multiselect.layered.css +1 -1
- package/dist/popover.css +1 -1
- package/dist/popover.layered.css +1 -1
- package/dist/recordpanel.css +6 -0
- package/dist/recordpanel.layered.css +6 -0
- package/dist/select.css +1 -1
- package/dist/select.layered.css +1 -1
- package/dist/styles.css +15 -6
- package/dist/styles.layered.css +15 -6
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +10 -1
- package/dist/types/core/floating-portal.d.ts +25 -0
- package/dist/types/core/floating-stack.d.ts +13 -0
- package/dist/types/core/index.d.ts +2 -0
- package/dist/types/product/recordpanel/recordpanel.d.ts +11 -1
- package/dist/types/product/sidebar/sidebar.d.ts +56 -0
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FloatingPortal = FloatingPortal;
|
|
4
|
+
const react_dom_1 = require("react-dom");
|
|
5
|
+
/**
|
|
6
|
+
* Portals a floating layer (a `position: fixed` listbox/menu/popover
|
|
7
|
+
* content) to `document.body` — exactly what Tooltip already did with a
|
|
8
|
+
* bare `createPortal` call, pulled out so `Select`, `MultiSelect`, `Menu`,
|
|
9
|
+
* `Popover` and `ContextMenu` share the one implementation (#339).
|
|
10
|
+
*
|
|
11
|
+
* Needed because a `position: fixed` box resolves against the nearest
|
|
12
|
+
* ancestor that establishes a containing block for it — not necessarily the
|
|
13
|
+
* viewport. `transform` (even the identity `scale(1)`), `filter` and
|
|
14
|
+
* `backdrop-filter` all qualify, and `Dialog`'s own measured surface sets
|
|
15
|
+
* both a resting `transform: scale(1)` (so its `@starting-style` entrance
|
|
16
|
+
* has something to animate from) and `backdrop-filter: blur(15px)` (Attio's
|
|
17
|
+
* frosted-glass modal). A floating layer opened from a trigger inside a
|
|
18
|
+
* `Dialog` was rendering offset by the dialog's own box instead of anchored
|
|
19
|
+
* to its trigger.
|
|
20
|
+
*
|
|
21
|
+
* Every caller renders this conditionally on its own `open` state, which
|
|
22
|
+
* starts `false` and is driven only by client interaction — never a prop a
|
|
23
|
+
* fixture sets true at mount (unlike Dialog/Drawer/Command) — so this never
|
|
24
|
+
* touches `document` during SSR, the same safety Tooltip already had.
|
|
25
|
+
*/
|
|
26
|
+
function FloatingPortal({ children, }) {
|
|
27
|
+
return (0, react_dom_1.createPortal)(children, document.body);
|
|
28
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerFloatingLayer = registerFloatingLayer;
|
|
4
|
+
exports.isInsideLaterFloatingLayer = isInsideLaterFloatingLayer;
|
|
5
|
+
/**
|
|
6
|
+
* Tracks every currently-open `FloatingPortal` layer, in open order, so a
|
|
7
|
+
* layer's outside-click check can tell "outside me" from "inside a layer I
|
|
8
|
+
* spawned" (#339).
|
|
9
|
+
*
|
|
10
|
+
* Portaling `Select`/`MultiSelect`/`Menu`/`Popover`/`ContextMenu` to
|
|
11
|
+
* `document.body` fixed their own mis-positioning inside a `Dialog`, but it
|
|
12
|
+
* broke a composition each of them supports: one of these opened INSIDE
|
|
13
|
+
* another (TableToolbar's sort popover is a `Menu` holding a per-row
|
|
14
|
+
* `Select`). Before portaling, the inner layer was a DOM descendant of the
|
|
15
|
+
* outer one, so the outer's `rootRef.contains(target)` check saw a click
|
|
16
|
+
* inside the inner layer as "inside". Once both portal independently to
|
|
17
|
+
* `document.body`, they become DOM SIBLINGS — nested in the React tree, not
|
|
18
|
+
* the DOM tree — so that same check reads the inner layer's own pointerdown
|
|
19
|
+
* as "outside", and the outer layer closes itself, unmounting the inner one
|
|
20
|
+
* mid-interaction.
|
|
21
|
+
*
|
|
22
|
+
* The fix: each layer registers its content node here on open (removed on
|
|
23
|
+
* close/unmount), and a layer's outside-click check also accepts a target
|
|
24
|
+
* inside any layer registered AFTER it — i.e. any layer that could only have
|
|
25
|
+
* opened from inside it.
|
|
26
|
+
*/
|
|
27
|
+
const stack = [];
|
|
28
|
+
/** Call when a layer opens; call the returned cleanup when it closes. */
|
|
29
|
+
function registerFloatingLayer(el) {
|
|
30
|
+
stack.push(el);
|
|
31
|
+
return () => {
|
|
32
|
+
const i = stack.indexOf(el);
|
|
33
|
+
if (i >= 0)
|
|
34
|
+
stack.splice(i, 1);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* True when `target` sits inside a layer that was registered strictly after
|
|
39
|
+
* `self` — a layer that could only have been spawned from inside `self`,
|
|
40
|
+
* directly or through further nesting.
|
|
41
|
+
*
|
|
42
|
+
* Pass `null` for `self` when the caller is a hand-rolled dropdown that
|
|
43
|
+
* doesn't register itself on this stack (RecordsTable's and TableToolbar's
|
|
44
|
+
* own popovers, e.g.) — every registered layer then counts, which is the
|
|
45
|
+
* right query for "was this click inside some floating child I opened".
|
|
46
|
+
*/
|
|
47
|
+
function isInsideLaterFloatingLayer(target, self) {
|
|
48
|
+
const selfIndex = self ? stack.indexOf(self) : -1;
|
|
49
|
+
for (let i = selfIndex + 1; i < stack.length; i++) {
|
|
50
|
+
if (stack[i]?.contains(target))
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
package/dist/cjs/core/index.js
CHANGED
|
@@ -14,7 +14,7 @@ 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.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.formValueStyle = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = void 0;
|
|
17
|
+
exports.usePrefersReducedMotion = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.formValueStyle = exports.registerFloatingLayer = exports.isInsideLaterFloatingLayer = exports.FloatingPortal = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = void 0;
|
|
18
18
|
var cell_date_1 = require("./cell-date");
|
|
19
19
|
Object.defineProperty(exports, "formatCellDate", { enumerable: true, get: function () { return cell_date_1.formatCellDate; } });
|
|
20
20
|
Object.defineProperty(exports, "parseCellDate", { enumerable: true, get: function () { return cell_date_1.parseCellDate; } });
|
|
@@ -26,6 +26,11 @@ var dialog_description_1 = require("./dialog-description");
|
|
|
26
26
|
Object.defineProperty(exports, "withDescribedBy", { enumerable: true, get: function () { return dialog_description_1.withDescribedBy; } });
|
|
27
27
|
var direction_1 = require("./direction");
|
|
28
28
|
Object.defineProperty(exports, "isRtlElement", { enumerable: true, get: function () { return direction_1.isRtlElement; } });
|
|
29
|
+
var floating_portal_1 = require("./floating-portal");
|
|
30
|
+
Object.defineProperty(exports, "FloatingPortal", { enumerable: true, get: function () { return floating_portal_1.FloatingPortal; } });
|
|
31
|
+
var floating_stack_1 = require("./floating-stack");
|
|
32
|
+
Object.defineProperty(exports, "isInsideLaterFloatingLayer", { enumerable: true, get: function () { return floating_stack_1.isInsideLaterFloatingLayer; } });
|
|
33
|
+
Object.defineProperty(exports, "registerFloatingLayer", { enumerable: true, get: function () { return floating_stack_1.registerFloatingLayer; } });
|
|
29
34
|
var form_value_1 = require("./form-value");
|
|
30
35
|
Object.defineProperty(exports, "formValueStyle", { enumerable: true, get: function () { return form_value_1.formValueStyle; } });
|
|
31
36
|
var place_floating_1 = require("./place-floating");
|
|
@@ -62,7 +62,8 @@ function FieldValue({ col, row, emptyLabel, }) {
|
|
|
62
62
|
* relative` container (e.g. the app main area); it slides in from the right and
|
|
63
63
|
* overlays the content.
|
|
64
64
|
*/
|
|
65
|
-
function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", detailsLabel = "Details", emptyLabel = "Empty", ...rest }) {
|
|
65
|
+
function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", title, detailsLabel = "Details", emptyLabel = "Empty", footer, ...rest }) {
|
|
66
|
+
const titleId = React.useId();
|
|
66
67
|
// close on Escape while open
|
|
67
68
|
React.useEffect(() => {
|
|
68
69
|
if (!record)
|
|
@@ -78,6 +79,7 @@ function RecordPanel({ record, columns, onClose, className, closeLabel = "Close
|
|
|
78
79
|
? record[entityCol.key]
|
|
79
80
|
: undefined;
|
|
80
81
|
const fields = columns.filter((c) => c.type !== "entity");
|
|
81
|
-
|
|
82
|
+
const heading = title ?? entity?.name ?? "Record";
|
|
83
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpanel_recipe_1.recordpanelRecipe)({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-header", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpanel-fav", src: entity.avatar, alt: "" })) : entity ? ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "md", shape: "rounded", tone: "brand", children: entity.name.charAt(0) })) : null, (0, jsx_runtime_1.jsx)("span", { id: titleId, className: "crisp-recordpanel-title", children: heading }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 18, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-body", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-sectionlabel", children: detailsLabel }), fields.map((col) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-field-value", children: (0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, emptyLabel: emptyLabel }) })] }, col.key)))] }), footer != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
|
|
82
84
|
}
|
|
83
85
|
RecordPanel.displayName = "RecordPanel";
|
|
@@ -39,6 +39,14 @@ const config = (0, core_1.defineRecipe)({
|
|
|
39
39
|
${recordstable_tag_1.recordTagCss}
|
|
40
40
|
& .crisp-recordpanel-link { color: ${tokens_1.contract.bgBrandSolid}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
41
41
|
& .crisp-recordpanel-link:hover { text-decoration: underline; }
|
|
42
|
+
|
|
43
|
+
/* #329: an optional slot for record actions (e.g. Edit). Reuses the
|
|
44
|
+
header's own measured values rather than inventing new ones -- the
|
|
45
|
+
same horizontal inset (space.3), the same hairline border token
|
|
46
|
+
(borderSubtle, flipped to border-top since this sits at the opposite
|
|
47
|
+
edge), and the header/body gap (space.2) for its own vertical padding
|
|
48
|
+
and for spacing between whatever it's given. */
|
|
49
|
+
& .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; border-top: 1px solid ${tokens_1.contract.borderSubtle}; }
|
|
42
50
|
`,
|
|
43
51
|
variants: {},
|
|
44
52
|
defaultVariants: {},
|
|
@@ -499,8 +499,15 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
|
|
|
499
499
|
return;
|
|
500
500
|
const onDown = (e) => {
|
|
501
501
|
const t = e.target;
|
|
502
|
-
if (
|
|
503
|
-
|
|
502
|
+
if (t.closest(".crisp-recordstable-menu, [data-menu-trigger]"))
|
|
503
|
+
return;
|
|
504
|
+
// #339: a Select/MultiSelect/Menu opened from inside this popover now
|
|
505
|
+
// portals to document.body, so it's no longer inside
|
|
506
|
+
// .crisp-recordstable-menu by DOM containment -- check the shared
|
|
507
|
+
// floating-layer stack too.
|
|
508
|
+
if ((0, core_1.isInsideLaterFloatingLayer)(t, null))
|
|
509
|
+
return;
|
|
510
|
+
setOpenMenu(null);
|
|
504
511
|
};
|
|
505
512
|
const onKey = (e) => e.key === "Escape" && setOpenMenu(null);
|
|
506
513
|
document.addEventListener("mousedown", onDown);
|
|
@@ -515,8 +522,12 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
|
|
|
515
522
|
if (!ctxMenu)
|
|
516
523
|
return;
|
|
517
524
|
const onDown = (e) => {
|
|
518
|
-
|
|
519
|
-
|
|
525
|
+
const t = e.target;
|
|
526
|
+
if (t.closest(".crisp-recordstable-ctx"))
|
|
527
|
+
return;
|
|
528
|
+
if ((0, core_1.isInsideLaterFloatingLayer)(t, null))
|
|
529
|
+
return;
|
|
530
|
+
setCtxMenu(null);
|
|
520
531
|
};
|
|
521
532
|
const onKey = (e) => e.key === "Escape" && setCtxMenu(null);
|
|
522
533
|
document.addEventListener("mousedown", onDown);
|
|
@@ -117,8 +117,14 @@ exports.ATTIO_SIDEBAR_FOOTER = {
|
|
|
117
117
|
function SectionHeader({ label, open, onToggle, }) {
|
|
118
118
|
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-section-header", "data-collapsed": open ? undefined : "", "aria-expanded": open, onClick: onToggle, children: [(0, jsx_runtime_1.jsx)(IconChevronMini, {}), (0, jsx_runtime_1.jsx)("span", { children: label })] }));
|
|
119
119
|
}
|
|
120
|
-
exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = exports.ATTIO_SIDEBAR_WORKSPACE, items = exports.ATTIO_SIDEBAR_ITEMS, recordsSection = exports.ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = exports.ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = exports.ATTIO_SIDEBAR_CHATS_SECTION, footer = exports.ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
|
|
121
|
-
|
|
120
|
+
exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = exports.ATTIO_SIDEBAR_WORKSPACE, items = exports.ATTIO_SIDEBAR_ITEMS, recordsSection = exports.ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = exports.ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = exports.ATTIO_SIDEBAR_CHATS_SECTION, groups, footer = exports.ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
|
|
121
|
+
// Seeded once, on mount, from whatever groups start collapsed — not
|
|
122
|
+
// reactive to `groups` changing later, matching every other piece of
|
|
123
|
+
// this component's state (recordsSection/listsSection/chatsSection have
|
|
124
|
+
// never supported a starting-collapsed prop at all).
|
|
125
|
+
const [collapsedSections, setCollapsedSections] = React.useState(() => new Set(groups
|
|
126
|
+
?.filter((g) => g.defaultCollapsed)
|
|
127
|
+
.map((g) => g.key ?? g.label)));
|
|
122
128
|
const [commandOpen, setCommandOpen] = React.useState(false);
|
|
123
129
|
React.useEffect(() => {
|
|
124
130
|
function handleKeyDown(event) {
|
|
@@ -143,6 +149,13 @@ exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, coll
|
|
|
143
149
|
return next;
|
|
144
150
|
});
|
|
145
151
|
}, []);
|
|
146
|
-
return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.div, { ref: ref, asChild: asChild, className: (0, sidebar_recipe_1.sidebarRecipe)({ className }), ...props, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-ws", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), (0, jsx_runtime_1.jsx)(IconChevronDown, {}), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: (0, jsx_runtime_1.jsx)(IconCollapsePanel, {}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-searchrow", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-label", children: searchLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: (0, jsx_runtime_1.jsx)(IconInbox, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, children: label }, label))) }),
|
|
152
|
+
return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.div, { ref: ref, asChild: asChild, className: (0, sidebar_recipe_1.sidebarRecipe)({ className }), ...props, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-ws", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), (0, jsx_runtime_1.jsx)(IconChevronDown, {}), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: (0, jsx_runtime_1.jsx)(IconCollapsePanel, {}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-searchrow", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-label", children: searchLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: (0, jsx_runtime_1.jsx)(IconInbox, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, children: label }, label))) }), groups ? (
|
|
153
|
+
// #325: navigation as data, reusing the exact section markup/
|
|
154
|
+
// recipe above — SectionHeader + .crisp-sidebar-section — instead
|
|
155
|
+
// of the three fixed Attio sections.
|
|
156
|
+
groups.map((group) => {
|
|
157
|
+
const groupKey = group.key ?? group.label;
|
|
158
|
+
return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [(0, jsx_runtime_1.jsx)(SectionHeader, { label: group.label, open: !collapsedSections.has(groupKey), onToggle: () => toggleSection(groupKey) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has(groupKey) ? "" : undefined, children: group.items.map((item) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: item.href ?? "#", active: item.active, icon: item.icon ? (0, jsx_runtime_1.jsx)(item.icon, {}) : undefined, trailing: item.count, onClick: item.onSelect, children: item.label }, item.href ?? item.label))) })] }, groupKey));
|
|
159
|
+
})) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", shape: "rounded", tone: tone, children: (0, jsx_runtime_1.jsx)(Glyph, {}) }), children: label }, label))) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-sidebar-newlist", children: listsSection.newListLabel }) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), children: label }, label))) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-footer", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", className: "crisp-sidebar-pill", children: footer.gettingStartedLabel }), (0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(IconInvite, {}), children: footer.inviteLabel }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-trial", children: [(0, jsx_runtime_1.jsx)("span", { children: footer.trialText }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] }), (0, jsx_runtime_1.jsx)(command_1.Command, { open: commandOpen, onOpenChange: setCommandOpen })] }));
|
|
147
160
|
});
|
|
148
161
|
exports.Sidebar.displayName = "Sidebar";
|
package/dist/contextmenu.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.crisp-contextmenu {
|
|
2
2
|
display: contents; font-family: var(--crisp-font-sans);
|
|
3
|
-
& .crisp-contextmenu-content { position: fixed; z-index:
|
|
3
|
+
& .crisp-contextmenu-content { position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
4
4
|
& .crisp-contextmenu-item { display: flex; align-items: center; gap: var(--crisp-space-2); width: 100%; height: var(--crisp-size-menuRow); box-sizing: border-box; padding: 0 var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); text-align: start; white-space: nowrap; cursor: pointer; }
|
|
5
5
|
& .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
|
|
6
6
|
& .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@layer crisp-components {
|
|
2
2
|
.crisp-contextmenu {
|
|
3
3
|
display: contents; font-family: var(--crisp-font-sans);
|
|
4
|
-
& .crisp-contextmenu-content { position: fixed; z-index:
|
|
4
|
+
& .crisp-contextmenu-content { position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
5
5
|
& .crisp-contextmenu-item { display: flex; align-items: center; gap: var(--crisp-space-2); width: 100%; height: var(--crisp-size-menuRow); box-sizing: border-box; padding: 0 var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); text-align: start; white-space: nowrap; cursor: pointer; }
|
|
6
6
|
& .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
|
|
7
7
|
& .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
|
package/dist/dialog.css
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
& .crisp-dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); overflow: hidden; }
|
|
7
7
|
& .crisp-dialog-header { display: flex; align-items: center; gap: var(--crisp-space-2); flex-shrink: 0; height: 52px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
|
|
8
8
|
& .crisp-dialog-header svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
|
|
9
|
-
& .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
|
|
9
|
+
& .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); margin: 0; }
|
|
10
10
|
& .crisp-dialog-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
|
|
11
11
|
& .crisp-dialog-close:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
|
|
12
12
|
& .crisp-dialog-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--crisp-space-4); padding: var(--crisp-space-5) var(--crisp-space-4); }
|
package/dist/dialog.layered.css
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
& .crisp-dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); overflow: hidden; }
|
|
8
8
|
& .crisp-dialog-header { display: flex; align-items: center; gap: var(--crisp-space-2); flex-shrink: 0; height: 52px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
|
|
9
9
|
& .crisp-dialog-header svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
|
|
10
|
-
& .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
|
|
10
|
+
& .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); margin: 0; }
|
|
11
11
|
& .crisp-dialog-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
|
|
12
12
|
& .crisp-dialog-close:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
|
|
13
13
|
& .crisp-dialog-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--crisp-space-4); padding: var(--crisp-space-5) var(--crisp-space-4); }
|
|
@@ -8,7 +8,7 @@ import { bulkbarRecipe } from "./bulkbar.recipe.js";
|
|
|
8
8
|
* (e.g. `count > 0 && <BulkBar …/>`). Measured from a records table.
|
|
9
9
|
*/
|
|
10
10
|
export function BulkBar({ count, onClear, countLabel = "selected", countText = (count, label) => (_jsxs(_Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
|
|
11
|
-
return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-bulkbar-bar", children: [count != null && (_jsxs("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), _jsx(X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
|
|
11
|
+
return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-bulkbar-bar", children: [count != null && (_jsxs("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), _jsx("span", { className: "crisp-bulkbar-clearlabel", children: clearLabel }), _jsx(X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
|
|
12
12
|
}
|
|
13
13
|
BulkBar.displayName = "BulkBar";
|
|
14
14
|
/** A styled action button for use inside a `<BulkBar>` (or as a `<Menu>` trigger). */
|
|
@@ -15,6 +15,12 @@ const config = defineRecipe({
|
|
|
15
15
|
& .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; margin: 0; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
16
16
|
& .crisp-bulkbar-count:hover { background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); color: ${c.fgPrimary}; }
|
|
17
17
|
& .crisp-bulkbar-count svg { flex-shrink: 0; }
|
|
18
|
+
/* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
|
|
19
|
+
-- read, never drawn. Same technique as Slider's thumblabel. */
|
|
20
|
+
& .crisp-bulkbar-clearlabel {
|
|
21
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
22
|
+
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
|
|
23
|
+
}
|
|
18
24
|
& .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: transparent; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
19
25
|
& .crisp-bulkbar-btn:hover { background: ${c.bgRaised}; }
|
|
20
26
|
& .crisp-bulkbar-btn--danger { color: ${c.bgDanger}; }
|
|
@@ -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 { FloatingPortal, isInsideLaterFloatingLayer, registerFloatingLayer, } from "../../core/index.js";
|
|
3
4
|
import { useMenuKeyboard } from "../menu/index.js";
|
|
4
5
|
import { contextmenuRecipe } from "./contextmenu.recipe.js";
|
|
5
6
|
/**
|
|
@@ -41,12 +42,28 @@ export function ContextMenu({ children, items, menuLabel = "Context menu", class
|
|
|
41
42
|
dismiss: () => close(),
|
|
42
43
|
contentRef: menuRef,
|
|
43
44
|
});
|
|
45
|
+
// Registers the (now-portaled) menu on the shared floating-layer stack
|
|
46
|
+
// (#339) so this doesn't get closed out from under itself when it holds
|
|
47
|
+
// another floating layer -- see core/floating-stack.ts.
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
if (!pos || !menuRef.current)
|
|
50
|
+
return;
|
|
51
|
+
return registerFloatingLayer(menuRef.current);
|
|
52
|
+
}, [pos]);
|
|
44
53
|
React.useEffect(() => {
|
|
45
54
|
if (!pos)
|
|
46
55
|
return;
|
|
47
56
|
const onDown = (e) => {
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
// menuRef too (#339): the menu content portals to document.body, so
|
|
58
|
+
// it's no longer a descendant of rootRef.
|
|
59
|
+
const target = e.target;
|
|
60
|
+
if (rootRef.current?.contains(target))
|
|
61
|
+
return;
|
|
62
|
+
if (menuRef.current?.contains(target))
|
|
63
|
+
return;
|
|
64
|
+
if (isInsideLaterFloatingLayer(target, menuRef.current))
|
|
65
|
+
return;
|
|
66
|
+
close();
|
|
50
67
|
};
|
|
51
68
|
const onKey = (e) => e.key === "Escape" && close(true);
|
|
52
69
|
document.addEventListener("pointerdown", onDown);
|
|
@@ -56,11 +73,11 @@ export function ContextMenu({ children, items, menuLabel = "Context menu", class
|
|
|
56
73
|
document.removeEventListener("keydown", onKey);
|
|
57
74
|
};
|
|
58
75
|
}, [pos, close]);
|
|
59
|
-
return (_jsxs("div", { ref: rootRef, className: contextmenuRecipe({ className }), children: [_jsx("div", { onContextMenu: open, children: children }), pos && (_jsx("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? (_jsx("hr", { className: "crisp-contextmenu-sep" }, i)) : (_jsxs("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
return (_jsxs("div", { ref: rootRef, className: contextmenuRecipe({ className }), children: [_jsx("div", { onContextMenu: open, children: children }), pos && (_jsx(FloatingPortal, { children: _jsx("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? (_jsx("hr", { className: "crisp-contextmenu-sep" }, i)) : (_jsxs("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
|
|
77
|
+
? "crisp-contextmenu-item crisp-contextmenu-item--danger"
|
|
78
|
+
: "crisp-contextmenu-item", tabIndex: -1, onClick: () => {
|
|
79
|
+
item.onSelect?.();
|
|
80
|
+
close(true);
|
|
81
|
+
}, children: [item.icon, item.label, item.shortcut && (_jsx("span", { className: "crisp-contextmenu-shortcut", children: item.shortcut }))] }, i))) }) }))] }));
|
|
65
82
|
}
|
|
66
83
|
ContextMenu.displayName = "ContextMenu";
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
2
|
import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
3
|
-
// A right-click menu on the measured popover surface, positioned at the
|
|
3
|
+
// A right-click menu on the measured popover surface, positioned at the
|
|
4
|
+
// cursor. The content portals to document.body (#339) so no ancestor's
|
|
5
|
+
// transform/backdrop-filter can capture its position: fixed box; z-index 70
|
|
6
|
+
// keeps it above Dialog/Drawer's 60/61.
|
|
4
7
|
const config = defineRecipe({
|
|
5
8
|
name: "contextmenu",
|
|
6
9
|
base: `display: contents; font-family: ${c.font.sans};
|
|
7
|
-
& .crisp-contextmenu-content { position: fixed; z-index:
|
|
10
|
+
& .crisp-contextmenu-content { position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; padding: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: ${c.bgPopover}; box-shadow: ${e.floating}; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
8
11
|
& .crisp-contextmenu-item { display: flex; align-items: center; gap: ${c.space["2"]}; width: 100%; height: ${c.size.menuRow}; box-sizing: border-box; padding: 0 ${c.space["2"]}; margin: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.lg}; text-align: start; white-space: nowrap; cursor: pointer; }
|
|
9
12
|
& .crisp-contextmenu-item svg { flex-shrink: 0; color: ${c.fgMuted}; }
|
|
10
13
|
& .crisp-contextmenu-item:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
|
|
@@ -41,7 +41,7 @@ export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-lab
|
|
|
41
41
|
return (_jsx(Portal, { children: _jsxs("div", { className: dialogRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "crisp-dialog-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "crisp-dialog-positioner", children: _jsx("div", { ...rest, ...withDescribedBy(api.getContentProps(), describedBy), className: cx("crisp-dialog-content", className), "data-crisp-dialog": id,
|
|
42
42
|
// `width` is the component's own knob, so it wins over a passed
|
|
43
43
|
// maxWidth; everything else a consumer sets survives
|
|
44
|
-
style: { ...style, maxWidth: `min(${width}px, 90vw)` }, children: _jsxs("div", { className: "crisp-dialog-card", children: [title != null && (_jsxs("div", { className: "crisp-dialog-header", children: [icon, _jsx("
|
|
44
|
+
style: { ...style, maxWidth: `min(${width}px, 90vw)` }, children: _jsxs("div", { className: "crisp-dialog-card", children: [title != null && (_jsxs("div", { className: "crisp-dialog-header", children: [icon, _jsx("h2", { ...api.getTitleProps(), className: "crisp-dialog-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "crisp-dialog-close", "aria-label": closeLabel, children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) }) })] }) }));
|
|
45
45
|
}
|
|
46
46
|
Dialog.displayName = "Dialog";
|
|
47
47
|
export function DialogBody({ children, className, }) {
|
|
@@ -14,7 +14,7 @@ const config = defineRecipe({
|
|
|
14
14
|
& .crisp-dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; box-shadow: 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 8%, transparent); overflow: hidden; }
|
|
15
15
|
& .crisp-dialog-header { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; height: 52px; padding: 0 ${c.space["2"]} 0 ${c.space["4"]}; border-bottom: 1px solid ${c.borderSubtle}; }
|
|
16
16
|
& .crisp-dialog-header svg { flex-shrink: 0; color: ${c.fgMuted}; }
|
|
17
|
-
& .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
|
|
17
|
+
& .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; margin: 0; }
|
|
18
18
|
& .crisp-dialog-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgMuted}; font-family: inherit; cursor: pointer; }
|
|
19
19
|
& .crisp-dialog-close:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
|
|
20
20
|
& .crisp-dialog-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: ${c.space["4"]}; padding: ${c.space["5"]} ${c.space["4"]}; }
|
|
@@ -1,7 +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
|
+
import { FloatingPortal, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, } from "../../core/index.js";
|
|
5
5
|
import { menuRecipe } from "./menu.recipe.js";
|
|
6
6
|
import { useMenuKeyboard } from "./menu-keyboard.js";
|
|
7
7
|
const MenuContext = React.createContext(null);
|
|
@@ -58,13 +58,30 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
58
58
|
setOpen(false);
|
|
59
59
|
triggerRef.current?.focus?.();
|
|
60
60
|
}, [setOpen]);
|
|
61
|
+
// Registers the (now-portaled) content on the shared floating-layer stack
|
|
62
|
+
// (#339) so this doesn't get closed out from under itself when it holds
|
|
63
|
+
// another floating layer (a Select in a sort popover, e.g.) -- see
|
|
64
|
+
// core/floating-stack.ts.
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
if (!open || !contentRef.current)
|
|
67
|
+
return;
|
|
68
|
+
return registerFloatingLayer(contentRef.current);
|
|
69
|
+
}, [open]);
|
|
61
70
|
// close on outside pointer-down
|
|
62
71
|
React.useEffect(() => {
|
|
63
72
|
if (!open)
|
|
64
73
|
return;
|
|
65
74
|
const onDown = (e) => {
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
// contentRef too (#339): the menu content portals to document.body, so
|
|
76
|
+
// it's no longer a descendant of rootRef.
|
|
77
|
+
const target = e.target;
|
|
78
|
+
if (rootRef.current?.contains(target))
|
|
79
|
+
return;
|
|
80
|
+
if (contentRef.current?.contains(target))
|
|
81
|
+
return;
|
|
82
|
+
if (isInsideLaterFloatingLayer(target, contentRef.current))
|
|
83
|
+
return;
|
|
84
|
+
setOpen(false);
|
|
68
85
|
};
|
|
69
86
|
document.addEventListener("pointerdown", onDown);
|
|
70
87
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
@@ -88,7 +105,7 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
88
105
|
setOpen(!open);
|
|
89
106
|
},
|
|
90
107
|
});
|
|
91
|
-
return (_jsxs("div", { ref: rootRef, className: menuRecipe({ className }), children: [triggerEl, open && (_jsx(MenuContext.Provider, { value: { close }, children: _jsx("div", { ref: contentRef, className: "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: children }) }))] }));
|
|
108
|
+
return (_jsxs("div", { ref: rootRef, className: menuRecipe({ className }), children: [triggerEl, open && (_jsx(FloatingPortal, { children: _jsx(MenuContext.Provider, { value: { close }, children: _jsx("div", { ref: contentRef, className: "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: children }) }) }))] }));
|
|
92
109
|
}
|
|
93
110
|
Menu.displayName = "Menu";
|
|
94
111
|
export function MenuItem({ children, icon, shortcut, danger, disabled, onSelect, closeOnSelect = true, }) {
|
|
@@ -3,8 +3,9 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
|
3
3
|
// A dropdown menu — same popover surface used for ⋯ / sort / filter
|
|
4
4
|
// menus (measured from listboxes): radius 12, 4px padding, a layered
|
|
5
5
|
// soft shadow, over 32px items (radius 8, icon + label + optional shortcut /
|
|
6
|
-
// check). The content
|
|
7
|
-
//
|
|
6
|
+
// check). The content portals to document.body (#339) so no ancestor's
|
|
7
|
+
// transform/backdrop-filter can capture its position: fixed box; z-index 70
|
|
8
|
+
// keeps it above Dialog/Drawer's 60/61.
|
|
8
9
|
const config = defineRecipe({
|
|
9
10
|
name: "menu",
|
|
10
11
|
base: `
|
|
@@ -12,7 +13,7 @@ const config = defineRecipe({
|
|
|
12
13
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
13
14
|
|
|
14
15
|
& .crisp-menu-content {
|
|
15
|
-
position: fixed; z-index:
|
|
16
|
+
position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
|
|
16
17
|
min-width: 200px; max-height: 360px; overflow-y: auto; padding: ${c.space["1"]};
|
|
17
18
|
border-radius: ${c.radius.xl}; background: ${c.bgPopover};
|
|
18
19
|
box-shadow: ${e.floating};
|
|
@@ -1,7 +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 { formValueStyle, placeFloating, useFormReset } from "../../core/index.js";
|
|
4
|
+
import { FloatingPortal, formValueStyle, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useFormReset, } from "../../core/index.js";
|
|
5
5
|
import { useListboxKeyboard } from "../select/index.js";
|
|
6
6
|
import { multiselectRecipe } from "./multiselect.recipe.js";
|
|
7
7
|
const TONES = [
|
|
@@ -46,6 +46,9 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
46
46
|
const [query, setQuery] = React.useState("");
|
|
47
47
|
const [highlight, setHighlight] = React.useState(0);
|
|
48
48
|
const rootRef = React.useRef(null);
|
|
49
|
+
// The listbox portals to document.body (#339), so it's no longer a
|
|
50
|
+
// descendant of rootRef -- outside-click detection needs this ref too.
|
|
51
|
+
const listboxRef = React.useRef(null);
|
|
49
52
|
// `form.reset()` restores native controls; a button + listbox holds its value
|
|
50
53
|
// in React, so it has to listen (#245). Controlled multiselects are left
|
|
51
54
|
// alone.
|
|
@@ -119,12 +122,26 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
119
122
|
// Tab is already moving focus out; `close` would drag it back to the trigger
|
|
120
123
|
dismiss: () => setOpen(false),
|
|
121
124
|
});
|
|
125
|
+
// Registers the (now-portaled) listbox on the shared floating-layer stack
|
|
126
|
+
// (#339) so this doesn't get closed out from under itself when nested
|
|
127
|
+
// inside another floating layer -- see Select's matching comment.
|
|
128
|
+
React.useEffect(() => {
|
|
129
|
+
if (!open || !listboxRef.current)
|
|
130
|
+
return;
|
|
131
|
+
return registerFloatingLayer(listboxRef.current);
|
|
132
|
+
}, [open]);
|
|
122
133
|
React.useEffect(() => {
|
|
123
134
|
if (!open)
|
|
124
135
|
return;
|
|
125
136
|
const onDown = (e) => {
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
const target = e.target;
|
|
138
|
+
if (rootRef.current?.contains(target))
|
|
139
|
+
return;
|
|
140
|
+
if (listboxRef.current?.contains(target))
|
|
141
|
+
return;
|
|
142
|
+
if (isInsideLaterFloatingLayer(target, listboxRef.current))
|
|
143
|
+
return;
|
|
144
|
+
setOpen(false);
|
|
128
145
|
};
|
|
129
146
|
document.addEventListener("pointerdown", onDown);
|
|
130
147
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
@@ -137,12 +154,12 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
137
154
|
e.preventDefault();
|
|
138
155
|
openMenu();
|
|
139
156
|
}
|
|
140
|
-
}, children: [_jsx("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
157
|
+
}, children: [_jsx("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
158
|
+
setQuery(e.target.value);
|
|
159
|
+
setHighlight(0);
|
|
160
|
+
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
161
|
+
const sel = selectedSet.has(opt.value);
|
|
162
|
+
return (_jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [_jsx("span", { className: "crisp-multiselect-option-chip", children: _jsx(Chip, { opt: opt }) }), _jsx("span", { className: "crisp-multiselect-check", children: sel && _jsx(Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
|
|
163
|
+
}))] }) }))] }));
|
|
147
164
|
}
|
|
148
165
|
MultiSelect.displayName = "MultiSelect";
|
|
@@ -3,8 +3,9 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
|
3
3
|
// A multi-select / tag editor — measured from a cell tag picker: an
|
|
4
4
|
// outlined trigger showing the selected values as coloured chips, opening a
|
|
5
5
|
// popover with a search field over rows of "[coloured chip] … [checkbox]" that
|
|
6
|
-
// toggle on click. The popover
|
|
7
|
-
//
|
|
6
|
+
// toggle on click. The popover portals to document.body (#339) so no
|
|
7
|
+
// ancestor's transform/backdrop-filter can capture its position: fixed box;
|
|
8
|
+
// z-index 70 keeps it above Dialog/Drawer's 60/61.
|
|
8
9
|
const config = defineRecipe({
|
|
9
10
|
name: "multiselect",
|
|
10
11
|
base: `
|
|
@@ -27,7 +28,7 @@ const config = defineRecipe({
|
|
|
27
28
|
& .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
|
|
28
29
|
|
|
29
30
|
& .crisp-multiselect-listbox {
|
|
30
|
-
position: fixed; z-index:
|
|
31
|
+
position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
|
|
31
32
|
max-height: 300px; overflow-y: auto; padding: ${c.space["1"]};
|
|
32
33
|
border-radius: ${c.radius.xl}; background: ${c.bgPopover};
|
|
33
34
|
box-shadow: ${e.floating};
|