@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.
- package/dist/cjs/components/contextmenu/contextmenu.recipe.js +6 -1
- package/dist/cjs/components/menu/menu.recipe.js +6 -1
- package/dist/cjs/components/multiselect/multiselect.js +6 -1
- package/dist/cjs/components/multiselect/multiselect.recipe.js +10 -1
- package/dist/cjs/components/popover/popover.recipe.js +6 -1
- package/dist/cjs/components/select/select.js +6 -1
- package/dist/cjs/components/select/select.recipe.js +10 -1
- package/dist/cjs/product/recordpanel/recordpanel.js +15 -2
- package/dist/contextmenu.css +6 -1
- package/dist/contextmenu.layered.css +6 -1
- package/dist/esm/components/contextmenu/contextmenu.recipe.js +6 -1
- package/dist/esm/components/menu/menu.recipe.js +6 -1
- package/dist/esm/components/multiselect/multiselect.js +6 -1
- package/dist/esm/components/multiselect/multiselect.recipe.js +10 -1
- package/dist/esm/components/popover/popover.recipe.js +6 -1
- package/dist/esm/components/select/select.js +6 -1
- package/dist/esm/components/select/select.recipe.js +10 -1
- package/dist/esm/product/recordpanel/recordpanel.js +15 -2
- package/dist/menu.css +6 -1
- package/dist/menu.layered.css +6 -1
- package/dist/multiselect.css +10 -1
- package/dist/multiselect.layered.css +10 -1
- package/dist/popover.css +6 -1
- package/dist/popover.layered.css +6 -1
- package/dist/select.css +10 -1
- package/dist/select.layered.css +10 -1
- package/dist/styles.css +38 -5
- package/dist/styles.layered.css +38 -5
- package/dist/types/components/multiselect/multiselect.d.ts +8 -0
- package/dist/types/components/select/select.d.ts +8 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/contextmenu.css
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/menu.layered.css
CHANGED
|
@@ -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);
|
package/dist/multiselect.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-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
|
-
|
|
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
|
|
package/dist/popover.layered.css
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
@layer crisp-components {
|
|
2
2
|
|
|
3
|
-
|
|
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);
|
package/dist/select.layered.css
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/styles.layered.css
CHANGED
|
@@ -460,7 +460,12 @@
|
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
|
|
463
|
-
|
|
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
|
-
|
|
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[];
|