@crisp-ui-kit/crisp 0.2.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 (244) hide show
  1. package/README.md +6 -0
  2. package/dist/base.css +25 -0
  3. package/dist/button.css +1 -1
  4. package/dist/command.css +1 -1
  5. package/dist/contextmenu.css +1 -1
  6. package/dist/datatable.css +60 -0
  7. package/dist/datatable.d.ts +7 -0
  8. package/dist/datepicker.css +1 -1
  9. package/dist/dialog.css +1 -1
  10. package/dist/esm/components/accordion/accordion.js +32 -0
  11. package/dist/esm/components/accordion/accordion.recipe.js +23 -0
  12. package/dist/esm/components/accordion/index.js +2 -0
  13. package/dist/esm/components/avatar/avatar.js +8 -0
  14. package/dist/esm/components/avatar/avatar.recipe.js +39 -0
  15. package/dist/esm/components/avatar/index.js +2 -0
  16. package/dist/esm/components/avatargroup/avatargroup.js +16 -0
  17. package/dist/esm/components/avatargroup/avatargroup.recipe.js +15 -0
  18. package/dist/esm/components/avatargroup/index.js +2 -0
  19. package/dist/esm/components/badge/badge.js +8 -0
  20. package/dist/esm/components/badge/badge.recipe.js +65 -0
  21. package/dist/esm/components/badge/index.js +2 -0
  22. package/dist/esm/components/barchart/barchart.js +29 -0
  23. package/dist/esm/components/barchart/barchart.recipe.js +26 -0
  24. package/dist/esm/components/barchart/index.js +2 -0
  25. package/dist/esm/components/breadcrumb/breadcrumb.js +17 -0
  26. package/dist/esm/components/breadcrumb/breadcrumb.recipe.js +18 -0
  27. package/dist/esm/components/breadcrumb/index.js +2 -0
  28. package/dist/esm/components/bulkbar/bulkbar.js +16 -0
  29. package/dist/esm/components/bulkbar/bulkbar.recipe.js +28 -0
  30. package/dist/esm/components/bulkbar/index.js +2 -0
  31. package/dist/esm/components/button/button.js +15 -0
  32. package/dist/esm/components/button/button.recipe.js +39 -0
  33. package/dist/esm/components/button/index.js +2 -0
  34. package/dist/esm/components/callout/callout.js +22 -0
  35. package/dist/esm/components/callout/callout.recipe.js +26 -0
  36. package/dist/esm/components/callout/index.js +2 -0
  37. package/dist/esm/components/card/card.js +15 -0
  38. package/dist/esm/components/card/card.recipe.js +28 -0
  39. package/dist/esm/components/card/index.js +2 -0
  40. package/dist/esm/components/checkbox/checkbox.js +20 -0
  41. package/dist/esm/components/checkbox/checkbox.recipe.js +23 -0
  42. package/dist/esm/components/checkbox/index.js +2 -0
  43. package/dist/esm/components/contextmenu/contextmenu.js +42 -0
  44. package/dist/esm/components/contextmenu/contextmenu.recipe.js +21 -0
  45. package/dist/esm/components/contextmenu/index.js +2 -0
  46. package/dist/esm/components/datatable/datatable.js +245 -0
  47. package/dist/esm/components/datatable/datatable.recipe.js +72 -0
  48. package/dist/esm/components/datatable/index.js +2 -0
  49. package/dist/esm/components/datepicker/datepicker.js +57 -0
  50. package/dist/esm/components/datepicker/datepicker.recipe.js +32 -0
  51. package/dist/esm/components/datepicker/index.js +2 -0
  52. package/dist/esm/components/dialog/dialog.js +48 -0
  53. package/dist/esm/components/dialog/dialog.recipe.js +32 -0
  54. package/dist/esm/components/dialog/index.js +2 -0
  55. package/dist/esm/components/divider/divider.js +7 -0
  56. package/dist/esm/components/divider/divider.recipe.js +14 -0
  57. package/dist/esm/components/divider/index.js +2 -0
  58. package/dist/esm/components/donutchart/donutchart.js +38 -0
  59. package/dist/esm/components/donutchart/donutchart.recipe.js +27 -0
  60. package/dist/esm/components/donutchart/index.js +2 -0
  61. package/dist/esm/components/drawer/drawer.js +42 -0
  62. package/dist/esm/components/drawer/drawer.recipe.js +27 -0
  63. package/dist/esm/components/drawer/index.js +2 -0
  64. package/dist/esm/components/emptystate/emptystate.js +7 -0
  65. package/dist/esm/components/emptystate/emptystate.recipe.js +31 -0
  66. package/dist/esm/components/emptystate/index.js +2 -0
  67. package/dist/esm/components/formfield/formfield.js +14 -0
  68. package/dist/esm/components/formfield/formfield.recipe.js +19 -0
  69. package/dist/esm/components/formfield/index.js +2 -0
  70. package/dist/esm/components/input/index.js +2 -0
  71. package/dist/esm/components/input/input.js +8 -0
  72. package/dist/esm/components/input/input.recipe.js +23 -0
  73. package/dist/esm/components/kbd/index.js +2 -0
  74. package/dist/esm/components/kbd/kbd.js +8 -0
  75. package/dist/esm/components/kbd/kbd.recipe.js +20 -0
  76. package/dist/esm/components/linechart/index.js +2 -0
  77. package/dist/esm/components/linechart/linechart.js +41 -0
  78. package/dist/esm/components/linechart/linechart.recipe.js +27 -0
  79. package/dist/esm/components/menu/index.js +2 -0
  80. package/dist/esm/components/menu/menu.js +137 -0
  81. package/dist/esm/components/menu/menu.recipe.js +51 -0
  82. package/dist/esm/components/multiselect/index.js +2 -0
  83. package/dist/esm/components/multiselect/multiselect.js +118 -0
  84. package/dist/esm/components/multiselect/multiselect.recipe.js +68 -0
  85. package/dist/esm/components/popover/index.js +2 -0
  86. package/dist/esm/components/popover/popover.js +70 -0
  87. package/dist/esm/components/popover/popover.recipe.js +18 -0
  88. package/dist/esm/components/progress/index.js +2 -0
  89. package/dist/esm/components/progress/progress.js +23 -0
  90. package/dist/esm/components/progress/progress.recipe.js +17 -0
  91. package/dist/esm/components/radiogroup/index.js +2 -0
  92. package/dist/esm/components/radiogroup/radiogroup.js +46 -0
  93. package/dist/esm/components/radiogroup/radiogroup.recipe.js +37 -0
  94. package/dist/esm/components/segmented/index.js +2 -0
  95. package/dist/esm/components/segmented/segmented.js +59 -0
  96. package/dist/esm/components/segmented/segmented.recipe.js +20 -0
  97. package/dist/esm/components/select/index.js +2 -0
  98. package/dist/esm/components/select/select.js +144 -0
  99. package/dist/esm/components/select/select.recipe.js +76 -0
  100. package/dist/esm/components/skeleton/index.js +2 -0
  101. package/dist/esm/components/skeleton/skeleton.js +7 -0
  102. package/dist/esm/components/skeleton/skeleton.recipe.js +14 -0
  103. package/dist/esm/components/spinner/index.js +2 -0
  104. package/dist/esm/components/spinner/spinner.js +11 -0
  105. package/dist/esm/components/spinner/spinner.recipe.js +13 -0
  106. package/dist/esm/components/stagebar/index.js +2 -0
  107. package/dist/esm/components/stagebar/stagebar.js +23 -0
  108. package/dist/esm/components/stagebar/stagebar.recipe.js +14 -0
  109. package/dist/esm/components/statcard/index.js +2 -0
  110. package/dist/esm/components/statcard/statcard.js +12 -0
  111. package/dist/esm/components/statcard/statcard.recipe.js +34 -0
  112. package/dist/esm/components/switch/index.js +2 -0
  113. package/dist/esm/components/switch/switch.js +22 -0
  114. package/dist/esm/components/switch/switch.recipe.js +26 -0
  115. package/dist/esm/components/table/index.js +2 -0
  116. package/dist/esm/components/table/table.js +30 -0
  117. package/dist/esm/components/table/table.recipe.js +40 -0
  118. package/dist/esm/components/tabletoolbar/index.js +2 -0
  119. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +79 -0
  120. package/dist/esm/components/tabletoolbar/tabletoolbar.js +191 -0
  121. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +23 -0
  122. package/dist/esm/components/tabs/index.js +2 -0
  123. package/dist/esm/components/tabs/tabs.js +70 -0
  124. package/dist/esm/components/tabs/tabs.recipe.js +33 -0
  125. package/dist/esm/components/textarea/index.js +2 -0
  126. package/dist/esm/components/textarea/textarea.js +20 -0
  127. package/dist/esm/components/textarea/textarea.recipe.js +31 -0
  128. package/dist/esm/components/toast/index.js +2 -0
  129. package/dist/esm/components/toast/toast.js +58 -0
  130. package/dist/esm/components/toast/toast.recipe.js +28 -0
  131. package/dist/esm/components/tooltip/index.js +2 -0
  132. package/dist/esm/components/tooltip/tooltip.js +45 -0
  133. package/dist/esm/components/tooltip/tooltip.recipe.js +13 -0
  134. package/dist/esm/core/cx.js +1 -0
  135. package/dist/esm/core/define.js +3 -0
  136. package/dist/esm/core/index.js +4 -0
  137. package/dist/esm/core/primitive.js +13 -0
  138. package/dist/esm/core/resolve.js +41 -0
  139. package/dist/esm/core/types.js +1 -0
  140. package/dist/esm/index.js +71 -0
  141. package/dist/esm/product/appshell/appshell.js +44 -0
  142. package/dist/esm/product/appshell/appshell.recipe.js +27 -0
  143. package/dist/esm/product/appshell/index.js +2 -0
  144. package/dist/esm/product/board/board.js +42 -0
  145. package/dist/esm/product/board/board.recipe.js +28 -0
  146. package/dist/esm/product/board/index.js +2 -0
  147. package/dist/esm/product/command/command.js +188 -0
  148. package/dist/esm/product/command/command.recipe.js +19 -0
  149. package/dist/esm/product/command/index.js +2 -0
  150. package/dist/esm/product/entitycard/entitycard.js +11 -0
  151. package/dist/esm/product/entitycard/entitycard.recipe.js +29 -0
  152. package/dist/esm/product/entitycard/index.js +2 -0
  153. package/dist/esm/product/flowcanvas/flowcanvas.js +85 -0
  154. package/dist/esm/product/flowcanvas/flowcanvas.recipe.js +25 -0
  155. package/dist/esm/product/flowcanvas/index.js +2 -0
  156. package/dist/esm/product/inbox/inbox.js +13 -0
  157. package/dist/esm/product/inbox/inbox.recipe.js +43 -0
  158. package/dist/esm/product/inbox/index.js +2 -0
  159. package/dist/esm/product/listpage/index.js +2 -0
  160. package/dist/esm/product/listpage/listpage.js +21 -0
  161. package/dist/esm/product/listpage/listpage.recipe.js +43 -0
  162. package/dist/esm/product/navitem/index.js +1 -0
  163. package/dist/esm/product/navitem/navitem.js +8 -0
  164. package/dist/esm/product/navitem/navitem.recipe.js +21 -0
  165. package/dist/esm/product/onboarding/index.js +2 -0
  166. package/dist/esm/product/onboarding/onboarding.js +14 -0
  167. package/dist/esm/product/onboarding/onboarding.recipe.js +41 -0
  168. package/dist/esm/product/recordpage/index.js +2 -0
  169. package/dist/esm/product/recordpage/recordpage.js +286 -0
  170. package/dist/esm/product/recordpage/recordpage.recipe.js +157 -0
  171. package/dist/esm/product/recordpanel/index.js +2 -0
  172. package/dist/esm/product/recordpanel/recordpanel.js +47 -0
  173. package/dist/esm/product/recordpanel/recordpanel.recipe.js +52 -0
  174. package/dist/esm/product/recordstable/company-logos.js +26 -0
  175. package/dist/esm/product/recordstable/index.js +2 -0
  176. package/dist/esm/product/recordstable/recordstable.js +963 -0
  177. package/dist/esm/product/recordstable/recordstable.recipe.js +149 -0
  178. package/dist/esm/product/richtext/index.js +2 -0
  179. package/dist/esm/product/richtext/richtext.js +111 -0
  180. package/dist/esm/product/richtext/richtext.recipe.js +40 -0
  181. package/dist/esm/product/settingssection/index.js +2 -0
  182. package/dist/esm/product/settingssection/settingssection.js +11 -0
  183. package/dist/esm/product/settingssection/settingssection.recipe.js +20 -0
  184. package/dist/esm/product/settingsshell/index.js +2 -0
  185. package/dist/esm/product/settingsshell/settingsshell.js +23 -0
  186. package/dist/esm/product/settingsshell/settingsshell.recipe.js +38 -0
  187. package/dist/esm/product/sidebar/index.js +2 -0
  188. package/dist/esm/product/sidebar/sidebar.js +112 -0
  189. package/dist/esm/product/sidebar/sidebar.recipe.js +18 -0
  190. package/dist/esm/product/tasklist/index.js +2 -0
  191. package/dist/esm/product/tasklist/tasklist.js +16 -0
  192. package/dist/esm/product/tasklist/tasklist.recipe.js +35 -0
  193. package/dist/esm/product/trialbanner/index.js +2 -0
  194. package/dist/esm/product/trialbanner/trialbanner.js +10 -0
  195. package/dist/esm/product/trialbanner/trialbanner.recipe.js +29 -0
  196. package/dist/esm/product/viewtoggle/index.js +2 -0
  197. package/dist/esm/product/viewtoggle/viewtoggle.js +11 -0
  198. package/dist/esm/product/viewtoggle/viewtoggle.recipe.js +24 -0
  199. package/dist/esm/tokens/contract.js +18 -0
  200. package/dist/esm/tokens/index.js +4 -0
  201. package/dist/esm/tokens/palette.js +30 -0
  202. package/dist/esm/tokens/scale.js +60 -0
  203. package/dist/esm/tokens/semantic.js +30 -0
  204. package/dist/menu.css +2 -2
  205. package/dist/multiselect.css +3 -3
  206. package/dist/popover.css +1 -1
  207. package/dist/recordpage.css +5 -5
  208. package/dist/recordstable.css +4 -4
  209. package/dist/select.css +10 -4
  210. package/dist/select.d.ts +2 -1
  211. package/dist/sidebar.css +1 -1
  212. package/dist/statcard.css +6 -0
  213. package/dist/statcard.d.ts +2 -2
  214. package/dist/styles.css +222 -27
  215. package/dist/table.css +26 -0
  216. package/dist/table.d.ts +8 -0
  217. package/dist/tabletoolbar.css +70 -0
  218. package/dist/tabletoolbar.d.ts +7 -0
  219. package/dist/tokens.css +24 -0
  220. package/dist/types/components/datatable/datatable.d.ts +98 -0
  221. package/dist/types/components/datatable/datatable.recipe.d.ts +3 -0
  222. package/dist/types/components/datatable/index.d.ts +2 -0
  223. package/dist/types/components/select/select.d.ts +3 -1
  224. package/dist/types/components/select/select.recipe.d.ts +12 -0
  225. package/dist/types/components/statcard/statcard.d.ts +6 -1
  226. package/dist/types/components/statcard/statcard.recipe.d.ts +12 -2
  227. package/dist/types/components/table/index.d.ts +2 -0
  228. package/dist/types/components/table/table.d.ts +53 -0
  229. package/dist/types/components/table/table.recipe.d.ts +21 -0
  230. package/dist/types/components/tabletoolbar/index.d.ts +2 -0
  231. package/dist/types/components/tabletoolbar/tabletoolbar.chrome.d.ts +2 -0
  232. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +63 -0
  233. package/dist/types/components/tabletoolbar/tabletoolbar.recipe.d.ts +3 -0
  234. package/dist/types/index.d.ts +3 -0
  235. package/dist/types/product/recordstable/company-logos.d.ts +1 -0
  236. package/dist/types/product/recordstable/recordstable.d.ts +42 -1
  237. package/dist/types/product/sidebar/index.d.ts +1 -1
  238. package/dist/types/product/sidebar/sidebar.d.ts +85 -0
  239. package/dist/types/tokens/contract.d.ts +12 -0
  240. package/dist/types/tokens/palette.d.ts +48 -0
  241. package/dist/types/tokens/semantic.d.ts +12 -0
  242. package/dist/viewtoggle.css +2 -2
  243. package/package.json +259 -9
  244. package/dist/index.js +0 -9690
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CalendarDays, ChevronLeft, ChevronRight } from "lucide-react";
3
+ import * as React from "react";
4
+ import { Popover } from "../popover/popover.js";
5
+ import { datepickerRecipe } from "./datepicker.recipe.js";
6
+ const DOW = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
7
+ const MONTHS = [
8
+ "January",
9
+ "February",
10
+ "March",
11
+ "April",
12
+ "May",
13
+ "June",
14
+ "July",
15
+ "August",
16
+ "September",
17
+ "October",
18
+ "November",
19
+ "December",
20
+ ];
21
+ const sameDay = (a, b) => a.getFullYear() === b.getFullYear() &&
22
+ a.getMonth() === b.getMonth() &&
23
+ a.getDate() === b.getDate();
24
+ const defaultFormat = (d) => `${MONTHS[d.getMonth()].slice(0, 3)} ${d.getDate()}, ${d.getFullYear()}`;
25
+ /**
26
+ * A date field with a calendar popover — composes [Popover] with a month grid.
27
+ * Controlled via `value`/`onChange` or self-managed from `defaultValue`.
28
+ */
29
+ export function DatePicker({ value, defaultValue, onChange, placeholder = "Set date…", variant = "default", format = defaultFormat, "aria-label": ariaLabel, className, }) {
30
+ const [internal, setInternal] = React.useState(defaultValue ?? null);
31
+ const selected = value ?? internal;
32
+ const [open, setOpen] = React.useState(false);
33
+ const [view, setView] = React.useState(() => selected ?? new Date());
34
+ // when reopening, jump the view to the selected month
35
+ React.useEffect(() => {
36
+ if (open)
37
+ setView(selected ?? new Date());
38
+ }, [open, selected]);
39
+ const pick = (d) => {
40
+ if (value === undefined)
41
+ setInternal(d);
42
+ onChange?.(d);
43
+ setOpen(false);
44
+ };
45
+ const today = new Date();
46
+ const year = view.getFullYear();
47
+ const month = view.getMonth();
48
+ const firstWeekday = new Date(year, month, 1).getDay();
49
+ const start = new Date(year, month, 1 - firstWeekday);
50
+ return (_jsx("div", { className: datepickerRecipe({ className }), children: _jsx(Popover, { open: open, onOpenChange: setOpen, "aria-label": ariaLabel ?? "Choose date", trigger: _jsxs("button", { type: "button", className: "datepicker-trigger", "data-ghost": variant === "ghost" ? "" : undefined, "data-placeholder": selected ? undefined : "", "aria-label": ariaLabel, children: [_jsx(CalendarDays, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), selected ? format(selected) : placeholder] }), children: _jsxs("div", { className: "datepicker-cal", children: [_jsxs("div", { className: "datepicker-head", children: [_jsx("button", { type: "button", className: "datepicker-nav", "aria-label": "Previous month", onClick: () => setView(new Date(year, month - 1, 1)), children: _jsx(ChevronLeft, { size: 16, strokeWidth: 1.75 }) }), _jsxs("span", { className: "datepicker-title", children: [MONTHS[month], " ", year] }), _jsx("button", { type: "button", className: "datepicker-nav", "aria-label": "Next month", onClick: () => setView(new Date(year, month + 1, 1)), children: _jsx(ChevronRight, { size: 16, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "datepicker-grid", role: "grid", children: [DOW.map((d) => (_jsx("span", { className: "datepicker-dow", "aria-hidden": "true", children: d }, d))), Array.from({ length: 42 }, (_, i) => {
51
+ const day = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i);
52
+ const outside = day.getMonth() !== month;
53
+ const isSel = selected != null && sameDay(day, selected);
54
+ return (_jsx("button", { type: "button", className: "datepicker-day", "aria-label": format(day), "aria-pressed": isSel, "data-outside": outside ? "" : undefined, "data-today": sameDay(day, today) ? "" : undefined, "data-selected": isSel ? "" : undefined, onClick: () => pick(day), children: day.getDate() }, day.toISOString()));
55
+ })] })] }) }) }));
56
+ }
57
+ DatePicker.displayName = "DatePicker";
@@ -0,0 +1,32 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A date field + calendar popover — measured from Attio's date cell editor: an
4
+ // outlined trigger showing the value, opening a month grid (weekday row, day
5
+ // cells, today outline, selected day brand-filled).
6
+ const config = defineRecipe({
7
+ name: "datepicker",
8
+ base: `display: contents; font-family: ${c.font.sans};
9
+ & .datepicker-trigger { display: inline-flex; align-items: center; gap: ${c.space["2"]}; box-sizing: border-box; height: 32px; padding: 0 ${c.space["2.5"]}; margin: 0; border: none; border-radius: ${c.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 7%, transparent); color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.lg}; white-space: nowrap; cursor: pointer; }
10
+ & .datepicker-trigger[data-ghost] { background: transparent; box-shadow: none; }
11
+ & .datepicker-trigger[data-ghost]:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); }
12
+ & .datepicker-trigger[data-placeholder] { color: ${c.fgMuted}; }
13
+ & .datepicker-trigger svg { flex-shrink: 0; color: ${c.fgMuted}; }
14
+ & .datepicker-cal { width: 244px; }
15
+ & .datepicker-head { display: flex; align-items: center; justify-content: space-between; height: 30px; padding: 0 ${c.space["1"]}; }
16
+ & .datepicker-title { font-size: ${c.text.md}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
17
+ & .datepicker-nav { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgMuted}; cursor: pointer; }
18
+ & .datepicker-nav:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
19
+ & .datepicker-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-top: ${c.space["1"]}; }
20
+ & .datepicker-dow { display: flex; align-items: center; justify-content: center; height: 24px; font-size: ${c.text.xs}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
21
+ & .datepicker-day { display: flex; align-items: center; justify-content: center; height: 30px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.md}; cursor: pointer; }
22
+ & .datepicker-day:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
23
+ & .datepicker-day[data-outside] { color: ${c.fgMuted}; opacity: 0.5; }
24
+ & .datepicker-day[data-today] { box-shadow: inset 0 0 0 1px ${c.borderDefault}; }
25
+ & .datepicker-day[data-selected] { background: ${c.bgBrandSolid}; color: ${c.fgOnBrand}; }
26
+ & .datepicker-day[data-selected]:hover { background: ${c.bgBrandSolidPressed}; }`,
27
+ variants: {},
28
+ defaultVariants: {},
29
+ compoundVariants: [],
30
+ });
31
+ export const datepickerRecipe = resolveRecipe(config);
32
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { DatePicker } from "./datepicker.js";
2
+ export { datepickerRecipe } from "./datepicker.recipe.js";
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as dialog from "@zag-js/dialog";
3
+ import { normalizeProps, Portal, useMachine } from "@zag-js/react";
4
+ import { X } from "lucide-react";
5
+ import * as React from "react";
6
+ import { dialogRecipe } from "./dialog.recipe.js";
7
+ // A globally unique id per mounted instance (see command.tsx for the rationale —
8
+ // the machine defers its dismissal listeners a frame, so tree-relative useId
9
+ // values could let a stale registration resolve to a new instance's node).
10
+ let instanceCounter = 0;
11
+ function useDialogInstanceId() {
12
+ const idRef = React.useRef(null);
13
+ if (idRef.current == null) {
14
+ instanceCounter += 1;
15
+ idRef.current = `crisp-dialog-${instanceCounter}`;
16
+ }
17
+ return idRef.current;
18
+ }
19
+ /**
20
+ * A modal dialog — composes Zag's dialog machine (focus trap, scroll lock,
21
+ * Escape / outside-click dismissal) with the measured Attio modal surface.
22
+ * Compose the body with `DialogBody` and actions with `DialogFooter`.
23
+ */
24
+ export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, children, }) {
25
+ const id = useDialogInstanceId();
26
+ const service = useMachine(dialog.machine, {
27
+ id,
28
+ open,
29
+ onOpenChange(details) {
30
+ onOpenChange(details.open);
31
+ },
32
+ // Focus the first form field on open (like the command palette focuses its
33
+ // search), rather than the close button; fall back to the machine default.
34
+ initialFocusEl: () => document.querySelector(`[data-crisp-dialog="${id}"] .dialog-body :where(input, textarea, select, [contenteditable]):not([disabled])`),
35
+ "aria-label": typeof title === "string" ? title : ariaLabel,
36
+ });
37
+ const api = dialog.connect(service, normalizeProps);
38
+ if (!api.open)
39
+ return null;
40
+ return (_jsx(Portal, { children: _jsxs("div", { className: dialogRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "dialog-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "dialog-positioner", children: _jsx("div", { ...api.getContentProps(), className: "dialog-content", "data-crisp-dialog": id, style: { maxWidth: `min(${width}px, 90vw)` }, children: _jsxs("div", { className: "dialog-card", children: [title != null && (_jsxs("div", { className: "dialog-header", children: [icon, _jsx("span", { ...api.getTitleProps(), className: "dialog-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "dialog-close", "aria-label": "Close", children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) }) })] }) }));
41
+ }
42
+ Dialog.displayName = "Dialog";
43
+ export function DialogBody({ children, className, }) {
44
+ return (_jsx("div", { className: className ? `dialog-body ${className}` : "dialog-body", children: children }));
45
+ }
46
+ export function DialogFooter({ children }) {
47
+ return _jsx("div", { className: "dialog-footer", children: children });
48
+ }
@@ -0,0 +1,32 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A modal dialog surface — measured from Attio's create-record modal: a radius-16
4
+ // frosted-glass card (60% tinted background + 15px backdrop blur, both re-measured
5
+ // per theme — light uses a blue-tinted 3-layer shadow, dark drops to a plain
6
+ // 2-layer black shadow) with a bordered header (title + close), a scrollable
7
+ // body, and a bordered footer for the primary action.
8
+ const config = defineRecipe({
9
+ name: "dialog",
10
+ base: `display: contents; font-family: ${c.font.sans};
11
+ & .dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; }
12
+ & .dialog-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"]}; }
13
+ & .dialog-content { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-height: 74vh; padding: ${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: 0 8px 28px -6px ${c.shadowOverlay1}, 0 18px 88px -4px ${c.shadowOverlay2}, 0 0 1px ${c.shadowOverlay3}; opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
14
+ & .dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; box-shadow: 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 8%, transparent); overflow: hidden; }
15
+ & .dialog-header { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; height: 52px; padding: 0 ${c.space["2"]} 0 ${c.space["4"]}; border-bottom: 1px solid ${c.borderSubtle}; }
16
+ & .dialog-header svg { flex-shrink: 0; color: ${c.fgMuted}; }
17
+ & .dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
18
+ & .dialog-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: ${c.fgMuted}; cursor: pointer; }
19
+ & .dialog-close:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
20
+ & .dialog-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: ${c.space["4"]}; padding: ${c.space["5"]} ${c.space["4"]}; }
21
+ & .dialog-footer { display: flex; align-items: center; justify-content: flex-end; gap: ${c.space["3"]}; flex-shrink: 0; min-height: 56px; padding: 0 ${c.space["4"]}; border-top: 1px solid ${c.borderSubtle}; }
22
+ & .dialog-field { display: flex; flex-direction: column; gap: ${c.space["1.5"]}; }
23
+ & .dialog-field-label { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent); }
24
+ & .dialog-more { display: inline-flex; align-items: center; gap: ${c.space["2"]}; margin-right: auto; font-size: ${c.text.md}; color: ${c.fgMuted}; }
25
+ & .dialog-submit { display: inline-flex; align-items: center; gap: ${c.space["2"]}; height: 28px; padding: 0 ${c.space["1"]} 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; }
26
+ & .dialog-submit-key { display: inline-flex; align-items: center; justify-content: center; height: 18px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: rgba(255,255,255,0.22); font-size: ${c.text.xs}; }`,
27
+ variants: {},
28
+ defaultVariants: {},
29
+ compoundVariants: [],
30
+ });
31
+ export const dialogRecipe = resolveRecipe(config);
32
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { Dialog, DialogBody, DialogFooter } from "./dialog.js";
2
+ export { dialogRecipe } from "./dialog.recipe.js";
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { dividerRecipe } from "./divider.recipe.js";
3
+ /** A hairline separator over `borderSubtle`, horizontal or vertical. */
4
+ export function Divider({ orientation = "horizontal", className, }) {
5
+ return (_jsx("hr", { "aria-orientation": orientation, "data-orientation": orientation, className: dividerRecipe({ className }) }));
6
+ }
7
+ Divider.displayName = "Divider";
@@ -0,0 +1,14 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A hairline separator — horizontal (default) or vertical.
4
+ const config = defineRecipe({
5
+ name: "divider",
6
+ base: `flex-shrink: 0; border: none; margin: 0; background: ${c.borderSubtle};
7
+ &[data-orientation="horizontal"] { width: 100%; height: 1px; }
8
+ &[data-orientation="vertical"] { width: 1px; align-self: stretch; min-height: 1em; }`,
9
+ variants: {},
10
+ defaultVariants: {},
11
+ compoundVariants: [],
12
+ });
13
+ export const dividerRecipe = resolveRecipe(config);
14
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { Divider } from "./divider.js";
2
+ export { dividerRecipe } from "./divider.recipe.js";
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ import { donutchartRecipe } from "./donutchart.recipe.js";
4
+ // default categorical palette (matches the tag/avatar tones)
5
+ const PALETTE = [
6
+ c.bgBrandSolid,
7
+ c.bgCyan,
8
+ c.bgPurple,
9
+ c.bgOrange,
10
+ c.bgSuccess,
11
+ c.bgWarning,
12
+ c.bgDanger,
13
+ ];
14
+ /**
15
+ * A donut / ring chart — measured from Attio's Reports breakdown reports. SVG
16
+ * ring using the r=15.915 (circumference 100) trick so slices are percentages;
17
+ * a centred total and an optional legend. `role="img"` with a summary.
18
+ */
19
+ export function DonutChart({ data, size = 120, thickness = 14, centerValue, centerCaption, legend = true, format = (n) => n.toLocaleString(), "aria-label": ariaLabel, className, }) {
20
+ const total = data.reduce((s, d) => s + Math.max(0, d.value), 0);
21
+ const R = 15.915; // circumference ≈ 100
22
+ let cursor = 0;
23
+ const slices = data.map((d, i) => {
24
+ const pct = total > 0 ? (Math.max(0, d.value) / total) * 100 : 0;
25
+ const slice = {
26
+ ...d,
27
+ color: d.color ?? PALETTE[i % PALETTE.length],
28
+ pct,
29
+ offset: 25 - cursor, // 25 = start at 12 o'clock, clockwise
30
+ };
31
+ cursor += pct;
32
+ return slice;
33
+ });
34
+ const summary = ariaLabel ??
35
+ `Donut chart: ${data.map((d) => `${d.label} ${format(d.value)}`).join(", ")}`;
36
+ return (_jsxs("div", { className: donutchartRecipe({ className }), role: "img", "aria-label": summary, children: [_jsxs("div", { className: "donutchart-ring", style: { width: size, height: size }, children: [_jsxs("svg", { className: "donutchart-svg", width: size, height: size, viewBox: "0 0 42 42", "aria-hidden": "true", children: [_jsx("circle", { className: "donutchart-track", cx: 21, cy: 21, r: R, strokeWidth: (thickness / size) * 42 }), slices.map((s) => (_jsx("circle", { className: "donutchart-seg", cx: 21, cy: 21, r: R, stroke: s.color, strokeWidth: (thickness / size) * 42, strokeDasharray: `${s.pct} ${100 - s.pct}`, strokeDashoffset: s.offset, children: _jsx("title", { children: `${s.label}: ${format(s.value)}` }) }, s.label)))] }), _jsxs("div", { className: "donutchart-center", children: [_jsx("span", { className: "donutchart-total", children: centerValue ?? format(total) }), centerCaption != null && (_jsx("span", { className: "donutchart-caption", children: centerCaption }))] })] }), legend && (_jsx("div", { className: "donutchart-legend", children: slices.map((s) => (_jsxs("span", { className: "donutchart-item", children: [_jsx("span", { className: "donutchart-swatch", style: { background: s.color } }), _jsx("span", { className: "donutchart-name", children: s.label }), _jsx("span", { className: "donutchart-val", children: format(s.value) })] }, s.label))) }))] }));
37
+ }
38
+ DonutChart.displayName = "DonutChart";
@@ -0,0 +1,27 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A donut / ring chart — measured from Attio's Reports breakdown reports: a
4
+ // segmented ring with a centred total, plus a legend of coloured swatches with
5
+ // labels and values. Pure SVG ring; HTML legend.
6
+ const config = defineRecipe({
7
+ name: "donutchart",
8
+ base: `display: inline-flex; align-items: center; gap: ${c.space["4"]}; box-sizing: border-box; font-family: ${c.font.sans};
9
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
10
+ & .donutchart-ring { position: relative; flex-shrink: 0; }
11
+ & .donutchart-svg { display: block; }
12
+ & .donutchart-seg { fill: none; transition: stroke-dasharray 0.3s ease; }
13
+ & .donutchart-track { fill: none; stroke: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
14
+ & .donutchart-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
15
+ & .donutchart-total { font-size: ${c.text.xl}; font-weight: ${c.weight.semibold}; letter-spacing: -0.3px; color: ${c.fgPrimary}; }
16
+ & .donutchart-caption { font-size: ${c.text.xs}; color: ${c.fgMuted}; }
17
+ & .donutchart-legend { display: flex; flex-direction: column; gap: ${c.space["1.5"]}; min-width: 0; }
18
+ & .donutchart-item { display: inline-flex; align-items: center; gap: ${c.space["2"]}; font-size: ${c.text.md}; color: ${c.fgPrimary}; }
19
+ & .donutchart-swatch { width: 10px; height: 10px; flex-shrink: 0; border-radius: 3px; }
20
+ & .donutchart-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: ${c.fgMuted}; }
21
+ & .donutchart-val { flex-shrink: 0; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }`,
22
+ variants: {},
23
+ defaultVariants: {},
24
+ compoundVariants: [],
25
+ });
26
+ export const donutchartRecipe = resolveRecipe(config);
27
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { DonutChart, } from "./donutchart.js";
2
+ export { donutchartRecipe } from "./donutchart.recipe.js";
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as dialog from "@zag-js/dialog";
3
+ import { normalizeProps, Portal, useMachine } from "@zag-js/react";
4
+ import { X } from "lucide-react";
5
+ import * as React from "react";
6
+ import { drawerRecipe } from "./drawer.recipe.js";
7
+ let instanceCounter = 0;
8
+ function useDrawerInstanceId() {
9
+ const idRef = React.useRef(null);
10
+ if (idRef.current == null) {
11
+ instanceCounter += 1;
12
+ idRef.current = `crisp-drawer-${instanceCounter}`;
13
+ }
14
+ return idRef.current;
15
+ }
16
+ /**
17
+ * A side sheet — composes Zag's dialog machine (focus trap, scroll lock,
18
+ * Escape / outside-click dismissal) with a panel that slides in from the edge.
19
+ * Compose the body with `DrawerBody` and actions with `DrawerFooter`.
20
+ */
21
+ export function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, children, }) {
22
+ const id = useDrawerInstanceId();
23
+ const service = useMachine(dialog.machine, {
24
+ id,
25
+ open,
26
+ onOpenChange(details) {
27
+ onOpenChange(details.open);
28
+ },
29
+ "aria-label": typeof title === "string" ? title : ariaLabel,
30
+ });
31
+ const api = dialog.connect(service, normalizeProps);
32
+ if (!api.open)
33
+ return null;
34
+ return (_jsx(Portal, { children: _jsxs("div", { className: drawerRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "drawer-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "drawer-positioner", "data-side": side, children: _jsxs("div", { ...api.getContentProps(), className: "drawer-content", children: [title != null && (_jsxs("div", { className: "drawer-header", children: [icon, _jsx("span", { ...api.getTitleProps(), className: "drawer-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "drawer-close", "aria-label": "Close", children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) })] }) }));
35
+ }
36
+ Drawer.displayName = "Drawer";
37
+ export function DrawerBody({ children }) {
38
+ return _jsx("div", { className: "drawer-body", children: children });
39
+ }
40
+ export function DrawerFooter({ children }) {
41
+ return _jsx("div", { className: "drawer-footer", children: children });
42
+ }
@@ -0,0 +1,27 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A side sheet — a focus-trapped panel that slides in from the right or left,
4
+ // over a scrim. Shares the Dialog header/body/footer language.
5
+ const config = defineRecipe({
6
+ name: "drawer",
7
+ base: `display: contents; font-family: ${c.font.sans};
8
+ & .drawer-backdrop { position: fixed; inset: 0; z-index: 60; background: color-mix(in srgb, ${c.fgPrimary} 18%, transparent); opacity: 1; transition: opacity 0.2s ease; @starting-style { opacity: 0; } }
9
+ & .drawer-positioner { position: fixed; inset: 0; z-index: 61; display: flex; }
10
+ & .drawer-positioner[data-side="right"] { justify-content: flex-end; }
11
+ & .drawer-positioner[data-side="left"] { justify-content: flex-start; }
12
+ & .drawer-content { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 420px; max-width: 92vw; height: 100%; background: ${c.bgSurface}; transform: none; transition: transform 0.24s cubic-bezier(.32,.72,0,1); }
13
+ & .drawer-positioner[data-side="right"] .drawer-content { box-shadow: -12px 0 32px -8px rgba(28,40,64,0.18); @starting-style { transform: translateX(100%); } }
14
+ & .drawer-positioner[data-side="left"] .drawer-content { box-shadow: 12px 0 32px -8px rgba(28,40,64,0.18); @starting-style { transform: translateX(-100%); } }
15
+ & .drawer-header { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; height: 52px; padding: 0 ${c.space["2"]} 0 ${c.space["4"]}; border-bottom: 1px solid ${c.borderSubtle}; }
16
+ & .drawer-header svg { flex-shrink: 0; color: ${c.fgMuted}; }
17
+ & .drawer-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
18
+ & .drawer-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: ${c.fgMuted}; cursor: pointer; }
19
+ & .drawer-close:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
20
+ & .drawer-body { flex: 1; min-height: 0; overflow-y: auto; padding: ${c.space["4"]}; }
21
+ & .drawer-footer { display: flex; align-items: center; justify-content: flex-end; gap: ${c.space["3"]}; flex-shrink: 0; min-height: 56px; padding: 0 ${c.space["4"]}; border-top: 1px solid ${c.borderSubtle}; }`,
22
+ variants: {},
23
+ defaultVariants: {},
24
+ compoundVariants: [],
25
+ });
26
+ export const drawerRecipe = resolveRecipe(config);
27
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { Drawer, DrawerBody, DrawerFooter } from "./drawer.js";
2
+ export { drawerRecipe } from "./drawer.recipe.js";
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { emptystateRecipe } from "./emptystate.recipe.js";
3
+ /** A centred empty-state panel — icon, title, description, optional action. */
4
+ export function EmptyState({ icon, title, description, action, variant, className, }) {
5
+ return (_jsxs("div", { className: emptystateRecipe({ variant, className }), children: [icon && _jsx("span", { className: "emptystate-icon", children: icon }), _jsx("div", { className: "emptystate-title", children: title }), description != null && (_jsx("div", { className: "emptystate-desc", children: description })), action && _jsx("div", { className: "emptystate-action", children: action })] }));
6
+ }
7
+ EmptyState.displayName = "EmptyState";
@@ -0,0 +1,31 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // An empty-state panel — icon + title + description + optional action.
4
+ const config = defineRecipe({
5
+ name: "emptystate",
6
+ base: `display: flex; flex-direction: column; align-items: center; justify-content: center; gap: ${c.space["1.5"]}; box-sizing: border-box; padding: ${c.space["8"]} ${c.space["4"]}; text-align: center; font-family: ${c.font.sans};
7
+ & .emptystate-icon { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; margin-bottom: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: color-mix(in srgb, ${c.fgPrimary} 5%, transparent); color: ${c.fgMuted}; }
8
+ & .emptystate-title { font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
9
+ & .emptystate-desc { max-width: 320px; font-size: ${c.text.md}; color: ${c.fgMuted}; }
10
+ & .emptystate-action { margin-top: ${c.space["2"]}; }`,
11
+ variants: {
12
+ variant: {
13
+ // compact icon tile (default)
14
+ default: ``,
15
+ // a large icon on a dotted-grid backdrop — Attio's Developers/Reports empty state
16
+ illustrated: `& .emptystate-icon { width: 132px; height: 96px; margin-bottom: ${c.space["2"]}; border-radius: 0; 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; }`,
17
+ // Attio's full-view empty state (measured from a filtered-to-zero records table):
18
+ // a 45px ringed circle icon, a 20/24 semibold heading, a 14/20 medium muted body,
19
+ // then a primary action. Gaps are 16 / 8 / 16 (icon→title→desc→action).
20
+ page: `gap: ${c.space["2"]};
21
+ & .emptystate-icon { width: 45px; height: 45px; margin-bottom: ${c.space["2"]}; border-radius: ${c.radius.full}; border: 1px solid ${c.borderSubtle}; background: none; color: ${c.fgMuted}; }
22
+ & .emptystate-title { font-size: 20px; line-height: 24px; }
23
+ & .emptystate-desc { max-width: 266px; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; line-height: 20px; }
24
+ & .emptystate-action { margin-top: ${c.space["2"]}; }`,
25
+ },
26
+ },
27
+ defaultVariants: { variant: "default" },
28
+ compoundVariants: [],
29
+ });
30
+ export const emptystateRecipe = resolveRecipe(config);
31
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { EmptyState } from "./emptystate.js";
2
+ export { emptystateRecipe } from "./emptystate.recipe.js";
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { formfieldRecipe } from "./formfield.recipe.js";
4
+ /**
5
+ * A labelled form field — measured from Attio's Settings forms. A muted label
6
+ * above any control, with an optional description or error line below. When
7
+ * `error` is set, it replaces the description and gets `aria-describedby` wiring.
8
+ */
9
+ export function FormField({ label, htmlFor, required, optional, description, error, children, className, }) {
10
+ const descId = React.useId();
11
+ const hasMsg = error != null || description != null;
12
+ return (_jsxs("div", { className: formfieldRecipe({ className }), children: [_jsxs("label", { className: "formfield-label", htmlFor: htmlFor, children: [label, required && (_jsx("span", { className: "formfield-required", "aria-hidden": "true", children: "*" })), optional && !required && (_jsx("span", { className: "formfield-optional", children: "(optional)" }))] }), children, error != null ? (_jsx("span", { className: "formfield-error", id: hasMsg ? descId : undefined, children: error })) : description != null ? (_jsx("span", { className: "formfield-desc", id: hasMsg ? descId : undefined, children: description })) : null] }));
13
+ }
14
+ FormField.displayName = "FormField";
@@ -0,0 +1,19 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A labelled form field — measured from Attio's Settings forms: a 12px/500 muted
4
+ // label above the control, with an optional description or danger error line
5
+ // below. Wraps any control (Input, Select, Textarea…).
6
+ const config = defineRecipe({
7
+ name: "formfield",
8
+ base: `display: flex; flex-direction: column; gap: ${c.space["1.5"]}; box-sizing: border-box; font-family: ${c.font.sans};
9
+ & .formfield-label { display: inline-flex; align-items: center; gap: ${c.space["1"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
10
+ & .formfield-required { color: ${c.bgDanger}; }
11
+ & .formfield-optional { color: color-mix(in srgb, ${c.fgPrimary} 40%, transparent); font-weight: ${c.weight.regular}; }
12
+ & .formfield-desc { font-size: ${c.text.sm}; line-height: 16px; color: ${c.fgMuted}; }
13
+ & .formfield-error { font-size: ${c.text.sm}; line-height: 16px; color: ${c.bgDanger}; }`,
14
+ variants: {},
15
+ defaultVariants: {},
16
+ compoundVariants: [],
17
+ });
18
+ export const formfieldRecipe = resolveRecipe(config);
19
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { FormField } from "./formfield.js";
2
+ export { formfieldRecipe } from "./formfield.recipe.js";
@@ -0,0 +1,2 @@
1
+ export { Input } from "./input.js";
2
+ export { inputRecipe } from "./input.recipe.js";
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Primitive } from "../../core/index.js";
4
+ import { inputRecipe } from "./input.recipe.js";
5
+ export const Input = React.forwardRef(({ size, invalid, className, ...props }, ref) => {
6
+ return (_jsx(Primitive.input, { ref: ref, className: inputRecipe({ size, invalid, className }), "aria-invalid": invalid === true || undefined, ...props }));
7
+ });
8
+ Input.displayName = "Input";
@@ -0,0 +1,23 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ const config = defineRecipe({
4
+ name: "input",
5
+ base: `box-sizing: border-box; display: inline-flex; width: 100%; font-family: ${c.font.sans}; font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgPrimary}; background: ${c.bgSurface}; border: none; outline: none; box-shadow: 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); transition: box-shadow .15s; &::placeholder { color: ${c.fgMuted}; } &:focus { box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; } &:disabled { opacity: 0.5; cursor: not-allowed; }`,
6
+ variants: {
7
+ size: {
8
+ medium: `height: 32px; padding-inline: ${c.space["2.5"]}; border-radius: ${c.radius.md};`,
9
+ large: `height: 36px; padding-inline: ${c.space["3"]}; border-radius: 10px;`,
10
+ },
11
+ invalid: {
12
+ true: `box-shadow: inset 0 0 0 1px ${c.bgDanger}; &:focus { box-shadow: inset 0 0 0 1px ${c.bgDanger}; }`,
13
+ false: ``,
14
+ },
15
+ },
16
+ defaultVariants: { size: "medium", invalid: false },
17
+ compoundVariants: [],
18
+ });
19
+ export const inputRecipe = Object.assign(resolveRecipe(config), {
20
+ config,
21
+ meta: { medium: { height: "32px", borderRadius: "8px", fontSize: "14px" } },
22
+ });
23
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { Kbd } from "./kbd.js";
2
+ export { kbdRecipe } from "./kbd.recipe.js";
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Primitive } from "../../core/index.js";
4
+ import { kbdRecipe } from "./kbd.recipe.js";
5
+ export const Kbd = React.forwardRef(({ className, asChild, ...props }, ref) => {
6
+ return (_jsx(Primitive.span, { ref: ref, asChild: asChild, className: kbdRecipe({ className }), ...props }));
7
+ });
8
+ Kbd.displayName = "Kbd";
@@ -0,0 +1,20 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ const config = defineRecipe({
4
+ name: "kbd",
5
+ base: `box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: ${c.space["0.5"]}; min-width: 20px; height: 20px; padding-inline: ${c.space["1"]}; border-radius: ${c.radius.sm}; font-family: ${c.font.sans}; font-size: ${c.text.xs}; font-weight: ${c.weight.regular}; color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 6%, transparent);`,
6
+ variants: {},
7
+ defaultVariants: {},
8
+ compoundVariants: [],
9
+ });
10
+ export const kbdRecipe = Object.assign(resolveRecipe(config), {
11
+ config,
12
+ meta: {
13
+ chip: {
14
+ height: "20px",
15
+ borderRadius: "6px",
16
+ fontSize: "11px",
17
+ },
18
+ },
19
+ });
20
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { LineChart } from "./linechart.js";
2
+ export { linechartRecipe } from "./linechart.recipe.js";
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { linechartRecipe } from "./linechart.recipe.js";
3
+ function niceNum(raw) {
4
+ if (raw <= 0)
5
+ return 1;
6
+ const mag = 10 ** Math.floor(Math.log10(raw));
7
+ const norm = raw / mag;
8
+ const step = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10;
9
+ return step * mag;
10
+ }
11
+ /**
12
+ * A responsive line chart — measured from Attio's Reports historical charts.
13
+ * A stretched SVG line/area (non-scaling stroke keeps it crisp) over dashed
14
+ * gridlines; dots are HTML so they stay round. `role="img"` with a summary.
15
+ */
16
+ export function LineChart({ data, height = 240, max, ticks = 5, color, area = true, dots = true, format = (n) => n.toLocaleString(), xAxisTitle, "aria-label": ariaLabel, className, }) {
17
+ const rawMax = Math.max(1, ...data.map((d) => d.value));
18
+ const step = max ? max / (ticks - 1) : niceNum(rawMax / (ticks - 1));
19
+ const top = step * (ticks - 1);
20
+ const tickValues = Array.from({ length: ticks }, (_, i) => step * i);
21
+ // points in a 0..100 viewBox (y inverted: 0 at top)
22
+ const n = data.length;
23
+ const pts = data.map((d, i) => {
24
+ const x = n === 1 ? 50 : (i / (n - 1)) * 100;
25
+ const frac = Math.max(0, Math.min(1, d.value / top));
26
+ return { x, y: 100 - frac * 100, frac };
27
+ });
28
+ const line = pts.map((p) => `${p.x},${p.y}`).join(" ");
29
+ const areaPath = pts.length > 1
30
+ ? `M ${pts[0].x},100 ${pts.map((p) => `L ${p.x},${p.y}`).join(" ")} L ${pts[pts.length - 1].x},100 Z`
31
+ : "";
32
+ const summary = ariaLabel ??
33
+ `Line chart: ${data.map((d) => `${d.label} ${format(d.value)}`).join(", ")}`;
34
+ return (_jsxs("div", { className: linechartRecipe({ className }), style: { height }, role: "img", "aria-label": summary, children: [_jsxs("div", { className: "linechart-body", children: [_jsx("div", { className: "linechart-yaxis", children: tickValues.map((t) => (_jsx("span", { className: "linechart-ytick", style: { bottom: `${(t / top) * 100}%` }, children: format(t) }, t))) }), _jsxs("div", { className: "linechart-plot", children: [tickValues.map((t) => (_jsx("div", { className: "linechart-grid", style: { bottom: `${(t / top) * 100}%` } }, t))), pts.length > 1 && (_jsxs("svg", { className: "linechart-svg", viewBox: "0 0 100 100", preserveAspectRatio: "none", "aria-hidden": "true", children: [area && _jsx("path", { className: "linechart-area", d: areaPath }), _jsx("polyline", { className: "linechart-line", points: line, style: { stroke: color } })] })), dots &&
35
+ pts.map((p, i) => (_jsx("span", { className: "linechart-dot-html", style: {
36
+ left: `${p.x}%`,
37
+ bottom: `${p.frac * 100}%`,
38
+ borderColor: color,
39
+ }, title: `${data[i].label}: ${format(data[i].value)}` }, i)))] })] }), _jsx("div", { className: "linechart-xaxis", children: data.map((d) => (_jsx("span", { className: "linechart-xlabel", children: d.label }, d.label))) }), xAxisTitle != null && (_jsx("div", { className: "linechart-axis-title", children: xAxisTitle }))] }));
40
+ }
41
+ LineChart.displayName = "LineChart";
@@ -0,0 +1,27 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A responsive line chart — measured from Attio's Reports historical charts: a
4
+ // brand line with a soft area fill over dashed horizontal gridlines, a value
5
+ // axis on the left, and category labels below. The plot is an SVG stretched to
6
+ // its box (non-scaling stroke keeps the line crisp); axes are HTML.
7
+ const config = defineRecipe({
8
+ name: "linechart",
9
+ base: `display: flex; flex-direction: column; box-sizing: border-box; font-family: ${c.font.sans};
10
+ & .linechart-body { display: flex; flex: 1; min-height: 0; }
11
+ & .linechart-yaxis { position: relative; flex-shrink: 0; width: 34px; }
12
+ & .linechart-ytick { position: absolute; right: ${c.space["2"]}; transform: translateY(50%); font-size: ${c.text.xs}; color: ${c.fgMuted}; white-space: nowrap; }
13
+ & .linechart-plot { position: relative; flex: 1; min-width: 0; border-bottom: 1px solid ${c.borderDefault}; }
14
+ & .linechart-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed ${c.borderSubtle}; pointer-events: none; }
15
+ & .linechart-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
16
+ & .linechart-area { fill: color-mix(in srgb, ${c.bgBrandSolid} 12%, transparent); stroke: none; }
17
+ & .linechart-line { fill: none; stroke: ${c.bgBrandSolid}; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
18
+ & .linechart-dot-html { position: absolute; width: 8px; height: 8px; box-sizing: border-box; border-radius: ${c.radius.full}; background: ${c.bgSurface}; border: 2px solid ${c.bgBrandSolid}; transform: translate(-50%, 50%); pointer-events: auto; }
19
+ & .linechart-xaxis { display: flex; margin-top: ${c.space["1.5"]}; padding-left: 34px; }
20
+ & .linechart-xlabel { flex: 1; min-width: 0; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.sm}; color: ${c.fgMuted}; }
21
+ & .linechart-axis-title { margin-top: ${c.space["1"]}; padding-left: 34px; text-align: center; font-size: ${c.text.sm}; color: ${c.fgMuted}; }`,
22
+ variants: {},
23
+ defaultVariants: {},
24
+ compoundVariants: [],
25
+ });
26
+ export const linechartRecipe = resolveRecipe(config);
27
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { Menu, MenuCheckboxItem, MenuItem, MenuLabel, MenuSeparator, } from "./menu.js";
2
+ export { menuRecipe } from "./menu.recipe.js";