@crisp-ui-kit/crisp 0.54.1 → 0.54.3

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 (171) hide show
  1. package/dist/chip.css +7 -1
  2. package/dist/chip.d.ts +2 -1
  3. package/dist/chip.layered.css +7 -1
  4. package/dist/cjs/components/chip/chip.js +3 -2
  5. package/dist/cjs/components/chip/chip.recipe.js +37 -1
  6. package/dist/cjs/components/datatable/datatable.js +490 -111
  7. package/dist/cjs/components/datatable/datatable.recipe.js +4 -0
  8. package/dist/cjs/components/emptystate/emptystate.js +3 -2
  9. package/dist/cjs/components/emptystate/emptystate.recipe.js +26 -1
  10. package/dist/cjs/components/mentionchip/index.js +7 -0
  11. package/dist/cjs/components/mentionchip/mentionchip.js +57 -0
  12. package/dist/cjs/components/mentionchip/mentionchip.recipe.js +38 -0
  13. package/dist/cjs/components/personchip/index.js +17 -0
  14. package/dist/cjs/components/personchip/personchip.js +50 -0
  15. package/dist/cjs/components/table/table.js +2 -2
  16. package/dist/cjs/components/table/table.recipe.js +15 -3
  17. package/dist/cjs/components/tabs/tabs.js +2 -2
  18. package/dist/cjs/components/tabs/tabs.recipe.js +30 -3
  19. package/dist/cjs/components/text/index.js +7 -0
  20. package/dist/cjs/components/text/text.js +55 -0
  21. package/dist/cjs/components/text/text.recipe.js +34 -0
  22. package/dist/cjs/core/floating-stack.js +18 -0
  23. package/dist/cjs/core/index.js +2 -1
  24. package/dist/cjs/index.js +8 -0
  25. package/dist/cjs/product/formdialog/formdialog.js +171 -0
  26. package/dist/cjs/product/formdialog/formdialog.recipe.js +26 -0
  27. package/dist/cjs/product/formdialog/index.js +8 -0
  28. package/dist/cjs/product/headerbar/headerbar.recipe.js +9 -1
  29. package/dist/cjs/product/importdialog/importdialog.js +134 -0
  30. package/dist/cjs/product/importdialog/importdialog.recipe.js +92 -0
  31. package/dist/cjs/product/importdialog/index.js +18 -0
  32. package/dist/cjs/product/recordpage/recordpage.recipe.js +10 -1
  33. package/dist/cjs/product/recordstable/recordstable.js +89 -6
  34. package/dist/cjs/product/richtext/richtext.recipe.js +26 -1
  35. package/dist/cjs/product/savedviews/index.js +7 -0
  36. package/dist/cjs/product/savedviews/savedviews.js +207 -0
  37. package/dist/cjs/product/savedviews/savedviews.recipe.js +103 -0
  38. package/dist/cjs/product/taskstable/index.js +7 -0
  39. package/dist/cjs/product/taskstable/taskstable.js +27 -0
  40. package/dist/cjs/product/taskstable/taskstable.recipe.js +47 -0
  41. package/dist/cjs/product/themepicker/index.js +17 -0
  42. package/dist/cjs/product/themepicker/themepicker.js +32 -0
  43. package/dist/cjs/product/themepicker/themepicker.recipe.js +98 -0
  44. package/dist/cjs/tokens/palette.js +30 -13
  45. package/dist/cjs/tokens/semantic.js +6 -0
  46. package/dist/datatable.css +20 -21
  47. package/dist/datatable.layered.css +20 -21
  48. package/dist/emptystate.css +15 -1
  49. package/dist/emptystate.d.ts +2 -2
  50. package/dist/emptystate.layered.css +15 -1
  51. package/dist/esm/components/chip/chip.js +3 -2
  52. package/dist/esm/components/chip/chip.recipe.js +37 -1
  53. package/dist/esm/components/datatable/datatable.js +489 -111
  54. package/dist/esm/components/datatable/datatable.recipe.js +4 -0
  55. package/dist/esm/components/emptystate/emptystate.js +4 -3
  56. package/dist/esm/components/emptystate/emptystate.recipe.js +26 -1
  57. package/dist/esm/components/emptystate/index.js +1 -1
  58. package/dist/esm/components/mentionchip/index.js +2 -0
  59. package/dist/esm/components/mentionchip/mentionchip.js +21 -0
  60. package/dist/esm/components/mentionchip/mentionchip.recipe.js +35 -0
  61. package/dist/esm/components/personchip/index.js +1 -0
  62. package/dist/esm/components/personchip/personchip.js +47 -0
  63. package/dist/esm/components/table/table.js +2 -2
  64. package/dist/esm/components/table/table.recipe.js +15 -3
  65. package/dist/esm/components/tabs/tabs.js +2 -2
  66. package/dist/esm/components/tabs/tabs.recipe.js +30 -3
  67. package/dist/esm/components/text/index.js +2 -0
  68. package/dist/esm/components/text/text.js +19 -0
  69. package/dist/esm/components/text/text.recipe.js +31 -0
  70. package/dist/esm/core/floating-stack.js +17 -0
  71. package/dist/esm/core/index.js +1 -1
  72. package/dist/esm/index.js +8 -0
  73. package/dist/esm/product/formdialog/formdialog.js +134 -0
  74. package/dist/esm/product/formdialog/formdialog.recipe.js +23 -0
  75. package/dist/esm/product/formdialog/index.js +2 -0
  76. package/dist/esm/product/headerbar/headerbar.recipe.js +9 -1
  77. package/dist/esm/product/importdialog/importdialog.js +97 -0
  78. package/dist/esm/product/importdialog/importdialog.recipe.js +89 -0
  79. package/dist/esm/product/importdialog/index.js +2 -0
  80. package/dist/esm/product/recordpage/recordpage.recipe.js +10 -1
  81. package/dist/esm/product/recordstable/recordstable.js +90 -8
  82. package/dist/esm/product/richtext/richtext.recipe.js +26 -1
  83. package/dist/esm/product/savedviews/index.js +2 -0
  84. package/dist/esm/product/savedviews/savedviews.js +171 -0
  85. package/dist/esm/product/savedviews/savedviews.recipe.js +100 -0
  86. package/dist/esm/product/taskstable/index.js +2 -0
  87. package/dist/esm/product/taskstable/taskstable.js +24 -0
  88. package/dist/esm/product/taskstable/taskstable.recipe.js +44 -0
  89. package/dist/esm/product/themepicker/index.js +1 -0
  90. package/dist/esm/product/themepicker/themepicker.js +29 -0
  91. package/dist/esm/product/themepicker/themepicker.recipe.js +95 -0
  92. package/dist/esm/tokens/palette.js +30 -13
  93. package/dist/esm/tokens/semantic.js +6 -0
  94. package/dist/formdialog.css +6 -0
  95. package/dist/formdialog.d.ts +7 -0
  96. package/dist/formdialog.layered.css +8 -0
  97. package/dist/headerbar.css +1 -1
  98. package/dist/headerbar.layered.css +1 -1
  99. package/dist/importdialog.css +38 -0
  100. package/dist/importdialog.d.ts +7 -0
  101. package/dist/importdialog.layered.css +40 -0
  102. package/dist/mentionchip.css +3 -0
  103. package/dist/mentionchip.d.ts +7 -0
  104. package/dist/mentionchip.layered.css +5 -0
  105. package/dist/native/tokens.d.ts +1 -1
  106. package/dist/native/tokens.js +14 -6
  107. package/dist/recordpage.css +13 -2
  108. package/dist/recordpage.layered.css +13 -2
  109. package/dist/richtext.css +11 -1
  110. package/dist/richtext.layered.css +11 -1
  111. package/dist/savedviews.css +78 -0
  112. package/dist/savedviews.d.ts +7 -0
  113. package/dist/savedviews.layered.css +80 -0
  114. package/dist/styles.css +287 -37
  115. package/dist/styles.layered.css +287 -37
  116. package/dist/table.css +15 -6
  117. package/dist/table.layered.css +15 -6
  118. package/dist/tabs.css +7 -1
  119. package/dist/tabs.d.ts +2 -2
  120. package/dist/tabs.layered.css +7 -1
  121. package/dist/taskstable.css +23 -0
  122. package/dist/taskstable.d.ts +7 -0
  123. package/dist/taskstable.layered.css +25 -0
  124. package/dist/text.css +9 -0
  125. package/dist/text.d.ts +7 -0
  126. package/dist/text.layered.css +11 -0
  127. package/dist/themepicker.css +27 -0
  128. package/dist/themepicker.d.ts +7 -0
  129. package/dist/themepicker.layered.css +29 -0
  130. package/dist/tokens.css +7 -3
  131. package/dist/tokens.json +14 -6
  132. package/dist/types/components/chip/chip.d.ts +32 -0
  133. package/dist/types/components/chip/chip.recipe.d.ts +31 -0
  134. package/dist/types/components/datatable/datatable.d.ts +39 -4
  135. package/dist/types/components/emptystate/emptystate.d.ts +29 -4
  136. package/dist/types/components/emptystate/emptystate.recipe.d.ts +2 -0
  137. package/dist/types/components/emptystate/index.d.ts +1 -1
  138. package/dist/types/components/mentionchip/index.d.ts +2 -0
  139. package/dist/types/components/mentionchip/mentionchip.d.ts +20 -0
  140. package/dist/types/components/mentionchip/mentionchip.recipe.d.ts +18 -0
  141. package/dist/types/components/personchip/index.d.ts +1 -0
  142. package/dist/types/components/personchip/personchip.d.ts +37 -0
  143. package/dist/types/components/table/table.d.ts +36 -0
  144. package/dist/types/components/tabs/tabs.d.ts +4 -2
  145. package/dist/types/components/tabs/tabs.recipe.d.ts +18 -3
  146. package/dist/types/components/text/index.d.ts +2 -0
  147. package/dist/types/components/text/text.d.ts +32 -0
  148. package/dist/types/components/text/text.recipe.d.ts +40 -0
  149. package/dist/types/core/floating-stack.d.ts +14 -0
  150. package/dist/types/core/index.d.ts +1 -1
  151. package/dist/types/index.d.ts +8 -0
  152. package/dist/types/product/formdialog/formdialog.d.ts +57 -0
  153. package/dist/types/product/formdialog/formdialog.recipe.d.ts +3 -0
  154. package/dist/types/product/formdialog/index.d.ts +2 -0
  155. package/dist/types/product/importdialog/importdialog.d.ts +86 -0
  156. package/dist/types/product/importdialog/importdialog.recipe.d.ts +3 -0
  157. package/dist/types/product/importdialog/index.d.ts +2 -0
  158. package/dist/types/product/recordstable/recordstable.d.ts +24 -2
  159. package/dist/types/product/savedviews/index.d.ts +2 -0
  160. package/dist/types/product/savedviews/savedviews.d.ts +55 -0
  161. package/dist/types/product/savedviews/savedviews.recipe.d.ts +3 -0
  162. package/dist/types/product/taskstable/index.d.ts +2 -0
  163. package/dist/types/product/taskstable/taskstable.d.ts +54 -0
  164. package/dist/types/product/taskstable/taskstable.recipe.d.ts +3 -0
  165. package/dist/types/product/themepicker/index.d.ts +1 -0
  166. package/dist/types/product/themepicker/themepicker.d.ts +21 -0
  167. package/dist/types/product/themepicker/themepicker.recipe.d.ts +3 -0
  168. package/dist/types/tokens/contract.d.ts +2 -0
  169. package/dist/types/tokens/palette.d.ts +11 -3
  170. package/dist/types/tokens/semantic.d.ts +6 -0
  171. package/package.json +41 -1
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskstableRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A grouped due-date table — Attio's Tasks page (app.attio.com/<ws>/tasks),
7
+ // measured live and recorded on crisp#462. Composes ListPage's own toolbar
8
+ // (`.crisp-listpage-toolbar`) and group-row (`.crisp-listpage-group`) chrome
9
+ // by carrying BOTH this recipe's root class and `listpageRecipe`'s root class
10
+ // on the same element (see taskstable.tsx) -- both recipes' descendant rules
11
+ // nest under their own root class, so this is a safe, ordinary composition,
12
+ // the same way `<ListRow>` already lets a caller compose into `ListPage`'s
13
+ // group bodies. Real deviation: ListPage's own `columns` colhead slot is
14
+ // pinned to `c.size.menuRow` (32px, correct for its Reports/Sequences
15
+ // reference surfaces per #479) but Tasks/Notes independently measure a 49px
16
+ // column-header row (#462) -- ListPage's colhead can't be asked to render at
17
+ // a different height per consumer today, so this component renders its own
18
+ // colhead instead of using that slot. Flagged in the PR, not fixed here (out
19
+ // of scope: don't change ListPage for this component).
20
+ const config = (0, core_1.defineRecipe)({
21
+ name: "taskstable",
22
+ base: `display: flex; flex-direction: column; min-height: 0; box-sizing: border-box; font-family: ${tokens_1.contract.font.sans}; color: ${tokens_1.contract.fgPrimary};
23
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
24
+
25
+ & .crisp-taskstable-colhead { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-height: 49px; max-height: 49px; padding: ${tokens_1.contract.space["2.5"]} ${tokens_1.contract.space["3"]}; box-shadow: inset 0 -1px 0 ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; }
26
+ & .crisp-taskstable-colhead-cell { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; }
27
+
28
+ & .crisp-taskstable-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; }
29
+ & .crisp-taskstable-cell { display: flex; align-items: center; min-width: 0; }
30
+
31
+ & .crisp-taskstable-check { appearance: none; -webkit-appearance: none; box-sizing: border-box; flex-shrink: 0; margin: 0; width: 16px; height: 16px; border: none; border-radius: ${tokens_1.contract.radius.full}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 9%, transparent); cursor: pointer; transition: background-color .12s, box-shadow .12s; }
32
+ & .crisp-taskstable-check:hover { box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 30%, transparent); }
33
+ & .crisp-taskstable-check:checked { background: ${tokens_1.contract.bgBrandSolid} center / 10px no-repeat; box-shadow: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 6.5 5 9l4.5-5'/%3E%3C/svg%3E"); }
34
+ & .crisp-taskstable-check:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 1px; }
35
+
36
+ & .crisp-taskstable-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; }
37
+
38
+ & .crisp-taskstable-due { font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgMuted}; white-space: nowrap; }
39
+ & .crisp-taskstable-due[data-overdue] { color: ${tokens_1.contract.fgDanger}; }
40
+
41
+ & .crisp-taskstable-empty { padding: ${tokens_1.contract.space["8"]} ${tokens_1.contract.space["4"]}; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }`,
42
+ variants: {},
43
+ defaultVariants: {},
44
+ compoundVariants: [],
45
+ });
46
+ exports.taskstableRecipe = (0, core_1.resolveRecipe)(config);
47
+ exports.default = config;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./themepicker"), exports);
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ThemePicker = ThemePicker;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const radiogroup_1 = require("../../components/radiogroup");
6
+ const core_1 = require("../../core");
7
+ const themepicker_recipe_1 = require("./themepicker.recipe");
8
+ const OPTIONS = ["light", "dark", "system"];
9
+ function PreviewSwatch({ swatch }) {
10
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-themepicker-preview", "data-swatch": swatch, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-themepicker-preview-side", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-themepicker-preview-main", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" })] })] }));
11
+ }
12
+ /**
13
+ * A light/dark/system theme picker — three cards, each a small preview of
14
+ * what that option looks like, radio-selectable. See the recipe's own doc
15
+ * comment for the live Attio measurement (crisp#462 gap 4) and for why the
16
+ * preview swatch colours are literal rather than `var(--crisp-*)` tokens.
17
+ */
18
+ function ThemePicker({ value, onChange, labels, className, ...rest }) {
19
+ const lightLabel = (0, core_1.useCrispMessage)(labels?.light, "Light");
20
+ const darkLabel = (0, core_1.useCrispMessage)(labels?.dark, "Dark");
21
+ const systemLabel = (0, core_1.useCrispMessage)(labels?.system, "System");
22
+ const labelFor = {
23
+ light: lightLabel,
24
+ dark: darkLabel,
25
+ system: systemLabel,
26
+ };
27
+ const { onKeyDown, groupRef } = (0, radiogroup_1.useRadioGroupKeyboard)({
28
+ select: (v) => onChange(v),
29
+ });
30
+ return ((0, jsx_runtime_1.jsx)("div", { ref: groupRef, className: (0, themepicker_recipe_1.themepickerRecipe)({ className }), role: "radiogroup", onKeyDown: onKeyDown, ...rest, children: OPTIONS.map((option) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "radio", "aria-checked": value === option, "data-value": option, className: "crisp-themepicker-option", "data-selected": value === option ? "" : undefined, onClick: () => onChange(option), children: [(0, jsx_runtime_1.jsx)(PreviewSwatch, { swatch: option }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-themepicker-label", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-themepicker-radio", "aria-hidden": "true" }), labelFor[option]] })] }, option))) }));
31
+ }
32
+ ThemePicker.displayName = "ThemePicker";
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.themepickerRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A theme picker (crisp#571) — measured live at
7
+ // app.attio.com/<ws>/settings/appearance (Settings -> Appearance -> Theme,
8
+ // its "Light" / "Dark" / "System" tiles):
9
+ // each option is a 224x152 card, 16px radius, on a flex row with an 8px
10
+ // gap. Selected carries a 1px solid brand-blue border (theme-constant
11
+ // #266df0, matches `bgBrandSolid`); unselected carries the ordinary
12
+ // hairline (`borderSubtle`). Inside, a ~214x114 mock-UI preview card sits
13
+ // on the same 12px radius / 1px hairline chrome the real Sidebar itself
14
+ // uses, with a label row below (a radio dot + 14px/500 text, `fgPrimary`
15
+ // when selected, muted when not).
16
+ //
17
+ // The preview card's OWN colours are deliberately literal, not `var(--crisp-*)`
18
+ // tokens: they must show what "Light"/"Dark" look like regardless of the
19
+ // picker's own ambient theme (a dark-mode app still needs its "Light" tile
20
+ // to render light), and crisp's token system only has one live value per
21
+ // custom property at a time -- there's no way to force a light subtree
22
+ // inside a dark ancestor without one. This is also how Attio's own tiles
23
+ // behave: their preview is a fixed illustration asset, not theme-reactive
24
+ // markup (confirmed by reading the SVG's own baked-in fills while the app
25
+ // itself was in light mode: the "Dark" tile's fills were already dark).
26
+ // The literals below are simply the EXISTING dual-theme tokens' own two
27
+ // values, spelled out per swatch rather than invented fresh: bgSidebar
28
+ // (#fbfbfb / #0e0f10), gray00/bgSurface (#ffffff / #0a0a0a), borderSubtle
29
+ // (#eeeff1 / #1c1d1f) -- each matches what Attio's own SVG measured
30
+ // (#0B0C0D-#101112 range for backgrounds, #1B1C1F for dividers) closely
31
+ // enough to treat as the same token rather than a fresh, barely-different
32
+ // one. `bgBrandSolid` (#266df0) IS referenced live since it's already
33
+ // theme-constant.
34
+ //
35
+ // "System" mirrors the OS's own effective scheme via `prefers-color-scheme`,
36
+ // not a guessed split illustration: this session's own System tile
37
+ // rendered with the SAME fills as the Light tile while the OS/browser
38
+ // reported light -- reads as "follows the OS", not a fixed half-and-half
39
+ // image there's no other evidence for.
40
+ //
41
+ // The mock-UI content inside each preview card (Attio's own hand-drawn
42
+ // sidebar/content wireframe SVG) is NOT reproduced pixel-for-pixel here --
43
+ // that illustration is decorative, not a measured design value, so this
44
+ // recipe draws a simplified schematic (a sidebar block + a few content
45
+ // bars) instead of a bespoke SVG asset.
46
+ //
47
+ // Attio's unselected label reads a black-alpha `rgba(0,0,0,0.63)`, the
48
+ // same pattern SettingsSection's own subtitle measured (crisp#522) and
49
+ // left unresolved for the same reason: no confirmed dark-mode value yet.
50
+ // `fgMuted` is used here as the closest existing (and already dual-theme)
51
+ // token rather than repeating an unmeasured-in-dark literal.
52
+ const config = (0, core_1.defineRecipe)({
53
+ name: "themepicker",
54
+ base: `display: flex; gap: ${tokens_1.contract.space["2"]}; font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
55
+ & .crisp-themepicker-option { box-sizing: border-box; display: flex; flex-direction: column; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; width: 224px; padding: ${tokens_1.contract.space["1"]}; border: 1px solid ${tokens_1.contract.borderSubtle}; border-radius: 16px; background: ${tokens_1.contract.bgSurface}; cursor: pointer; font-family: inherit; }
56
+ & .crisp-themepicker-option[data-selected] { border-color: ${tokens_1.contract.bgBrandSolid}; }
57
+
58
+ & .crisp-themepicker-preview { box-sizing: border-box; display: flex; width: 100%; height: 114px; padding: ${tokens_1.contract.space["2"]}; gap: ${tokens_1.contract.space["1.5"]}; border-radius: ${tokens_1.contract.radius.xl}; overflow: hidden; }
59
+ & .crisp-themepicker-preview-side { flex-shrink: 0; width: 28%; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["1"]}; }
60
+ & .crisp-themepicker-preview-bar { height: 4px; border-radius: ${tokens_1.contract.radius.full}; }
61
+ & .crisp-themepicker-preview-bar:nth-child(1) { width: 70%; background: ${tokens_1.contract.bgBrandSolid}; }
62
+ & .crisp-themepicker-preview-bar:nth-child(2) { width: 90%; }
63
+ & .crisp-themepicker-preview-bar:nth-child(3) { width: 60%; }
64
+ & .crisp-themepicker-preview-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["1.5"]}; padding: ${tokens_1.contract.space["1.5"]}; border-radius: ${tokens_1.contract.radius.md}; }
65
+
66
+ /* Light / System (default) swatch colours. */
67
+ & .crisp-themepicker-preview[data-swatch="light"],
68
+ & .crisp-themepicker-preview[data-swatch="system"] { border: 1px solid #eeeff1; background: #fbfbfb; }
69
+ & [data-swatch="light"] .crisp-themepicker-preview-bar:not(:nth-child(1)),
70
+ & [data-swatch="system"] .crisp-themepicker-preview-bar:not(:nth-child(1)) { background: #eeeff1; }
71
+ & [data-swatch="light"] .crisp-themepicker-preview-main,
72
+ & [data-swatch="system"] .crisp-themepicker-preview-main { background: #ffffff; }
73
+ & [data-swatch="light"] .crisp-themepicker-preview-main .crisp-themepicker-preview-bar,
74
+ & [data-swatch="system"] .crisp-themepicker-preview-main .crisp-themepicker-preview-bar { background: #eeeff1; }
75
+
76
+ /* Dark swatch colours -- always for "dark", and for "system" only
77
+ when the OS itself reports dark. */
78
+ & .crisp-themepicker-preview[data-swatch="dark"] { border: 1px solid #1c1d1f; background: #0e0f10; }
79
+ & [data-swatch="dark"] .crisp-themepicker-preview-bar:not(:nth-child(1)) { background: #1c1d1f; }
80
+ & [data-swatch="dark"] .crisp-themepicker-preview-main { background: #0a0a0a; }
81
+ & [data-swatch="dark"] .crisp-themepicker-preview-main .crisp-themepicker-preview-bar { background: #1c1d1f; }
82
+ @media (prefers-color-scheme: dark) {
83
+ & .crisp-themepicker-preview[data-swatch="system"] { border: 1px solid #1c1d1f; background: #0e0f10; }
84
+ & [data-swatch="system"] .crisp-themepicker-preview-bar:not(:nth-child(1)) { background: #1c1d1f; }
85
+ & [data-swatch="system"] .crisp-themepicker-preview-main { background: #0a0a0a; }
86
+ & [data-swatch="system"] .crisp-themepicker-preview-main .crisp-themepicker-preview-bar { background: #1c1d1f; }
87
+ }
88
+
89
+ & .crisp-themepicker-label { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
90
+ & .crisp-themepicker-option[data-selected] .crisp-themepicker-label { color: ${tokens_1.contract.fgPrimary}; }
91
+ & .crisp-themepicker-radio { flex-shrink: 0; width: 14px; height: 14px; border-radius: ${tokens_1.contract.radius.full}; border: 1px solid ${tokens_1.contract.borderDefault}; box-sizing: border-box; }
92
+ & .crisp-themepicker-option[data-selected] .crisp-themepicker-radio { border: 4px solid ${tokens_1.contract.bgBrandSolid}; }`,
93
+ variants: {},
94
+ defaultVariants: {},
95
+ compoundVariants: [],
96
+ });
97
+ exports.themepickerRecipe = (0, core_1.resolveRecipe)(config);
98
+ exports.default = config;
@@ -12,6 +12,14 @@ exports.palette = {
12
12
  gray900: { light: "#101112", dark: "#f5f5f5" },
13
13
  line200: { light: "#e4e4e7", dark: "#212224" },
14
14
  line100: { light: "#eeeff1", dark: "#1c1d1f" },
15
+ // ImportDialog's dropzone border — measured live from Attio's Import CSV
16
+ // flow (`rgb(230,231,234)`), distinct from `line200`/`line100` (#e4e4e7 /
17
+ // #eeeff1 are close but not equal, and AUTHORING's dimensional-scale rule
18
+ // against rounding a measurement to fit an existing token applies to color
19
+ // too). Dark value not independently re-measured (Attio's dark theme is
20
+ // off-limits to toggle, same restriction as `ringField`/`ringSwitch`) —
21
+ // reuses `line200`'s dark line colour rather than inventing a new one.
22
+ line150: { light: "#e6e7ea", dark: "#212224" },
15
23
  ringNeutral: { light: "transparent", dark: "#212224" },
16
24
  // Text-entry fields (Input, Textarea) — measured live on app.attio.com's
17
25
  // Settings page, a real typed field: `inset 0 0 0 1px rgba(0,0,0,.06)`,
@@ -52,6 +60,15 @@ exports.palette = {
52
60
  // #EB3B3B it uses for destructive menu TEXT. Does not flip: a filled
53
61
  // destructive control needs the same white-bearing fill in both themes.
54
62
  red700: { light: "#ba2525", dark: "#ba2525" },
63
+ // The #EB3B3B named in red700's own comment above (destructive menu text) —
64
+ // it had never actually been given a token. Independently re-measured live
65
+ // on `app.attio.com/.../tasks`, `rgb(235,59,59)`, as the overdue due-date
66
+ // text colour on the Tasks page (crisp#462) — same hex, second surface.
67
+ // Dark not independently re-measured (Attio's theme toggle is off-limits,
68
+ // same restriction noted throughout this file); reuses red600's dark value,
69
+ // the same "brightens for legibility on a dark surface" tone this red
70
+ // already sits next to, rather than inventing a third red.
71
+ red500: { light: "#eb3b3b", dark: "#ff5a5f" },
55
72
  // seed-design's field focus ring (stroke-neutral-contrast = gray-1000),
56
73
  // measured from @seed-design/css. A high-contrast neutral rather than a brand
57
74
  // hue: the ring reads as "this field has focus", not as a state colour.
@@ -101,17 +118,17 @@ exports.palette = {
101
118
  // Badge's soft-neutral text, so darkening it doesn't move any other surface.
102
119
  softNeutralText: { light: "#686870", dark: "#9a9aa2" },
103
120
  // Badge's purple/role-pill tone (crisp#506) -- measured live off Attio's
104
- // own role pill ("Admin", app.attio.com/<ws>/settings/workspace/members):
105
- // background rgb(245,240,255), text rgb(98,56,181). The ring re-uses a
106
- // second, independent measurement from the same session (a company's
107
- // Categories tag pill sharing the identical background, "Broadcasting",
108
- // app.attio.com/<ws>/companies) at rgb(232,221,254) -- same hue family,
109
- // cross-validated rather than guessed. Dark mode was NOT measured (no
110
- // live Attio session was reachable when this tone was added) -- these
111
- // three light values are reused as-is for dark rather than an invented
112
- // "brightened" guess (Lesson: don't add unmeasured "robustness"). Needs a
113
- // real dark-mode measurement before this note can be removed.
114
- purpleTint: { light: "#f5f0ff", dark: "#f5f0ff" },
115
- softPurpleText: { light: "#6238b5", dark: "#6238b5" },
116
- purpleRing: { light: "#e8ddfe", dark: "#e8ddfe" },
121
+ // own role pill ("Admin", app.attio.com/<ws>/settings/members-and-teams):
122
+ // background rgb(245,240,255), text rgb(98,56,181), ring rgb(232,221,254)
123
+ // (cross-validated against a second, independent pill sharing the
124
+ // identical background, a company's "Broadcasting" Categories tag).
125
+ // Dark mode (crisp#566) measured the same way, after switching Attio's
126
+ // own Appearance setting to Dark, reading the pill, then switching back
127
+ // to Light to restore the original state: background rgb(47,30,90),
128
+ // text rgb(216,196,255), ring rgb(69,41,125) -- verify-the-right-element
129
+ // note: the pill's own wrapper span inherits an unstyled rgb(241,242,243)
130
+ // text colour; the real styled colour sits on its nested leaf text node.
131
+ purpleTint: { light: "#f5f0ff", dark: "#2f1e5a" },
132
+ softPurpleText: { light: "#6238b5", dark: "#d8c4ff" },
133
+ purpleRing: { light: "#e8ddfe", dark: "#45297d" },
117
134
  };
@@ -13,6 +13,8 @@ exports.semantic = {
13
13
  bgRaised: "gray50",
14
14
  borderDefault: "line200",
15
15
  borderSubtle: "line100",
16
+ /** ImportDialog's dashed dropzone border — see palette.ts's `line150`. */
17
+ borderDropzone: "line150",
16
18
  ringNeutral: "ringNeutral",
17
19
  ringField: "ringField",
18
20
  ringSwitch: "ringSwitch",
@@ -32,6 +34,10 @@ exports.semantic = {
32
34
  bgDanger: "red600",
33
35
  /** Fill for a SOLID destructive control that carries white text. */
34
36
  bgDangerSolid: "red700",
37
+ /** Solid critical/destructive TEXT (not a fill) — an overdue due-date, a
38
+ * destructive menu item's label. Distinct from `bgDanger` (a fill colour)
39
+ * and `fgDangerSoft` (a soft badge pill's text, a different measured hex). */
40
+ fgDanger: "red500",
35
41
  /** The 2px ring a text field shows while focused. */
36
42
  strokeFieldFocus: "fieldFocus",
37
43
  /** Unavailable/disabled content: unselected wheel rows, unpickable dates. */
@@ -46,6 +46,9 @@
46
46
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
47
47
  & th[data-sticky], & td[data-sticky] { position: sticky; z-index: 1; background: var(--crisp-bg-surface); }
48
48
  & th[data-sticky] { z-index: 3; }
49
+ /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
50
+ `tfoot td { z-index: 2 }`), which `td[data-sticky]`'s own higher-specificity `z-index: 1` would otherwise undercut -- keep it level with the row it's in. */
51
+ & tfoot td[data-sticky] { z-index: 2; }
49
52
  & tbody tr[data-selected] td[data-sticky] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
50
53
  & tbody tr:hover td[data-sticky] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); }
51
54
  & .crisp-table-scroll[data-scrolled-x] [data-sticky-edge] { clip-path: inset(0 -38px 0 0); box-shadow: 6px 0 16px 4px rgba(0,0,0,0.12); }
@@ -63,23 +66,25 @@
63
66
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
64
67
  & .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; }
65
68
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
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. */
69
+ /* An async onEdit's in-flight state (#385) -- the field/picker dims and
70
+ stops accepting input rather than disappearing, so the just-typed or
71
+ just-chosen value stays visible while its commit is pending. Same
72
+ treatment Input already gives its own disabled state. */
67
73
  & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
68
74
  & .crisp-datatable-edit-pending { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); color: var(--crisp-fg-muted); vertical-align: middle; }
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
+ /* The rejection's message, beside the cell's own content -- a sibling in
76
+ 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
77
+ it. Block-level: the row grows to fit it rather than floating over
78
+ the row below, so it can never end up clipped by the scroll
79
+ container's overflow the way a position:fixed escape would need guarding against. */
72
80
  & .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); }
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). */
81
+ /* 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
82
+ shares RecordsTable's grid, Golden Rule 0). */
76
83
  & .crisp-datatable-td--picker { padding: 0; }
77
84
  & .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); }
78
85
  & .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; }
79
86
  & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: var(--crisp-space-2); }
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*. */
87
+ /* 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
88
  & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
84
89
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
85
90
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
@@ -96,12 +101,7 @@
96
101
  & .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
97
102
  & button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
98
103
  & .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); }
99
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
100
- & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
101
- & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
102
- & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
103
- & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
104
- & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
104
+ & .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); }
105
105
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
106
106
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
107
107
  & .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); }
@@ -114,11 +114,10 @@
114
114
  & .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)); }
115
115
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
116
116
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
117
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
118
- hairline doubling as the drop indicator (it tints toward the brand
119
- colour as the active drop target). The affordance + geometry are
120
- 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
121
- pointer-events implementation. */
117
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
118
+ 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
119
+ draggable + dataTransfer, the same one column reorder already uses
120
+ (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
122
121
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
123
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; }
124
123
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -47,6 +47,9 @@
47
47
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
48
48
  & th[data-sticky], & td[data-sticky] { position: sticky; z-index: 1; background: var(--crisp-bg-surface); }
49
49
  & th[data-sticky] { z-index: 3; }
50
+ /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
51
+ `tfoot td { z-index: 2 }`), which `td[data-sticky]`'s own higher-specificity `z-index: 1` would otherwise undercut -- keep it level with the row it's in. */
52
+ & tfoot td[data-sticky] { z-index: 2; }
50
53
  & tbody tr[data-selected] td[data-sticky] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
51
54
  & tbody tr:hover td[data-sticky] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); }
52
55
  & .crisp-table-scroll[data-scrolled-x] [data-sticky-edge] { clip-path: inset(0 -38px 0 0); box-shadow: 6px 0 16px 4px rgba(0,0,0,0.12); }
@@ -64,23 +67,25 @@
64
67
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
65
68
  & .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; }
66
69
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
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. */
70
+ /* An async onEdit's in-flight state (#385) -- the field/picker dims and
71
+ stops accepting input rather than disappearing, so the just-typed or
72
+ just-chosen value stays visible while its commit is pending. Same
73
+ treatment Input already gives its own disabled state. */
68
74
  & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
69
75
  & .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 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
+ /* The rejection's message, beside the cell's own content -- a sibling in
77
+ 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
78
+ it. Block-level: the row grows to fit it rather than floating over
79
+ the row below, so it can never end up clipped by the scroll
80
+ container's overflow the way a position:fixed escape would need guarding against. */
73
81
  & .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); }
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). */
82
+ /* 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
83
+ shares RecordsTable's grid, Golden Rule 0). */
77
84
  & .crisp-datatable-td--picker { padding: 0; }
78
85
  & .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); }
79
86
  & .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; }
80
87
  & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: var(--crisp-space-2); }
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*. */
88
+ /* 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
89
  & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
85
90
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
86
91
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
@@ -97,12 +102,7 @@
97
102
  & .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
98
103
  & button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
99
104
  & .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); }
100
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
101
- & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
102
- & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
103
- & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
104
- & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
105
- & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
105
+ & .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); }
106
106
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
107
107
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
108
108
  & .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); }
@@ -115,11 +115,10 @@
115
115
  & .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)); }
116
116
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
117
117
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
118
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
119
- hairline doubling as the drop indicator (it tints toward the brand
120
- colour as the active drop target). The affordance + geometry are
121
- 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
122
- pointer-events implementation. */
118
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
119
+ 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
120
+ draggable + dataTransfer, the same one column reorder already uses
121
+ (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
123
122
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
124
123
  & .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; }
125
124
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -3,7 +3,16 @@
3
3
  & .crisp-emptystate-icon { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; margin-bottom: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); color: var(--crisp-fg-muted); }
4
4
  & .crisp-emptystate-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
5
5
  & .crisp-emptystate-desc { max-width: 320px; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
6
- & .crisp-emptystate-action { margin-top: var(--crisp-space-2); }
6
+ /* #603: measured 8px gap between "Import CSV"/"Connect data source" -- inline-flex so one action (the common case) or several lay out the same way, wrapping if the container is narrow. */
7
+ & .crisp-emptystate-action { display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: center; gap: var(--crisp-space-2); margin-top: var(--crisp-space-2); }
8
+ /* #603: Attio's zero-records "Learn more" card -- a label above a
9
+ clickable icon+title tile. Left/start-aligned, unlike the rest of
10
+ this centred panel, so it overrides the base text-align. */
11
+ & .crisp-emptystate-learnmore { display: flex; flex-direction: column; align-items: stretch; gap: var(--crisp-space-2); margin-top: var(--crisp-space-6); text-align: start; }
12
+ & .crisp-emptystate-learnmore-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
13
+ & .crisp-emptystate-learnmore-card { display: flex; align-items: center; gap: var(--crisp-space-2); padding: var(--crisp-space-2) var(--crisp-space-3) var(--crisp-space-2) var(--crisp-space-2); margin: 0; border: 1px solid var(--crisp-border-subtle); border-radius: 18px; background: var(--crisp-bg-surface); font: inherit; text-align: start; text-decoration: none; color: inherit; cursor: pointer; }
14
+ & .crisp-emptystate-learnmore-icon { display: inline-flex; align-items: center; justify-content: center; width: 56px; height: 56px; flex-shrink: 0; border-radius: var(--crisp-radius-xl); background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); color: var(--crisp-fg-muted); }
15
+ & .crisp-emptystate-learnmore-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
7
16
  }
8
17
  .crisp-emptystate--variant_default {
9
18
 
@@ -18,3 +27,8 @@
18
27
  & .crisp-emptystate-desc { max-width: 266px; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); line-height: 20px; }
19
28
  & .crisp-emptystate-action { margin-top: var(--crisp-space-2); }
20
29
  }
30
+ .crisp-emptystate--variant_records {
31
+ & .crisp-emptystate-icon { width: 138px; height: 138px; margin-bottom: var(--crisp-space-2); border-radius: 0; color: color-mix(in srgb, var(--crisp-fg-primary) 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, var(--crisp-fg-primary) 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }
32
+ & .crisp-emptystate-title { font-size: 20px; line-height: 24px; }
33
+ & .crisp-emptystate-desc { max-width: 266px; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); line-height: 20px; }
34
+ }
@@ -1,7 +1,7 @@
1
1
  import type { RecipeFn } from "../core/types";
2
2
 
3
3
  export interface EmptystateVariants {
4
- variant?: "default" | "illustrated" | "page";
4
+ variant?: "default" | "illustrated" | "page" | "records";
5
5
  }
6
6
 
7
- export declare const emptystateRecipe: RecipeFn<{ variant: { default: string; illustrated: string; page: string } }>;
7
+ export declare const emptystateRecipe: RecipeFn<{ variant: { default: string; illustrated: string; page: string; records: string } }>;
@@ -4,7 +4,16 @@
4
4
  & .crisp-emptystate-icon { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; margin-bottom: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); color: var(--crisp-fg-muted); }
5
5
  & .crisp-emptystate-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
6
6
  & .crisp-emptystate-desc { max-width: 320px; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
7
- & .crisp-emptystate-action { margin-top: var(--crisp-space-2); }
7
+ /* #603: measured 8px gap between "Import CSV"/"Connect data source" -- inline-flex so one action (the common case) or several lay out the same way, wrapping if the container is narrow. */
8
+ & .crisp-emptystate-action { display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: center; gap: var(--crisp-space-2); margin-top: var(--crisp-space-2); }
9
+ /* #603: Attio's zero-records "Learn more" card -- a label above a
10
+ clickable icon+title tile. Left/start-aligned, unlike the rest of
11
+ this centred panel, so it overrides the base text-align. */
12
+ & .crisp-emptystate-learnmore { display: flex; flex-direction: column; align-items: stretch; gap: var(--crisp-space-2); margin-top: var(--crisp-space-6); text-align: start; }
13
+ & .crisp-emptystate-learnmore-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
14
+ & .crisp-emptystate-learnmore-card { display: flex; align-items: center; gap: var(--crisp-space-2); padding: var(--crisp-space-2) var(--crisp-space-3) var(--crisp-space-2) var(--crisp-space-2); margin: 0; border: 1px solid var(--crisp-border-subtle); border-radius: 18px; background: var(--crisp-bg-surface); font: inherit; text-align: start; text-decoration: none; color: inherit; cursor: pointer; }
15
+ & .crisp-emptystate-learnmore-icon { display: inline-flex; align-items: center; justify-content: center; width: 56px; height: 56px; flex-shrink: 0; border-radius: var(--crisp-radius-xl); background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); color: var(--crisp-fg-muted); }
16
+ & .crisp-emptystate-learnmore-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
8
17
  }
9
18
  .crisp-emptystate--variant_default {
10
19
 
@@ -19,4 +28,9 @@
19
28
  & .crisp-emptystate-desc { max-width: 266px; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); line-height: 20px; }
20
29
  & .crisp-emptystate-action { margin-top: var(--crisp-space-2); }
21
30
  }
31
+ .crisp-emptystate--variant_records {
32
+ & .crisp-emptystate-icon { width: 138px; height: 138px; margin-bottom: var(--crisp-space-2); border-radius: 0; color: color-mix(in srgb, var(--crisp-fg-primary) 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, var(--crisp-fg-primary) 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }
33
+ & .crisp-emptystate-title { font-size: 20px; line-height: 24px; }
34
+ & .crisp-emptystate-desc { max-width: 266px; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); line-height: 20px; }
35
+ }
22
36
  }
@@ -16,7 +16,8 @@ import { chipRecipe } from "./chip.recipe.js";
16
16
  * that surface can be measured; don't guess the icon, its hit target, or its
17
17
  * spacing from the label.
18
18
  */
19
- export const Chip = React.forwardRef(({ tone, className, asChild, children, ...props }, ref) => {
20
- return (_jsx(Primitive.span, { ref: ref, asChild: asChild, className: chipRecipe({ tone, className }), ...props, children: children }));
19
+ export const Chip = React.forwardRef(({ tone, truncate, className, asChild, children, ...props }, ref) => {
20
+ const content = truncate && !asChild && typeof children === "string" ? (_jsx("span", { className: "crisp-chip-label", children: children })) : (children);
21
+ return (_jsx(Primitive.span, { ref: ref, asChild: asChild, className: chipRecipe({ tone, truncate, className }), ...props, children: content }));
21
22
  });
22
23
  Chip.displayName = "Chip";
@@ -17,7 +17,16 @@ const spec = defineSpec({
17
17
  base: {
18
18
  display: "inline-flex",
19
19
  alignItems: "center",
20
+ // Measured on Attio's own multi-child chip (Tasks page, "Assigned to"
21
+ // column: a 16px avatar + name in a neutral chip) -- the 3px gap between
22
+ // them. A single plain-text child (the only case before PersonChip) is
23
+ // unaffected, since `gap` does nothing with one flex child.
24
+ gap: "3px",
20
25
  height: c.size.chip,
26
+ // Lets the chip shrink below its content width inside a flex/grid cell
27
+ // -- `inline-flex` items don't by default -- which `truncate` below
28
+ // depends on and any consumer constraining a Chip's width already needs.
29
+ minWidth: 0,
21
30
  paddingInline: c.space["1"],
22
31
  borderRadius: c.radius.chip,
23
32
  fontSize: c.text.lg,
@@ -26,6 +35,33 @@ const spec = defineSpec({
26
35
  whiteSpace: "nowrap",
27
36
  },
28
37
  variants: {
38
+ truncate: {
39
+ // Ellipsis + `min-width: 0`, the same convention crisp's own
40
+ // `.crisp-datatable-entity-name`/`.crisp-datatable-th-label` already
41
+ // use for cramped text -- there is no single-overflowing-chip pattern
42
+ // to measure from Attio itself (its multi-value fields show a "+N"
43
+ // badge instead of truncating one chip's own text: Categories,
44
+ // Companies). Targets `.crisp-chip-label`, the inner span `Chip`
45
+ // wraps its string children in when `truncate` is set, not the root
46
+ // -- the root stays a flex row so a leading icon/avatar never shrinks
47
+ // alongside the text.
48
+ true: {
49
+ decls: {},
50
+ parts: [
51
+ {
52
+ selector: "& .crisp-chip-label",
53
+ decls: {
54
+ minWidth: 0,
55
+ overflow: "hidden",
56
+ textOverflow: "ellipsis",
57
+ whiteSpace: "nowrap",
58
+ display: "block",
59
+ },
60
+ },
61
+ ],
62
+ },
63
+ false: {},
64
+ },
29
65
  tone: {
30
66
  neutral: {
31
67
  background: soft(c.fgPrimary, 6),
@@ -69,7 +105,7 @@ const spec = defineSpec({
69
105
  },
70
106
  },
71
107
  },
72
- defaultVariants: { tone: "neutral" },
108
+ defaultVariants: { tone: "neutral", truncate: false },
73
109
  });
74
110
  const config = specToRecipe(spec);
75
111
  export const chipRecipe = Object.assign(resolveRecipe(config), { config });