@crisp-ui-kit/crisp 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/cjs/components/composewindow/composewindow.js +144 -0
  2. package/dist/cjs/components/composewindow/composewindow.recipe.js +45 -0
  3. package/dist/cjs/components/composewindow/index.js +8 -0
  4. package/dist/cjs/components/datatable/datatable.js +224 -43
  5. package/dist/cjs/components/datatable/datatable.recipe.js +37 -3
  6. package/dist/cjs/components/datatable/index.js +6 -0
  7. package/dist/cjs/components/datepickerfield/datepickerfield.js +25 -15
  8. package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +25 -8
  9. package/dist/cjs/components/dialog/dialog.recipe.js +9 -1
  10. package/dist/cjs/components/menu/menu.js +4 -2
  11. package/dist/cjs/components/multiselect/multiselect.recipe.js +14 -8
  12. package/dist/cjs/components/select/select.js +2 -1
  13. package/dist/cjs/components/table/table.recipe.js +12 -3
  14. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +1 -1
  15. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +18 -7
  16. package/dist/cjs/core/cell-date.js +11 -0
  17. package/dist/cjs/core/index.js +2 -1
  18. package/dist/cjs/index.js +1 -0
  19. package/dist/cjs/product/recordpage/recordpage.js +88 -10
  20. package/dist/cjs/product/recordpage/recordpage.recipe.js +30 -0
  21. package/dist/cjs/product/recordpanel/recordpanel.js +54 -4
  22. package/dist/cjs/product/recordpanel/recordpanel.recipe.js +29 -0
  23. package/dist/cjs/product/recordstable/editable-field.js +110 -0
  24. package/dist/cjs/product/recordstable/index.js +4 -1
  25. package/dist/cjs/product/recordstable/recordstable.js +67 -11
  26. package/dist/cjs/product/recordstable/recordstable.recipe.js +9 -1
  27. package/dist/cjs/product/settingsshell/settingsshell.recipe.js +13 -4
  28. package/dist/cjs/product/sidebar/sidebar.js +15 -3
  29. package/dist/cjs/product/sidebar/sidebar.recipe.js +21 -2
  30. package/dist/composewindow.css +24 -0
  31. package/dist/composewindow.d.ts +7 -0
  32. package/dist/composewindow.layered.css +26 -0
  33. package/dist/datatable.css +33 -14
  34. package/dist/datatable.layered.css +33 -14
  35. package/dist/datepickerfield.css +9 -4
  36. package/dist/datepickerfield.layered.css +9 -4
  37. package/dist/dialog.css +2 -1
  38. package/dist/dialog.layered.css +2 -1
  39. package/dist/esm/components/composewindow/composewindow.js +107 -0
  40. package/dist/esm/components/composewindow/composewindow.recipe.js +42 -0
  41. package/dist/esm/components/composewindow/index.js +2 -0
  42. package/dist/esm/components/datatable/datatable.js +226 -45
  43. package/dist/esm/components/datatable/datatable.recipe.js +37 -3
  44. package/dist/esm/components/datatable/index.js +6 -0
  45. package/dist/esm/components/datepickerfield/datepickerfield.js +26 -16
  46. package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +25 -8
  47. package/dist/esm/components/dialog/dialog.recipe.js +9 -1
  48. package/dist/esm/components/menu/menu.js +4 -2
  49. package/dist/esm/components/multiselect/multiselect.recipe.js +14 -8
  50. package/dist/esm/components/select/select.js +2 -1
  51. package/dist/esm/components/table/table.recipe.js +12 -3
  52. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +1 -1
  53. package/dist/esm/components/tabletoolbar/tabletoolbar.js +18 -7
  54. package/dist/esm/core/cell-date.js +10 -0
  55. package/dist/esm/core/index.js +1 -1
  56. package/dist/esm/index.js +1 -0
  57. package/dist/esm/product/recordpage/recordpage.js +89 -11
  58. package/dist/esm/product/recordpage/recordpage.recipe.js +30 -0
  59. package/dist/esm/product/recordpanel/recordpanel.js +55 -5
  60. package/dist/esm/product/recordpanel/recordpanel.recipe.js +29 -0
  61. package/dist/esm/product/recordstable/editable-field.js +73 -0
  62. package/dist/esm/product/recordstable/index.js +1 -0
  63. package/dist/esm/product/recordstable/recordstable.js +69 -13
  64. package/dist/esm/product/recordstable/recordstable.recipe.js +9 -1
  65. package/dist/esm/product/settingsshell/settingsshell.recipe.js +13 -4
  66. package/dist/esm/product/sidebar/sidebar.js +15 -3
  67. package/dist/esm/product/sidebar/sidebar.recipe.js +21 -2
  68. package/dist/multiselect.css +8 -8
  69. package/dist/multiselect.layered.css +8 -8
  70. package/dist/recordpage.css +25 -13
  71. package/dist/recordpage.layered.css +25 -13
  72. package/dist/recordpanel.css +22 -5
  73. package/dist/recordpanel.layered.css +22 -5
  74. package/dist/recordstable.css +9 -2
  75. package/dist/recordstable.layered.css +9 -2
  76. package/dist/settingsshell.css +2 -2
  77. package/dist/settingsshell.layered.css +2 -2
  78. package/dist/sidebar.css +7 -2
  79. package/dist/sidebar.layered.css +7 -2
  80. package/dist/styles.css +147 -54
  81. package/dist/styles.layered.css +147 -54
  82. package/dist/table.css +4 -2
  83. package/dist/table.layered.css +4 -2
  84. package/dist/tabletoolbar.css +1 -1
  85. package/dist/tabletoolbar.layered.css +1 -1
  86. package/dist/types/components/composewindow/composewindow.d.ts +80 -0
  87. package/dist/types/components/composewindow/composewindow.recipe.d.ts +3 -0
  88. package/dist/types/components/composewindow/index.d.ts +2 -0
  89. package/dist/types/components/datatable/datatable.d.ts +58 -4
  90. package/dist/types/components/datatable/index.d.ts +1 -1
  91. package/dist/types/components/datepickerfield/datepickerfield.d.ts +18 -4
  92. package/dist/types/components/menu/menu.d.ts +10 -1
  93. package/dist/types/components/select/select.d.ts +6 -1
  94. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +29 -5
  95. package/dist/types/core/cell-date.d.ts +17 -0
  96. package/dist/types/core/index.d.ts +1 -1
  97. package/dist/types/index.d.ts +1 -0
  98. package/dist/types/product/recordpage/recordpage.d.ts +59 -1
  99. package/dist/types/product/recordpanel/recordpanel.d.ts +11 -1
  100. package/dist/types/product/recordstable/editable-field.d.ts +45 -0
  101. package/dist/types/product/recordstable/index.d.ts +1 -0
  102. package/dist/types/product/recordstable/recordstable.d.ts +29 -0
  103. package/dist/types/product/sidebar/sidebar.d.ts +60 -4
  104. package/package.json +6 -1
@@ -262,15 +262,17 @@ function EntityCell({ value, pinned, onOpenPage, avatarTone = "brand", labels, }
262
262
  }
263
263
  function CellView({ column, row, pinned, onOpenPage, avatarTone = "brand", labels, }) {
264
264
  const value = row[column.key];
265
- // `render` fully owns the cell's content — no empty-dash fallback, no
266
- // type renderer; it's the only thing drawn here (see the doc comment on
265
+ // `render` fully owns the cell's content — no empty fallback, no type
266
+ // renderer; it's the only thing drawn here (see the doc comment on
267
267
  // `Column.render`).
268
268
  if (column.render)
269
269
  return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: column.render(value, row) });
270
270
  if (value == null ||
271
271
  value === "" ||
272
272
  (Array.isArray(value) && !value.length)) {
273
- return (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-muted", children: "\u2014" });
273
+ // Attio's own empty cell is genuinely blank -- no placeholder character
274
+ // at all (#412).
275
+ return null;
274
276
  }
275
277
  // `format` keeps the type renderer's chrome below and only replaces the
276
278
  // text it displays.
@@ -313,11 +315,40 @@ function CellView({ column, row, pinned, onOpenPage, avatarTone = "brand", label
313
315
  function StatusCell({ column, row, onChange, }) {
314
316
  return ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", "aria-label": column.label, placeholder: "\u2014", value: row[column.key] ?? undefined, onValueChange: onChange, options: column.options ?? [] }));
315
317
  }
318
+ // A sentinel that can't collide with a real record id (crisp never generates
319
+ // one, and a consumer's own ids come from their own data store) -- picking
320
+ // this row calls `onCreateReference`, not `onChange` (#449).
321
+ const CREATE_REFERENCE_VALUE = "__crisp_create_reference__";
322
+ /** The 16px avatar Attio's own reference picker draws before every option's
323
+ * label -- a photo when the option carries one, a generic person glyph
324
+ * otherwise (measured live, #449: Attio's own fallback is an icon, not
325
+ * initials -- a different, deliberate choice from the entity cell's own
326
+ * initials fallback elsewhere in this file). */
327
+ function referenceOptionLabel(o) {
328
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordstable-refoption", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", className: "crisp-recordstable-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] }));
329
+ }
316
330
  function ReferenceCell({ column, row, onChange, labels, }) {
317
- return ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", searchable: true, searchPlaceholder: labels.searchRecordsPlaceholder, "aria-label": column.label, placeholder: labels.setRecordPlaceholder, value: row[column.key] ?? undefined, onValueChange: onChange, options: (column.refOptions ?? []).map((o) => ({
318
- value: o.value,
319
- label: o.label,
320
- })) }));
331
+ const [query, setQuery] = React.useState("");
332
+ const options = (column.refOptions ?? []).map((o) => ({
333
+ value: o.value,
334
+ label: referenceOptionLabel(o),
335
+ keywords: o.label,
336
+ }));
337
+ if (column.onCreateReference) {
338
+ options.push({
339
+ value: CREATE_REFERENCE_VALUE,
340
+ label: ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordstable-refoption crisp-recordstable-refoption--create", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), `Create ${column.createLabel ?? "record"}`] })),
341
+ // always reachable regardless of the typed query -- it's an action,
342
+ // not a record to filter away
343
+ keywords: query,
344
+ });
345
+ }
346
+ return ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", searchable: true, searchPlaceholder: labels.searchRecordsPlaceholder, onSearchChange: setQuery, "aria-label": column.label, placeholder: labels.setRecordPlaceholder, value: row[column.key] ?? undefined, onValueChange: (v) => {
347
+ if (v === CREATE_REFERENCE_VALUE)
348
+ column.onCreateReference?.(query);
349
+ else
350
+ onChange(v);
351
+ }, options: options }));
321
352
  }
322
353
  function TagsCell({ column, row, options, onChange, labels, }) {
323
354
  const value = row[column.key] ?? [];
@@ -343,10 +374,35 @@ function TagsCell({ column, row, options, onChange, labels, }) {
343
374
  const core_2 = require("../../core");
344
375
  Object.defineProperty(exports, "formatCellDate", { enumerable: true, get: function () { return core_2.formatCellDate; } });
345
376
  Object.defineProperty(exports, "parseCellDate", { enumerable: true, get: function () { return core_2.parseCellDate; } });
377
+ // "1998" for a year-only raw value, "Sep 1998" for year-month, and the full
378
+ // "Sep 4, 1998" only once the raw value actually named a day (#312) --
379
+ // DatePickerField's own default format always prints full precision, since
380
+ // as a general calendar field it has no raw string to read a precision from.
381
+ function formatByPrecision(d, precision) {
382
+ if (precision === "year")
383
+ return String(d.getFullYear());
384
+ return d.toLocaleDateString(undefined, {
385
+ month: "short",
386
+ day: precision === "month" ? undefined : "numeric",
387
+ year: "numeric",
388
+ });
389
+ }
346
390
  function DateCell({ column, row, onChange, }) {
347
391
  const raw = row[column.key];
348
392
  const value = (0, core_2.parseCellDate)(raw);
349
- return ((0, jsx_runtime_1.jsx)(datepickerfield_1.DatePickerField, { variant: "ghost", "aria-label": column.label, value: value, onChange: (d) => onChange((0, core_2.formatCellDate)(d)) }));
393
+ const precision = raw ? (0, core_2.cellDatePrecision)(raw) : "day";
394
+ return ((0, jsx_runtime_1.jsx)(datepickerfield_1.DatePickerField, { variant: "ghost", "aria-label": column.label, value: value,
395
+ // Out of scope for #394 (which added Clear to DatePickerField itself):
396
+ // a cell's OWN clear-to-empty affordance is a different, unmeasured
397
+ // question, and this callback's `string` contract has no "empty" of
398
+ // its own to report yet. Keeps this cell's exact pre-#394 behavior.
399
+ clearable: false,
400
+ // #417: the trigger displays the value at its stored precision
401
+ // (year-only stays year-only) rather than always a full date.
402
+ format: (d) => formatByPrecision(d, precision), onChange: (d) => {
403
+ if (d)
404
+ onChange((0, core_2.formatCellDate)(d));
405
+ } }));
350
406
  }
351
407
  /**
352
408
  * The "New <object>" create modal — measured from a create-record dialog:
@@ -374,7 +430,7 @@ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate
374
430
  row[col.key] = { name: raw || labels.untitled };
375
431
  }
376
432
  else if (!raw) {
377
- // leave empty fields unset so the cell renders its "—" placeholder
433
+ // leave empty fields unset so the cell renders blank (#412)
378
434
  }
379
435
  else if (col.type === "tags") {
380
436
  row[col.key] = raw
@@ -406,7 +462,7 @@ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate
406
462
  return ((0, jsx_runtime_1.jsxs)(dialog_1.Dialog, { open: open, onOpenChange: onOpenChange, title: labels.newRecordTitle(entityLabel), icon: EntityIcon ? (0, jsx_runtime_1.jsx)(EntityIcon, { size: 16, strokeWidth: 1.75 }) : undefined, width: 720, children: [(0, jsx_runtime_1.jsx)(dialog_1.DialogBody, { children: columns.map((col, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-dialog-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "crisp-dialog-field-label", htmlFor: `create-${col.key}`, children: col.label }), (0, jsx_runtime_1.jsx)(input_1.Input, { id: `create-${col.key}`, ref: i === 0 ? firstRef : undefined, value: values[col.key] ?? "", onChange: (e) => set(col.key, e.currentTarget.value), placeholder: labels.setFieldPlaceholder(col.label), onKeyDown: (e) => {
407
463
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey))
408
464
  submit();
409
- } })] }, col.key))) }), (0, jsx_runtime_1.jsxs)(dialog_1.DialogFooter, { children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-dialog-more", children: [labels.createMore, (0, jsx_runtime_1.jsx)(switch_1.Switch, { checked: createMore, onCheckedChange: setCreateMore, "aria-label": labels.createMore })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-dialog-submit", onClick: submit, children: [labels.createRecord, (0, jsx_runtime_1.jsx)("span", { className: "crisp-dialog-submit-key", children: "\u2318\u21B5" })] })] })] }));
465
+ } })] }, col.key))) }), (0, jsx_runtime_1.jsxs)(dialog_1.DialogFooter, { children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-dialog-more", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { checked: createMore, onCheckedChange: setCreateMore, "aria-label": labels.createMore }), labels.createMore] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-dialog-submit", onClick: submit, children: [labels.createRecord, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-dialog-submit-key", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("kbd", { children: "\u2318" }), (0, jsx_runtime_1.jsx)("kbd", { children: (0, jsx_runtime_1.jsx)(lucide_react_1.CornerDownLeft, { size: 10, strokeWidth: 2 }) })] })] })] })] }));
410
466
  }
411
467
  /**
412
468
  * A measured-style record grid — fully featured. Toolbar (search / sort status /
@@ -1073,7 +1129,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
1073
1129
  setDragCol(col.key);
1074
1130
  e.dataTransfer.effectAllowed = "move";
1075
1131
  }
1076
- : undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 14, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-th-label", children: col.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-sort", children: (0, jsx_runtime_1.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
1132
+ : undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-th-label", children: col.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-sort", children: (0, jsx_runtime_1.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
1077
1133
  rootRef.current &&
1078
1134
  (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { ref: menuRef, className: "crisp-recordstable-menu crisp-recordstable-menu--fixed crisp-recordstable-menu--hdr", role: "menu", "aria-label": `${col.label} column`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "asc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { size: 15, strokeWidth: 1.75 }), t.sortAscending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "desc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { size: 15, strokeWidth: 1.75 }), t.sortDescending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveLeft] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i === 0 || i >= cols.length - 1, onClick: () => moveColumn(col.key, 1), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveRight] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordstable-menu-sep" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => {
1079
1135
  setRenamingCol(col.key);
@@ -74,6 +74,14 @@ ${(0, tabletoolbar_chrome_1.toolbarChromeBody)("recordstable")}
74
74
  & .crisp-recordstable-td--picker .crisp-multiselect-chips .crisp-recordstable-tag { flex-shrink: 0; max-width: none; }
75
75
  & .crisp-recordstable-td--picker .crisp-multiselect-chevron { display: none; }
76
76
  & .crisp-recordstable-td--picker .crisp-multiselect-placeholder { color: ${tokens_1.contract.fgMuted}; }
77
+ /* reference picker: a leading 16px avatar per option, plus a trailing
78
+ "Create X" row when the consumer supplies one (#449) -- measured live
79
+ against Attio's own picker (16px circular avatar, 16px plain icon for
80
+ the create row, both sitting in the option's existing 8px gap, no new
81
+ gap value invented). */
82
+ & .crisp-recordstable-refoption { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
83
+ & .crisp-recordstable-refoption-avatar { flex-shrink: 0; }
84
+ & .crisp-recordstable-refoption--create { color: ${tokens_1.contract.fgMuted}; }
77
85
  & .crisp-recordstable-status { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; }
78
86
  & .crisp-recordstable-status-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${tokens_1.contract.radius.full}; }
79
87
  & .crisp-recordstable-cell { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 100%; min-width: 0; }
@@ -132,7 +140,7 @@ ${(0, tabletoolbar_chrome_1.toolbarChromeBody)("recordstable")}
132
140
  & .crisp-recordstable-empty { display: flex; align-items: center; justify-content: center; min-height: 320px; padding: ${tokens_1.contract.space["8"]} ${tokens_1.contract.space["4"]}; }
133
141
 
134
142
  /* summary footer — per-column aggregation, sticky to the bottom */
135
- & .crisp-recordstable-sum { position: sticky; bottom: 0; z-index: 2; height: ${tokens_1.contract.size.tableRow}; padding: 0; border-top: 1px solid ${tokens_1.contract.borderSubtle}; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; background: ${tokens_1.contract.bgSurface}; }
143
+ & .crisp-recordstable-sum { position: sticky; bottom: 0; z-index: 2; height: ${tokens_1.contract.size.tableRow}; padding: 0; border-top: 1px solid ${tokens_1.contract.borderSubtle}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; background: ${tokens_1.contract.bgSurface}; }
136
144
  & tfoot .crisp-recordstable-col-check, & tfoot .crisp-recordstable-col-entity { position: sticky; bottom: 0; z-index: 3; }
137
145
  & .crisp-recordstable-sum:last-child { border-inline-end: none; }
138
146
  & .crisp-recordstable-sum-btn { display: flex; align-items: center; justify-content: flex-end; gap: ${tokens_1.contract.space["1.5"]}; width: 100%; height: 100%; margin: 0; padding: 0 ${tokens_1.contract.space["3"]}; border: none; background: none; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; cursor: pointer; white-space: nowrap; overflow: hidden; }
@@ -3,15 +3,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.settingsshellRecipe = void 0;
4
4
  const core_1 = require("../../core");
5
5
  const tokens_1 = require("../../tokens");
6
- // The Settings layout — measured from a Settings screen: a 256px left nav with a
6
+ // The Settings layout — measured from a Settings screen: a 275px left nav with a
7
7
  // back header, a search field, and grouped nav items (active row raised), beside
8
- // a scrolling content pane. Sits inside whatever height its container gives it.
8
+ // a scrolling content pane. Fills whatever height its container gives it, the
9
+ // way Attio's settings nav runs the container's full height (#435) — the root
10
+ // used to carry only `min-height: 0` (lets it *shrink* inside a flex ancestor)
11
+ // with no `height` of its own, so it sized to its content instead of filling,
12
+ // and the nav's own border-inline-end stopped where the nav content ended
13
+ // rather than running the full height. A consumer (GRC) had to paper over it
14
+ // with an external `h-full` class.
15
+ // 275px, not the 256px this recipe used to claim (#410) — the same width
16
+ // AppShell's own Attio-measured `defaultWidth` already uses for the main
17
+ // sidebar pane; re-measured live at app.attio.com/<workspace>/settings/account.
9
18
  const config = (0, core_1.defineRecipe)({
10
19
  name: "settingsshell",
11
- base: `display: flex; min-height: 0; box-sizing: border-box; font-family: ${tokens_1.contract.font.sans}; color: ${tokens_1.contract.fgPrimary};
20
+ base: `display: flex; height: 100%; min-height: 0; box-sizing: border-box; font-family: ${tokens_1.contract.font.sans}; color: ${tokens_1.contract.fgPrimary};
12
21
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
13
22
 
14
- & .crisp-settingsshell-nav { display: flex; flex-direction: column; width: 256px; flex-shrink: 0; min-height: 0; padding: ${tokens_1.contract.space["3"]}; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; }
23
+ & .crisp-settingsshell-nav { display: flex; flex-direction: column; width: 275px; flex-shrink: 0; min-height: 0; padding: ${tokens_1.contract.space["3"]}; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; }
15
24
  & .crisp-settingsshell-back { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 28px; padding: 0 ${tokens_1.contract.space["1"]}; margin: 0 0 ${tokens_1.contract.space["2"]}; border: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; color: ${tokens_1.contract.fgPrimary}; cursor: pointer; border-radius: ${tokens_1.contract.radius.md}; }
16
25
  & .crisp-settingsshell-back svg { color: ${tokens_1.contract.fgMuted}; }
17
26
  & .crisp-settingsshell-back:hover svg { color: ${tokens_1.contract.fgPrimary}; }
@@ -40,10 +40,20 @@ const React = __importStar(require("react"));
40
40
  const avatar_1 = require("../../components/avatar");
41
41
  const badge_1 = require("../../components/badge");
42
42
  const button_1 = require("../../components/button");
43
+ const menu_1 = require("../../components/menu");
43
44
  const core_1 = require("../../core");
44
45
  const command_1 = require("../command");
45
46
  const navitem_1 = require("../navitem");
46
47
  const sidebar_recipe_1 = require("./sidebar.recipe");
48
+ /** A plain `{ gettingStartedLabel, inviteLabel, trialText, trialActionLabel }`
49
+ * object (not a React element) — the shape `footer` renders as Attio's fixed
50
+ * onboarding/invite/trial copy, as opposed to a caller-supplied override. */
51
+ function isSidebarFooterCopy(value) {
52
+ return (typeof value === "object" &&
53
+ value !== null &&
54
+ !React.isValidElement(value) &&
55
+ "gettingStartedLabel" in value);
56
+ }
47
57
  const IconHome = ({ className }) => ((0, jsx_runtime_1.jsx)(lucide_react_1.House, { className: className, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
48
58
  const IconTasks = ({ className }) => ((0, jsx_runtime_1.jsx)(lucide_react_1.SquareCheck, { className: className, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
49
59
  const IconNotes = ({ className }) => ((0, jsx_runtime_1.jsx)(lucide_react_1.FileText, { className: className, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }));
@@ -117,7 +127,7 @@ exports.ATTIO_SIDEBAR_FOOTER = {
117
127
  function SectionHeader({ label, open, onToggle, }) {
118
128
  return ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-section-header", "data-collapsed": open ? undefined : "", "aria-expanded": open, onClick: onToggle, children: [(0, jsx_runtime_1.jsx)(IconChevronMini, {}), (0, jsx_runtime_1.jsx)("span", { children: label })] }));
119
129
  }
120
- exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = exports.ATTIO_SIDEBAR_WORKSPACE, items = exports.ATTIO_SIDEBAR_ITEMS, recordsSection = exports.ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = exports.ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = exports.ATTIO_SIDEBAR_CHATS_SECTION, groups, footer = exports.ATTIO_SIDEBAR_FOOTER, commandItems, commandOpen: commandOpenProp, defaultCommandOpen, onCommandOpenChange, commandShortcut = true, "aria-label": ariaLabel = "Main", ...props }, ref) => {
130
+ exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = exports.ATTIO_SIDEBAR_WORKSPACE, workspaceMenu, items = exports.ATTIO_SIDEBAR_ITEMS, recordsSection = exports.ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = exports.ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = exports.ATTIO_SIDEBAR_CHATS_SECTION, groups, footer = exports.ATTIO_SIDEBAR_FOOTER, commandItems, commandOpen: commandOpenProp, defaultCommandOpen, onCommandOpenChange, commandShortcut = true, "aria-label": ariaLabel = "Main", ...props }, ref) => {
121
131
  // Seeded once, on mount, from whatever groups start collapsed — not
122
132
  // reactive to `groups` changing later, matching every other piece of
123
133
  // this component's state (recordsSection/listsSection/chatsSection have
@@ -163,13 +173,15 @@ exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, coll
163
173
  return next;
164
174
  });
165
175
  }, []);
166
- return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.nav, { ref: ref, asChild: asChild, "aria-label": ariaLabel, className: (0, sidebar_recipe_1.sidebarRecipe)({ className }), ...props, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-ws", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), (0, jsx_runtime_1.jsx)(IconChevronDown, {}), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: (0, jsx_runtime_1.jsx)(IconCollapsePanel, {}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-searchrow", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-label", children: searchLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: (0, jsx_runtime_1.jsx)(IconInbox, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, children: label }, label))) }), groups ? (
176
+ const workspaceTrigger = ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-ws", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), (0, jsx_runtime_1.jsx)(IconChevronDown, {})] }));
177
+ return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.nav, { ref: ref, asChild: asChild, "aria-label": ariaLabel, className: (0, sidebar_recipe_1.sidebarRecipe)({ className }), ...props, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-ws-row", children: [workspaceMenu ? ((0, jsx_runtime_1.jsx)(menu_1.Menu, { trigger: workspaceTrigger, className: "crisp-sidebar-ws-menuroot", contentClassName: "crisp-sidebar-ws-menu-content", children: workspaceMenu })) : (workspaceTrigger), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: (0, jsx_runtime_1.jsx)(IconCollapsePanel, {}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-searchrow", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-label", children: searchLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: (0, jsx_runtime_1.jsx)(IconInbox, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing, href, onSelect }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: href ?? "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, onClick: onSelect, children: label }, href ?? label))) }), groups ? (
167
178
  // #325: navigation as data, reusing the exact section markup/
168
179
  // recipe above — SectionHeader + .crisp-sidebar-section — instead
169
180
  // of the three fixed Attio sections.
170
181
  groups.map((group) => {
171
182
  const groupKey = group.key ?? group.label;
172
183
  return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [(0, jsx_runtime_1.jsx)(SectionHeader, { label: group.label, open: !collapsedSections.has(groupKey), onToggle: () => toggleSection(groupKey) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has(groupKey) ? "" : undefined, children: group.items.map((item) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: item.href ?? "#", active: item.active, icon: item.icon ? (0, jsx_runtime_1.jsx)(item.icon, {}) : undefined, trailing: item.count, onClick: item.onSelect, children: item.label }, item.href ?? item.label))) })] }, groupKey));
173
- })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", shape: "rounded", tone: tone, children: (0, jsx_runtime_1.jsx)(Glyph, {}) }), children: label }, label))) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-sidebar-newlist", children: listsSection.newListLabel }) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), children: label }, label))) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-footer", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", className: "crisp-sidebar-pill", children: footer.gettingStartedLabel }), (0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(IconInvite, {}), children: footer.inviteLabel }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-trial", children: [(0, jsx_runtime_1.jsx)("span", { children: footer.trialText }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] }), (0, jsx_runtime_1.jsx)(command_1.Command, { open: commandOpen, onOpenChange: setCommandOpen, items: commandItems })] }));
184
+ })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", shape: "rounded", tone: tone, children: (0, jsx_runtime_1.jsx)(Glyph, {}) }), children: label }, label))) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-sidebar-newlist", children: listsSection.newListLabel }) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon, active, trailing, href, onSelect, }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: href ?? "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, onClick: onSelect, children: label }, href ?? label))) })] })), footer !== false &&
185
+ (isSidebarFooterCopy(footer) ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-footer", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", className: "crisp-sidebar-pill", children: footer.gettingStartedLabel }), (0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(IconInvite, {}), children: footer.inviteLabel }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-trial", children: [(0, jsx_runtime_1.jsx)("span", { children: footer.trialText }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] })) : ((0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-footer", children: footer }))), (0, jsx_runtime_1.jsx)(command_1.Command, { open: commandOpen, onOpenChange: setCommandOpen, items: commandItems })] }));
174
186
  });
175
187
  exports.Sidebar.displayName = "Sidebar";
@@ -3,6 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sidebarRecipe = void 0;
4
4
  const core_1 = require("../../core");
5
5
  const tokens_1 = require("../../tokens");
6
+ // The workspace switcher's menu (#415) is 280px wide on app.attio.com --
7
+ // measured live via getComputedStyle on its [role="menu"], not derived from
8
+ // the trigger's own width (which reads 295-296px including its collapse
9
+ // button) or from Menu's generic 200px content-sized min-width. Everything
10
+ // else about the popover -- 4px padding, 12px radius, 32px/8px-radius items,
11
+ // 14px/500 labels, the layered shadow -- already matches Menu's own measured
12
+ // chrome, confirmed against this specific menu rather than assumed from it.
6
13
  const config = (0, core_1.defineRecipe)({
7
14
  name: "sidebar",
8
15
  base: `box-sizing: border-box; display: flex; flex-direction: column; width: 275px; height: 100%; background: ${tokens_1.contract.bgSidebar}
@@ -15,10 +22,12 @@ const config = (0, core_1.defineRecipe)({
15
22
  & .crisp-sidebar-section-header { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; width: 100%; margin: 0; border: none; background: none; padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["0.5"]}; font-family: ${tokens_1.contract.font.sans}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; cursor: pointer; }
16
23
  & .crisp-sidebar-section-header svg { flex-shrink: 0; transition: transform .15s ease; }
17
24
  & .crisp-sidebar-section-header[data-collapsed] svg { transform: rotate(-90deg); }
18
- & .crisp-sidebar-ws { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.header}; padding: 0 ${tokens_1.contract.space["2"]}; border-radius: ${tokens_1.contract.radius.lg}; cursor: pointer; transition: background-color 0.16s; }
25
+ & .crisp-sidebar-ws-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.header}; padding: 0 ${tokens_1.contract.space["2"]}; }
26
+ & .crisp-sidebar-ws-menuroot { display: flex; flex: 1; min-width: 0; }
27
+ & .crisp-sidebar-ws { box-sizing: border-box; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex: 1; min-width: 0; height: 100%; margin: 0; padding: 0; border: none; background: none; border-radius: ${tokens_1.contract.radius.lg}; font-family: inherit; font-size: inherit; color: inherit; text-align: start; cursor: pointer; transition: background-color 0.16s; }
19
28
  & .crisp-sidebar-ws:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 5%, transparent); }
20
29
  & .crisp-sidebar-ws-name { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
21
- & .crisp-sidebar-ws-collapse { margin-inline-start: auto; }
30
+ & .crisp-sidebar-ws-collapse { flex-shrink: 0; }
22
31
  & .crisp-sidebar-searchrow { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; margin-top: ${tokens_1.contract.space["2.5"]}; padding-inline-end: ${tokens_1.contract.space["2"]}; }
23
32
  & .crisp-sidebar-footer { margin-top: auto; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["0.5"]}; }
24
33
  & .crisp-sidebar-trial { display: flex; align-items: center; justify-content: space-between; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; }
@@ -32,6 +41,16 @@ const config = (0, core_1.defineRecipe)({
32
41
  & .crisp-sidebar-newlist:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
33
42
 
34
43
  ${core_1.rtlFlipCss}
44
+ `,
45
+ // Menu's own floating content is portaled to document.body (#357), so a
46
+ // `&`-nested rule here -- which compiles to a real descendant combinator
47
+ // scoped to .crisp-sidebar -- matches nothing once the popover leaves the
48
+ // sidebar's DOM subtree. Same fix Menu's own recipe already uses: the
49
+ // content's override lives in globalCss instead, keyed off the
50
+ // \`contentClassName\` Menu now hands the workspace switcher's portaled
51
+ // surface (#415).
52
+ globalCss: `
53
+ .crisp-sidebar-ws-menu-content { width: 280px; }
35
54
  `,
36
55
  variants: {},
37
56
  defaultVariants: {},
@@ -0,0 +1,24 @@
1
+ .crisp-composewindow {
2
+ display: contents; font-family: var(--crisp-font-sans);
3
+ & .crisp-composewindow-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; pointer-events: none; }
4
+ & .crisp-composewindow-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh var(--crisp-space-4) var(--crisp-space-4); pointer-events: none; }
5
+ & .crisp-composewindow-content { pointer-events: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-height: 74vh; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-2xl); background: color-mix(in srgb, var(--crisp-bg-overlay-frost) 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: 0 8px 28px -6px var(--crisp-shadow-overlay1), 0 18px 88px -4px var(--crisp-shadow-overlay2), 0 0 1px var(--crisp-shadow-overlay3); opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
6
+ & .crisp-composewindow-content[data-minimized] { visibility: hidden; opacity: 0; }
7
+ & .crisp-composewindow-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); overflow: hidden; }
8
+ & .crisp-composewindow-header { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-2); flex-shrink: 0; height: 52px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
9
+ & .crisp-composewindow-breadcrumb { display: flex; align-items: center; gap: var(--crisp-space-1); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
10
+ & .crisp-composewindow-actions { display: flex; align-items: center; gap: var(--crisp-space-0-5); flex-shrink: 0; }
11
+ & .crisp-composewindow-action-btn { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
12
+ & .crisp-composewindow-action-btn:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
13
+ & .crisp-composewindow-action-btn[data-state="on"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-bg-brand-solid); }
14
+ & .crisp-composewindow-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; padding: var(--crisp-space-5) var(--crisp-space-4); }
15
+ & .crisp-composewindow-content-body { display: flex; flex-direction: column; gap: var(--crisp-space-4); }
16
+ }
17
+
18
+ @media (prefers-reduced-motion: reduce) {
19
+ .crisp-composewindow, .crisp-composewindow * {
20
+ animation-duration: 0.01ms !important;
21
+ animation-iteration-count: 1 !important;
22
+ transition-duration: 0.01ms !important;
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface ComposewindowVariants {
4
+
5
+ }
6
+
7
+ export declare const composewindowRecipe: RecipeFn<{ }>;
@@ -0,0 +1,26 @@
1
+ @layer crisp-components {
2
+ .crisp-composewindow {
3
+ display: contents; font-family: var(--crisp-font-sans);
4
+ & .crisp-composewindow-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; pointer-events: none; }
5
+ & .crisp-composewindow-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh var(--crisp-space-4) var(--crisp-space-4); pointer-events: none; }
6
+ & .crisp-composewindow-content { pointer-events: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-height: 74vh; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-2xl); background: color-mix(in srgb, var(--crisp-bg-overlay-frost) 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: 0 8px 28px -6px var(--crisp-shadow-overlay1), 0 18px 88px -4px var(--crisp-shadow-overlay2), 0 0 1px var(--crisp-shadow-overlay3); opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
7
+ & .crisp-composewindow-content[data-minimized] { visibility: hidden; opacity: 0; }
8
+ & .crisp-composewindow-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); overflow: hidden; }
9
+ & .crisp-composewindow-header { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-2); flex-shrink: 0; height: 52px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
10
+ & .crisp-composewindow-breadcrumb { display: flex; align-items: center; gap: var(--crisp-space-1); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
11
+ & .crisp-composewindow-actions { display: flex; align-items: center; gap: var(--crisp-space-0-5); flex-shrink: 0; }
12
+ & .crisp-composewindow-action-btn { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
13
+ & .crisp-composewindow-action-btn:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
14
+ & .crisp-composewindow-action-btn[data-state="on"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-bg-brand-solid); }
15
+ & .crisp-composewindow-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; padding: var(--crisp-space-5) var(--crisp-space-4); }
16
+ & .crisp-composewindow-content-body { display: flex; flex-direction: column; gap: var(--crisp-space-4); }
17
+ }
18
+
19
+ @media (prefers-reduced-motion: reduce) {
20
+ .crisp-composewindow, .crisp-composewindow * {
21
+ animation-duration: 0.01ms !important;
22
+ animation-iteration-count: 1 !important;
23
+ transition-duration: 0.01ms !important;
24
+ }
25
+ }
26
+ }
@@ -32,10 +32,13 @@
32
32
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
33
33
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
34
34
  & .crisp-datatable-sort { display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; font: inherit; letter-spacing: inherit; color: inherit; text-align: start; cursor: pointer; }
35
- & th[data-align="right"] .crisp-datatable-sort { justify-content: flex-end; padding-inline-end: 0; }
36
- & th[data-align="right"] .crisp-datatable-th-inner { justify-content: flex-end; }
35
+ /* A right-aligned column's header keeps its field-icon + label packed at
36
+ the START, matching its own values only in that the VALUES right-align
37
+ -- Attio's own right-aligned header (measured live: "Twitter follower
38
+ count") keeps the icon+label left and only the trailing column-menu icon sits pinned to the right edge, independent of the label's width
39
+ (#406, reversing #393's own justify-content: flex-end here). The chevron's own margin-inline-start: auto below already pins IT to the
40
+ end of a flex-start row with no override needed -- that's what pins the trailing icon on Attio too. */
37
41
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
38
- & th[data-align="right"] .crisp-datatable-chevron { margin-inline-start: 0; order: -1; }
39
42
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
40
43
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
41
44
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
@@ -58,6 +61,28 @@
58
61
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
59
62
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; vertical-align: middle; }
60
63
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
64
+ /* An async onEdit's in-flight state (#385) -- the field/picker dims and
65
+ stops accepting input rather than disappearing, so the just-typed or
66
+ just-chosen value stays visible while its commit is pending. Same
67
+ treatment Input already gives its own disabled state. */
68
+ & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
69
+ & .crisp-datatable-edit-pending { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); color: var(--crisp-fg-muted); vertical-align: middle; }
70
+ /* The rejection's message, beside the cell's own content -- a sibling in
71
+ the TableCell, never nested inside a column's own render output, so a hover-only wrapper (a Tooltip, say) around a custom render can't hide
72
+ it. Block-level: the row grows to fit it rather than floating over
73
+ the row below, so it can never end up clipped by the scroll
74
+ container's overflow the way a position:fixed escape would need guarding against. */
75
+ & .crisp-datatable-edit-error { display: block; padding: var(--crisp-space-0-5) 0 0; font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
76
+ /* editor: "select"/"switch" -- always live, no separate edit mode, so the cell IS the picker: same chromeless-fill treatment as RecordsTable's own --picker cells (measured there already; DataTable
77
+ shares RecordsTable's grid, Golden Rule 0). */
78
+ & .crisp-datatable-td--picker { padding: 0; }
79
+ & .crisp-datatable-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
80
+ & .crisp-datatable-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; height: 100%; min-height: 0; border-radius: 0; box-shadow: none; padding: 0; }
81
+ & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: var(--crisp-space-2); }
82
+ /* reference picker: a leading 16px avatar per option, plus a trailing "Create X" row when the consumer supplies one (#449) -- same measured values as RecordsTable's own .crisp-recordstable-refoption*. */
83
+ & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
84
+ & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
85
+ & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
61
86
  & .crisp-datatable-empty { padding: var(--crisp-space-8) var(--crisp-space-3); text-align: center; color: var(--crisp-fg-muted); font-size: var(--crisp-text-lg); }
62
87
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
63
88
  & table { table-layout: fixed; }
@@ -69,12 +94,7 @@
69
94
  & .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
70
95
  & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
71
96
  & .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
72
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
73
- & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
74
- & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
75
- & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
76
- & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
77
- & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
97
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); } & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); } & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); } & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); } & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); } & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
78
98
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
79
99
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
80
100
  & .crisp-datatable-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); }
@@ -87,11 +107,10 @@
87
107
  & .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)); }
88
108
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
89
109
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
90
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
91
- hairline doubling as the drop indicator (it tints toward the brand
92
- colour as the active drop target). The affordance + geometry are
93
- measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
94
- pointer-events implementation. */
110
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
111
+ a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
112
+ draggable + dataTransfer, the same one column reorder already uses
113
+ (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
95
114
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
96
115
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
97
116
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -33,10 +33,13 @@
33
33
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
34
34
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
35
35
  & .crisp-datatable-sort { display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; font: inherit; letter-spacing: inherit; color: inherit; text-align: start; cursor: pointer; }
36
- & th[data-align="right"] .crisp-datatable-sort { justify-content: flex-end; padding-inline-end: 0; }
37
- & th[data-align="right"] .crisp-datatable-th-inner { justify-content: flex-end; }
36
+ /* A right-aligned column's header keeps its field-icon + label packed at
37
+ the START, matching its own values only in that the VALUES right-align
38
+ -- Attio's own right-aligned header (measured live: "Twitter follower
39
+ count") keeps the icon+label left and only the trailing column-menu icon sits pinned to the right edge, independent of the label's width
40
+ (#406, reversing #393's own justify-content: flex-end here). The chevron's own margin-inline-start: auto below already pins IT to the
41
+ end of a flex-start row with no override needed -- that's what pins the trailing icon on Attio too. */
38
42
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
39
- & th[data-align="right"] .crisp-datatable-chevron { margin-inline-start: 0; order: -1; }
40
43
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
41
44
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
42
45
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
@@ -59,6 +62,28 @@
59
62
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
60
63
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; vertical-align: middle; }
61
64
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
65
+ /* An async onEdit's in-flight state (#385) -- the field/picker dims and
66
+ stops accepting input rather than disappearing, so the just-typed or
67
+ just-chosen value stays visible while its commit is pending. Same
68
+ treatment Input already gives its own disabled state. */
69
+ & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
70
+ & .crisp-datatable-edit-pending { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); color: var(--crisp-fg-muted); vertical-align: middle; }
71
+ /* The rejection's message, beside the cell's own content -- a sibling in
72
+ the TableCell, never nested inside a column's own render output, so a hover-only wrapper (a Tooltip, say) around a custom render can't hide
73
+ it. Block-level: the row grows to fit it rather than floating over
74
+ the row below, so it can never end up clipped by the scroll
75
+ container's overflow the way a position:fixed escape would need guarding against. */
76
+ & .crisp-datatable-edit-error { display: block; padding: var(--crisp-space-0-5) 0 0; font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
77
+ /* editor: "select"/"switch" -- always live, no separate edit mode, so the cell IS the picker: same chromeless-fill treatment as RecordsTable's own --picker cells (measured there already; DataTable
78
+ shares RecordsTable's grid, Golden Rule 0). */
79
+ & .crisp-datatable-td--picker { padding: 0; }
80
+ & .crisp-datatable-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
81
+ & .crisp-datatable-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; height: 100%; min-height: 0; border-radius: 0; box-shadow: none; padding: 0; }
82
+ & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: var(--crisp-space-2); }
83
+ /* reference picker: a leading 16px avatar per option, plus a trailing "Create X" row when the consumer supplies one (#449) -- same measured values as RecordsTable's own .crisp-recordstable-refoption*. */
84
+ & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
85
+ & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
86
+ & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
62
87
  & .crisp-datatable-empty { padding: var(--crisp-space-8) var(--crisp-space-3); text-align: center; color: var(--crisp-fg-muted); font-size: var(--crisp-text-lg); }
63
88
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
64
89
  & table { table-layout: fixed; }
@@ -70,12 +95,7 @@
70
95
  & .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
71
96
  & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
72
97
  & .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
73
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
74
- & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
75
- & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
76
- & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
77
- & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
78
- & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
98
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); } & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); } & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); } & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); } & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); } & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
79
99
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
80
100
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
81
101
  & .crisp-datatable-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); }
@@ -88,11 +108,10 @@
88
108
  & .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)); }
89
109
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
90
110
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
91
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
92
- hairline doubling as the drop indicator (it tints toward the brand
93
- colour as the active drop target). The affordance + geometry are
94
- measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
95
- pointer-events implementation. */
111
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
112
+ a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
113
+ draggable + dataTransfer, the same one column reorder already uses
114
+ (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
96
115
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
97
116
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
98
117
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -1,10 +1,15 @@
1
1
  .crisp-datepickerfield {
2
2
  display: inline-flex; font-family: var(--crisp-font-sans);
3
- & .crisp-datepickerfield-trigger { display: inline-flex; align-items: center; gap: var(--crisp-space-2); box-sizing: border-box; height: 32px; padding: 0 var(--crisp-space-2-5); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); white-space: nowrap; cursor: pointer; }
4
- & .crisp-datepickerfield-trigger[data-ghost] { background: transparent; box-shadow: none; }
5
- & .crisp-datepickerfield-trigger[data-ghost]:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
6
- & .crisp-datepickerfield-trigger[data-placeholder] { color: var(--crisp-fg-muted); }
3
+ & .crisp-datepickerfield-box { display: inline-flex; align-items: center; box-sizing: border-box; height: 32px; margin: 0; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); }
4
+ & .crisp-datepickerfield-box[data-ghost] { background: transparent; box-shadow: none; }
5
+ & .crisp-datepickerfield-box[data-ghost]:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
6
+ & .crisp-datepickerfield-trigger { display: inline-flex; align-items: center; gap: var(--crisp-space-2); height: 100%; padding: 0 var(--crisp-space-2-5); margin: 0; border: none; background: none; color: inherit; font-family: inherit; font-size: inherit; white-space: nowrap; cursor: pointer; }
7
+ & .crisp-datepickerfield-box[data-placeholder] .crisp-datepickerfield-trigger { color: var(--crisp-fg-muted); }
7
8
  & .crisp-datepickerfield-trigger svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
9
+ /* Reuses TableToolbar's measured filter-chip remove button
10
+ (tabletoolbar.chrome.ts's .crisp-tabletoolbar-fchip-x) -- the same small inline "clear this value" affordance, not a freshly-measured one: no live Attio composition for a DatePickerField-equivalent clear was reachable this round (#394). */
11
+ & .crisp-datepickerfield-clear { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin-inline-end: var(--crisp-space-1); padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
12
+ & .crisp-datepickerfield-clear:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 10%, transparent); color: var(--crisp-fg-primary); }
8
13
  & .crisp-datepickerfield-panel { padding: var(--crisp-space-2); width: 300px; }
9
14
  & .crisp-datepickerfield-presets { display: flex; flex-wrap: wrap; gap: var(--crisp-space-1); margin-top: var(--crisp-space-2); }
10
15
  & .crisp-datepickerfield-preset { height: var(--crisp-size-chip); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-chip); background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-sm); cursor: pointer; }