@crisp-ui-kit/crisp 0.49.0 → 0.51.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 (104) hide show
  1. package/dist/cjs/components/composewindow/composewindow.js +144 -0
  2. package/dist/cjs/components/composewindow/composewindow.recipe.js +45 -0
  3. package/dist/cjs/components/composewindow/index.js +8 -0
  4. package/dist/cjs/components/datatable/datatable.js +224 -43
  5. package/dist/cjs/components/datatable/datatable.recipe.js +38 -4
  6. package/dist/cjs/components/datatable/index.js +6 -0
  7. package/dist/cjs/components/datepickerfield/datepickerfield.js +25 -15
  8. package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +25 -8
  9. package/dist/cjs/components/dialog/dialog.recipe.js +9 -1
  10. package/dist/cjs/components/menu/menu.js +4 -2
  11. package/dist/cjs/components/multiselect/multiselect.recipe.js +14 -8
  12. package/dist/cjs/components/select/select.js +2 -1
  13. package/dist/cjs/components/table/table.recipe.js +12 -3
  14. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +2 -1
  15. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +39 -8
  16. package/dist/cjs/core/cell-date.js +11 -0
  17. package/dist/cjs/core/index.js +2 -1
  18. package/dist/cjs/index.js +1 -0
  19. package/dist/cjs/product/recordpage/recordpage.js +88 -10
  20. package/dist/cjs/product/recordpage/recordpage.recipe.js +30 -0
  21. package/dist/cjs/product/recordpanel/recordpanel.js +54 -4
  22. package/dist/cjs/product/recordpanel/recordpanel.recipe.js +29 -0
  23. package/dist/cjs/product/recordstable/editable-field.js +110 -0
  24. package/dist/cjs/product/recordstable/index.js +4 -1
  25. package/dist/cjs/product/recordstable/recordstable.js +67 -11
  26. package/dist/cjs/product/recordstable/recordstable.recipe.js +9 -1
  27. package/dist/cjs/product/settingsshell/settingsshell.recipe.js +13 -4
  28. package/dist/cjs/product/sidebar/sidebar.js +15 -3
  29. package/dist/cjs/product/sidebar/sidebar.recipe.js +21 -2
  30. package/dist/composewindow.css +24 -0
  31. package/dist/composewindow.d.ts +7 -0
  32. package/dist/composewindow.layered.css +26 -0
  33. package/dist/datatable.css +34 -15
  34. package/dist/datatable.layered.css +34 -15
  35. package/dist/datepickerfield.css +9 -4
  36. package/dist/datepickerfield.layered.css +9 -4
  37. package/dist/dialog.css +2 -1
  38. package/dist/dialog.layered.css +2 -1
  39. package/dist/esm/components/composewindow/composewindow.js +107 -0
  40. package/dist/esm/components/composewindow/composewindow.recipe.js +42 -0
  41. package/dist/esm/components/composewindow/index.js +2 -0
  42. package/dist/esm/components/datatable/datatable.js +226 -45
  43. package/dist/esm/components/datatable/datatable.recipe.js +38 -4
  44. package/dist/esm/components/datatable/index.js +6 -0
  45. package/dist/esm/components/datepickerfield/datepickerfield.js +26 -16
  46. package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +25 -8
  47. package/dist/esm/components/dialog/dialog.recipe.js +9 -1
  48. package/dist/esm/components/menu/menu.js +4 -2
  49. package/dist/esm/components/multiselect/multiselect.recipe.js +14 -8
  50. package/dist/esm/components/select/select.js +2 -1
  51. package/dist/esm/components/table/table.recipe.js +12 -3
  52. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +2 -1
  53. package/dist/esm/components/tabletoolbar/tabletoolbar.js +38 -8
  54. package/dist/esm/core/cell-date.js +10 -0
  55. package/dist/esm/core/index.js +1 -1
  56. package/dist/esm/index.js +1 -0
  57. package/dist/esm/product/recordpage/recordpage.js +89 -11
  58. package/dist/esm/product/recordpage/recordpage.recipe.js +30 -0
  59. package/dist/esm/product/recordpanel/recordpanel.js +55 -5
  60. package/dist/esm/product/recordpanel/recordpanel.recipe.js +29 -0
  61. package/dist/esm/product/recordstable/editable-field.js +73 -0
  62. package/dist/esm/product/recordstable/index.js +1 -0
  63. package/dist/esm/product/recordstable/recordstable.js +69 -13
  64. package/dist/esm/product/recordstable/recordstable.recipe.js +9 -1
  65. package/dist/esm/product/settingsshell/settingsshell.recipe.js +13 -4
  66. package/dist/esm/product/sidebar/sidebar.js +15 -3
  67. package/dist/esm/product/sidebar/sidebar.recipe.js +21 -2
  68. package/dist/multiselect.css +8 -8
  69. package/dist/multiselect.layered.css +8 -8
  70. package/dist/recordpage.css +25 -13
  71. package/dist/recordpage.layered.css +25 -13
  72. package/dist/recordpanel.css +22 -5
  73. package/dist/recordpanel.layered.css +22 -5
  74. package/dist/recordstable.css +10 -2
  75. package/dist/recordstable.layered.css +10 -2
  76. package/dist/settingsshell.css +2 -2
  77. package/dist/settingsshell.layered.css +2 -2
  78. package/dist/sidebar.css +7 -2
  79. package/dist/sidebar.layered.css +7 -2
  80. package/dist/styles.css +150 -55
  81. package/dist/styles.layered.css +150 -55
  82. package/dist/table.css +4 -2
  83. package/dist/table.layered.css +4 -2
  84. package/dist/tabletoolbar.css +2 -1
  85. package/dist/tabletoolbar.layered.css +2 -1
  86. package/dist/types/components/composewindow/composewindow.d.ts +80 -0
  87. package/dist/types/components/composewindow/composewindow.recipe.d.ts +3 -0
  88. package/dist/types/components/composewindow/index.d.ts +2 -0
  89. package/dist/types/components/datatable/datatable.d.ts +58 -4
  90. package/dist/types/components/datatable/index.d.ts +1 -1
  91. package/dist/types/components/datepickerfield/datepickerfield.d.ts +18 -4
  92. package/dist/types/components/menu/menu.d.ts +10 -1
  93. package/dist/types/components/select/select.d.ts +6 -1
  94. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +62 -5
  95. package/dist/types/core/cell-date.d.ts +17 -0
  96. package/dist/types/core/index.d.ts +1 -1
  97. package/dist/types/index.d.ts +1 -0
  98. package/dist/types/product/recordpage/recordpage.d.ts +59 -1
  99. package/dist/types/product/recordpanel/recordpanel.d.ts +11 -1
  100. package/dist/types/product/recordstable/editable-field.d.ts +45 -0
  101. package/dist/types/product/recordstable/index.d.ts +1 -0
  102. package/dist/types/product/recordstable/recordstable.d.ts +29 -0
  103. package/dist/types/product/sidebar/sidebar.d.ts +60 -4
  104. package/package.json +6 -1
@@ -10,7 +10,7 @@ const tokens_1 = require("../../tokens");
10
10
  const config = (0, core_1.defineRecipe)({
11
11
  name: "datatable",
12
12
  base: `display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2.5"]}; font-family: ${tokens_1.contract.font.sans};
13
- & .crisp-datatable-toolbar { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; }
13
+ & .crisp-datatable-toolbar { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; min-height: 49px; border-top: 1px solid ${tokens_1.contract.borderSubtle}; }
14
14
  & .crisp-datatable-search { position: relative; flex: 1; max-width: 280px; display: inline-flex; align-items: center; }
15
15
  & .crisp-datatable-search svg { position: absolute; inset-inline-start: ${tokens_1.contract.space["2"]}; color: ${tokens_1.contract.fgMuted}; pointer-events: none; }
16
16
  & .crisp-datatable-search input { width: 100%; height: 32px; padding: 0 ${tokens_1.contract.space["2"]} 0 ${tokens_1.contract.space["6"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: ${tokens_1.elevation.field}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; outline: none; transition: box-shadow .15s; }
@@ -51,10 +51,16 @@ const config = (0, core_1.defineRecipe)({
51
51
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; }
52
52
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
53
53
  & .crisp-datatable-sort { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; width: 100%; min-width: 0; height: 100%; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; background: none; font: inherit; letter-spacing: inherit; color: inherit; text-align: start; cursor: pointer; }
54
- & th[data-align="right"] .crisp-datatable-sort { justify-content: flex-end; padding-inline-end: 0; }
55
- & th[data-align="right"] .crisp-datatable-th-inner { justify-content: flex-end; }
54
+ /* A right-aligned column's header keeps its field-icon + label packed at
55
+ the START, matching its own values only in that the VALUES right-align
56
+ -- Attio's own right-aligned header (measured live: "Twitter follower
57
+ count") keeps the icon+label left and only the trailing column-menu
58
+ icon sits pinned to the right edge, independent of the label's width
59
+ (#406, reversing #393's own justify-content: flex-end here). The
60
+ chevron's own margin-inline-start: auto below already pins IT to the
61
+ end of a flex-start row with no override needed -- that's what pins
62
+ the trailing icon on Attio too. */
56
63
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; opacity: 0; transition: opacity .12s; }
57
- & th[data-align="right"] .crisp-datatable-chevron { margin-inline-start: 0; order: -1; }
58
64
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
59
65
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: ${tokens_1.contract.fgPrimary}; }
60
66
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
@@ -77,6 +83,34 @@ const config = (0, core_1.defineRecipe)({
77
83
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, ${tokens_1.contract.bgSurface}); cursor: text; }
78
84
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 ${tokens_1.contract.space["1.5"]}; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; outline: none; vertical-align: middle; }
79
85
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; }
86
+ /* An async onEdit's in-flight state (#385) -- the field/picker dims and
87
+ stops accepting input rather than disappearing, so the just-typed or
88
+ just-chosen value stays visible while its commit is pending. Same
89
+ treatment Input already gives its own disabled state. */
90
+ & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
91
+ & .crisp-datatable-edit-pending { flex-shrink: 0; margin-inline-start: ${tokens_1.contract.space["1"]}; color: ${tokens_1.contract.fgMuted}; vertical-align: middle; }
92
+ /* The rejection's message, beside the cell's own content -- a sibling in
93
+ the TableCell, never nested inside a column's own render output, so a
94
+ hover-only wrapper (a Tooltip, say) around a custom render can't hide
95
+ it. Block-level: the row grows to fit it rather than floating over
96
+ the row below, so it can never end up clipped by the scroll
97
+ container's overflow the way a position:fixed escape would need
98
+ guarding against. */
99
+ & .crisp-datatable-edit-error { display: block; padding: ${tokens_1.contract.space["0.5"]} 0 0; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.bgDanger}; }
100
+ /* editor: "select"/"switch" -- always live, no separate edit mode, so
101
+ the cell IS the picker: same chromeless-fill treatment as
102
+ RecordsTable's own --picker cells (measured there already; DataTable
103
+ shares RecordsTable's grid, Golden Rule 0). */
104
+ & .crisp-datatable-td--picker { padding: 0; }
105
+ & .crisp-datatable-edit-picker { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]}; }
106
+ & .crisp-datatable-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; height: 100%; min-height: 0; border-radius: 0; box-shadow: none; padding: 0; }
107
+ & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: ${tokens_1.contract.space["2"]}; }
108
+ /* reference picker: a leading 16px avatar per option, plus a trailing
109
+ "Create X" row when the consumer supplies one (#449) -- same measured
110
+ values as RecordsTable's own .crisp-recordstable-refoption*. */
111
+ & .crisp-datatable-refoption { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
112
+ & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
113
+ & .crisp-datatable-refoption--create { color: ${tokens_1.contract.fgMuted}; }
80
114
  & .crisp-datatable-empty { padding: ${tokens_1.contract.space["8"]} ${tokens_1.contract.space["3"]}; text-align: center; color: ${tokens_1.contract.fgMuted}; font-size: ${tokens_1.contract.text.lg}; }
81
115
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
82
116
  & table { table-layout: fixed; }
@@ -3,5 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.datatableRecipe = exports.DataTable = void 0;
4
4
  var datatable_1 = require("./datatable");
5
5
  Object.defineProperty(exports, "DataTable", { enumerable: true, get: function () { return datatable_1.DataTable; } });
6
+ // `EntityValue` (identical shape, `{ name: string; avatar?: string }`) is
7
+ // deliberately NOT re-exported here: `product/recordstable`'s own barrel
8
+ // already exports a type of that name at the package root, and re-exporting
9
+ // this module's copy too makes `EntityValue` an ambiguous export (TS2308) --
10
+ // verified by trying it. A DataTable consumer needing the type imports it
11
+ // from there; it's the same shape either table would want.
6
12
  var datatable_recipe_1 = require("./datatable.recipe");
7
13
  Object.defineProperty(exports, "datatableRecipe", { enumerable: true, get: function () { return datatable_recipe_1.datatableRecipe; } });
@@ -57,7 +57,7 @@ const fromDate = (d) => ({
57
57
  * popover. This is what `DatePicker` used to be; `DatePicker` is now
58
58
  * seed-design's bare calendar, and this composes it.
59
59
  */
60
- function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder = "Set date…", variant = "default", format = defaultFormat, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
60
+ function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder = "Set date…", variant = "default", format = defaultFormat, clearable = true, clearLabel = "Clear date", presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
61
61
  const [internal, setInternal] = React.useState(defaultValue ?? null);
62
62
  const selected = value ?? internal;
63
63
  const [open, setOpen] = React.useState(false);
@@ -69,19 +69,29 @@ function DatePickerField({ value, defaultValue, onValueChange, onChange, placeho
69
69
  onChange?.(d);
70
70
  setOpen(false);
71
71
  };
72
- return ((0, jsx_runtime_1.jsx)("div", { className: (0, datepickerfield_recipe_1.datepickerfieldRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsx)(popover_1.Popover, { open: open, onOpenChange: setOpen, "aria-label": ariaLabel, trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datepickerfield-trigger", "data-ghost": variant === "ghost" ? "" : undefined, "data-placeholder": selected ? undefined : "", "aria-label": ariaLabel, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CalendarDays, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), selected ? format(selected) : placeholder] }), children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-datepickerfield-panel", children: [(0, jsx_runtime_1.jsx)(datepicker_1.DatePicker
73
- // DatePickerProps is a union over selectionMode, and TypeScript
74
- // cannot narrow it through a spread: it tries every branch and
75
- // reports the range one as missing props. The field is always
76
- // single-select, so the shape is pinned here rather than inferred.
77
- , { ...{
78
- ...calendarProps,
79
- selectionMode: "single",
80
- value: selected ? fromDate(selected) : undefined,
81
- onValueChange: (next) => {
82
- if (next)
83
- pick(toDate(next));
84
- },
85
- } }), presets && presets.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-datepickerfield-presets", children: presets.map((p, i) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datepickerfield-preset", onClick: () => pick(p.date()), children: p.label }, typeof p.label === "string" ? p.label : i))) }))] }) }) }));
72
+ const clear = () => {
73
+ if (value === undefined)
74
+ setInternal(null);
75
+ onValueChange?.(null);
76
+ onChange?.(null);
77
+ };
78
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, datepickerfield_recipe_1.datepickerfieldRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-datepickerfield-box", "data-ghost": variant === "ghost" ? "" : undefined, "data-placeholder": selected ? undefined : "", children: [(0, jsx_runtime_1.jsx)(popover_1.Popover, { open: open, onOpenChange: setOpen, "aria-label": ariaLabel, trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datepickerfield-trigger", "aria-label": ariaLabel, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CalendarDays, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), selected ? format(selected) : placeholder] }), children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-datepickerfield-panel", children: [(0, jsx_runtime_1.jsx)(datepicker_1.DatePicker
79
+ // DatePickerProps is a union over selectionMode, and TypeScript
80
+ // cannot narrow it through a spread: it tries every branch and
81
+ // reports the range one as missing props. The field is always
82
+ // single-select, so the shape is pinned here rather than inferred.
83
+ , { ...{
84
+ ...calendarProps,
85
+ selectionMode: "single",
86
+ value: selected ? fromDate(selected) : undefined,
87
+ onValueChange: (next) => {
88
+ if (next)
89
+ pick(toDate(next));
90
+ },
91
+ } }), presets && presets.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-datepickerfield-presets", children: presets.map((p, i) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datepickerfield-preset", onClick: () => pick(p.date()), children: p.label }, typeof p.label === "string" ? p.label : i))) }))] }) }), clearable && selected && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datepickerfield-clear", "aria-label": clearLabel, onClick: (e) => {
92
+ // not the trigger button's own click -- Popover would open
93
+ e.stopPropagation();
94
+ clear();
95
+ }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 1.75, "aria-hidden": "true" }) }))] }) }));
86
96
  }
87
97
  DatePickerField.displayName = "DatePickerField";
@@ -12,18 +12,35 @@ const config = (0, core_1.defineRecipe)({
12
12
  base: `
13
13
  display: inline-flex; font-family: ${tokens_1.contract.font.sans};
14
14
 
15
- & .crisp-datepickerfield-trigger {
16
- display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["2"]};
17
- box-sizing: border-box; height: 32px; padding: 0 ${tokens_1.contract.space["2.5"]}; margin: 0;
18
- border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent;
15
+ & .crisp-datepickerfield-box {
16
+ display: inline-flex; align-items: center;
17
+ box-sizing: border-box; height: 32px; margin: 0;
18
+ border-radius: ${tokens_1.contract.radius.md}; background: transparent;
19
19
  box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 7%, transparent);
20
20
  color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.lg};
21
- white-space: nowrap; cursor: pointer;
22
21
  }
23
- & .crisp-datepickerfield-trigger[data-ghost] { background: transparent; box-shadow: none; }
24
- & .crisp-datepickerfield-trigger[data-ghost]:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); }
25
- & .crisp-datepickerfield-trigger[data-placeholder] { color: ${tokens_1.contract.fgMuted}; }
22
+ & .crisp-datepickerfield-box[data-ghost] { background: transparent; box-shadow: none; }
23
+ & .crisp-datepickerfield-box[data-ghost]:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); }
24
+ & .crisp-datepickerfield-trigger {
25
+ display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["2"]};
26
+ height: 100%; padding: 0 ${tokens_1.contract.space["2.5"]}; margin: 0;
27
+ border: none; background: none; color: inherit; font-family: inherit;
28
+ font-size: inherit; white-space: nowrap; cursor: pointer;
29
+ }
30
+ & .crisp-datepickerfield-box[data-placeholder] .crisp-datepickerfield-trigger { color: ${tokens_1.contract.fgMuted}; }
26
31
  & .crisp-datepickerfield-trigger svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
32
+ /* Reuses TableToolbar's measured filter-chip remove button
33
+ (tabletoolbar.chrome.ts's .crisp-tabletoolbar-fchip-x) -- the same
34
+ small inline "clear this value" affordance, not a freshly-measured
35
+ one: no live Attio composition for a DatePickerField-equivalent
36
+ clear was reachable this round (#394). */
37
+ & .crisp-datepickerfield-clear {
38
+ display: inline-flex; align-items: center; justify-content: center;
39
+ width: 20px; height: 20px; margin-inline-end: ${tokens_1.contract.space["1"]}; padding: 0;
40
+ border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none;
41
+ color: ${tokens_1.contract.fgMuted}; font-family: inherit; cursor: pointer;
42
+ }
43
+ & .crisp-datepickerfield-clear:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 10%, transparent); color: ${tokens_1.contract.fgPrimary}; }
27
44
 
28
45
  & .crisp-datepickerfield-panel { padding: ${tokens_1.contract.space["2"]}; width: 300px; }
29
46
  & .crisp-datepickerfield-presets { display: flex; flex-wrap: wrap; gap: ${tokens_1.contract.space["1"]}; margin-top: ${tokens_1.contract.space["2"]}; }
@@ -8,6 +8,13 @@ const tokens_1 = require("../../tokens");
8
8
  // per theme — light uses a blue-tinted 3-layer shadow, dark drops to a plain
9
9
  // 2-layer black shadow) with a bordered header (title + close), a scrollable
10
10
  // body, and a bordered footer for the primary action.
11
+ //
12
+ // #414: the footer's submit-key caps (⌘/Enter) used to be one span drawing a
13
+ // visible pill (18px, 22%-white background). Attio's own are two separate
14
+ // TRANSPARENT 16x16 glyphs, 4px apart. Their 3px/10px/4px measured values sit
15
+ // between crisp's token steps (space has no 3px, radius starts at 6px, text
16
+ // starts at 11px) — raw px per Golden Rule 3's "genuinely intrinsic size"
17
+ // exception, same as a chip's odd 7px corner.
11
18
  const config = (0, core_1.defineRecipe)({
12
19
  name: "dialog",
13
20
  base: `display: contents; font-family: ${tokens_1.contract.font.sans};
@@ -26,7 +33,8 @@ const config = (0, core_1.defineRecipe)({
26
33
  & .crisp-dialog-field-label { font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 50%, transparent); }
27
34
  & .crisp-dialog-more { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; margin-inline-end: auto; border: none; background: none; padding: 0; font-family: inherit; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; cursor: pointer; }
28
35
  & .crisp-dialog-submit { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 28px; padding: 0 ${tokens_1.contract.space["1"]} 0 ${tokens_1.contract.space["2.5"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgBrandSolid}; color: ${tokens_1.contract.fgOnBrand}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; cursor: pointer; }
29
- & .crisp-dialog-submit-key { display: inline-flex; align-items: center; justify-content: center; height: 18px; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; background: rgba(255,255,255,0.22); font-size: ${tokens_1.contract.text.xs}; }`,
36
+ & .crisp-dialog-submit-key { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; }
37
+ & .crisp-dialog-submit-key kbd { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0 3px; border-radius: 4px; background: transparent; color: ${tokens_1.contract.fgOnBrand}; font-family: inherit; font-style: normal; font-size: 10px; }`,
30
38
  variants: {},
31
39
  defaultVariants: {},
32
40
  compoundVariants: [],
@@ -52,7 +52,7 @@ const MenuContext = React.createContext(null);
52
52
  * typeahead, Enter/Space select, Esc closes and returns focus to the trigger);
53
53
  * closes on outside click. Flips up / right-aligns when space is tight.
54
54
  */
55
- function Menu({ trigger, children, align = "start", open: openProp, defaultOpen, onOpenChange, className, "aria-label": ariaLabel, }) {
55
+ function Menu({ trigger, children, align = "start", open: openProp, defaultOpen, onOpenChange, className, contentClassName, "aria-label": ariaLabel, }) {
56
56
  // The surface and the button that opens it were two unconnected nodes: the
57
57
  // menu/dialog had no id, so nothing could point at it, and no accessible
58
58
  // name, so a screen reader announced a bare "menu"/"dialog". Labelling the
@@ -163,7 +163,9 @@ function Menu({ trigger, children, align = "start", open: openProp, defaultOpen,
163
163
  setOpen(!open);
164
164
  },
165
165
  });
166
- return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, menu_recipe_1.menuRecipe)({ className }), children: [triggerEl, open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsx)("div", { ref: contentRef, className: "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: (0, jsx_runtime_1.jsx)(MenuContext.Provider, { value: { close }, children: children }) }) }))] }));
166
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, menu_recipe_1.menuRecipe)({ className }), children: [triggerEl, open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsx)("div", { ref: contentRef, className: contentClassName
167
+ ? `crisp-menu-content ${contentClassName}`
168
+ : "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: (0, jsx_runtime_1.jsx)(MenuContext.Provider, { value: { close }, children: children }) }) }))] }));
167
169
  }
168
170
  Menu.displayName = "Menu";
169
171
  function MenuItem({ children, icon, shortcut, danger, disabled, onSelect, closeOnSelect = true, }) {
@@ -22,6 +22,12 @@ const tokens_1 = require("../../tokens");
22
22
  // intact WITHIN the portaled subtree, only the link back to the
23
23
  // trigger-holding root broke). A flat recordstable-tag selector also works
24
24
  // unchanged wherever it's rendered, portaled or not, so it needs no split.
25
+ // This is a third, hand-pasted copy of `recordTagCss`
26
+ // (product/recordstable/recordstable.tag.ts) rather than a splice of it:
27
+ // that helper is written `& .crisp-recordstable-tag { ... }` for nesting
28
+ // under a recipe's `base`, which doesn't compile to anything inside a flat
29
+ // `globalCss` block. Keep the two in sync by hand (#380 added the measured
30
+ // `box-shadow: inset 0 0 0 1px <tone ring colour>` here to match).
25
31
  const config = (0, core_1.defineRecipe)({
26
32
  name: "multiselect",
27
33
  base: `
@@ -77,14 +83,14 @@ const config = (0, core_1.defineRecipe)({
77
83
  .crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: ${tokens_1.contract.bgBrandSolid}; box-shadow: none; color: #fff; }
78
84
  .crisp-multiselect-empty { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]}; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
79
85
 
80
- .crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.chip}; font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 82%, transparent); }
81
- .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 14%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 82%, ${tokens_1.contract.fgPrimary}); }
82
- .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, ${tokens_1.contract.bgSuccess} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgSuccess} 78%, ${tokens_1.contract.fgPrimary}); }
83
- .crisp-recordstable-tag[data-tone="warning"] { background: color-mix(in srgb, ${tokens_1.contract.bgWarning} 18%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgWarning} 82%, ${tokens_1.contract.fgPrimary}); }
84
- .crisp-recordstable-tag[data-tone="danger"] { background: color-mix(in srgb, ${tokens_1.contract.bgDanger} 15%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgDanger} 80%, ${tokens_1.contract.fgPrimary}); }
85
- .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, ${tokens_1.contract.bgPurple} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgPurple} 80%, ${tokens_1.contract.fgPrimary}); }
86
- .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, ${tokens_1.contract.bgCyan} 18%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgCyan} 82%, ${tokens_1.contract.fgPrimary}); }
87
- .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${tokens_1.contract.bgOrange} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgOrange} 82%, ${tokens_1.contract.fgPrimary}); }
86
+ .crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.chip}; font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 82%, transparent); box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; }
87
+ .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 14%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 82%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 30%, transparent); }
88
+ .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, ${tokens_1.contract.bgSuccess} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgSuccess} 78%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgSuccess} 30%, transparent); }
89
+ .crisp-recordstable-tag[data-tone="warning"] { background: color-mix(in srgb, ${tokens_1.contract.bgWarning} 18%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgWarning} 82%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgWarning} 30%, transparent); }
90
+ .crisp-recordstable-tag[data-tone="danger"] { background: color-mix(in srgb, ${tokens_1.contract.bgDanger} 15%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgDanger} 80%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgDanger} 30%, transparent); }
91
+ .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, ${tokens_1.contract.bgPurple} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgPurple} 80%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgPurple} 30%, transparent); }
92
+ .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, ${tokens_1.contract.bgCyan} 18%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgCyan} 82%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgCyan} 30%, transparent); }
93
+ .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${tokens_1.contract.bgOrange} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgOrange} 82%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgOrange} 30%, transparent); }
88
94
  `,
89
95
  variants: {
90
96
  // Same ring Select's `invalid` draws — see that recipe's comment.
@@ -52,7 +52,7 @@ function optionText(o) {
52
52
  * "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
53
53
  * Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
54
54
  */
55
- function Select({ options, value, defaultValue, onValueChange, name, required, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
55
+ function Select({ options, value, defaultValue, onValueChange, name, required, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", onSearchChange, emptyLabel = "No results", disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
56
56
  // Virtual focus: DOM focus stays on the trigger (or the search field), and the
57
57
  // active option is named by `aria-activedescendant`. That is the only thing a
58
58
  // screen reader has to go on -- `data-highlighted` is invisible to it -- so the
@@ -201,6 +201,7 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
201
201
  onChange: () => { } })), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
202
202
  setQuery(e.target.value);
203
203
  setHighlight(0);
204
+ onSearchChange?.(e.target.value);
204
205
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
205
206
  const isSel = opt.value === selected;
206
207
  // #343: a header renders once per run of same-`group`
@@ -14,11 +14,11 @@ const config = (0, core_1.defineRecipe)({
14
14
  & .crisp-table-scroll { overflow-x: auto; }
15
15
  & table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: ${tokens_1.contract.font.sans}; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; }
16
16
  & th, & td { position: relative; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
17
- & th { height: ${tokens_1.contract.size.tableHeader}; background: ${tokens_1.contract.bgSurface}; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; letter-spacing: -0.14px; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); user-select: none; }
17
+ & th { height: ${tokens_1.contract.size.tableHeader}; background: ${tokens_1.contract.bgSurface}; border-top: 1px solid ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; letter-spacing: -0.14px; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); user-select: none; }
18
18
  & td { color: ${tokens_1.contract.fgPrimary}; background: ${tokens_1.contract.bgSurface}; }
19
19
  & th:last-child, & td:last-child { border-inline-end: none; }
20
20
  & tbody tr:last-child td { border-bottom: none; }
21
- & tfoot td { border-top: 1px solid ${tokens_1.contract.borderSubtle}; border-bottom: none; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
21
+ & tfoot td { border-top: 1px solid ${tokens_1.contract.borderSubtle}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
22
22
  & caption { padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["2"]}; text-align: start; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; caption-side: bottom; }
23
23
  & td[data-align="right"], & th[data-align="right"] { text-align: end; }
24
24
  & td[data-align="center"], & th[data-align="center"] { text-align: center; }`,
@@ -31,8 +31,17 @@ const config = (0, core_1.defineRecipe)({
31
31
  // card (default): the bordered rounded surface, unchanged for existing
32
32
  // consumers. bleed: drops the outer border/radius/inset so the grid runs
33
33
  // edge-to-edge, like RecordsTable's real full-page records grid.
34
+ //
35
+ // #439: th/tfoot td now carry their own top/bottom border (below) so a
36
+ // bleed table's header and calc footer close the same way Attio's own
37
+ // do. card's own 4-side border already draws that top/bottom edge, so
38
+ // strip the cells' copies there -- same reasoning as `:last-child`
39
+ // dropping the inline-end border and `tbody tr:last-child` dropping the
40
+ // bottom one: the container's edge wins, cells never double it.
34
41
  surface: {
35
- card: `border: 1px solid ${tokens_1.contract.borderSubtle}; border-radius: ${tokens_1.contract.radius.xl}; overflow: hidden;`,
42
+ card: `border: 1px solid ${tokens_1.contract.borderSubtle}; border-radius: ${tokens_1.contract.radius.xl}; overflow: hidden;
43
+ & th { border-top: none; }
44
+ & tfoot td { border-bottom: none; }`,
36
45
  bleed: `border: none; border-radius: 0;`,
37
46
  },
38
47
  },
@@ -18,11 +18,12 @@ const tokens_1 = require("../../tokens");
18
18
  // split preserves that cascade exactly, which is what lets the generated CSS
19
19
  // stay byte-for-byte identical to what shipped before the extraction.
20
20
  const toolbarChromeHead = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-toolbar { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; padding: 0 ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]}; }
21
+ & .${namespace_1.CLASS_PREFIX}${p}-toolbar[data-inline] { padding-bottom: 0; }
21
22
  & .${namespace_1.CLASS_PREFIX}${p}-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: ${tokens_1.contract.size.row}; padding: 0 ${tokens_1.contract.space["2.5"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; color: ${tokens_1.contract.fgMuted}; }
22
23
  & .${namespace_1.CLASS_PREFIX}${p}-search svg { flex-shrink: 0; }
23
24
  & .${namespace_1.CLASS_PREFIX}${p}-search input { border: none; outline: none; background: transparent; font-family: inherit; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgPrimary}; width: 180px; }
24
25
  & .${namespace_1.CLASS_PREFIX}${p}-search input::placeholder { color: ${tokens_1.contract.fgMuted}; }
25
- & .${namespace_1.CLASS_PREFIX}${p}-tool { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.row}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDefault}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; cursor: pointer; transition: background-color .12s; }
26
+ & .${namespace_1.CLASS_PREFIX}${p}-tool { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.row}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDefault}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
26
27
  & .${namespace_1.CLASS_PREFIX}${p}-tool:hover { background: ${tokens_1.contract.bgRaised}; }
27
28
  & .${namespace_1.CLASS_PREFIX}${p}-tool svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
28
29
  & .${namespace_1.CLASS_PREFIX}${p}-tool--active { background: ${tokens_1.contract.bgRaised}; }
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ToolbarTool = void 0;
36
37
  exports.applyFilters = applyFilters;
37
38
  exports.applySorts = applySorts;
38
39
  exports.TableToolbar = TableToolbar;
@@ -148,8 +149,19 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
148
149
  * `RecordsTable` renders this same measured chrome internally; this is the
149
150
  * composable version, to put above `DataTable` or any table of your own. Pair
150
151
  * it with `applyFilters` / `applySorts` to turn its state into rows.
152
+ *
153
+ * Sort and Filter read the same `columns` list by default, but can be given
154
+ * their own (`sortColumns`/`filterColumns`) when a consumer's two menus
155
+ * genuinely offer different fields — Attio's own Tasks page does exactly
156
+ * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
157
+ * Created by), which one shared list can't reproduce.
151
158
  */
152
- function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel = "Sort", reorderSortLabel = "Reorder sort", noSortsLabel = "No sorts applied to this view.", addSortLabel = "Add sort", learnSortingLabel = "Learn about sorting", selectConditionLabel = "Select condition", sortFieldLabel = "Sort field", sortDirectionLabel = "Sort direction", removeSortLabel = "Remove sort", filterFieldLabel = "Filter by field", filterFieldPlaceholder = "Filter by…", filterValuePlaceholder = "Value…", ascendingLabel = "Ascending", descendingLabel = "Descending", sortTriggerLabel, filterTriggerLabel, ...rest }) {
159
+ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel = "Sort", reorderSortLabel = "Reorder sort", noSortsLabel = "No sorts applied to this view.", addSortLabel = "Add sort", learnSortingLabel = "Learn about sorting", selectConditionLabel = "Select condition", sortFieldLabel = "Sort field", sortDirectionLabel = "Sort direction", removeSortLabel = "Remove sort", filterFieldLabel = "Filter by field", filterFieldPlaceholder = "Filter by…", filterValuePlaceholder = "Value…", ascendingLabel = "Ascending", descendingLabel = "Descending", sortTriggerLabel, filterTriggerLabel, inline, ...rest }) {
160
+ // Each side falls back to the shared `columns` independently — a caller
161
+ // that only overrides one (Filter's own Record/Created by, say) still
162
+ // gets `columns` on the other, unchanged from before either prop existed.
163
+ const sortColumns = sortColumnsProp ?? columns ?? [];
164
+ const filterColumns = filterColumnsProp ?? columns ?? [];
153
165
  const [sortsState, setSortsState] = React.useState(defaultSorts ?? []);
154
166
  const [filtersState, setFiltersState] = React.useState(defaultFilters ?? []);
155
167
  const sorts = sortsProp ?? sortsState;
@@ -196,7 +208,7 @@ function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange,
196
208
  }, [openMenu]);
197
209
  const addSort = () => {
198
210
  const used = new Set(sorts.map((s) => s.key));
199
- const col = columns.find((c) => !used.has(c.key));
211
+ const col = sortColumns.find((c) => !used.has(c.key));
200
212
  if (col)
201
213
  putSorts([...sorts, { key: col.key, dir: "asc" }]);
202
214
  };
@@ -235,18 +247,18 @@ function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange,
235
247
  // The default phrase only works in English word order, so a caller can
236
248
  // replace the whole thing rather than fill a template.
237
249
  const sortedLabel = sortTriggerLabel
238
- ? sortTriggerLabel(sorts, columns)
250
+ ? sortTriggerLabel(sorts, sortColumns)
239
251
  : sorts.length
240
- ? `Sorted by ${columns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
252
+ ? `Sorted by ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
241
253
  : "Sort";
242
- return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
254
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
243
255
  if (dragSort !== null)
244
256
  e.preventDefault();
245
257
  }, onDrop: () => {
246
258
  if (dragSort !== null)
247
259
  moveSort(dragSort, i);
248
260
  setDragSort(null);
249
- }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)(select_1.Select, { className: "crisp-tabletoolbar-sort-field", searchable: true, value: s.key, onValueChange: (k) => setSortField(i, k), options: columns.map((c) => ({
261
+ }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)(select_1.Select, { className: "crisp-tabletoolbar-sort-field", searchable: true, value: s.key, onValueChange: (k) => setSortField(i, k), options: sortColumns.map((c) => ({
250
262
  value: c.key,
251
263
  label: c.icon ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(c.icon, { size: 15, strokeWidth: 1.75 }), c.label] })) : (c.label),
252
264
  keywords: c.label,
@@ -267,12 +279,12 @@ function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange,
267
279
  ? `Filter · ${activeFilters}`
268
280
  : "Filter"] }), openMenu === "filter" && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
269
281
  // biome-ignore lint/a11y/noAutofocus: focus the picker on open
270
- autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), columns
282
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
271
283
  .filter((col) => col.label
272
284
  .toLowerCase()
273
285
  .includes(filterQuery.trim().toLowerCase()))
274
286
  .map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => addFilter(col.key), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key)))] }))] }), filters.map((cond) => {
275
- const col = columns.find((c) => c.key === cond.key);
287
+ const col = filterColumns.find((c) => c.key === cond.key);
276
288
  const open = openMenu === `fcond:${cond.id}`;
277
289
  const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
278
290
  (!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
@@ -301,3 +313,22 @@ function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange,
301
313
  })] }))] }))] }, cond.id));
302
314
  }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
303
315
  }
316
+ /**
317
+ * The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
318
+ * (#399) — for a control drawn like Sort/Filter that isn't Sort or Filter (a
319
+ * screen's own quick filter, e.g.). `.crisp-tabletoolbar-tool` alone renders
320
+ * unstyled: the rule lives nested under `TableToolbar`'s own root class, the
321
+ * same way every other rule in its shared chrome does, so it only paints on
322
+ * a descendant of an actual `TableToolbar`. This wraps one in, `display:
323
+ * contents` so it contributes no box of its own — the same technique
324
+ * `TableToolbar`'s own root already uses to disappear from layout.
325
+ */
326
+ exports.ToolbarTool = React.forwardRef(({ active, ghost, className, ...rest }, ref) => ((0, jsx_runtime_1.jsx)("span", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)(), children: (0, jsx_runtime_1.jsx)("button", { ref: ref, type: "button", className: [
327
+ "crisp-tabletoolbar-tool",
328
+ active && "crisp-tabletoolbar-tool--active",
329
+ ghost && "crisp-tabletoolbar-tool--ghost",
330
+ className,
331
+ ]
332
+ .filter(Boolean)
333
+ .join(" "), ...rest }) })));
334
+ exports.ToolbarTool.displayName = "ToolbarTool";
@@ -16,6 +16,7 @@
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.parseCellDate = parseCellDate;
18
18
  exports.formatCellDate = formatCellDate;
19
+ exports.cellDatePrecision = cellDatePrecision;
19
20
  /** `YYYY`, `YYYY-MM`, `YYYY-MM-DD` — the forms ECMA-262 reads as UTC. */
20
21
  const DATE_ONLY = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/;
21
22
  function parseCellDate(raw) {
@@ -35,3 +36,13 @@ function formatCellDate(d) {
35
36
  const pad = (n) => String(n).padStart(2, "0");
36
37
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
37
38
  }
39
+ function cellDatePrecision(raw) {
40
+ const m = DATE_ONLY.exec(raw);
41
+ if (!m)
42
+ return "day";
43
+ if (!m[2])
44
+ return "year";
45
+ if (!m[3])
46
+ return "month";
47
+ return "day";
48
+ }
@@ -14,8 +14,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.usePrefersReducedMotion = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.formValueStyle = exports.registerFloatingLayer = exports.isInsideLaterFloatingLayer = exports.hasOpenFloatingLayer = exports.FloatingPortal = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = void 0;
17
+ exports.usePrefersReducedMotion = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.formValueStyle = exports.registerFloatingLayer = exports.isInsideLaterFloatingLayer = exports.hasOpenFloatingLayer = exports.FloatingPortal = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = exports.cellDatePrecision = void 0;
18
18
  var cell_date_1 = require("./cell-date");
19
+ Object.defineProperty(exports, "cellDatePrecision", { enumerable: true, get: function () { return cell_date_1.cellDatePrecision; } });
19
20
  Object.defineProperty(exports, "formatCellDate", { enumerable: true, get: function () { return cell_date_1.formatCellDate; } });
20
21
  Object.defineProperty(exports, "parseCellDate", { enumerable: true, get: function () { return cell_date_1.parseCellDate; } });
21
22
  var cx_1 = require("./cx");
package/dist/cjs/index.js CHANGED
@@ -31,6 +31,7 @@ __exportStar(require("./components/card"), exports);
31
31
  __exportStar(require("./components/checkbox"), exports);
32
32
  __exportStar(require("./components/chip"), exports);
33
33
  __exportStar(require("./components/columncalc"), exports);
34
+ __exportStar(require("./components/composewindow"), exports);
34
35
  __exportStar(require("./components/contextmenu"), exports);
35
36
  __exportStar(require("./components/datatable"), exports);
36
37
  __exportStar(require("./components/datepicker"), exports);