@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
@@ -18,23 +18,68 @@ const tokens_1 = require("../../tokens");
18
18
  // reference lacks).
19
19
  const config = (0, core_1.defineRecipe)({
20
20
  name: "board",
21
- base: `display: flex; gap: ${tokens_1.contract.space["2"]}; height: 100%; box-sizing: border-box; padding: ${tokens_1.contract.space["3"]}; overflow-x: auto; align-items: flex-start; font-family: ${tokens_1.contract.font.sans};
22
- & .crisp-board-col { display: flex; flex-direction: column; width: 268px; flex-shrink: 0; max-height: 100%; box-sizing: border-box; }
21
+ base: `display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]}; height: 100%; box-sizing: border-box; font-family: ${tokens_1.contract.font.sans};
22
+ /* #718: the density toolbar's own row -- everything that used to be on
23
+ the root (padding/overflow/the columns themselves) moves to
24
+ .crisp-board-row below so this can sit above it, unscrolled. */
25
+ & .crisp-board-toolbar { display: flex; align-items: center; justify-content: flex-end; gap: ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["3"]}; flex-shrink: 0; }
26
+ /* Measured live (Deals pipeline): a 54×28px group, 24×24px buttons,
27
+ 2px padding/gap -- an 8px gap between the two groups comes from the
28
+ toolbar's own \`gap\` above. */
29
+ & .crisp-board-density { display: flex; align-items: center; gap: 2px; padding: 2px; border-radius: 10px; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); }
30
+ & .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, ${tokens_1.contract.fgPrimary} 63%, transparent); font-family: inherit; cursor: pointer; }
31
+ & .crisp-board-density button[data-state="on"] { background: ${tokens_1.contract.bgSurface}; color: ${tokens_1.contract.fgPrimary}; }
32
+ & .crisp-board-row { display: flex; gap: ${tokens_1.contract.space["2"]}; flex: 1; min-height: 0; box-sizing: border-box; padding: ${tokens_1.contract.space["3"]}; overflow-x: auto; align-items: flex-start; }
33
+ & .crisp-board-col { display: flex; flex-direction: column; width: 268px; flex-shrink: 0; max-height: 100%; box-sizing: border-box; transition: width 0.15s; }
34
+ /* #718: a collapsed column is a 52px vertical rail -- header only (dot
35
+ + count stacked above a vertical-rl title), body/footer hidden
36
+ entirely. Measured live: 52px width, 8px 0 padding on the header,
37
+ 14px/600 title text. */
38
+ & .crisp-board-col[data-density="collapse"] { width: 52px; }
39
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-header { flex-direction: column; align-items: center; height: auto; padding: ${tokens_1.contract.space["2"]} 0; gap: ${tokens_1.contract.space["1"]}; }
40
+ /* Measured stacking order (top to bottom): dot, count, THEN the vertical
41
+ title -- reordered visually via \`order\`, not by changing the JSX/DOM
42
+ order, which stays dot/title/count (reading order, unaffected by
43
+ density). */
44
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-title { order: 1; writing-mode: vertical-rl; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; }
45
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-count { order: 0; }
46
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-body,
47
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-footer { display: none; }
23
48
  & .crisp-board-col-header { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 36px; padding: 0 ${tokens_1.contract.space["1"]}; flex-shrink: 0; }
24
49
  & .crisp-board-col-dot { width: 12px; height: 12px; border-radius: ${tokens_1.contract.radius.full}; flex-shrink: 0; }
25
50
  & .crisp-board-col-title { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; color: ${tokens_1.contract.fgPrimary}; }
26
- & .crisp-board-col-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
51
+ /* crisp#697: HANDOFF's #472 note claimed the count pill's
52
+ color-mix(fgPrimary, 63%, transparent) reproduced the measured
53
+ rgb(80,81,85) -- it doesn't; 63% of fgPrimary composited on white
54
+ paints rgb(104,105,105), visibly lighter. Text now reads
55
+ fgNeutralSoft directly (the same rgb(80,81,85) token Badge's neutral
56
+ tone uses, #696), and the ring the pill was also missing
57
+ (inset 0 0 0 1px borderSubtle, measured rgb(238,239,241)) is added. */
58
+ & .crisp-board-col-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.sm}; line-height: 16px; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgNeutralSoft}; }
27
59
  & .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: ${tokens_1.contract.radius.sm}; background: none; color: ${tokens_1.contract.fgMuted}; font-family: inherit; cursor: pointer; }
28
60
  & .crisp-board-col-add:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); color: ${tokens_1.contract.fgPrimary}; }
29
61
  & .crisp-board-col-body { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]}; flex: 1; min-height: 48px; padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.lg}; overflow-y: auto; transition: background 0.12s; }
30
62
  & .crisp-board-col[data-over] .crisp-board-col-body { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 7%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 30%, transparent); }
31
63
  & .crisp-board-newcard { display: flex; align-items: center; justify-content: center; gap: ${tokens_1.contract.space["1.5"]}; height: 32px; flex-shrink: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); color: ${tokens_1.contract.fgMuted}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; cursor: pointer; }
32
64
  & .crisp-board-newcard:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 7%, transparent); color: ${tokens_1.contract.fgPrimary}; }
33
- & .crisp-board-card { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]}; box-sizing: border-box; padding: ${tokens_1.contract.space["2.5"]} ${tokens_1.contract.space["3"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; cursor: grab; }
65
+ & .crisp-board-card { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]}; box-sizing: border-box; padding: ${tokens_1.contract.space["2.5"]} ${tokens_1.contract.space["3"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; cursor: grab; touch-action: none; }
66
+ /* #718: a collapsed card is a single 40px row -- measured live and
67
+ independent of \`c.size.tableRow\`/\`tableHeader\` (36/40px) even
68
+ though it happens to match the latter; a plain literal, not a
69
+ cross-reference to a token measured for a different surface.
70
+ \`renderCard\`'s own second argument is what actually decides what a
71
+ collapsed card SHOWS (Board never sees inside its opaque output);
72
+ this only supplies the row-shaped box that content sits in. */
73
+ & .crisp-board-card[data-density="collapse"] { flex-direction: row; align-items: center; height: 40px; padding: 0 ${tokens_1.contract.space["3"]}; }
34
74
  & .crisp-board-card:active { cursor: grabbing; }
35
75
  & .crisp-board-card[data-dragging] { opacity: 0.4; }
36
76
  & .crisp-board-card:focus { outline: none; }
37
77
  & .crisp-board-card:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 1px; }
78
+ /* #674: the pointer-events drag's own drag image -- the same card
79
+ surface above, just pinned to the pointer instead of the flex flow.
80
+ No shadow lift or scale: neither was measured against Attio, and this
81
+ is the same card, momentarily somewhere else, not a new look. */
82
+ & .crisp-board-ghost { position: fixed; z-index: 1; pointer-events: none; cursor: grabbing; }
38
83
  /* #604: a per-column calc footer, mirroring the table's own (measured
39
84
  live, Deals pipeline). Sits below the scrollable card body, not
40
85
  inside it or pinned to it -- Attio's own is position: static, not
@@ -45,6 +90,13 @@ const config = (0, core_1.defineRecipe)({
45
90
  & .crisp-board-col-calc[data-empty] { opacity: 0.7; }
46
91
  & .crisp-board-col-calc-label { text-transform: capitalize; }
47
92
  & .crisp-board-col-calc-value { color: ${tokens_1.contract.fgPrimary}; font-variant-numeric: tabular-nums; }
93
+ /* #718: "Add stage" -- measured live: 52×52px, transparent, a 1px
94
+ dashed border (rgb(230,231,234), exactly \`borderDropzone\`/\`line150\`
95
+ -- the same dashed-border colour ImportDialog's own dropzone already
96
+ uses, not borderSubtle/line100, a close but different value), radius.xl
97
+ (16px), a centered 16px "+". */
98
+ & .crisp-board-addstage { display: inline-flex; align-items: center; justify-content: center; width: 52px; height: 52px; flex-shrink: 0; border: 1px dashed ${tokens_1.contract.borderDropzone}; border-radius: ${tokens_1.contract.radius.xl}; background: none; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; cursor: pointer; }
99
+ & .crisp-board-addstage:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); }
48
100
  /* announced, never drawn */
49
101
  & .crisp-board-live {
50
102
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
@@ -304,8 +304,8 @@ const Composer = React.forwardRef(function Composer({ placeholder, postLabel, me
304
304
  close: closeMention,
305
305
  })] }));
306
306
  });
307
- function CommentRow({ comment, replyLabel, reactLabel, resolveLabel, moreLabel, copyLinkLabel, deleteLabel, addReactionLabel, reactionSearchLabel, emojiOptions, resolvable, resolved, resolvedLabel, onReply, onReact, onResolve, onCopyLink, onDeleteRequest, }) {
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 })] })] })] }));
307
+ function CommentRow({ comment, replyLabel, reactLabel, resolveLabel, moreLabel, copyLinkLabel, deleteLabel, addReactionLabel, reactionSearchLabel, emojiOptions, resolvable, resolved, resolvedLabel, reactable, onReply, onReact, onResolve, onCopyLink, onDeleteRequest, }) {
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: (emoji) => onReact?.(emoji) }) })] }))] }), (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" }) })), reactable && ((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: (emoji) => onReact?.(emoji) }) })), 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 }), onDeleteRequest && ((0, jsx_runtime_1.jsx)(menu_1.MenuItem, { danger: true, onSelect: onDeleteRequest, children: deleteLabel }))] })] })] }));
309
309
  }
310
310
  /**
311
311
  * Per-record comment threads (crisp#625) — measured live on app.attio.com's
@@ -369,29 +369,42 @@ function CommentsPanel({ threads, currentUserId, subject, subjectIcon, showResol
369
369
  const activeThread = activeThreadId
370
370
  ? threads.find((t) => t.id === activeThreadId)
371
371
  : null;
372
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, comments_recipe_1.commentsRecipe)({ className }), role: "dialog", "aria-label": title, ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-head", children: [activeThread ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": backLabel, onClick: () => setActiveThreadId(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-title-wrap", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-title", children: activeThread ? threadTitle : title }), activeThread && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-comments-breadcrumb", children: [breadcrumbPrefix, " ", subjectIcon, " ", subject] }))] }), !activeThread && ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": filterLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: showResolved, onCheckedChange: onShowResolvedChange, children: showResolvedLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: onlyMine, onCheckedChange: onOnlyMineChange, children: onlyMineLabel })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": closeLabel, onClick: onClose, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-body crisp-comments-body--pad-top", children: activeThread ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.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
372
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, comments_recipe_1.commentsRecipe)({ className }), role: "dialog", "aria-label": title, ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-head", children: [activeThread ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": backLabel, onClick: () => setActiveThreadId(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-title-wrap", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-title", children: activeThread ? threadTitle : title }), activeThread && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-comments-breadcrumb", children: [breadcrumbPrefix, " ", subjectIcon, " ", subject] }))] }), !activeThread && ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": filterLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: showResolved, onCheckedChange: onShowResolvedChange, children: showResolvedLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: onlyMine, onCheckedChange: onOnlyMineChange, children: onlyMineLabel })] })), onClose && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": closeLabel, onClick: onClose, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }))] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-body crisp-comments-body--pad-top", children: activeThread ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.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
373
373
  ? activeThread.resolvedBy?.id === currentUserId ||
374
374
  !activeThread.resolvedBy
375
375
  ? resolvedByYouLabel
376
376
  : (resolvedByLabel?.(activeThread.resolvedBy.name) ??
377
377
  `Resolved by ${activeThread.resolvedBy.name}`)
378
- : 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({
379
- threadId: activeThread.id,
380
- commentId: activeThread.root.id,
381
- }) }), activeThread.replies && activeThread.replies.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-replies", children: activeThread.replies.map((reply) => ((0, jsx_runtime_1.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({
378
+ : undefined, reactable: !!onReact, onReact: onReact
379
+ ? (emoji) => onReact(activeThread.id, activeThread.root.id, emoji)
380
+ : undefined, onResolve: onResolve ? () => onResolve(activeThread.id) : undefined, onCopyLink: () => copyLink(activeThread.id, activeThread.root.id), onDeleteRequest: onDelete
381
+ ? () => setDeleteTarget({
382
382
  threadId: activeThread.id,
383
- commentId: reply.id,
384
- }) }, reply.id))) }))] })) : visibleThreads.length === 0 && emptyLabel != null ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-empty", children: emptyLabel })) : (visibleThreads.map((t) => ((0, jsx_runtime_1.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
383
+ commentId: activeThread.root.id,
384
+ })
385
+ : undefined }), activeThread.replies && activeThread.replies.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-replies", children: activeThread.replies.map((reply) => ((0, jsx_runtime_1.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
386
+ ? (emoji) => onReact(activeThread.id, reply.id, emoji)
387
+ : undefined, onCopyLink: () => copyLink(activeThread.id, reply.id), onDeleteRequest: onDelete
388
+ ? () => setDeleteTarget({
389
+ threadId: activeThread.id,
390
+ commentId: reply.id,
391
+ })
392
+ : undefined }, reply.id))) }))] })) : visibleThreads.length === 0 && emptyLabel != null ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-empty", children: emptyLabel })) : (visibleThreads.map((t) => ((0, jsx_runtime_1.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
385
393
  ? t.resolvedBy?.id === currentUserId || !t.resolvedBy
386
394
  ? resolvedByYouLabel
387
395
  : (resolvedByLabel?.(t.resolvedBy.name) ??
388
396
  `Resolved by ${t.resolvedBy.name}`)
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) => {
397
+ : 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
398
+ ? () => setDeleteTarget({ threadId: t.id, commentId: t.root.id })
399
+ : undefined }, 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) => {
390
400
  if (!open)
391
401
  setDeleteTarget(null);
392
402
  }, 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: () => {
403
+ // `deleteTarget` can only ever be set via `onDeleteRequest`
404
+ // (above), which itself only exists when `onDelete` was
405
+ // given -- this call is never reached otherwise.
393
406
  if (deleteTarget)
394
- onDelete(deleteTarget.threadId, deleteTarget.commentId);
407
+ onDelete?.(deleteTarget.threadId, deleteTarget.commentId);
395
408
  setDeleteTarget(null);
396
409
  }, children: deleteConfirmActionLabel })] })] })] }));
397
410
  }
@@ -413,6 +426,15 @@ function CommentCountBadge({ count, hovered, onOpen, onOpenPreview, label: label
413
426
  ? `crisp-comments-badge ${className}`
414
427
  : "crisp-comments-badge", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-badge-dot" }) }));
415
428
  }
416
- return ((0, jsx_runtime_1.jsxs)("span", { className: className ? `crisp-comments-badge ${className}` : "crisp-comments-badge", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-comments-badge", "aria-label": label, onClick: onOpen, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.MessageSquare, { size: 12, strokeWidth: 2, "aria-hidden": "true" }), count > 0 && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-badge-count", children: count }))] }), onOpenPreview && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-badge", "aria-label": previewLabel, onClick: onOpenPreview, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpRight, { "data-rtl-flip": "", size: 12, strokeWidth: 2, "aria-hidden": "true" }) }))] }));
429
+ return ((0, jsx_runtime_1.jsxs)("span", { className: className ? `crisp-comments-badge ${className}` : "crisp-comments-badge", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-comments-badge-trigger", "aria-label": label, onClick: (ev) => {
430
+ // this badge lives in a grid row (crisp/#601 supersession) whose
431
+ // own click opens the record -- the same stopPropagation every
432
+ // other per-row affordance in that cell already uses.
433
+ ev.stopPropagation();
434
+ onOpen();
435
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.MessageSquare, { size: 12, strokeWidth: 2, "aria-hidden": "true" }), count > 0 && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-badge-count", children: count }))] }), onOpenPreview && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-badge-preview", "aria-label": previewLabel, onClick: (ev) => {
436
+ ev.stopPropagation();
437
+ onOpenPreview();
438
+ }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }))] }));
417
439
  }
418
440
  CommentCountBadge.displayName = "CommentCountBadge";
@@ -36,12 +36,26 @@ const config = (0, core_1.defineRecipe)({
36
36
 
37
37
  /* One comment row -- avatar 16px (Avatar size="sm"), 6px gap to the
38
38
  name/time line, 14px/600 name + 12px/500 63%-muted time, 14px/400 body
39
- two lines below. Hover actions fade in flush with the name line. */
39
+ two lines below. Hover actions fade in flush with the name line.
40
+ .crisp-comments-row-head's own height: 20px (above) is a single
41
+ text-line's worth, on the same assumption .crisp-comments-title/
42
+ .crisp-comments-breadcrumb already act on higher up in this same
43
+ recipe -- a name that doesn't fit wraps to two lines with nothing
44
+ reserving room for the second one, overflowing straight into the
45
+ body text underneath (found live: this app's own dev-bypass author
46
+ name, an email long enough to wrap, sat on top of the comment's own
47
+ body text). .crisp-comments-author gets the identical nowrap-
48
+ ellipsis treatment those two already have, plus min-width: 0 so a
49
+ flex item is actually allowed to shrink below its own text's natural
50
+ width in the first place; .crisp-comments-time gets flex-shrink: 0
51
+ so it's the name that gives, never the timestamp -- the same "one
52
+ side shrinks, the other one doesn't" split PersonChip's own name/
53
+ avatar pair already uses (crisp#668/#671). */
40
54
  & .crisp-comments-row { position: relative; display: flex; gap: ${tokens_1.contract.space["1.5"]}; }
41
55
  & .crisp-comments-row-main { flex: 1; min-width: 0; }
42
56
  & .crisp-comments-row-head { display: flex; align-items: baseline; gap: ${tokens_1.contract.space["1.5"]}; height: 20px; }
43
- & .crisp-comments-author { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; color: ${tokens_1.contract.fgPrimary}; }
44
- & .crisp-comments-time { font-size: 12px; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); white-space: nowrap; }
57
+ & .crisp-comments-author { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; color: ${tokens_1.contract.fgPrimary}; }
58
+ & .crisp-comments-time { flex-shrink: 0; font-size: 12px; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); white-space: nowrap; }
45
59
  & .crisp-comments-resolved-label { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; margin-bottom: ${tokens_1.contract.space["1"]}; font-size: 12px; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
46
60
  & .crisp-comments-body-text { font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.regular}; color: ${tokens_1.contract.fgPrimary}; white-space: pre-wrap; word-break: break-word; }
47
61
 
@@ -129,11 +143,21 @@ const config = (0, core_1.defineRecipe)({
129
143
 
130
144
  /* Row badge (CommentCountBadge) -- lives in its own class family so the
131
145
  grid lanes can drop it into a cell without pulling in the panel's
132
- CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
133
- icon + 12px count text, both at 55% ink, replacing the dot. */
134
- .crisp-comments-badge { box-sizing: border-box; font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; border: none; background: none; padding: 0; margin: 0; cursor: pointer; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); }
146
+ CSS. Re-measured live on a real commented row (crisp/#601
147
+ supersession, right-aligned in a DataTable primary column): a 6px
148
+ grey dot at rest (unchanged, already matched); on hover, the trigger
149
+ button is auto-width with a 12px icon + 12px/500 count text,
150
+ "padding: 2px 6px 2px 4px", radius 6px -- distinct from the preview
151
+ button beside it, a fixed 20px square with a 16px icon, same radius.
152
+ Both share the 55%-ink idle colour and darken on their OWN hover
153
+ (color-mix 4%, the same formula DataTable's own #601 icons use). */
154
+ .crisp-comments-badge { box-sizing: border-box; font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; }
135
155
  .crisp-comments-badge-dot { box-sizing: border-box; width: 6px; height: 6px; border-radius: ${tokens_1.contract.radius.full}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 38%, transparent); }
136
- .crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
156
+ .crisp-comments-badge-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: 20px; margin: 0; padding: 2px ${tokens_1.contract.space["1.5"]} 2px ${tokens_1.contract.space["1"]}; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; font-family: inherit; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); cursor: pointer; }
157
+ .crisp-comments-badge-trigger:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); color: ${tokens_1.contract.fgPrimary}; }
158
+ .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: ${tokens_1.contract.radius.sm}; background: none; font-family: inherit; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); cursor: pointer; flex-shrink: 0; }
159
+ .crisp-comments-badge-preview:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); color: ${tokens_1.contract.fgPrimary}; }
160
+ .crisp-comments-badge-count { font-size: 12px; line-height: 16px; font-weight: ${tokens_1.contract.weight.medium}; }
137
161
  [dir="rtl"] .crisp-comments-badge [data-rtl-flip] { transform: scaleX(-1); }
138
162
  `,
139
163
  });
@@ -69,13 +69,19 @@ function useFormDialogOptional(required) {
69
69
  * per-field wiring). A record needs several fields at once to be valid,
70
70
  * which is why records are not created by inline editing.
71
71
  */
72
- 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 }) {
72
+ 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 }) {
73
73
  const createMoreLabel = (0, core_1.useCrispMessage)(createMoreLabelProp, "Create more");
74
74
  const createRecordLabel = (0, core_1.useCrispMessage)(undefined, "Create record");
75
75
  const cancelLabel = (0, core_1.useCrispMessage)(cancelLabelProp, "Cancel");
76
- const discardTitle = (0, core_1.useCrispMessage)(discardTitleProp, "Discard changes? Your changes will be lost.");
76
+ // crisp#677: Attio's own confirm is two lines (a title, then a separate
77
+ // sentence), not one combined string — measured live, both quoted
78
+ // verbatim off app.attio.com's create-attribute dialog's own discard
79
+ // confirm. The buttons read "Cancel"/"Discard" there too, not crisp's
80
+ // prior "Keep editing" (a paraphrase, never independently measured).
81
+ const discardTitle = (0, core_1.useCrispMessage)(discardTitleProp, "Discard changes");
82
+ const discardDescription = (0, core_1.useCrispMessage)(discardDescriptionProp, "Are you sure you want to discard these changes?");
77
83
  const discardConfirmLabel = (0, core_1.useCrispMessage)(discardConfirmLabelProp, "Discard");
78
- const discardCancelLabel = (0, core_1.useCrispMessage)(discardCancelLabelProp, "Keep editing");
84
+ const discardCancelLabel = (0, core_1.useCrispMessage)(discardCancelLabelProp, "Cancel");
79
85
  const formId = React.useId();
80
86
  const formRef = React.useRef(null);
81
87
  const switchId = React.useId();
@@ -163,7 +169,7 @@ function FormDialog({ title, submitLabel, onSubmit, onClose, busy, error, childr
163
169
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(dialog_1.Dialog, { open: true, onOpenChange: (open) => !open && requestClose(), title: title, width: size ?? (wide ? 760 : 640), className: (0, formdialog_recipe_1.formdialogRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)(dialog_1.DialogBody, { children: [(0, jsx_runtime_1.jsx)(OptionalLabelsContext.Provider, { value: createMode, children: (0, jsx_runtime_1.jsx)("form", { ref: formRef, id: formId, className: "crisp-formdialog-form", onSubmit: (e) => {
164
170
  e.preventDefault();
165
171
  onSubmit(createMore);
166
- }, onChange: markDirty, onInput: markDirty, onClick: markDirty, children: children }) }), error != null && (0, jsx_runtime_1.jsx)("p", { className: "crisp-formdialog-error", children: error })] }), (0, jsx_runtime_1.jsx)(dialog_1.DialogFooter, { children: createMode ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-formdialog-createfoot", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-dialog-more", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { id: switchId, checked: createMore, onCheckedChange: setCreateMore, "aria-label": createMoreLabel }), (0, jsx_runtime_1.jsx)("label", { className: "crisp-formdialog-more-label", htmlFor: switchId, children: createMoreLabel })] }), (0, jsx_runtime_1.jsxs)("button", { type: "submit", form: formId, className: "crisp-dialog-submit", disabled: busy, children: [createRecordLabel, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-dialog-submit-key", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("kbd", { children: "\u2318" }), (0, jsx_runtime_1.jsx)("kbd", { children: "\u21B5" })] })] })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", intent: "neutral", onClick: requestClose, children: cancelLabel }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "submit", form: formId, intent: "primary", loading: busy, children: submitLabel })] })) })] }), (0, jsx_runtime_1.jsx)(dialog_1.Dialog, { open: confirmingDiscard, onOpenChange: (open) => !open && setConfirmingDiscard(false), title: discardTitle, width: 400, children: (0, jsx_runtime_1.jsxs)(dialog_1.DialogFooter, { children: [(0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", intent: "neutral", onClick: () => setConfirmingDiscard(false), children: discardCancelLabel }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", intent: "danger", onClick: () => {
172
+ }, onChange: markDirty, onInput: markDirty, onClick: markDirty, children: children }) }), error != null && (0, jsx_runtime_1.jsx)("p", { className: "crisp-formdialog-error", children: error })] }), (0, jsx_runtime_1.jsx)(dialog_1.DialogFooter, { children: createMode ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-formdialog-createfoot", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-dialog-more", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { id: switchId, checked: createMore, onCheckedChange: setCreateMore, "aria-label": createMoreLabel }), (0, jsx_runtime_1.jsx)("label", { className: "crisp-formdialog-more-label", htmlFor: switchId, children: createMoreLabel })] }), (0, jsx_runtime_1.jsxs)("button", { type: "submit", form: formId, className: "crisp-dialog-submit", disabled: busy, children: [createRecordLabel, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-dialog-submit-key", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("kbd", { children: "\u2318" }), (0, jsx_runtime_1.jsx)("kbd", { children: "\u21B5" })] })] })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", intent: "neutral", onClick: requestClose, children: cancelLabel }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "submit", form: formId, intent: "primary", loading: busy, children: submitLabel })] })) })] }), (0, jsx_runtime_1.jsx)(dialog_1.Dialog, { open: confirmingDiscard, onOpenChange: (open) => !open && setConfirmingDiscard(false), variant: "alert", title: discardTitle, description: discardDescription, width: 440, children: (0, jsx_runtime_1.jsxs)(dialog_1.DialogFooter, { variant: "alert", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", intent: "neutral", onClick: () => setConfirmingDiscard(false), children: discardCancelLabel }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", intent: "danger", onClick: () => {
167
173
  setConfirmingDiscard(false);
168
174
  onClose();
169
175
  }, children: discardConfirmLabel })] }) })] }));
@@ -36,9 +36,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.NavItem = void 0;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const React = __importStar(require("react"));
39
+ const kbd_1 = require("../../components/kbd");
40
+ const tooltip_1 = require("../../components/tooltip");
39
41
  const core_1 = require("../../core");
40
42
  const navitem_recipe_1 = require("./navitem.recipe");
41
- exports.NavItem = React.forwardRef(({ active, icon, trailing, className, children, ...props }, ref) => {
42
- return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.a, { ref: ref, "data-active": active || undefined, className: (0, navitem_recipe_1.navitemRecipe)({ className }), ...props, children: [icon, (0, jsx_runtime_1.jsx)("span", { className: "crisp-navitem-label", children: children }), trailing != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-navitem-trailing", children: trailing }))] }));
43
+ exports.NavItem = React.forwardRef(({ active, icon, trailing, shortcut, className, children, ...props }, ref) => {
44
+ const row = ((0, jsx_runtime_1.jsxs)(core_1.Primitive.a, { ref: ref, "data-active": active || undefined, className: (0, navitem_recipe_1.navitemRecipe)({ className }), ...props, children: [icon, (0, jsx_runtime_1.jsx)("span", { className: "crisp-navitem-label", children: children }), trailing != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-navitem-trailing", children: trailing }))] }));
45
+ if (!shortcut)
46
+ return row;
47
+ return ((0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { asChild: true, side: "right", delay: 500, content: (0, jsx_runtime_1.jsxs)("span", { className: "crisp-navitem-shortcut", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-navitem-shortcut-desc", children: shortcut.description }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-navitem-shortcut-chord", children: shortcut.keys.map((key, i) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [i > 0 && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-navitem-shortcut-then", children: "then" })), (0, jsx_runtime_1.jsx)(kbd_1.Kbd, { children: key })] }, i))) })] }), children: row }));
43
48
  });
44
49
  exports.NavItem.displayName = "NavItem";
@@ -21,6 +21,22 @@ const config = (0, core_1.defineRecipe)({
21
21
  variants: {},
22
22
  defaultVariants: {},
23
23
  compoundVariants: [],
24
+ // Tooltip portals its content to document.body (same reason sidebar.recipe.ts's
25
+ // menu content lives in globalCss, not a `&`-nested rule): a shortcut hint
26
+ // never sits inside `.crisp-navitem` in the live DOM, so it needs an
27
+ // unscoped rule to be reached at all. Values measured live on
28
+ // app.attio.com's own "G then <letter>" hint (crisp#633): the description
29
+ // inherits Tooltip's own #eeeff1 text colour for free (it's the same hex),
30
+ // but "then" and the Kbd chip are their own greys, hardcoded raw rather
31
+ // than through a token -- same call tooltip.recipe.ts already made for
32
+ // this exact bubble, which is always dark regardless of the app's theme.
33
+ globalCss: `
34
+ .crisp-navitem-shortcut { display: flex; align-items: baseline; justify-content: space-between; gap: ${tokens_1.contract.space["3"]}; }
35
+ .crisp-navitem-shortcut-desc { font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; line-height: 16px; white-space: nowrap; }
36
+ .crisp-navitem-shortcut-chord { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; flex-shrink: 0; }
37
+ .crisp-navitem-shortcut-then { font-size: ${tokens_1.contract.text.xs}; font-weight: ${tokens_1.contract.weight.medium}; color: #898a8d; }
38
+ .crisp-navitem-shortcut-chord .crisp-kbd { text-transform: uppercase; font-weight: ${tokens_1.contract.weight.regular}; color: #a2a4a7; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2); }
39
+ `,
24
40
  });
25
41
  exports.navitemRecipe = Object.assign((0, core_1.resolveRecipe)(config), {
26
42
  config,
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.objectsettingsRecipe = exports.ObjectsList = exports.ObjectSettings = exports.ObjectGeneralSection = exports.ObjectDetailPage = exports.ObjectAppearanceSection = exports.CreateAttributeDialog = exports.AttributeTypeSelect = exports.AttributesTable = exports.ATTRIBUTE_TYPE_OPTIONS = exports.ATTRIBUTE_AI_AUTOFILL_OPTIONS = void 0;
3
+ exports.objectsettingsRecipe = exports.ObjectsList = exports.ObjectGeneralSection = exports.ObjectDetailPage = exports.ObjectAppearanceSection = exports.CreateAttributeDialog = exports.AttributeTypeSelect = exports.AttributesTable = exports.ATTRIBUTE_TYPE_OPTIONS = exports.ATTRIBUTE_AI_AUTOFILL_OPTIONS = void 0;
4
4
  var objectsettings_1 = require("./objectsettings");
5
5
  Object.defineProperty(exports, "ATTRIBUTE_AI_AUTOFILL_OPTIONS", { enumerable: true, get: function () { return objectsettings_1.ATTRIBUTE_AI_AUTOFILL_OPTIONS; } });
6
6
  Object.defineProperty(exports, "ATTRIBUTE_TYPE_OPTIONS", { enumerable: true, get: function () { return objectsettings_1.ATTRIBUTE_TYPE_OPTIONS; } });
@@ -10,7 +10,6 @@ Object.defineProperty(exports, "CreateAttributeDialog", { enumerable: true, get:
10
10
  Object.defineProperty(exports, "ObjectAppearanceSection", { enumerable: true, get: function () { return objectsettings_1.ObjectAppearanceSection; } });
11
11
  Object.defineProperty(exports, "ObjectDetailPage", { enumerable: true, get: function () { return objectsettings_1.ObjectDetailPage; } });
12
12
  Object.defineProperty(exports, "ObjectGeneralSection", { enumerable: true, get: function () { return objectsettings_1.ObjectGeneralSection; } });
13
- Object.defineProperty(exports, "ObjectSettings", { enumerable: true, get: function () { return objectsettings_1.ObjectSettings; } });
14
13
  Object.defineProperty(exports, "ObjectsList", { enumerable: true, get: function () { return objectsettings_1.ObjectsList; } });
15
14
  var objectsettings_recipe_1 = require("./objectsettings.recipe");
16
15
  Object.defineProperty(exports, "objectsettingsRecipe", { enumerable: true, get: function () { return objectsettings_recipe_1.objectsettingsRecipe; } });
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.ObjectSettings = exports.ATTRIBUTE_AI_AUTOFILL_OPTIONS = exports.ATTRIBUTE_TYPE_OPTIONS = void 0;
36
+ exports.ATTRIBUTE_AI_AUTOFILL_OPTIONS = exports.ATTRIBUTE_TYPE_OPTIONS = void 0;
37
37
  exports.ObjectsList = ObjectsList;
38
38
  exports.ObjectDetailPage = ObjectDetailPage;
39
39
  exports.ObjectGeneralSection = ObjectGeneralSection;
@@ -157,7 +157,7 @@ function ObjectDetailPage({ icon, iconColor, name, typeLabel, description, tab,
157
157
  setInternalTab(next);
158
158
  onTabChange?.(next);
159
159
  };
160
- return ((0, jsx_runtime_1.jsx)("div", { className: (0, objectsettings_recipe_1.objectsettingsRecipe)(), children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-page", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-objectsettings-head", children: (0, jsx_runtime_1.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 }, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-objectsettings-icon crisp-objectsettings-icon--lg", style: { background: iconColor ?? "currentColor" }, children: icon }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-heading", children: [(0, jsx_runtime_1.jsxs)("h1", { className: "crisp-objectsettings-title", children: [name, typeLabel != null && ((0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: "neutral", size: "small", children: typeLabel }))] }), description != null && ((0, jsx_runtime_1.jsx)("p", { className: "crisp-objectsettings-subtitle", children: description }))] })] }) }), (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { value: active, onValueChange: setTab, tabs: [
160
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, objectsettings_recipe_1.objectsettingsRecipe)(), children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-page", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-objectsettings-head", children: (0, jsx_runtime_1.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 }, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-objectsettings-icon crisp-objectsettings-icon--lg", style: { background: iconColor ?? "currentColor" }, children: icon }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-heading", children: [(0, jsx_runtime_1.jsxs)("h1", { className: "crisp-objectsettings-title", children: [name, typeLabel != null && ((0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: "neutral", size: "medium", children: typeLabel }))] }), description != null && ((0, jsx_runtime_1.jsx)("p", { className: "crisp-objectsettings-subtitle", children: description }))] })] }) }), (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { value: active, onValueChange: setTab, tabs: [
161
161
  { value: "configuration", label: configurationLabel },
162
162
  {
163
163
  value: "attributes",
@@ -245,7 +245,7 @@ function AttributesTable({ attributes, onOpenAttribute, onEditAttribute, onArchi
245
245
  {
246
246
  key: "properties",
247
247
  header: propertiesColumnLabel,
248
- render: (_v, row) => ((0, jsx_runtime_1.jsxs)("span", { style: { display: "flex", gap: 6 }, children: [row.system && ((0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: "neutral", size: "small", children: systemLabel })), row.enriched && ((0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: "purple", size: "small", children: enrichedLabel }))] })),
248
+ render: (_v, row) => ((0, jsx_runtime_1.jsxs)("span", { style: { display: "flex", gap: 6 }, children: [row.system && ((0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: "neutral", size: "medium", children: systemLabel })), row.enriched && ((0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: "purple", size: "medium", children: enrichedLabel }))] })),
249
249
  },
250
250
  {
251
251
  key: "actions",
@@ -353,13 +353,3 @@ function CreateAttributeDialog({ type, onTypeChange, typeOptions, aiAutofillType
353
353
  return ((0, jsx_runtime_1.jsxs)(formdialog_1.FormDialog, { title: title, submitLabel: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [createLabel, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-objectsettings-kbd", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("kbd", { children: "\u2318" }), (0, jsx_runtime_1.jsx)("kbd", { children: "\u21B5" })] })] }), onSubmit: onSubmit, onClose: onClose, busy: busy, error: error, size: 520, className: (0, objectsettings_recipe_1.objectsettingsRecipe)(), children: [(0, jsx_runtime_1.jsx)(formfield_1.FormField, { label: attributeTypeLabel, children: (0, jsx_runtime_1.jsx)(AttributeTypeSelect, { value: type, onValueChange: onTypeChange, options: typeOptions, aiAutofillOptions: aiAutofillTypeOptions }) }), (0, jsx_runtime_1.jsx)(formfield_1.FormField, { label: nameLabel, htmlFor: nameId, children: (0, jsx_runtime_1.jsx)(input_1.Input, { id: nameId, autoFocus: true, value: name, onChange: (e) => onNameChange(e.target.value) }) }), (0, jsx_runtime_1.jsx)(formfield_1.FormField, { label: descriptionLabel, optional: true, children: (0, jsx_runtime_1.jsx)(textarea_1.Textarea, { placeholder: descriptionPlaceholder, value: description, onChange: (e) => onDescriptionChange(e.target.value) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-aiautofill", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-objectsettings-aiautofill-label", children: aiAutofillLabel }), (0, jsx_runtime_1.jsxs)("label", { className: "crisp-objectsettings-aiautofill-row", htmlFor: switchId, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14 }), (0, jsx_runtime_1.jsx)("span", { children: setUpAiAutofillLabel }), (0, jsx_runtime_1.jsx)(switch_1.Switch, { id: switchId, checked: aiAutofillEnabled, onCheckedChange: onAiAutofillChange })] })] })] }));
354
354
  }
355
355
  CreateAttributeDialog.displayName = "CreateAttributeDialog";
356
- /**
357
- * This folder holds several components (`ObjectsList`, `ObjectDetailPage`,
358
- * `AttributesTable`, `CreateAttributeDialog`, …), not one named after the
359
- * slug — unlike every other product dir, whose docs `<ManualInstallation>`
360
- * resolves an import binding from the folder slug ("avatargroup" →
361
- * `AvatarGroup`) by reading this file's exports. This alias exists only so
362
- * that resolver finds a match for "objectsettings"; consumers should import
363
- * the real names above, never this one.
364
- */
365
- exports.ObjectSettings = ObjectsList;
@@ -193,7 +193,6 @@ exports.recordsTableLabels = {
193
193
  selectionAnnouncement: (count) => count === 0 ? "Selection cleared" : `${count} selected`,
194
194
  delete: "Delete",
195
195
  sendEmail: "Send email",
196
- exportCsv: "Export CSV",
197
196
  runWorkflow: "Run workflow",
198
197
  calculate: "Calculate",
199
198
  emptyPlaceholder: "Empty",
@@ -1043,21 +1042,6 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
1043
1042
  setData((prev) => prev.filter((r) => !selected.has(r.id)));
1044
1043
  setSelected(new Set());
1045
1044
  };
1046
- const exportSelected = () => {
1047
- const picked = data.filter((r) => selected.has(r.id));
1048
- const esc = (s) => `"${s.replace(/"/g, '""')}"`;
1049
- const csv = [
1050
- cols.map((c) => esc(c.label)).join(","),
1051
- ...picked.map((r) => cols.map((c) => esc(cellText(c, r))).join(",")),
1052
- ].join("\n");
1053
- const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
1054
- const a = document.createElement("a");
1055
- a.href = url;
1056
- a.download = "records.csv";
1057
- a.click();
1058
- URL.revokeObjectURL(url);
1059
- setOpenMenu(null);
1060
- };
1061
1045
  const entityCol = cols.find((c) => c.type === "entity");
1062
1046
  // "Add to list" → pin the selected rows (shows a ★ on the entity)
1063
1047
  const addToList = () => {
@@ -1303,7 +1287,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
1303
1287
  // filtered-to-zero ring -- two different Attio surfaces.
1304
1288
  variant: !activeFilters && emptyStateExtra ? "records" : "page", icon: activeFilters ? ((0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Inbox, { size: !activeFilters && emptyStateExtra ? 40 : 18, strokeWidth: 1.25, "aria-hidden": "true" })), title: activeFilters ? "No records match that filter" : t.noRecords, description: activeFilters
1305
1289
  ? "None of your records match the filters that are set."
1306
- : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] }), emptyStateExtra?.onImportCsv && ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onImportCsv, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 15, strokeWidth: 1.75 }), t.importCsv] })), emptyStateExtra?.onConnectDataSource && ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onConnectDataSource, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plug, { size: 15, strokeWidth: 1.75 }), t.connectDataSource] }))] })), learnMore: !activeFilters ? emptyStateExtra?.learnMore : undefined }) })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-addrow", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && ((0, jsx_runtime_1.jsxs)(bulkbar_1.BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [(0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.moreActions, trigger: (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), (0, jsx_runtime_1.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 &&
1290
+ : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] }), emptyStateExtra?.onImportCsv && ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onImportCsv, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 15, strokeWidth: 1.75 }), t.importCsv] })), emptyStateExtra?.onConnectDataSource && ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onConnectDataSource, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plug, { size: 15, strokeWidth: 1.75 }), t.connectDataSource] }))] })), learnMore: !activeFilters ? emptyStateExtra?.learnMore : undefined }) })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-addrow", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && ((0, jsx_runtime_1.jsxs)(bulkbar_1.BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [(0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), (0, jsx_runtime_1.jsx)(menu_1.Menu, { align: "end", "aria-label": t.moreActions, trigger: (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete }) })] })), (0, jsx_runtime_1.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 &&
1307
1291
  (0, react_dom_1.createPortal)((0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => copyRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), t.copy] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => duplicateRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CopyPlus, { size: 15, strokeWidth: 1.75 }), t.duplicate] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => pinRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), t.addToList] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item crisp-recordstable-menu-item--danger", role: "menuitem", tabIndex: -1, onClick: () => deleteRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), t.delete] })] }), rootRef.current ?? document.body)] }));
1308
1292
  }
1309
1293
  exports.defaultColumns = [
@@ -106,6 +106,71 @@ function matchSlashTrigger(textBeforeCaret, trigger = "/") {
106
106
  return { query, start };
107
107
  }
108
108
  const exec = (cmd, value) => document.execCommand(cmd, false, value);
109
+ /**
110
+ * #685: `execCommand("insertUnorderedList"/"insertOrderedList")` is built
111
+ * through DOM APIs, not the HTML parser, so it can leave the live DOM in a
112
+ * shape the parser itself would never produce: a `<ul>`/`<ol>` as a literal
113
+ * child of the `<p>` it converted, e.g. `<p><ul><li>text</li></ul></p>`.
114
+ * That's invalid content for a `<p>` — reading `innerHTML` back and later
115
+ * re-parsing it (a fresh mount, GRC reloading a stored note) auto-closes the
116
+ * `<p>` the instant the parser meets the `<ul>`, which leaves a now-empty
117
+ * `<p></p>` sitting next to the list instead. Two different malformed
118
+ * shapes, one root cause, so one function fixes both: unwrap a `<ul>`/`<ol>`
119
+ * that's still literally nested inside a `<p>` (the freshly-converted case),
120
+ * and drop an empty `<p>` that ended up glued to a list (the reloaded-old-
121
+ * content case) — matching Attio's own structure, where the list is always
122
+ * a sibling of the surrounding `<p>` blocks, never nested in or padded by
123
+ * one.
124
+ */
125
+ const normalizeListStructure = (root) => {
126
+ for (const list of root.querySelectorAll("p > ul, p > ol")) {
127
+ const p = list.parentElement;
128
+ if (!p)
129
+ continue;
130
+ const before = [];
131
+ const after = [];
132
+ let seenList = false;
133
+ for (const child of Array.from(p.childNodes)) {
134
+ if (child === list) {
135
+ seenList = true;
136
+ }
137
+ else if (seenList) {
138
+ after.push(child);
139
+ }
140
+ else {
141
+ before.push(child);
142
+ }
143
+ }
144
+ const isBlank = (nodes) => nodes.every((n) => (n.textContent ?? "").trim() === "");
145
+ const parent = p.parentElement;
146
+ if (!parent)
147
+ continue;
148
+ if (!isBlank(before)) {
149
+ const leading = document.createElement("p");
150
+ for (const n of before)
151
+ leading.appendChild(n);
152
+ parent.insertBefore(leading, p);
153
+ }
154
+ parent.insertBefore(list, p);
155
+ if (!isBlank(after)) {
156
+ const trailing = document.createElement("p");
157
+ for (const n of after)
158
+ trailing.appendChild(n);
159
+ parent.insertBefore(trailing, p);
160
+ }
161
+ parent.removeChild(p);
162
+ }
163
+ for (const list of root.querySelectorAll("ul, ol")) {
164
+ const isEmptyP = (el) => !!el &&
165
+ el.tagName === "P" &&
166
+ el.children.length === 0 &&
167
+ (el.textContent ?? "").trim() === "";
168
+ if (isEmptyP(list.previousElementSibling))
169
+ list.previousElementSibling?.remove();
170
+ if (isEmptyP(list.nextElementSibling))
171
+ list.nextElementSibling?.remove();
172
+ }
173
+ };
109
174
  /**
110
175
  * A block rich-text editor — measured from Attio's own notes editor. A large
111
176
  * title, a prose body (h1/h2/h3, bulleted/numbered lists, an image block,
@@ -217,8 +282,14 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
217
282
  React.useEffect(() => {
218
283
  if (titleRef.current)
219
284
  titleRef.current.textContent = defaultTitle;
220
- if (bodyRef.current)
285
+ if (bodyRef.current) {
221
286
  bodyRef.current.innerHTML = defaultHtml;
287
+ // #685: normalize any pre-existing `<p><ul>` nesting (or its
288
+ // reloaded shape, an empty `<p>` glued to the list) from content
289
+ // stored before the fix, so it renders correctly instead of staying
290
+ // broken forever.
291
+ normalizeListStructure(bodyRef.current);
292
+ }
222
293
  syncEmpty(titleRef.current);
223
294
  syncEmpty(bodyRef.current);
224
295
  }, []);
@@ -407,6 +478,10 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
407
478
  const run = (cmd, value) => {
408
479
  bodyRef.current?.focus();
409
480
  exec(cmd, value);
481
+ // #685: insertUnorderedList/insertOrderedList can leave the new list
482
+ // nested inside the `<p>` it converted instead of replacing it.
483
+ if (bodyRef.current)
484
+ normalizeListStructure(bodyRef.current);
410
485
  updateBar();
411
486
  syncEmpty(bodyRef.current);
412
487
  emit();