@crisp-ui-kit/crisp 0.55.6 → 0.57.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/callout.css +6 -0
- package/dist/callout.d.ts +2 -2
- package/dist/callout.layered.css +6 -0
- package/dist/cjs/components/callout/callout.js +2 -2
- package/dist/cjs/components/callout/callout.recipe.js +15 -2
- package/dist/cjs/components/input/input.recipe.js +6 -0
- package/dist/cjs/components/menu/menu.js +2 -2
- package/dist/cjs/components/menu/menu.recipe.js +8 -0
- package/dist/cjs/components/multiselect/multiselect.js +23 -10
- package/dist/cjs/components/multiselect/multiselect.recipe.js +95 -65
- package/dist/cjs/components/select/listbox-keyboard.js +7 -1
- package/dist/cjs/components/select/select.js +24 -11
- package/dist/cjs/components/select/select.recipe.js +150 -78
- package/dist/cjs/components/statusdot/index.js +7 -0
- package/dist/cjs/components/statusdot/statusdot.js +15 -0
- package/dist/cjs/components/statusdot/statusdot.recipe.js +34 -0
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +3 -3
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
- package/dist/cjs/components/tabs/tabs.js +63 -4
- package/dist/cjs/components/tabs/tabs.recipe.js +13 -0
- package/dist/cjs/components/text/text.recipe.js +10 -0
- package/dist/cjs/components/textlink/textlink.js +2 -2
- package/dist/cjs/components/textlink/textlink.recipe.js +18 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/product/recordpage/recordpage.js +17 -4
- package/dist/cjs/tokens/elevation.js +7 -0
- package/dist/cjs/tokens/palette.js +41 -0
- package/dist/cjs/tokens/scale.js +16 -1
- package/dist/cjs/tokens/semantic.js +18 -0
- package/dist/esm/components/callout/callout.js +2 -2
- package/dist/esm/components/callout/callout.recipe.js +15 -2
- package/dist/esm/components/input/input.recipe.js +6 -0
- package/dist/esm/components/menu/menu.js +2 -2
- package/dist/esm/components/menu/menu.recipe.js +8 -0
- package/dist/esm/components/multiselect/multiselect.js +23 -10
- package/dist/esm/components/multiselect/multiselect.recipe.js +95 -65
- package/dist/esm/components/select/listbox-keyboard.js +7 -1
- package/dist/esm/components/select/select.js +24 -11
- package/dist/esm/components/select/select.recipe.js +150 -78
- package/dist/esm/components/statusdot/index.js +2 -0
- package/dist/esm/components/statusdot/statusdot.js +12 -0
- package/dist/esm/components/statusdot/statusdot.recipe.js +31 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +3 -3
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
- package/dist/esm/components/tabs/tabs.js +63 -4
- package/dist/esm/components/tabs/tabs.recipe.js +13 -0
- package/dist/esm/components/text/text.recipe.js +10 -0
- package/dist/esm/components/textlink/textlink.js +2 -2
- package/dist/esm/components/textlink/textlink.recipe.js +18 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/product/recordpage/recordpage.js +18 -5
- package/dist/esm/tokens/elevation.js +7 -0
- package/dist/esm/tokens/palette.js +41 -0
- package/dist/esm/tokens/scale.js +16 -1
- package/dist/esm/tokens/semantic.js +18 -0
- package/dist/input.css +4 -0
- package/dist/input.d.ts +2 -2
- package/dist/input.layered.css +4 -0
- package/dist/menu.css +8 -0
- package/dist/menu.layered.css +8 -0
- package/dist/multiselect.css +53 -29
- package/dist/multiselect.d.ts +3 -1
- package/dist/multiselect.layered.css +53 -29
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +50 -7
- package/dist/select.css +80 -42
- package/dist/select.d.ts +3 -2
- package/dist/select.layered.css +80 -42
- package/dist/statusdot.css +3 -0
- package/dist/statusdot.d.ts +7 -0
- package/dist/statusdot.layered.css +5 -0
- package/dist/styles.css +238 -73
- package/dist/styles.layered.css +238 -73
- package/dist/tabletoolbar.css +8 -1
- package/dist/tabletoolbar.layered.css +8 -1
- package/dist/tabs.css +6 -0
- package/dist/tabs.layered.css +6 -0
- package/dist/text.css +6 -0
- package/dist/text.d.ts +2 -2
- package/dist/text.layered.css +6 -0
- package/dist/textlink.css +9 -0
- package/dist/textlink.d.ts +2 -1
- package/dist/textlink.layered.css +9 -0
- package/dist/tokens.css +54 -1
- package/dist/tokens.json +50 -7
- package/dist/types/components/callout/callout.d.ts +9 -1
- package/dist/types/components/callout/callout.recipe.d.ts +12 -2
- package/dist/types/components/input/input.d.ts +2 -0
- package/dist/types/components/input/input.recipe.d.ts +3 -0
- package/dist/types/components/menu/menu.d.ts +7 -1
- package/dist/types/components/multiselect/multiselect.d.ts +24 -6
- package/dist/types/components/multiselect/multiselect.recipe.d.ts +24 -0
- package/dist/types/components/select/listbox-keyboard.d.ts +10 -1
- package/dist/types/components/select/select.d.ts +28 -6
- package/dist/types/components/select/select.recipe.d.ts +22 -10
- package/dist/types/components/statusdot/index.d.ts +2 -0
- package/dist/types/components/statusdot/statusdot.d.ts +21 -0
- package/dist/types/components/statusdot/statusdot.recipe.d.ts +15 -0
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +14 -0
- package/dist/types/components/tabs/tabs.d.ts +9 -1
- package/dist/types/components/text/text.d.ts +4 -0
- package/dist/types/components/text/text.recipe.d.ts +12 -0
- package/dist/types/components/textlink/textlink.d.ts +8 -1
- package/dist/types/components/textlink/textlink.recipe.d.ts +10 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/product/recordpage/recordpage.d.ts +25 -1
- package/dist/types/tokens/contract.d.ts +13 -3
- package/dist/types/tokens/elevation.d.ts +7 -0
- package/dist/types/tokens/palette.d.ts +36 -0
- package/dist/types/tokens/scale.d.ts +6 -1
- package/dist/types/tokens/semantic.d.ts +18 -0
- package/package.json +6 -1
package/dist/callout.css
CHANGED
|
@@ -14,3 +14,9 @@
|
|
|
14
14
|
&[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
|
|
15
15
|
&[data-tone="danger"] .crisp-callout-icon { color: var(--crisp-bg-danger); }
|
|
16
16
|
}
|
|
17
|
+
.crisp-callout--bordered_true {
|
|
18
|
+
border: 1px solid var(--crisp-ring-brand-soft); border-radius: var(--crisp-radius-xl); padding: var(--crisp-space-2) var(--crisp-space-3); background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent);
|
|
19
|
+
}
|
|
20
|
+
.crisp-callout--bordered_false {
|
|
21
|
+
|
|
22
|
+
}
|
package/dist/callout.d.ts
CHANGED
package/dist/callout.layered.css
CHANGED
|
@@ -14,5 +14,11 @@
|
|
|
14
14
|
&[data-tone="warning"] .crisp-callout-icon { color: var(--crisp-bg-warning); }
|
|
15
15
|
&[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
|
|
16
16
|
&[data-tone="danger"] .crisp-callout-icon { color: var(--crisp-bg-danger); }
|
|
17
|
+
}
|
|
18
|
+
.crisp-callout--bordered_true {
|
|
19
|
+
border: 1px solid var(--crisp-ring-brand-soft); border-radius: var(--crisp-radius-xl); padding: var(--crisp-space-2) var(--crisp-space-3); background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent);
|
|
20
|
+
}
|
|
21
|
+
.crisp-callout--bordered_false {
|
|
22
|
+
|
|
17
23
|
}
|
|
18
24
|
}
|
|
@@ -15,11 +15,11 @@ const DEFAULT_ICONS = {
|
|
|
15
15
|
* An inline callout / banner — measured from info notices. `tone` tints
|
|
16
16
|
* the background and icon; pass a `title` and/or body `children`.
|
|
17
17
|
*/
|
|
18
|
-
function Callout({ tone = "neutral", icon, title, children, action, className, ...rest }) {
|
|
18
|
+
function Callout({ tone = "neutral", icon, title, children, action, bordered, className, ...rest }) {
|
|
19
19
|
const Fallback = DEFAULT_ICONS[tone];
|
|
20
20
|
const iconNode = icon === undefined
|
|
21
21
|
? Fallback && (0, jsx_runtime_1.jsx)(Fallback, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" })
|
|
22
22
|
: icon;
|
|
23
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, callout_recipe_1.calloutRecipe)({ className }), ...rest, "data-tone": tone, role: "note", children: [iconNode && (0, jsx_runtime_1.jsx)("span", { className: "crisp-callout-icon", children: iconNode }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-callout-body", children: [title != null && (0, jsx_runtime_1.jsx)("div", { className: "crisp-callout-title", children: title }), children != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-callout-desc", children: children }))] }), action && (0, jsx_runtime_1.jsx)("div", { className: "crisp-callout-action", children: action })] }));
|
|
23
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, callout_recipe_1.calloutRecipe)({ bordered, className }), ...rest, "data-tone": tone, role: "note", children: [iconNode && (0, jsx_runtime_1.jsx)("span", { className: "crisp-callout-icon", children: iconNode }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-callout-body", children: [title != null && (0, jsx_runtime_1.jsx)("div", { className: "crisp-callout-title", children: title }), children != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-callout-desc", children: children }))] }), action && (0, jsx_runtime_1.jsx)("div", { className: "crisp-callout-action", children: action })] }));
|
|
24
24
|
}
|
|
25
25
|
Callout.displayName = "Callout";
|
|
@@ -21,8 +21,21 @@ const config = (0, core_1.defineRecipe)({
|
|
|
21
21
|
&[data-tone="warning"] .crisp-callout-icon { color: ${tokens_1.contract.bgWarning}; }
|
|
22
22
|
&[data-tone="danger"] { background: color-mix(in srgb, ${tokens_1.contract.bgDanger} 10%, transparent); }
|
|
23
23
|
&[data-tone="danger"] .crisp-callout-icon { color: ${tokens_1.contract.bgDanger}; }`,
|
|
24
|
-
variants: {
|
|
25
|
-
|
|
24
|
+
variants: {
|
|
25
|
+
// crisp#869: a page-level banner look, measured live on Attio's Objects
|
|
26
|
+
// settings page (the "Create custom objects... Upgrade" Pro-plan
|
|
27
|
+
// nudge) — a real 1px opaque border, 12px radius, 8px/12px padding, and
|
|
28
|
+
// a 14%-brand-tinted background (the same `color-mix` ratio Badge's own
|
|
29
|
+
// soft-brand variant already uses). Only this one brand/info-toned
|
|
30
|
+
// instance was found live; `bordered` doesn't attempt a per-tone border
|
|
31
|
+
// colour for success/warning/danger — no such Attio banner was found to
|
|
32
|
+
// measure one against.
|
|
33
|
+
bordered: {
|
|
34
|
+
true: `border: 1px solid ${tokens_1.contract.ringBrandSoft}; border-radius: ${tokens_1.contract.radius.xl}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 14%, transparent);`,
|
|
35
|
+
false: "",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: { bordered: false },
|
|
26
39
|
compoundVariants: [],
|
|
27
40
|
});
|
|
28
41
|
exports.calloutRecipe = (0, core_1.resolveRecipe)(config);
|
|
@@ -44,6 +44,12 @@ const config = (0, core_1.defineRecipe)({
|
|
|
44
44
|
`,
|
|
45
45
|
variants: {
|
|
46
46
|
size: {
|
|
47
|
+
// crisp#878: not a fresh click-measurement -- reuses `TableToolbar`'s
|
|
48
|
+
// own compact search box shape (`tabletoolbar.chrome.ts`'s `-search`
|
|
49
|
+
// rule), already corroborated against Attio, at the same height as
|
|
50
|
+
// `c.size.row` ("nav item · search · control height"). Same radius and
|
|
51
|
+
// padding `medium` already uses, just 4px shorter.
|
|
52
|
+
sm: `height: ${tokens_1.contract.size.row}; border-radius: ${tokens_1.contract.radius.md}; & .crisp-input-value { padding-inline: ${tokens_1.contract.space["2.5"]}; }`,
|
|
47
53
|
medium: `height: 32px; border-radius: ${tokens_1.contract.radius.md}; & .crisp-input-value { padding-inline: ${tokens_1.contract.space["2.5"]}; }`,
|
|
48
54
|
large: `height: 36px; border-radius: 10px; & .crisp-input-value { padding-inline: ${tokens_1.contract.space["3"]}; }`,
|
|
49
55
|
},
|
|
@@ -168,13 +168,13 @@ function Menu({ trigger, children, align = "start", open: openProp, defaultOpen,
|
|
|
168
168
|
: "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: (0, jsx_runtime_1.jsx)(MenuContext.Provider, { value: { close }, children: children }) }) }))] }));
|
|
169
169
|
}
|
|
170
170
|
Menu.displayName = "Menu";
|
|
171
|
-
function MenuItem({ children, icon, shortcut, danger, disabled, onSelect, closeOnSelect = true, }) {
|
|
171
|
+
function MenuItem({ children, icon, shortcut, count, danger, disabled, onSelect, closeOnSelect = true, }) {
|
|
172
172
|
const ctx = React.useContext(MenuContext);
|
|
173
173
|
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", tabIndex: -1, disabled: disabled, "data-disabled": disabled ? "" : undefined, className: `crisp-menu-item${danger ? " crisp-menu-item--danger" : ""}`, onClick: () => {
|
|
174
174
|
onSelect?.();
|
|
175
175
|
if (closeOnSelect)
|
|
176
176
|
ctx?.close();
|
|
177
|
-
}, children: [icon, (0, jsx_runtime_1.jsx)("span", { className: "crisp-menu-item-label", children: children }), shortcut && (0, jsx_runtime_1.jsx)("span", { className: "crisp-menu-item-shortcut", children: shortcut })] }));
|
|
177
|
+
}, children: [icon, (0, jsx_runtime_1.jsx)("span", { className: "crisp-menu-item-label", children: children }), count != null && (0, jsx_runtime_1.jsx)("span", { className: "crisp-menu-item-count", children: count }), shortcut && (0, jsx_runtime_1.jsx)("span", { className: "crisp-menu-item-shortcut", children: shortcut })] }));
|
|
178
178
|
}
|
|
179
179
|
MenuItem.displayName = "MenuItem";
|
|
180
180
|
function MenuCheckboxItem({ children, checked, onCheckedChange, disabled, }) {
|
|
@@ -57,6 +57,14 @@ const config = (0, core_1.defineRecipe)({
|
|
|
57
57
|
.crisp-menu-item--danger svg { color: ${tokens_1.contract.bgDanger}; }
|
|
58
58
|
.crisp-menu-item--danger:hover, .crisp-menu-item--danger:focus-visible, .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, ${tokens_1.contract.bgDanger} 10%, transparent); }
|
|
59
59
|
.crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
60
|
+
/* crisp#875: same measured neutral chip TableToolbar's own ToolbarTool
|
|
61
|
+
count prop uses (re-confirmed on two live Attio surfaces this round:
|
|
62
|
+
the Sort tool's own "+N" overflow, a Kanban board's stage-column
|
|
63
|
+
count) -- a small badge, not bare text. */
|
|
64
|
+
.crisp-menu-item-count { flex-shrink: 0; margin-inline-start: ${tokens_1.contract.space["1.5"]}; display: inline-flex; align-items: center; height: 16px; box-sizing: border-box;
|
|
65
|
+
padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent);
|
|
66
|
+
box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgNeutralSoft};
|
|
67
|
+
font-variant-numeric: tabular-nums; }
|
|
60
68
|
.crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: ${tokens_1.contract.space["3"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; letter-spacing: 0.02em; }
|
|
61
69
|
.crisp-menu-item-check { flex-shrink: 0; color: ${tokens_1.contract.bgBrandSolid}; }
|
|
62
70
|
.crisp-menu-item-check--placeholder { width: 15px; }
|
|
@@ -60,16 +60,18 @@ function Chip({ opt }) {
|
|
|
60
60
|
return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-tag", "data-tone": opt.tone ?? toneFor(opt.value), children: opt.label }));
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* A multi-select / tag editor
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
63
|
+
* A multi-select / tag editor, its trigger/listbox/item chrome built to the
|
|
64
|
+
* same seed-design parity as `Select` (crisp/#882, following #867/#879) —
|
|
65
|
+
* its own chip/checkbox picker behaviour is unchanged. The outlined trigger
|
|
66
|
+
* shows the selected values as coloured chips; it opens a popover with a
|
|
67
|
+
* search field over rows of "[chip] … [checkbox]" that toggle on click (the
|
|
68
|
+
* menu stays open). Keyboard: ↑/↓ move, Enter/Space toggle, Esc closes.
|
|
67
69
|
*/
|
|
68
70
|
function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, required,
|
|
69
71
|
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
70
72
|
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
71
73
|
// above this control named nothing (#247). Select already did this.
|
|
72
|
-
id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, ...rest }) {
|
|
74
|
+
id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, size = "medium", variant = "default", className, "aria-label": ariaLabel, ...rest }) {
|
|
73
75
|
// crisp/#471: same resolution order as Select's own.
|
|
74
76
|
const placeholder = (0, core_1.useCrispMessage)(placeholderProp, "Select…");
|
|
75
77
|
const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search…");
|
|
@@ -106,8 +108,12 @@ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPl
|
|
|
106
108
|
const filtered = searchable && q
|
|
107
109
|
? options.filter((o) => o.label.toLowerCase().includes(q))
|
|
108
110
|
: options;
|
|
111
|
+
// seed-design's `interactive` (crisp/#882, matching Select): disabled AND
|
|
112
|
+
// readOnly both refuse to open, but only `disabled` greys the text and
|
|
113
|
+
// removes the button from the tab order.
|
|
114
|
+
const interactive = !disabled && !readOnly;
|
|
109
115
|
const openMenu = () => {
|
|
110
|
-
if (
|
|
116
|
+
if (!interactive)
|
|
111
117
|
return;
|
|
112
118
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
113
119
|
if (rect) {
|
|
@@ -116,7 +122,7 @@ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPl
|
|
|
116
122
|
// direction that is (#210)
|
|
117
123
|
const { style, up: flipUp } = (0, core_1.placeFloating)({
|
|
118
124
|
rect,
|
|
119
|
-
gap: 4
|
|
125
|
+
gap: 8, // seed-design's gutter (crisp/#882, matching Select; was 4)
|
|
120
126
|
flipBelow: 300,
|
|
121
127
|
anchor: triggerRef.current,
|
|
122
128
|
});
|
|
@@ -161,6 +167,10 @@ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPl
|
|
|
161
167
|
close,
|
|
162
168
|
// Tab is already moving focus out; `close` would drag it back to the trigger
|
|
163
169
|
dismiss: () => setOpen(false),
|
|
170
|
+
// Space also toggles the highlighted option (crisp/#882, matching
|
|
171
|
+
// Select's Enter/Space parity) when the trigger itself owns navigation.
|
|
172
|
+
// Never when searchable -- see Select's own comment on this option.
|
|
173
|
+
commitOnSpace: !searchable,
|
|
164
174
|
});
|
|
165
175
|
// Registers the (now-portaled) listbox on the shared floating-layer stack
|
|
166
176
|
// (#339) so this doesn't get closed out from under itself when nested
|
|
@@ -198,11 +208,14 @@ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPl
|
|
|
198
208
|
document.addEventListener("keydown", onKey);
|
|
199
209
|
return () => document.removeEventListener("keydown", onKey);
|
|
200
210
|
}, [open, close]);
|
|
201
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ invalid, className }), ...rest, children: [name &&
|
|
202
|
-
(value.length > 0 ? value : required ? [""] : []).map((v) => ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, id: id, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
211
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ size, variant, invalid, className }), ...rest, children: [name &&
|
|
212
|
+
(value.length > 0 ? value : required ? [""] : []).map((v) => ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, id: id, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
213
|
+
if (!interactive)
|
|
214
|
+
return;
|
|
203
215
|
if (open && !searchable)
|
|
204
216
|
onNavKeys(e);
|
|
205
|
-
else if (!open &&
|
|
217
|
+
else if (!open &&
|
|
218
|
+
(e.key === "ArrowDown" || e.key === "Enter" || e.key === " ")) {
|
|
206
219
|
e.preventDefault();
|
|
207
220
|
openMenu();
|
|
208
221
|
}
|
|
@@ -3,31 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.multiselectRecipe = void 0;
|
|
4
4
|
const core_1 = require("../../core");
|
|
5
5
|
const tokens_1 = require("../../tokens");
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// (
|
|
27
|
-
// that helper is written `& .crisp-recordstable-tag { ... }` for nesting
|
|
28
|
-
// under a recipe's `base`, which doesn't compile to anything inside a flat
|
|
29
|
-
// `globalCss` block. Keep the two in sync by hand (#380 added the measured
|
|
30
|
-
// `box-shadow: inset 0 0 0 1px <tone ring colour>` here to match).
|
|
6
|
+
// MultiSelect reads as the same family as Select (crisp/#882, following
|
|
7
|
+
// #867/#879): the trigger/listbox/item chrome below reuses Select's
|
|
8
|
+
// seed-design tokens verbatim (`strokeSelectTrigger`, `bgSelectPressed`,
|
|
9
|
+
// `bgSelectDisabled`, `strokeSelectInvalid`, `bgSelectListbox`,
|
|
10
|
+
// `selectFloating`, `fgSelectNeutral`, `fgSelectPlaceholder`, `radius["3xl"]`,
|
|
11
|
+
// `size.selectTriggerMd/Lg`) rather than resolving a second seed spec --
|
|
12
|
+
// seed-design has no multi-value combobox of its own (`packages/rootage/
|
|
13
|
+
// components/` has no `combobox`/`multiselect`; `chip.yaml`/`tag-group*.yaml`
|
|
14
|
+
// are a different, standalone pill pattern, not a field's multi-value
|
|
15
|
+
// trigger). What stays untouched: the `.crisp-recordstable-tag` chip
|
|
16
|
+
// styling (shared with RecordsTable/DataTable elsewhere), the checkbox-style
|
|
17
|
+
// selected indicator, and the toggle-without-closing interaction.
|
|
18
|
+
//
|
|
19
|
+
// The search row's geometry (36px height, 6px gap, no divider) is an
|
|
20
|
+
// Attio measurement (#481) `build/spec-check.mjs` guards directly -- it is
|
|
21
|
+
// NOT part of the seed-parity change and must not move. Only its text/icon
|
|
22
|
+
// colours follow the rest of this file onto the shared tokens.
|
|
23
|
+
//
|
|
24
|
+
// The listbox portals to `document.body` (#339), so its rules live in
|
|
25
|
+
// `globalCss`, not nested under `&` -- see Select's own recipe for the full
|
|
26
|
+
// reasoning (identical here).
|
|
31
27
|
const config = (0, core_1.defineRecipe)({
|
|
32
28
|
name: "multiselect",
|
|
33
29
|
base: `
|
|
@@ -35,53 +31,63 @@ const config = (0, core_1.defineRecipe)({
|
|
|
35
31
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
36
32
|
|
|
37
33
|
& .crisp-multiselect-trigger {
|
|
38
|
-
box-sizing: border-box; display: flex; align-items: center;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
transition: background-color .12s ease, box-shadow .12s ease;
|
|
34
|
+
box-sizing: border-box; display: flex; align-items: center; width: 100%; min-width: 0; margin: 0;
|
|
35
|
+
border: 1px solid ${tokens_1.contract.strokeSelectTrigger}; background: transparent; cursor: pointer; text-align: start;
|
|
36
|
+
font-family: inherit; color: ${tokens_1.contract.fgSelectNeutral};
|
|
37
|
+
transition: background-color .15s cubic-bezier(.35,0,.35,1), border-color .1s cubic-bezier(.35,0,.35,1), border-width .1s cubic-bezier(.35,0,.35,1);
|
|
43
38
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
& .crisp-multiselect-trigger:disabled {
|
|
39
|
+
/* Same pressed/disabled/readonly/focus model as Select's trigger -- see
|
|
40
|
+
that recipe's comment for why hover and an open/focus ring are gone. */
|
|
41
|
+
& .crisp-multiselect-trigger:active:not(:disabled):not([aria-readonly="true"]) { background: ${tokens_1.contract.bgSelectPressed}; }
|
|
42
|
+
& .crisp-multiselect-trigger:focus-visible { outline: 2px solid ${tokens_1.contract.strokeFocusRing}; outline-offset: -2px; }
|
|
43
|
+
& .crisp-multiselect-trigger:disabled { background: ${tokens_1.contract.bgSelectDisabled}; cursor: not-allowed; }
|
|
44
|
+
& .crisp-multiselect-trigger:disabled .crisp-multiselect-placeholder,
|
|
45
|
+
& .crisp-multiselect-trigger:disabled .crisp-multiselect-chevron { color: ${tokens_1.contract.fgDisabled}; }
|
|
46
|
+
& .crisp-multiselect-trigger[aria-readonly="true"] { background: ${tokens_1.contract.bgSelectDisabled}; cursor: default; }
|
|
47
47
|
& .crisp-multiselect-chips { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; }
|
|
48
|
-
& .crisp-multiselect-placeholder { color: ${tokens_1.contract.
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
& .crisp-multiselect-placeholder { color: ${tokens_1.contract.fgSelectPlaceholder}; }
|
|
49
|
+
/* closeRotateDuration/openRotateDuration, same asymmetric timing as
|
|
50
|
+
Select's own chevron (see that recipe's comment). */
|
|
51
|
+
& .crisp-multiselect-chevron { flex-shrink: 0; color: ${tokens_1.contract.fgNeutralMuted}; transition: transform .1s cubic-bezier(.35,0,.35,1); }
|
|
52
|
+
& .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); transition-duration: .15s; }
|
|
51
53
|
`,
|
|
52
54
|
globalCss: `
|
|
53
|
-
/* pointer-events: auto (#375)
|
|
54
|
-
on document.body while open (Zag re-enables it only on layers IT
|
|
55
|
-
tracks), and the portaled listbox is outside Zag's knowledge -- it
|
|
56
|
-
inherited the lock, so every option was unclickable, hit-testing
|
|
57
|
-
right through to whatever real element sat behind it. */
|
|
55
|
+
/* pointer-events: auto (#375) -- see Select's matching comment. */
|
|
58
56
|
.crisp-multiselect-listbox {
|
|
59
57
|
font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
60
|
-
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space
|
|
61
|
-
max-height:
|
|
62
|
-
border-radius: ${tokens_1.contract.radius
|
|
63
|
-
box-shadow: ${tokens_1.elevation.
|
|
64
|
-
transform-origin: top left; opacity: 1; transform: none;
|
|
58
|
+
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]};
|
|
59
|
+
max-height: 480px; overflow-y: auto; padding: ${tokens_1.contract.space["2"]};
|
|
60
|
+
border-radius: ${tokens_1.contract.radius["3xl"]}; background: ${tokens_1.contract.bgSelectListbox};
|
|
61
|
+
box-shadow: ${tokens_1.elevation.selectFloating};
|
|
62
|
+
transform-origin: top left; opacity: 1; transform: none;
|
|
63
|
+
transition: opacity .15s cubic-bezier(0,0,.15,1), transform .15s cubic-bezier(0,0,.15,1);
|
|
65
64
|
}
|
|
66
65
|
.crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
|
|
67
|
-
.crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.
|
|
68
|
-
|
|
66
|
+
.crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
|
|
67
|
+
/* LOCKED geometry (#481, guarded by build/spec-check.mjs): height 36px,
|
|
68
|
+
gap space["1.5"] (6px), no border-bottom -- an Attio measurement, not
|
|
69
|
+
part of this seed-parity pass. Only colours below follow Select. */
|
|
70
|
+
.crisp-multiselect-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 36px; flex-shrink: 0; margin: calc(-1 * ${tokens_1.contract.space["2"]}) calc(-1 * ${tokens_1.contract.space["2"]}) ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; color: ${tokens_1.contract.fgNeutralMuted}; }
|
|
69
71
|
.crisp-multiselect-search svg { flex-shrink: 0; }
|
|
70
|
-
.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.
|
|
71
|
-
.crisp-multiselect-search input::placeholder { color: ${tokens_1.contract.
|
|
72
|
-
/*
|
|
73
|
-
|
|
74
|
-
same "small muted heading above a run of rows" treatment. */
|
|
75
|
-
.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}; }
|
|
72
|
+
.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.fgSelectNeutral}; }
|
|
73
|
+
.crisp-multiselect-search input::placeholder { color: ${tokens_1.contract.fgSelectPlaceholder}; }
|
|
74
|
+
/* Same groupLabel treatment as Select's (13px/regular/neutral-subtle). */
|
|
75
|
+
.crisp-multiselect-label { padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["4"]} 0; font-size: ${tokens_1.contract.text.md}; line-height: 18px; font-weight: ${tokens_1.contract.weight.regular}; color: ${tokens_1.contract.fgNeutralSubtle}; }
|
|
76
76
|
.crisp-multiselect-option {
|
|
77
|
-
box-sizing: border-box; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]};
|
|
78
|
-
padding:
|
|
77
|
+
box-sizing: border-box; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex-shrink: 0;
|
|
78
|
+
padding: ${tokens_1.contract.space["2.5"]} ${tokens_1.contract.space["4"]}; margin: 0; border: none; background: none; border-radius: ${tokens_1.contract.radius.xl};
|
|
79
|
+
font-family: inherit; text-align: start; cursor: pointer;
|
|
80
|
+
transition: background-color .15s cubic-bezier(.35,0,.35,1);
|
|
79
81
|
}
|
|
82
|
+
/* Keyboard/hover highlight keeps crisp's own visible neutral fill, same
|
|
83
|
+
reasoning as Select's matching rule (seed's own pressed tint is too
|
|
84
|
+
faint for a keyboard-navigation indicator). */
|
|
80
85
|
.crisp-multiselect-option[data-highlighted] { background: ${tokens_1.contract.bgRaised}; }
|
|
86
|
+
.crisp-multiselect-option:active { background: ${tokens_1.contract.bgSelectPressed}; }
|
|
81
87
|
.crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
|
|
82
88
|
.crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: ${tokens_1.contract.radius.sm}; box-shadow: inset 0 0 0 1.5px ${tokens_1.contract.borderDefault}; color: transparent; }
|
|
83
89
|
.crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: ${tokens_1.contract.bgBrandSolid}; box-shadow: none; color: #fff; }
|
|
84
|
-
.crisp-multiselect-empty { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["
|
|
90
|
+
.crisp-multiselect-empty { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["4"]}; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgNeutralSubtle}; }
|
|
85
91
|
|
|
86
92
|
.crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.chip}; font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 82%, transparent); box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; }
|
|
87
93
|
.crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 14%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 82%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 30%, transparent); }
|
|
@@ -93,18 +99,42 @@ const config = (0, core_1.defineRecipe)({
|
|
|
93
99
|
.crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${tokens_1.contract.bgOrange} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgOrange} 82%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgOrange} 30%, transparent); }
|
|
94
100
|
`,
|
|
95
101
|
variants: {
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
& .crisp-multiselect-trigger:
|
|
101
|
-
& .crisp-multiselect-
|
|
102
|
-
|
|
102
|
+
// seed's two trigger sizes, same tokens as Select's. medium is the
|
|
103
|
+
// default -- closest to crisp's previous single size.
|
|
104
|
+
size: {
|
|
105
|
+
medium: `
|
|
106
|
+
& .crisp-multiselect-trigger { min-height: ${tokens_1.contract.size.selectTriggerMd}; border-radius: ${tokens_1.contract.radius.md}; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["3.5"]}; }
|
|
107
|
+
& .crisp-multiselect-chevron { width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; }
|
|
108
|
+
`,
|
|
109
|
+
large: `
|
|
110
|
+
& .crisp-multiselect-trigger { min-height: ${tokens_1.contract.size.selectTriggerLg}; border-radius: ${tokens_1.contract.radius.xl}; gap: ${tokens_1.contract.space["2.5"]}; padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["4"]}; }
|
|
111
|
+
& .crisp-multiselect-chevron { width: ${tokens_1.contract.size.selectIconLg}; height: ${tokens_1.contract.size.selectIconLg}; }
|
|
103
112
|
`,
|
|
113
|
+
},
|
|
114
|
+
// Real border width+colour change, same as Select's -- see that
|
|
115
|
+
// recipe's comment.
|
|
116
|
+
invalid: {
|
|
117
|
+
true: `& .crisp-multiselect-trigger { border-width: 2px; border-color: ${tokens_1.contract.strokeSelectInvalid}; }`,
|
|
104
118
|
false: ``,
|
|
105
119
|
},
|
|
120
|
+
// ghost: fills whatever row it's embedded in instead of seed's fixed
|
|
121
|
+
// trigger height -- RecordsTable's TagsCell embeds a MultiSelect inline
|
|
122
|
+
// in a 36px table row, the exact regression #879 caught for Select's
|
|
123
|
+
// own datatable consumers. Not a seed state; crisp's own escape hatch.
|
|
124
|
+
//
|
|
125
|
+
// Declared LAST on purpose, same reasoning as Select's recipe: same-
|
|
126
|
+
// specificity CSS rules resolve by source order, and ghost's
|
|
127
|
+
// `min-height: 0; height: 100%` below must come after `size`'s fixed
|
|
128
|
+
// min-height to win the cascade.
|
|
129
|
+
variant: {
|
|
130
|
+
default: "",
|
|
131
|
+
ghost: `
|
|
132
|
+
& .crisp-multiselect-trigger { min-height: 0; height: 100%; border-color: transparent; }
|
|
133
|
+
& .crisp-multiselect-trigger:hover:not(:disabled) { background: ${tokens_1.contract.bgSelectPressed}; }
|
|
134
|
+
`,
|
|
135
|
+
},
|
|
106
136
|
},
|
|
107
|
-
defaultVariants: { invalid: false },
|
|
137
|
+
defaultVariants: { size: "medium", invalid: false, variant: "default" },
|
|
108
138
|
compoundVariants: [],
|
|
109
139
|
});
|
|
110
140
|
exports.multiselectRecipe = Object.assign((0, core_1.resolveRecipe)(config), {
|
|
@@ -14,7 +14,7 @@ exports.useListboxKeyboard = useListboxKeyboard;
|
|
|
14
14
|
* NOT for `WheelPicker`: that is an always-visible multi-column wheel that
|
|
15
15
|
* commits on move, so Enter, Escape and Tab have no meaning in it.
|
|
16
16
|
*/
|
|
17
|
-
function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, close, dismiss = close, }) {
|
|
17
|
+
function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, close, dismiss = close, commitOnSpace = false, }) {
|
|
18
18
|
const move = (dir) => {
|
|
19
19
|
if (enabledIndexes.length === 0)
|
|
20
20
|
return;
|
|
@@ -53,6 +53,12 @@ function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, c
|
|
|
53
53
|
e.preventDefault();
|
|
54
54
|
commit();
|
|
55
55
|
break;
|
|
56
|
+
case " ":
|
|
57
|
+
if (!commitOnSpace)
|
|
58
|
+
break;
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
commit();
|
|
61
|
+
break;
|
|
56
62
|
case "Escape":
|
|
57
63
|
e.preventDefault();
|
|
58
64
|
close();
|
|
@@ -46,13 +46,16 @@ function optionText(o) {
|
|
|
46
46
|
return typeof o.label === "string" ? o.label : o.value;
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
* A select / dropdown
|
|
50
|
-
*
|
|
51
|
-
* `
|
|
52
|
-
*
|
|
53
|
-
*
|
|
49
|
+
* A select / dropdown, built to seed-design's Select (crisp/#867) rather than
|
|
50
|
+
* measured from Attio — a deliberate one-component exception, the same way
|
|
51
|
+
* `DatePicker` follows seed's calendar structure — with an optional
|
|
52
|
+
* Linear-style search seed itself has no equivalent of. A `role="combobox"`
|
|
53
|
+
* trigger opens a `role="listbox"`; pass `searchable` to add a filter field
|
|
54
|
+
* at the top (with a "No results" empty state). Full keyboard model — ↑/↓
|
|
55
|
+
* move, Enter/Space pick, Esc close, Home/End jump, opens upward when tight.
|
|
56
|
+
* Controlled or uncontrolled.
|
|
54
57
|
*/
|
|
55
|
-
function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
58
|
+
function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", size = "medium", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
56
59
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
57
60
|
// CrispMessagesProvider's translation of the English default, or that
|
|
58
61
|
// default itself with no provider — unchanged from before this existed.
|
|
@@ -99,8 +102,12 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
99
102
|
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
100
103
|
? optionId(highlight)
|
|
101
104
|
: undefined;
|
|
105
|
+
// seed-design's `interactive` (crisp/#867): disabled AND readOnly both
|
|
106
|
+
// refuse to open, but only `disabled` removes the button from the tab
|
|
107
|
+
// order and greys its text — readOnly stays focusable, full-strength ink.
|
|
108
|
+
const interactive = !disabled && !readOnly;
|
|
102
109
|
const openMenu = () => {
|
|
103
|
-
if (
|
|
110
|
+
if (!interactive)
|
|
104
111
|
return;
|
|
105
112
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
106
113
|
if (rect) {
|
|
@@ -109,7 +116,7 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
109
116
|
// direction that is (#210)
|
|
110
117
|
const { style, up: flipUp } = (0, core_1.placeFloating)({
|
|
111
118
|
rect,
|
|
112
|
-
gap: 4
|
|
119
|
+
gap: 8, // seed-design's select.yaml `gutter` (crisp/#867; was 4)
|
|
113
120
|
flipBelow: 260,
|
|
114
121
|
anchor: triggerRef.current,
|
|
115
122
|
});
|
|
@@ -143,8 +150,14 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
143
150
|
commit: () => pick(filtered[highlight]),
|
|
144
151
|
close,
|
|
145
152
|
dismiss: () => setOpen(false),
|
|
153
|
+
// Space also commits when the trigger itself owns navigation (crisp/#867,
|
|
154
|
+
// matching seed-design). Never on the search input's own wiring below —
|
|
155
|
+
// that one omits this option, so Space stays a normal character there.
|
|
156
|
+
commitOnSpace: !searchable,
|
|
146
157
|
});
|
|
147
158
|
const onTriggerKeyDown = (e) => {
|
|
159
|
+
if (!interactive)
|
|
160
|
+
return;
|
|
148
161
|
if (!open) {
|
|
149
162
|
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
|
150
163
|
e.preventDefault();
|
|
@@ -201,10 +214,10 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
201
214
|
document.addEventListener("keydown", onKey);
|
|
202
215
|
return () => document.removeEventListener("keydown", onKey);
|
|
203
216
|
}, [open, close]);
|
|
204
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, invalid, className }), ...rest, children: [name && (selected || required) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
217
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, size, invalid, className }), ...rest, children: [name && (selected || required) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
205
218
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
206
219
|
// validation, which is the only reason this input is not hidden
|
|
207
|
-
onChange: () => { } })), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
220
|
+
onChange: () => { } })), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
208
221
|
setQuery(e.target.value);
|
|
209
222
|
setHighlight(0);
|
|
210
223
|
onSearchChange?.(e.target.value);
|
|
@@ -215,7 +228,7 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
215
228
|
// heading, not one each.
|
|
216
229
|
const showGroupLabel = opt.group !== undefined &&
|
|
217
230
|
opt.group !== filtered[i - 1]?.group;
|
|
218
|
-
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));
|
|
231
|
+
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 } })), opt.description !== undefined ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-select-option-body", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-label", children: opt.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-description", children: opt.description })] })) : ((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));
|
|
219
232
|
}))] }) }))] }));
|
|
220
233
|
}
|
|
221
234
|
Select.displayName = "Select";
|