@crisp-ui-kit/crisp 0.56.0 → 0.57.1

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 (139) hide show
  1. package/dist/accentpicker.css +5 -0
  2. package/dist/accentpicker.d.ts +7 -0
  3. package/dist/accentpicker.layered.css +7 -0
  4. package/dist/avatar.css +1 -1
  5. package/dist/avatar.layered.css +1 -1
  6. package/dist/badge.css +2 -2
  7. package/dist/badge.layered.css +2 -2
  8. package/dist/button.css +3 -3
  9. package/dist/button.layered.css +3 -3
  10. package/dist/callout.css +6 -0
  11. package/dist/callout.d.ts +2 -2
  12. package/dist/callout.layered.css +6 -0
  13. package/dist/cjs/components/accentpicker/accentpicker.js +66 -0
  14. package/dist/cjs/components/accentpicker/accentpicker.recipe.js +25 -0
  15. package/dist/cjs/components/accentpicker/index.js +7 -0
  16. package/dist/cjs/components/avatar/avatar.recipe.js +4 -1
  17. package/dist/cjs/components/badge/badge.recipe.js +16 -2
  18. package/dist/cjs/components/button/button.recipe.js +16 -6
  19. package/dist/cjs/components/callout/callout.js +2 -2
  20. package/dist/cjs/components/callout/callout.recipe.js +15 -2
  21. package/dist/cjs/components/input/input.js +2 -2
  22. package/dist/cjs/components/input/input.recipe.js +14 -0
  23. package/dist/cjs/components/menu/menu.js +2 -2
  24. package/dist/cjs/components/menu/menu.recipe.js +8 -0
  25. package/dist/cjs/components/multiselect/multiselect.js +23 -10
  26. package/dist/cjs/components/multiselect/multiselect.recipe.js +104 -65
  27. package/dist/cjs/components/progress/progress.js +21 -4
  28. package/dist/cjs/components/progress/progress.recipe.js +30 -0
  29. package/dist/cjs/components/select/listbox-keyboard.js +7 -1
  30. package/dist/cjs/components/select/select.js +24 -11
  31. package/dist/cjs/components/select/select.recipe.js +177 -78
  32. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +3 -3
  33. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
  34. package/dist/cjs/components/tabs/tabs.js +63 -4
  35. package/dist/cjs/components/tabs/tabs.recipe.js +13 -0
  36. package/dist/cjs/components/text/text.recipe.js +8 -7
  37. package/dist/cjs/components/textarea/textarea.js +3 -2
  38. package/dist/cjs/components/textarea/textarea.recipe.js +2 -1
  39. package/dist/cjs/components/textlink/textlink.js +2 -2
  40. package/dist/cjs/components/textlink/textlink.recipe.js +24 -3
  41. package/dist/cjs/index.js +1 -0
  42. package/dist/cjs/product/recordpage/recordpage.js +61 -7
  43. package/dist/cjs/tokens/elevation.js +7 -0
  44. package/dist/cjs/tokens/palette.js +120 -25
  45. package/dist/cjs/tokens/scale.js +9 -0
  46. package/dist/cjs/tokens/semantic.js +42 -0
  47. package/dist/esm/components/accentpicker/accentpicker.js +30 -0
  48. package/dist/esm/components/accentpicker/accentpicker.recipe.js +22 -0
  49. package/dist/esm/components/accentpicker/index.js +2 -0
  50. package/dist/esm/components/avatar/avatar.recipe.js +4 -1
  51. package/dist/esm/components/badge/badge.recipe.js +16 -2
  52. package/dist/esm/components/button/button.recipe.js +16 -6
  53. package/dist/esm/components/callout/callout.js +2 -2
  54. package/dist/esm/components/callout/callout.recipe.js +15 -2
  55. package/dist/esm/components/input/input.js +3 -3
  56. package/dist/esm/components/input/input.recipe.js +14 -0
  57. package/dist/esm/components/menu/menu.js +2 -2
  58. package/dist/esm/components/menu/menu.recipe.js +8 -0
  59. package/dist/esm/components/multiselect/multiselect.js +23 -10
  60. package/dist/esm/components/multiselect/multiselect.recipe.js +104 -65
  61. package/dist/esm/components/progress/progress.js +22 -5
  62. package/dist/esm/components/progress/progress.recipe.js +30 -0
  63. package/dist/esm/components/select/listbox-keyboard.js +7 -1
  64. package/dist/esm/components/select/select.js +24 -11
  65. package/dist/esm/components/select/select.recipe.js +177 -78
  66. package/dist/esm/components/tabletoolbar/tabletoolbar.js +3 -3
  67. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
  68. package/dist/esm/components/tabs/tabs.js +63 -4
  69. package/dist/esm/components/tabs/tabs.recipe.js +13 -0
  70. package/dist/esm/components/text/text.recipe.js +8 -7
  71. package/dist/esm/components/textarea/textarea.js +3 -2
  72. package/dist/esm/components/textarea/textarea.recipe.js +2 -1
  73. package/dist/esm/components/textlink/textlink.js +2 -2
  74. package/dist/esm/components/textlink/textlink.recipe.js +24 -3
  75. package/dist/esm/index.js +1 -0
  76. package/dist/esm/product/recordpage/recordpage.js +62 -8
  77. package/dist/esm/tokens/elevation.js +7 -0
  78. package/dist/esm/tokens/palette.js +120 -25
  79. package/dist/esm/tokens/scale.js +9 -0
  80. package/dist/esm/tokens/semantic.js +42 -0
  81. package/dist/input.css +11 -0
  82. package/dist/input.d.ts +2 -2
  83. package/dist/input.layered.css +11 -0
  84. package/dist/menu.css +8 -0
  85. package/dist/menu.layered.css +8 -0
  86. package/dist/multiselect.css +57 -29
  87. package/dist/multiselect.d.ts +3 -1
  88. package/dist/multiselect.layered.css +57 -29
  89. package/dist/native/tokens.d.ts +1 -1
  90. package/dist/native/tokens.js +82 -28
  91. package/dist/progress.css +3 -0
  92. package/dist/progress.layered.css +3 -0
  93. package/dist/select.css +100 -42
  94. package/dist/select.d.ts +3 -2
  95. package/dist/select.layered.css +100 -42
  96. package/dist/styles.css +268 -93
  97. package/dist/styles.layered.css +268 -93
  98. package/dist/tabletoolbar.css +8 -1
  99. package/dist/tabletoolbar.layered.css +8 -1
  100. package/dist/tabs.css +6 -0
  101. package/dist/tabs.layered.css +6 -0
  102. package/dist/text.css +1 -1
  103. package/dist/text.layered.css +1 -1
  104. package/dist/textarea.css +2 -1
  105. package/dist/textarea.layered.css +2 -1
  106. package/dist/textlink.css +11 -2
  107. package/dist/textlink.d.ts +2 -1
  108. package/dist/textlink.layered.css +11 -2
  109. package/dist/tokens.css +43 -11
  110. package/dist/tokens.json +82 -28
  111. package/dist/types/components/accentpicker/accentpicker.d.ts +23 -0
  112. package/dist/types/components/accentpicker/accentpicker.recipe.d.ts +3 -0
  113. package/dist/types/components/accentpicker/index.d.ts +2 -0
  114. package/dist/types/components/button/button.recipe.d.ts +2 -2
  115. package/dist/types/components/callout/callout.d.ts +9 -1
  116. package/dist/types/components/callout/callout.recipe.d.ts +12 -2
  117. package/dist/types/components/input/input.d.ts +14 -0
  118. package/dist/types/components/input/input.recipe.d.ts +3 -0
  119. package/dist/types/components/menu/menu.d.ts +7 -1
  120. package/dist/types/components/multiselect/multiselect.d.ts +25 -6
  121. package/dist/types/components/multiselect/multiselect.recipe.d.ts +27 -0
  122. package/dist/types/components/progress/progress.d.ts +16 -4
  123. package/dist/types/components/progress/progress.recipe.d.ts +22 -0
  124. package/dist/types/components/select/listbox-keyboard.d.ts +10 -1
  125. package/dist/types/components/select/select.d.ts +29 -6
  126. package/dist/types/components/select/select.recipe.d.ts +25 -10
  127. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +14 -0
  128. package/dist/types/components/tabs/tabs.d.ts +9 -1
  129. package/dist/types/components/textarea/textarea.d.ts +16 -0
  130. package/dist/types/components/textlink/textlink.d.ts +8 -1
  131. package/dist/types/components/textlink/textlink.recipe.d.ts +10 -0
  132. package/dist/types/index.d.ts +1 -0
  133. package/dist/types/product/recordpage/recordpage.d.ts +61 -1
  134. package/dist/types/tokens/contract.d.ts +16 -3
  135. package/dist/types/tokens/elevation.d.ts +7 -0
  136. package/dist/types/tokens/palette.d.ts +55 -11
  137. package/dist/types/tokens/scale.d.ts +6 -0
  138. package/dist/types/tokens/semantic.d.ts +33 -0
  139. package/package.json +6 -1
@@ -21,11 +21,32 @@ const config = (0, core_1.defineRecipe)({
21
21
  variants: {
22
22
  tone: {
23
23
  // The bold, always-brand-coloured trailing link a Card's own `action`
24
- // slot uses ("View all →", "All registers →").
25
- brand: `font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.bgBrandSolid};`,
24
+ // slot uses ("View all →", "All registers →"). your-moon/crisp#897:
25
+ // `fgBrandContrast`, not `bgBrandSolid` (#0298d9) directly -- as TEXT
26
+ // on a white page the accent itself only clears 3.23:1; the darker
27
+ // contrast-safe sibling clears 4.5:1+.
28
+ brand: `font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgBrandContrast};`,
26
29
  // A quieter in-context reference (a sample record under a finding)
27
30
  // that only goes brand on hover.
28
- muted: `font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.regular}; color: ${tokens_1.contract.fgMuted}; &:hover { color: ${tokens_1.contract.bgBrandSolid}; }`,
31
+ muted: `font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.regular}; color: ${tokens_1.contract.fgMuted}; &:hover { color: ${tokens_1.contract.fgBrandContrast}; }`,
32
+ },
33
+ // crisp#874: an explicit, OPTIONAL size override -- no `defaultVariants`
34
+ // entry below, so omitting `size` emits no `--size_*` class at all and
35
+ // each tone's own hard-coded font-size (above) stands completely
36
+ // unchanged. Only when a caller passes `size` explicitly does this
37
+ // override it: `size`'s CSS block is emitted AFTER `tone`'s in the
38
+ // generated stylesheet (`generate-recipes.mjs` walks `variants` in
39
+ // object-key order), so at equal specificity it wins the cascade.
40
+ // Measured live (a running GRC server, `getComputedStyle`):
41
+ // internal/grc's two primary-column record links
42
+ // (routes/lists/Lists.tsx:51, routes/registers/Registers.tsx:63) are
43
+ // 14px/500 ambient, inherited from DataTable's own
44
+ // `.crisp-datatable-entity-name` -- `size="lg"` matches that context
45
+ // without changing `brand`'s weight/colour.
46
+ size: {
47
+ sm: `font-size: ${tokens_1.contract.text.sm};`,
48
+ md: `font-size: ${tokens_1.contract.text.md};`,
49
+ lg: `font-size: ${tokens_1.contract.text.lg};`,
29
50
  },
30
51
  },
31
52
  defaultVariants: { tone: "muted" },
package/dist/cjs/index.js CHANGED
@@ -18,6 +18,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  // primitives (components/)
21
+ __exportStar(require("./components/accentpicker"), exports);
21
22
  __exportStar(require("./components/accordion"), exports);
22
23
  __exportStar(require("./components/avatar"), exports);
23
24
  __exportStar(require("./components/avatargroup"), exports);
@@ -41,12 +41,14 @@ const lucide_react_1 = require("lucide-react");
41
41
  const React = __importStar(require("react"));
42
42
  const avatar_1 = require("../../components/avatar");
43
43
  const checkbox_1 = require("../../components/checkbox");
44
+ const drawer_1 = require("../../components/drawer");
44
45
  const menu_1 = require("../../components/menu");
45
46
  const select_1 = require("../../components/select");
46
47
  const spinner_1 = require("../../components/spinner");
47
48
  const switch_1 = require("../../components/switch");
48
49
  const tabs_1 = require("../../components/tabs");
49
50
  const core_1 = require("../../core");
51
+ const comments_1 = require("../comments");
50
52
  const column_1 = require("./column");
51
53
  const editable_field_1 = require("./editable-field");
52
54
  const recordpage_recipe_1 = require("./recordpage.recipe");
@@ -71,19 +73,32 @@ exports.recordPageLabels = {
71
73
  breadcrumb: "Breadcrumb",
72
74
  editRecord: "Edit record",
73
75
  recordActions: "Record actions",
74
- addToFavourites: "Add to favourites",
75
- removeFromFavourites: "Remove from favourites",
76
+ // US spelling, matching Attio's own live text exactly (#621) — this string
77
+ // is shared with the entity header's favourite-star aria-label, so both
78
+ // read "favorites" now, not just the menu.
79
+ addToFavourites: "Add to favorites",
80
+ removeFromFavourites: "Remove from favorites",
76
81
  copyRecordLink: "Copy record link",
82
+ configurePage: "Configure page",
83
+ copyPageUrl: "Copy page URL",
84
+ copyRecordId: "Copy record ID",
85
+ mergeRecords: "Merge records",
77
86
  deleteRecord: "Delete record",
78
87
  linkRecords: "Link records",
79
- logANote: "Log a note",
80
- createATask: "Create a task",
88
+ // Tooltip copy re-measured live for #865 (was "Log a note"/"Create a
89
+ // task" — the field names stay, only the visible text changes to match
90
+ // Attio's own React-tooltip strings on these two icon buttons exactly).
91
+ logANote: "New note",
92
+ createATask: "New task",
93
+ runWorkflow: "Run workflow",
81
94
  fullScreen: "Full screen",
82
95
  exitFullScreen: "Exit full screen",
83
96
  resizeDetails: "Resize details column",
84
97
  composeEmail: "Compose email",
85
98
  askAttio: "Ask AI",
86
99
  addToList: "Add to list",
100
+ share: "Share",
101
+ comments: "Comments",
87
102
  noLists: "This record has not been added to any lists.",
88
103
  highlights: "Highlights",
89
104
  recordDetails: "Record Details",
@@ -457,7 +472,7 @@ RecordDetails.displayName = "RecordDetails";
457
472
  * Emails sections (Notes & Tasks are live — add and check things off). Drop it
458
473
  * into the app's main content area.
459
474
  */
460
- function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, ...rest }) {
475
+ function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, quickActions: quickActionsProp, comments: commentsProp, onPostComment, currentUser, onShare, ...rest }) {
461
476
  const t = useRecordPageLabels(labels);
462
477
  const [favorite, setFavorite] = React.useState(false);
463
478
  // Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
@@ -469,6 +484,11 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
469
484
  const [internalTasks, setInternalTasks] = React.useState([]);
470
485
  const notes = notesProp ?? internalNotes;
471
486
  const tasks = tasksProp ?? internalTasks;
487
+ // Comments follow the same controlled/self-contained duality (#864).
488
+ const [internalComments, setInternalComments] = React.useState([]);
489
+ const comments = commentsProp ?? internalComments;
490
+ const [commentsOpen, setCommentsOpen] = React.useState(false);
491
+ const me = currentUser ?? { id: "me", name: "You" };
472
492
  const [showAllActivity, setShowAllActivity] = React.useState(false);
473
493
  // Drag-resizable left details column.
474
494
  const [detailsW, setDetailsW] = React.useState(DETAILS_DEFAULT_W);
@@ -566,6 +586,15 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
566
586
  },
567
587
  ];
568
588
  const highlights = highlightsProp === undefined ? defaultHighlights : highlightsProp;
589
+ // #865: the built-in CRM icon set for the quick-action row, used when the
590
+ // caller omits `quickActions` -- same "caller's own list, not a merge"
591
+ // choice `tabs` (#379) and `highlights` (#528) already make.
592
+ const defaultQuickActions = [
593
+ { label: t.logANote, icon: lucide_react_1.FileText },
594
+ { label: t.runWorkflow, icon: lucide_react_1.Workflow },
595
+ { label: t.createATask, icon: lucide_react_1.SquareCheck },
596
+ ];
597
+ const quickActions = quickActionsProp ?? defaultQuickActions;
569
598
  const ti = { size: 15, strokeWidth: 1.75 };
570
599
  const builtinTabs = [
571
600
  { value: "overview", label: t.tabs.overview, icon: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { ...ti }) },
@@ -618,6 +647,31 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
618
647
  }
619
648
  setInternalTasks((n) => n.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
620
649
  };
650
+ const postComment = (text, threadId) => {
651
+ if (commentsProp) {
652
+ onPostComment?.(text, threadId);
653
+ return;
654
+ }
655
+ if (threadId == null) {
656
+ setInternalComments((prev) => [
657
+ {
658
+ id: nextId(),
659
+ root: { id: nextId(), author: me, time: t.justNow, body: text },
660
+ },
661
+ ...prev,
662
+ ]);
663
+ return;
664
+ }
665
+ setInternalComments((prev) => prev.map((th) => th.id === threadId
666
+ ? {
667
+ ...th,
668
+ replies: [
669
+ ...(th.replies ?? []),
670
+ { id: nextId(), author: me, time: t.justNow, body: text },
671
+ ],
672
+ }
673
+ : th));
674
+ };
621
675
  // A small, plausible activity feed derived from the record's own fields.
622
676
  const domain = byKey.get("domain");
623
677
  const cats = byKey.get("categories");
@@ -674,7 +728,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
674
728
  const effectivePanel = tabsProp
675
729
  ? (active) => tabsProp.find((tb) => tb.id === active)?.render()
676
730
  : builtinPanel;
677
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpage_recipe_1.recordpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-topbar", children: [(0, jsx_runtime_1.jsxs)("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-name", children: name })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Minimize2, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Maximize2, { size: 15, strokeWidth: 1.75 })) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.recordActions, trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), (0, jsx_runtime_1.jsx)(menu_1.MenuSeparator, {}), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-body", children: [(0, jsx_runtime_1.jsxs)("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-entity-name", children: name }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-quickactions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: (0, jsx_runtime_1.jsx)(lucide_react_1.FileText, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Share2, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsx)(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.lists, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-addbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": DETAILS_MIN_W, "aria-valuemax": DETAILS_MAX_W, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
731
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpage_recipe_1.recordpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-topbar", children: [(0, jsx_runtime_1.jsxs)("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-name", children: name })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-actions", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", onClick: onShare, children: t.share }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: () => setCommentsOpen(true), "aria-label": t.comments, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MessageSquare, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Minimize2, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Maximize2, { size: 15, strokeWidth: 1.75 })) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.recordActions, trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Merge, { size: 15, strokeWidth: 1.75 }), children: t.mergeRecords }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Settings, { size: 15, strokeWidth: 1.75 }), children: t.configurePage }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyPageUrl }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyRecordId }), (0, jsx_runtime_1.jsx)(menu_1.MenuSeparator, {}), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), (0, jsx_runtime_1.jsx)(menu_1.MenuSeparator, {}), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, children: t.deleteRecord })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-body", children: [(0, jsx_runtime_1.jsxs)("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-entity-name", children: name }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-quickactions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 16, strokeWidth: 1.75 }), t.addToList] }), quickActions.map((a, i) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": a.label, onClick: a.onSelect, children: (0, jsx_runtime_1.jsx)(a.icon, { size: 15, strokeWidth: 1.75 }) }, i)))] }), (0, jsx_runtime_1.jsx)(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.lists, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-addbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": DETAILS_MIN_W, "aria-valuemax": DETAILS_MAX_W, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
678
732
  if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
679
733
  return;
680
734
  // The pointer drag learned its direction in #212; this did not.
@@ -686,6 +740,6 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
686
740
  setDetailsW((w) => grow
687
741
  ? Math.min(DETAILS_MAX_W, w + 16)
688
742
  : Math.max(DETAILS_MIN_W, w - 16));
689
- } }), (0, jsx_runtime_1.jsx)("main", { className: "crisp-recordpage-main", children: (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] })] }));
743
+ } }), (0, jsx_runtime_1.jsx)("main", { className: "crisp-recordpage-main", children: (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] }), (0, jsx_runtime_1.jsx)(drawer_1.Drawer, { open: commentsOpen, onOpenChange: setCommentsOpen, "aria-label": t.comments, style: { width: 320 }, children: (0, jsx_runtime_1.jsx)(comments_1.CommentsPanel, { threads: comments, currentUserId: me.id, onPost: postComment, onClose: () => setCommentsOpen(false), role: undefined }) })] }));
690
744
  }
691
745
  RecordPage.displayName = "RecordPage";
@@ -59,4 +59,11 @@ exports.elevation = {
59
59
  * nobody measured for them.
60
60
  */
61
61
  fieldNoRing: `0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04)`,
62
+ /**
63
+ * Select's own listbox shadow (crisp/#867) — seed-design's `$shadow.s3`, a
64
+ * single layer, NOT `floating` above. This component follows seed, not
65
+ * Attio, for the same reason `DatePicker` follows seed's calendar
66
+ * structure: an explicit owner request to match seed-design exactly.
67
+ */
68
+ selectFloating: `0 4px 16px ${contract_1.contract.shadowSelect}`,
62
69
  };
@@ -8,9 +8,14 @@ exports.palette = {
8
8
  // on a bright tone fill. Same value as gray900's light side (the measured
9
9
  // near-black), named separately because its job is not "primary foreground".
10
10
  ink: { light: "#101112", dark: "#101112" },
11
- gray50: { light: "#f4f4f5", dark: "#101112" },
11
+ // seed-identity migration (your-moon/crisp#890): seed's bg.layer-default-
12
+ // pressed / gray-100. Light side only — dark is untouched, unmeasured
13
+ // Attio territory this migration doesn't scope into.
14
+ gray50: { light: "#f7f8f9", dark: "#101112" },
12
15
  gray900: { light: "#101112", dark: "#f5f5f5" },
13
- line200: { light: "#e4e4e7", dark: "#212224" },
16
+ // seed-identity migration (your-moon/crisp#890): seed's stroke.neutral-
17
+ // weak / gray-400. Light side only, same scoping as `gray50` above.
18
+ line200: { light: "#dcdee3", dark: "#212224" },
14
19
  line100: { light: "#eeeff1", dark: "#1c1d1f" },
15
20
  // ImportDialog's dropzone border — measured live from Attio's Import CSV
16
21
  // flow (`rgb(230,231,234)`), distinct from `line200`/`line100` (#e4e4e7 /
@@ -22,8 +27,11 @@ exports.palette = {
22
27
  line150: { light: "#e6e7ea", dark: "#212224" },
23
28
  // The filter chip's field/value segments while the condition is
24
29
  // incomplete (no value typed yet) — measured live on Attio's own filter
25
- // chip (`rgb(246,247,247)`), lighter than `gray50` (#f4f4f5, close but not
26
- // equal). The chip's operator/kebab segments stay `gray00` in the same
30
+ // chip (`rgb(246,247,247)`), close to but not equal to `gray50`
31
+ // (`#f7f8f9` since the seed-identity migration, your-moon/crisp#890 — the
32
+ // two used to differ the other direction, gray25 lighter; the relative
33
+ // order isn't the point, staying off `gray50` while remaining a
34
+ // near-neighbour is). The chip's operator/kebab segments stay `gray00` in the same
27
35
  // state; only the two text-content segments carry this muted fill. Dark
28
36
  // value not independently re-measured (Attio's dark theme is off-limits to
29
37
  // toggle) — reuses `gray50`'s dark value, the nearest existing "muted
@@ -47,6 +55,15 @@ exports.palette = {
47
55
  // both themes (Switch's own spec), so this reuses ringField's dark value
48
56
  // rather than inventing a third "field-adjacent ring, dark" colour.
49
57
  ringSwitch: { light: "#d9dade", dark: "#212224" },
58
+ // Callout's bordered/banner look (crisp#869) — measured live on Attio's
59
+ // Objects settings page (the "Create custom objects... Upgrade" Pro-plan
60
+ // banner): `border: 1px solid rgb(214,229,255)`, an opaque colour, not a
61
+ // composited tint of `bgBrandSolid` (checked: no single color-mix
62
+ // percentage matches all three channels at once). Dark value not
63
+ // independently re-measured (Attio's own theme toggle is off-limits) —
64
+ // reuses `ringField`/`ringSwitch`'s existing dark ring (#212224), the same
65
+ // "edge visible against a dark surface" role those already establish.
66
+ ringBrandSoft: { light: "#d6e5ff", dark: "#212224" },
50
67
  tileActive: { light: "#ffffff", dark: "#101112" },
51
68
  tileActiveShadow: { light: "rgba(0,0,0,0.04)", dark: "rgba(0,0,0,0)" },
52
69
  // Dialog's page-dimming backdrop (crisp#677) — measured live behind the
@@ -75,25 +92,54 @@ exports.palette = {
75
92
  shadowPopover2: { light: "rgba(28,40,64,0.12)", dark: "rgba(0,0,0,0.24)" },
76
93
  shadowPopover3: { light: "rgba(24,41,75,0.16)", dark: "rgba(0,0,0,0.32)" },
77
94
  gray400: { light: "#71717a", dark: "#9a9aa2" },
78
- brand600: { light: "#266df0", dark: "#266df0" },
79
- brand700: { light: "#1f5fd6", dark: "#1f5fd6" },
80
- red600: { light: "#e5484d", dark: "#ff5a5f" },
81
- // A red dark enough to carry white text. red600 cannot: white on it is
82
- // 3.91:1 light / 3.05:1 dark, and the solid button's hover DARKENS the fill
83
- // (filter: brightness), so dark ink fails there instead (4.49 at hover).
84
- // Measured from the reference stylesheet, which ships this red alongside the
85
- // #EB3B3B it uses for destructive menu TEXT. Does not flip: a filled
86
- // destructive control needs the same white-bearing fill in both themes.
87
- red700: { light: "#ba2525", dark: "#ba2525" },
88
- // The #EB3B3B named in red700's own comment above (destructive menu text) —
89
- // it had never actually been given a token. Independently re-measured live
90
- // on `app.attio.com/.../tasks`, `rgb(235,59,59)`, as the overdue due-date
91
- // text colour on the Tasks page (crisp#462) — same hex, second surface.
92
- // Dark not independently re-measured (Attio's theme toggle is off-limits,
93
- // same restriction noted throughout this file); reuses red600's dark value,
94
- // the same "brightens for legibility on a dark surface" tone this red
95
- // already sits next to, rather than inventing a third red.
96
- red500: { light: "#eb3b3b", dark: "#ff5a5f" },
95
+ // seed-identity migration (your-moon/crisp#890, "Attio structure, seed
96
+ // look"): the owner's TechPartners primary (#0298D9, sourced from the
97
+ // official logo SVG + the live site's own compiled CSS — see
98
+ // ~/grc-parity/seed-identity-spec.md §1) replaces the old measured-from-
99
+ // Attio brand blue, built as seed's own carrot-scale RELATIVE lightness
100
+ // shape around that exact hex at step 600 ("Method A" in the spec — the
101
+ // verified logo hex stays literally in the scale, at the step that's
102
+ // actually rendered, rather than a mathematically tidier neighbour).
103
+ // Does not flip with theme, matching this token's own prior precedent.
104
+ brand600: { light: "#0298d9", dark: "#0298d9" },
105
+ // Method A's raw carrot-shape output (L 37.1%) — the hover/pressed-darken
106
+ // step of the ACCENT colour above, same role brand700 has always had.
107
+ // Not tuned for any particular contrast target; see `brandContrast` below
108
+ // for the token that carries that job.
109
+ brand700: { light: "#0283bb", dark: "#0283bb" },
110
+ // your-moon/crisp#897: #0298d9 (the verified brand hex, kept exactly as
111
+ // the ACCENT everywhere else — focus rings, selected states, icons,
112
+ // tints) only clears 3.23:1 against white, short of the 4.5:1 a SOLID
113
+ // fill carrying white text needs (Button primary, Badge solid+brand,
114
+ // Avatar's brand tone), or brand-coloured TEXT sitting directly on a white
115
+ // page (TextLink/Text's brand tone). This is that darker, AA-safe step —
116
+ // same hue (198°), L dropped to 34% — clearing 4.88:1 with white. An
117
+ // earlier round of this fix used dark ink text instead of finding this
118
+ // value; that read off-brand (seed's own solid buttons use white) and got
119
+ // reverted in favour of this.
120
+ brandContrast: { light: "#0278ac", dark: "#0278ac" },
121
+ // seed's red-700 (fg.critical) — the accent used for plain danger TEXT
122
+ // (an overdue due-date, a destructive menu label) and for tinting
123
+ // (Chip/Badge's soft-danger `color-mix()` inputs). NOT used for a solid
124
+ // fill paired with white text — see `red700` below for why that split
125
+ // matters here specifically.
126
+ red600: { light: "#fa342c", dark: "#ff5a5f" },
127
+ // seed's red-800 (bg.critical-solid-**pressed**), not red-700
128
+ // (bg.critical-solid-enabled, `red600` above) — deliberately the darker
129
+ // of the two. `red700` is crisp's SOLID destructive fill (Button danger,
130
+ // Badge solid+danger), which pairs with white text and, on Button,
131
+ // DARKENS further on hover/active via a brightness filter (not a second
132
+ // token, per this token's own original comment below this migration note
133
+ // — that invariant is why a dark-enough red matters at all). Seed's own
134
+ // red-700 only clears 3.76:1 with white (measured) — short of AA even
135
+ // before any hover darkening — so reusing crisp's existing "needs to
136
+ // survive getting darker" role means red-800 (5.68:1 at rest, improving
137
+ // through hover/active) is the correct step, not seed's literal
138
+ // "enabled" one. `red500` below is pure text again (no fill/contrast
139
+ // constraint), so it stays on the
140
+ // lighter red-700, matching seed exactly.
141
+ red700: { light: "#ca1d13", dark: "#ba2525" },
142
+ red500: { light: "#fa342c", dark: "#ff5a5f" },
97
143
  // seed-design's field focus ring (stroke-neutral-contrast = gray-1000),
98
144
  // measured from @seed-design/css. A high-contrast neutral rather than a brand
99
145
  // hue: the ring reads as "this field has focus", not as a state colour.
@@ -113,8 +159,25 @@ exports.palette = {
113
159
  neutralSubtle: { light: "#868b94", dark: "#b0b3ba" },
114
160
  neutralMuted: { light: "#555d6d", dark: "#dcdee3" },
115
161
  focusRing: { light: "#1e82eb", dark: "#5e98fe" },
116
- green600: { light: "#1f9d57", dark: "#2ea968" },
117
- amber600: { light: "#b5730f", dark: "#e0982a" },
162
+ // seed's fg.positive / bg.positive-solid (green-700).
163
+ green600: { light: "#079171", dark: "#2ea968" },
164
+ // seed's fg.warning / stroke.warning-solid (yellow-700) — the ACCENT used
165
+ // for tinting (Badge/Chip's `soft()`/`ring()` colour-mix formulas), not
166
+ // seed's actual solid-fill colour, which is a much paler yellow at a
167
+ // different scale step — see `amberSolid` below for why the two need to
168
+ // stay separate tokens.
169
+ amber600: { light: "#9b7821", dark: "#e0982a" },
170
+ // seed's bg.warning-solid (yellow-300) — genuinely NOT amber600 tinted
171
+ // lighter: seed's warning-solid is its own pale scale step, distinct from
172
+ // the darker accent every other tone's solid fill derives from (see the
173
+ // spec's Badge section). Badge's `variant="solid" tone="warning"` uses
174
+ // this directly, unmixed, the same way `red700`'s solid destructive fill
175
+ // is used directly rather than computed from a tint. Dark side is NOT
176
+ // independently sourced (this is a new token; no prior Attio measurement
177
+ // exists to preserve) — reuses amber600's own dark value as the nearest
178
+ // existing "warning, dark theme" tone, same convention this file uses
179
+ // throughout for an unmeasured dark side (ringSwitch, line150, gray25).
180
+ amberSolid: { light: "#fbdc65", dark: "#e0982a" },
118
181
  orange600: { light: "#f97514", dark: "#f97514" },
119
182
  cyan600: { light: "#00b9eb", dark: "#00b9eb" },
120
183
  purple600: { light: "#9b69ff", dark: "#9b69ff" },
@@ -162,4 +225,36 @@ exports.palette = {
162
225
  purpleTint: { light: "#f5f0ff", dark: "#2f1e5a" },
163
226
  softPurpleText: { light: "#6238b5", dark: "#d8c4ff" },
164
227
  purpleRing: { light: "#e8ddfe", dark: "#45297d" },
228
+ // Select rebuilt to seed-design parity (crisp/#867), NOT Attio -- a
229
+ // deliberate one-component exception, the same way DatePicker already
230
+ // follows seed's calendar structure. Values below are resolved directly
231
+ // from `@seed-design/rootage`'s color.yaml (gray palette), not measured
232
+ // live, since there is no Attio reference for this component anymore.
233
+ // gray-00 / gray-200 (bg.layer-floating) -- the listbox surface. Distinct
234
+ // from `popoverBg` (crisp's Attio-measured popover, #fff/#17181a): the dark
235
+ // value differs (#1d2025), so this is its own token, not a reuse.
236
+ selectListboxBg: { light: "#ffffff", dark: "#1d2025" },
237
+ // gray-400 (stroke.neutral-weak) -- the trigger's real 1px border at rest.
238
+ selectTriggerStroke: { light: "#dcdee3", dark: "#393d46" },
239
+ // static-black/white-alpha (bg.transparent-pressed) -- the trigger/item
240
+ // press tint. Deliberately alpha, not a gray, so it reads the same over
241
+ // either surface colour.
242
+ selectPressedTint: { light: "#00000007", dark: "#ffffff0d" },
243
+ // gray-200 / gray-300 (bg.disabled) -- the trigger's disabled AND readonly
244
+ // fill. Distinct from `neutralWeak` (light matches, dark is #1d2025 not
245
+ // #2b2e35), so its own token rather than a reuse.
246
+ selectDisabledBg: { light: "#f3f4f5", dark: "#2b2e35" },
247
+ // red-700 (stroke.critical-solid) -- the trigger's invalid border. Distinct
248
+ // from crisp's own `bgDangerSolid`/`red700` (#ba2525, an Attio red): seed's
249
+ // red is a different hue entirely, and this component follows seed.
250
+ selectInvalidStroke: { light: "#fa342c", dark: "#ff6e60" },
251
+ // static-black/white-alpha (stroke.neutral-muted) -- the listbox's own
252
+ // divider slot (defined in select.yaml; not yet exposed on any consumer).
253
+ selectDivider: { light: "#00000010", dark: "#ffffff17" },
254
+ // gray-600 (fg.placeholder) -- distinct from `neutralSubtle` (its light/dark
255
+ // pair is the same two hexes, swapped), so its own token, not a reuse.
256
+ selectPlaceholder: { light: "#b0b3ba", dark: "#868b94" },
257
+ // s3 (shadow.yaml) -- the listbox's single-layer floating shadow, in place
258
+ // of crisp's own multi-layer `elevation.floating` for this component only.
259
+ selectShadow: { light: "#0000001f", dark: "#000000cc" },
165
260
  };
@@ -20,6 +20,7 @@ exports.space = {
20
20
  "2": "8px",
21
21
  "2.5": "10px",
22
22
  "3": "12px",
23
+ "3.5": "14px", // seed-design's $dimension.x3_5 — Select's medium trigger paddingX (crisp/#867)
23
24
  "4": "16px",
24
25
  "5": "20px",
25
26
  "6": "24px",
@@ -28,12 +29,14 @@ exports.space = {
28
29
  // Corner radius. sm/md/lg are the three used on stacked surfaces
29
30
  // (inputs, rows, tiles); full is the pill.
30
31
  exports.radius = {
32
+ xs: "4px", // seed-design's $radius.r1 — small badge / checkbox box corner
31
33
  chip: "7px", // tag / chip corner — the single most used radius
32
34
  sm: "6px", // chips, badges
33
35
  md: "8px", // inputs, search box
34
36
  lg: "9px", // rows, tiles, list items
35
37
  xl: "12px", // overlay items (command rows)
36
38
  "2xl": "16px", // overlay panels (command surface)
39
+ "3xl": "20px", // seed-design's $radius.r5 — Select's listbox corner (crisp/#867)
37
40
  full: "999px", // pills
38
41
  };
39
42
  // Component sizing — recurring measured dimensions that are neither spacing nor
@@ -52,6 +55,12 @@ exports.size = {
52
55
  // whole cells and a column gap would cut it.
53
56
  calendarCell: "34px", // calendar day pitch (32px day + 2px gap)
54
57
  calendarPill: "32px", // the day highlight itself
58
+ // seed-design's Select, resolved from select-trigger.yaml/select-item.yaml
59
+ // (crisp/#867) — this component follows seed, not Attio, so these sit
60
+ // alongside the measured scale rather than folding into it.
61
+ selectTriggerMd: "40px", // $dimension.x10 — the medium (default) trigger height
62
+ selectTriggerLg: "52px", // $dimension.x13 — the large trigger height
63
+ selectIconLg: "20px", // $dimension.x5 — large trigger's chevron
55
64
  };
56
65
  // Typography — the measured UI runs entirely on Inter. Sizes measured in the sidebar
57
66
  // collapse onto 11/12/13/14/16 (lg = 14px is the default body/label size);
@@ -4,6 +4,17 @@ exports.semantic = void 0;
4
4
  exports.semantic = {
5
5
  bgBrandSolid: "brand600",
6
6
  bgBrandSolidPressed: "brand700",
7
+ /** your-moon/crisp#897: the AA-safe darker sibling of `bgBrandSolid` — a
8
+ * SOLID fill carrying white text (Button primary, Badge solid+brand,
9
+ * Avatar's brand tone). `bgBrandSolid` itself stays the exact brand
10
+ * accent (focus rings, selected states, icons, tints), unpaired with
11
+ * white text anywhere that matters for contrast. */
12
+ bgBrandContrast: "brandContrast",
13
+ /** your-moon/crisp#897: brand-coloured TEXT sitting on a white page
14
+ * (TextLink/Text's `brand` tone) — same value as `bgBrandContrast`,
15
+ * named separately because one's a background and one's a foreground,
16
+ * matching every other fg/bg pair in this file. */
17
+ fgBrandContrast: "brandContrast",
7
18
  fgOnBrand: "white",
8
19
  // text on a solid success/warning/danger fill. Those tone tokens BRIGHTEN in
9
20
  // dark (they are tuned for tinted backgrounds and coloured text), so white on
@@ -41,6 +52,15 @@ exports.semantic = {
41
52
  shadowPopover2: "shadowPopover2",
42
53
  shadowPopover3: "shadowPopover3",
43
54
  fgPrimary: "gray900",
55
+ // seed-identity migration (your-moon/crisp#890): NOT touched here, on
56
+ // purpose. seed's closest analog (fg.neutral-subtle, #868b94) is lighter
57
+ // than crisp's current #71717a, and Badge's neutral-SOLID variant reuses
58
+ // this exact token as a BACKGROUND paired with white text
59
+ // (`badge.recipe.ts`'s compound variant) — #868b94 drops that pairing to
60
+ // 3.42:1, below AA. Retargeting `fgMuted` and fixing that one background
61
+ // choice belong together in Badge's own upcoming look PR, not split
62
+ // across two PRs where the token change alone would ship a known
63
+ // contrast regression.
44
64
  fgMuted: "gray400",
45
65
  bgDanger: "red600",
46
66
  /** Fill for a SOLID destructive control that carries white text. */
@@ -66,6 +86,10 @@ exports.semantic = {
66
86
  strokeFocusRing: "focusRing",
67
87
  bgSuccess: "green600",
68
88
  bgWarning: "amber600",
89
+ /** A SOLID warning fill (Badge's `variant="solid" tone="warning"`) — seed's
90
+ * own warning-solid is a distinct, paler yellow, not `bgWarning` tinted
91
+ * lighter. See `amberSolid` in palette.ts. */
92
+ bgWarningSolid: "amberSolid",
69
93
  bgOrange: "orange600",
70
94
  bgCyan: "cyan600",
71
95
  bgPurple: "purple600",
@@ -85,4 +109,22 @@ exports.semantic = {
85
109
  bgPurpleSoft: "purpleTint",
86
110
  fgPurpleSoft: "softPurpleText",
87
111
  ringPurpleSoft: "purpleRing",
112
+ /** Callout's bordered/banner look (crisp#869) — an opaque measured ring,
113
+ * not a `color-mix` tint of `bgBrandSolid` (see palette.ts's own comment). */
114
+ ringBrandSoft: "ringBrandSoft",
115
+ /** Select rebuilt to seed-design parity (crisp/#867) — see palette.ts's
116
+ * comment above `selectListboxBg` for why these are their own tokens
117
+ * rather than a reuse of an Attio-measured one. */
118
+ /** Seed's neutral ink (gray-1000) used as TEXT/icon colour — same palette
119
+ * entry as `strokeFieldFocus` (a ring), aliased separately because the
120
+ * role here is content colour, not a focus ring. */
121
+ fgSelectNeutral: "fieldFocus",
122
+ bgSelectListbox: "selectListboxBg",
123
+ strokeSelectTrigger: "selectTriggerStroke",
124
+ bgSelectPressed: "selectPressedTint",
125
+ bgSelectDisabled: "selectDisabledBg",
126
+ strokeSelectInvalid: "selectInvalidStroke",
127
+ strokeSelectDivider: "selectDivider",
128
+ fgSelectPlaceholder: "selectPlaceholder",
129
+ shadowSelect: "selectShadow",
88
130
  };
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { useRadioGroupKeyboard } from "../radiogroup/index.js";
4
+ import { accentpickerRecipe } from "./accentpicker.recipe.js";
5
+ /**
6
+ * A row of selectable colour swatches — Attio's own accent-colour picker
7
+ * (Settings > Appearance). Not a fixed palette: pass the colours (and the
8
+ * accent isn't crisp's concern to theme the app with — this only picks one).
9
+ * Same roving-focus/arrow-key model every other crisp radio-like control
10
+ * shares (`useRadioGroupKeyboard`, also used by `RadioGroup` and
11
+ * `Segmented`).
12
+ */
13
+ export function AccentPicker({ colors, value, defaultValue, onValueChange, className, onKeyDown: onKeyDownProp, ...rest }) {
14
+ const [internal, setInternal] = React.useState(defaultValue);
15
+ const selected = value ?? internal;
16
+ const select = (v) => {
17
+ if (value === undefined)
18
+ setInternal(v);
19
+ onValueChange?.(v);
20
+ };
21
+ const { onKeyDown, groupRef } = useRadioGroupKeyboard({ select });
22
+ return (_jsx("div", { ref: groupRef, role: "radiogroup", className: accentpickerRecipe({ className }), ...rest, onKeyDown: (e) => {
23
+ onKeyDown(e);
24
+ onKeyDownProp?.(e);
25
+ }, children: colors.map((color) => {
26
+ const checked = selected === color;
27
+ return (_jsx("button", { type: "button", role: "radio", "aria-checked": checked, "aria-label": color, "data-value": color, tabIndex: checked ? 0 : -1, className: "crisp-accentpicker-swatch", style: { borderColor: checked ? color : "transparent" }, onClick: () => select(color), children: _jsx("svg", { width: "22", height: "22", viewBox: "0 0 22 22", fill: "none", "aria-hidden": "true", children: _jsx("circle", { cx: "11", cy: "11", r: "9", fill: color }) }) }, color));
28
+ }) }));
29
+ }
30
+ AccentPicker.displayName = "AccentPicker";
@@ -0,0 +1,22 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A row of selectable colour swatches — measured live on Attio's Appearance
4
+ // settings page (crisp#635): `role="radiogroup"`, `display: flex; gap: 8px`.
5
+ // Each swatch is a 24x24 circular button, `border: 1px solid transparent`
6
+ // at rest; the CHECKED swatch's border switches to its own colour (not a
7
+ // fixed brand ring) — measured directly on the DOM (`border: 1px solid
8
+ // rgb(38,109,240)` on the checked blue swatch, `1px solid rgba(0,0,0,0)` on
9
+ // every unchecked one). That per-swatch colour is data, not a class, so it's
10
+ // set inline by the component, not here. Inside sits a 22x22 SVG with one
11
+ // solid r=9 (18px) circle -- no separate ring/dot layer, contrary to an
12
+ // earlier, less precise read of this same control.
13
+ const config = defineRecipe({
14
+ name: "accentpicker",
15
+ base: `display: flex; gap: ${c.space["2"]};
16
+ & .crisp-accentpicker-swatch { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
17
+ width: 24px; height: 24px; padding: 0; margin: 0; border-radius: ${c.radius.full}; border: 1px solid transparent;
18
+ background: none; font-family: inherit; cursor: pointer; }
19
+ & .crisp-accentpicker-swatch:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 2px; }`,
20
+ });
21
+ export const accentpickerRecipe = resolveRecipe(config);
22
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { AccentPicker } from "./accentpicker.js";
2
+ export { accentpickerRecipe } from "./accentpicker.recipe.js";
@@ -36,7 +36,10 @@ const spec = defineSpec({
36
36
  color: c.fgNeutralMuted,
37
37
  boxShadow: `inset 0 0 0 1px ${c.borderDefault}`,
38
38
  },
39
- brand: { background: c.bgBrandSolid, color: "#fff" },
39
+ // your-moon/crisp#897: `bgBrandContrast`, the darker AA-safe sibling
40
+ // of the accent blue (`bgBrandSolid`, #0298d9, only 3.23:1 with
41
+ // white) -- keeps white text, matching every other tone here.
42
+ brand: { background: c.bgBrandContrast, color: "#fff" },
40
43
  orange: { background: c.bgOrange, color: "#fff" },
41
44
  cyan: { background: c.bgCyan, color: "#fff" },
42
45
  purple: { background: c.bgPurple, color: "#fff" },