@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
@@ -33,13 +33,14 @@ const spec = defineSpec({
33
33
  muted: { color: c.fgNeutralMuted },
34
34
  // #858: no new measurement -- both reuse tokens already measured
35
35
  // against Attio for the same colours in a different arrangement.
36
- // `brand` is `textlink`'s own reused brand-text colour (`bgBrandSolid`,
37
- // the same alias GRC's own `--color-fg-brand` resolves to). `danger`
38
- // is `fgDanger` ("solid critical/destructive TEXT... an overdue
39
- // due-date, a destructive menu item's label" -- see its own doc in
40
- // tokens/semantic.ts), not `fgDangerSoft` (that one's a soft Badge
41
- // pill's text, a different measured hex).
42
- brand: { color: c.bgBrandSolid },
36
+ // `brand` is `textlink`'s own reused brand-text colour, now
37
+ // `fgBrandContrast` (your-moon/crisp#897: the plain accent,
38
+ // `bgBrandSolid`, only clears 3.23:1 as text on a white page).
39
+ // `danger` is `fgDanger` ("solid critical/destructive TEXT... an
40
+ // overdue due-date, a destructive menu item's label" -- see its own
41
+ // doc in tokens/semantic.ts), not `fgDangerSoft` (that one's a soft
42
+ // Badge pill's text, a different measured hex).
43
+ brand: { color: c.fgBrandContrast },
43
44
  danger: { color: c.fgDanger },
44
45
  },
45
46
  },
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
+ import { Progress } from "../progress/index.js";
3
4
  import { textareaRecipe } from "./textarea.recipe.js";
4
5
  /**
5
6
  * A multiline text field — measured from settings. Forwards every native
6
7
  * `<textarea>` prop; pass `showCount` with `maxLength` to render a live
7
8
  * character counter (which turns red if the value exceeds the limit).
8
9
  */
9
- export const Textarea = React.forwardRef(({ showCount, maxLength, className, wrapperClassName, value, defaultValue, onChange,
10
+ export const Textarea = React.forwardRef(({ showCount, showProgress, maxLength, className, wrapperClassName, value, defaultValue, onChange,
10
11
  // `style` is pulled out and put on the ROOT, the way Input does it.
11
12
  // Everything else belongs on the <textarea> -- id, name, form
12
13
  // association, aria -- but `style` sizes the visual box, and the box is
@@ -22,6 +23,6 @@ style, ...props }, ref) => {
22
23
  if (value === undefined)
23
24
  setCount(e.target.value.length);
24
25
  onChange?.(e);
25
- }, ...props }) }), counted && (_jsxs("span", { className: "crisp-textarea-count", "data-over": len > (maxLength ?? 0) ? "" : undefined, "aria-hidden": "true", children: [len, " / ", maxLength] }))] }));
26
+ }, ...props }) }), counted && (_jsxs("span", { className: "crisp-textarea-count", "data-over": len > (maxLength ?? 0) ? "" : undefined, "aria-hidden": "true", children: [showProgress && (_jsx(Progress, { variant: "circular", value: len, max: maxLength ?? 0, className: "crisp-textarea-progress" })), len, " / ", maxLength] }))] }));
26
27
  });
27
28
  Textarea.displayName = "Textarea";
@@ -46,8 +46,9 @@ const config = defineRecipe({
46
46
  &[data-invalid] .crisp-textarea-wrap::after { border-color: ${c.bgDanger}; }
47
47
  &[data-invalid] .crisp-textarea-wrap:focus-within::after { border-color: ${c.bgDanger}; }
48
48
  &[data-counted] .crisp-textarea-field { padding-bottom: ${c.space["5"]}; }
49
- & .crisp-textarea-count { position: absolute; inset-inline-end: 14px; bottom: ${c.space["2.5"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; pointer-events: none; }
49
+ & .crisp-textarea-count { position: absolute; inset-inline-end: 14px; bottom: ${c.space["2.5"]}; display: flex; align-items: center; gap: ${c.space["1"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; pointer-events: none; }
50
50
  & .crisp-textarea-count[data-over] { color: ${c.bgDanger}; }
51
+ & .crisp-textarea-progress { flex-shrink: 0; }
51
52
  `,
52
53
  variants: {},
53
54
  defaultVariants: {},
@@ -7,7 +7,7 @@ import { textlinkRecipe } from "./textlink.recipe.js";
7
7
  * `event.preventDefault()` pairs it with client-side routing (e.g.
8
8
  * react-router's own `Link`).
9
9
  */
10
- export function TextLink({ tone = "muted", className, ...rest }) {
11
- return _jsx("a", { className: textlinkRecipe({ tone, className }), ...rest });
10
+ export function TextLink({ tone = "muted", size, className, ...rest }) {
11
+ return _jsx("a", { className: textlinkRecipe({ tone, size, className }), ...rest });
12
12
  }
13
13
  TextLink.displayName = "TextLink";
@@ -18,11 +18,32 @@ const config = defineRecipe({
18
18
  variants: {
19
19
  tone: {
20
20
  // The bold, always-brand-coloured trailing link a Card's own `action`
21
- // slot uses ("View all →", "All registers →").
22
- brand: `font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.bgBrandSolid};`,
21
+ // slot uses ("View all →", "All registers →"). your-moon/crisp#897:
22
+ // `fgBrandContrast`, not `bgBrandSolid` (#0298d9) directly -- as TEXT
23
+ // on a white page the accent itself only clears 3.23:1; the darker
24
+ // contrast-safe sibling clears 4.5:1+.
25
+ brand: `font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgBrandContrast};`,
23
26
  // A quieter in-context reference (a sample record under a finding)
24
27
  // that only goes brand on hover.
25
- muted: `font-size: ${c.text.md}; font-weight: ${c.weight.regular}; color: ${c.fgMuted}; &:hover { color: ${c.bgBrandSolid}; }`,
28
+ muted: `font-size: ${c.text.md}; font-weight: ${c.weight.regular}; color: ${c.fgMuted}; &:hover { color: ${c.fgBrandContrast}; }`,
29
+ },
30
+ // crisp#874: an explicit, OPTIONAL size override -- no `defaultVariants`
31
+ // entry below, so omitting `size` emits no `--size_*` class at all and
32
+ // each tone's own hard-coded font-size (above) stands completely
33
+ // unchanged. Only when a caller passes `size` explicitly does this
34
+ // override it: `size`'s CSS block is emitted AFTER `tone`'s in the
35
+ // generated stylesheet (`generate-recipes.mjs` walks `variants` in
36
+ // object-key order), so at equal specificity it wins the cascade.
37
+ // Measured live (a running GRC server, `getComputedStyle`):
38
+ // internal/grc's two primary-column record links
39
+ // (routes/lists/Lists.tsx:51, routes/registers/Registers.tsx:63) are
40
+ // 14px/500 ambient, inherited from DataTable's own
41
+ // `.crisp-datatable-entity-name` -- `size="lg"` matches that context
42
+ // without changing `brand`'s weight/colour.
43
+ size: {
44
+ sm: `font-size: ${c.text.sm};`,
45
+ md: `font-size: ${c.text.md};`,
46
+ lg: `font-size: ${c.text.lg};`,
26
47
  },
27
48
  },
28
49
  defaultVariants: { tone: "muted" },
package/dist/esm/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // component, the design tokens, and the recipe core. Consumers also import the
3
3
  // stylesheet once: import "@crisp-ui-kit/crisp/styles.css";
4
4
  // primitives (components/)
5
+ export * from "./components/accentpicker/index.js";
5
6
  export * from "./components/accordion/index.js";
6
7
  export * from "./components/avatar/index.js";
7
8
  export * from "./components/avatargroup/index.js";
@@ -1,14 +1,16 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Activity as ActivityIcon, CalendarClock, ChevronDown, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, Mail, Maximize2, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search, Share2, Sparkles, SquareCheck, Star, Trash2, Users, UsersRound, X, Zap, } from "lucide-react";
2
+ import { Activity as ActivityIcon, CalendarClock, ChevronDown, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, Mail, Maximize2, Merge, MessageSquare, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search, Settings, Sparkles, SquareCheck, Star, Trash2, Users, UsersRound, Workflow, X, Zap, } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { Avatar } from "../../components/avatar/index.js";
5
5
  import { Checkbox } from "../../components/checkbox/index.js";
6
+ import { Drawer } from "../../components/drawer/index.js";
6
7
  import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
7
8
  import { Select } from "../../components/select/index.js";
8
9
  import { Spinner } from "../../components/spinner/index.js";
9
10
  import { Switch } from "../../components/switch/index.js";
10
11
  import { Tabs } from "../../components/tabs/index.js";
11
12
  import { cx, isRtlElement, useCrispComponentDefaults } from "../../core/index.js";
13
+ import { CommentsPanel, } from "../comments/index.js";
12
14
  import { tagTone } from "./column.js";
13
15
  import { editableInputType, useEditableField } from "./editable-field.js";
14
16
  import { recordpageRecipe } from "./recordpage.recipe.js";
@@ -33,19 +35,32 @@ export const recordPageLabels = {
33
35
  breadcrumb: "Breadcrumb",
34
36
  editRecord: "Edit record",
35
37
  recordActions: "Record actions",
36
- addToFavourites: "Add to favourites",
37
- removeFromFavourites: "Remove from favourites",
38
+ // US spelling, matching Attio's own live text exactly (#621) — this string
39
+ // is shared with the entity header's favourite-star aria-label, so both
40
+ // read "favorites" now, not just the menu.
41
+ addToFavourites: "Add to favorites",
42
+ removeFromFavourites: "Remove from favorites",
38
43
  copyRecordLink: "Copy record link",
44
+ configurePage: "Configure page",
45
+ copyPageUrl: "Copy page URL",
46
+ copyRecordId: "Copy record ID",
47
+ mergeRecords: "Merge records",
39
48
  deleteRecord: "Delete record",
40
49
  linkRecords: "Link records",
41
- logANote: "Log a note",
42
- createATask: "Create a task",
50
+ // Tooltip copy re-measured live for #865 (was "Log a note"/"Create a
51
+ // task" — the field names stay, only the visible text changes to match
52
+ // Attio's own React-tooltip strings on these two icon buttons exactly).
53
+ logANote: "New note",
54
+ createATask: "New task",
55
+ runWorkflow: "Run workflow",
43
56
  fullScreen: "Full screen",
44
57
  exitFullScreen: "Exit full screen",
45
58
  resizeDetails: "Resize details column",
46
59
  composeEmail: "Compose email",
47
60
  askAttio: "Ask AI",
48
61
  addToList: "Add to list",
62
+ share: "Share",
63
+ comments: "Comments",
49
64
  noLists: "This record has not been added to any lists.",
50
65
  highlights: "Highlights",
51
66
  recordDetails: "Record Details",
@@ -419,7 +434,7 @@ RecordDetails.displayName = "RecordDetails";
419
434
  * Emails sections (Notes & Tasks are live — add and check things off). Drop it
420
435
  * into the app's main content area.
421
436
  */
422
- export 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 }) {
437
+ export 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 }) {
423
438
  const t = useRecordPageLabels(labels);
424
439
  const [favorite, setFavorite] = React.useState(false);
425
440
  // Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
@@ -431,6 +446,11 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
431
446
  const [internalTasks, setInternalTasks] = React.useState([]);
432
447
  const notes = notesProp ?? internalNotes;
433
448
  const tasks = tasksProp ?? internalTasks;
449
+ // Comments follow the same controlled/self-contained duality (#864).
450
+ const [internalComments, setInternalComments] = React.useState([]);
451
+ const comments = commentsProp ?? internalComments;
452
+ const [commentsOpen, setCommentsOpen] = React.useState(false);
453
+ const me = currentUser ?? { id: "me", name: "You" };
434
454
  const [showAllActivity, setShowAllActivity] = React.useState(false);
435
455
  // Drag-resizable left details column.
436
456
  const [detailsW, setDetailsW] = React.useState(DETAILS_DEFAULT_W);
@@ -528,6 +548,15 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
528
548
  },
529
549
  ];
530
550
  const highlights = highlightsProp === undefined ? defaultHighlights : highlightsProp;
551
+ // #865: the built-in CRM icon set for the quick-action row, used when the
552
+ // caller omits `quickActions` -- same "caller's own list, not a merge"
553
+ // choice `tabs` (#379) and `highlights` (#528) already make.
554
+ const defaultQuickActions = [
555
+ { label: t.logANote, icon: FileText },
556
+ { label: t.runWorkflow, icon: Workflow },
557
+ { label: t.createATask, icon: SquareCheck },
558
+ ];
559
+ const quickActions = quickActionsProp ?? defaultQuickActions;
531
560
  const ti = { size: 15, strokeWidth: 1.75 };
532
561
  const builtinTabs = [
533
562
  { value: "overview", label: t.tabs.overview, icon: _jsx(LayoutGrid, { ...ti }) },
@@ -580,6 +609,31 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
580
609
  }
581
610
  setInternalTasks((n) => n.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
582
611
  };
612
+ const postComment = (text, threadId) => {
613
+ if (commentsProp) {
614
+ onPostComment?.(text, threadId);
615
+ return;
616
+ }
617
+ if (threadId == null) {
618
+ setInternalComments((prev) => [
619
+ {
620
+ id: nextId(),
621
+ root: { id: nextId(), author: me, time: t.justNow, body: text },
622
+ },
623
+ ...prev,
624
+ ]);
625
+ return;
626
+ }
627
+ setInternalComments((prev) => prev.map((th) => th.id === threadId
628
+ ? {
629
+ ...th,
630
+ replies: [
631
+ ...(th.replies ?? []),
632
+ { id: nextId(), author: me, time: t.justNow, body: text },
633
+ ],
634
+ }
635
+ : th));
636
+ };
583
637
  // A small, plausible activity feed derived from the record's own fields.
584
638
  const domain = byKey.get("domain");
585
639
  const cats = byKey.get("categories");
@@ -636,7 +690,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
636
690
  const effectivePanel = tabsProp
637
691
  ? (active) => tabsProp.find((tb) => tb.id === active)?.render()
638
692
  : builtinPanel;
639
- return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _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: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: _jsx(Copy, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: _jsx(FileText, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: _jsx(Share2, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: _jsx(SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _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) => {
693
+ return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", onClick: onShare, children: t.share }), _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: () => setCommentsOpen(true), "aria-label": t.comments, children: _jsx(MessageSquare, { size: 15, strokeWidth: 1.75 }) }), _jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Merge, { size: 15, strokeWidth: 1.75 }), children: t.mergeRecords }), _jsx(MenuItem, { icon: _jsx(Settings, { size: 15, strokeWidth: 1.75 }), children: t.configurePage }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyPageUrl }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyRecordId }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _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: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Plus, { size: 16, strokeWidth: 1.75 }), t.addToList] }), quickActions.map((a, i) => (_jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": a.label, onClick: a.onSelect, children: _jsx(a.icon, { size: 15, strokeWidth: 1.75 }) }, i)))] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _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) => {
640
694
  if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
641
695
  return;
642
696
  // The pointer drag learned its direction in #212; this did not.
@@ -648,6 +702,6 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
648
702
  setDetailsW((w) => grow
649
703
  ? Math.min(DETAILS_MAX_W, w + 16)
650
704
  : Math.max(DETAILS_MIN_W, w - 16));
651
- } }), _jsx("main", { className: "crisp-recordpage-main", children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] })] }));
705
+ } }), _jsx("main", { className: "crisp-recordpage-main", children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] }), _jsx(Drawer, { open: commentsOpen, onOpenChange: setCommentsOpen, "aria-label": t.comments, style: { width: 320 }, children: _jsx(CommentsPanel, { threads: comments, currentUserId: me.id, onPost: postComment, onClose: () => setCommentsOpen(false), role: undefined }) })] }));
652
706
  }
653
707
  RecordPage.displayName = "RecordPage";
@@ -56,4 +56,11 @@ export const elevation = {
56
56
  * nobody measured for them.
57
57
  */
58
58
  fieldNoRing: `0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04)`,
59
+ /**
60
+ * Select's own listbox shadow (crisp/#867) — seed-design's `$shadow.s3`, a
61
+ * single layer, NOT `floating` above. This component follows seed, not
62
+ * Attio, for the same reason `DatePicker` follows seed's calendar
63
+ * structure: an explicit owner request to match seed-design exactly.
64
+ */
65
+ selectFloating: `0 4px 16px ${c.shadowSelect}`,
59
66
  };
@@ -5,9 +5,14 @@ export const palette = {
5
5
  // on a bright tone fill. Same value as gray900's light side (the measured
6
6
  // near-black), named separately because its job is not "primary foreground".
7
7
  ink: { light: "#101112", dark: "#101112" },
8
- gray50: { light: "#f4f4f5", dark: "#101112" },
8
+ // seed-identity migration (your-moon/crisp#890): seed's bg.layer-default-
9
+ // pressed / gray-100. Light side only — dark is untouched, unmeasured
10
+ // Attio territory this migration doesn't scope into.
11
+ gray50: { light: "#f7f8f9", dark: "#101112" },
9
12
  gray900: { light: "#101112", dark: "#f5f5f5" },
10
- line200: { light: "#e4e4e7", dark: "#212224" },
13
+ // seed-identity migration (your-moon/crisp#890): seed's stroke.neutral-
14
+ // weak / gray-400. Light side only, same scoping as `gray50` above.
15
+ line200: { light: "#dcdee3", dark: "#212224" },
11
16
  line100: { light: "#eeeff1", dark: "#1c1d1f" },
12
17
  // ImportDialog's dropzone border — measured live from Attio's Import CSV
13
18
  // flow (`rgb(230,231,234)`), distinct from `line200`/`line100` (#e4e4e7 /
@@ -19,8 +24,11 @@ export const palette = {
19
24
  line150: { light: "#e6e7ea", dark: "#212224" },
20
25
  // The filter chip's field/value segments while the condition is
21
26
  // incomplete (no value typed yet) — measured live on Attio's own filter
22
- // chip (`rgb(246,247,247)`), lighter than `gray50` (#f4f4f5, close but not
23
- // equal). The chip's operator/kebab segments stay `gray00` in the same
27
+ // chip (`rgb(246,247,247)`), close to but not equal to `gray50`
28
+ // (`#f7f8f9` since the seed-identity migration, your-moon/crisp#890 — the
29
+ // two used to differ the other direction, gray25 lighter; the relative
30
+ // order isn't the point, staying off `gray50` while remaining a
31
+ // near-neighbour is). The chip's operator/kebab segments stay `gray00` in the same
24
32
  // state; only the two text-content segments carry this muted fill. Dark
25
33
  // value not independently re-measured (Attio's dark theme is off-limits to
26
34
  // toggle) — reuses `gray50`'s dark value, the nearest existing "muted
@@ -44,6 +52,15 @@ export const palette = {
44
52
  // both themes (Switch's own spec), so this reuses ringField's dark value
45
53
  // rather than inventing a third "field-adjacent ring, dark" colour.
46
54
  ringSwitch: { light: "#d9dade", dark: "#212224" },
55
+ // Callout's bordered/banner look (crisp#869) — measured live on Attio's
56
+ // Objects settings page (the "Create custom objects... Upgrade" Pro-plan
57
+ // banner): `border: 1px solid rgb(214,229,255)`, an opaque colour, not a
58
+ // composited tint of `bgBrandSolid` (checked: no single color-mix
59
+ // percentage matches all three channels at once). Dark value not
60
+ // independently re-measured (Attio's own theme toggle is off-limits) —
61
+ // reuses `ringField`/`ringSwitch`'s existing dark ring (#212224), the same
62
+ // "edge visible against a dark surface" role those already establish.
63
+ ringBrandSoft: { light: "#d6e5ff", dark: "#212224" },
47
64
  tileActive: { light: "#ffffff", dark: "#101112" },
48
65
  tileActiveShadow: { light: "rgba(0,0,0,0.04)", dark: "rgba(0,0,0,0)" },
49
66
  // Dialog's page-dimming backdrop (crisp#677) — measured live behind the
@@ -72,25 +89,54 @@ export const palette = {
72
89
  shadowPopover2: { light: "rgba(28,40,64,0.12)", dark: "rgba(0,0,0,0.24)" },
73
90
  shadowPopover3: { light: "rgba(24,41,75,0.16)", dark: "rgba(0,0,0,0.32)" },
74
91
  gray400: { light: "#71717a", dark: "#9a9aa2" },
75
- brand600: { light: "#266df0", dark: "#266df0" },
76
- brand700: { light: "#1f5fd6", dark: "#1f5fd6" },
77
- red600: { light: "#e5484d", dark: "#ff5a5f" },
78
- // A red dark enough to carry white text. red600 cannot: white on it is
79
- // 3.91:1 light / 3.05:1 dark, and the solid button's hover DARKENS the fill
80
- // (filter: brightness), so dark ink fails there instead (4.49 at hover).
81
- // Measured from the reference stylesheet, which ships this red alongside the
82
- // #EB3B3B it uses for destructive menu TEXT. Does not flip: a filled
83
- // destructive control needs the same white-bearing fill in both themes.
84
- red700: { light: "#ba2525", dark: "#ba2525" },
85
- // The #EB3B3B named in red700's own comment above (destructive menu text) —
86
- // it had never actually been given a token. Independently re-measured live
87
- // on `app.attio.com/.../tasks`, `rgb(235,59,59)`, as the overdue due-date
88
- // text colour on the Tasks page (crisp#462) — same hex, second surface.
89
- // Dark not independently re-measured (Attio's theme toggle is off-limits,
90
- // same restriction noted throughout this file); reuses red600's dark value,
91
- // the same "brightens for legibility on a dark surface" tone this red
92
- // already sits next to, rather than inventing a third red.
93
- red500: { light: "#eb3b3b", dark: "#ff5a5f" },
92
+ // seed-identity migration (your-moon/crisp#890, "Attio structure, seed
93
+ // look"): the owner's TechPartners primary (#0298D9, sourced from the
94
+ // official logo SVG + the live site's own compiled CSS — see
95
+ // ~/grc-parity/seed-identity-spec.md §1) replaces the old measured-from-
96
+ // Attio brand blue, built as seed's own carrot-scale RELATIVE lightness
97
+ // shape around that exact hex at step 600 ("Method A" in the spec — the
98
+ // verified logo hex stays literally in the scale, at the step that's
99
+ // actually rendered, rather than a mathematically tidier neighbour).
100
+ // Does not flip with theme, matching this token's own prior precedent.
101
+ brand600: { light: "#0298d9", dark: "#0298d9" },
102
+ // Method A's raw carrot-shape output (L 37.1%) — the hover/pressed-darken
103
+ // step of the ACCENT colour above, same role brand700 has always had.
104
+ // Not tuned for any particular contrast target; see `brandContrast` below
105
+ // for the token that carries that job.
106
+ brand700: { light: "#0283bb", dark: "#0283bb" },
107
+ // your-moon/crisp#897: #0298d9 (the verified brand hex, kept exactly as
108
+ // the ACCENT everywhere else — focus rings, selected states, icons,
109
+ // tints) only clears 3.23:1 against white, short of the 4.5:1 a SOLID
110
+ // fill carrying white text needs (Button primary, Badge solid+brand,
111
+ // Avatar's brand tone), or brand-coloured TEXT sitting directly on a white
112
+ // page (TextLink/Text's brand tone). This is that darker, AA-safe step —
113
+ // same hue (198°), L dropped to 34% — clearing 4.88:1 with white. An
114
+ // earlier round of this fix used dark ink text instead of finding this
115
+ // value; that read off-brand (seed's own solid buttons use white) and got
116
+ // reverted in favour of this.
117
+ brandContrast: { light: "#0278ac", dark: "#0278ac" },
118
+ // seed's red-700 (fg.critical) — the accent used for plain danger TEXT
119
+ // (an overdue due-date, a destructive menu label) and for tinting
120
+ // (Chip/Badge's soft-danger `color-mix()` inputs). NOT used for a solid
121
+ // fill paired with white text — see `red700` below for why that split
122
+ // matters here specifically.
123
+ red600: { light: "#fa342c", dark: "#ff5a5f" },
124
+ // seed's red-800 (bg.critical-solid-**pressed**), not red-700
125
+ // (bg.critical-solid-enabled, `red600` above) — deliberately the darker
126
+ // of the two. `red700` is crisp's SOLID destructive fill (Button danger,
127
+ // Badge solid+danger), which pairs with white text and, on Button,
128
+ // DARKENS further on hover/active via a brightness filter (not a second
129
+ // token, per this token's own original comment below this migration note
130
+ // — that invariant is why a dark-enough red matters at all). Seed's own
131
+ // red-700 only clears 3.76:1 with white (measured) — short of AA even
132
+ // before any hover darkening — so reusing crisp's existing "needs to
133
+ // survive getting darker" role means red-800 (5.68:1 at rest, improving
134
+ // through hover/active) is the correct step, not seed's literal
135
+ // "enabled" one. `red500` below is pure text again (no fill/contrast
136
+ // constraint), so it stays on the
137
+ // lighter red-700, matching seed exactly.
138
+ red700: { light: "#ca1d13", dark: "#ba2525" },
139
+ red500: { light: "#fa342c", dark: "#ff5a5f" },
94
140
  // seed-design's field focus ring (stroke-neutral-contrast = gray-1000),
95
141
  // measured from @seed-design/css. A high-contrast neutral rather than a brand
96
142
  // hue: the ring reads as "this field has focus", not as a state colour.
@@ -110,8 +156,25 @@ export const palette = {
110
156
  neutralSubtle: { light: "#868b94", dark: "#b0b3ba" },
111
157
  neutralMuted: { light: "#555d6d", dark: "#dcdee3" },
112
158
  focusRing: { light: "#1e82eb", dark: "#5e98fe" },
113
- green600: { light: "#1f9d57", dark: "#2ea968" },
114
- amber600: { light: "#b5730f", dark: "#e0982a" },
159
+ // seed's fg.positive / bg.positive-solid (green-700).
160
+ green600: { light: "#079171", dark: "#2ea968" },
161
+ // seed's fg.warning / stroke.warning-solid (yellow-700) — the ACCENT used
162
+ // for tinting (Badge/Chip's `soft()`/`ring()` colour-mix formulas), not
163
+ // seed's actual solid-fill colour, which is a much paler yellow at a
164
+ // different scale step — see `amberSolid` below for why the two need to
165
+ // stay separate tokens.
166
+ amber600: { light: "#9b7821", dark: "#e0982a" },
167
+ // seed's bg.warning-solid (yellow-300) — genuinely NOT amber600 tinted
168
+ // lighter: seed's warning-solid is its own pale scale step, distinct from
169
+ // the darker accent every other tone's solid fill derives from (see the
170
+ // spec's Badge section). Badge's `variant="solid" tone="warning"` uses
171
+ // this directly, unmixed, the same way `red700`'s solid destructive fill
172
+ // is used directly rather than computed from a tint. Dark side is NOT
173
+ // independently sourced (this is a new token; no prior Attio measurement
174
+ // exists to preserve) — reuses amber600's own dark value as the nearest
175
+ // existing "warning, dark theme" tone, same convention this file uses
176
+ // throughout for an unmeasured dark side (ringSwitch, line150, gray25).
177
+ amberSolid: { light: "#fbdc65", dark: "#e0982a" },
115
178
  orange600: { light: "#f97514", dark: "#f97514" },
116
179
  cyan600: { light: "#00b9eb", dark: "#00b9eb" },
117
180
  purple600: { light: "#9b69ff", dark: "#9b69ff" },
@@ -159,4 +222,36 @@ export const palette = {
159
222
  purpleTint: { light: "#f5f0ff", dark: "#2f1e5a" },
160
223
  softPurpleText: { light: "#6238b5", dark: "#d8c4ff" },
161
224
  purpleRing: { light: "#e8ddfe", dark: "#45297d" },
225
+ // Select rebuilt to seed-design parity (crisp/#867), NOT Attio -- a
226
+ // deliberate one-component exception, the same way DatePicker already
227
+ // follows seed's calendar structure. Values below are resolved directly
228
+ // from `@seed-design/rootage`'s color.yaml (gray palette), not measured
229
+ // live, since there is no Attio reference for this component anymore.
230
+ // gray-00 / gray-200 (bg.layer-floating) -- the listbox surface. Distinct
231
+ // from `popoverBg` (crisp's Attio-measured popover, #fff/#17181a): the dark
232
+ // value differs (#1d2025), so this is its own token, not a reuse.
233
+ selectListboxBg: { light: "#ffffff", dark: "#1d2025" },
234
+ // gray-400 (stroke.neutral-weak) -- the trigger's real 1px border at rest.
235
+ selectTriggerStroke: { light: "#dcdee3", dark: "#393d46" },
236
+ // static-black/white-alpha (bg.transparent-pressed) -- the trigger/item
237
+ // press tint. Deliberately alpha, not a gray, so it reads the same over
238
+ // either surface colour.
239
+ selectPressedTint: { light: "#00000007", dark: "#ffffff0d" },
240
+ // gray-200 / gray-300 (bg.disabled) -- the trigger's disabled AND readonly
241
+ // fill. Distinct from `neutralWeak` (light matches, dark is #1d2025 not
242
+ // #2b2e35), so its own token rather than a reuse.
243
+ selectDisabledBg: { light: "#f3f4f5", dark: "#2b2e35" },
244
+ // red-700 (stroke.critical-solid) -- the trigger's invalid border. Distinct
245
+ // from crisp's own `bgDangerSolid`/`red700` (#ba2525, an Attio red): seed's
246
+ // red is a different hue entirely, and this component follows seed.
247
+ selectInvalidStroke: { light: "#fa342c", dark: "#ff6e60" },
248
+ // static-black/white-alpha (stroke.neutral-muted) -- the listbox's own
249
+ // divider slot (defined in select.yaml; not yet exposed on any consumer).
250
+ selectDivider: { light: "#00000010", dark: "#ffffff17" },
251
+ // gray-600 (fg.placeholder) -- distinct from `neutralSubtle` (its light/dark
252
+ // pair is the same two hexes, swapped), so its own token, not a reuse.
253
+ selectPlaceholder: { light: "#b0b3ba", dark: "#868b94" },
254
+ // s3 (shadow.yaml) -- the listbox's single-layer floating shadow, in place
255
+ // of crisp's own multi-layer `elevation.floating` for this component only.
256
+ selectShadow: { light: "#0000001f", dark: "#000000cc" },
162
257
  };
@@ -17,6 +17,7 @@ export const space = {
17
17
  "2": "8px",
18
18
  "2.5": "10px",
19
19
  "3": "12px",
20
+ "3.5": "14px", // seed-design's $dimension.x3_5 — Select's medium trigger paddingX (crisp/#867)
20
21
  "4": "16px",
21
22
  "5": "20px",
22
23
  "6": "24px",
@@ -25,12 +26,14 @@ export const space = {
25
26
  // Corner radius. sm/md/lg are the three used on stacked surfaces
26
27
  // (inputs, rows, tiles); full is the pill.
27
28
  export const radius = {
29
+ xs: "4px", // seed-design's $radius.r1 — small badge / checkbox box corner
28
30
  chip: "7px", // tag / chip corner — the single most used radius
29
31
  sm: "6px", // chips, badges
30
32
  md: "8px", // inputs, search box
31
33
  lg: "9px", // rows, tiles, list items
32
34
  xl: "12px", // overlay items (command rows)
33
35
  "2xl": "16px", // overlay panels (command surface)
36
+ "3xl": "20px", // seed-design's $radius.r5 — Select's listbox corner (crisp/#867)
34
37
  full: "999px", // pills
35
38
  };
36
39
  // Component sizing — recurring measured dimensions that are neither spacing nor
@@ -49,6 +52,12 @@ export const size = {
49
52
  // whole cells and a column gap would cut it.
50
53
  calendarCell: "34px", // calendar day pitch (32px day + 2px gap)
51
54
  calendarPill: "32px", // the day highlight itself
55
+ // seed-design's Select, resolved from select-trigger.yaml/select-item.yaml
56
+ // (crisp/#867) — this component follows seed, not Attio, so these sit
57
+ // alongside the measured scale rather than folding into it.
58
+ selectTriggerMd: "40px", // $dimension.x10 — the medium (default) trigger height
59
+ selectTriggerLg: "52px", // $dimension.x13 — the large trigger height
60
+ selectIconLg: "20px", // $dimension.x5 — large trigger's chevron
52
61
  };
53
62
  // Typography — the measured UI runs entirely on Inter. Sizes measured in the sidebar
54
63
  // collapse onto 11/12/13/14/16 (lg = 14px is the default body/label size);
@@ -1,6 +1,17 @@
1
1
  export const semantic = {
2
2
  bgBrandSolid: "brand600",
3
3
  bgBrandSolidPressed: "brand700",
4
+ /** your-moon/crisp#897: the AA-safe darker sibling of `bgBrandSolid` — a
5
+ * SOLID fill carrying white text (Button primary, Badge solid+brand,
6
+ * Avatar's brand tone). `bgBrandSolid` itself stays the exact brand
7
+ * accent (focus rings, selected states, icons, tints), unpaired with
8
+ * white text anywhere that matters for contrast. */
9
+ bgBrandContrast: "brandContrast",
10
+ /** your-moon/crisp#897: brand-coloured TEXT sitting on a white page
11
+ * (TextLink/Text's `brand` tone) — same value as `bgBrandContrast`,
12
+ * named separately because one's a background and one's a foreground,
13
+ * matching every other fg/bg pair in this file. */
14
+ fgBrandContrast: "brandContrast",
4
15
  fgOnBrand: "white",
5
16
  // text on a solid success/warning/danger fill. Those tone tokens BRIGHTEN in
6
17
  // dark (they are tuned for tinted backgrounds and coloured text), so white on
@@ -38,6 +49,15 @@ export const semantic = {
38
49
  shadowPopover2: "shadowPopover2",
39
50
  shadowPopover3: "shadowPopover3",
40
51
  fgPrimary: "gray900",
52
+ // seed-identity migration (your-moon/crisp#890): NOT touched here, on
53
+ // purpose. seed's closest analog (fg.neutral-subtle, #868b94) is lighter
54
+ // than crisp's current #71717a, and Badge's neutral-SOLID variant reuses
55
+ // this exact token as a BACKGROUND paired with white text
56
+ // (`badge.recipe.ts`'s compound variant) — #868b94 drops that pairing to
57
+ // 3.42:1, below AA. Retargeting `fgMuted` and fixing that one background
58
+ // choice belong together in Badge's own upcoming look PR, not split
59
+ // across two PRs where the token change alone would ship a known
60
+ // contrast regression.
41
61
  fgMuted: "gray400",
42
62
  bgDanger: "red600",
43
63
  /** Fill for a SOLID destructive control that carries white text. */
@@ -63,6 +83,10 @@ export const semantic = {
63
83
  strokeFocusRing: "focusRing",
64
84
  bgSuccess: "green600",
65
85
  bgWarning: "amber600",
86
+ /** A SOLID warning fill (Badge's `variant="solid" tone="warning"`) — seed's
87
+ * own warning-solid is a distinct, paler yellow, not `bgWarning` tinted
88
+ * lighter. See `amberSolid` in palette.ts. */
89
+ bgWarningSolid: "amberSolid",
66
90
  bgOrange: "orange600",
67
91
  bgCyan: "cyan600",
68
92
  bgPurple: "purple600",
@@ -82,4 +106,22 @@ export const semantic = {
82
106
  bgPurpleSoft: "purpleTint",
83
107
  fgPurpleSoft: "softPurpleText",
84
108
  ringPurpleSoft: "purpleRing",
109
+ /** Callout's bordered/banner look (crisp#869) — an opaque measured ring,
110
+ * not a `color-mix` tint of `bgBrandSolid` (see palette.ts's own comment). */
111
+ ringBrandSoft: "ringBrandSoft",
112
+ /** Select rebuilt to seed-design parity (crisp/#867) — see palette.ts's
113
+ * comment above `selectListboxBg` for why these are their own tokens
114
+ * rather than a reuse of an Attio-measured one. */
115
+ /** Seed's neutral ink (gray-1000) used as TEXT/icon colour — same palette
116
+ * entry as `strokeFieldFocus` (a ring), aliased separately because the
117
+ * role here is content colour, not a focus ring. */
118
+ fgSelectNeutral: "fieldFocus",
119
+ bgSelectListbox: "selectListboxBg",
120
+ strokeSelectTrigger: "selectTriggerStroke",
121
+ bgSelectPressed: "selectPressedTint",
122
+ bgSelectDisabled: "selectDisabledBg",
123
+ strokeSelectInvalid: "selectInvalidStroke",
124
+ strokeSelectDivider: "selectDivider",
125
+ fgSelectPlaceholder: "selectPlaceholder",
126
+ shadowSelect: "selectShadow",
85
127
  };
package/dist/input.css CHANGED
@@ -6,6 +6,17 @@
6
6
  & .crisp-input-value::placeholder { color: var(--crisp-fg-muted); }
7
7
  & .crisp-input-value:disabled { opacity: 0.5; cursor: not-allowed; }
8
8
  &:has(.crisp-input-value:disabled) { opacity: 1; }
9
+ /* crisp#634: measured live on Attio's own Settings > Profile read-only
10
+ primary-email field -- a tinted field, not just muted text, driven off
11
+ the native `readOnly` attribute. `ringField` (rgba(0,0,0,.06)) is an
12
+ exact match for the measured ring; the 2%-fgPrimary tint reuses the
13
+ same color-mix idiom every other soft crisp surface already uses. */
14
+ &[data-readonly] { background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); box-shadow: inset 0 0 0 1px var(--crisp-ring-field); }
15
+ & .crisp-input-suffix { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); }
16
+ }
17
+ .crisp-input--size_sm {
18
+ height: var(--crisp-size-row); border-radius: var(--crisp-radius-md);
19
+ & .crisp-input-value { padding-inline: var(--crisp-space-2-5); }
9
20
  }
10
21
  .crisp-input--size_medium {
11
22
  height: 32px; border-radius: var(--crisp-radius-md);
package/dist/input.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { RecipeFn } from "../core/types";
2
2
 
3
3
  export interface InputVariants {
4
- size?: "medium" | "large";
4
+ size?: "sm" | "medium" | "large";
5
5
  invalid?: boolean;
6
6
  }
7
7
 
8
- export declare const inputRecipe: RecipeFn<{ size: { medium: string; large: string }; invalid: { true: string; false: string } }>;
8
+ export declare const inputRecipe: RecipeFn<{ size: { sm: string; medium: string; large: string }; invalid: { true: string; false: string } }>;