@crisp-ui-kit/crisp 0.36.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.
Files changed (53) hide show
  1. package/dist/cjs/components/contextmenu/contextmenu.js +25 -8
  2. package/dist/cjs/components/contextmenu/contextmenu.recipe.js +5 -2
  3. package/dist/cjs/components/dialog/dialog.js +1 -1
  4. package/dist/cjs/components/dialog/dialog.recipe.js +1 -1
  5. package/dist/cjs/components/menu/menu.js +20 -3
  6. package/dist/cjs/components/menu/menu.recipe.js +4 -3
  7. package/dist/cjs/components/multiselect/multiselect.js +26 -9
  8. package/dist/cjs/components/multiselect/multiselect.recipe.js +4 -3
  9. package/dist/cjs/components/popover/popover.js +25 -8
  10. package/dist/cjs/components/popover/popover.recipe.js +5 -3
  11. package/dist/cjs/components/select/select.js +30 -9
  12. package/dist/cjs/components/select/select.recipe.js +5 -3
  13. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +50 -6
  14. package/dist/cjs/core/floating-portal.js +28 -0
  15. package/dist/cjs/core/floating-stack.js +54 -0
  16. package/dist/cjs/core/index.js +6 -1
  17. package/dist/cjs/product/recordstable/recordstable.js +15 -4
  18. package/dist/contextmenu.css +1 -1
  19. package/dist/contextmenu.layered.css +1 -1
  20. package/dist/dialog.css +1 -1
  21. package/dist/dialog.layered.css +1 -1
  22. package/dist/esm/components/contextmenu/contextmenu.js +25 -8
  23. package/dist/esm/components/contextmenu/contextmenu.recipe.js +5 -2
  24. package/dist/esm/components/dialog/dialog.js +1 -1
  25. package/dist/esm/components/dialog/dialog.recipe.js +1 -1
  26. package/dist/esm/components/menu/menu.js +21 -4
  27. package/dist/esm/components/menu/menu.recipe.js +4 -3
  28. package/dist/esm/components/multiselect/multiselect.js +27 -10
  29. package/dist/esm/components/multiselect/multiselect.recipe.js +4 -3
  30. package/dist/esm/components/popover/popover.js +26 -9
  31. package/dist/esm/components/popover/popover.recipe.js +5 -3
  32. package/dist/esm/components/select/select.js +31 -10
  33. package/dist/esm/components/select/select.recipe.js +5 -3
  34. package/dist/esm/components/tabletoolbar/tabletoolbar.js +51 -7
  35. package/dist/esm/core/floating-portal.js +25 -0
  36. package/dist/esm/core/floating-stack.js +50 -0
  37. package/dist/esm/core/index.js +2 -0
  38. package/dist/esm/product/recordstable/recordstable.js +16 -5
  39. package/dist/menu.css +1 -1
  40. package/dist/menu.layered.css +1 -1
  41. package/dist/multiselect.css +1 -1
  42. package/dist/multiselect.layered.css +1 -1
  43. package/dist/popover.css +1 -1
  44. package/dist/popover.layered.css +1 -1
  45. package/dist/select.css +1 -1
  46. package/dist/select.layered.css +1 -1
  47. package/dist/styles.css +6 -6
  48. package/dist/styles.layered.css +6 -6
  49. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +10 -1
  50. package/dist/types/core/floating-portal.d.ts +25 -0
  51. package/dist/types/core/floating-stack.d.ts +13 -0
  52. package/dist/types/core/index.d.ts +2 -0
  53. package/package.json +1 -1
@@ -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 into this root (which is position:relative) so it escapes overflow
8
- // while keeping these nested styles.
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: 60; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
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 (!t.closest(".crisp-tabletoolbar-menu, [data-menu-trigger]"))
126
- setOpenMenu(null);
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
- 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 }))] })) : (_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"] }), OP_ORDER.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`,
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 }) }))] }))] }, cond.id));
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
+ }
@@ -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";
@@ -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 (!t.closest(".crisp-recordstable-menu, [data-menu-trigger]"))
464
- setOpenMenu(null);
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
- if (!e.target.closest(".crisp-recordstable-ctx"))
480
- setCtxMenu(null);
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);
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: 60; 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; }
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; }
@@ -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: 60; 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
+ & .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; }
@@ -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: 60; 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; }
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: 60; 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
+ & .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: 60; 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); } }
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
  }
@@ -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: 60; 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
+ & .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
  }
package/dist/select.css CHANGED
@@ -8,7 +8,7 @@
8
8
  & .crisp-select-placeholder { color: var(--crisp-fg-muted); }
9
9
  & .crisp-select-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
10
10
  & .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
11
- & .crisp-select-listbox { position: fixed; z-index: 60; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 280px; 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; }
11
+ & .crisp-select-listbox { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 280px; 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-select-listbox[data-up] { transform-origin: bottom; }
13
13
  & .crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
14
14
  & .crisp-select-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-select-placeholder { color: var(--crisp-fg-muted); }
10
10
  & .crisp-select-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
11
11
  & .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
12
- & .crisp-select-listbox { position: fixed; z-index: 60; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 280px; 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
+ & .crisp-select-listbox { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 280px; 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-select-listbox[data-up] { transform-origin: bottom; }
14
14
  & .crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
15
15
  & .crisp-select-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/styles.css CHANGED
@@ -450,7 +450,7 @@
450
450
 
451
451
  .crisp-contextmenu {
452
452
  display: contents; font-family: var(--crisp-font-sans);
453
- & .crisp-contextmenu-content { position: fixed; z-index: 60; 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); } }
453
+ & .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); } }
454
454
  & .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; }
455
455
  & .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
456
456
  & .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
@@ -710,7 +710,7 @@
710
710
  & .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; }
711
711
  & .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); }
712
712
  & .crisp-dialog-header svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
713
- & .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); }
713
+ & .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; }
714
714
  & .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; }
715
715
  & .crisp-dialog-close:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
716
716
  & .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); }
@@ -973,7 +973,7 @@
973
973
 
974
974
  .crisp-menu {
975
975
  position: relative; display: inline-flex; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
976
- & .crisp-menu-content { position: fixed; z-index: 60; 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; }
976
+ & .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; }
977
977
  & .crisp-menu-content[data-align="end"] { transform-origin: top right; }
978
978
  & .crisp-menu-content[data-up] { transform-origin: bottom left; }
979
979
  & .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
@@ -1012,7 +1012,7 @@
1012
1012
  & .crisp-multiselect-placeholder { color: var(--crisp-fg-muted); }
1013
1013
  & .crisp-multiselect-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
1014
1014
  & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
1015
- & .crisp-multiselect-listbox { position: fixed; z-index: 60; 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; }
1015
+ & .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; }
1016
1016
  & .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
1017
1017
  & .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
1018
1018
  & .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); }
@@ -1045,7 +1045,7 @@
1045
1045
 
1046
1046
  .crisp-popover {
1047
1047
  display: contents; font-family: var(--crisp-font-sans);
1048
- & .crisp-popover-content { position: fixed; z-index: 60; 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); } }
1048
+ & .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); } }
1049
1049
  & .crisp-popover-content[data-align="end"] { transform-origin: top right; }
1050
1050
  & .crisp-popover-content[data-up] { transform-origin: bottom left; }
1051
1051
  }
@@ -1217,7 +1217,7 @@
1217
1217
  & .crisp-select-placeholder { color: var(--crisp-fg-muted); }
1218
1218
  & .crisp-select-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
1219
1219
  & .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
1220
- & .crisp-select-listbox { position: fixed; z-index: 60; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 280px; 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; }
1220
+ & .crisp-select-listbox { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 280px; 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; }
1221
1221
  & .crisp-select-listbox[data-up] { transform-origin: bottom; }
1222
1222
  & .crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
1223
1223
  & .crisp-select-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); }
@@ -451,7 +451,7 @@
451
451
 
452
452
  .crisp-contextmenu {
453
453
  display: contents; font-family: var(--crisp-font-sans);
454
- & .crisp-contextmenu-content { position: fixed; z-index: 60; 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); } }
454
+ & .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); } }
455
455
  & .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; }
456
456
  & .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
457
457
  & .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
@@ -711,7 +711,7 @@
711
711
  & .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; }
712
712
  & .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); }
713
713
  & .crisp-dialog-header svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
714
- & .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); }
714
+ & .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; }
715
715
  & .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; }
716
716
  & .crisp-dialog-close:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
717
717
  & .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); }
@@ -974,7 +974,7 @@
974
974
 
975
975
  .crisp-menu {
976
976
  position: relative; display: inline-flex; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
977
- & .crisp-menu-content { position: fixed; z-index: 60; 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; }
977
+ & .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; }
978
978
  & .crisp-menu-content[data-align="end"] { transform-origin: top right; }
979
979
  & .crisp-menu-content[data-up] { transform-origin: bottom left; }
980
980
  & .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
@@ -1013,7 +1013,7 @@
1013
1013
  & .crisp-multiselect-placeholder { color: var(--crisp-fg-muted); }
1014
1014
  & .crisp-multiselect-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
1015
1015
  & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
1016
- & .crisp-multiselect-listbox { position: fixed; z-index: 60; 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; }
1016
+ & .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; }
1017
1017
  & .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
1018
1018
  & .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
1019
1019
  & .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); }
@@ -1046,7 +1046,7 @@
1046
1046
 
1047
1047
  .crisp-popover {
1048
1048
  display: contents; font-family: var(--crisp-font-sans);
1049
- & .crisp-popover-content { position: fixed; z-index: 60; 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); } }
1049
+ & .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); } }
1050
1050
  & .crisp-popover-content[data-align="end"] { transform-origin: top right; }
1051
1051
  & .crisp-popover-content[data-up] { transform-origin: bottom left; }
1052
1052
  }
@@ -1218,7 +1218,7 @@
1218
1218
  & .crisp-select-placeholder { color: var(--crisp-fg-muted); }
1219
1219
  & .crisp-select-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
1220
1220
  & .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
1221
- & .crisp-select-listbox { position: fixed; z-index: 60; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 280px; 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; }
1221
+ & .crisp-select-listbox { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 280px; 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; }
1222
1222
  & .crisp-select-listbox[data-up] { transform-origin: bottom; }
1223
1223
  & .crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
1224
1224
  & .crisp-select-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); }
@@ -8,18 +8,27 @@ export type ToolbarSort = {
8
8
  key: string;
9
9
  dir: SortDir;
10
10
  };
11
- export type ToolbarFilterOp = "contains" | "is" | "isnot" | "empty" | "notempty";
11
+ export type ToolbarFilterOp = "contains" | "is" | "isnot" | "isanyof" | "empty" | "notempty";
12
12
  export type ToolbarFilter = {
13
13
  id: string;
14
14
  key: string;
15
15
  op: ToolbarFilterOp;
16
16
  value: string;
17
+ /** Selected values for `"isanyof"` — ignored by every other op. */
18
+ values?: string[];
17
19
  };
18
20
  export type TableToolbarColumn = {
19
21
  key: string;
20
22
  label: string;
21
23
  /** Leading field-type icon, shown in the field pickers. */
22
24
  icon?: IconType;
25
+ /**
26
+ * This column's distinct values (a status/select field's option set, e.g.).
27
+ * Supplying it adds `"isanyof"` — a searchable checkbox list, Attio's
28
+ * faceted multi-select filter — to the column's condition picker. Omit it
29
+ * for a free-text column; there is nothing to check off.
30
+ */
31
+ values?: string[];
23
32
  };
24
33
  export type TableToolbarProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"> & {
25
34
  /** The fields that can be sorted and filtered on. */
@@ -0,0 +1,25 @@
1
+ import type * as React from "react";
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 declare function FloatingPortal({ children, }: {
24
+ children: React.ReactNode;
25
+ }): React.ReactPortal;
@@ -0,0 +1,13 @@
1
+ /** Call when a layer opens; call the returned cleanup when it closes. */
2
+ export declare function registerFloatingLayer(el: HTMLElement): () => void;
3
+ /**
4
+ * True when `target` sits inside a layer that was registered strictly after
5
+ * `self` — a layer that could only have been spawned from inside `self`,
6
+ * directly or through further nesting.
7
+ *
8
+ * Pass `null` for `self` when the caller is a hand-rolled dropdown that
9
+ * doesn't register itself on this stack (RecordsTable's and TableToolbar's
10
+ * own popovers, e.g.) — every registered layer then counts, which is the
11
+ * right query for "was this click inside some floating child I opened".
12
+ */
13
+ export declare function isInsideLaterFloatingLayer(target: Node, self: HTMLElement | null): boolean;
@@ -3,6 +3,8 @@ export { cx } from "./cx";
3
3
  export { defineRecipe } from "./define";
4
4
  export { withDescribedBy } from "./dialog-description";
5
5
  export { isRtlElement } from "./direction";
6
+ export { FloatingPortal } from "./floating-portal";
7
+ export { isInsideLaterFloatingLayer, registerFloatingLayer, } from "./floating-stack";
6
8
  export { formValueStyle } from "./form-value";
7
9
  export { type FloatingAlign, type PlaceFloatingOptions, placeFloating, } from "./place-floating";
8
10
  export { Primitive } from "./primitive";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",