@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
|
@@ -1,6 +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
|
+
import { FloatingPortal, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, } from "../../core/index.js";
|
|
4
4
|
import { popoverRecipe } from "./popover.recipe.js";
|
|
5
5
|
/**
|
|
6
6
|
* A generic anchored popover — the Menu/Select surface holding arbitrary
|
|
@@ -19,6 +19,9 @@ export function Popover({ trigger, children, align = "start", open: openProp, de
|
|
|
19
19
|
const open = openProp ?? internalOpen;
|
|
20
20
|
const rootRef = React.useRef(null);
|
|
21
21
|
const triggerRef = React.useRef(null);
|
|
22
|
+
// The content portals to document.body (#339), so it's no longer a
|
|
23
|
+
// descendant of rootRef -- outside-click detection needs this ref too.
|
|
24
|
+
const contentRef = React.useRef(null);
|
|
22
25
|
const [pos, setPos] = React.useState({});
|
|
23
26
|
const [up, setUp] = React.useState(false);
|
|
24
27
|
const setOpen = React.useCallback((next) => {
|
|
@@ -46,12 +49,26 @@ export function Popover({ trigger, children, align = "start", open: openProp, de
|
|
|
46
49
|
if (open)
|
|
47
50
|
place();
|
|
48
51
|
}, [open]);
|
|
52
|
+
// Registers the (now-portaled) content on the shared floating-layer stack
|
|
53
|
+
// (#339) so this doesn't get closed out from under itself when it holds
|
|
54
|
+
// another floating layer -- see core/floating-stack.ts.
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
if (!open || !contentRef.current)
|
|
57
|
+
return;
|
|
58
|
+
return registerFloatingLayer(contentRef.current);
|
|
59
|
+
}, [open]);
|
|
49
60
|
React.useEffect(() => {
|
|
50
61
|
if (!open)
|
|
51
62
|
return;
|
|
52
63
|
const onDown = (e) => {
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
const target = e.target;
|
|
65
|
+
if (rootRef.current?.contains(target))
|
|
66
|
+
return;
|
|
67
|
+
if (contentRef.current?.contains(target))
|
|
68
|
+
return;
|
|
69
|
+
if (isInsideLaterFloatingLayer(target, contentRef.current))
|
|
70
|
+
return;
|
|
71
|
+
setOpen(false);
|
|
55
72
|
};
|
|
56
73
|
document.addEventListener("pointerdown", onDown);
|
|
57
74
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
@@ -69,11 +86,11 @@ export function Popover({ trigger, children, align = "start", open: openProp, de
|
|
|
69
86
|
setOpen(!open);
|
|
70
87
|
},
|
|
71
88
|
});
|
|
72
|
-
return (_jsxs("div", { ref: rootRef, className: popoverRecipe({ className }), children: [triggerEl, open && (_jsx("div", { className: "crisp-popover-content", role: "dialog", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: (e) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
return (_jsxs("div", { ref: rootRef, className: popoverRecipe({ className }), children: [triggerEl, open && (_jsx(FloatingPortal, { children: _jsx("div", { ref: contentRef, className: "crisp-popover-content", role: "dialog", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: (e) => {
|
|
90
|
+
if (e.key === "Escape") {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
setOpen(false);
|
|
93
|
+
}
|
|
94
|
+
}, children: children }) }))] }));
|
|
78
95
|
}
|
|
79
96
|
Popover.displayName = "Popover";
|
|
@@ -2,12 +2,14 @@ import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
|
2
2
|
import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
3
3
|
// A generic anchored popover — the same measured surface as Menu / Select
|
|
4
4
|
// (radius 12, 4px padding, layered soft shadow, inset border), but holding
|
|
5
|
-
// arbitrary content.
|
|
6
|
-
//
|
|
5
|
+
// arbitrary content. The content portals to document.body (#339) so no
|
|
6
|
+
// ancestor's transform/backdrop-filter can capture its position: fixed box;
|
|
7
|
+
// z-index 70 keeps it above Dialog/Drawer's 60/61. The display:contents root
|
|
8
|
+
// is what's left in place, so a consumer's own styling of it survives.
|
|
7
9
|
const config = defineRecipe({
|
|
8
10
|
name: "popover",
|
|
9
11
|
base: `display: contents; font-family: ${c.font.sans};
|
|
10
|
-
& .crisp-popover-content { position: fixed; z-index:
|
|
12
|
+
& .crisp-popover-content { position: fixed; z-index: 70; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: ${c.bgPopover}; color: ${c.fgPrimary}; box-shadow: ${e.floating}; opacity: 1; transform: none; transition: opacity 0.08s ease, transform 0.08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
11
13
|
& .crisp-popover-content[data-align="end"] { transform-origin: top right; }
|
|
12
14
|
& .crisp-popover-content[data-up] { transform-origin: bottom left; }`,
|
|
13
15
|
variants: {},
|
|
@@ -1,7 +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 { formValueStyle, placeFloating, useFormReset } from "../../core/index.js";
|
|
4
|
+
import { FloatingPortal, formValueStyle, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useFormReset, } from "../../core/index.js";
|
|
5
5
|
import { useListboxKeyboard } from "./listbox-keyboard.js";
|
|
6
6
|
import { selectRecipe } from "./select.recipe.js";
|
|
7
7
|
function optionText(o) {
|
|
@@ -33,6 +33,10 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
33
33
|
const [highlight, setHighlight] = React.useState(0);
|
|
34
34
|
const [query, setQuery] = React.useState("");
|
|
35
35
|
const rootRef = React.useRef(null);
|
|
36
|
+
// The listbox portals to document.body (#339), so it's no longer a
|
|
37
|
+
// descendant of rootRef -- outside-click detection needs this ref too, or
|
|
38
|
+
// every click inside the listbox reads as "outside" and closes it.
|
|
39
|
+
const listboxRef = React.useRef(null);
|
|
36
40
|
// `form.reset()` restores native controls; a button + listbox holds its value
|
|
37
41
|
// in React, so it has to listen (#245). Controlled selects are left alone.
|
|
38
42
|
useFormReset(rootRef, () => {
|
|
@@ -110,12 +114,29 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
110
114
|
if (!searchable)
|
|
111
115
|
onNavKeys(e);
|
|
112
116
|
};
|
|
117
|
+
// Registers the (now-portaled) listbox on the shared floating-layer stack
|
|
118
|
+
// (#339) so a Select nested inside another floating layer (a Menu's sort
|
|
119
|
+
// popover, e.g.) doesn't get closed out from under itself: once both
|
|
120
|
+
// portal independently to document.body they're DOM siblings, not
|
|
121
|
+
// ancestor/descendant, so the outer layer's own outside-click check can no
|
|
122
|
+
// longer see "inside me" by DOM containment alone.
|
|
123
|
+
React.useEffect(() => {
|
|
124
|
+
if (!open || !listboxRef.current)
|
|
125
|
+
return;
|
|
126
|
+
return registerFloatingLayer(listboxRef.current);
|
|
127
|
+
}, [open]);
|
|
113
128
|
React.useEffect(() => {
|
|
114
129
|
if (!open)
|
|
115
130
|
return;
|
|
116
131
|
const onDown = (e) => {
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
const target = e.target;
|
|
133
|
+
if (rootRef.current?.contains(target))
|
|
134
|
+
return;
|
|
135
|
+
if (listboxRef.current?.contains(target))
|
|
136
|
+
return;
|
|
137
|
+
if (isInsideLaterFloatingLayer(target, listboxRef.current))
|
|
138
|
+
return;
|
|
139
|
+
setOpen(false);
|
|
119
140
|
};
|
|
120
141
|
document.addEventListener("pointerdown", onDown);
|
|
121
142
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
@@ -123,12 +144,12 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
123
144
|
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [name && (selected || required) && (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
124
145
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
125
146
|
// validation, which is the only reason this input is not hidden
|
|
126
|
-
onChange: () => { } })), _jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "crisp-select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-select-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) => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
onChange: () => { } })), _jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "crisp-select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-select-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) => {
|
|
148
|
+
setQuery(e.target.value);
|
|
149
|
+
setHighlight(0);
|
|
150
|
+
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
151
|
+
const isSel = opt.value === selected;
|
|
152
|
+
return (_jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "crisp-select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
|
|
153
|
+
}))] }) }))] }));
|
|
133
154
|
}
|
|
134
155
|
Select.displayName = "Select";
|
|
@@ -4,8 +4,10 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
|
4
4
|
// (radius 9, value + a 16px chevron) over a listbox — radius 12, 4px padding, a
|
|
5
5
|
// layered soft shadow — of 32px options (radius 8, 6/8 padding) that show a
|
|
6
6
|
// check on the selected one and highlight on hover / keyboard. The listbox is
|
|
7
|
-
// portaled
|
|
8
|
-
//
|
|
7
|
+
// portaled to document.body (#339) so no ancestor's transform/backdrop-filter
|
|
8
|
+
// (Dialog's own measured surface sets both) can capture its position: fixed
|
|
9
|
+
// box; z-index 70 keeps it above Dialog/Drawer's z-index 60/61 layers, since
|
|
10
|
+
// it's no longer nested inside whichever one opened it.
|
|
9
11
|
const config = defineRecipe({
|
|
10
12
|
name: "select",
|
|
11
13
|
base: `
|
|
@@ -28,7 +30,7 @@ const config = defineRecipe({
|
|
|
28
30
|
& .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
|
|
29
31
|
|
|
30
32
|
& .crisp-select-listbox {
|
|
31
|
-
position: fixed; z-index:
|
|
33
|
+
position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
|
|
32
34
|
max-height: 280px; overflow-y: auto; padding: ${c.space["1"]};
|
|
33
35
|
border-radius: ${c.radius.xl}; background: ${c.bgPopover};
|
|
34
36
|
box-shadow: ${e.floating};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, CircleHelp, GripVertical, ListFilter, Plus, X, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { useFocusAfterRemoval } from "../../core/index.js";
|
|
4
|
+
import { isInsideLaterFloatingLayer, useFocusAfterRemoval } from "../../core/index.js";
|
|
5
|
+
import { MenuCheckboxItem } from "../menu/index.js";
|
|
5
6
|
import { Select } from "../select/index.js";
|
|
6
7
|
import { tabletoolbarRecipe } from "./tabletoolbar.recipe.js";
|
|
7
8
|
const OP_LABELS = {
|
|
8
9
|
contains: "contains",
|
|
9
10
|
is: "is",
|
|
10
11
|
isnot: "is not",
|
|
12
|
+
isanyof: "is any of",
|
|
11
13
|
empty: "is empty",
|
|
12
14
|
notempty: "is not empty",
|
|
13
15
|
};
|
|
@@ -18,7 +20,16 @@ const OP_ORDER = [
|
|
|
18
20
|
"empty",
|
|
19
21
|
"notempty",
|
|
20
22
|
];
|
|
23
|
+
// "isanyof" is appended only for a column that supplies `values` — there is
|
|
24
|
+
// nothing to check off otherwise.
|
|
25
|
+
const opsFor = (col) => col?.values?.length ? [...OP_ORDER, "isanyof"] : OP_ORDER;
|
|
21
26
|
const opNeedsValue = (op) => op === "contains" || op === "is" || op === "isnot";
|
|
27
|
+
const opNeedsValues = (op) => op === "isanyof";
|
|
28
|
+
// "Department is any of IT, Finance" past two values becomes "… +N": the
|
|
29
|
+
// chip is one line, and a field can have far more than two facet values.
|
|
30
|
+
const isAnyOfSummary = (values) => values.length <= 2
|
|
31
|
+
? values.join(", ")
|
|
32
|
+
: `${values.slice(0, 2).join(", ")} +${values.length - 2}`;
|
|
22
33
|
const isEmptyValue = (v) => v == null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
23
34
|
const asText = (v) => {
|
|
24
35
|
if (isEmptyValue(v))
|
|
@@ -44,7 +55,8 @@ const asText = (v) => {
|
|
|
44
55
|
* display string, so a grouped-thousands "$1,200" still matches "1200".
|
|
45
56
|
*/
|
|
46
57
|
export function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
47
|
-
const active = filters.filter((f) => !opNeedsValue(f.op) || f.value.trim() !== "")
|
|
58
|
+
const active = filters.filter((f) => (!opNeedsValue(f.op) || f.value.trim() !== "") &&
|
|
59
|
+
(!opNeedsValues(f.op) || (f.values?.length ?? 0) > 0));
|
|
48
60
|
if (!active.length)
|
|
49
61
|
return rows;
|
|
50
62
|
return rows.filter((row) => active.every((f) => {
|
|
@@ -54,6 +66,8 @@ export function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
|
54
66
|
if (f.op === "notempty")
|
|
55
67
|
return !isEmptyValue(raw);
|
|
56
68
|
const t = asText(raw).toLowerCase();
|
|
69
|
+
if (f.op === "isanyof")
|
|
70
|
+
return (f.values ?? []).some((v) => v.toLowerCase() === t);
|
|
57
71
|
const v = f.value.trim().toLowerCase();
|
|
58
72
|
if (f.op === "contains")
|
|
59
73
|
return t.includes(v);
|
|
@@ -115,6 +129,8 @@ export function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsC
|
|
|
115
129
|
const [openMenu, setOpenMenu] = React.useState(null);
|
|
116
130
|
const [dragSort, setDragSort] = React.useState(null);
|
|
117
131
|
const [filterQuery, setFilterQuery] = React.useState("");
|
|
132
|
+
// filters the "isanyof" checkbox list, same lifetime as filterQuery above
|
|
133
|
+
const [anyOfQuery, setAnyOfQuery] = React.useState("");
|
|
118
134
|
// close any open menu on outside click (anything not inside a menu or its
|
|
119
135
|
// trigger) or Escape
|
|
120
136
|
React.useEffect(() => {
|
|
@@ -122,8 +138,15 @@ export function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsC
|
|
|
122
138
|
return;
|
|
123
139
|
const onDown = (e) => {
|
|
124
140
|
const t = e.target;
|
|
125
|
-
if (
|
|
126
|
-
|
|
141
|
+
if (t.closest(".crisp-tabletoolbar-menu, [data-menu-trigger]"))
|
|
142
|
+
return;
|
|
143
|
+
// #339: a Select opened from inside this popover (the sort row's
|
|
144
|
+
// direction control) now portals to document.body, so it's no longer
|
|
145
|
+
// inside .crisp-tabletoolbar-menu by DOM containment -- check the
|
|
146
|
+
// shared floating-layer stack too.
|
|
147
|
+
if (isInsideLaterFloatingLayer(t, null))
|
|
148
|
+
return;
|
|
149
|
+
setOpenMenu(null);
|
|
127
150
|
};
|
|
128
151
|
const onKey = (e) => e.key === "Escape" && setOpenMenu(null);
|
|
129
152
|
document.addEventListener("mousedown", onDown);
|
|
@@ -213,9 +236,30 @@ export function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsC
|
|
|
213
236
|
.map((col) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => addFilter(col.key), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key)))] }))] }), filters.map((cond) => {
|
|
214
237
|
const col = columns.find((c) => c.key === cond.key);
|
|
215
238
|
const open = openMenu === `fcond:${cond.id}`;
|
|
216
|
-
const hasValue = !opNeedsValue(cond.op) || cond.value.trim()
|
|
217
|
-
|
|
239
|
+
const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
|
|
240
|
+
(!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
|
|
241
|
+
return (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("span", { className: "crisp-tabletoolbar-fchip", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [_jsx("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-tabletoolbar-fchip-op", children: OP_LABELS[cond.op] }), opNeedsValue(cond.op) && (_jsx("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && (_jsx("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : (_jsx("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "aria-label": `Remove ${col?.label} filter`, onClick: () => removeFilter(cond.id), children: _jsx(X, { size: 13, strokeWidth: 2 }) })] }), open && (_jsxs("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} condition`, children: [_jsxs("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " condition"] }), opsFor(col).map((op) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [OP_LABELS[op], cond.op === op ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && (_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
|
|
218
242
|
// biome-ignore lint/a11y/noAutofocus: focus the value field
|
|
219
|
-
autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) }))
|
|
243
|
+
autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })), cond.op === "isanyof" && col?.values && (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
|
|
244
|
+
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
|
245
|
+
autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
|
|
246
|
+
.filter((v) => v
|
|
247
|
+
.toLowerCase()
|
|
248
|
+
.includes(anyOfQuery.trim().toLowerCase()))
|
|
249
|
+
.map((v) => {
|
|
250
|
+
const checked = (cond.values ?? []).includes(v);
|
|
251
|
+
return (_jsx(MenuCheckboxItem, { checked: checked, onCheckedChange: (next) => {
|
|
252
|
+
const set = new Set(cond.values ?? []);
|
|
253
|
+
if (next)
|
|
254
|
+
set.add(v);
|
|
255
|
+
else
|
|
256
|
+
set.delete(v);
|
|
257
|
+
// the column's own order, not click order —
|
|
258
|
+
// what the chip summary reads out
|
|
259
|
+
patchFilter(cond.id, {
|
|
260
|
+
values: col.values?.filter((x) => set.has(x)),
|
|
261
|
+
});
|
|
262
|
+
}, children: v }, v));
|
|
263
|
+
})] }))] }))] }, cond.id));
|
|
220
264
|
}), _jsx("span", { className: "crisp-tabletoolbar-spacer" }), count != null && (_jsx("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
|
|
221
265
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createPortal } from "react-dom";
|
|
2
|
+
/**
|
|
3
|
+
* Portals a floating layer (a `position: fixed` listbox/menu/popover
|
|
4
|
+
* content) to `document.body` — exactly what Tooltip already did with a
|
|
5
|
+
* bare `createPortal` call, pulled out so `Select`, `MultiSelect`, `Menu`,
|
|
6
|
+
* `Popover` and `ContextMenu` share the one implementation (#339).
|
|
7
|
+
*
|
|
8
|
+
* Needed because a `position: fixed` box resolves against the nearest
|
|
9
|
+
* ancestor that establishes a containing block for it — not necessarily the
|
|
10
|
+
* viewport. `transform` (even the identity `scale(1)`), `filter` and
|
|
11
|
+
* `backdrop-filter` all qualify, and `Dialog`'s own measured surface sets
|
|
12
|
+
* both a resting `transform: scale(1)` (so its `@starting-style` entrance
|
|
13
|
+
* has something to animate from) and `backdrop-filter: blur(15px)` (Attio's
|
|
14
|
+
* frosted-glass modal). A floating layer opened from a trigger inside a
|
|
15
|
+
* `Dialog` was rendering offset by the dialog's own box instead of anchored
|
|
16
|
+
* to its trigger.
|
|
17
|
+
*
|
|
18
|
+
* Every caller renders this conditionally on its own `open` state, which
|
|
19
|
+
* starts `false` and is driven only by client interaction — never a prop a
|
|
20
|
+
* fixture sets true at mount (unlike Dialog/Drawer/Command) — so this never
|
|
21
|
+
* touches `document` during SSR, the same safety Tooltip already had.
|
|
22
|
+
*/
|
|
23
|
+
export function FloatingPortal({ children, }) {
|
|
24
|
+
return createPortal(children, document.body);
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks every currently-open `FloatingPortal` layer, in open order, so a
|
|
3
|
+
* layer's outside-click check can tell "outside me" from "inside a layer I
|
|
4
|
+
* spawned" (#339).
|
|
5
|
+
*
|
|
6
|
+
* Portaling `Select`/`MultiSelect`/`Menu`/`Popover`/`ContextMenu` to
|
|
7
|
+
* `document.body` fixed their own mis-positioning inside a `Dialog`, but it
|
|
8
|
+
* broke a composition each of them supports: one of these opened INSIDE
|
|
9
|
+
* another (TableToolbar's sort popover is a `Menu` holding a per-row
|
|
10
|
+
* `Select`). Before portaling, the inner layer was a DOM descendant of the
|
|
11
|
+
* outer one, so the outer's `rootRef.contains(target)` check saw a click
|
|
12
|
+
* inside the inner layer as "inside". Once both portal independently to
|
|
13
|
+
* `document.body`, they become DOM SIBLINGS — nested in the React tree, not
|
|
14
|
+
* the DOM tree — so that same check reads the inner layer's own pointerdown
|
|
15
|
+
* as "outside", and the outer layer closes itself, unmounting the inner one
|
|
16
|
+
* mid-interaction.
|
|
17
|
+
*
|
|
18
|
+
* The fix: each layer registers its content node here on open (removed on
|
|
19
|
+
* close/unmount), and a layer's outside-click check also accepts a target
|
|
20
|
+
* inside any layer registered AFTER it — i.e. any layer that could only have
|
|
21
|
+
* opened from inside it.
|
|
22
|
+
*/
|
|
23
|
+
const stack = [];
|
|
24
|
+
/** Call when a layer opens; call the returned cleanup when it closes. */
|
|
25
|
+
export function registerFloatingLayer(el) {
|
|
26
|
+
stack.push(el);
|
|
27
|
+
return () => {
|
|
28
|
+
const i = stack.indexOf(el);
|
|
29
|
+
if (i >= 0)
|
|
30
|
+
stack.splice(i, 1);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* True when `target` sits inside a layer that was registered strictly after
|
|
35
|
+
* `self` — a layer that could only have been spawned from inside `self`,
|
|
36
|
+
* directly or through further nesting.
|
|
37
|
+
*
|
|
38
|
+
* Pass `null` for `self` when the caller is a hand-rolled dropdown that
|
|
39
|
+
* doesn't register itself on this stack (RecordsTable's and TableToolbar's
|
|
40
|
+
* own popovers, e.g.) — every registered layer then counts, which is the
|
|
41
|
+
* right query for "was this click inside some floating child I opened".
|
|
42
|
+
*/
|
|
43
|
+
export function isInsideLaterFloatingLayer(target, self) {
|
|
44
|
+
const selfIndex = self ? stack.indexOf(self) : -1;
|
|
45
|
+
for (let i = selfIndex + 1; i < stack.length; i++) {
|
|
46
|
+
if (stack[i]?.contains(target))
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
package/dist/esm/core/index.js
CHANGED
|
@@ -3,6 +3,8 @@ export { cx } from "./cx.js";
|
|
|
3
3
|
export { defineRecipe } from "./define.js";
|
|
4
4
|
export { withDescribedBy } from "./dialog-description.js";
|
|
5
5
|
export { isRtlElement } from "./direction.js";
|
|
6
|
+
export { FloatingPortal } from "./floating-portal.js";
|
|
7
|
+
export { isInsideLaterFloatingLayer, registerFloatingLayer, } from "./floating-stack.js";
|
|
6
8
|
export { formValueStyle } from "./form-value.js";
|
|
7
9
|
export { placeFloating, } from "./place-floating.js";
|
|
8
10
|
export { Primitive } from "./primitive.js";
|
|
@@ -26,7 +26,8 @@ function FieldValue({ col, row, emptyLabel, }) {
|
|
|
26
26
|
* relative` container (e.g. the app main area); it slides in from the right and
|
|
27
27
|
* overlays the content.
|
|
28
28
|
*/
|
|
29
|
-
export function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", detailsLabel = "Details", emptyLabel = "Empty", ...rest }) {
|
|
29
|
+
export function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", title, detailsLabel = "Details", emptyLabel = "Empty", footer, ...rest }) {
|
|
30
|
+
const titleId = React.useId();
|
|
30
31
|
// close on Escape while open
|
|
31
32
|
React.useEffect(() => {
|
|
32
33
|
if (!record)
|
|
@@ -42,6 +43,7 @@ export function RecordPanel({ record, columns, onClose, className, closeLabel =
|
|
|
42
43
|
? record[entityCol.key]
|
|
43
44
|
: undefined;
|
|
44
45
|
const fields = columns.filter((c) => c.type !== "entity");
|
|
45
|
-
|
|
46
|
+
const heading = title ?? entity?.name ?? "Record";
|
|
47
|
+
return (_jsxs("div", { className: recordpanelRecipe({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [_jsxs("div", { className: "crisp-recordpanel-header", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpanel-fav", src: entity.avatar, alt: "" })) : entity ? (_jsx(Avatar, { size: "md", shape: "rounded", tone: "brand", children: entity.name.charAt(0) })) : null, _jsx("span", { id: titleId, className: "crisp-recordpanel-title", children: heading }), _jsx("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 18, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpanel-body", children: [_jsx("div", { className: "crisp-recordpanel-sectionlabel", children: detailsLabel }), fields.map((col) => (_jsxs("div", { className: "crisp-recordpanel-field", children: [_jsxs("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? _jsx(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), _jsx("span", { className: "crisp-recordpanel-field-value", children: _jsx(FieldValue, { col: col, row: record, emptyLabel: emptyLabel }) })] }, col.key)))] }), footer != null && (_jsx("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
|
|
46
48
|
}
|
|
47
49
|
RecordPanel.displayName = "RecordPanel";
|
|
@@ -36,6 +36,14 @@ const config = defineRecipe({
|
|
|
36
36
|
${recordTagCss}
|
|
37
37
|
& .crisp-recordpanel-link { color: ${c.bgBrandSolid}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
38
38
|
& .crisp-recordpanel-link:hover { text-decoration: underline; }
|
|
39
|
+
|
|
40
|
+
/* #329: an optional slot for record actions (e.g. Edit). Reuses the
|
|
41
|
+
header's own measured values rather than inventing new ones -- the
|
|
42
|
+
same horizontal inset (space.3), the same hairline border token
|
|
43
|
+
(borderSubtle, flipped to border-top since this sits at the opposite
|
|
44
|
+
edge), and the header/body gap (space.2) for its own vertical padding
|
|
45
|
+
and for spacing between whatever it's given. */
|
|
46
|
+
& .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: ${c.space["2"]}; padding: ${c.space["2"]} ${c.space["3"]}; border-top: 1px solid ${c.borderSubtle}; }
|
|
39
47
|
`,
|
|
40
48
|
variants: {},
|
|
41
49
|
defaultVariants: {},
|
|
@@ -14,7 +14,7 @@ import { Menu, MenuItem, useMenuKeyboard } from "../../components/menu/index.js"
|
|
|
14
14
|
import { MultiSelect } from "../../components/multiselect/index.js";
|
|
15
15
|
import { Select } from "../../components/select/index.js";
|
|
16
16
|
import { Switch } from "../../components/switch/index.js";
|
|
17
|
-
import { isRtlElement, usePrefersReducedMotion } from "../../core/index.js";
|
|
17
|
+
import { isInsideLaterFloatingLayer, isRtlElement, usePrefersReducedMotion, } from "../../core/index.js";
|
|
18
18
|
import { companyLogos } from "./company-logos.js";
|
|
19
19
|
import { recordstableRecipe } from "./recordstable.recipe.js";
|
|
20
20
|
const EDITABLE = new Set(["text", "number", "url", "currency"]);
|
|
@@ -460,8 +460,15 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
|
|
|
460
460
|
return;
|
|
461
461
|
const onDown = (e) => {
|
|
462
462
|
const t = e.target;
|
|
463
|
-
if (
|
|
464
|
-
|
|
463
|
+
if (t.closest(".crisp-recordstable-menu, [data-menu-trigger]"))
|
|
464
|
+
return;
|
|
465
|
+
// #339: a Select/MultiSelect/Menu opened from inside this popover now
|
|
466
|
+
// portals to document.body, so it's no longer inside
|
|
467
|
+
// .crisp-recordstable-menu by DOM containment -- check the shared
|
|
468
|
+
// floating-layer stack too.
|
|
469
|
+
if (isInsideLaterFloatingLayer(t, null))
|
|
470
|
+
return;
|
|
471
|
+
setOpenMenu(null);
|
|
465
472
|
};
|
|
466
473
|
const onKey = (e) => e.key === "Escape" && setOpenMenu(null);
|
|
467
474
|
document.addEventListener("mousedown", onDown);
|
|
@@ -476,8 +483,12 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
|
|
|
476
483
|
if (!ctxMenu)
|
|
477
484
|
return;
|
|
478
485
|
const onDown = (e) => {
|
|
479
|
-
|
|
480
|
-
|
|
486
|
+
const t = e.target;
|
|
487
|
+
if (t.closest(".crisp-recordstable-ctx"))
|
|
488
|
+
return;
|
|
489
|
+
if (isInsideLaterFloatingLayer(t, null))
|
|
490
|
+
return;
|
|
491
|
+
setCtxMenu(null);
|
|
481
492
|
};
|
|
482
493
|
const onKey = (e) => e.key === "Escape" && setCtxMenu(null);
|
|
483
494
|
document.addEventListener("mousedown", onDown);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Building2, ChartNoAxesColumn, ChevronDown, CircleDollarSign, FileText, House, Inbox, LayoutGrid, Mail, MessageSquare, PanelLeft, Phone, Search, Send, SquareCheck, User, UserPlus, Users, Waypoints, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Avatar } from "../../components/avatar/index.js";
|
|
@@ -81,8 +81,14 @@ export const ATTIO_SIDEBAR_FOOTER = {
|
|
|
81
81
|
function SectionHeader({ label, open, onToggle, }) {
|
|
82
82
|
return (_jsxs("button", { type: "button", className: "crisp-sidebar-section-header", "data-collapsed": open ? undefined : "", "aria-expanded": open, onClick: onToggle, children: [_jsx(IconChevronMini, {}), _jsx("span", { children: label })] }));
|
|
83
83
|
}
|
|
84
|
-
export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = ATTIO_SIDEBAR_WORKSPACE, items = ATTIO_SIDEBAR_ITEMS, recordsSection = ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = ATTIO_SIDEBAR_CHATS_SECTION, footer = ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
|
|
85
|
-
|
|
84
|
+
export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = ATTIO_SIDEBAR_WORKSPACE, items = ATTIO_SIDEBAR_ITEMS, recordsSection = ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = ATTIO_SIDEBAR_CHATS_SECTION, groups, footer = ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
|
|
85
|
+
// Seeded once, on mount, from whatever groups start collapsed — not
|
|
86
|
+
// reactive to `groups` changing later, matching every other piece of
|
|
87
|
+
// this component's state (recordsSection/listsSection/chatsSection have
|
|
88
|
+
// never supported a starting-collapsed prop at all).
|
|
89
|
+
const [collapsedSections, setCollapsedSections] = React.useState(() => new Set(groups
|
|
90
|
+
?.filter((g) => g.defaultCollapsed)
|
|
91
|
+
.map((g) => g.key ?? g.label)));
|
|
86
92
|
const [commandOpen, setCommandOpen] = React.useState(false);
|
|
87
93
|
React.useEffect(() => {
|
|
88
94
|
function handleKeyDown(event) {
|
|
@@ -107,6 +113,13 @@ export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse,
|
|
|
107
113
|
return next;
|
|
108
114
|
});
|
|
109
115
|
}, []);
|
|
110
|
-
return (_jsxs(Primitive.div, { ref: ref, asChild: asChild, className: sidebarRecipe({ className }), ...props, children: [_jsxs("div", { className: "crisp-sidebar-ws", children: [_jsx(Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), _jsx("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), _jsx(IconChevronDown, {}), _jsx(Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: _jsx(IconCollapsePanel, {}) })] }), _jsxs("div", { className: "crisp-sidebar-searchrow", children: [_jsxs("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [_jsx(IconSearch, {}), _jsx("span", { className: "crisp-sidebar-search-label", children: searchLabel }), _jsx("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), _jsx(Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: _jsx(IconInbox, {}) })] }), _jsx("div", { className: "crisp-sidebar-divider" }), _jsx("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => (_jsx(NavItem, { href: "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, children: label }, label))) }),
|
|
116
|
+
return (_jsxs(Primitive.div, { ref: ref, asChild: asChild, className: sidebarRecipe({ className }), ...props, children: [_jsxs("div", { className: "crisp-sidebar-ws", children: [_jsx(Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), _jsx("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), _jsx(IconChevronDown, {}), _jsx(Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: _jsx(IconCollapsePanel, {}) })] }), _jsxs("div", { className: "crisp-sidebar-searchrow", children: [_jsxs("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [_jsx(IconSearch, {}), _jsx("span", { className: "crisp-sidebar-search-label", children: searchLabel }), _jsx("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), _jsx(Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: _jsx(IconInbox, {}) })] }), _jsx("div", { className: "crisp-sidebar-divider" }), _jsx("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => (_jsx(NavItem, { href: "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, children: label }, label))) }), groups ? (
|
|
117
|
+
// #325: navigation as data, reusing the exact section markup/
|
|
118
|
+
// recipe above — SectionHeader + .crisp-sidebar-section — instead
|
|
119
|
+
// of the three fixed Attio sections.
|
|
120
|
+
groups.map((group) => {
|
|
121
|
+
const groupKey = group.key ?? group.label;
|
|
122
|
+
return (_jsxs(React.Fragment, { children: [_jsx(SectionHeader, { label: group.label, open: !collapsedSections.has(groupKey), onToggle: () => toggleSection(groupKey) }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has(groupKey) ? "" : undefined, children: group.items.map((item) => (_jsx(NavItem, { href: item.href ?? "#", active: item.active, icon: item.icon ? _jsx(item.icon, {}) : undefined, trailing: item.count, onClick: item.onSelect, children: item.label }, item.href ?? item.label))) })] }, groupKey));
|
|
123
|
+
})) : (_jsxs(_Fragment, { children: [_jsx(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => (_jsx(NavItem, { href: "#", icon: _jsx(Avatar, { size: "sm", shape: "rounded", tone: tone, children: _jsx(Glyph, {}) }), children: label }, label))) }), _jsx(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: _jsx("button", { type: "button", className: "crisp-sidebar-newlist", children: listsSection.newListLabel }) }), _jsx(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon }) => (_jsx(NavItem, { href: "#", icon: _jsx(ItemIcon, {}), children: label }, label))) })] })), _jsxs("div", { className: "crisp-sidebar-footer", children: [_jsx(Button, { intent: "neutral", size: "sm", className: "crisp-sidebar-pill", children: footer.gettingStartedLabel }), _jsx(NavItem, { href: "#", icon: _jsx(IconInvite, {}), children: footer.inviteLabel }), _jsx("div", { className: "crisp-sidebar-divider" }), _jsxs("div", { className: "crisp-sidebar-trial", children: [_jsx("span", { children: footer.trialText }), _jsx(Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] }), _jsx(Command, { open: commandOpen, onOpenChange: setCommandOpen })] }));
|
|
111
124
|
});
|
|
112
125
|
Sidebar.displayName = "Sidebar";
|
package/dist/menu.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.crisp-menu {
|
|
2
2
|
position: relative; display: inline-flex; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
3
|
-
& .crisp-menu-content { position: fixed; z-index:
|
|
3
|
+
& .crisp-menu-content { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); min-width: 200px; max-height: 360px; overflow-y: auto; 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); transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; }
|
|
4
4
|
& .crisp-menu-content[data-align="end"] { transform-origin: top right; }
|
|
5
5
|
& .crisp-menu-content[data-up] { transform-origin: bottom left; }
|
|
6
6
|
& .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
|
package/dist/menu.layered.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@layer crisp-components {
|
|
2
2
|
.crisp-menu {
|
|
3
3
|
position: relative; display: inline-flex; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
4
|
-
& .crisp-menu-content { position: fixed; z-index:
|
|
4
|
+
& .crisp-menu-content { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); min-width: 200px; max-height: 360px; overflow-y: auto; 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); transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; }
|
|
5
5
|
& .crisp-menu-content[data-align="end"] { transform-origin: top right; }
|
|
6
6
|
& .crisp-menu-content[data-up] { transform-origin: bottom left; }
|
|
7
7
|
& .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
|
package/dist/multiselect.css
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
& .crisp-multiselect-placeholder { color: var(--crisp-fg-muted); }
|
|
9
9
|
& .crisp-multiselect-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
|
|
10
10
|
& .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
|
|
11
|
-
& .crisp-multiselect-listbox { position: fixed; z-index:
|
|
11
|
+
& .crisp-multiselect-listbox { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 300px; overflow-y: auto; 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); transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; }
|
|
12
12
|
& .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
|
|
13
13
|
& .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
14
14
|
& .crisp-multiselect-search { display: flex; align-items: center; gap: var(--crisp-space-2); height: 34px; flex-shrink: 0; margin: calc(-1 * var(--crisp-space-1)) calc(-1 * var(--crisp-space-1)) var(--crisp-space-1); padding: 0 var(--crisp-space-2-5); border-bottom: 1px solid var(--crisp-border-subtle); color: var(--crisp-fg-muted); }
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
& .crisp-multiselect-placeholder { color: var(--crisp-fg-muted); }
|
|
10
10
|
& .crisp-multiselect-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
|
|
11
11
|
& .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
|
|
12
|
-
& .crisp-multiselect-listbox { position: fixed; z-index:
|
|
12
|
+
& .crisp-multiselect-listbox { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 300px; overflow-y: auto; 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); transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; }
|
|
13
13
|
& .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
|
|
14
14
|
& .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
15
15
|
& .crisp-multiselect-search { display: flex; align-items: center; gap: var(--crisp-space-2); height: 34px; flex-shrink: 0; margin: calc(-1 * var(--crisp-space-1)) calc(-1 * var(--crisp-space-1)) var(--crisp-space-1); padding: 0 var(--crisp-space-2-5); border-bottom: 1px solid var(--crisp-border-subtle); color: var(--crisp-fg-muted); }
|
package/dist/popover.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.crisp-popover {
|
|
2
2
|
display: contents; font-family: var(--crisp-font-sans);
|
|
3
|
-
& .crisp-popover-content { position: fixed; z-index:
|
|
3
|
+
& .crisp-popover-content { position: fixed; z-index: 70; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); color: var(--crisp-fg-primary); 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 0.08s ease, transform 0.08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
4
4
|
& .crisp-popover-content[data-align="end"] { transform-origin: top right; }
|
|
5
5
|
& .crisp-popover-content[data-up] { transform-origin: bottom left; }
|
|
6
6
|
}
|
package/dist/popover.layered.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@layer crisp-components {
|
|
2
2
|
.crisp-popover {
|
|
3
3
|
display: contents; font-family: var(--crisp-font-sans);
|
|
4
|
-
& .crisp-popover-content { position: fixed; z-index:
|
|
4
|
+
& .crisp-popover-content { position: fixed; z-index: 70; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); color: var(--crisp-fg-primary); 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 0.08s ease, transform 0.08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
5
5
|
& .crisp-popover-content[data-align="end"] { transform-origin: top right; }
|
|
6
6
|
& .crisp-popover-content[data-up] { transform-origin: bottom left; }
|
|
7
7
|
}
|