@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,144 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ComposeWindow = ComposeWindow;
37
+ exports.ComposeWindowBody = ComposeWindowBody;
38
+ const jsx_runtime_1 = require("react/jsx-runtime");
39
+ const dialog = __importStar(require("@zag-js/dialog"));
40
+ const react_1 = require("@zag-js/react");
41
+ const lucide_react_1 = require("lucide-react");
42
+ const React = __importStar(require("react"));
43
+ const core_1 = require("../../core");
44
+ const composewindow_recipe_1 = require("./composewindow.recipe");
45
+ // A globally unique id per mounted instance — see command.tsx/dialog.tsx for
46
+ // the rationale (the machine defers its dismissal listeners a frame, so a
47
+ // tree-relative id could let a stale registration resolve to a new instance).
48
+ let instanceCounter = 0;
49
+ function useComposeWindowInstanceId() {
50
+ const idRef = React.useRef(null);
51
+ if (idRef.current == null) {
52
+ instanceCounter += 1;
53
+ idRef.current = `crisp-composewindow-${instanceCounter}`;
54
+ }
55
+ return idRef.current;
56
+ }
57
+ /**
58
+ * A floating, minimizable, pinnable window — Attio's own note "compose
59
+ * window" chrome (measured live, read-only, 2026-09-24; see HANDOFF), not a
60
+ * new visual recipe: it reuses `Dialog`'s exact measured frosted-glass panel
61
+ * (`bgOverlayFrost` 60% + `blur(15px)`, `elevation.modal`'s 3-layer shadow,
62
+ * 16px outer radius) — those numbers matched Attio's compose window exactly
63
+ * when re-checked against it, panel-for-panel. What's actually different
64
+ * from `Dialog` is the header (a breadcrumb plus minimize/pin/close, not a
65
+ * title plus close) and two extra pieces of state Dialog has no concept of.
66
+ *
67
+ * `minimized` is measured, not guessed — the FIRST version of this file built
68
+ * an invented "minimized bar" (a Gmail-style dock, since a bar was never
69
+ * actually exercised against Attio). Re-measured properly: minimizing an
70
+ * existing note does not produce any bar, tray, dock, or badge anywhere on
71
+ * screen. Confirmed four independent ways, both with synthetic and real
72
+ * CDP-dispatched trusted clicks — the element count under the note's own
73
+ * subtree drops by ~110 nodes, `[role="dialog"]` stops matching it, its
74
+ * `data-hidden="true"` ancestor computes `visibility: hidden; opacity: 0`,
75
+ * and the URL drops the `?id=…&modal=note` query exactly like Close does —
76
+ * but the subtree is NOT removed: `document.querySelectorAll('[aria-label=
77
+ * "Minimize"]')` still finds it afterward, `visibility: hidden` (not
78
+ * `display: none`) preserves layout, and nothing in localStorage or the DOM
79
+ * suggests state was discarded. So: minimized hides the panel — mounted,
80
+ * `visibility: hidden; opacity: 0` — rather than unmounting or closing it,
81
+ * and crisp renders no restore affordance of its own (Attio has none
82
+ * either); reopening is the consumer's own UI, driven by `minimized`/
83
+ * `onMinimizedChange` the same way `open` already works.
84
+ *
85
+ * "Pinned" is implemented as the one directly-supported behavior that
86
+ * matches its name (Zag's `closeOnInteractOutside`, off while pinned) —
87
+ * Attio's own multi-instance stacking/docking when pinned was not exercised,
88
+ * so nothing here repositions the window when pinned.
89
+ *
90
+ * Not modal: Attio's own compose window dims nothing behind it (`Dialog`'s
91
+ * backdrop is `background: transparent` too — the blur alone does the
92
+ * separation), and the page behind stays visibly interactive in the
93
+ * measured screenshot. `trapFocus` follows the same call — a picture-in-
94
+ * picture window, not a modal, so Tab is not caught inside it.
95
+ */
96
+ function ComposeWindow({ open, onOpenChange, breadcrumb, minimized: minimizedProp, defaultMinimized, onMinimizedChange, pinned: pinnedProp, defaultPinned, onPinnedChange, width = 968, "aria-label": ariaLabel, minimizeLabel = "Minimize", pinLabel = "Pin to screen", closeLabel = "Close", children, className, style, "aria-describedby": describedBy, ...rest }) {
97
+ const id = useComposeWindowInstanceId();
98
+ const [internalMinimized, setInternalMinimized] = React.useState(defaultMinimized ?? false);
99
+ const minimized = minimizedProp ?? internalMinimized;
100
+ const setMinimized = React.useCallback((next) => {
101
+ if (minimizedProp === undefined)
102
+ setInternalMinimized(next);
103
+ onMinimizedChange?.(next);
104
+ }, [minimizedProp, onMinimizedChange]);
105
+ const [internalPinned, setInternalPinned] = React.useState(defaultPinned ?? false);
106
+ const pinned = pinnedProp ?? internalPinned;
107
+ const setPinned = React.useCallback((next) => {
108
+ if (pinnedProp === undefined)
109
+ setInternalPinned(next);
110
+ onPinnedChange?.(next);
111
+ }, [pinnedProp, onPinnedChange]);
112
+ const service = (0, react_1.useMachine)(dialog.machine, {
113
+ id,
114
+ // Always `open` while the consumer says so — NOT `open && !minimized`.
115
+ // Zag's own "closed" state sets `hidden` on the content element, which
116
+ // the browser's UA stylesheet turns into `display: none` — the exact
117
+ // thing minimized must NOT be (Attio's own measured minimized subtree
118
+ // keeps `display: flex`, only `visibility`/`opacity` change). Disabling
119
+ // `closeOnEscape`/`closeOnInteractOutside` below gets the same "stand
120
+ // down while minimized" result without that side effect.
121
+ open,
122
+ onOpenChange(details) {
123
+ // Zag only ever asks to close (Escape / outside pointerdown) — it
124
+ // never asks to open on its own, so this is always a close request.
125
+ if (!details.open)
126
+ onOpenChange(false);
127
+ },
128
+ modal: false,
129
+ trapFocus: false,
130
+ closeOnInteractOutside: !pinned && !minimized,
131
+ closeOnEscape: !minimized,
132
+ "aria-label": ariaLabel,
133
+ });
134
+ const api = dialog.connect(service, react_1.normalizeProps);
135
+ if (!open)
136
+ return null;
137
+ return ((0, jsx_runtime_1.jsx)(react_1.Portal, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, composewindow_recipe_1.composewindowRecipe)(), children: [(0, jsx_runtime_1.jsx)("div", { ...api.getBackdropProps(), className: "crisp-composewindow-backdrop" }), (0, jsx_runtime_1.jsx)("div", { ...api.getPositionerProps(), className: "crisp-composewindow-positioner", children: (0, jsx_runtime_1.jsx)("div", { ...rest, ...(0, core_1.withDescribedBy)(api.getContentProps(), describedBy), className: (0, core_1.cx)("crisp-composewindow-content", className), "data-crisp-composewindow": id, "data-minimized": minimized || undefined, style: { ...style, maxWidth: `min(${width}px, 90vw)` }, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-composewindow-card", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-composewindow-header", children: [(0, jsx_runtime_1.jsx)("span", { ...api.getTitleProps(), className: "crisp-composewindow-breadcrumb", children: breadcrumb }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-composewindow-actions", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-composewindow-action-btn", "aria-label": minimizeLabel, onClick: () => setMinimized(true), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Minus, { size: 14, strokeWidth: 2, "aria-hidden": "true" }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-composewindow-action-btn", "aria-label": pinLabel, "aria-pressed": pinned, "data-state": pinned ? "on" : "off", onClick: () => setPinned(!pinned), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pin, { size: 14, strokeWidth: 2, "aria-hidden": "true" }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", ...api.getCloseTriggerProps(), className: "crisp-composewindow-action-btn", "aria-label": closeLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 14, strokeWidth: 2, "aria-hidden": "true" }) })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-composewindow-body", children: children })] }) }) })] }) }));
138
+ }
139
+ ComposeWindow.displayName = "ComposeWindow";
140
+ function ComposeWindowBody({ children, className, }) {
141
+ return ((0, jsx_runtime_1.jsx)("div", { className: className
142
+ ? `crisp-composewindow-content-body ${className}`
143
+ : "crisp-composewindow-content-body", children: children }));
144
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.composewindowRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // Attio's floating "compose window" — measured live from an existing note,
7
+ // read-only, 2026-09-24 (#427). Re-checked independently against the same
8
+ // live note a second time before shipping — every number below matched.
9
+ //
10
+ // The panel itself is NOT a new visual recipe: `bgOverlayFrost` 60% +
11
+ // `blur(15px)` + `elevation.modal`'s 3-layer shadow + 16px radius (`2xl`) are
12
+ // the exact same measured values `Dialog` already carries — Attio's own note
13
+ // window and crisp's existing modal panel are the same surface family. What
14
+ // IS new here is the header (a breadcrumb plus minimize/pin/close, not a
15
+ // title plus close) and `[data-minimized]`.
16
+ //
17
+ // Minimizing does NOT produce a bar/tray/dock — that was this file's first
18
+ // pass, before minimizing was ever actually exercised against a live note.
19
+ // Re-measured: nothing appears anywhere on screen; the note's own subtree
20
+ // stays mounted with `visibility: hidden; opacity: 0` (see composewindow.tsx's
21
+ // doc comment for the four ways this was confirmed). `[data-minimized]`
22
+ // reproduces exactly that — hidden, not display:none, so state and scroll
23
+ // position survive until a consumer sets `minimized={false}` again.
24
+ const config = (0, core_1.defineRecipe)({
25
+ name: "composewindow",
26
+ base: `display: contents; font-family: ${tokens_1.contract.font.sans};
27
+ & .crisp-composewindow-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; pointer-events: none; }
28
+ & .crisp-composewindow-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh ${tokens_1.contract.space["4"]} ${tokens_1.contract.space["4"]}; pointer-events: none; }
29
+ & .crisp-composewindow-content { pointer-events: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-height: 74vh; padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius["2xl"]}; background: color-mix(in srgb, ${tokens_1.contract.bgOverlayFrost} 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: ${tokens_1.elevation.modal}; opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
30
+ & .crisp-composewindow-content[data-minimized] { visibility: hidden; opacity: 0; }
31
+ & .crisp-composewindow-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgSurface}; box-shadow: 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); overflow: hidden; }
32
+ & .crisp-composewindow-header { display: flex; align-items: center; justify-content: space-between; gap: ${tokens_1.contract.space["2"]}; flex-shrink: 0; height: 52px; padding: 0 ${tokens_1.contract.space["2"]} 0 ${tokens_1.contract.space["4"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
33
+ & .crisp-composewindow-breadcrumb { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
34
+ & .crisp-composewindow-actions { display: flex; align-items: center; gap: ${tokens_1.contract.space["0.5"]}; flex-shrink: 0; }
35
+ & .crisp-composewindow-action-btn { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: none; color: ${tokens_1.contract.fgMuted}; font-family: inherit; cursor: pointer; }
36
+ & .crisp-composewindow-action-btn:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); color: ${tokens_1.contract.fgPrimary}; }
37
+ & .crisp-composewindow-action-btn[data-state="on"] { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 14%, transparent); color: ${tokens_1.contract.bgBrandSolid}; }
38
+ & .crisp-composewindow-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; padding: ${tokens_1.contract.space["5"]} ${tokens_1.contract.space["4"]}; }
39
+ & .crisp-composewindow-content-body { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["4"]}; }`,
40
+ variants: {},
41
+ defaultVariants: {},
42
+ compoundVariants: [],
43
+ });
44
+ exports.composewindowRecipe = (0, core_1.resolveRecipe)(config);
45
+ exports.default = config;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.composewindowRecipe = exports.ComposeWindowBody = exports.ComposeWindow = void 0;
4
+ var composewindow_1 = require("./composewindow");
5
+ Object.defineProperty(exports, "ComposeWindow", { enumerable: true, get: function () { return composewindow_1.ComposeWindow; } });
6
+ Object.defineProperty(exports, "ComposeWindowBody", { enumerable: true, get: function () { return composewindow_1.ComposeWindowBody; } });
7
+ var composewindow_recipe_1 = require("./composewindow.recipe");
8
+ Object.defineProperty(exports, "composewindowRecipe", { enumerable: true, get: function () { return composewindow_recipe_1.composewindowRecipe; } });
@@ -38,8 +38,12 @@ const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const lucide_react_1 = require("lucide-react");
39
39
  const React = __importStar(require("react"));
40
40
  const core_1 = require("../../core");
41
+ const avatar_1 = require("../avatar");
41
42
  const checkbox_1 = require("../checkbox");
42
43
  const menu_1 = require("../menu");
44
+ const select_1 = require("../select");
45
+ const spinner_1 = require("../spinner");
46
+ const switch_1 = require("../switch");
43
47
  const table_1 = require("../table");
44
48
  const datatable_recipe_1 = require("./datatable.recipe");
45
49
  /** A `date` input needs `yyyy-mm-dd`; cells may hold a Date, an ISO string or
@@ -58,6 +62,26 @@ function toDateInputValue(v) {
58
62
  }
59
63
  return "";
60
64
  }
65
+ /** A `datetime-local` input needs `yyyy-mm-ddThh:mm`, in local time. Unlike
66
+ * `toDateInputValue`, this reads the value with a REAL `new Date(v)`, not
67
+ * `parseCellDate` -- `parseCellDate` exists specifically to treat a
68
+ * date-ONLY string as local midnight rather than UTC midnight (#221); a
69
+ * datetime value already carries its own time-of-day (and, if present, its
70
+ * own offset), so the ordinary `Date` constructor is the correct parse, and
71
+ * `parseCellDate` would silently discard the time half this type exists to
72
+ * keep (#432). Returns "" when it cannot be parsed, same contract as
73
+ * `toDateInputValue`. */
74
+ function toDateTimeInputValue(v) {
75
+ const d = v instanceof Date
76
+ ? v
77
+ : typeof v === "string" || typeof v === "number"
78
+ ? new Date(v)
79
+ : null;
80
+ if (!d || Number.isNaN(d.getTime()))
81
+ return "";
82
+ const pad = (n) => String(n).padStart(2, "0");
83
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
84
+ }
61
85
  function cellValue(col, row) {
62
86
  if (col.accessor)
63
87
  return col.accessor(row);
@@ -92,21 +116,46 @@ function initials(name) {
92
116
  const p = name.trim().split(/\s+/);
93
117
  return ((p[0]?.[0] ?? "") + (p[1]?.[0] ?? "")).toUpperCase() || "?";
94
118
  }
95
- // One Intl.DateTimeFormat per locale, built lazily and reused — constructing
96
- // a formatter per cell render would be wasteful on a large grid.
119
+ // One Intl.DateTimeFormat per locale (and per precision it's asked to print),
120
+ // built lazily and reused — constructing a formatter per cell render would be
121
+ // wasteful on a large grid.
97
122
  const DATE_FMT_CACHE = new Map();
98
- function dateFmt(locale) {
99
- let fmt = DATE_FMT_CACHE.get(locale);
123
+ function dateFmtCached(cache, locale, options) {
124
+ let fmt = cache.get(locale);
100
125
  if (!fmt) {
101
- fmt = new Intl.DateTimeFormat(locale, {
102
- month: "short",
103
- day: "numeric",
104
- year: "numeric",
105
- });
106
- DATE_FMT_CACHE.set(locale, fmt);
126
+ fmt = new Intl.DateTimeFormat(locale, options);
127
+ cache.set(locale, fmt);
107
128
  }
108
129
  return fmt;
109
130
  }
131
+ function dateFmt(locale) {
132
+ return dateFmtCached(DATE_FMT_CACHE, locale, {
133
+ month: "short",
134
+ day: "numeric",
135
+ year: "numeric",
136
+ });
137
+ }
138
+ // "Sep 1998" — a value that named a month but not a day (#312).
139
+ const MONTH_FMT_CACHE = new Map();
140
+ function monthFmt(locale) {
141
+ return dateFmtCached(MONTH_FMT_CACHE, locale, {
142
+ month: "short",
143
+ year: "numeric",
144
+ });
145
+ }
146
+ // Same caching, extended with the time-of-day fields `dateFmt` deliberately
147
+ // omits (#432) -- its own cache/formatter, not `dateFmt` with options bolted
148
+ // on, so `date`'s own output can never shift by adding this type.
149
+ const DATETIME_FMT_CACHE = new Map();
150
+ function dateTimeFmt(locale) {
151
+ return dateFmtCached(DATETIME_FMT_CACHE, locale, {
152
+ month: "short",
153
+ day: "numeric",
154
+ year: "numeric",
155
+ hour: "numeric",
156
+ minute: "2-digit",
157
+ });
158
+ }
110
159
  function formatCell(type, v, symbol, locale = "en-US") {
111
160
  if (type === "number")
112
161
  return typeof v === "number" ? v.toLocaleString(locale) : asText(v);
@@ -118,8 +167,29 @@ function formatCell(type, v, symbol, locale = "en-US") {
118
167
  // parseCellDate, not `new Date(v)`: a date-only value is UTC midnight per
119
168
  // ECMA-262, and formatting that in the reader's timezone renders it a day
120
169
  // early west of UTC (#221, the same defect #220 fixed in RecordsTable).
121
- const d = v ? (0, core_1.parseCellDate)(String(v)) : null;
122
- return d ? dateFmt(locale).format(d) : asText(v);
170
+ const raw = v ? String(v) : "";
171
+ const d = raw ? (0, core_1.parseCellDate)(raw) : null;
172
+ if (!d)
173
+ return asText(v);
174
+ // parseCellDate fills in a missing month/day so a year-only or
175
+ // year-month value can become a Date; only print the precision the raw
176
+ // value actually carried, or "1998" renders as an invented "Jan 1, 1998"
177
+ // (#312).
178
+ const precision = (0, core_1.cellDatePrecision)(raw);
179
+ if (precision === "year")
180
+ return String(d.getFullYear());
181
+ if (precision === "month")
182
+ return monthFmt(locale).format(d);
183
+ return dateFmt(locale).format(d);
184
+ }
185
+ if (type === "datetime") {
186
+ // A REAL `new Date(v)`, not `parseCellDate` -- see toDateTimeInputValue's
187
+ // own comment; this value carries a time-of-day `parseCellDate` would
188
+ // discard.
189
+ const d = v instanceof Date ? v : v ? new Date(String(v)) : null;
190
+ return d && !Number.isNaN(d.getTime())
191
+ ? dateTimeFmt(locale).format(d)
192
+ : asText(v);
123
193
  }
124
194
  return asText(v);
125
195
  }
@@ -132,7 +202,9 @@ function formatCell(type, v, symbol, locale = "en-US") {
132
202
  // display string belongs -- as an entity's name, a status' label -- while the
133
203
  // raw value keeps driving structure and tone.
134
204
  function renderTyped(col, value, avatarTone, display, locale = "en-US") {
135
- const empty = (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-muted", children: "\u2014" });
205
+ // Attio's own empty cell is genuinely blank -- no placeholder character at
206
+ // all, for any column type (#412).
207
+ const empty = null;
136
208
  switch (col.type) {
137
209
  case "entity": {
138
210
  const raw = (typeof value === "object" && value ? value : { name: asText(value) });
@@ -171,6 +243,44 @@ function renderTyped(col, value, avatarTone, display, locale = "en-US") {
171
243
  }
172
244
  }
173
245
  const CHECK_W = 40;
246
+ // A sentinel that can't collide with a real record id (crisp never generates
247
+ // one, and a consumer's own ids come from their own data store) -- picking
248
+ // this row calls `onCreateReference`, not the cell's own commit (#449).
249
+ const CREATE_REFERENCE_VALUE = "__crisp_create_reference__";
250
+ /** The 16px avatar Attio's own reference picker draws before every option's
251
+ * label -- a photo when the option carries one, a generic person glyph
252
+ * otherwise (measured live, #449 -- the same measurement and component
253
+ * shape as RecordsTable's own `ReferenceCell`, which this mirrors). */
254
+ function dataReferenceOptionLabel(o) {
255
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-refoption", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", className: "crisp-datatable-refoption-avatar", children: o.avatar ? ((0, jsx_runtime_1.jsx)("img", { src: o.avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.CircleUserRound, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" })) }), o.label] }));
256
+ }
257
+ /** `editor: "reference"`'s picker, its own component (not inline in the
258
+ * row-render loop) because it needs its own `useState` for the search query
259
+ * -- one instance per cell, same reasoning `select`/`switch` don't need a
260
+ * component of their own but this does. */
261
+ function DataReferenceEditor({ col, value, isPending, onCommit, }) {
262
+ const [query, setQuery] = React.useState("");
263
+ const options = (col.refOptions ?? []).map((o) => ({
264
+ value: o.value,
265
+ label: dataReferenceOptionLabel(o),
266
+ keywords: o.label,
267
+ }));
268
+ if (col.onCreateReference) {
269
+ options.push({
270
+ value: CREATE_REFERENCE_VALUE,
271
+ label: ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-refoption crisp-datatable-refoption--create", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), `Create ${col.createLabel ?? "record"}`] })),
272
+ // always reachable regardless of the typed query -- it's an action,
273
+ // not a record to filter away
274
+ keywords: query,
275
+ });
276
+ }
277
+ return ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", searchable: true, searchPlaceholder: col.refSearchPlaceholder, onSearchChange: setQuery, "aria-label": `Edit ${col.key}`, value: asText(value) || undefined, disabled: isPending, options: options, onValueChange: (v) => {
278
+ if (v === CREATE_REFERENCE_VALUE)
279
+ col.onCreateReference?.(query);
280
+ else
281
+ onCommit(v);
282
+ } }));
283
+ }
174
284
  function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel = "Select all rows", selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement = (count) => count === 0 ? "Selection cleared" : `${count} selected`, columnMenu = false, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder = "Search…", searchLabel = "Search table", onEdit, stickyFirst = true, stickyLast = false, locale: localeProp, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", empty = "No results.", showCount = true, countLabel = (shown, total) => shown === total
175
285
  ? `${total} ${total === 1 ? "row" : "rows"}`
176
286
  : `${shown} of ${total}`, toolbar, className, reorderRowLabel = "Reorder row", ...rest }) {
@@ -270,6 +380,10 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
270
380
  setSelectionText(selectionAnnouncement(selected.size));
271
381
  }
272
382
  const [editing, setEditing] = React.useState(null);
383
+ // Set only while an async `onEdit` is in flight (#385) -- a sync `onEdit`
384
+ // never touches either of these.
385
+ const [pending, setPending] = React.useState(null);
386
+ const [editError, setEditError] = React.useState(null);
273
387
  const [scrolledX, setScrolledX] = React.useState(false);
274
388
  // Mirrors `scrolledX` for the trailing edge: true while there is still
275
389
  // unrevealed content sliding under the sticky trailing column, i.e. while
@@ -380,9 +494,18 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
380
494
  * sibling — rather than giving every type the same text box:
381
495
  * number/currency numeric, with a decimal keypad on touch
382
496
  * date a native date field
497
+ * datetime a native datetime field -- `type="date"` drops the
498
+ * time-of-day half of the value on commit; this is the
499
+ * one type that keeps it (#432), via a native
500
+ * `datetime-local` field rather than a custom popover:
501
+ * the plain-editor family is native-input-backed
502
+ * throughout, and the browser's own picker already
503
+ * carries both a calendar and a time control.
383
504
  * text/entity a plain text field
384
505
  * `status`, `reference` and `tags` are pickers in RecordsTable, not
385
- * double-click editors, so they are not editable here either.
506
+ * double-click editors, so they are not editable here either -- except
507
+ * `editor: "reference"` (#432), which is a picker for the same reason
508
+ * `select`/`switch` are: always live, no double-click gate.
386
509
  */
387
510
  function editorFor(col) {
388
511
  switch (col.type) {
@@ -391,6 +514,8 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
391
514
  return { type: "number", inputMode: "decimal", step: "any" };
392
515
  case "date":
393
516
  return { type: "date" };
517
+ case "datetime":
518
+ return { type: "datetime-local" };
394
519
  default:
395
520
  return { type: "text", inputMode: "text" };
396
521
  }
@@ -434,11 +559,15 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
434
559
  return;
435
560
  }
436
561
  else if (e.key === "Enter") {
437
- if (columns[c]?.editable) {
562
+ // select/switch columns have no separate editing mode to enter --
563
+ // they're already their own live widget (#385); Enter there is a
564
+ // no-op here, same as any other non-editable cell that isn't wired to
565
+ // onRowClick.
566
+ if (columns[c]?.editable && !columns[c]?.editor) {
438
567
  e.preventDefault();
439
- setEditing({ id: selCell.id, key: selCell.key });
568
+ openEditing(selCell.id, selCell.key);
440
569
  }
441
- else if (onRowClick) {
570
+ else if (!columns[c]?.editable && onRowClick) {
442
571
  e.preventDefault();
443
572
  onRowClick(rows[r]);
444
573
  }
@@ -449,8 +578,43 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
449
578
  e.preventDefault();
450
579
  setSelCell({ id: rows[nr].id, key: columns[nc].key });
451
580
  }
581
+ // Opening a cell's editor also clears any stale error left over from a
582
+ // PREVIOUS failed commit on it (#385) -- otherwise a rejection from three
583
+ // edits ago would still be sitting there while the reader tries again.
584
+ function openEditing(id, key) {
585
+ setEditError(null);
586
+ setEditing({ id, key });
587
+ }
588
+ // Async-aware (#385): a sync `onEdit` (returns undefined) commits exactly
589
+ // as before -- close the editor immediately. A Promise-returning `onEdit`
590
+ // keeps the editor open with a pending state while it settles, then either
591
+ // closes it (resolve) or closes it AND reverts to the last-known-good
592
+ // value while showing the rejection's message beside the cell (reject).
593
+ // The revert needs no extra code: this component never mutates `data`
594
+ // itself, so once `editing` clears, the cell simply shows `data` again.
452
595
  function commitEdit(id, key, value) {
453
- onEdit?.(id, key, value);
596
+ setEditError(null);
597
+ const result = onEdit?.(id, key, value);
598
+ if (result && typeof result.then === "function") {
599
+ setPending({ id, key });
600
+ result.then(() => {
601
+ setPending(null);
602
+ setEditing(null);
603
+ }, (err) => {
604
+ setPending(null);
605
+ setEditing(null);
606
+ setEditError({
607
+ id,
608
+ key,
609
+ message: err instanceof Error
610
+ ? err.message
611
+ : typeof err === "string"
612
+ ? err
613
+ : "Could not save",
614
+ });
615
+ });
616
+ return;
617
+ }
454
618
  setEditing(null);
455
619
  }
456
620
  const totalCols = columns.length + (selectable ? 1 : 0) + (showGrip ? 1 : 0);
@@ -584,7 +748,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
584
748
  ? "ascending"
585
749
  : "descending"
586
750
  : "none";
587
- const inner = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-th-inner", children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 14, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-th-label", children: col.header }), col.sortable ? (0, jsx_runtime_1.jsx)(ChevronFor, { colKey: col.key }) : null] }));
751
+ const inner = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-th-inner", children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-th-label", children: col.header }), col.sortable ? (0, jsx_runtime_1.jsx)(ChevronFor, { colKey: col.key }) : null] }));
588
752
  return ((0, jsx_runtime_1.jsxs)(table_1.TableHead, { align: alignFor(col), "aria-sort": ariaSort, ...dataStickyCol(i), children: [renaming === col.key ? ((0, jsx_runtime_1.jsx)("input", {
589
753
  // biome-ignore lint/a11y/noAutofocus: the rename item opens this field
590
754
  autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": `Rename column`, onBlur: (e) => {
@@ -626,38 +790,55 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
626
790
  const isEditing = editing?.id === row.id && editing?.key === col.key;
627
791
  const value = cellValue(col, row);
628
792
  const sticky = dataStickyCol(i);
793
+ const isPending = pending?.id === row.id && pending?.key === col.key;
794
+ const errorHere = editError?.id === row.id && editError?.key === col.key
795
+ ? editError.message
796
+ : null;
797
+ // A select/switch editable column is ALWAYS its own
798
+ // live widget -- no double-click gate, no separate
799
+ // edit/view mode, the same convention RecordsTable's
800
+ // status/reference pickers already use: choosing a
801
+ // value or flipping a toggle IS the commit gesture
802
+ // (#385).
803
+ if (col.editable && col.editor) {
804
+ return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [col.editor === "select" ? ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", "aria-label": `Edit ${col.key}`, value: asText(value) || undefined, disabled: isPending, options: col.options ?? [], onValueChange: (v) => commitEdit(row.id, col.key, v) })) : col.editor === "reference" ? ((0, jsx_runtime_1.jsx)(DataReferenceEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : ((0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) })), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
805
+ }
629
806
  if (isEditing) {
630
- return ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), ...sticky, onClick: (e) => e.stopPropagation(), children: (0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit",
631
- // biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
632
- autoFocus: true, ...editorFor(col), "data-editor": editorFor(col).type, defaultValue: col.type === "date"
633
- ? toDateInputValue(value)
634
- : asText(value), "aria-label": `Edit ${col.key}`, onKeyDown: (e) => {
635
- if (e.key === "Enter")
807
+ return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), ...sticky, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit",
808
+ // biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
809
+ autoFocus: true, ...editorFor(col), "data-editor": editorFor(col).type, disabled: isPending, defaultValue: col.type === "date"
810
+ ? toDateInputValue(value)
811
+ : col.type === "datetime"
812
+ ? toDateTimeInputValue(value)
813
+ : asText(value), "aria-label": `Edit ${col.key}`, onKeyDown: (e) => {
814
+ if (isPending)
815
+ return;
816
+ if (e.key === "Enter")
817
+ commitEdit(row.id, col.key, e.currentTarget.value);
818
+ else if (e.key === "Escape")
819
+ setEditing(null);
820
+ }, onBlur: (e) => {
821
+ if (isPending)
822
+ return;
636
823
  commitEdit(row.id, col.key, e.currentTarget.value);
637
- else if (e.key === "Escape")
638
- setEditing(null);
639
- }, onBlur: (e) => commitEdit(row.id, col.key, e.currentTarget.value) }) }, col.key));
824
+ } }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }, col.key));
640
825
  }
641
- return ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), ...sticky, className: col.editable
826
+ return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), ...sticky, className: col.editable
642
827
  ? "crisp-datatable-editable"
643
828
  : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
644
829
  ? ""
645
- : undefined, onClick: (e) => {
646
- // the keyboard model's anchor; also opens the
647
- // editor when the column allows it
830
+ : undefined, onClick: () => {
831
+ // the keyboard model's anchor -- a single click
832
+ // only selects. A genuine double-click (or Enter
833
+ // on the selected cell) opens the editor (#411),
834
+ // matching RecordsTable's own onDoubleClick.
648
835
  setSelCell({ id: row.id, key: col.key });
649
836
  gridRef.current?.focus({ preventScroll: true });
650
- if (col.editable) {
651
- // Opening the inline editor must not also fire
652
- // the row's onRowClick and navigate away. Every
653
- // other cell click has to keep bubbling, or
654
- // onRowClick never fires at all.
655
- e.stopPropagation();
656
- setEditing({ id: row.id, key: col.key });
657
- }
658
- }, children: col.render
659
- ? col.render(value, row)
660
- : renderTyped(col, value, avatarTone, col.format?.(value, row), locale) }, col.key));
837
+ }, onDoubleClick: col.editable
838
+ ? () => openEditing(row.id, col.key)
839
+ : undefined, children: [col.render
840
+ ? col.render(value, row)
841
+ : renderTyped(col, value, avatarTone, col.format?.(value, row), locale), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
661
842
  })] }, row.id)))] }, group ?? "__ungrouped")))) }), (hasSummary || onAddRow) && ((0, jsx_runtime_1.jsxs)(table_1.TableFooter, { children: [hasSummary && ((0, jsx_runtime_1.jsxs)(table_1.TableRow, { children: [showGrip && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-2) }), selectable && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-1) }), columns.map((col, i) => ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), ...dataStickyCol(i), children: col.summary ? col.summary(rows) : null }, col.key)))] })), onAddRow && ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-addcell", colSpan: totalCols, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-addrow", onClick: onAddRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
662
843
  }
663
844
  DataTable.displayName = "DataTable";