@crisp-ui-kit/crisp 0.50.0 → 0.52.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 (128) 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 +37 -3
  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 +1 -1
  15. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +18 -7
  16. package/dist/cjs/core/caret-rect.js +73 -0
  17. package/dist/cjs/core/cell-date.js +11 -0
  18. package/dist/cjs/core/index.js +8 -1
  19. package/dist/cjs/core/mention.js +32 -0
  20. package/dist/cjs/core/use-mention-trigger.js +124 -0
  21. package/dist/cjs/index.js +1 -0
  22. package/dist/cjs/product/recordpage/recordpage.js +88 -10
  23. package/dist/cjs/product/recordpage/recordpage.recipe.js +30 -0
  24. package/dist/cjs/product/recordpanel/recordpanel.js +11 -29
  25. package/dist/cjs/product/recordpanel/recordpanel.recipe.js +55 -17
  26. package/dist/cjs/product/recordstable/editable-field.js +113 -0
  27. package/dist/cjs/product/recordstable/index.js +4 -1
  28. package/dist/cjs/product/recordstable/recordstable.js +67 -11
  29. package/dist/cjs/product/recordstable/recordstable.recipe.js +9 -1
  30. package/dist/cjs/product/recordstable/recordstable.tag.js +8 -3
  31. package/dist/cjs/product/richtext/index.js +2 -1
  32. package/dist/cjs/product/richtext/richtext.js +440 -23
  33. package/dist/cjs/product/richtext/richtext.recipe.js +59 -9
  34. package/dist/cjs/product/settingsshell/settingsshell.recipe.js +13 -4
  35. package/dist/cjs/product/sidebar/sidebar.js +15 -3
  36. package/dist/cjs/product/sidebar/sidebar.recipe.js +21 -2
  37. package/dist/composewindow.css +24 -0
  38. package/dist/composewindow.d.ts +7 -0
  39. package/dist/composewindow.layered.css +26 -0
  40. package/dist/datatable.css +33 -14
  41. package/dist/datatable.layered.css +33 -14
  42. package/dist/datepickerfield.css +9 -4
  43. package/dist/datepickerfield.layered.css +9 -4
  44. package/dist/dialog.css +2 -1
  45. package/dist/dialog.layered.css +2 -1
  46. package/dist/esm/components/composewindow/composewindow.js +107 -0
  47. package/dist/esm/components/composewindow/composewindow.recipe.js +42 -0
  48. package/dist/esm/components/composewindow/index.js +2 -0
  49. package/dist/esm/components/datatable/datatable.js +226 -45
  50. package/dist/esm/components/datatable/datatable.recipe.js +37 -3
  51. package/dist/esm/components/datatable/index.js +6 -0
  52. package/dist/esm/components/datepickerfield/datepickerfield.js +26 -16
  53. package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +25 -8
  54. package/dist/esm/components/dialog/dialog.recipe.js +9 -1
  55. package/dist/esm/components/menu/menu.js +4 -2
  56. package/dist/esm/components/multiselect/multiselect.recipe.js +14 -8
  57. package/dist/esm/components/select/select.js +2 -1
  58. package/dist/esm/components/table/table.recipe.js +12 -3
  59. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +1 -1
  60. package/dist/esm/components/tabletoolbar/tabletoolbar.js +18 -7
  61. package/dist/esm/core/caret-rect.js +70 -0
  62. package/dist/esm/core/cell-date.js +10 -0
  63. package/dist/esm/core/index.js +4 -1
  64. package/dist/esm/core/mention.js +29 -0
  65. package/dist/esm/core/use-mention-trigger.js +88 -0
  66. package/dist/esm/index.js +1 -0
  67. package/dist/esm/product/recordpage/recordpage.js +89 -11
  68. package/dist/esm/product/recordpage/recordpage.recipe.js +30 -0
  69. package/dist/esm/product/recordpanel/index.js +1 -1
  70. package/dist/esm/product/recordpanel/recordpanel.js +13 -31
  71. package/dist/esm/product/recordpanel/recordpanel.recipe.js +56 -18
  72. package/dist/esm/product/recordstable/editable-field.js +76 -0
  73. package/dist/esm/product/recordstable/index.js +1 -0
  74. package/dist/esm/product/recordstable/recordstable.js +69 -13
  75. package/dist/esm/product/recordstable/recordstable.recipe.js +9 -1
  76. package/dist/esm/product/recordstable/recordstable.tag.js +8 -3
  77. package/dist/esm/product/richtext/index.js +1 -1
  78. package/dist/esm/product/richtext/richtext.js +439 -22
  79. package/dist/esm/product/richtext/richtext.recipe.js +60 -10
  80. package/dist/esm/product/settingsshell/settingsshell.recipe.js +13 -4
  81. package/dist/esm/product/sidebar/sidebar.js +15 -3
  82. package/dist/esm/product/sidebar/sidebar.recipe.js +21 -2
  83. package/dist/multiselect.css +8 -8
  84. package/dist/multiselect.layered.css +8 -8
  85. package/dist/recordpage.css +25 -13
  86. package/dist/recordpage.layered.css +25 -13
  87. package/dist/recordpanel.css +43 -20
  88. package/dist/recordpanel.layered.css +43 -20
  89. package/dist/recordstable.css +9 -2
  90. package/dist/recordstable.layered.css +9 -2
  91. package/dist/richtext.css +31 -5
  92. package/dist/richtext.layered.css +31 -5
  93. package/dist/settingsshell.css +2 -2
  94. package/dist/settingsshell.layered.css +2 -2
  95. package/dist/sidebar.css +7 -2
  96. package/dist/sidebar.layered.css +7 -2
  97. package/dist/styles.css +199 -74
  98. package/dist/styles.layered.css +199 -74
  99. package/dist/table.css +4 -2
  100. package/dist/table.layered.css +4 -2
  101. package/dist/tabletoolbar.css +1 -1
  102. package/dist/tabletoolbar.layered.css +1 -1
  103. package/dist/types/components/composewindow/composewindow.d.ts +80 -0
  104. package/dist/types/components/composewindow/composewindow.recipe.d.ts +3 -0
  105. package/dist/types/components/composewindow/index.d.ts +2 -0
  106. package/dist/types/components/datatable/datatable.d.ts +58 -4
  107. package/dist/types/components/datatable/index.d.ts +1 -1
  108. package/dist/types/components/datepickerfield/datepickerfield.d.ts +18 -4
  109. package/dist/types/components/menu/menu.d.ts +10 -1
  110. package/dist/types/components/select/select.d.ts +6 -1
  111. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +29 -5
  112. package/dist/types/core/caret-rect.d.ts +16 -0
  113. package/dist/types/core/cell-date.d.ts +17 -0
  114. package/dist/types/core/index.d.ts +4 -1
  115. package/dist/types/core/mention.d.ts +27 -0
  116. package/dist/types/core/use-mention-trigger.d.ts +41 -0
  117. package/dist/types/index.d.ts +1 -0
  118. package/dist/types/product/recordpage/recordpage.d.ts +59 -1
  119. package/dist/types/product/recordpanel/index.d.ts +1 -1
  120. package/dist/types/product/recordpanel/recordpanel.d.ts +81 -17
  121. package/dist/types/product/recordstable/editable-field.d.ts +48 -0
  122. package/dist/types/product/recordstable/index.d.ts +1 -0
  123. package/dist/types/product/recordstable/recordstable.d.ts +29 -0
  124. package/dist/types/product/recordstable/recordstable.tag.d.ts +8 -3
  125. package/dist/types/product/richtext/index.d.ts +1 -1
  126. package/dist/types/product/richtext/richtext.d.ts +168 -7
  127. package/dist/types/product/sidebar/sidebar.d.ts +60 -4
  128. package/package.json +6 -1
@@ -8,7 +8,7 @@
8
8
  & .crisp-tabletoolbar-search svg { flex-shrink: 0; }
9
9
  & .crisp-tabletoolbar-search input { border: none; outline: none; background: transparent; font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-primary); width: 180px; }
10
10
  & .crisp-tabletoolbar-search input::placeholder { color: var(--crisp-fg-muted); }
11
- & .crisp-tabletoolbar-tool { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px var(--crisp-border-default); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; transition: background-color .12s; }
11
+ & .crisp-tabletoolbar-tool { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px var(--crisp-border-default); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
12
12
  & .crisp-tabletoolbar-tool:hover { background: var(--crisp-bg-raised); }
13
13
  & .crisp-tabletoolbar-tool svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
14
14
  & .crisp-tabletoolbar-tool--active { background: var(--crisp-bg-raised); }
@@ -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,8 +92,9 @@ 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. */
@@ -110,8 +128,14 @@ export declare function applySorts<T>(rows: T[], sorts: ToolbarSort[], get?: (ro
110
128
  * `RecordsTable` renders this same measured chrome internally; this is the
111
129
  * composable version, to put above `DataTable` or any table of your own. Pair
112
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.
113
137
  */
114
- 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, inline, ...rest }: TableToolbarProps): React.JSX.Element;
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;
115
139
  export type ToolbarToolProps = React.ComponentPropsWithoutRef<"button"> & {
116
140
  /** Drawn the way Sort/Filter are once a condition is set (#399). */
117
141
  active?: boolean;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The pixel position of a caret index inside a `<textarea>`/`<input>`, in
3
+ * viewport coordinates — where a mention picker anchors.
4
+ *
5
+ * Neither element exposes caret geometry directly, so this uses the standard
6
+ * mirror-div technique: an off-screen div replicating every computed style
7
+ * that affects text layout (font, padding, border, wrapping), holding the
8
+ * same text up to the caret plus a marker span. The marker's own
9
+ * `getBoundingClientRect()`, translated back onto the real field's box, is
10
+ * the caret's actual rendered position — nothing here guesses at line
11
+ * height or character width itself.
12
+ */
13
+ export declare function caretRectInField(field: HTMLTextAreaElement | HTMLInputElement, index: number): {
14
+ x: number;
15
+ y: number;
16
+ };
@@ -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,5 @@
1
- export { formatCellDate, parseCellDate } from "./cell-date";
1
+ export { caretRectInField } from "./caret-rect";
2
+ export { type CellDatePrecision, cellDatePrecision, formatCellDate, parseCellDate, } from "./cell-date";
2
3
  export { cx } from "./cx";
3
4
  export { defineRecipe } from "./define";
4
5
  export { withDescribedBy } from "./dialog-description";
@@ -6,6 +7,7 @@ export { isRtlElement } from "./direction";
6
7
  export { FloatingPortal } from "./floating-portal";
7
8
  export { hasOpenFloatingLayer, isInsideLaterFloatingLayer, registerFloatingLayer, } from "./floating-stack";
8
9
  export { formValueStyle } from "./form-value";
10
+ export { type MentionAnchor, type MentionMatch, matchMentionTrigger, } from "./mention";
9
11
  export { type FloatingAlign, type PlaceFloatingOptions, placeFloating, } from "./place-floating";
10
12
  export { Primitive } from "./primitive";
11
13
  export { resolveRecipe } from "./resolve";
@@ -15,4 +17,5 @@ export type * from "./types";
15
17
  export { useFocusAfterRemoval } from "./use-focus-after-removal";
16
18
  export { useFormReset } from "./use-form-reset";
17
19
  export { useLabelAssociation } from "./use-label-association";
20
+ export { type MentionTrigger, type MentionTriggerState, type UseMentionTriggerOptions, useMentionTrigger, } from "./use-mention-trigger";
18
21
  export { usePrefersReducedMotion } from "./use-prefers-reduced-motion";
@@ -0,0 +1,27 @@
1
+ export type MentionMatch = {
2
+ /** Text typed after the trigger character — what a picker filters by. */
3
+ query: string;
4
+ /** Index, in the text passed in, where the trigger character itself sits. */
5
+ start: number;
6
+ };
7
+ /**
8
+ * Is there an active "@query" mention trigger ending at the caret?
9
+ *
10
+ * Matches the rule every "@mention" implementation (GitHub, Slack, Linear)
11
+ * actually uses: the trigger character must start a word — the very first
12
+ * character, or preceded by whitespace — and nothing between it and the
13
+ * caret may contain whitespace. `foo@bar` is an email/handle stuck to a word,
14
+ * not a trigger; `@jo next` has already moved past the mention it typed.
15
+ *
16
+ * Takes plain text rather than a live DOM node so it is one pure function
17
+ * usable against a `<textarea>`'s `value` and a `contentEditable` text
18
+ * node's content alike, and is the one thing here worth unit-testing on its
19
+ * own — everything downstream of it (caret geometry, DOM insertion) needs a
20
+ * real browser to verify.
21
+ */
22
+ export declare function matchMentionTrigger(textBeforeCaret: string, trigger?: string): MentionMatch | null;
23
+ /** A caret's position in viewport coordinates — where a picker anchors. */
24
+ export type MentionAnchor = {
25
+ x: number;
26
+ y: number;
27
+ };
@@ -0,0 +1,41 @@
1
+ import * as React from "react";
2
+ import { type MentionAnchor } from "./mention";
3
+ export type MentionTriggerState = {
4
+ open: boolean;
5
+ query: string;
6
+ /** Where to anchor a picker — null while closed. */
7
+ anchor: MentionAnchor | null;
8
+ };
9
+ export type UseMentionTriggerOptions = {
10
+ /** Character that opens the picker. Default `"@"`. */
11
+ trigger?: string;
12
+ };
13
+ export type MentionTrigger = MentionTriggerState & {
14
+ /** Dismiss the picker without inserting anything — e.g. on Escape. */
15
+ close: () => void;
16
+ /** Replace the "@query" text (the trigger included) with `text`, restore
17
+ * focus and the caret right after it, and dismiss. */
18
+ insert: (text: string) => void;
19
+ };
20
+ /**
21
+ * Watches a `<textarea>`/`<input>` for an "@query" mention trigger and
22
+ * reports where to anchor a consumer-supplied picker (#372).
23
+ *
24
+ * crisp draws no picker of its own here — GitHub/Slack/Linear-style @mention
25
+ * UI varies too much by app (avatars, groups, recent-first ordering, a
26
+ * "create new person" row) to have one measured shape, and this repo's rule
27
+ * is measure-first, never invented polish. What is shared and worth owning
28
+ * centrally is the fiddly, easy-to-get-wrong part: deciding when "@" is
29
+ * actually a trigger rather than part of a word or an email address (see
30
+ * `matchMentionTrigger`), and where its picker anchors on screen (see
31
+ * `caretRectInField`, since neither element exposes caret geometry itself).
32
+ *
33
+ * Does not close on blur: a picker built from this needs to survive the
34
+ * field losing focus to a `mousedown` on one of its own rows, the same
35
+ * problem every floating surface in this repo solves for itself (see
36
+ * `RichTextEditor`'s toolbar `keepSel`, or `Popover`'s own outside-pointer-
37
+ * down listener) — so closing on outside interaction is left to the
38
+ * consumer's picker, which already knows what counts as "outside" for its
39
+ * own markup.
40
+ */
41
+ export declare function useMentionTrigger(ref: React.RefObject<HTMLTextAreaElement | HTMLInputElement | null>, { trigger }?: UseMentionTriggerOptions): MentionTrigger;
@@ -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
  }
@@ -1,2 +1,2 @@
1
- export { RecordPanel, type RecordPanelProps } from "./recordpanel";
1
+ export { RecordPanel, type RecordPanelHighlight, type RecordPanelProps, } from "./recordpanel";
2
2
  export { recordpanelRecipe } from "./recordpanel.recipe";