@crisp-ui-kit/crisp 0.46.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/avatar.css CHANGED
@@ -1,5 +1,5 @@
1
1
  .crisp-avatar {
2
- display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-semibold); overflow: hidden; user-select: none;
2
+ display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); overflow: hidden; user-select: none;
3
3
  & img { width: 100%; height: 100%; object-fit: cover; }
4
4
  }
5
5
  .crisp-avatar--size_sm {
@@ -9,7 +9,7 @@
9
9
  height: 20px; width: 20px; font-size: 10px;
10
10
  }
11
11
  .crisp-avatar--size_lg {
12
- height: 24px; width: 24px; font-size: var(--crisp-text-sm);
12
+ height: 24px; width: 24px; font-size: var(--crisp-text-lg);
13
13
  }
14
14
  .crisp-avatar--size_xl {
15
15
  height: 32px; width: 32px; font-size: var(--crisp-text-lg);
@@ -21,7 +21,7 @@
21
21
  border-radius: 30%;
22
22
  }
23
23
  .crisp-avatar--tone_neutral {
24
- background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
24
+ background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
25
25
  }
26
26
  .crisp-avatar--tone_brand {
27
27
  background: var(--crisp-bg-brand-solid); color: #fff;
@@ -1,6 +1,6 @@
1
1
  @layer crisp-components {
2
2
  .crisp-avatar {
3
- display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-semibold); overflow: hidden; user-select: none;
3
+ display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); overflow: hidden; user-select: none;
4
4
  & img { width: 100%; height: 100%; object-fit: cover; }
5
5
  }
6
6
  .crisp-avatar--size_sm {
@@ -10,7 +10,7 @@
10
10
  height: 20px; width: 20px; font-size: 10px;
11
11
  }
12
12
  .crisp-avatar--size_lg {
13
- height: 24px; width: 24px; font-size: var(--crisp-text-sm);
13
+ height: 24px; width: 24px; font-size: var(--crisp-text-lg);
14
14
  }
15
15
  .crisp-avatar--size_xl {
16
16
  height: 32px; width: 32px; font-size: var(--crisp-text-lg);
@@ -22,7 +22,7 @@
22
22
  border-radius: 30%;
23
23
  }
24
24
  .crisp-avatar--tone_neutral {
25
- background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
25
+ background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
26
26
  }
27
27
  .crisp-avatar--tone_brand {
28
28
  background: var(--crisp-bg-brand-solid); color: #fff;
@@ -11,7 +11,7 @@ const spec = (0, core_1.defineSpec)({
11
11
  justifyContent: "center",
12
12
  flexShrink: 0,
13
13
  fontFamily: tokens_1.contract.font.sans,
14
- fontWeight: tokens_1.contract.weight.semibold,
14
+ fontWeight: tokens_1.contract.weight.medium,
15
15
  overflow: "hidden",
16
16
  userSelect: "none",
17
17
  },
@@ -26,7 +26,7 @@ const spec = (0, core_1.defineSpec)({
26
26
  size: {
27
27
  sm: { height: "16px", width: "16px", fontSize: "9px" },
28
28
  md: { height: "20px", width: "20px", fontSize: "10px" },
29
- lg: { height: "24px", width: "24px", fontSize: tokens_1.contract.text.sm },
29
+ lg: { height: "24px", width: "24px", fontSize: tokens_1.contract.text.lg },
30
30
  xl: { height: "32px", width: "32px", fontSize: tokens_1.contract.text.lg },
31
31
  },
32
32
  shape: {
@@ -36,7 +36,7 @@ const spec = (0, core_1.defineSpec)({
36
36
  tone: {
37
37
  neutral: {
38
38
  background: tokens_1.contract.bgRaised,
39
- color: tokens_1.contract.fgMuted,
39
+ color: tokens_1.contract.fgNeutralMuted,
40
40
  boxShadow: `inset 0 0 0 1px ${tokens_1.contract.borderDefault}`,
41
41
  },
42
42
  brand: { background: tokens_1.contract.bgBrandSolid, color: "#fff" },
@@ -13,7 +13,7 @@ const config = (0, core_1.defineRecipe)({
13
13
  base: `display: block; box-sizing: border-box; background: ${tokens_1.contract.bgSurface};
14
14
  & .crisp-table-scroll { overflow-x: auto; }
15
15
  & table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: ${tokens_1.contract.font.sans}; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; }
16
- & th, & td { position: relative; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
16
+ & th, & td { position: relative; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
17
17
  & th { height: ${tokens_1.contract.size.tableHeader}; background: ${tokens_1.contract.bgSurface}; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; letter-spacing: -0.14px; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); user-select: none; }
18
18
  & td { color: ${tokens_1.contract.fgPrimary}; background: ${tokens_1.contract.bgSurface}; }
19
19
  & th:last-child, & td:last-child { border-inline-end: none; }
@@ -33,19 +33,55 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Tooltip = Tooltip;
36
+ exports.Tooltip = void 0;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
+ const react_slot_1 = require("@radix-ui/react-slot");
38
39
  const React = __importStar(require("react"));
39
40
  const react_dom_1 = require("react-dom");
40
41
  const tooltip_recipe_1 = require("./tooltip.recipe");
42
+ function mergeRefs(...refs) {
43
+ return (node) => {
44
+ for (const ref of refs) {
45
+ if (!ref)
46
+ continue;
47
+ if (typeof ref === "function")
48
+ ref(node);
49
+ else
50
+ ref.current = node;
51
+ }
52
+ };
53
+ }
41
54
  /**
42
55
  * A hover/focus tooltip — a dark hint chip measured live. Wraps its
43
56
  * trigger in an inline element, shows after `delay` on pointer-enter / focus,
44
- * and hides on leave / blur / Escape. The bubble is portaled and positioned
45
- * above the trigger (flipping below near the top edge).
57
+ * and hides on leave / blur / Escape (`asChild` skips the wrapper — see its
58
+ * doc). The bubble is portaled and positioned above the trigger (flipping
59
+ * below near the top edge).
60
+ *
61
+ * Composable as another component's trigger (#360: wrapping a `Popover`'s
62
+ * or `Menu`'s trigger in a `Tooltip` silently broke it). A composer clones
63
+ * a `ref`/`id`/`aria-*`/`onClick` onto whatever element it's handed —
64
+ * `Tooltip` wasn't `forwardRef` and didn't read any of the rest, so they
65
+ * landed on the `<Tooltip>` element itself, where nothing forwarded them
66
+ * anywhere. Both now merge onto the actual trigger via `Slot` (the same
67
+ * mechanism `ContextMenu`'s `asChild` uses) — the wrapping `<span>` can't
68
+ * carry them for the same reason it can't carry `aria-describedby` (below):
69
+ * a screen reader reads state off the focused element, not an ancestor.
46
70
  */
47
- function Tooltip({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, }) {
71
+ exports.Tooltip = React.forwardRef(({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, asChild = false, "aria-describedby": restDescribedBy,
72
+ // Not forwarded, even though a composer's other rest props are
73
+ // (below): `style` sizes a VISUAL box, and Tooltip's wrapper has none
74
+ // of its own -- the box that matters is whatever the consumer's own
75
+ // trigger renders as, which they already own and can style directly.
76
+ // Also matches Popover/Menu's own composer contract: their
77
+ // cloneElement injects ref/id/aria-*/onClick, never style.
78
+ style: _style, ...rest }, forwardedRef) => {
48
79
  const tipId = React.useId();
80
+ // The trigger's box for positioning the bubble: the wrapper `<span>` in
81
+ // the default mode (it exists and hugs the child tightly even when
82
+ // `children` isn't a single element, so hover positioning never depends
83
+ // on Slot succeeding), or the child itself in `asChild` mode (there is
84
+ // no wrapper to measure).
49
85
  const triggerRef = React.useRef(null);
50
86
  const timer = React.useRef(undefined);
51
87
  const [pos, setPos] = React.useState(null);
@@ -87,27 +123,50 @@ function Tooltip({ content, children, side = "top", delay = 350, open: openProp,
87
123
  // the only way a screen reader learns of one is `aria-describedby` on the
88
124
  // element that gets focus. It cannot go on the wrapper: a description on a
89
125
  // non-focusable span around a button is never announced. So the trigger is
90
- // cloned, keeping any aria-describedby it already had, and the reference is
91
- // only present while the bubble is (a reference to a removed node is dead
126
+ // Slot-merged, keeping any aria-describedby it already had (plus a
127
+ // composer's, if any — see restDescribedBy above), and the tipId is only
128
+ // present while the bubble is (a reference to a removed node is dead
92
129
  // wiring, #225).
93
- const described = React.isValidElement(children) && open
94
- ? React.cloneElement(children, {
95
- "aria-describedby": [
96
- children.props["aria-describedby"],
97
- tipId,
98
- ]
99
- .filter(Boolean)
100
- .join(" "),
101
- })
130
+ const isElement = React.isValidElement(children);
131
+ const ownDescribedBy = isElement
132
+ ? children.props["aria-describedby"]
133
+ : undefined;
134
+ const describedBy = [ownDescribedBy, restDescribedBy, open ? tipId : undefined]
135
+ .filter(Boolean)
136
+ .join(" ") || undefined;
137
+ // Baked onto a CLONE, not passed as a prop to <Slot> below: Slot's own
138
+ // mergeProps lets the child's OWN pre-existing prop win over anything
139
+ // unrecognised (it only special-cases style/className/on* handlers), so
140
+ // a `describedBy` passed to Slot would be silently discarded in favour
141
+ // of the child's original, un-merged `aria-describedby` the moment the
142
+ // child already had one.
143
+ const describedChild = isElement
144
+ ? React.cloneElement(children, { "aria-describedby": describedBy })
102
145
  : children;
103
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { ref: triggerRef, style: { display: "inline-flex" }, onPointerEnter: openAfterDelay, onPointerLeave: close, onFocusCapture: openAfterDelay, onBlurCapture: close, onKeyDown: (e) => e.key === "Escape" && close(), children: described }), open &&
104
- pos != null &&
105
- (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { id: tipId, role: "tooltip", className: (0, tooltip_recipe_1.tooltipRecipe)(), style: {
106
- left: pos.left,
107
- top: pos.top,
108
- transform: pos.below
109
- ? "translate(-50%, 0)"
110
- : "translate(-50%, -100%)",
111
- }, children: content }), document.body)] }));
112
- }
113
- Tooltip.displayName = "Tooltip";
146
+ const hoverHandlers = {
147
+ onPointerEnter: openAfterDelay,
148
+ onPointerLeave: close,
149
+ onFocusCapture: openAfterDelay,
150
+ onBlurCapture: close,
151
+ onKeyDown: (e) => e.key === "Escape" && close(),
152
+ };
153
+ const portal = open &&
154
+ pos != null &&
155
+ (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { id: tipId, role: "tooltip", className: (0, tooltip_recipe_1.tooltipRecipe)(), style: {
156
+ left: pos.left,
157
+ top: pos.top,
158
+ transform: pos.below
159
+ ? "translate(-50%, 0)"
160
+ : "translate(-50%, -100%)",
161
+ }, children: content }), document.body);
162
+ if (asChild) {
163
+ // No wrapper at all: the child carries the hover/focus/Escape
164
+ // handling AND the composer's ref/id/aria-*/onClick directly. Not an
165
+ // element (asChild has nothing to attach to) degrades the same way
166
+ // #225 already documented for aria-describedby: renders, but neither
167
+ // hovers nor composes.
168
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [isElement ? ((0, jsx_runtime_1.jsx)(react_slot_1.Slot, { ref: mergeRefs(triggerRef, forwardedRef), ...hoverHandlers, ...rest, children: describedChild })) : (children), portal] }));
169
+ }
170
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { ref: triggerRef, style: { display: "inline-flex" }, ...hoverHandlers, children: isElement ? ((0, jsx_runtime_1.jsx)(react_slot_1.Slot, { ref: forwardedRef, ...rest, children: describedChild })) : (children) }), portal] }));
171
+ });
172
+ exports.Tooltip.displayName = "Tooltip";
@@ -8,7 +8,7 @@ const spec = defineSpec({
8
8
  justifyContent: "center",
9
9
  flexShrink: 0,
10
10
  fontFamily: c.font.sans,
11
- fontWeight: c.weight.semibold,
11
+ fontWeight: c.weight.medium,
12
12
  overflow: "hidden",
13
13
  userSelect: "none",
14
14
  },
@@ -23,7 +23,7 @@ const spec = defineSpec({
23
23
  size: {
24
24
  sm: { height: "16px", width: "16px", fontSize: "9px" },
25
25
  md: { height: "20px", width: "20px", fontSize: "10px" },
26
- lg: { height: "24px", width: "24px", fontSize: c.text.sm },
26
+ lg: { height: "24px", width: "24px", fontSize: c.text.lg },
27
27
  xl: { height: "32px", width: "32px", fontSize: c.text.lg },
28
28
  },
29
29
  shape: {
@@ -33,7 +33,7 @@ const spec = defineSpec({
33
33
  tone: {
34
34
  neutral: {
35
35
  background: c.bgRaised,
36
- color: c.fgMuted,
36
+ color: c.fgNeutralMuted,
37
37
  boxShadow: `inset 0 0 0 1px ${c.borderDefault}`,
38
38
  },
39
39
  brand: { background: c.bgBrandSolid, color: "#fff" },
@@ -10,7 +10,7 @@ const config = defineRecipe({
10
10
  base: `display: block; box-sizing: border-box; background: ${c.bgSurface};
11
11
  & .crisp-table-scroll { overflow-x: auto; }
12
12
  & table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: ${c.font.sans}; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
13
- & th, & td { position: relative; height: ${c.size.tableRow}; padding: 0 ${c.space["2"]}; border-bottom: 1px solid ${c.borderSubtle}; border-inline-end: 1px solid ${c.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
13
+ & th, & td { position: relative; height: ${c.size.tableRow}; padding: 0 ${c.space["2"]}; border-bottom: 1px solid ${c.borderSubtle}; border-inline-end: 1px solid ${c.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
14
14
  & th { height: ${c.size.tableHeader}; background: ${c.bgSurface}; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.14px; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); user-select: none; }
15
15
  & td { color: ${c.fgPrimary}; background: ${c.bgSurface}; }
16
16
  & th:last-child, & td:last-child { border-inline-end: none; }
@@ -1,15 +1,51 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Slot } from "@radix-ui/react-slot";
2
3
  import * as React from "react";
3
4
  import { createPortal } from "react-dom";
4
5
  import { tooltipRecipe } from "./tooltip.recipe.js";
6
+ function mergeRefs(...refs) {
7
+ return (node) => {
8
+ for (const ref of refs) {
9
+ if (!ref)
10
+ continue;
11
+ if (typeof ref === "function")
12
+ ref(node);
13
+ else
14
+ ref.current = node;
15
+ }
16
+ };
17
+ }
5
18
  /**
6
19
  * A hover/focus tooltip — a dark hint chip measured live. Wraps its
7
20
  * trigger in an inline element, shows after `delay` on pointer-enter / focus,
8
- * and hides on leave / blur / Escape. The bubble is portaled and positioned
9
- * above the trigger (flipping below near the top edge).
21
+ * and hides on leave / blur / Escape (`asChild` skips the wrapper — see its
22
+ * doc). The bubble is portaled and positioned above the trigger (flipping
23
+ * below near the top edge).
24
+ *
25
+ * Composable as another component's trigger (#360: wrapping a `Popover`'s
26
+ * or `Menu`'s trigger in a `Tooltip` silently broke it). A composer clones
27
+ * a `ref`/`id`/`aria-*`/`onClick` onto whatever element it's handed —
28
+ * `Tooltip` wasn't `forwardRef` and didn't read any of the rest, so they
29
+ * landed on the `<Tooltip>` element itself, where nothing forwarded them
30
+ * anywhere. Both now merge onto the actual trigger via `Slot` (the same
31
+ * mechanism `ContextMenu`'s `asChild` uses) — the wrapping `<span>` can't
32
+ * carry them for the same reason it can't carry `aria-describedby` (below):
33
+ * a screen reader reads state off the focused element, not an ancestor.
10
34
  */
11
- export function Tooltip({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, }) {
35
+ export const Tooltip = React.forwardRef(({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, asChild = false, "aria-describedby": restDescribedBy,
36
+ // Not forwarded, even though a composer's other rest props are
37
+ // (below): `style` sizes a VISUAL box, and Tooltip's wrapper has none
38
+ // of its own -- the box that matters is whatever the consumer's own
39
+ // trigger renders as, which they already own and can style directly.
40
+ // Also matches Popover/Menu's own composer contract: their
41
+ // cloneElement injects ref/id/aria-*/onClick, never style.
42
+ style: _style, ...rest }, forwardedRef) => {
12
43
  const tipId = React.useId();
44
+ // The trigger's box for positioning the bubble: the wrapper `<span>` in
45
+ // the default mode (it exists and hugs the child tightly even when
46
+ // `children` isn't a single element, so hover positioning never depends
47
+ // on Slot succeeding), or the child itself in `asChild` mode (there is
48
+ // no wrapper to measure).
13
49
  const triggerRef = React.useRef(null);
14
50
  const timer = React.useRef(undefined);
15
51
  const [pos, setPos] = React.useState(null);
@@ -51,27 +87,50 @@ export function Tooltip({ content, children, side = "top", delay = 350, open: op
51
87
  // the only way a screen reader learns of one is `aria-describedby` on the
52
88
  // element that gets focus. It cannot go on the wrapper: a description on a
53
89
  // non-focusable span around a button is never announced. So the trigger is
54
- // cloned, keeping any aria-describedby it already had, and the reference is
55
- // only present while the bubble is (a reference to a removed node is dead
90
+ // Slot-merged, keeping any aria-describedby it already had (plus a
91
+ // composer's, if any — see restDescribedBy above), and the tipId is only
92
+ // present while the bubble is (a reference to a removed node is dead
56
93
  // wiring, #225).
57
- const described = React.isValidElement(children) && open
58
- ? React.cloneElement(children, {
59
- "aria-describedby": [
60
- children.props["aria-describedby"],
61
- tipId,
62
- ]
63
- .filter(Boolean)
64
- .join(" "),
65
- })
94
+ const isElement = React.isValidElement(children);
95
+ const ownDescribedBy = isElement
96
+ ? children.props["aria-describedby"]
97
+ : undefined;
98
+ const describedBy = [ownDescribedBy, restDescribedBy, open ? tipId : undefined]
99
+ .filter(Boolean)
100
+ .join(" ") || undefined;
101
+ // Baked onto a CLONE, not passed as a prop to <Slot> below: Slot's own
102
+ // mergeProps lets the child's OWN pre-existing prop win over anything
103
+ // unrecognised (it only special-cases style/className/on* handlers), so
104
+ // a `describedBy` passed to Slot would be silently discarded in favour
105
+ // of the child's original, un-merged `aria-describedby` the moment the
106
+ // child already had one.
107
+ const describedChild = isElement
108
+ ? React.cloneElement(children, { "aria-describedby": describedBy })
66
109
  : children;
67
- return (_jsxs(_Fragment, { children: [_jsx("span", { ref: triggerRef, style: { display: "inline-flex" }, onPointerEnter: openAfterDelay, onPointerLeave: close, onFocusCapture: openAfterDelay, onBlurCapture: close, onKeyDown: (e) => e.key === "Escape" && close(), children: described }), open &&
68
- pos != null &&
69
- createPortal(_jsx("div", { id: tipId, role: "tooltip", className: tooltipRecipe(), style: {
70
- left: pos.left,
71
- top: pos.top,
72
- transform: pos.below
73
- ? "translate(-50%, 0)"
74
- : "translate(-50%, -100%)",
75
- }, children: content }), document.body)] }));
76
- }
110
+ const hoverHandlers = {
111
+ onPointerEnter: openAfterDelay,
112
+ onPointerLeave: close,
113
+ onFocusCapture: openAfterDelay,
114
+ onBlurCapture: close,
115
+ onKeyDown: (e) => e.key === "Escape" && close(),
116
+ };
117
+ const portal = open &&
118
+ pos != null &&
119
+ createPortal(_jsx("div", { id: tipId, role: "tooltip", className: tooltipRecipe(), style: {
120
+ left: pos.left,
121
+ top: pos.top,
122
+ transform: pos.below
123
+ ? "translate(-50%, 0)"
124
+ : "translate(-50%, -100%)",
125
+ }, children: content }), document.body);
126
+ if (asChild) {
127
+ // No wrapper at all: the child carries the hover/focus/Escape
128
+ // handling AND the composer's ref/id/aria-*/onClick directly. Not an
129
+ // element (asChild has nothing to attach to) degrades the same way
130
+ // #225 already documented for aria-describedby: renders, but neither
131
+ // hovers nor composes.
132
+ return (_jsxs(_Fragment, { children: [isElement ? (_jsx(Slot, { ref: mergeRefs(triggerRef, forwardedRef), ...hoverHandlers, ...rest, children: describedChild })) : (children), portal] }));
133
+ }
134
+ return (_jsxs(_Fragment, { children: [_jsx("span", { ref: triggerRef, style: { display: "inline-flex" }, ...hoverHandlers, children: isElement ? (_jsx(Slot, { ref: forwardedRef, ...rest, children: describedChild })) : (children) }), portal] }));
135
+ });
77
136
  Tooltip.displayName = "Tooltip";
package/dist/styles.css CHANGED
@@ -155,7 +155,7 @@
155
155
  }
156
156
 
157
157
  .crisp-avatar {
158
- display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-semibold); overflow: hidden; user-select: none;
158
+ display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); overflow: hidden; user-select: none;
159
159
  & img { width: 100%; height: 100%; object-fit: cover; }
160
160
  }
161
161
  .crisp-avatar--size_sm {
@@ -165,7 +165,7 @@
165
165
  height: 20px; width: 20px; font-size: 10px;
166
166
  }
167
167
  .crisp-avatar--size_lg {
168
- height: 24px; width: 24px; font-size: var(--crisp-text-sm);
168
+ height: 24px; width: 24px; font-size: var(--crisp-text-lg);
169
169
  }
170
170
  .crisp-avatar--size_xl {
171
171
  height: 32px; width: 32px; font-size: var(--crisp-text-lg);
@@ -177,7 +177,7 @@
177
177
  border-radius: 30%;
178
178
  }
179
179
  .crisp-avatar--tone_neutral {
180
- background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
180
+ background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
181
181
  }
182
182
  .crisp-avatar--tone_brand {
183
183
  background: var(--crisp-bg-brand-solid); color: #fff;
@@ -1514,7 +1514,7 @@
1514
1514
  display: block; box-sizing: border-box; background: var(--crisp-bg-surface);
1515
1515
  & .crisp-table-scroll { overflow-x: auto; }
1516
1516
  & table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
1517
- & th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
1517
+ & th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1518
1518
  & th { height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); user-select: none; }
1519
1519
  & td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
1520
1520
  & th:last-child, & td:last-child { border-inline-end: none; }
@@ -156,7 +156,7 @@
156
156
  }
157
157
 
158
158
  .crisp-avatar {
159
- display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-semibold); overflow: hidden; user-select: none;
159
+ display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); overflow: hidden; user-select: none;
160
160
  & img { width: 100%; height: 100%; object-fit: cover; }
161
161
  }
162
162
  .crisp-avatar--size_sm {
@@ -166,7 +166,7 @@
166
166
  height: 20px; width: 20px; font-size: 10px;
167
167
  }
168
168
  .crisp-avatar--size_lg {
169
- height: 24px; width: 24px; font-size: var(--crisp-text-sm);
169
+ height: 24px; width: 24px; font-size: var(--crisp-text-lg);
170
170
  }
171
171
  .crisp-avatar--size_xl {
172
172
  height: 32px; width: 32px; font-size: var(--crisp-text-lg);
@@ -178,7 +178,7 @@
178
178
  border-radius: 30%;
179
179
  }
180
180
  .crisp-avatar--tone_neutral {
181
- background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
181
+ background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-muted); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
182
182
  }
183
183
  .crisp-avatar--tone_brand {
184
184
  background: var(--crisp-bg-brand-solid); color: #fff;
@@ -1515,7 +1515,7 @@
1515
1515
  display: block; box-sizing: border-box; background: var(--crisp-bg-surface);
1516
1516
  & .crisp-table-scroll { overflow-x: auto; }
1517
1517
  & table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
1518
- & th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
1518
+ & th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1519
1519
  & th { height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); user-select: none; }
1520
1520
  & td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
1521
1521
  & th:last-child, & td:last-child { border-inline-end: none; }
package/dist/table.css CHANGED
@@ -2,7 +2,7 @@
2
2
  display: block; box-sizing: border-box; background: var(--crisp-bg-surface);
3
3
  & .crisp-table-scroll { overflow-x: auto; }
4
4
  & table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
5
- & th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
5
+ & th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
6
6
  & th { height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); user-select: none; }
7
7
  & td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
8
8
  & th:last-child, & td:last-child { border-inline-end: none; }
@@ -3,7 +3,7 @@
3
3
  display: block; box-sizing: border-box; background: var(--crisp-bg-surface);
4
4
  & .crisp-table-scroll { overflow-x: auto; }
5
5
  & table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
6
- & th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; }
6
+ & th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
7
7
  & th { height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); user-select: none; }
8
8
  & td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
9
9
  & th:last-child, & td:last-child { border-inline-end: none; }
@@ -1,12 +1,14 @@
1
1
  import * as React from "react";
2
- export type TooltipProps = {
2
+ export type TooltipProps = Omit<React.ComponentPropsWithoutRef<"span">, "content" | "children"> & {
3
3
  /** The hint text/content shown on hover or focus. */
4
4
  content: React.ReactNode;
5
5
  /**
6
- * The trigger element. A SINGLE element: it is cloned to carry the
7
- * `aria-describedby` that makes the hint reachable, so anything else — a
8
- * string, a fragment, several children — renders and hovers correctly but
9
- * cannot be described to a screen reader.
6
+ * The trigger element. A SINGLE element: it carries the
7
+ * `aria-describedby` that makes the hint reachable (and, when `Tooltip`
8
+ * itself is composed as another component's trigger, e.g. `Popover`'s
9
+ * `trigger` prop cloning in a `ref`/`id`/`aria-*`/`onClick` — #360 — those
10
+ * too), so anything else — a string, a fragment, several children —
11
+ * renders and hovers correctly but can't carry either.
10
12
  */
11
13
  children: React.ReactNode;
12
14
  /** Preferred side; flips to the other axis end when there's no room. */
@@ -20,14 +22,67 @@ export type TooltipProps = {
20
22
  defaultOpen?: boolean;
21
23
  /** Fires with the next open state, controlled or not. */
22
24
  onOpenChange?: (open: boolean) => void;
25
+ /**
26
+ * Attach directly to the child instead of wrapping it in a `<span>`
27
+ * (#360). The wrapper's `display: inline-flex` makes the child a flex
28
+ * item of a NEW container; flex items default to `min-width: auto` and
29
+ * won't shrink below their own content's width, so a child depending on
30
+ * an ANCESTOR's width constraint — most commonly `text-overflow:
31
+ * ellipsis` inside a fixed-width cell — silently stops truncating once
32
+ * it's wrapped. `asChild` requires a single element, the same as
33
+ * `ContextMenu`'s.
34
+ */
35
+ asChild?: boolean;
23
36
  };
24
37
  /**
25
38
  * A hover/focus tooltip — a dark hint chip measured live. Wraps its
26
39
  * trigger in an inline element, shows after `delay` on pointer-enter / focus,
27
- * and hides on leave / blur / Escape. The bubble is portaled and positioned
28
- * above the trigger (flipping below near the top edge).
40
+ * and hides on leave / blur / Escape (`asChild` skips the wrapper — see its
41
+ * doc). The bubble is portaled and positioned above the trigger (flipping
42
+ * below near the top edge).
43
+ *
44
+ * Composable as another component's trigger (#360: wrapping a `Popover`'s
45
+ * or `Menu`'s trigger in a `Tooltip` silently broke it). A composer clones
46
+ * a `ref`/`id`/`aria-*`/`onClick` onto whatever element it's handed —
47
+ * `Tooltip` wasn't `forwardRef` and didn't read any of the rest, so they
48
+ * landed on the `<Tooltip>` element itself, where nothing forwarded them
49
+ * anywhere. Both now merge onto the actual trigger via `Slot` (the same
50
+ * mechanism `ContextMenu`'s `asChild` uses) — the wrapping `<span>` can't
51
+ * carry them for the same reason it can't carry `aria-describedby` (below):
52
+ * a screen reader reads state off the focused element, not an ancestor.
29
53
  */
30
- export declare function Tooltip({ content, children, side, delay, open: openProp, defaultOpen, onOpenChange, }: TooltipProps): React.JSX.Element;
31
- export declare namespace Tooltip {
32
- var displayName: string;
33
- }
54
+ export declare const Tooltip: React.ForwardRefExoticComponent<Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref">, "children" | "content"> & {
55
+ /** The hint text/content shown on hover or focus. */
56
+ content: React.ReactNode;
57
+ /**
58
+ * The trigger element. A SINGLE element: it carries the
59
+ * `aria-describedby` that makes the hint reachable (and, when `Tooltip`
60
+ * itself is composed as another component's trigger, e.g. `Popover`'s
61
+ * `trigger` prop cloning in a `ref`/`id`/`aria-*`/`onClick` — #360 — those
62
+ * too), so anything else — a string, a fragment, several children —
63
+ * renders and hovers correctly but can't carry either.
64
+ */
65
+ children: React.ReactNode;
66
+ /** Preferred side; flips to the other axis end when there's no room. */
67
+ side?: "top" | "bottom";
68
+ /** Delay in ms before showing (default 350). Hides immediately. */
69
+ delay?: number;
70
+ /** Controlled open state. Omit to let Tooltip manage it (see defaultOpen).
71
+ * A controlled tooltip shows at once — `delay` only paces hover. */
72
+ open?: boolean;
73
+ /** Initially open, when uncontrolled. */
74
+ defaultOpen?: boolean;
75
+ /** Fires with the next open state, controlled or not. */
76
+ onOpenChange?: (open: boolean) => void;
77
+ /**
78
+ * Attach directly to the child instead of wrapping it in a `<span>`
79
+ * (#360). The wrapper's `display: inline-flex` makes the child a flex
80
+ * item of a NEW container; flex items default to `min-width: auto` and
81
+ * won't shrink below their own content's width, so a child depending on
82
+ * an ANCESTOR's width constraint — most commonly `text-overflow:
83
+ * ellipsis` inside a fixed-width cell — silently stops truncating once
84
+ * it's wrapped. `asChild` requires a single element, the same as
85
+ * `ContextMenu`'s.
86
+ */
87
+ asChild?: boolean;
88
+ } & React.RefAttributes<HTMLElement>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",