@crisp-ui-kit/crisp 0.57.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 (96) 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/cjs/components/accentpicker/accentpicker.js +66 -0
  11. package/dist/cjs/components/accentpicker/accentpicker.recipe.js +25 -0
  12. package/dist/cjs/components/accentpicker/index.js +7 -0
  13. package/dist/cjs/components/avatar/avatar.recipe.js +4 -1
  14. package/dist/cjs/components/badge/badge.recipe.js +16 -2
  15. package/dist/cjs/components/button/button.recipe.js +16 -6
  16. package/dist/cjs/components/input/input.js +2 -2
  17. package/dist/cjs/components/input/input.recipe.js +8 -0
  18. package/dist/cjs/components/multiselect/multiselect.js +1 -1
  19. package/dist/cjs/components/multiselect/multiselect.recipe.js +12 -3
  20. package/dist/cjs/components/progress/progress.js +21 -4
  21. package/dist/cjs/components/progress/progress.recipe.js +30 -0
  22. package/dist/cjs/components/select/select.js +2 -2
  23. package/dist/cjs/components/select/select.recipe.js +32 -5
  24. package/dist/cjs/components/text/text.recipe.js +8 -7
  25. package/dist/cjs/components/textarea/textarea.js +3 -2
  26. package/dist/cjs/components/textarea/textarea.recipe.js +2 -1
  27. package/dist/cjs/components/textlink/textlink.recipe.js +6 -3
  28. package/dist/cjs/index.js +1 -0
  29. package/dist/cjs/product/recordpage/recordpage.js +46 -5
  30. package/dist/cjs/tokens/palette.js +79 -25
  31. package/dist/cjs/tokens/scale.js +1 -0
  32. package/dist/cjs/tokens/semantic.js +24 -0
  33. package/dist/esm/components/accentpicker/accentpicker.js +30 -0
  34. package/dist/esm/components/accentpicker/accentpicker.recipe.js +22 -0
  35. package/dist/esm/components/accentpicker/index.js +2 -0
  36. package/dist/esm/components/avatar/avatar.recipe.js +4 -1
  37. package/dist/esm/components/badge/badge.recipe.js +16 -2
  38. package/dist/esm/components/button/button.recipe.js +16 -6
  39. package/dist/esm/components/input/input.js +3 -3
  40. package/dist/esm/components/input/input.recipe.js +8 -0
  41. package/dist/esm/components/multiselect/multiselect.js +1 -1
  42. package/dist/esm/components/multiselect/multiselect.recipe.js +12 -3
  43. package/dist/esm/components/progress/progress.js +22 -5
  44. package/dist/esm/components/progress/progress.recipe.js +30 -0
  45. package/dist/esm/components/select/select.js +2 -2
  46. package/dist/esm/components/select/select.recipe.js +32 -5
  47. package/dist/esm/components/text/text.recipe.js +8 -7
  48. package/dist/esm/components/textarea/textarea.js +3 -2
  49. package/dist/esm/components/textarea/textarea.recipe.js +2 -1
  50. package/dist/esm/components/textlink/textlink.recipe.js +6 -3
  51. package/dist/esm/index.js +1 -0
  52. package/dist/esm/product/recordpage/recordpage.js +47 -6
  53. package/dist/esm/tokens/palette.js +79 -25
  54. package/dist/esm/tokens/scale.js +1 -0
  55. package/dist/esm/tokens/semantic.js +24 -0
  56. package/dist/input.css +7 -0
  57. package/dist/input.layered.css +7 -0
  58. package/dist/multiselect.css +4 -0
  59. package/dist/multiselect.d.ts +2 -2
  60. package/dist/multiselect.layered.css +4 -0
  61. package/dist/native/tokens.d.ts +1 -1
  62. package/dist/native/tokens.js +33 -22
  63. package/dist/progress.css +3 -0
  64. package/dist/progress.layered.css +3 -0
  65. package/dist/select.css +22 -2
  66. package/dist/select.d.ts +2 -2
  67. package/dist/select.layered.css +22 -2
  68. package/dist/styles.css +71 -23
  69. package/dist/styles.layered.css +71 -23
  70. package/dist/text.css +1 -1
  71. package/dist/text.layered.css +1 -1
  72. package/dist/textarea.css +2 -1
  73. package/dist/textarea.layered.css +2 -1
  74. package/dist/textlink.css +2 -2
  75. package/dist/textlink.layered.css +2 -2
  76. package/dist/tokens.css +18 -11
  77. package/dist/tokens.json +33 -22
  78. package/dist/types/components/accentpicker/accentpicker.d.ts +23 -0
  79. package/dist/types/components/accentpicker/accentpicker.recipe.d.ts +3 -0
  80. package/dist/types/components/accentpicker/index.d.ts +2 -0
  81. package/dist/types/components/button/button.recipe.d.ts +2 -2
  82. package/dist/types/components/input/input.d.ts +12 -0
  83. package/dist/types/components/multiselect/multiselect.d.ts +4 -3
  84. package/dist/types/components/multiselect/multiselect.recipe.d.ts +3 -0
  85. package/dist/types/components/progress/progress.d.ts +16 -4
  86. package/dist/types/components/progress/progress.recipe.d.ts +22 -0
  87. package/dist/types/components/select/select.d.ts +6 -5
  88. package/dist/types/components/select/select.recipe.d.ts +3 -0
  89. package/dist/types/components/textarea/textarea.d.ts +16 -0
  90. package/dist/types/index.d.ts +1 -0
  91. package/dist/types/product/recordpage/recordpage.d.ts +37 -1
  92. package/dist/types/tokens/contract.d.ts +4 -1
  93. package/dist/types/tokens/palette.d.ts +19 -11
  94. package/dist/types/tokens/scale.d.ts +1 -0
  95. package/dist/types/tokens/semantic.d.ts +15 -0
  96. package/package.json +6 -1
@@ -21,11 +21,14 @@ 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}; }`,
29
32
  },
30
33
  // crisp#874: an explicit, OPTIONAL size override -- no `defaultVariants`
31
34
  // entry below, so omitting `size` emits no `--size_*` class at all and
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,9 +73,16 @@ 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
88
  // Tooltip copy re-measured live for #865 (was "Log a note"/"Create a
@@ -88,6 +97,8 @@ exports.recordPageLabels = {
88
97
  composeEmail: "Compose email",
89
98
  askAttio: "Ask AI",
90
99
  addToList: "Add to list",
100
+ share: "Share",
101
+ comments: "Comments",
91
102
  noLists: "This record has not been added to any lists.",
92
103
  highlights: "Highlights",
93
104
  recordDetails: "Record Details",
@@ -461,7 +472,7 @@ RecordDetails.displayName = "RecordDetails";
461
472
  * Emails sections (Notes & Tasks are live — add and check things off). Drop it
462
473
  * into the app's main content area.
463
474
  */
464
- 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, ...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 }) {
465
476
  const t = useRecordPageLabels(labels);
466
477
  const [favorite, setFavorite] = React.useState(false);
467
478
  // Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
@@ -473,6 +484,11 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
473
484
  const [internalTasks, setInternalTasks] = React.useState([]);
474
485
  const notes = notesProp ?? internalNotes;
475
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" };
476
492
  const [showAllActivity, setShowAllActivity] = React.useState(false);
477
493
  // Drag-resizable left details column.
478
494
  const [detailsW, setDetailsW] = React.useState(DETAILS_DEFAULT_W);
@@ -631,6 +647,31 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
631
647
  }
632
648
  setInternalTasks((n) => n.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
633
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
+ };
634
675
  // A small, plausible activity feed derived from the record's own fields.
635
676
  const domain = byKey.get("domain");
636
677
  const cats = byKey.get("categories");
@@ -687,7 +728,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
687
728
  const effectivePanel = tabsProp
688
729
  ? (active) => tabsProp.find((tb) => tb.id === active)?.render()
689
730
  : builtinPanel;
690
- 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.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) => {
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) => {
691
732
  if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
692
733
  return;
693
734
  // The pointer drag learned its direction in #212; this did not.
@@ -699,6 +740,6 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
699
740
  setDetailsW((w) => grow
700
741
  ? Math.min(DETAILS_MAX_W, w + 16)
701
742
  : Math.max(DETAILS_MIN_W, w - 16));
702
- } }), (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 }) })] }));
703
744
  }
704
745
  RecordPage.displayName = "RecordPage";
@@ -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
@@ -84,25 +92,54 @@ exports.palette = {
84
92
  shadowPopover2: { light: "rgba(28,40,64,0.12)", dark: "rgba(0,0,0,0.24)" },
85
93
  shadowPopover3: { light: "rgba(24,41,75,0.16)", dark: "rgba(0,0,0,0.32)" },
86
94
  gray400: { light: "#71717a", dark: "#9a9aa2" },
87
- brand600: { light: "#266df0", dark: "#266df0" },
88
- brand700: { light: "#1f5fd6", dark: "#1f5fd6" },
89
- red600: { light: "#e5484d", dark: "#ff5a5f" },
90
- // A red dark enough to carry white text. red600 cannot: white on it is
91
- // 3.91:1 light / 3.05:1 dark, and the solid button's hover DARKENS the fill
92
- // (filter: brightness), so dark ink fails there instead (4.49 at hover).
93
- // Measured from the reference stylesheet, which ships this red alongside the
94
- // #EB3B3B it uses for destructive menu TEXT. Does not flip: a filled
95
- // destructive control needs the same white-bearing fill in both themes.
96
- red700: { light: "#ba2525", dark: "#ba2525" },
97
- // The #EB3B3B named in red700's own comment above (destructive menu text) —
98
- // it had never actually been given a token. Independently re-measured live
99
- // on `app.attio.com/.../tasks`, `rgb(235,59,59)`, as the overdue due-date
100
- // text colour on the Tasks page (crisp#462) — same hex, second surface.
101
- // Dark not independently re-measured (Attio's theme toggle is off-limits,
102
- // same restriction noted throughout this file); reuses red600's dark value,
103
- // the same "brightens for legibility on a dark surface" tone this red
104
- // already sits next to, rather than inventing a third red.
105
- 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" },
106
143
  // seed-design's field focus ring (stroke-neutral-contrast = gray-1000),
107
144
  // measured from @seed-design/css. A high-contrast neutral rather than a brand
108
145
  // hue: the ring reads as "this field has focus", not as a state colour.
@@ -122,8 +159,25 @@ exports.palette = {
122
159
  neutralSubtle: { light: "#868b94", dark: "#b0b3ba" },
123
160
  neutralMuted: { light: "#555d6d", dark: "#dcdee3" },
124
161
  focusRing: { light: "#1e82eb", dark: "#5e98fe" },
125
- green600: { light: "#1f9d57", dark: "#2ea968" },
126
- 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" },
127
181
  orange600: { light: "#f97514", dark: "#f97514" },
128
182
  cyan600: { light: "#00b9eb", dark: "#00b9eb" },
129
183
  purple600: { light: "#9b69ff", dark: "#9b69ff" },
@@ -29,6 +29,7 @@ exports.space = {
29
29
  // Corner radius. sm/md/lg are the three used on stacked surfaces
30
30
  // (inputs, rows, tiles); full is the pill.
31
31
  exports.radius = {
32
+ xs: "4px", // seed-design's $radius.r1 — small badge / checkbox box corner
32
33
  chip: "7px", // tag / chip corner — the single most used radius
33
34
  sm: "6px", // chips, badges
34
35
  md: "8px", // inputs, search box
@@ -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",
@@ -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" },
@@ -126,8 +126,14 @@ const spec = defineSpec({
126
126
  {
127
127
  variant: "solid",
128
128
  tone: "brand",
129
+ // your-moon/crisp#897: `bgBrandContrast`, not `bgBrandSolid` — white
130
+ // text on the exact accent blue (#0298d9) only clears 3.23:1;
131
+ // `bgBrandContrast` is the darker sibling built to clear 4.5:1+ with
132
+ // white, so `fgOnBrand` (white) stays right rather than `fgOnTone`
133
+ // (ink read off-brand next to solid `success`/`warning`/`danger`,
134
+ // which really do need it).
129
135
  decls: {
130
- background: c.bgBrandSolid,
136
+ background: c.bgBrandContrast,
131
137
  color: c.fgOnBrand,
132
138
  boxShadow: "none",
133
139
  },
@@ -140,7 +146,15 @@ const spec = defineSpec({
140
146
  {
141
147
  variant: "solid",
142
148
  tone: "warning",
143
- decls: { background: c.bgWarning, color: c.fgOnTone, boxShadow: "none" },
149
+ // seed-identity migration (your-moon/crisp#890): `bgWarningSolid`, not
150
+ // `bgWarning` — seed's own solid warning fill is a distinct, paler
151
+ // yellow from the accent every other tone's solid fill reuses
152
+ // directly (see tokens/palette.ts's `amberSolid` comment).
153
+ decls: {
154
+ background: c.bgWarningSolid,
155
+ color: c.fgOnTone,
156
+ boxShadow: "none",
157
+ },
144
158
  },
145
159
  {
146
160
  variant: "solid",
@@ -27,7 +27,13 @@ const spec = defineSpec({
27
27
  intent: {
28
28
  primary: {
29
29
  decls: {
30
- background: c.bgBrandSolid,
30
+ // your-moon/crisp#897: `bgBrandContrast`, not `bgBrandSolid` —
31
+ // white text on the exact accent blue (#0298d9) only clears
32
+ // 3.23:1; `bgBrandContrast` is the darker sibling built to clear
33
+ // white at 4.5:1+, keeping `fgOnBrand` (white) rather than
34
+ // reaching for `fgOnTone` (ink read off-brand — seed's own solid
35
+ // buttons use white).
36
+ background: c.bgBrandContrast,
31
37
  color: c.fgOnBrand,
32
38
  // crisp#494: the inset ring is a black tint, not white -- live-
33
39
  // measured on Attio's own "New Company" button as
@@ -36,12 +42,16 @@ const spec = defineSpec({
36
42
  // exactly.
37
43
  boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08)",
38
44
  },
45
+ // your-moon/crisp#897: a brightness filter, not a second background
46
+ // swap — matches `danger`'s own pattern below. `bgBrandSolidPressed`
47
+ // remains the accent's own hover-darken for the many OTHER
48
+ // consumers that pair `bgBrandSolid` with white already (out of
49
+ // scope for this fix; tracked separately), so reusing it here would
50
+ // have made primary's rest-to-hover jump inconsistent with its own
51
+ // rest fill's new, darker starting point.
39
52
  parts: [
40
- { selector: "&:hover", decls: { background: c.bgBrandSolidPressed } },
41
- {
42
- selector: "&:active",
43
- decls: { background: c.bgBrandSolidPressed },
44
- },
53
+ { selector: "&:hover", decls: { filter: "brightness(0.96)" } },
54
+ { selector: "&:active", decls: { filter: "brightness(0.92)" } },
45
55
  ],
46
56
  },
47
57
  neutral: {
@@ -1,11 +1,11 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { inputRecipe } from "./input.recipe.js";
4
- export const Input = React.forwardRef(({ size, invalid, className, style, ...props }, ref) => {
4
+ export const Input = React.forwardRef(({ size, invalid, className, style, suffix, ...props }, ref) => {
5
5
  // The wrapper exists only to host the ::after focus ring — an <input> is a
6
6
  // replaced element and cannot have one. Everything the consumer passes
7
7
  // still lands on the <input> itself, so `id`, `name`, `onChange`, form
8
8
  // association and every native attribute behave exactly as before.
9
- return (_jsx("span", { className: inputRecipe({ size, invalid, className }), style: style, children: _jsx("input", { ref: ref, className: "crisp-input-value", "aria-invalid": invalid === true || undefined, ...props }) }));
9
+ return (_jsxs("span", { className: inputRecipe({ size, invalid, className }), style: style, "data-readonly": props.readOnly ? "" : undefined, children: [_jsx("input", { ref: ref, className: "crisp-input-value", "aria-invalid": invalid === true || undefined, ...props }), suffix != null && _jsx("span", { className: "crisp-input-suffix", children: suffix })] }));
10
10
  });
11
11
  Input.displayName = "Input";
@@ -38,6 +38,14 @@ const config = defineRecipe({
38
38
  & .crisp-input-value::placeholder { color: ${c.fgMuted}; }
39
39
  & .crisp-input-value:disabled { opacity: 0.5; cursor: not-allowed; }
40
40
  &:has(.crisp-input-value:disabled) { opacity: 1; }
41
+
42
+ /* crisp#634: measured live on Attio's own Settings > Profile read-only
43
+ primary-email field -- a tinted field, not just muted text, driven off
44
+ the native \`readOnly\` attribute. \`ringField\` (rgba(0,0,0,.06)) is an
45
+ exact match for the measured ring; the 2%-fgPrimary tint reuses the
46
+ same color-mix idiom every other soft crisp surface already uses. */
47
+ &[data-readonly] { background: color-mix(in srgb, ${c.fgPrimary} 2%, transparent); box-shadow: inset 0 0 0 1px ${c.ringField}; }
48
+ & .crisp-input-suffix { flex-shrink: 0; margin-inline-start: ${c.space["1"]}; }
41
49
  `,
42
50
  variants: {
43
51
  size: {
@@ -35,7 +35,7 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
35
35
  // The id belongs on the trigger, not the wrapper: `<label for>` only names a
36
36
  // LABELABLE element, so an id left on the root div meant a FormField label
37
37
  // above this control named nothing (#247). Select already did this.
38
- id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, size = "medium", variant = "default", className, "aria-label": ariaLabel, ...rest }) {
38
+ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, size = "small", variant = "default", className, "aria-label": ariaLabel, ...rest }) {
39
39
  // crisp/#471: same resolution order as Select's own.
40
40
  const placeholder = useCrispMessage(placeholderProp, "Select…");
41
41
  const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search…");
@@ -96,9 +96,18 @@ const config = defineRecipe({
96
96
  .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgOrange} 30%, transparent); }
97
97
  `,
98
98
  variants: {
99
- // seed's two trigger sizes, same tokens as Select's. medium is the
100
- // default -- closest to crisp's previous single size.
99
+ // Owner's follow-up after #867/#879/#882 (crisp/#889): SIZE is Attio's
100
+ // again, LOOK stays seed's -- see Select's own recipe comment for the
101
+ // full reasoning (measured live in GRC's "New employee" dialog: a 40px
102
+ // trigger next to a 32px `Input` visibly mismatched). `small` is the new
103
+ // default: `c.size.menuRow` (32px) and the original, pre-#882
104
+ // Attio-measured asymmetric padding for a chip-wrapping trigger.
105
+ // `medium`/`large` (seed's own 40/52px) stay, opt-in.
101
106
  size: {
107
+ small: `
108
+ & .crisp-multiselect-trigger { min-height: ${c.size.menuRow}; border-radius: ${c.radius.md}; gap: ${c.space["1"]}; padding: ${c.space["0.5"]} ${c.space["1.5"]} ${c.space["0.5"]} ${c.space["2"]}; }
109
+ & .crisp-multiselect-chevron { width: ${c.size.icon}; height: ${c.size.icon}; }
110
+ `,
102
111
  medium: `
103
112
  & .crisp-multiselect-trigger { min-height: ${c.size.selectTriggerMd}; border-radius: ${c.radius.md}; gap: ${c.space["2"]}; padding: ${c.space["1"]} ${c.space["3.5"]}; }
104
113
  & .crisp-multiselect-chevron { width: ${c.size.icon}; height: ${c.size.icon}; }
@@ -131,7 +140,7 @@ const config = defineRecipe({
131
140
  `,
132
141
  },
133
142
  },
134
- defaultVariants: { size: "medium", invalid: false, variant: "default" },
143
+ defaultVariants: { size: "small", invalid: false, variant: "default" },
135
144
  compoundVariants: [],
136
145
  });
137
146
  export const multiselectRecipe = Object.assign(resolveRecipe(config), {