@crisp-ui-kit/crisp 0.22.0 → 0.23.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.
@@ -41,6 +41,7 @@ const dialog = __importStar(require("@zag-js/dialog"));
41
41
  const react_1 = require("@zag-js/react");
42
42
  const lucide_react_1 = require("lucide-react");
43
43
  const React = __importStar(require("react"));
44
+ const core_1 = require("../../core");
44
45
  const dialog_recipe_1 = require("./dialog.recipe");
45
46
  // A globally unique id per mounted instance (see command.tsx for the rationale —
46
47
  // the machine defers its dismissal listeners a frame, so tree-relative useId
@@ -59,7 +60,7 @@ function useDialogInstanceId() {
59
60
  * Escape / outside-click dismissal) with the measured Attio modal surface.
60
61
  * Compose the body with `DialogBody` and actions with `DialogFooter`.
61
62
  */
62
- function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel = "Close", children, }) {
63
+ function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel = "Close", children, className, style, "aria-describedby": describedBy, ...rest }) {
63
64
  const id = useDialogInstanceId();
64
65
  const service = (0, react_1.useMachine)(dialog.machine, {
65
66
  id,
@@ -75,7 +76,10 @@ function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ar
75
76
  const api = dialog.connect(service, react_1.normalizeProps);
76
77
  if (!api.open)
77
78
  return null;
78
- return ((0, jsx_runtime_1.jsx)(react_1.Portal, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, dialog_recipe_1.dialogRecipe)(), children: [(0, jsx_runtime_1.jsx)("div", { ...api.getBackdropProps(), className: "dialog-backdrop" }), (0, jsx_runtime_1.jsx)("div", { ...api.getPositionerProps(), className: "dialog-positioner", children: (0, jsx_runtime_1.jsx)("div", { ...api.getContentProps(), className: "dialog-content", "data-crisp-dialog": id, style: { maxWidth: `min(${width}px, 90vw)` }, children: (0, jsx_runtime_1.jsxs)("div", { className: "dialog-card", children: [title != null && ((0, jsx_runtime_1.jsxs)("div", { className: "dialog-header", children: [icon, (0, jsx_runtime_1.jsx)("span", { ...api.getTitleProps(), className: "dialog-title", children: title }), (0, jsx_runtime_1.jsx)("button", { type: "button", ...api.getCloseTriggerProps(), className: "dialog-close", "aria-label": closeLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) }) })] }) }));
79
+ return ((0, jsx_runtime_1.jsx)(react_1.Portal, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, dialog_recipe_1.dialogRecipe)(), children: [(0, jsx_runtime_1.jsx)("div", { ...api.getBackdropProps(), className: "dialog-backdrop" }), (0, jsx_runtime_1.jsx)("div", { ...api.getPositionerProps(), className: "dialog-positioner", children: (0, jsx_runtime_1.jsx)("div", { ...rest, ...(0, core_1.withDescribedBy)(api.getContentProps(), describedBy), className: (0, core_1.cx)("dialog-content", className), "data-crisp-dialog": id,
80
+ // `width` is the component's own knob, so it wins over a passed
81
+ // maxWidth; everything else a consumer sets survives
82
+ style: { ...style, maxWidth: `min(${width}px, 90vw)` }, children: (0, jsx_runtime_1.jsxs)("div", { className: "dialog-card", children: [title != null && ((0, jsx_runtime_1.jsxs)("div", { className: "dialog-header", children: [icon, (0, jsx_runtime_1.jsx)("span", { ...api.getTitleProps(), className: "dialog-title", children: title }), (0, jsx_runtime_1.jsx)("button", { type: "button", ...api.getCloseTriggerProps(), className: "dialog-close", "aria-label": closeLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) }) })] }) }));
79
83
  }
80
84
  Dialog.displayName = "Dialog";
81
85
  function DialogBody({ children, className, }) {
@@ -41,6 +41,7 @@ const dialog = __importStar(require("@zag-js/dialog"));
41
41
  const react_1 = require("@zag-js/react");
42
42
  const lucide_react_1 = require("lucide-react");
43
43
  const React = __importStar(require("react"));
44
+ const core_1 = require("../../core");
44
45
  const drawer_recipe_1 = require("./drawer.recipe");
45
46
  let instanceCounter = 0;
46
47
  function useDrawerInstanceId() {
@@ -56,7 +57,7 @@ function useDrawerInstanceId() {
56
57
  * Escape / outside-click dismissal) with a panel that slides in from the edge.
57
58
  * Compose the body with `DrawerBody` and actions with `DrawerFooter`.
58
59
  */
59
- function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel = "Close", children, }) {
60
+ function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel = "Close", children, className, "aria-describedby": describedBy, ...rest }) {
60
61
  const id = useDrawerInstanceId();
61
62
  const service = (0, react_1.useMachine)(dialog.machine, {
62
63
  id,
@@ -69,7 +70,7 @@ function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label":
69
70
  const api = dialog.connect(service, react_1.normalizeProps);
70
71
  if (!api.open)
71
72
  return null;
72
- return ((0, jsx_runtime_1.jsx)(react_1.Portal, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, drawer_recipe_1.drawerRecipe)(), children: [(0, jsx_runtime_1.jsx)("div", { ...api.getBackdropProps(), className: "drawer-backdrop" }), (0, jsx_runtime_1.jsx)("div", { ...api.getPositionerProps(), className: "drawer-positioner", "data-side": side, children: (0, jsx_runtime_1.jsxs)("div", { ...api.getContentProps(), className: "drawer-content", children: [title != null && ((0, jsx_runtime_1.jsxs)("div", { className: "drawer-header", children: [icon, (0, jsx_runtime_1.jsx)("span", { ...api.getTitleProps(), className: "drawer-title", children: title }), (0, jsx_runtime_1.jsx)("button", { type: "button", ...api.getCloseTriggerProps(), className: "drawer-close", "aria-label": closeLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) })] }) }));
73
+ return ((0, jsx_runtime_1.jsx)(react_1.Portal, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, drawer_recipe_1.drawerRecipe)(), children: [(0, jsx_runtime_1.jsx)("div", { ...api.getBackdropProps(), className: "drawer-backdrop" }), (0, jsx_runtime_1.jsx)("div", { ...api.getPositionerProps(), className: "drawer-positioner", "data-side": side, children: (0, jsx_runtime_1.jsxs)("div", { ...rest, ...(0, core_1.withDescribedBy)(api.getContentProps(), describedBy), className: (0, core_1.cx)("drawer-content", className), children: [title != null && ((0, jsx_runtime_1.jsxs)("div", { className: "drawer-header", children: [icon, (0, jsx_runtime_1.jsx)("span", { ...api.getTitleProps(), className: "drawer-title", children: title }), (0, jsx_runtime_1.jsx)("button", { type: "button", ...api.getCloseTriggerProps(), className: "drawer-close", "aria-label": closeLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) })] }) }));
73
74
  }
74
75
  Drawer.displayName = "Drawer";
75
76
  function DrawerBody({ children }) {
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withDescribedBy = withDescribedBy;
4
+ /**
5
+ * Zag's dialog machine always advertises a description: `getContentProps()`
6
+ * returns an `aria-describedby` pointing at the id of a description element.
7
+ * crisp renders no such element — Attio's modals have a title and a body, no
8
+ * description line — so the reference dangles, and a dangling id is not a
9
+ * relationship a screen reader can follow.
10
+ *
11
+ * It also silenced the consumer's own: the machine's props are spread after
12
+ * anything passed in, so a real `aria-describedby` was overwritten by the
13
+ * missing one every time (#223).
14
+ *
15
+ * Drop the machine's value, and let the caller's stand when there is one. A
16
+ * consumer who wants a description points at a node inside their own children,
17
+ * which is the only place one exists.
18
+ */
19
+ function withDescribedBy(contentProps, describedBy) {
20
+ const { "aria-describedby": _machine, ...rest } = contentProps;
21
+ return describedBy ? { ...rest, "aria-describedby": describedBy } : rest;
22
+ }
@@ -14,11 +14,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.usePrefersReducedMotion = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.isRtlElement = exports.defineRecipe = exports.cx = void 0;
17
+ exports.usePrefersReducedMotion = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = void 0;
18
18
  var cx_1 = require("./cx");
19
19
  Object.defineProperty(exports, "cx", { enumerable: true, get: function () { return cx_1.cx; } });
20
20
  var define_1 = require("./define");
21
21
  Object.defineProperty(exports, "defineRecipe", { enumerable: true, get: function () { return define_1.defineRecipe; } });
22
+ var dialog_description_1 = require("./dialog-description");
23
+ Object.defineProperty(exports, "withDescribedBy", { enumerable: true, get: function () { return dialog_description_1.withDescribedBy; } });
22
24
  var direction_1 = require("./direction");
23
25
  Object.defineProperty(exports, "isRtlElement", { enumerable: true, get: function () { return direction_1.isRtlElement; } });
24
26
  var place_floating_1 = require("./place-floating");
@@ -42,6 +42,7 @@ const react_1 = require("@zag-js/react");
42
42
  const lucide_react_1 = require("lucide-react");
43
43
  const React = __importStar(require("react"));
44
44
  const kbd_1 = require("../../components/kbd");
45
+ const core_1 = require("../../core");
45
46
  const command_recipe_1 = require("./command.recipe");
46
47
  const IconHome = () => ((0, jsx_runtime_1.jsx)(lucide_react_1.House, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
47
48
  const IconTasks = () => ((0, jsx_runtime_1.jsx)(lucide_react_1.SquareCheck, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
@@ -146,7 +147,7 @@ function useCommandInstanceId() {
146
147
  * option list) so keyboard/focus correctness comes from the machines, not
147
148
  * hand-rolled event handling.
148
149
  */
149
- function Command({ open, onOpenChange, items = exports.defaultCommandItems, placeholder = "Search actions and records…", "aria-label": ariaLabel = "Command palette", navigateLabel = "Navigate", selectLabel = "Select", emptyLabel = "No matching commands", query: queryProp, onQueryChange, filterItems = true, }) {
150
+ function Command({ open, onOpenChange, items = exports.defaultCommandItems, placeholder = "Search actions and records…", "aria-label": ariaLabel = "Command palette", navigateLabel = "Navigate", selectLabel = "Select", emptyLabel = "No matching commands", query: queryProp, onQueryChange, filterItems = true, className, "aria-describedby": describedBy, ...rest }) {
150
151
  const instanceId = useCommandInstanceId();
151
152
  const dialogId = `${instanceId}-dialog`;
152
153
  const comboId = `${instanceId}-combobox`;
@@ -230,7 +231,7 @@ function Command({ open, onOpenChange, items = exports.defaultCommandItems, plac
230
231
  };
231
232
  if (!dlg.open)
232
233
  return null;
233
- return ((0, jsx_runtime_1.jsx)(react_1.Portal, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, command_recipe_1.commandRecipe)(), children: [(0, jsx_runtime_1.jsx)("div", { ...dlg.getBackdropProps(), className: "command-backdrop" }), (0, jsx_runtime_1.jsx)("div", { ...dlg.getPositionerProps(), className: "command-positioner", children: (0, jsx_runtime_1.jsx)("div", { ...dlg.getContentProps(), className: "command-content", children: (0, jsx_runtime_1.jsxs)("div", { className: "command-card", children: [(0, jsx_runtime_1.jsxs)("div", { className: "command-control", children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("input", { ref: inputRef, ...combo.getInputProps(), className: "command-input", placeholder: placeholder })] }), (0, jsx_runtime_1.jsx)("div", { ...combo.getContentProps(), className: "command-list", children: filteredItems.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "command-empty", children: emptyLabel })) : (filteredItems.map((item, i) => {
234
+ return ((0, jsx_runtime_1.jsx)(react_1.Portal, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, command_recipe_1.commandRecipe)(), children: [(0, jsx_runtime_1.jsx)("div", { ...dlg.getBackdropProps(), className: "command-backdrop" }), (0, jsx_runtime_1.jsx)("div", { ...dlg.getPositionerProps(), className: "command-positioner", children: (0, jsx_runtime_1.jsx)("div", { ...rest, ...(0, core_1.withDescribedBy)(dlg.getContentProps(), describedBy), className: (0, core_1.cx)("command-content", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "command-card", children: [(0, jsx_runtime_1.jsxs)("div", { className: "command-control", children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("input", { ref: inputRef, ...combo.getInputProps(), className: "command-input", placeholder: placeholder })] }), (0, jsx_runtime_1.jsx)("div", { ...combo.getContentProps(), className: "command-list", children: filteredItems.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "command-empty", children: emptyLabel })) : (filteredItems.map((item, i) => {
234
235
  const Icon = item.icon;
235
236
  const showGroup = item.group && item.group !== filteredItems[i - 1]?.group;
236
237
  return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [showGroup && ((0, jsx_runtime_1.jsx)("div", { className: "command-group-label", "aria-hidden": "true", children: item.group })), (0, jsx_runtime_1.jsxs)("div", { ...combo.getItemProps({ item }), className: "command-item", children: [(0, jsx_runtime_1.jsx)(Icon, {}), (0, jsx_runtime_1.jsx)("span", { className: "command-item-label", children: item.label }), item.shortcut && ((0, jsx_runtime_1.jsx)("span", { className: "command-item-shortcut", children: item.shortcut.map((key) => ((0, jsx_runtime_1.jsx)(kbd_1.Kbd, { children: key }, key))) }))] })] }, item.id));
@@ -3,6 +3,7 @@ import * as dialog from "@zag-js/dialog";
3
3
  import { normalizeProps, Portal, useMachine } from "@zag-js/react";
4
4
  import { X } from "lucide-react";
5
5
  import * as React from "react";
6
+ import { cx, withDescribedBy } from "../../core/index.js";
6
7
  import { dialogRecipe } from "./dialog.recipe.js";
7
8
  // A globally unique id per mounted instance (see command.tsx for the rationale —
8
9
  // the machine defers its dismissal listeners a frame, so tree-relative useId
@@ -21,7 +22,7 @@ function useDialogInstanceId() {
21
22
  * Escape / outside-click dismissal) with the measured Attio modal surface.
22
23
  * Compose the body with `DialogBody` and actions with `DialogFooter`.
23
24
  */
24
- export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel = "Close", children, }) {
25
+ export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel = "Close", children, className, style, "aria-describedby": describedBy, ...rest }) {
25
26
  const id = useDialogInstanceId();
26
27
  const service = useMachine(dialog.machine, {
27
28
  id,
@@ -37,7 +38,10 @@ export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-lab
37
38
  const api = dialog.connect(service, normalizeProps);
38
39
  if (!api.open)
39
40
  return null;
40
- return (_jsx(Portal, { children: _jsxs("div", { className: dialogRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "dialog-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "dialog-positioner", children: _jsx("div", { ...api.getContentProps(), className: "dialog-content", "data-crisp-dialog": id, style: { maxWidth: `min(${width}px, 90vw)` }, children: _jsxs("div", { className: "dialog-card", children: [title != null && (_jsxs("div", { className: "dialog-header", children: [icon, _jsx("span", { ...api.getTitleProps(), className: "dialog-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "dialog-close", "aria-label": closeLabel, children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) }) })] }) }));
41
+ return (_jsx(Portal, { children: _jsxs("div", { className: dialogRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "dialog-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "dialog-positioner", children: _jsx("div", { ...rest, ...withDescribedBy(api.getContentProps(), describedBy), className: cx("dialog-content", className), "data-crisp-dialog": id,
42
+ // `width` is the component's own knob, so it wins over a passed
43
+ // maxWidth; everything else a consumer sets survives
44
+ style: { ...style, maxWidth: `min(${width}px, 90vw)` }, children: _jsxs("div", { className: "dialog-card", children: [title != null && (_jsxs("div", { className: "dialog-header", children: [icon, _jsx("span", { ...api.getTitleProps(), className: "dialog-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "dialog-close", "aria-label": closeLabel, children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) }) })] }) }));
41
45
  }
42
46
  Dialog.displayName = "Dialog";
43
47
  export function DialogBody({ children, className, }) {
@@ -3,6 +3,7 @@ import * as dialog from "@zag-js/dialog";
3
3
  import { normalizeProps, Portal, useMachine } from "@zag-js/react";
4
4
  import { X } from "lucide-react";
5
5
  import * as React from "react";
6
+ import { cx, withDescribedBy } from "../../core/index.js";
6
7
  import { drawerRecipe } from "./drawer.recipe.js";
7
8
  let instanceCounter = 0;
8
9
  function useDrawerInstanceId() {
@@ -18,7 +19,7 @@ function useDrawerInstanceId() {
18
19
  * Escape / outside-click dismissal) with a panel that slides in from the edge.
19
20
  * Compose the body with `DrawerBody` and actions with `DrawerFooter`.
20
21
  */
21
- export function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel = "Close", children, }) {
22
+ export function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel = "Close", children, className, "aria-describedby": describedBy, ...rest }) {
22
23
  const id = useDrawerInstanceId();
23
24
  const service = useMachine(dialog.machine, {
24
25
  id,
@@ -31,7 +32,7 @@ export function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-
31
32
  const api = dialog.connect(service, normalizeProps);
32
33
  if (!api.open)
33
34
  return null;
34
- return (_jsx(Portal, { children: _jsxs("div", { className: drawerRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "drawer-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "drawer-positioner", "data-side": side, children: _jsxs("div", { ...api.getContentProps(), className: "drawer-content", children: [title != null && (_jsxs("div", { className: "drawer-header", children: [icon, _jsx("span", { ...api.getTitleProps(), className: "drawer-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "drawer-close", "aria-label": closeLabel, children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) })] }) }));
35
+ return (_jsx(Portal, { children: _jsxs("div", { className: drawerRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "drawer-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "drawer-positioner", "data-side": side, children: _jsxs("div", { ...rest, ...withDescribedBy(api.getContentProps(), describedBy), className: cx("drawer-content", className), children: [title != null && (_jsxs("div", { className: "drawer-header", children: [icon, _jsx("span", { ...api.getTitleProps(), className: "drawer-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "drawer-close", "aria-label": closeLabel, children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) })] }) }));
35
36
  }
36
37
  Drawer.displayName = "Drawer";
37
38
  export function DrawerBody({ children }) {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Zag's dialog machine always advertises a description: `getContentProps()`
3
+ * returns an `aria-describedby` pointing at the id of a description element.
4
+ * crisp renders no such element — Attio's modals have a title and a body, no
5
+ * description line — so the reference dangles, and a dangling id is not a
6
+ * relationship a screen reader can follow.
7
+ *
8
+ * It also silenced the consumer's own: the machine's props are spread after
9
+ * anything passed in, so a real `aria-describedby` was overwritten by the
10
+ * missing one every time (#223).
11
+ *
12
+ * Drop the machine's value, and let the caller's stand when there is one. A
13
+ * consumer who wants a description points at a node inside their own children,
14
+ * which is the only place one exists.
15
+ */
16
+ export function withDescribedBy(contentProps, describedBy) {
17
+ const { "aria-describedby": _machine, ...rest } = contentProps;
18
+ return describedBy ? { ...rest, "aria-describedby": describedBy } : rest;
19
+ }
@@ -1,5 +1,6 @@
1
1
  export { cx } from "./cx.js";
2
2
  export { defineRecipe } from "./define.js";
3
+ export { withDescribedBy } from "./dialog-description.js";
3
4
  export { isRtlElement } from "./direction.js";
4
5
  export { placeFloating, } from "./place-floating.js";
5
6
  export { Primitive } from "./primitive.js";
@@ -5,6 +5,7 @@ import { normalizeProps, Portal, useMachine } from "@zag-js/react";
5
5
  import { Building2, CircleDollarSign, House, ListTodo, Search, Settings, SquareCheck, User, UserPlus, } from "lucide-react";
6
6
  import * as React from "react";
7
7
  import { Kbd } from "../../components/kbd/index.js";
8
+ import { cx, withDescribedBy } from "../../core/index.js";
8
9
  import { commandRecipe } from "./command.recipe.js";
9
10
  const IconHome = () => (_jsx(House, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
10
11
  const IconTasks = () => (_jsx(SquareCheck, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
@@ -109,7 +110,7 @@ function useCommandInstanceId() {
109
110
  * option list) so keyboard/focus correctness comes from the machines, not
110
111
  * hand-rolled event handling.
111
112
  */
112
- export function Command({ open, onOpenChange, items = defaultCommandItems, placeholder = "Search actions and records…", "aria-label": ariaLabel = "Command palette", navigateLabel = "Navigate", selectLabel = "Select", emptyLabel = "No matching commands", query: queryProp, onQueryChange, filterItems = true, }) {
113
+ export function Command({ open, onOpenChange, items = defaultCommandItems, placeholder = "Search actions and records…", "aria-label": ariaLabel = "Command palette", navigateLabel = "Navigate", selectLabel = "Select", emptyLabel = "No matching commands", query: queryProp, onQueryChange, filterItems = true, className, "aria-describedby": describedBy, ...rest }) {
113
114
  const instanceId = useCommandInstanceId();
114
115
  const dialogId = `${instanceId}-dialog`;
115
116
  const comboId = `${instanceId}-combobox`;
@@ -193,7 +194,7 @@ export function Command({ open, onOpenChange, items = defaultCommandItems, place
193
194
  };
194
195
  if (!dlg.open)
195
196
  return null;
196
- return (_jsx(Portal, { children: _jsxs("div", { className: commandRecipe(), children: [_jsx("div", { ...dlg.getBackdropProps(), className: "command-backdrop" }), _jsx("div", { ...dlg.getPositionerProps(), className: "command-positioner", children: _jsx("div", { ...dlg.getContentProps(), className: "command-content", children: _jsxs("div", { className: "command-card", children: [_jsxs("div", { className: "command-control", children: [_jsx(IconSearch, {}), _jsx("input", { ref: inputRef, ...combo.getInputProps(), className: "command-input", placeholder: placeholder })] }), _jsx("div", { ...combo.getContentProps(), className: "command-list", children: filteredItems.length === 0 ? (_jsx("div", { className: "command-empty", children: emptyLabel })) : (filteredItems.map((item, i) => {
197
+ return (_jsx(Portal, { children: _jsxs("div", { className: commandRecipe(), children: [_jsx("div", { ...dlg.getBackdropProps(), className: "command-backdrop" }), _jsx("div", { ...dlg.getPositionerProps(), className: "command-positioner", children: _jsx("div", { ...rest, ...withDescribedBy(dlg.getContentProps(), describedBy), className: cx("command-content", className), children: _jsxs("div", { className: "command-card", children: [_jsxs("div", { className: "command-control", children: [_jsx(IconSearch, {}), _jsx("input", { ref: inputRef, ...combo.getInputProps(), className: "command-input", placeholder: placeholder })] }), _jsx("div", { ...combo.getContentProps(), className: "command-list", children: filteredItems.length === 0 ? (_jsx("div", { className: "command-empty", children: emptyLabel })) : (filteredItems.map((item, i) => {
197
198
  const Icon = item.icon;
198
199
  const showGroup = item.group && item.group !== filteredItems[i - 1]?.group;
199
200
  return (_jsxs(React.Fragment, { children: [showGroup && (_jsx("div", { className: "command-group-label", "aria-hidden": "true", children: item.group })), _jsxs("div", { ...combo.getItemProps({ item }), className: "command-item", children: [_jsx(Icon, {}), _jsx("span", { className: "command-item-label", children: item.label }), item.shortcut && (_jsx("span", { className: "command-item-shortcut", children: item.shortcut.map((key) => (_jsx(Kbd, { children: key }, key))) }))] })] }, item.id));
@@ -1,5 +1,15 @@
1
1
  import * as React from "react";
2
- export type DialogProps = {
2
+ /**
3
+ * Root props land on the DIALOG SURFACE — the element that carries
4
+ * `role="dialog"` — not on the portal wrapper. That is what a consumer needs
5
+ * to reach: `aria-describedby` has to sit beside `aria-modal` to associate the
6
+ * description, and a `data-testid` or a `style` is wanted on the panel, not on
7
+ * the full-viewport layer behind it (#223).
8
+ *
9
+ * `id` and the aria wiring are the machine's: it mints the ids that link the
10
+ * title and the description, so those are spread after anything passed in.
11
+ */
12
+ export type DialogProps = Omit<React.ComponentPropsWithoutRef<"div">, "title" | "children"> & {
3
13
  /** Controlled open state. */
4
14
  open: boolean;
5
15
  onOpenChange: (open: boolean) => void;
@@ -19,7 +29,7 @@ export type DialogProps = {
19
29
  * Escape / outside-click dismissal) with the measured Attio modal surface.
20
30
  * Compose the body with `DialogBody` and actions with `DialogFooter`.
21
31
  */
22
- export declare function Dialog({ open, onOpenChange, title, icon, width, "aria-label": ariaLabel, closeLabel, children, }: DialogProps): React.JSX.Element | null;
32
+ export declare function Dialog({ open, onOpenChange, title, icon, width, "aria-label": ariaLabel, closeLabel, children, className, style, "aria-describedby": describedBy, ...rest }: DialogProps): React.JSX.Element | null;
23
33
  export declare namespace Dialog {
24
34
  var displayName: string;
25
35
  }
@@ -1,5 +1,9 @@
1
1
  import * as React from "react";
2
- export type DrawerProps = {
2
+ /**
3
+ * Root props land on the DRAWER SURFACE — the element carrying `role="dialog"`
4
+ * — not on the portal wrapper, for the same reason as Dialog (#223).
5
+ */
6
+ export type DrawerProps = Omit<React.ComponentPropsWithoutRef<"div">, "title" | "children"> & {
3
7
  open: boolean;
4
8
  onOpenChange: (open: boolean) => void;
5
9
  /** Which edge the sheet slides in from (default right). */
@@ -16,7 +20,7 @@ export type DrawerProps = {
16
20
  * Escape / outside-click dismissal) with a panel that slides in from the edge.
17
21
  * Compose the body with `DrawerBody` and actions with `DrawerFooter`.
18
22
  */
19
- export declare function Drawer({ open, onOpenChange, side, title, icon, "aria-label": ariaLabel, closeLabel, children, }: DrawerProps): React.JSX.Element | null;
23
+ export declare function Drawer({ open, onOpenChange, side, title, icon, "aria-label": ariaLabel, closeLabel, children, className, "aria-describedby": describedBy, ...rest }: DrawerProps): React.JSX.Element | null;
20
24
  export declare namespace Drawer {
21
25
  var displayName: string;
22
26
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Zag's dialog machine always advertises a description: `getContentProps()`
3
+ * returns an `aria-describedby` pointing at the id of a description element.
4
+ * crisp renders no such element — Attio's modals have a title and a body, no
5
+ * description line — so the reference dangles, and a dangling id is not a
6
+ * relationship a screen reader can follow.
7
+ *
8
+ * It also silenced the consumer's own: the machine's props are spread after
9
+ * anything passed in, so a real `aria-describedby` was overwritten by the
10
+ * missing one every time (#223).
11
+ *
12
+ * Drop the machine's value, and let the caller's stand when there is one. A
13
+ * consumer who wants a description points at a node inside their own children,
14
+ * which is the only place one exists.
15
+ */
16
+ export declare function withDescribedBy<T extends Record<string, unknown>>(contentProps: T, describedBy: string | undefined): Omit<T, "aria-describedby"> & {
17
+ "aria-describedby"?: string;
18
+ };
@@ -1,5 +1,6 @@
1
1
  export { cx } from "./cx";
2
2
  export { defineRecipe } from "./define";
3
+ export { withDescribedBy } from "./dialog-description";
3
4
  export { isRtlElement } from "./direction";
4
5
  export { type FloatingAlign, type PlaceFloatingOptions, placeFloating, } from "./place-floating";
5
6
  export { Primitive } from "./primitive";
@@ -13,7 +13,11 @@ export type CommandItem = {
13
13
  };
14
14
  /** crisp's own generic command set (not copied from any product). */
15
15
  export declare const defaultCommandItems: CommandItem[];
16
- export type CommandProps = {
16
+ /**
17
+ * Root props land on the PALETTE SURFACE — the element carrying
18
+ * `role="dialog"` — not on the portal wrapper (#223).
19
+ */
20
+ export type CommandProps = Omit<React.ComponentPropsWithoutRef<"div">, "children" | "onSelect"> & {
17
21
  /** Controlled open state — the caller owns when the palette is shown. */
18
22
  open: boolean;
19
23
  onOpenChange: (open: boolean) => void;
@@ -50,4 +54,4 @@ export type CommandProps = {
50
54
  * option list) so keyboard/focus correctness comes from the machines, not
51
55
  * hand-rolled event handling.
52
56
  */
53
- export declare function Command({ open, onOpenChange, items, placeholder, "aria-label": ariaLabel, navigateLabel, selectLabel, emptyLabel, query: queryProp, onQueryChange, filterItems, }: CommandProps): React.JSX.Element | null;
57
+ export declare function Command({ open, onOpenChange, items, placeholder, "aria-label": ariaLabel, navigateLabel, selectLabel, emptyLabel, query: queryProp, onQueryChange, filterItems, className, "aria-describedby": describedBy, ...rest }: CommandProps): React.JSX.Element | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",