@crisp-ui-kit/crisp 0.55.1 → 0.55.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.
@@ -210,6 +210,19 @@ function parsePhoneValue(v) {
210
210
  return null;
211
211
  return { iso2: s.slice(0, i), digits: s.slice(i + 1) };
212
212
  }
213
+ // crisp/#816 -- a `multiValue` phone column's raw value is several
214
+ // `parsePhoneValue`-shaped entries joined with `;`. Empty entries (a blank
215
+ // trailing slot that was never filled) are dropped rather than rendered as
216
+ // a stray chip.
217
+ function parsePhoneValues(v) {
218
+ const s = typeof v === "string" ? v : "";
219
+ if (!s)
220
+ return [];
221
+ return s
222
+ .split(";")
223
+ .map((entry) => parsePhoneValue(entry))
224
+ .filter((p) => !!p?.digits);
225
+ }
213
226
  // stable pastel tone per string value — colour-coded tags/statuses
214
227
  const TAG_TONES = [
215
228
  "brand",
@@ -430,6 +443,22 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
430
443
  return (0, jsx_runtime_1.jsx)(RatingStars, { value: n, max: col.maxRating ?? 5 });
431
444
  }
432
445
  case "phone": {
446
+ // crisp/#816, confirmed live -- a `multiValue` phone cell renders
447
+ // each value as its own real `tel:` link (Attio: a measured link
448
+ // blue, underlined, no flag/dial-code decoration, `4px` flex gap
449
+ // between values), unlike the single-value cell above/below (a plain
450
+ // flag+dial+digits span, never a link). The whole cell hard-clips
451
+ // when it overflows the column -- confirmed live, Attio does not
452
+ // ellipsis a multi-value phone cell.
453
+ if (col.multiValue) {
454
+ const values = parsePhoneValues(value);
455
+ if (!values.length)
456
+ return empty;
457
+ return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-phone-multi", children: values.map((p) => {
458
+ const country = (0, countries_1.countryByIso2)(p.iso2);
459
+ return ((0, jsx_runtime_1.jsxs)("a", { className: "crisp-datatable-phone-link", href: `tel:+${country?.dialCode ?? ""}${p.digits}`, onClick: (ev) => ev.stopPropagation(), children: ["+", country?.dialCode ?? "?", p.digits] }, `${p.iso2}|${p.digits}`));
460
+ }) }));
461
+ }
433
462
  const parsed = parsePhoneValue(value);
434
463
  if (!parsed?.digits)
435
464
  return empty;
@@ -446,6 +475,16 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
446
475
  // link's own navigation must not also fire the row's onRowClick.
447
476
  onClick: (ev) => ev.stopPropagation(), children: text }));
448
477
  }
478
+ case "date":
479
+ case "datetime": {
480
+ const text = display || formatCell(col.type, value, col.symbol, locale);
481
+ if (!text)
482
+ return empty;
483
+ // crisp#822 -- a column-level predicate, not `tones` (see its doc
484
+ // comment): the danger state is computed over the row, not read back
485
+ // off this formatted string.
486
+ return col.danger?.(value, row) ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-danger", children: text })) : (text);
487
+ }
449
488
  default: {
450
489
  const text = display || formatCell(col.type, value, col.symbol, locale);
451
490
  const isNum = col.type === "number" || col.type === "currency";
@@ -619,6 +658,44 @@ function DataPhoneEditor({ col, value, isPending, onCommit, }) {
619
658
  commit(iso2, digits);
620
659
  } })] }));
621
660
  }
661
+ // crisp/#816, confirmed live -- Attio's own multi-value Phone Number
662
+ // editor (`col.multiValue`): a stack of already-committed values (a
663
+ // drag grip, the value as a real `tel:` link, and a hover-revealed
664
+ // copy/delete pair -- measured `32px` row, `5px 8px` padding, `8px` gap,
665
+ // `16×16` icons) plus one ALWAYS-blank trailing "country + digits" row in
666
+ // `DataPhoneEditor`'s own shape. The blank row itself is the only
667
+ // affordance for adding another value -- there is no separate "+ Add"
668
+ // button; committing it (Enter/blur with digits) appends and clears back
669
+ // to blank so the next value can be typed immediately. Drag-to-reorder
670
+ // (the grip icon) was measured present but isn't wired here -- reordering
671
+ // a committed array is a distinct interaction from add/remove/copy and is
672
+ // left as a follow-up rather than a partial, non-functional drag handle.
673
+ function DataPhoneMultiEditor({ col, value, isPending, onCommit, }) {
674
+ const committed = parsePhoneValues(value);
675
+ const [draftIso2, setDraftIso2] = React.useState("US");
676
+ const [draftDigits, setDraftDigits] = React.useState("");
677
+ const countryOptions = countries_1.COUNTRIES.map((c) => ({
678
+ value: c.iso2,
679
+ label: `${(0, countries_1.flagEmoji)(c.iso2)} ${c.name} +${c.dialCode}`,
680
+ keywords: `${c.name} ${c.iso2} ${c.dialCode}`,
681
+ }));
682
+ const join = (list) => list.map((p) => `${p.iso2}|${p.digits}`).join(";");
683
+ const removeAt = (i) => onCommit(join(committed.filter((_, idx) => idx !== i)));
684
+ const commitDraft = () => {
685
+ if (!draftDigits)
686
+ return;
687
+ onCommit(join([...committed, { iso2: draftIso2, digits: draftDigits }]));
688
+ setDraftDigits("");
689
+ };
690
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-phone-multi-editor", children: [committed.map((p, i) => {
691
+ const country = (0, countries_1.countryByIso2)(p.iso2);
692
+ const tel = `+${country?.dialCode ?? "?"}${p.digits}`;
693
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-phone-multi-row", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 16, strokeWidth: 1.75, className: "crisp-datatable-phone-multi-grip", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("a", { className: "crisp-datatable-phone-link", href: `tel:${tel}`, onClick: (ev) => ev.stopPropagation(), children: tel }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-phone-multi-actions", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-phone-multi-action", "aria-label": `Copy ${col.key} value`, onClick: () => navigator.clipboard?.writeText(tel), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 16, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-phone-multi-action", "aria-label": `Remove ${col.key} value`, disabled: isPending, onClick: () => removeAt(i), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 16, strokeWidth: 1.75 }) })] })] }, `${p.iso2}|${p.digits}`));
694
+ }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-phone-editor", children: [(0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", searchable: true, "aria-label": `Edit ${col.key} country`, value: draftIso2, disabled: isPending, options: countryOptions, onValueChange: setDraftIso2 }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-phone-dial", "aria-hidden": "true", children: ["+", (0, countries_1.countryByIso2)(draftIso2)?.dialCode ?? "?"] }), (0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit", "aria-label": `Edit ${col.key} number (add another)`, value: draftDigits, disabled: isPending, onChange: (e) => setDraftDigits(e.currentTarget.value), onBlur: commitDraft, onKeyDown: (e) => {
695
+ if (e.key === "Enter")
696
+ commitDraft();
697
+ } })] })] }));
698
+ }
622
699
  // crisp/#762, confirmed live -- Attio's own Timestamp editor is a calendar +
623
700
  // time + preset picker, not the native `datetime-local` input `type:
624
701
  // "datetime"` gets by default (that stays the DEFAULT, unchanged, for
@@ -1651,17 +1728,23 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1651
1728
  // value while showing the rejection's message beside the cell (reject).
1652
1729
  // The revert needs no extra code: this component never mutates `data`
1653
1730
  // itself, so once `editing` clears, the cell simply shows `data` again.
1654
- function commitEdit(id, key, value) {
1731
+ // crisp/#816 -- `keepEditing` lets a multi-value editor stay open across
1732
+ // a commit (adding/removing one value must not close the whole cell, the
1733
+ // way it does for every other, single-value editor). Default false
1734
+ // preserves every existing editor's own close-on-commit behaviour.
1735
+ function commitEdit(id, key, value, keepEditing = false) {
1655
1736
  setEditError(null);
1656
1737
  const result = onEdit?.(id, key, value);
1657
1738
  if (result && typeof result.then === "function") {
1658
1739
  setPending({ id, key });
1659
1740
  result.then(() => {
1660
1741
  setPending(null);
1661
- setEditing(null);
1742
+ if (!keepEditing)
1743
+ setEditing(null);
1662
1744
  }, (err) => {
1663
1745
  setPending(null);
1664
- setEditing(null);
1746
+ if (!keepEditing)
1747
+ setEditing(null);
1665
1748
  setEditError({
1666
1749
  id,
1667
1750
  key,
@@ -1674,7 +1757,8 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1674
1757
  });
1675
1758
  return;
1676
1759
  }
1677
- setEditing(null);
1760
+ if (!keepEditing)
1761
+ setEditing(null);
1678
1762
  }
1679
1763
  const reduceMotion = (0, core_1.usePrefersReducedMotion)();
1680
1764
  // crisp/#577 -- keyboard nav to an off-screen (virtualized-out) cell
@@ -2213,7 +2297,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
2213
2297
  // portaled popover, so a handler here can't
2214
2298
  // rely on the key event ever bubbling through
2215
2299
  // this element (#453).
2216
- 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) })) : col.editor === "phone" ? ((0, jsx_runtime_1.jsx)(DataPhoneEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : col.editor === "datetime" ? ((0, jsx_runtime_1.jsx)(DataDateTimeEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : ((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));
2300
+ 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) })) : col.editor === "phone" && col.multiValue ? ((0, jsx_runtime_1.jsx)(DataPhoneMultiEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v, true) })) : col.editor === "phone" ? ((0, jsx_runtime_1.jsx)(DataPhoneEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : col.editor === "datetime" ? ((0, jsx_runtime_1.jsx)(DataDateTimeEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : ((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));
2217
2301
  }
2218
2302
  if (isEditing) {
2219
2303
  return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit",
@@ -277,6 +277,10 @@ const config = (0, core_1.defineRecipe)({
277
277
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, ${tokens_1.contract.bgOrange} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgOrange} 82%, ${tokens_1.contract.fgPrimary}); }
278
278
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
279
279
  & .crisp-datatable-muted { color: ${tokens_1.contract.fgMuted}; }
280
+ /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
281
+ TasksTable's own overdue due-date (crisp#462), plain text colour, no
282
+ fill/weight change. */
283
+ & .crisp-datatable-danger { color: ${tokens_1.contract.fgDanger}; }
280
284
  /* url/email cell types (#654) -- same measured values as RecordsTable's
281
285
  own .crisp-recordstable-link, which this mirrors: no underline at
282
286
  rest, underline on hover, own-colour text rather than a browser-blue
@@ -290,6 +294,35 @@ const config = (0, core_1.defineRecipe)({
290
294
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
291
295
  & .crisp-datatable-phone-editor { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]}; }
292
296
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
297
+ /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
298
+ values" Phone Number attribute, and the built-in People "Phone
299
+ numbers" field): the collapsed cell is a 4px-gap flex row of real
300
+ tel: links in a measured link blue (~rgb(59,108,232) pixel-sampled;
301
+ reused as the closest already-tokened blue, brand600, rather than
302
+ adding a third blue token for a delta this small), underlined,
303
+ UNLIKE the single-value cell above (plain text, no link, no
304
+ underline) -- multi-value is the one shape Attio makes into a real
305
+ link. The cell hard-clips on overflow (no ellipsis -- confirmed
306
+ live, a second value can end mid-digit with no "..."). */
307
+ & .crisp-datatable-phone-multi { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; overflow: hidden; }
308
+ & .crisp-datatable-phone-link { color: ${tokens_1.contract.bgBrandSolid}; text-decoration: underline; white-space: nowrap; }
309
+ /* multi-value editor (#816) -- confirmed live: a stack of committed-
310
+ value rows (32px = size.menuRow, 5px 8px padding, 8px gap between
311
+ the grip/value/actions, 16px icons) above one ALWAYS-blank trailing
312
+ DataPhoneEditor-shaped row -- that blank row is the only "add
313
+ another" affordance, there's no separate button for it. The
314
+ copy/delete pair reuses .crisp-datatable-entity-action's own
315
+ hover-reveal chrome (not independently re-measured as its own hit
316
+ target this pass, only the 16px icon glyphs were). */
317
+ & .crisp-datatable-phone-multi-editor { display: flex; flex-direction: column; }
318
+ & .crisp-datatable-phone-multi-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.menuRow}; padding: 5px ${tokens_1.contract.space["2"]}; }
319
+ & .crisp-datatable-phone-multi-row .crisp-datatable-phone-link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
320
+ & .crisp-datatable-phone-multi-grip { color: ${tokens_1.contract.fgMuted}; flex-shrink: 0; cursor: grab; }
321
+ & .crisp-datatable-phone-multi-actions { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
322
+ & .crisp-datatable-phone-multi-row:hover .crisp-datatable-phone-multi-actions,
323
+ & .crisp-datatable-phone-multi-actions:focus-within { opacity: 1; }
324
+ & .crisp-datatable-phone-multi-action { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; font-family: inherit; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); cursor: pointer; flex-shrink: 0; }
325
+ & .crisp-datatable-phone-multi-action:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); color: ${tokens_1.contract.fgPrimary}; }
293
326
  /* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
294
327
  & .crisp-datatable-datetime-editor { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]}; }
295
328
  /* rating cell type/editor (#760) -- measured live: 16px stars on an
@@ -208,6 +208,9 @@
208
208
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
209
209
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
210
210
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
211
+ /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
212
+ TasksTable's own overdue due-date (crisp#462), plain text colour, no fill/weight change. */
213
+ & .crisp-datatable-danger { color: var(--crisp-fg-danger); }
211
214
  /* url/email cell types (#654) -- same measured values as RecordsTable's
212
215
  own .crisp-recordstable-link, which this mirrors: no underline at
213
216
  rest, underline on hover, own-colour text rather than a browser-blue
@@ -221,6 +224,34 @@
221
224
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
222
225
  & .crisp-datatable-phone-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
223
226
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
227
+ /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
228
+ values" Phone Number attribute, and the built-in People "Phone
229
+ numbers" field): the collapsed cell is a 4px-gap flex row of real
230
+ tel: links in a measured link blue (~rgb(59,108,232) pixel-sampled;
231
+ reused as the closest already-tokened blue, brand600, rather than
232
+ adding a third blue token for a delta this small), underlined,
233
+ UNLIKE the single-value cell above (plain text, no link, no
234
+ underline) -- multi-value is the one shape Attio makes into a real
235
+ link. The cell hard-clips on overflow (no ellipsis -- confirmed
236
+ live, a second value can end mid-digit with no "..."). */
237
+ & .crisp-datatable-phone-multi { display: inline-flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
238
+ & .crisp-datatable-phone-link { color: var(--crisp-bg-brand-solid); text-decoration: underline; white-space: nowrap; }
239
+ /* multi-value editor (#816) -- confirmed live: a stack of committed-
240
+ value rows (32px = size.menuRow, 5px 8px padding, 8px gap between
241
+ the grip/value/actions, 16px icons) above one ALWAYS-blank trailing
242
+ DataPhoneEditor-shaped row -- that blank row is the only "add
243
+ another" affordance, there's no separate button for it. The copy/delete pair reuses .crisp-datatable-entity-action's own
244
+ hover-reveal chrome (not independently re-measured as its own hit
245
+ target this pass, only the 16px icon glyphs were). */
246
+ & .crisp-datatable-phone-multi-editor { display: flex; flex-direction: column; }
247
+ & .crisp-datatable-phone-multi-row { display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); padding: 5px var(--crisp-space-2); }
248
+ & .crisp-datatable-phone-multi-row .crisp-datatable-phone-link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
249
+ & .crisp-datatable-phone-multi-grip { color: var(--crisp-fg-muted); flex-shrink: 0; cursor: grab; }
250
+ & .crisp-datatable-phone-multi-actions { display: flex; align-items: center; gap: var(--crisp-space-1); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
251
+ & .crisp-datatable-phone-multi-row:hover .crisp-datatable-phone-multi-actions,
252
+ & .crisp-datatable-phone-multi-actions:focus-within { opacity: 1; }
253
+ & .crisp-datatable-phone-multi-action { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; font-family: inherit; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; flex-shrink: 0; }
254
+ & .crisp-datatable-phone-multi-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
224
255
  /* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
225
256
  & .crisp-datatable-datetime-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
226
257
  /* rating cell type/editor (#760) -- measured live: 16px stars on an
@@ -209,6 +209,9 @@
209
209
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
210
210
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
211
211
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
212
+ /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
213
+ TasksTable's own overdue due-date (crisp#462), plain text colour, no fill/weight change. */
214
+ & .crisp-datatable-danger { color: var(--crisp-fg-danger); }
212
215
  /* url/email cell types (#654) -- same measured values as RecordsTable's
213
216
  own .crisp-recordstable-link, which this mirrors: no underline at
214
217
  rest, underline on hover, own-colour text rather than a browser-blue
@@ -222,6 +225,34 @@
222
225
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
223
226
  & .crisp-datatable-phone-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
224
227
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
228
+ /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
229
+ values" Phone Number attribute, and the built-in People "Phone
230
+ numbers" field): the collapsed cell is a 4px-gap flex row of real
231
+ tel: links in a measured link blue (~rgb(59,108,232) pixel-sampled;
232
+ reused as the closest already-tokened blue, brand600, rather than
233
+ adding a third blue token for a delta this small), underlined,
234
+ UNLIKE the single-value cell above (plain text, no link, no
235
+ underline) -- multi-value is the one shape Attio makes into a real
236
+ link. The cell hard-clips on overflow (no ellipsis -- confirmed
237
+ live, a second value can end mid-digit with no "..."). */
238
+ & .crisp-datatable-phone-multi { display: inline-flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
239
+ & .crisp-datatable-phone-link { color: var(--crisp-bg-brand-solid); text-decoration: underline; white-space: nowrap; }
240
+ /* multi-value editor (#816) -- confirmed live: a stack of committed-
241
+ value rows (32px = size.menuRow, 5px 8px padding, 8px gap between
242
+ the grip/value/actions, 16px icons) above one ALWAYS-blank trailing
243
+ DataPhoneEditor-shaped row -- that blank row is the only "add
244
+ another" affordance, there's no separate button for it. The copy/delete pair reuses .crisp-datatable-entity-action's own
245
+ hover-reveal chrome (not independently re-measured as its own hit
246
+ target this pass, only the 16px icon glyphs were). */
247
+ & .crisp-datatable-phone-multi-editor { display: flex; flex-direction: column; }
248
+ & .crisp-datatable-phone-multi-row { display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); padding: 5px var(--crisp-space-2); }
249
+ & .crisp-datatable-phone-multi-row .crisp-datatable-phone-link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
250
+ & .crisp-datatable-phone-multi-grip { color: var(--crisp-fg-muted); flex-shrink: 0; cursor: grab; }
251
+ & .crisp-datatable-phone-multi-actions { display: flex; align-items: center; gap: var(--crisp-space-1); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
252
+ & .crisp-datatable-phone-multi-row:hover .crisp-datatable-phone-multi-actions,
253
+ & .crisp-datatable-phone-multi-actions:focus-within { opacity: 1; }
254
+ & .crisp-datatable-phone-multi-action { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; font-family: inherit; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; flex-shrink: 0; }
255
+ & .crisp-datatable-phone-multi-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
225
256
  /* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
226
257
  & .crisp-datatable-datetime-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
227
258
  /* rating cell type/editor (#760) -- measured live: 16px stars on an
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ArrowDown, ArrowUp, Check, ChevronsUpDown, CircleUserRound, ClipboardPaste, Copy, GripVertical, History, Lock, PanelRight, Plus, Search, SlidersHorizontal, X, } from "lucide-react";
2
+ import { ArrowDown, ArrowUp, Check, ChevronsUpDown, CircleUserRound, ClipboardPaste, Copy, GripVertical, History, Lock, PanelRight, Plus, Search, SlidersHorizontal, Trash2, X, } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { cellDatePrecision, formatCellDate, isRtlElement, parseCellDate, useCrispMessage, useCrispMessages, usePrefersReducedMotion, } from "../../core/index.js";
5
5
  // crisp/#601 supersession: a deliberate exception to "DS atoms stay
@@ -172,6 +172,19 @@ function parsePhoneValue(v) {
172
172
  return null;
173
173
  return { iso2: s.slice(0, i), digits: s.slice(i + 1) };
174
174
  }
175
+ // crisp/#816 -- a `multiValue` phone column's raw value is several
176
+ // `parsePhoneValue`-shaped entries joined with `;`. Empty entries (a blank
177
+ // trailing slot that was never filled) are dropped rather than rendered as
178
+ // a stray chip.
179
+ function parsePhoneValues(v) {
180
+ const s = typeof v === "string" ? v : "";
181
+ if (!s)
182
+ return [];
183
+ return s
184
+ .split(";")
185
+ .map((entry) => parsePhoneValue(entry))
186
+ .filter((p) => !!p?.digits);
187
+ }
175
188
  // stable pastel tone per string value — colour-coded tags/statuses
176
189
  const TAG_TONES = [
177
190
  "brand",
@@ -392,6 +405,22 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
392
405
  return _jsx(RatingStars, { value: n, max: col.maxRating ?? 5 });
393
406
  }
394
407
  case "phone": {
408
+ // crisp/#816, confirmed live -- a `multiValue` phone cell renders
409
+ // each value as its own real `tel:` link (Attio: a measured link
410
+ // blue, underlined, no flag/dial-code decoration, `4px` flex gap
411
+ // between values), unlike the single-value cell above/below (a plain
412
+ // flag+dial+digits span, never a link). The whole cell hard-clips
413
+ // when it overflows the column -- confirmed live, Attio does not
414
+ // ellipsis a multi-value phone cell.
415
+ if (col.multiValue) {
416
+ const values = parsePhoneValues(value);
417
+ if (!values.length)
418
+ return empty;
419
+ return (_jsx("span", { className: "crisp-datatable-phone-multi", children: values.map((p) => {
420
+ const country = countryByIso2(p.iso2);
421
+ return (_jsxs("a", { className: "crisp-datatable-phone-link", href: `tel:+${country?.dialCode ?? ""}${p.digits}`, onClick: (ev) => ev.stopPropagation(), children: ["+", country?.dialCode ?? "?", p.digits] }, `${p.iso2}|${p.digits}`));
422
+ }) }));
423
+ }
395
424
  const parsed = parsePhoneValue(value);
396
425
  if (!parsed?.digits)
397
426
  return empty;
@@ -408,6 +437,16 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
408
437
  // link's own navigation must not also fire the row's onRowClick.
409
438
  onClick: (ev) => ev.stopPropagation(), children: text }));
410
439
  }
440
+ case "date":
441
+ case "datetime": {
442
+ const text = display || formatCell(col.type, value, col.symbol, locale);
443
+ if (!text)
444
+ return empty;
445
+ // crisp#822 -- a column-level predicate, not `tones` (see its doc
446
+ // comment): the danger state is computed over the row, not read back
447
+ // off this formatted string.
448
+ return col.danger?.(value, row) ? (_jsx("span", { className: "crisp-datatable-danger", children: text })) : (text);
449
+ }
411
450
  default: {
412
451
  const text = display || formatCell(col.type, value, col.symbol, locale);
413
452
  const isNum = col.type === "number" || col.type === "currency";
@@ -581,6 +620,44 @@ function DataPhoneEditor({ col, value, isPending, onCommit, }) {
581
620
  commit(iso2, digits);
582
621
  } })] }));
583
622
  }
623
+ // crisp/#816, confirmed live -- Attio's own multi-value Phone Number
624
+ // editor (`col.multiValue`): a stack of already-committed values (a
625
+ // drag grip, the value as a real `tel:` link, and a hover-revealed
626
+ // copy/delete pair -- measured `32px` row, `5px 8px` padding, `8px` gap,
627
+ // `16×16` icons) plus one ALWAYS-blank trailing "country + digits" row in
628
+ // `DataPhoneEditor`'s own shape. The blank row itself is the only
629
+ // affordance for adding another value -- there is no separate "+ Add"
630
+ // button; committing it (Enter/blur with digits) appends and clears back
631
+ // to blank so the next value can be typed immediately. Drag-to-reorder
632
+ // (the grip icon) was measured present but isn't wired here -- reordering
633
+ // a committed array is a distinct interaction from add/remove/copy and is
634
+ // left as a follow-up rather than a partial, non-functional drag handle.
635
+ function DataPhoneMultiEditor({ col, value, isPending, onCommit, }) {
636
+ const committed = parsePhoneValues(value);
637
+ const [draftIso2, setDraftIso2] = React.useState("US");
638
+ const [draftDigits, setDraftDigits] = React.useState("");
639
+ const countryOptions = COUNTRIES.map((c) => ({
640
+ value: c.iso2,
641
+ label: `${flagEmoji(c.iso2)} ${c.name} +${c.dialCode}`,
642
+ keywords: `${c.name} ${c.iso2} ${c.dialCode}`,
643
+ }));
644
+ const join = (list) => list.map((p) => `${p.iso2}|${p.digits}`).join(";");
645
+ const removeAt = (i) => onCommit(join(committed.filter((_, idx) => idx !== i)));
646
+ const commitDraft = () => {
647
+ if (!draftDigits)
648
+ return;
649
+ onCommit(join([...committed, { iso2: draftIso2, digits: draftDigits }]));
650
+ setDraftDigits("");
651
+ };
652
+ return (_jsxs("span", { className: "crisp-datatable-phone-multi-editor", children: [committed.map((p, i) => {
653
+ const country = countryByIso2(p.iso2);
654
+ const tel = `+${country?.dialCode ?? "?"}${p.digits}`;
655
+ return (_jsxs("span", { className: "crisp-datatable-phone-multi-row", children: [_jsx(GripVertical, { size: 16, strokeWidth: 1.75, className: "crisp-datatable-phone-multi-grip", "aria-hidden": "true" }), _jsx("a", { className: "crisp-datatable-phone-link", href: `tel:${tel}`, onClick: (ev) => ev.stopPropagation(), children: tel }), _jsxs("span", { className: "crisp-datatable-phone-multi-actions", children: [_jsx("button", { type: "button", className: "crisp-datatable-phone-multi-action", "aria-label": `Copy ${col.key} value`, onClick: () => navigator.clipboard?.writeText(tel), children: _jsx(Copy, { size: 16, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-datatable-phone-multi-action", "aria-label": `Remove ${col.key} value`, disabled: isPending, onClick: () => removeAt(i), children: _jsx(Trash2, { size: 16, strokeWidth: 1.75 }) })] })] }, `${p.iso2}|${p.digits}`));
656
+ }), _jsxs("span", { className: "crisp-datatable-phone-editor", children: [_jsx(Select, { variant: "ghost", searchable: true, "aria-label": `Edit ${col.key} country`, value: draftIso2, disabled: isPending, options: countryOptions, onValueChange: setDraftIso2 }), _jsxs("span", { className: "crisp-datatable-phone-dial", "aria-hidden": "true", children: ["+", countryByIso2(draftIso2)?.dialCode ?? "?"] }), _jsx("input", { className: "crisp-datatable-edit", "aria-label": `Edit ${col.key} number (add another)`, value: draftDigits, disabled: isPending, onChange: (e) => setDraftDigits(e.currentTarget.value), onBlur: commitDraft, onKeyDown: (e) => {
657
+ if (e.key === "Enter")
658
+ commitDraft();
659
+ } })] })] }));
660
+ }
584
661
  // crisp/#762, confirmed live -- Attio's own Timestamp editor is a calendar +
585
662
  // time + preset picker, not the native `datetime-local` input `type:
586
663
  // "datetime"` gets by default (that stays the DEFAULT, unchanged, for
@@ -1613,17 +1690,23 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
1613
1690
  // value while showing the rejection's message beside the cell (reject).
1614
1691
  // The revert needs no extra code: this component never mutates `data`
1615
1692
  // itself, so once `editing` clears, the cell simply shows `data` again.
1616
- function commitEdit(id, key, value) {
1693
+ // crisp/#816 -- `keepEditing` lets a multi-value editor stay open across
1694
+ // a commit (adding/removing one value must not close the whole cell, the
1695
+ // way it does for every other, single-value editor). Default false
1696
+ // preserves every existing editor's own close-on-commit behaviour.
1697
+ function commitEdit(id, key, value, keepEditing = false) {
1617
1698
  setEditError(null);
1618
1699
  const result = onEdit?.(id, key, value);
1619
1700
  if (result && typeof result.then === "function") {
1620
1701
  setPending({ id, key });
1621
1702
  result.then(() => {
1622
1703
  setPending(null);
1623
- setEditing(null);
1704
+ if (!keepEditing)
1705
+ setEditing(null);
1624
1706
  }, (err) => {
1625
1707
  setPending(null);
1626
- setEditing(null);
1708
+ if (!keepEditing)
1709
+ setEditing(null);
1627
1710
  setEditError({
1628
1711
  id,
1629
1712
  key,
@@ -1636,7 +1719,8 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
1636
1719
  });
1637
1720
  return;
1638
1721
  }
1639
- setEditing(null);
1722
+ if (!keepEditing)
1723
+ setEditing(null);
1640
1724
  }
1641
1725
  const reduceMotion = usePrefersReducedMotion();
1642
1726
  // crisp/#577 -- keyboard nav to an off-screen (virtualized-out) cell
@@ -2175,7 +2259,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
2175
2259
  // portaled popover, so a handler here can't
2176
2260
  // rely on the key event ever bubbling through
2177
2261
  // this element (#453).
2178
- 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) })) : col.editor === "phone" ? (_jsx(DataPhoneEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : col.editor === "datetime" ? (_jsx(DataDateTimeEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : (_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));
2262
+ 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) })) : col.editor === "phone" && col.multiValue ? (_jsx(DataPhoneMultiEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v, true) })) : col.editor === "phone" ? (_jsx(DataPhoneEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : col.editor === "datetime" ? (_jsx(DataDateTimeEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v) })) : (_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));
2179
2263
  }
2180
2264
  if (isEditing) {
2181
2265
  return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, onClick: (e) => e.stopPropagation(), children: [_jsx("input", { className: "crisp-datatable-edit",
@@ -274,6 +274,10 @@ const config = defineRecipe({
274
274
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); }
275
275
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
276
276
  & .crisp-datatable-muted { color: ${c.fgMuted}; }
277
+ /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
278
+ TasksTable's own overdue due-date (crisp#462), plain text colour, no
279
+ fill/weight change. */
280
+ & .crisp-datatable-danger { color: ${c.fgDanger}; }
277
281
  /* url/email cell types (#654) -- same measured values as RecordsTable's
278
282
  own .crisp-recordstable-link, which this mirrors: no underline at
279
283
  rest, underline on hover, own-colour text rather than a browser-blue
@@ -287,6 +291,35 @@ const config = defineRecipe({
287
291
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
288
292
  & .crisp-datatable-phone-editor { display: flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.tableRow}; padding: 0 ${c.space["2"]}; }
289
293
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
294
+ /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
295
+ values" Phone Number attribute, and the built-in People "Phone
296
+ numbers" field): the collapsed cell is a 4px-gap flex row of real
297
+ tel: links in a measured link blue (~rgb(59,108,232) pixel-sampled;
298
+ reused as the closest already-tokened blue, brand600, rather than
299
+ adding a third blue token for a delta this small), underlined,
300
+ UNLIKE the single-value cell above (plain text, no link, no
301
+ underline) -- multi-value is the one shape Attio makes into a real
302
+ link. The cell hard-clips on overflow (no ellipsis -- confirmed
303
+ live, a second value can end mid-digit with no "..."). */
304
+ & .crisp-datatable-phone-multi { display: inline-flex; align-items: center; gap: ${c.space["1"]}; overflow: hidden; }
305
+ & .crisp-datatable-phone-link { color: ${c.bgBrandSolid}; text-decoration: underline; white-space: nowrap; }
306
+ /* multi-value editor (#816) -- confirmed live: a stack of committed-
307
+ value rows (32px = size.menuRow, 5px 8px padding, 8px gap between
308
+ the grip/value/actions, 16px icons) above one ALWAYS-blank trailing
309
+ DataPhoneEditor-shaped row -- that blank row is the only "add
310
+ another" affordance, there's no separate button for it. The
311
+ copy/delete pair reuses .crisp-datatable-entity-action's own
312
+ hover-reveal chrome (not independently re-measured as its own hit
313
+ target this pass, only the 16px icon glyphs were). */
314
+ & .crisp-datatable-phone-multi-editor { display: flex; flex-direction: column; }
315
+ & .crisp-datatable-phone-multi-row { display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.menuRow}; padding: 5px ${c.space["2"]}; }
316
+ & .crisp-datatable-phone-multi-row .crisp-datatable-phone-link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
317
+ & .crisp-datatable-phone-multi-grip { color: ${c.fgMuted}; flex-shrink: 0; cursor: grab; }
318
+ & .crisp-datatable-phone-multi-actions { display: flex; align-items: center; gap: ${c.space["1"]}; margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
319
+ & .crisp-datatable-phone-multi-row:hover .crisp-datatable-phone-multi-actions,
320
+ & .crisp-datatable-phone-multi-actions:focus-within { opacity: 1; }
321
+ & .crisp-datatable-phone-multi-action { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.sm}; background: none; font-family: inherit; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); cursor: pointer; flex-shrink: 0; }
322
+ & .crisp-datatable-phone-multi-action:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgPrimary}; }
290
323
  /* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
291
324
  & .crisp-datatable-datetime-editor { display: flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.tableRow}; padding: 0 ${c.space["2"]}; }
292
325
  /* rating cell type/editor (#760) -- measured live: 16px stars on an
package/dist/styles.css CHANGED
@@ -807,6 +807,9 @@
807
807
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
808
808
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
809
809
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
810
+ /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
811
+ TasksTable's own overdue due-date (crisp#462), plain text colour, no fill/weight change. */
812
+ & .crisp-datatable-danger { color: var(--crisp-fg-danger); }
810
813
  /* url/email cell types (#654) -- same measured values as RecordsTable's
811
814
  own .crisp-recordstable-link, which this mirrors: no underline at
812
815
  rest, underline on hover, own-colour text rather than a browser-blue
@@ -820,6 +823,34 @@
820
823
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
821
824
  & .crisp-datatable-phone-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
822
825
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
826
+ /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
827
+ values" Phone Number attribute, and the built-in People "Phone
828
+ numbers" field): the collapsed cell is a 4px-gap flex row of real
829
+ tel: links in a measured link blue (~rgb(59,108,232) pixel-sampled;
830
+ reused as the closest already-tokened blue, brand600, rather than
831
+ adding a third blue token for a delta this small), underlined,
832
+ UNLIKE the single-value cell above (plain text, no link, no
833
+ underline) -- multi-value is the one shape Attio makes into a real
834
+ link. The cell hard-clips on overflow (no ellipsis -- confirmed
835
+ live, a second value can end mid-digit with no "..."). */
836
+ & .crisp-datatable-phone-multi { display: inline-flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
837
+ & .crisp-datatable-phone-link { color: var(--crisp-bg-brand-solid); text-decoration: underline; white-space: nowrap; }
838
+ /* multi-value editor (#816) -- confirmed live: a stack of committed-
839
+ value rows (32px = size.menuRow, 5px 8px padding, 8px gap between
840
+ the grip/value/actions, 16px icons) above one ALWAYS-blank trailing
841
+ DataPhoneEditor-shaped row -- that blank row is the only "add
842
+ another" affordance, there's no separate button for it. The copy/delete pair reuses .crisp-datatable-entity-action's own
843
+ hover-reveal chrome (not independently re-measured as its own hit
844
+ target this pass, only the 16px icon glyphs were). */
845
+ & .crisp-datatable-phone-multi-editor { display: flex; flex-direction: column; }
846
+ & .crisp-datatable-phone-multi-row { display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); padding: 5px var(--crisp-space-2); }
847
+ & .crisp-datatable-phone-multi-row .crisp-datatable-phone-link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
848
+ & .crisp-datatable-phone-multi-grip { color: var(--crisp-fg-muted); flex-shrink: 0; cursor: grab; }
849
+ & .crisp-datatable-phone-multi-actions { display: flex; align-items: center; gap: var(--crisp-space-1); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
850
+ & .crisp-datatable-phone-multi-row:hover .crisp-datatable-phone-multi-actions,
851
+ & .crisp-datatable-phone-multi-actions:focus-within { opacity: 1; }
852
+ & .crisp-datatable-phone-multi-action { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; font-family: inherit; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; flex-shrink: 0; }
853
+ & .crisp-datatable-phone-multi-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
823
854
  /* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
824
855
  & .crisp-datatable-datetime-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
825
856
  /* rating cell type/editor (#760) -- measured live: 16px stars on an
@@ -808,6 +808,9 @@
808
808
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
809
809
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
810
810
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
811
+ /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
812
+ TasksTable's own overdue due-date (crisp#462), plain text colour, no fill/weight change. */
813
+ & .crisp-datatable-danger { color: var(--crisp-fg-danger); }
811
814
  /* url/email cell types (#654) -- same measured values as RecordsTable's
812
815
  own .crisp-recordstable-link, which this mirrors: no underline at
813
816
  rest, underline on hover, own-colour text rather than a browser-blue
@@ -821,6 +824,34 @@
821
824
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
822
825
  & .crisp-datatable-phone-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
823
826
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
827
+ /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
828
+ values" Phone Number attribute, and the built-in People "Phone
829
+ numbers" field): the collapsed cell is a 4px-gap flex row of real
830
+ tel: links in a measured link blue (~rgb(59,108,232) pixel-sampled;
831
+ reused as the closest already-tokened blue, brand600, rather than
832
+ adding a third blue token for a delta this small), underlined,
833
+ UNLIKE the single-value cell above (plain text, no link, no
834
+ underline) -- multi-value is the one shape Attio makes into a real
835
+ link. The cell hard-clips on overflow (no ellipsis -- confirmed
836
+ live, a second value can end mid-digit with no "..."). */
837
+ & .crisp-datatable-phone-multi { display: inline-flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
838
+ & .crisp-datatable-phone-link { color: var(--crisp-bg-brand-solid); text-decoration: underline; white-space: nowrap; }
839
+ /* multi-value editor (#816) -- confirmed live: a stack of committed-
840
+ value rows (32px = size.menuRow, 5px 8px padding, 8px gap between
841
+ the grip/value/actions, 16px icons) above one ALWAYS-blank trailing
842
+ DataPhoneEditor-shaped row -- that blank row is the only "add
843
+ another" affordance, there's no separate button for it. The copy/delete pair reuses .crisp-datatable-entity-action's own
844
+ hover-reveal chrome (not independently re-measured as its own hit
845
+ target this pass, only the 16px icon glyphs were). */
846
+ & .crisp-datatable-phone-multi-editor { display: flex; flex-direction: column; }
847
+ & .crisp-datatable-phone-multi-row { display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); padding: 5px var(--crisp-space-2); }
848
+ & .crisp-datatable-phone-multi-row .crisp-datatable-phone-link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
849
+ & .crisp-datatable-phone-multi-grip { color: var(--crisp-fg-muted); flex-shrink: 0; cursor: grab; }
850
+ & .crisp-datatable-phone-multi-actions { display: flex; align-items: center; gap: var(--crisp-space-1); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
851
+ & .crisp-datatable-phone-multi-row:hover .crisp-datatable-phone-multi-actions,
852
+ & .crisp-datatable-phone-multi-actions:focus-within { opacity: 1; }
853
+ & .crisp-datatable-phone-multi-action { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; font-family: inherit; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; flex-shrink: 0; }
854
+ & .crisp-datatable-phone-multi-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
824
855
  /* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
825
856
  & .crisp-datatable-datetime-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
826
857
  /* rating cell type/editor (#760) -- measured live: 16px stars on an
@@ -50,9 +50,9 @@ export type DataCellType = "text" | "entity" | "tags" | "status" | "date" | "dat
50
50
  * country selection survives even for a national number that's still
51
51
  * being typed (an E.164 string alone can't distinguish "no country
52
52
  * chosen yet" from "the digits happen to start with a valid dial
53
- * code"). See `editor: "phone"` for the editable picker. Multi-value
54
- * (Attio's own "Multiple values" constraint on this type) is explicitly
55
- * out of scope -- tracked as its own follow-up, not this type. */
53
+ * code"). See `editor: "phone"` for the editable picker. See `multiValue`
54
+ * (crisp#816) for Attio's own "Multiple values" constraint on this
55
+ * type. */
56
56
  | "phone";
57
57
  export type DataTableColumn<T> = {
58
58
  key: string;
@@ -87,6 +87,26 @@ export type DataTableColumn<T> = {
87
87
  resizable?: boolean;
88
88
  /** Tone overrides for `status`/`tags` values (else a stable colour per value). */
89
89
  tones?: Record<string, string>;
90
+ /** `type: "date"`/`"datetime"` only: render this cell in Attio's own
91
+ * measured overdue/danger red (`fgDanger`, `#eb3b3b` — the same token
92
+ * `TasksTable`'s own overdue due-date already uses, crisp#462) when the
93
+ * predicate returns true. A per-row PREDICATE rather than `tones`'s
94
+ * value-keyed lookup, because a date's danger state is computed over the
95
+ * WHOLE ROW (e.g. a precomputed "overdue" bucket) — `tones` can only key
96
+ * off the formatted string, which a date doesn't have a closed set of
97
+ * (crisp#822). */
98
+ danger?: (value: unknown, row: T) => boolean;
99
+ /** This column holds more than one value per row -- Attio's own
100
+ * "Multiple values" constraint (crisp#816, confirmed live on the Phone
101
+ * Number attribute type: Constraints → Multiple values, a per-attribute
102
+ * toggle, absent for e.g. Text). Built out for `type`/`editor: "phone"`
103
+ * only this pass; the raw value is that type's normal per-value string,
104
+ * several of them joined with `;` (`"MN|88112233;US|4155550123"`) rather
105
+ * than JSON -- the same "a real separator earns its keep" reasoning
106
+ * `phone`'s own single-value `"{iso2}|{digits}"` shape already uses.
107
+ * Structural flag lives on the column (type-agnostic, per the issue)
108
+ * even though only one type renders it today. */
109
+ multiValue?: boolean;
90
110
  /** Enable click-to-sort on this column's header. */
91
111
  sortable?: boolean;
92
112
  /** Exclude this column from the search filter (default: included). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.55.1",
3
+ "version": "0.55.2",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",