@crisp-ui-kit/crisp 0.54.4 → 0.54.6

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 (49) hide show
  1. package/dist/authshell.css +4 -0
  2. package/dist/authshell.layered.css +4 -0
  3. package/dist/chip.css +1 -1
  4. package/dist/chip.layered.css +1 -1
  5. package/dist/cjs/components/chip/chip.recipe.js +12 -0
  6. package/dist/cjs/components/datatable/datatable.js +28 -5
  7. package/dist/cjs/components/datatable/datatable.recipe.js +6 -0
  8. package/dist/cjs/components/personchip/personchip.js +13 -3
  9. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +75 -2
  10. package/dist/cjs/components/tooltip/tooltip.js +2 -2
  11. package/dist/cjs/components/tooltip/tooltip.recipe.js +37 -0
  12. package/dist/cjs/product/authshell/authshell.js +1 -1
  13. package/dist/cjs/product/authshell/authshell.recipe.js +18 -0
  14. package/dist/cjs/product/comments/comments.js +13 -21
  15. package/dist/cjs/product/comments/comments.recipe.js +29 -14
  16. package/dist/cjs/product/recordstable/recordstable.js +12 -3
  17. package/dist/cjs/product/richtext/richtext.js +31 -4
  18. package/dist/comments.css +21 -15
  19. package/dist/comments.layered.css +21 -15
  20. package/dist/datatable.css +8 -4
  21. package/dist/datatable.layered.css +8 -4
  22. package/dist/esm/components/chip/chip.recipe.js +12 -0
  23. package/dist/esm/components/datatable/datatable.js +28 -5
  24. package/dist/esm/components/datatable/datatable.recipe.js +6 -0
  25. package/dist/esm/components/personchip/personchip.js +13 -3
  26. package/dist/esm/components/tabletoolbar/tabletoolbar.js +76 -3
  27. package/dist/esm/components/tooltip/tooltip.js +2 -2
  28. package/dist/esm/components/tooltip/tooltip.recipe.js +37 -0
  29. package/dist/esm/product/authshell/authshell.js +1 -1
  30. package/dist/esm/product/authshell/authshell.recipe.js +18 -0
  31. package/dist/esm/product/comments/comments.js +13 -21
  32. package/dist/esm/product/comments/comments.recipe.js +29 -14
  33. package/dist/esm/product/recordstable/recordstable.js +12 -3
  34. package/dist/esm/product/richtext/richtext.js +31 -4
  35. package/dist/styles.css +41 -20
  36. package/dist/styles.layered.css +41 -20
  37. package/dist/tooltip.css +7 -0
  38. package/dist/tooltip.d.ts +2 -2
  39. package/dist/tooltip.layered.css +7 -0
  40. package/dist/types/components/chip/chip.recipe.d.ts +1 -0
  41. package/dist/types/components/datatable/datatable.d.ts +50 -11
  42. package/dist/types/components/personchip/personchip.d.ts +14 -2
  43. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +32 -1
  44. package/dist/types/components/tooltip/tooltip.d.ts +16 -0
  45. package/dist/types/components/tooltip/tooltip.recipe.d.ts +30 -2
  46. package/dist/types/product/authshell/authshell.d.ts +4 -1
  47. package/dist/types/product/comments/comments.d.ts +1 -2
  48. package/dist/types/product/recordstable/recordstable.d.ts +12 -0
  49. 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); }
package/dist/chip.css CHANGED
@@ -1,5 +1,5 @@
1
1
  .crisp-chip {
2
- display: inline-flex; align-items: center; gap: 3px; height: var(--crisp-size-chip); min-width: 0; 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;
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
5
  & .crisp-chip-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
@@ -1,6 +1,6 @@
1
1
  @layer crisp-components {
2
2
  .crisp-chip {
3
- display: inline-flex; align-items: center; gap: 3px; height: var(--crisp-size-chip); min-width: 0; 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
+ 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
6
  & .crisp-chip-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
@@ -30,6 +30,18 @@ const spec = (0, core_1.defineSpec)({
30
30
  // -- `inline-flex` items don't by default -- which `truncate` below
31
31
  // depends on and any consumer constraining a Chip's width already needs.
32
32
  minWidth: 0,
33
+ // Caps the chip at its containing block's width (#668) -- `min-width: 0`
34
+ // alone only lets it shrink inside a flex/grid row with `flex-shrink`;
35
+ // it does nothing in a plain block or table-cell ancestor (GRC's own
36
+ // `DataTable` cells, a render-uniform `<td>`, not a flex row), where the
37
+ // chip just grew to content width and overflowed instead. Checked
38
+ // Attio's own actor chip for a value to match instead of guessing one:
39
+ // `max-width: none` there too -- it has no pixel cap either, and stays
40
+ // contained only because its own ancestor chain is a real flex row the
41
+ // whole way down, which `Chip` alone can't assume for every consumer.
42
+ // `100%` is the honest equivalent: never wider than the container,
43
+ // whatever kind of container it is.
44
+ maxWidth: "100%",
33
45
  paddingInline: tokens_1.contract.space["1"],
34
46
  borderRadius: tokens_1.contract.radius.chip,
35
47
  fontSize: tokens_1.contract.text.lg,
@@ -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,9 +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 }) {
48
- 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), 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 })] }));
51
+ function PersonChip({ name, avatar, muted, trailing, className, ...props }) {
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),
53
+ // Explicit rather than relying on the flex default (a fixed-size,
54
+ // non-wrapping element's own min-content floor already resists
55
+ // shrinking in every browser tested) — #668 asked "does the avatar
56
+ // stay" as its own question, so it gets its own undeniable answer
57
+ // rather than an implicit one.
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 }))] }));
49
59
  }
50
60
  PersonChip.displayName = "PersonChip";
@@ -177,7 +177,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
177
177
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
178
178
  * Created by), which one shared list can't reproduce.
179
179
  */
180
- function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, ...rest }) {
180
+ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, ...rest }) {
181
181
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
182
182
  // CrispMessagesProvider's translation of the English default below, or
183
183
  // that default itself with no provider — unchanged from before this
@@ -200,6 +200,10 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
200
200
  const deleteFilterLabel = (0, core_1.useCrispMessage)(deleteFilterLabelProp, "Delete filter");
201
201
  const ascendingLabel = (0, core_1.useCrispMessage)(ascendingLabelProp, "Ascending");
202
202
  const descendingLabel = (0, core_1.useCrispMessage)(descendingLabelProp, "Descending");
203
+ const viewSettingsLabel = (0, core_1.useCrispMessage)(viewSettingsLabelProp, "View settings");
204
+ const reorderAttributeLabel = (0, core_1.useCrispMessage)(reorderAttributeLabelProp, "Reorder attribute");
205
+ const addAttributeToViewLabel = (0, core_1.useCrispMessage)(addAttributeToViewLabelProp, "Add attribute to view");
206
+ const searchAttributesPlaceholder = (0, core_1.useCrispMessage)(searchAttributesPlaceholderProp, "Search attributes…");
203
207
  // OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
204
208
  // their own — a whole lookup table, not one string — so they translate
205
209
  // straight from the provider's dictionary at the two spots they render
@@ -211,10 +215,17 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
211
215
  // gets `columns` on the other, unchanged from before either prop existed.
212
216
  const sortColumns = sortColumnsProp ?? columns ?? [];
213
217
  const filterColumns = filterColumnsProp ?? columns ?? [];
218
+ const viewColumns = viewColumnsProp ?? columns ?? [];
214
219
  const [sortsState, setSortsState] = React.useState(defaultSorts ?? []);
215
220
  const [filtersState, setFiltersState] = React.useState(defaultFilters ?? []);
221
+ // #590: "everything shown" unless told otherwise -- lazy initializer so
222
+ // this only reads `viewColumns` once, at mount, the same way `defaultSorts`/
223
+ // `defaultFilters` above seed once and are never re-derived from a prop
224
+ // that may keep changing identity across renders.
225
+ const [shownKeysState, setShownKeysState] = React.useState(() => defaultShownKeys ?? viewColumns.map((c) => c.key));
216
226
  const sorts = sortsProp ?? sortsState;
217
227
  const filters = filtersProp ?? filtersState;
228
+ const shownKeys = shownKeysProp ?? shownKeysState;
218
229
  const putSorts = (next) => {
219
230
  if (sortsProp === undefined)
220
231
  setSortsState(next);
@@ -225,11 +236,28 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
225
236
  setFiltersState(next);
226
237
  onFiltersChange?.(next);
227
238
  };
239
+ const putShownKeys = (next) => {
240
+ if (shownKeysProp === undefined)
241
+ setShownKeysState(next);
242
+ onShownKeysChange?.(next);
243
+ };
228
244
  const [openMenu, setOpenMenu] = React.useState(null);
229
245
  const [dragSort, setDragSort] = React.useState(null);
230
246
  const [filterQuery, setFilterQuery] = React.useState("");
231
247
  // filters the "isanyof" checkbox list, same lifetime as filterQuery above
232
248
  const [anyOfQuery, setAnyOfQuery] = React.useState("");
249
+ const [dragAttr, setDragAttr] = React.useState(null);
250
+ // #590: the popover has two screens -- the draggable shown-attribute list
251
+ // (default) and, behind "Add attribute to view", a search list of the
252
+ // rest. Measured live, 2026-09-25: clicking "View settings" opens a
253
+ // `role="dialog"` of dnd-kit `aria-roledescription="sortable"` rows --
254
+ // grip, type icon, label -- with no search box, ending in a static
255
+ // "Add attribute to view" row; each row also carries a "View attribute
256
+ // menu" kebab whose own contents weren't explored (undefined scope, not
257
+ // built here -- hiding an attribute is a trailing control instead, see
258
+ // the hide button below).
259
+ const [addingAttribute, setAddingAttribute] = React.useState(false);
260
+ const [addAttrQuery, setAddAttrQuery] = React.useState("");
233
261
  // close any open menu on outside click (anything not inside a menu or its
234
262
  // trigger) or Escape
235
263
  React.useEffect(() => {
@@ -292,6 +320,27 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
292
320
  putFilters(filters.filter((c) => c.id !== id));
293
321
  setOpenMenu(null);
294
322
  };
323
+ // #590: shown attributes, in `shownKeys`' order -- a key with no matching
324
+ // `viewColumns` entry (stale state after a column is removed upstream)
325
+ // drops out rather than rendering a blank row.
326
+ const shownAttrs = shownKeys
327
+ .map((k) => viewColumns.find((c) => c.key === k))
328
+ .filter((c) => c != null);
329
+ const hiddenAttrs = viewColumns.filter((c) => !shownKeys.includes(c.key));
330
+ const moveAttr = (from, to) => {
331
+ if (from === to)
332
+ return;
333
+ const next = [...shownKeys];
334
+ const [moved] = next.splice(from, 1);
335
+ next.splice(to, 0, moved);
336
+ putShownKeys(next);
337
+ };
338
+ const hideAttr = (key) => putShownKeys(shownKeys.filter((k) => k !== key));
339
+ const showAttr = (key) => {
340
+ putShownKeys([...shownKeys, key]);
341
+ setAddingAttribute(false);
342
+ setAddAttrQuery("");
343
+ };
295
344
  // #594: the filter chip's kebab is a real role="menu" (crisp/#188's shared
296
345
  // model — arrows, Home/End, typeahead, Escape), not a hand-rolled div like
297
346
  // the sort/condition popovers above (those are role="dialog", which makes
@@ -381,7 +430,31 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
381
430
  });
382
431
  }, children: v }, v));
383
432
  })] }))] }))] }, cond.id));
384
- }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
433
+ }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost", onClick: () => {
434
+ if (openMenu === "viewsettings") {
435
+ setOpenMenu(null);
436
+ }
437
+ else {
438
+ setAddingAttribute(false);
439
+ setAddAttrQuery("");
440
+ setOpenMenu("viewsettings");
441
+ }
442
+ }, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel] }), openMenu === "viewsettings" &&
443
+ (addingAttribute ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: searchAttributesPlaceholder, "aria-label": searchAttributesPlaceholder,
444
+ // biome-ignore lint/a11y/noAutofocus: focus the search box on open, same as Filter's field picker
445
+ autoFocus: true, value: addAttrQuery, onChange: (e) => setAddAttrQuery(e.target.value) }), hiddenAttrs
446
+ .filter((col) => col.label
447
+ .toLowerCase()
448
+ .includes(addAttrQuery.trim().toLowerCase()))
449
+ .map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => showAttr(col.key), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key)))] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
450
+ if (dragAttr !== null)
451
+ e.preventDefault();
452
+ }, onDrop: () => {
453
+ if (dragAttr !== null)
454
+ moveAttr(dragAttr, i);
455
+ setDragAttr(null);
456
+ }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
457
+ `${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
385
458
  }
386
459
  /**
387
460
  * The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
@@ -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
 
@@ -46,13 +46,6 @@ const menu_1 = require("../../components/menu");
46
46
  const popover_1 = require("../../components/popover");
47
47
  const core_1 = require("../../core");
48
48
  const comments_recipe_1 = require("./comments.recipe");
49
- /** A single literal JSX occurrence for the reply glyph, used at two sizes --
50
- * so the RTL-mirroring sweep (which counts the flip-marker attribute against
51
- * source-text occurrences, not runtime renders) sees one mark for one
52
- * direction-bearing icon, not two. */
53
- function ReplyGlyph({ size }) {
54
- return ((0, jsx_runtime_1.jsx)(lucide_react_1.CornerUpLeft, { "data-rtl-flip": "", size: size, strokeWidth: 2, "aria-hidden": "true" }));
55
- }
56
49
  const DEFAULT_EMOJI = [
57
50
  "👍",
58
51
  "👎",
@@ -159,7 +152,7 @@ function serializeComposerField(field) {
159
152
  * disabled until the field holds text. Uncontrolled — reads its own DOM on
160
153
  * submit, the same shape `RichTextEditor` uses for its body.
161
154
  */
162
- const Composer = React.forwardRef(function Composer({ placeholder, postLabel, mentionTrigger, renderMentionPicker, replyToolLabel, mentionToolLabel, onSubmit, }, ref) {
155
+ const Composer = React.forwardRef(function Composer({ placeholder, postLabel, mentionTrigger, renderMentionPicker, mentionToolLabel, onSubmit, }, ref) {
163
156
  const fieldRef = React.useRef(null);
164
157
  const [empty, setEmpty] = React.useState(true);
165
158
  const [mention, setMention] = React.useState(null);
@@ -295,15 +288,15 @@ const Composer = React.forwardRef(function Composer({ placeholder, postLabel, me
295
288
  e.preventDefault();
296
289
  submit();
297
290
  }
298
- } }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-composer-foot", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-composer-tools", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-tool", "aria-label": replyToolLabel, children: (0, jsx_runtime_1.jsx)(ReplyGlyph, { size: 14 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-tool", "aria-label": mentionToolLabel, onClick: () => {
299
- const field = fieldRef.current;
300
- if (!field)
301
- return;
302
- field.focus();
303
- document.execCommand("insertText", false, mentionTrigger);
304
- syncEmpty();
305
- updateMention();
306
- }, children: "@" })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-post", disabled: empty, onClick: submit, children: postLabel })] }), mention &&
291
+ } }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-composer-foot", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-composer-tools", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-tool", "aria-label": mentionToolLabel, onClick: () => {
292
+ const field = fieldRef.current;
293
+ if (!field)
294
+ return;
295
+ field.focus();
296
+ document.execCommand("insertText", false, mentionTrigger);
297
+ syncEmpty();
298
+ updateMention();
299
+ }, children: "@" }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-post", disabled: empty, onClick: submit, children: postLabel })] }), mention &&
307
300
  renderMentionPicker?.({
308
301
  query: mention.query,
309
302
  anchor: mention.anchor,
@@ -312,7 +305,7 @@ const Composer = React.forwardRef(function Composer({ placeholder, postLabel, me
312
305
  })] }));
313
306
  });
314
307
  function CommentRow({ comment, replyLabel, reactLabel, resolveLabel, moreLabel, copyLinkLabel, deleteLabel, addReactionLabel, reactionSearchLabel, emojiOptions, resolvable, resolved, resolvedLabel, onReply, onReact, onResolve, onCopyLink, onDeleteRequest, }) {
315
- return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row", children: [(0, jsx_runtime_1.jsx)(AuthorAvatar, { author: comment.author }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row-main", children: [resolvedLabel != null && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-resolved-label", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 2.5, "aria-hidden": "true" }), resolvedLabel] })), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-author", children: comment.author.name }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-time", children: comment.time })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-body-text", children: comment.body }), comment.reactions && comment.reactions.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-reactions", children: [comment.reactions.map((r) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-comments-reaction", "data-reacted": r.reactedByMe ? "" : undefined, onClick: () => onReact(r.emoji), children: [(0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", children: r.emoji }), r.count] }, r.emoji))), (0, jsx_runtime_1.jsx)(popover_1.Popover, { align: "start", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-reaction-add", "aria-label": addReactionLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SmilePlus, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: (0, jsx_runtime_1.jsx)(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: onReact }) })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row-actions", children: [onReply && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": replyLabel, onClick: onReply, children: (0, jsx_runtime_1.jsx)(ReplyGlyph, { size: 16 }) })), (0, jsx_runtime_1.jsx)(popover_1.Popover, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": reactLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SmilePlus, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: (0, jsx_runtime_1.jsx)(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: onReact }) }), resolvable && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": resolveLabel, "aria-pressed": !!resolved, "data-active": resolved ? "" : undefined, onClick: onResolve, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": moreLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: onCopyLink, children: copyLinkLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { danger: true, onSelect: onDeleteRequest, children: deleteLabel })] })] })] }));
308
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row", children: [(0, jsx_runtime_1.jsx)(AuthorAvatar, { author: comment.author }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row-main", children: [resolvedLabel != null && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-resolved-label", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 2.5, "aria-hidden": "true" }), resolvedLabel] })), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-author", children: comment.author.name }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-time", children: comment.time })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-body-text", children: comment.body }), comment.reactions && comment.reactions.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-reactions", children: [comment.reactions.map((r) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-comments-reaction", "data-reacted": r.reactedByMe ? "" : undefined, onClick: () => onReact(r.emoji), children: [(0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", children: r.emoji }), r.count] }, r.emoji))), (0, jsx_runtime_1.jsx)(popover_1.Popover, { align: "start", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-reaction-add", "aria-label": addReactionLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SmilePlus, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: (0, jsx_runtime_1.jsx)(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: onReact }) })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row-actions", children: [onReply && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": replyLabel, onClick: onReply, children: (0, jsx_runtime_1.jsx)(lucide_react_1.CornerUpLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 2, "aria-hidden": "true" }) })), (0, jsx_runtime_1.jsx)(popover_1.Popover, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": reactLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SmilePlus, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: (0, jsx_runtime_1.jsx)(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: onReact }) }), resolvable && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": resolveLabel, "aria-pressed": !!resolved, "data-active": resolved ? "" : undefined, onClick: onResolve, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": moreLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: onCopyLink, children: copyLinkLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { danger: true, onSelect: onDeleteRequest, children: deleteLabel })] })] })] }));
316
309
  }
317
310
  /**
318
311
  * Per-record comment threads (crisp#625) — measured live on app.attio.com's
@@ -323,7 +316,7 @@ function CommentRow({ comment, replyLabel, reactLabel, resolveLabel, moreLabel,
323
316
  * separate row affordance (P-051) — deliberately NOT part of this
324
317
  * component, since it lives in a table cell this component doesn't own.
325
318
  */
326
- function CommentsPanel({ threads, currentUserId, subject, subjectIcon, showResolved = false, onShowResolvedChange, onlyMine = false, onOnlyMineChange, activeThreadId: activeThreadIdProp, onActiveThreadChange, onPost, onReact, onResolve, onDelete, onCopyLink, emojiOptions = DEFAULT_EMOJI, mentionTrigger = "@", renderMentionPicker, onClose, closeLabel: closeLabelProp, backLabel: backLabelProp, title: titleProp, threadTitle: threadTitleProp, breadcrumbPrefix: breadcrumbPrefixProp, composerPlaceholder: composerPlaceholderProp, replyPlaceholder: replyPlaceholderProp, postLabel: postLabelProp, replyPostLabel: replyPostLabelProp, emptyLabel, filterLabel: filterLabelProp, showResolvedLabel: showResolvedLabelProp, onlyMineLabel: onlyMineLabelProp, replyActionLabel: replyActionLabelProp, reactActionLabel: reactActionLabelProp, resolveActionLabel: resolveActionLabelProp, resolvedByYouLabel: resolvedByYouLabelProp, resolvedByLabel, moreActionsLabel: moreActionsLabelProp, copyLinkLabel: copyLinkLabelProp, deleteLabel: deleteLabelProp, addReactionLabel: addReactionLabelProp, reactionSearchLabel: reactionSearchLabelProp, deleteConfirmTitle: deleteConfirmTitleProp, deleteConfirmBody: deleteConfirmBodyProp, deleteConfirmActionLabel: deleteConfirmActionLabelProp, cancelLabel: cancelLabelProp, composerReplyToolLabel: composerReplyToolLabelProp, composerMentionToolLabel: composerMentionToolLabelProp, className, ...rest }) {
319
+ function CommentsPanel({ threads, currentUserId, subject, subjectIcon, showResolved = false, onShowResolvedChange, onlyMine = false, onOnlyMineChange, activeThreadId: activeThreadIdProp, onActiveThreadChange, onPost, onReact, onResolve, onDelete, onCopyLink, emojiOptions = DEFAULT_EMOJI, mentionTrigger = "@", renderMentionPicker, onClose, closeLabel: closeLabelProp, backLabel: backLabelProp, title: titleProp, threadTitle: threadTitleProp, breadcrumbPrefix: breadcrumbPrefixProp, composerPlaceholder: composerPlaceholderProp, replyPlaceholder: replyPlaceholderProp, postLabel: postLabelProp, replyPostLabel: replyPostLabelProp, emptyLabel, filterLabel: filterLabelProp, showResolvedLabel: showResolvedLabelProp, onlyMineLabel: onlyMineLabelProp, replyActionLabel: replyActionLabelProp, reactActionLabel: reactActionLabelProp, resolveActionLabel: resolveActionLabelProp, resolvedByYouLabel: resolvedByYouLabelProp, resolvedByLabel, moreActionsLabel: moreActionsLabelProp, copyLinkLabel: copyLinkLabelProp, deleteLabel: deleteLabelProp, addReactionLabel: addReactionLabelProp, reactionSearchLabel: reactionSearchLabelProp, deleteConfirmTitle: deleteConfirmTitleProp, deleteConfirmBody: deleteConfirmBodyProp, deleteConfirmActionLabel: deleteConfirmActionLabelProp, cancelLabel: cancelLabelProp, composerMentionToolLabel: composerMentionToolLabelProp, className, ...rest }) {
327
320
  const closeLabel = (0, core_1.useCrispMessage)(closeLabelProp, "Close");
328
321
  const backLabel = (0, core_1.useCrispMessage)(backLabelProp, "Back");
329
322
  const title = (0, core_1.useCrispMessage)(titleProp, "All comments");
@@ -349,7 +342,6 @@ function CommentsPanel({ threads, currentUserId, subject, subjectIcon, showResol
349
342
  const deleteConfirmBody = (0, core_1.useCrispMessage)(deleteConfirmBodyProp, "You're about to delete this comment. Once deleted it cannot be restored.");
350
343
  const deleteConfirmActionLabel = (0, core_1.useCrispMessage)(deleteConfirmActionLabelProp, "Delete");
351
344
  const cancelLabel = (0, core_1.useCrispMessage)(cancelLabelProp, "Cancel");
352
- const composerReplyToolLabel = (0, core_1.useCrispMessage)(composerReplyToolLabelProp, "Reply");
353
345
  const composerMentionToolLabel = (0, core_1.useCrispMessage)(composerMentionToolLabelProp, "Mention someone");
354
346
  const [internalActiveId, setInternalActiveId] = React.useState(null);
355
347
  const activeThreadId = activeThreadIdProp ?? internalActiveId;
@@ -394,7 +386,7 @@ function CommentsPanel({ threads, currentUserId, subject, subjectIcon, showResol
394
386
  ? resolvedByYouLabel
395
387
  : (resolvedByLabel?.(t.resolvedBy.name) ??
396
388
  `Resolved by ${t.resolvedBy.name}`)
397
- : undefined, onReply: () => setActiveThreadId(t.id), onReact: (emoji) => onReact(t.id, t.root.id, emoji), onResolve: onResolve ? () => onResolve(t.id) : undefined, onCopyLink: () => copyLink(t.id, t.root.id), onDeleteRequest: () => setDeleteTarget({ threadId: t.id, commentId: t.root.id }) }, t.id)))) }), (0, jsx_runtime_1.jsx)(Composer, { placeholder: activeThread ? replyPlaceholder : composerPlaceholder, postLabel: activeThread ? replyPostLabel : postLabel, mentionTrigger: mentionTrigger, renderMentionPicker: renderMentionPicker, replyToolLabel: composerReplyToolLabel, mentionToolLabel: composerMentionToolLabel, onSubmit: (text) => onPost(text, activeThread ? activeThread.id : null) }, activeThread ? activeThread.id : "root"), (0, jsx_runtime_1.jsxs)(dialog_1.Dialog, { open: deleteTarget != null, onOpenChange: (open) => {
389
+ : undefined, onReply: () => setActiveThreadId(t.id), onReact: (emoji) => onReact(t.id, t.root.id, emoji), onResolve: onResolve ? () => onResolve(t.id) : undefined, onCopyLink: () => copyLink(t.id, t.root.id), onDeleteRequest: () => setDeleteTarget({ threadId: t.id, commentId: t.root.id }) }, t.id)))) }), (0, jsx_runtime_1.jsx)(Composer, { placeholder: activeThread ? replyPlaceholder : composerPlaceholder, postLabel: activeThread ? replyPostLabel : postLabel, mentionTrigger: mentionTrigger, renderMentionPicker: renderMentionPicker, mentionToolLabel: composerMentionToolLabel, onSubmit: (text) => onPost(text, activeThread ? activeThread.id : null) }, activeThread ? activeThread.id : "root"), (0, jsx_runtime_1.jsxs)(dialog_1.Dialog, { open: deleteTarget != null, onOpenChange: (open) => {
398
390
  if (!open)
399
391
  setDeleteTarget(null);
400
392
  }, title: deleteConfirmTitle, children: [(0, jsx_runtime_1.jsx)(dialog_1.DialogBody, { children: deleteConfirmBody }), (0, jsx_runtime_1.jsxs)(dialog_1.DialogFooter, { children: [(0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", intent: "neutral", size: "md", onClick: () => setDeleteTarget(null), children: cancelLabel }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", intent: "danger", size: "md", onClick: () => {