@crisp-ui-kit/crisp 0.54.5 → 0.54.7

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 (43) hide show
  1. package/dist/authshell.css +4 -0
  2. package/dist/authshell.layered.css +4 -0
  3. package/dist/cjs/components/datatable/datatable.js +28 -5
  4. package/dist/cjs/components/datatable/datatable.recipe.js +6 -0
  5. package/dist/cjs/components/personchip/personchip.js +7 -3
  6. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +10 -1
  7. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +9 -3
  8. package/dist/cjs/components/tooltip/tooltip.js +2 -2
  9. package/dist/cjs/components/tooltip/tooltip.recipe.js +37 -0
  10. package/dist/cjs/product/authshell/authshell.js +1 -1
  11. package/dist/cjs/product/authshell/authshell.recipe.js +18 -0
  12. package/dist/cjs/product/recordstable/recordstable.js +12 -3
  13. package/dist/cjs/product/richtext/richtext.js +31 -4
  14. package/dist/datatable.css +8 -4
  15. package/dist/datatable.layered.css +8 -4
  16. package/dist/esm/components/datatable/datatable.js +28 -5
  17. package/dist/esm/components/datatable/datatable.recipe.js +6 -0
  18. package/dist/esm/components/personchip/personchip.js +7 -3
  19. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +10 -1
  20. package/dist/esm/components/tabletoolbar/tabletoolbar.js +10 -4
  21. package/dist/esm/components/tooltip/tooltip.js +2 -2
  22. package/dist/esm/components/tooltip/tooltip.recipe.js +37 -0
  23. package/dist/esm/product/authshell/authshell.js +1 -1
  24. package/dist/esm/product/authshell/authshell.recipe.js +18 -0
  25. package/dist/esm/product/recordstable/recordstable.js +12 -3
  26. package/dist/esm/product/richtext/richtext.js +31 -4
  27. package/dist/recordstable.css +2 -1
  28. package/dist/recordstable.layered.css +2 -1
  29. package/dist/styles.css +23 -6
  30. package/dist/styles.layered.css +23 -6
  31. package/dist/tabletoolbar.css +2 -1
  32. package/dist/tabletoolbar.layered.css +2 -1
  33. package/dist/tooltip.css +7 -0
  34. package/dist/tooltip.d.ts +2 -2
  35. package/dist/tooltip.layered.css +7 -0
  36. package/dist/types/components/datatable/datatable.d.ts +50 -11
  37. package/dist/types/components/personchip/personchip.d.ts +14 -2
  38. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +4 -2
  39. package/dist/types/components/tooltip/tooltip.d.ts +16 -0
  40. package/dist/types/components/tooltip/tooltip.recipe.d.ts +30 -2
  41. package/dist/types/product/authshell/authshell.d.ts +4 -1
  42. package/dist/types/product/recordstable/recordstable.d.ts +12 -0
  43. package/package.json +1 -1
@@ -1,13 +1,17 @@
1
1
  .crisp-authshell {
2
2
  min-height: 100%; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; position: relative; background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; color: var(--crisp-fg-primary); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
+ &[data-single-pane] { justify-content: center; }
3
4
  & .crisp-authshell-progress { position: absolute; top: 0; left: 0; width: 100%; height: 2px; background: var(--crisp-border-default); }
4
5
  & .crisp-authshell-progress-fill { height: 2px; background: var(--crisp-fg-primary); border-radius: 4px; transition: width .2s; }
5
6
  & .crisp-authshell-logo { margin-top: var(--crisp-space-8); height: 24px; display: flex; align-items: center; }
6
7
  & .crisp-authshell-card { margin-top: var(--crisp-space-6); display: flex; height: 692px; border-radius: 20px; border: 1px solid var(--crisp-border-subtle); overflow: hidden; }
8
+ &[data-single-pane] .crisp-authshell-card { height: auto; border: none; border-radius: 0; overflow: visible; }
7
9
  & .crisp-authshell-pane { flex: 0 0 564px; padding: 72px; display: flex; flex-direction: column; box-sizing: border-box; }
10
+ &[data-single-pane] .crisp-authshell-pane { flex: 0 0 auto; padding: 0; }
8
11
  & .crisp-authshell-preview { flex: 0 0 564px; padding: 96px 64px; background: var(--crisp-bg-subtle); border-inline-start: 1px solid color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); position: relative; overflow: hidden; box-sizing: border-box; }
9
12
  & .crisp-authshell-preview::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 120px; background: linear-gradient(color-mix(in srgb, var(--crisp-bg-subtle) 0%, transparent) 0%, var(--crisp-bg-subtle) 100%); pointer-events: none; }
10
13
  & .crisp-authshell-body { width: 420px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--crisp-space-8); align-items: center; }
14
+ &[data-single-pane] .crisp-authshell-body { width: 360px; }
11
15
  & .crisp-authshell-title { font-size: 20px; line-height: 24px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.4px; color: var(--crisp-fg-primary); align-self: stretch; }
12
16
  & .crisp-authshell-form { align-self: stretch; display: flex; flex-direction: column; gap: 28px; }
13
17
  & .crisp-authshell-footer { margin-top: var(--crisp-space-6); display: flex; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
@@ -1,14 +1,18 @@
1
1
  @layer crisp-components {
2
2
  .crisp-authshell {
3
3
  min-height: 100%; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; position: relative; background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; color: var(--crisp-fg-primary); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
+ &[data-single-pane] { justify-content: center; }
4
5
  & .crisp-authshell-progress { position: absolute; top: 0; left: 0; width: 100%; height: 2px; background: var(--crisp-border-default); }
5
6
  & .crisp-authshell-progress-fill { height: 2px; background: var(--crisp-fg-primary); border-radius: 4px; transition: width .2s; }
6
7
  & .crisp-authshell-logo { margin-top: var(--crisp-space-8); height: 24px; display: flex; align-items: center; }
7
8
  & .crisp-authshell-card { margin-top: var(--crisp-space-6); display: flex; height: 692px; border-radius: 20px; border: 1px solid var(--crisp-border-subtle); overflow: hidden; }
9
+ &[data-single-pane] .crisp-authshell-card { height: auto; border: none; border-radius: 0; overflow: visible; }
8
10
  & .crisp-authshell-pane { flex: 0 0 564px; padding: 72px; display: flex; flex-direction: column; box-sizing: border-box; }
11
+ &[data-single-pane] .crisp-authshell-pane { flex: 0 0 auto; padding: 0; }
9
12
  & .crisp-authshell-preview { flex: 0 0 564px; padding: 96px 64px; background: var(--crisp-bg-subtle); border-inline-start: 1px solid color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); position: relative; overflow: hidden; box-sizing: border-box; }
10
13
  & .crisp-authshell-preview::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 120px; background: linear-gradient(color-mix(in srgb, var(--crisp-bg-subtle) 0%, transparent) 0%, var(--crisp-bg-subtle) 100%); pointer-events: none; }
11
14
  & .crisp-authshell-body { width: 420px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--crisp-space-8); align-items: center; }
15
+ &[data-single-pane] .crisp-authshell-body { width: 360px; }
12
16
  & .crisp-authshell-title { font-size: 20px; line-height: 24px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.4px; color: var(--crisp-fg-primary); align-self: stretch; }
13
17
  & .crisp-authshell-form { align-self: stretch; display: flex; flex-direction: column; gap: 28px; }
14
18
  & .crisp-authshell-footer { margin-top: var(--crisp-space-6); display: flex; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
@@ -63,8 +63,16 @@ function isLabelTruncated(el) {
63
63
  * viewport change, header text change) via `ResizeObserver`; SSR-safe the
64
64
  * same way `MiddleTruncate` is -- the DOM is only touched inside an effect,
65
65
  * so the server (and the first client paint) render the closed tooltip.
66
+ *
67
+ * #580: a `referencedType` column appends that type's icon + name after the
68
+ * label (Attio's own reference-column tooltip, e.g. "Primary location" +
69
+ * Country's icon + "Country") and uses `Tooltip`'s `"reference"` chrome
70
+ * variant -- Attio's second, structurally distinct tooltip (same
71
+ * background/colour/radius/padding/font as the plain one, a different
72
+ * `box-shadow`, crisp#563). Every other column stays exactly as #534/#558
73
+ * shipped: label-only, the `"plain"` variant.
66
74
  */
67
- function ThLabel({ header }) {
75
+ function ThLabel({ header, referencedType, }) {
68
76
  const ref = React.useRef(null);
69
77
  const [overflowing, setOverflowing] = React.useState(false);
70
78
  // biome-ignore lint/correctness/useExhaustiveDependencies: `header` isn't read in the body, but a rename (col.header changing with the column's own width unchanged) needs a re-measure a ResizeObserver on `el` won't fire for -- same reasoning as Tabs' own re-measure effect.
@@ -93,7 +101,9 @@ function ThLabel({ header }) {
93
101
  // actually show. `open={false}` when it fits keeps the Tooltip mounted
94
102
  // (asChild costs no extra DOM either way) but pins it permanently closed,
95
103
  // so hovering a non-overflowing header still opens nothing.
96
- return ((0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { content: header, asChild: true, open: overflowing ? undefined : false, children: (0, jsx_runtime_1.jsx)("span", { ref: ref, className: "crisp-datatable-th-label", children: header }) }));
104
+ const ReferenceIcon = referencedType?.icon;
105
+ const content = referencedType ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tooltip-reference-row", children: [header, ReferenceIcon && (0, jsx_runtime_1.jsx)(ReferenceIcon, { size: 16, strokeWidth: 1.75 }), referencedType.label] })) : (header);
106
+ return ((0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { content: content, asChild: true, variant: referencedType ? "reference" : "plain", open: overflowing ? undefined : false, children: (0, jsx_runtime_1.jsx)("span", { ref: ref, className: "crisp-datatable-th-label", children: header }) }));
97
107
  }
98
108
  /** A `date` input needs `yyyy-mm-dd`; cells may hold a Date, an ISO string or
99
109
  * a display string. Returns "" when it cannot be parsed, so the field opens
@@ -289,6 +299,16 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US") {
289
299
  // translation) can never silently repaint the chip
290
300
  return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: display || s }));
291
301
  }
302
+ case "url":
303
+ case "email": {
304
+ const text = display || asText(value);
305
+ if (!text)
306
+ return empty;
307
+ return ((0, jsx_runtime_1.jsx)("a", { className: "crisp-datatable-link", href: col.type === "url" ? `https://${text}` : `mailto:${text}`, target: col.type === "url" ? "_blank" : undefined, rel: col.type === "url" ? "noreferrer" : undefined,
308
+ // Same reasoning as `entity`'s `onEntityClick` link above: the
309
+ // link's own navigation must not also fire the row's onRowClick.
310
+ onClick: (ev) => ev.stopPropagation(), children: text }));
311
+ }
292
312
  default: {
293
313
  const text = display || formatCell(col.type, value, col.symbol, locale);
294
314
  const isNum = col.type === "number" || col.type === "currency";
@@ -424,7 +444,8 @@ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
424
444
  keywords: query,
425
445
  });
426
446
  }
427
- return ((0, jsx_runtime_1.jsx)("span", { ref: ref, style: { display: "contents" }, children: (0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", searchable: true, searchPlaceholder: col.refSearchPlaceholder, onSearchChange: setQuery, "aria-label": `Edit ${col.key}`, value: asText(value) || undefined, disabled: isPending, options: options, onValueChange: (v) => {
447
+ const isActor = col.editor === "actor";
448
+ return ((0, jsx_runtime_1.jsx)("span", { ref: ref, style: { display: "contents" }, children: (0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", searchable: true, searchPlaceholder: col.refSearchPlaceholder ?? (isActor ? "Search people…" : undefined), emptyLabel: col.refEmptyLabel ?? (isActor ? "No users" : undefined), onSearchChange: setQuery, "aria-label": `Edit ${col.key}`, value: asText(value) || undefined, disabled: isPending, options: options, onValueChange: (v) => {
428
449
  if (v === CREATE_REFERENCE_VALUE)
429
450
  col.onCreateReference?.(query);
430
451
  else
@@ -1199,7 +1220,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1199
1220
  ? "ascending"
1200
1221
  : "descending"
1201
1222
  : "none";
1202
- const inner = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-th-inner", children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)(ThLabel, { header: col.header }), col.sortable ? (0, jsx_runtime_1.jsx)(ChevronFor, { colKey: col.key }) : null] }));
1223
+ const inner = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-th-inner", children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)(ThLabel, { header: col.header, referencedType: col.referencedType }), col.sortable ? (0, jsx_runtime_1.jsx)(ChevronFor, { colKey: col.key }) : null] }));
1203
1224
  return ((0, jsx_runtime_1.jsxs)(table_1.TableHead, { align: alignFor(col), "aria-sort": ariaSort, "aria-colindex": colAriaIndex(i), ...dataStickyCol(i), children: [renaming === col.key ? ((0, jsx_runtime_1.jsx)("input", {
1204
1225
  // biome-ignore lint/a11y/noAutofocus: the rename item opens this field
1205
1226
  autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": renameColumnLabel, onBlur: (e) => {
@@ -1280,7 +1301,9 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1280
1301
  return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
1281
1302
  }
1282
1303
  if (isEditing &&
1283
- (col.editor === "select" || col.editor === "reference")) {
1304
+ (col.editor === "select" ||
1305
+ col.editor === "reference" ||
1306
+ col.editor === "actor")) {
1284
1307
  return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined,
1285
1308
  // Same as the plain text editor's own cell:
1286
1309
  // opening/using the picker must not also fire
@@ -144,6 +144,12 @@ const config = (0, core_1.defineRecipe)({
144
144
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, ${tokens_1.contract.bgOrange} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgOrange} 82%, ${tokens_1.contract.fgPrimary}); }
145
145
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
146
146
  & .crisp-datatable-muted { color: ${tokens_1.contract.fgMuted}; }
147
+ /* url/email cell types (#654) -- same measured values as RecordsTable's
148
+ own .crisp-recordstable-link, which this mirrors: no underline at
149
+ rest, underline on hover, own-colour text rather than a browser-blue
150
+ link. */
151
+ & .crisp-datatable-link { color: ${tokens_1.contract.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
152
+ & .crisp-datatable-link:hover { text-decoration: underline; }
147
153
  /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
148
154
  a leading 20px grip column, a 16px grip icon, and the row's own
149
155
  hairline doubling as the drop indicator (it tints toward the brand
@@ -42,15 +42,19 @@ function initials(name) {
42
42
  * Composes `Chip`'s `truncate` CSS directly rather than its string-only
43
43
  * auto-wrap (children here is an avatar plus a name, not a single string),
44
44
  * so the name gets its own `.crisp-chip-label` span while the avatar stays
45
- * fixed-size.
45
+ * fixed-size. `trailing` (#683) is the same idea for a consumer's own
46
+ * icon/badge after the name — inside the chip's own flex row rather than a
47
+ * second one a consumer would otherwise wrap this in, which needs its own
48
+ * `min-width: 0` to keep shrinking correctly and, missing that, overflows
49
+ * its container the same way #668 did before `Chip` itself was fixed.
46
50
  */
47
- function PersonChip({ name, avatar, muted, className, ...props }) {
51
+ function PersonChip({ name, avatar, muted, trailing, className, ...props }) {
48
52
  return ((0, jsx_runtime_1.jsxs)(chip_1.Chip, { tone: "neutral", truncate: true, className: className, ...props, children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", shape: "circle", tone: avatar ? "neutral" : hashTone(name),
49
53
  // Explicit rather than relying on the flex default (a fixed-size,
50
54
  // non-wrapping element's own min-content floor already resists
51
55
  // shrinking in every browser tested) — #668 asked "does the avatar
52
56
  // stay" as its own question, so it gets its own undeniable answer
53
57
  // rather than an implicit one.
54
- style: { flexShrink: 0 }, children: avatar ? (0, jsx_runtime_1.jsx)("img", { src: avatar, alt: "" }) : initials(name) }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-chip-label", style: muted ? { color: tokens_1.contract.fgNeutralMuted } : undefined, children: name })] }));
58
+ style: { flexShrink: 0 }, children: avatar ? (0, jsx_runtime_1.jsx)("img", { src: avatar, alt: "" }) : initials(name) }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-chip-label", style: muted ? { color: tokens_1.contract.fgNeutralMuted } : undefined, children: name }), trailing != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-personchip-trailing", style: { flexShrink: 0 }, children: trailing }))] }));
55
59
  }
56
60
  PersonChip.displayName = "PersonChip";
@@ -17,6 +17,14 @@ const tokens_1 = require("../../tokens");
17
17
  // block, and those depend on following `.menu` in source order. Keeping the
18
18
  // split preserves that cascade exactly, which is what lets the generated CSS
19
19
  // stay byte-for-byte identical to what shipped before the extraction.
20
+ //
21
+ // `.menu--op` (#594 follow-up) is the filter condition's own operator list —
22
+ // measured live against Attio's own popover at 135px (rounds to 134px), not
23
+ // the default 232px every other menu here uses. `.menu-item` also picked up
24
+ // `line-height: 20px` and `font-weight: medium` from the same measurement
25
+ // (its 8 rows read 14px/20px/500) — shared by every menu that renders one,
26
+ // not just the operator list, since crisp's menu-item typography was
27
+ // previously unset rather than wrong.
20
28
  const toolbarChromeHead = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-toolbar { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]}; }
21
29
  & .${namespace_1.CLASS_PREFIX}${p}-toolbar[data-inline] { padding-bottom: 0; }
22
30
  & .${namespace_1.CLASS_PREFIX}${p}-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: ${tokens_1.contract.size.row}; padding: 0 ${tokens_1.contract.space["2.5"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; color: ${tokens_1.contract.fgMuted}; }
@@ -56,7 +64,7 @@ const toolbarChromeBody = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-menu-he
56
64
  & .${namespace_1.CLASS_PREFIX}${p}-sort-learn { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; font-size: ${tokens_1.contract.text.lg}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); text-decoration: none; }
57
65
  & .${namespace_1.CLASS_PREFIX}${p}-sort-learn svg { flex-shrink: 0; }
58
66
  & .${namespace_1.CLASS_PREFIX}${p}-sort-learn:hover { color: ${tokens_1.contract.fgPrimary}; }
59
- & .${namespace_1.CLASS_PREFIX}${p}-menu-item { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; width: 100%; height: ${tokens_1.contract.size.menuRow}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; background: none; border-radius: ${tokens_1.contract.radius.md}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; text-align: left; cursor: pointer; }
67
+ & .${namespace_1.CLASS_PREFIX}${p}-menu-item { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; width: 100%; height: ${tokens_1.contract.size.menuRow}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; background: none; border-radius: ${tokens_1.contract.radius.md}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; text-align: left; cursor: pointer; }
60
68
  & .${namespace_1.CLASS_PREFIX}${p}-menu-item:hover { background: ${tokens_1.contract.bgRaised}; }
61
69
  & .${namespace_1.CLASS_PREFIX}${p}-menu-item:disabled { opacity: 0.4; cursor: default; }
62
70
  & .${namespace_1.CLASS_PREFIX}${p}-menu-item:disabled:hover { background: none; }
@@ -69,6 +77,7 @@ const toolbarChromeBody = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-menu-he
69
77
  & .${namespace_1.CLASS_PREFIX}${p}-menu--right { left: auto; right: 0; }
70
78
  & .${namespace_1.CLASS_PREFIX}${p}-menu--fixed { position: fixed; top: auto; left: auto; z-index: 40; }
71
79
  & .${namespace_1.CLASS_PREFIX}${p}-menu--hdr { min-width: 200px; }
80
+ & .${namespace_1.CLASS_PREFIX}${p}-menu--op { min-width: 134px; }
72
81
  & .${namespace_1.CLASS_PREFIX}${p}-menu-item--danger { color: ${tokens_1.contract.bgDanger}; }
73
82
  & .${namespace_1.CLASS_PREFIX}${p}-menu-item--danger svg { color: ${tokens_1.contract.bgDanger}; }
74
83
  & .${namespace_1.CLASS_PREFIX}${p}-menu-item--danger:hover { background: color-mix(in srgb, ${tokens_1.contract.bgDanger} 10%, transparent); }
@@ -52,8 +52,14 @@ const OP_LABELS = {
52
52
  is: "is",
53
53
  isnot: "is not",
54
54
  isanyof: "is any of",
55
- empty: "is empty",
56
- notempty: "is not empty",
55
+ // Attio's own text says "empty"/"not empty", not "is empty"/"is not empty"
56
+ // -- #619's own issue body quotes it verbatim this way, and #593's number/
57
+ // date operator sets ("is / is not / less than / greater than / empty /
58
+ // not empty") repeat it. #619 ported the 3 new ops but left these two
59
+ // untouched from the pre-existing 5-op set, which had drifted; caught in
60
+ // review.
61
+ empty: "empty",
62
+ notempty: "not empty",
57
63
  };
58
64
  // #619: order and label set match Attio's own text-filter operator picker
59
65
  // verbatim (measured live, per RecordsTable's own #578 port of the same
@@ -407,7 +413,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
407
413
  const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
408
414
  (!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
409
415
  return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fchip", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: crispMessages[OP_LABELS[cond.op]] ??
410
- OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsx)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => removeFilter(cond.id), children: deleteFilterLabel }) })), open && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " ", crispMessages.condition ?? "condition"] }), opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
416
+ OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), open && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "dialog", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " ", crispMessages.condition ?? "condition"] }), opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
411
417
  // biome-ignore lint/a11y/noAutofocus: focus the value field
412
418
  autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })), cond.op === "isanyof" && col?.values && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
413
419
  // biome-ignore lint/a11y/noAutofocus: focus the search field
@@ -68,7 +68,7 @@ function mergeRefs(...refs) {
68
68
  * carry them for the same reason it can't carry `aria-describedby` (below):
69
69
  * a screen reader reads state off the focused element, not an ancestor.
70
70
  */
71
- exports.Tooltip = React.forwardRef(({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, asChild = false, "aria-describedby": restDescribedBy,
71
+ exports.Tooltip = React.forwardRef(({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, asChild = false, variant = "plain", "aria-describedby": restDescribedBy,
72
72
  // Not forwarded, even though a composer's other rest props are
73
73
  // (below): `style` sizes a VISUAL box, and Tooltip's wrapper has none
74
74
  // of its own -- the box that matters is whatever the consumer's own
@@ -152,7 +152,7 @@ style: _style, ...rest }, forwardedRef) => {
152
152
  };
153
153
  const portal = open &&
154
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: {
155
+ (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { id: tipId, role: "tooltip", className: (0, tooltip_recipe_1.tooltipRecipe)({ variant }), style: {
156
156
  left: pos.left,
157
157
  top: pos.top,
158
158
  transform: pos.below
@@ -14,6 +14,16 @@ const tokens_1 = require("../../tokens");
14
14
  // (#101112, the same "dark pill" ink NavItem's own text uses) and
15
15
  // line100's light hex (#eeeff1) — both theme-constant, same as before,
16
16
  // just the wrong pair.
17
+ //
18
+ // crisp#563: this `boxShadow` is Attio's PLAIN tooltip (a truncated header
19
+ // label, measured on "Twitter follower count") — still an uncorroborated
20
+ // single sample, flagged for its own re-measurement pass, not touched here.
21
+ // Attio has a SECOND, visually similar but structurally different tooltip
22
+ // for a reference-column header (icon + the referenced type's name after
23
+ // the label, crisp#580) with its own measured shadow — an inset ring plus
24
+ // two offset shadows, `variant: "reference"` below. Background, text
25
+ // colour, radius, padding and font are identical between the two measured
26
+ // samples; only the shadow differs.
17
27
  const spec = (0, core_1.defineSpec)({
18
28
  name: "tooltip",
19
29
  base: {
@@ -38,7 +48,34 @@ const spec = (0, core_1.defineSpec)({
38
48
  // An at-rule whose body is declarations rather than rules, so a part
39
49
  // expresses it directly: the entry state the overlay animates from.
40
50
  { selector: "@starting-style", decls: { opacity: 0 } },
51
+ // #580: the icon + referenced-type-name row a `variant="reference"`
52
+ // consumer (DataTable/RecordsTable's `ThLabel`) composes into `content`.
53
+ // Lives here, not in the caller's inline style, so it reads its gap from
54
+ // the token contract like every other measured value on this surface —
55
+ // `content` is portaled straight into this bubble regardless of which
56
+ // component built the JSX, so a nested selector reaches it the same way
57
+ // it reaches anything else rendered inside `.crisp-tooltip`.
58
+ {
59
+ selector: "& .crisp-tooltip-reference-row",
60
+ decls: {
61
+ display: "inline-flex",
62
+ alignItems: "center",
63
+ gap: tokens_1.contract.space["1"],
64
+ },
65
+ },
41
66
  ],
67
+ variants: {
68
+ variant: {
69
+ // Inherits `base`'s boxShadow untouched -- crisp#563 updates that
70
+ // single value when it re-measures the plain tooltip, and this
71
+ // variant picks the correction up for free.
72
+ plain: {},
73
+ reference: {
74
+ boxShadow: "rgba(0,0,0,0) 0 0 0 1px inset, rgba(28,40,64,0.18) 0 0 2px 0, rgba(0,0,0,0.04) 0 1px 3px 0",
75
+ },
76
+ },
77
+ },
78
+ defaultVariants: { variant: "plain" },
42
79
  });
43
80
  exports.spec = spec;
44
81
  const config = (0, core_1.specToRecipe)(spec);
@@ -16,6 +16,6 @@ function AuthShell({ progress, logo, title, children, preview, footer, className
16
16
  const pct = hasProgress
17
17
  ? Math.round(Math.max(0, Math.min(1, progress)) * 100)
18
18
  : 0;
19
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, authshell_recipe_1.authshellRecipe)({ className }), ...rest, children: [hasProgress && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-progress", role: "progressbar", "aria-valuenow": pct, "aria-valuemin": 0, "aria-valuemax": 100, children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-progress-fill", style: { width: `${pct}%` } }) })), logo != null && (0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-logo", children: logo }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-authshell-card", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-pane", children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-authshell-body", children: [title != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-title", children: title })), (0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-form", children: children })] }) }), preview != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-preview", children: preview }))] }), footer != null && (0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-footer", children: footer })] }));
19
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, authshell_recipe_1.authshellRecipe)({ className }), "data-single-pane": preview == null ? "" : undefined, ...rest, children: [hasProgress && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-progress", role: "progressbar", "aria-valuenow": pct, "aria-valuemin": 0, "aria-valuemax": 100, children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-progress-fill", style: { width: `${pct}%` } }) })), logo != null && (0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-logo", children: logo }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-authshell-card", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-pane", children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-authshell-body", children: [title != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-title", children: title })), (0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-form", children: children })] }) }), preview != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-preview", children: preview }))] }), footer != null && (0, jsx_runtime_1.jsx)("div", { className: "crisp-authshell-footer", children: footer })] }));
20
20
  }
21
21
  AuthShell.displayName = "AuthShell";
@@ -10,25 +10,43 @@ const tokens_1 = require("../../tokens");
10
10
  // from the live app, EXCEPT the 1200px preview-hiding breakpoint (called out
11
11
  // at the container query below) — the pane drops out in JS somewhere between a
12
12
  // measured 954px and 1393px viewport width, so 1200px is chosen, not measured.
13
+ //
14
+ // crisp#653: that two-pane card's own 692px height, border and 420px pane
15
+ // width were measured for a SPECIFIC screen with a real product-preview
16
+ // pane -- omitting `preview` (`data-single-pane`, set by the component) used
17
+ // to keep all three anyway, leaving a bordered box with a huge empty area
18
+ // under short content. Attio's own single-pane screen (`app.attio.com/login`,
19
+ // reachable in a fresh logged-out browser context -- no session needed) has
20
+ // NO card at all: every ancestor of its "Sign in" heading measured
21
+ // `border: 0px none`, `box-shadow: none`, `background: transparent`, a
22
+ // 360-364px content column (not 420px), sized to its own content and
23
+ // roughly centred in the viewport rather than pinned near the top.
24
+ // `data-single-pane` now drops the card chrome and narrows the body to
25
+ // match; the two-pane card is unchanged.
13
26
  const config = (0, core_1.defineRecipe)({
14
27
  name: "authshell",
15
28
  base: `min-height: 100%; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; position: relative;
16
29
  background: ${tokens_1.contract.bgSurface}; font-family: ${tokens_1.contract.font.sans}; font-size: ${tokens_1.contract.text.lg}; line-height: 20px; color: ${tokens_1.contract.fgPrimary};
17
30
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
18
31
 
32
+ &[data-single-pane] { justify-content: center; }
33
+
19
34
  & .crisp-authshell-progress { position: absolute; top: 0; left: 0; width: 100%; height: 2px; background: ${tokens_1.contract.borderDefault}; }
20
35
  & .crisp-authshell-progress-fill { height: 2px; background: ${tokens_1.contract.fgPrimary}; border-radius: 4px; transition: width .2s; }
21
36
 
22
37
  & .crisp-authshell-logo { margin-top: ${tokens_1.contract.space["8"]}; height: 24px; display: flex; align-items: center; }
23
38
 
24
39
  & .crisp-authshell-card { margin-top: ${tokens_1.contract.space["6"]}; display: flex; height: 692px; border-radius: 20px; border: 1px solid ${tokens_1.contract.borderSubtle}; overflow: hidden; }
40
+ &[data-single-pane] .crisp-authshell-card { height: auto; border: none; border-radius: 0; overflow: visible; }
25
41
 
26
42
  & .crisp-authshell-pane { flex: 0 0 564px; padding: 72px; display: flex; flex-direction: column; box-sizing: border-box; }
43
+ &[data-single-pane] .crisp-authshell-pane { flex: 0 0 auto; padding: 0; }
27
44
 
28
45
  & .crisp-authshell-preview { flex: 0 0 564px; padding: 96px 64px; background: ${tokens_1.contract.bgSubtle}; border-inline-start: 1px solid color-mix(in srgb, ${tokens_1.contract.fgPrimary} 5%, transparent); position: relative; overflow: hidden; box-sizing: border-box; }
29
46
  & .crisp-authshell-preview::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 120px; background: linear-gradient(color-mix(in srgb, ${tokens_1.contract.bgSubtle} 0%, transparent) 0%, ${tokens_1.contract.bgSubtle} 100%); pointer-events: none; }
30
47
 
31
48
  & .crisp-authshell-body { width: 420px; margin: 0 auto; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["8"]}; align-items: center; }
49
+ &[data-single-pane] .crisp-authshell-body { width: 360px; }
32
50
 
33
51
  & .crisp-authshell-title { font-size: 20px; line-height: 24px; font-weight: ${tokens_1.contract.weight.semibold}; letter-spacing: -0.4px; color: ${tokens_1.contract.fgPrimary}; align-self: stretch; }
34
52
 
@@ -76,8 +76,15 @@ function isLabelTruncated(el) {
76
76
  * viewport change, label change) via `ResizeObserver`; SSR-safe the same way
77
77
  * `MiddleTruncate` is -- the DOM is only touched inside an effect, so the
78
78
  * server (and the first client paint) renders the plain span.
79
+ *
80
+ * #580: a `referencedType` column (Attio's own `type: "reference"`) appends
81
+ * that type's icon + name after the label and switches to `Tooltip`'s
82
+ * `"reference"` chrome variant (Attio's second, structurally distinct
83
+ * tooltip — same background/colour/radius/padding/font as the plain one, a
84
+ * different `box-shadow`, crisp#563). Every other column stays label-only,
85
+ * the `"plain"` variant #534/#558 shipped.
79
86
  */
80
- function ThLabel({ label }) {
87
+ function ThLabel({ label, referencedType, }) {
81
88
  const ref = React.useRef(null);
82
89
  const [overflowing, setOverflowing] = React.useState(false);
83
90
  // biome-ignore lint/correctness/useExhaustiveDependencies: `label` isn't read in the body, but a rename (col.label changing with the column's own width unchanged) needs a re-measure a ResizeObserver on `el` won't fire for -- same reasoning as Tabs' own re-measure effect.
@@ -106,7 +113,9 @@ function ThLabel({ label }) {
106
113
  // actually show. `open={false}` when it fits keeps the Tooltip mounted
107
114
  // (asChild costs no extra DOM either way) but pins it permanently closed,
108
115
  // so hovering a non-overflowing header still opens nothing.
109
- return ((0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { content: label, asChild: true, open: overflowing ? undefined : false, children: (0, jsx_runtime_1.jsx)("span", { ref: ref, className: "crisp-recordstable-th-label", children: label }) }));
116
+ const ReferenceIcon = referencedType?.icon;
117
+ const content = referencedType ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tooltip-reference-row", children: [label, ReferenceIcon && (0, jsx_runtime_1.jsx)(ReferenceIcon, { size: 16, strokeWidth: 1.75 }), referencedType.label] })) : (label);
118
+ return ((0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { content: content, asChild: true, variant: referencedType ? "reference" : "plain", open: overflowing ? undefined : false, children: (0, jsx_runtime_1.jsx)("span", { ref: ref, className: "crisp-recordstable-th-label", children: label }) }));
110
119
  }
111
120
  const EDITABLE = new Set(["text", "number", "url", "currency"]);
112
121
  // types whose cell is an always-interactive picker (not double-click-to-edit)
@@ -1213,7 +1222,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
1213
1222
  setDragCol(col.key);
1214
1223
  e.dataTransfer.effectAllowed = "move";
1215
1224
  }
1216
- : undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)(ThLabel, { label: col.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-sort", children: (0, jsx_runtime_1.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
1225
+ : undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)(ThLabel, { label: col.label, referencedType: col.referencedType }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-sort", children: (0, jsx_runtime_1.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
1217
1226
  rootRef.current &&
1218
1227
  (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { ref: menuRef, className: "crisp-recordstable-menu crisp-recordstable-menu--fixed crisp-recordstable-menu--hdr", role: "menu", "aria-label": `${col.label} column`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "asc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { size: 15, strokeWidth: 1.75 }), t.sortAscending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "desc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { size: 15, strokeWidth: 1.75 }), t.sortDescending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveLeft] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i === 0 || i >= cols.length - 1, onClick: () => moveColumn(col.key, 1), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveRight] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordstable-menu-sep" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => {
1219
1228
  setRenamingCol(col.key);
@@ -159,6 +159,17 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
159
159
  // The Range covering a live "/query" (trigger included), mirroring the
160
160
  // mention primitive below — only set in "slash" mode.
161
161
  const slashRangeRef = React.useRef(null);
162
+ // #672: the selection the user actually had when they opened the toolbar's
163
+ // "Body ▾" menu — only set in "toolbar" mode. That menu's own search
164
+ // input steals DOM focus from the body the moment it opens, so by the
165
+ // time an item is picked, `window.getSelection()` no longer points at
166
+ // the block the user meant; without this, `run()`'s `body.focus()`
167
+ // resets the caret to the start of the document (the FIRST block)
168
+ // instead, and every conversion landed there regardless of what was
169
+ // selected. Cloned, since the live Range would otherwise track DOM
170
+ // mutations (and the selection moving to the search input) between now
171
+ // and when it's restored.
172
+ const toolbarSelectionRef = React.useRef(null);
162
173
  const blockSearchRef = React.useRef(null);
163
174
  const [mention, setMention] = React.useState(null);
164
175
  // The Range covering the live "@query" text (trigger included) — kept
@@ -264,6 +275,7 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
264
275
  }, [readOnly]);
265
276
  const closeBlockMenu = React.useCallback(() => {
266
277
  slashRangeRef.current = null;
278
+ toolbarSelectionRef.current = null;
267
279
  setBlockMenu(null);
268
280
  }, []);
269
281
  /** Mirrors the mention-trigger detection below: only handles the caret
@@ -442,6 +454,14 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
442
454
  sel?.removeAllRanges();
443
455
  sel?.addRange(range);
444
456
  }
457
+ else if (blockMenu?.mode === "toolbar" && toolbarSelectionRef.current) {
458
+ // #672: no trigger text to delete here (nothing was typed to open
459
+ // this menu) -- just put the selection back where the user actually
460
+ // had it, before `applyBlockItem` -> `run()` refocuses the body.
461
+ const sel = window.getSelection();
462
+ sel?.removeAllRanges();
463
+ sel?.addRange(toolbarSelectionRef.current);
464
+ }
445
465
  applyBlockItem(item.id);
446
466
  closeBlockMenu();
447
467
  };
@@ -464,14 +484,21 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
464
484
  const toggleBlockMenuFromToolbar = () => {
465
485
  if (!bar)
466
486
  return;
467
- setBlockMenu((cur) => cur?.mode === "toolbar"
468
- ? null
469
- : {
487
+ setBlockMenu((cur) => {
488
+ if (cur?.mode === "toolbar")
489
+ return null;
490
+ // #672: capture the live selection NOW, before the menu's own search
491
+ // input (focused by the effect below) steals it away.
492
+ const sel = window.getSelection();
493
+ toolbarSelectionRef.current =
494
+ sel && sel.rangeCount > 0 ? sel.getRangeAt(0).cloneRange() : null;
495
+ return {
470
496
  anchor: { x: bar.x, y: bar.y },
471
497
  query: "",
472
498
  highlighted: 0,
473
499
  mode: "toolbar",
474
- });
500
+ };
501
+ });
475
502
  };
476
503
  // biome-ignore lint/correctness/useExhaustiveDependencies: syncEmpty only reads/writes stable refs, same as run()'s own call to it just below
477
504
  const insertMention = React.useCallback((item) => {
@@ -114,10 +114,14 @@
114
114
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
115
115
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
116
116
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
117
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
118
- a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
119
- draggable + dataTransfer, the same one column reorder already uses
120
- (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
117
+ /* url/email cell types (#654) -- same measured values as RecordsTable's own .crisp-recordstable-link, which this mirrors: no underline at rest, underline on hover, own-colour text rather than a browser-blue link. */
118
+ & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
119
+ & .crisp-datatable-link:hover { text-decoration: underline; }
120
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
121
+ hairline doubling as the drop indicator (it tints toward the brand
122
+ colour as the active drop target). The affordance + geometry are
123
+ measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
124
+ pointer-events implementation. */
121
125
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
122
126
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
123
127
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -115,10 +115,14 @@
115
115
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
116
116
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
117
117
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
118
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
119
- a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
120
- draggable + dataTransfer, the same one column reorder already uses
121
- (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
118
+ /* url/email cell types (#654) -- same measured values as RecordsTable's own .crisp-recordstable-link, which this mirrors: no underline at rest, underline on hover, own-colour text rather than a browser-blue link. */
119
+ & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
120
+ & .crisp-datatable-link:hover { text-decoration: underline; }
121
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
122
+ hairline doubling as the drop indicator (it tints toward the brand
123
+ colour as the active drop target). The affordance + geometry are
124
+ measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
125
+ pointer-events implementation. */
122
126
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
123
127
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
124
128
  & .crisp-datatable-grip:active { cursor: grabbing; }