@crisp-ui-kit/crisp 0.52.0 → 0.52.2

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",
@@ -125,6 +125,53 @@ exports.recordPageLabels = {
125
125
  recordCreated: (name) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("b", { children: name }), " record was created"] })),
126
126
  noEmailsWith: (name) => (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["No emails logged with ", name, " yet."] }),
127
127
  };
128
+ /** `Select` owns its own closed-by-default `open` state with no prop or
129
+ * imperative way to start it open -- the only thing that opens it is a real
130
+ * click on its trigger button. Mounting one on click (rather than rendering
131
+ * it already open) leaves the reader with a second click to make before
132
+ * they can pick anything, so drive that click ourselves the moment it
133
+ * mounts. Mirrors `DataTable`'s own `useAutoOpenSelect` (#453) -- kept as
134
+ * its own copy here rather than a shared import, the same "duplicated per
135
+ * product surface" choice already made for `EditableOption`/
136
+ * `editableInputType` above. Guarded against firing twice (StrictMode
137
+ * double-invokes effects): `Select`'s own trigger toggles, so a second
138
+ * click would close what the first one opened. */
139
+ function useAutoOpenSelect() {
140
+ const ref = React.useRef(null);
141
+ const opened = React.useRef(false);
142
+ React.useEffect(() => {
143
+ if (opened.current)
144
+ return;
145
+ opened.current = true;
146
+ ref.current
147
+ ?.querySelector(".crisp-select-trigger")
148
+ ?.click();
149
+ }, []);
150
+ return ref;
151
+ }
152
+ /** `Select` closes its own popover on Escape but has no callback telling
153
+ * the FIELD to leave editing mode -- and DOM focus doesn't reliably end up
154
+ * inside the newly-opened popover either, so a focus-dependent React
155
+ * `onKeyDown` can't be trusted to see the key at all. Mirrors `Select`'s
156
+ * own "document-level fallback for Escape" (see select.tsx) and
157
+ * `DataTable`'s identical fix for the same problem (#453). */
158
+ function useEscapeExitsEditing(onCancel) {
159
+ React.useEffect(() => {
160
+ const onKey = (e) => {
161
+ if (e.key === "Escape")
162
+ onCancel();
163
+ };
164
+ document.addEventListener("keydown", onKey);
165
+ return () => document.removeEventListener("keydown", onKey);
166
+ }, [onCancel]);
167
+ }
168
+ /** `editor: "select"`'s picker, its own component (not inline in the field
169
+ * loop) because it needs the two hooks above (#457). */
170
+ function RecordPageSelectEditor({ col, value, isPending, onCommit, onCancel, }) {
171
+ const ref = useAutoOpenSelect();
172
+ useEscapeExitsEditing(onCancel);
173
+ return ((0, jsx_runtime_1.jsxs)("span", { ref: ref, className: "crisp-recordpage-edit-picker", children: [(0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", "aria-label": `Edit ${col.label}`, value: value != null ? String(value) : undefined, disabled: isPending, options: col.editOptions ?? [], onValueChange: onCommit }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }));
174
+ }
128
175
  function TagsValue({ values }) {
129
176
  const [expanded, setExpanded] = React.useState(false);
130
177
  const overflow = values.length - 1;
@@ -135,6 +182,21 @@ function FieldValue({ col, row, labels, }) {
135
182
  const v = row[col.key];
136
183
  if (valueEmpty(v))
137
184
  return (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-empty", children: labels.setAValue });
185
+ // `editor: "select"` at rest (#457) -- checked ahead of `col.type` since
186
+ // the editor is orthogonal to it. Same dot+label shape as RecordsTable's
187
+ // own measured status chip, but this component's own classes
188
+ // (`crisp-recordpage-status[-dot]`): RecordsTable's own pair is scoped
189
+ // under `.crisp-recordstable`'s recipe root, so reusing its classnames
190
+ // verbatim here would resolve to nothing. Shown when the matched option
191
+ // carries a `color`, same as every other status-shaped value in this
192
+ // codebase; a colourless option (or no match) falls back to plain text.
193
+ if (col.editor === "select") {
194
+ const opt = col.editOptions?.find((o) => o.value === v);
195
+ const text = opt?.label ?? String(v);
196
+ if (!opt?.color)
197
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: text });
198
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-status", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-status-dot", style: { background: opt.color } }), text] }));
199
+ }
138
200
  switch (col.type) {
139
201
  case "tags":
140
202
  return (0, jsx_runtime_1.jsx)(TagsValue, { values: v });
@@ -455,14 +517,31 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
455
517
  const isPending = pendingKey === col.key;
456
518
  const fieldError = error?.key === col.key ? error.message : null;
457
519
  const label = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-label", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-field-label-text", children: col.label })] }));
458
- // select/switch are always their own live widget -- no
459
- // click-to-open, no separate edit/view mode, the same
460
- // convention DataTable's identical `editor` columns use
461
- // (#385): choosing a value or flipping a toggle IS the
462
- // commit gesture.
463
- if (canEdit && col.editor) {
520
+ // `switch` is always its own live widget -- no
521
+ // click-to-open, no separate edit/view mode: flipping the
522
+ // toggle IS the commit gesture, and there's no
523
+ // meaningfully different "rest" display for a boolean
524
+ // (same exception `DataTable`'s own `editor` makes, #453).
525
+ if (canEdit && col.editor === "switch") {
526
+ const value = record[col.key];
527
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-edit-picker", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
528
+ }
529
+ // `select` opens crisp's own `Select` on a SINGLE click
530
+ // (#457, corrects #431/#385's original always-live
531
+ // copy of the same regression #453 fixed on `DataTable`)
532
+ // -- like every other editable type here, at rest it
533
+ // shows its value (see `FieldValue`'s own `col.editor
534
+ // === "select"` case) via the generic click-to-open
535
+ // branch below, and only mounts `Select` once
536
+ // `editingKey` matches. `DataTable`'s own double-click
537
+ // gate doesn't apply: `RecordPage`'s plain text/date
538
+ // fields already open on a single click (measured live
539
+ // for #431), so `select` matches that, not `DataTable`.
540
+ if (canEdit &&
541
+ editingKey === col.key &&
542
+ col.editor === "select") {
464
543
  const value = record[col.key];
465
- return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-edit-picker", children: [col.editor === "select" ? ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", "aria-label": `Edit ${col.label}`, value: value != null ? String(value) : undefined, disabled: isPending, options: col.editOptions ?? [], onValueChange: (v) => commit(record.id, col.key, v) })) : ((0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) })), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
544
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsx)(RecordPageSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commit(record.id, col.key, v), onCancel: closeEditing }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
466
545
  }
467
546
  if (canEdit && editingKey === col.key) {
468
547
  const value = record[col.key];
@@ -108,6 +108,13 @@ const config = (0, core_1.defineRecipe)({
108
108
  & .crisp-recordpage-edit:disabled { opacity: 0.5; cursor: not-allowed; }
109
109
  & .crisp-recordpage-edit-pending { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
110
110
  & .crisp-recordpage-edit-error { flex-basis: 100%; margin-top: ${tokens_1.contract.space["0.5"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.bgDanger}; }
111
+ /* #457: an \`editor: "select"\` field's at-rest chip -- same shape as
112
+ RecordsTable's own \`.crisp-recordstable-status[-dot]\` (already
113
+ measured against Attio there), but its own classes: that pair is
114
+ scoped under \`.crisp-recordstable\`'s own recipe root, so reusing the
115
+ classnames verbatim here would resolve to nothing. */
116
+ & .crisp-recordpage-status { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; }
117
+ & .crisp-recordpage-status-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${tokens_1.contract.radius.full}; }
111
118
  & .crisp-recordpage-edit-picker { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; width: 100%; }
112
119
  & .crisp-recordpage-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; }
113
120
  & .crisp-recordpage-link { color: ${tokens_1.contract.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
@@ -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",
@@ -88,6 +88,53 @@ export const recordPageLabels = {
88
88
  recordCreated: (name) => (_jsxs(_Fragment, { children: [_jsx("b", { children: name }), " record was created"] })),
89
89
  noEmailsWith: (name) => _jsxs(_Fragment, { children: ["No emails logged with ", name, " yet."] }),
90
90
  };
91
+ /** `Select` owns its own closed-by-default `open` state with no prop or
92
+ * imperative way to start it open -- the only thing that opens it is a real
93
+ * click on its trigger button. Mounting one on click (rather than rendering
94
+ * it already open) leaves the reader with a second click to make before
95
+ * they can pick anything, so drive that click ourselves the moment it
96
+ * mounts. Mirrors `DataTable`'s own `useAutoOpenSelect` (#453) -- kept as
97
+ * its own copy here rather than a shared import, the same "duplicated per
98
+ * product surface" choice already made for `EditableOption`/
99
+ * `editableInputType` above. Guarded against firing twice (StrictMode
100
+ * double-invokes effects): `Select`'s own trigger toggles, so a second
101
+ * click would close what the first one opened. */
102
+ function useAutoOpenSelect() {
103
+ const ref = React.useRef(null);
104
+ const opened = React.useRef(false);
105
+ React.useEffect(() => {
106
+ if (opened.current)
107
+ return;
108
+ opened.current = true;
109
+ ref.current
110
+ ?.querySelector(".crisp-select-trigger")
111
+ ?.click();
112
+ }, []);
113
+ return ref;
114
+ }
115
+ /** `Select` closes its own popover on Escape but has no callback telling
116
+ * the FIELD to leave editing mode -- and DOM focus doesn't reliably end up
117
+ * inside the newly-opened popover either, so a focus-dependent React
118
+ * `onKeyDown` can't be trusted to see the key at all. Mirrors `Select`'s
119
+ * own "document-level fallback for Escape" (see select.tsx) and
120
+ * `DataTable`'s identical fix for the same problem (#453). */
121
+ function useEscapeExitsEditing(onCancel) {
122
+ React.useEffect(() => {
123
+ const onKey = (e) => {
124
+ if (e.key === "Escape")
125
+ onCancel();
126
+ };
127
+ document.addEventListener("keydown", onKey);
128
+ return () => document.removeEventListener("keydown", onKey);
129
+ }, [onCancel]);
130
+ }
131
+ /** `editor: "select"`'s picker, its own component (not inline in the field
132
+ * loop) because it needs the two hooks above (#457). */
133
+ function RecordPageSelectEditor({ col, value, isPending, onCommit, onCancel, }) {
134
+ const ref = useAutoOpenSelect();
135
+ useEscapeExitsEditing(onCancel);
136
+ return (_jsxs("span", { ref: ref, className: "crisp-recordpage-edit-picker", children: [_jsx(Select, { variant: "ghost", "aria-label": `Edit ${col.label}`, value: value != null ? String(value) : undefined, disabled: isPending, options: col.editOptions ?? [], onValueChange: onCommit }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }));
137
+ }
91
138
  function TagsValue({ values }) {
92
139
  const [expanded, setExpanded] = React.useState(false);
93
140
  const overflow = values.length - 1;
@@ -98,6 +145,21 @@ function FieldValue({ col, row, labels, }) {
98
145
  const v = row[col.key];
99
146
  if (valueEmpty(v))
100
147
  return _jsx("span", { className: "crisp-recordpage-empty", children: labels.setAValue });
148
+ // `editor: "select"` at rest (#457) -- checked ahead of `col.type` since
149
+ // the editor is orthogonal to it. Same dot+label shape as RecordsTable's
150
+ // own measured status chip, but this component's own classes
151
+ // (`crisp-recordpage-status[-dot]`): RecordsTable's own pair is scoped
152
+ // under `.crisp-recordstable`'s recipe root, so reusing its classnames
153
+ // verbatim here would resolve to nothing. Shown when the matched option
154
+ // carries a `color`, same as every other status-shaped value in this
155
+ // codebase; a colourless option (or no match) falls back to plain text.
156
+ if (col.editor === "select") {
157
+ const opt = col.editOptions?.find((o) => o.value === v);
158
+ const text = opt?.label ?? String(v);
159
+ if (!opt?.color)
160
+ return _jsx(_Fragment, { children: text });
161
+ return (_jsxs("span", { className: "crisp-recordpage-status", children: [_jsx("span", { className: "crisp-recordpage-status-dot", style: { background: opt.color } }), text] }));
162
+ }
101
163
  switch (col.type) {
102
164
  case "tags":
103
165
  return _jsx(TagsValue, { values: v });
@@ -418,14 +480,31 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
418
480
  const isPending = pendingKey === col.key;
419
481
  const fieldError = error?.key === col.key ? error.message : null;
420
482
  const label = (_jsxs("span", { className: "crisp-recordpage-field-label", children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : null, _jsx("span", { className: "crisp-recordpage-field-label-text", children: col.label })] }));
421
- // select/switch are always their own live widget -- no
422
- // click-to-open, no separate edit/view mode, the same
423
- // convention DataTable's identical `editor` columns use
424
- // (#385): choosing a value or flipping a toggle IS the
425
- // commit gesture.
426
- if (canEdit && col.editor) {
483
+ // `switch` is always its own live widget -- no
484
+ // click-to-open, no separate edit/view mode: flipping the
485
+ // toggle IS the commit gesture, and there's no
486
+ // meaningfully different "rest" display for a boolean
487
+ // (same exception `DataTable`'s own `editor` makes, #453).
488
+ if (canEdit && col.editor === "switch") {
489
+ const value = record[col.key];
490
+ return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", children: [_jsxs("span", { className: "crisp-recordpage-edit-picker", children: [_jsx(Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
491
+ }
492
+ // `select` opens crisp's own `Select` on a SINGLE click
493
+ // (#457, corrects #431/#385's original always-live
494
+ // copy of the same regression #453 fixed on `DataTable`)
495
+ // -- like every other editable type here, at rest it
496
+ // shows its value (see `FieldValue`'s own `col.editor
497
+ // === "select"` case) via the generic click-to-open
498
+ // branch below, and only mounts `Select` once
499
+ // `editingKey` matches. `DataTable`'s own double-click
500
+ // gate doesn't apply: `RecordPage`'s plain text/date
501
+ // fields already open on a single click (measured live
502
+ // for #431), so `select` matches that, not `DataTable`.
503
+ if (canEdit &&
504
+ editingKey === col.key &&
505
+ col.editor === "select") {
427
506
  const value = record[col.key];
428
- return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", children: [_jsxs("span", { className: "crisp-recordpage-edit-picker", children: [col.editor === "select" ? (_jsx(Select, { variant: "ghost", "aria-label": `Edit ${col.label}`, value: value != null ? String(value) : undefined, disabled: isPending, options: col.editOptions ?? [], onValueChange: (v) => commit(record.id, col.key, v) })) : (_jsx(Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) })), isPending && (_jsx(Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
507
+ return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", children: [_jsx(RecordPageSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commit(record.id, col.key, v), onCancel: closeEditing }), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
429
508
  }
430
509
  if (canEdit && editingKey === col.key) {
431
510
  const value = record[col.key];
@@ -105,6 +105,13 @@ const config = defineRecipe({
105
105
  & .crisp-recordpage-edit:disabled { opacity: 0.5; cursor: not-allowed; }
106
106
  & .crisp-recordpage-edit-pending { flex-shrink: 0; color: ${c.fgMuted}; }
107
107
  & .crisp-recordpage-edit-error { flex-basis: 100%; margin-top: ${c.space["0.5"]}; font-size: ${c.text.sm}; color: ${c.bgDanger}; }
108
+ /* #457: an \`editor: "select"\` field's at-rest chip -- same shape as
109
+ RecordsTable's own \`.crisp-recordstable-status[-dot]\` (already
110
+ measured against Attio there), but its own classes: that pair is
111
+ scoped under \`.crisp-recordstable\`'s own recipe root, so reusing the
112
+ classnames verbatim here would resolve to nothing. */
113
+ & .crisp-recordpage-status { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; min-width: 0; }
114
+ & .crisp-recordpage-status-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${c.radius.full}; }
108
115
  & .crisp-recordpage-edit-picker { display: flex; align-items: center; gap: ${c.space["1"]}; width: 100%; }
109
116
  & .crisp-recordpage-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; }
110
117
  & .crisp-recordpage-link { color: ${c.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
@@ -83,23 +83,33 @@
83
83
  & .crisp-recordpage-edit:disabled { opacity: 0.5; cursor: not-allowed; }
84
84
  & .crisp-recordpage-edit-pending { flex-shrink: 0; color: var(--crisp-fg-muted); }
85
85
  & .crisp-recordpage-edit-error { flex-basis: 100%; margin-top: var(--crisp-space-0-5); font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
86
+ /* #457: an `editor: "select"` field's at-rest chip -- same shape as
87
+ RecordsTable's own `.crisp-recordstable-status[-dot]` (already measured against Attio there), but its own classes: that pair is scoped under `.crisp-recordstable`'s own recipe root, so reusing the
88
+ classnames verbatim here would resolve to nothing. */
89
+ & .crisp-recordpage-status { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; }
90
+ & .crisp-recordpage-status-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: var(--crisp-radius-full); }
86
91
  & .crisp-recordpage-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; }
87
92
  & .crisp-recordpage-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; }
88
93
  & .crisp-recordpage-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
89
94
  & .crisp-recordpage-link:hover { text-decoration: underline; }
90
95
  & .crisp-recordpage-emptystate { padding: var(--crisp-space-2) var(--crisp-space-1); font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
96
+
91
97
  & .crisp-recordpage-main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: var(--crisp-space-3) var(--crisp-space-5); }
98
+
92
99
  & .crisp-recordpage-panel { padding-top: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-4); }
93
100
  & .crisp-recordpage-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); background: var(--crisp-bg-surface); overflow: hidden; }
94
101
  & .crisp-recordpage-card-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.14px; color: var(--crisp-fg-primary); padding: var(--crisp-space-3) var(--crisp-space-3) var(--crisp-space-2); }
95
102
  & .crisp-recordpage-hl-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); overflow: hidden; }
96
- /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any column count, so the grid stays responsive (auto-fit) without per-cell nth-child border hacks that break when the column count changes. */
103
+ /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any
104
+ column count, so the grid stays responsive (auto-fit) without per-cell
105
+ nth-child border hacks that break when the column count changes. */
97
106
  & .crisp-recordpage-hl-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1px; background: var(--crisp-border-subtle); }
98
107
  & .crisp-recordpage-hl-cell { position: relative; min-height: 84px; padding: var(--crisp-space-2-5) var(--crisp-space-3); background: var(--crisp-bg-surface); }
99
108
  & .crisp-recordpage-hl-icon { position: absolute; top: var(--crisp-space-2-5); inset-inline-end: var(--crisp-space-3); color: var(--crisp-fg-muted); }
100
109
  & .crisp-recordpage-hl-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); margin-bottom: var(--crisp-space-2-5); padding-inline-end: var(--crisp-space-4); }
101
110
  & .crisp-recordpage-hl-value { display: flex; flex-wrap: wrap; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
102
111
  & .crisp-recordpage-hl-value--empty { color: var(--crisp-fg-muted); font-weight: var(--crisp-weight-regular); }
112
+
103
113
  & .crisp-recordpage-feed { display: flex; flex-direction: column; }
104
114
  & .crisp-recordpage-feed-item { display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2) 0; }
105
115
  & .crisp-recordpage-feed-dot { position: relative; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
@@ -108,6 +118,7 @@
108
118
  & .crisp-recordpage-feed-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); margin-top: 2px; }
109
119
  & .crisp-recordpage-feed-more { align-self: flex-start; margin-top: var(--crisp-space-1); border: none; background: none; padding: var(--crisp-space-1) 0; color: var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
110
120
  & .crisp-recordpage-feed-more:hover { text-decoration: underline; }
121
+
111
122
  & .crisp-recordpage-section-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-1); }
112
123
  & .crisp-recordpage-section-head .crisp-recordpage-section-title { padding: 0; }
113
124
  & .crisp-recordpage-section-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
@@ -115,6 +126,7 @@
115
126
  & .crisp-recordpage-addbtn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: 26px; padding: 0 var(--crisp-space-1-5); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
116
127
  & .crisp-recordpage-addbtn:hover { background: var(--crisp-bg-raised); }
117
128
  & .crisp-recordpage-addbtn svg { flex-shrink: 0; }
129
+
118
130
  & .crisp-recordpage-composer { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding: var(--crisp-space-2); border: 1px solid var(--crisp-border-default); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); margin-bottom: var(--crisp-space-2); }
119
131
  & .crisp-recordpage-composer textarea { width: 100%; min-height: 56px; resize: vertical; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
120
132
  & .crisp-recordpage-composer textarea:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); border-radius: var(--crisp-radius-md); }
@@ -124,18 +136,23 @@
124
136
  & .crisp-recordpage-btn:hover { background: var(--crisp-bg-raised); }
125
137
  & .crisp-recordpage-btn--primary { box-shadow: none; background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); }
126
138
  & .crisp-recordpage-btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
139
+
127
140
  & .crisp-recordpage-note { display: flex; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
128
141
  & .crisp-recordpage-note-body { flex: 1; min-width: 0; }
129
142
  & .crisp-recordpage-note-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-bottom: var(--crisp-space-0-5); }
130
143
  & .crisp-recordpage-note-author { font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
131
144
  & .crisp-recordpage-note-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
132
145
  & .crisp-recordpage-note-text { font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); white-space: pre-wrap; word-break: break-word; }
146
+
133
147
  & .crisp-recordpage-task { display: flex; align-items: flex-start; gap: var(--crisp-space-2); padding: var(--crisp-space-1-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
134
148
  & .crisp-recordpage-task-text { flex: 1; min-width: 0; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); padding-top: 1px; }
135
149
  & .crisp-recordpage-task[data-done] .crisp-recordpage-task-text { text-decoration: line-through; color: var(--crisp-fg-muted); }
150
+
136
151
  & .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-6) var(--crisp-space-3); border: 1px dashed var(--crisp-border-default); border-radius: var(--crisp-radius-xl); color: var(--crisp-fg-muted); font-size: var(--crisp-text-md); text-align: center; }
137
152
  & .crisp-recordpage-emptycard svg { color: var(--crisp-fg-muted); opacity: 0.7; }
153
+
138
154
  & .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
155
+
139
156
  & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
140
157
  & .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
141
158
  & .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
@@ -84,23 +84,33 @@
84
84
  & .crisp-recordpage-edit:disabled { opacity: 0.5; cursor: not-allowed; }
85
85
  & .crisp-recordpage-edit-pending { flex-shrink: 0; color: var(--crisp-fg-muted); }
86
86
  & .crisp-recordpage-edit-error { flex-basis: 100%; margin-top: var(--crisp-space-0-5); font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
87
+ /* #457: an `editor: "select"` field's at-rest chip -- same shape as
88
+ RecordsTable's own `.crisp-recordstable-status[-dot]` (already measured against Attio there), but its own classes: that pair is scoped under `.crisp-recordstable`'s own recipe root, so reusing the
89
+ classnames verbatim here would resolve to nothing. */
90
+ & .crisp-recordpage-status { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; }
91
+ & .crisp-recordpage-status-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: var(--crisp-radius-full); }
87
92
  & .crisp-recordpage-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; }
88
93
  & .crisp-recordpage-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; }
89
94
  & .crisp-recordpage-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
90
95
  & .crisp-recordpage-link:hover { text-decoration: underline; }
91
96
  & .crisp-recordpage-emptystate { padding: var(--crisp-space-2) var(--crisp-space-1); font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
97
+
92
98
  & .crisp-recordpage-main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: var(--crisp-space-3) var(--crisp-space-5); }
99
+
93
100
  & .crisp-recordpage-panel { padding-top: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-4); }
94
101
  & .crisp-recordpage-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); background: var(--crisp-bg-surface); overflow: hidden; }
95
102
  & .crisp-recordpage-card-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.14px; color: var(--crisp-fg-primary); padding: var(--crisp-space-3) var(--crisp-space-3) var(--crisp-space-2); }
96
103
  & .crisp-recordpage-hl-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); overflow: hidden; }
97
- /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any column count, so the grid stays responsive (auto-fit) without per-cell nth-child border hacks that break when the column count changes. */
104
+ /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any
105
+ column count, so the grid stays responsive (auto-fit) without per-cell
106
+ nth-child border hacks that break when the column count changes. */
98
107
  & .crisp-recordpage-hl-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1px; background: var(--crisp-border-subtle); }
99
108
  & .crisp-recordpage-hl-cell { position: relative; min-height: 84px; padding: var(--crisp-space-2-5) var(--crisp-space-3); background: var(--crisp-bg-surface); }
100
109
  & .crisp-recordpage-hl-icon { position: absolute; top: var(--crisp-space-2-5); inset-inline-end: var(--crisp-space-3); color: var(--crisp-fg-muted); }
101
110
  & .crisp-recordpage-hl-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); margin-bottom: var(--crisp-space-2-5); padding-inline-end: var(--crisp-space-4); }
102
111
  & .crisp-recordpage-hl-value { display: flex; flex-wrap: wrap; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
103
112
  & .crisp-recordpage-hl-value--empty { color: var(--crisp-fg-muted); font-weight: var(--crisp-weight-regular); }
113
+
104
114
  & .crisp-recordpage-feed { display: flex; flex-direction: column; }
105
115
  & .crisp-recordpage-feed-item { display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2) 0; }
106
116
  & .crisp-recordpage-feed-dot { position: relative; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
@@ -109,6 +119,7 @@
109
119
  & .crisp-recordpage-feed-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); margin-top: 2px; }
110
120
  & .crisp-recordpage-feed-more { align-self: flex-start; margin-top: var(--crisp-space-1); border: none; background: none; padding: var(--crisp-space-1) 0; color: var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
111
121
  & .crisp-recordpage-feed-more:hover { text-decoration: underline; }
122
+
112
123
  & .crisp-recordpage-section-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-1); }
113
124
  & .crisp-recordpage-section-head .crisp-recordpage-section-title { padding: 0; }
114
125
  & .crisp-recordpage-section-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
@@ -116,6 +127,7 @@
116
127
  & .crisp-recordpage-addbtn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: 26px; padding: 0 var(--crisp-space-1-5); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
117
128
  & .crisp-recordpage-addbtn:hover { background: var(--crisp-bg-raised); }
118
129
  & .crisp-recordpage-addbtn svg { flex-shrink: 0; }
130
+
119
131
  & .crisp-recordpage-composer { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding: var(--crisp-space-2); border: 1px solid var(--crisp-border-default); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); margin-bottom: var(--crisp-space-2); }
120
132
  & .crisp-recordpage-composer textarea { width: 100%; min-height: 56px; resize: vertical; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
121
133
  & .crisp-recordpage-composer textarea:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); border-radius: var(--crisp-radius-md); }
@@ -125,18 +137,23 @@
125
137
  & .crisp-recordpage-btn:hover { background: var(--crisp-bg-raised); }
126
138
  & .crisp-recordpage-btn--primary { box-shadow: none; background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); }
127
139
  & .crisp-recordpage-btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
140
+
128
141
  & .crisp-recordpage-note { display: flex; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
129
142
  & .crisp-recordpage-note-body { flex: 1; min-width: 0; }
130
143
  & .crisp-recordpage-note-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-bottom: var(--crisp-space-0-5); }
131
144
  & .crisp-recordpage-note-author { font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
132
145
  & .crisp-recordpage-note-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
133
146
  & .crisp-recordpage-note-text { font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); white-space: pre-wrap; word-break: break-word; }
147
+
134
148
  & .crisp-recordpage-task { display: flex; align-items: flex-start; gap: var(--crisp-space-2); padding: var(--crisp-space-1-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
135
149
  & .crisp-recordpage-task-text { flex: 1; min-width: 0; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); padding-top: 1px; }
136
150
  & .crisp-recordpage-task[data-done] .crisp-recordpage-task-text { text-decoration: line-through; color: var(--crisp-fg-muted); }
151
+
137
152
  & .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-6) var(--crisp-space-3); border: 1px dashed var(--crisp-border-default); border-radius: var(--crisp-radius-xl); color: var(--crisp-fg-muted); font-size: var(--crisp-text-md); text-align: center; }
138
153
  & .crisp-recordpage-emptycard svg { color: var(--crisp-fg-muted); opacity: 0.7; }
154
+
139
155
  & .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
156
+
140
157
  & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
141
158
  & .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
142
159
  & .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
package/dist/styles.css CHANGED
@@ -2163,23 +2163,33 @@
2163
2163
  & .crisp-recordpage-edit:disabled { opacity: 0.5; cursor: not-allowed; }
2164
2164
  & .crisp-recordpage-edit-pending { flex-shrink: 0; color: var(--crisp-fg-muted); }
2165
2165
  & .crisp-recordpage-edit-error { flex-basis: 100%; margin-top: var(--crisp-space-0-5); font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
2166
+ /* #457: an `editor: "select"` field's at-rest chip -- same shape as
2167
+ RecordsTable's own `.crisp-recordstable-status[-dot]` (already measured against Attio there), but its own classes: that pair is scoped under `.crisp-recordstable`'s own recipe root, so reusing the
2168
+ classnames verbatim here would resolve to nothing. */
2169
+ & .crisp-recordpage-status { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; }
2170
+ & .crisp-recordpage-status-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: var(--crisp-radius-full); }
2166
2171
  & .crisp-recordpage-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; }
2167
2172
  & .crisp-recordpage-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; }
2168
2173
  & .crisp-recordpage-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
2169
2174
  & .crisp-recordpage-link:hover { text-decoration: underline; }
2170
2175
  & .crisp-recordpage-emptystate { padding: var(--crisp-space-2) var(--crisp-space-1); font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
2176
+
2171
2177
  & .crisp-recordpage-main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: var(--crisp-space-3) var(--crisp-space-5); }
2178
+
2172
2179
  & .crisp-recordpage-panel { padding-top: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-4); }
2173
2180
  & .crisp-recordpage-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); background: var(--crisp-bg-surface); overflow: hidden; }
2174
2181
  & .crisp-recordpage-card-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.14px; color: var(--crisp-fg-primary); padding: var(--crisp-space-3) var(--crisp-space-3) var(--crisp-space-2); }
2175
2182
  & .crisp-recordpage-hl-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); overflow: hidden; }
2176
- /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any column count, so the grid stays responsive (auto-fit) without per-cell nth-child border hacks that break when the column count changes. */
2183
+ /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any
2184
+ column count, so the grid stays responsive (auto-fit) without per-cell
2185
+ nth-child border hacks that break when the column count changes. */
2177
2186
  & .crisp-recordpage-hl-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1px; background: var(--crisp-border-subtle); }
2178
2187
  & .crisp-recordpage-hl-cell { position: relative; min-height: 84px; padding: var(--crisp-space-2-5) var(--crisp-space-3); background: var(--crisp-bg-surface); }
2179
2188
  & .crisp-recordpage-hl-icon { position: absolute; top: var(--crisp-space-2-5); inset-inline-end: var(--crisp-space-3); color: var(--crisp-fg-muted); }
2180
2189
  & .crisp-recordpage-hl-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); margin-bottom: var(--crisp-space-2-5); padding-inline-end: var(--crisp-space-4); }
2181
2190
  & .crisp-recordpage-hl-value { display: flex; flex-wrap: wrap; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
2182
2191
  & .crisp-recordpage-hl-value--empty { color: var(--crisp-fg-muted); font-weight: var(--crisp-weight-regular); }
2192
+
2183
2193
  & .crisp-recordpage-feed { display: flex; flex-direction: column; }
2184
2194
  & .crisp-recordpage-feed-item { display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2) 0; }
2185
2195
  & .crisp-recordpage-feed-dot { position: relative; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
@@ -2188,6 +2198,7 @@
2188
2198
  & .crisp-recordpage-feed-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); margin-top: 2px; }
2189
2199
  & .crisp-recordpage-feed-more { align-self: flex-start; margin-top: var(--crisp-space-1); border: none; background: none; padding: var(--crisp-space-1) 0; color: var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
2190
2200
  & .crisp-recordpage-feed-more:hover { text-decoration: underline; }
2201
+
2191
2202
  & .crisp-recordpage-section-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-1); }
2192
2203
  & .crisp-recordpage-section-head .crisp-recordpage-section-title { padding: 0; }
2193
2204
  & .crisp-recordpage-section-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
@@ -2195,6 +2206,7 @@
2195
2206
  & .crisp-recordpage-addbtn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: 26px; padding: 0 var(--crisp-space-1-5); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
2196
2207
  & .crisp-recordpage-addbtn:hover { background: var(--crisp-bg-raised); }
2197
2208
  & .crisp-recordpage-addbtn svg { flex-shrink: 0; }
2209
+
2198
2210
  & .crisp-recordpage-composer { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding: var(--crisp-space-2); border: 1px solid var(--crisp-border-default); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); margin-bottom: var(--crisp-space-2); }
2199
2211
  & .crisp-recordpage-composer textarea { width: 100%; min-height: 56px; resize: vertical; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
2200
2212
  & .crisp-recordpage-composer textarea:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); border-radius: var(--crisp-radius-md); }
@@ -2204,18 +2216,23 @@
2204
2216
  & .crisp-recordpage-btn:hover { background: var(--crisp-bg-raised); }
2205
2217
  & .crisp-recordpage-btn--primary { box-shadow: none; background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); }
2206
2218
  & .crisp-recordpage-btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
2219
+
2207
2220
  & .crisp-recordpage-note { display: flex; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
2208
2221
  & .crisp-recordpage-note-body { flex: 1; min-width: 0; }
2209
2222
  & .crisp-recordpage-note-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-bottom: var(--crisp-space-0-5); }
2210
2223
  & .crisp-recordpage-note-author { font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
2211
2224
  & .crisp-recordpage-note-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
2212
2225
  & .crisp-recordpage-note-text { font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); white-space: pre-wrap; word-break: break-word; }
2226
+
2213
2227
  & .crisp-recordpage-task { display: flex; align-items: flex-start; gap: var(--crisp-space-2); padding: var(--crisp-space-1-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
2214
2228
  & .crisp-recordpage-task-text { flex: 1; min-width: 0; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); padding-top: 1px; }
2215
2229
  & .crisp-recordpage-task[data-done] .crisp-recordpage-task-text { text-decoration: line-through; color: var(--crisp-fg-muted); }
2230
+
2216
2231
  & .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-6) var(--crisp-space-3); border: 1px dashed var(--crisp-border-default); border-radius: var(--crisp-radius-xl); color: var(--crisp-fg-muted); font-size: var(--crisp-text-md); text-align: center; }
2217
2232
  & .crisp-recordpage-emptycard svg { color: var(--crisp-fg-muted); opacity: 0.7; }
2233
+
2218
2234
  & .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
2235
+
2219
2236
  & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
2220
2237
  & .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
2221
2238
  & .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
@@ -2164,23 +2164,33 @@
2164
2164
  & .crisp-recordpage-edit:disabled { opacity: 0.5; cursor: not-allowed; }
2165
2165
  & .crisp-recordpage-edit-pending { flex-shrink: 0; color: var(--crisp-fg-muted); }
2166
2166
  & .crisp-recordpage-edit-error { flex-basis: 100%; margin-top: var(--crisp-space-0-5); font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
2167
+ /* #457: an `editor: "select"` field's at-rest chip -- same shape as
2168
+ RecordsTable's own `.crisp-recordstable-status[-dot]` (already measured against Attio there), but its own classes: that pair is scoped under `.crisp-recordstable`'s own recipe root, so reusing the
2169
+ classnames verbatim here would resolve to nothing. */
2170
+ & .crisp-recordpage-status { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; }
2171
+ & .crisp-recordpage-status-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: var(--crisp-radius-full); }
2167
2172
  & .crisp-recordpage-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; }
2168
2173
  & .crisp-recordpage-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; }
2169
2174
  & .crisp-recordpage-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
2170
2175
  & .crisp-recordpage-link:hover { text-decoration: underline; }
2171
2176
  & .crisp-recordpage-emptystate { padding: var(--crisp-space-2) var(--crisp-space-1); font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
2177
+
2172
2178
  & .crisp-recordpage-main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: var(--crisp-space-3) var(--crisp-space-5); }
2179
+
2173
2180
  & .crisp-recordpage-panel { padding-top: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-4); }
2174
2181
  & .crisp-recordpage-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); background: var(--crisp-bg-surface); overflow: hidden; }
2175
2182
  & .crisp-recordpage-card-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.14px; color: var(--crisp-fg-primary); padding: var(--crisp-space-3) var(--crisp-space-3) var(--crisp-space-2); }
2176
2183
  & .crisp-recordpage-hl-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); overflow: hidden; }
2177
- /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any column count, so the grid stays responsive (auto-fit) without per-cell nth-child border hacks that break when the column count changes. */
2184
+ /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any
2185
+ column count, so the grid stays responsive (auto-fit) without per-cell
2186
+ nth-child border hacks that break when the column count changes. */
2178
2187
  & .crisp-recordpage-hl-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1px; background: var(--crisp-border-subtle); }
2179
2188
  & .crisp-recordpage-hl-cell { position: relative; min-height: 84px; padding: var(--crisp-space-2-5) var(--crisp-space-3); background: var(--crisp-bg-surface); }
2180
2189
  & .crisp-recordpage-hl-icon { position: absolute; top: var(--crisp-space-2-5); inset-inline-end: var(--crisp-space-3); color: var(--crisp-fg-muted); }
2181
2190
  & .crisp-recordpage-hl-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); margin-bottom: var(--crisp-space-2-5); padding-inline-end: var(--crisp-space-4); }
2182
2191
  & .crisp-recordpage-hl-value { display: flex; flex-wrap: wrap; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
2183
2192
  & .crisp-recordpage-hl-value--empty { color: var(--crisp-fg-muted); font-weight: var(--crisp-weight-regular); }
2193
+
2184
2194
  & .crisp-recordpage-feed { display: flex; flex-direction: column; }
2185
2195
  & .crisp-recordpage-feed-item { display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2) 0; }
2186
2196
  & .crisp-recordpage-feed-dot { position: relative; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
@@ -2189,6 +2199,7 @@
2189
2199
  & .crisp-recordpage-feed-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); margin-top: 2px; }
2190
2200
  & .crisp-recordpage-feed-more { align-self: flex-start; margin-top: var(--crisp-space-1); border: none; background: none; padding: var(--crisp-space-1) 0; color: var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
2191
2201
  & .crisp-recordpage-feed-more:hover { text-decoration: underline; }
2202
+
2192
2203
  & .crisp-recordpage-section-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-1); }
2193
2204
  & .crisp-recordpage-section-head .crisp-recordpage-section-title { padding: 0; }
2194
2205
  & .crisp-recordpage-section-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
@@ -2196,6 +2207,7 @@
2196
2207
  & .crisp-recordpage-addbtn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: 26px; padding: 0 var(--crisp-space-1-5); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
2197
2208
  & .crisp-recordpage-addbtn:hover { background: var(--crisp-bg-raised); }
2198
2209
  & .crisp-recordpage-addbtn svg { flex-shrink: 0; }
2210
+
2199
2211
  & .crisp-recordpage-composer { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding: var(--crisp-space-2); border: 1px solid var(--crisp-border-default); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); margin-bottom: var(--crisp-space-2); }
2200
2212
  & .crisp-recordpage-composer textarea { width: 100%; min-height: 56px; resize: vertical; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
2201
2213
  & .crisp-recordpage-composer textarea:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); border-radius: var(--crisp-radius-md); }
@@ -2205,18 +2217,23 @@
2205
2217
  & .crisp-recordpage-btn:hover { background: var(--crisp-bg-raised); }
2206
2218
  & .crisp-recordpage-btn--primary { box-shadow: none; background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); }
2207
2219
  & .crisp-recordpage-btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
2220
+
2208
2221
  & .crisp-recordpage-note { display: flex; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
2209
2222
  & .crisp-recordpage-note-body { flex: 1; min-width: 0; }
2210
2223
  & .crisp-recordpage-note-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-bottom: var(--crisp-space-0-5); }
2211
2224
  & .crisp-recordpage-note-author { font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
2212
2225
  & .crisp-recordpage-note-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
2213
2226
  & .crisp-recordpage-note-text { font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); white-space: pre-wrap; word-break: break-word; }
2227
+
2214
2228
  & .crisp-recordpage-task { display: flex; align-items: flex-start; gap: var(--crisp-space-2); padding: var(--crisp-space-1-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
2215
2229
  & .crisp-recordpage-task-text { flex: 1; min-width: 0; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); padding-top: 1px; }
2216
2230
  & .crisp-recordpage-task[data-done] .crisp-recordpage-task-text { text-decoration: line-through; color: var(--crisp-fg-muted); }
2231
+
2217
2232
  & .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-6) var(--crisp-space-3); border: 1px dashed var(--crisp-border-default); border-radius: var(--crisp-radius-xl); color: var(--crisp-fg-muted); font-size: var(--crisp-text-md); text-align: center; }
2218
2233
  & .crisp-recordpage-emptycard svg { color: var(--crisp-fg-muted); opacity: 0.7; }
2234
+
2219
2235
  & .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
2236
+
2220
2237
  & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
2221
2238
  & .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
2222
2239
  & .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
@@ -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.2",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",