@crisp-ui-kit/crisp 0.41.0 → 0.43.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.
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ContextMenu = ContextMenu;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
+ const react_slot_1 = require("@radix-ui/react-slot");
38
39
  const React = __importStar(require("react"));
39
40
  const core_1 = require("../../core");
40
41
  const menu_1 = require("../menu");
@@ -44,7 +45,7 @@ const contextmenu_recipe_1 = require("./contextmenu.recipe");
44
45
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
45
46
  * edges), closing on select, outside pointer-down, and Escape.
46
47
  */
47
- function ContextMenu({ children, items, menuLabel = "Context menu", className, }) {
48
+ function ContextMenu({ children, items, menuLabel = "Context menu", className, asChild = false, }) {
48
49
  const rootRef = React.useRef(null);
49
50
  const menuRef = React.useRef(null);
50
51
  const returnFocusTo = React.useRef(null);
@@ -109,7 +110,7 @@ function ContextMenu({ children, items, menuLabel = "Context menu", className, }
109
110
  document.removeEventListener("keydown", onKey);
110
111
  };
111
112
  }, [pos, close]);
112
- return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, contextmenu_recipe_1.contextmenuRecipe)({ className }), children: [(0, jsx_runtime_1.jsx)("div", { onContextMenu: open, children: children }), pos && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-contextmenu-sep" }, i)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
113
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [asChild ? ((0, jsx_runtime_1.jsx)(react_slot_1.Slot, { ref: rootRef, className: className, onContextMenu: open, children: children })) : ((0, jsx_runtime_1.jsx)("div", { ref: rootRef, className: (0, contextmenu_recipe_1.contextmenuRecipe)({ className }), children: (0, jsx_runtime_1.jsx)("div", { onContextMenu: open, children: children }) })), pos && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-contextmenu-sep" }, i)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
113
114
  ? "crisp-contextmenu-item crisp-contextmenu-item--danger"
114
115
  : "crisp-contextmenu-item", tabIndex: -1, onClick: () => {
115
116
  item.onSelect?.();
@@ -195,7 +195,12 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
195
195
  setHighlight(0);
196
196
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
197
197
  const sel = selectedSet.has(opt.value);
198
- return ((0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-option-chip", children: (0, jsx_runtime_1.jsx)(Chip, { opt: opt }) }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-check", children: sel && (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
198
+ // #343: a header renders once per run of same-`group`
199
+ // options, so consecutive rows sharing a group render one
200
+ // heading, not one each.
201
+ const showGroupLabel = opt.group !== undefined &&
202
+ opt.group !== filtered[i - 1]?.group;
203
+ return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [showGroupLabel && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-multiselect-label", children: opt.group })), (0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-option-chip", children: (0, jsx_runtime_1.jsx)(Chip, { opt: opt }) }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-check", children: sel && (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 3 }) })] })] }, opt.value));
199
204
  }))] }) }))] }));
200
205
  }
201
206
  MultiSelect.displayName = "MultiSelect";
@@ -58,6 +58,10 @@ const config = (0, core_1.defineRecipe)({
58
58
  .crisp-multiselect-search svg { flex-shrink: 0; }
59
59
  .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; }
60
60
  .crisp-multiselect-search input::placeholder { color: ${tokens_1.contract.fgMuted}; }
61
+ /* #343: same measured values as Menu's own group heading
62
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
63
+ same "small muted heading above a run of rows" treatment. */
64
+ .crisp-multiselect-label { padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["0.5"]}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
61
65
  .crisp-multiselect-option {
62
66
  box-sizing: border-box; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.menuRow}; flex-shrink: 0;
63
67
  padding: 0 ${tokens_1.contract.space["2"]}; margin: 0; border: none; background: none; border-radius: ${tokens_1.contract.radius.md}; font-family: inherit; cursor: pointer; text-align: start;
@@ -185,7 +185,12 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
185
185
  setHighlight(0);
186
186
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
187
187
  const isSel = opt.value === selected;
188
- return ((0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
188
+ // #343: a header renders once per run of same-`group`
189
+ // options, so consecutive rows sharing a group render one
190
+ // heading, not one each.
191
+ const showGroupLabel = opt.group !== undefined &&
192
+ opt.group !== filtered[i - 1]?.group;
193
+ return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [showGroupLabel && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-select-label", children: opt.group })), (0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] })] }, opt.value));
189
194
  }))] }) }))] }));
190
195
  }
191
196
  Select.displayName = "Select";
@@ -59,6 +59,10 @@ const config = (0, core_1.defineRecipe)({
59
59
  .crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; }
60
60
  .crisp-select-search input::placeholder { color: ${tokens_1.contract.fgMuted}; }
61
61
  .crisp-select-empty { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]}; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
62
+ /* #343: same measured values as Menu's own group heading
63
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
64
+ same "small muted heading above a run of rows" treatment. */
65
+ .crisp-select-label { padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["0.5"]}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
62
66
  .crisp-select-option {
63
67
  box-sizing: border-box; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.menuRow}; flex-shrink: 0;
64
68
  padding: 0 ${tokens_1.contract.space["2"]}; margin: 0; border: none; background: none; border-radius: ${tokens_1.contract.radius.md};
@@ -1,4 +1,5 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Slot } from "@radix-ui/react-slot";
2
3
  import * as React from "react";
3
4
  import { FloatingPortal, isInsideLaterFloatingLayer, registerFloatingLayer, } from "../../core/index.js";
4
5
  import { useMenuKeyboard } from "../menu/index.js";
@@ -8,7 +9,7 @@ import { contextmenuRecipe } from "./contextmenu.recipe.js";
8
9
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
9
10
  * edges), closing on select, outside pointer-down, and Escape.
10
11
  */
11
- export function ContextMenu({ children, items, menuLabel = "Context menu", className, }) {
12
+ export function ContextMenu({ children, items, menuLabel = "Context menu", className, asChild = false, }) {
12
13
  const rootRef = React.useRef(null);
13
14
  const menuRef = React.useRef(null);
14
15
  const returnFocusTo = React.useRef(null);
@@ -73,7 +74,7 @@ export function ContextMenu({ children, items, menuLabel = "Context menu", class
73
74
  document.removeEventListener("keydown", onKey);
74
75
  };
75
76
  }, [pos, close]);
76
- return (_jsxs("div", { ref: rootRef, className: contextmenuRecipe({ className }), children: [_jsx("div", { onContextMenu: open, children: children }), pos && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? (_jsx("hr", { className: "crisp-contextmenu-sep" }, i)) : (_jsxs("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
77
+ return (_jsxs(_Fragment, { children: [asChild ? (_jsx(Slot, { ref: rootRef, className: className, onContextMenu: open, children: children })) : (_jsx("div", { ref: rootRef, className: contextmenuRecipe({ className }), children: _jsx("div", { onContextMenu: open, children: children }) })), pos && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? (_jsx("hr", { className: "crisp-contextmenu-sep" }, i)) : (_jsxs("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
77
78
  ? "crisp-contextmenu-item crisp-contextmenu-item--danger"
78
79
  : "crisp-contextmenu-item", tabIndex: -1, onClick: () => {
79
80
  item.onSelect?.();
@@ -159,7 +159,12 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
159
159
  setHighlight(0);
160
160
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
161
161
  const sel = selectedSet.has(opt.value);
162
- return (_jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [_jsx("span", { className: "crisp-multiselect-option-chip", children: _jsx(Chip, { opt: opt }) }), _jsx("span", { className: "crisp-multiselect-check", children: sel && _jsx(Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
162
+ // #343: a header renders once per run of same-`group`
163
+ // options, so consecutive rows sharing a group render one
164
+ // heading, not one each.
165
+ const showGroupLabel = opt.group !== undefined &&
166
+ opt.group !== filtered[i - 1]?.group;
167
+ return (_jsxs(React.Fragment, { children: [showGroupLabel && (_jsx("div", { className: "crisp-multiselect-label", children: opt.group })), _jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [_jsx("span", { className: "crisp-multiselect-option-chip", children: _jsx(Chip, { opt: opt }) }), _jsx("span", { className: "crisp-multiselect-check", children: sel && _jsx(Check, { size: 12, strokeWidth: 3 }) })] })] }, opt.value));
163
168
  }))] }) }))] }));
164
169
  }
165
170
  MultiSelect.displayName = "MultiSelect";
@@ -55,6 +55,10 @@ const config = defineRecipe({
55
55
  .crisp-multiselect-search svg { flex-shrink: 0; }
56
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
57
  .crisp-multiselect-search input::placeholder { color: ${c.fgMuted}; }
58
+ /* #343: same measured values as Menu's own group heading
59
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
60
+ same "small muted heading above a run of rows" treatment. */
61
+ .crisp-multiselect-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}; }
58
62
  .crisp-multiselect-option {
59
63
  box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.menuRow}; flex-shrink: 0;
60
64
  padding: 0 ${c.space["2"]}; margin: 0; border: none; background: none; border-radius: ${c.radius.md}; font-family: inherit; cursor: pointer; text-align: start;
@@ -149,7 +149,12 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
149
149
  setHighlight(0);
150
150
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
151
151
  const isSel = opt.value === selected;
152
- return (_jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "crisp-select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
152
+ // #343: a header renders once per run of same-`group`
153
+ // options, so consecutive rows sharing a group render one
154
+ // heading, not one each.
155
+ const showGroupLabel = opt.group !== undefined &&
156
+ opt.group !== filtered[i - 1]?.group;
157
+ return (_jsxs(React.Fragment, { children: [showGroupLabel && (_jsx("div", { className: "crisp-select-label", children: opt.group })), _jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "crisp-select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] })] }, opt.value));
153
158
  }))] }) }))] }));
154
159
  }
155
160
  Select.displayName = "Select";
@@ -56,6 +56,10 @@ const config = defineRecipe({
56
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
57
  .crisp-select-search input::placeholder { color: ${c.fgMuted}; }
58
58
  .crisp-select-empty { padding: ${c.space["3"]} ${c.space["2"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }
59
+ /* #343: same measured values as Menu's own group heading
60
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
61
+ same "small muted heading above a run of rows" treatment. */
62
+ .crisp-select-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}; }
59
63
  .crisp-select-option {
60
64
  box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.menuRow}; flex-shrink: 0;
61
65
  padding: 0 ${c.space["2"]}; margin: 0; border: none; background: none; border-radius: ${c.radius.md};
@@ -13,6 +13,10 @@
13
13
  .crisp-multiselect-search svg { flex-shrink: 0; }
14
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
15
  .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
16
+ /* #343: same measured values as Menu's own group heading
17
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
18
+ same "small muted heading above a run of rows" treatment. */
19
+ .crisp-multiselect-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); }
16
20
  .crisp-multiselect-option {
17
21
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
18
22
  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;
@@ -14,6 +14,10 @@
14
14
  .crisp-multiselect-search svg { flex-shrink: 0; }
15
15
  .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); }
16
16
  .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
17
+ /* #343: same measured values as Menu's own group heading
18
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
19
+ same "small muted heading above a run of rows" treatment. */
20
+ .crisp-multiselect-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); }
17
21
  .crisp-multiselect-option {
18
22
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
19
23
  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;
package/dist/select.css CHANGED
@@ -14,6 +14,10 @@
14
14
  .crisp-select-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
15
  .crisp-select-search input::placeholder { color: var(--crisp-fg-muted); }
16
16
  .crisp-select-empty { padding: var(--crisp-space-3) var(--crisp-space-2); text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
17
+ /* #343: same measured values as Menu's own group heading
18
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
19
+ same "small muted heading above a run of rows" treatment. */
20
+ .crisp-select-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); }
17
21
  .crisp-select-option {
18
22
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
19
23
  padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md);
@@ -15,6 +15,10 @@
15
15
  .crisp-select-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); }
16
16
  .crisp-select-search input::placeholder { color: var(--crisp-fg-muted); }
17
17
  .crisp-select-empty { padding: var(--crisp-space-3) var(--crisp-space-2); text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
18
+ /* #343: same measured values as Menu's own group heading
19
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
20
+ same "small muted heading above a run of rows" treatment. */
21
+ .crisp-select-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); }
18
22
  .crisp-select-option {
19
23
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
20
24
  padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md);
package/dist/styles.css CHANGED
@@ -1045,6 +1045,10 @@
1045
1045
  .crisp-multiselect-search svg { flex-shrink: 0; }
1046
1046
  .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); }
1047
1047
  .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
1048
+ /* #343: same measured values as Menu's own group heading
1049
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
1050
+ same "small muted heading above a run of rows" treatment. */
1051
+ .crisp-multiselect-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); }
1048
1052
  .crisp-multiselect-option {
1049
1053
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
1050
1054
  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;
@@ -1272,6 +1276,10 @@
1272
1276
  .crisp-select-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); }
1273
1277
  .crisp-select-search input::placeholder { color: var(--crisp-fg-muted); }
1274
1278
  .crisp-select-empty { padding: var(--crisp-space-3) var(--crisp-space-2); text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
1279
+ /* #343: same measured values as Menu's own group heading
1280
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
1281
+ same "small muted heading above a run of rows" treatment. */
1282
+ .crisp-select-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); }
1275
1283
  .crisp-select-option {
1276
1284
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
1277
1285
  padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md);
@@ -1046,6 +1046,10 @@
1046
1046
  .crisp-multiselect-search svg { flex-shrink: 0; }
1047
1047
  .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); }
1048
1048
  .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
1049
+ /* #343: same measured values as Menu's own group heading
1050
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
1051
+ same "small muted heading above a run of rows" treatment. */
1052
+ .crisp-multiselect-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); }
1049
1053
  .crisp-multiselect-option {
1050
1054
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
1051
1055
  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;
@@ -1273,6 +1277,10 @@
1273
1277
  .crisp-select-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); }
1274
1278
  .crisp-select-search input::placeholder { color: var(--crisp-fg-muted); }
1275
1279
  .crisp-select-empty { padding: var(--crisp-space-3) var(--crisp-space-2); text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
1280
+ /* #343: same measured values as Menu's own group heading
1281
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
1282
+ same "small muted heading above a run of rows" treatment. */
1283
+ .crisp-select-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); }
1276
1284
  .crisp-select-option {
1277
1285
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
1278
1286
  padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md);
@@ -17,13 +17,23 @@ export type ContextMenuProps = {
17
17
  /** Accessible name for the menu surface. */
18
18
  menuLabel?: string;
19
19
  className?: string;
20
+ /**
21
+ * Render `onContextMenu` and the trigger ref directly onto `children`
22
+ * instead of wrapping it in a `<div>` (#340) — needed when `children` is
23
+ * an element that cannot have a wrapping element inserted around it, such
24
+ * as a `<tr>` inside a `<tbody>` (the browser's HTML parser hoists the row
25
+ * out of the wrapping `<div>`, breaking both the layout and the
26
+ * right-click target). `children` must then be a single valid React
27
+ * element, same as any other `asChild` in crisp (`core/primitive.tsx`).
28
+ */
29
+ asChild?: boolean;
20
30
  };
21
31
  /**
22
32
  * A right-click context menu — the popover surface positioned at the cursor.
23
33
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
24
34
  * edges), closing on select, outside pointer-down, and Escape.
25
35
  */
26
- export declare function ContextMenu({ children, items, menuLabel, className, }: ContextMenuProps): React.JSX.Element;
36
+ export declare function ContextMenu({ children, items, menuLabel, className, asChild, }: ContextMenuProps): React.JSX.Element;
27
37
  export declare namespace ContextMenu {
28
38
  var displayName: string;
29
39
  }
@@ -4,6 +4,14 @@ export type MultiSelectOption = {
4
4
  label: string;
5
5
  /** Chip colour tone; falls back to a deterministic pastel per value. */
6
6
  tone?: string;
7
+ /**
8
+ * Group heading, rendered above this option (#343). A header renders once
9
+ * per run of consecutive options sharing the same `group` — like native
10
+ * `<optgroup>`, grouping is positional: pre-sort `options` so same-group
11
+ * rows are adjacent, or the same label renders more than once. Options
12
+ * with no `group` render exactly as before this existed.
13
+ */
14
+ group?: string;
7
15
  };
8
16
  export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" | "defaultValue" | "onChange"> & {
9
17
  options: MultiSelectOption[];
@@ -7,6 +7,14 @@ export type SelectOption = {
7
7
  /** A status colour dot shown before the label (status/select fields). */
8
8
  color?: string;
9
9
  disabled?: boolean;
10
+ /**
11
+ * Group heading, rendered above this option (#343). A header renders once
12
+ * per run of consecutive options sharing the same `group` — like native
13
+ * `<optgroup>`, grouping is positional: pre-sort `options` so same-group
14
+ * rows are adjacent, or the same label renders more than once. Options
15
+ * with no `group` render exactly as before this existed.
16
+ */
17
+ group?: string;
10
18
  };
11
19
  export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" | "defaultValue" | "onChange"> & {
12
20
  options: SelectOption[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.41.0",
3
+ "version": "0.43.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",