@crisp-ui-kit/crisp 0.54.7 → 0.54.8

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 (113) 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 +13 -9
  6. package/dist/board.layered.css +13 -9
  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/datatable/datatable.js +66 -7
  13. package/dist/cjs/components/datatable/datatable.recipe.js +57 -2
  14. package/dist/cjs/components/dialog/dialog.js +8 -5
  15. package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
  16. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +12 -3
  17. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +231 -48
  18. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +44 -0
  19. package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
  20. package/dist/cjs/components/tooltip/tooltip.js +23 -9
  21. package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
  22. package/dist/cjs/product/authshell/authshell.js +14 -1
  23. package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
  24. package/dist/cjs/product/board/board.js +122 -27
  25. package/dist/cjs/product/board/board.recipe.js +14 -2
  26. package/dist/cjs/product/comments/comments.js +23 -10
  27. package/dist/cjs/product/comments/comments.recipe.js +17 -3
  28. package/dist/cjs/product/formdialog/formdialog.js +10 -4
  29. package/dist/cjs/product/navitem/navitem.js +7 -2
  30. package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
  31. package/dist/cjs/product/objectsettings/index.js +1 -2
  32. package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
  33. package/dist/cjs/product/richtext/richtext.js +76 -1
  34. package/dist/cjs/product/sidebar/sidebar.js +68 -15
  35. package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
  36. package/dist/cjs/tokens/palette.js +32 -1
  37. package/dist/cjs/tokens/semantic.js +11 -0
  38. package/dist/comments.css +36 -10
  39. package/dist/comments.layered.css +36 -10
  40. package/dist/datatable.css +47 -9
  41. package/dist/datatable.layered.css +47 -9
  42. package/dist/dialog.css +13 -1
  43. package/dist/dialog.layered.css +13 -1
  44. package/dist/esm/components/badge/badge.recipe.js +28 -5
  45. package/dist/esm/components/button/button.js +34 -3
  46. package/dist/esm/components/button/button.recipe.js +31 -5
  47. package/dist/esm/components/datatable/datatable.js +67 -8
  48. package/dist/esm/components/datatable/datatable.recipe.js +57 -2
  49. package/dist/esm/components/dialog/dialog.js +8 -5
  50. package/dist/esm/components/dialog/dialog.recipe.js +15 -1
  51. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +12 -3
  52. package/dist/esm/components/tabletoolbar/tabletoolbar.js +231 -48
  53. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +44 -0
  54. package/dist/esm/components/tabs/tabs.recipe.js +11 -2
  55. package/dist/esm/components/tooltip/tooltip.js +23 -9
  56. package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
  57. package/dist/esm/product/authshell/authshell.js +15 -2
  58. package/dist/esm/product/authshell/authshell.recipe.js +13 -1
  59. package/dist/esm/product/authshell/index.js +1 -1
  60. package/dist/esm/product/board/board.js +122 -27
  61. package/dist/esm/product/board/board.recipe.js +14 -2
  62. package/dist/esm/product/comments/comments.js +23 -10
  63. package/dist/esm/product/comments/comments.recipe.js +17 -3
  64. package/dist/esm/product/formdialog/formdialog.js +10 -4
  65. package/dist/esm/product/navitem/index.js +1 -1
  66. package/dist/esm/product/navitem/navitem.js +7 -2
  67. package/dist/esm/product/navitem/navitem.recipe.js +16 -0
  68. package/dist/esm/product/objectsettings/index.js +1 -1
  69. package/dist/esm/product/objectsettings/objectsettings.js +2 -12
  70. package/dist/esm/product/richtext/richtext.js +76 -1
  71. package/dist/esm/product/sidebar/sidebar.js +69 -16
  72. package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
  73. package/dist/esm/tokens/palette.js +32 -1
  74. package/dist/esm/tokens/semantic.js +11 -0
  75. package/dist/native/tokens.d.ts +1 -1
  76. package/dist/native/tokens.js +16 -2
  77. package/dist/navitem.css +7 -0
  78. package/dist/navitem.layered.css +7 -0
  79. package/dist/recordstable.css +1 -1
  80. package/dist/recordstable.layered.css +1 -1
  81. package/dist/sidebar.css +6 -1
  82. package/dist/sidebar.layered.css +6 -1
  83. package/dist/styles.css +163 -42
  84. package/dist/styles.layered.css +163 -42
  85. package/dist/tabletoolbar.css +16 -1
  86. package/dist/tabletoolbar.layered.css +16 -1
  87. package/dist/tabs.css +1 -1
  88. package/dist/tabs.layered.css +1 -1
  89. package/dist/tokens.css +9 -1
  90. package/dist/tokens.json +16 -2
  91. package/dist/tooltip.css +1 -1
  92. package/dist/tooltip.layered.css +1 -1
  93. package/dist/types/components/badge/badge.recipe.d.ts +5 -1
  94. package/dist/types/components/button/button.recipe.d.ts +18 -5
  95. package/dist/types/components/datatable/datatable.d.ts +9 -0
  96. package/dist/types/components/dialog/dialog.d.ts +25 -2
  97. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +43 -3
  98. package/dist/types/components/tooltip/tooltip.d.ts +12 -4
  99. package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
  100. package/dist/types/product/authshell/authshell.d.ts +26 -2
  101. package/dist/types/product/authshell/index.d.ts +1 -1
  102. package/dist/types/product/board/board.d.ts +5 -4
  103. package/dist/types/product/comments/comments.d.ts +10 -2
  104. package/dist/types/product/formdialog/formdialog.d.ts +2 -1
  105. package/dist/types/product/navitem/index.d.ts +1 -1
  106. package/dist/types/product/navitem/navitem.d.ts +23 -0
  107. package/dist/types/product/objectsettings/index.d.ts +1 -1
  108. package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
  109. package/dist/types/product/sidebar/sidebar.d.ts +33 -5
  110. package/dist/types/tokens/contract.d.ts +4 -0
  111. package/dist/types/tokens/palette.d.ts +13 -1
  112. package/dist/types/tokens/semantic.d.ts +11 -0
  113. package/package.json +1 -1
@@ -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();
@@ -80,42 +80,83 @@ exports.ATTIO_SIDEBAR_WORKSPACE = {
80
80
  avatarLabel: "T",
81
81
  avatarTone: "cyan",
82
82
  };
83
- /** The measured main nav — the default `items` prop. */
83
+ /** The measured main nav — the default `items` prop.
84
+ *
85
+ * `shortcut` is set only where a live hover produced Attio's "G then
86
+ * <letter>" tooltip (crisp#633, P-108) -- Calls/Sequences/Workflows showed
87
+ * no tooltip at all after a 1.2s hover, tried repeatedly, so they're left
88
+ * without one rather than guessed. */
84
89
  exports.ATTIO_SIDEBAR_ITEMS = [
85
- { label: "Home", icon: IconHome },
90
+ {
91
+ label: "Home",
92
+ icon: IconHome,
93
+ shortcut: { description: "Go to home page", keys: ["g", "h"] },
94
+ },
86
95
  {
87
96
  label: "Tasks",
88
97
  icon: IconTasks,
89
98
  active: true,
90
99
  trailing: ((0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: "brand", variant: "solid", size: "small", children: "1" })),
100
+ shortcut: { description: "Go to tasks page", keys: ["g", "t"] },
101
+ },
102
+ {
103
+ label: "Notes",
104
+ icon: IconNotes,
105
+ shortcut: { description: "Go to all notes page", keys: ["g", "n"] },
106
+ },
107
+ {
108
+ label: "Emails",
109
+ icon: IconEmails,
110
+ shortcut: { description: "Go to email drafts page", keys: ["g", "e"] },
91
111
  },
92
- { label: "Notes", icon: IconNotes },
93
- { label: "Emails", icon: IconEmails },
94
112
  { label: "Calls", icon: IconCalls },
95
- { label: "Reports", icon: IconReports },
113
+ {
114
+ label: "Reports",
115
+ icon: IconReports,
116
+ shortcut: { description: "Go to reports page", keys: ["g", "d"] },
117
+ },
96
118
  { label: "Sequences", icon: IconSequences },
97
119
  { label: "Workflows", icon: IconWorkflows },
98
120
  ];
99
- /** The measured Records section — the default `recordsSection` prop. */
121
+ /** The measured Records section — the default `recordsSection` prop. Same
122
+ * `shortcut` caveat as `ATTIO_SIDEBAR_ITEMS`: Deals/Users/Workspaces showed
123
+ * no hover tooltip. `actions` is Attio's own gear + add glyph (P-109),
124
+ * measured aria-labels "Record options" / "Create new objects or enable
125
+ * standard objects". */
100
126
  exports.ATTIO_SIDEBAR_RECORDS_SECTION = {
101
127
  label: "Records",
102
128
  items: [
103
- { label: "Companies", tone: "brand", glyph: GlyphCompanies },
104
- { label: "People", tone: "brand", glyph: GlyphPeople },
129
+ {
130
+ label: "Companies",
131
+ tone: "brand",
132
+ glyph: GlyphCompanies,
133
+ shortcut: { description: "Go to all companies page", keys: ["g", "c"] },
134
+ },
135
+ {
136
+ label: "People",
137
+ tone: "brand",
138
+ glyph: GlyphPeople,
139
+ shortcut: { description: "Go to all people page", keys: ["g", "p"] },
140
+ },
105
141
  { label: "Deals", tone: "orange", glyph: GlyphDeals },
106
142
  { label: "Users", tone: "cyan", glyph: GlyphUsers },
107
143
  { label: "Workspaces", tone: "purple", glyph: GlyphWorkspaces },
108
144
  ],
145
+ actions: defaultSectionActions("Record options", "Create new objects or enable standard objects"),
109
146
  };
110
- /** The measured Lists section — the default `listsSection` prop. */
147
+ /** The measured Lists section — the default `listsSection` prop. `actions`
148
+ * per P-109, measured aria-labels "List options" / "Add list". */
111
149
  exports.ATTIO_SIDEBAR_LISTS_SECTION = {
112
150
  label: "Lists",
113
151
  newListLabel: "+ New list",
152
+ actions: defaultSectionActions("List options", "Add list"),
114
153
  };
115
- /** The measured Chats section — the default `chatsSection` prop. */
154
+ /** The measured Chats section — the default `chatsSection` prop. `actions`
155
+ * per P-109, measured aria-labels "Chat options" / "New chat". */
116
156
  exports.ATTIO_SIDEBAR_CHATS_SECTION = {
117
157
  label: "Chats",
118
158
  items: [{ label: "In-app notification for deal wins", icon: IconChat }],
159
+ actions: defaultSectionActions("Chat options", "New chat"),
119
160
  };
120
161
  /** The measured footer copy — the default `footer` prop. */
121
162
  exports.ATTIO_SIDEBAR_FOOTER = {
@@ -124,8 +165,20 @@ exports.ATTIO_SIDEBAR_FOOTER = {
124
165
  trialText: "13 days left on trial!",
125
166
  trialActionLabel: "Keep Pro",
126
167
  };
127
- function SectionHeader({ label, open, onToggle, }) {
128
- return ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-section-header", "data-collapsed": open ? undefined : "", "aria-expanded": open, onClick: onToggle, children: [(0, jsx_runtime_1.jsx)(IconChevronMini, {}), (0, jsx_runtime_1.jsx)("span", { children: label })] }));
168
+ function SectionHeader({ label, open, onToggle, actions, }) {
169
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-section-header-row", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-section-header", "data-collapsed": open ? undefined : "", "aria-expanded": open, onClick: onToggle, children: [(0, jsx_runtime_1.jsx)(IconChevronMini, {}), (0, jsx_runtime_1.jsx)("span", { children: label })] }), actions != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section-actions", children: actions }))] }));
170
+ }
171
+ /** Attio's own two hover-only header actions (crisp#633, P-109), measured on
172
+ * the Records/Lists/Chats section headers: a gear ("<Section> options") and
173
+ * an add glyph -- re-measured on all three, the add icon is the SAME glyph
174
+ * (`icon-defs/cab3218f7a08cd13.svg`) reused everywhere, not a per-section
175
+ * one, so `PackagePlus` covers Lists/Chats too, not just Records' own
176
+ * "Create new objects" link. Both are decorative defaults with no handler,
177
+ * the same trade a consumer already makes with `footer`'s default
178
+ * "Getting started"/"Keep Pro" buttons — a consumer wanting the real
179
+ * behaviour passes its own `actions` node instead. */
180
+ function defaultSectionActions(optionsLabel, addLabel) {
181
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-sidebar-section-action", "aria-label": optionsLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Settings, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-sidebar-section-action", "aria-label": addLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PackagePlus, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }));
129
182
  }
130
183
  exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = exports.ATTIO_SIDEBAR_WORKSPACE, workspaceMenu, items = exports.ATTIO_SIDEBAR_ITEMS, recordsSection = exports.ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = exports.ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = exports.ATTIO_SIDEBAR_CHATS_SECTION, groups, footer = exports.ATTIO_SIDEBAR_FOOTER, commandItems, commandOpen: commandOpenProp, defaultCommandOpen, onCommandOpenChange, commandShortcut = true, "aria-label": ariaLabel = "Main", ...props }, ref) => {
131
184
  // Seeded once, on mount, from whatever groups start collapsed — not
@@ -174,14 +227,14 @@ exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, coll
174
227
  });
175
228
  }, []);
176
229
  const workspaceTrigger = ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-ws", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), (0, jsx_runtime_1.jsx)(IconChevronDown, {})] }));
177
- return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.nav, { ref: ref, asChild: asChild, "aria-label": ariaLabel, className: (0, sidebar_recipe_1.sidebarRecipe)({ className }), ...props, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-ws-row", children: [workspaceMenu ? ((0, jsx_runtime_1.jsx)(menu_1.Menu, { trigger: workspaceTrigger, className: "crisp-sidebar-ws-menuroot", contentClassName: "crisp-sidebar-ws-menu-content", children: workspaceMenu })) : (workspaceTrigger), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: (0, jsx_runtime_1.jsx)(IconCollapsePanel, {}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-searchrow", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-label", children: searchLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: (0, jsx_runtime_1.jsx)(IconInbox, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing, href, onSelect }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: href ?? "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, onClick: onSelect, children: label }, href ?? label))) }), groups ? (
230
+ return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.nav, { ref: ref, asChild: asChild, "aria-label": ariaLabel, className: (0, sidebar_recipe_1.sidebarRecipe)({ className }), ...props, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-ws-row", children: [workspaceMenu ? ((0, jsx_runtime_1.jsx)(menu_1.Menu, { trigger: workspaceTrigger, className: "crisp-sidebar-ws-menuroot", contentClassName: "crisp-sidebar-ws-menu-content", children: workspaceMenu })) : (workspaceTrigger), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: (0, jsx_runtime_1.jsx)(IconCollapsePanel, {}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-searchrow", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-label", children: searchLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: (0, jsx_runtime_1.jsx)(IconInbox, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing, href, onSelect, shortcut, }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: href ?? "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, onClick: onSelect, shortcut: shortcut, children: label }, href ?? label))) }), groups ? (
178
231
  // #325: navigation as data, reusing the exact section markup/
179
232
  // recipe above — SectionHeader + .crisp-sidebar-section — instead
180
233
  // of the three fixed Attio sections.
181
234
  groups.map((group) => {
182
235
  const groupKey = group.key ?? group.label;
183
- return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [(0, jsx_runtime_1.jsx)(SectionHeader, { label: group.label, open: !collapsedSections.has(groupKey), onToggle: () => toggleSection(groupKey) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has(groupKey) ? "" : undefined, children: group.items.map((item) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: item.href ?? "#", active: item.active, icon: item.icon ? (0, jsx_runtime_1.jsx)(item.icon, {}) : undefined, trailing: item.count, onClick: item.onSelect, children: item.label }, item.href ?? item.label))) })] }, groupKey));
184
- })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", shape: "rounded", tone: tone, children: (0, jsx_runtime_1.jsx)(Glyph, {}) }), children: label }, label))) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-sidebar-newlist", children: listsSection.newListLabel }) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon, active, trailing, href, onSelect, }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: href ?? "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, onClick: onSelect, children: label }, href ?? label))) })] })), footer !== false &&
236
+ return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [(0, jsx_runtime_1.jsx)(SectionHeader, { label: group.label, open: !collapsedSections.has(groupKey), onToggle: () => toggleSection(groupKey), actions: group.actions }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has(groupKey) ? "" : undefined, children: group.items.map((item) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: item.href ?? "#", active: item.active, icon: item.icon ? (0, jsx_runtime_1.jsx)(item.icon, {}) : undefined, trailing: item.count, onClick: item.onSelect, shortcut: item.shortcut, children: item.label }, item.href ?? item.label))) })] }, groupKey));
237
+ })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records"), actions: recordsSection.actions }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph, shortcut }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", shape: "rounded", tone: tone, children: (0, jsx_runtime_1.jsx)(Glyph, {}) }), shortcut: shortcut, children: label }, label))) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists"), actions: listsSection.actions }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-sidebar-newlist", children: listsSection.newListLabel }) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats"), actions: chatsSection.actions }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon, active, trailing, href, onSelect, shortcut, }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: href ?? "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, onClick: onSelect, shortcut: shortcut, children: label }, href ?? label))) })] })), footer !== false &&
185
238
  (isSidebarFooterCopy(footer) ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-footer", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", className: "crisp-sidebar-pill", children: footer.gettingStartedLabel }), (0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(IconInvite, {}), children: footer.inviteLabel }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-trial", children: [(0, jsx_runtime_1.jsx)("span", { children: footer.trialText }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] })) : ((0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-footer", children: footer }))), (0, jsx_runtime_1.jsx)(command_1.Command, { open: commandOpen, onOpenChange: setCommandOpen, items: commandItems })] }));
186
239
  });
187
240
  exports.Sidebar.displayName = "Sidebar";
@@ -19,9 +19,14 @@ const config = (0, core_1.defineRecipe)({
19
19
  ; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overflow-y: auto; & .crisp-sidebar-section { display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px}; margin-top: ${tokens_1.contract.space["2"]}; }
20
20
  & .crisp-sidebar-section[data-collapsed] { display: none; }
21
21
  & .crisp-sidebar-label { font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["0.5"]}; }
22
- & .crisp-sidebar-section-header { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; width: 100%; margin: 0; border: none; background: none; padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["0.5"]}; font-family: ${tokens_1.contract.font.sans}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; cursor: pointer; }
22
+ & .crisp-sidebar-section-header-row { display: flex; align-items: center; }
23
+ & .crisp-sidebar-section-header { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; flex: 1; min-width: 0; margin: 0; border: none; background: none; padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["0.5"]}; font-family: ${tokens_1.contract.font.sans}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; cursor: pointer; }
23
24
  & .crisp-sidebar-section-header svg { flex-shrink: 0; transition: transform .15s ease; }
24
25
  & .crisp-sidebar-section-header[data-collapsed] svg { transform: rotate(-90deg); }
26
+ & .crisp-sidebar-section-actions { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; flex-shrink: 0; padding-inline-end: ${tokens_1.contract.space["1"]}; opacity: 0; transition: opacity .12s ease; }
27
+ & .crisp-sidebar-section-header-row:hover .crisp-sidebar-section-actions, & .crisp-sidebar-section-header-row:focus-within .crisp-sidebar-section-actions { opacity: 1; }
28
+ & .crisp-sidebar-section-action { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: ${tokens_1.contract.space["5"]}; height: ${tokens_1.contract.space["5"]}; margin: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: transparent; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); font-family: inherit; cursor: pointer; text-decoration: none; transition: background-color 0.14s, color 0.14s; }
29
+ & .crisp-sidebar-section-action:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
25
30
  & .crisp-sidebar-ws-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.header}; padding: 0 ${tokens_1.contract.space["2"]}; }
26
31
  & .crisp-sidebar-ws-menuroot { display: flex; flex: 1; min-width: 0; }
27
32
  & .crisp-sidebar-ws { box-sizing: border-box; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex: 1; min-width: 0; height: 100%; margin: 0; padding: 0; border: none; background: none; border-radius: ${tokens_1.contract.radius.lg}; font-family: inherit; font-size: inherit; color: inherit; text-align: start; cursor: pointer; transition: background-color 0.16s; }
@@ -20,6 +20,15 @@ exports.palette = {
20
20
  // off-limits to toggle, same restriction as `ringField`/`ringSwitch`) —
21
21
  // reuses `line200`'s dark line colour rather than inventing a new one.
22
22
  line150: { light: "#e6e7ea", dark: "#212224" },
23
+ // The filter chip's field/value segments while the condition is
24
+ // incomplete (no value typed yet) — measured live on Attio's own filter
25
+ // chip (`rgb(246,247,247)`), lighter than `gray50` (#f4f4f5, close but not
26
+ // equal). The chip's operator/kebab segments stay `gray00` in the same
27
+ // state; only the two text-content segments carry this muted fill. Dark
28
+ // value not independently re-measured (Attio's dark theme is off-limits to
29
+ // toggle) — reuses `gray50`'s dark value, the nearest existing "muted
30
+ // surface" tone, rather than inventing a third dark gray.
31
+ gray25: { light: "#f6f7f7", dark: "#101112" },
23
32
  ringNeutral: { light: "transparent", dark: "#212224" },
24
33
  // Text-entry fields (Input, Textarea) — measured live on app.attio.com's
25
34
  // Settings page, a real typed field: `inset 0 0 0 1px rgba(0,0,0,.06)`,
@@ -40,6 +49,22 @@ exports.palette = {
40
49
  ringSwitch: { light: "#d9dade", dark: "#212224" },
41
50
  tileActive: { light: "#ffffff", dark: "#101112" },
42
51
  tileActiveShadow: { light: "rgba(0,0,0,0.04)", dark: "rgba(0,0,0,0)" },
52
+ // Dialog's page-dimming backdrop (crisp#677) — measured live behind the
53
+ // Create-attribute FormDialog, both themes: `rgba(0,0,0,.25)` light,
54
+ // `rgba(0,0,0,.55)` dark. `Dialog`'s own backdrop had shipped fully
55
+ // `transparent` (AUTHORING's Lesson 14 measured the command palette has
56
+ // NO dark backdrop and that got generalized to every `Dialog`, which this
57
+ // corrects — the palette is a different surface from a plain content
58
+ // dialog and really does dim the page).
59
+ bgScrim: { light: "rgba(0,0,0,0.25)", dark: "rgba(0,0,0,0.55)" },
60
+ // The discard-confirm's description line (crisp#677, `Dialog`'s new
61
+ // `variant="alert"`) — measured live, both themes, over `popoverBg`: a
62
+ // theme-relative alpha rather than a flat gray, so it stays legible if
63
+ // `popoverBg` itself ever shifts.
64
+ fgAlertDescription: {
65
+ light: "rgba(0,0,0,0.63)",
66
+ dark: "rgba(255,255,255,0.6)",
67
+ },
43
68
  overlayFrost: { light: "#ffffff", dark: "#1c1d1f" },
44
69
  shadowOverlay1: { light: "rgba(28,40,64,0.12)", dark: "rgba(0,0,0,0.48)" },
45
70
  shadowOverlay2: { light: "rgba(28,40,64,0.16)", dark: "rgba(0,0,0,0.64)" },
@@ -116,7 +141,13 @@ exports.palette = {
116
141
  // fg-muted/bg-raised themselves (system-wide, measured from Attio, kept
117
142
  // as-is per build/spec-check.mjs's own note), this token exists only for
118
143
  // Badge's soft-neutral text, so darkening it doesn't move any other surface.
119
- softNeutralText: { light: "#686870", dark: "#9a9aa2" },
144
+ // crisp#696: re-measured live (Standard/System pills, object settings) at
145
+ // rgb(80,81,85) -- darker than the #686870 above, which only ever cleared
146
+ // AA, never matched the reference. Only the light side moves: dark
147
+ // (#9a9aa2) already passed and Attio's own dark theme stays off-limits to
148
+ // toggle, the same restriction this file notes throughout (ringField,
149
+ // ringSwitch, line150).
150
+ softNeutralText: { light: "#505155", dark: "#9a9aa2" },
120
151
  // Badge's purple/role-pill tone (crisp#506) -- measured live off Attio's
121
152
  // own role pill ("Admin", app.attio.com/<ws>/settings/members-and-teams):
122
153
  // background rgb(245,240,255), text rgb(98,56,181), ring rgb(232,221,254)
@@ -11,15 +11,26 @@ exports.semantic = {
11
11
  fgOnTone: "ink",
12
12
  bgSurface: "gray00",
13
13
  bgRaised: "gray50",
14
+ /** A surface lighter than `bgRaised` for content that isn't actionable
15
+ * yet — the filter chip's field/value segments before the condition has
16
+ * a value (see palette.ts's `gray25`). */
17
+ bgSurfaceMuted: "gray25",
14
18
  borderDefault: "line200",
15
19
  borderSubtle: "line100",
16
20
  /** ImportDialog's dashed dropzone border — see palette.ts's `line150`. */
17
21
  borderDropzone: "line150",
22
+ /** A hairline between adjacent same-height segments (the filter chip's
23
+ * field/operator/value/kebab boxes) — same measured hex as
24
+ * `borderDropzone` (`line150`), aliased under its own name because the
25
+ * two consumers share nothing but the colour. */
26
+ borderDivider: "line150",
18
27
  ringNeutral: "ringNeutral",
19
28
  ringField: "ringField",
20
29
  ringSwitch: "ringSwitch",
21
30
  bgTileActive: "tileActive",
22
31
  shadowTileActive: "tileActiveShadow",
32
+ bgScrim: "bgScrim",
33
+ fgAlertDescription: "fgAlertDescription",
23
34
  bgOverlayFrost: "overlayFrost",
24
35
  shadowOverlay1: "shadowOverlay1",
25
36
  shadowOverlay2: "shadowOverlay2",
package/dist/comments.css CHANGED
@@ -30,31 +30,50 @@
30
30
  & .crisp-comments-breadcrumb { display: flex; align-items: center; gap: var(--crisp-space-1); font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
31
31
  & .crisp-comments-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-4); }
32
32
  & .crisp-comments-body--pad-top { padding-top: var(--crisp-space-3); }
33
- /* One comment row -- avatar 16px (Avatar size="sm"), 6px gap to the name/time line, 14px/600 name + 12px/500 63%-muted time, 14px/400 body two lines below. Hover actions fade in flush with the name line. */
33
+ /* One comment row -- avatar 16px (Avatar size="sm"), 6px gap to the name/time line, 14px/600 name + 12px/500 63%-muted time, 14px/400 body two lines below. Hover actions fade in flush with the name line. .crisp-comments-row-head's own height: 20px (above) is a single
34
+ text-line's worth, on the same assumption .crisp-comments-title/ .crisp-comments-breadcrumb already act on higher up in this same recipe -- a name that doesn't fit wraps to two lines with nothing
35
+ reserving room for the second one, overflowing straight into the
36
+ body text underneath (found live: this app's own dev-bypass author name, an email long enough to wrap, sat on top of the comment's own
37
+ body text). .crisp-comments-author gets the identical nowrap-
38
+ ellipsis treatment those two already have, plus min-width: 0 so a
39
+ flex item is actually allowed to shrink below its own text's natural width in the first place; .crisp-comments-time gets flex-shrink: 0 so it's the name that gives, never the timestamp -- the same "one
40
+ side shrinks, the other one doesn't" split PersonChip's own name/
41
+ avatar pair already uses (crisp#668/#671). */
34
42
  & .crisp-comments-row { position: relative; display: flex; gap: var(--crisp-space-1-5); }
35
43
  & .crisp-comments-row-main { flex: 1; min-width: 0; }
36
44
  & .crisp-comments-row-head { display: flex; align-items: baseline; gap: var(--crisp-space-1-5); height: 20px; }
37
- & .crisp-comments-author { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
38
- & .crisp-comments-time { font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); white-space: nowrap; }
45
+ & .crisp-comments-author { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
46
+ & .crisp-comments-time { flex-shrink: 0; font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); white-space: nowrap; }
39
47
  & .crisp-comments-resolved-label { display: flex; align-items: center; gap: var(--crisp-space-1); margin-bottom: var(--crisp-space-1); font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
40
48
  & .crisp-comments-body-text { font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-regular); color: var(--crisp-fg-primary); white-space: pre-wrap; word-break: break-word; }
41
- /* Hover actions -- measured: 4 buttons, 28x28, radius 8, flush side-by-side (0 gap), transparent by default, 16px icon at 55% ink. */
49
+
50
+ /* Hover actions -- measured: 4 buttons, 28x28, radius 8, flush
51
+ side-by-side (0 gap), transparent by default, 16px icon at 55% ink. */
42
52
  & .crisp-comments-row-actions { position: absolute; top: 0; inset-inline-end: 0; display: flex; opacity: 0; pointer-events: none; background: var(--crisp-bg-surface); }
43
- & .crisp-comments-row:hover .crisp-comments-row-actions, & .crisp-comments-row-actions:focus-within { opacity: 1; pointer-events: auto; }
53
+ & .crisp-comments-row:hover .crisp-comments-row-actions,
54
+ & .crisp-comments-row-actions:focus-within { opacity: 1; pointer-events: auto; }
44
55
  & .crisp-comments-action { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: transparent; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
45
56
  & .crisp-comments-action:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
46
57
  & .crisp-comments-action[data-active] { color: var(--crisp-bg-brand-solid); }
47
- /* Reactions -- measured: 28px pill, radius 8, subtle 4% ink fill, padding 4px 8px 4px 6px, gap 4px between the emoji glyph and count. */
58
+
59
+ /* Reactions -- measured: 28px pill, radius 8, subtle 4% ink fill,
60
+ padding 4px 8px 4px 6px, gap 4px between the emoji glyph and count. */
48
61
  & .crisp-comments-reactions { display: flex; flex-wrap: wrap; gap: var(--crisp-space-1); margin-top: var(--crisp-space-1-5); }
49
62
  & .crisp-comments-reaction { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: 28px; padding: 4px 8px 4px 6px; border: none; border-radius: var(--crisp-radius-md); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); font-family: inherit; font-size: 12px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); cursor: pointer; }
50
63
  & .crisp-comments-reaction[data-reacted] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 12%, transparent); color: var(--crisp-bg-brand-solid); }
51
64
  & .crisp-comments-reaction-add { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: none; border-radius: var(--crisp-radius-md); background: transparent; font-family: inherit; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; opacity: 0; transition: opacity .12s, background-color .12s; }
52
65
  & .crisp-comments-row:hover .crisp-comments-reaction-add { opacity: 1; }
53
66
  & .crisp-comments-reaction-add:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
67
+
54
68
  /* Reply-in-thread view -- an indented block of replies below the root. */
55
69
  & .crisp-comments-replies { display: flex; flex-direction: column; gap: var(--crisp-space-4); margin-top: var(--crisp-space-3); padding-inline-start: var(--crisp-space-4); border-inline-start: 1px solid var(--crisp-border-subtle); }
70
+
56
71
  & .crisp-comments-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--crisp-space-1); padding: var(--crisp-space-6) var(--crisp-space-4); text-align: center; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-size: var(--crisp-text-lg); }
57
- /* Composer -- measured: 14px/20px body text, 16px gap above the reply/mention icon row, Comment button 84x32 padding 6px 10px radius 9 (= radius.lg) brand-solid fill, 14px/500 white label, opacity .4 disabled / 1 enabled (no color/background change between the two). */
72
+
73
+ /* Composer -- measured: 14px/20px body text, 16px gap above the
74
+ reply/mention icon row, Comment button 84x32 padding 6px 10px radius
75
+ 9 (= radius.lg) brand-solid fill, 14px/500 white label, opacity .4
76
+ disabled / 1 enabled (no color/background change between the two). */
58
77
  & .crisp-comments-composer { flex-shrink: 0; padding: var(--crisp-space-3) var(--crisp-space-4) var(--crisp-space-4); border-top: 1px solid var(--crisp-border-subtle); }
59
78
  & .crisp-comments-field { min-height: 20px; max-height: 120px; overflow-y: auto; font-family: inherit; font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-regular); color: var(--crisp-fg-primary); outline: none; white-space: pre-wrap; word-break: break-word; }
60
79
  & .crisp-comments-field:focus-visible { outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: 2px; }
@@ -64,15 +83,22 @@
64
83
  & .crisp-comments-tool { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; color: color-mix(in srgb, var(--crisp-fg-primary) 45%, transparent); font-family: inherit; cursor: pointer; }
65
84
  & .crisp-comments-tool:hover { color: var(--crisp-fg-primary); }
66
85
  /* Same fill/shadow as Button's own intent="primary" (crisp/button.recipe.ts) --
67
- confirmed byte-identical against the live "Comment" button's computed box-shadow, so this reuses that literal rather than re-deriving it. 32px/radius.lg/6px-10px padding has no matching Button `size` yet (24/28/40 are the three that exist), so this stays hand-rolled instead of stretching one of those. */
86
+ confirmed byte-identical against the live "Comment" button's computed
87
+ box-shadow, so this reuses that literal rather than re-deriving it.
88
+ 32px/radius.lg/6px-10px padding has no matching Button `size` yet
89
+ (24/28/40 are the three that exist), so this stays hand-rolled instead
90
+ of stretching one of those. */
68
91
  & .crisp-comments-post { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 6px var(--crisp-space-2-5); border: none; border-radius: var(--crisp-radius-lg); background: var(--crisp-bg-brand-solid); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-on-brand); cursor: pointer; opacity: 1; transition: opacity .12s, background-color .12s; }
69
92
  & .crisp-comments-post:hover:not(:disabled) { background: var(--crisp-bg-brand-solid-pressed); }
70
93
  & .crisp-comments-post:disabled { opacity: .4; cursor: default; }
71
- /* Filter menu -- measured: 220px, radius 12 (radius.xl), 4px padding, elevation.floating -- a plain reuse of crisp's own Menu, so no rules
94
+
95
+ /* Filter menu -- measured: 220px, radius 12 (radius.xl), 4px padding,
96
+ elevation.floating -- a plain reuse of crisp's own Menu, so no rules
72
97
  live here; kept as a comment for the record. */
73
98
 
74
99
  /* An inserted "@mention" chip in the composer -- same shape as
75
- RichTextEditor's own `.crisp-rte-mention` (crisp#428), reused here rather than re-derived since it's the same atomic-chip affordance. */
100
+ RichTextEditor's own `.crisp-rte-mention` (crisp#428), reused here
101
+ rather than re-derived since it's the same atomic-chip affordance. */
76
102
  & .crisp-comments-mention { display: inline; padding: 0 2px; border-radius: 4px; background: color-mix(in srgb, var(--crisp-bg-brand-solid) 12%, transparent); color: var(--crisp-bg-brand-solid); font-weight: var(--crisp-weight-medium); user-select: none; }
77
103
  [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
78
104
  }
@@ -31,31 +31,50 @@
31
31
  & .crisp-comments-breadcrumb { display: flex; align-items: center; gap: var(--crisp-space-1); font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
32
32
  & .crisp-comments-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-4); }
33
33
  & .crisp-comments-body--pad-top { padding-top: var(--crisp-space-3); }
34
- /* One comment row -- avatar 16px (Avatar size="sm"), 6px gap to the name/time line, 14px/600 name + 12px/500 63%-muted time, 14px/400 body two lines below. Hover actions fade in flush with the name line. */
34
+ /* One comment row -- avatar 16px (Avatar size="sm"), 6px gap to the name/time line, 14px/600 name + 12px/500 63%-muted time, 14px/400 body two lines below. Hover actions fade in flush with the name line. .crisp-comments-row-head's own height: 20px (above) is a single
35
+ text-line's worth, on the same assumption .crisp-comments-title/ .crisp-comments-breadcrumb already act on higher up in this same recipe -- a name that doesn't fit wraps to two lines with nothing
36
+ reserving room for the second one, overflowing straight into the
37
+ body text underneath (found live: this app's own dev-bypass author name, an email long enough to wrap, sat on top of the comment's own
38
+ body text). .crisp-comments-author gets the identical nowrap-
39
+ ellipsis treatment those two already have, plus min-width: 0 so a
40
+ flex item is actually allowed to shrink below its own text's natural width in the first place; .crisp-comments-time gets flex-shrink: 0 so it's the name that gives, never the timestamp -- the same "one
41
+ side shrinks, the other one doesn't" split PersonChip's own name/
42
+ avatar pair already uses (crisp#668/#671). */
35
43
  & .crisp-comments-row { position: relative; display: flex; gap: var(--crisp-space-1-5); }
36
44
  & .crisp-comments-row-main { flex: 1; min-width: 0; }
37
45
  & .crisp-comments-row-head { display: flex; align-items: baseline; gap: var(--crisp-space-1-5); height: 20px; }
38
- & .crisp-comments-author { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
39
- & .crisp-comments-time { font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); white-space: nowrap; }
46
+ & .crisp-comments-author { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
47
+ & .crisp-comments-time { flex-shrink: 0; font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); white-space: nowrap; }
40
48
  & .crisp-comments-resolved-label { display: flex; align-items: center; gap: var(--crisp-space-1); margin-bottom: var(--crisp-space-1); font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
41
49
  & .crisp-comments-body-text { font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-regular); color: var(--crisp-fg-primary); white-space: pre-wrap; word-break: break-word; }
42
- /* Hover actions -- measured: 4 buttons, 28x28, radius 8, flush side-by-side (0 gap), transparent by default, 16px icon at 55% ink. */
50
+
51
+ /* Hover actions -- measured: 4 buttons, 28x28, radius 8, flush
52
+ side-by-side (0 gap), transparent by default, 16px icon at 55% ink. */
43
53
  & .crisp-comments-row-actions { position: absolute; top: 0; inset-inline-end: 0; display: flex; opacity: 0; pointer-events: none; background: var(--crisp-bg-surface); }
44
- & .crisp-comments-row:hover .crisp-comments-row-actions, & .crisp-comments-row-actions:focus-within { opacity: 1; pointer-events: auto; }
54
+ & .crisp-comments-row:hover .crisp-comments-row-actions,
55
+ & .crisp-comments-row-actions:focus-within { opacity: 1; pointer-events: auto; }
45
56
  & .crisp-comments-action { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: transparent; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
46
57
  & .crisp-comments-action:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
47
58
  & .crisp-comments-action[data-active] { color: var(--crisp-bg-brand-solid); }
48
- /* Reactions -- measured: 28px pill, radius 8, subtle 4% ink fill, padding 4px 8px 4px 6px, gap 4px between the emoji glyph and count. */
59
+
60
+ /* Reactions -- measured: 28px pill, radius 8, subtle 4% ink fill,
61
+ padding 4px 8px 4px 6px, gap 4px between the emoji glyph and count. */
49
62
  & .crisp-comments-reactions { display: flex; flex-wrap: wrap; gap: var(--crisp-space-1); margin-top: var(--crisp-space-1-5); }
50
63
  & .crisp-comments-reaction { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: 28px; padding: 4px 8px 4px 6px; border: none; border-radius: var(--crisp-radius-md); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); font-family: inherit; font-size: 12px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); cursor: pointer; }
51
64
  & .crisp-comments-reaction[data-reacted] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 12%, transparent); color: var(--crisp-bg-brand-solid); }
52
65
  & .crisp-comments-reaction-add { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: none; border-radius: var(--crisp-radius-md); background: transparent; font-family: inherit; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; opacity: 0; transition: opacity .12s, background-color .12s; }
53
66
  & .crisp-comments-row:hover .crisp-comments-reaction-add { opacity: 1; }
54
67
  & .crisp-comments-reaction-add:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
68
+
55
69
  /* Reply-in-thread view -- an indented block of replies below the root. */
56
70
  & .crisp-comments-replies { display: flex; flex-direction: column; gap: var(--crisp-space-4); margin-top: var(--crisp-space-3); padding-inline-start: var(--crisp-space-4); border-inline-start: 1px solid var(--crisp-border-subtle); }
71
+
57
72
  & .crisp-comments-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--crisp-space-1); padding: var(--crisp-space-6) var(--crisp-space-4); text-align: center; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-size: var(--crisp-text-lg); }
58
- /* Composer -- measured: 14px/20px body text, 16px gap above the reply/mention icon row, Comment button 84x32 padding 6px 10px radius 9 (= radius.lg) brand-solid fill, 14px/500 white label, opacity .4 disabled / 1 enabled (no color/background change between the two). */
73
+
74
+ /* Composer -- measured: 14px/20px body text, 16px gap above the
75
+ reply/mention icon row, Comment button 84x32 padding 6px 10px radius
76
+ 9 (= radius.lg) brand-solid fill, 14px/500 white label, opacity .4
77
+ disabled / 1 enabled (no color/background change between the two). */
59
78
  & .crisp-comments-composer { flex-shrink: 0; padding: var(--crisp-space-3) var(--crisp-space-4) var(--crisp-space-4); border-top: 1px solid var(--crisp-border-subtle); }
60
79
  & .crisp-comments-field { min-height: 20px; max-height: 120px; overflow-y: auto; font-family: inherit; font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-regular); color: var(--crisp-fg-primary); outline: none; white-space: pre-wrap; word-break: break-word; }
61
80
  & .crisp-comments-field:focus-visible { outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: 2px; }
@@ -65,15 +84,22 @@
65
84
  & .crisp-comments-tool { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; color: color-mix(in srgb, var(--crisp-fg-primary) 45%, transparent); font-family: inherit; cursor: pointer; }
66
85
  & .crisp-comments-tool:hover { color: var(--crisp-fg-primary); }
67
86
  /* Same fill/shadow as Button's own intent="primary" (crisp/button.recipe.ts) --
68
- confirmed byte-identical against the live "Comment" button's computed box-shadow, so this reuses that literal rather than re-deriving it. 32px/radius.lg/6px-10px padding has no matching Button `size` yet (24/28/40 are the three that exist), so this stays hand-rolled instead of stretching one of those. */
87
+ confirmed byte-identical against the live "Comment" button's computed
88
+ box-shadow, so this reuses that literal rather than re-deriving it.
89
+ 32px/radius.lg/6px-10px padding has no matching Button `size` yet
90
+ (24/28/40 are the three that exist), so this stays hand-rolled instead
91
+ of stretching one of those. */
69
92
  & .crisp-comments-post { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 6px var(--crisp-space-2-5); border: none; border-radius: var(--crisp-radius-lg); background: var(--crisp-bg-brand-solid); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-on-brand); cursor: pointer; opacity: 1; transition: opacity .12s, background-color .12s; }
70
93
  & .crisp-comments-post:hover:not(:disabled) { background: var(--crisp-bg-brand-solid-pressed); }
71
94
  & .crisp-comments-post:disabled { opacity: .4; cursor: default; }
72
- /* Filter menu -- measured: 220px, radius 12 (radius.xl), 4px padding, elevation.floating -- a plain reuse of crisp's own Menu, so no rules
95
+
96
+ /* Filter menu -- measured: 220px, radius 12 (radius.xl), 4px padding,
97
+ elevation.floating -- a plain reuse of crisp's own Menu, so no rules
73
98
  live here; kept as a comment for the record. */
74
99
 
75
100
  /* An inserted "@mention" chip in the composer -- same shape as
76
- RichTextEditor's own `.crisp-rte-mention` (crisp#428), reused here rather than re-derived since it's the same atomic-chip affordance. */
101
+ RichTextEditor's own `.crisp-rte-mention` (crisp#428), reused here
102
+ rather than re-derived since it's the same atomic-chip affordance. */
77
103
  & .crisp-comments-mention { display: inline; padding: 0 2px; border-radius: 4px; background: color-mix(in srgb, var(--crisp-bg-brand-solid) 12%, transparent); color: var(--crisp-bg-brand-solid); font-weight: var(--crisp-weight-medium); user-select: none; }
78
104
  [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
79
105
  }