@crisp-ui-kit/crisp 0.7.0 → 0.7.2

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.
@@ -81,16 +81,26 @@ const config = (0, core_1.defineRecipe)({
81
81
  }
82
82
  & .datepicker-empty-cell { min-height: ${tokens_1.contract.size.calendarCell}; }
83
83
 
84
- /* the bridge joining days inside a completed range */
84
+ /* The bridge joining days inside a completed range.
85
+ The endpoints do NOT carry data-in-range -- only the interior days do --
86
+ so these are three independent rules, not a base plus two modifiers. A
87
+ modifier that sets only inset-inline-start never generates a pseudo
88
+ element on an endpoint, which leaves a half-cell of white between the
89
+ endpoint's pill and the run of grey beside it. Each rule declares its
90
+ own content. */
85
91
  & .datepicker-cell[data-range-complete][data-in-range]::before {
86
92
  content: ""; position: absolute; z-index: -1; inset-inline: 0;
87
93
  top: ${pillInset}; height: ${tokens_1.contract.size.calendarPill}; background: ${tokens_1.contract.bgNeutralWeak};
88
94
  }
89
95
  & .datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
90
- inset-inline-start: 50%;
96
+ content: ""; position: absolute; z-index: -1;
97
+ inset-inline-start: 50%; inset-inline-end: 0;
98
+ top: ${pillInset}; height: ${tokens_1.contract.size.calendarPill}; background: ${tokens_1.contract.bgNeutralWeak};
91
99
  }
92
100
  & .datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
93
- inset-inline-end: 50%;
101
+ content: ""; position: absolute; z-index: -1;
102
+ inset-inline-start: 0; inset-inline-end: 50%;
103
+ top: ${pillInset}; height: ${tokens_1.contract.size.calendarPill}; background: ${tokens_1.contract.bgNeutralWeak};
94
104
  }
95
105
 
96
106
  & .datepicker-date {
@@ -64,6 +64,12 @@ function Chip({ opt }) {
64
64
  * click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
65
65
  */
66
66
  function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, ...rest }) {
67
+ // Virtual focus: DOM focus stays on the trigger (or the search field) and the
68
+ // active option is named by `aria-activedescendant`. `data-highlighted` is a
69
+ // styling hook only -- a screen reader cannot see it.
70
+ const autoId = React.useId();
71
+ const listboxId = `${autoId}-listbox`;
72
+ const optionId = (i) => `${autoId}-opt-${i}`;
67
73
  const [open, setOpen] = React.useState(false);
68
74
  const [up, setUp] = React.useState(false);
69
75
  const [pos, setPos] = React.useState({});
@@ -105,6 +111,9 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
105
111
  setOpen(false);
106
112
  triggerRef.current?.focus({ preventScroll: true });
107
113
  };
114
+ const activeId = open && highlight >= 0 && highlight < filtered.length
115
+ ? optionId(highlight)
116
+ : undefined;
108
117
  const toggle = (v) => {
109
118
  const next = selectedSet.has(v)
110
119
  ? value.filter((x) => x !== v)
@@ -143,19 +152,19 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
143
152
  document.addEventListener("pointerdown", onDown);
144
153
  return () => document.removeEventListener("pointerdown", onDown);
145
154
  }, [open]);
146
- return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
155
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
147
156
  if (open && !searchable)
148
157
  onNavKeys(e);
149
158
  else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
150
159
  e.preventDefault();
151
160
  openMenu();
152
161
  }
153
- }, children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => (0, jsx_runtime_1.jsx)(Chip, { opt: o }, o.value))) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "multiselect-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, value: query, onChange: (e) => {
162
+ }, children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => (0, jsx_runtime_1.jsx)(Chip, { opt: o }, o.value))) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { id: listboxId, className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "multiselect-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) => {
154
163
  setQuery(e.target.value);
155
164
  setHighlight(0);
156
165
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "multiselect-empty", children: "No results" })) : (filtered.map((opt, i) => {
157
166
  const sel = selectedSet.has(opt.value);
158
- return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "option", "aria-selected": sel, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-option-chip", children: (0, jsx_runtime_1.jsx)(Chip, { opt: opt }) }), (0, jsx_runtime_1.jsx)("span", { className: "multiselect-check", children: sel && (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
167
+ return ((0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-option-chip", children: (0, jsx_runtime_1.jsx)(Chip, { opt: opt }) }), (0, jsx_runtime_1.jsx)("span", { className: "multiselect-check", children: sel && (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
159
168
  }))] }))] }));
160
169
  }
161
170
  MultiSelect.displayName = "MultiSelect";
@@ -51,6 +51,14 @@ function optionText(o) {
51
51
  * Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
52
52
  */
53
53
  function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
54
+ // Virtual focus: DOM focus stays on the trigger (or the search field), and the
55
+ // active option is named by `aria-activedescendant`. That is the only thing a
56
+ // screen reader has to go on -- `data-highlighted` is invisible to it -- so the
57
+ // ids below have to resolve to the element the highlight is actually drawn on.
58
+ const autoId = React.useId();
59
+ const baseId = id ?? autoId;
60
+ const listboxId = `${baseId}-listbox`;
61
+ const optionId = (i) => `${baseId}-opt-${i}`;
54
62
  const [internal, setInternal] = React.useState(defaultValue);
55
63
  const selected = value ?? internal;
56
64
  const [open, setOpen] = React.useState(false);
@@ -70,6 +78,9 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
70
78
  const enabledIndexes = filtered
71
79
  .map((o, i) => (o.disabled ? -1 : i))
72
80
  .filter((i) => i >= 0);
81
+ const activeId = open && highlight >= 0 && highlight < filtered.length
82
+ ? optionId(highlight)
83
+ : undefined;
73
84
  const openMenu = () => {
74
85
  if (disabled)
75
86
  return;
@@ -169,12 +180,12 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
169
180
  document.addEventListener("pointerdown", onDown);
170
181
  return () => document.removeEventListener("pointerdown", onDown);
171
182
  }, [open]);
172
- return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "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, value: query, onChange: (e) => {
183
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [(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, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { id: listboxId, className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "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) => {
173
184
  setQuery(e.target.value);
174
185
  setHighlight(0);
175
186
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "select-empty", children: "No results" })) : (filtered.map((opt, i) => {
176
187
  const isSel = opt.value === selected;
177
- return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "option", "aria-selected": isSel, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
188
+ return ((0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
178
189
  }))] }))] }));
179
190
  }
180
191
  Select.displayName = "Select";
@@ -62,16 +62,26 @@
62
62
  }
63
63
  & .datepicker-empty-cell { min-height: var(--size-calendarCell); }
64
64
 
65
- /* the bridge joining days inside a completed range */
65
+ /* The bridge joining days inside a completed range.
66
+ The endpoints do NOT carry data-in-range -- only the interior days do --
67
+ so these are three independent rules, not a base plus two modifiers. A
68
+ modifier that sets only inset-inline-start never generates a pseudo
69
+ element on an endpoint, which leaves a half-cell of white between the
70
+ endpoint's pill and the run of grey beside it. Each rule declares its
71
+ own content. */
66
72
  & .datepicker-cell[data-range-complete][data-in-range]::before {
67
73
  content: ""; position: absolute; z-index: -1; inset-inline: 0;
68
74
  top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
69
75
  }
70
76
  & .datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
71
- inset-inline-start: 50%;
77
+ content: ""; position: absolute; z-index: -1;
78
+ inset-inline-start: 50%; inset-inline-end: 0;
79
+ top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
72
80
  }
73
81
  & .datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
74
- inset-inline-end: 50%;
82
+ content: ""; position: absolute; z-index: -1;
83
+ inset-inline-start: 0; inset-inline-end: 50%;
84
+ top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
75
85
  }
76
86
 
77
87
  & .datepicker-date {
@@ -78,16 +78,26 @@ const config = defineRecipe({
78
78
  }
79
79
  & .datepicker-empty-cell { min-height: ${c.size.calendarCell}; }
80
80
 
81
- /* the bridge joining days inside a completed range */
81
+ /* The bridge joining days inside a completed range.
82
+ The endpoints do NOT carry data-in-range -- only the interior days do --
83
+ so these are three independent rules, not a base plus two modifiers. A
84
+ modifier that sets only inset-inline-start never generates a pseudo
85
+ element on an endpoint, which leaves a half-cell of white between the
86
+ endpoint's pill and the run of grey beside it. Each rule declares its
87
+ own content. */
82
88
  & .datepicker-cell[data-range-complete][data-in-range]::before {
83
89
  content: ""; position: absolute; z-index: -1; inset-inline: 0;
84
90
  top: ${pillInset}; height: ${c.size.calendarPill}; background: ${c.bgNeutralWeak};
85
91
  }
86
92
  & .datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
87
- inset-inline-start: 50%;
93
+ content: ""; position: absolute; z-index: -1;
94
+ inset-inline-start: 50%; inset-inline-end: 0;
95
+ top: ${pillInset}; height: ${c.size.calendarPill}; background: ${c.bgNeutralWeak};
88
96
  }
89
97
  & .datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
90
- inset-inline-end: 50%;
98
+ content: ""; position: absolute; z-index: -1;
99
+ inset-inline-start: 0; inset-inline-end: 50%;
100
+ top: ${pillInset}; height: ${c.size.calendarPill}; background: ${c.bgNeutralWeak};
91
101
  }
92
102
 
93
103
  & .datepicker-date {
@@ -28,6 +28,12 @@ function Chip({ opt }) {
28
28
  * click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
29
29
  */
30
30
  export function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, ...rest }) {
31
+ // Virtual focus: DOM focus stays on the trigger (or the search field) and the
32
+ // active option is named by `aria-activedescendant`. `data-highlighted` is a
33
+ // styling hook only -- a screen reader cannot see it.
34
+ const autoId = React.useId();
35
+ const listboxId = `${autoId}-listbox`;
36
+ const optionId = (i) => `${autoId}-opt-${i}`;
31
37
  const [open, setOpen] = React.useState(false);
32
38
  const [up, setUp] = React.useState(false);
33
39
  const [pos, setPos] = React.useState({});
@@ -69,6 +75,9 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
69
75
  setOpen(false);
70
76
  triggerRef.current?.focus({ preventScroll: true });
71
77
  };
78
+ const activeId = open && highlight >= 0 && highlight < filtered.length
79
+ ? optionId(highlight)
80
+ : undefined;
72
81
  const toggle = (v) => {
73
82
  const next = selectedSet.has(v)
74
83
  ? value.filter((x) => x !== v)
@@ -107,19 +116,19 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
107
116
  document.addEventListener("pointerdown", onDown);
108
117
  return () => document.removeEventListener("pointerdown", onDown);
109
118
  }, [open]);
110
- return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
119
+ return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
111
120
  if (open && !searchable)
112
121
  onNavKeys(e);
113
122
  else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
114
123
  e.preventDefault();
115
124
  openMenu();
116
125
  }
117
- }, children: [_jsx("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => {
126
+ }, children: [_jsx("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { id: listboxId, className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
118
127
  setQuery(e.target.value);
119
128
  setHighlight(0);
120
129
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "multiselect-empty", children: "No results" })) : (filtered.map((opt, i) => {
121
130
  const sel = selectedSet.has(opt.value);
122
- return (_jsxs("button", { type: "button", role: "option", "aria-selected": sel, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [_jsx("span", { className: "multiselect-option-chip", children: _jsx(Chip, { opt: opt }) }), _jsx("span", { className: "multiselect-check", children: sel && _jsx(Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
131
+ return (_jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [_jsx("span", { className: "multiselect-option-chip", children: _jsx(Chip, { opt: opt }) }), _jsx("span", { className: "multiselect-check", children: sel && _jsx(Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
123
132
  }))] }))] }));
124
133
  }
125
134
  MultiSelect.displayName = "MultiSelect";
@@ -15,6 +15,14 @@ function optionText(o) {
15
15
  * Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
16
16
  */
17
17
  export function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
18
+ // Virtual focus: DOM focus stays on the trigger (or the search field), and the
19
+ // active option is named by `aria-activedescendant`. That is the only thing a
20
+ // screen reader has to go on -- `data-highlighted` is invisible to it -- so the
21
+ // ids below have to resolve to the element the highlight is actually drawn on.
22
+ const autoId = React.useId();
23
+ const baseId = id ?? autoId;
24
+ const listboxId = `${baseId}-listbox`;
25
+ const optionId = (i) => `${baseId}-opt-${i}`;
18
26
  const [internal, setInternal] = React.useState(defaultValue);
19
27
  const selected = value ?? internal;
20
28
  const [open, setOpen] = React.useState(false);
@@ -34,6 +42,9 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
34
42
  const enabledIndexes = filtered
35
43
  .map((o, i) => (o.disabled ? -1 : i))
36
44
  .filter((i) => i >= 0);
45
+ const activeId = open && highlight >= 0 && highlight < filtered.length
46
+ ? optionId(highlight)
47
+ : undefined;
37
48
  const openMenu = () => {
38
49
  if (disabled)
39
50
  return;
@@ -133,12 +144,12 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
133
144
  document.addEventListener("pointerdown", onDown);
134
145
  return () => document.removeEventListener("pointerdown", onDown);
135
146
  }, [open]);
136
- return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : (_jsx("span", { className: "select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "select-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => {
147
+ return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [_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, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : (_jsx("span", { className: "select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { id: listboxId, className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "select-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
137
148
  setQuery(e.target.value);
138
149
  setHighlight(0);
139
150
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "select-empty", children: "No results" })) : (filtered.map((opt, i) => {
140
151
  const isSel = opt.value === selected;
141
- return (_jsxs("button", { type: "button", role: "option", "aria-selected": isSel, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
152
+ return (_jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
142
153
  }))] }))] }));
143
154
  }
144
155
  Select.displayName = "Select";
package/dist/styles.css CHANGED
@@ -632,16 +632,26 @@
632
632
  }
633
633
  & .datepicker-empty-cell { min-height: var(--size-calendarCell); }
634
634
 
635
- /* the bridge joining days inside a completed range */
635
+ /* The bridge joining days inside a completed range.
636
+ The endpoints do NOT carry data-in-range -- only the interior days do --
637
+ so these are three independent rules, not a base plus two modifiers. A
638
+ modifier that sets only inset-inline-start never generates a pseudo
639
+ element on an endpoint, which leaves a half-cell of white between the
640
+ endpoint's pill and the run of grey beside it. Each rule declares its
641
+ own content. */
636
642
  & .datepicker-cell[data-range-complete][data-in-range]::before {
637
643
  content: ""; position: absolute; z-index: -1; inset-inline: 0;
638
644
  top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
639
645
  }
640
646
  & .datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
641
- inset-inline-start: 50%;
647
+ content: ""; position: absolute; z-index: -1;
648
+ inset-inline-start: 50%; inset-inline-end: 0;
649
+ top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
642
650
  }
643
651
  & .datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
644
- inset-inline-end: 50%;
652
+ content: ""; position: absolute; z-index: -1;
653
+ inset-inline-start: 0; inset-inline-end: 50%;
654
+ top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
645
655
  }
646
656
 
647
657
  & .datepicker-date {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",