@crisp-ui-kit/crisp 0.57.2 → 0.57.4

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 (200) hide show
  1. package/dist/assigneepicker.css +13 -0
  2. package/dist/assigneepicker.d.ts +7 -0
  3. package/dist/assigneepicker.layered.css +15 -0
  4. package/dist/button.css +3 -0
  5. package/dist/button.d.ts +2 -2
  6. package/dist/button.layered.css +3 -0
  7. package/dist/chip.css +1 -1
  8. package/dist/chip.layered.css +1 -1
  9. package/dist/cjs/components/breadcrumb/breadcrumb.js +1 -1
  10. package/dist/cjs/components/button/button.recipe.js +33 -0
  11. package/dist/cjs/components/chip/chip.recipe.js +3 -1
  12. package/dist/cjs/components/datatable/datatable.js +6 -3
  13. package/dist/cjs/components/datatable/datatable.recipe.js +45 -6
  14. package/dist/cjs/components/datepickerfield/datepickerfield.js +2 -2
  15. package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +16 -3
  16. package/dist/cjs/components/menu/menu.js +65 -9
  17. package/dist/cjs/components/multiselect/multiselect.js +3 -0
  18. package/dist/cjs/components/multiselect/multiselect.recipe.js +18 -0
  19. package/dist/cjs/components/select/select.js +2 -2
  20. package/dist/cjs/components/select/select.recipe.js +19 -0
  21. package/dist/cjs/components/table/table.recipe.js +7 -1
  22. package/dist/cjs/core/floating-portal.js +7 -4
  23. package/dist/cjs/core/index.js +2 -1
  24. package/dist/cjs/core/place-floating.js +38 -0
  25. package/dist/cjs/index.js +8 -0
  26. package/dist/cjs/product/assigneepicker/assigneepicker.js +127 -0
  27. package/dist/cjs/product/assigneepicker/assigneepicker.recipe.js +36 -0
  28. package/dist/cjs/product/assigneepicker/index.js +7 -0
  29. package/dist/cjs/product/inbox/inbox.js +11 -3
  30. package/dist/cjs/product/notesgallery/index.js +7 -0
  31. package/dist/cjs/product/notesgallery/notesgallery.js +80 -0
  32. package/dist/cjs/product/notesgallery/notesgallery.recipe.js +46 -0
  33. package/dist/cjs/product/objectpagemenu/index.js +17 -0
  34. package/dist/cjs/product/objectpagemenu/objectpagemenu.js +26 -0
  35. package/dist/cjs/product/objectpagemenu/objectpagemenu.recipe.js +30 -0
  36. package/dist/cjs/product/objectsettings/objectsettings.js +107 -14
  37. package/dist/cjs/product/recordpage/index.js +2 -1
  38. package/dist/cjs/product/recordpage/recordpage.js +112 -7
  39. package/dist/cjs/product/recordpage/recordpage.recipe.js +63 -0
  40. package/dist/cjs/product/recordpicker/index.js +7 -0
  41. package/dist/cjs/product/recordpicker/recordpicker.js +100 -0
  42. package/dist/cjs/product/recordpicker/recordpicker.recipe.js +36 -0
  43. package/dist/cjs/product/savedviews/index.js +2 -1
  44. package/dist/cjs/product/savedviews/savedviews.js +22 -0
  45. package/dist/cjs/product/savedviews/savedviews.recipe.js +47 -0
  46. package/dist/cjs/product/share/index.js +7 -0
  47. package/dist/cjs/product/share/share.js +179 -0
  48. package/dist/cjs/product/share/share.recipe.js +114 -0
  49. package/dist/cjs/product/shortcutssheet/index.js +17 -0
  50. package/dist/cjs/product/shortcutssheet/shortcutssheet.js +125 -0
  51. package/dist/cjs/product/shortcutssheet/shortcutssheet.recipe.js +50 -0
  52. package/dist/cjs/product/taskeditpopover/index.js +7 -0
  53. package/dist/cjs/product/taskeditpopover/taskeditpopover.js +159 -0
  54. package/dist/cjs/product/taskeditpopover/taskeditpopover.recipe.js +68 -0
  55. package/dist/cjs/product/workspaceswitchermenu/index.js +17 -0
  56. package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.js +38 -0
  57. package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +34 -0
  58. package/dist/cjs/tokens/palette.js +27 -0
  59. package/dist/cjs/tokens/semantic.js +20 -0
  60. package/dist/datatable.css +50 -11
  61. package/dist/datatable.layered.css +50 -11
  62. package/dist/datepickerfield.css +8 -2
  63. package/dist/datepickerfield.d.ts +2 -2
  64. package/dist/datepickerfield.layered.css +8 -2
  65. package/dist/esm/components/breadcrumb/breadcrumb.js +1 -1
  66. package/dist/esm/components/button/button.recipe.js +33 -0
  67. package/dist/esm/components/chip/chip.recipe.js +3 -1
  68. package/dist/esm/components/datatable/datatable.js +6 -3
  69. package/dist/esm/components/datatable/datatable.recipe.js +45 -6
  70. package/dist/esm/components/datepickerfield/datepickerfield.js +2 -2
  71. package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +16 -3
  72. package/dist/esm/components/menu/menu.js +64 -8
  73. package/dist/esm/components/multiselect/multiselect.js +3 -0
  74. package/dist/esm/components/multiselect/multiselect.recipe.js +18 -0
  75. package/dist/esm/components/select/select.js +2 -2
  76. package/dist/esm/components/select/select.recipe.js +19 -0
  77. package/dist/esm/components/table/table.recipe.js +7 -1
  78. package/dist/esm/core/floating-portal.js +7 -4
  79. package/dist/esm/core/index.js +1 -1
  80. package/dist/esm/core/place-floating.js +37 -0
  81. package/dist/esm/index.js +8 -0
  82. package/dist/esm/product/assigneepicker/assigneepicker.js +91 -0
  83. package/dist/esm/product/assigneepicker/assigneepicker.recipe.js +33 -0
  84. package/dist/esm/product/assigneepicker/index.js +2 -0
  85. package/dist/esm/product/inbox/inbox.js +11 -3
  86. package/dist/esm/product/notesgallery/index.js +2 -0
  87. package/dist/esm/product/notesgallery/notesgallery.js +44 -0
  88. package/dist/esm/product/notesgallery/notesgallery.recipe.js +43 -0
  89. package/dist/esm/product/objectpagemenu/index.js +1 -0
  90. package/dist/esm/product/objectpagemenu/objectpagemenu.js +23 -0
  91. package/dist/esm/product/objectpagemenu/objectpagemenu.recipe.js +27 -0
  92. package/dist/esm/product/objectsettings/objectsettings.js +106 -13
  93. package/dist/esm/product/recordpage/index.js +1 -1
  94. package/dist/esm/product/recordpage/recordpage.js +112 -8
  95. package/dist/esm/product/recordpage/recordpage.recipe.js +63 -0
  96. package/dist/esm/product/recordpicker/index.js +2 -0
  97. package/dist/esm/product/recordpicker/recordpicker.js +64 -0
  98. package/dist/esm/product/recordpicker/recordpicker.recipe.js +33 -0
  99. package/dist/esm/product/savedviews/index.js +1 -1
  100. package/dist/esm/product/savedviews/savedviews.js +22 -1
  101. package/dist/esm/product/savedviews/savedviews.recipe.js +47 -0
  102. package/dist/esm/product/share/index.js +2 -0
  103. package/dist/esm/product/share/share.js +143 -0
  104. package/dist/esm/product/share/share.recipe.js +111 -0
  105. package/dist/esm/product/shortcutssheet/index.js +1 -0
  106. package/dist/esm/product/shortcutssheet/shortcutssheet.js +88 -0
  107. package/dist/esm/product/shortcutssheet/shortcutssheet.recipe.js +47 -0
  108. package/dist/esm/product/taskeditpopover/index.js +2 -0
  109. package/dist/esm/product/taskeditpopover/taskeditpopover.js +123 -0
  110. package/dist/esm/product/taskeditpopover/taskeditpopover.recipe.js +65 -0
  111. package/dist/esm/product/workspaceswitchermenu/index.js +1 -0
  112. package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.js +35 -0
  113. package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +31 -0
  114. package/dist/esm/tokens/palette.js +27 -0
  115. package/dist/esm/tokens/semantic.js +20 -0
  116. package/dist/multiselect.css +18 -0
  117. package/dist/multiselect.layered.css +18 -0
  118. package/dist/native/tokens.d.ts +1 -1
  119. package/dist/native/tokens.js +56 -4
  120. package/dist/notesgallery.css +15 -0
  121. package/dist/notesgallery.d.ts +7 -0
  122. package/dist/notesgallery.layered.css +17 -0
  123. package/dist/objectpagemenu.css +3 -0
  124. package/dist/objectpagemenu.d.ts +7 -0
  125. package/dist/objectpagemenu.layered.css +5 -0
  126. package/dist/recordpage.css +59 -0
  127. package/dist/recordpage.layered.css +59 -0
  128. package/dist/recordpicker.css +13 -0
  129. package/dist/recordpicker.d.ts +7 -0
  130. package/dist/recordpicker.layered.css +15 -0
  131. package/dist/savedviews.css +47 -0
  132. package/dist/savedviews.layered.css +47 -0
  133. package/dist/select.css +19 -0
  134. package/dist/select.layered.css +19 -0
  135. package/dist/share.css +84 -0
  136. package/dist/share.d.ts +7 -0
  137. package/dist/share.layered.css +86 -0
  138. package/dist/shortcutssheet.css +24 -0
  139. package/dist/shortcutssheet.d.ts +7 -0
  140. package/dist/shortcutssheet.layered.css +26 -0
  141. package/dist/styles.css +438 -16
  142. package/dist/styles.layered.css +438 -16
  143. package/dist/table.css +7 -2
  144. package/dist/table.layered.css +7 -2
  145. package/dist/taskeditpopover.css +36 -0
  146. package/dist/taskeditpopover.d.ts +7 -0
  147. package/dist/taskeditpopover.layered.css +38 -0
  148. package/dist/tokens.css +26 -0
  149. package/dist/tokens.json +56 -4
  150. package/dist/types/components/breadcrumb/breadcrumb.d.ts +9 -1
  151. package/dist/types/components/button/button.d.ts +2 -0
  152. package/dist/types/components/button/button.recipe.d.ts +12 -0
  153. package/dist/types/components/chip/chip.recipe.d.ts +1 -1
  154. package/dist/types/components/datatable/datatable.d.ts +8 -0
  155. package/dist/types/components/datepickerfield/datepickerfield.d.ts +4 -1
  156. package/dist/types/components/datepickerfield/datepickerfield.recipe.d.ts +18 -3
  157. package/dist/types/components/menu/menu.d.ts +50 -7
  158. package/dist/types/components/multiselect/multiselect.d.ts +9 -0
  159. package/dist/types/components/select/select.d.ts +10 -0
  160. package/dist/types/core/floating-portal.d.ts +6 -5
  161. package/dist/types/core/index.d.ts +1 -1
  162. package/dist/types/core/place-floating.d.ts +37 -0
  163. package/dist/types/index.d.ts +8 -0
  164. package/dist/types/product/assigneepicker/assigneepicker.d.ts +36 -0
  165. package/dist/types/product/assigneepicker/assigneepicker.recipe.d.ts +3 -0
  166. package/dist/types/product/assigneepicker/index.d.ts +2 -0
  167. package/dist/types/product/inbox/inbox.d.ts +13 -1
  168. package/dist/types/product/notesgallery/index.d.ts +2 -0
  169. package/dist/types/product/notesgallery/notesgallery.d.ts +54 -0
  170. package/dist/types/product/notesgallery/notesgallery.recipe.d.ts +3 -0
  171. package/dist/types/product/objectpagemenu/index.d.ts +1 -0
  172. package/dist/types/product/objectpagemenu/objectpagemenu.d.ts +40 -0
  173. package/dist/types/product/objectpagemenu/objectpagemenu.recipe.d.ts +17 -0
  174. package/dist/types/product/objectsettings/objectsettings.d.ts +71 -15
  175. package/dist/types/product/recordpage/index.d.ts +1 -1
  176. package/dist/types/product/recordpage/recordpage.d.ts +142 -1
  177. package/dist/types/product/recordpicker/index.d.ts +2 -0
  178. package/dist/types/product/recordpicker/recordpicker.d.ts +38 -0
  179. package/dist/types/product/recordpicker/recordpicker.recipe.d.ts +3 -0
  180. package/dist/types/product/savedviews/index.d.ts +1 -1
  181. package/dist/types/product/savedviews/savedviews.d.ts +36 -0
  182. package/dist/types/product/share/index.d.ts +2 -0
  183. package/dist/types/product/share/share.d.ts +85 -0
  184. package/dist/types/product/share/share.recipe.d.ts +3 -0
  185. package/dist/types/product/shortcutssheet/index.d.ts +1 -0
  186. package/dist/types/product/shortcutssheet/shortcutssheet.d.ts +49 -0
  187. package/dist/types/product/shortcutssheet/shortcutssheet.recipe.d.ts +3 -0
  188. package/dist/types/product/taskeditpopover/index.d.ts +2 -0
  189. package/dist/types/product/taskeditpopover/taskeditpopover.d.ts +67 -0
  190. package/dist/types/product/taskeditpopover/taskeditpopover.recipe.d.ts +3 -0
  191. package/dist/types/product/workspaceswitchermenu/index.d.ts +1 -0
  192. package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.d.ts +54 -0
  193. package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.recipe.d.ts +19 -0
  194. package/dist/types/tokens/contract.d.ts +13 -0
  195. package/dist/types/tokens/palette.d.ts +52 -0
  196. package/dist/types/tokens/semantic.d.ts +20 -0
  197. package/dist/workspaceswitchermenu.css +4 -0
  198. package/dist/workspaceswitchermenu.d.ts +7 -0
  199. package/dist/workspaceswitchermenu.layered.css +6 -0
  200. package/package.json +41 -1
@@ -124,6 +124,19 @@ const config = defineRecipe({
124
124
  the component's own comment) -- NOT \`display: contents\`, which
125
125
  drops a button's accessible semantics in some engines. */
126
126
  & .crisp-recordpage-edit-trigger { display: inline-flex; flex-wrap: wrap; align-items: center; gap: ${c.space["1"]}; margin: 0; padding: 0; border: none; background: none; text-align: start; font: inherit; color: inherit; cursor: pointer; }
127
+ /* #990: measured live on app.attio.com's URL/domain attribute (hover the
128
+ row) -- the link itself never carries the edit affordance, a
129
+ separate 20px button trails it, mounting only on hover: 16px icon,
130
+ rgba(0,0,0,0.55) (crisp's own color-mix(fgPrimary 55%) -- the exact
131
+ formula .crisp-datatable-entity-action's identical muted icon
132
+ already uses), 6px radius (radius.sm). Reveal is opacity-based,
133
+ not the mount/unmount Attio's own DOM does, so a keyboard user
134
+ tabbing to it (:focus-visible) doesn't need the row hovered first --
135
+ same mechanism DataTable's own row actions (#601) already use. */
136
+ & .crisp-recordpage-edit-affordance { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.sm}; background: none; font-family: inherit; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); cursor: pointer; opacity: 0; transition: opacity .12s, background-color .12s, color .12s; }
137
+ & .crisp-recordpage-field:hover .crisp-recordpage-edit-affordance,
138
+ & .crisp-recordpage-edit-affordance:focus-visible { opacity: 1; }
139
+ & .crisp-recordpage-edit-affordance:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
127
140
  & .crisp-recordpage-empty { color: ${c.fgMuted}; }
128
141
 
129
142
  /* #431: measured live on app.attio.com's record page Details column --
@@ -226,6 +239,31 @@ const config = defineRecipe({
226
239
  & .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: ${c.space["1.5"]}; padding: ${c.space["6"]} ${c.space["3"]}; border: 1px dashed ${c.borderDefault}; border-radius: ${c.radius.xl}; color: ${c.fgMuted}; font-size: ${c.text.md}; text-align: center; }
227
240
  & .crisp-recordpage-emptycard svg { color: ${c.fgMuted}; opacity: 0.7; }
228
241
 
242
+ /* #622: the Activity tab's own toolbar -- "Filters" + "+ Add meeting",
243
+ both the same measured button chrome as the topbar's own action
244
+ buttons (28px, e.field shadow, 14/500 label). */
245
+ & .crisp-recordpage-tabtoolbar { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; }
246
+
247
+ /* #622: Files tab's empty state -- measured live on Attio's own record
248
+ page (a company with no files uploaded). The 138x138 dotted-grid
249
+ backdrop is the same measured tile EmptyState's records variant uses
250
+ (crisp/#603), duplicated here rather than composed through that
251
+ component because this surface's title (16/600) and body (14/500,
252
+ 63%-mixed fgPrimary, not the flat fgMuted EmptyState uses) measure to
253
+ their own values, matching neither of that component's existing
254
+ variants. */
255
+ & .crisp-recordpage-panel--center { flex: 1; align-items: center; justify-content: center; }
256
+ & .crisp-recordpage-illustrated { display: flex; flex-direction: column; align-items: center; gap: ${c.space["1.5"]}; text-align: center; }
257
+ & .crisp-recordpage-illustrated-icon { display: flex; align-items: center; justify-content: center; width: 138px; height: 138px; margin-bottom: ${c.space["2"]}; color: color-mix(in srgb, ${c.fgPrimary} 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, ${c.fgPrimary} 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }
258
+ & .crisp-recordpage-illustrated-title { font-size: ${c.text.xl}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
259
+ /* #988: the Calls tab's own gated state measures a larger 20/24 title
260
+ than Files/Deals' 16/600 -- Attio's own "empty view" pattern (the
261
+ same 20/600 EmptyState's page/records variants already carry, not a
262
+ one-off). */
263
+ & .crisp-recordpage-illustrated-title--lg { font-size: 20px; line-height: 24px; }
264
+ & .crisp-recordpage-illustrated-desc { max-width: 320px; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
265
+ & .crisp-recordpage-illustrated-actions { display: inline-flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: ${c.space["1.5"]}; margin-top: ${c.space["2"]}; }
266
+
229
267
  & .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
230
268
 
231
269
  & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${c.size.chip}; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.chip}; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; white-space: nowrap; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${c.fgPrimary} 82%, transparent); }
@@ -236,6 +274,31 @@ const config = defineRecipe({
236
274
  & .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, ${c.bgPurple} 16%, transparent); color: color-mix(in srgb, ${c.bgPurple} 80%, ${c.fgPrimary}); }
237
275
  & .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, ${c.bgCyan} 18%, transparent); color: color-mix(in srgb, ${c.bgCyan} 82%, ${c.fgPrimary}); }
238
276
  & .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); }
277
+
278
+ /* #620: the "Run workflow" quick action's own popover -- measured live:
279
+ a centered, top-anchored overlay sharing product/command's own
280
+ Cmd-K-palette shell exactly (elevation.modal, bgOverlayFrost 60% +
281
+ blur(15px), radius 2xl/xl outer/inner) -- duplicated here rather than
282
+ imported, the same "own copy per product surface" choice already
283
+ made elsewhere in this codebase (product/command sits outside this
284
+ lane's own product/recordpage ownership). */
285
+ & .crisp-recordpage-workflow-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; }
286
+ & .crisp-recordpage-workflow-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh ${c.space["4"]} ${c.space["4"]}; }
287
+ & .crisp-recordpage-workflow-content { position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-width: 808px; padding: ${c.space["1"]}; border-radius: ${c.radius["2xl"]}; background: color-mix(in srgb, ${c.bgOverlayFrost} 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: ${e.modal}; opacity: 1; transform: scale(1); transition: opacity .2s ease, transform .2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
288
+ & .crisp-recordpage-workflow-card { display: flex; flex-direction: column; box-sizing: border-box; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; overflow: hidden; }
289
+ & .crisp-recordpage-workflow-header { display: flex; align-items: center; gap: ${c.space["2"]}; height: 52px; flex-shrink: 0; padding: 0 ${c.space["2"]} 0 ${c.space["4"]}; }
290
+ & .crisp-recordpage-workflow-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.07px; color: ${c.fgPrimary}; }
291
+ & .crisp-recordpage-workflow-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
292
+ & .crisp-recordpage-workflow-close:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
293
+ & .crisp-recordpage-workflow-search { display: flex; align-items: center; min-height: 52px; padding: ${c.space["4"]}; }
294
+ & .crisp-recordpage-workflow-input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font-family: inherit; font-size: ${c.text.xl}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
295
+ & .crisp-recordpage-workflow-input::placeholder { color: ${c.fgMuted}; }
296
+ & .crisp-recordpage-workflow-sep { height: 1px; flex-shrink: 0; background: ${c.borderSubtle}; }
297
+ & .crisp-recordpage-workflow-empty { display: flex; flex-direction: column; align-items: center; gap: ${c.space["2"]}; padding: ${c.space["6"]} ${c.space["3"]}; text-align: center; }
298
+ & .crisp-recordpage-workflow-create { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 32px; padding: 0 ${c.space["3"]}; margin-top: ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: ${e.field}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; cursor: pointer; transition: background-color .12s; }
299
+ & .crisp-recordpage-workflow-create:hover { background: ${c.bgRaised}; }
300
+ & .crisp-recordpage-workflow-footer { display: flex; align-items: center; height: 44px; flex-shrink: 0; padding: 0 ${c.space["2.5"]}; border-top: 1px solid ${c.borderSubtle}; }
301
+ & .crisp-recordpage-workflow-footer-nav { display: flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
239
302
  `,
240
303
  variants: {},
241
304
  defaultVariants: {},
@@ -0,0 +1,2 @@
1
+ export { RecordPicker, } from "./recordpicker.js";
2
+ export { recordpickerRecipe } from "./recordpicker.recipe.js";
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Search } from "lucide-react";
3
+ import * as React from "react";
4
+ import { useListboxKeyboard } from "../../components/select/index.js";
5
+ import { Spinner } from "../../components/spinner/index.js";
6
+ import { useCrispMessage, useCrispMessages } from "../../core/index.js";
7
+ import { recordpickerRecipe } from "./recordpicker.recipe.js";
8
+ /**
9
+ * The record-search picker behind a task's "Add record" chip -- measured
10
+ * live on app.attio.com's Tasks page ("Edit task" -> "Add record"): a
11
+ * "Search..." row, then a FLAT list mixed across every object type (a
12
+ * person result sat first, ahead of several companies, with no section
13
+ * headers -- unlike the mention picker's own grouped Users/Records shape,
14
+ * crisp#646). Attio's own live search took a moment to resolve, showing a
15
+ * centred spinner + "Loading..." row meanwhile; not independently
16
+ * re-measured against a second empty-results case, so `emptyLabel`'s
17
+ * default copy ("No records") is inferred by symmetry with
18
+ * `AssigneePicker`'s own measured "No users", not itself pixel-measured.
19
+ */
20
+ export function RecordPicker({ records, loading = false, onSelect, query: queryProp, onQueryChange, searchLabel: searchLabelProp, loadingLabel: loadingLabelProp, emptyLabel: emptyLabelProp, className, ...rest }) {
21
+ const searchLabel = useCrispMessage(searchLabelProp, "Search...");
22
+ const loadingLabel = useCrispMessage(loadingLabelProp, "Loading...");
23
+ // crisp/#471: `emptyLabel` is typed `React.ReactNode`, so it resolves by
24
+ // hand rather than through `useCrispMessage`, which is string-only --
25
+ // same reasoning as `Inbox`'s own `emptyTitle`/`emptySubtitle`.
26
+ const crispMessages = useCrispMessages();
27
+ const emptyLabel = emptyLabelProp ?? crispMessages["No records"] ?? "No records";
28
+ const [internalQuery, setInternalQuery] = React.useState("");
29
+ const query = queryProp ?? internalQuery;
30
+ const setQuery = (q) => {
31
+ if (queryProp === undefined)
32
+ setInternalQuery(q);
33
+ onQueryChange?.(q);
34
+ };
35
+ const [highlight, setHighlight] = React.useState(0);
36
+ const autoId = React.useId();
37
+ const listboxId = `${autoId}-listbox`;
38
+ const optionId = (i) => `${autoId}-opt-${i}`;
39
+ const select = (r) => {
40
+ onSelect?.(r);
41
+ };
42
+ const { onKeyDown } = useListboxKeyboard({
43
+ enabledIndexes: records.map((_, i) => i),
44
+ highlight,
45
+ setHighlight,
46
+ commit: () => {
47
+ const r = records[highlight];
48
+ if (r)
49
+ select(r);
50
+ },
51
+ close: () => { },
52
+ dismiss: () => { },
53
+ });
54
+ const activeId = !loading && highlight >= 0 && highlight < records.length
55
+ ? optionId(highlight)
56
+ : undefined;
57
+ return (_jsxs("div", { className: recordpickerRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpicker-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { type: "text", className: "crisp-recordpicker-search-input", placeholder: searchLabel, value: query, onChange: (e) => {
58
+ setQuery(e.target.value);
59
+ setHighlight(0);
60
+ }, onKeyDown: onKeyDown, role: "combobox", "aria-expanded": "true", "aria-controls": listboxId, "aria-activedescendant": activeId,
61
+ // biome-ignore lint/a11y/noAutofocus: focus the search field the moment its enclosing popover opens
62
+ autoFocus: true })] }), loading ? (_jsxs("div", { className: "crisp-recordpicker-loading", children: [_jsx(Spinner, { size: 16 }), loadingLabel] })) : records.length === 0 ? (_jsx("div", { className: "crisp-recordpicker-empty", children: emptyLabel })) : (_jsx("div", { className: "crisp-recordpicker-list", role: "listbox", id: listboxId, children: records.map((r, i) => (_jsxs("button", { type: "button", id: optionId(i), role: "option", tabIndex: -1, "aria-selected": i === highlight, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-recordpicker-option", onMouseEnter: () => setHighlight(i), onClick: () => select(r), children: [r.icon, r.name] }, r.id))) }))] }));
63
+ }
64
+ RecordPicker.displayName = "RecordPicker";
@@ -0,0 +1,33 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // The record picker behind a task's "Add record" chip -- measured live on
4
+ // app.attio.com's Tasks page ("Edit task" -> "Add record" -> its own
5
+ // sub-popover, same measured 350px width as `AssigneePicker`'s): a
6
+ // borderless "Search..." row, then a flat list mixed across every object
7
+ // type (not grouped into sections -- that's the *mention* picker's own
8
+ // shape, crisp#646, a different, still-ungrouped-by-default surface here),
9
+ // each row a consumer-supplied type icon + the record's name. A "Loading..."
10
+ // row appears while results are in flight (the same measured centred
11
+ // Spinner + text DataTable's own #826 loading state already uses).
12
+ const config = defineRecipe({
13
+ name: "recordpicker",
14
+ base: `display: flex; flex-direction: column; box-sizing: border-box; width: 350px; font-family: ${c.font.sans};
15
+
16
+ & .crisp-recordpicker-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 36px; padding: 0 ${c.space["3"]}; color: ${c.fgMuted}; }
17
+ & .crisp-recordpicker-search svg { flex-shrink: 0; }
18
+ & .crisp-recordpicker-search-input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; line-height: 20px; color: ${c.fgPrimary}; }
19
+ & .crisp-recordpicker-search-input::placeholder { color: ${c.fgMuted}; }
20
+
21
+ & .crisp-recordpicker-loading { display: flex; align-items: center; justify-content: center; gap: 10px; padding: ${c.space["8"]} ${c.space["3"]}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; line-height: 20px; }
22
+ & .crisp-recordpicker-empty { padding: ${c.space["4"]} ${c.space["3"]}; text-align: center; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
23
+
24
+ & .crisp-recordpicker-list { display: flex; flex-direction: column; margin: 0; padding: ${c.space["1"]}; list-style: none; max-height: 280px; overflow-y: auto; }
25
+ & .crisp-recordpicker-option { display: flex; align-items: center; gap: ${c.space["2"]}; width: 100%; box-sizing: border-box; height: 32px; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: start; cursor: pointer; }
26
+ & .crisp-recordpicker-option svg { flex-shrink: 0; color: ${c.fgMuted}; }
27
+ & .crisp-recordpicker-option:hover, & .crisp-recordpicker-option[data-highlighted] { background: ${c.bgRaised}; }`,
28
+ variants: {},
29
+ defaultVariants: {},
30
+ compoundVariants: [],
31
+ });
32
+ export const recordpickerRecipe = resolveRecipe(config);
33
+ export default config;
@@ -1,2 +1,2 @@
1
- export { SavedViews, } from "./savedviews.js";
1
+ export { SavedViews, SavedViewsSaveBar, } from "./savedviews.js";
2
2
  export { savedviewsRecipe } from "./savedviews.recipe.js";
@@ -3,7 +3,7 @@ import { ChevronDown, MoreVertical, Plus, Users2 } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
5
5
  import { useListboxKeyboard } from "../../components/select/index.js";
6
- import { FloatingPortal, hasNestedFloatingLayer, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
6
+ import { cx, FloatingPortal, hasNestedFloatingLayer, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
7
7
  import { savedviewsRecipe } from "./savedviews.recipe.js";
8
8
  // #618: the same fallback Select's own `optionText` gives a non-string
9
9
  // `label` — `keywords` first, then the plain string itself, then (rather
@@ -200,3 +200,24 @@ export function SavedViews({ label, icon, views, activeId, onSelect, onCreate, o
200
200
  }, children: [_jsx("span", { className: "crisp-savedviews-row-icon", "aria-hidden": "true", children: _jsx(Plus, { size: 16, strokeWidth: 1.75 }) }), _jsx("span", { className: "crisp-savedviews-row-label", children: createLabel })] })] }) }))] }));
201
201
  }
202
202
  SavedViews.displayName = "SavedViews";
203
+ /**
204
+ * The "unsaved changes" bar for a currently loaded saved view — Attio's own
205
+ * "Discard changes" + a split "Save for everyone ▾" button, measured live
206
+ * (app.attio.com, a saved Companies view with its sort changed). Not a part
207
+ * of `SavedViews`' own trigger/panel: Attio renders this in the page
208
+ * toolbar's own trailing slot, replacing whatever sits there at rest
209
+ * (Import/Export, "+ New …") — the same slot `TableToolbar`'s own
210
+ * `children` prop already documents ("Right-aligned actions after the
211
+ * count"). A consumer swaps this in for that slot's usual content once its
212
+ * own dirty check (live filters/sort/columns vs. the loaded view's
213
+ * snapshot) is true, and swaps it back out once it's false — this
214
+ * component itself carries no dirty state or visibility toggle.
215
+ */
216
+ export function SavedViewsSaveBar({ onSave, onDiscard, onSaveAsNew, discardLabel: discardLabelProp, saveLabel: saveLabelProp, saveAsNewLabel: saveAsNewLabelProp, saveOptionsLabel: saveOptionsLabelProp, className, ...rest }) {
217
+ const discardLabel = useCrispMessage(discardLabelProp, "Discard changes");
218
+ const saveLabel = useCrispMessage(saveLabelProp, "Save for everyone");
219
+ const saveAsNewLabel = useCrispMessage(saveAsNewLabelProp, "Save as new view");
220
+ const saveOptionsLabel = useCrispMessage(saveOptionsLabelProp, "More save options");
221
+ return (_jsxs("div", { className: cx("crisp-savedviews-savebar", className), ...rest, children: [_jsx("button", { type: "button", className: "crisp-savedviews-savebar-discard", onClick: onDiscard, children: discardLabel }), _jsxs("div", { className: "crisp-savedviews-savebar-save-group", children: [_jsx("button", { type: "button", className: "crisp-savedviews-savebar-save", onClick: onSave, children: saveLabel }), onSaveAsNew && (_jsx(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-savedviews-savebar-save-chevron", "aria-label": saveOptionsLabel, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) }), children: _jsx(MenuItem, { onSelect: onSaveAsNew, children: saveAsNewLabel }) }))] })] }));
222
+ }
223
+ SavedViewsSaveBar.displayName = "SavedViewsSaveBar";
@@ -120,6 +120,53 @@ const config = defineRecipe({
120
120
  font-size: ${c.text.sm}; font-weight: ${c.weight.medium};
121
121
  color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent);
122
122
  }
123
+
124
+ /* crisp#568: the "Discard changes" / "Save for everyone ▾" bar a
125
+ consumer swaps into its own toolbar's trailing slot (TableToolbar's
126
+ own children prop already documents "right-aligned actions after the
127
+ count") while the loaded view is dirty -- measured live on
128
+ app.attio.com replacing that exact slot's usual Import/Export + New
129
+ button. Not rendered inside SavedViews' own trigger/panel, which
130
+ stays untouched; this is its own export, SavedViewsSaveBar. */
131
+ .crisp-savedviews-savebar { display: flex; align-items: center; gap: ${c.space["2"]}; }
132
+ /* Rest-state color is a 55% color-mix of fgPrimary -- measured
133
+ identical to recordpanel's own ".crisp-recordpanel-section-action"
134
+ (its own comment: "measured 10px/400, a lighter mix..." -- font size
135
+ there was the SAME outer-vs-inner-div miss #688 corrected; the 55%
136
+ mix itself checks out independently here). Hover repeats crisp's own
137
+ already-measured Button ghost intent exactly (bgRaised + fgPrimary),
138
+ confirmed live (hover bg rgba(0,0,0,.04) + ink text matches bgRaised
139
+ + fgPrimary bit for bit) -- reused, not re-derived. */
140
+ .crisp-savedviews-savebar-discard {
141
+ box-sizing: border-box; display: inline-flex; align-items: center; height: 28px;
142
+ margin: 0; padding-inline: ${c.space["2"]}; border: none; border-radius: ${c.radius.md};
143
+ background: none; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium};
144
+ color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); cursor: pointer;
145
+ transition: background-color .12s ease, color .12s ease;
146
+ }
147
+ .crisp-savedviews-savebar-discard:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
148
+ /* the split "Save for everyone ▾" pill -- two adjacent buttons sharing
149
+ one visual pill (measured: label half rounds only its outer/left
150
+ corners, the chevron half only its outer/right corners, seamless
151
+ between). Fill is crisp's OWN measured bgBrandContrast, not Attio's
152
+ own literal button blue -- the owner's directive is Attio structure/
153
+ sizing, seed-identity color (crisp direction doc). Hover/active reuse
154
+ Button's own already-measured primary-intent brightness filter
155
+ (crisp#910) rather than Attio's own literal darker fill, for the
156
+ same reason. */
157
+ .crisp-savedviews-savebar-save-group { display: flex; }
158
+ .crisp-savedviews-savebar-save, .crisp-savedviews-savebar-save-chevron {
159
+ box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
160
+ height: 28px; margin: 0; border: none; background: ${c.bgBrandContrast}; color: ${c.fgOnBrand};
161
+ cursor: pointer; transition: filter .2s;
162
+ }
163
+ .crisp-savedviews-savebar-save:hover, .crisp-savedviews-savebar-save-chevron:hover { filter: brightness(0.96); }
164
+ .crisp-savedviews-savebar-save:active, .crisp-savedviews-savebar-save-chevron:active { filter: brightness(0.92); }
165
+ .crisp-savedviews-savebar-save {
166
+ padding-inline: ${c.space["2"]} ${c.space["1.5"]}; border-radius: ${c.radius.md} 0 0 ${c.radius.md};
167
+ font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium};
168
+ }
169
+ .crisp-savedviews-savebar-save-chevron { width: 28px; padding: 0; border-radius: 0 ${c.radius.md} ${c.radius.md} 0; }
123
170
  `,
124
171
  variants: {},
125
172
  defaultVariants: {},
@@ -0,0 +1,2 @@
1
+ export { SharePopover, } from "./share.js";
2
+ export { shareRecipe } from "./share.recipe.js";
@@ -0,0 +1,143 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { HelpCircle, Play, Users } from "lucide-react";
3
+ import * as React from "react";
4
+ import { Avatar } from "../../components/avatar/index.js";
5
+ import { Input } from "../../components/input/index.js";
6
+ import { Select } from "../../components/select/index.js";
7
+ import { useTablistKeyboard } from "../../components/tabs/index.js";
8
+ import { FloatingPortal, hasNestedFloatingLayer, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
9
+ import { shareRecipe } from "./share.recipe.js";
10
+ /**
11
+ * A Share/permissions popover — measured live on app.attio.com (both an
12
+ * object table's and a List's own "Share" trigger, techpartners workspace):
13
+ * Members/Automations pill tabs, a "Workspace access" row (avatar + label +
14
+ * a `Read and write ⌄`-style role select), an optional plan-gated upsell
15
+ * slot, a "Clear permissions" action, an "Individual members" list (one row
16
+ * per member, its own role select), and — on the Automations tab — a
17
+ * search box + role select + "Add" row. Both tabs share a trailing "Learn
18
+ * more about permissions" link.
19
+ *
20
+ * Its own floating shell (not the generic `Popover`): the measured surface
21
+ * is 440px wide, past `Popover`'s 360px content cap. See share.recipe.ts.
22
+ */
23
+ export function SharePopover({ trigger, align = "end", open: openProp, defaultOpen, onOpenChange, tab: tabProp, defaultTab = "members", onTabChange, membersTabLabel: membersTabLabelProp, automationsTabLabel: automationsTabLabelProp, workspaceIcon, workspaceAccessLabel: workspaceAccessLabelProp, workspaceAccessValue, workspaceAccessOptions, onWorkspaceAccessChange, advancedSharing, onClearPermissions, clearPermissionsLabel: clearPermissionsLabelProp, members, membersSectionLabel: membersSectionLabelProp, memberRoleOptions, onMemberAccessChange, automationSearchValue, defaultAutomationSearchValue, onAutomationSearchChange, automationSearchPlaceholder: automationSearchPlaceholderProp, automationRoleValue, automationRoleOptions, onAutomationRoleChange, onAddAutomation, addAutomationDisabled, addAutomationLabel: addAutomationLabelProp, learnMoreHref, learnMoreLabel: learnMoreLabelProp, "aria-label": ariaLabel, className, ...rest }) {
24
+ const membersTabLabel = useCrispMessage(membersTabLabelProp, "Members");
25
+ const automationsTabLabel = useCrispMessage(automationsTabLabelProp, "Automations");
26
+ const workspaceAccessLabel = useCrispMessage(workspaceAccessLabelProp, "Workspace access");
27
+ const clearPermissionsLabel = useCrispMessage(clearPermissionsLabelProp, "Clear permissions");
28
+ const membersSectionLabel = useCrispMessage(membersSectionLabelProp, "Individual members");
29
+ const automationSearchPlaceholder = useCrispMessage(automationSearchPlaceholderProp, "Search...");
30
+ const addAutomationLabel = useCrispMessage(addAutomationLabelProp, "Add");
31
+ const learnMoreLabel = useCrispMessage(learnMoreLabelProp, "Learn more about permissions");
32
+ const surfaceId = React.useId();
33
+ const triggerId = trigger.props.id ?? `${surfaceId}-trigger`;
34
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
35
+ const open = openProp ?? internalOpen;
36
+ const setOpen = React.useCallback((next) => {
37
+ if (openProp === undefined)
38
+ setInternalOpen(next);
39
+ onOpenChange?.(next);
40
+ }, [openProp, onOpenChange]);
41
+ const [internalTab, setInternalTab] = React.useState(defaultTab);
42
+ const activeTab = tabProp ?? internalTab;
43
+ const selectTab = (t) => {
44
+ if (tabProp === undefined)
45
+ setInternalTab(t);
46
+ onTabChange?.(t);
47
+ };
48
+ const [internalSearch, setInternalSearch] = React.useState(defaultAutomationSearchValue ?? "");
49
+ const searchValue = automationSearchValue ?? internalSearch;
50
+ const setSearch = (q) => {
51
+ if (automationSearchValue === undefined)
52
+ setInternalSearch(q);
53
+ onAutomationSearchChange?.(q);
54
+ };
55
+ const rootRef = React.useRef(null);
56
+ const triggerRef = React.useRef(null);
57
+ const contentRef = React.useRef(null);
58
+ const [pos, setPos] = React.useState({});
59
+ const [up, setUp] = React.useState(false);
60
+ const place = () => {
61
+ const el = triggerRef.current;
62
+ if (!el)
63
+ return;
64
+ const { style, up: flipUp } = placeFloating({
65
+ rect: el.getBoundingClientRect(),
66
+ align,
67
+ gap: 6,
68
+ flipBelow: 320,
69
+ anchor: el,
70
+ });
71
+ setUp(flipUp);
72
+ setPos(style);
73
+ };
74
+ // biome-ignore lint/correctness/useExhaustiveDependencies: reposition each time it opens
75
+ React.useLayoutEffect(() => {
76
+ if (open)
77
+ place();
78
+ }, [open]);
79
+ React.useEffect(() => {
80
+ if (!open || !contentRef.current)
81
+ return;
82
+ return registerFloatingLayer(contentRef.current);
83
+ }, [open]);
84
+ const close = React.useCallback(() => {
85
+ setOpen(false);
86
+ }, [setOpen]);
87
+ React.useEffect(() => {
88
+ if (!open)
89
+ return;
90
+ const onDown = (e) => {
91
+ const target = e.target;
92
+ if (rootRef.current?.contains(target))
93
+ return;
94
+ if (contentRef.current?.contains(target))
95
+ return;
96
+ if (isInsideLaterFloatingLayer(target, contentRef.current))
97
+ return;
98
+ close();
99
+ };
100
+ document.addEventListener("pointerdown", onDown);
101
+ return () => document.removeEventListener("pointerdown", onDown);
102
+ }, [open, close]);
103
+ React.useEffect(() => {
104
+ if (!open)
105
+ return;
106
+ const onKey = (e) => {
107
+ if (e.key !== "Escape")
108
+ return;
109
+ if (hasNestedFloatingLayer(contentRef.current))
110
+ return;
111
+ close();
112
+ };
113
+ document.addEventListener("keydown", onKey);
114
+ return () => document.removeEventListener("keydown", onKey);
115
+ }, [open, close]);
116
+ const triggerEl = React.cloneElement(trigger, {
117
+ ref: triggerRef,
118
+ id: triggerId,
119
+ "aria-haspopup": "dialog",
120
+ "aria-expanded": open,
121
+ "aria-controls": open ? surfaceId : undefined,
122
+ onClick: (e) => {
123
+ trigger.props.onClick?.(e);
124
+ setOpen(!open);
125
+ },
126
+ });
127
+ const tablistRef = React.useRef(null);
128
+ const tabEntries = React.useMemo(() => [{ value: "members" }, { value: "automations" }], []);
129
+ const { onKeyDown: onTabKeyDown, tabIndexFor } = useTablistKeyboard({
130
+ tabs: tabEntries,
131
+ active: activeTab,
132
+ select: (v) => selectTab(v),
133
+ listRef: tablistRef,
134
+ });
135
+ return (_jsxs("div", { ref: rootRef, className: shareRecipe({ className }), children: [triggerEl, open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: contentRef, className: "crisp-share-content", role: "dialog", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-up": up ? "" : undefined, style: pos, ...rest, children: [_jsxs("div", { ref: tablistRef, className: "crisp-share-tabs", role: "tablist", onKeyDown: onTabKeyDown, children: [_jsxs("button", { type: "button", role: "tab", "data-value": "members", "aria-selected": activeTab === "members", tabIndex: tabIndexFor("members"), className: "crisp-share-tab", onClick: () => selectTab("members"), children: [_jsx(Users, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), membersTabLabel] }), _jsxs("button", { type: "button", role: "tab", "data-value": "automations", "aria-selected": activeTab === "automations", tabIndex: tabIndexFor("automations"), className: "crisp-share-tab", onClick: () => selectTab("automations"), children: [_jsx(Play, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), automationsTabLabel] })] }), activeTab === "members" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "crisp-share-row", children: [workspaceIcon ?? (_jsx(Avatar, { size: "lg", tone: "brand", children: typeof workspaceAccessLabel === "string"
136
+ ? workspaceAccessLabel.charAt(0)
137
+ : null })), _jsx("span", { className: "crisp-share-row-label", children: workspaceAccessLabel }), _jsx(Select, { variant: "ghost", width: "auto", options: workspaceAccessOptions, value: workspaceAccessValue, onValueChange: onWorkspaceAccessChange, "aria-label": workspaceAccessLabel })] }), advancedSharing, onClearPermissions && (_jsx("button", { type: "button", className: "crisp-share-clear", onClick: onClearPermissions, children: clearPermissionsLabel })), members && members.length > 0 && (_jsxs("div", { children: [_jsx("p", { className: "crisp-share-section-label", children: membersSectionLabel }), _jsx("div", { className: "crisp-share-members", children: members.map((m) => (_jsxs("div", { className: "crisp-share-member", children: [m.avatar ?? (_jsx(Avatar, { size: "lg", tone: "brand", children: typeof m.name === "string"
138
+ ? m.name.charAt(0).toUpperCase()
139
+ : null })), _jsxs("span", { className: "crisp-share-member-text", children: [_jsx("span", { className: "crisp-share-member-name", children: m.name }), m.email && (_jsx("span", { className: "crisp-share-member-email", children: m.email }))] }), _jsx(Select, { variant: "ghost", width: "auto", options: memberRoleOptions, value: m.access, disabled: m.accessDisabled, onValueChange: (v) => onMemberAccessChange?.(m.id, v), "aria-label": typeof m.name === "string"
140
+ ? `${m.name}'s access`
141
+ : undefined })] }, m.id))) })] }))] })) : (_jsxs("div", { className: "crisp-share-automations-row", children: [_jsx(Input, { value: searchValue, onChange: (e) => setSearch(e.target.value), placeholder: automationSearchPlaceholder, "aria-label": automationSearchPlaceholder }), _jsx(Select, { variant: "ghost", width: "auto", options: automationRoleOptions, value: automationRoleValue, onValueChange: onAutomationRoleChange, "aria-label": automationsTabLabel }), _jsx("button", { type: "button", className: "crisp-share-add", disabled: addAutomationDisabled, onClick: onAddAutomation, children: addAutomationLabel })] })), _jsxs("a", { className: "crisp-share-learnmore", href: learnMoreHref, target: "_blank", rel: "noreferrer", children: [_jsx(HelpCircle, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), learnMoreLabel] })] }) }))] }));
142
+ }
143
+ SharePopover.displayName = "SharePopover";
@@ -0,0 +1,111 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c, elevation as e } from "../../tokens/index.js";
3
+ // SharePopover's shell — measured live on app.attio.com (both the object
4
+ // table's and a List's own "Share" trigger, techpartners workspace,
5
+ // 2026-09-27): a 440px-wide floating panel, radius 12 (xl), the same
6
+ // layered popover shadow every other floating surface uses (`e.floating`
7
+ // matched the measured box-shadow's four layers exactly, so no new shadow
8
+ // literal was needed).
9
+ //
10
+ // This is its own floating shell rather than a reuse of the generic
11
+ // `Popover` (components/popover): that recipe caps its content at
12
+ // `min(360px, 92vw)`, and 440px measured wider than that on every sample.
13
+ // `product/savedviews` already sets the precedent for a product surface
14
+ // owning its own portaled shell over the generic one when the measured
15
+ // geometry doesn't fit it.
16
+ //
17
+ // Three muted-text alphas were measured that don't match any existing
18
+ // color token: the workspace-access/role value text (`rgba(0,0,0,.55)`),
19
+ // the "Individual members" section label (`rgba(0,0,0,.5)`), and the
20
+ // inactive-tab/footer-link text (`rgba(0,0,0,.63)` — this one incidentally
21
+ // equals `fgAlertDescription`'s light value, but that token is Dialog's own
22
+ // alert-description colour, a different surface, so it is not reused here).
23
+ // Rather than add new palette entries for three one-off alphas (tokens/**
24
+ // is another lane's file), each is expressed as `color-mix(in srgb,
25
+ // fgPrimary N%, transparent)` — the same idiom `segmented`'s own track
26
+ // background already uses for its 5% tint — so the value is theme-reactive
27
+ // (it scales with fgPrimary's dark value too) without a new token or a
28
+ // flat literal that would go stale in dark mode.
29
+ const mutedText = (pct) => `color-mix(in srgb, ${c.fgPrimary} ${pct}%, transparent)`;
30
+ const config = defineRecipe({
31
+ name: "share",
32
+ base: `display: contents; font-family: ${c.font.sans};`,
33
+ globalCss: `
34
+ .crisp-share-content {
35
+ font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
36
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box;
37
+ width: 440px; max-width: 92vw;
38
+ padding: ${c.space["3"]}; display: flex; flex-direction: column; gap: ${c.space["3"]};
39
+ border-radius: ${c.radius.xl}; background: ${c.bgPopover}; color: ${c.fgPrimary};
40
+ box-shadow: ${e.floating};
41
+ opacity: 1; transform: none; transform-origin: top right;
42
+ transition: opacity 0.08s ease, transform 0.08s ease;
43
+ @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); }
44
+ }
45
+ .crisp-share-content[data-up] { transform-origin: bottom right; }
46
+
47
+ .crisp-share-tabs { display: flex; align-items: center; gap: ${c.space["1"]}; }
48
+ .crisp-share-tab {
49
+ box-sizing: border-box; display: inline-flex; align-items: center; gap: ${c.space["1.5"]};
50
+ height: ${c.size.row}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md};
51
+ background: transparent; color: ${mutedText(63)};
52
+ font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.07px;
53
+ cursor: pointer; transition: background-color .12s ease, color .12s ease;
54
+ }
55
+ .crisp-share-tab svg { flex-shrink: 0; }
56
+ .crisp-share-tab:hover { color: ${c.fgPrimary}; }
57
+ .crisp-share-tab[aria-selected="true"] { background: ${c.bgSurfaceMuted}; color: ${c.fgPrimary}; }
58
+
59
+ .crisp-share-row { display: flex; align-items: center; gap: ${c.space["2"]}; min-height: 34px; }
60
+ .crisp-share-row-label { flex: 1; min-width: 0; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; letter-spacing: -0.07px; }
61
+ .crisp-share-row .crisp-select { flex-shrink: 0; }
62
+ .crisp-share-row .crisp-select .crisp-select-value,
63
+ .crisp-share-row .crisp-select .crisp-select-chevron { color: ${mutedText(55)}; }
64
+
65
+ .crisp-share-clear {
66
+ align-self: flex-start; margin: 0; padding: 0; border: none; background: none;
67
+ font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium};
68
+ color: ${mutedText(63)}; cursor: pointer;
69
+ }
70
+ .crisp-share-clear:hover { color: ${c.fgPrimary}; text-decoration: underline; }
71
+
72
+ .crisp-share-section-label {
73
+ margin: 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${mutedText(50)};
74
+ }
75
+ .crisp-share-members { display: flex; flex-direction: column; gap: ${c.space["2"]}; }
76
+ .crisp-share-member { display: flex; align-items: center; gap: ${c.space["2"]}; }
77
+ .crisp-share-member-text { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: ${c.space["1.5"]}; overflow: hidden; }
78
+ .crisp-share-member-name { flex-shrink: 0; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; letter-spacing: -0.07px; }
79
+ .crisp-share-member-email { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${mutedText(55)}; letter-spacing: -0.07px; }
80
+ .crisp-share-member .crisp-select { flex-shrink: 0; }
81
+ .crisp-share-member .crisp-select .crisp-select-value,
82
+ .crisp-share-member .crisp-select .crisp-select-chevron { color: ${mutedText(55)}; }
83
+
84
+ .crisp-share-automations-row { display: flex; align-items: center; gap: ${c.space["2"]}; }
85
+ .crisp-share-automations-row .crisp-input { flex: 1; min-width: 0; }
86
+ .crisp-share-automations-row .crisp-select { flex-shrink: 0; }
87
+
88
+ .crisp-share-add {
89
+ flex-shrink: 0; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
90
+ height: 32px; padding: 0 ${c.space["2.5"]}; border: none; border-radius: ${c.radius.lg};
91
+ background: ${c.bgBrandSolid}; color: ${c.fgOnBrand};
92
+ font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.07px;
93
+ cursor: pointer; transition: background-color .12s ease, opacity .12s ease;
94
+ }
95
+ .crisp-share-add:hover:not(:disabled) { background: ${c.bgBrandSolidPressed}; }
96
+ .crisp-share-add:disabled { opacity: 0.4; cursor: not-allowed; }
97
+
98
+ .crisp-share-learnmore {
99
+ display: inline-flex; align-items: center; gap: ${c.space["1.5"]};
100
+ font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${mutedText(63)};
101
+ text-decoration: none;
102
+ }
103
+ .crisp-share-learnmore:hover { color: ${c.fgPrimary}; }
104
+ .crisp-share-learnmore svg { flex-shrink: 0; }
105
+ `,
106
+ variants: {},
107
+ defaultVariants: {},
108
+ compoundVariants: [],
109
+ });
110
+ export const shareRecipe = resolveRecipe(config);
111
+ export default config;
@@ -0,0 +1 @@
1
+ export * from "./shortcutssheet.js";