@crisp-ui-kit/crisp 0.52.4 → 0.52.6

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 (78) hide show
  1. package/dist/board.css +6 -6
  2. package/dist/board.layered.css +6 -6
  3. package/dist/bulkbar.css +2 -2
  4. package/dist/bulkbar.layered.css +2 -2
  5. package/dist/cjs/components/bulkbar/bulkbar.js +1 -1
  6. package/dist/cjs/components/bulkbar/bulkbar.recipe.js +2 -2
  7. package/dist/cjs/components/datatable/datatable.recipe.js +4 -1
  8. package/dist/cjs/components/input/input.recipe.js +6 -1
  9. package/dist/cjs/components/multiselect/multiselect.js +1 -1
  10. package/dist/cjs/components/multiselect/multiselect.recipe.js +1 -1
  11. package/dist/cjs/components/switch/switch.recipe.js +4 -0
  12. package/dist/cjs/components/table/table.recipe.js +5 -1
  13. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +5 -4
  14. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +1 -1
  15. package/dist/cjs/components/textarea/textarea.recipe.js +6 -1
  16. package/dist/cjs/components/tooltip/tooltip.recipe.js +13 -4
  17. package/dist/cjs/product/board/board.recipe.js +16 -6
  18. package/dist/cjs/product/entitycard/entitycard.recipe.js +20 -4
  19. package/dist/cjs/product/listpage/listpage.js +1 -1
  20. package/dist/cjs/product/listpage/listpage.recipe.js +5 -7
  21. package/dist/cjs/tokens/elevation.js +17 -2
  22. package/dist/cjs/tokens/palette.js +17 -0
  23. package/dist/cjs/tokens/semantic.js +2 -0
  24. package/dist/datatable.css +30 -24
  25. package/dist/datatable.layered.css +30 -24
  26. package/dist/entitycard.css +4 -4
  27. package/dist/entitycard.layered.css +4 -4
  28. package/dist/esm/components/bulkbar/bulkbar.js +1 -1
  29. package/dist/esm/components/bulkbar/bulkbar.recipe.js +2 -2
  30. package/dist/esm/components/datatable/datatable.recipe.js +4 -1
  31. package/dist/esm/components/input/input.recipe.js +6 -1
  32. package/dist/esm/components/multiselect/multiselect.js +1 -1
  33. package/dist/esm/components/multiselect/multiselect.recipe.js +1 -1
  34. package/dist/esm/components/switch/switch.recipe.js +4 -0
  35. package/dist/esm/components/table/table.recipe.js +5 -1
  36. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +5 -4
  37. package/dist/esm/components/tabletoolbar/tabletoolbar.js +1 -1
  38. package/dist/esm/components/textarea/textarea.recipe.js +6 -1
  39. package/dist/esm/components/tooltip/tooltip.recipe.js +13 -4
  40. package/dist/esm/product/board/board.recipe.js +16 -6
  41. package/dist/esm/product/entitycard/entitycard.recipe.js +20 -4
  42. package/dist/esm/product/listpage/listpage.js +2 -2
  43. package/dist/esm/product/listpage/listpage.recipe.js +5 -7
  44. package/dist/esm/tokens/elevation.js +17 -2
  45. package/dist/esm/tokens/palette.js +17 -0
  46. package/dist/esm/tokens/semantic.js +2 -0
  47. package/dist/input.css +1 -1
  48. package/dist/input.layered.css +1 -1
  49. package/dist/listpage.css +5 -5
  50. package/dist/listpage.layered.css +5 -5
  51. package/dist/multiselect.css +1 -1
  52. package/dist/multiselect.layered.css +1 -1
  53. package/dist/native/tokens.d.ts +1 -1
  54. package/dist/native/tokens.js +8 -0
  55. package/dist/recordstable.css +5 -4
  56. package/dist/recordstable.layered.css +5 -4
  57. package/dist/styles.css +69 -55
  58. package/dist/styles.layered.css +69 -55
  59. package/dist/switch.css +1 -1
  60. package/dist/switch.layered.css +1 -1
  61. package/dist/table.css +3 -1
  62. package/dist/table.layered.css +3 -1
  63. package/dist/tabletoolbar.css +5 -4
  64. package/dist/tabletoolbar.layered.css +5 -4
  65. package/dist/textarea.css +1 -1
  66. package/dist/textarea.layered.css +1 -1
  67. package/dist/tokens.css +4 -0
  68. package/dist/tokens.json +8 -0
  69. package/dist/tooltip.css +1 -1
  70. package/dist/tooltip.layered.css +1 -1
  71. package/dist/types/components/bulkbar/bulkbar.d.ts +3 -3
  72. package/dist/types/components/switch/switch.recipe.d.ts +1 -0
  73. package/dist/types/components/tooltip/tooltip.recipe.d.ts +2 -2
  74. package/dist/types/tokens/contract.d.ts +2 -0
  75. package/dist/types/tokens/elevation.d.ts +17 -2
  76. package/dist/types/tokens/palette.d.ts +8 -0
  77. package/dist/types/tokens/semantic.d.ts +2 -0
  78. package/package.json +1 -1
@@ -31,13 +31,15 @@
31
31
  & .crisp-datatable-th-rename { width: 100%; min-width: 0; height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-1); border: none; border-radius: var(--crisp-radius-md); 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); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); outline: none; }
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
- & .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
- /* 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. */
34
+ /* #465: 12px per the comment above (and Attio's measured header cell
35
+ padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
36
+ own documented value. */
37
+ & .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-3); border: none; background: none; font: inherit; letter-spacing: inherit; color: inherit; text-align: start; cursor: pointer; }
38
+ /* A right-aligned column's header keeps its field-icon + label packed at the START, matching its own values only in that the VALUES right-align -- Attio's own right-aligned header (measured live: "Twitter follower
39
+ count") keeps the icon+label left and only the trailing column-menu
40
+ icon sits pinned to the right edge, independent of the label's width (#406, reversing #393's own justify-content: flex-end here). The
41
+ chevron's own margin-inline-start: auto below already pins IT to the end of a flex-start row with no override needed -- that's what pins
42
+ the trailing icon on Attio too. */
41
43
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
42
44
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
43
45
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
@@ -61,25 +63,23 @@
61
63
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
62
64
  & .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; }
63
65
  & .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. */
66
+ /* An async onEdit's in-flight state (#385) -- the field/picker dims and stops accepting input rather than disappearing, so the just-typed or just-chosen value stays visible while its commit is pending. Same treatment Input already gives its own disabled state. */
68
67
  & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
69
68
  & .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. */
69
+ /* The rejection's message, beside the cell's own content -- a sibling in the TableCell, never nested inside a column's own render output, so a
70
+ hover-only wrapper (a Tooltip, say) around a custom render can't hide it. Block-level: the row grows to fit it rather than floating over the row below, so it can never end up clipped by the scroll container's overflow the way a position:fixed escape would need
71
+ guarding against. */
75
72
  & .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). */
73
+ /* editor: "select"/"switch" -- always live, no separate edit mode, so
74
+ the cell IS the picker: same chromeless-fill treatment as
75
+ RecordsTable's own --picker cells (measured there already; DataTable shares RecordsTable's grid, Golden Rule 0). */
78
76
  & .crisp-datatable-td--picker { padding: 0; }
79
77
  & .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
78
  & .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
79
  & .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*. */
80
+ /* reference picker: a leading 16px avatar per option, plus a trailing
81
+ "Create X" row when the consumer supplies one (#449) -- same measured
82
+ values as RecordsTable's own .crisp-recordstable-refoption*. */
83
83
  & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
84
84
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
85
85
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
@@ -94,7 +94,12 @@
94
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); }
95
95
  & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
96
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); }
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); }
97
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
98
+ & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
99
+ & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
100
+ & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
101
+ & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
102
+ & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
98
103
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
99
104
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
100
105
  & .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); }
@@ -107,10 +112,11 @@
107
112
  & .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)); }
108
113
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
109
114
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
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. */
115
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
116
+ hairline doubling as the drop indicator (it tints toward the brand
117
+ colour as the active drop target). The affordance + geometry are
118
+ 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
119
+ pointer-events implementation. */
114
120
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
115
121
  & .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; }
116
122
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -32,13 +32,15 @@
32
32
  & .crisp-datatable-th-rename { width: 100%; min-width: 0; height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-1); border: none; border-radius: var(--crisp-radius-md); 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); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); outline: none; }
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
- & .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
- /* 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. */
35
+ /* #465: 12px per the comment above (and Attio's measured header cell
36
+ padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
37
+ own documented value. */
38
+ & .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-3); border: none; background: none; font: inherit; letter-spacing: inherit; color: inherit; text-align: start; cursor: pointer; }
39
+ /* A right-aligned column's header keeps its field-icon + label packed at the START, matching its own values only in that the VALUES right-align -- Attio's own right-aligned header (measured live: "Twitter follower
40
+ count") keeps the icon+label left and only the trailing column-menu
41
+ icon sits pinned to the right edge, independent of the label's width (#406, reversing #393's own justify-content: flex-end here). The
42
+ chevron's own margin-inline-start: auto below already pins IT to the end of a flex-start row with no override needed -- that's what pins
43
+ the trailing icon on Attio too. */
42
44
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
43
45
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
44
46
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
@@ -62,25 +64,23 @@
62
64
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
63
65
  & .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; }
64
66
  & .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. */
67
+ /* An async onEdit's in-flight state (#385) -- the field/picker dims and stops accepting input rather than disappearing, so the just-typed or just-chosen value stays visible while its commit is pending. Same treatment Input already gives its own disabled state. */
69
68
  & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
70
69
  & .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. */
70
+ /* The rejection's message, beside the cell's own content -- a sibling in the TableCell, never nested inside a column's own render output, so a
71
+ hover-only wrapper (a Tooltip, say) around a custom render can't hide it. Block-level: the row grows to fit it rather than floating over the row below, so it can never end up clipped by the scroll container's overflow the way a position:fixed escape would need
72
+ guarding against. */
76
73
  & .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). */
74
+ /* editor: "select"/"switch" -- always live, no separate edit mode, so
75
+ the cell IS the picker: same chromeless-fill treatment as
76
+ RecordsTable's own --picker cells (measured there already; DataTable shares RecordsTable's grid, Golden Rule 0). */
79
77
  & .crisp-datatable-td--picker { padding: 0; }
80
78
  & .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
79
  & .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
80
  & .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*. */
81
+ /* reference picker: a leading 16px avatar per option, plus a trailing
82
+ "Create X" row when the consumer supplies one (#449) -- same measured
83
+ values as RecordsTable's own .crisp-recordstable-refoption*. */
84
84
  & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
85
85
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
86
86
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
@@ -95,7 +95,12 @@
95
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); }
96
96
  & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
97
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); }
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); }
98
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
99
+ & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
100
+ & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
101
+ & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
102
+ & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
103
+ & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
99
104
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
100
105
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
101
106
  & .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); }
@@ -108,10 +113,11 @@
108
113
  & .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)); }
109
114
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
110
115
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
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. */
116
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
117
+ hairline doubling as the drop indicator (it tints toward the brand
118
+ colour as the active drop target). The affordance + geometry are
119
+ 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
120
+ pointer-events implementation. */
115
121
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
116
122
  & .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; }
117
123
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -2,11 +2,11 @@
2
2
  display: flex; flex-direction: column; box-sizing: border-box; width: 100%; min-width: 0; text-align: start; padding: var(--crisp-space-3) var(--crisp-space-4); margin: 0; border: none; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); color: var(--crisp-fg-primary); font-family: var(--crisp-font-sans); cursor: pointer; transition: box-shadow .12s ease, background-color .12s ease; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
3
  &:hover { box-shadow: inset 0 0 0 1px var(--crisp-border-default), 0 1px 2px rgba(0,0,0,0.04), 0 6px 16px -6px rgba(28,40,64,0.12); }
4
4
  &:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 2px; }
5
- & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
6
- & .crisp-entitycard-title { margin-top: var(--crisp-space-2); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.1px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
7
- & .crisp-entitycard-excerpt { margin-top: var(--crisp-space-1); font-size: var(--crisp-text-md); line-height: 18px; color: var(--crisp-fg-muted); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
5
+ & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
6
+ & .crisp-entitycard-title { margin-top: var(--crisp-space-2); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.1px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
7
+ & .crisp-entitycard-excerpt { margin-top: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
8
8
  & .crisp-entitycard-spacer { flex: 1; min-height: var(--crisp-space-3); }
9
- & .crisp-entitycard-foot { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding-top: var(--crisp-space-2-5); margin-top: var(--crisp-space-1); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
9
+ & .crisp-entitycard-foot { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding-top: var(--crisp-space-2-5); margin-top: var(--crisp-space-1); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-sm); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
10
10
  & .crisp-entitycard-foot-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--crisp-fg-primary); }
11
11
  & .crisp-entitycard-date { margin-inline-start: auto; flex-shrink: 0; white-space: nowrap; }
12
12
  }
@@ -3,11 +3,11 @@
3
3
  display: flex; flex-direction: column; box-sizing: border-box; width: 100%; min-width: 0; text-align: start; padding: var(--crisp-space-3) var(--crisp-space-4); margin: 0; border: none; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); color: var(--crisp-fg-primary); font-family: var(--crisp-font-sans); cursor: pointer; transition: box-shadow .12s ease, background-color .12s ease; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
4
  &:hover { box-shadow: inset 0 0 0 1px var(--crisp-border-default), 0 1px 2px rgba(0,0,0,0.04), 0 6px 16px -6px rgba(28,40,64,0.12); }
5
5
  &:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 2px; }
6
- & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
7
- & .crisp-entitycard-title { margin-top: var(--crisp-space-2); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.1px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
8
- & .crisp-entitycard-excerpt { margin-top: var(--crisp-space-1); font-size: var(--crisp-text-md); line-height: 18px; color: var(--crisp-fg-muted); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
6
+ & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
7
+ & .crisp-entitycard-title { margin-top: var(--crisp-space-2); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.1px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
8
+ & .crisp-entitycard-excerpt { margin-top: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
9
9
  & .crisp-entitycard-spacer { flex: 1; min-height: var(--crisp-space-3); }
10
- & .crisp-entitycard-foot { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding-top: var(--crisp-space-2-5); margin-top: var(--crisp-space-1); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
10
+ & .crisp-entitycard-foot { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding-top: var(--crisp-space-2-5); margin-top: var(--crisp-space-1); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-sm); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
11
11
  & .crisp-entitycard-foot-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--crisp-fg-primary); }
12
12
  & .crisp-entitycard-date { margin-inline-start: auto; flex-shrink: 0; white-space: nowrap; }
13
13
  }
@@ -7,7 +7,7 @@ import { bulkbarRecipe } from "./bulkbar.recipe.js";
7
7
  * positioned ancestor when a selection is active; render it conditionally
8
8
  * (e.g. `count > 0 && <BulkBar …/>`). Measured from a records table.
9
9
  */
10
- export function BulkBar({ count, onClear, countLabel = "selected", countText = (count, label) => (_jsxs(_Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
10
+ export function BulkBar({ count, onClear, countLabel = "Selected", countText = (count, label) => (_jsxs(_Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
11
11
  return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-bulkbar-bar", children: [count != null && (_jsxs("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), _jsx("span", { className: "crisp-bulkbar-clearlabel", children: clearLabel }), _jsx(X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
12
12
  }
13
13
  BulkBar.displayName = "BulkBar";
@@ -12,7 +12,7 @@ const config = defineRecipe({
12
12
  base: `position: absolute; left: 0; right: 0; bottom: ${c.space["4"]}; display: flex; justify-content: center; pointer-events: none; z-index: 10;
13
13
  & .crisp-bulkbar-bar { pointer-events: auto; display: flex; align-items: center; gap: ${c.space["0.5"]}; height: 48px; padding: 0 ${c.space["1"]} 0 ${c.space["1.5"]}; border-radius: 14px; background: ${c.bgSurface}; box-shadow: ${e.floating}; color: ${c.fgPrimary}; transition: opacity .16s ease, transform .16s ease; }
14
14
  & .crisp-bulkbar-bar { @starting-style { opacity: 0; transform: translateY(10px); } }
15
- & .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; margin: 0; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
15
+ & .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; margin: 0; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.lg}; background: color-mix(in srgb, ${c.fgPrimary} 2%, transparent); font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
16
16
  & .crisp-bulkbar-count:hover { background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); color: ${c.fgPrimary}; }
17
17
  & .crisp-bulkbar-count svg { flex-shrink: 0; }
18
18
  /* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
@@ -21,7 +21,7 @@ const config = defineRecipe({
21
21
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
22
22
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
23
23
  }
24
- & .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: transparent; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
24
+ & .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.lg}; background: ${c.bgSurface}; box-shadow: ${e.field}; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
25
25
  & .crisp-bulkbar-btn:hover { background: ${c.bgRaised}; }
26
26
  & .crisp-bulkbar-btn--danger { color: ${c.bgDanger}; }
27
27
  & .crisp-bulkbar-btn--danger:hover { background: color-mix(in srgb, ${c.bgDanger} 10%, transparent); }
@@ -47,7 +47,10 @@ const config = defineRecipe({
47
47
  & .crisp-datatable-th-rename { width: 100%; min-width: 0; height: ${c.size.row}; margin: 0; padding: 0 ${c.space["1"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; outline: none; }
48
48
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
49
49
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
50
- & .crisp-datatable-sort { display: flex; align-items: center; gap: ${c.space["1.5"]}; width: 100%; min-width: 0; height: 100%; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; font: inherit; letter-spacing: inherit; color: inherit; text-align: start; cursor: pointer; }
50
+ /* #465: 12px per the comment above (and Attio's measured header cell
51
+ padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
52
+ own documented value. */
53
+ & .crisp-datatable-sort { display: flex; align-items: center; gap: ${c.space["1.5"]}; width: 100%; min-width: 0; height: 100%; margin: 0; padding: 0 ${c.space["3"]}; border: none; background: none; font: inherit; letter-spacing: inherit; color: inherit; text-align: start; cursor: pointer; }
51
54
  /* A right-aligned column's header keeps its field-icon + label packed at
52
55
  the START, matching its own values only in that the VALUES right-align
53
56
  -- Attio's own right-aligned header (measured live: "Twitter follower
@@ -1,6 +1,11 @@
1
1
  import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
2
  import { contract as c, elevation as e } from "../../tokens/index.js";
3
3
  // Geometry is measured: 32/36px tall, r8/r10, 10/12px side padding.
4
+ // The shadow is `elevation.textField` (#466), not `elevation.field` — a real
5
+ // typed field on Attio's own Settings page carries one 1px ring, not
6
+ // `field`'s button-chrome glow/drop-shadow layers too; `field`'s own doc
7
+ // comment had claimed "inputs, textareas" among its consumers, which this
8
+ // corrects.
4
9
  // The FOCUS TREATMENT is seed-design's, measured from @seed-design/css: a 2px
5
10
  // ring on an ::after overlay that is present-but-transparent at rest, so
6
11
  // focusing cannot shift layout the way a width change would, and animates
@@ -15,7 +20,7 @@ const config = defineRecipe({
15
20
  name: "input",
16
21
  base: `
17
22
  position: relative; box-sizing: border-box; display: inline-flex; align-items: center;
18
- width: 100%; background: ${c.bgSurface}; box-shadow: ${e.field};
23
+ width: 100%; background: ${c.bgSurface}; box-shadow: ${e.textField};
19
24
  font-family: ${c.font.sans}; font-size: ${c.text.lg}; line-height: 20px;
20
25
 
21
26
  &::after {
@@ -166,7 +166,7 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
166
166
  e.preventDefault();
167
167
  openMenu();
168
168
  }
169
- }, children: [_jsx("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
169
+ }, children: [_jsx("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-multiselect-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
170
170
  setQuery(e.target.value);
171
171
  setHighlight(0);
172
172
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
@@ -62,7 +62,7 @@ const config = defineRecipe({
62
62
  }
63
63
  .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
64
64
  .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
65
- .crisp-multiselect-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["1"]}) calc(-1 * ${c.space["1"]}) ${c.space["1"]}; padding: 0 ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgMuted}; }
65
+ .crisp-multiselect-search { display: flex; align-items: center; gap: ${c.space["1.5"]}; height: 36px; flex-shrink: 0; margin: calc(-1 * ${c.space["1"]}) calc(-1 * ${c.space["1"]}) ${c.space["1"]}; padding: ${c.space["2"]} ${c.space["3"]}; color: ${c.fgMuted}; }
66
66
  .crisp-multiselect-search svg { flex-shrink: 0; }
67
67
  .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
68
68
  .crisp-multiselect-search input::placeholder { color: ${c.fgMuted}; }
@@ -3,6 +3,9 @@ import { contract as c } from "../../tokens/index.js";
3
3
  // A settings toggle, measured from the live app: a 24×16 pill track that
4
4
  // flips from a muted grey to brand blue (#266df0), with a 12px white thumb
5
5
  // (2px inset) that slides 8px across. background + transform both animate .2s.
6
+ // The thumb also carries a 1px ring (#463, `rgb(217,218,222)`) — missed in
7
+ // the original measurement pass; without it the white thumb has no defined
8
+ // edge against the track's own light grey.
6
9
  const spec = defineSpec({
7
10
  name: "switch",
8
11
  base: {
@@ -30,6 +33,7 @@ const spec = defineSpec({
30
33
  margin: "0 2px",
31
34
  borderRadius: c.radius.full,
32
35
  background: "#fff",
36
+ boxShadow: `0 0 0 1px ${c.ringSwitch}`,
33
37
  transition: "transform .2s ease",
34
38
  },
35
39
  },
@@ -10,7 +10,11 @@ const config = defineRecipe({
10
10
  base: `display: block; box-sizing: border-box; background: ${c.bgSurface};
11
11
  & .crisp-table-scroll { overflow-x: auto; }
12
12
  & table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: ${c.font.sans}; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
13
- & th, & td { position: relative; height: ${c.size.tableRow}; padding: 0 ${c.space["2"]}; border-bottom: 1px solid ${c.borderSubtle}; border-inline-end: 1px solid ${c.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
13
+ /* #465: 12px, not crisp's old 8px -- measured on a live header cell
14
+ (padding: 0 12px) and independently on a body cell's text inset
15
+ (getBoundingClientRect delta from the cell edge). Matches
16
+ ColumnCalc's own already-correct 12px trigger inset. */
17
+ & th, & td { position: relative; height: ${c.size.tableRow}; padding: 0 ${c.space["3"]}; border-bottom: 1px solid ${c.borderSubtle}; border-inline-end: 1px solid ${c.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
14
18
  & th { height: ${c.size.tableHeader}; background: ${c.bgSurface}; border-top: 1px solid ${c.borderSubtle}; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.14px; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); user-select: none; }
15
19
  & td { color: ${c.fgPrimary}; background: ${c.bgSurface}; }
16
20
  & th:last-child, & td:last-child { border-inline-end: none; }
@@ -14,7 +14,7 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
14
14
  // block, and those depend on following `.menu` in source order. Keeping the
15
15
  // split preserves that cascade exactly, which is what lets the generated CSS
16
16
  // stay byte-for-byte identical to what shipped before the extraction.
17
- export const toolbarChromeHead = (p) => ` & .${CLASS_PREFIX}${p}-toolbar { display: flex; align-items: center; gap: ${c.space["1.5"]}; padding: 0 ${c.space["3"]} ${c.space["2"]}; }
17
+ export const toolbarChromeHead = (p) => ` & .${CLASS_PREFIX}${p}-toolbar { display: flex; align-items: center; gap: ${c.space["2"]}; padding: 0 ${c.space["3"]} ${c.space["2"]}; }
18
18
  & .${CLASS_PREFIX}${p}-toolbar[data-inline] { padding-bottom: 0; }
19
19
  & .${CLASS_PREFIX}${p}-search { display: flex; align-items: center; gap: ${c.space["1.5"]}; height: ${c.size.row}; padding: 0 ${c.space["2.5"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; color: ${c.fgMuted}; }
20
20
  & .${CLASS_PREFIX}${p}-search svg { flex-shrink: 0; }
@@ -27,10 +27,11 @@ export const toolbarChromeHead = (p) => ` & .${CLASS_PREFIX}${p}-toolbar { di
27
27
  & .${CLASS_PREFIX}${p}-tool--ghost { box-shadow: none; background: color-mix(in srgb, ${c.fgPrimary} 3%, transparent); color: ${c.fgMuted}; }
28
28
  & .${CLASS_PREFIX}${p}-tool--ghost:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
29
29
  & .${CLASS_PREFIX}${p}-toolgroup { position: relative; display: inline-flex; }
30
- & .${CLASS_PREFIX}${p}-menu { position: absolute; top: calc(100% + ${c.space["1"]}); left: 0; z-index: 30; min-width: 232px; padding: ${c.space["1"]}; border-radius: ${c.radius.lg}; background: ${c.bgPopover}; box-shadow: ${e.floating}; display: flex; flex-direction: column; gap: ${c.space.px}; transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }
30
+ & .${CLASS_PREFIX}${p}-divider { flex-shrink: 0; width: 1px; height: 20px; background: ${c.borderSubtle}; }
31
+ & .${CLASS_PREFIX}${p}-menu { position: absolute; top: calc(100% + ${c.space["1"]}); left: 0; z-index: 30; min-width: 232px; padding: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: ${c.bgPopover}; box-shadow: ${e.floating}; display: flex; flex-direction: column; gap: ${c.space.px}; transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }
31
32
  & .${CLASS_PREFIX}${p}-menu { @starting-style { opacity: 0; transform: scale(0.96); } }`;
32
33
  export const toolbarChromeBody = (p) => ` & .${CLASS_PREFIX}${p}-menu-head { padding: ${c.space["1"]} ${c.space["2"]} ${c.space["0.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
33
- & .${CLASS_PREFIX}${p}-sortpop { min-width: 460px; overflow: visible; padding: ${c.space["1.5"]}; gap: ${c.space["1"]}; }
34
+ & .${CLASS_PREFIX}${p}-sortpop { min-width: 480px; overflow: visible; padding: ${c.space["3"]} ${c.space["1.5"]}; gap: ${c.space["2"]}; }
34
35
  & .${CLASS_PREFIX}${p}-sort-empty { padding: ${c.space["1"]} ${c.space["1"]} ${c.space["2"]}; font-size: ${c.text.md}; color: ${c.fgMuted}; }
35
36
  & .${CLASS_PREFIX}${p}-sort-row { display: flex; align-items: center; gap: ${c.space["1.5"]}; }
36
37
  & .${CLASS_PREFIX}${p}-sort-grip { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 28px; flex-shrink: 0; margin-right: calc(-1 * ${c.space["1"]}); padding: 0; border: none; background: none; color: ${c.fgMuted}; cursor: grab; }
@@ -51,7 +52,7 @@ export const toolbarChromeBody = (p) => ` & .${CLASS_PREFIX}${p}-menu-head {
51
52
  & .${CLASS_PREFIX}${p}-sort-learn { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.lg}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); text-decoration: none; }
52
53
  & .${CLASS_PREFIX}${p}-sort-learn svg { flex-shrink: 0; }
53
54
  & .${CLASS_PREFIX}${p}-sort-learn:hover { color: ${c.fgPrimary}; }
54
- & .${CLASS_PREFIX}${p}-menu-item { display: flex; align-items: center; gap: ${c.space["2"]}; width: 100%; height: ${c.size.row}; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; border-radius: ${c.radius.md}; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: left; cursor: pointer; }
55
+ & .${CLASS_PREFIX}${p}-menu-item { display: flex; align-items: center; gap: ${c.space["1.5"]}; width: 100%; height: ${c.size.menuRow}; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; border-radius: ${c.radius.md}; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: left; cursor: pointer; }
55
56
  & .${CLASS_PREFIX}${p}-menu-item:hover { background: ${c.bgRaised}; }
56
57
  & .${CLASS_PREFIX}${p}-menu-item:disabled { opacity: 0.4; cursor: default; }
57
58
  & .${CLASS_PREFIX}${p}-menu-item:disabled:hover { background: none; }
@@ -234,7 +234,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
234
234
  label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
235
235
  keywords: descendingLabel,
236
236
  },
237
- ], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] }), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "filter" ? null : "filter"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
237
+ ], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] }), _jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" }), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "filter" ? null : "filter"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
238
238
  ? filterTriggerLabel(activeFilters)
239
239
  : activeFilters
240
240
  ? `Filter · ${activeFilters}`
@@ -4,6 +4,11 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
4
4
  // optional character counter pinned bottom-right (12px, muted). Focus rings
5
5
  // the field brand.
6
6
  //
7
+ // The ring is `elevation.textField` (#466), not `elevation.field` — this
8
+ // comment already correctly described "an inset 1px ring", but the code
9
+ // pulled in `field`'s own button-chrome shadow (a ring PLUS a glow and a
10
+ // drop-shadow layer), which live Attio never carries on a real typed field.
11
+ //
7
12
  // The inset is seed-design's, not Attio's -- the second deliberate divergence
8
13
  // after the focus treatment. seed's text-input sets its inline inset to
9
14
  // `$dimension.x3_5`, which rootage/dimension.yaml gives as **14px**, against
@@ -20,7 +25,7 @@ const config = defineRecipe({
20
25
  & .crisp-textarea-field {
21
26
  box-sizing: border-box; display: block; width: 100%; min-height: 80px; resize: vertical;
22
27
  margin: 0; padding: ${c.space["2.5"]} 14px; border: none; border-radius: ${c.radius.md};
23
- background: ${c.bgSurface}; box-shadow: ${e.field};
28
+ background: ${c.bgSurface}; box-shadow: ${e.textField};
24
29
  font-family: inherit; font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgPrimary};
25
30
  outline: none; transition: box-shadow .15s ease;
26
31
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
@@ -2,6 +2,15 @@ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
3
  // The floating bubble for a Tooltip — a dark hint chip. Applied to the
4
4
  // portaled bubble itself (not a wrapper), so its styles survive the portal.
5
+ //
6
+ // Re-measured live against Attio's own tooltip (crisp#465): background,
7
+ // text colour, vertical padding and radius were all off — #20242c/#fff/4px
8
+ // vertical/6px radius were an earlier, uncorroborated guess (not the same
9
+ // surface Lesson 24 in AUTHORING.md describes for the RichText toolbar,
10
+ // despite the similar wording). The real values are gray900's light hex
11
+ // (#101112, the same "dark pill" ink NavItem's own text uses) and
12
+ // line100's light hex (#eeeff1) — both theme-constant, same as before,
13
+ // just the wrong pair.
5
14
  const spec = defineSpec({
6
15
  name: "tooltip",
7
16
  base: {
@@ -10,10 +19,10 @@ const spec = defineSpec({
10
19
  boxSizing: "border-box",
11
20
  pointerEvents: "none",
12
21
  maxWidth: "260px",
13
- padding: `${c.space["1"]} ${c.space["2"]}`,
14
- borderRadius: c.radius.sm,
15
- background: "#20242c",
16
- color: "#fff",
22
+ padding: `${c.space["1.5"]} ${c.space["2"]}`,
23
+ borderRadius: c.radius.md,
24
+ background: "#101112",
25
+ color: "#eeeff1",
17
26
  fontFamily: c.font.sans,
18
27
  fontSize: c.text.sm,
19
28
  fontWeight: c.weight.medium,
@@ -3,21 +3,31 @@ import { contract as c } from "../../tokens/index.js";
3
3
  // A kanban board — measured from a Deals pipeline: horizontally scrolling
4
4
  // columns (dot + title + count header, a "New" button, a stack of draggable
5
5
  // cards), with the hovered drop column highlighted.
6
+ //
7
+ // Re-measured directly against a live Deals pipeline board (#463): column
8
+ // 268px (was an unmeasured 280), 8px gap between columns (was space[3]/12,
9
+ // measured space[2]), the stage dot 12x12 (was a guessed 9x9), the header's
10
+ // "+" 20x20 at radius.sm (was 24x24 at radius.md), the count as a filled
11
+ // 16px pill rather than bare text, and the card at radius.xl/12px (was
12
+ // radius.lg/9) with a single measured 1px ring and no second shadow layer —
13
+ // the extra `0 1px 2px rgba(0,0,0,0.04)` layer doesn't exist on the
14
+ // reference card at all (see AUTHORING Lesson 22: don't add polish the
15
+ // reference lacks).
6
16
  const config = defineRecipe({
7
17
  name: "board",
8
- base: `display: flex; gap: ${c.space["3"]}; height: 100%; box-sizing: border-box; padding: ${c.space["3"]}; overflow-x: auto; align-items: flex-start; font-family: ${c.font.sans};
9
- & .crisp-board-col { display: flex; flex-direction: column; width: 280px; flex-shrink: 0; max-height: 100%; box-sizing: border-box; }
18
+ base: `display: flex; gap: ${c.space["2"]}; height: 100%; box-sizing: border-box; padding: ${c.space["3"]}; overflow-x: auto; align-items: flex-start; font-family: ${c.font.sans};
19
+ & .crisp-board-col { display: flex; flex-direction: column; width: 268px; flex-shrink: 0; max-height: 100%; box-sizing: border-box; }
10
20
  & .crisp-board-col-header { display: flex; align-items: center; gap: ${c.space["2"]}; height: 36px; padding: 0 ${c.space["1"]}; flex-shrink: 0; }
11
- & .crisp-board-col-dot { width: 9px; height: 9px; border-radius: ${c.radius.full}; flex-shrink: 0; }
21
+ & .crisp-board-col-dot { width: 12px; height: 12px; border-radius: ${c.radius.full}; flex-shrink: 0; }
12
22
  & .crisp-board-col-title { font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
13
- & .crisp-board-col-count { font-size: ${c.text.md}; color: ${c.fgMuted}; }
14
- & .crisp-board-col-add { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-inline-start: auto; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgMuted}; font-family: inherit; cursor: pointer; }
23
+ & .crisp-board-col-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
24
+ & .crisp-board-col-add { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin-inline-start: auto; padding: 0; border: none; border-radius: ${c.radius.sm}; background: none; color: ${c.fgMuted}; font-family: inherit; cursor: pointer; }
15
25
  & .crisp-board-col-add:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
16
26
  & .crisp-board-col-body { display: flex; flex-direction: column; gap: ${c.space["2"]}; flex: 1; min-height: 48px; padding: ${c.space["1"]}; border-radius: ${c.radius.lg}; overflow-y: auto; transition: background 0.12s; }
17
27
  & .crisp-board-col[data-over] .crisp-board-col-body { background: color-mix(in srgb, ${c.bgBrandSolid} 7%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgBrandSolid} 30%, transparent); }
18
28
  & .crisp-board-newcard { display: flex; align-items: center; justify-content: center; gap: ${c.space["1.5"]}; height: 32px; flex-shrink: 0; border: none; border-radius: ${c.radius.md}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgMuted}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; cursor: pointer; }
19
29
  & .crisp-board-newcard:hover { background: color-mix(in srgb, ${c.fgPrimary} 7%, transparent); color: ${c.fgPrimary}; }
20
- & .crisp-board-card { display: flex; flex-direction: column; gap: ${c.space["2"]}; box-sizing: border-box; padding: ${c.space["2.5"]} ${c.space["3"]}; border-radius: ${c.radius.lg}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}, 0 1px 2px rgba(0,0,0,0.04); cursor: grab; }
30
+ & .crisp-board-card { display: flex; flex-direction: column; gap: ${c.space["2"]}; box-sizing: border-box; padding: ${c.space["2.5"]} ${c.space["3"]}; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; cursor: grab; }
21
31
  & .crisp-board-card:active { cursor: grabbing; }
22
32
  & .crisp-board-card[data-dragging] { opacity: 0.4; }
23
33
  & .crisp-board-card:focus { outline: none; }
@@ -3,6 +3,22 @@ import { contract as c } from "../../tokens/index.js";
3
3
  // A note / entity card — measured from a Notes grid: a bordered white card
4
4
  // with an author row (avatar + name) at the top, a title, a clamped excerpt,
5
5
  // and a footer row (avatar + name … date). The whole card is clickable.
6
+ //
7
+ // Re-measured directly against a live note card (#474): the author name
8
+ // renders at fgPrimary/text.sm (12px), not a muted 13px — it reads as
9
+ // regular body text, not a caption. The title is weight.medium (500), not
10
+ // semibold — "my" measured 500 on the live card, and semibold was an
11
+ // unmeasured guess. The excerpt is text.sm (12px)/16px line-height, and
12
+ // both the excerpt body and the footer date share one muted tone that is
13
+ // darker than `fgMuted` (measured `rgba(0,0,0,0.63)` on the excerpt
14
+ // paragraph AND, independently, on the footer date — the same value also
15
+ // turned up on Board's "29d" age pill in the same audit, so it's a real,
16
+ // reused Attio tone and not noise). `fgMuted` (gray400, #71717a) doesn't
17
+ // carry that ring, so this reads it as 63% of `fgPrimary` instead of adding
18
+ // an unmeasured new dark-mode literal (AUTHORING Lesson 47) — `fgPrimary`
19
+ // already flips correctly per theme, so the mix preserves the *relationship*
20
+ // Attio is expressing (a darker secondary text tone) in both themes.
21
+ const secondaryFg = `color-mix(in srgb, ${c.fgPrimary} 63%, transparent)`;
6
22
  const config = defineRecipe({
7
23
  name: "entitycard",
8
24
  base: `display: flex; flex-direction: column; box-sizing: border-box; width: 100%; min-width: 0; text-align: start;
@@ -14,11 +30,11 @@ const config = defineRecipe({
14
30
  &:hover { box-shadow: inset 0 0 0 1px ${c.borderDefault}, 0 1px 2px rgba(0,0,0,0.04), 0 6px 16px -6px rgba(28,40,64,0.12); }
15
31
  &:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 2px; }
16
32
 
17
- & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
18
- & .crisp-entitycard-title { margin-top: ${c.space["2"]}; font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; letter-spacing: -0.1px; color: ${c.fgPrimary}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
19
- & .crisp-entitycard-excerpt { margin-top: ${c.space["1"]}; font-size: ${c.text.md}; line-height: 18px; color: ${c.fgMuted}; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
33
+ & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
34
+ & .crisp-entitycard-title { margin-top: ${c.space["2"]}; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.1px; color: ${c.fgPrimary}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
35
+ & .crisp-entitycard-excerpt { margin-top: ${c.space["1"]}; font-size: ${c.text.sm}; line-height: 16px; color: ${secondaryFg}; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
20
36
  & .crisp-entitycard-spacer { flex: 1; min-height: ${c.space["3"]}; }
21
- & .crisp-entitycard-foot { display: flex; align-items: center; gap: ${c.space["1.5"]}; padding-top: ${c.space["2.5"]}; margin-top: ${c.space["1"]}; border-top: 1px solid ${c.borderSubtle}; font-size: ${c.text.sm}; color: ${c.fgMuted}; }
37
+ & .crisp-entitycard-foot { display: flex; align-items: center; gap: ${c.space["1.5"]}; padding-top: ${c.space["2.5"]}; margin-top: ${c.space["1"]}; border-top: 1px solid ${c.borderSubtle}; font-size: ${c.text.sm}; color: ${secondaryFg}; }
22
38
  & .crisp-entitycard-foot-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: ${c.fgPrimary}; }
23
39
  & .crisp-entitycard-date { margin-inline-start: auto; flex-shrink: 0; white-space: nowrap; }`,
24
40
  variants: {},
@@ -1,4 +1,4 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { listpageRecipe } from "./listpage.recipe.js";
3
3
  /** A single list row wrapper (52px, hover, top border). Compose cells inside.
4
4
  * Renders a real `<button>` when `onClick` is given, else a plain row. */
@@ -16,6 +16,6 @@ export function ListRow({ children, onClick, }) {
16
16
  */
17
17
  export function ListPage({ leading, trailing, count, favorites, favoritesLabel = "Favorites", columns, groups, emptyLabel = "Nothing here yet", className, ...rest }) {
18
18
  const total = groups?.reduce((s, g) => s + g.rows.length, 0) ?? 0;
19
- return (_jsxs("div", { className: listpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-listpage-toolbar", children: [_jsx("div", { className: "crisp-listpage-toolbar-lead", children: leading }), count != null && _jsx("div", { className: "crisp-listpage-count", children: count }), trailing != null && (_jsx("div", { className: "crisp-listpage-toolbar-trail", children: trailing }))] }), _jsxs("div", { className: "crisp-listpage-body", children: [favorites != null && (_jsxs(_Fragment, { children: [_jsx("div", { className: "crisp-listpage-section-label", children: favoritesLabel }), _jsx("div", { className: "crisp-listpage-favorites", "data-dashed": "", children: favorites })] })), columns && columns.length > 0 && (_jsx("div", { className: "crisp-listpage-colhead", children: columns.map((col, i) => (_jsxs("span", { className: "crisp-listpage-colhead-cell", children: [col.icon, col.label] }, i))) })), total === 0 && !groups?.length ? (_jsx("div", { className: "crisp-listpage-empty", children: emptyLabel })) : (groups?.map((g) => (_jsxs("div", { children: [_jsxs("div", { className: "crisp-listpage-group", children: [g.label, g.count != null && (_jsx("span", { className: "crisp-listpage-group-count", children: g.count }))] }), g.rows] }, g.id))))] })] }));
19
+ return (_jsxs("div", { className: listpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-listpage-toolbar", children: [_jsx("div", { className: "crisp-listpage-toolbar-lead", children: leading }), count != null && _jsx("div", { className: "crisp-listpage-count", children: count }), trailing != null && (_jsx("div", { className: "crisp-listpage-toolbar-trail", children: trailing }))] }), _jsxs("div", { className: "crisp-listpage-body", children: [favorites != null && (_jsxs("div", { className: "crisp-listpage-favorites-section", children: [_jsx("div", { className: "crisp-listpage-section-label", children: favoritesLabel }), _jsx("div", { className: "crisp-listpage-favorites", children: favorites })] })), columns && columns.length > 0 && (_jsx("div", { className: "crisp-listpage-colhead", children: columns.map((col, i) => (_jsxs("span", { className: "crisp-listpage-colhead-cell", children: [col.icon, col.label] }, i))) })), total === 0 && !groups?.length ? (_jsx("div", { className: "crisp-listpage-empty", children: emptyLabel })) : (groups?.map((g) => (_jsxs("div", { children: [_jsxs("div", { className: "crisp-listpage-group", children: [g.label, g.count != null && (_jsx("span", { className: "crisp-listpage-group-count", children: g.count }))] }), g.rows] }, g.id))))] })] }));
20
20
  }
21
21
  ListPage.displayName = "ListPage";