@crisp-ui-kit/crisp 0.57.2 → 0.57.3

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.
Files changed (53) hide show
  1. package/dist/button.css +3 -0
  2. package/dist/button.d.ts +2 -2
  3. package/dist/button.layered.css +3 -0
  4. package/dist/chip.css +1 -1
  5. package/dist/chip.layered.css +1 -1
  6. package/dist/cjs/components/breadcrumb/breadcrumb.js +1 -1
  7. package/dist/cjs/components/button/button.recipe.js +28 -0
  8. package/dist/cjs/components/chip/chip.recipe.js +3 -1
  9. package/dist/cjs/components/datatable/datatable.recipe.js +10 -4
  10. package/dist/cjs/components/datepickerfield/datepickerfield.js +2 -2
  11. package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +16 -3
  12. package/dist/cjs/components/menu/menu.js +18 -2
  13. package/dist/cjs/components/table/table.recipe.js +7 -1
  14. package/dist/cjs/core/floating-portal.js +7 -4
  15. package/dist/cjs/product/inbox/inbox.js +11 -3
  16. package/dist/cjs/product/recordpage/index.js +2 -1
  17. package/dist/cjs/product/recordpage/recordpage.js +49 -3
  18. package/dist/cjs/product/recordpage/recordpage.recipe.js +25 -0
  19. package/dist/datatable.css +10 -4
  20. package/dist/datatable.layered.css +10 -4
  21. package/dist/datepickerfield.css +8 -2
  22. package/dist/datepickerfield.d.ts +2 -2
  23. package/dist/datepickerfield.layered.css +8 -2
  24. package/dist/esm/components/breadcrumb/breadcrumb.js +1 -1
  25. package/dist/esm/components/button/button.recipe.js +28 -0
  26. package/dist/esm/components/chip/chip.recipe.js +3 -1
  27. package/dist/esm/components/datatable/datatable.recipe.js +10 -4
  28. package/dist/esm/components/datepickerfield/datepickerfield.js +2 -2
  29. package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +16 -3
  30. package/dist/esm/components/menu/menu.js +18 -2
  31. package/dist/esm/components/table/table.recipe.js +7 -1
  32. package/dist/esm/core/floating-portal.js +7 -4
  33. package/dist/esm/product/inbox/inbox.js +11 -3
  34. package/dist/esm/product/recordpage/index.js +1 -1
  35. package/dist/esm/product/recordpage/recordpage.js +49 -4
  36. package/dist/esm/product/recordpage/recordpage.recipe.js +25 -0
  37. package/dist/recordpage.css +23 -0
  38. package/dist/recordpage.layered.css +23 -0
  39. package/dist/styles.css +52 -9
  40. package/dist/styles.layered.css +52 -9
  41. package/dist/table.css +7 -2
  42. package/dist/table.layered.css +7 -2
  43. package/dist/types/components/breadcrumb/breadcrumb.d.ts +9 -1
  44. package/dist/types/components/button/button.d.ts +2 -0
  45. package/dist/types/components/button/button.recipe.d.ts +13 -0
  46. package/dist/types/components/chip/chip.recipe.d.ts +1 -1
  47. package/dist/types/components/datepickerfield/datepickerfield.d.ts +4 -1
  48. package/dist/types/components/datepickerfield/datepickerfield.recipe.d.ts +18 -3
  49. package/dist/types/core/floating-portal.d.ts +6 -5
  50. package/dist/types/product/inbox/inbox.d.ts +13 -1
  51. package/dist/types/product/recordpage/index.d.ts +1 -1
  52. package/dist/types/product/recordpage/recordpage.d.ts +46 -0
  53. package/package.json +1 -1
package/dist/button.css CHANGED
@@ -24,6 +24,9 @@
24
24
  .crisp-button--size_sm {
25
25
  height: 24px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-md); border-radius: var(--crisp-radius-md);
26
26
  }
27
+ .crisp-button--size_xs {
28
+ height: 20px; padding-inline: var(--crisp-space-1-5); font-size: 10px; font-weight: var(--crisp-weight-regular); line-height: 15px; letter-spacing: normal; border-radius: var(--crisp-radius-sm); gap: var(--crisp-space-1);
29
+ }
27
30
  .crisp-button--size_md {
28
31
  height: 28px; padding-inline: var(--crisp-space-2); font-size: var(--crisp-text-lg); border-radius: var(--crisp-radius-md);
29
32
  &[data-icon-start] { padding-inline-start: var(--crisp-space-1-5); }
package/dist/button.d.ts CHANGED
@@ -2,9 +2,9 @@ import type { RecipeFn } from "../core/types";
2
2
 
3
3
  export interface ButtonVariants {
4
4
  intent?: "primary" | "neutral" | "danger" | "ghost";
5
- size?: "sm" | "md" | "lg";
5
+ size?: "sm" | "xs" | "md" | "lg";
6
6
  shape?: "text" | "icon";
7
7
  touchTarget?: boolean;
8
8
  }
9
9
 
10
- export declare const buttonRecipe: RecipeFn<{ intent: { primary: string; neutral: string; danger: string; ghost: string }; size: { sm: string; md: string; lg: string }; shape: { text: string; icon: string }; touchTarget: { true: string; false: string } }>;
10
+ export declare const buttonRecipe: RecipeFn<{ intent: { primary: string; neutral: string; danger: string; ghost: string }; size: { sm: string; xs: string; md: string; lg: string }; shape: { text: string; icon: string }; touchTarget: { true: string; false: string } }>;
@@ -25,6 +25,9 @@
25
25
  .crisp-button--size_sm {
26
26
  height: 24px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-md); border-radius: var(--crisp-radius-md);
27
27
  }
28
+ .crisp-button--size_xs {
29
+ height: 20px; padding-inline: var(--crisp-space-1-5); font-size: 10px; font-weight: var(--crisp-weight-regular); line-height: 15px; letter-spacing: normal; border-radius: var(--crisp-radius-sm); gap: var(--crisp-space-1);
30
+ }
28
31
  .crisp-button--size_md {
29
32
  height: 28px; padding-inline: var(--crisp-space-2); font-size: var(--crisp-text-lg); border-radius: var(--crisp-radius-md);
30
33
  &[data-icon-start] { padding-inline-start: var(--crisp-space-1-5); }
package/dist/chip.css CHANGED
@@ -2,7 +2,7 @@
2
2
  display: inline-flex; align-items: center; gap: 3px; height: var(--crisp-size-chip); min-width: 0; max-width: 100%; padding-inline: var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap;
3
3
  }
4
4
  .crisp-chip--truncate_true {
5
- & .crisp-chip-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
5
+ & .crisp-chip-label { min-width: 0; overflow: clip; text-overflow: ellipsis; white-space: nowrap; display: block; }
6
6
  }
7
7
  .crisp-chip--truncate_false {
8
8
 
@@ -3,7 +3,7 @@
3
3
  display: inline-flex; align-items: center; gap: 3px; height: var(--crisp-size-chip); min-width: 0; max-width: 100%; padding-inline: var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap;
4
4
  }
5
5
  .crisp-chip--truncate_true {
6
- & .crisp-chip-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
6
+ & .crisp-chip-label { min-width: 0; overflow: clip; text-overflow: ellipsis; white-space: nowrap; display: block; }
7
7
  }
8
8
  .crisp-chip--truncate_false {
9
9
 
@@ -55,7 +55,7 @@ function Breadcrumb({ items, separator, "aria-label": ariaLabelProp, className,
55
55
  return ((0, jsx_runtime_1.jsx)("nav", { "aria-label": ariaLabel, className: (0, breadcrumb_recipe_1.breadcrumbRecipe)({ className }), ...rest, children: items.map((item, i) => {
56
56
  const last = i === items.length - 1;
57
57
  const content = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [item.icon, item.label] }));
58
- return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [last ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-breadcrumb-item", "aria-current": "page", children: content })) : item.href ? ((0, jsx_runtime_1.jsx)("a", { className: "crisp-breadcrumb-item", href: item.href, children: content })) : ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-breadcrumb-item", onClick: item.onClick, children: content })), !last && (0, jsx_runtime_1.jsx)("span", { className: "crisp-breadcrumb-sep", children: sep })] }, i));
58
+ return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [last ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-breadcrumb-item", "aria-current": "page", children: content })) : item.href ? ((0, jsx_runtime_1.jsx)("a", { className: "crisp-breadcrumb-item", href: item.href, onClick: item.onClick, children: content })) : ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-breadcrumb-item", onClick: item.onClick, children: content })), !last && (0, jsx_runtime_1.jsx)("span", { className: "crisp-breadcrumb-sep", children: sep })] }, i));
59
59
  }) }));
60
60
  }
61
61
  Breadcrumb.displayName = "Breadcrumb";
@@ -124,6 +124,34 @@ const spec = (0, core_1.defineSpec)({
124
124
  fontSize: tokens_1.contract.text.md,
125
125
  borderRadius: tokens_1.contract.radius.md,
126
126
  },
127
+ // crisp#688: Attio's own 20px small button, split out of #682 rather
128
+ // than redefining `sm` -- 72 existing `size="sm"` call sites (sidebar,
129
+ // comments, recordpage, workspacegeneralsettings, personchip,
130
+ // datatable) already rely on sm's 24px, unverified against `xs`'s own
131
+ // context. Box measured live on app.attio.com Home/Tasks ("View all",
132
+ // "Today, <date>", the date-stepper's Previous/Next-day icon
133
+ // buttons): 20px height, 6px radius, 6px inline padding on text
134
+ // buttons (a 20x20 icon-only button centers a 16px icon the same way
135
+ // shape="icon" already does for every other size, so it needs no
136
+ // extra rule), 4px gap between multiple children.
137
+ //
138
+ // The label itself is NOT 14px/#682's tracking -- getComputedStyle on
139
+ // the live button (fonts loaded, DPR 2, zero zoom/transform anywhere
140
+ // in the ancestor chain) reads 10px/regular/15px line-height/normal
141
+ // tracking, independently matching `recordpanel.recipe.ts`'s own
142
+ // "View all" measurement (10px/400) elsewhere in this codebase. 10px
143
+ // sits below the text scale's 11px floor, so it's a literal here, the
144
+ // same as recordpanel's.
145
+ xs: {
146
+ height: "20px",
147
+ paddingInline: tokens_1.contract.space["1.5"],
148
+ fontSize: "10px",
149
+ fontWeight: tokens_1.contract.weight.regular,
150
+ lineHeight: "15px",
151
+ letterSpacing: "normal",
152
+ borderRadius: tokens_1.contract.radius.sm,
153
+ gap: tokens_1.contract.space["1"],
154
+ },
127
155
  md: {
128
156
  decls: {
129
157
  height: "28px",
@@ -76,7 +76,9 @@ const spec = (0, core_1.defineSpec)({
76
76
  selector: "& .crisp-chip-label",
77
77
  decls: {
78
78
  minWidth: 0,
79
- overflow: "hidden",
79
+ // #719: `clip`, not `hidden` -- immune to a programmatic
80
+ // `scrollLeft` assignment; renders the ellipsis identically.
81
+ overflow: "clip",
80
82
  textOverflow: "ellipsis",
81
83
  whiteSpace: "nowrap",
82
84
  display: "block",
@@ -116,7 +116,13 @@ const config = (0, core_1.defineRecipe)({
116
116
  & .crisp-datatable-group-count { margin-inline-start: ${tokens_1.contract.space["1.5"]}; color: ${tokens_1.contract.fgMuted}; font-variant-numeric: tabular-nums; }
117
117
  & .crisp-datatable-th-rename { width: 100%; min-width: 0; height: ${tokens_1.contract.size.row}; margin: 0; padding: 0 ${tokens_1.contract.space["1"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; outline: none; }
118
118
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; }
119
- & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
119
+ /* #719: overflow: clip, not hidden, on every truncation rule below --
120
+ hidden still honours a programmatic scrollLeft assignment (found via
121
+ a screenshot tool that pans anything with scrollWidth > clientWidth),
122
+ blanking or fragmenting the visible text even though real user
123
+ scrolling was already blocked. clip renders the ellipsis identically
124
+ and leaves no scroll position to move. */
125
+ & .crisp-datatable-th-label { overflow: clip; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
120
126
  /* #465: 12px per the comment above (and Attio's measured header cell
121
127
  padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
122
128
  own documented value. */
@@ -203,7 +209,7 @@ const config = (0, core_1.defineRecipe)({
203
209
  /* reference picker: a leading 16px avatar per option, plus a trailing
204
210
  "Create X" row when the consumer supplies one (#449) -- same measured
205
211
  values as RecordsTable's own .crisp-recordstable-refoption*. */
206
- & .crisp-datatable-refoption { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
212
+ & .crisp-datatable-refoption { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; min-width: 0; overflow: clip; text-overflow: ellipsis; }
207
213
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
208
214
  & .crisp-datatable-refoption--create { color: ${tokens_1.contract.fgMuted}; }
209
215
  & .crisp-datatable-empty { padding: ${tokens_1.contract.space["8"]} ${tokens_1.contract.space["3"]}; text-align: center; color: ${tokens_1.contract.fgMuted}; font-size: ${tokens_1.contract.text.lg}; }
@@ -289,7 +295,7 @@ const config = (0, core_1.defineRecipe)({
289
295
  (#833/#835/#848) that happened to apply the same 500 to every
290
296
  primary-column body cell regardless of type. Made explicit here so
291
297
  fixing that leak doesn't also (wrongly) drop entity names to 400. */
292
- & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: ${tokens_1.contract.weight.medium}; text-decoration: underline; text-decoration-color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 9%, transparent); }
298
+ & .crisp-datatable-entity-name { overflow: clip; text-overflow: ellipsis; white-space: nowrap; font-weight: ${tokens_1.contract.weight.medium}; text-decoration: underline; text-decoration-color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 9%, transparent); }
293
299
  /* #893: shared by the button (onEntityClick, a different record) and
294
300
  the anchor (href, the row's own record) -- was button-only, which
295
301
  silently left a real <a> unstyled the day #893 added one. */
@@ -335,7 +341,7 @@ const config = (0, core_1.defineRecipe)({
335
341
  own .crisp-recordstable-link, which this mirrors: no underline at
336
342
  rest, underline on hover, own-colour text rather than a browser-blue
337
343
  link. */
338
- & .crisp-datatable-link { color: ${tokens_1.contract.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
344
+ & .crisp-datatable-link { color: ${tokens_1.contract.fgPrimary}; text-decoration: none; overflow: clip; text-overflow: ellipsis; }
339
345
  & .crisp-datatable-link:hover { text-decoration: underline; }
340
346
  /* phone cell type/editor (#765) -- measured live: the dial-code prefix
341
347
  is a muted rgba(0,0,0,0.63) at 14px, matching every other muted-text
@@ -58,7 +58,7 @@ const fromDate = (d) => ({
58
58
  * popover. This is what `DatePicker` used to be; `DatePicker` is now
59
59
  * seed-design's bare calendar, and this composes it.
60
60
  */
61
- function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder: placeholderProp, variant = "default", format = defaultFormat, clearable = true, clearLabel: clearLabelProp, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
61
+ function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder: placeholderProp, variant = "default", width = "full", format = defaultFormat, clearable = true, clearLabel: clearLabelProp, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
62
62
  // crisp/#471: same resolution order as Select's own.
63
63
  const placeholder = (0, core_1.useCrispMessage)(placeholderProp, "Set date…");
64
64
  const clearLabel = (0, core_1.useCrispMessage)(clearLabelProp, "Clear date");
@@ -79,7 +79,7 @@ function DatePickerField({ value, defaultValue, onValueChange, onChange, placeho
79
79
  onValueChange?.(null);
80
80
  onChange?.(null);
81
81
  };
82
- return ((0, jsx_runtime_1.jsx)("div", { className: (0, datepickerfield_recipe_1.datepickerfieldRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-datepickerfield-box", "data-ghost": variant === "ghost" ? "" : undefined, "data-placeholder": selected ? undefined : "", children: [(0, jsx_runtime_1.jsx)(popover_1.Popover, { open: open, onOpenChange: setOpen, "aria-label": ariaLabel, trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datepickerfield-trigger", "aria-label": ariaLabel, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CalendarDays, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), selected ? format(selected) : placeholder] }), children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-datepickerfield-panel", children: [(0, jsx_runtime_1.jsx)(datepicker_1.DatePicker
82
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, datepickerfield_recipe_1.datepickerfieldRecipe)({ width, className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-datepickerfield-box", "data-ghost": variant === "ghost" ? "" : undefined, "data-placeholder": selected ? undefined : "", children: [(0, jsx_runtime_1.jsx)(popover_1.Popover, { open: open, onOpenChange: setOpen, "aria-label": ariaLabel, trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datepickerfield-trigger", "aria-label": ariaLabel, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CalendarDays, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), selected ? format(selected) : placeholder] }), children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-datepickerfield-panel", children: [(0, jsx_runtime_1.jsx)(datepicker_1.DatePicker
83
83
  // DatePickerProps is a union over selectionMode, and TypeScript
84
84
  // cannot narrow it through a spread: it tries every branch and
85
85
  // reports the range one as missing props. The field is always
@@ -13,7 +13,7 @@ const config = (0, core_1.defineRecipe)({
13
13
  display: inline-flex; font-family: ${tokens_1.contract.font.sans};
14
14
 
15
15
  & .crisp-datepickerfield-box {
16
- display: inline-flex; align-items: center;
16
+ display: inline-flex; align-items: center; width: 100%;
17
17
  box-sizing: border-box; height: 32px; margin: 0;
18
18
  border-radius: ${tokens_1.contract.radius.md}; background: transparent;
19
19
  box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 7%, transparent);
@@ -22,6 +22,7 @@ const config = (0, core_1.defineRecipe)({
22
22
  & .crisp-datepickerfield-box[data-ghost] { background: transparent; box-shadow: none; }
23
23
  & .crisp-datepickerfield-box[data-ghost]:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); }
24
24
  & .crisp-datepickerfield-trigger {
25
+ flex: 1; min-width: 0;
25
26
  display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["2"]};
26
27
  height: 100%; padding: 0 ${tokens_1.contract.space["2.5"]}; margin: 0;
27
28
  border: none; background: none; color: inherit; font-family: inherit;
@@ -51,8 +52,20 @@ const config = (0, core_1.defineRecipe)({
51
52
  }
52
53
  & .crisp-datepickerfield-preset:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); }
53
54
  `,
54
- variants: {},
55
- defaultVariants: {},
55
+ variants: {
56
+ // crisp#980: full (default) fills its container, matching Select's own
57
+ // width variant/default -- so a form composing DatePickerField alongside
58
+ // Input/Select via FormField gets one consistent column width across
59
+ // every field type, not just some of them. auto sizes to content, the
60
+ // behaviour every consumer got unconditionally before this.
61
+ width: {
62
+ full: `width: 100%;`,
63
+ auto: `width: auto;`,
64
+ },
65
+ },
66
+ defaultVariants: {
67
+ width: "full",
68
+ },
56
69
  compoundVariants: [],
57
70
  });
58
71
  exports.datepickerfieldRecipe = Object.assign((0, core_1.resolveRecipe)(config), {
@@ -144,12 +144,28 @@ function Menu({ trigger, children, align = "start", open: openProp, defaultOpen,
144
144
  document.addEventListener("keydown", onKey);
145
145
  return () => document.removeEventListener("keydown", onKey);
146
146
  }, [open, close]);
147
- const { onKeyDown: onContentKeyDown } = (0, menu_keyboard_1.useMenuKeyboard)({
147
+ const { onKeyDown: onContentKeyDown, items } = (0, menu_keyboard_1.useMenuKeyboard)({
148
148
  open,
149
149
  close,
150
150
  dismiss: () => setOpen(false),
151
151
  contentRef,
152
152
  });
153
+ // Roving tabindex (crisp#964): every item hardcodes tabIndex={-1} for the
154
+ // arrow-key model above, which means none of them is ever a real Tab stop
155
+ // on its own -- kept in sync with wherever focus actually lands, same as
156
+ // MenuSheet's identical fix (#966).
157
+ const onContentFocus = React.useCallback((e) => {
158
+ for (const el of items())
159
+ el.tabIndex = -1;
160
+ e.target.tabIndex = 0;
161
+ }, [items]);
162
+ React.useEffect(() => {
163
+ if (!open)
164
+ return;
165
+ const [first] = items();
166
+ if (first)
167
+ first.tabIndex = 0;
168
+ }, [open, items]);
153
169
  const triggerEl = React.cloneElement(trigger, {
154
170
  ref: triggerRef,
155
171
  id: triggerId,
@@ -165,7 +181,7 @@ function Menu({ trigger, children, align = "start", open: openProp, defaultOpen,
165
181
  });
166
182
  return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, menu_recipe_1.menuRecipe)({ className }), children: [triggerEl, open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsx)("div", { ref: contentRef, className: contentClassName
167
183
  ? `crisp-menu-content ${contentClassName}`
168
- : "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: (0, jsx_runtime_1.jsx)(MenuContext.Provider, { value: { close }, children: children }) }) }))] }));
184
+ : "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, onFocus: onContentFocus, children: (0, jsx_runtime_1.jsx)(MenuContext.Provider, { value: { close }, children: children }) }) }))] }));
169
185
  }
170
186
  Menu.displayName = "Menu";
171
187
  function MenuItem({ children, icon, shortcut, count, danger, disabled, onSelect, closeOnSelect = true, }) {
@@ -24,7 +24,13 @@ const config = (0, core_1.defineRecipe)({
24
24
  (padding: 0 12px) and independently on a body cell's text inset
25
25
  (getBoundingClientRect delta from the cell edge). Matches
26
26
  ColumnCalc's own already-correct 12px trigger inset. */
27
- & th, & td { position: relative; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["3"]}; 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; }
27
+ /* #719: overflow: clip, not hidden -- both render the ellipsis
28
+ identically, but hidden still honours a programmatic scrollLeft
29
+ assignment (a screenshot tool, a browser extension) even though it
30
+ already blocks user-driven wheel/scrollbar input; clip leaves no
31
+ scroll position for anything to move, so the truncated text can
32
+ never be panned out of view. */
33
+ & th, & td { position: relative; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["3"]}; 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: clip; text-overflow: ellipsis; }
28
34
  /* #554: sticky on the vertical axis, mirroring RecordsTable's own
29
35
  \`.crisp-recordstable-th\`. z-index 2 sits above a plain body cell (auto)
30
36
  and below a sticky-column corner cell (DataTable's \`th[data-sticky]\`,
@@ -52,10 +52,11 @@ const react_dom_1 = require("react-dom");
52
52
  * `Dialog` was rendering offset by the dialog's own box instead of anchored
53
53
  * to its trigger.
54
54
  *
55
- * Every caller renders this conditionally on its own `open` state, which
56
- * starts `false` and is driven only by client interaction — never a prop a
57
- * fixture sets true at mount (unlike Dialog/Drawer/Command) — so this never
58
- * touches `document` during SSR, the same safety Tooltip already had.
55
+ * Every caller renders this conditionally on its own `open` state. A fixture
56
+ * CAN set that true at mount (crisp#964 needs exactly that, to give Menu's
57
+ * roving-tabindex guard something to inspect), so this checks for `window`
58
+ * itself rather than assuming the caller never does — the same guard
59
+ * `@zag-js/react`'s own `Portal` uses for the identical reason.
59
60
  *
60
61
  * `sourceRef` is the caller's own root — still in its original DOM position,
61
62
  * never portaled — used to carry a `data-theme` set on some ancestor of it
@@ -71,6 +72,8 @@ const react_dom_1 = require("react-dom");
71
72
  * element carrying that attribute.
72
73
  */
73
74
  function FloatingPortal({ children, sourceRef, }) {
75
+ if (typeof window === "undefined")
76
+ return children;
74
77
  const theme = sourceRef.current
75
78
  ?.closest("[data-theme]")
76
79
  ?.getAttribute("data-theme");
@@ -54,10 +54,18 @@ function Inbox({ notifications = [], title: titleProp, tabs, activeTab, onTabCha
54
54
  // own `empty`.
55
55
  const crispMessages = (0, core_1.useCrispMessages)();
56
56
  const title = titleProp ?? crispMessages.Notifications ?? "Notifications";
57
- const emptyTitle = emptyTitleProp ?? crispMessages["No notifications"] ?? "No notifications";
58
- const emptySubtitle = emptySubtitleProp ??
57
+ // A tab's own empty copy wins over the instance-level prop, which wins
58
+ // over the built-in default — same precedence as `title`/`emptyAction`.
59
+ const activeTabDef = tabs?.find((t) => t.id === activeTab);
60
+ const emptyTitle = activeTabDef?.emptyTitle ??
61
+ emptyTitleProp ??
62
+ crispMessages["No notifications"] ??
63
+ "No notifications";
64
+ const emptySubtitle = activeTabDef?.emptySubtitle ??
65
+ emptySubtitleProp ??
59
66
  crispMessages["Notifications such as mentions and important updates will be displayed here."] ??
60
67
  "Notifications such as mentions and important updates will be displayed here.";
68
+ const resolvedEmptyAction = activeTabDef?.emptyAction ?? emptyAction;
61
69
  const tabsLabel = (0, core_1.useCrispMessage)(tabsLabelProp, "Notification views");
62
70
  // crisp's shared tablist model, the one `Tabs` runs (#195). Hand-rolling a
63
71
  // second one is how Inbox ended up with no arrow keys, no roving tabindex
@@ -68,6 +76,6 @@ function Inbox({ notifications = [], title: titleProp, tabs, activeTab, onTabCha
68
76
  select: (id) => onTabChange?.(id),
69
77
  listRef,
70
78
  });
71
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, inbox_recipe_1.inboxRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-inbox-head", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-inbox-title", children: title }), tabs && tabs.length > 0 && ((0, jsx_runtime_1.jsx)("div", { ref: listRef, className: "crisp-inbox-tabs", role: "tablist", "aria-label": tabsLabel, onKeyDown: onKeyDown, children: tabs.map((t) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "tab", "aria-selected": t.id === activeTab, tabIndex: tabIndexFor(t.id), "data-value": t.id, disabled: t.disabled, className: "crisp-inbox-tab", "data-active": t.id === activeTab ? "" : undefined, onClick: () => onTabChange?.(t.id), children: [t.label, t.count != null && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-inbox-tab-count", children: ["(", t.count, ")"] }))] }, t.id))) }))] }), notifications.length === 0 ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-inbox-empty", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-inbox-empty-icon", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Bell, { size: 40, strokeWidth: 1.25 }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-inbox-empty-title", children: emptyTitle }), emptySubtitle != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-inbox-empty-sub", children: emptySubtitle })), emptyAction != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-inbox-empty-action", children: emptyAction }))] })) : ((0, jsx_runtime_1.jsx)("div", { className: "crisp-inbox-list", children: notifications.map((n) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-inbox-row", onClick: n.onClick, children: [n.avatar != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-inbox-row-avatar", children: n.avatar })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-inbox-row-body", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-inbox-row-msg", children: n.message }), n.time != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-inbox-row-time", children: n.time }))] }), n.unread && (0, jsx_runtime_1.jsx)("span", { className: "crisp-inbox-row-unread" })] }, n.id))) }))] }));
79
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, inbox_recipe_1.inboxRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-inbox-head", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-inbox-title", children: title }), tabs && tabs.length > 0 && ((0, jsx_runtime_1.jsx)("div", { ref: listRef, className: "crisp-inbox-tabs", role: "tablist", "aria-label": tabsLabel, onKeyDown: onKeyDown, children: tabs.map((t) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "tab", "aria-selected": t.id === activeTab, tabIndex: tabIndexFor(t.id), "data-value": t.id, disabled: t.disabled, className: "crisp-inbox-tab", "data-active": t.id === activeTab ? "" : undefined, onClick: () => onTabChange?.(t.id), children: [t.label, t.count != null && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-inbox-tab-count", children: ["(", t.count, ")"] }))] }, t.id))) }))] }), notifications.length === 0 ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-inbox-empty", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-inbox-empty-icon", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Bell, { size: 40, strokeWidth: 1.25 }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-inbox-empty-title", children: emptyTitle }), emptySubtitle != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-inbox-empty-sub", children: emptySubtitle })), resolvedEmptyAction != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-inbox-empty-action", children: resolvedEmptyAction }))] })) : ((0, jsx_runtime_1.jsx)("div", { className: "crisp-inbox-list", children: notifications.map((n) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-inbox-row", onClick: n.onClick, children: [n.avatar != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-inbox-row-avatar", children: n.avatar })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-inbox-row-body", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-inbox-row-msg", children: n.message }), n.time != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-inbox-row-time", children: n.time }))] }), n.unread && (0, jsx_runtime_1.jsx)("span", { className: "crisp-inbox-row-unread" })] }, n.id))) }))] }));
72
80
  }
73
81
  Inbox.displayName = "Inbox";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.recordpageRecipe = exports.recordPageLabels = exports.RecordPage = exports.RecordDetails = exports.useEditableField = exports.editableInputType = exports.tagTone = void 0;
3
+ exports.recordpageRecipe = exports.recordPageLabels = exports.RecordPageMain = exports.RecordPage = exports.RecordDetails = exports.useEditableField = exports.editableInputType = exports.tagTone = void 0;
4
4
  var column_1 = require("./column");
5
5
  Object.defineProperty(exports, "tagTone", { enumerable: true, get: function () { return column_1.tagTone; } });
6
6
  var editable_field_1 = require("./editable-field");
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "useEditableField", { enumerable: true, get: func
9
9
  var recordpage_1 = require("./recordpage");
10
10
  Object.defineProperty(exports, "RecordDetails", { enumerable: true, get: function () { return recordpage_1.RecordDetails; } });
11
11
  Object.defineProperty(exports, "RecordPage", { enumerable: true, get: function () { return recordpage_1.RecordPage; } });
12
+ Object.defineProperty(exports, "RecordPageMain", { enumerable: true, get: function () { return recordpage_1.RecordPageMain; } });
12
13
  Object.defineProperty(exports, "recordPageLabels", { enumerable: true, get: function () { return recordpage_1.recordPageLabels; } });
13
14
  var recordpage_recipe_1 = require("./recordpage.recipe");
14
15
  Object.defineProperty(exports, "recordpageRecipe", { enumerable: true, get: function () { return recordpage_recipe_1.recordpageRecipe; } });
@@ -35,11 +35,13 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.recordPageLabels = void 0;
37
37
  exports.RecordDetails = RecordDetails;
38
+ exports.RecordPageMain = RecordPageMain;
38
39
  exports.RecordPage = RecordPage;
39
40
  const jsx_runtime_1 = require("react/jsx-runtime");
40
41
  const lucide_react_1 = require("lucide-react");
41
42
  const React = __importStar(require("react"));
42
43
  const avatar_1 = require("../../components/avatar");
44
+ const button_1 = require("../../components/button");
43
45
  const checkbox_1 = require("../../components/checkbox");
44
46
  const drawer_1 = require("../../components/drawer");
45
47
  const menu_1 = require("../../components/menu");
@@ -100,6 +102,22 @@ exports.recordPageLabels = {
100
102
  share: "Share",
101
103
  comments: "Comments",
102
104
  noLists: "This record has not been added to any lists.",
105
+ filters: "Filters",
106
+ addMeeting: "Add meeting",
107
+ noFiles: "No files",
108
+ filesHint: "Drag a file here or choose one from your computer.",
109
+ newFolder: "New folder",
110
+ uploadFile: "Upload file",
111
+ fileMoreActions: "More file actions",
112
+ noDealsFound: "No Deals found",
113
+ noDealsHint: "There are no related records for this relationship.",
114
+ newDeal: "New Deal",
115
+ noEmails: "No emails",
116
+ noEmailsHint: "This record doesn't have any emails, or they may be hidden due to permissions.",
117
+ configureMailbox: "Configure mailbox",
118
+ upgradeToViewCalls: "Upgrade to view calls",
119
+ callsUpgradeHint: "No call recordings yet! Upgrade to Pro to start recording calls.",
120
+ upgrade: "Upgrade",
103
121
  highlights: "Highlights",
104
122
  recordDetails: "Record Details",
105
123
  searchFields: "Search fields",
@@ -515,6 +533,28 @@ function RecordDetails({ record, columns, sections, onEdit, labels, className, r
515
533
  } }))] }));
516
534
  }
517
535
  RecordDetails.displayName = "RecordDetails";
536
+ /**
537
+ * The main/tab-content column's own box, on its own (#750) — the same
538
+ * "standalone piece" `RecordDetails` already is for the Details column,
539
+ * mirrored to the other side of the layout. `RecordPage` composes this
540
+ * internally for its own tabbed body (below) — this exact box, not a second
541
+ * copy — so a consumer building a custom record page (its own tab set, its
542
+ * own menu, `RecordDetails` for the left column, but not the full
543
+ * `RecordPage`, which hardcodes the CRM tab set) gets the same
544
+ * Attio-measured inset: `flex: 1; min-width: 0; display: flex;
545
+ * flex-direction: column; min-height: 0; overflow-y: auto; padding: 12px
546
+ * 20px` (`recordpage.recipe.ts`'s own `.crisp-recordpage-main`) — without
547
+ * reaching for that internal class name directly, the only option before
548
+ * this existed.
549
+ *
550
+ * Needs the same single `.crisp-recordpage`-scoped ancestor `RecordDetails`
551
+ * already documents — wrap both in `recordpageRecipe()`, the way
552
+ * `RecordPage`'s own root does.
553
+ */
554
+ function RecordPageMain({ className, children, ...rest }) {
555
+ return ((0, jsx_runtime_1.jsx)("main", { className: (0, core_1.cx)("crisp-recordpage-main", className), ...rest, children: children }));
556
+ }
557
+ RecordPageMain.displayName = "RecordPageMain";
518
558
  /**
519
559
  * The full-page company record view — the page that opens when you click into a
520
560
  * record (as opposed to the peek [RecordPanel]). A top breadcrumb bar with
@@ -755,13 +795,19 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
755
795
  case "overview":
756
796
  return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-panel", children: [highlights !== false && ((0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-section-title", children: t.highlights }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-card", children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-grid", children: highlights.map((h) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-hl-cell", children: [h.icon && ((0, jsx_runtime_1.jsx)(h.icon, { className: "crisp-recordpage-hl-icon", size: 15, strokeWidth: 1.75 })), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-label", children: h.label }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-value", children: h.value ?? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-hl-value--empty", children: h.empty })) })] }, h.label))) }) })] })), activityFeed, (0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.emails, count: 0 }), emailsEmpty] }), (0, jsx_runtime_1.jsx)(NotesSection, { notes: notes, onAdd: addNote, labels: t }), (0, jsx_runtime_1.jsx)(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t })] }));
757
797
  case "activity":
758
- return (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel", children: activityFeed });
798
+ return ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel", children: (0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.activity, action: (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-tabtoolbar", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 14, strokeWidth: 1.75 }), t.filters] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 1.75 }), t.addMeeting] })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-feed", children: activity.map((a) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-feed-item", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-feed-dot", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Activity, { size: 13, strokeWidth: 2 }) }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-feed-body", children: [a.text, (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-feed-time", children: a.time })] })] }, a.id))) })] }) }));
799
+ case "files":
800
+ return ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-illustrated", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Folder, { size: 40, strokeWidth: 1.25 }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-illustrated-title", children: t.noFiles }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-illustrated-desc", children: t.filesHint }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-illustrated-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 1.75 }), t.newFolder] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 1.75 }), t.uploadFile] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.fileMoreActions, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) })] })] }) }));
801
+ case "deals":
802
+ return ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-illustrated", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Merge, { size: 40, strokeWidth: 1.25 }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-illustrated-title", children: t.noDealsFound }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-illustrated-desc", children: t.noDealsHint }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-illustrated-actions", children: (0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "md", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), t.newDeal] }) })] }) }));
759
803
  case "notes":
760
804
  return ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel", children: (0, jsx_runtime_1.jsx)(NotesSection, { notes: notes, onAdd: addNote, labels: t }) }));
761
805
  case "tasks":
762
806
  return ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel", children: (0, jsx_runtime_1.jsx)(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t }) }));
763
807
  case "emails":
764
- return (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel", children: emailsEmpty });
808
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-panel", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.emails }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-illustrated", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 40, strokeWidth: 1.25 }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-illustrated-title", children: t.noEmails }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-illustrated-desc", children: t.noEmailsHint }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-illustrated-actions", children: (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "primary", size: "md", children: t.configureMailbox }) })] }) })] }));
809
+ case "calls":
810
+ return ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-illustrated", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Phone, { size: 40, strokeWidth: 1.25 }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-illustrated-title crisp-recordpage-illustrated-title--lg", children: t.upgradeToViewCalls }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-illustrated-desc", children: t.callsUpgradeHint }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-illustrated-actions", children: (0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "md", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Zap, { size: 15, strokeWidth: 1.75 }), t.upgrade] }) })] }) }));
765
811
  default:
766
812
  return ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel", children: (0, jsx_runtime_1.jsx)(EmptyCard, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Folder, { size: 22, strokeWidth: 1.5 }), children: t.nothingHereYet }) }));
767
813
  }
@@ -791,6 +837,6 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
791
837
  setDetailsW((w) => grow
792
838
  ? Math.min(DETAILS_MAX_W, w + 16)
793
839
  : Math.max(DETAILS_MIN_W, w - 16));
794
- } }), (0, jsx_runtime_1.jsx)("main", { className: "crisp-recordpage-main", children: (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] }), (0, jsx_runtime_1.jsx)(drawer_1.Drawer, { open: commentsOpen, onOpenChange: setCommentsOpen, "aria-label": t.comments, style: { width: 320 }, children: (0, jsx_runtime_1.jsx)(comments_1.CommentsPanel, { threads: comments, currentUserId: me.id, onPost: postComment, onClose: () => setCommentsOpen(false), role: undefined }) })] }));
840
+ } }), (0, jsx_runtime_1.jsx)(RecordPageMain, { children: (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] }), (0, jsx_runtime_1.jsx)(drawer_1.Drawer, { open: commentsOpen, onOpenChange: setCommentsOpen, "aria-label": t.comments, style: { width: 320 }, children: (0, jsx_runtime_1.jsx)(comments_1.CommentsPanel, { threads: comments, currentUserId: me.id, onPost: postComment, onClose: () => setCommentsOpen(false), role: undefined }) })] }));
795
841
  }
796
842
  RecordPage.displayName = "RecordPage";
@@ -229,6 +229,31 @@ const config = (0, core_1.defineRecipe)({
229
229
  & .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; padding: ${tokens_1.contract.space["6"]} ${tokens_1.contract.space["3"]}; border: 1px dashed ${tokens_1.contract.borderDefault}; border-radius: ${tokens_1.contract.radius.xl}; color: ${tokens_1.contract.fgMuted}; font-size: ${tokens_1.contract.text.md}; text-align: center; }
230
230
  & .crisp-recordpage-emptycard svg { color: ${tokens_1.contract.fgMuted}; opacity: 0.7; }
231
231
 
232
+ /* #622: the Activity tab's own toolbar -- "Filters" + "+ Add meeting",
233
+ both the same measured button chrome as the topbar's own action
234
+ buttons (28px, e.field shadow, 14/500 label). */
235
+ & .crisp-recordpage-tabtoolbar { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; }
236
+
237
+ /* #622: Files tab's empty state -- measured live on Attio's own record
238
+ page (a company with no files uploaded). The 138x138 dotted-grid
239
+ backdrop is the same measured tile EmptyState's records variant uses
240
+ (crisp/#603), duplicated here rather than composed through that
241
+ component because this surface's title (16/600) and body (14/500,
242
+ 63%-mixed fgPrimary, not the flat fgMuted EmptyState uses) measure to
243
+ their own values, matching neither of that component's existing
244
+ variants. */
245
+ & .crisp-recordpage-panel--center { flex: 1; align-items: center; justify-content: center; }
246
+ & .crisp-recordpage-illustrated { display: flex; flex-direction: column; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; text-align: center; }
247
+ & .crisp-recordpage-illustrated-icon { display: flex; align-items: center; justify-content: center; width: 138px; height: 138px; margin-bottom: ${tokens_1.contract.space["2"]}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, ${tokens_1.contract.fgPrimary} 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }
248
+ & .crisp-recordpage-illustrated-title { font-size: ${tokens_1.contract.text.xl}; font-weight: ${tokens_1.contract.weight.semibold}; color: ${tokens_1.contract.fgPrimary}; }
249
+ /* #988: the Calls tab's own gated state measures a larger 20/24 title
250
+ than Files/Deals' 16/600 -- Attio's own "empty view" pattern (the
251
+ same 20/600 EmptyState's page/records variants already carry, not a
252
+ one-off). */
253
+ & .crisp-recordpage-illustrated-title--lg { font-size: 20px; line-height: 24px; }
254
+ & .crisp-recordpage-illustrated-desc { max-width: 320px; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
255
+ & .crisp-recordpage-illustrated-actions { display: inline-flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: ${tokens_1.contract.space["1.5"]}; margin-top: ${tokens_1.contract.space["2"]}; }
256
+
232
257
  & .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
233
258
 
234
259
  & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.chip}; font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 82%, transparent); }
@@ -98,7 +98,13 @@
98
98
  & .crisp-datatable-group-count { margin-inline-start: var(--crisp-space-1-5); color: var(--crisp-fg-muted); font-variant-numeric: tabular-nums; }
99
99
  & .crisp-datatable-th-rename { width: 100%; min-width: 0; height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-1); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); outline: none; }
100
100
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
101
- & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
101
+ /* #719: overflow: clip, not hidden, on every truncation rule below --
102
+ hidden still honours a programmatic scrollLeft assignment (found via
103
+ a screenshot tool that pans anything with scrollWidth > clientWidth),
104
+ blanking or fragmenting the visible text even though real user
105
+ scrolling was already blocked. clip renders the ellipsis identically
106
+ and leaves no scroll position to move. */
107
+ & .crisp-datatable-th-label { overflow: clip; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
102
108
  /* #465: 12px per the comment above (and Attio's measured header cell
103
109
  padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
104
110
  own documented value. */
@@ -185,7 +191,7 @@
185
191
  /* reference picker: a leading 16px avatar per option, plus a trailing
186
192
  "Create X" row when the consumer supplies one (#449) -- same measured
187
193
  values as RecordsTable's own .crisp-recordstable-refoption*. */
188
- & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
194
+ & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: clip; text-overflow: ellipsis; }
189
195
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
190
196
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
191
197
  & .crisp-datatable-empty { padding: var(--crisp-space-8) var(--crisp-space-3); text-align: center; color: var(--crisp-fg-muted); font-size: var(--crisp-text-lg); }
@@ -261,7 +267,7 @@
261
267
  (#833/#835/#848) that happened to apply the same 500 to every
262
268
  primary-column body cell regardless of type. Made explicit here so
263
269
  fixing that leak doesn't also (wrongly) drop entity names to 400. */
264
- & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--crisp-weight-medium); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--crisp-fg-primary) 9%, transparent); }
270
+ & .crisp-datatable-entity-name { overflow: clip; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--crisp-weight-medium); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--crisp-fg-primary) 9%, transparent); }
265
271
  /* #893: shared by the button (onEntityClick, a different record) and
266
272
  the anchor (href, the row's own record) -- was button-only, which
267
273
  silently left a real <a> unstyled the day #893 added one. */
@@ -312,7 +318,7 @@
312
318
  own .crisp-recordstable-link, which this mirrors: no underline at
313
319
  rest, underline on hover, own-colour text rather than a browser-blue
314
320
  link. */
315
- & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
321
+ & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: clip; text-overflow: ellipsis; }
316
322
  & .crisp-datatable-link:hover { text-decoration: underline; }
317
323
  /* phone cell type/editor (#765) -- measured live: the dial-code prefix
318
324
  is a muted rgba(0,0,0,0.63) at 14px, matching every other muted-text
@@ -99,7 +99,13 @@
99
99
  & .crisp-datatable-group-count { margin-inline-start: var(--crisp-space-1-5); color: var(--crisp-fg-muted); font-variant-numeric: tabular-nums; }
100
100
  & .crisp-datatable-th-rename { width: 100%; min-width: 0; height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-1); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); outline: none; }
101
101
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
102
- & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
102
+ /* #719: overflow: clip, not hidden, on every truncation rule below --
103
+ hidden still honours a programmatic scrollLeft assignment (found via
104
+ a screenshot tool that pans anything with scrollWidth > clientWidth),
105
+ blanking or fragmenting the visible text even though real user
106
+ scrolling was already blocked. clip renders the ellipsis identically
107
+ and leaves no scroll position to move. */
108
+ & .crisp-datatable-th-label { overflow: clip; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
103
109
  /* #465: 12px per the comment above (and Attio's measured header cell
104
110
  padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
105
111
  own documented value. */
@@ -186,7 +192,7 @@
186
192
  /* reference picker: a leading 16px avatar per option, plus a trailing
187
193
  "Create X" row when the consumer supplies one (#449) -- same measured
188
194
  values as RecordsTable's own .crisp-recordstable-refoption*. */
189
- & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
195
+ & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: clip; text-overflow: ellipsis; }
190
196
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
191
197
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
192
198
  & .crisp-datatable-empty { padding: var(--crisp-space-8) var(--crisp-space-3); text-align: center; color: var(--crisp-fg-muted); font-size: var(--crisp-text-lg); }
@@ -262,7 +268,7 @@
262
268
  (#833/#835/#848) that happened to apply the same 500 to every
263
269
  primary-column body cell regardless of type. Made explicit here so
264
270
  fixing that leak doesn't also (wrongly) drop entity names to 400. */
265
- & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--crisp-weight-medium); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--crisp-fg-primary) 9%, transparent); }
271
+ & .crisp-datatable-entity-name { overflow: clip; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--crisp-weight-medium); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--crisp-fg-primary) 9%, transparent); }
266
272
  /* #893: shared by the button (onEntityClick, a different record) and
267
273
  the anchor (href, the row's own record) -- was button-only, which
268
274
  silently left a real <a> unstyled the day #893 added one. */
@@ -313,7 +319,7 @@
313
319
  own .crisp-recordstable-link, which this mirrors: no underline at
314
320
  rest, underline on hover, own-colour text rather than a browser-blue
315
321
  link. */
316
- & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
322
+ & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: clip; text-overflow: ellipsis; }
317
323
  & .crisp-datatable-link:hover { text-decoration: underline; }
318
324
  /* phone cell type/editor (#765) -- measured live: the dial-code prefix
319
325
  is a muted rgba(0,0,0,0.63) at 14px, matching every other muted-text