@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,963 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ArrowDown, ArrowDownWideNarrow, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown, ArrowUpNarrowWide, ArrowUpRight, AtSign, Briefcase, Building2, Calendar, Check, CircleHelp, Copy, CopyPlus, Download, EyeOff, Globe, GripVertical, Hash, Inbox, ListFilter, ListPlus, Mail, MapPin, MoreHorizontal, Pencil, Plus, Star, Tag, Trash2, TrendingUp, Users, Workflow, X, } from "lucide-react";
3
+ import * as React from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { Avatar } from "../../components/avatar/index.js";
6
+ import { BulkBar, BulkBarButton } from "../../components/bulkbar/index.js";
7
+ import { Button } from "../../components/button/index.js";
8
+ import { Checkbox } from "../../components/checkbox/index.js";
9
+ import { DatePicker } from "../../components/datepicker/index.js";
10
+ import { Dialog, DialogBody, DialogFooter } from "../../components/dialog/index.js";
11
+ import { EmptyState } from "../../components/emptystate/index.js";
12
+ import { Input } from "../../components/input/index.js";
13
+ import { Menu, MenuItem } from "../../components/menu/index.js";
14
+ import { MultiSelect } from "../../components/multiselect/index.js";
15
+ import { Select } from "../../components/select/index.js";
16
+ import { Switch } from "../../components/switch/index.js";
17
+ import { companyLogos } from "./company-logos.js";
18
+ import { recordstableRecipe } from "./recordstable.recipe.js";
19
+ const EDITABLE = new Set(["text", "number", "url", "currency"]);
20
+ // types whose cell is an always-interactive picker (not double-click-to-edit)
21
+ const PICKER = new Set(["status", "reference", "date", "tags"]);
22
+ // a `render` column owns its content fully — never editable, never a picker
23
+ const canEditCol = (col) => EDITABLE.has(col.type ?? "text") && !col.render;
24
+ const isPickerCol = (col) => PICKER.has(col.type ?? "text") && !col.render;
25
+ // deterministic pastel colour per tag value (a select option keeps its colour)
26
+ const TAG_TONES = [
27
+ "brand",
28
+ "success",
29
+ "warning",
30
+ "danger",
31
+ "purple",
32
+ "cyan",
33
+ "orange",
34
+ ];
35
+ export function tagTone(t) {
36
+ let h = 0;
37
+ for (let i = 0; i < t.length; i++)
38
+ h = (h * 31 + t.charCodeAt(i)) >>> 0;
39
+ return TAG_TONES[h % TAG_TONES.length];
40
+ }
41
+ // deterministic per-name colour for `avatarTone="hashed"` — a distinct
42
+ // palette from TAG_TONES because it must stay within the tones the `Avatar`
43
+ // DS component itself supports (no "success"/"warning"/"danger").
44
+ const AVATAR_TONES = ["brand", "orange", "cyan", "purple", "green"];
45
+ function avatarToneFor(name) {
46
+ let h = 0;
47
+ for (let i = 0; i < name.length; i++)
48
+ h = (h * 31 + name.charCodeAt(i)) >>> 0;
49
+ return AVATAR_TONES[h % AVATAR_TONES.length];
50
+ }
51
+ const AGG_LABELS = {
52
+ none: "None",
53
+ count: "Count all",
54
+ filled: "Not empty",
55
+ sum: "Sum",
56
+ avg: "Average",
57
+ min: "Min",
58
+ max: "Max",
59
+ };
60
+ // currency is numeric data too — it gets the same numeric aggregations as
61
+ // `number` (only `number` auto-defaults to Sum on mount; currency starts at
62
+ // "none" like every other type and is picked explicitly from this menu)
63
+ const aggOptions = (col) => col.type === "number" || col.type === "currency"
64
+ ? ["sum", "avg", "min", "max", "count", "filled", "none"]
65
+ : ["count", "filled", "none"];
66
+ const compact = new Intl.NumberFormat("en", {
67
+ notation: "compact",
68
+ maximumFractionDigits: 1,
69
+ });
70
+ const CHECK_W = 40;
71
+ const ADDCOL_W = 44;
72
+ const colWidth = (col) => col.width ?? (col.type === "entity" ? 240 : 180);
73
+ const OP_LABELS = {
74
+ contains: "contains",
75
+ is: "is",
76
+ isnot: "is not",
77
+ empty: "is empty",
78
+ notempty: "is not empty",
79
+ };
80
+ const OP_ORDER = ["contains", "is", "isnot", "empty", "notempty"];
81
+ const opNeedsValue = (op) => op === "contains" || op === "is" || op === "isnot";
82
+ function matchesFilter(col, row, cond) {
83
+ if (cond.op === "empty")
84
+ return !isFilled(col, row);
85
+ if (cond.op === "notempty")
86
+ return isFilled(col, row);
87
+ // filter always reads the raw underlying value, never `format`'s display
88
+ // string — a formatted "$1,200" must still match a filter value of "1200"
89
+ const t = rawText(col, row).toLowerCase();
90
+ const v = cond.value.trim().toLowerCase();
91
+ if (!v)
92
+ return true;
93
+ if (cond.op === "contains")
94
+ return t.includes(v);
95
+ if (cond.op === "is")
96
+ return t === v;
97
+ return t !== v; // isnot
98
+ }
99
+ // the raw value stringified, ignoring `format`/`render` entirely — the basis
100
+ // for filtering and the fallback for `cellText`'s copy path
101
+ function rawText(col, row) {
102
+ const v = row[col.key];
103
+ if (v == null)
104
+ return "";
105
+ if (col.type === "entity")
106
+ return v.name ?? "";
107
+ if (Array.isArray(v))
108
+ return v.join(", ");
109
+ return String(v);
110
+ }
111
+ // what ⌘C / row-copy / CSV export puts on the clipboard for this cell: a
112
+ // `render` column has no serializable node, so this never touches `render` —
113
+ // it uses `format`'s display string when the column has one, else the raw
114
+ // value stringified exactly like a plain column.
115
+ function cellText(col, row) {
116
+ if (col.format)
117
+ return col.format(row[col.key], row);
118
+ return rawText(col, row);
119
+ }
120
+ function isFilled(col, row) {
121
+ const v = row[col.key];
122
+ if (v == null || v === "")
123
+ return false;
124
+ if (Array.isArray(v))
125
+ return v.length > 0;
126
+ if (col.type === "entity")
127
+ return Boolean(v.name);
128
+ return true;
129
+ }
130
+ function summarize(col, agg, rows) {
131
+ if (agg === "none")
132
+ return null;
133
+ if (agg === "count")
134
+ return { label: "count", value: String(rows.length) };
135
+ if (agg === "filled") {
136
+ const n = rows.filter((r) => isFilled(col, r)).length;
137
+ return { label: "filled", value: String(n) };
138
+ }
139
+ const nums = rows
140
+ .map((r) => r[col.key])
141
+ .filter((v) => typeof v === "number");
142
+ if (!nums.length)
143
+ return { label: agg, value: "—" };
144
+ let n;
145
+ if (agg === "sum")
146
+ n = nums.reduce((a, b) => a + b, 0);
147
+ else if (agg === "avg")
148
+ n = nums.reduce((a, b) => a + b, 0) / nums.length;
149
+ else if (agg === "min")
150
+ n = Math.min(...nums);
151
+ else
152
+ n = Math.max(...nums);
153
+ return { label: agg, value: compact.format(n) };
154
+ }
155
+ function EntityCell({ value, pinned, onOpenPage, avatarTone = "brand", }) {
156
+ // a real favicon/logo when we have one; fall back to the letter tile if it
157
+ // 404s or the row has no avatar
158
+ const [imgFailed, setImgFailed] = React.useState(false);
159
+ const showImg = Boolean(value.avatar) && !imgFailed;
160
+ return (_jsxs("span", { className: "recordstable-entity", children: [showImg ? (_jsx("img", { className: "recordstable-fav", src: value.avatar, alt: "", onError: () => setImgFailed(true) })) : (_jsx(Avatar, { size: "sm", shape: "rounded", tone: avatarTone === "hashed" ? avatarToneFor(value.name) : "brand", children: value.name.charAt(0) })), onOpenPage ? (_jsx("button", { type: "button", className: "recordstable-entity-name recordstable-entity-name--link", onClick: (e) => {
161
+ e.stopPropagation();
162
+ onOpenPage();
163
+ }, children: value.name })) : (_jsx("span", { className: "recordstable-entity-name", children: value.name })), pinned ? (_jsx(Star, { className: "recordstable-pin", size: 13, strokeWidth: 2, "aria-label": "In a list" })) : null] }));
164
+ }
165
+ function CellView({ column, row, pinned, onOpenPage, avatarTone = "brand", }) {
166
+ const value = row[column.key];
167
+ // `render` fully owns the cell's content — no empty-dash fallback, no
168
+ // type renderer; it's the only thing drawn here (see the doc comment on
169
+ // `Column.render`).
170
+ if (column.render)
171
+ return _jsx(_Fragment, { children: column.render(value, row) });
172
+ if (value == null ||
173
+ value === "" ||
174
+ (Array.isArray(value) && !value.length)) {
175
+ return _jsx("span", { className: "recordstable-muted", children: "\u2014" });
176
+ }
177
+ // `format` keeps the type renderer's chrome below and only replaces the
178
+ // text it displays.
179
+ const display = column.format?.(value, row);
180
+ switch (column.type) {
181
+ case "entity":
182
+ return (_jsx(EntityCell, { value: value, pinned: pinned, onOpenPage: onOpenPage, avatarTone: avatarTone }));
183
+ case "tags":
184
+ return (_jsx("span", { className: "recordstable-cell recordstable-cell--tags", children: value.map((t) => (_jsx("span", { className: "recordstable-tag", "data-tone": tagTone(t), children: t }, t))) }));
185
+ case "number":
186
+ return (_jsx("span", { className: "recordstable-cell recordstable-cell--num", children: display ??
187
+ (typeof value === "number" ? value.toLocaleString() : value) }));
188
+ case "url":
189
+ return (_jsx("a", { className: "recordstable-link", href: `https://${value}`, target: "_blank", rel: "noreferrer", onClick: (e) => e.stopPropagation(), children: display ?? value }));
190
+ case "date":
191
+ return _jsx("span", { className: "recordstable-muted", children: display ?? value });
192
+ case "currency": {
193
+ const n = typeof value === "number" ? value : Number(value);
194
+ return (_jsx("span", { className: "recordstable-cell recordstable-cell--num", children: display ??
195
+ `${column.symbol ?? "$"}${Number.isFinite(n) ? n.toLocaleString() : value}` }));
196
+ }
197
+ case "status": {
198
+ const opt = column.options?.find((o) => o.value === value);
199
+ if (!opt)
200
+ return _jsx("span", { className: "recordstable-muted", children: "\u2014" });
201
+ return (_jsxs("span", { className: "recordstable-status", children: [_jsx("span", { className: "recordstable-status-dot", style: { background: opt.color } }), display ?? opt.label] }));
202
+ }
203
+ case "reference": {
204
+ const opt = column.refOptions?.find((o) => o.value === value);
205
+ const label = display ?? opt?.label ?? String(value);
206
+ return (_jsxs("span", { className: "recordstable-entity", children: [_jsx(Avatar, { size: "sm", shape: "rounded", tone: avatarTone === "hashed" ? avatarToneFor(label) : "brand", children: label.charAt(0) }), _jsx("span", { className: "recordstable-entity-name", children: label })] }));
207
+ }
208
+ default:
209
+ return (_jsx("span", { className: "recordstable-entity-name", children: display ?? value }));
210
+ }
211
+ }
212
+ // Always-interactive cell editors for the picker column types (status / date /
213
+ // reference). Each is a ghost crisp control that reads the cell value and
214
+ // commits the new one — matching Attio's inline field editors.
215
+ function StatusCell({ column, row, onChange, }) {
216
+ return (_jsx(Select, { variant: "ghost", "aria-label": column.label, placeholder: "\u2014", value: row[column.key] ?? undefined, onValueChange: onChange, options: column.options ?? [] }));
217
+ }
218
+ function ReferenceCell({ column, row, onChange, }) {
219
+ return (_jsx(Select, { variant: "ghost", searchable: true, searchPlaceholder: "Search records\u2026", "aria-label": column.label, placeholder: "Set a record\u2026", value: row[column.key] ?? undefined, onValueChange: onChange, options: (column.refOptions ?? []).map((o) => ({
220
+ value: o.value,
221
+ label: o.label,
222
+ })) }));
223
+ }
224
+ function TagsCell({ column, row, options, onChange, }) {
225
+ const value = row[column.key] ?? [];
226
+ return (_jsx(MultiSelect, { options: options, value: value, onValueChange: onChange, placeholder: "Empty", searchPlaceholder: "Search options\u2026", "aria-label": column.label }));
227
+ }
228
+ function DateCell({ column, row, onChange, }) {
229
+ const raw = row[column.key];
230
+ const value = raw ? new Date(raw) : null;
231
+ return (_jsx(DatePicker, { variant: "ghost", "aria-label": column.label, value: value, onChange: (d) => onChange(d.toISOString().slice(0, 10)) }));
232
+ }
233
+ /**
234
+ * The "New <object>" create modal — measured from Attio's create-record dialog:
235
+ * a stacked label + "Set …" input per column, a "Create more" toggle, and a
236
+ * primary "Create record" action (⌘↵). Building the row mirrors the inline
237
+ * editor's per-type coercion so a created record reads back like an edited one.
238
+ */
239
+ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate, }) {
240
+ const [values, setValues] = React.useState({});
241
+ const [createMore, setCreateMore] = React.useState(false);
242
+ const firstRef = React.useRef(null);
243
+ // reset the form each time the modal (re)opens
244
+ React.useEffect(() => {
245
+ if (open)
246
+ setValues({});
247
+ }, [open]);
248
+ const set = (key, v) => setValues((prev) => ({ ...prev, [key]: v }));
249
+ const buildRow = () => {
250
+ const row = {
251
+ id: `new-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
252
+ };
253
+ for (const col of columns) {
254
+ const raw = (values[col.key] ?? "").trim();
255
+ if (col.type === "entity") {
256
+ row[col.key] = { name: raw || "Untitled" };
257
+ }
258
+ else if (!raw) {
259
+ // leave empty fields unset so the cell renders its "—" placeholder
260
+ }
261
+ else if (col.type === "tags") {
262
+ row[col.key] = raw
263
+ .split(",")
264
+ .map((t) => t.trim())
265
+ .filter(Boolean);
266
+ }
267
+ else if (col.type === "number") {
268
+ row[col.key] = Number(raw.replace(/[^0-9.-]/g, "")) || null;
269
+ }
270
+ else {
271
+ row[col.key] = raw;
272
+ }
273
+ }
274
+ return row;
275
+ };
276
+ const submit = () => {
277
+ onCreate(buildRow());
278
+ if (createMore) {
279
+ setValues({});
280
+ firstRef.current?.focus();
281
+ }
282
+ else {
283
+ onOpenChange(false);
284
+ }
285
+ };
286
+ const entityCol = columns.find((c) => c.type === "entity");
287
+ const EntityIcon = entityCol?.icon;
288
+ return (_jsxs(Dialog, { open: open, onOpenChange: onOpenChange, title: `New ${entityLabel}`, icon: EntityIcon ? _jsx(EntityIcon, { size: 16, strokeWidth: 1.75 }) : undefined, width: 720, children: [_jsx(DialogBody, { children: columns.map((col, i) => (_jsxs("div", { className: "dialog-field", children: [_jsx("label", { className: "dialog-field-label", htmlFor: `create-${col.key}`, children: col.label }), _jsx(Input, { id: `create-${col.key}`, ref: i === 0 ? firstRef : undefined, value: values[col.key] ?? "", onChange: (e) => set(col.key, e.currentTarget.value), placeholder: `Set ${col.label}…`, onKeyDown: (e) => {
289
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey))
290
+ submit();
291
+ } })] }, col.key))) }), _jsxs(DialogFooter, { children: [_jsxs("span", { className: "dialog-more", children: ["Create more", _jsx(Switch, { checked: createMore, onCheckedChange: setCreateMore, "aria-label": "Create more" })] }), _jsxs("button", { type: "button", className: "dialog-submit", onClick: submit, children: ["Create record", _jsx("span", { className: "dialog-submit-key", children: "\u2318\u21B5" })] })] })] }));
292
+ }
293
+ /**
294
+ * Attio-style record grid — fully featured. Toolbar (search / sort status /
295
+ * count / New), sticky checkbox + entity columns, 36px rows, typed cells,
296
+ * click-to-sort headers, column resize by dragging a header edge, inline cell
297
+ * editing (double-click a text/number/url/date cell), add-row and add-column,
298
+ * and a floating bulk-action bar (delete / deselect) when rows are selected.
299
+ * Selection can be controlled via `selectedIds`/`onSelectionChange`.
300
+ */
301
+ export function RecordsTable({ columns: columnsProp = defaultColumns, rows: rowsProp = defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", className, }) {
302
+ const [cols, setCols] = React.useState(columnsProp);
303
+ const [data, setData] = React.useState(rowsProp);
304
+ // Attio's multi-criteria sort: an ordered list of {field, direction} rows.
305
+ const [sorts, setSorts] = React.useState([]);
306
+ const [dragSort, setDragSort] = React.useState(null);
307
+ const [editing, setEditing] = React.useState(null);
308
+ const [renamingCol, setRenamingCol] = React.useState(null);
309
+ const [creating, setCreating] = React.useState(false);
310
+ // Attio-style filter: an ordered list of conditions, each shown as a toolbar
311
+ // chip with an editable value (operator is "contains")
312
+ const [filterConds, setFilterConds] = React.useState([]);
313
+ const [filterQuery, setFilterQuery] = React.useState("");
314
+ // one popover open at a time: "sort" | "filter" | "more" | `sum:<key>`
315
+ const [openMenu, setOpenMenu] = React.useState(null);
316
+ // viewport-anchored position for menus that live inside the scroll container
317
+ // (summary footer, column headers) — rendered fixed to escape the overflow clip
318
+ const [menuPos, setMenuPos] = React.useState(null);
319
+ const [pinned, setPinned] = React.useState(() => new Set());
320
+ const [scrolledX, setScrolledX] = React.useState(false);
321
+ const [scrolledY, setScrolledY] = React.useState(false);
322
+ const idCounter = React.useRef(0);
323
+ // fixed menus (summary / header) portal here — outside the sticky cells'
324
+ // stacking contexts, but still inside .recordstable so the styles apply
325
+ const rootRef = React.useRef(null);
326
+ const scrollRef = React.useRef(null);
327
+ // spreadsheet-style cell selection (click a cell, arrow-key to navigate)
328
+ const [selCell, setSelCell] = React.useState(null);
329
+ // shift-click range selection — anchor row + whether shift was held on click
330
+ const selAnchor = React.useRef(null);
331
+ const shiftHeld = React.useRef(false);
332
+ // right-click row context menu (fixed at the cursor)
333
+ const [ctxMenu, setCtxMenu] = React.useState(null);
334
+ // column drag-to-reorder — key of the header currently being dragged
335
+ const [dragCol, setDragCol] = React.useState(null);
336
+ // Let a plain vertical wheel scroll the (wide) table horizontally when it has
337
+ // no vertical overflow of its own — otherwise the wheel just scrolls the page
338
+ // and the extra columns feel unreachable. Non-passive so we can preventDefault.
339
+ React.useEffect(() => {
340
+ const el = scrollRef.current;
341
+ if (!el)
342
+ return;
343
+ const onWheel = (e) => {
344
+ // don't hijack the wheel when it's over an open cell popover (its own list
345
+ // scrolls) — only map vertical wheel to horizontal for the table itself
346
+ if (e.target?.closest?.(".select-listbox, .multiselect-listbox, .datepicker-cal, .recordstable-menu"))
347
+ return;
348
+ const canH = el.scrollWidth > el.clientWidth;
349
+ const canV = el.scrollHeight > el.clientHeight;
350
+ if (canH && !canV && e.deltaX === 0 && e.deltaY !== 0) {
351
+ el.scrollLeft += e.deltaY;
352
+ e.preventDefault();
353
+ }
354
+ };
355
+ el.addEventListener("wheel", onWheel, { passive: false });
356
+ return () => el.removeEventListener("wheel", onWheel);
357
+ }, []);
358
+ // close any open menu on outside click (anything not inside a menu or its
359
+ // trigger) or Escape
360
+ React.useEffect(() => {
361
+ if (!openMenu)
362
+ return;
363
+ const onDown = (e) => {
364
+ const t = e.target;
365
+ if (!t.closest(".recordstable-menu, [data-menu-trigger]"))
366
+ setOpenMenu(null);
367
+ };
368
+ const onKey = (e) => e.key === "Escape" && setOpenMenu(null);
369
+ document.addEventListener("mousedown", onDown);
370
+ document.addEventListener("keydown", onKey);
371
+ return () => {
372
+ document.removeEventListener("mousedown", onDown);
373
+ document.removeEventListener("keydown", onKey);
374
+ };
375
+ }, [openMenu]);
376
+ // dismiss the right-click row context menu on any outside click / Escape
377
+ React.useEffect(() => {
378
+ if (!ctxMenu)
379
+ return;
380
+ const onDown = (e) => {
381
+ if (!e.target.closest(".recordstable-ctx"))
382
+ setCtxMenu(null);
383
+ };
384
+ const onKey = (e) => e.key === "Escape" && setCtxMenu(null);
385
+ document.addEventListener("mousedown", onDown);
386
+ document.addEventListener("keydown", onKey);
387
+ return () => {
388
+ document.removeEventListener("mousedown", onDown);
389
+ document.removeEventListener("keydown", onKey);
390
+ };
391
+ }, [ctxMenu]);
392
+ const activeFilters = filterConds.length;
393
+ const addFilter = (key) => {
394
+ idCounter.current += 1;
395
+ const id = `filter-${idCounter.current}`;
396
+ setFilterConds((prev) => [...prev, { id, key, op: "contains", value: "" }]);
397
+ setFilterQuery("");
398
+ setOpenMenu(`fcond:${id}`); // open its condition editor right away
399
+ };
400
+ const patchFilter = (id, patch) => setFilterConds((prev) => prev.map((c) => (c.id === id ? { ...c, ...patch } : c)));
401
+ const removeFilter = (id) => {
402
+ setFilterConds((prev) => prev.filter((c) => c.id !== id));
403
+ setOpenMenu(null);
404
+ };
405
+ const [agg, setAgg] = React.useState(() => {
406
+ const m = {};
407
+ for (const col of columnsProp) {
408
+ m[col.key] =
409
+ col.type === "number"
410
+ ? "sum"
411
+ : col.type === "entity"
412
+ ? "count"
413
+ : "none";
414
+ }
415
+ return m;
416
+ });
417
+ // selection — controlled if `selectedIds` is provided, else internal
418
+ const [internalSel, setInternalSel] = React.useState(() => new Set());
419
+ const selected = React.useMemo(() => (selectedIds ? new Set(selectedIds) : internalSel), [selectedIds, internalSel]);
420
+ const setSelected = React.useCallback((next) => {
421
+ onSelectionChange?.([...next]);
422
+ if (!selectedIds)
423
+ setInternalSel(next);
424
+ }, [onSelectionChange, selectedIds]);
425
+ const filtered = React.useMemo(() => {
426
+ const active = filterConds.filter((c) => !opNeedsValue(c.op) || c.value.trim());
427
+ if (!active.length)
428
+ return data;
429
+ return data.filter((row) => active.every((cond) => {
430
+ const col = cols.find((c) => c.key === cond.key);
431
+ return col ? matchesFilter(col, row, cond) : true;
432
+ }));
433
+ }, [data, cols, filterConds]);
434
+ const visibleRows = React.useMemo(() => {
435
+ if (sorts.length === 0)
436
+ return filtered;
437
+ const getters = sorts.map((s) => {
438
+ const col = cols.find((c) => c.key === s.key);
439
+ return (r) => {
440
+ const v = r[s.key];
441
+ if (col?.type === "entity")
442
+ return v?.name ?? "";
443
+ if (Array.isArray(v))
444
+ return v[0] ?? "";
445
+ return v ?? "";
446
+ };
447
+ });
448
+ return [...filtered].sort((a, b) => {
449
+ for (let i = 0; i < sorts.length; i++) {
450
+ const get = getters[i];
451
+ const av = get(a);
452
+ const bv = get(b);
453
+ const cmp = typeof av === "number" && typeof bv === "number"
454
+ ? av - bv
455
+ : String(av).localeCompare(String(bv));
456
+ if (cmp !== 0)
457
+ return sorts[i].dir === "asc" ? cmp : -cmp;
458
+ }
459
+ return 0;
460
+ });
461
+ }, [filtered, cols, sorts]);
462
+ // arrow-key navigation over the selected cell (spreadsheet-style)
463
+ const onGridKeyDown = (e) => {
464
+ if (!selCell || editing)
465
+ return;
466
+ // ⌘C / Ctrl+C — copy the selected cell's rendered text to the clipboard
467
+ if ((e.metaKey || e.ctrlKey) && (e.key === "c" || e.key === "C")) {
468
+ const row = visibleRows.find((rw) => rw.id === selCell.id);
469
+ const col = cols.find((cc) => cc.key === selCell.key);
470
+ if (row && col)
471
+ navigator.clipboard?.writeText(cellText(col, row));
472
+ e.preventDefault();
473
+ return;
474
+ }
475
+ const r = visibleRows.findIndex((row) => row.id === selCell.id);
476
+ const c = cols.findIndex((col) => col.key === selCell.key);
477
+ if (r < 0 || c < 0)
478
+ return;
479
+ let nr = r;
480
+ let nc = c;
481
+ if (e.key === "ArrowUp")
482
+ nr = Math.max(0, r - 1);
483
+ else if (e.key === "ArrowDown")
484
+ nr = Math.min(visibleRows.length - 1, r + 1);
485
+ else if (e.key === "ArrowLeft")
486
+ nc = Math.max(0, c - 1);
487
+ else if (e.key === "ArrowRight")
488
+ nc = Math.min(cols.length - 1, c + 1);
489
+ else if (e.key === "Escape") {
490
+ setSelCell(null);
491
+ return;
492
+ }
493
+ else if (e.key === "Enter") {
494
+ if (canEditCol(cols[c])) {
495
+ e.preventDefault();
496
+ setEditing({ id: selCell.id, key: selCell.key });
497
+ }
498
+ return;
499
+ }
500
+ else
501
+ return;
502
+ e.preventDefault();
503
+ setSelCell({ id: visibleRows[nr].id, key: cols[nc].key });
504
+ };
505
+ const allSelected = visibleRows.length > 0 && visibleRows.every((r) => selected.has(r.id));
506
+ const someSelected = selected.size > 0 && !allSelected;
507
+ const toggleAll = () => setSelected(allSelected ? new Set() : new Set(visibleRows.map((r) => r.id)));
508
+ const toggleRow = (id) => {
509
+ const next = new Set(selected);
510
+ // shift-click → select the contiguous range from the anchor to this row
511
+ if (shiftHeld.current && selAnchor.current) {
512
+ const a = visibleRows.findIndex((r) => r.id === selAnchor.current);
513
+ const b = visibleRows.findIndex((r) => r.id === id);
514
+ if (a >= 0 && b >= 0) {
515
+ const [lo, hi] = a < b ? [a, b] : [b, a];
516
+ for (let i = lo; i <= hi; i++)
517
+ next.add(visibleRows[i].id);
518
+ shiftHeld.current = false;
519
+ setSelected(next);
520
+ return;
521
+ }
522
+ }
523
+ if (next.has(id))
524
+ next.delete(id);
525
+ else
526
+ next.add(id);
527
+ selAnchor.current = id;
528
+ shiftHeld.current = false;
529
+ setSelected(next);
530
+ };
531
+ const addSort = () => {
532
+ const used = new Set(sorts.map((s) => s.key));
533
+ const col = cols.find((c) => !used.has(c.key));
534
+ if (col)
535
+ setSorts([...sorts, { key: col.key, dir: "asc" }]);
536
+ };
537
+ const moveSort = (from, to) => setSorts((prev) => {
538
+ if (from === to)
539
+ return prev;
540
+ const next = [...prev];
541
+ const [moved] = next.splice(from, 1);
542
+ next.splice(to, 0, moved);
543
+ return next;
544
+ });
545
+ const setSortField = (i, key) => setSorts(sorts.map((s, j) => (j === i ? { ...s, key } : s)));
546
+ const setSortDir = (i, dir) => setSorts(sorts.map((s, j) => (j === i ? { ...s, dir } : s)));
547
+ const removeSort = (i) => setSorts(sorts.filter((_, j) => j !== i));
548
+ const commitEdit = (id, key, raw) => {
549
+ const col = cols.find((c) => c.key === key);
550
+ const numeric = col?.type === "number" || col?.type === "currency";
551
+ const coerce = (r) => {
552
+ if (numeric)
553
+ return raw === "" ? null : Number(raw.replace(/[^0-9.-]/g, ""));
554
+ if (col?.type === "tags")
555
+ return raw
556
+ .split(",")
557
+ .map((t) => t.trim())
558
+ .filter(Boolean);
559
+ if (col?.type === "entity")
560
+ return { ...r[key], name: raw };
561
+ return raw;
562
+ };
563
+ setData((prev) => prev.map((r) => (r.id === id ? { ...r, [key]: coerce(r) } : r)));
564
+ setEditing(null);
565
+ };
566
+ // the option set for a `tags` column — the union of every value in that column,
567
+ // so the tag editor offers the existing options (each keeps a stable colour).
568
+ const tagOptionsFor = React.useCallback((key) => {
569
+ const set = new Set();
570
+ for (const r of data) {
571
+ const v = r[key];
572
+ if (Array.isArray(v))
573
+ for (const t of v)
574
+ set.add(t);
575
+ }
576
+ return [...set].map((t) => ({ value: t, label: t, tone: tagTone(t) }));
577
+ }, [data]);
578
+ // set any cell value directly (used by the status / date / reference pickers)
579
+ // biome-ignore lint/suspicious/noExplicitAny: cell values are column-type dependent
580
+ const setCell = (id, key, value) => {
581
+ setData((prev) => prev.map((r) => (r.id === id ? { ...r, [key]: value } : r)));
582
+ };
583
+ const addRow = () => {
584
+ idCounter.current += 1;
585
+ const id = `new-${idCounter.current}`;
586
+ const entityKey = cols.find((c) => c.type === "entity")?.key;
587
+ const row = { id };
588
+ if (entityKey)
589
+ row[entityKey] = { name: "Untitled" };
590
+ setData((prev) => [...prev, row]);
591
+ if (entityKey)
592
+ setEditing(null);
593
+ };
594
+ const sortBy = (key, dir) => {
595
+ setSorts([{ key, dir }]);
596
+ setOpenMenu(null);
597
+ };
598
+ const hideColumn = (key) => {
599
+ setCols((prev) => prev.filter((c) => c.key !== key));
600
+ setOpenMenu(null);
601
+ };
602
+ // Move a column one slot left/right. The first (entity) column is sticky and
603
+ // stays pinned, so reordering is clamped to index >= 1.
604
+ const moveColumn = (key, dir) => {
605
+ setCols((prev) => {
606
+ const i = prev.findIndex((c) => c.key === key);
607
+ const j = i + dir;
608
+ if (i < 1 || j < 1 || j >= prev.length)
609
+ return prev;
610
+ const next = [...prev];
611
+ [next[i], next[j]] = [next[j], next[i]];
612
+ return next;
613
+ });
614
+ setOpenMenu(null);
615
+ };
616
+ // drop the dragged column immediately before `toKey` (drag-to-reorder). The
617
+ // sticky entity column (index 0) stays pinned — never reorder into slot 0.
618
+ const reorderColumn = (fromKey, toKey) => {
619
+ setDragCol(null);
620
+ if (fromKey === toKey)
621
+ return;
622
+ setCols((prev) => {
623
+ const from = prev.findIndex((c) => c.key === fromKey);
624
+ let to = prev.findIndex((c) => c.key === toKey);
625
+ if (from < 1 || to < 1)
626
+ return prev;
627
+ const next = [...prev];
628
+ const [moved] = next.splice(from, 1);
629
+ to = next.findIndex((c) => c.key === toKey);
630
+ next.splice(to, 0, moved);
631
+ return next;
632
+ });
633
+ };
634
+ const renameColumn = (key, label) => {
635
+ const name = label.trim();
636
+ if (name)
637
+ setCols((prev) => prev.map((c) => (c.key === key ? { ...c, label: name } : c)));
638
+ setRenamingCol(null);
639
+ };
640
+ const openColumnMenu = (key, el) => {
641
+ if (openMenu === `col:${key}`) {
642
+ setOpenMenu(null);
643
+ return;
644
+ }
645
+ const r = el.getBoundingClientRect();
646
+ setMenuPos({ top: r.bottom + 4, left: r.left });
647
+ setOpenMenu(`col:${key}`);
648
+ };
649
+ const addColumn = () => {
650
+ idCounter.current += 1;
651
+ const key = `field-${idCounter.current}`;
652
+ setCols((prev) => [
653
+ ...prev,
654
+ { key, label: `Field ${prev.length}`, type: "text", width: 160 },
655
+ ]);
656
+ };
657
+ const deleteSelected = () => {
658
+ setData((prev) => prev.filter((r) => !selected.has(r.id)));
659
+ setSelected(new Set());
660
+ };
661
+ const exportSelected = () => {
662
+ const picked = data.filter((r) => selected.has(r.id));
663
+ const esc = (s) => `"${s.replace(/"/g, '""')}"`;
664
+ const csv = [
665
+ cols.map((c) => esc(c.label)).join(","),
666
+ ...picked.map((r) => cols.map((c) => esc(cellText(c, r))).join(",")),
667
+ ].join("\n");
668
+ const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
669
+ const a = document.createElement("a");
670
+ a.href = url;
671
+ a.download = "records.csv";
672
+ a.click();
673
+ URL.revokeObjectURL(url);
674
+ setOpenMenu(null);
675
+ };
676
+ const entityKey = cols.find((c) => c.type === "entity")?.key;
677
+ // "Add to list" → pin the selected rows (shows a ★ on the entity)
678
+ const addToList = () => {
679
+ setPinned((prev) => new Set([...prev, ...selected]));
680
+ setSelected(new Set());
681
+ };
682
+ // "Send email" → open the mail client addressed to the selected records
683
+ const sendEmail = () => {
684
+ const names = data
685
+ .filter((r) => selected.has(r.id))
686
+ .map((r) => entityKey ? cellText({ key: entityKey, label: "" }, r) : r.id);
687
+ window.open(`mailto:?subject=${encodeURIComponent(names.join(", "))}`);
688
+ };
689
+ // "Run workflow" → a demo automation: duplicate the selected records
690
+ const runWorkflow = () => {
691
+ setData((prev) => {
692
+ const copies = prev
693
+ .filter((r) => selected.has(r.id))
694
+ .map((r) => {
695
+ idCounter.current += 1;
696
+ return { ...r, id: `${r.id}-copy-${idCounter.current}` };
697
+ });
698
+ return [...prev, ...copies];
699
+ });
700
+ setSelected(new Set());
701
+ };
702
+ // single-row actions used by the right-click context menu
703
+ const duplicateRow = (id) => {
704
+ setData((prev) => {
705
+ const i = prev.findIndex((r) => r.id === id);
706
+ if (i < 0)
707
+ return prev;
708
+ idCounter.current += 1;
709
+ const copy = { ...prev[i], id: `${id}-copy-${idCounter.current}` };
710
+ const next = [...prev];
711
+ next.splice(i + 1, 0, copy);
712
+ return next;
713
+ });
714
+ setCtxMenu(null);
715
+ };
716
+ const deleteRow = (id) => {
717
+ setData((prev) => prev.filter((r) => r.id !== id));
718
+ if (selected.has(id)) {
719
+ const next = new Set(selected);
720
+ next.delete(id);
721
+ setSelected(next);
722
+ }
723
+ setCtxMenu(null);
724
+ };
725
+ const copyRow = (id) => {
726
+ const row = data.find((r) => r.id === id);
727
+ const col = cols.find((c) => c.type === "entity");
728
+ if (row && col)
729
+ navigator.clipboard?.writeText(cellText(col, row));
730
+ setCtxMenu(null);
731
+ };
732
+ const pinRow = (id) => {
733
+ setPinned((prev) => new Set([...prev, id]));
734
+ setCtxMenu(null);
735
+ };
736
+ const setAggValue = (key, value) => {
737
+ setAgg((prev) => ({ ...prev, [key]: value }));
738
+ setOpenMenu(null);
739
+ };
740
+ // column resize via pointer drag on the header edge
741
+ const resizeCol = (key, startX, startW) => {
742
+ // guard the whole document during the drag: kill text selection and force a
743
+ // resize cursor so the gesture doesn't select cells or flicker (was buggy)
744
+ const prevSelect = document.body.style.userSelect;
745
+ const prevCursor = document.body.style.cursor;
746
+ document.body.style.userSelect = "none";
747
+ document.body.style.cursor = "col-resize";
748
+ const onMove = (e) => {
749
+ const w = Math.round(Math.max(72, startW + (e.clientX - startX)));
750
+ setCols((prev) => prev.map((c) => (c.key === key ? { ...c, width: w } : c)));
751
+ };
752
+ const onUp = () => {
753
+ document.body.style.userSelect = prevSelect;
754
+ document.body.style.cursor = prevCursor;
755
+ window.removeEventListener("pointermove", onMove);
756
+ window.removeEventListener("pointerup", onUp);
757
+ };
758
+ window.addEventListener("pointermove", onMove);
759
+ window.addEventListener("pointerup", onUp);
760
+ };
761
+ return (_jsxs("div", { className: recordstableRecipe({ className }), ref: rootRef, children: [_jsxs("div", { className: "recordstable-toolbar", children: [_jsxs("span", { className: "recordstable-toolgroup", children: [_jsxs("button", { type: "button", className: `recordstable-tool${sorts.length ? " recordstable-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" }), sorts.length
762
+ ? `Sorted by ${cols.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
763
+ : "Sort"] }), openMenu === "sort" && (_jsxs("div", { className: "recordstable-menu recordstable-sortpop", role: "dialog", "aria-label": "Sort", children: [sorts.length === 0 ? (_jsx("div", { className: "recordstable-sort-empty", children: "No sorts applied to this view." })) : (sorts.map((s, i) => (_jsxs("div", { className: "recordstable-sort-row", onDragOver: (e) => {
764
+ if (dragSort !== null)
765
+ e.preventDefault();
766
+ }, onDrop: () => {
767
+ if (dragSort !== null)
768
+ moveSort(dragSort, i);
769
+ setDragSort(null);
770
+ }, children: [_jsx("button", { type: "button", className: "recordstable-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: "recordstable-sort-field", searchable: true, value: s.key, onValueChange: (k) => setSortField(i, k), options: cols.map((c) => ({
771
+ value: c.key,
772
+ label: c.icon ? (_jsxs("span", { className: "recordstable-sort-optlabel", children: [_jsx(c.icon, { size: 15, strokeWidth: 1.75 }), c.label] })) : (c.label),
773
+ keywords: c.label,
774
+ })), "aria-label": "Sort field" }), _jsx(Select, { className: "recordstable-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
775
+ {
776
+ value: "asc",
777
+ label: (_jsxs("span", { className: "recordstable-sort-optlabel", children: [_jsx(ArrowUpNarrowWide, { size: 15, strokeWidth: 1.75 }), "Ascending"] })),
778
+ keywords: "Ascending",
779
+ },
780
+ {
781
+ value: "desc",
782
+ label: (_jsxs("span", { className: "recordstable-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), "Descending"] })),
783
+ keywords: "Descending",
784
+ },
785
+ ], "aria-label": "Sort direction" }), _jsx("button", { type: "button", className: "recordstable-sort-remove", onClick: () => removeSort(i), "aria-label": "Remove sort", children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "recordstable-sort-addrow", children: _jsxs("button", { type: "button", className: "recordstable-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), "Add sort"] }) }), _jsx("div", { className: "recordstable-sort-foot", children: _jsxs("a", { className: "recordstable-sort-learn", href: "https://attio.com/help", target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), "Learn about sorting"] }) })] }))] }), _jsxs("span", { className: "recordstable-toolgroup", children: [_jsxs("button", { type: "button", className: `recordstable-tool recordstable-tool--ghost${activeFilters ? " recordstable-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: "recordstable-menu", role: "menu", children: [_jsx("input", { className: "recordstable-menu-input", placeholder: "Filter by\u2026", "aria-label": "Filter by field",
786
+ // biome-ignore lint/a11y/noAutofocus: focus the picker on open
787
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), cols
788
+ .filter((col) => col.label
789
+ .toLowerCase()
790
+ .includes(filterQuery.trim().toLowerCase()))
791
+ .map((col) => (_jsxs("button", { type: "button", className: "recordstable-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)))] }))] }), filterConds.map((cond) => {
792
+ const col = cols.find((c) => c.key === cond.key);
793
+ const open = openMenu === `fcond:${cond.id}`;
794
+ const hasValue = !opNeedsValue(cond.op) || cond.value.trim();
795
+ return (_jsxs("span", { className: "recordstable-toolgroup", children: [_jsxs("span", { className: "recordstable-fchip", children: [_jsxs("button", { type: "button", className: "recordstable-fchip-main", "data-menu-trigger": true, "aria-haspopup": "menu", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [_jsx("span", { className: "recordstable-fchip-field", children: col?.label }), hasValue ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "recordstable-fchip-op", children: OP_LABELS[cond.op] }), opNeedsValue(cond.op) && (_jsx("span", { className: "recordstable-fchip-val", children: cond.value }))] })) : (_jsx("span", { className: "recordstable-fchip-op", children: "Select condition" }))] }), _jsx("button", { type: "button", className: "recordstable-fchip-x", "aria-label": `Remove ${col?.label} filter`, onClick: () => removeFilter(cond.id), children: _jsx(X, { size: 13, strokeWidth: 2 }) })] }), open && (_jsxs("div", { className: "recordstable-menu", role: "menu", children: [_jsxs("div", { className: "recordstable-menu-head", children: [col?.label, " condition"] }), OP_ORDER.map((op) => (_jsxs("button", { type: "button", className: "recordstable-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [OP_LABELS[op], cond.op === op ? (_jsx(Check, { className: "recordstable-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && (_jsx("input", { className: "recordstable-menu-input", placeholder: "Value\u2026", "aria-label": `${col?.label} filter value`,
796
+ // biome-ignore lint/a11y/noAutofocus: focus the value field
797
+ autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) }))] }))] }, cond.id));
798
+ }), _jsx("span", { className: "recordstable-spacer" }), _jsxs("span", { className: "recordstable-count", children: [visibleRows.length, " record", visibleRows.length === 1 ? "" : "s"] }), showNew && (_jsxs("button", { type: "button", className: "recordstable-new", onClick: () => setCreating(true), children: [_jsx(Plus, { size: 15, strokeWidth: 2, "aria-hidden": "true" }), "New"] }))] }), _jsxs("div", { ref: scrollRef, className: "recordstable-scroll", tabIndex: -1, "data-scrolled": scrolledX ? "" : undefined, "data-scrolled-y": scrolledY ? "" : undefined, onKeyDown: onGridKeyDown, onScroll: (e) => {
799
+ const left = e.currentTarget.scrollLeft > 0;
800
+ const top = e.currentTarget.scrollTop > 0;
801
+ setScrolledX((prev) => (prev === left ? prev : left));
802
+ setScrolledY((prev) => (prev === top ? prev : top));
803
+ }, children: [_jsxs("table", { className: "recordstable-table", style: {
804
+ // fill the container; the trailing add-column absorbs the slack (a
805
+ // flexible filler). minWidth keeps real column widths and drives the
806
+ // horizontal scroll once the columns outgrow the container.
807
+ width: "100%",
808
+ minWidth: CHECK_W +
809
+ ADDCOL_W +
810
+ cols.reduce((sum, c) => sum + colWidth(c), 0),
811
+ }, children: [_jsxs("colgroup", { children: [_jsx("col", { style: { width: CHECK_W } }), cols.map((col) => (_jsx("col", { style: { width: colWidth(col) } }, col.key))), _jsx("col", {})] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { className: "recordstable-th recordstable-col-check", children: _jsx("span", { className: "recordstable-check-inner", children: _jsx(Checkbox, { "aria-label": "Select all", checked: allSelected, indeterminate: someSelected, onChange: toggleAll }) }) }), cols.map((col, i) => {
812
+ const Icon = col.icon;
813
+ const sc = sorts.find((s) => s.key === col.key);
814
+ return (_jsxs("th", { className: `recordstable-th${i === 0 ? " recordstable-col-entity" : ""}`, "data-sorted": sc?.dir, "data-drop": dragCol && dragCol !== col.key && i >= 1 ? "" : undefined, onDragOver: dragCol && i >= 1 ? (e) => e.preventDefault() : undefined, onDrop: dragCol && i >= 1
815
+ ? (e) => {
816
+ e.preventDefault();
817
+ reorderColumn(dragCol, col.key);
818
+ }
819
+ : undefined, children: [renamingCol === col.key ? (_jsx("input", { className: "recordstable-th-rename",
820
+ // biome-ignore lint/a11y/noAutofocus: focus the rename field the moment it opens
821
+ autoFocus: true, defaultValue: col.label, "aria-label": "Column label", onKeyDown: (e) => {
822
+ if (e.key === "Enter")
823
+ renameColumn(col.key, e.currentTarget.value);
824
+ else if (e.key === "Escape")
825
+ setRenamingCol(null);
826
+ }, onBlur: (e) => renameColumn(col.key, e.currentTarget.value) })) : (_jsxs("button", { type: "button", className: "recordstable-th-inner", "data-menu-trigger": true, "aria-haspopup": "menu", draggable: i >= 1, onDragStart: i >= 1
827
+ ? (e) => {
828
+ setDragCol(col.key);
829
+ e.dataTransfer.effectAllowed = "move";
830
+ }
831
+ : undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? _jsx(Icon, { size: 14, strokeWidth: 1.75 }) : null, _jsx("span", { className: "recordstable-th-label", children: col.label }), _jsx("span", { className: "recordstable-sort", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
832
+ rootRef.current &&
833
+ createPortal(_jsxs("div", { className: "recordstable-menu recordstable-menu--fixed recordstable-menu--hdr", role: "menu", style: menuPos ?? undefined, children: [_jsxs("button", { type: "button", className: "recordstable-menu-item", onClick: () => sortBy(col.key, "asc"), children: [_jsx(ArrowUp, { size: 15, strokeWidth: 1.75 }), "Sort ascending"] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", onClick: () => sortBy(col.key, "desc"), children: [_jsx(ArrowDown, { size: 15, strokeWidth: 1.75 }), "Sort descending"] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [_jsx(ArrowLeft, { size: 15, strokeWidth: 1.75 }), "Move left"] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", disabled: i === 0 || i >= cols.length - 1, onClick: () => moveColumn(col.key, 1), children: [_jsx(ArrowRight, { size: 15, strokeWidth: 1.75 }), "Move right"] }), _jsx("div", { className: "recordstable-menu-sep" }), _jsxs("button", { type: "button", className: "recordstable-menu-item", onClick: () => {
834
+ setRenamingCol(col.key);
835
+ setOpenMenu(null);
836
+ }, children: [_jsx(Pencil, { size: 15, strokeWidth: 1.75 }), "Edit column label"] }), cols.length > 1 && (_jsxs("button", { type: "button", className: "recordstable-menu-item", onClick: () => hideColumn(col.key), children: [_jsx(EyeOff, { size: 15, strokeWidth: 1.75 }), "Hide from view"] }))] }), rootRef.current), _jsx("span", { className: "recordstable-resize", "aria-hidden": "true", onPointerDown: (e) => {
837
+ e.preventDefault();
838
+ e.stopPropagation();
839
+ const th = e.currentTarget
840
+ .parentElement;
841
+ resizeCol(col.key, e.clientX, th ? th.getBoundingClientRect().width : 180);
842
+ }, onClick: (e) => e.stopPropagation() })] }, col.key));
843
+ }), _jsx("th", { className: "recordstable-th recordstable-addcol", children: allowAddColumn && (_jsx("button", { type: "button", onClick: addColumn, "aria-label": "Add column", children: _jsx(Plus, { size: 15, strokeWidth: 1.75 }) })) })] }) }), _jsx("tbody", { children: visibleRows.map((row) => (_jsxs("tr", { "data-selected": selected.has(row.id) ? "" : undefined, onContextMenu: (e) => {
844
+ e.preventDefault();
845
+ setCtxMenu({ x: e.clientX, y: e.clientY, id: row.id });
846
+ }, children: [_jsx("td", { className: "recordstable-td recordstable-col-check", children: _jsx("span", { className: "recordstable-check-inner", children: _jsx(Checkbox, { "aria-label": `Select ${row.id}`, checked: selected.has(row.id), onClick: (e) => {
847
+ shiftHeld.current = e.shiftKey;
848
+ }, onChange: () => toggleRow(row.id) }) }) }), cols.map((col, i) => {
849
+ const canEdit = canEditCol(col);
850
+ const isPicker = isPickerCol(col);
851
+ const isEditing = editing?.id === row.id && editing?.key === col.key;
852
+ return (_jsxs("td", { className: `recordstable-td${i === 0 ? " recordstable-col-entity" : ""}${canEdit ? " recordstable-td--editable" : ""}${isPicker ? " recordstable-td--picker" : ""}`, "data-selected": selCell?.id === row.id && selCell?.key === col.key
853
+ ? ""
854
+ : undefined, onClick: () => {
855
+ if (isPicker)
856
+ return;
857
+ setSelCell({ id: row.id, key: col.key });
858
+ scrollRef.current?.focus({ preventScroll: true });
859
+ }, onDoubleClick: canEdit
860
+ ? () => setEditing({ id: row.id, key: col.key })
861
+ : undefined, children: [isPicker ? (col.type === "status" ? (_jsx(StatusCell, { column: col, row: row, onChange: (v) => setCell(row.id, col.key, v) })) : col.type === "reference" ? (_jsx(ReferenceCell, { column: col, row: row, onChange: (v) => setCell(row.id, col.key, v) })) : col.type === "tags" ? (_jsx(TagsCell, { column: col, row: row, options: tagOptionsFor(col.key), onChange: (v) => setCell(row.id, col.key, v) })) : (_jsx(DateCell, { column: col, row: row, onChange: (v) => setCell(row.id, col.key, v) }))) : isEditing ? (_jsx("input", { className: `recordstable-edit${col.type === "number" ? " recordstable-edit--num" : ""}`,
862
+ // biome-ignore lint/a11y/noAutofocus: focusing the just-opened editor is the expected behavior
863
+ autoFocus: true, defaultValue: cellText(col, row), onBlur: (e) => commitEdit(row.id, col.key, e.target.value), onKeyDown: (e) => {
864
+ if (e.key === "Enter")
865
+ commitEdit(row.id, col.key, e.target.value);
866
+ if (e.key === "Escape")
867
+ setEditing(null);
868
+ } })) : (_jsx(CellView, { column: col, row: row, pinned: i === 0 && pinned.has(row.id), onOpenPage: i === 0 && onOpenRecordPage
869
+ ? () => onOpenRecordPage(row)
870
+ : undefined, avatarTone: avatarTone })), i === 0 && onOpenRecord && !isEditing && (_jsx("button", { type: "button", className: "recordstable-open", "aria-label": `Open ${cellText(col, row)}`, onClick: (e) => {
871
+ e.stopPropagation();
872
+ onOpenRecord(row);
873
+ }, children: _jsx(ArrowUpRight, { size: 15, strokeWidth: 1.75 }) }))] }, col.key));
874
+ }), _jsx("td", { className: "recordstable-td recordstable-addcol" })] }, row.id))) }), _jsx("tfoot", { children: _jsxs("tr", { children: [_jsx("td", { className: "recordstable-sum recordstable-col-check" }), cols.map((col, i) => {
875
+ const a = agg[col.key] ?? "none";
876
+ const res = summarize(col, a, visibleRows);
877
+ const sumOpen = openMenu === `sum:${col.key}`;
878
+ return (_jsx("td", { className: `recordstable-sum${i === 0 ? " recordstable-col-entity" : ""}`, children: _jsxs("span", { className: "recordstable-toolgroup recordstable-sumgroup", children: [_jsx("button", { type: "button", className: "recordstable-sum-btn", "data-empty": res ? undefined : "", "data-menu-trigger": true, "aria-haspopup": "menu", onClick: (e) => {
879
+ if (sumOpen) {
880
+ setOpenMenu(null);
881
+ return;
882
+ }
883
+ const r = e.currentTarget.getBoundingClientRect();
884
+ setMenuPos({
885
+ bottom: window.innerHeight - r.top + 4,
886
+ right: window.innerWidth - r.right,
887
+ });
888
+ setOpenMenu(`sum:${col.key}`);
889
+ }, children: res ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "recordstable-sum-label", children: AGG_LABELS[a] }), _jsx("span", { className: "recordstable-sum-value", children: res.value })] })) : (_jsx("span", { className: "recordstable-sum-label", children: "Calculate" })) }), sumOpen &&
890
+ rootRef.current &&
891
+ createPortal(_jsx("div", { className: "recordstable-menu recordstable-menu--fixed recordstable-menu--up", role: "menu", style: menuPos ?? undefined, children: aggOptions(col).map((opt) => (_jsxs("button", { type: "button", className: "recordstable-menu-item", "data-on": a === opt ? "" : undefined, onClick: () => setAggValue(col.key, opt), children: [AGG_LABELS[opt], a === opt ? (_jsx(Check, { className: "recordstable-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, opt))) }), rootRef.current)] }) }, col.key));
892
+ }), _jsx("td", { className: "recordstable-sum recordstable-addcol" })] }) })] }), visibleRows.length === 0 ? (_jsx("div", { className: "recordstable-empty", children: _jsx(EmptyState, { variant: "page", icon: activeFilters ? (_jsx(ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : (_jsx(Inbox, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })), title: activeFilters
893
+ ? "No records match that filter"
894
+ : "No records yet", description: activeFilters
895
+ ? "None of your records match the filters that are set."
896
+ : `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? (_jsxs(Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [_jsx(ListFilter, { size: 15, strokeWidth: 1.75 }), "Clear filter"] })) : (_jsxs(Button, { intent: "primary", size: "sm", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), "New record"] })) }) })) : (_jsxs("button", { type: "button", className: "recordstable-addrow", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), "New record"] }))] }), selected.size > 0 && (_jsxs(BulkBar, { count: selected.size, countLabel: "Selected", onClear: () => setSelected(new Set()), children: [_jsx(BulkBarButton, { icon: _jsx(ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: "Add to list" }), _jsx(BulkBarButton, { icon: _jsx(Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: "Send email" }), _jsx(BulkBarButton, { icon: _jsx(Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: "Run workflow" }), _jsxs(Menu, { align: "end", "aria-label": "More actions", trigger: _jsx(BulkBarButton, { icon: _jsx(MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: "More" }), children: [_jsx(MenuItem, { icon: _jsx(Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: "Export CSV" }), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: "Delete" })] })] })), _jsx(CreateRecordDialog, { open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
897
+ createPortal(_jsxs("div", { className: "recordstable-menu recordstable-ctx", role: "menu", style: { position: "fixed", top: ctxMenu.y, left: ctxMenu.x }, children: [_jsxs("button", { type: "button", className: "recordstable-menu-item", onClick: () => copyRow(ctxMenu.id), children: [_jsx(Copy, { size: 15, strokeWidth: 1.75 }), "Copy"] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", onClick: () => duplicateRow(ctxMenu.id), children: [_jsx(CopyPlus, { size: 15, strokeWidth: 1.75 }), "Duplicate"] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", onClick: () => pinRow(ctxMenu.id), children: [_jsx(Star, { size: 15, strokeWidth: 1.75 }), "Add to list"] }), _jsxs("button", { type: "button", className: "recordstable-menu-item recordstable-menu-item--danger", onClick: () => deleteRow(ctxMenu.id), children: [_jsx(Trash2, { size: 15, strokeWidth: 1.75 }), "Delete"] })] }), rootRef.current ?? document.body)] }));
898
+ }
899
+ export const defaultColumns = [
900
+ { key: "company", label: "Company", type: "entity", icon: Building2 },
901
+ { key: "domain", label: "Domain", type: "url", icon: Globe },
902
+ {
903
+ key: "categories",
904
+ label: "Categories",
905
+ type: "tags",
906
+ icon: Tag,
907
+ width: 240,
908
+ },
909
+ {
910
+ key: "employees",
911
+ label: "Employees",
912
+ type: "number",
913
+ icon: Users,
914
+ width: 120,
915
+ },
916
+ { key: "location", label: "Location", type: "text", icon: MapPin },
917
+ { key: "stage", label: "Stage", type: "tags", icon: TrendingUp, width: 150 },
918
+ { key: "ceo", label: "CEO", type: "text", icon: Briefcase, width: 170 },
919
+ { key: "twitter", label: "Twitter", type: "url", icon: AtSign, width: 150 },
920
+ {
921
+ key: "followers",
922
+ label: "Followers",
923
+ type: "number",
924
+ icon: Users,
925
+ width: 120,
926
+ },
927
+ {
928
+ key: "linkedin",
929
+ label: "LinkedIn",
930
+ type: "url",
931
+ icon: Globe,
932
+ width: 180,
933
+ },
934
+ {
935
+ key: "founded",
936
+ label: "Founded",
937
+ type: "date",
938
+ icon: Calendar,
939
+ width: 110,
940
+ },
941
+ { key: "arr", label: "Est. ARR", type: "number", icon: Hash, width: 130 },
942
+ ];
943
+ // biome-ignore format: one row per line keeps the wide demo dataset readable
944
+ const RAW_ROWS = [
945
+ { id: "google", company: { name: "Google" }, domain: "google.com", categories: ["Search", "Ads", "Cloud", "AI", "Android", "Maps", "Hardware", "Workspace", "Chrome"], employees: 182000, location: "Mountain View, CA", stage: ["Public"], ceo: "Sundar Pichai", twitter: "twitter.com/google", followers: 29800000, linkedin: "linkedin.com/company/google", founded: "1998", arr: 307000000000 },
946
+ { id: "stripe", company: { name: "Stripe" }, domain: "stripe.com", categories: ["Payments", "Fintech"], employees: 8000, location: "San Francisco, CA", stage: ["Series I"], ceo: "Patrick Collison", twitter: "twitter.com/stripe", followers: 1200000, linkedin: "linkedin.com/company/stripe", founded: "2010", arr: 16000000000 },
947
+ { id: "figma", company: { name: "Figma" }, domain: "figma.com", categories: ["Design", "SaaS"], employees: 1300, location: "San Francisco, CA", stage: ["Series E"], ceo: "Dylan Field", twitter: "twitter.com/figma", followers: 540000, linkedin: "linkedin.com/company/figma", founded: "2012", arr: 600000000 },
948
+ { id: "notion", company: { name: "Notion" }, domain: "notion.so", categories: ["Productivity", "SaaS"], employees: 800, location: "San Francisco, CA", stage: ["Series C"], ceo: "Ivan Zhao", twitter: "twitter.com/notionhq", followers: 480000, linkedin: "linkedin.com/company/notionhq", founded: "2013", arr: 400000000 },
949
+ { id: "linear", company: { name: "Linear" }, domain: "linear.app", categories: ["Dev tools"], employees: 60, location: "Remote", stage: ["Series B"], ceo: "Karri Saarinen", twitter: "twitter.com/linear", followers: 120000, linkedin: "linkedin.com/company/linear", founded: "2019", arr: 30000000 },
950
+ { id: "vercel", company: { name: "Vercel" }, domain: "vercel.com", categories: ["Dev tools", "Cloud"], employees: 500, location: "San Francisco, CA", stage: ["Series E"], ceo: "Guillermo Rauch", twitter: "twitter.com/vercel", followers: 310000, linkedin: "linkedin.com/company/vercel", founded: "2015", arr: 100000000 },
951
+ { id: "openai", company: { name: "OpenAI" }, domain: "openai.com", categories: ["AI", "Research"], employees: 3500, location: "San Francisco, CA", stage: ["Late stage"], ceo: "Sam Altman", twitter: "twitter.com/openai", followers: 4200000, linkedin: "linkedin.com/company/openai", founded: "2015", arr: 3400000000 },
952
+ { id: "anthropic", company: { name: "Anthropic" }, domain: "anthropic.com", categories: ["AI", "Research"], employees: 1000, location: "San Francisco, CA", stage: ["Series E"], ceo: "Dario Amodei", twitter: "twitter.com/anthropicai", followers: 520000, linkedin: "linkedin.com/company/anthropic", founded: "2021", arr: 1000000000 },
953
+ ];
954
+ // Attach a real favicon per company (derived from the domain); the entity cell
955
+ // falls back to a letter tile if it fails to load.
956
+ export const defaultRows = RAW_ROWS.map((r) => ({
957
+ ...r,
958
+ company: {
959
+ ...r.company,
960
+ avatar: companyLogos[r.domain] ??
961
+ `https://www.google.com/s2/favicons?domain=${r.domain}&sz=64`,
962
+ },
963
+ }));