@crisp-ui-kit/crisp 0.52.0 → 0.52.1

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.
@@ -254,12 +254,61 @@ const CREATE_REFERENCE_VALUE = "__crisp_create_reference__";
254
254
  function dataReferenceOptionLabel(o) {
255
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
256
  }
257
+ /** `Select` owns its own closed-by-default `open` state with no prop or
258
+ * imperative way to start it open (see select.tsx) -- the only thing that
259
+ * opens it is a real click on its trigger button. Attio's own picker opens
260
+ * the instant a `select`/`reference` cell enters edit mode (measured live
261
+ * against a GRC test company, 2026-09-24), not after a second click on an
262
+ * already-mounted, still-closed trigger, so drive that click ourselves.
263
+ * Guarded against firing twice (StrictMode double-invokes effects) --
264
+ * `Select`'s own trigger toggles, so a second click would close what the
265
+ * first one opened. */
266
+ function useAutoOpenSelect() {
267
+ const ref = React.useRef(null);
268
+ const opened = React.useRef(false);
269
+ React.useEffect(() => {
270
+ if (opened.current)
271
+ return;
272
+ opened.current = true;
273
+ ref.current
274
+ ?.querySelector(".crisp-select-trigger")
275
+ ?.click();
276
+ }, []);
277
+ return ref;
278
+ }
279
+ /** `select`/`reference` open crisp's own `Select`, which closes its own
280
+ * popover on Escape (see select.tsx's "document-level fallback for Escape")
281
+ * but has no callback telling the CELL to leave editing mode -- and, same
282
+ * problem that fallback exists for, DOM focus doesn't reliably end up
283
+ * inside the newly-opened popover, so a `TableCell`-level `onKeyDown` can't
284
+ * be trusted to see the key at all (#453). Mirror Select's own fix for
285
+ * that: a raw `document` listener, not a focus-dependent React one. */
286
+ function useEscapeExitsEditing(onCancel) {
287
+ React.useEffect(() => {
288
+ const onKey = (e) => {
289
+ if (e.key === "Escape")
290
+ onCancel();
291
+ };
292
+ document.addEventListener("keydown", onKey);
293
+ return () => document.removeEventListener("keydown", onKey);
294
+ }, [onCancel]);
295
+ }
296
+ /** `editor: "select"`'s picker, its own component (not inline in the
297
+ * row-render loop, like `DataReferenceEditor` below) because it needs the
298
+ * open-on-mount and escape-exits-editing effects above (#453). */
299
+ function DataSelectEditor({ col, value, isPending, onCommit, onCancel, }) {
300
+ const ref = useAutoOpenSelect();
301
+ useEscapeExitsEditing(onCancel);
302
+ return ((0, jsx_runtime_1.jsx)("span", { ref: ref, style: { display: "contents" }, children: (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: onCommit }) }));
303
+ }
257
304
  /** `editor: "reference"`'s picker, its own component (not inline in the
258
305
  * row-render loop) because it needs its own `useState` for the search query
259
306
  * -- one instance per cell, same reasoning `select`/`switch` don't need a
260
307
  * component of their own but this does. */
261
- function DataReferenceEditor({ col, value, isPending, onCommit, }) {
308
+ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
262
309
  const [query, setQuery] = React.useState("");
310
+ const ref = useAutoOpenSelect();
311
+ useEscapeExitsEditing(onCancel);
263
312
  const options = (col.refOptions ?? []).map((o) => ({
264
313
  value: o.value,
265
314
  label: dataReferenceOptionLabel(o),
@@ -274,12 +323,12 @@ function DataReferenceEditor({ col, value, isPending, onCommit, }) {
274
323
  keywords: query,
275
324
  });
276
325
  }
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
- } }));
326
+ return ((0, jsx_runtime_1.jsx)("span", { ref: ref, style: { display: "contents" }, children: (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) => {
327
+ if (v === CREATE_REFERENCE_VALUE)
328
+ col.onCreateReference?.(query);
329
+ else
330
+ onCommit(v);
331
+ } }) }));
283
332
  }
284
333
  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
285
334
  ? `${total} ${total === 1 ? "row" : "rows"}`
@@ -502,10 +551,11 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
502
551
  * throughout, and the browser's own picker already
503
552
  * carries both a calendar and a time control.
504
553
  * text/entity a plain text field
505
- * `status`, `reference` and `tags` are pickers in RecordsTable, not
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.
554
+ * `editor: "select"`/`"reference"` open their own picker instead of this
555
+ * native-input family, but through the SAME double-click gate (#453) --
556
+ * see the row-render loop's `isEditing` branch for those, this function
557
+ * only covers the native-input-backed types. `editor: "switch"` is the
558
+ * one editor with no double-click gate at all (always live).
509
559
  */
510
560
  function editorFor(col) {
511
561
  switch (col.type) {
@@ -559,11 +609,12 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
559
609
  return;
560
610
  }
561
611
  else if (e.key === "Enter") {
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) {
612
+ // #453: `switch` has no separate editing mode to enter -- it's
613
+ // already its own live widget (#385); Enter there is a no-op, same
614
+ // as any other non-editable cell that isn't wired to onRowClick.
615
+ // `select`/`reference` DO have one now (like every other editor),
616
+ // so Enter opens them too.
617
+ if (columns[c]?.editable && columns[c]?.editor !== "switch") {
567
618
  e.preventDefault();
568
619
  openEditing(selCell.id, selCell.key);
569
620
  }
@@ -794,14 +845,37 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
794
845
  const errorHere = editError?.id === row.id && editError?.key === col.key
795
846
  ? editError.message
796
847
  : 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));
848
+ // #453: a `switch` editable column IS always its own
849
+ // live widget -- flipping the toggle IS the commit
850
+ // gesture, there's no meaningfully different "rest"
851
+ // display for a boolean. `select`/`reference` are
852
+ // NOT this: at rest they must show the column's own
853
+ // render/format like every other editable type
854
+ // (#412 empty handling included), and only open on
855
+ // double-click (#411) -- this branch previously
856
+ // rendered them unconditionally, replacing every
857
+ // row's own render with a picker trigger, which is
858
+ // the regression #453 reported (also #432/#449,
859
+ // which never had a "rest" state of their own to get
860
+ // right). See the `isEditing` block below for their
861
+ // actual editor.
862
+ if (col.editable && col.editor === "switch") {
863
+ 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: [(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));
864
+ }
865
+ if (isEditing &&
866
+ (col.editor === "select" || col.editor === "reference")) {
867
+ return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined,
868
+ // Same as the plain text editor's own cell:
869
+ // opening/using the picker must not also fire
870
+ // the row's onRowClick. Escape is handled by
871
+ // `DataSelectEditor`/`DataReferenceEditor`
872
+ // themselves (a document-level listener, not a
873
+ // `TableCell`-level `onKeyDown`) -- opening the
874
+ // picker moves DOM focus into a floating,
875
+ // portaled popover, so a handler here can't
876
+ // rely on the key event ever bubbling through
877
+ // this element (#453).
878
+ onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [col.editor === "select" ? ((0, jsx_runtime_1.jsx)(DataSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })) : ((0, jsx_runtime_1.jsx)(DataReferenceEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })), 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
879
  }
806
880
  if (isEditing) {
807
881
  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",
@@ -218,12 +218,61 @@ const CREATE_REFERENCE_VALUE = "__crisp_create_reference__";
218
218
  function dataReferenceOptionLabel(o) {
219
219
  return (_jsxs("span", { className: "crisp-datatable-refoption", children: [_jsx(Avatar, { size: "sm", className: "crisp-datatable-refoption-avatar", children: o.avatar ? (_jsx("img", { src: o.avatar, alt: "" })) : (_jsx(CircleUserRound, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" })) }), o.label] }));
220
220
  }
221
+ /** `Select` owns its own closed-by-default `open` state with no prop or
222
+ * imperative way to start it open (see select.tsx) -- the only thing that
223
+ * opens it is a real click on its trigger button. Attio's own picker opens
224
+ * the instant a `select`/`reference` cell enters edit mode (measured live
225
+ * against a GRC test company, 2026-09-24), not after a second click on an
226
+ * already-mounted, still-closed trigger, so drive that click ourselves.
227
+ * Guarded against firing twice (StrictMode double-invokes effects) --
228
+ * `Select`'s own trigger toggles, so a second click would close what the
229
+ * first one opened. */
230
+ function useAutoOpenSelect() {
231
+ const ref = React.useRef(null);
232
+ const opened = React.useRef(false);
233
+ React.useEffect(() => {
234
+ if (opened.current)
235
+ return;
236
+ opened.current = true;
237
+ ref.current
238
+ ?.querySelector(".crisp-select-trigger")
239
+ ?.click();
240
+ }, []);
241
+ return ref;
242
+ }
243
+ /** `select`/`reference` open crisp's own `Select`, which closes its own
244
+ * popover on Escape (see select.tsx's "document-level fallback for Escape")
245
+ * but has no callback telling the CELL to leave editing mode -- and, same
246
+ * problem that fallback exists for, DOM focus doesn't reliably end up
247
+ * inside the newly-opened popover, so a `TableCell`-level `onKeyDown` can't
248
+ * be trusted to see the key at all (#453). Mirror Select's own fix for
249
+ * that: a raw `document` listener, not a focus-dependent React one. */
250
+ function useEscapeExitsEditing(onCancel) {
251
+ React.useEffect(() => {
252
+ const onKey = (e) => {
253
+ if (e.key === "Escape")
254
+ onCancel();
255
+ };
256
+ document.addEventListener("keydown", onKey);
257
+ return () => document.removeEventListener("keydown", onKey);
258
+ }, [onCancel]);
259
+ }
260
+ /** `editor: "select"`'s picker, its own component (not inline in the
261
+ * row-render loop, like `DataReferenceEditor` below) because it needs the
262
+ * open-on-mount and escape-exits-editing effects above (#453). */
263
+ function DataSelectEditor({ col, value, isPending, onCommit, onCancel, }) {
264
+ const ref = useAutoOpenSelect();
265
+ useEscapeExitsEditing(onCancel);
266
+ return (_jsx("span", { ref: ref, style: { display: "contents" }, children: _jsx(Select, { variant: "ghost", "aria-label": `Edit ${col.key}`, value: asText(value) || undefined, disabled: isPending, options: col.options ?? [], onValueChange: onCommit }) }));
267
+ }
221
268
  /** `editor: "reference"`'s picker, its own component (not inline in the
222
269
  * row-render loop) because it needs its own `useState` for the search query
223
270
  * -- one instance per cell, same reasoning `select`/`switch` don't need a
224
271
  * component of their own but this does. */
225
- function DataReferenceEditor({ col, value, isPending, onCommit, }) {
272
+ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
226
273
  const [query, setQuery] = React.useState("");
274
+ const ref = useAutoOpenSelect();
275
+ useEscapeExitsEditing(onCancel);
227
276
  const options = (col.refOptions ?? []).map((o) => ({
228
277
  value: o.value,
229
278
  label: dataReferenceOptionLabel(o),
@@ -238,12 +287,12 @@ function DataReferenceEditor({ col, value, isPending, onCommit, }) {
238
287
  keywords: query,
239
288
  });
240
289
  }
241
- return (_jsx(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) => {
242
- if (v === CREATE_REFERENCE_VALUE)
243
- col.onCreateReference?.(query);
244
- else
245
- onCommit(v);
246
- } }));
290
+ return (_jsx("span", { ref: ref, style: { display: "contents" }, children: _jsx(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) => {
291
+ if (v === CREATE_REFERENCE_VALUE)
292
+ col.onCreateReference?.(query);
293
+ else
294
+ onCommit(v);
295
+ } }) }));
247
296
  }
248
297
  export 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
249
298
  ? `${total} ${total === 1 ? "row" : "rows"}`
@@ -466,10 +515,11 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
466
515
  * throughout, and the browser's own picker already
467
516
  * carries both a calendar and a time control.
468
517
  * text/entity a plain text field
469
- * `status`, `reference` and `tags` are pickers in RecordsTable, not
470
- * double-click editors, so they are not editable here either -- except
471
- * `editor: "reference"` (#432), which is a picker for the same reason
472
- * `select`/`switch` are: always live, no double-click gate.
518
+ * `editor: "select"`/`"reference"` open their own picker instead of this
519
+ * native-input family, but through the SAME double-click gate (#453) --
520
+ * see the row-render loop's `isEditing` branch for those, this function
521
+ * only covers the native-input-backed types. `editor: "switch"` is the
522
+ * one editor with no double-click gate at all (always live).
473
523
  */
474
524
  function editorFor(col) {
475
525
  switch (col.type) {
@@ -523,11 +573,12 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
523
573
  return;
524
574
  }
525
575
  else if (e.key === "Enter") {
526
- // select/switch columns have no separate editing mode to enter --
527
- // they're already their own live widget (#385); Enter there is a
528
- // no-op here, same as any other non-editable cell that isn't wired to
529
- // onRowClick.
530
- if (columns[c]?.editable && !columns[c]?.editor) {
576
+ // #453: `switch` has no separate editing mode to enter -- it's
577
+ // already its own live widget (#385); Enter there is a no-op, same
578
+ // as any other non-editable cell that isn't wired to onRowClick.
579
+ // `select`/`reference` DO have one now (like every other editor),
580
+ // so Enter opens them too.
581
+ if (columns[c]?.editable && columns[c]?.editor !== "switch") {
531
582
  e.preventDefault();
532
583
  openEditing(selCell.id, selCell.key);
533
584
  }
@@ -758,14 +809,37 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
758
809
  const errorHere = editError?.id === row.id && editError?.key === col.key
759
810
  ? editError.message
760
811
  : null;
761
- // A select/switch editable column is ALWAYS its own
762
- // live widget -- no double-click gate, no separate
763
- // edit/view mode, the same convention RecordsTable's
764
- // status/reference pickers already use: choosing a
765
- // value or flipping a toggle IS the commit gesture
766
- // (#385).
767
- if (col.editable && col.editor) {
768
- return (_jsxs(TableCell, { align: alignFor(col), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [col.editor === "select" ? (_jsx(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" ? (_jsx(DataReferenceEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : (_jsx(Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) })), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
812
+ // #453: a `switch` editable column IS always its own
813
+ // live widget -- flipping the toggle IS the commit
814
+ // gesture, there's no meaningfully different "rest"
815
+ // display for a boolean. `select`/`reference` are
816
+ // NOT this: at rest they must show the column's own
817
+ // render/format like every other editable type
818
+ // (#412 empty handling included), and only open on
819
+ // double-click (#411) -- this branch previously
820
+ // rendered them unconditionally, replacing every
821
+ // row's own render with a picker trigger, which is
822
+ // the regression #453 reported (also #432/#449,
823
+ // which never had a "rest" state of their own to get
824
+ // right). See the `isEditing` block below for their
825
+ // actual editor.
826
+ if (col.editable && col.editor === "switch") {
827
+ return (_jsxs(TableCell, { align: alignFor(col), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_jsx(Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
828
+ }
829
+ if (isEditing &&
830
+ (col.editor === "select" || col.editor === "reference")) {
831
+ return (_jsxs(TableCell, { align: alignFor(col), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined,
832
+ // Same as the plain text editor's own cell:
833
+ // opening/using the picker must not also fire
834
+ // the row's onRowClick. Escape is handled by
835
+ // `DataSelectEditor`/`DataReferenceEditor`
836
+ // themselves (a document-level listener, not a
837
+ // `TableCell`-level `onKeyDown`) -- opening the
838
+ // picker moves DOM focus into a floating,
839
+ // portaled popover, so a handler here can't
840
+ // rely on the key event ever bubbling through
841
+ // this element (#453).
842
+ onClick: (e) => e.stopPropagation(), children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [col.editor === "select" ? (_jsx(DataSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })) : (_jsx(DataReferenceEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
769
843
  }
770
844
  if (isEditing) {
771
845
  return (_jsxs(TableCell, { align: alignFor(col), ...sticky, onClick: (e) => e.stopPropagation(), children: [_jsx("input", { className: "crisp-datatable-edit",
@@ -45,16 +45,17 @@ export type DataTableColumn<T> = {
45
45
  * commits) — see `editor` for `select`/`switch`/`reference`. */
46
46
  editable?: boolean;
47
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`. */
48
+ * inferred from `type` (number/date/datetime/text). `"select"`/
49
+ * `"reference"` render crisp's own `Select` the same way — at rest the
50
+ * cell shows the column's `render`/formatted value like every other
51
+ * editable type (#453), and double-click (or Enter on the selected
52
+ * cell) opens the picker; choosing a value commits and closes it,
53
+ * Escape cancels. `select` reads `options`; `reference` reads
54
+ * `refOptions` and is search-enabled, the same searchable-`Select`
55
+ * shape as `RecordsTable`'s own `ReferenceCell`. `"switch"` is the one
56
+ * exception — ALWAYS its own live widget, no double-click gate, since
57
+ * flipping the toggle IS the commit gesture and there's no meaningfully
58
+ * different "rest" display for a boolean. */
58
59
  editor?: "select" | "switch" | "reference";
59
60
  /** Options for `editor: "select"` (value/label, an optional `color` a
60
61
  * caller's own `render` can read) — same shape as RecordsTable's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.52.0",
3
+ "version": "0.52.1",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",