@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
@@ -0,0 +1,80 @@
1
+ import * as React from "react";
2
+ export type ComposeWindowProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"> & {
3
+ /** Controlled open state — `ComposeWindow` never manages this itself, the
4
+ * same controlled-only shape as `Dialog`/`Drawer`/`Command`. This is
5
+ * deliberate: crisp has no router dependency, so making the window fully
6
+ * controlled (this, and `minimized`/`pinned` below) is what lets a
7
+ * consumer sync it to a URL itself — the way Attio's own note window is
8
+ * addressable via `?id=…&modal=note` — without crisp owning any routing. */
9
+ open: boolean;
10
+ onOpenChange: (open: boolean) => void;
11
+ /** The header's leading area — typically a link to whatever record this
12
+ * window is attached to. Rendered as-is; crisp does not style its
13
+ * contents beyond spacing, since the exact breadcrumb chip inside it
14
+ * wasn't independently re-measured (see the component's own doc comment). */
15
+ breadcrumb?: React.ReactNode;
16
+ /** Minimized state. Omit for uncontrolled (see `defaultMinimized`). */
17
+ minimized?: boolean;
18
+ defaultMinimized?: boolean;
19
+ onMinimizedChange?: (minimized: boolean) => void;
20
+ /** Pinned state. While pinned, clicking outside the window does not close
21
+ * it. Omit for uncontrolled (see `defaultPinned`). */
22
+ pinned?: boolean;
23
+ defaultPinned?: boolean;
24
+ onPinnedChange?: (pinned: boolean) => void;
25
+ /** Content width in px (clamped to 90vw), matching `Dialog`'s own knob.
26
+ * Default 968 — Attio's own measured note window width. */
27
+ width?: number;
28
+ "aria-label"?: string;
29
+ minimizeLabel?: string;
30
+ pinLabel?: string;
31
+ closeLabel?: string;
32
+ children?: React.ReactNode;
33
+ };
34
+ /**
35
+ * A floating, minimizable, pinnable window — Attio's own note "compose
36
+ * window" chrome (measured live, read-only, 2026-09-24; see HANDOFF), not a
37
+ * new visual recipe: it reuses `Dialog`'s exact measured frosted-glass panel
38
+ * (`bgOverlayFrost` 60% + `blur(15px)`, `elevation.modal`'s 3-layer shadow,
39
+ * 16px outer radius) — those numbers matched Attio's compose window exactly
40
+ * when re-checked against it, panel-for-panel. What's actually different
41
+ * from `Dialog` is the header (a breadcrumb plus minimize/pin/close, not a
42
+ * title plus close) and two extra pieces of state Dialog has no concept of.
43
+ *
44
+ * `minimized` is measured, not guessed — the FIRST version of this file built
45
+ * an invented "minimized bar" (a Gmail-style dock, since a bar was never
46
+ * actually exercised against Attio). Re-measured properly: minimizing an
47
+ * existing note does not produce any bar, tray, dock, or badge anywhere on
48
+ * screen. Confirmed four independent ways, both with synthetic and real
49
+ * CDP-dispatched trusted clicks — the element count under the note's own
50
+ * subtree drops by ~110 nodes, `[role="dialog"]` stops matching it, its
51
+ * `data-hidden="true"` ancestor computes `visibility: hidden; opacity: 0`,
52
+ * and the URL drops the `?id=…&modal=note` query exactly like Close does —
53
+ * but the subtree is NOT removed: `document.querySelectorAll('[aria-label=
54
+ * "Minimize"]')` still finds it afterward, `visibility: hidden` (not
55
+ * `display: none`) preserves layout, and nothing in localStorage or the DOM
56
+ * suggests state was discarded. So: minimized hides the panel — mounted,
57
+ * `visibility: hidden; opacity: 0` — rather than unmounting or closing it,
58
+ * and crisp renders no restore affordance of its own (Attio has none
59
+ * either); reopening is the consumer's own UI, driven by `minimized`/
60
+ * `onMinimizedChange` the same way `open` already works.
61
+ *
62
+ * "Pinned" is implemented as the one directly-supported behavior that
63
+ * matches its name (Zag's `closeOnInteractOutside`, off while pinned) —
64
+ * Attio's own multi-instance stacking/docking when pinned was not exercised,
65
+ * so nothing here repositions the window when pinned.
66
+ *
67
+ * Not modal: Attio's own compose window dims nothing behind it (`Dialog`'s
68
+ * backdrop is `background: transparent` too — the blur alone does the
69
+ * separation), and the page behind stays visibly interactive in the
70
+ * measured screenshot. `trapFocus` follows the same call — a picture-in-
71
+ * picture window, not a modal, so Tab is not caught inside it.
72
+ */
73
+ export declare function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width, "aria-label": ariaLabel, minimizeLabel, pinLabel, closeLabel, children, className, style, "aria-describedby": describedBy, ...rest }: ComposeWindowProps): React.JSX.Element | null;
74
+ export declare namespace ComposeWindow {
75
+ var displayName: string;
76
+ }
77
+ export declare function ComposeWindowBody({ children, className, }: {
78
+ children?: React.ReactNode;
79
+ className?: string;
80
+ }): React.JSX.Element;
@@ -0,0 +1,3 @@
1
+ declare const config: import("../..").RecipeConfig<{}>;
2
+ export declare const composewindowRecipe: import("../..").RecipeFn<{}>;
3
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { ComposeWindow, ComposeWindowBody, type ComposeWindowProps, } from "./composewindow";
2
+ export { composewindowRecipe } from "./composewindow.recipe";
@@ -7,7 +7,18 @@ export type EntityValue = {
7
7
  name: string;
8
8
  avatar?: string;
9
9
  };
10
- export type DataCellType = "text" | "entity" | "tags" | "status" | "date" | "number" | "currency";
10
+ /** An `editor: "select"` column's choices. Same shape as RecordsTable's
11
+ * `StatusOption`. */
12
+ export type DataEditableOption = {
13
+ value: string;
14
+ label: string;
15
+ color?: string;
16
+ /** `editor: "reference"` only (#449) — a photo URL for the option's
17
+ * leading avatar; a generic icon renders when omitted. Unused by
18
+ * `editor: "select"`. */
19
+ avatar?: string;
20
+ };
21
+ export type DataCellType = "text" | "entity" | "tags" | "status" | "date" | "datetime" | "number" | "currency";
11
22
  export type DataTableColumn<T> = {
12
23
  key: string;
13
24
  /** Truncates with an ellipsis when it's wider than the
@@ -29,8 +40,44 @@ export type DataTableColumn<T> = {
29
40
  sortable?: boolean;
30
41
  /** Exclude this column from the search filter (default: included). */
31
42
  filterable?: boolean;
32
- /** Allow inline editing (click the cell → text input → Enter/blur commits). */
43
+ /** Allow inline editing. A plain `editable` column opens a text/number/
44
+ * date/datetime field on double-click (Enter/blur/choosing a value
45
+ * commits) — see `editor` for `select`/`switch`/`reference`. */
33
46
  editable?: boolean;
47
+ /** The inline editor an `editable` column opens. Omitted: a plain field
48
+ * inferred from `type` (number/date/datetime/text), gated behind the
49
+ * double-click `editable` opens today. `"select"`/`"switch"`/`"reference"`
50
+ * instead render crisp's own `Select`/`Switch` as the cell's live content,
51
+ * ALWAYS interactive — no double-click gate, no separate edit/view mode —
52
+ * the same convention RecordsTable's `status`/`reference` pickers already
53
+ * use, since choosing an option or flipping a toggle IS the commit
54
+ * gesture; there is nothing for a second click to open. `select` reads
55
+ * `options`; `reference` reads `refOptions` and is search-enabled, the
56
+ * same searchable-`Select` shape as `RecordsTable`'s own
57
+ * `ReferenceCell`. */
58
+ editor?: "select" | "switch" | "reference";
59
+ /** Options for `editor: "select"` (value/label, an optional `color` a
60
+ * caller's own `render` can read) — same shape as RecordsTable's
61
+ * `Column.options` for its `status` picker. */
62
+ options?: DataEditableOption[];
63
+ /** Options for `editor: "reference"` — same shape as `options`, plus
64
+ * `avatar` (#449): a 16px avatar renders before each option's label, a
65
+ * photo when given, a generic fallback icon otherwise — matching
66
+ * RecordsTable's own `ReferenceCell`, which this picker mirrors. */
67
+ refOptions?: DataEditableOption[];
68
+ /** Placeholder for `editor: "reference"`'s search input. Defaults to
69
+ * `Select`'s own generic "Search…" — a generic primitive doesn't hardcode
70
+ * Attio's own copy ("Search records..."), which is workspace/object
71
+ * text, not a crisp-measured value. */
72
+ refSearchPlaceholder?: string;
73
+ /** Adds a trailing "Create {createLabel}" row to `editor: "reference"`'s
74
+ * picker (Attio's own always has one, #449) — rendered only when this is
75
+ * provided. Picking it hands back the picker's current search text, e.g.
76
+ * to open a create flow pre-seeded with it. */
77
+ onCreateReference?: (query: string) => void;
78
+ /** The object noun in "Create {createLabel}" (Attio: "Create Person").
79
+ * Only used with `onCreateReference`; defaults to "record". */
80
+ createLabel?: string;
34
81
  align?: "left" | "right" | "center";
35
82
  width?: number;
36
83
  /** Custom cell renderer. */
@@ -125,8 +172,15 @@ export type DataTableProps<T extends {
125
172
  * of `searchPlaceholder`, which is visible text — this is the
126
173
  * screen-reader-only label. */
127
174
  searchLabel?: string;
128
- /** Commit handler for inline edits. */
129
- onEdit?: (id: string, key: string, value: string) => void;
175
+ /** Commit handler for inline edits. Returning (or resolving) nothing
176
+ * commits synchronously, same as before -- `Promise<void>` is additive.
177
+ * Returning a `Promise` that rejects shows a pending state on the cell
178
+ * while it's in flight, then reverts the cell to its last-known-good value
179
+ * (the cell only ever shows `data`, which this never mutates itself) and
180
+ * renders the rejection's message in an error slot beside the cell,
181
+ * outside `render`'s own output so a hover-only wrapper (a `Tooltip`, say)
182
+ * can't swallow it. */
183
+ onEdit?: (id: string, key: string, value: string) => Promise<void> | void;
130
184
  /** Sticky the first column(s) with a horizontal scroll shadow (default true). */
131
185
  stickyFirst?: boolean;
132
186
  /** Sticky the LAST column with a horizontal scroll shadow, mirroring
@@ -1,2 +1,2 @@
1
- export { DataTable, type DataTableColumn, type DataTableProps, type SortState, } from "./datatable";
1
+ export { type DataCellType, type DataEditableOption, DataTable, type DataTableColumn, type DataTableProps, type SortState, } from "./datatable";
2
2
  export { datatableRecipe } from "./datatable.recipe";
@@ -11,20 +11,34 @@ export type DatePickerFieldProps = Omit<React.ComponentPropsWithoutRef<"div">, "
11
11
  /** Controlled value. Omit to let the field manage it (see defaultValue). */
12
12
  value?: Date | null;
13
13
  defaultValue?: Date | null;
14
- /** Fires with the picked date, controlled or not. */
15
- onValueChange?: (date: Date) => void;
14
+ /**
15
+ * Fires with the picked date, controlled or not — or `null`, when the
16
+ * field's own Clear button is pressed (#394). A field whose value is
17
+ * meaningfully optional (a range's From/To, e.g. — blank legitimately
18
+ * means "unconstrained") needs a way back to that blank state without
19
+ * leaving the field; there was none before this.
20
+ */
21
+ onValueChange?: (date: Date | null) => void;
16
22
  /**
17
23
  * The original name for `onValueChange`, kept working so no consumer breaks.
18
24
  * Prefer `onValueChange`: it is what every other crisp component calls this,
19
25
  * and passing the house name to a component that only knew `onChange` was
20
26
  * not a type error — the handler simply never fired (#202).
21
27
  */
22
- onChange?: (date: Date) => void;
28
+ onChange?: (date: Date | null) => void;
23
29
  placeholder?: string;
24
30
  /** `default` = outlined trigger; `ghost` = transparent (for table cells). */
25
31
  variant?: "default" | "ghost";
26
32
  /** Formats the trigger label (default: e.g. "Jun 3, 2026"). */
27
33
  format?: (date: Date) => string;
34
+ /**
35
+ * Shows a Clear (×) button next to the trigger once a value is set
36
+ * (default `true`). Set `false` for a field whose value is never
37
+ * meaningfully optional.
38
+ */
39
+ clearable?: boolean;
40
+ /** aria-label on the Clear button (default "Clear date"). */
41
+ clearLabel?: string;
28
42
  /** Quick-pick chips under the calendar. */
29
43
  presets?: DatePreset[];
30
44
  /** Forwarded to the calendar — constraints, locale, ariaLabels and the rest. */
@@ -37,7 +51,7 @@ export type DatePickerFieldProps = Omit<React.ComponentPropsWithoutRef<"div">, "
37
51
  * popover. This is what `DatePicker` used to be; `DatePicker` is now
38
52
  * seed-design's bare calendar, and this composes it.
39
53
  */
40
- export declare function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder, variant, format, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }: DatePickerFieldProps): React.JSX.Element;
54
+ export declare function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder, variant, format, clearable, clearLabel, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }: DatePickerFieldProps): React.JSX.Element;
41
55
  export declare namespace DatePickerField {
42
56
  var displayName: string;
43
57
  }
@@ -13,6 +13,15 @@ export type MenuProps = {
13
13
  /** Fires with the next open state, controlled or not. */
14
14
  onOpenChange?: (open: boolean) => void;
15
15
  className?: string;
16
+ /**
17
+ * Extra class(es) for the popover surface itself (`.crisp-menu-content`).
18
+ * `className` alone can't reach it: the surface is portaled to
19
+ * `document.body` (#339) so it's no longer a DOM descendant of the root
20
+ * `className` lands on, and a descendant selector scoped to the root
21
+ * silently matches nothing there — found wiring the workspace switcher's
22
+ * wider, Attio-measured popover (#415).
23
+ */
24
+ contentClassName?: string;
16
25
  "aria-label"?: string;
17
26
  };
18
27
  /**
@@ -22,7 +31,7 @@ export type MenuProps = {
22
31
  * typeahead, Enter/Space select, Esc closes and returns focus to the trigger);
23
32
  * closes on outside click. Flips up / right-aligns when space is tight.
24
33
  */
25
- export declare function Menu({ trigger, children, align, open: openProp, defaultOpen, onOpenChange, className, "aria-label": ariaLabel, }: MenuProps): React.JSX.Element;
34
+ export declare function Menu({ trigger, children, align, open: openProp, defaultOpen, onOpenChange, className, contentClassName, "aria-label": ariaLabel, }: MenuProps): React.JSX.Element;
26
35
  export declare namespace Menu {
27
36
  var displayName: string;
28
37
  }
@@ -47,6 +47,11 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
47
47
  /** Show a search field at the top of the list that filters options (Linear/cmdk style). */
48
48
  searchable?: boolean;
49
49
  searchPlaceholder?: string;
50
+ /** Fires on every keystroke in the search field. `Select` filters `options`
51
+ * itself, so this exists for a consumer who needs the raw text too — a
52
+ * trailing "create" row seeded with it, a server-side search, and so on.
53
+ * Only meaningful with `searchable`. */
54
+ onSearchChange?: (query: string) => void;
50
55
  /** Shown when the search matches nothing. */
51
56
  emptyLabel?: string;
52
57
  disabled?: boolean;
@@ -64,7 +69,7 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
64
69
  * "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
65
70
  * Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
66
71
  */
67
- export declare function Select({ options, value, defaultValue, onValueChange, name, required, placeholder, variant, width, searchable, searchPlaceholder, emptyLabel, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
72
+ export declare function Select({ options, value, defaultValue, onValueChange, name, required, placeholder, variant, width, searchable, searchPlaceholder, onSearchChange, emptyLabel, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
68
73
  export declare namespace Select {
69
74
  var displayName: string;
70
75
  }
@@ -31,8 +31,25 @@ export type TableToolbarColumn = {
31
31
  values?: string[];
32
32
  };
33
33
  export type TableToolbarProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"> & {
34
- /** The fields that can be sorted and filtered on. */
35
- columns: TableToolbarColumn[];
34
+ /**
35
+ * The fields Sort and Filter both offer, when neither overrides it below.
36
+ * Optional only when `sortColumns` AND `filterColumns` are both supplied —
37
+ * TableToolbar has nothing left to fall back to otherwise, and a side left
38
+ * with neither offers no fields at all (its trigger still renders; its
39
+ * popover is just empty).
40
+ */
41
+ columns?: TableToolbarColumn[];
42
+ /**
43
+ * The fields Sort offers, when they differ from Filter's — Attio's own
44
+ * Tasks page is the reference (measured live, `app.attio.com/<ws>/tasks`):
45
+ * Sort offers Due date/Assignee/Creation date, Filter offers Assignee/
46
+ * Record/Created by, and neither is the other's full set. Defaults to
47
+ * `columns`.
48
+ */
49
+ sortColumns?: TableToolbarColumn[];
50
+ /** The fields Filter offers, when they differ from Sort's. Defaults to
51
+ * `columns`. */
52
+ filterColumns?: TableToolbarColumn[];
36
53
  /** Controlled sorts. Omit to let the toolbar manage them internally. */
37
54
  sorts?: ToolbarSort[];
38
55
  defaultSorts?: ToolbarSort[];
@@ -75,12 +92,22 @@ export type TableToolbarProps = Omit<React.ComponentPropsWithoutRef<"div">, "chi
75
92
  ascendingLabel?: string;
76
93
  /** Label on the "descending" sort-direction option (default "Descending"). */
77
94
  descendingLabel?: string;
78
- /** Text on the sort trigger. Receives the active sorts and the columns, so
79
- * the sorted-by phrase can be built in the caller's own word order —
95
+ /** Text on the sort trigger. Receives the active sorts and the *sort*
96
+ * fields (`sortColumns`, or `columns` when that's not set), so the
97
+ * sorted-by phrase can be built in the caller's own word order —
80
98
  * "Sorted by Name +2" does not survive a template with holes. */
81
99
  sortTriggerLabel?: (sorts: ToolbarSort[], columns: TableToolbarColumn[]) => React.ReactNode;
82
100
  /** Text on the filter trigger. Receives the number of active filters. */
83
101
  filterTriggerLabel?: (activeFilters: number) => React.ReactNode;
102
+ /**
103
+ * Set when nesting `TableToolbar` inside a consumer's own toolbar row
104
+ * (`DataTable`'s `toolbar` slot, e.g.) alongside other same-height
105
+ * controls. Drops the row's own bottom padding, which exists for when
106
+ * `TableToolbar` sits directly above a table with nothing else in its
107
+ * row — that gap isn't the row's to add when something else in the same
108
+ * row already centres on the same 28px line (#398).
109
+ */
110
+ inline?: boolean;
84
111
  className?: string;
85
112
  };
86
113
  /**
@@ -101,6 +128,36 @@ export declare function applySorts<T>(rows: T[], sorts: ToolbarSort[], get?: (ro
101
128
  * `RecordsTable` renders this same measured chrome internally; this is the
102
129
  * composable version, to put above `DataTable` or any table of your own. Pair
103
130
  * it with `applyFilters` / `applySorts` to turn its state into rows.
131
+ *
132
+ * Sort and Filter read the same `columns` list by default, but can be given
133
+ * their own (`sortColumns`/`filterColumns`) when a consumer's two menus
134
+ * genuinely offer different fields — Attio's own Tasks page does exactly
135
+ * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
136
+ * Created by), which one shared list can't reproduce.
137
+ */
138
+ export declare function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel, reorderSortLabel, noSortsLabel, addSortLabel, learnSortingLabel, selectConditionLabel, sortFieldLabel, sortDirectionLabel, removeSortLabel, filterFieldLabel, filterFieldPlaceholder, filterValuePlaceholder, ascendingLabel, descendingLabel, sortTriggerLabel, filterTriggerLabel, inline, ...rest }: TableToolbarProps): React.JSX.Element;
139
+ export type ToolbarToolProps = React.ComponentPropsWithoutRef<"button"> & {
140
+ /** Drawn the way Sort/Filter are once a condition is set (#399). */
141
+ active?: boolean;
142
+ /** The quieter, no-ring variant Sort/Filter never use themselves, for a
143
+ * chip whose "on" state should read as less committed than a real filter. */
144
+ ghost?: boolean;
145
+ };
146
+ /**
147
+ * The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
148
+ * (#399) — for a control drawn like Sort/Filter that isn't Sort or Filter (a
149
+ * screen's own quick filter, e.g.). `.crisp-tabletoolbar-tool` alone renders
150
+ * unstyled: the rule lives nested under `TableToolbar`'s own root class, the
151
+ * same way every other rule in its shared chrome does, so it only paints on
152
+ * a descendant of an actual `TableToolbar`. This wraps one in, `display:
153
+ * contents` so it contributes no box of its own — the same technique
154
+ * `TableToolbar`'s own root already uses to disappear from layout.
104
155
  */
105
- export declare function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel, reorderSortLabel, noSortsLabel, addSortLabel, learnSortingLabel, selectConditionLabel, sortFieldLabel, sortDirectionLabel, removeSortLabel, filterFieldLabel, filterFieldPlaceholder, filterValuePlaceholder, ascendingLabel, descendingLabel, sortTriggerLabel, filterTriggerLabel, ...rest }: TableToolbarProps): React.JSX.Element;
156
+ export declare const ToolbarTool: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & {
157
+ /** Drawn the way Sort/Filter are once a condition is set (#399). */
158
+ active?: boolean;
159
+ /** The quieter, no-ring variant Sort/Filter never use themselves, for a
160
+ * chip whose "on" state should read as less committed than a real filter. */
161
+ ghost?: boolean;
162
+ } & React.RefAttributes<HTMLButtonElement>>;
106
163
  export {};
@@ -15,3 +15,20 @@
15
15
  export declare function parseCellDate(raw: string | undefined): Date | null;
16
16
  /** The inverse: the picked date's own calendar day, not the UTC one. */
17
17
  export declare function formatCellDate(d: Date): string;
18
+ /**
19
+ * How much of a date-only value a raw cell string actually carried.
20
+ *
21
+ * `parseCellDate` fills a missing month/day with January/the 1st so a
22
+ * year-only or year-month value can become a `Date` — right for parsing,
23
+ * which must produce an instant, but wrong for display: a *display*
24
+ * formatter that always prints `month day, year` renders a value that named
25
+ * one field as though it named three (#312 — `"1998"` rendered "Jan 1,
26
+ * 1998"). A caller that formats for display reads this first and only
27
+ * requests the precision the raw string actually had.
28
+ *
29
+ * A value the `DATE_ONLY` forms don't match (it carries a time, or isn't a
30
+ * recognised date at all) names a specific instant rather than a calendar
31
+ * field, so it's always `"day"` — full precision, same as today.
32
+ */
33
+ export type CellDatePrecision = "year" | "month" | "day";
34
+ export declare function cellDatePrecision(raw: string): CellDatePrecision;
@@ -1,4 +1,4 @@
1
- export { formatCellDate, parseCellDate } from "./cell-date";
1
+ export { type CellDatePrecision, cellDatePrecision, formatCellDate, parseCellDate, } from "./cell-date";
2
2
  export { cx } from "./cx";
3
3
  export { defineRecipe } from "./define";
4
4
  export { withDescribedBy } from "./dialog-description";
@@ -11,6 +11,7 @@ export * from "./components/card";
11
11
  export * from "./components/checkbox";
12
12
  export * from "./components/chip";
13
13
  export * from "./components/columncalc";
14
+ export * from "./components/composewindow";
14
15
  export * from "./components/contextmenu";
15
16
  export * from "./components/datatable";
16
17
  export * from "./components/datepicker";
@@ -1,8 +1,42 @@
1
1
  import * as React from "react";
2
2
  import { type Column, type Row } from "../recordstable";
3
+ export type RecordPageNote = {
4
+ id: string;
5
+ text: string;
6
+ time: string;
7
+ };
8
+ export type RecordPageTask = {
9
+ id: string;
10
+ text: string;
11
+ done: boolean;
12
+ };
13
+ /**
14
+ * A consumer-supplied tab, replacing one of the built-in CRM tabs
15
+ * (Overview/Activity/Emails/Calls/Team/Notes/Tasks/Files/Deals). `render`
16
+ * is called only while its tab is active, the same lazy-body contract the
17
+ * built-in tabs already follow.
18
+ */
19
+ export type RecordPageTab = {
20
+ id: string;
21
+ label: string;
22
+ /** Optional count/badge shown after the label (e.g. "Notes · 3"). */
23
+ count?: number;
24
+ render: () => React.ReactNode;
25
+ };
3
26
  export type RecordPageProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"> & {
4
27
  record: Row;
5
28
  columns: Column[];
29
+ /**
30
+ * Commit handler for a `column.editable` Details field (#431) — same
31
+ * shape `DataTable`'s own `onEdit` uses (#385/#430): returning (or
32
+ * resolving) nothing commits synchronously; returning a `Promise` that
33
+ * rejects shows a pending state on the field while it's in flight, then
34
+ * reverts to the last-known-good value (this component never mutates
35
+ * `record` itself) and renders the rejection's message beside the field.
36
+ * A field with `editable` but no `onEdit` never opens — there's nothing
37
+ * to commit to.
38
+ */
39
+ onEdit?: (id: string, key: string, value: string) => Promise<void> | void;
6
40
  /** Breadcrumb label + back button target (e.g. "Companies"). */
7
41
  collection?: string;
8
42
  /** Partial override of the chrome strings; see `recordPageLabels`. */
@@ -12,6 +46,30 @@ export type RecordPageProps = Omit<React.ComponentPropsWithoutRef<"div">, "child
12
46
  onToggleFull?: () => void;
13
47
  full?: boolean;
14
48
  className?: string;
49
+ /**
50
+ * Replaces the built-in CRM tab set. An app with a non-CRM domain (a
51
+ * custom register's own sections, a Related/backlinks tab) supplies its
52
+ * own list instead of rebuilding RecordPage's chrome by hand. Omit to
53
+ * keep the default Overview/Activity/Emails/Calls/Team/Notes/Tasks/
54
+ * Files/Deals set.
55
+ */
56
+ tabs?: RecordPageTab[];
57
+ /** Active tab id on first render, when `tabs` is supplied. Defaults to the first tab. Ignored for the built-in tab set (Overview is always first there). */
58
+ defaultTab?: string;
59
+ /**
60
+ * Controlled Notes: when supplied (even as `[]`), RecordPage renders
61
+ * this list instead of managing its own, and the composer calls
62
+ * `onAddNote` instead of appending to internal state — the same
63
+ * "caller owns the data, crisp owns the chrome" shape `Board`'s
64
+ * `cards`/`onCardsChange` uses. Omit both to keep the self-contained
65
+ * Notes section (crisp owns the data too, as today).
66
+ */
67
+ notes?: RecordPageNote[];
68
+ onAddNote?: (text: string) => void;
69
+ /** Controlled Tasks — same shape as `notes`/`onAddNote`. */
70
+ tasks?: RecordPageTask[];
71
+ onAddTask?: (text: string) => void;
72
+ onToggleTask?: (id: string) => void;
15
73
  };
16
74
  /**
17
75
  * Every chrome string RecordPage renders. Supplied as a PARTIAL map, so
@@ -97,7 +155,7 @@ export type RecordPageLabelsInput = Partial<Omit<RecordPageLabels, "tabs" | "hig
97
155
  * Emails sections (Notes & Tasks are live — add and check things off). Drop it
98
156
  * into the app's main content area.
99
157
  */
100
- export declare function RecordPage({ record, columns, collection, labels, onBack, onToggleFull, full, className, ...rest }: RecordPageProps): React.JSX.Element;
158
+ export declare function RecordPage({ record, columns, collection, labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, ...rest }: RecordPageProps): React.JSX.Element;
101
159
  export declare namespace RecordPage {
102
160
  var displayName: string;
103
161
  }
@@ -21,6 +21,16 @@ export type RecordPanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "chil
21
21
  record: Row | null;
22
22
  columns: Column[];
23
23
  onClose: () => void;
24
+ /**
25
+ * Commit handler for a `column.editable` field (#431) — same shape
26
+ * `DataTable`'s own `onEdit` uses (#385/#430): returning (or resolving)
27
+ * nothing commits synchronously; returning a `Promise` that rejects shows
28
+ * a pending state on the field while it's in flight, then reverts to the
29
+ * last-known-good value (this component never mutates `record` itself)
30
+ * and renders the rejection's message beside the field. A field with
31
+ * `editable` but no `onEdit` never opens — there's nothing to commit to.
32
+ */
33
+ onEdit?: (id: string, key: string, value: string) => Promise<void> | void;
24
34
  className?: string;
25
35
  };
26
36
  /**
@@ -29,7 +39,7 @@ export type RecordPanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "chil
29
39
  * relative` container (e.g. the app main area); it slides in from the right and
30
40
  * overlays the content.
31
41
  */
32
- export declare function RecordPanel({ record, columns, onClose, className, closeLabel, title, detailsLabel, emptyLabel, footer, ...rest }: RecordPanelProps): React.JSX.Element | null;
42
+ export declare function RecordPanel({ record, columns, onClose, className, closeLabel, title, detailsLabel, emptyLabel, footer, onEdit, ...rest }: RecordPanelProps): React.JSX.Element | null;
33
43
  export declare namespace RecordPanel {
34
44
  var displayName: string;
35
45
  }
@@ -0,0 +1,45 @@
1
+ /** A `select`-editor field's choices — same shape DataTable's own
2
+ * `DataEditableOption` uses (#385/#430), kept as its own type (not
3
+ * `Column.options`/`StatusOption`, whose `color` is required for the
4
+ * `status` picker) so this stays purely additive. */
5
+ export type EditableOption = {
6
+ value: string;
7
+ label: string;
8
+ color?: string;
9
+ };
10
+ /**
11
+ * The async-commit state machine behind `RecordPanel`/`RecordPage`'s
12
+ * editable fields (#431) — the same shape `DataTable`'s own `commitEdit`
13
+ * uses (#385/#430): a sync `onEdit` (returns `undefined`) commits and closes
14
+ * immediately; a Promise-returning `onEdit` keeps the field open and pending
15
+ * while it settles, then either closes it (resolve) or closes it AND
16
+ * reverts while showing the rejection's message (reject). The revert needs
17
+ * no extra code here either: neither `RecordPanel` nor `RecordPage` mutates
18
+ * `record` itself, so once `editingKey` clears, the field simply renders
19
+ * `record[key]` again — whatever it still says.
20
+ *
21
+ * One hook shared by both consumers rather than two copies of the same
22
+ * three `useState`s and the same `.then` branching.
23
+ */
24
+ export declare function useEditableField(onEdit?: (id: string, key: string, value: string) => Promise<void> | void): {
25
+ editingKey: string | null;
26
+ pendingKey: string | null;
27
+ error: {
28
+ key: string;
29
+ message: string;
30
+ } | null;
31
+ openEditing: (key: string) => void;
32
+ closeEditing: () => void;
33
+ commit: (id: string, key: string, value: string) => void;
34
+ };
35
+ /**
36
+ * Which plain editor a column gets when it's `editable` without an
37
+ * `editor: "select" | "switch"` — identical to `DataTable`'s own
38
+ * `editorFor` (#385): number/currency get a numeric field, date a native
39
+ * date field, everything else (text, entity, tags, url, status, reference)
40
+ * a plain text field. Kept as one function so the two never drift apart.
41
+ */
42
+ export declare function editableInputType(colType: string | undefined): {
43
+ type: string;
44
+ inputMode?: "decimal" | "text";
45
+ };
@@ -1,2 +1,3 @@
1
+ export { type EditableOption, editableInputType, useEditableField, } from "./editable-field";
1
2
  export { type Column, type ColumnType, defaultColumns, defaultRows, type EntityValue, RecordsTable, type RecordsTableProps, type Row, tagTone, } from "./recordstable";
2
3
  export { recordstableRecipe } from "./recordstable.recipe";
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import type { EditableOption } from "./editable-field";
2
3
  type IconType = React.ComponentType<{
3
4
  size?: number;
4
5
  strokeWidth?: number;
@@ -29,6 +30,14 @@ export type Column = {
29
30
  options?: StatusOption[];
30
31
  /** Choices for a `reference` column (a searchable record picker). */
31
32
  refOptions?: ReferenceOption[];
33
+ /** Adds a trailing "Create {createLabel}" row to the picker (Attio's own
34
+ * always has one) — rendered only when this is provided. Picking it hands
35
+ * back the picker's current search text, e.g. to open a create flow
36
+ * pre-seeded with it. */
37
+ onCreateReference?: (query: string) => void;
38
+ /** The object noun in "Create {createLabel}" (Attio: "Create Person").
39
+ * Only used with `onCreateReference`; defaults to "record". */
40
+ createLabel?: string;
32
41
  /**
33
42
  * Full custom cell content — an escape hatch for a display crisp can't
34
43
  * express via `type` (sign-coloured currency, a maskable amount, a custom
@@ -65,6 +74,26 @@ export type Column = {
65
74
  * currency string still sums and sorts numerically.
66
75
  */
67
76
  format?: (value: unknown, row: Row) => string;
77
+ /**
78
+ * Opts this column into `RecordPanel`/`RecordPage`'s editable-field API
79
+ * (#431) — click-to-edit there (not this table's own type-driven
80
+ * mechanism, which those two components don't use). Ignored by
81
+ * `RecordsTable` itself. A plain `editable` column, with no `editor`,
82
+ * opens a text/number/date field (inferred from `type`, same inference
83
+ * `DataTable`'s own `editable` columns use — #385); `editor` below picks
84
+ * a `select`/`switch` picker instead. Both consumers require `onEdit` to
85
+ * actually be given for a field to open at all.
86
+ */
87
+ editable?: boolean;
88
+ /** `select`/`switch` render as the field's own live, always-interactive
89
+ * content instead of opening on click — see `DataTableColumn.editor`
90
+ * (#385) for the identical convention and reasoning. `select` reads
91
+ * `editOptions`. */
92
+ editor?: "select" | "switch";
93
+ /** Options for `editor: "select"` — same shape `DataTable`'s own
94
+ * `DataEditableOption` uses. Not `options` (`StatusOption` requires
95
+ * `color`, for this type's own `status` picker) so this stays additive. */
96
+ editOptions?: EditableOption[];
68
97
  };
69
98
  export type EntityValue = {
70
99
  name: string;