@crisp-ui-kit/crisp 0.38.0 → 0.39.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 (36) hide show
  1. package/dist/cjs/components/contextmenu/contextmenu.js +1 -1
  2. package/dist/cjs/components/contextmenu/contextmenu.recipe.js +22 -10
  3. package/dist/cjs/components/menu/menu.js +1 -1
  4. package/dist/cjs/components/menu/menu.recipe.js +32 -20
  5. package/dist/cjs/components/multiselect/multiselect.js +1 -1
  6. package/dist/cjs/components/multiselect/multiselect.recipe.js +37 -22
  7. package/dist/cjs/components/popover/popover.js +1 -1
  8. package/dist/cjs/components/popover/popover.recipe.js +19 -4
  9. package/dist/cjs/components/select/select.js +1 -1
  10. package/dist/cjs/components/select/select.recipe.js +28 -15
  11. package/dist/cjs/core/floating-portal.js +55 -2
  12. package/dist/contextmenu.css +11 -9
  13. package/dist/contextmenu.layered.css +11 -9
  14. package/dist/esm/components/contextmenu/contextmenu.js +1 -1
  15. package/dist/esm/components/contextmenu/contextmenu.recipe.js +22 -10
  16. package/dist/esm/components/menu/menu.js +1 -1
  17. package/dist/esm/components/menu/menu.recipe.js +32 -20
  18. package/dist/esm/components/multiselect/multiselect.js +1 -1
  19. package/dist/esm/components/multiselect/multiselect.recipe.js +37 -22
  20. package/dist/esm/components/popover/popover.js +1 -1
  21. package/dist/esm/components/popover/popover.recipe.js +19 -4
  22. package/dist/esm/components/select/select.js +1 -1
  23. package/dist/esm/components/select/select.recipe.js +28 -15
  24. package/dist/esm/core/floating-portal.js +22 -2
  25. package/dist/menu.css +34 -19
  26. package/dist/menu.layered.css +34 -19
  27. package/dist/multiselect.css +34 -21
  28. package/dist/multiselect.layered.css +34 -21
  29. package/dist/popover.css +5 -3
  30. package/dist/popover.layered.css +5 -3
  31. package/dist/select.css +27 -14
  32. package/dist/select.layered.css +27 -14
  33. package/dist/styles.css +111 -66
  34. package/dist/styles.layered.css +111 -66
  35. package/dist/types/core/floating-portal.d.ts +17 -3
  36. package/package.json +3 -3
@@ -6,43 +6,55 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
6
6
  // check). The content portals to document.body (#339) so no ancestor's
7
7
  // transform/backdrop-filter can capture its position: fixed box; z-index 70
8
8
  // keeps it above Dialog/Drawer's 60/61.
9
+ //
10
+ // The menu's floating content and everything inside it live in `globalCss`
11
+ // below, not nested under `&` (#357): nesting compiles to a real descendant
12
+ // combinator scoped to the recipe's root class, which needs actual DOM
13
+ // containment — exactly what the portal to `document.body` breaks. Nested,
14
+ // these rules matched nothing once the content moved and the menu rendered
15
+ // completely unstyled. `font-family`/smoothing are restated on the content's
16
+ // own rule (items then pick font up via their own `font-family: inherit`,
17
+ // which still works — that inheritance is intact WITHIN the portaled
18
+ // subtree, only the link back to the trigger-holding root broke).
9
19
  const config = defineRecipe({
10
20
  name: "menu",
11
21
  base: `
12
22
  position: relative; display: inline-flex; font-family: ${c.font.sans};
13
23
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
14
-
15
- & .crisp-menu-content {
24
+ `,
25
+ globalCss: `
26
+ .crisp-menu-content {
27
+ font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
16
28
  position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
17
29
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: ${c.space["1"]};
18
30
  border-radius: ${c.radius.xl}; background: ${c.bgPopover};
19
31
  box-shadow: ${e.floating};
20
32
  transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
21
33
  }
22
- & .crisp-menu-content[data-align="end"] { transform-origin: top right; }
23
- & .crisp-menu-content[data-up] { transform-origin: bottom left; }
24
- & .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
25
- & .crisp-menu-content { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
34
+ .crisp-menu-content[data-align="end"] { transform-origin: top right; }
35
+ .crisp-menu-content[data-up] { transform-origin: bottom left; }
36
+ .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
37
+ .crisp-menu-content { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
26
38
 
27
- & .crisp-menu-item {
39
+ .crisp-menu-item {
28
40
  box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["1.5"]}; width: 100%; height: ${c.size.menuRow}; flex-shrink: 0;
29
41
  margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; border-radius: ${c.radius.md};
30
42
  font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: start; cursor: pointer; white-space: nowrap;
31
43
  }
32
- & .crisp-menu-item svg { flex-shrink: 0; color: ${c.fgMuted}; }
33
- & .crisp-menu-item:focus { outline: none; }
34
- & .crisp-menu-item:hover:not([data-disabled]), & .crisp-menu-item:focus-visible:not([data-disabled]), & .crisp-menu-item[data-highlighted]:not([data-disabled]) { background: ${c.bgRaised}; }
35
- & .crisp-menu-item[data-disabled] { opacity: 0.5; cursor: not-allowed; }
36
- & .crisp-menu-item--danger { color: ${c.bgDanger}; }
37
- & .crisp-menu-item--danger svg { color: ${c.bgDanger}; }
38
- & .crisp-menu-item--danger:hover, & .crisp-menu-item--danger:focus-visible, & .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, ${c.bgDanger} 10%, transparent); }
39
- & .crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
40
- & .crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: ${c.space["3"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; letter-spacing: 0.02em; }
41
- & .crisp-menu-item-check { flex-shrink: 0; color: ${c.bgBrandSolid}; }
42
- & .crisp-menu-item-check--placeholder { width: 15px; }
44
+ .crisp-menu-item svg { flex-shrink: 0; color: ${c.fgMuted}; }
45
+ .crisp-menu-item:focus { outline: none; }
46
+ .crisp-menu-item:hover:not([data-disabled]), .crisp-menu-item:focus-visible:not([data-disabled]), .crisp-menu-item[data-highlighted]:not([data-disabled]) { background: ${c.bgRaised}; }
47
+ .crisp-menu-item[data-disabled] { opacity: 0.5; cursor: not-allowed; }
48
+ .crisp-menu-item--danger { color: ${c.bgDanger}; }
49
+ .crisp-menu-item--danger svg { color: ${c.bgDanger}; }
50
+ .crisp-menu-item--danger:hover, .crisp-menu-item--danger:focus-visible, .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, ${c.bgDanger} 10%, transparent); }
51
+ .crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
52
+ .crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: ${c.space["3"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; letter-spacing: 0.02em; }
53
+ .crisp-menu-item-check { flex-shrink: 0; color: ${c.bgBrandSolid}; }
54
+ .crisp-menu-item-check--placeholder { width: 15px; }
43
55
 
44
- & .crisp-menu-sep { height: 1px; flex-shrink: 0; border: none; margin: ${c.space["1"]} calc(-1 * ${c.space["1"]}); background: ${c.borderSubtle}; }
45
- & .crisp-menu-label { padding: ${c.space["1.5"]} ${c.space["2"]} ${c.space["0.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
56
+ .crisp-menu-sep { height: 1px; flex-shrink: 0; border: none; margin: ${c.space["1"]} calc(-1 * ${c.space["1"]}); background: ${c.borderSubtle}; }
57
+ .crisp-menu-label { padding: ${c.space["1.5"]} ${c.space["2"]} ${c.space["0.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
46
58
  `,
47
59
  variants: {},
48
60
  defaultVariants: {},
@@ -154,7 +154,7 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
154
154
  e.preventDefault();
155
155
  openMenu();
156
156
  }
157
- }, children: [_jsx("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
157
+ }, children: [_jsx("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
158
158
  setQuery(e.target.value);
159
159
  setHighlight(0);
160
160
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
@@ -6,6 +6,19 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
6
6
  // toggle on click. The popover portals to document.body (#339) so no
7
7
  // ancestor's transform/backdrop-filter can capture its position: fixed box;
8
8
  // z-index 70 keeps it above Dialog/Drawer's 60/61.
9
+ // The listbox and everything inside it (including the recordstable-tag
10
+ // chips, which render inside listbox option rows as well as the trigger)
11
+ // live in `globalCss` below, not nested under `&` (#357): nesting compiles
12
+ // to a real descendant combinator scoped to the recipe's root class, which
13
+ // needs actual DOM containment — exactly what the portal to `document.body`
14
+ // breaks. Nested, these rules matched nothing once the listbox moved and it
15
+ // rendered completely unstyled. The trigger and its own parts stay nested
16
+ // under `&` — the trigger is NOT portaled. `font-family`/smoothing are
17
+ // restated on the listbox's own rule (its children pick font up via their
18
+ // own `font-family: inherit`, which still works — that inheritance is
19
+ // intact WITHIN the portaled subtree, only the link back to the
20
+ // trigger-holding root broke). A flat recordstable-tag selector also works
21
+ // unchanged wherever it's rendered, portaled or not, so it needs no split.
9
22
  const config = defineRecipe({
10
23
  name: "multiselect",
11
24
  base: `
@@ -26,38 +39,40 @@ const config = defineRecipe({
26
39
  & .crisp-multiselect-placeholder { color: ${c.fgMuted}; }
27
40
  & .crisp-multiselect-chevron { flex-shrink: 0; color: ${c.fgMuted}; transition: transform .15s ease; }
28
41
  & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
29
-
30
- & .crisp-multiselect-listbox {
42
+ `,
43
+ globalCss: `
44
+ .crisp-multiselect-listbox {
45
+ font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
31
46
  position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
32
47
  max-height: 300px; overflow-y: auto; padding: ${c.space["1"]};
33
48
  border-radius: ${c.radius.xl}; background: ${c.bgPopover};
34
49
  box-shadow: ${e.floating};
35
50
  transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
36
51
  }
37
- & .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
38
- & .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
39
- & .crisp-multiselect-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["1"]}) calc(-1 * ${c.space["1"]}) ${c.space["1"]}; padding: 0 ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgMuted}; }
40
- & .crisp-multiselect-search svg { flex-shrink: 0; }
41
- & .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
42
- & .crisp-multiselect-search input::placeholder { color: ${c.fgMuted}; }
43
- & .crisp-multiselect-option {
52
+ .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
53
+ .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
54
+ .crisp-multiselect-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["1"]}) calc(-1 * ${c.space["1"]}) ${c.space["1"]}; padding: 0 ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgMuted}; }
55
+ .crisp-multiselect-search svg { flex-shrink: 0; }
56
+ .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
57
+ .crisp-multiselect-search input::placeholder { color: ${c.fgMuted}; }
58
+ .crisp-multiselect-option {
44
59
  box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.menuRow}; flex-shrink: 0;
45
60
  padding: 0 ${c.space["2"]}; margin: 0; border: none; background: none; border-radius: ${c.radius.md}; font-family: inherit; cursor: pointer; text-align: start;
46
61
  }
47
- & .crisp-multiselect-option[data-highlighted] { background: ${c.bgRaised}; }
48
- & .crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
49
- & .crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: ${c.radius.sm}; box-shadow: inset 0 0 0 1.5px ${c.borderDefault}; color: transparent; }
50
- & .crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: ${c.bgBrandSolid}; box-shadow: none; color: #fff; }
51
- & .crisp-multiselect-empty { padding: ${c.space["3"]} ${c.space["2"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }
62
+ .crisp-multiselect-option[data-highlighted] { background: ${c.bgRaised}; }
63
+ .crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
64
+ .crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: ${c.radius.sm}; box-shadow: inset 0 0 0 1.5px ${c.borderDefault}; color: transparent; }
65
+ .crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: ${c.bgBrandSolid}; box-shadow: none; color: #fff; }
66
+ .crisp-multiselect-empty { padding: ${c.space["3"]} ${c.space["2"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }
52
67
 
53
- & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${c.size.chip}; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.chip}; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${c.fgPrimary} 82%, transparent); }
54
- & .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, ${c.bgBrandSolid} 14%, transparent); color: color-mix(in srgb, ${c.bgBrandSolid} 82%, ${c.fgPrimary}); }
55
- & .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, ${c.bgSuccess} 16%, transparent); color: color-mix(in srgb, ${c.bgSuccess} 78%, ${c.fgPrimary}); }
56
- & .crisp-recordstable-tag[data-tone="warning"] { background: color-mix(in srgb, ${c.bgWarning} 18%, transparent); color: color-mix(in srgb, ${c.bgWarning} 82%, ${c.fgPrimary}); }
57
- & .crisp-recordstable-tag[data-tone="danger"] { background: color-mix(in srgb, ${c.bgDanger} 15%, transparent); color: color-mix(in srgb, ${c.bgDanger} 80%, ${c.fgPrimary}); }
58
- & .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, ${c.bgPurple} 16%, transparent); color: color-mix(in srgb, ${c.bgPurple} 80%, ${c.fgPrimary}); }
59
- & .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, ${c.bgCyan} 18%, transparent); color: color-mix(in srgb, ${c.bgCyan} 82%, ${c.fgPrimary}); }
60
- & .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); }
68
+ .crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${c.size.chip}; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.chip}; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${c.fgPrimary} 82%, transparent); }
69
+ .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, ${c.bgBrandSolid} 14%, transparent); color: color-mix(in srgb, ${c.bgBrandSolid} 82%, ${c.fgPrimary}); }
70
+ .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, ${c.bgSuccess} 16%, transparent); color: color-mix(in srgb, ${c.bgSuccess} 78%, ${c.fgPrimary}); }
71
+ .crisp-recordstable-tag[data-tone="warning"] { background: color-mix(in srgb, ${c.bgWarning} 18%, transparent); color: color-mix(in srgb, ${c.bgWarning} 82%, ${c.fgPrimary}); }
72
+ .crisp-recordstable-tag[data-tone="danger"] { background: color-mix(in srgb, ${c.bgDanger} 15%, transparent); color: color-mix(in srgb, ${c.bgDanger} 80%, ${c.fgPrimary}); }
73
+ .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, ${c.bgPurple} 16%, transparent); color: color-mix(in srgb, ${c.bgPurple} 80%, ${c.fgPrimary}); }
74
+ .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, ${c.bgCyan} 18%, transparent); color: color-mix(in srgb, ${c.bgCyan} 82%, ${c.fgPrimary}); }
75
+ .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); }
61
76
  `,
62
77
  variants: {
63
78
  // Same ring Select's `invalid` draws — see that recipe's comment.
@@ -86,7 +86,7 @@ export function Popover({ trigger, children, align = "start", open: openProp, de
86
86
  setOpen(!open);
87
87
  },
88
88
  });
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) => {
89
+ return (_jsxs("div", { ref: rootRef, className: popoverRecipe({ className }), children: [triggerEl, open && (_jsx(FloatingPortal, { sourceRef: rootRef, 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
90
  if (e.key === "Escape") {
91
91
  e.preventDefault();
92
92
  setOpen(false);
@@ -6,12 +6,27 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
6
6
  // ancestor's transform/backdrop-filter can capture its position: fixed box;
7
7
  // z-index 70 keeps it above Dialog/Drawer's 60/61. The display:contents root
8
8
  // is what's left in place, so a consumer's own styling of it survives.
9
+ //
10
+ // The popover's floating content is declared via `globalCss` below, not
11
+ // nested under `&` (#357): nesting compiles to a genuine descendant
12
+ // combinator scoped to the recipe's root class, and the portal moves the
13
+ // content to a DOM sibling of `document.body`, not a descendant of the root
14
+ // — so a nested rule silently matched nothing and the popover rendered
15
+ // completely unstyled (`position: static`, no background, no shadow).
16
+ // `globalCss` is already the escape hatch for CSS a property can't express
17
+ // (`@keyframes`); a portaled part is the same problem, one selector at a
18
+ // time. Every declaration a portaled selector needs — including ones
19
+ // normally inherited from the root, like `font-family` — has to be restated
20
+ // here, since DOM inheritance from the root is exactly what the portal
21
+ // breaks.
9
22
  const config = defineRecipe({
10
23
  name: "popover",
11
- base: `display: contents; font-family: ${c.font.sans};
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); } }
13
- & .crisp-popover-content[data-align="end"] { transform-origin: top right; }
14
- & .crisp-popover-content[data-up] { transform-origin: bottom left; }`,
24
+ base: `display: contents; font-family: ${c.font.sans};`,
25
+ globalCss: `
26
+ .crisp-popover-content { font-family: ${c.font.sans}; 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); } }
27
+ .crisp-popover-content[data-align="end"] { transform-origin: top right; }
28
+ .crisp-popover-content[data-up] { transform-origin: bottom left; }
29
+ `,
15
30
  variants: {},
16
31
  defaultVariants: {},
17
32
  compoundVariants: [],
@@ -144,7 +144,7 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
144
144
  return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, invalid, className }), ...rest, children: [name && (selected || required) && (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
145
145
  // controlled by the listbox above; `readOnly` would bar it from
146
146
  // validation, which is the only reason this input is not hidden
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, "aria-invalid": invalid === true || undefined, 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) => {
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, "aria-invalid": invalid === true || undefined, 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, { sourceRef: rootRef, 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
148
  setQuery(e.target.value);
149
149
  setHighlight(0);
150
150
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
@@ -8,6 +8,17 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
8
8
  // (Dialog's own measured surface sets both) can capture its position: fixed
9
9
  // box; z-index 70 keeps it above Dialog/Drawer's z-index 60/61 layers, since
10
10
  // it's no longer nested inside whichever one opened it.
11
+ // The listbox and everything inside it live in `globalCss` below, not
12
+ // nested under `&` (#357): nesting compiles to a real descendant combinator
13
+ // scoped to the recipe's root class, which needs actual DOM containment —
14
+ // exactly what the portal to `document.body` breaks. Nested, these rules
15
+ // matched nothing once the listbox moved and it rendered completely
16
+ // unstyled. The trigger and its own parts stay nested under `&` — the
17
+ // trigger is NOT portaled, it stays where it was authored. `font-family`/
18
+ // smoothing are restated on the listbox's own rule (its children pick font
19
+ // up via their own `font-family: inherit`, which still works — that
20
+ // inheritance is intact WITHIN the portaled subtree, only the link back to
21
+ // the trigger-holding root broke).
11
22
  const config = defineRecipe({
12
23
  name: "select",
13
24
  base: `
@@ -28,31 +39,33 @@ const config = defineRecipe({
28
39
  & .crisp-select-placeholder { color: ${c.fgMuted}; }
29
40
  & .crisp-select-chevron { flex-shrink: 0; color: ${c.fgMuted}; transition: transform .15s ease; }
30
41
  & .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
31
-
32
- & .crisp-select-listbox {
42
+ `,
43
+ globalCss: `
44
+ .crisp-select-listbox {
45
+ font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
33
46
  position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
34
47
  max-height: 280px; overflow-y: auto; padding: ${c.space["1"]};
35
48
  border-radius: ${c.radius.xl}; background: ${c.bgPopover};
36
49
  box-shadow: ${e.floating};
37
50
  transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
38
51
  }
39
- & .crisp-select-listbox[data-up] { transform-origin: bottom; }
40
- & .crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
41
- & .crisp-select-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["1"]}) calc(-1 * ${c.space["1"]}) ${c.space["1"]}; padding: 0 ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgMuted}; }
42
- & .crisp-select-search svg { flex-shrink: 0; }
43
- & .crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
44
- & .crisp-select-search input::placeholder { color: ${c.fgMuted}; }
45
- & .crisp-select-empty { padding: ${c.space["3"]} ${c.space["2"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }
46
- & .crisp-select-option {
52
+ .crisp-select-listbox[data-up] { transform-origin: bottom; }
53
+ .crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
54
+ .crisp-select-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["1"]}) calc(-1 * ${c.space["1"]}) ${c.space["1"]}; padding: 0 ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgMuted}; }
55
+ .crisp-select-search svg { flex-shrink: 0; }
56
+ .crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
57
+ .crisp-select-search input::placeholder { color: ${c.fgMuted}; }
58
+ .crisp-select-empty { padding: ${c.space["3"]} ${c.space["2"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }
59
+ .crisp-select-option {
47
60
  box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.menuRow}; flex-shrink: 0;
48
61
  padding: 0 ${c.space["2"]}; margin: 0; border: none; background: none; border-radius: ${c.radius.md};
49
62
  font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: start; cursor: pointer; white-space: nowrap;
50
63
  }
51
- & .crisp-select-option[data-highlighted] { background: ${c.bgRaised}; }
52
- & .crisp-select-option[data-disabled] { opacity: 0.5; cursor: not-allowed; }
53
- & .crisp-select-option-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${c.radius.full}; }
54
- & .crisp-select-option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
55
- & .crisp-select-option-check { flex-shrink: 0; color: ${c.bgBrandSolid}; }
64
+ .crisp-select-option[data-highlighted] { background: ${c.bgRaised}; }
65
+ .crisp-select-option[data-disabled] { opacity: 0.5; cursor: not-allowed; }
66
+ .crisp-select-option-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${c.radius.full}; }
67
+ .crisp-select-option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
68
+ .crisp-select-option-check { flex-shrink: 0; color: ${c.bgBrandSolid}; }
56
69
  `,
57
70
  variants: {
58
71
  // ghost: borderless at rest (transparent), reveals a subtle fill on hover —
@@ -1,3 +1,4 @@
1
+ import * as React from "react";
1
2
  import { createPortal } from "react-dom";
2
3
  /**
3
4
  * Portals a floating layer (a `position: fixed` listbox/menu/popover
@@ -19,7 +20,26 @@ import { createPortal } from "react-dom";
19
20
  * starts `false` and is driven only by client interaction — never a prop a
20
21
  * fixture sets true at mount (unlike Dialog/Drawer/Command) — so this never
21
22
  * touches `document` during SSR, the same safety Tooltip already had.
23
+ *
24
+ * `sourceRef` is the caller's own root — still in its original DOM position,
25
+ * never portaled — used to carry a `data-theme` set on some ancestor of it
26
+ * onto the portaled content (#357). `[data-theme="dark"]` is a normal
27
+ * attribute selector: it needs the themed element to be an actual DOM
28
+ * ancestor, which a consumer that scopes theming below `<html>`/`<body>`
29
+ * (crisp's own visual-test harness does exactly this, per case) no longer
30
+ * is once the content moves to `document.body` — the popover rendered in
31
+ * the PAGE's theme instead of the SUBTREE's. Re-declaring the attribute
32
+ * directly on the portaled root re-establishes it for CSS attribute
33
+ * selectors AND for `var()` resolution, since a custom property set under
34
+ * `[data-theme="dark"] { --crisp-x: ... }` only reaches descendants of an
35
+ * element carrying that attribute.
22
36
  */
23
- export function FloatingPortal({ children, }) {
24
- return createPortal(children, document.body);
37
+ export function FloatingPortal({ children, sourceRef, }) {
38
+ const theme = sourceRef.current
39
+ ?.closest("[data-theme]")
40
+ ?.getAttribute("data-theme");
41
+ const content = theme
42
+ ? React.cloneElement(children, { "data-theme": theme })
43
+ : children;
44
+ return createPortal(content, document.body);
25
45
  }
package/dist/menu.css CHANGED
@@ -1,24 +1,39 @@
1
+
2
+ .crisp-menu-content {
3
+ font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
+ position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
5
+ min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
6
+ border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
7
+ 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);
8
+ transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
9
+ }
10
+ .crisp-menu-content[data-align="end"] { transform-origin: top right; }
11
+ .crisp-menu-content[data-up] { transform-origin: bottom left; }
12
+ .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
13
+ .crisp-menu-content { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
14
+
15
+ .crisp-menu-item {
16
+ box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; height: var(--crisp-size-menuRow); flex-shrink: 0;
17
+ margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; border-radius: var(--crisp-radius-md);
18
+ font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); text-align: start; cursor: pointer; white-space: nowrap;
19
+ }
20
+ .crisp-menu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
21
+ .crisp-menu-item:focus { outline: none; }
22
+ .crisp-menu-item:hover:not([data-disabled]), .crisp-menu-item:focus-visible:not([data-disabled]), .crisp-menu-item[data-highlighted]:not([data-disabled]) { background: var(--crisp-bg-raised); }
23
+ .crisp-menu-item[data-disabled] { opacity: 0.5; cursor: not-allowed; }
24
+ .crisp-menu-item--danger { color: var(--crisp-bg-danger); }
25
+ .crisp-menu-item--danger svg { color: var(--crisp-bg-danger); }
26
+ .crisp-menu-item--danger:hover, .crisp-menu-item--danger:focus-visible, .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
27
+ .crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
28
+ .crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: var(--crisp-space-3); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); letter-spacing: 0.02em; }
29
+ .crisp-menu-item-check { flex-shrink: 0; color: var(--crisp-bg-brand-solid); }
30
+ .crisp-menu-item-check--placeholder { width: 15px; }
31
+
32
+ .crisp-menu-sep { height: 1px; flex-shrink: 0; border: none; margin: var(--crisp-space-1) calc(-1 * var(--crisp-space-1)); background: var(--crisp-border-subtle); }
33
+ .crisp-menu-label { padding: var(--crisp-space-1-5) var(--crisp-space-2) var(--crisp-space-0-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
34
+
1
35
  .crisp-menu {
2
36
  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: 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
- & .crisp-menu-content[data-align="end"] { transform-origin: top right; }
5
- & .crisp-menu-content[data-up] { transform-origin: bottom left; }
6
- & .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
7
- & .crisp-menu-content { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
8
- & .crisp-menu-item { box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; height: var(--crisp-size-menuRow); flex-shrink: 0; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; border-radius: var(--crisp-radius-md); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); text-align: start; cursor: pointer; white-space: nowrap; }
9
- & .crisp-menu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
10
- & .crisp-menu-item:focus { outline: none; }
11
- & .crisp-menu-item:hover:not([data-disabled]), & .crisp-menu-item:focus-visible:not([data-disabled]), & .crisp-menu-item[data-highlighted]:not([data-disabled]) { background: var(--crisp-bg-raised); }
12
- & .crisp-menu-item[data-disabled] { opacity: 0.5; cursor: not-allowed; }
13
- & .crisp-menu-item--danger { color: var(--crisp-bg-danger); }
14
- & .crisp-menu-item--danger svg { color: var(--crisp-bg-danger); }
15
- & .crisp-menu-item--danger:hover, & .crisp-menu-item--danger:focus-visible, & .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
16
- & .crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
17
- & .crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: var(--crisp-space-3); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); letter-spacing: 0.02em; }
18
- & .crisp-menu-item-check { flex-shrink: 0; color: var(--crisp-bg-brand-solid); }
19
- & .crisp-menu-item-check--placeholder { width: 15px; }
20
- & .crisp-menu-sep { height: 1px; flex-shrink: 0; border: none; margin: var(--crisp-space-1) calc(-1 * var(--crisp-space-1)); background: var(--crisp-border-subtle); }
21
- & .crisp-menu-label { padding: var(--crisp-space-1-5) var(--crisp-space-2) var(--crisp-space-0-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
22
37
  }
23
38
 
24
39
  @media (prefers-reduced-motion: reduce) {
@@ -1,25 +1,40 @@
1
1
  @layer crisp-components {
2
+
3
+ .crisp-menu-content {
4
+ font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
5
+ position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
6
+ min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
7
+ border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
8
+ 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);
9
+ transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
10
+ }
11
+ .crisp-menu-content[data-align="end"] { transform-origin: top right; }
12
+ .crisp-menu-content[data-up] { transform-origin: bottom left; }
13
+ .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
14
+ .crisp-menu-content { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
15
+
16
+ .crisp-menu-item {
17
+ box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; height: var(--crisp-size-menuRow); flex-shrink: 0;
18
+ margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; border-radius: var(--crisp-radius-md);
19
+ font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); text-align: start; cursor: pointer; white-space: nowrap;
20
+ }
21
+ .crisp-menu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
22
+ .crisp-menu-item:focus { outline: none; }
23
+ .crisp-menu-item:hover:not([data-disabled]), .crisp-menu-item:focus-visible:not([data-disabled]), .crisp-menu-item[data-highlighted]:not([data-disabled]) { background: var(--crisp-bg-raised); }
24
+ .crisp-menu-item[data-disabled] { opacity: 0.5; cursor: not-allowed; }
25
+ .crisp-menu-item--danger { color: var(--crisp-bg-danger); }
26
+ .crisp-menu-item--danger svg { color: var(--crisp-bg-danger); }
27
+ .crisp-menu-item--danger:hover, .crisp-menu-item--danger:focus-visible, .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
28
+ .crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
29
+ .crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: var(--crisp-space-3); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); letter-spacing: 0.02em; }
30
+ .crisp-menu-item-check { flex-shrink: 0; color: var(--crisp-bg-brand-solid); }
31
+ .crisp-menu-item-check--placeholder { width: 15px; }
32
+
33
+ .crisp-menu-sep { height: 1px; flex-shrink: 0; border: none; margin: var(--crisp-space-1) calc(-1 * var(--crisp-space-1)); background: var(--crisp-border-subtle); }
34
+ .crisp-menu-label { padding: var(--crisp-space-1-5) var(--crisp-space-2) var(--crisp-space-0-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
35
+
2
36
  .crisp-menu {
3
37
  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: 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
- & .crisp-menu-content[data-align="end"] { transform-origin: top right; }
6
- & .crisp-menu-content[data-up] { transform-origin: bottom left; }
7
- & .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
8
- & .crisp-menu-content { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
9
- & .crisp-menu-item { box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; height: var(--crisp-size-menuRow); flex-shrink: 0; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; border-radius: var(--crisp-radius-md); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); text-align: start; cursor: pointer; white-space: nowrap; }
10
- & .crisp-menu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
11
- & .crisp-menu-item:focus { outline: none; }
12
- & .crisp-menu-item:hover:not([data-disabled]), & .crisp-menu-item:focus-visible:not([data-disabled]), & .crisp-menu-item[data-highlighted]:not([data-disabled]) { background: var(--crisp-bg-raised); }
13
- & .crisp-menu-item[data-disabled] { opacity: 0.5; cursor: not-allowed; }
14
- & .crisp-menu-item--danger { color: var(--crisp-bg-danger); }
15
- & .crisp-menu-item--danger svg { color: var(--crisp-bg-danger); }
16
- & .crisp-menu-item--danger:hover, & .crisp-menu-item--danger:focus-visible, & .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
17
- & .crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
18
- & .crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: var(--crisp-space-3); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); letter-spacing: 0.02em; }
19
- & .crisp-menu-item-check { flex-shrink: 0; color: var(--crisp-bg-brand-solid); }
20
- & .crisp-menu-item-check--placeholder { width: 15px; }
21
- & .crisp-menu-sep { height: 1px; flex-shrink: 0; border: none; margin: var(--crisp-space-1) calc(-1 * var(--crisp-space-1)); background: var(--crisp-border-subtle); }
22
- & .crisp-menu-label { padding: var(--crisp-space-1-5) var(--crisp-space-2) var(--crisp-space-0-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
23
38
  }
24
39
 
25
40
  @media (prefers-reduced-motion: reduce) {
@@ -1,3 +1,37 @@
1
+
2
+ .crisp-multiselect-listbox {
3
+ font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
+ position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
5
+ max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1);
6
+ border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
7
+ 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);
8
+ transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
9
+ }
10
+ .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
11
+ .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
12
+ .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); }
13
+ .crisp-multiselect-search svg { flex-shrink: 0; }
14
+ .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
15
+ .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
16
+ .crisp-multiselect-option {
17
+ box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
18
+ padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md); font-family: inherit; cursor: pointer; text-align: start;
19
+ }
20
+ .crisp-multiselect-option[data-highlighted] { background: var(--crisp-bg-raised); }
21
+ .crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
22
+ .crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: var(--crisp-radius-sm); box-shadow: inset 0 0 0 1.5px var(--crisp-border-default); color: transparent; }
23
+ .crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: var(--crisp-bg-brand-solid); box-shadow: none; color: #fff; }
24
+ .crisp-multiselect-empty { padding: var(--crisp-space-3) var(--crisp-space-2); text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
25
+
26
+ .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
27
+ .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
28
+ .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
29
+ .crisp-recordstable-tag[data-tone="warning"] { background: color-mix(in srgb, var(--crisp-bg-warning) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-warning) 82%, var(--crisp-fg-primary)); }
30
+ .crisp-recordstable-tag[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 15%, transparent); color: color-mix(in srgb, var(--crisp-bg-danger) 80%, var(--crisp-fg-primary)); }
31
+ .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, var(--crisp-bg-purple) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-purple) 80%, var(--crisp-fg-primary)); }
32
+ .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, var(--crisp-bg-cyan) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-cyan) 82%, var(--crisp-fg-primary)); }
33
+ .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
34
+
1
35
  .crisp-multiselect {
2
36
  position: relative; display: inline-flex; width: 100%; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
37
  & .crisp-multiselect-trigger { box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; min-width: 0; min-height: var(--crisp-size-menuRow); margin: 0; padding: var(--crisp-space-0-5) var(--crisp-space-1-5) var(--crisp-space-0-5) var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); cursor: pointer; text-align: start; transition: background-color .12s ease, box-shadow .12s ease; }
@@ -8,27 +42,6 @@
8
42
  & .crisp-multiselect-placeholder { color: var(--crisp-fg-muted); }
9
43
  & .crisp-multiselect-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
10
44
  & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
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
- & .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
13
- & .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
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); }
15
- & .crisp-multiselect-search svg { flex-shrink: 0; }
16
- & .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
17
- & .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
18
- & .crisp-multiselect-option { box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0; padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md); font-family: inherit; cursor: pointer; text-align: start; }
19
- & .crisp-multiselect-option[data-highlighted] { background: var(--crisp-bg-raised); }
20
- & .crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
21
- & .crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: var(--crisp-radius-sm); box-shadow: inset 0 0 0 1.5px var(--crisp-border-default); color: transparent; }
22
- & .crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: var(--crisp-bg-brand-solid); box-shadow: none; color: #fff; }
23
- & .crisp-multiselect-empty { padding: var(--crisp-space-3) var(--crisp-space-2); text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
24
- & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
25
- & .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
26
- & .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
27
- & .crisp-recordstable-tag[data-tone="warning"] { background: color-mix(in srgb, var(--crisp-bg-warning) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-warning) 82%, var(--crisp-fg-primary)); }
28
- & .crisp-recordstable-tag[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 15%, transparent); color: color-mix(in srgb, var(--crisp-bg-danger) 80%, var(--crisp-fg-primary)); }
29
- & .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, var(--crisp-bg-purple) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-purple) 80%, var(--crisp-fg-primary)); }
30
- & .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, var(--crisp-bg-cyan) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-cyan) 82%, var(--crisp-fg-primary)); }
31
- & .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
32
45
  }
33
46
  .crisp-multiselect--invalid_true {
34
47
  & .crisp-multiselect-trigger, & .crisp-multiselect-trigger:hover, & .crisp-multiselect-trigger[data-open], & .crisp-multiselect-trigger:focus-visible { box-shadow: inset 0 0 0 1px var(--crisp-bg-danger); }