@crisp-ui-kit/crisp 0.26.0 → 0.27.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.
@@ -65,7 +65,7 @@ function Chip({ opt }) {
65
65
  * popover with a search field over rows of "[chip] … [checkbox]" that toggle on
66
66
  * click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
67
67
  */
68
- function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
68
+ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
69
69
  // Virtual focus: DOM focus stays on the trigger (or the search field) and the
70
70
  // active option is named by `aria-activedescendant`. `data-highlighted` is a
71
71
  // styling hook only -- a screen reader cannot see it.
@@ -154,7 +154,8 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
154
154
  document.addEventListener("pointerdown", onDown);
155
155
  return () => document.removeEventListener("pointerdown", onDown);
156
156
  }, [open]);
157
- 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) => {
157
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [name &&
158
+ value.map((v) => (0, jsx_runtime_1.jsx)("input", { type: "hidden", name: name, value: v }, v)), (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) => {
158
159
  if (open && !searchable)
159
160
  onNavKeys(e);
160
161
  else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
@@ -57,7 +57,7 @@ function RadioGroup({ value, defaultValue, onValueChange, variant = "default", n
57
57
  onValueChange?.(v);
58
58
  }, [value, onValueChange]);
59
59
  const { onKeyDown, groupRef } = (0, radiogroup_keyboard_1.useRadioGroupKeyboard)({ select });
60
- return ((0, jsx_runtime_1.jsx)(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: (0, jsx_runtime_1.jsx)("div", { ref: groupRef, role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: (0, radiogroup_recipe_1.radiogroupRecipe)({ className }), ...rest, onKeyDown: onKeyDown, children: children }) }));
60
+ return ((0, jsx_runtime_1.jsx)(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: (0, jsx_runtime_1.jsxs)("div", { ref: groupRef, role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: (0, radiogroup_recipe_1.radiogroupRecipe)({ className }), ...rest, onKeyDown: onKeyDown, children: [children, name && selected && ((0, jsx_runtime_1.jsx)("input", { type: "hidden", name: name, value: selected }))] }) }));
61
61
  }
62
62
  RadioGroup.displayName = "RadioGroup";
63
63
  function Radio({ value, label, children, disabled }) {
@@ -65,7 +65,7 @@ function Radio({ value, label, children, disabled }) {
65
65
  if (!ctx)
66
66
  throw new Error("Radio must be used within a RadioGroup");
67
67
  const checked = ctx.value === value;
68
- return ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "radio", "aria-checked": checked, name: ctx.name, disabled: disabled, "data-value": value, "data-checked": checked ? "" : undefined,
68
+ return ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "radio", "aria-checked": checked, disabled: disabled, "data-value": value, "data-checked": checked ? "" : undefined,
69
69
  // roving tabindex: the selected (or first, when none) radio is tabbable
70
70
  tabIndex: checked ? 0 : -1, className: "radio", onClick: () => ctx.select(value), children: ctx.variant === "card" ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "radio-preview", children: children }), (0, jsx_runtime_1.jsx)("span", { className: "radio-cardfoot", children: (0, jsx_runtime_1.jsx)("span", { className: "radio-label", children: label }) })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "radio-dot" }), (0, jsx_runtime_1.jsx)("span", { className: "radio-label", children: label ?? children })] })) }));
71
71
  }
@@ -52,7 +52,7 @@ function optionText(o) {
52
52
  * "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
53
53
  * Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
54
54
  */
55
- function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
55
+ function Select({ options, value, defaultValue, onValueChange, name, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
56
56
  // Virtual focus: DOM focus stays on the trigger (or the search field), and the
57
57
  // active option is named by `aria-activedescendant`. That is the only thing a
58
58
  // screen reader has to go on -- `data-highlighted` is invisible to it -- so the
@@ -150,7 +150,7 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
150
150
  document.addEventListener("pointerdown", onDown);
151
151
  return () => document.removeEventListener("pointerdown", onDown);
152
152
  }, [open]);
153
- 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) => {
153
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [name && selected && (0, jsx_runtime_1.jsx)("input", { type: "hidden", name: name, value: selected }), (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) => {
154
154
  setQuery(e.target.value);
155
155
  setHighlight(0);
156
156
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
@@ -43,16 +43,16 @@ const switch_recipe_1 = require("./switch.recipe");
43
43
  * label association and keyboard (Space/Enter) come for free. Controlled via
44
44
  * `checked`/`onCheckedChange`, or uncontrolled from `defaultChecked`.
45
45
  */
46
- exports.Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, ...props }, ref) => {
46
+ exports.Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, name, value = "on", ...props }, ref) => {
47
47
  const [internal, setInternal] = React.useState(defaultChecked);
48
48
  const on = checked ?? internal;
49
- return ((0, jsx_runtime_1.jsx)("button", { ref: ref, type: "button", role: "switch", "aria-checked": on, disabled: disabled, "data-checked": on ? "" : undefined, className: (0, switch_recipe_1.switchRecipe)({ className }), onClick: (e) => {
50
- onClick?.(e);
51
- if (e.defaultPrevented)
52
- return;
53
- if (checked === undefined)
54
- setInternal((v) => !v);
55
- onCheckedChange?.(!on);
56
- }, ...props, children: (0, jsx_runtime_1.jsx)("span", { className: "switch-thumb" }) }));
49
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { ref: ref, type: "button", role: "switch", "aria-checked": on, disabled: disabled, "data-checked": on ? "" : undefined, className: (0, switch_recipe_1.switchRecipe)({ className }), onClick: (e) => {
50
+ onClick?.(e);
51
+ if (e.defaultPrevented)
52
+ return;
53
+ if (checked === undefined)
54
+ setInternal((v) => !v);
55
+ onCheckedChange?.(!on);
56
+ }, ...props, children: (0, jsx_runtime_1.jsx)("span", { className: "switch-thumb" }) }), name && on && (0, jsx_runtime_1.jsx)("input", { type: "hidden", name: name, value: value })] }));
57
57
  });
58
58
  exports.Switch.displayName = "Switch";
@@ -159,6 +159,31 @@ function Command({ open, onOpenChange, items = exports.defaultCommandItems, plac
159
159
  if (queryProp === undefined)
160
160
  setInternalQuery(next);
161
161
  }, [onQueryChange, queryProp]);
162
+ /**
163
+ * Where the user was when the palette opened.
164
+ *
165
+ * The dialog machine restores focus on close, and for Dialog and Drawer that
166
+ * lands on the trigger: focus-trap remembers whatever was focused when the
167
+ * trap activated. The palette is different — the combobox focuses the search
168
+ * input as part of the same commit, so the element the trap remembers is the
169
+ * input, which is then removed with the palette, and focus falls to
170
+ * `document.body`. The palette is the one surface opened from anywhere with
171
+ * a shortcut, so losing the caller's place is worst exactly here (#241).
172
+ *
173
+ * Recorded by watching focus WHILE CLOSED rather than reading
174
+ * `document.activeElement` when `open` flips: by the time an effect of ours
175
+ * runs, the input may already hold focus.
176
+ */
177
+ const openerRef = React.useRef(null);
178
+ React.useEffect(() => {
179
+ if (open)
180
+ return;
181
+ const onFocusIn = (e) => {
182
+ openerRef.current = e.target;
183
+ };
184
+ document.addEventListener("focusin", onFocusIn);
185
+ return () => document.removeEventListener("focusin", onFocusIn);
186
+ }, [open]);
162
187
  const dialogService = (0, react_1.useMachine)(dialog.machine, {
163
188
  id: dialogId,
164
189
  open,
@@ -166,6 +191,7 @@ function Command({ open, onOpenChange, items = exports.defaultCommandItems, plac
166
191
  onOpenChange(details.open);
167
192
  },
168
193
  initialFocusEl: () => inputRef.current,
194
+ finalFocusEl: () => openerRef.current,
169
195
  "aria-label": ariaLabel,
170
196
  });
171
197
  const dlg = dialog.connect(dialogService, react_1.normalizeProps);
@@ -29,7 +29,7 @@ function Chip({ opt }) {
29
29
  * popover with a search field over rows of "[chip] … [checkbox]" that toggle on
30
30
  * click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
31
31
  */
32
- export function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
32
+ export function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
33
33
  // Virtual focus: DOM focus stays on the trigger (or the search field) and the
34
34
  // active option is named by `aria-activedescendant`. `data-highlighted` is a
35
35
  // styling hook only -- a screen reader cannot see it.
@@ -118,7 +118,8 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
118
118
  document.addEventListener("pointerdown", onDown);
119
119
  return () => document.removeEventListener("pointerdown", onDown);
120
120
  }, [open]);
121
- 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) => {
121
+ return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [name &&
122
+ value.map((v) => _jsx("input", { type: "hidden", name: name, value: v }, v)), _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) => {
122
123
  if (open && !searchable)
123
124
  onNavKeys(e);
124
125
  else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
@@ -20,7 +20,7 @@ export function RadioGroup({ value, defaultValue, onValueChange, variant = "defa
20
20
  onValueChange?.(v);
21
21
  }, [value, onValueChange]);
22
22
  const { onKeyDown, groupRef } = useRadioGroupKeyboard({ select });
23
- return (_jsx(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: _jsx("div", { ref: groupRef, role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: radiogroupRecipe({ className }), ...rest, onKeyDown: onKeyDown, children: children }) }));
23
+ return (_jsx(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: _jsxs("div", { ref: groupRef, role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: radiogroupRecipe({ className }), ...rest, onKeyDown: onKeyDown, children: [children, name && selected && (_jsx("input", { type: "hidden", name: name, value: selected }))] }) }));
24
24
  }
25
25
  RadioGroup.displayName = "RadioGroup";
26
26
  export function Radio({ value, label, children, disabled }) {
@@ -28,7 +28,7 @@ export function Radio({ value, label, children, disabled }) {
28
28
  if (!ctx)
29
29
  throw new Error("Radio must be used within a RadioGroup");
30
30
  const checked = ctx.value === value;
31
- return (_jsx("button", { type: "button", role: "radio", "aria-checked": checked, name: ctx.name, disabled: disabled, "data-value": value, "data-checked": checked ? "" : undefined,
31
+ return (_jsx("button", { type: "button", role: "radio", "aria-checked": checked, disabled: disabled, "data-value": value, "data-checked": checked ? "" : undefined,
32
32
  // roving tabindex: the selected (or first, when none) radio is tabbable
33
33
  tabIndex: checked ? 0 : -1, className: "radio", onClick: () => ctx.select(value), children: ctx.variant === "card" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "radio-preview", children: children }), _jsx("span", { className: "radio-cardfoot", children: _jsx("span", { className: "radio-label", children: label }) })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "radio-dot" }), _jsx("span", { className: "radio-label", children: label ?? children })] })) }));
34
34
  }
@@ -16,7 +16,7 @@ function optionText(o) {
16
16
  * "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
17
17
  * Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
18
18
  */
19
- export function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
19
+ export function Select({ options, value, defaultValue, onValueChange, name, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
20
20
  // Virtual focus: DOM focus stays on the trigger (or the search field), and the
21
21
  // active option is named by `aria-activedescendant`. That is the only thing a
22
22
  // screen reader has to go on -- `data-highlighted` is invisible to it -- so the
@@ -114,7 +114,7 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
114
114
  document.addEventListener("pointerdown", onDown);
115
115
  return () => document.removeEventListener("pointerdown", onDown);
116
116
  }, [open]);
117
- 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) => {
117
+ return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [name && selected && _jsx("input", { type: "hidden", name: name, value: selected }), _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) => {
118
118
  setQuery(e.target.value);
119
119
  setHighlight(0);
120
120
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { switchRecipe } from "./switch.recipe.js";
4
4
  /**
@@ -7,16 +7,16 @@ import { switchRecipe } from "./switch.recipe.js";
7
7
  * label association and keyboard (Space/Enter) come for free. Controlled via
8
8
  * `checked`/`onCheckedChange`, or uncontrolled from `defaultChecked`.
9
9
  */
10
- export const Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, ...props }, ref) => {
10
+ export const Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, name, value = "on", ...props }, ref) => {
11
11
  const [internal, setInternal] = React.useState(defaultChecked);
12
12
  const on = checked ?? internal;
13
- return (_jsx("button", { ref: ref, type: "button", role: "switch", "aria-checked": on, disabled: disabled, "data-checked": on ? "" : undefined, className: switchRecipe({ className }), onClick: (e) => {
14
- onClick?.(e);
15
- if (e.defaultPrevented)
16
- return;
17
- if (checked === undefined)
18
- setInternal((v) => !v);
19
- onCheckedChange?.(!on);
20
- }, ...props, children: _jsx("span", { className: "switch-thumb" }) }));
13
+ return (_jsxs(_Fragment, { children: [_jsx("button", { ref: ref, type: "button", role: "switch", "aria-checked": on, disabled: disabled, "data-checked": on ? "" : undefined, className: switchRecipe({ className }), onClick: (e) => {
14
+ onClick?.(e);
15
+ if (e.defaultPrevented)
16
+ return;
17
+ if (checked === undefined)
18
+ setInternal((v) => !v);
19
+ onCheckedChange?.(!on);
20
+ }, ...props, children: _jsx("span", { className: "switch-thumb" }) }), name && on && _jsx("input", { type: "hidden", name: name, value: value })] }));
21
21
  });
22
22
  Switch.displayName = "Switch";
@@ -122,6 +122,31 @@ export function Command({ open, onOpenChange, items = defaultCommandItems, place
122
122
  if (queryProp === undefined)
123
123
  setInternalQuery(next);
124
124
  }, [onQueryChange, queryProp]);
125
+ /**
126
+ * Where the user was when the palette opened.
127
+ *
128
+ * The dialog machine restores focus on close, and for Dialog and Drawer that
129
+ * lands on the trigger: focus-trap remembers whatever was focused when the
130
+ * trap activated. The palette is different — the combobox focuses the search
131
+ * input as part of the same commit, so the element the trap remembers is the
132
+ * input, which is then removed with the palette, and focus falls to
133
+ * `document.body`. The palette is the one surface opened from anywhere with
134
+ * a shortcut, so losing the caller's place is worst exactly here (#241).
135
+ *
136
+ * Recorded by watching focus WHILE CLOSED rather than reading
137
+ * `document.activeElement` when `open` flips: by the time an effect of ours
138
+ * runs, the input may already hold focus.
139
+ */
140
+ const openerRef = React.useRef(null);
141
+ React.useEffect(() => {
142
+ if (open)
143
+ return;
144
+ const onFocusIn = (e) => {
145
+ openerRef.current = e.target;
146
+ };
147
+ document.addEventListener("focusin", onFocusIn);
148
+ return () => document.removeEventListener("focusin", onFocusIn);
149
+ }, [open]);
125
150
  const dialogService = useMachine(dialog.machine, {
126
151
  id: dialogId,
127
152
  open,
@@ -129,6 +154,7 @@ export function Command({ open, onOpenChange, items = defaultCommandItems, place
129
154
  onOpenChange(details.open);
130
155
  },
131
156
  initialFocusEl: () => inputRef.current,
157
+ finalFocusEl: () => openerRef.current,
132
158
  "aria-label": ariaLabel,
133
159
  });
134
160
  const dlg = dialog.connect(dialogService, normalizeProps);
@@ -11,6 +11,13 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
11
11
  value?: string[];
12
12
  /** Initially selected values, when uncontrolled. */
13
13
  defaultValue?: string[];
14
+ /**
15
+ * Submits with the form under this name. crisp's MultiSelect is a
16
+ * a button and a listbox, not a native control, so the value reaches a `<form>` only through a
17
+ * hidden input — without one, `name` type-checks, renders, and contributes
18
+ * nothing to FormData (#243).
19
+ */
20
+ name?: string;
14
21
  /** Fires with the next value on every toggle, controlled or not. */
15
22
  onValueChange?: (value: string[]) => void;
16
23
  placeholder?: string;
@@ -29,7 +36,7 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
29
36
  * popover with a search field over rows of "[chip] … [checkbox]" that toggle on
30
37
  * click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
31
38
  */
32
- export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
39
+ export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, placeholder, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
33
40
  export declare namespace MultiSelect {
34
41
  var displayName: string;
35
42
  }
@@ -6,6 +6,12 @@ export type RadioGroupProps = Omit<React.ComponentPropsWithoutRef<"div">, "value
6
6
  onValueChange?: (value: string) => void;
7
7
  /** `default` = rows (dot + label); `card` = bordered preview cards. */
8
8
  variant?: "default" | "card";
9
+ /**
10
+ * Submits with the form under this name. It used to land on each radio
11
+ * `<button>`, where a name means nothing unless the button submits the form:
12
+ * the prop type-checked, rendered, and contributed nothing to FormData
13
+ * (#243). The value now rides on a hidden input.
14
+ */
9
15
  name?: string;
10
16
  className?: string;
11
17
  "aria-label"?: string;
@@ -14,6 +14,13 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
14
14
  value?: string;
15
15
  defaultValue?: string;
16
16
  onValueChange?: (value: string) => void;
17
+ /**
18
+ * Submits with the form under this name. crisp's Select is a
19
+ * a button and a listbox, not a native control, so the value reaches a `<form>` only through a
20
+ * hidden input — without one, `name` type-checks, renders, and contributes
21
+ * nothing to FormData (#243).
22
+ */
23
+ name?: string;
17
24
  placeholder?: string;
18
25
  /** `ghost` is borderless until hover — for inline selects (e.g. sort rows). */
19
26
  variant?: "default" | "ghost";
@@ -36,7 +43,7 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
36
43
  * "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
37
44
  * Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
38
45
  */
39
- export declare function Select({ options, value, defaultValue, onValueChange, placeholder, variant, width, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
46
+ export declare function Select({ options, value, defaultValue, onValueChange, name, placeholder, variant, width, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
40
47
  export declare namespace Select {
41
48
  var displayName: string;
42
49
  }
@@ -4,6 +4,15 @@ export type SwitchProps = Omit<React.ComponentPropsWithoutRef<"button">, "onChan
4
4
  checked?: boolean;
5
5
  defaultChecked?: boolean;
6
6
  onCheckedChange?: (checked: boolean) => void;
7
+ /**
8
+ * Submits with the form under this name. crisp's Switch is a
9
+ * `role="switch"` button`, not a native control, so the value reaches a `<form>` only
10
+ * through a hidden input — without one, `name` type-checks, renders, and
11
+ * contributes nothing to FormData (#243).
12
+ */
13
+ name?: string;
14
+ /** The value submitted when it is on (default "on", as a checkbox does). */
15
+ value?: string;
7
16
  };
8
17
  /**
9
18
  * An on/off toggle — the settings switch, measured from Attio. Renders a
@@ -16,4 +25,13 @@ export declare const Switch: React.ForwardRefExoticComponent<Omit<Omit<React.Det
16
25
  checked?: boolean;
17
26
  defaultChecked?: boolean;
18
27
  onCheckedChange?: (checked: boolean) => void;
28
+ /**
29
+ * Submits with the form under this name. crisp's Switch is a
30
+ * `role="switch"` button`, not a native control, so the value reaches a `<form>` only
31
+ * through a hidden input — without one, `name` type-checks, renders, and
32
+ * contributes nothing to FormData (#243).
33
+ */
34
+ name?: string;
35
+ /** The value submitted when it is on (default "on", as a checkbox does). */
36
+ value?: string;
19
37
  } & React.RefAttributes<HTMLButtonElement>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",