@crisp-ui-kit/crisp 0.54.7 → 0.54.9

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 (130) hide show
  1. package/dist/authshell.css +5 -1
  2. package/dist/authshell.layered.css +5 -1
  3. package/dist/badge.css +5 -5
  4. package/dist/badge.layered.css +5 -5
  5. package/dist/board.css +36 -8
  6. package/dist/board.layered.css +36 -8
  7. package/dist/button.css +3 -1
  8. package/dist/button.layered.css +3 -1
  9. package/dist/cjs/components/badge/badge.recipe.js +28 -5
  10. package/dist/cjs/components/button/button.js +33 -2
  11. package/dist/cjs/components/button/button.recipe.js +31 -5
  12. package/dist/cjs/components/contextmenu/contextmenu.js +2 -2
  13. package/dist/cjs/components/contextmenu/contextmenu.recipe.js +24 -0
  14. package/dist/cjs/components/datatable/datatable.js +590 -41
  15. package/dist/cjs/components/datatable/datatable.recipe.js +118 -5
  16. package/dist/cjs/components/dialog/dialog.js +72 -5
  17. package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
  18. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
  19. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +365 -49
  20. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +78 -0
  21. package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
  22. package/dist/cjs/components/tooltip/tooltip.js +23 -9
  23. package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
  24. package/dist/cjs/product/authshell/authshell.js +14 -1
  25. package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
  26. package/dist/cjs/product/board/board.js +175 -46
  27. package/dist/cjs/product/board/board.recipe.js +56 -4
  28. package/dist/cjs/product/comments/comments.js +34 -12
  29. package/dist/cjs/product/comments/comments.recipe.js +31 -7
  30. package/dist/cjs/product/formdialog/formdialog.js +10 -4
  31. package/dist/cjs/product/navitem/navitem.js +7 -2
  32. package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
  33. package/dist/cjs/product/objectsettings/index.js +1 -2
  34. package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
  35. package/dist/cjs/product/recordstable/recordstable.js +1 -17
  36. package/dist/cjs/product/richtext/richtext.js +76 -1
  37. package/dist/cjs/product/savedviews/savedviews.js +55 -24
  38. package/dist/cjs/product/savedviews/savedviews.recipe.js +27 -0
  39. package/dist/cjs/product/sidebar/sidebar.js +68 -15
  40. package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
  41. package/dist/cjs/tokens/palette.js +32 -1
  42. package/dist/cjs/tokens/semantic.js +11 -0
  43. package/dist/comments.css +50 -14
  44. package/dist/comments.layered.css +50 -14
  45. package/dist/contextmenu.css +24 -0
  46. package/dist/contextmenu.layered.css +24 -0
  47. package/dist/datatable.css +101 -37
  48. package/dist/datatable.layered.css +101 -37
  49. package/dist/dialog.css +13 -1
  50. package/dist/dialog.layered.css +13 -1
  51. package/dist/esm/components/badge/badge.recipe.js +28 -5
  52. package/dist/esm/components/button/button.js +34 -3
  53. package/dist/esm/components/button/button.recipe.js +31 -5
  54. package/dist/esm/components/contextmenu/contextmenu.js +2 -2
  55. package/dist/esm/components/contextmenu/contextmenu.recipe.js +24 -0
  56. package/dist/esm/components/datatable/datatable.js +590 -42
  57. package/dist/esm/components/datatable/datatable.recipe.js +119 -6
  58. package/dist/esm/components/dialog/dialog.js +72 -5
  59. package/dist/esm/components/dialog/dialog.recipe.js +15 -1
  60. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
  61. package/dist/esm/components/tabletoolbar/tabletoolbar.js +366 -50
  62. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +80 -2
  63. package/dist/esm/components/tabs/tabs.recipe.js +11 -2
  64. package/dist/esm/components/tooltip/tooltip.js +23 -9
  65. package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
  66. package/dist/esm/product/authshell/authshell.js +15 -2
  67. package/dist/esm/product/authshell/authshell.recipe.js +13 -1
  68. package/dist/esm/product/authshell/index.js +1 -1
  69. package/dist/esm/product/board/board.js +176 -47
  70. package/dist/esm/product/board/board.recipe.js +56 -4
  71. package/dist/esm/product/comments/comments.js +35 -13
  72. package/dist/esm/product/comments/comments.recipe.js +31 -7
  73. package/dist/esm/product/formdialog/formdialog.js +10 -4
  74. package/dist/esm/product/navitem/index.js +1 -1
  75. package/dist/esm/product/navitem/navitem.js +7 -2
  76. package/dist/esm/product/navitem/navitem.recipe.js +16 -0
  77. package/dist/esm/product/objectsettings/index.js +1 -1
  78. package/dist/esm/product/objectsettings/objectsettings.js +2 -12
  79. package/dist/esm/product/recordstable/recordstable.js +2 -18
  80. package/dist/esm/product/richtext/richtext.js +76 -1
  81. package/dist/esm/product/savedviews/savedviews.js +57 -26
  82. package/dist/esm/product/savedviews/savedviews.recipe.js +27 -0
  83. package/dist/esm/product/sidebar/sidebar.js +69 -16
  84. package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
  85. package/dist/esm/tokens/palette.js +32 -1
  86. package/dist/esm/tokens/semantic.js +11 -0
  87. package/dist/native/tokens.d.ts +1 -1
  88. package/dist/native/tokens.js +16 -2
  89. package/dist/navitem.css +7 -0
  90. package/dist/navitem.layered.css +7 -0
  91. package/dist/recordstable.css +34 -32
  92. package/dist/recordstable.layered.css +34 -32
  93. package/dist/savedviews.css +27 -0
  94. package/dist/savedviews.layered.css +27 -0
  95. package/dist/sidebar.css +6 -1
  96. package/dist/sidebar.layered.css +6 -1
  97. package/dist/styles.css +369 -106
  98. package/dist/styles.layered.css +369 -106
  99. package/dist/tabletoolbar.css +47 -3
  100. package/dist/tabletoolbar.layered.css +47 -3
  101. package/dist/tabs.css +1 -1
  102. package/dist/tabs.layered.css +1 -1
  103. package/dist/tokens.css +9 -1
  104. package/dist/tokens.json +16 -2
  105. package/dist/tooltip.css +1 -1
  106. package/dist/tooltip.layered.css +1 -1
  107. package/dist/types/components/badge/badge.recipe.d.ts +5 -1
  108. package/dist/types/components/button/button.recipe.d.ts +18 -5
  109. package/dist/types/components/contextmenu/contextmenu.d.ts +8 -1
  110. package/dist/types/components/datatable/datatable.d.ts +98 -1
  111. package/dist/types/components/dialog/dialog.d.ts +25 -2
  112. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +113 -3
  113. package/dist/types/components/tooltip/tooltip.d.ts +12 -4
  114. package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
  115. package/dist/types/product/authshell/authshell.d.ts +26 -2
  116. package/dist/types/product/authshell/index.d.ts +1 -1
  117. package/dist/types/product/board/board.d.ts +53 -6
  118. package/dist/types/product/comments/comments.d.ts +16 -3
  119. package/dist/types/product/formdialog/formdialog.d.ts +2 -1
  120. package/dist/types/product/navitem/index.d.ts +1 -1
  121. package/dist/types/product/navitem/navitem.d.ts +23 -0
  122. package/dist/types/product/objectsettings/index.d.ts +1 -1
  123. package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
  124. package/dist/types/product/recordstable/recordstable.d.ts +0 -1
  125. package/dist/types/product/savedviews/savedviews.d.ts +45 -8
  126. package/dist/types/product/sidebar/sidebar.d.ts +33 -5
  127. package/dist/types/tokens/contract.d.ts +4 -0
  128. package/dist/types/tokens/palette.d.ts +13 -1
  129. package/dist/types/tokens/semantic.d.ts +11 -0
  130. package/package.json +1 -1
@@ -15,23 +15,68 @@ import { contract as c } from "../../tokens/index.js";
15
15
  // reference lacks).
16
16
  const config = defineRecipe({
17
17
  name: "board",
18
- base: `display: flex; gap: ${c.space["2"]}; height: 100%; box-sizing: border-box; padding: ${c.space["3"]}; overflow-x: auto; align-items: flex-start; font-family: ${c.font.sans};
19
- & .crisp-board-col { display: flex; flex-direction: column; width: 268px; flex-shrink: 0; max-height: 100%; box-sizing: border-box; }
18
+ base: `display: flex; flex-direction: column; gap: ${c.space["2"]}; height: 100%; box-sizing: border-box; font-family: ${c.font.sans};
19
+ /* #718: the density toolbar's own row -- everything that used to be on
20
+ the root (padding/overflow/the columns themselves) moves to
21
+ .crisp-board-row below so this can sit above it, unscrolled. */
22
+ & .crisp-board-toolbar { display: flex; align-items: center; justify-content: flex-end; gap: ${c.space["2"]}; padding: 0 ${c.space["3"]}; flex-shrink: 0; }
23
+ /* Measured live (Deals pipeline): a 54×28px group, 24×24px buttons,
24
+ 2px padding/gap -- an 8px gap between the two groups comes from the
25
+ toolbar's own \`gap\` above. */
26
+ & .crisp-board-density { display: flex; align-items: center; gap: 2px; padding: 2px; border-radius: 10px; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); }
27
+ & .crisp-board-density button { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: none; border-radius: 8px; background: none; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); font-family: inherit; cursor: pointer; }
28
+ & .crisp-board-density button[data-state="on"] { background: ${c.bgSurface}; color: ${c.fgPrimary}; }
29
+ & .crisp-board-row { display: flex; gap: ${c.space["2"]}; flex: 1; min-height: 0; box-sizing: border-box; padding: ${c.space["3"]}; overflow-x: auto; align-items: flex-start; }
30
+ & .crisp-board-col { display: flex; flex-direction: column; width: 268px; flex-shrink: 0; max-height: 100%; box-sizing: border-box; transition: width 0.15s; }
31
+ /* #718: a collapsed column is a 52px vertical rail -- header only (dot
32
+ + count stacked above a vertical-rl title), body/footer hidden
33
+ entirely. Measured live: 52px width, 8px 0 padding on the header,
34
+ 14px/600 title text. */
35
+ & .crisp-board-col[data-density="collapse"] { width: 52px; }
36
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-header { flex-direction: column; align-items: center; height: auto; padding: ${c.space["2"]} 0; gap: ${c.space["1"]}; }
37
+ /* Measured stacking order (top to bottom): dot, count, THEN the vertical
38
+ title -- reordered visually via \`order\`, not by changing the JSX/DOM
39
+ order, which stays dot/title/count (reading order, unaffected by
40
+ density). */
41
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-title { order: 1; writing-mode: vertical-rl; font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; }
42
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-count { order: 0; }
43
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-body,
44
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-footer { display: none; }
20
45
  & .crisp-board-col-header { display: flex; align-items: center; gap: ${c.space["2"]}; height: 36px; padding: 0 ${c.space["1"]}; flex-shrink: 0; }
21
46
  & .crisp-board-col-dot { width: 12px; height: 12px; border-radius: ${c.radius.full}; flex-shrink: 0; }
22
47
  & .crisp-board-col-title { font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
23
- & .crisp-board-col-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
48
+ /* crisp#697: HANDOFF's #472 note claimed the count pill's
49
+ color-mix(fgPrimary, 63%, transparent) reproduced the measured
50
+ rgb(80,81,85) -- it doesn't; 63% of fgPrimary composited on white
51
+ paints rgb(104,105,105), visibly lighter. Text now reads
52
+ fgNeutralSoft directly (the same rgb(80,81,85) token Badge's neutral
53
+ tone uses, #696), and the ring the pill was also missing
54
+ (inset 0 0 0 1px borderSubtle, measured rgb(238,239,241)) is added. */
55
+ & .crisp-board-col-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); box-shadow: inset 0 0 0 1px ${c.borderSubtle}; font-size: ${c.text.sm}; line-height: 16px; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft}; }
24
56
  & .crisp-board-col-add { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin-inline-start: auto; padding: 0; border: none; border-radius: ${c.radius.sm}; background: none; color: ${c.fgMuted}; font-family: inherit; cursor: pointer; }
25
57
  & .crisp-board-col-add:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
26
58
  & .crisp-board-col-body { display: flex; flex-direction: column; gap: ${c.space["2"]}; flex: 1; min-height: 48px; padding: ${c.space["1"]}; border-radius: ${c.radius.lg}; overflow-y: auto; transition: background 0.12s; }
27
59
  & .crisp-board-col[data-over] .crisp-board-col-body { background: color-mix(in srgb, ${c.bgBrandSolid} 7%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgBrandSolid} 30%, transparent); }
28
60
  & .crisp-board-newcard { display: flex; align-items: center; justify-content: center; gap: ${c.space["1.5"]}; height: 32px; flex-shrink: 0; border: none; border-radius: ${c.radius.md}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgMuted}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; cursor: pointer; }
29
61
  & .crisp-board-newcard:hover { background: color-mix(in srgb, ${c.fgPrimary} 7%, transparent); color: ${c.fgPrimary}; }
30
- & .crisp-board-card { display: flex; flex-direction: column; gap: ${c.space["2"]}; box-sizing: border-box; padding: ${c.space["2.5"]} ${c.space["3"]}; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; cursor: grab; }
62
+ & .crisp-board-card { display: flex; flex-direction: column; gap: ${c.space["2"]}; box-sizing: border-box; padding: ${c.space["2.5"]} ${c.space["3"]}; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; cursor: grab; touch-action: none; }
63
+ /* #718: a collapsed card is a single 40px row -- measured live and
64
+ independent of \`c.size.tableRow\`/\`tableHeader\` (36/40px) even
65
+ though it happens to match the latter; a plain literal, not a
66
+ cross-reference to a token measured for a different surface.
67
+ \`renderCard\`'s own second argument is what actually decides what a
68
+ collapsed card SHOWS (Board never sees inside its opaque output);
69
+ this only supplies the row-shaped box that content sits in. */
70
+ & .crisp-board-card[data-density="collapse"] { flex-direction: row; align-items: center; height: 40px; padding: 0 ${c.space["3"]}; }
31
71
  & .crisp-board-card:active { cursor: grabbing; }
32
72
  & .crisp-board-card[data-dragging] { opacity: 0.4; }
33
73
  & .crisp-board-card:focus { outline: none; }
34
74
  & .crisp-board-card:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 1px; }
75
+ /* #674: the pointer-events drag's own drag image -- the same card
76
+ surface above, just pinned to the pointer instead of the flex flow.
77
+ No shadow lift or scale: neither was measured against Attio, and this
78
+ is the same card, momentarily somewhere else, not a new look. */
79
+ & .crisp-board-ghost { position: fixed; z-index: 1; pointer-events: none; cursor: grabbing; }
35
80
  /* #604: a per-column calc footer, mirroring the table's own (measured
36
81
  live, Deals pipeline). Sits below the scrollable card body, not
37
82
  inside it or pinned to it -- Attio's own is position: static, not
@@ -42,6 +87,13 @@ const config = defineRecipe({
42
87
  & .crisp-board-col-calc[data-empty] { opacity: 0.7; }
43
88
  & .crisp-board-col-calc-label { text-transform: capitalize; }
44
89
  & .crisp-board-col-calc-value { color: ${c.fgPrimary}; font-variant-numeric: tabular-nums; }
90
+ /* #718: "Add stage" -- measured live: 52×52px, transparent, a 1px
91
+ dashed border (rgb(230,231,234), exactly \`borderDropzone\`/\`line150\`
92
+ -- the same dashed-border colour ImportDialog's own dropzone already
93
+ uses, not borderSubtle/line100, a close but different value), radius.xl
94
+ (16px), a centered 16px "+". */
95
+ & .crisp-board-addstage { display: inline-flex; align-items: center; justify-content: center; width: 52px; height: 52px; flex-shrink: 0; border: 1px dashed ${c.borderDropzone}; border-radius: ${c.radius.xl}; background: none; color: ${c.fgPrimary}; font-family: inherit; cursor: pointer; }
96
+ & .crisp-board-addstage:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); }
45
97
  /* announced, never drawn */
46
98
  & .crisp-board-live {
47
99
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ArrowUpRight, Check, ChevronLeft, CornerUpLeft, ListFilter, MessageSquare, MoreHorizontal, SmilePlus, X, } from "lucide-react";
2
+ import { Check, ChevronLeft, CornerUpLeft, ListFilter, MessageSquare, MoreHorizontal, PanelRight, SmilePlus, X, } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { Avatar } from "../../components/avatar/index.js";
5
5
  import { Button } from "../../components/button/index.js";
@@ -266,8 +266,8 @@ const Composer = React.forwardRef(function Composer({ placeholder, postLabel, me
266
266
  close: closeMention,
267
267
  })] }));
268
268
  });
269
- function CommentRow({ comment, replyLabel, reactLabel, resolveLabel, moreLabel, copyLinkLabel, deleteLabel, addReactionLabel, reactionSearchLabel, emojiOptions, resolvable, resolved, resolvedLabel, onReply, onReact, onResolve, onCopyLink, onDeleteRequest, }) {
270
- return (_jsxs("div", { className: "crisp-comments-row", children: [_jsx(AuthorAvatar, { author: comment.author }), _jsxs("div", { className: "crisp-comments-row-main", children: [resolvedLabel != null && (_jsxs("div", { className: "crisp-comments-resolved-label", children: [_jsx(Check, { size: 12, strokeWidth: 2.5, "aria-hidden": "true" }), resolvedLabel] })), _jsxs("div", { className: "crisp-comments-row-head", children: [_jsx("span", { className: "crisp-comments-author", children: comment.author.name }), _jsx("span", { className: "crisp-comments-time", children: comment.time })] }), _jsx("div", { className: "crisp-comments-body-text", children: comment.body }), comment.reactions && comment.reactions.length > 0 && (_jsxs("div", { className: "crisp-comments-reactions", children: [comment.reactions.map((r) => (_jsxs("button", { type: "button", className: "crisp-comments-reaction", "data-reacted": r.reactedByMe ? "" : undefined, onClick: () => onReact(r.emoji), children: [_jsx("span", { "aria-hidden": "true", children: r.emoji }), r.count] }, r.emoji))), _jsx(Popover, { align: "start", trigger: _jsx("button", { type: "button", className: "crisp-comments-reaction-add", "aria-label": addReactionLabel, children: _jsx(SmilePlus, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: _jsx(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: onReact }) })] }))] }), _jsxs("div", { className: "crisp-comments-row-actions", children: [onReply && (_jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": replyLabel, onClick: onReply, children: _jsx(CornerUpLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 2, "aria-hidden": "true" }) })), _jsx(Popover, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": reactLabel, children: _jsx(SmilePlus, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: _jsx(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: onReact }) }), resolvable && (_jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": resolveLabel, "aria-pressed": !!resolved, "data-active": resolved ? "" : undefined, onClick: onResolve, children: _jsx(Check, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })), _jsxs(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": moreLabel, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [_jsx(MenuItem, { onSelect: onCopyLink, children: copyLinkLabel }), _jsx(MenuItem, { danger: true, onSelect: onDeleteRequest, children: deleteLabel })] })] })] }));
269
+ function CommentRow({ comment, replyLabel, reactLabel, resolveLabel, moreLabel, copyLinkLabel, deleteLabel, addReactionLabel, reactionSearchLabel, emojiOptions, resolvable, resolved, resolvedLabel, reactable, onReply, onReact, onResolve, onCopyLink, onDeleteRequest, }) {
270
+ return (_jsxs("div", { className: "crisp-comments-row", children: [_jsx(AuthorAvatar, { author: comment.author }), _jsxs("div", { className: "crisp-comments-row-main", children: [resolvedLabel != null && (_jsxs("div", { className: "crisp-comments-resolved-label", children: [_jsx(Check, { size: 12, strokeWidth: 2.5, "aria-hidden": "true" }), resolvedLabel] })), _jsxs("div", { className: "crisp-comments-row-head", children: [_jsx("span", { className: "crisp-comments-author", children: comment.author.name }), _jsx("span", { className: "crisp-comments-time", children: comment.time })] }), _jsx("div", { className: "crisp-comments-body-text", children: comment.body }), comment.reactions && comment.reactions.length > 0 && (_jsxs("div", { className: "crisp-comments-reactions", children: [comment.reactions.map((r) => (_jsxs("button", { type: "button", className: "crisp-comments-reaction", "data-reacted": r.reactedByMe ? "" : undefined, onClick: () => onReact?.(r.emoji), children: [_jsx("span", { "aria-hidden": "true", children: r.emoji }), r.count] }, r.emoji))), _jsx(Popover, { align: "start", trigger: _jsx("button", { type: "button", className: "crisp-comments-reaction-add", "aria-label": addReactionLabel, children: _jsx(SmilePlus, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: _jsx(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: (emoji) => onReact?.(emoji) }) })] }))] }), _jsxs("div", { className: "crisp-comments-row-actions", children: [onReply && (_jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": replyLabel, onClick: onReply, children: _jsx(CornerUpLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 2, "aria-hidden": "true" }) })), reactable && (_jsx(Popover, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": reactLabel, children: _jsx(SmilePlus, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: _jsx(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: (emoji) => onReact?.(emoji) }) })), resolvable && (_jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": resolveLabel, "aria-pressed": !!resolved, "data-active": resolved ? "" : undefined, onClick: onResolve, children: _jsx(Check, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })), _jsxs(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": moreLabel, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [_jsx(MenuItem, { onSelect: onCopyLink, children: copyLinkLabel }), onDeleteRequest && (_jsx(MenuItem, { danger: true, onSelect: onDeleteRequest, children: deleteLabel }))] })] })] }));
271
271
  }
272
272
  /**
273
273
  * Per-record comment threads (crisp#625) — measured live on app.attio.com's
@@ -331,29 +331,42 @@ export function CommentsPanel({ threads, currentUserId, subject, subjectIcon, sh
331
331
  const activeThread = activeThreadId
332
332
  ? threads.find((t) => t.id === activeThreadId)
333
333
  : null;
334
- return (_jsxs("div", { className: commentsRecipe({ className }), role: "dialog", "aria-label": title, ...rest, children: [_jsxs("div", { className: "crisp-comments-head", children: [activeThread ? (_jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": backLabel, onClick: () => setActiveThreadId(null), children: _jsx(ChevronLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })) : null, _jsxs("div", { className: "crisp-comments-title-wrap", children: [_jsx("span", { className: "crisp-comments-title", children: activeThread ? threadTitle : title }), activeThread && (_jsxs("span", { className: "crisp-comments-breadcrumb", children: [breadcrumbPrefix, " ", subjectIcon, " ", subject] }))] }), !activeThread && (_jsxs(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": filterLabel, children: _jsx(ListFilter, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [_jsx(MenuCheckboxItem, { checked: showResolved, onCheckedChange: onShowResolvedChange, children: showResolvedLabel }), _jsx(MenuCheckboxItem, { checked: onlyMine, onCheckedChange: onOnlyMineChange, children: onlyMineLabel })] })), _jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), _jsx("div", { className: "crisp-comments-body crisp-comments-body--pad-top", children: activeThread ? (_jsxs("div", { children: [_jsx(CommentRow, { comment: activeThread.root, currentUserId: currentUserId, replyLabel: replyActionLabel, reactLabel: reactActionLabel, resolveLabel: resolveActionLabel, moreLabel: moreActionsLabel, copyLinkLabel: copyLinkLabel, deleteLabel: deleteLabel, addReactionLabel: addReactionLabel, reactionSearchLabel: reactionSearchLabel, emojiOptions: emojiOptions, resolvable: !!onResolve, resolved: activeThread.resolved, resolvedLabel: activeThread.resolved
334
+ return (_jsxs("div", { className: commentsRecipe({ className }), role: "dialog", "aria-label": title, ...rest, children: [_jsxs("div", { className: "crisp-comments-head", children: [activeThread ? (_jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": backLabel, onClick: () => setActiveThreadId(null), children: _jsx(ChevronLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })) : null, _jsxs("div", { className: "crisp-comments-title-wrap", children: [_jsx("span", { className: "crisp-comments-title", children: activeThread ? threadTitle : title }), activeThread && (_jsxs("span", { className: "crisp-comments-breadcrumb", children: [breadcrumbPrefix, " ", subjectIcon, " ", subject] }))] }), !activeThread && (_jsxs(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": filterLabel, children: _jsx(ListFilter, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [_jsx(MenuCheckboxItem, { checked: showResolved, onCheckedChange: onShowResolvedChange, children: showResolvedLabel }), _jsx(MenuCheckboxItem, { checked: onlyMine, onCheckedChange: onOnlyMineChange, children: onlyMineLabel })] })), onClose && (_jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }))] }), _jsx("div", { className: "crisp-comments-body crisp-comments-body--pad-top", children: activeThread ? (_jsxs("div", { children: [_jsx(CommentRow, { comment: activeThread.root, currentUserId: currentUserId, replyLabel: replyActionLabel, reactLabel: reactActionLabel, resolveLabel: resolveActionLabel, moreLabel: moreActionsLabel, copyLinkLabel: copyLinkLabel, deleteLabel: deleteLabel, addReactionLabel: addReactionLabel, reactionSearchLabel: reactionSearchLabel, emojiOptions: emojiOptions, resolvable: !!onResolve, resolved: activeThread.resolved, resolvedLabel: activeThread.resolved
335
335
  ? activeThread.resolvedBy?.id === currentUserId ||
336
336
  !activeThread.resolvedBy
337
337
  ? resolvedByYouLabel
338
338
  : (resolvedByLabel?.(activeThread.resolvedBy.name) ??
339
339
  `Resolved by ${activeThread.resolvedBy.name}`)
340
- : undefined, onReact: (emoji) => onReact(activeThread.id, activeThread.root.id, emoji), onResolve: onResolve ? () => onResolve(activeThread.id) : undefined, onCopyLink: () => copyLink(activeThread.id, activeThread.root.id), onDeleteRequest: () => setDeleteTarget({
341
- threadId: activeThread.id,
342
- commentId: activeThread.root.id,
343
- }) }), activeThread.replies && activeThread.replies.length > 0 && (_jsx("div", { className: "crisp-comments-replies", children: activeThread.replies.map((reply) => (_jsx(CommentRow, { comment: reply, currentUserId: currentUserId, replyLabel: replyActionLabel, reactLabel: reactActionLabel, resolveLabel: resolveActionLabel, moreLabel: moreActionsLabel, copyLinkLabel: copyLinkLabel, deleteLabel: deleteLabel, addReactionLabel: addReactionLabel, reactionSearchLabel: reactionSearchLabel, emojiOptions: emojiOptions, onReact: (emoji) => onReact(activeThread.id, reply.id, emoji), onCopyLink: () => copyLink(activeThread.id, reply.id), onDeleteRequest: () => setDeleteTarget({
340
+ : undefined, reactable: !!onReact, onReact: onReact
341
+ ? (emoji) => onReact(activeThread.id, activeThread.root.id, emoji)
342
+ : undefined, onResolve: onResolve ? () => onResolve(activeThread.id) : undefined, onCopyLink: () => copyLink(activeThread.id, activeThread.root.id), onDeleteRequest: onDelete
343
+ ? () => setDeleteTarget({
344
344
  threadId: activeThread.id,
345
- commentId: reply.id,
346
- }) }, reply.id))) }))] })) : visibleThreads.length === 0 && emptyLabel != null ? (_jsx("div", { className: "crisp-comments-empty", children: emptyLabel })) : (visibleThreads.map((t) => (_jsx(CommentRow, { comment: t.root, currentUserId: currentUserId, replyLabel: replyActionLabel, reactLabel: reactActionLabel, resolveLabel: resolveActionLabel, moreLabel: moreActionsLabel, copyLinkLabel: copyLinkLabel, deleteLabel: deleteLabel, addReactionLabel: addReactionLabel, reactionSearchLabel: reactionSearchLabel, emojiOptions: emojiOptions, resolvable: !!onResolve, resolved: t.resolved, resolvedLabel: t.resolved
345
+ commentId: activeThread.root.id,
346
+ })
347
+ : undefined }), activeThread.replies && activeThread.replies.length > 0 && (_jsx("div", { className: "crisp-comments-replies", children: activeThread.replies.map((reply) => (_jsx(CommentRow, { comment: reply, currentUserId: currentUserId, replyLabel: replyActionLabel, reactLabel: reactActionLabel, resolveLabel: resolveActionLabel, moreLabel: moreActionsLabel, copyLinkLabel: copyLinkLabel, deleteLabel: deleteLabel, addReactionLabel: addReactionLabel, reactionSearchLabel: reactionSearchLabel, emojiOptions: emojiOptions, reactable: !!onReact, onReact: onReact
348
+ ? (emoji) => onReact(activeThread.id, reply.id, emoji)
349
+ : undefined, onCopyLink: () => copyLink(activeThread.id, reply.id), onDeleteRequest: onDelete
350
+ ? () => setDeleteTarget({
351
+ threadId: activeThread.id,
352
+ commentId: reply.id,
353
+ })
354
+ : undefined }, reply.id))) }))] })) : visibleThreads.length === 0 && emptyLabel != null ? (_jsx("div", { className: "crisp-comments-empty", children: emptyLabel })) : (visibleThreads.map((t) => (_jsx(CommentRow, { comment: t.root, currentUserId: currentUserId, replyLabel: replyActionLabel, reactLabel: reactActionLabel, resolveLabel: resolveActionLabel, moreLabel: moreActionsLabel, copyLinkLabel: copyLinkLabel, deleteLabel: deleteLabel, addReactionLabel: addReactionLabel, reactionSearchLabel: reactionSearchLabel, emojiOptions: emojiOptions, resolvable: !!onResolve, resolved: t.resolved, resolvedLabel: t.resolved
347
355
  ? t.resolvedBy?.id === currentUserId || !t.resolvedBy
348
356
  ? resolvedByYouLabel
349
357
  : (resolvedByLabel?.(t.resolvedBy.name) ??
350
358
  `Resolved by ${t.resolvedBy.name}`)
351
- : 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)))) }), _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"), _jsxs(Dialog, { open: deleteTarget != null, onOpenChange: (open) => {
359
+ : undefined, onReply: () => setActiveThreadId(t.id), reactable: !!onReact, onReact: onReact ? (emoji) => onReact(t.id, t.root.id, emoji) : undefined, onResolve: onResolve ? () => onResolve(t.id) : undefined, onCopyLink: () => copyLink(t.id, t.root.id), onDeleteRequest: onDelete
360
+ ? () => setDeleteTarget({ threadId: t.id, commentId: t.root.id })
361
+ : undefined }, t.id)))) }), _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"), _jsxs(Dialog, { open: deleteTarget != null, onOpenChange: (open) => {
352
362
  if (!open)
353
363
  setDeleteTarget(null);
354
364
  }, title: deleteConfirmTitle, children: [_jsx(DialogBody, { children: deleteConfirmBody }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", intent: "neutral", size: "md", onClick: () => setDeleteTarget(null), children: cancelLabel }), _jsx(Button, { type: "button", intent: "danger", size: "md", onClick: () => {
365
+ // `deleteTarget` can only ever be set via `onDeleteRequest`
366
+ // (above), which itself only exists when `onDelete` was
367
+ // given -- this call is never reached otherwise.
355
368
  if (deleteTarget)
356
- onDelete(deleteTarget.threadId, deleteTarget.commentId);
369
+ onDelete?.(deleteTarget.threadId, deleteTarget.commentId);
357
370
  setDeleteTarget(null);
358
371
  }, children: deleteConfirmActionLabel })] })] })] }));
359
372
  }
@@ -375,6 +388,15 @@ export function CommentCountBadge({ count, hovered, onOpen, onOpenPreview, label
375
388
  ? `crisp-comments-badge ${className}`
376
389
  : "crisp-comments-badge", "aria-hidden": "true", children: _jsx("span", { className: "crisp-comments-badge-dot" }) }));
377
390
  }
378
- return (_jsxs("span", { className: className ? `crisp-comments-badge ${className}` : "crisp-comments-badge", children: [_jsxs("button", { type: "button", className: "crisp-comments-badge", "aria-label": label, onClick: onOpen, children: [_jsx(MessageSquare, { size: 12, strokeWidth: 2, "aria-hidden": "true" }), count > 0 && (_jsx("span", { className: "crisp-comments-badge-count", children: count }))] }), onOpenPreview && (_jsx("button", { type: "button", className: "crisp-comments-badge", "aria-label": previewLabel, onClick: onOpenPreview, children: _jsx(ArrowUpRight, { "data-rtl-flip": "", size: 12, strokeWidth: 2, "aria-hidden": "true" }) }))] }));
391
+ return (_jsxs("span", { className: className ? `crisp-comments-badge ${className}` : "crisp-comments-badge", children: [_jsxs("button", { type: "button", className: "crisp-comments-badge-trigger", "aria-label": label, onClick: (ev) => {
392
+ // this badge lives in a grid row (crisp/#601 supersession) whose
393
+ // own click opens the record -- the same stopPropagation every
394
+ // other per-row affordance in that cell already uses.
395
+ ev.stopPropagation();
396
+ onOpen();
397
+ }, children: [_jsx(MessageSquare, { size: 12, strokeWidth: 2, "aria-hidden": "true" }), count > 0 && (_jsx("span", { className: "crisp-comments-badge-count", children: count }))] }), onOpenPreview && (_jsx("button", { type: "button", className: "crisp-comments-badge-preview", "aria-label": previewLabel, onClick: (ev) => {
398
+ ev.stopPropagation();
399
+ onOpenPreview();
400
+ }, children: _jsx(PanelRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }))] }));
379
401
  }
380
402
  CommentCountBadge.displayName = "CommentCountBadge";
@@ -33,12 +33,26 @@ const config = defineRecipe({
33
33
 
34
34
  /* One comment row -- avatar 16px (Avatar size="sm"), 6px gap to the
35
35
  name/time line, 14px/600 name + 12px/500 63%-muted time, 14px/400 body
36
- two lines below. Hover actions fade in flush with the name line. */
36
+ two lines below. Hover actions fade in flush with the name line.
37
+ .crisp-comments-row-head's own height: 20px (above) is a single
38
+ text-line's worth, on the same assumption .crisp-comments-title/
39
+ .crisp-comments-breadcrumb already act on higher up in this same
40
+ recipe -- a name that doesn't fit wraps to two lines with nothing
41
+ reserving room for the second one, overflowing straight into the
42
+ body text underneath (found live: this app's own dev-bypass author
43
+ name, an email long enough to wrap, sat on top of the comment's own
44
+ body text). .crisp-comments-author gets the identical nowrap-
45
+ ellipsis treatment those two already have, plus min-width: 0 so a
46
+ flex item is actually allowed to shrink below its own text's natural
47
+ width in the first place; .crisp-comments-time gets flex-shrink: 0
48
+ so it's the name that gives, never the timestamp -- the same "one
49
+ side shrinks, the other one doesn't" split PersonChip's own name/
50
+ avatar pair already uses (crisp#668/#671). */
37
51
  & .crisp-comments-row { position: relative; display: flex; gap: ${c.space["1.5"]}; }
38
52
  & .crisp-comments-row-main { flex: 1; min-width: 0; }
39
53
  & .crisp-comments-row-head { display: flex; align-items: baseline; gap: ${c.space["1.5"]}; height: 20px; }
40
- & .crisp-comments-author { font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
41
- & .crisp-comments-time { font-size: 12px; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); white-space: nowrap; }
54
+ & .crisp-comments-author { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
55
+ & .crisp-comments-time { flex-shrink: 0; font-size: 12px; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); white-space: nowrap; }
42
56
  & .crisp-comments-resolved-label { display: flex; align-items: center; gap: ${c.space["1"]}; margin-bottom: ${c.space["1"]}; font-size: 12px; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
43
57
  & .crisp-comments-body-text { font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.regular}; color: ${c.fgPrimary}; white-space: pre-wrap; word-break: break-word; }
44
58
 
@@ -126,11 +140,21 @@ const config = defineRecipe({
126
140
 
127
141
  /* Row badge (CommentCountBadge) -- lives in its own class family so the
128
142
  grid lanes can drop it into a cell without pulling in the panel's
129
- CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
130
- icon + 12px count text, both at 55% ink, replacing the dot. */
131
- .crisp-comments-badge { box-sizing: border-box; font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; display: inline-flex; align-items: center; gap: ${c.space["1"]}; border: none; background: none; padding: 0; margin: 0; cursor: pointer; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); }
143
+ CSS. Re-measured live on a real commented row (crisp/#601
144
+ supersession, right-aligned in a DataTable primary column): a 6px
145
+ grey dot at rest (unchanged, already matched); on hover, the trigger
146
+ button is auto-width with a 12px icon + 12px/500 count text,
147
+ "padding: 2px 6px 2px 4px", radius 6px -- distinct from the preview
148
+ button beside it, a fixed 20px square with a 16px icon, same radius.
149
+ Both share the 55%-ink idle colour and darken on their OWN hover
150
+ (color-mix 4%, the same formula DataTable's own #601 icons use). */
151
+ .crisp-comments-badge { box-sizing: border-box; font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; }
132
152
  .crisp-comments-badge-dot { box-sizing: border-box; width: 6px; height: 6px; border-radius: ${c.radius.full}; background: color-mix(in srgb, ${c.fgPrimary} 38%, transparent); }
133
- .crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
153
+ .crisp-comments-badge-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: 20px; margin: 0; padding: 2px ${c.space["1.5"]} 2px ${c.space["1"]}; border: none; border-radius: ${c.radius.sm}; background: none; font-family: inherit; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); cursor: pointer; }
154
+ .crisp-comments-badge-trigger:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgPrimary}; }
155
+ .crisp-comments-badge-preview { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.sm}; background: none; font-family: inherit; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); cursor: pointer; flex-shrink: 0; }
156
+ .crisp-comments-badge-preview:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgPrimary}; }
157
+ .crisp-comments-badge-count { font-size: 12px; line-height: 16px; font-weight: ${c.weight.medium}; }
134
158
  [dir="rtl"] .crisp-comments-badge [data-rtl-flip] { transform: scaleX(-1); }
135
159
  `,
136
160
  });
@@ -32,13 +32,19 @@ export function useFormDialogOptional(required) {
32
32
  * per-field wiring). A record needs several fields at once to be valid,
33
33
  * which is why records are not created by inline editing.
34
34
  */
35
- export function FormDialog({ title, submitLabel, onSubmit, onClose, busy, error, children, wide = false, size, createMode = false, createMoreLabel: createMoreLabelProp, cancelLabel: cancelLabelProp, discardTitle: discardTitleProp, discardConfirmLabel: discardConfirmLabelProp, discardCancelLabel: discardCancelLabelProp, className, ...rest }) {
35
+ export function FormDialog({ title, submitLabel, onSubmit, onClose, busy, error, children, wide = false, size, createMode = false, createMoreLabel: createMoreLabelProp, cancelLabel: cancelLabelProp, discardTitle: discardTitleProp, discardDescription: discardDescriptionProp, discardConfirmLabel: discardConfirmLabelProp, discardCancelLabel: discardCancelLabelProp, className, ...rest }) {
36
36
  const createMoreLabel = useCrispMessage(createMoreLabelProp, "Create more");
37
37
  const createRecordLabel = useCrispMessage(undefined, "Create record");
38
38
  const cancelLabel = useCrispMessage(cancelLabelProp, "Cancel");
39
- const discardTitle = useCrispMessage(discardTitleProp, "Discard changes? Your changes will be lost.");
39
+ // crisp#677: Attio's own confirm is two lines (a title, then a separate
40
+ // sentence), not one combined string — measured live, both quoted
41
+ // verbatim off app.attio.com's create-attribute dialog's own discard
42
+ // confirm. The buttons read "Cancel"/"Discard" there too, not crisp's
43
+ // prior "Keep editing" (a paraphrase, never independently measured).
44
+ const discardTitle = useCrispMessage(discardTitleProp, "Discard changes");
45
+ const discardDescription = useCrispMessage(discardDescriptionProp, "Are you sure you want to discard these changes?");
40
46
  const discardConfirmLabel = useCrispMessage(discardConfirmLabelProp, "Discard");
41
- const discardCancelLabel = useCrispMessage(discardCancelLabelProp, "Keep editing");
47
+ const discardCancelLabel = useCrispMessage(discardCancelLabelProp, "Cancel");
42
48
  const formId = React.useId();
43
49
  const formRef = React.useRef(null);
44
50
  const switchId = React.useId();
@@ -126,7 +132,7 @@ export function FormDialog({ title, submitLabel, onSubmit, onClose, busy, error,
126
132
  return (_jsxs(_Fragment, { children: [_jsxs(Dialog, { open: true, onOpenChange: (open) => !open && requestClose(), title: title, width: size ?? (wide ? 760 : 640), className: formdialogRecipe({ className }), ...rest, children: [_jsxs(DialogBody, { children: [_jsx(OptionalLabelsContext.Provider, { value: createMode, children: _jsx("form", { ref: formRef, id: formId, className: "crisp-formdialog-form", onSubmit: (e) => {
127
133
  e.preventDefault();
128
134
  onSubmit(createMore);
129
- }, onChange: markDirty, onInput: markDirty, onClick: markDirty, children: children }) }), error != null && _jsx("p", { className: "crisp-formdialog-error", children: error })] }), _jsx(DialogFooter, { children: createMode ? (_jsxs("div", { className: "crisp-formdialog-createfoot", children: [_jsxs("span", { className: "crisp-dialog-more", children: [_jsx(Switch, { id: switchId, checked: createMore, onCheckedChange: setCreateMore, "aria-label": createMoreLabel }), _jsx("label", { className: "crisp-formdialog-more-label", htmlFor: switchId, children: createMoreLabel })] }), _jsxs("button", { type: "submit", form: formId, className: "crisp-dialog-submit", disabled: busy, children: [createRecordLabel, _jsxs("span", { className: "crisp-dialog-submit-key", "aria-hidden": "true", children: [_jsx("kbd", { children: "\u2318" }), _jsx("kbd", { children: "\u21B5" })] })] })] })) : (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", intent: "neutral", onClick: requestClose, children: cancelLabel }), _jsx(Button, { type: "submit", form: formId, intent: "primary", loading: busy, children: submitLabel })] })) })] }), _jsx(Dialog, { open: confirmingDiscard, onOpenChange: (open) => !open && setConfirmingDiscard(false), title: discardTitle, width: 400, children: _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", intent: "neutral", onClick: () => setConfirmingDiscard(false), children: discardCancelLabel }), _jsx(Button, { type: "button", intent: "danger", onClick: () => {
135
+ }, onChange: markDirty, onInput: markDirty, onClick: markDirty, children: children }) }), error != null && _jsx("p", { className: "crisp-formdialog-error", children: error })] }), _jsx(DialogFooter, { children: createMode ? (_jsxs("div", { className: "crisp-formdialog-createfoot", children: [_jsxs("span", { className: "crisp-dialog-more", children: [_jsx(Switch, { id: switchId, checked: createMore, onCheckedChange: setCreateMore, "aria-label": createMoreLabel }), _jsx("label", { className: "crisp-formdialog-more-label", htmlFor: switchId, children: createMoreLabel })] }), _jsxs("button", { type: "submit", form: formId, className: "crisp-dialog-submit", disabled: busy, children: [createRecordLabel, _jsxs("span", { className: "crisp-dialog-submit-key", "aria-hidden": "true", children: [_jsx("kbd", { children: "\u2318" }), _jsx("kbd", { children: "\u21B5" })] })] })] })) : (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", intent: "neutral", onClick: requestClose, children: cancelLabel }), _jsx(Button, { type: "submit", form: formId, intent: "primary", loading: busy, children: submitLabel })] })) })] }), _jsx(Dialog, { open: confirmingDiscard, onOpenChange: (open) => !open && setConfirmingDiscard(false), variant: "alert", title: discardTitle, description: discardDescription, width: 440, children: _jsxs(DialogFooter, { variant: "alert", children: [_jsx(Button, { type: "button", intent: "neutral", onClick: () => setConfirmingDiscard(false), children: discardCancelLabel }), _jsx(Button, { type: "button", intent: "danger", onClick: () => {
130
136
  setConfirmingDiscard(false);
131
137
  onClose();
132
138
  }, children: discardConfirmLabel })] }) })] }));
@@ -1 +1 @@
1
- export { NavItem } from "./navitem.js";
1
+ export { NavItem, } from "./navitem.js";
@@ -1,8 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
+ import { Kbd } from "../../components/kbd/index.js";
4
+ import { Tooltip } from "../../components/tooltip/index.js";
3
5
  import { Primitive } from "../../core/index.js";
4
6
  import { navitemRecipe } from "./navitem.recipe.js";
5
- export const NavItem = React.forwardRef(({ active, icon, trailing, className, children, ...props }, ref) => {
6
- return (_jsxs(Primitive.a, { ref: ref, "data-active": active || undefined, className: navitemRecipe({ className }), ...props, children: [icon, _jsx("span", { className: "crisp-navitem-label", children: children }), trailing != null && (_jsx("span", { className: "crisp-navitem-trailing", children: trailing }))] }));
7
+ export const NavItem = React.forwardRef(({ active, icon, trailing, shortcut, className, children, ...props }, ref) => {
8
+ const row = (_jsxs(Primitive.a, { ref: ref, "data-active": active || undefined, className: navitemRecipe({ className }), ...props, children: [icon, _jsx("span", { className: "crisp-navitem-label", children: children }), trailing != null && (_jsx("span", { className: "crisp-navitem-trailing", children: trailing }))] }));
9
+ if (!shortcut)
10
+ return row;
11
+ return (_jsx(Tooltip, { asChild: true, side: "right", delay: 500, content: _jsxs("span", { className: "crisp-navitem-shortcut", children: [_jsx("span", { className: "crisp-navitem-shortcut-desc", children: shortcut.description }), _jsx("span", { className: "crisp-navitem-shortcut-chord", children: shortcut.keys.map((key, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "crisp-navitem-shortcut-then", children: "then" })), _jsx(Kbd, { children: key })] }, i))) })] }), children: row }));
7
12
  });
8
13
  NavItem.displayName = "NavItem";
@@ -18,6 +18,22 @@ const config = defineRecipe({
18
18
  variants: {},
19
19
  defaultVariants: {},
20
20
  compoundVariants: [],
21
+ // Tooltip portals its content to document.body (same reason sidebar.recipe.ts's
22
+ // menu content lives in globalCss, not a `&`-nested rule): a shortcut hint
23
+ // never sits inside `.crisp-navitem` in the live DOM, so it needs an
24
+ // unscoped rule to be reached at all. Values measured live on
25
+ // app.attio.com's own "G then <letter>" hint (crisp#633): the description
26
+ // inherits Tooltip's own #eeeff1 text colour for free (it's the same hex),
27
+ // but "then" and the Kbd chip are their own greys, hardcoded raw rather
28
+ // than through a token -- same call tooltip.recipe.ts already made for
29
+ // this exact bubble, which is always dark regardless of the app's theme.
30
+ globalCss: `
31
+ .crisp-navitem-shortcut { display: flex; align-items: baseline; justify-content: space-between; gap: ${c.space["3"]}; }
32
+ .crisp-navitem-shortcut-desc { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; line-height: 16px; white-space: nowrap; }
33
+ .crisp-navitem-shortcut-chord { display: inline-flex; align-items: center; gap: ${c.space["1"]}; flex-shrink: 0; }
34
+ .crisp-navitem-shortcut-then { font-size: ${c.text.xs}; font-weight: ${c.weight.medium}; color: #898a8d; }
35
+ .crisp-navitem-shortcut-chord .crisp-kbd { text-transform: uppercase; font-weight: ${c.weight.regular}; color: #a2a4a7; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2); }
36
+ `,
21
37
  });
22
38
  export const navitemRecipe = Object.assign(resolveRecipe(config), {
23
39
  config,
@@ -1,2 +1,2 @@
1
- export { ATTRIBUTE_AI_AUTOFILL_OPTIONS, ATTRIBUTE_TYPE_OPTIONS, AttributesTable, AttributeTypeSelect, CreateAttributeDialog, ObjectAppearanceSection, ObjectDetailPage, ObjectGeneralSection, ObjectSettings, ObjectsList, } from "./objectsettings.js";
1
+ export { ATTRIBUTE_AI_AUTOFILL_OPTIONS, ATTRIBUTE_TYPE_OPTIONS, AttributesTable, AttributeTypeSelect, CreateAttributeDialog, ObjectAppearanceSection, ObjectDetailPage, ObjectGeneralSection, ObjectsList, } from "./objectsettings.js";
2
2
  export { objectsettingsRecipe } from "./objectsettings.recipe.js";
@@ -114,7 +114,7 @@ export function ObjectDetailPage({ icon, iconColor, name, typeLabel, description
114
114
  setInternalTab(next);
115
115
  onTabChange?.(next);
116
116
  };
117
- return (_jsx("div", { className: objectsettingsRecipe(), children: _jsxs("div", { className: "crisp-objectsettings-page", children: [_jsx("div", { className: "crisp-objectsettings-head", children: _jsxs("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 }, children: [_jsx("span", { className: "crisp-objectsettings-icon crisp-objectsettings-icon--lg", style: { background: iconColor ?? "currentColor" }, children: icon }), _jsxs("div", { className: "crisp-objectsettings-heading", children: [_jsxs("h1", { className: "crisp-objectsettings-title", children: [name, typeLabel != null && (_jsx(Badge, { tone: "neutral", size: "small", children: typeLabel }))] }), description != null && (_jsx("p", { className: "crisp-objectsettings-subtitle", children: description }))] })] }) }), _jsx(Tabs, { value: active, onValueChange: setTab, tabs: [
117
+ return (_jsx("div", { className: objectsettingsRecipe(), children: _jsxs("div", { className: "crisp-objectsettings-page", children: [_jsx("div", { className: "crisp-objectsettings-head", children: _jsxs("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 }, children: [_jsx("span", { className: "crisp-objectsettings-icon crisp-objectsettings-icon--lg", style: { background: iconColor ?? "currentColor" }, children: icon }), _jsxs("div", { className: "crisp-objectsettings-heading", children: [_jsxs("h1", { className: "crisp-objectsettings-title", children: [name, typeLabel != null && (_jsx(Badge, { tone: "neutral", size: "medium", children: typeLabel }))] }), description != null && (_jsx("p", { className: "crisp-objectsettings-subtitle", children: description }))] })] }) }), _jsx(Tabs, { value: active, onValueChange: setTab, tabs: [
118
118
  { value: "configuration", label: configurationLabel },
119
119
  {
120
120
  value: "attributes",
@@ -202,7 +202,7 @@ export function AttributesTable({ attributes, onOpenAttribute, onEditAttribute,
202
202
  {
203
203
  key: "properties",
204
204
  header: propertiesColumnLabel,
205
- render: (_v, row) => (_jsxs("span", { style: { display: "flex", gap: 6 }, children: [row.system && (_jsx(Badge, { tone: "neutral", size: "small", children: systemLabel })), row.enriched && (_jsx(Badge, { tone: "purple", size: "small", children: enrichedLabel }))] })),
205
+ render: (_v, row) => (_jsxs("span", { style: { display: "flex", gap: 6 }, children: [row.system && (_jsx(Badge, { tone: "neutral", size: "medium", children: systemLabel })), row.enriched && (_jsx(Badge, { tone: "purple", size: "medium", children: enrichedLabel }))] })),
206
206
  },
207
207
  {
208
208
  key: "actions",
@@ -310,13 +310,3 @@ export function CreateAttributeDialog({ type, onTypeChange, typeOptions, aiAutof
310
310
  return (_jsxs(FormDialog, { title: title, submitLabel: _jsxs(_Fragment, { children: [createLabel, _jsxs("span", { className: "crisp-objectsettings-kbd", "aria-hidden": "true", children: [_jsx("kbd", { children: "\u2318" }), _jsx("kbd", { children: "\u21B5" })] })] }), onSubmit: onSubmit, onClose: onClose, busy: busy, error: error, size: 520, className: objectsettingsRecipe(), children: [_jsx(FormField, { label: attributeTypeLabel, children: _jsx(AttributeTypeSelect, { value: type, onValueChange: onTypeChange, options: typeOptions, aiAutofillOptions: aiAutofillTypeOptions }) }), _jsx(FormField, { label: nameLabel, htmlFor: nameId, children: _jsx(Input, { id: nameId, autoFocus: true, value: name, onChange: (e) => onNameChange(e.target.value) }) }), _jsx(FormField, { label: descriptionLabel, optional: true, children: _jsx(Textarea, { placeholder: descriptionPlaceholder, value: description, onChange: (e) => onDescriptionChange(e.target.value) }) }), _jsxs("div", { className: "crisp-objectsettings-aiautofill", children: [_jsx("span", { className: "crisp-objectsettings-aiautofill-label", children: aiAutofillLabel }), _jsxs("label", { className: "crisp-objectsettings-aiautofill-row", htmlFor: switchId, children: [_jsx(Sparkles, { size: 14 }), _jsx("span", { children: setUpAiAutofillLabel }), _jsx(Switch, { id: switchId, checked: aiAutofillEnabled, onCheckedChange: onAiAutofillChange })] })] })] }));
311
311
  }
312
312
  CreateAttributeDialog.displayName = "CreateAttributeDialog";
313
- /**
314
- * This folder holds several components (`ObjectsList`, `ObjectDetailPage`,
315
- * `AttributesTable`, `CreateAttributeDialog`, …), not one named after the
316
- * slug — unlike every other product dir, whose docs `<ManualInstallation>`
317
- * resolves an import binding from the folder slug ("avatargroup" →
318
- * `AvatarGroup`) by reading this file's exports. This alias exists only so
319
- * that resolver finds a match for "objectsettings"; consumers should import
320
- * the real names above, never this one.
321
- */
322
- export const ObjectSettings = ObjectsList;
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ArrowDown, ArrowDownWideNarrow, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown, ArrowUpNarrowWide, ArrowUpRight, AtSign, Briefcase, Building2, Calendar, Check, CircleHelp, CircleUserRound, Copy, CopyPlus, CornerDownLeft, Download, EyeOff, Globe, GripVertical, Hash, Inbox, ListFilter, ListPlus, Mail, MapPin, MoreHorizontal, Pencil, Plug, Plus, Star, Tag, Trash2, TrendingUp, Upload, Users, Workflow, X, } from "lucide-react";
2
+ import { ArrowDown, ArrowDownWideNarrow, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown, ArrowUpNarrowWide, ArrowUpRight, AtSign, Briefcase, Building2, Calendar, Check, CircleHelp, CircleUserRound, Copy, CopyPlus, CornerDownLeft, EyeOff, Globe, GripVertical, Hash, Inbox, ListFilter, ListPlus, Mail, MapPin, MoreHorizontal, Pencil, Plug, Plus, Star, Tag, Trash2, TrendingUp, Upload, Users, Workflow, X, } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { Avatar } from "../../components/avatar/index.js";
@@ -154,7 +154,6 @@ export const recordsTableLabels = {
154
154
  selectionAnnouncement: (count) => count === 0 ? "Selection cleared" : `${count} selected`,
155
155
  delete: "Delete",
156
156
  sendEmail: "Send email",
157
- exportCsv: "Export CSV",
158
157
  runWorkflow: "Run workflow",
159
158
  calculate: "Calculate",
160
159
  emptyPlaceholder: "Empty",
@@ -1003,21 +1002,6 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
1003
1002
  setData((prev) => prev.filter((r) => !selected.has(r.id)));
1004
1003
  setSelected(new Set());
1005
1004
  };
1006
- const exportSelected = () => {
1007
- const picked = data.filter((r) => selected.has(r.id));
1008
- const esc = (s) => `"${s.replace(/"/g, '""')}"`;
1009
- const csv = [
1010
- cols.map((c) => esc(c.label)).join(","),
1011
- ...picked.map((r) => cols.map((c) => esc(cellText(c, r))).join(",")),
1012
- ].join("\n");
1013
- const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
1014
- const a = document.createElement("a");
1015
- a.href = url;
1016
- a.download = "records.csv";
1017
- a.click();
1018
- URL.revokeObjectURL(url);
1019
- setOpenMenu(null);
1020
- };
1021
1005
  const entityCol = cols.find((c) => c.type === "entity");
1022
1006
  // "Add to list" → pin the selected rows (shows a ★ on the entity)
1023
1007
  const addToList = () => {
@@ -1263,7 +1247,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
1263
1247
  // filtered-to-zero ring -- two different Attio surfaces.
1264
1248
  variant: !activeFilters && emptyStateExtra ? "records" : "page", icon: activeFilters ? (_jsx(ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : (_jsx(Inbox, { size: !activeFilters && emptyStateExtra ? 40 : 18, strokeWidth: 1.25, "aria-hidden": "true" })), title: activeFilters ? "No records match that filter" : t.noRecords, description: activeFilters
1265
1249
  ? "None of your records match the filters that are set."
1266
- : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? (_jsxs(Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [_jsx(ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : (_jsxs(_Fragment, { children: [_jsxs(Button, { intent: "primary", size: "sm", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] }), emptyStateExtra?.onImportCsv && (_jsxs(Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onImportCsv, children: [_jsx(Upload, { size: 15, strokeWidth: 1.75 }), t.importCsv] })), emptyStateExtra?.onConnectDataSource && (_jsxs(Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onConnectDataSource, children: [_jsx(Plug, { size: 15, strokeWidth: 1.75 }), t.connectDataSource] }))] })), learnMore: !activeFilters ? emptyStateExtra?.learnMore : undefined }) })) : (_jsxs("button", { type: "button", className: "crisp-recordstable-addrow", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), _jsx("div", { className: "crisp-recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && (_jsxs(BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [_jsx(BulkBarButton, { icon: _jsx(ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), _jsx(BulkBarButton, { icon: _jsx(Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), _jsx(BulkBarButton, { icon: _jsx(Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), _jsxs(Menu, { align: "end", "aria-label": t.moreActions, trigger: _jsx(BulkBarButton, { icon: _jsx(MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [_jsx(MenuItem, { icon: _jsx(Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), _jsx(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
1250
+ : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? (_jsxs(Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [_jsx(ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : (_jsxs(_Fragment, { children: [_jsxs(Button, { intent: "primary", size: "sm", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] }), emptyStateExtra?.onImportCsv && (_jsxs(Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onImportCsv, children: [_jsx(Upload, { size: 15, strokeWidth: 1.75 }), t.importCsv] })), emptyStateExtra?.onConnectDataSource && (_jsxs(Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onConnectDataSource, children: [_jsx(Plug, { size: 15, strokeWidth: 1.75 }), t.connectDataSource] }))] })), learnMore: !activeFilters ? emptyStateExtra?.learnMore : undefined }) })) : (_jsxs("button", { type: "button", className: "crisp-recordstable-addrow", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), _jsx("div", { className: "crisp-recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && (_jsxs(BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [_jsx(BulkBarButton, { icon: _jsx(ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), _jsx(BulkBarButton, { icon: _jsx(Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), _jsx(BulkBarButton, { icon: _jsx(Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), _jsx(Menu, { align: "end", "aria-label": t.moreActions, trigger: _jsx(BulkBarButton, { icon: _jsx(MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete }) })] })), _jsx(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
1267
1251
  createPortal(_jsxs("div", { ref: ctxRef, className: "crisp-recordstable-menu crisp-recordstable-ctx", role: "menu", "aria-label": t.recordActions, style: { position: "fixed", top: ctxMenu.y, left: ctxMenu.x }, onKeyDown: onCtxKeyDown, children: [_jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => copyRow(ctxMenu.id), children: [_jsx(Copy, { size: 15, strokeWidth: 1.75 }), t.copy] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => duplicateRow(ctxMenu.id), children: [_jsx(CopyPlus, { size: 15, strokeWidth: 1.75 }), t.duplicate] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => pinRow(ctxMenu.id), children: [_jsx(Star, { size: 15, strokeWidth: 1.75 }), t.addToList] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item crisp-recordstable-menu-item--danger", role: "menuitem", tabIndex: -1, onClick: () => deleteRow(ctxMenu.id), children: [_jsx(Trash2, { size: 15, strokeWidth: 1.75 }), t.delete] })] }), rootRef.current ?? document.body)] }));
1268
1252
  }
1269
1253
  export const defaultColumns = [
@@ -70,6 +70,71 @@ function matchSlashTrigger(textBeforeCaret, trigger = "/") {
70
70
  return { query, start };
71
71
  }
72
72
  const exec = (cmd, value) => document.execCommand(cmd, false, value);
73
+ /**
74
+ * #685: `execCommand("insertUnorderedList"/"insertOrderedList")` is built
75
+ * through DOM APIs, not the HTML parser, so it can leave the live DOM in a
76
+ * shape the parser itself would never produce: a `<ul>`/`<ol>` as a literal
77
+ * child of the `<p>` it converted, e.g. `<p><ul><li>text</li></ul></p>`.
78
+ * That's invalid content for a `<p>` — reading `innerHTML` back and later
79
+ * re-parsing it (a fresh mount, GRC reloading a stored note) auto-closes the
80
+ * `<p>` the instant the parser meets the `<ul>`, which leaves a now-empty
81
+ * `<p></p>` sitting next to the list instead. Two different malformed
82
+ * shapes, one root cause, so one function fixes both: unwrap a `<ul>`/`<ol>`
83
+ * that's still literally nested inside a `<p>` (the freshly-converted case),
84
+ * and drop an empty `<p>` that ended up glued to a list (the reloaded-old-
85
+ * content case) — matching Attio's own structure, where the list is always
86
+ * a sibling of the surrounding `<p>` blocks, never nested in or padded by
87
+ * one.
88
+ */
89
+ const normalizeListStructure = (root) => {
90
+ for (const list of root.querySelectorAll("p > ul, p > ol")) {
91
+ const p = list.parentElement;
92
+ if (!p)
93
+ continue;
94
+ const before = [];
95
+ const after = [];
96
+ let seenList = false;
97
+ for (const child of Array.from(p.childNodes)) {
98
+ if (child === list) {
99
+ seenList = true;
100
+ }
101
+ else if (seenList) {
102
+ after.push(child);
103
+ }
104
+ else {
105
+ before.push(child);
106
+ }
107
+ }
108
+ const isBlank = (nodes) => nodes.every((n) => (n.textContent ?? "").trim() === "");
109
+ const parent = p.parentElement;
110
+ if (!parent)
111
+ continue;
112
+ if (!isBlank(before)) {
113
+ const leading = document.createElement("p");
114
+ for (const n of before)
115
+ leading.appendChild(n);
116
+ parent.insertBefore(leading, p);
117
+ }
118
+ parent.insertBefore(list, p);
119
+ if (!isBlank(after)) {
120
+ const trailing = document.createElement("p");
121
+ for (const n of after)
122
+ trailing.appendChild(n);
123
+ parent.insertBefore(trailing, p);
124
+ }
125
+ parent.removeChild(p);
126
+ }
127
+ for (const list of root.querySelectorAll("ul, ol")) {
128
+ const isEmptyP = (el) => !!el &&
129
+ el.tagName === "P" &&
130
+ el.children.length === 0 &&
131
+ (el.textContent ?? "").trim() === "";
132
+ if (isEmptyP(list.previousElementSibling))
133
+ list.previousElementSibling?.remove();
134
+ if (isEmptyP(list.nextElementSibling))
135
+ list.nextElementSibling?.remove();
136
+ }
137
+ };
73
138
  /**
74
139
  * A block rich-text editor — measured from Attio's own notes editor. A large
75
140
  * title, a prose body (h1/h2/h3, bulleted/numbered lists, an image block,
@@ -181,8 +246,14 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
181
246
  React.useEffect(() => {
182
247
  if (titleRef.current)
183
248
  titleRef.current.textContent = defaultTitle;
184
- if (bodyRef.current)
249
+ if (bodyRef.current) {
185
250
  bodyRef.current.innerHTML = defaultHtml;
251
+ // #685: normalize any pre-existing `<p><ul>` nesting (or its
252
+ // reloaded shape, an empty `<p>` glued to the list) from content
253
+ // stored before the fix, so it renders correctly instead of staying
254
+ // broken forever.
255
+ normalizeListStructure(bodyRef.current);
256
+ }
186
257
  syncEmpty(titleRef.current);
187
258
  syncEmpty(bodyRef.current);
188
259
  }, []);
@@ -371,6 +442,10 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
371
442
  const run = (cmd, value) => {
372
443
  bodyRef.current?.focus();
373
444
  exec(cmd, value);
445
+ // #685: insertUnorderedList/insertOrderedList can leave the new list
446
+ // nested inside the `<p>` it converted instead of replacing it.
447
+ if (bodyRef.current)
448
+ normalizeListStructure(bodyRef.current);
374
449
  updateBar();
375
450
  syncEmpty(bodyRef.current);
376
451
  emit();