@crisp-ui-kit/crisp 0.42.0 → 0.44.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 (31) hide show
  1. package/dist/cjs/components/contextmenu/contextmenu.recipe.js +6 -1
  2. package/dist/cjs/components/menu/menu.recipe.js +6 -1
  3. package/dist/cjs/components/multiselect/multiselect.js +6 -1
  4. package/dist/cjs/components/multiselect/multiselect.recipe.js +10 -1
  5. package/dist/cjs/components/popover/popover.recipe.js +6 -1
  6. package/dist/cjs/components/select/select.js +6 -1
  7. package/dist/cjs/components/select/select.recipe.js +10 -1
  8. package/dist/cjs/product/recordpanel/recordpanel.js +15 -2
  9. package/dist/contextmenu.css +6 -1
  10. package/dist/contextmenu.layered.css +6 -1
  11. package/dist/esm/components/contextmenu/contextmenu.recipe.js +6 -1
  12. package/dist/esm/components/menu/menu.recipe.js +6 -1
  13. package/dist/esm/components/multiselect/multiselect.js +6 -1
  14. package/dist/esm/components/multiselect/multiselect.recipe.js +10 -1
  15. package/dist/esm/components/popover/popover.recipe.js +6 -1
  16. package/dist/esm/components/select/select.js +6 -1
  17. package/dist/esm/components/select/select.recipe.js +10 -1
  18. package/dist/esm/product/recordpanel/recordpanel.js +15 -2
  19. package/dist/menu.css +6 -1
  20. package/dist/menu.layered.css +6 -1
  21. package/dist/multiselect.css +10 -1
  22. package/dist/multiselect.layered.css +10 -1
  23. package/dist/popover.css +6 -1
  24. package/dist/popover.layered.css +6 -1
  25. package/dist/select.css +10 -1
  26. package/dist/select.layered.css +10 -1
  27. package/dist/styles.css +38 -5
  28. package/dist/styles.layered.css +38 -5
  29. package/dist/types/components/multiselect/multiselect.d.ts +8 -0
  30. package/dist/types/components/select/select.d.ts +8 -0
  31. package/package.json +1 -1
@@ -21,7 +21,12 @@ const config = (0, core_1.defineRecipe)({
21
21
  name: "contextmenu",
22
22
  base: `display: contents; font-family: ${tokens_1.contract.font.sans};`,
23
23
  globalCss: `
24
- .crisp-contextmenu-content { font-family: ${tokens_1.contract.font.sans}; position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; box-shadow: ${tokens_1.elevation.floating}; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
24
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
25
+ on document.body while open (Zag re-enables it only on layers IT
26
+ tracks), and the portaled content is outside Zag's knowledge -- it
27
+ inherited the lock, unclickable, hit-testing right through to
28
+ whatever real element sat behind it. */
29
+ .crisp-contextmenu-content { font-family: ${tokens_1.contract.font.sans}; position: fixed; z-index: 70; pointer-events: auto; min-width: 200px; box-sizing: border-box; padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; box-shadow: ${tokens_1.elevation.floating}; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
25
30
  .crisp-contextmenu-item { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; width: 100%; height: ${tokens_1.contract.size.menuRow}; box-sizing: border-box; padding: 0 ${tokens_1.contract.space["2"]}; margin: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: none; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; text-align: start; white-space: nowrap; cursor: pointer; }
26
31
  .crisp-contextmenu-item svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
27
32
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); }
@@ -26,9 +26,14 @@ const config = (0, core_1.defineRecipe)({
26
26
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
27
27
  `,
28
28
  globalCss: `
29
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
30
+ on document.body while open (Zag re-enables it only on layers IT
31
+ tracks), and the portaled content is outside Zag's knowledge -- it
32
+ inherited the lock, so every item was unclickable, hit-testing right
33
+ through to whatever real element sat behind it. */
29
34
  .crisp-menu-content {
30
35
  font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
31
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
36
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
32
37
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: ${tokens_1.contract.space["1"]};
33
38
  border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover};
34
39
  box-shadow: ${tokens_1.elevation.floating};
@@ -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";
@@ -44,9 +44,14 @@ const config = (0, core_1.defineRecipe)({
44
44
  & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
45
45
  `,
46
46
  globalCss: `
47
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
48
+ on document.body while open (Zag re-enables it only on layers IT
49
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
50
+ inherited the lock, so every option was unclickable, hit-testing
51
+ right through to whatever real element sat behind it. */
47
52
  .crisp-multiselect-listbox {
48
53
  font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
49
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
54
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
50
55
  max-height: 300px; overflow-y: auto; padding: ${tokens_1.contract.space["1"]};
51
56
  border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover};
52
57
  box-shadow: ${tokens_1.elevation.floating};
@@ -58,6 +63,10 @@ const config = (0, core_1.defineRecipe)({
58
63
  .crisp-multiselect-search svg { flex-shrink: 0; }
59
64
  .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
65
  .crisp-multiselect-search input::placeholder { color: ${tokens_1.contract.fgMuted}; }
66
+ /* #343: same measured values as Menu's own group heading
67
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
68
+ same "small muted heading above a run of rows" treatment. */
69
+ .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
70
  .crisp-multiselect-option {
62
71
  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
72
  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;
@@ -26,7 +26,12 @@ const config = (0, core_1.defineRecipe)({
26
26
  name: "popover",
27
27
  base: `display: contents; font-family: ${tokens_1.contract.font.sans};`,
28
28
  globalCss: `
29
- .crisp-popover-content { font-family: ${tokens_1.contract.font.sans}; position: fixed; z-index: 70; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; color: ${tokens_1.contract.fgPrimary}; box-shadow: ${tokens_1.elevation.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); } }
29
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
30
+ on document.body while open (Zag re-enables it only on layers IT
31
+ tracks), and the portaled content is outside Zag's knowledge -- it
32
+ inherited the lock, unclickable, hit-testing right through to
33
+ whatever real element sat behind it. */
34
+ .crisp-popover-content { font-family: ${tokens_1.contract.font.sans}; position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; color: ${tokens_1.contract.fgPrimary}; box-shadow: ${tokens_1.elevation.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); } }
30
35
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
31
36
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
32
37
  `,
@@ -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";
@@ -44,9 +44,14 @@ const config = (0, core_1.defineRecipe)({
44
44
  & .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
45
45
  `,
46
46
  globalCss: `
47
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
48
+ on document.body while open (Zag re-enables it only on layers IT
49
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
50
+ inherited the lock, so every option was unclickable, hit-testing
51
+ right through to whatever real element sat behind it. */
47
52
  .crisp-select-listbox {
48
53
  font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
49
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
54
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
50
55
  max-height: 280px; overflow-y: auto; padding: ${tokens_1.contract.space["1"]};
51
56
  border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover};
52
57
  box-shadow: ${tokens_1.elevation.floating};
@@ -59,6 +64,10 @@ const config = (0, core_1.defineRecipe)({
59
64
  .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
65
  .crisp-select-search input::placeholder { color: ${tokens_1.contract.fgMuted}; }
61
66
  .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}; }
67
+ /* #343: same measured values as Menu's own group heading
68
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
69
+ same "small muted heading above a run of rows" treatment. */
70
+ .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
71
  .crisp-select-option {
63
72
  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
73
  padding: 0 ${tokens_1.contract.space["2"]}; margin: 0; border: none; background: none; border-radius: ${tokens_1.contract.radius.md};
@@ -72,6 +72,19 @@ function RecordPanel({ record, columns, onClose, className, closeLabel = "Close
72
72
  // both close on the same keypress, since neither knows about the other --
73
73
  // none of the five are built on this listener or registered anywhere it
74
74
  // can see, only on the shared floating-stack this checks instead.
75
+ //
76
+ // The listener has to run in the CAPTURE phase, not bubble (#376): Menu's
77
+ // own Escape handling is a React onKeyDown on its content div -- a bubble
78
+ // -phase synthetic handler, which fires (and, closing Menu, synchronously
79
+ // unregisters it from the floating stack) BEFORE a bubble-phase listener
80
+ // on `document` ever sees the event. `hasOpenFloatingLayer()` read AFTER
81
+ // that already reflects Menu's own reaction, not the state Escape actually
82
+ // arrived in, so it raced and lost every time -- confirmed by logging the
83
+ // registration order in a real browser, not by reasoning about it. Capture
84
+ // fires top-down, before Menu's bubble handler runs at all, so the check
85
+ // sees the state as it stood when the key was pressed. This mirrors how
86
+ // Zag's own `trackEscapeKeydown` (Dialog/Drawer's Escape handling) already
87
+ // listens on `document` in capture, for the same reason.
75
88
  React.useEffect(() => {
76
89
  if (!record)
77
90
  return;
@@ -80,8 +93,8 @@ function RecordPanel({ record, columns, onClose, className, closeLabel = "Close
80
93
  return;
81
94
  onClose();
82
95
  };
83
- document.addEventListener("keydown", onKey);
84
- return () => document.removeEventListener("keydown", onKey);
96
+ document.addEventListener("keydown", onKey, true);
97
+ return () => document.removeEventListener("keydown", onKey, true);
85
98
  }, [record, onClose]);
86
99
  if (!record)
87
100
  return null;
@@ -1,5 +1,10 @@
1
1
 
2
- .crisp-contextmenu-content { font-family: var(--crisp-font-sans); 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); } }
2
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
3
+ on document.body while open (Zag re-enables it only on layers IT
4
+ tracks), and the portaled content is outside Zag's knowledge -- it
5
+ inherited the lock, unclickable, hit-testing right through to
6
+ whatever real element sat behind it. */
7
+ .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; 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); } }
3
8
  .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; }
4
9
  .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
5
10
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
@@ -1,6 +1,11 @@
1
1
  @layer crisp-components {
2
2
 
3
- .crisp-contextmenu-content { font-family: var(--crisp-font-sans); 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); } }
3
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
4
+ on document.body while open (Zag re-enables it only on layers IT
5
+ tracks), and the portaled content is outside Zag's knowledge -- it
6
+ inherited the lock, unclickable, hit-testing right through to
7
+ whatever real element sat behind it. */
8
+ .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; min-width: 200px; box-sizing: border-box; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
4
9
  .crisp-contextmenu-item { display: flex; align-items: center; gap: var(--crisp-space-2); width: 100%; height: var(--crisp-size-menuRow); box-sizing: border-box; padding: 0 var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); text-align: start; white-space: nowrap; cursor: pointer; }
5
10
  .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
6
11
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
@@ -18,7 +18,12 @@ const config = defineRecipe({
18
18
  name: "contextmenu",
19
19
  base: `display: contents; font-family: ${c.font.sans};`,
20
20
  globalCss: `
21
- .crisp-contextmenu-content { font-family: ${c.font.sans}; position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; padding: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: ${c.bgPopover}; box-shadow: ${e.floating}; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
21
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
22
+ on document.body while open (Zag re-enables it only on layers IT
23
+ tracks), and the portaled content is outside Zag's knowledge -- it
24
+ inherited the lock, unclickable, hit-testing right through to
25
+ whatever real element sat behind it. */
26
+ .crisp-contextmenu-content { font-family: ${c.font.sans}; position: fixed; z-index: 70; pointer-events: auto; min-width: 200px; box-sizing: border-box; padding: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: ${c.bgPopover}; box-shadow: ${e.floating}; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
22
27
  .crisp-contextmenu-item { display: flex; align-items: center; gap: ${c.space["2"]}; width: 100%; height: ${c.size.menuRow}; box-sizing: border-box; padding: 0 ${c.space["2"]}; margin: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.lg}; text-align: start; white-space: nowrap; cursor: pointer; }
23
28
  .crisp-contextmenu-item svg { flex-shrink: 0; color: ${c.fgMuted}; }
24
29
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
@@ -23,9 +23,14 @@ const config = defineRecipe({
23
23
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
24
24
  `,
25
25
  globalCss: `
26
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
27
+ on document.body while open (Zag re-enables it only on layers IT
28
+ tracks), and the portaled content is outside Zag's knowledge -- it
29
+ inherited the lock, so every item was unclickable, hit-testing right
30
+ through to whatever real element sat behind it. */
26
31
  .crisp-menu-content {
27
32
  font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
28
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
33
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
29
34
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: ${c.space["1"]};
30
35
  border-radius: ${c.radius.xl}; background: ${c.bgPopover};
31
36
  box-shadow: ${e.floating};
@@ -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";
@@ -41,9 +41,14 @@ const config = defineRecipe({
41
41
  & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
42
42
  `,
43
43
  globalCss: `
44
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
45
+ on document.body while open (Zag re-enables it only on layers IT
46
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
47
+ inherited the lock, so every option was unclickable, hit-testing
48
+ right through to whatever real element sat behind it. */
44
49
  .crisp-multiselect-listbox {
45
50
  font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
46
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
51
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
47
52
  max-height: 300px; overflow-y: auto; padding: ${c.space["1"]};
48
53
  border-radius: ${c.radius.xl}; background: ${c.bgPopover};
49
54
  box-shadow: ${e.floating};
@@ -55,6 +60,10 @@ const config = defineRecipe({
55
60
  .crisp-multiselect-search svg { flex-shrink: 0; }
56
61
  .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
62
  .crisp-multiselect-search input::placeholder { color: ${c.fgMuted}; }
63
+ /* #343: same measured values as Menu's own group heading
64
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
65
+ same "small muted heading above a run of rows" treatment. */
66
+ .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
67
  .crisp-multiselect-option {
59
68
  box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.menuRow}; flex-shrink: 0;
60
69
  padding: 0 ${c.space["2"]}; margin: 0; border: none; background: none; border-radius: ${c.radius.md}; font-family: inherit; cursor: pointer; text-align: start;
@@ -23,7 +23,12 @@ const config = defineRecipe({
23
23
  name: "popover",
24
24
  base: `display: contents; font-family: ${c.font.sans};`,
25
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); } }
26
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
27
+ on document.body while open (Zag re-enables it only on layers IT
28
+ tracks), and the portaled content is outside Zag's knowledge -- it
29
+ inherited the lock, unclickable, hit-testing right through to
30
+ whatever real element sat behind it. */
31
+ .crisp-popover-content { font-family: ${c.font.sans}; position: fixed; z-index: 70; pointer-events: auto; 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
32
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
28
33
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
29
34
  `,
@@ -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";
@@ -41,9 +41,14 @@ const config = defineRecipe({
41
41
  & .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
42
42
  `,
43
43
  globalCss: `
44
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
45
+ on document.body while open (Zag re-enables it only on layers IT
46
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
47
+ inherited the lock, so every option was unclickable, hit-testing
48
+ right through to whatever real element sat behind it. */
44
49
  .crisp-select-listbox {
45
50
  font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
46
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
51
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
47
52
  max-height: 280px; overflow-y: auto; padding: ${c.space["1"]};
48
53
  border-radius: ${c.radius.xl}; background: ${c.bgPopover};
49
54
  box-shadow: ${e.floating};
@@ -56,6 +61,10 @@ const config = defineRecipe({
56
61
  .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
62
  .crisp-select-search input::placeholder { color: ${c.fgMuted}; }
58
63
  .crisp-select-empty { padding: ${c.space["3"]} ${c.space["2"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }
64
+ /* #343: same measured values as Menu's own group heading
65
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
66
+ same "small muted heading above a run of rows" treatment. */
67
+ .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
68
  .crisp-select-option {
60
69
  box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.menuRow}; flex-shrink: 0;
61
70
  padding: 0 ${c.space["2"]}; margin: 0; border: none; background: none; border-radius: ${c.radius.md};
@@ -36,6 +36,19 @@ export function RecordPanel({ record, columns, onClose, className, closeLabel =
36
36
  // both close on the same keypress, since neither knows about the other --
37
37
  // none of the five are built on this listener or registered anywhere it
38
38
  // can see, only on the shared floating-stack this checks instead.
39
+ //
40
+ // The listener has to run in the CAPTURE phase, not bubble (#376): Menu's
41
+ // own Escape handling is a React onKeyDown on its content div -- a bubble
42
+ // -phase synthetic handler, which fires (and, closing Menu, synchronously
43
+ // unregisters it from the floating stack) BEFORE a bubble-phase listener
44
+ // on `document` ever sees the event. `hasOpenFloatingLayer()` read AFTER
45
+ // that already reflects Menu's own reaction, not the state Escape actually
46
+ // arrived in, so it raced and lost every time -- confirmed by logging the
47
+ // registration order in a real browser, not by reasoning about it. Capture
48
+ // fires top-down, before Menu's bubble handler runs at all, so the check
49
+ // sees the state as it stood when the key was pressed. This mirrors how
50
+ // Zag's own `trackEscapeKeydown` (Dialog/Drawer's Escape handling) already
51
+ // listens on `document` in capture, for the same reason.
39
52
  React.useEffect(() => {
40
53
  if (!record)
41
54
  return;
@@ -44,8 +57,8 @@ export function RecordPanel({ record, columns, onClose, className, closeLabel =
44
57
  return;
45
58
  onClose();
46
59
  };
47
- document.addEventListener("keydown", onKey);
48
- return () => document.removeEventListener("keydown", onKey);
60
+ document.addEventListener("keydown", onKey, true);
61
+ return () => document.removeEventListener("keydown", onKey, true);
49
62
  }, [record, onClose]);
50
63
  if (!record)
51
64
  return null;
package/dist/menu.css CHANGED
@@ -1,7 +1,12 @@
1
1
 
2
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
3
+ on document.body while open (Zag re-enables it only on layers IT
4
+ tracks), and the portaled content is outside Zag's knowledge -- it
5
+ inherited the lock, so every item was unclickable, hit-testing right
6
+ through to whatever real element sat behind it. */
2
7
  .crisp-menu-content {
3
8
  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);
9
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
5
10
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
6
11
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
7
12
  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);
@@ -1,8 +1,13 @@
1
1
  @layer crisp-components {
2
2
 
3
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
4
+ on document.body while open (Zag re-enables it only on layers IT
5
+ tracks), and the portaled content is outside Zag's knowledge -- it
6
+ inherited the lock, so every item was unclickable, hit-testing right
7
+ through to whatever real element sat behind it. */
3
8
  .crisp-menu-content {
4
9
  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);
10
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
6
11
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
7
12
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
8
13
  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);
@@ -1,7 +1,12 @@
1
1
 
2
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
3
+ on document.body while open (Zag re-enables it only on layers IT
4
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
5
+ inherited the lock, so every option was unclickable, hit-testing
6
+ right through to whatever real element sat behind it. */
2
7
  .crisp-multiselect-listbox {
3
8
  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);
9
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
5
10
  max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1);
6
11
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
7
12
  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);
@@ -13,6 +18,10 @@
13
18
  .crisp-multiselect-search svg { flex-shrink: 0; }
14
19
  .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
20
  .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
21
+ /* #343: same measured values as Menu's own group heading
22
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
23
+ same "small muted heading above a run of rows" treatment. */
24
+ .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
25
  .crisp-multiselect-option {
17
26
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
18
27
  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;
@@ -1,8 +1,13 @@
1
1
  @layer crisp-components {
2
2
 
3
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
4
+ on document.body while open (Zag re-enables it only on layers IT
5
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
6
+ inherited the lock, so every option was unclickable, hit-testing
7
+ right through to whatever real element sat behind it. */
3
8
  .crisp-multiselect-listbox {
4
9
  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);
10
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
6
11
  max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1);
7
12
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
8
13
  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);
@@ -14,6 +19,10 @@
14
19
  .crisp-multiselect-search svg { flex-shrink: 0; }
15
20
  .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
21
  .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
22
+ /* #343: same measured values as Menu's own group heading
23
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
24
+ same "small muted heading above a run of rows" treatment. */
25
+ .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
26
  .crisp-multiselect-option {
18
27
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
19
28
  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/popover.css CHANGED
@@ -1,5 +1,10 @@
1
1
 
2
- .crisp-popover-content { font-family: var(--crisp-font-sans); 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); } }
2
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
3
+ on document.body while open (Zag re-enables it only on layers IT
4
+ tracks), and the portaled content is outside Zag's knowledge -- it
5
+ inherited the lock, unclickable, hit-testing right through to
6
+ whatever real element sat behind it. */
7
+ .crisp-popover-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; 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
8
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
4
9
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
5
10
 
@@ -1,6 +1,11 @@
1
1
  @layer crisp-components {
2
2
 
3
- .crisp-popover-content { font-family: var(--crisp-font-sans); 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); } }
3
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
4
+ on document.body while open (Zag re-enables it only on layers IT
5
+ tracks), and the portaled content is outside Zag's knowledge -- it
6
+ inherited the lock, unclickable, hit-testing right through to
7
+ whatever real element sat behind it. */
8
+ .crisp-popover-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; 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
9
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
5
10
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
6
11
 
package/dist/select.css CHANGED
@@ -1,7 +1,12 @@
1
1
 
2
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
3
+ on document.body while open (Zag re-enables it only on layers IT
4
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
5
+ inherited the lock, so every option was unclickable, hit-testing
6
+ right through to whatever real element sat behind it. */
2
7
  .crisp-select-listbox {
3
8
  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);
9
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
5
10
  max-height: 280px; overflow-y: auto; padding: var(--crisp-space-1);
6
11
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
7
12
  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);
@@ -14,6 +19,10 @@
14
19
  .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
20
  .crisp-select-search input::placeholder { color: var(--crisp-fg-muted); }
16
21
  .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); }
22
+ /* #343: same measured values as Menu's own group heading
23
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
24
+ same "small muted heading above a run of rows" treatment. */
25
+ .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
26
  .crisp-select-option {
18
27
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
19
28
  padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md);
@@ -1,8 +1,13 @@
1
1
  @layer crisp-components {
2
2
 
3
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
4
+ on document.body while open (Zag re-enables it only on layers IT
5
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
6
+ inherited the lock, so every option was unclickable, hit-testing
7
+ right through to whatever real element sat behind it. */
3
8
  .crisp-select-listbox {
4
9
  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);
10
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
6
11
  max-height: 280px; overflow-y: auto; padding: var(--crisp-space-1);
7
12
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
8
13
  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);
@@ -15,6 +20,10 @@
15
20
  .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
21
  .crisp-select-search input::placeholder { color: var(--crisp-fg-muted); }
17
22
  .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); }
23
+ /* #343: same measured values as Menu's own group heading
24
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
25
+ same "small muted heading above a run of rows" treatment. */
26
+ .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
27
  .crisp-select-option {
19
28
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
20
29
  padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md);
package/dist/styles.css CHANGED
@@ -459,7 +459,12 @@
459
459
  }
460
460
 
461
461
 
462
- .crisp-contextmenu-content { font-family: var(--crisp-font-sans); 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); } }
462
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
463
+ on document.body while open (Zag re-enables it only on layers IT
464
+ tracks), and the portaled content is outside Zag's knowledge -- it
465
+ inherited the lock, unclickable, hit-testing right through to
466
+ whatever real element sat behind it. */
467
+ .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; 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); } }
463
468
  .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; }
464
469
  .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
465
470
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
@@ -985,9 +990,14 @@
985
990
  }
986
991
 
987
992
 
993
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
994
+ on document.body while open (Zag re-enables it only on layers IT
995
+ tracks), and the portaled content is outside Zag's knowledge -- it
996
+ inherited the lock, so every item was unclickable, hit-testing right
997
+ through to whatever real element sat behind it. */
988
998
  .crisp-menu-content {
989
999
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
990
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1000
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
991
1001
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
992
1002
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
993
1003
  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);
@@ -1031,9 +1041,14 @@
1031
1041
  }
1032
1042
 
1033
1043
 
1044
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1045
+ on document.body while open (Zag re-enables it only on layers IT
1046
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
1047
+ inherited the lock, so every option was unclickable, hit-testing
1048
+ right through to whatever real element sat behind it. */
1034
1049
  .crisp-multiselect-listbox {
1035
1050
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1036
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1051
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1037
1052
  max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1);
1038
1053
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
1039
1054
  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);
@@ -1045,6 +1060,10 @@
1045
1060
  .crisp-multiselect-search svg { flex-shrink: 0; }
1046
1061
  .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
1062
  .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
1063
+ /* #343: same measured values as Menu's own group heading
1064
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
1065
+ same "small muted heading above a run of rows" treatment. */
1066
+ .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
1067
  .crisp-multiselect-option {
1049
1068
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
1050
1069
  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;
@@ -1091,7 +1110,12 @@
1091
1110
  }
1092
1111
 
1093
1112
 
1094
- .crisp-popover-content { font-family: var(--crisp-font-sans); 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); } }
1113
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1114
+ on document.body while open (Zag re-enables it only on layers IT
1115
+ tracks), and the portaled content is outside Zag's knowledge -- it
1116
+ inherited the lock, unclickable, hit-testing right through to
1117
+ whatever real element sat behind it. */
1118
+ .crisp-popover-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; 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); } }
1095
1119
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
1096
1120
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
1097
1121
 
@@ -1257,9 +1281,14 @@
1257
1281
  }
1258
1282
 
1259
1283
 
1284
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1285
+ on document.body while open (Zag re-enables it only on layers IT
1286
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
1287
+ inherited the lock, so every option was unclickable, hit-testing
1288
+ right through to whatever real element sat behind it. */
1260
1289
  .crisp-select-listbox {
1261
1290
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1262
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1291
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1263
1292
  max-height: 280px; overflow-y: auto; padding: var(--crisp-space-1);
1264
1293
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
1265
1294
  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);
@@ -1272,6 +1301,10 @@
1272
1301
  .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
1302
  .crisp-select-search input::placeholder { color: var(--crisp-fg-muted); }
1274
1303
  .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); }
1304
+ /* #343: same measured values as Menu's own group heading
1305
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
1306
+ same "small muted heading above a run of rows" treatment. */
1307
+ .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
1308
  .crisp-select-option {
1276
1309
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
1277
1310
  padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md);
@@ -460,7 +460,12 @@
460
460
  }
461
461
 
462
462
 
463
- .crisp-contextmenu-content { font-family: var(--crisp-font-sans); 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); } }
463
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
464
+ on document.body while open (Zag re-enables it only on layers IT
465
+ tracks), and the portaled content is outside Zag's knowledge -- it
466
+ inherited the lock, unclickable, hit-testing right through to
467
+ whatever real element sat behind it. */
468
+ .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; 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); } }
464
469
  .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; }
465
470
  .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
466
471
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
@@ -986,9 +991,14 @@
986
991
  }
987
992
 
988
993
 
994
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
995
+ on document.body while open (Zag re-enables it only on layers IT
996
+ tracks), and the portaled content is outside Zag's knowledge -- it
997
+ inherited the lock, so every item was unclickable, hit-testing right
998
+ through to whatever real element sat behind it. */
989
999
  .crisp-menu-content {
990
1000
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
991
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1001
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
992
1002
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
993
1003
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
994
1004
  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);
@@ -1032,9 +1042,14 @@
1032
1042
  }
1033
1043
 
1034
1044
 
1045
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1046
+ on document.body while open (Zag re-enables it only on layers IT
1047
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
1048
+ inherited the lock, so every option was unclickable, hit-testing
1049
+ right through to whatever real element sat behind it. */
1035
1050
  .crisp-multiselect-listbox {
1036
1051
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1037
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1052
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1038
1053
  max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1);
1039
1054
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
1040
1055
  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);
@@ -1046,6 +1061,10 @@
1046
1061
  .crisp-multiselect-search svg { flex-shrink: 0; }
1047
1062
  .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
1063
  .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
1064
+ /* #343: same measured values as Menu's own group heading
1065
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
1066
+ same "small muted heading above a run of rows" treatment. */
1067
+ .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
1068
  .crisp-multiselect-option {
1050
1069
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
1051
1070
  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;
@@ -1092,7 +1111,12 @@
1092
1111
  }
1093
1112
 
1094
1113
 
1095
- .crisp-popover-content { font-family: var(--crisp-font-sans); 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); } }
1114
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1115
+ on document.body while open (Zag re-enables it only on layers IT
1116
+ tracks), and the portaled content is outside Zag's knowledge -- it
1117
+ inherited the lock, unclickable, hit-testing right through to
1118
+ whatever real element sat behind it. */
1119
+ .crisp-popover-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; 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); } }
1096
1120
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
1097
1121
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
1098
1122
 
@@ -1258,9 +1282,14 @@
1258
1282
  }
1259
1283
 
1260
1284
 
1285
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1286
+ on document.body while open (Zag re-enables it only on layers IT
1287
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
1288
+ inherited the lock, so every option was unclickable, hit-testing
1289
+ right through to whatever real element sat behind it. */
1261
1290
  .crisp-select-listbox {
1262
1291
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1263
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1292
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1264
1293
  max-height: 280px; overflow-y: auto; padding: var(--crisp-space-1);
1265
1294
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
1266
1295
  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);
@@ -1273,6 +1302,10 @@
1273
1302
  .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
1303
  .crisp-select-search input::placeholder { color: var(--crisp-fg-muted); }
1275
1304
  .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); }
1305
+ /* #343: same measured values as Menu's own group heading
1306
+ (crisp-menu-label) -- reused rather than re-measured, since it's the
1307
+ same "small muted heading above a run of rows" treatment. */
1308
+ .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
1309
  .crisp-select-option {
1277
1310
  box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
1278
1311
  padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md);
@@ -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.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",