@crisp-ui-kit/crisp 0.1.0 → 0.5.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 (249) hide show
  1. package/README.md +6 -0
  2. package/dist/base.css +25 -0
  3. package/dist/bulkbar.css +13 -0
  4. package/dist/bulkbar.d.ts +7 -0
  5. package/dist/button.css +1 -1
  6. package/dist/command.css +1 -1
  7. package/dist/contextmenu.css +1 -1
  8. package/dist/datatable.css +60 -0
  9. package/dist/datatable.d.ts +7 -0
  10. package/dist/datepicker.css +1 -1
  11. package/dist/dialog.css +1 -1
  12. package/dist/esm/components/accordion/accordion.js +32 -0
  13. package/dist/esm/components/accordion/accordion.recipe.js +23 -0
  14. package/dist/esm/components/accordion/index.js +2 -0
  15. package/dist/esm/components/avatar/avatar.js +8 -0
  16. package/dist/esm/components/avatar/avatar.recipe.js +39 -0
  17. package/dist/esm/components/avatar/index.js +2 -0
  18. package/dist/esm/components/avatargroup/avatargroup.js +16 -0
  19. package/dist/esm/components/avatargroup/avatargroup.recipe.js +15 -0
  20. package/dist/esm/components/avatargroup/index.js +2 -0
  21. package/dist/esm/components/badge/badge.js +8 -0
  22. package/dist/esm/components/badge/badge.recipe.js +65 -0
  23. package/dist/esm/components/badge/index.js +2 -0
  24. package/dist/esm/components/barchart/barchart.js +29 -0
  25. package/dist/esm/components/barchart/barchart.recipe.js +26 -0
  26. package/dist/esm/components/barchart/index.js +2 -0
  27. package/dist/esm/components/breadcrumb/breadcrumb.js +17 -0
  28. package/dist/esm/components/breadcrumb/breadcrumb.recipe.js +18 -0
  29. package/dist/esm/components/breadcrumb/index.js +2 -0
  30. package/dist/esm/components/bulkbar/bulkbar.js +16 -0
  31. package/dist/esm/components/bulkbar/bulkbar.recipe.js +28 -0
  32. package/dist/esm/components/bulkbar/index.js +2 -0
  33. package/dist/esm/components/button/button.js +15 -0
  34. package/dist/esm/components/button/button.recipe.js +39 -0
  35. package/dist/esm/components/button/index.js +2 -0
  36. package/dist/esm/components/callout/callout.js +22 -0
  37. package/dist/esm/components/callout/callout.recipe.js +26 -0
  38. package/dist/esm/components/callout/index.js +2 -0
  39. package/dist/esm/components/card/card.js +15 -0
  40. package/dist/esm/components/card/card.recipe.js +28 -0
  41. package/dist/esm/components/card/index.js +2 -0
  42. package/dist/esm/components/checkbox/checkbox.js +20 -0
  43. package/dist/esm/components/checkbox/checkbox.recipe.js +23 -0
  44. package/dist/esm/components/checkbox/index.js +2 -0
  45. package/dist/esm/components/contextmenu/contextmenu.js +42 -0
  46. package/dist/esm/components/contextmenu/contextmenu.recipe.js +21 -0
  47. package/dist/esm/components/contextmenu/index.js +2 -0
  48. package/dist/esm/components/datatable/datatable.js +245 -0
  49. package/dist/esm/components/datatable/datatable.recipe.js +72 -0
  50. package/dist/esm/components/datatable/index.js +2 -0
  51. package/dist/esm/components/datepicker/datepicker.js +57 -0
  52. package/dist/esm/components/datepicker/datepicker.recipe.js +32 -0
  53. package/dist/esm/components/datepicker/index.js +2 -0
  54. package/dist/esm/components/dialog/dialog.js +48 -0
  55. package/dist/esm/components/dialog/dialog.recipe.js +32 -0
  56. package/dist/esm/components/dialog/index.js +2 -0
  57. package/dist/esm/components/divider/divider.js +7 -0
  58. package/dist/esm/components/divider/divider.recipe.js +14 -0
  59. package/dist/esm/components/divider/index.js +2 -0
  60. package/dist/esm/components/donutchart/donutchart.js +38 -0
  61. package/dist/esm/components/donutchart/donutchart.recipe.js +27 -0
  62. package/dist/esm/components/donutchart/index.js +2 -0
  63. package/dist/esm/components/drawer/drawer.js +42 -0
  64. package/dist/esm/components/drawer/drawer.recipe.js +27 -0
  65. package/dist/esm/components/drawer/index.js +2 -0
  66. package/dist/esm/components/emptystate/emptystate.js +7 -0
  67. package/dist/esm/components/emptystate/emptystate.recipe.js +31 -0
  68. package/dist/esm/components/emptystate/index.js +2 -0
  69. package/dist/esm/components/formfield/formfield.js +14 -0
  70. package/dist/esm/components/formfield/formfield.recipe.js +19 -0
  71. package/dist/esm/components/formfield/index.js +2 -0
  72. package/dist/esm/components/input/index.js +2 -0
  73. package/dist/esm/components/input/input.js +8 -0
  74. package/dist/esm/components/input/input.recipe.js +23 -0
  75. package/dist/esm/components/kbd/index.js +2 -0
  76. package/dist/esm/components/kbd/kbd.js +8 -0
  77. package/dist/esm/components/kbd/kbd.recipe.js +20 -0
  78. package/dist/esm/components/linechart/index.js +2 -0
  79. package/dist/esm/components/linechart/linechart.js +41 -0
  80. package/dist/esm/components/linechart/linechart.recipe.js +27 -0
  81. package/dist/esm/components/menu/index.js +2 -0
  82. package/dist/esm/components/menu/menu.js +137 -0
  83. package/dist/esm/components/menu/menu.recipe.js +51 -0
  84. package/dist/esm/components/multiselect/index.js +2 -0
  85. package/dist/esm/components/multiselect/multiselect.js +118 -0
  86. package/dist/esm/components/multiselect/multiselect.recipe.js +68 -0
  87. package/dist/esm/components/popover/index.js +2 -0
  88. package/dist/esm/components/popover/popover.js +70 -0
  89. package/dist/esm/components/popover/popover.recipe.js +18 -0
  90. package/dist/esm/components/progress/index.js +2 -0
  91. package/dist/esm/components/progress/progress.js +23 -0
  92. package/dist/esm/components/progress/progress.recipe.js +17 -0
  93. package/dist/esm/components/radiogroup/index.js +2 -0
  94. package/dist/esm/components/radiogroup/radiogroup.js +46 -0
  95. package/dist/esm/components/radiogroup/radiogroup.recipe.js +37 -0
  96. package/dist/esm/components/segmented/index.js +2 -0
  97. package/dist/esm/components/segmented/segmented.js +59 -0
  98. package/dist/esm/components/segmented/segmented.recipe.js +20 -0
  99. package/dist/esm/components/select/index.js +2 -0
  100. package/dist/esm/components/select/select.js +144 -0
  101. package/dist/esm/components/select/select.recipe.js +76 -0
  102. package/dist/esm/components/skeleton/index.js +2 -0
  103. package/dist/esm/components/skeleton/skeleton.js +7 -0
  104. package/dist/esm/components/skeleton/skeleton.recipe.js +14 -0
  105. package/dist/esm/components/spinner/index.js +2 -0
  106. package/dist/esm/components/spinner/spinner.js +11 -0
  107. package/dist/esm/components/spinner/spinner.recipe.js +13 -0
  108. package/dist/esm/components/stagebar/index.js +2 -0
  109. package/dist/esm/components/stagebar/stagebar.js +23 -0
  110. package/dist/esm/components/stagebar/stagebar.recipe.js +14 -0
  111. package/dist/esm/components/statcard/index.js +2 -0
  112. package/dist/esm/components/statcard/statcard.js +12 -0
  113. package/dist/esm/components/statcard/statcard.recipe.js +34 -0
  114. package/dist/esm/components/switch/index.js +2 -0
  115. package/dist/esm/components/switch/switch.js +22 -0
  116. package/dist/esm/components/switch/switch.recipe.js +26 -0
  117. package/dist/esm/components/table/index.js +2 -0
  118. package/dist/esm/components/table/table.js +30 -0
  119. package/dist/esm/components/table/table.recipe.js +40 -0
  120. package/dist/esm/components/tabletoolbar/index.js +2 -0
  121. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +79 -0
  122. package/dist/esm/components/tabletoolbar/tabletoolbar.js +191 -0
  123. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +23 -0
  124. package/dist/esm/components/tabs/index.js +2 -0
  125. package/dist/esm/components/tabs/tabs.js +70 -0
  126. package/dist/esm/components/tabs/tabs.recipe.js +33 -0
  127. package/dist/esm/components/textarea/index.js +2 -0
  128. package/dist/esm/components/textarea/textarea.js +20 -0
  129. package/dist/esm/components/textarea/textarea.recipe.js +31 -0
  130. package/dist/esm/components/toast/index.js +2 -0
  131. package/dist/esm/components/toast/toast.js +58 -0
  132. package/dist/esm/components/toast/toast.recipe.js +28 -0
  133. package/dist/esm/components/tooltip/index.js +2 -0
  134. package/dist/esm/components/tooltip/tooltip.js +45 -0
  135. package/dist/esm/components/tooltip/tooltip.recipe.js +13 -0
  136. package/dist/esm/core/cx.js +1 -0
  137. package/dist/esm/core/define.js +3 -0
  138. package/dist/esm/core/index.js +4 -0
  139. package/dist/esm/core/primitive.js +13 -0
  140. package/dist/esm/core/resolve.js +41 -0
  141. package/dist/esm/core/types.js +1 -0
  142. package/dist/esm/index.js +71 -0
  143. package/dist/esm/product/appshell/appshell.js +44 -0
  144. package/dist/esm/product/appshell/appshell.recipe.js +27 -0
  145. package/dist/esm/product/appshell/index.js +2 -0
  146. package/dist/esm/product/board/board.js +42 -0
  147. package/dist/esm/product/board/board.recipe.js +28 -0
  148. package/dist/esm/product/board/index.js +2 -0
  149. package/dist/esm/product/command/command.js +188 -0
  150. package/dist/esm/product/command/command.recipe.js +19 -0
  151. package/dist/esm/product/command/index.js +2 -0
  152. package/dist/esm/product/entitycard/entitycard.js +11 -0
  153. package/dist/esm/product/entitycard/entitycard.recipe.js +29 -0
  154. package/dist/esm/product/entitycard/index.js +2 -0
  155. package/dist/esm/product/flowcanvas/flowcanvas.js +85 -0
  156. package/dist/esm/product/flowcanvas/flowcanvas.recipe.js +25 -0
  157. package/dist/esm/product/flowcanvas/index.js +2 -0
  158. package/dist/esm/product/inbox/inbox.js +13 -0
  159. package/dist/esm/product/inbox/inbox.recipe.js +43 -0
  160. package/dist/esm/product/inbox/index.js +2 -0
  161. package/dist/esm/product/listpage/index.js +2 -0
  162. package/dist/esm/product/listpage/listpage.js +21 -0
  163. package/dist/esm/product/listpage/listpage.recipe.js +43 -0
  164. package/dist/esm/product/navitem/index.js +1 -0
  165. package/dist/esm/product/navitem/navitem.js +8 -0
  166. package/dist/esm/product/navitem/navitem.recipe.js +21 -0
  167. package/dist/esm/product/onboarding/index.js +2 -0
  168. package/dist/esm/product/onboarding/onboarding.js +14 -0
  169. package/dist/esm/product/onboarding/onboarding.recipe.js +41 -0
  170. package/dist/esm/product/recordpage/index.js +2 -0
  171. package/dist/esm/product/recordpage/recordpage.js +286 -0
  172. package/dist/esm/product/recordpage/recordpage.recipe.js +157 -0
  173. package/dist/esm/product/recordpanel/index.js +2 -0
  174. package/dist/esm/product/recordpanel/recordpanel.js +47 -0
  175. package/dist/esm/product/recordpanel/recordpanel.recipe.js +52 -0
  176. package/dist/esm/product/recordstable/company-logos.js +26 -0
  177. package/dist/esm/product/recordstable/index.js +2 -0
  178. package/dist/esm/product/recordstable/recordstable.js +963 -0
  179. package/dist/esm/product/recordstable/recordstable.recipe.js +149 -0
  180. package/dist/esm/product/richtext/index.js +2 -0
  181. package/dist/esm/product/richtext/richtext.js +111 -0
  182. package/dist/esm/product/richtext/richtext.recipe.js +40 -0
  183. package/dist/esm/product/settingssection/index.js +2 -0
  184. package/dist/esm/product/settingssection/settingssection.js +11 -0
  185. package/dist/esm/product/settingssection/settingssection.recipe.js +20 -0
  186. package/dist/esm/product/settingsshell/index.js +2 -0
  187. package/dist/esm/product/settingsshell/settingsshell.js +23 -0
  188. package/dist/esm/product/settingsshell/settingsshell.recipe.js +38 -0
  189. package/dist/esm/product/sidebar/index.js +2 -0
  190. package/dist/esm/product/sidebar/sidebar.js +112 -0
  191. package/dist/esm/product/sidebar/sidebar.recipe.js +18 -0
  192. package/dist/esm/product/tasklist/index.js +2 -0
  193. package/dist/esm/product/tasklist/tasklist.js +16 -0
  194. package/dist/esm/product/tasklist/tasklist.recipe.js +35 -0
  195. package/dist/esm/product/trialbanner/index.js +2 -0
  196. package/dist/esm/product/trialbanner/trialbanner.js +10 -0
  197. package/dist/esm/product/trialbanner/trialbanner.recipe.js +29 -0
  198. package/dist/esm/product/viewtoggle/index.js +2 -0
  199. package/dist/esm/product/viewtoggle/viewtoggle.js +11 -0
  200. package/dist/esm/product/viewtoggle/viewtoggle.recipe.js +24 -0
  201. package/dist/esm/tokens/contract.js +18 -0
  202. package/dist/esm/tokens/index.js +4 -0
  203. package/dist/esm/tokens/palette.js +30 -0
  204. package/dist/esm/tokens/scale.js +60 -0
  205. package/dist/esm/tokens/semantic.js +30 -0
  206. package/dist/menu.css +2 -2
  207. package/dist/multiselect.css +3 -3
  208. package/dist/popover.css +1 -1
  209. package/dist/recordpage.css +5 -5
  210. package/dist/recordstable.css +5 -14
  211. package/dist/select.css +10 -4
  212. package/dist/select.d.ts +2 -1
  213. package/dist/sidebar.css +1 -1
  214. package/dist/statcard.css +6 -0
  215. package/dist/statcard.d.ts +2 -2
  216. package/dist/styles.css +237 -37
  217. package/dist/table.css +26 -0
  218. package/dist/table.d.ts +8 -0
  219. package/dist/tabletoolbar.css +70 -0
  220. package/dist/tabletoolbar.d.ts +7 -0
  221. package/dist/tokens.css +24 -0
  222. package/dist/types/components/bulkbar/bulkbar.d.ts +32 -0
  223. package/dist/types/components/bulkbar/bulkbar.recipe.d.ts +3 -0
  224. package/dist/types/components/bulkbar/index.d.ts +2 -0
  225. package/dist/types/components/datatable/datatable.d.ts +98 -0
  226. package/dist/types/components/datatable/datatable.recipe.d.ts +3 -0
  227. package/dist/types/components/datatable/index.d.ts +2 -0
  228. package/dist/types/components/select/select.d.ts +3 -1
  229. package/dist/types/components/select/select.recipe.d.ts +12 -0
  230. package/dist/types/components/statcard/statcard.d.ts +6 -1
  231. package/dist/types/components/statcard/statcard.recipe.d.ts +12 -2
  232. package/dist/types/components/table/index.d.ts +2 -0
  233. package/dist/types/components/table/table.d.ts +53 -0
  234. package/dist/types/components/table/table.recipe.d.ts +21 -0
  235. package/dist/types/components/tabletoolbar/index.d.ts +2 -0
  236. package/dist/types/components/tabletoolbar/tabletoolbar.chrome.d.ts +2 -0
  237. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +63 -0
  238. package/dist/types/components/tabletoolbar/tabletoolbar.recipe.d.ts +3 -0
  239. package/dist/types/index.d.ts +4 -0
  240. package/dist/types/product/recordstable/company-logos.d.ts +1 -0
  241. package/dist/types/product/recordstable/recordstable.d.ts +44 -1
  242. package/dist/types/product/sidebar/index.d.ts +1 -1
  243. package/dist/types/product/sidebar/sidebar.d.ts +85 -0
  244. package/dist/types/tokens/contract.d.ts +12 -0
  245. package/dist/types/tokens/palette.d.ts +48 -0
  246. package/dist/types/tokens/semantic.d.ts +12 -0
  247. package/dist/viewtoggle.css +2 -2
  248. package/package.json +259 -9
  249. package/dist/index.js +0 -9653
@@ -0,0 +1,79 @@
1
+ import { contract as c } from "../../tokens/index.js";
2
+ // The measured chrome of Attio's table toolbar: the Sort/Filter controls, the
3
+ // menu/popover surface they open into, the sort-condition rows, and the filter
4
+ // chips. It lives here, parameterised by class prefix, because TWO surfaces
5
+ // render it — the standalone `TableToolbar` primitive and `RecordsTable`, whose
6
+ // markup predates it and must keep its own `recordstable-` class names.
7
+ //
8
+ // These strings were MOVED out of `recordstable.recipe.ts` verbatim, not
9
+ // retyped: every value is still the one measured from Attio. Do not "tidy" them.
10
+ //
11
+ // It comes in two halves because RecordsTable interleaves three rules of its own
12
+ // (the context-menu z-index and the column drop-target) into the middle of the
13
+ // block, and those depend on following `.menu` in source order. Keeping the
14
+ // split preserves that cascade exactly, which is what lets the generated CSS
15
+ // stay byte-for-byte identical to what shipped before the extraction.
16
+ export const toolbarChromeHead = (p) => ` & .${p}-toolbar { display: flex; align-items: center; gap: ${c.space["1.5"]}; padding: 0 ${c.space["3"]} ${c.space["2"]}; }
17
+ & .${p}-search { display: flex; align-items: center; gap: ${c.space["1.5"]}; height: ${c.size.row}; padding: 0 ${c.space["2.5"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; color: ${c.fgMuted}; }
18
+ & .${p}-search svg { flex-shrink: 0; }
19
+ & .${p}-search input { border: none; outline: none; background: transparent; font-family: inherit; font-size: ${c.text.md}; color: ${c.fgPrimary}; width: 180px; }
20
+ & .${p}-search input::placeholder { color: ${c.fgMuted}; }
21
+ & .${p}-tool { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.row}; margin: 0; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px ${c.borderDefault}; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; cursor: pointer; transition: background-color .12s; }
22
+ & .${p}-tool:hover { background: ${c.bgRaised}; }
23
+ & .${p}-tool svg { flex-shrink: 0; color: ${c.fgMuted}; }
24
+ & .${p}-tool--active { background: ${c.bgRaised}; }
25
+ & .${p}-tool--ghost { box-shadow: none; background: color-mix(in srgb, ${c.fgPrimary} 3%, transparent); color: ${c.fgMuted}; }
26
+ & .${p}-tool--ghost:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
27
+ & .${p}-toolgroup { position: relative; display: inline-flex; }
28
+ & .${p}-menu { position: absolute; top: calc(100% + ${c.space["1"]}); left: 0; z-index: 30; min-width: 232px; padding: ${c.space["1"]}; border-radius: ${c.radius.lg}; background: ${c.bgPopover}; box-shadow: inset 0 0 0 1px ${c.ringPopover}, 0 0 0 1px ${c.shadowPopover1}, 0 4px 8px -4px ${c.shadowPopover2}, 0 4px 12px -2px ${c.shadowPopover3}; display: flex; flex-direction: column; gap: ${c.space.px}; transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }
29
+ & .${p}-menu { @starting-style { opacity: 0; transform: scale(0.96); } }`;
30
+ export const toolbarChromeBody = (p) => ` & .${p}-menu-head { padding: ${c.space["1"]} ${c.space["2"]} ${c.space["0.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
31
+ & .${p}-sortpop { min-width: 460px; overflow: visible; padding: ${c.space["1.5"]}; gap: ${c.space["1"]}; }
32
+ & .${p}-sort-empty { padding: ${c.space["1"]} ${c.space["1"]} ${c.space["2"]}; font-size: ${c.text.md}; color: ${c.fgMuted}; }
33
+ & .${p}-sort-row { display: flex; align-items: center; gap: ${c.space["1.5"]}; }
34
+ & .${p}-sort-grip { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 28px; flex-shrink: 0; margin-right: calc(-1 * ${c.space["1"]}); padding: 0; border: none; background: none; color: ${c.fgMuted}; cursor: grab; }
35
+ & .${p}-sort-grip:active { cursor: grabbing; }
36
+ & .${p}-sort-grip:hover { color: ${c.fgPrimary}; }
37
+ & .${p}-sort-optlabel { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; min-width: 0; }
38
+ & .${p}-sort-optlabel svg { flex-shrink: 0; color: ${c.fgMuted}; }
39
+ & .${p}-sort-field { flex: 1; min-width: 0; }
40
+ & .${p}-sort-dir { width: 136px; flex-shrink: 0; }
41
+ & .${p}-sort-field .select-trigger, & .${p}-sort-dir .select-trigger { height: 28px; padding-right: ${c.space["1"]}; }
42
+ & .${p}-sort-remove { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; margin: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgMuted}; cursor: pointer; transition: background-color .12s, color .12s; }
43
+ & .${p}-sort-remove:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
44
+ & .${p}-sort-addrow { display: flex; padding: ${c.space["0.5"]} 0; }
45
+ & .${p}-sort-add { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 28px; padding: 0 ${c.space["2"]} 0 ${c.space["1.5"]}; margin: 0; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.ringNeutral}, 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.regular}; color: ${c.fgPrimary}; cursor: pointer; transition: background-color .12s; }
46
+ & .${p}-sort-add:hover { background: ${c.bgRaised}; }
47
+ & .${p}-sort-add svg { flex-shrink: 0; color: ${c.fgMuted}; }
48
+ & .${p}-sort-foot { display: flex; padding: ${c.space["1.5"]} ${c.space["1"]} ${c.space["0.5"]}; }
49
+ & .${p}-sort-learn { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.lg}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); text-decoration: none; }
50
+ & .${p}-sort-learn svg { flex-shrink: 0; }
51
+ & .${p}-sort-learn:hover { color: ${c.fgPrimary}; }
52
+ & .${p}-menu-item { display: flex; align-items: center; gap: ${c.space["2"]}; width: 100%; height: ${c.size.row}; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; border-radius: ${c.radius.md}; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: left; cursor: pointer; }
53
+ & .${p}-menu-item:hover { background: ${c.bgRaised}; }
54
+ & .${p}-menu-item:disabled { opacity: 0.4; cursor: default; }
55
+ & .${p}-menu-item:disabled:hover { background: none; }
56
+ & .${p}-menu-item svg { flex-shrink: 0; color: ${c.fgMuted}; }
57
+ & .${p}-menu-item[data-on] svg { color: ${c.bgBrandSolid}; }
58
+ & .${p}-menu-item .${p}-menu-check { margin-left: auto; }
59
+ & .${p}-menu-input { height: ${c.size.row}; margin: ${c.space["0.5"]}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; background: ${c.bgSurface}; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; outline: none; }
60
+ & .${p}-menu-sep { height: 1px; background: ${c.borderSubtle}; margin: ${c.space["0.5"]} 0; }
61
+ & .${p}-menu--up { top: auto; bottom: calc(100% + ${c.space["1"]}); transform-origin: bottom; }
62
+ & .${p}-menu--right { left: auto; right: 0; }
63
+ & .${p}-menu--fixed { position: fixed; top: auto; left: auto; z-index: 40; }
64
+ & .${p}-menu--hdr { min-width: 200px; }
65
+ & .${p}-menu-item--danger { color: ${c.bgDanger}; }
66
+ & .${p}-menu-item--danger svg { color: ${c.bgDanger}; }
67
+ & .${p}-menu-item--danger:hover { background: color-mix(in srgb, ${c.bgDanger} 10%, transparent); }
68
+ & .${p}-menu-check { margin-left: auto; color: ${c.bgBrandSolid}; }
69
+ & .${p}-fchip { display: inline-flex; align-items: center; height: ${c.size.row}; padding-right: ${c.space["1"]}; border-radius: ${c.radius.md}; box-shadow: inset 0 0 0 1px ${c.borderDefault}; background: ${c.bgSurface}; font-size: ${c.text.md}; color: ${c.fgPrimary}; white-space: nowrap; }
70
+ & .${p}-fchip-main { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 100%; margin: 0; padding: 0 ${c.space["1"]} 0 ${c.space["2"]}; border: none; background: none; font-family: inherit; font-size: ${c.text.md}; color: ${c.fgPrimary}; cursor: pointer; }
71
+ & .${p}-fchip-field { font-weight: ${c.weight.medium}; }
72
+ & .${p}-fchip-op { color: ${c.fgMuted}; }
73
+ & .${p}-fchip-val { font-weight: ${c.weight.medium}; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
74
+ & .${p}-fchip-x { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; padding: 0; border: none; border-radius: ${c.radius.sm}; background: none; color: ${c.fgMuted}; cursor: pointer; }
75
+ & .${p}-fchip-x:hover { background: color-mix(in srgb, ${c.fgPrimary} 10%, transparent); color: ${c.fgPrimary}; }
76
+ & .${p}-spacer { flex: 1; }
77
+ & .${p}-count { font-size: ${c.text.md}; color: ${c.fgMuted}; }
78
+ & .${p}-new { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.row}; padding: 0 ${c.space["2.5"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgBrandSolid}; color: ${c.fgOnBrand}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; cursor: pointer; transition: background-color .12s; }
79
+ & .${p}-new:hover { background: ${c.bgBrandSolidPressed}; }`;
@@ -0,0 +1,191 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, CircleHelp, GripVertical, ListFilter, Plus, X, } from "lucide-react";
3
+ import * as React from "react";
4
+ import { Select } from "../select/index.js";
5
+ import { tabletoolbarRecipe } from "./tabletoolbar.recipe.js";
6
+ const OP_LABELS = {
7
+ contains: "contains",
8
+ is: "is",
9
+ isnot: "is not",
10
+ empty: "is empty",
11
+ notempty: "is not empty",
12
+ };
13
+ const OP_ORDER = [
14
+ "contains",
15
+ "is",
16
+ "isnot",
17
+ "empty",
18
+ "notempty",
19
+ ];
20
+ const opNeedsValue = (op) => op === "contains" || op === "is" || op === "isnot";
21
+ const isEmptyValue = (v) => v == null || v === "" || (Array.isArray(v) && v.length === 0);
22
+ const asText = (v) => {
23
+ if (isEmptyValue(v))
24
+ return "";
25
+ if (Array.isArray(v))
26
+ return v.join(", ");
27
+ if (typeof v === "object") {
28
+ const name = v.name;
29
+ return typeof name === "string" ? name : "";
30
+ }
31
+ return String(v);
32
+ };
33
+ /**
34
+ * Applies a toolbar's filter conditions to any row array. Reads the raw value
35
+ * at `row[key]` (override with `get` for derived fields) — never a formatted
36
+ * display string, so a grouped-thousands "$1,200" still matches "1200".
37
+ */
38
+ export function applyFilters(rows, filters, get = (row, key) => row[key]) {
39
+ const active = filters.filter((f) => !opNeedsValue(f.op) || f.value.trim() !== "");
40
+ if (!active.length)
41
+ return rows;
42
+ return rows.filter((row) => active.every((f) => {
43
+ const raw = get(row, f.key);
44
+ if (f.op === "empty")
45
+ return isEmptyValue(raw);
46
+ if (f.op === "notempty")
47
+ return !isEmptyValue(raw);
48
+ const t = asText(raw).toLowerCase();
49
+ const v = f.value.trim().toLowerCase();
50
+ if (f.op === "contains")
51
+ return t.includes(v);
52
+ if (f.op === "is")
53
+ return t === v;
54
+ return t !== v;
55
+ }));
56
+ }
57
+ /**
58
+ * Applies a toolbar's sorts to any row array, in order — sort 0 wins, later
59
+ * sorts break ties. Numbers compare numerically, everything else by locale.
60
+ */
61
+ export function applySorts(rows, sorts, get = (row, key) => row[key]) {
62
+ if (!sorts.length)
63
+ return rows;
64
+ return [...rows].sort((a, b) => {
65
+ for (const s of sorts) {
66
+ const av = get(a, s.key);
67
+ const bv = get(b, s.key);
68
+ let d = 0;
69
+ if (typeof av === "number" && typeof bv === "number")
70
+ d = av - bv;
71
+ else
72
+ d = asText(av).localeCompare(asText(bv));
73
+ if (d !== 0)
74
+ return s.dir === "asc" ? d : -d;
75
+ }
76
+ return 0;
77
+ });
78
+ }
79
+ /**
80
+ * Attio's table toolbar as a standalone primitive: the Sort and Filter
81
+ * controls, their menus, the sort-condition rows and the filter chips.
82
+ *
83
+ * `RecordsTable` renders this same measured chrome internally; this is the
84
+ * composable version, to put above `DataTable` or any table of your own. Pair
85
+ * it with `applyFilters` / `applySorts` to turn its state into rows.
86
+ */
87
+ export function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, }) {
88
+ const [sortsState, setSortsState] = React.useState(defaultSorts ?? []);
89
+ const [filtersState, setFiltersState] = React.useState(defaultFilters ?? []);
90
+ const sorts = sortsProp ?? sortsState;
91
+ const filters = filtersProp ?? filtersState;
92
+ const putSorts = (next) => {
93
+ if (sortsProp === undefined)
94
+ setSortsState(next);
95
+ onSortsChange?.(next);
96
+ };
97
+ const putFilters = (next) => {
98
+ if (filtersProp === undefined)
99
+ setFiltersState(next);
100
+ onFiltersChange?.(next);
101
+ };
102
+ const [openMenu, setOpenMenu] = React.useState(null);
103
+ const [dragSort, setDragSort] = React.useState(null);
104
+ const [filterQuery, setFilterQuery] = React.useState("");
105
+ // close any open menu on outside click (anything not inside a menu or its
106
+ // trigger) or Escape
107
+ React.useEffect(() => {
108
+ if (!openMenu)
109
+ return;
110
+ const onDown = (e) => {
111
+ const t = e.target;
112
+ if (!t.closest(".tabletoolbar-menu, [data-menu-trigger]"))
113
+ setOpenMenu(null);
114
+ };
115
+ const onKey = (e) => e.key === "Escape" && setOpenMenu(null);
116
+ document.addEventListener("mousedown", onDown);
117
+ document.addEventListener("keydown", onKey);
118
+ return () => {
119
+ document.removeEventListener("mousedown", onDown);
120
+ document.removeEventListener("keydown", onKey);
121
+ };
122
+ }, [openMenu]);
123
+ const addSort = () => {
124
+ const used = new Set(sorts.map((s) => s.key));
125
+ const col = columns.find((c) => !used.has(c.key));
126
+ if (col)
127
+ putSorts([...sorts, { key: col.key, dir: "asc" }]);
128
+ };
129
+ const moveSort = (from, to) => {
130
+ if (from === to)
131
+ return;
132
+ const next = [...sorts];
133
+ const [moved] = next.splice(from, 1);
134
+ next.splice(to, 0, moved);
135
+ putSorts(next);
136
+ };
137
+ const setSortField = (i, key) => putSorts(sorts.map((s, j) => (j === i ? { ...s, key } : s)));
138
+ const setSortDir = (i, dir) => putSorts(sorts.map((s, j) => (j === i ? { ...s, dir } : s)));
139
+ const removeSort = (i) => putSorts(sorts.filter((_, j) => j !== i));
140
+ const addFilter = (key) => {
141
+ const id = `f${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
142
+ putFilters([...filters, { id, key, op: "contains", value: "" }]);
143
+ setFilterQuery("");
144
+ setOpenMenu(`fcond:${id}`);
145
+ };
146
+ const patchFilter = (id, patch) => putFilters(filters.map((c) => (c.id === id ? { ...c, ...patch } : c)));
147
+ const removeFilter = (id) => {
148
+ putFilters(filters.filter((c) => c.id !== id));
149
+ setOpenMenu(null);
150
+ };
151
+ const activeFilters = filters.length;
152
+ const sortedLabel = sorts.length
153
+ ? `Sorted by ${columns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
154
+ : "Sort";
155
+ return (_jsx("div", { className: tabletoolbarRecipe({ className }), children: _jsxs("div", { className: "tabletoolbar-toolbar", children: [_jsxs("span", { className: "tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `tabletoolbar-tool${sorts.length ? " tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && (_jsxs("div", { className: "tabletoolbar-menu tabletoolbar-sortpop", role: "dialog", "aria-label": "Sort", children: [sorts.length === 0 ? (_jsx("div", { className: "tabletoolbar-sort-empty", children: "No sorts applied to this view." })) : (sorts.map((s, i) => (_jsxs("div", { className: "tabletoolbar-sort-row", onDragOver: (e) => {
156
+ if (dragSort !== null)
157
+ e.preventDefault();
158
+ }, onDrop: () => {
159
+ if (dragSort !== null)
160
+ moveSort(dragSort, i);
161
+ setDragSort(null);
162
+ }, children: [_jsx("button", { type: "button", className: "tabletoolbar-sort-grip", "aria-label": "Reorder sort", draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), _jsx(Select, { className: "tabletoolbar-sort-field", searchable: true, value: s.key, onValueChange: (k) => setSortField(i, k), options: columns.map((c) => ({
163
+ value: c.key,
164
+ label: c.icon ? (_jsxs("span", { className: "tabletoolbar-sort-optlabel", children: [_jsx(c.icon, { size: 15, strokeWidth: 1.75 }), c.label] })) : (c.label),
165
+ keywords: c.label,
166
+ })), "aria-label": "Sort field" }), _jsx(Select, { className: "tabletoolbar-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
167
+ {
168
+ value: "asc",
169
+ label: (_jsxs("span", { className: "tabletoolbar-sort-optlabel", children: [_jsx(ArrowUpNarrowWide, { size: 15, strokeWidth: 1.75 }), "Ascending"] })),
170
+ keywords: "Ascending",
171
+ },
172
+ {
173
+ value: "desc",
174
+ label: (_jsxs("span", { className: "tabletoolbar-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), "Descending"] })),
175
+ keywords: "Descending",
176
+ },
177
+ ], "aria-label": "Sort direction" }), _jsx("button", { type: "button", className: "tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": "Remove sort", children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), "Add sort"] }) }), helpHref && (_jsx("div", { className: "tabletoolbar-sort-foot", children: _jsxs("a", { className: "tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), "Learn about sorting"] }) }))] }))] }), _jsxs("span", { className: "tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `tabletoolbar-tool tabletoolbar-tool--ghost${activeFilters ? " tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "filter" ? null : "filter"), "aria-haspopup": "menu", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), activeFilters ? `Filter · ${activeFilters}` : "Filter"] }), openMenu === "filter" && (_jsxs("div", { className: "tabletoolbar-menu", role: "menu", children: [_jsx("input", { className: "tabletoolbar-menu-input", placeholder: "Filter by\u2026", "aria-label": "Filter by field",
178
+ // biome-ignore lint/a11y/noAutofocus: focus the picker on open
179
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), columns
180
+ .filter((col) => col.label
181
+ .toLowerCase()
182
+ .includes(filterQuery.trim().toLowerCase()))
183
+ .map((col) => (_jsxs("button", { type: "button", className: "tabletoolbar-menu-item", onClick: () => addFilter(col.key), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key)))] }))] }), filters.map((cond) => {
184
+ const col = columns.find((c) => c.key === cond.key);
185
+ const open = openMenu === `fcond:${cond.id}`;
186
+ const hasValue = !opNeedsValue(cond.op) || cond.value.trim();
187
+ return (_jsxs("span", { className: "tabletoolbar-toolgroup", children: [_jsxs("span", { className: "tabletoolbar-fchip", children: [_jsxs("button", { type: "button", className: "tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "menu", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [_jsx("span", { className: "tabletoolbar-fchip-field", children: col?.label }), hasValue ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "tabletoolbar-fchip-op", children: OP_LABELS[cond.op] }), opNeedsValue(cond.op) && (_jsx("span", { className: "tabletoolbar-fchip-val", children: cond.value }))] })) : (_jsx("span", { className: "tabletoolbar-fchip-op", children: "Select condition" }))] }), _jsx("button", { type: "button", className: "tabletoolbar-fchip-x", "aria-label": `Remove ${col?.label} filter`, onClick: () => removeFilter(cond.id), children: _jsx(X, { size: 13, strokeWidth: 2 }) })] }), open && (_jsxs("div", { className: "tabletoolbar-menu", role: "menu", children: [_jsxs("div", { className: "tabletoolbar-menu-head", children: [col?.label, " condition"] }), OP_ORDER.map((op) => (_jsxs("button", { type: "button", className: "tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [OP_LABELS[op], cond.op === op ? (_jsx(Check, { className: "tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && (_jsx("input", { className: "tabletoolbar-menu-input", placeholder: "Value\u2026", "aria-label": `${col?.label} filter value`,
188
+ // biome-ignore lint/a11y/noAutofocus: focus the value field
189
+ autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) }))] }))] }, cond.id));
190
+ }), _jsx("span", { className: "tabletoolbar-spacer" }), count != null && _jsx("span", { className: "tabletoolbar-count", children: count }), children] }) }));
191
+ }
@@ -0,0 +1,23 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ import { toolbarChromeBody, toolbarChromeHead } from "./tabletoolbar.chrome.js";
4
+ // Attio's table toolbar, measured: 28px controls, the Sort/Filter buttons and
5
+ // the menus they open, sort-condition rows, and the filter chips. The chrome
6
+ // itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
7
+ // same measured surface under its own class prefix — one source, two prefixes,
8
+ // so the two can never drift apart again.
9
+ //
10
+ // The root is `display: contents` on purpose: the emitted `-toolbar` rule is
11
+ // the flex row, so the wrapper contributes no box of its own and a TableToolbar
12
+ // lays out exactly like the bare toolbar it replaces.
13
+ const config = defineRecipe({
14
+ name: "tabletoolbar",
15
+ base: `
16
+ display: contents; font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
17
+ & * { box-sizing: border-box; }
18
+ ${toolbarChromeHead("tabletoolbar")}
19
+ ${toolbarChromeBody("tabletoolbar")}
20
+ `,
21
+ });
22
+ export const tabletoolbarRecipe = resolveRecipe(config);
23
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { Tabs } from "./tabs.js";
2
+ export { tabsRecipe } from "./tabs.recipe.js";
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { tabsRecipe } from "./tabs.recipe.js";
4
+ /**
5
+ * A row of text tabs over a sliding ink underline — the record-view tab bar,
6
+ * measured from Attio. Roving-tabindex keyboard model (←/→ move + activate,
7
+ * Home/End jump); activation follows focus, matching Attio. Controlled via
8
+ * `value`/`onValueChange`, or uncontrolled from `defaultValue`.
9
+ */
10
+ export function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, className, "aria-label": ariaLabel, }) {
11
+ const [internal, setInternal] = React.useState(defaultValue ?? tabs[0]?.value);
12
+ const active = value ?? internal;
13
+ const listRef = React.useRef(null);
14
+ const [indicator, setIndicator] = React.useState(null);
15
+ const select = React.useCallback((v) => {
16
+ if (value === undefined)
17
+ setInternal(v);
18
+ onValueChange?.(v);
19
+ }, [value, onValueChange]);
20
+ // Position the underline under the active tab; re-measure on tab/value change
21
+ // and on resize.
22
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `tabs` re-measures when the tab set (labels/counts) changes width
23
+ React.useEffect(() => {
24
+ const list = listRef.current;
25
+ if (!list)
26
+ return;
27
+ const measure = () => {
28
+ const el = list.querySelector(`[data-value="${CSS.escape(active)}"]`);
29
+ if (el)
30
+ setIndicator({ left: el.offsetLeft, width: el.offsetWidth });
31
+ };
32
+ measure();
33
+ const ro = new ResizeObserver(measure);
34
+ ro.observe(list);
35
+ return () => ro.disconnect();
36
+ }, [active, tabs]);
37
+ const onKeyDown = (e) => {
38
+ const enabled = tabs.filter((t) => !t.disabled);
39
+ const idx = enabled.findIndex((t) => t.value === active);
40
+ let next = -1;
41
+ if (e.key === "ArrowRight")
42
+ next = (idx + 1) % enabled.length;
43
+ else if (e.key === "ArrowLeft")
44
+ next = (idx - 1 + enabled.length) % enabled.length;
45
+ else if (e.key === "Home")
46
+ next = 0;
47
+ else if (e.key === "End")
48
+ next = enabled.length - 1;
49
+ if (next < 0)
50
+ return;
51
+ e.preventDefault();
52
+ const nextValue = enabled[next]?.value;
53
+ if (!nextValue)
54
+ return;
55
+ select(nextValue);
56
+ listRef.current
57
+ ?.querySelector(`[data-value="${CSS.escape(nextValue)}"]`)
58
+ ?.focus();
59
+ };
60
+ return (_jsxs("div", { className: tabsRecipe({ className }), children: [_jsxs("div", { ref: listRef, className: "tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && _jsx("span", { className: "tabs-leading", children: leading }), tabs.map((t) => {
61
+ const selected = t.value === active;
62
+ return (_jsxs("button", { type: "button", role: "tab", id: `tab-${t.value}`, "aria-selected": selected, "aria-controls": `panel-${t.value}`, tabIndex: selected ? 0 : -1, "data-value": t.value, "data-selected": selected ? "" : undefined, disabled: t.disabled, className: "tabs-tab", onClick: () => select(t.value), children: [t.icon, _jsx("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && (_jsx("span", { className: "tabs-tab-badge", children: t.badge }))] }, t.value));
63
+ }), indicator && (_jsx("span", { className: "tabs-indicator", style: {
64
+ width: indicator.width,
65
+ transform: `translateX(${indicator.left}px)`,
66
+ } }))] }), children && (_jsx("div", { className: "tabs-panel", role: "tabpanel", id: `panel-${active}`, "aria-labelledby": `tab-${active}`,
67
+ // biome-ignore lint/a11y/noNoninteractiveTabindex: a tabpanel is focusable per the WAI-ARIA tabs pattern
68
+ tabIndex: 0, children: children(active) }))] }));
69
+ }
70
+ Tabs.displayName = "Tabs";
@@ -0,0 +1,33 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // Attio's record-view tabs: a row of segmented tab buttons over a 1px baseline.
4
+ // The active tab is a light pill — bg #fbfbfb + inset ring borderSubtle, r8 —
5
+ // with a 2px ink underline that slides along the baseline; inactive tabs are
6
+ // muted and fill to a subtle pill on hover. Each tab carries a 16px leading icon
7
+ // and an optional count badge. Measured from the live app: 28px pills, 4/8
8
+ // padding, 6px icon-label gap, 14/500 labels, 10px badges (rgba(0,0,0,.04)).
9
+ const config = defineRecipe({
10
+ name: "tabs",
11
+ base: `
12
+ display: flex; flex-direction: column; min-height: 0; font-family: ${c.font.sans};
13
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
14
+
15
+ & .tabs-list { position: relative; display: flex; align-items: center; gap: ${c.space["1"]}; height: 40px; flex-shrink: 0; box-shadow: inset 0 -1px 0 ${c.borderSubtle}; }
16
+ & .tabs-leading { display: inline-flex; align-items: center; margin-right: ${c.space["1"]}; }
17
+ & .tabs-tab { position: relative; display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 28px; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; box-shadow: inset 0 0 0 1px transparent; border-radius: ${c.radius.md}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.14px; color: ${c.fgMuted}; cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease, box-shadow .12s ease; }
18
+ & .tabs-tab svg { flex-shrink: 0; opacity: 0.85; }
19
+ & .tabs-tab:hover:not([data-selected]) { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgPrimary}; }
20
+ & .tabs-tab[data-selected] { color: ${c.fgPrimary}; background: ${c.bgRaised}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; }
21
+ & .tabs-tab:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 1px; }
22
+ & .tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); font-size: ${c.text.xs}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
23
+ & .tabs-tab[data-selected] .tabs-tab-badge { color: ${c.fgPrimary}; }
24
+ & .tabs-indicator { position: absolute; bottom: 0; left: 0; height: 2px; background: ${c.fgPrimary}; border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
25
+ & .tabs-panel { flex: 1; min-height: 0; }
26
+ & .tabs-panel:focus-visible { outline: none; }
27
+ `,
28
+ variants: {},
29
+ defaultVariants: {},
30
+ compoundVariants: [],
31
+ });
32
+ export const tabsRecipe = Object.assign(resolveRecipe(config), { config });
33
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { Textarea } from "./textarea.js";
2
+ export { textareaRecipe } from "./textarea.recipe.js";
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { textareaRecipe } from "./textarea.recipe.js";
4
+ /**
5
+ * A multiline text field — measured from Attio's settings. Forwards every native
6
+ * `<textarea>` prop; pass `showCount` with `maxLength` to render a live
7
+ * character counter (which turns red if the value exceeds the limit).
8
+ */
9
+ export const Textarea = React.forwardRef(({ showCount, maxLength, className, wrapperClassName, value, defaultValue, onChange, ...props }, ref) => {
10
+ const initial = String(value ?? defaultValue ?? "").length;
11
+ const [count, setCount] = React.useState(initial);
12
+ const len = value !== undefined ? String(value).length : count;
13
+ const counted = showCount && maxLength != null;
14
+ return (_jsxs("div", { className: textareaRecipe({ className: wrapperClassName }), "data-counted": counted ? "" : undefined, children: [_jsx("textarea", { ref: ref, className: `textarea-field${className ? ` ${className}` : ""}`, maxLength: maxLength, value: value, defaultValue: defaultValue, onChange: (e) => {
15
+ if (value === undefined)
16
+ setCount(e.target.value.length);
17
+ onChange?.(e);
18
+ }, ...props }), counted && (_jsxs("span", { className: "textarea-count", "data-over": len > (maxLength ?? 0) ? "" : undefined, "aria-hidden": "true", children: [len, " / ", maxLength] }))] }));
19
+ });
20
+ Textarea.displayName = "Textarea";
@@ -0,0 +1,31 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A multiline text field, measured from Attio's settings "Personal context": an
4
+ // inset 1px ring (radius 8), 14px text, 6/10 padding, and an optional character
5
+ // counter pinned bottom-right (12px, muted). Focus rings the field brand.
6
+ const config = defineRecipe({
7
+ name: "textarea",
8
+ base: `
9
+ position: relative; display: block; width: 100%; font-family: ${c.font.sans};
10
+
11
+ & .textarea-field {
12
+ box-sizing: border-box; display: block; width: 100%; min-height: 80px; resize: vertical;
13
+ margin: 0; padding: ${c.space["1.5"]} ${c.space["2.5"]}; border: none; border-radius: ${c.radius.md};
14
+ background: ${c.bgSurface}; box-shadow: 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04);
15
+ font-family: inherit; font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgPrimary};
16
+ outline: none; transition: box-shadow .15s ease;
17
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
18
+ }
19
+ & .textarea-field::placeholder { color: ${c.fgMuted}; }
20
+ & .textarea-field:focus { box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
21
+ & .textarea-field:disabled { opacity: 0.5; cursor: not-allowed; }
22
+ &[data-counted] .textarea-field { padding-bottom: ${c.space["5"]}; }
23
+ & .textarea-count { position: absolute; right: ${c.space["2.5"]}; bottom: ${c.space["1.5"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; pointer-events: none; }
24
+ & .textarea-count[data-over] { color: ${c.bgDanger}; }
25
+ `,
26
+ variants: {},
27
+ defaultVariants: {},
28
+ compoundVariants: [],
29
+ });
30
+ export const textareaRecipe = Object.assign(resolveRecipe(config), { config });
31
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { ToastProvider, useToast, } from "./toast.js";
2
+ export { toastRecipe } from "./toast.recipe.js";
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CircleAlert, CircleCheck, Info, TriangleAlert, X } from "lucide-react";
3
+ import * as React from "react";
4
+ import { toastRecipe } from "./toast.recipe.js";
5
+ const ToastContext = React.createContext(null);
6
+ const ICONS = {
7
+ info: Info,
8
+ success: CircleCheck,
9
+ warning: TriangleAlert,
10
+ danger: CircleAlert,
11
+ };
12
+ function ToastCard({ item, onClose, }) {
13
+ const Icon = ICONS[item.tone ?? "info"];
14
+ return (_jsxs("div", { className: "toast", role: "status", "data-tone": item.tone ?? "info", children: [_jsx("span", { className: "toast-icon", children: _jsx(Icon, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsxs("div", { className: "toast-body", children: [_jsx("div", { className: "toast-title", children: item.title }), item.description != null && (_jsx("div", { className: "toast-desc", children: item.description })), item.action && (_jsx("button", { type: "button", className: "toast-action", onClick: () => {
15
+ item.action?.onClick();
16
+ onClose();
17
+ }, children: item.action.label }))] }), _jsx("button", { type: "button", className: "toast-close", "aria-label": "Dismiss", onClick: onClose, children: _jsx(X, { size: 15, strokeWidth: 2, "aria-hidden": "true" }) })] }));
18
+ }
19
+ /**
20
+ * Wrap your app in a `ToastProvider`, then call `useToast().toast({...})` from
21
+ * anywhere to push a notification. Toasts stack bottom-right and auto-dismiss
22
+ * after `duration` (default 4s; 0 keeps them until dismissed).
23
+ */
24
+ export function ToastProvider({ children, duration = 4000, }) {
25
+ const [items, setItems] = React.useState([]);
26
+ const idRef = React.useRef(0);
27
+ const timers = React.useRef(new Map());
28
+ const dismiss = React.useCallback((id) => {
29
+ setItems((list) => list.filter((t) => t.id !== id));
30
+ const t = timers.current.get(id);
31
+ if (t) {
32
+ clearTimeout(t);
33
+ timers.current.delete(id);
34
+ }
35
+ }, []);
36
+ const toast = React.useCallback((o) => {
37
+ idRef.current += 1;
38
+ const id = idRef.current;
39
+ setItems((list) => [...list, { ...o, id }]);
40
+ const d = o.duration ?? duration;
41
+ if (d > 0)
42
+ timers.current.set(id, setTimeout(() => dismiss(id), d));
43
+ return id;
44
+ }, [duration, dismiss]);
45
+ React.useEffect(() => {
46
+ const map = timers.current;
47
+ return () => map.forEach(clearTimeout);
48
+ }, []);
49
+ const api = React.useMemo(() => ({ toast, dismiss }), [toast, dismiss]);
50
+ return (_jsxs(ToastContext.Provider, { value: api, children: [children, _jsx("div", { className: toastRecipe(), children: _jsx("div", { className: "toast-viewport", role: "region", "aria-label": "Notifications", children: items.map((t) => (_jsx(ToastCard, { item: t, onClose: () => dismiss(t.id) }, t.id))) }) })] }));
51
+ }
52
+ ToastProvider.displayName = "ToastProvider";
53
+ export function useToast() {
54
+ const ctx = React.useContext(ToastContext);
55
+ if (!ctx)
56
+ throw new Error("useToast must be used within a ToastProvider");
57
+ return ctx;
58
+ }
@@ -0,0 +1,28 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // Toast notifications — a fixed bottom-right stack of cards on the measured
4
+ // popover surface, each with a tone icon, title/description, optional action,
5
+ // and a close button.
6
+ const config = defineRecipe({
7
+ name: "toast",
8
+ base: `display: contents; font-family: ${c.font.sans};
9
+ & .toast-viewport { position: fixed; z-index: 80; right: ${c.space["4"]}; bottom: ${c.space["4"]}; display: flex; flex-direction: column; gap: ${c.space["2"]}; width: 360px; max-width: calc(100vw - ${c.space["8"]}); pointer-events: none; }
10
+ & .toast { pointer-events: auto; display: flex; align-items: flex-start; gap: ${c.space["2.5"]}; box-sizing: border-box; padding: ${c.space["3"]}; border-radius: ${c.radius.xl}; background: ${c.bgRaised}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 5%, transparent), 0 8px 28px -6px rgba(28,40,64,0.16), 0 2px 6px -2px rgba(28,40,64,0.10); opacity: 1; transform: translateY(0); transition: opacity 0.15s ease, transform 0.15s ease; @starting-style { opacity: 0; transform: translateY(8px); } }
11
+ & .toast-icon { flex-shrink: 0; margin-top: 1px; display: inline-flex; color: ${c.fgMuted}; }
12
+ & .toast[data-tone="info"] .toast-icon { color: ${c.bgBrandSolid}; }
13
+ & .toast[data-tone="success"] .toast-icon { color: ${c.bgSuccess}; }
14
+ & .toast[data-tone="warning"] .toast-icon { color: ${c.bgWarning}; }
15
+ & .toast[data-tone="danger"] .toast-icon { color: ${c.bgDanger}; }
16
+ & .toast-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
17
+ & .toast-title { font-size: ${c.text.md}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
18
+ & .toast-desc { font-size: ${c.text.md}; color: ${c.fgMuted}; }
19
+ & .toast-action { align-self: flex-start; margin-top: ${c.space["1.5"]}; height: 24px; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; cursor: pointer; }
20
+ & .toast-action:hover { background: color-mix(in srgb, ${c.fgPrimary} 10%, transparent); }
21
+ & .toast-close { flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin: -2px -2px 0 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgMuted}; cursor: pointer; }
22
+ & .toast-close:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }`,
23
+ variants: {},
24
+ defaultVariants: {},
25
+ compoundVariants: [],
26
+ });
27
+ export const toastRecipe = resolveRecipe(config);
28
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { Tooltip } from "./tooltip.js";
2
+ export { tooltipRecipe } from "./tooltip.recipe.js";
@@ -0,0 +1,45 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { tooltipRecipe } from "./tooltip.recipe.js";
5
+ /**
6
+ * A hover/focus tooltip — a dark hint chip measured from Attio. Wraps its
7
+ * trigger in an inline element, shows after `delay` on pointer-enter / focus,
8
+ * and hides on leave / blur / Escape. The bubble is portaled and positioned
9
+ * above the trigger (flipping below near the top edge).
10
+ */
11
+ export function Tooltip({ content, children, side = "top", delay = 350, }) {
12
+ const triggerRef = React.useRef(null);
13
+ const timer = React.useRef(undefined);
14
+ const [pos, setPos] = React.useState(null);
15
+ const show = () => {
16
+ const el = triggerRef.current;
17
+ if (!el)
18
+ return;
19
+ const r = el.getBoundingClientRect();
20
+ const below = side === "bottom" || r.top < 48;
21
+ setPos({
22
+ left: Math.round(r.left + r.width / 2),
23
+ top: Math.round(below ? r.bottom + 6 : r.top - 6),
24
+ below,
25
+ });
26
+ };
27
+ const open = () => {
28
+ clearTimeout(timer.current);
29
+ timer.current = setTimeout(show, delay);
30
+ };
31
+ const close = () => {
32
+ clearTimeout(timer.current);
33
+ setPos(null);
34
+ };
35
+ React.useEffect(() => () => clearTimeout(timer.current), []);
36
+ return (_jsxs(_Fragment, { children: [_jsx("span", { ref: triggerRef, style: { display: "inline-flex" }, onPointerEnter: open, onPointerLeave: close, onFocusCapture: open, onBlurCapture: close, onKeyDown: (e) => e.key === "Escape" && close(), children: children }), pos != null &&
37
+ createPortal(_jsx("div", { role: "tooltip", className: tooltipRecipe(), style: {
38
+ left: pos.left,
39
+ top: pos.top,
40
+ transform: pos.below
41
+ ? "translate(-50%, 0)"
42
+ : "translate(-50%, -100%)",
43
+ }, children: content }), document.body)] }));
44
+ }
45
+ Tooltip.displayName = "Tooltip";