@crisp-ui-kit/crisp 0.50.0 → 0.52.0

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 (128) hide show
  1. package/dist/cjs/components/composewindow/composewindow.js +144 -0
  2. package/dist/cjs/components/composewindow/composewindow.recipe.js +45 -0
  3. package/dist/cjs/components/composewindow/index.js +8 -0
  4. package/dist/cjs/components/datatable/datatable.js +224 -43
  5. package/dist/cjs/components/datatable/datatable.recipe.js +37 -3
  6. package/dist/cjs/components/datatable/index.js +6 -0
  7. package/dist/cjs/components/datepickerfield/datepickerfield.js +25 -15
  8. package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +25 -8
  9. package/dist/cjs/components/dialog/dialog.recipe.js +9 -1
  10. package/dist/cjs/components/menu/menu.js +4 -2
  11. package/dist/cjs/components/multiselect/multiselect.recipe.js +14 -8
  12. package/dist/cjs/components/select/select.js +2 -1
  13. package/dist/cjs/components/table/table.recipe.js +12 -3
  14. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +1 -1
  15. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +18 -7
  16. package/dist/cjs/core/caret-rect.js +73 -0
  17. package/dist/cjs/core/cell-date.js +11 -0
  18. package/dist/cjs/core/index.js +8 -1
  19. package/dist/cjs/core/mention.js +32 -0
  20. package/dist/cjs/core/use-mention-trigger.js +124 -0
  21. package/dist/cjs/index.js +1 -0
  22. package/dist/cjs/product/recordpage/recordpage.js +88 -10
  23. package/dist/cjs/product/recordpage/recordpage.recipe.js +30 -0
  24. package/dist/cjs/product/recordpanel/recordpanel.js +11 -29
  25. package/dist/cjs/product/recordpanel/recordpanel.recipe.js +55 -17
  26. package/dist/cjs/product/recordstable/editable-field.js +113 -0
  27. package/dist/cjs/product/recordstable/index.js +4 -1
  28. package/dist/cjs/product/recordstable/recordstable.js +67 -11
  29. package/dist/cjs/product/recordstable/recordstable.recipe.js +9 -1
  30. package/dist/cjs/product/recordstable/recordstable.tag.js +8 -3
  31. package/dist/cjs/product/richtext/index.js +2 -1
  32. package/dist/cjs/product/richtext/richtext.js +440 -23
  33. package/dist/cjs/product/richtext/richtext.recipe.js +59 -9
  34. package/dist/cjs/product/settingsshell/settingsshell.recipe.js +13 -4
  35. package/dist/cjs/product/sidebar/sidebar.js +15 -3
  36. package/dist/cjs/product/sidebar/sidebar.recipe.js +21 -2
  37. package/dist/composewindow.css +24 -0
  38. package/dist/composewindow.d.ts +7 -0
  39. package/dist/composewindow.layered.css +26 -0
  40. package/dist/datatable.css +33 -14
  41. package/dist/datatable.layered.css +33 -14
  42. package/dist/datepickerfield.css +9 -4
  43. package/dist/datepickerfield.layered.css +9 -4
  44. package/dist/dialog.css +2 -1
  45. package/dist/dialog.layered.css +2 -1
  46. package/dist/esm/components/composewindow/composewindow.js +107 -0
  47. package/dist/esm/components/composewindow/composewindow.recipe.js +42 -0
  48. package/dist/esm/components/composewindow/index.js +2 -0
  49. package/dist/esm/components/datatable/datatable.js +226 -45
  50. package/dist/esm/components/datatable/datatable.recipe.js +37 -3
  51. package/dist/esm/components/datatable/index.js +6 -0
  52. package/dist/esm/components/datepickerfield/datepickerfield.js +26 -16
  53. package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +25 -8
  54. package/dist/esm/components/dialog/dialog.recipe.js +9 -1
  55. package/dist/esm/components/menu/menu.js +4 -2
  56. package/dist/esm/components/multiselect/multiselect.recipe.js +14 -8
  57. package/dist/esm/components/select/select.js +2 -1
  58. package/dist/esm/components/table/table.recipe.js +12 -3
  59. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +1 -1
  60. package/dist/esm/components/tabletoolbar/tabletoolbar.js +18 -7
  61. package/dist/esm/core/caret-rect.js +70 -0
  62. package/dist/esm/core/cell-date.js +10 -0
  63. package/dist/esm/core/index.js +4 -1
  64. package/dist/esm/core/mention.js +29 -0
  65. package/dist/esm/core/use-mention-trigger.js +88 -0
  66. package/dist/esm/index.js +1 -0
  67. package/dist/esm/product/recordpage/recordpage.js +89 -11
  68. package/dist/esm/product/recordpage/recordpage.recipe.js +30 -0
  69. package/dist/esm/product/recordpanel/index.js +1 -1
  70. package/dist/esm/product/recordpanel/recordpanel.js +13 -31
  71. package/dist/esm/product/recordpanel/recordpanel.recipe.js +56 -18
  72. package/dist/esm/product/recordstable/editable-field.js +76 -0
  73. package/dist/esm/product/recordstable/index.js +1 -0
  74. package/dist/esm/product/recordstable/recordstable.js +69 -13
  75. package/dist/esm/product/recordstable/recordstable.recipe.js +9 -1
  76. package/dist/esm/product/recordstable/recordstable.tag.js +8 -3
  77. package/dist/esm/product/richtext/index.js +1 -1
  78. package/dist/esm/product/richtext/richtext.js +439 -22
  79. package/dist/esm/product/richtext/richtext.recipe.js +60 -10
  80. package/dist/esm/product/settingsshell/settingsshell.recipe.js +13 -4
  81. package/dist/esm/product/sidebar/sidebar.js +15 -3
  82. package/dist/esm/product/sidebar/sidebar.recipe.js +21 -2
  83. package/dist/multiselect.css +8 -8
  84. package/dist/multiselect.layered.css +8 -8
  85. package/dist/recordpage.css +25 -13
  86. package/dist/recordpage.layered.css +25 -13
  87. package/dist/recordpanel.css +43 -20
  88. package/dist/recordpanel.layered.css +43 -20
  89. package/dist/recordstable.css +9 -2
  90. package/dist/recordstable.layered.css +9 -2
  91. package/dist/richtext.css +31 -5
  92. package/dist/richtext.layered.css +31 -5
  93. package/dist/settingsshell.css +2 -2
  94. package/dist/settingsshell.layered.css +2 -2
  95. package/dist/sidebar.css +7 -2
  96. package/dist/sidebar.layered.css +7 -2
  97. package/dist/styles.css +199 -74
  98. package/dist/styles.layered.css +199 -74
  99. package/dist/table.css +4 -2
  100. package/dist/table.layered.css +4 -2
  101. package/dist/tabletoolbar.css +1 -1
  102. package/dist/tabletoolbar.layered.css +1 -1
  103. package/dist/types/components/composewindow/composewindow.d.ts +80 -0
  104. package/dist/types/components/composewindow/composewindow.recipe.d.ts +3 -0
  105. package/dist/types/components/composewindow/index.d.ts +2 -0
  106. package/dist/types/components/datatable/datatable.d.ts +58 -4
  107. package/dist/types/components/datatable/index.d.ts +1 -1
  108. package/dist/types/components/datepickerfield/datepickerfield.d.ts +18 -4
  109. package/dist/types/components/menu/menu.d.ts +10 -1
  110. package/dist/types/components/select/select.d.ts +6 -1
  111. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +29 -5
  112. package/dist/types/core/caret-rect.d.ts +16 -0
  113. package/dist/types/core/cell-date.d.ts +17 -0
  114. package/dist/types/core/index.d.ts +4 -1
  115. package/dist/types/core/mention.d.ts +27 -0
  116. package/dist/types/core/use-mention-trigger.d.ts +41 -0
  117. package/dist/types/index.d.ts +1 -0
  118. package/dist/types/product/recordpage/recordpage.d.ts +59 -1
  119. package/dist/types/product/recordpanel/index.d.ts +1 -1
  120. package/dist/types/product/recordpanel/recordpanel.d.ts +81 -17
  121. package/dist/types/product/recordstable/editable-field.d.ts +48 -0
  122. package/dist/types/product/recordstable/index.d.ts +1 -0
  123. package/dist/types/product/recordstable/recordstable.d.ts +29 -0
  124. package/dist/types/product/recordstable/recordstable.tag.d.ts +8 -3
  125. package/dist/types/product/richtext/index.d.ts +1 -1
  126. package/dist/types/product/richtext/richtext.d.ts +168 -7
  127. package/dist/types/product/sidebar/sidebar.d.ts +60 -4
  128. package/package.json +6 -1
@@ -3,15 +3,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.settingsshellRecipe = void 0;
4
4
  const core_1 = require("../../core");
5
5
  const tokens_1 = require("../../tokens");
6
- // The Settings layout — measured from a Settings screen: a 256px left nav with a
6
+ // The Settings layout — measured from a Settings screen: a 275px left nav with a
7
7
  // back header, a search field, and grouped nav items (active row raised), beside
8
- // a scrolling content pane. Sits inside whatever height its container gives it.
8
+ // a scrolling content pane. Fills whatever height its container gives it, the
9
+ // way Attio's settings nav runs the container's full height (#435) — the root
10
+ // used to carry only `min-height: 0` (lets it *shrink* inside a flex ancestor)
11
+ // with no `height` of its own, so it sized to its content instead of filling,
12
+ // and the nav's own border-inline-end stopped where the nav content ended
13
+ // rather than running the full height. A consumer (GRC) had to paper over it
14
+ // with an external `h-full` class.
15
+ // 275px, not the 256px this recipe used to claim (#410) — the same width
16
+ // AppShell's own Attio-measured `defaultWidth` already uses for the main
17
+ // sidebar pane; re-measured live at app.attio.com/<workspace>/settings/account.
9
18
  const config = (0, core_1.defineRecipe)({
10
19
  name: "settingsshell",
11
- base: `display: flex; min-height: 0; box-sizing: border-box; font-family: ${tokens_1.contract.font.sans}; color: ${tokens_1.contract.fgPrimary};
20
+ base: `display: flex; height: 100%; min-height: 0; box-sizing: border-box; font-family: ${tokens_1.contract.font.sans}; color: ${tokens_1.contract.fgPrimary};
12
21
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
13
22
 
14
- & .crisp-settingsshell-nav { display: flex; flex-direction: column; width: 256px; flex-shrink: 0; min-height: 0; padding: ${tokens_1.contract.space["3"]}; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; }
23
+ & .crisp-settingsshell-nav { display: flex; flex-direction: column; width: 275px; flex-shrink: 0; min-height: 0; padding: ${tokens_1.contract.space["3"]}; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; }
15
24
  & .crisp-settingsshell-back { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 28px; padding: 0 ${tokens_1.contract.space["1"]}; margin: 0 0 ${tokens_1.contract.space["2"]}; border: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; color: ${tokens_1.contract.fgPrimary}; cursor: pointer; border-radius: ${tokens_1.contract.radius.md}; }
16
25
  & .crisp-settingsshell-back svg { color: ${tokens_1.contract.fgMuted}; }
17
26
  & .crisp-settingsshell-back:hover svg { color: ${tokens_1.contract.fgPrimary}; }
@@ -40,10 +40,20 @@ const React = __importStar(require("react"));
40
40
  const avatar_1 = require("../../components/avatar");
41
41
  const badge_1 = require("../../components/badge");
42
42
  const button_1 = require("../../components/button");
43
+ const menu_1 = require("../../components/menu");
43
44
  const core_1 = require("../../core");
44
45
  const command_1 = require("../command");
45
46
  const navitem_1 = require("../navitem");
46
47
  const sidebar_recipe_1 = require("./sidebar.recipe");
48
+ /** A plain `{ gettingStartedLabel, inviteLabel, trialText, trialActionLabel }`
49
+ * object (not a React element) — the shape `footer` renders as Attio's fixed
50
+ * onboarding/invite/trial copy, as opposed to a caller-supplied override. */
51
+ function isSidebarFooterCopy(value) {
52
+ return (typeof value === "object" &&
53
+ value !== null &&
54
+ !React.isValidElement(value) &&
55
+ "gettingStartedLabel" in value);
56
+ }
47
57
  const IconHome = ({ className }) => ((0, jsx_runtime_1.jsx)(lucide_react_1.House, { className: className, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
48
58
  const IconTasks = ({ className }) => ((0, jsx_runtime_1.jsx)(lucide_react_1.SquareCheck, { className: className, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
49
59
  const IconNotes = ({ className }) => ((0, jsx_runtime_1.jsx)(lucide_react_1.FileText, { className: className, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
@@ -117,7 +127,7 @@ exports.ATTIO_SIDEBAR_FOOTER = {
117
127
  function SectionHeader({ label, open, onToggle, }) {
118
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 })] }));
119
129
  }
120
- exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = exports.ATTIO_SIDEBAR_WORKSPACE, 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) => {
130
+ 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) => {
121
131
  // Seeded once, on mount, from whatever groups start collapsed — not
122
132
  // reactive to `groups` changing later, matching every other piece of
123
133
  // this component's state (recordsSection/listsSection/chatsSection have
@@ -163,13 +173,15 @@ exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, coll
163
173
  return next;
164
174
  });
165
175
  }, []);
166
- 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", 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, {}), (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 }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, children: label }, label))) }), groups ? (
176
+ 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 ? (
167
178
  // #325: navigation as data, reusing the exact section markup/
168
179
  // recipe above — SectionHeader + .crisp-sidebar-section — instead
169
180
  // of the three fixed Attio sections.
170
181
  groups.map((group) => {
171
182
  const groupKey = group.key ?? group.label;
172
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));
173
- })) : ((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 }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), children: label }, label))) })] })), (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)(command_1.Command, { open: commandOpen, onOpenChange: setCommandOpen, items: commandItems })] }));
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 &&
185
+ (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 })] }));
174
186
  });
175
187
  exports.Sidebar.displayName = "Sidebar";
@@ -3,6 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sidebarRecipe = void 0;
4
4
  const core_1 = require("../../core");
5
5
  const tokens_1 = require("../../tokens");
6
+ // The workspace switcher's menu (#415) is 280px wide on app.attio.com --
7
+ // measured live via getComputedStyle on its [role="menu"], not derived from
8
+ // the trigger's own width (which reads 295-296px including its collapse
9
+ // button) or from Menu's generic 200px content-sized min-width. Everything
10
+ // else about the popover -- 4px padding, 12px radius, 32px/8px-radius items,
11
+ // 14px/500 labels, the layered shadow -- already matches Menu's own measured
12
+ // chrome, confirmed against this specific menu rather than assumed from it.
6
13
  const config = (0, core_1.defineRecipe)({
7
14
  name: "sidebar",
8
15
  base: `box-sizing: border-box; display: flex; flex-direction: column; width: 275px; height: 100%; background: ${tokens_1.contract.bgSidebar}
@@ -15,10 +22,12 @@ const config = (0, core_1.defineRecipe)({
15
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; }
16
23
  & .crisp-sidebar-section-header svg { flex-shrink: 0; transition: transform .15s ease; }
17
24
  & .crisp-sidebar-section-header[data-collapsed] svg { transform: rotate(-90deg); }
18
- & .crisp-sidebar-ws { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.header}; padding: 0 ${tokens_1.contract.space["2"]}; border-radius: ${tokens_1.contract.radius.lg}; cursor: pointer; transition: background-color 0.16s; }
25
+ & .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
+ & .crisp-sidebar-ws-menuroot { display: flex; flex: 1; min-width: 0; }
27
+ & .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; }
19
28
  & .crisp-sidebar-ws:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 5%, transparent); }
20
29
  & .crisp-sidebar-ws-name { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
21
- & .crisp-sidebar-ws-collapse { margin-inline-start: auto; }
30
+ & .crisp-sidebar-ws-collapse { flex-shrink: 0; }
22
31
  & .crisp-sidebar-searchrow { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; margin-top: ${tokens_1.contract.space["2.5"]}; padding-inline-end: ${tokens_1.contract.space["2"]}; }
23
32
  & .crisp-sidebar-footer { margin-top: auto; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["0.5"]}; }
24
33
  & .crisp-sidebar-trial { display: flex; align-items: center; justify-content: space-between; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; }
@@ -32,6 +41,16 @@ const config = (0, core_1.defineRecipe)({
32
41
  & .crisp-sidebar-newlist:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
33
42
 
34
43
  ${core_1.rtlFlipCss}
44
+ `,
45
+ // Menu's own floating content is portaled to document.body (#357), so a
46
+ // `&`-nested rule here -- which compiles to a real descendant combinator
47
+ // scoped to .crisp-sidebar -- matches nothing once the popover leaves the
48
+ // sidebar's DOM subtree. Same fix Menu's own recipe already uses: the
49
+ // content's override lives in globalCss instead, keyed off the
50
+ // \`contentClassName\` Menu now hands the workspace switcher's portaled
51
+ // surface (#415).
52
+ globalCss: `
53
+ .crisp-sidebar-ws-menu-content { width: 280px; }
35
54
  `,
36
55
  variants: {},
37
56
  defaultVariants: {},
@@ -0,0 +1,24 @@
1
+ .crisp-composewindow {
2
+ display: contents; font-family: var(--crisp-font-sans);
3
+ & .crisp-composewindow-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; pointer-events: none; }
4
+ & .crisp-composewindow-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh var(--crisp-space-4) var(--crisp-space-4); pointer-events: none; }
5
+ & .crisp-composewindow-content { pointer-events: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-height: 74vh; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-2xl); background: color-mix(in srgb, var(--crisp-bg-overlay-frost) 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: 0 8px 28px -6px var(--crisp-shadow-overlay1), 0 18px 88px -4px var(--crisp-shadow-overlay2), 0 0 1px var(--crisp-shadow-overlay3); opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
6
+ & .crisp-composewindow-content[data-minimized] { visibility: hidden; opacity: 0; }
7
+ & .crisp-composewindow-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); overflow: hidden; }
8
+ & .crisp-composewindow-header { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-2); flex-shrink: 0; height: 52px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
9
+ & .crisp-composewindow-breadcrumb { display: flex; align-items: center; gap: var(--crisp-space-1); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
10
+ & .crisp-composewindow-actions { display: flex; align-items: center; gap: var(--crisp-space-0-5); flex-shrink: 0; }
11
+ & .crisp-composewindow-action-btn { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
12
+ & .crisp-composewindow-action-btn:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
13
+ & .crisp-composewindow-action-btn[data-state="on"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-bg-brand-solid); }
14
+ & .crisp-composewindow-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; padding: var(--crisp-space-5) var(--crisp-space-4); }
15
+ & .crisp-composewindow-content-body { display: flex; flex-direction: column; gap: var(--crisp-space-4); }
16
+ }
17
+
18
+ @media (prefers-reduced-motion: reduce) {
19
+ .crisp-composewindow, .crisp-composewindow * {
20
+ animation-duration: 0.01ms !important;
21
+ animation-iteration-count: 1 !important;
22
+ transition-duration: 0.01ms !important;
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface ComposewindowVariants {
4
+
5
+ }
6
+
7
+ export declare const composewindowRecipe: RecipeFn<{ }>;
@@ -0,0 +1,26 @@
1
+ @layer crisp-components {
2
+ .crisp-composewindow {
3
+ display: contents; font-family: var(--crisp-font-sans);
4
+ & .crisp-composewindow-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; pointer-events: none; }
5
+ & .crisp-composewindow-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh var(--crisp-space-4) var(--crisp-space-4); pointer-events: none; }
6
+ & .crisp-composewindow-content { pointer-events: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-height: 74vh; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-2xl); background: color-mix(in srgb, var(--crisp-bg-overlay-frost) 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: 0 8px 28px -6px var(--crisp-shadow-overlay1), 0 18px 88px -4px var(--crisp-shadow-overlay2), 0 0 1px var(--crisp-shadow-overlay3); opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
7
+ & .crisp-composewindow-content[data-minimized] { visibility: hidden; opacity: 0; }
8
+ & .crisp-composewindow-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); overflow: hidden; }
9
+ & .crisp-composewindow-header { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-2); flex-shrink: 0; height: 52px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
10
+ & .crisp-composewindow-breadcrumb { display: flex; align-items: center; gap: var(--crisp-space-1); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
11
+ & .crisp-composewindow-actions { display: flex; align-items: center; gap: var(--crisp-space-0-5); flex-shrink: 0; }
12
+ & .crisp-composewindow-action-btn { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
13
+ & .crisp-composewindow-action-btn:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
14
+ & .crisp-composewindow-action-btn[data-state="on"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-bg-brand-solid); }
15
+ & .crisp-composewindow-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; padding: var(--crisp-space-5) var(--crisp-space-4); }
16
+ & .crisp-composewindow-content-body { display: flex; flex-direction: column; gap: var(--crisp-space-4); }
17
+ }
18
+
19
+ @media (prefers-reduced-motion: reduce) {
20
+ .crisp-composewindow, .crisp-composewindow * {
21
+ animation-duration: 0.01ms !important;
22
+ animation-iteration-count: 1 !important;
23
+ transition-duration: 0.01ms !important;
24
+ }
25
+ }
26
+ }
@@ -32,10 +32,13 @@
32
32
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
33
33
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
34
34
  & .crisp-datatable-sort { display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; font: inherit; letter-spacing: inherit; color: inherit; text-align: start; cursor: pointer; }
35
- & th[data-align="right"] .crisp-datatable-sort { justify-content: flex-end; padding-inline-end: 0; }
36
- & th[data-align="right"] .crisp-datatable-th-inner { justify-content: flex-end; }
35
+ /* A right-aligned column's header keeps its field-icon + label packed at
36
+ the START, matching its own values only in that the VALUES right-align
37
+ -- Attio's own right-aligned header (measured live: "Twitter follower
38
+ count") keeps the icon+label left and only the trailing column-menu icon sits pinned to the right edge, independent of the label's width
39
+ (#406, reversing #393's own justify-content: flex-end here). The chevron's own margin-inline-start: auto below already pins IT to the
40
+ end of a flex-start row with no override needed -- that's what pins the trailing icon on Attio too. */
37
41
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
38
- & th[data-align="right"] .crisp-datatable-chevron { margin-inline-start: 0; order: -1; }
39
42
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
40
43
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
41
44
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
@@ -58,6 +61,28 @@
58
61
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
59
62
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; vertical-align: middle; }
60
63
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
64
+ /* An async onEdit's in-flight state (#385) -- the field/picker dims and
65
+ stops accepting input rather than disappearing, so the just-typed or
66
+ just-chosen value stays visible while its commit is pending. Same
67
+ treatment Input already gives its own disabled state. */
68
+ & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
69
+ & .crisp-datatable-edit-pending { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); color: var(--crisp-fg-muted); vertical-align: middle; }
70
+ /* The rejection's message, beside the cell's own content -- a sibling in
71
+ the TableCell, never nested inside a column's own render output, so a hover-only wrapper (a Tooltip, say) around a custom render can't hide
72
+ it. Block-level: the row grows to fit it rather than floating over
73
+ the row below, so it can never end up clipped by the scroll
74
+ container's overflow the way a position:fixed escape would need guarding against. */
75
+ & .crisp-datatable-edit-error { display: block; padding: var(--crisp-space-0-5) 0 0; font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
76
+ /* editor: "select"/"switch" -- always live, no separate edit mode, so the cell IS the picker: same chromeless-fill treatment as RecordsTable's own --picker cells (measured there already; DataTable
77
+ shares RecordsTable's grid, Golden Rule 0). */
78
+ & .crisp-datatable-td--picker { padding: 0; }
79
+ & .crisp-datatable-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
80
+ & .crisp-datatable-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; height: 100%; min-height: 0; border-radius: 0; box-shadow: none; padding: 0; }
81
+ & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: var(--crisp-space-2); }
82
+ /* reference picker: a leading 16px avatar per option, plus a trailing "Create X" row when the consumer supplies one (#449) -- same measured values as RecordsTable's own .crisp-recordstable-refoption*. */
83
+ & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
84
+ & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
85
+ & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
61
86
  & .crisp-datatable-empty { padding: var(--crisp-space-8) var(--crisp-space-3); text-align: center; color: var(--crisp-fg-muted); font-size: var(--crisp-text-lg); }
62
87
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
63
88
  & table { table-layout: fixed; }
@@ -69,12 +94,7 @@
69
94
  & .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
70
95
  & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
71
96
  & .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
72
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
73
- & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
74
- & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
75
- & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
76
- & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
77
- & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
97
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); } & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); } & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); } & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); } & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); } & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
78
98
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
79
99
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
80
100
  & .crisp-datatable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
@@ -87,11 +107,10 @@
87
107
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
88
108
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
89
109
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
90
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
91
- hairline doubling as the drop indicator (it tints toward the brand
92
- colour as the active drop target). The affordance + geometry are
93
- measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
94
- pointer-events implementation. */
110
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
111
+ a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
112
+ draggable + dataTransfer, the same one column reorder already uses
113
+ (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
95
114
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
96
115
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
97
116
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -33,10 +33,13 @@
33
33
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
34
34
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
35
35
  & .crisp-datatable-sort { display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; font: inherit; letter-spacing: inherit; color: inherit; text-align: start; cursor: pointer; }
36
- & th[data-align="right"] .crisp-datatable-sort { justify-content: flex-end; padding-inline-end: 0; }
37
- & th[data-align="right"] .crisp-datatable-th-inner { justify-content: flex-end; }
36
+ /* A right-aligned column's header keeps its field-icon + label packed at
37
+ the START, matching its own values only in that the VALUES right-align
38
+ -- Attio's own right-aligned header (measured live: "Twitter follower
39
+ count") keeps the icon+label left and only the trailing column-menu icon sits pinned to the right edge, independent of the label's width
40
+ (#406, reversing #393's own justify-content: flex-end here). The chevron's own margin-inline-start: auto below already pins IT to the
41
+ end of a flex-start row with no override needed -- that's what pins the trailing icon on Attio too. */
38
42
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
39
- & th[data-align="right"] .crisp-datatable-chevron { margin-inline-start: 0; order: -1; }
40
43
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
41
44
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
42
45
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
@@ -59,6 +62,28 @@
59
62
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
60
63
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; vertical-align: middle; }
61
64
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
65
+ /* An async onEdit's in-flight state (#385) -- the field/picker dims and
66
+ stops accepting input rather than disappearing, so the just-typed or
67
+ just-chosen value stays visible while its commit is pending. Same
68
+ treatment Input already gives its own disabled state. */
69
+ & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
70
+ & .crisp-datatable-edit-pending { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); color: var(--crisp-fg-muted); vertical-align: middle; }
71
+ /* The rejection's message, beside the cell's own content -- a sibling in
72
+ the TableCell, never nested inside a column's own render output, so a hover-only wrapper (a Tooltip, say) around a custom render can't hide
73
+ it. Block-level: the row grows to fit it rather than floating over
74
+ the row below, so it can never end up clipped by the scroll
75
+ container's overflow the way a position:fixed escape would need guarding against. */
76
+ & .crisp-datatable-edit-error { display: block; padding: var(--crisp-space-0-5) 0 0; font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
77
+ /* editor: "select"/"switch" -- always live, no separate edit mode, so the cell IS the picker: same chromeless-fill treatment as RecordsTable's own --picker cells (measured there already; DataTable
78
+ shares RecordsTable's grid, Golden Rule 0). */
79
+ & .crisp-datatable-td--picker { padding: 0; }
80
+ & .crisp-datatable-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
81
+ & .crisp-datatable-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; height: 100%; min-height: 0; border-radius: 0; box-shadow: none; padding: 0; }
82
+ & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: var(--crisp-space-2); }
83
+ /* reference picker: a leading 16px avatar per option, plus a trailing "Create X" row when the consumer supplies one (#449) -- same measured values as RecordsTable's own .crisp-recordstable-refoption*. */
84
+ & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
85
+ & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
86
+ & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
62
87
  & .crisp-datatable-empty { padding: var(--crisp-space-8) var(--crisp-space-3); text-align: center; color: var(--crisp-fg-muted); font-size: var(--crisp-text-lg); }
63
88
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
64
89
  & table { table-layout: fixed; }
@@ -70,12 +95,7 @@
70
95
  & .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
71
96
  & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
72
97
  & .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
73
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
74
- & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
75
- & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
76
- & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
77
- & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
78
- & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
98
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); } & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); } & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); } & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); } & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); } & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
79
99
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
80
100
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
81
101
  & .crisp-datatable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
@@ -88,11 +108,10 @@
88
108
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
89
109
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
90
110
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
91
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
92
- hairline doubling as the drop indicator (it tints toward the brand
93
- colour as the active drop target). The affordance + geometry are
94
- measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
95
- pointer-events implementation. */
111
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
112
+ a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
113
+ draggable + dataTransfer, the same one column reorder already uses
114
+ (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
96
115
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
97
116
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
98
117
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -1,10 +1,15 @@
1
1
  .crisp-datepickerfield {
2
2
  display: inline-flex; font-family: var(--crisp-font-sans);
3
- & .crisp-datepickerfield-trigger { display: inline-flex; align-items: center; gap: var(--crisp-space-2); box-sizing: border-box; height: 32px; padding: 0 var(--crisp-space-2-5); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); white-space: nowrap; cursor: pointer; }
4
- & .crisp-datepickerfield-trigger[data-ghost] { background: transparent; box-shadow: none; }
5
- & .crisp-datepickerfield-trigger[data-ghost]:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
6
- & .crisp-datepickerfield-trigger[data-placeholder] { color: var(--crisp-fg-muted); }
3
+ & .crisp-datepickerfield-box { display: inline-flex; align-items: center; box-sizing: border-box; height: 32px; margin: 0; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); }
4
+ & .crisp-datepickerfield-box[data-ghost] { background: transparent; box-shadow: none; }
5
+ & .crisp-datepickerfield-box[data-ghost]:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
6
+ & .crisp-datepickerfield-trigger { display: inline-flex; align-items: center; gap: var(--crisp-space-2); height: 100%; padding: 0 var(--crisp-space-2-5); margin: 0; border: none; background: none; color: inherit; font-family: inherit; font-size: inherit; white-space: nowrap; cursor: pointer; }
7
+ & .crisp-datepickerfield-box[data-placeholder] .crisp-datepickerfield-trigger { color: var(--crisp-fg-muted); }
7
8
  & .crisp-datepickerfield-trigger svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
9
+ /* Reuses TableToolbar's measured filter-chip remove button
10
+ (tabletoolbar.chrome.ts's .crisp-tabletoolbar-fchip-x) -- the same small inline "clear this value" affordance, not a freshly-measured one: no live Attio composition for a DatePickerField-equivalent clear was reachable this round (#394). */
11
+ & .crisp-datepickerfield-clear { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin-inline-end: var(--crisp-space-1); padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
12
+ & .crisp-datepickerfield-clear:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 10%, transparent); color: var(--crisp-fg-primary); }
8
13
  & .crisp-datepickerfield-panel { padding: var(--crisp-space-2); width: 300px; }
9
14
  & .crisp-datepickerfield-presets { display: flex; flex-wrap: wrap; gap: var(--crisp-space-1); margin-top: var(--crisp-space-2); }
10
15
  & .crisp-datepickerfield-preset { height: var(--crisp-size-chip); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-chip); background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-sm); cursor: pointer; }
@@ -1,11 +1,16 @@
1
1
  @layer crisp-components {
2
2
  .crisp-datepickerfield {
3
3
  display: inline-flex; font-family: var(--crisp-font-sans);
4
- & .crisp-datepickerfield-trigger { display: inline-flex; align-items: center; gap: var(--crisp-space-2); box-sizing: border-box; height: 32px; padding: 0 var(--crisp-space-2-5); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); white-space: nowrap; cursor: pointer; }
5
- & .crisp-datepickerfield-trigger[data-ghost] { background: transparent; box-shadow: none; }
6
- & .crisp-datepickerfield-trigger[data-ghost]:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
7
- & .crisp-datepickerfield-trigger[data-placeholder] { color: var(--crisp-fg-muted); }
4
+ & .crisp-datepickerfield-box { display: inline-flex; align-items: center; box-sizing: border-box; height: 32px; margin: 0; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); }
5
+ & .crisp-datepickerfield-box[data-ghost] { background: transparent; box-shadow: none; }
6
+ & .crisp-datepickerfield-box[data-ghost]:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
7
+ & .crisp-datepickerfield-trigger { display: inline-flex; align-items: center; gap: var(--crisp-space-2); height: 100%; padding: 0 var(--crisp-space-2-5); margin: 0; border: none; background: none; color: inherit; font-family: inherit; font-size: inherit; white-space: nowrap; cursor: pointer; }
8
+ & .crisp-datepickerfield-box[data-placeholder] .crisp-datepickerfield-trigger { color: var(--crisp-fg-muted); }
8
9
  & .crisp-datepickerfield-trigger svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
10
+ /* Reuses TableToolbar's measured filter-chip remove button
11
+ (tabletoolbar.chrome.ts's .crisp-tabletoolbar-fchip-x) -- the same small inline "clear this value" affordance, not a freshly-measured one: no live Attio composition for a DatePickerField-equivalent clear was reachable this round (#394). */
12
+ & .crisp-datepickerfield-clear { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin-inline-end: var(--crisp-space-1); padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
13
+ & .crisp-datepickerfield-clear:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 10%, transparent); color: var(--crisp-fg-primary); }
9
14
  & .crisp-datepickerfield-panel { padding: var(--crisp-space-2); width: 300px; }
10
15
  & .crisp-datepickerfield-presets { display: flex; flex-wrap: wrap; gap: var(--crisp-space-1); margin-top: var(--crisp-space-2); }
11
16
  & .crisp-datepickerfield-preset { height: var(--crisp-size-chip); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-chip); background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-sm); cursor: pointer; }
package/dist/dialog.css CHANGED
@@ -15,7 +15,8 @@
15
15
  & .crisp-dialog-field-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
16
16
  & .crisp-dialog-more { display: inline-flex; align-items: center; gap: var(--crisp-space-2); margin-inline-end: auto; border: none; background: none; padding: 0; font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); cursor: pointer; }
17
17
  & .crisp-dialog-submit { display: inline-flex; align-items: center; gap: var(--crisp-space-2); height: 28px; padding: 0 var(--crisp-space-1) 0 var(--crisp-space-2-5); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
18
- & .crisp-dialog-submit-key { display: inline-flex; align-items: center; justify-content: center; height: 18px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: rgba(255,255,255,0.22); font-size: var(--crisp-text-xs); }
18
+ & .crisp-dialog-submit-key { display: inline-flex; align-items: center; gap: var(--crisp-space-1); }
19
+ & .crisp-dialog-submit-key kbd { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0 3px; border-radius: 4px; background: transparent; color: var(--crisp-fg-on-brand); font-family: inherit; font-style: normal; font-size: 10px; }
19
20
  }
20
21
 
21
22
  @media (prefers-reduced-motion: reduce) {
@@ -16,7 +16,8 @@
16
16
  & .crisp-dialog-field-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
17
17
  & .crisp-dialog-more { display: inline-flex; align-items: center; gap: var(--crisp-space-2); margin-inline-end: auto; border: none; background: none; padding: 0; font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); cursor: pointer; }
18
18
  & .crisp-dialog-submit { display: inline-flex; align-items: center; gap: var(--crisp-space-2); height: 28px; padding: 0 var(--crisp-space-1) 0 var(--crisp-space-2-5); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
19
- & .crisp-dialog-submit-key { display: inline-flex; align-items: center; justify-content: center; height: 18px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: rgba(255,255,255,0.22); font-size: var(--crisp-text-xs); }
19
+ & .crisp-dialog-submit-key { display: inline-flex; align-items: center; gap: var(--crisp-space-1); }
20
+ & .crisp-dialog-submit-key kbd { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0 3px; border-radius: 4px; background: transparent; color: var(--crisp-fg-on-brand); font-family: inherit; font-style: normal; font-size: 10px; }
20
21
  }
21
22
 
22
23
  @media (prefers-reduced-motion: reduce) {
@@ -0,0 +1,107 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as dialog from "@zag-js/dialog";
3
+ import { normalizeProps, Portal, useMachine } from "@zag-js/react";
4
+ import { Minus, Pin, X } from "lucide-react";
5
+ import * as React from "react";
6
+ import { cx, withDescribedBy } from "../../core/index.js";
7
+ import { composewindowRecipe } from "./composewindow.recipe.js";
8
+ // A globally unique id per mounted instance — see command.tsx/dialog.tsx for
9
+ // the rationale (the machine defers its dismissal listeners a frame, so a
10
+ // tree-relative id could let a stale registration resolve to a new instance).
11
+ let instanceCounter = 0;
12
+ function useComposeWindowInstanceId() {
13
+ const idRef = React.useRef(null);
14
+ if (idRef.current == null) {
15
+ instanceCounter += 1;
16
+ idRef.current = `crisp-composewindow-${instanceCounter}`;
17
+ }
18
+ return idRef.current;
19
+ }
20
+ /**
21
+ * A floating, minimizable, pinnable window — Attio's own note "compose
22
+ * window" chrome (measured live, read-only, 2026-09-24; see HANDOFF), not a
23
+ * new visual recipe: it reuses `Dialog`'s exact measured frosted-glass panel
24
+ * (`bgOverlayFrost` 60% + `blur(15px)`, `elevation.modal`'s 3-layer shadow,
25
+ * 16px outer radius) — those numbers matched Attio's compose window exactly
26
+ * when re-checked against it, panel-for-panel. What's actually different
27
+ * from `Dialog` is the header (a breadcrumb plus minimize/pin/close, not a
28
+ * title plus close) and two extra pieces of state Dialog has no concept of.
29
+ *
30
+ * `minimized` is measured, not guessed — the FIRST version of this file built
31
+ * an invented "minimized bar" (a Gmail-style dock, since a bar was never
32
+ * actually exercised against Attio). Re-measured properly: minimizing an
33
+ * existing note does not produce any bar, tray, dock, or badge anywhere on
34
+ * screen. Confirmed four independent ways, both with synthetic and real
35
+ * CDP-dispatched trusted clicks — the element count under the note's own
36
+ * subtree drops by ~110 nodes, `[role="dialog"]` stops matching it, its
37
+ * `data-hidden="true"` ancestor computes `visibility: hidden; opacity: 0`,
38
+ * and the URL drops the `?id=…&modal=note` query exactly like Close does —
39
+ * but the subtree is NOT removed: `document.querySelectorAll('[aria-label=
40
+ * "Minimize"]')` still finds it afterward, `visibility: hidden` (not
41
+ * `display: none`) preserves layout, and nothing in localStorage or the DOM
42
+ * suggests state was discarded. So: minimized hides the panel — mounted,
43
+ * `visibility: hidden; opacity: 0` — rather than unmounting or closing it,
44
+ * and crisp renders no restore affordance of its own (Attio has none
45
+ * either); reopening is the consumer's own UI, driven by `minimized`/
46
+ * `onMinimizedChange` the same way `open` already works.
47
+ *
48
+ * "Pinned" is implemented as the one directly-supported behavior that
49
+ * matches its name (Zag's `closeOnInteractOutside`, off while pinned) —
50
+ * Attio's own multi-instance stacking/docking when pinned was not exercised,
51
+ * so nothing here repositions the window when pinned.
52
+ *
53
+ * Not modal: Attio's own compose window dims nothing behind it (`Dialog`'s
54
+ * backdrop is `background: transparent` too — the blur alone does the
55
+ * separation), and the page behind stays visibly interactive in the
56
+ * measured screenshot. `trapFocus` follows the same call — a picture-in-
57
+ * picture window, not a modal, so Tab is not caught inside it.
58
+ */
59
+ export function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width = 968, "aria-label": ariaLabel, minimizeLabel = "Minimize", pinLabel = "Pin to screen", closeLabel = "Close", children, className, style, "aria-describedby": describedBy, ...rest }) {
60
+ const id = useComposeWindowInstanceId();
61
+ const [internalMinimized, setInternalMinimized] = React.useState(defaultMinimized ?? false);
62
+ const minimized = minimizedProp ?? internalMinimized;
63
+ const setMinimized = React.useCallback((next) => {
64
+ if (minimizedProp === undefined)
65
+ setInternalMinimized(next);
66
+ onMinimizedChange?.(next);
67
+ }, [minimizedProp, onMinimizedChange]);
68
+ const [internalPinned, setInternalPinned] = React.useState(defaultPinned ?? false);
69
+ const pinned = pinnedProp ?? internalPinned;
70
+ const setPinned = React.useCallback((next) => {
71
+ if (pinnedProp === undefined)
72
+ setInternalPinned(next);
73
+ onPinnedChange?.(next);
74
+ }, [pinnedProp, onPinnedChange]);
75
+ const service = useMachine(dialog.machine, {
76
+ id,
77
+ // Always `open` while the consumer says so — NOT `open && !minimized`.
78
+ // Zag's own "closed" state sets `hidden` on the content element, which
79
+ // the browser's UA stylesheet turns into `display: none` — the exact
80
+ // thing minimized must NOT be (Attio's own measured minimized subtree
81
+ // keeps `display: flex`, only `visibility`/`opacity` change). Disabling
82
+ // `closeOnEscape`/`closeOnInteractOutside` below gets the same "stand
83
+ // down while minimized" result without that side effect.
84
+ open,
85
+ onOpenChange(details) {
86
+ // Zag only ever asks to close (Escape / outside pointerdown) — it
87
+ // never asks to open on its own, so this is always a close request.
88
+ if (!details.open)
89
+ onOpenChange(false);
90
+ },
91
+ modal: false,
92
+ trapFocus: false,
93
+ closeOnInteractOutside: !pinned && !minimized,
94
+ closeOnEscape: !minimized,
95
+ "aria-label": ariaLabel,
96
+ });
97
+ const api = dialog.connect(service, normalizeProps);
98
+ if (!open)
99
+ return null;
100
+ return (_jsx(Portal, { children: _jsxs("div", { className: composewindowRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "crisp-composewindow-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "crisp-composewindow-positioner", children: _jsx("div", { ...rest, ...withDescribedBy(api.getContentProps(), describedBy), className: cx("crisp-composewindow-content", className), "data-crisp-composewindow": id, "data-minimized": minimized || undefined, style: { ...style, maxWidth: `min(${width}px, 90vw)` }, children: _jsxs("div", { className: "crisp-composewindow-card", children: [_jsxs("div", { className: "crisp-composewindow-header", children: [_jsx("span", { ...api.getTitleProps(), className: "crisp-composewindow-breadcrumb", children: breadcrumb }), _jsxs("div", { className: "crisp-composewindow-actions", children: [_jsx("button", { type: "button", className: "crisp-composewindow-action-btn", "aria-label": minimizeLabel, onClick: () => setMinimized(true), children: _jsx(Minus, { size: 14, strokeWidth: 2, "aria-hidden": "true" }) }), _jsx("button", { type: "button", className: "crisp-composewindow-action-btn", "aria-label": pinLabel, "aria-pressed": pinned, "data-state": pinned ? "on" : "off", onClick: () => setPinned(!pinned), children: _jsx(Pin, { size: 14, strokeWidth: 2, "aria-hidden": "true" }) }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "crisp-composewindow-action-btn", "aria-label": closeLabel, children: _jsx(X, { size: 14, strokeWidth: 2, "aria-hidden": "true" }) })] })] }), _jsx("div", { className: "crisp-composewindow-body", children: children })] }) }) })] }) }));
101
+ }
102
+ ComposeWindow.displayName = "ComposeWindow";
103
+ export function ComposeWindowBody({ children, className, }) {
104
+ return (_jsx("div", { className: className
105
+ ? `crisp-composewindow-content-body ${className}`
106
+ : "crisp-composewindow-content-body", children: children }));
107
+ }