@crisp-ui-kit/crisp 0.53.5 → 0.54.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 (124) hide show
  1. package/dist/activityfeed.css +12 -0
  2. package/dist/activityfeed.d.ts +7 -0
  3. package/dist/activityfeed.layered.css +14 -0
  4. package/dist/badge.css +3 -0
  5. package/dist/badge.d.ts +2 -2
  6. package/dist/badge.layered.css +3 -0
  7. package/dist/cjs/components/badge/badge.recipe.js +10 -0
  8. package/dist/cjs/components/bulkbar/bulkbar.js +5 -1
  9. package/dist/cjs/components/fileupload/fileupload.js +19 -3
  10. package/dist/cjs/components/formfield/formfield.js +8 -1
  11. package/dist/cjs/components/heatmapchart/heatmapchart.js +14 -2
  12. package/dist/cjs/components/skiplink/index.js +17 -0
  13. package/dist/cjs/components/skiplink/skiplink.js +16 -0
  14. package/dist/cjs/components/skiplink/skiplink.recipe.js +33 -0
  15. package/dist/cjs/components/textlink/index.js +7 -0
  16. package/dist/cjs/components/textlink/textlink.js +16 -0
  17. package/dist/cjs/components/textlink/textlink.recipe.js +35 -0
  18. package/dist/cjs/components/visuallyhidden/index.js +17 -0
  19. package/dist/cjs/components/visuallyhidden/visuallyhidden.js +16 -0
  20. package/dist/cjs/components/visuallyhidden/visuallyhidden.recipe.js +20 -0
  21. package/dist/cjs/index.js +5 -0
  22. package/dist/cjs/product/activityfeed/activityfeed.js +38 -0
  23. package/dist/cjs/product/activityfeed/activityfeed.recipe.js +35 -0
  24. package/dist/cjs/product/activityfeed/index.js +10 -0
  25. package/dist/cjs/product/board/board.js +11 -2
  26. package/dist/cjs/product/command/command.js +13 -1
  27. package/dist/cjs/product/flowcanvas/flowcanvas.js +9 -2
  28. package/dist/cjs/product/headerbar/headerbar.js +15 -0
  29. package/dist/cjs/product/headerbar/headerbar.recipe.js +35 -0
  30. package/dist/cjs/product/headerbar/index.js +17 -0
  31. package/dist/cjs/product/inbox/inbox.js +13 -1
  32. package/dist/cjs/product/onboarding/onboarding.js +22 -1
  33. package/dist/cjs/product/recordpage/index.js +2 -1
  34. package/dist/cjs/product/recordpage/recordpage.js +197 -123
  35. package/dist/cjs/product/recordpage/recordpage.recipe.js +5 -0
  36. package/dist/cjs/product/richtext/richtext.js +43 -0
  37. package/dist/cjs/product/tasklist/tasklist.js +8 -1
  38. package/dist/cjs/tokens/palette.js +14 -0
  39. package/dist/cjs/tokens/semantic.js +5 -0
  40. package/dist/esm/components/badge/badge.recipe.js +10 -0
  41. package/dist/esm/components/bulkbar/bulkbar.js +5 -1
  42. package/dist/esm/components/fileupload/fileupload.js +20 -4
  43. package/dist/esm/components/formfield/formfield.js +8 -1
  44. package/dist/esm/components/heatmapchart/heatmapchart.js +14 -2
  45. package/dist/esm/components/skiplink/index.js +1 -0
  46. package/dist/esm/components/skiplink/skiplink.js +13 -0
  47. package/dist/esm/components/skiplink/skiplink.recipe.js +30 -0
  48. package/dist/esm/components/textlink/index.js +2 -0
  49. package/dist/esm/components/textlink/textlink.js +13 -0
  50. package/dist/esm/components/textlink/textlink.recipe.js +32 -0
  51. package/dist/esm/components/visuallyhidden/index.js +1 -0
  52. package/dist/esm/components/visuallyhidden/visuallyhidden.js +13 -0
  53. package/dist/esm/components/visuallyhidden/visuallyhidden.recipe.js +17 -0
  54. package/dist/esm/index.js +5 -0
  55. package/dist/esm/product/activityfeed/activityfeed.js +32 -0
  56. package/dist/esm/product/activityfeed/activityfeed.recipe.js +32 -0
  57. package/dist/esm/product/activityfeed/index.js +2 -0
  58. package/dist/esm/product/board/board.js +12 -3
  59. package/dist/esm/product/command/command.js +14 -2
  60. package/dist/esm/product/flowcanvas/flowcanvas.js +9 -2
  61. package/dist/esm/product/headerbar/headerbar.js +12 -0
  62. package/dist/esm/product/headerbar/headerbar.recipe.js +32 -0
  63. package/dist/esm/product/headerbar/index.js +1 -0
  64. package/dist/esm/product/inbox/inbox.js +13 -1
  65. package/dist/esm/product/onboarding/onboarding.js +22 -1
  66. package/dist/esm/product/recordpage/index.js +1 -1
  67. package/dist/esm/product/recordpage/recordpage.js +197 -124
  68. package/dist/esm/product/recordpage/recordpage.recipe.js +5 -0
  69. package/dist/esm/product/richtext/richtext.js +43 -0
  70. package/dist/esm/product/tasklist/tasklist.js +8 -1
  71. package/dist/esm/tokens/palette.js +14 -0
  72. package/dist/esm/tokens/semantic.js +5 -0
  73. package/dist/headerbar.css +7 -0
  74. package/dist/headerbar.d.ts +7 -0
  75. package/dist/headerbar.layered.css +9 -0
  76. package/dist/native/tokens.d.ts +1 -1
  77. package/dist/native/tokens.js +16 -4
  78. package/dist/recordpage.css +17 -31
  79. package/dist/recordpage.layered.css +17 -31
  80. package/dist/skiplink.css +5 -0
  81. package/dist/skiplink.d.ts +7 -0
  82. package/dist/skiplink.layered.css +7 -0
  83. package/dist/styles.css +69 -31
  84. package/dist/styles.layered.css +69 -31
  85. package/dist/textlink.css +11 -0
  86. package/dist/textlink.d.ts +7 -0
  87. package/dist/textlink.layered.css +13 -0
  88. package/dist/tokens.css +6 -0
  89. package/dist/tokens.json +16 -4
  90. package/dist/types/components/badge/badge.d.ts +2 -0
  91. package/dist/types/components/badge/badge.recipe.d.ts +8 -0
  92. package/dist/types/components/bulkbar/bulkbar.d.ts +1 -1
  93. package/dist/types/components/heatmapchart/heatmapchart.d.ts +1 -1
  94. package/dist/types/components/skiplink/index.d.ts +1 -0
  95. package/dist/types/components/skiplink/skiplink.d.ts +18 -0
  96. package/dist/types/components/skiplink/skiplink.recipe.d.ts +3 -0
  97. package/dist/types/components/textlink/index.d.ts +2 -0
  98. package/dist/types/components/textlink/textlink.d.ts +19 -0
  99. package/dist/types/components/textlink/textlink.recipe.d.ts +13 -0
  100. package/dist/types/components/visuallyhidden/index.d.ts +1 -0
  101. package/dist/types/components/visuallyhidden/visuallyhidden.d.ts +15 -0
  102. package/dist/types/components/visuallyhidden/visuallyhidden.recipe.d.ts +3 -0
  103. package/dist/types/index.d.ts +5 -0
  104. package/dist/types/product/activityfeed/activityfeed.d.ts +57 -0
  105. package/dist/types/product/activityfeed/activityfeed.recipe.d.ts +3 -0
  106. package/dist/types/product/activityfeed/index.d.ts +2 -0
  107. package/dist/types/product/board/board.d.ts +1 -1
  108. package/dist/types/product/command/command.d.ts +1 -1
  109. package/dist/types/product/flowcanvas/flowcanvas.d.ts +1 -1
  110. package/dist/types/product/headerbar/headerbar.d.ts +27 -0
  111. package/dist/types/product/headerbar/headerbar.recipe.d.ts +3 -0
  112. package/dist/types/product/headerbar/index.d.ts +1 -0
  113. package/dist/types/product/inbox/inbox.d.ts +1 -1
  114. package/dist/types/product/onboarding/onboarding.d.ts +1 -1
  115. package/dist/types/product/recordpage/index.d.ts +1 -1
  116. package/dist/types/product/recordpage/recordpage.d.ts +90 -1
  117. package/dist/types/product/tasklist/tasklist.d.ts +1 -1
  118. package/dist/types/tokens/contract.d.ts +3 -0
  119. package/dist/types/tokens/palette.d.ts +12 -0
  120. package/dist/types/tokens/semantic.d.ts +5 -0
  121. package/dist/visuallyhidden.css +3 -0
  122. package/dist/visuallyhidden.d.ts +7 -0
  123. package/dist/visuallyhidden.layered.css +5 -0
  124. package/package.json +26 -1
@@ -147,6 +147,14 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
147
147
  // (documented React behaviour), so a plain onFocus/onBlur pair on this
148
148
  // same wrapper sees the portal's contents correctly.
149
149
  const mentionPickerHasFocusRef = React.useRef(false);
150
+ // A caret position `insertMention` just established, to be re-asserted
151
+ // the next time `body` regains focus (#514 fixed the picker stealing
152
+ // focus on OPEN; this is the same "caret resets to offset 0" mechanism
153
+ // on SELECT — see insertMention's own comment for why one synchronous
154
+ // `sel.addRange` isn't enough). One-shot: consumed and cleared by the
155
+ // very next focus or input, whichever comes first, so a stale pointer
156
+ // can never later hijack an unrelated focus.
157
+ const pendingCaretRef = React.useRef(null);
150
158
  const syncEmpty = (el) => {
151
159
  if (el)
152
160
  el.toggleAttribute("data-empty", (el.textContent ?? "") === "");
@@ -458,6 +466,20 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
458
466
  after.collapse(true);
459
467
  sel.removeAllRanges();
460
468
  sel.addRange(after);
469
+ // `select`ing a mention is usually the LAST thing the consumer's
470
+ // picker does before it unmounts (ActorPicker-shaped consumers call
471
+ // `close` right after `select`, same as this file's own doc example).
472
+ // A picker built on a dialog primitive (crisp's own `Command`
473
+ // included) commonly restores focus to whatever had it before the
474
+ // dialog opened as part of ITS OWN unmount cleanup — a plain
475
+ // `.focus()` with no Range of its own, which collapses a
476
+ // contentEditable's caret to offset 0. That cleanup runs after this
477
+ // function returns (the picker only unmounts once `closeMention`
478
+ // below flips `mention` to null and React commits it), so the
479
+ // Selection set two lines up can already be correct here and still
480
+ // get clobbered a tick later. Re-assert it the next time `body`
481
+ // actually regains focus, whenever that turns out to be.
482
+ pendingCaretRef.current = { node: space, offset: 1 };
461
483
  closeMention();
462
484
  syncEmpty(bodyRef.current);
463
485
  emit();
@@ -522,10 +544,31 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
522
544
  } })), _jsx("div", { ref: bodyRef, className: "crisp-rte-body", contentEditable: !readOnly, suppressContentEditableWarning: true, role: "textbox", tabIndex: readOnly ? -1 : 0, "aria-multiline": "true", "aria-label": bodyLabel, "aria-readonly": readOnly || undefined, "data-placeholder": placeholder, onInput: readOnly
523
545
  ? undefined
524
546
  : () => {
547
+ // A real keystroke landing here means `body` already has
548
+ // focus on its own terms — any caret a prior mention
549
+ // selection was waiting to reassert (see insertMention) is
550
+ // moot now and must not later jump the caret backward on
551
+ // some unrelated future focus.
552
+ pendingCaretRef.current = null;
525
553
  syncEmpty(bodyRef.current);
526
554
  updateSlashTrigger();
527
555
  updateMention();
528
556
  emit();
557
+ }, onFocus: readOnly
558
+ ? undefined
559
+ : () => {
560
+ const pending = pendingCaretRef.current;
561
+ pendingCaretRef.current = null;
562
+ if (!pending?.node.isConnected)
563
+ return;
564
+ const sel = window.getSelection();
565
+ if (!sel)
566
+ return;
567
+ const range = document.createRange();
568
+ range.setStart(pending.node, pending.offset);
569
+ range.collapse(true);
570
+ sel.removeAllRanges();
571
+ sel.addRange(range);
529
572
  }, onMouseUp: readOnly
530
573
  ? undefined
531
574
  : () => {
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCrispMessage, useCrispMessages } from "../../core/index.js";
2
3
  import { tasklistRecipe } from "./tasklist.recipe.js";
3
4
  function Row({ task, onToggle, completeLabel, incompleteLabel, }) {
4
5
  return (_jsxs("div", { className: "crisp-tasklist-row", "data-done": task.done ? "" : undefined, children: [_jsx("input", { type: "checkbox", className: "crisp-tasklist-check", checked: !!task.done, "aria-label": task.done ? incompleteLabel : completeLabel, onChange: () => onToggle?.(task.id, !task.done) }), _jsx("span", { className: "crisp-tasklist-title", children: task.title }), (task.due != null || task.meta != null) && (_jsxs("span", { className: "crisp-tasklist-meta", children: [task.due != null && (_jsx("span", { className: "crisp-tasklist-due", "data-overdue": task.overdue ? "" : undefined, children: task.due })), task.meta] }))] }));
@@ -8,9 +9,15 @@ function Row({ task, onToggle, completeLabel, incompleteLabel, }) {
8
9
  * count, then rows of a round complete-checkbox + title + trailing meta.
9
10
  * Completed rows strike through; overdue dates go red.
10
11
  */
11
- export function TaskList({ tasks, groups, onToggle, emptyLabel = "No tasks", className, completeLabel = "Mark complete", incompleteLabel = "Mark incomplete", ...rest }) {
12
+ export function TaskList({ tasks, groups, onToggle, emptyLabel: emptyLabelProp, className, completeLabel: completeLabelProp, incompleteLabel: incompleteLabelProp, ...rest }) {
12
13
  const resolved = groups ?? (tasks?.length ? [{ label: null, tasks }] : []);
13
14
  const total = resolved.reduce((s, g) => s + g.tasks.length, 0);
15
+ // crisp/#471: `emptyLabel` is typed `React.ReactNode`, so it resolves by
16
+ // hand rather than through `useCrispMessage`, which is string-only.
17
+ const crispMessages = useCrispMessages();
18
+ const emptyLabel = emptyLabelProp ?? crispMessages["No tasks"] ?? "No tasks";
19
+ const completeLabel = useCrispMessage(completeLabelProp, "Mark complete");
20
+ const incompleteLabel = useCrispMessage(incompleteLabelProp, "Mark incomplete");
14
21
  return (_jsx("div", { className: tasklistRecipe({ className }), ...rest, children: total === 0 ? (_jsx("div", { className: "crisp-tasklist-empty", children: emptyLabel })) : (resolved.map((g, gi) => (_jsxs("div", { children: [g.label != null && (_jsxs("div", { className: "crisp-tasklist-group", children: [g.label, _jsx("span", { className: "crisp-tasklist-group-count", children: g.tasks.length })] })), g.tasks.map((t) => (_jsx(Row, { task: t, onToggle: onToggle, completeLabel: completeLabel, incompleteLabel: incompleteLabel }, t.id)))] }, gi)))) }));
15
22
  }
16
23
  TaskList.displayName = "TaskList";
@@ -97,4 +97,18 @@ export const palette = {
97
97
  // as-is per build/spec-check.mjs's own note), this token exists only for
98
98
  // Badge's soft-neutral text, so darkening it doesn't move any other surface.
99
99
  softNeutralText: { light: "#686870", dark: "#9a9aa2" },
100
+ // Badge's purple/role-pill tone (crisp#506) -- measured live off Attio's
101
+ // own role pill ("Admin", app.attio.com/<ws>/settings/workspace/members):
102
+ // background rgb(245,240,255), text rgb(98,56,181). The ring re-uses a
103
+ // second, independent measurement from the same session (a company's
104
+ // Categories tag pill sharing the identical background, "Broadcasting",
105
+ // app.attio.com/<ws>/companies) at rgb(232,221,254) -- same hue family,
106
+ // cross-validated rather than guessed. Dark mode was NOT measured (no
107
+ // live Attio session was reachable when this tone was added) -- these
108
+ // three light values are reused as-is for dark rather than an invented
109
+ // "brightened" guess (Lesson: don't add unmeasured "robustness"). Needs a
110
+ // real dark-mode measurement before this note can be removed.
111
+ purpleTint: { light: "#f5f0ff", dark: "#f5f0ff" },
112
+ softPurpleText: { light: "#6238b5", dark: "#6238b5" },
113
+ purpleRing: { light: "#e8ddfe", dark: "#e8ddfe" },
100
114
  };
@@ -60,4 +60,9 @@ export const semantic = {
60
60
  fgWarningSoft: "softWarningText",
61
61
  fgDangerSoft: "softDangerText",
62
62
  fgNeutralSoft: "softNeutralText",
63
+ /** Badge's purple/role-pill tone (crisp#506) — see palette.ts's comment
64
+ * on `purpleTint`/`softPurpleText`/`purpleRing` for the measurement. */
65
+ bgPurpleSoft: "purpleTint",
66
+ fgPurpleSoft: "softPurpleText",
67
+ ringPurpleSoft: "purpleRing",
63
68
  };
@@ -0,0 +1,7 @@
1
+ .crisp-headerbar {
2
+ box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-2); height: var(--crisp-size-header); flex-shrink: 0; padding: 0 var(--crisp-space-3); font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
+ &[data-bordered] { border-bottom: 1px solid var(--crisp-border-subtle); }
4
+ & .crisp-headerbar-leading { display: flex; align-items: center; gap: var(--crisp-space-2); flex-shrink: 0; min-width: 0; }
5
+ & .crisp-headerbar-title { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--crisp-space-2); overflow: hidden; }
6
+ & .crisp-headerbar-trailing { display: flex; align-items: center; gap: var(--crisp-space-2); flex-shrink: 0; }
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface HeaderbarVariants {
4
+
5
+ }
6
+
7
+ export declare const headerbarRecipe: RecipeFn<{ }>;
@@ -0,0 +1,9 @@
1
+ @layer crisp-components {
2
+ .crisp-headerbar {
3
+ box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-2); height: var(--crisp-size-header); flex-shrink: 0; padding: 0 var(--crisp-space-3); font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
+ &[data-bordered] { border-bottom: 1px solid var(--crisp-border-subtle); }
5
+ & .crisp-headerbar-leading { display: flex; align-items: center; gap: var(--crisp-space-2); flex-shrink: 0; min-width: 0; }
6
+ & .crisp-headerbar-title { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--crisp-space-2); overflow: hidden; }
7
+ & .crisp-headerbar-trailing { display: flex; align-items: center; gap: var(--crisp-space-2); flex-shrink: 0; }
8
+ }
9
+ }
@@ -2,7 +2,7 @@
2
2
  // The same tokens the CSS custom properties carry, resolved per theme, with
3
3
  // dimensions as numbers for React Native and Lynx style objects.
4
4
  export type Theme = "light" | "dark";
5
- export type ColorTokens = Record<"bgBrandSolid" | "bgBrandSolidPressed" | "fgOnBrand" | "fgOnTone" | "bgSurface" | "bgRaised" | "borderDefault" | "borderSubtle" | "ringNeutral" | "ringField" | "ringSwitch" | "bgTileActive" | "shadowTileActive" | "bgOverlayFrost" | "shadowOverlay1" | "shadowOverlay2" | "shadowOverlay3" | "bgPopover" | "ringPopover" | "shadowPopover1" | "shadowPopover2" | "shadowPopover3" | "fgPrimary" | "fgMuted" | "bgDanger" | "bgDangerSolid" | "strokeFieldFocus" | "fgDisabled" | "bgNeutralWeak" | "bgNeutralInverted" | "fgOnNeutralInverted" | "fgNeutralSubtle" | "fgNeutralMuted" | "strokeFocusRing" | "bgSuccess" | "bgWarning" | "bgOrange" | "bgCyan" | "bgPurple" | "bgSidebar" | "bgSubtle" | "fgBrandSoft" | "fgSuccessSoft" | "fgWarningSoft" | "fgDangerSoft" | "fgNeutralSoft", string>;
5
+ export type ColorTokens = Record<"bgBrandSolid" | "bgBrandSolidPressed" | "fgOnBrand" | "fgOnTone" | "bgSurface" | "bgRaised" | "borderDefault" | "borderSubtle" | "ringNeutral" | "ringField" | "ringSwitch" | "bgTileActive" | "shadowTileActive" | "bgOverlayFrost" | "shadowOverlay1" | "shadowOverlay2" | "shadowOverlay3" | "bgPopover" | "ringPopover" | "shadowPopover1" | "shadowPopover2" | "shadowPopover3" | "fgPrimary" | "fgMuted" | "bgDanger" | "bgDangerSolid" | "strokeFieldFocus" | "fgDisabled" | "bgNeutralWeak" | "bgNeutralInverted" | "fgOnNeutralInverted" | "fgNeutralSubtle" | "fgNeutralMuted" | "strokeFocusRing" | "bgSuccess" | "bgWarning" | "bgOrange" | "bgCyan" | "bgPurple" | "bgSidebar" | "bgSubtle" | "fgBrandSoft" | "fgSuccessSoft" | "fgWarningSoft" | "fgDangerSoft" | "fgNeutralSoft" | "bgPurpleSoft" | "fgPurpleSoft" | "ringPurpleSoft", string>;
6
6
  export declare const tokens: {
7
7
  color: Record<Theme, ColorTokens>;
8
8
  palette: Record<Theme, Record<string, string>>;
@@ -49,7 +49,10 @@ export const tokens = {
49
49
  "fgSuccessSoft": "#186b3d",
50
50
  "fgWarningSoft": "#80520d",
51
51
  "fgDangerSoft": "#a33539",
52
- "fgNeutralSoft": "#686870"
52
+ "fgNeutralSoft": "#686870",
53
+ "bgPurpleSoft": "#f5f0ff",
54
+ "fgPurpleSoft": "#6238b5",
55
+ "ringPurpleSoft": "#e8ddfe"
53
56
  },
54
57
  "dark": {
55
58
  "bgBrandSolid": "#266df0",
@@ -97,7 +100,10 @@ export const tokens = {
97
100
  "fgSuccessSoft": "#2ea968",
98
101
  "fgWarningSoft": "#e0982a",
99
102
  "fgDangerSoft": "#ff5a5f",
100
- "fgNeutralSoft": "#9a9aa2"
103
+ "fgNeutralSoft": "#9a9aa2",
104
+ "bgPurpleSoft": "#f5f0ff",
105
+ "fgPurpleSoft": "#6238b5",
106
+ "ringPurpleSoft": "#e8ddfe"
101
107
  }
102
108
  },
103
109
  "palette": {
@@ -146,7 +152,10 @@ export const tokens = {
146
152
  "softSuccessText": "#186b3d",
147
153
  "softWarningText": "#80520d",
148
154
  "softDangerText": "#a33539",
149
- "softNeutralText": "#686870"
155
+ "softNeutralText": "#686870",
156
+ "purpleTint": "#f5f0ff",
157
+ "softPurpleText": "#6238b5",
158
+ "purpleRing": "#e8ddfe"
150
159
  },
151
160
  "dark": {
152
161
  "gray00": "#0a0a0a",
@@ -193,7 +202,10 @@ export const tokens = {
193
202
  "softSuccessText": "#2ea968",
194
203
  "softWarningText": "#e0982a",
195
204
  "softDangerText": "#ff5a5f",
196
- "softNeutralText": "#9a9aa2"
205
+ "softNeutralText": "#9a9aa2",
206
+ "purpleTint": "#f5f0ff",
207
+ "softPurpleText": "#6238b5",
208
+ "purpleRing": "#e8ddfe"
197
209
  }
198
210
  },
199
211
  "space": {
@@ -47,6 +47,10 @@
47
47
  & .crisp-recordpage-detailsearch { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: var(--crisp-size-chip); flex-shrink: 0; padding: 0; margin: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
48
48
  & .crisp-recordpage-detailsearch:hover, & .crisp-recordpage-detailsearch[data-active] { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
49
49
  & .crisp-recordpage-noresults { padding: var(--crisp-space-1-5) var(--crisp-space-1); font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
50
+ /* A caller-supplied `sections` grouping (#528) is not an Attio-measured
51
+ shape (its own Details column is flat) -- this heading reuses
52
+ .crisp-recordpage-section-title's own type treatment verbatim, just spaced from the group above it. */
53
+ & .crisp-recordpage-fieldsection-title { margin-top: var(--crisp-space-3); }
50
54
  & .crisp-recordpage-morevalues { display: inline-flex; align-items: center; padding: 0; margin: 0; border: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-muted); cursor: pointer; }
51
55
  & .crisp-recordpage-morevalues:hover { color: var(--crisp-fg-primary); }
52
56
  & .crisp-recordpage-viewall { flex-basis: 100%; margin-top: var(--crisp-space-0-5); border: none; background: none; padding: 0; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-family: inherit; font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-regular); cursor: pointer; text-align: start; }
@@ -61,31 +65,25 @@
61
65
  & .crisp-recordpage-field-label-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
62
66
  & .crisp-recordpage-field-value { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--crisp-space-1); padding-top: 2px; line-height: 20px; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
63
67
  & .crisp-recordpage-field-value[data-editable] { cursor: pointer; }
64
- /* the click-to-edit trigger is a real <button> (keyboard-operable),
65
- reset back to plain inline text and made a flex-wrap container of its
66
- own -- FieldValue can render several chips (tags), which still need
67
- to wrap the way they did before this had a button around them at
68
- all. It wraps only FieldValue, never the error slot beside it (see
69
- the component's own comment) -- NOT `display: contents`, which drops a button's accessible semantics in some engines. */
68
+ /* the click-to-edit trigger is a real <button> (keyboard-operable), reset back to plain inline text and made a flex-wrap container of its own -- FieldValue can render several chips (tags), which still need to wrap the way they did before this had a button around them at all. It wraps only FieldValue, never the error slot beside it (see the component's own comment) -- NOT `display: contents`, which
69
+ drops a button's accessible semantics in some engines. */
70
70
  & .crisp-recordpage-edit-trigger { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--crisp-space-1); margin: 0; padding: 0; border: none; background: none; text-align: start; font: inherit; color: inherit; cursor: pointer; }
71
71
  & .crisp-recordpage-empty { color: var(--crisp-fg-muted); }
72
-
73
- /* #431: measured live on app.attio.com's record page Details column -- clicking a field's value opens a plain, borderless field exactly
74
- filling the value's own box (no border, no background, no box-shadow), same font as the value it replaces (hence `font: inherit`, not a restated size/weight). A field row's own hover tint
75
- above already carries the "this is interactive" affordance; the
76
- pending/error treatment isn't Attio-measured -- it reuses the same tokens DataTable's identical states already use (opacity 0.5,
77
- `bgDanger`), per #385/#430. */
72
+ /* #431: measured live on app.attio.com's record page Details column --
73
+ clicking a field's value opens a plain, borderless field exactly filling the value's own box (no border, no background, no
74
+ box-shadow), same font as the value it replaces (hence `font:
75
+ inherit`, not a restated size/weight). A field row's own hover tint above already carries the "this is interactive" affordance; the pending/error treatment isn't Attio-measured -- it reuses the same
76
+ tokens DataTable's identical states already use (opacity 0.5, `bgDanger`), per #385/#430. */
78
77
  & .crisp-recordpage-edit { display: block; width: 100%; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; box-shadow: none; outline: none; font: inherit; color: inherit; }
79
- /* not Attio-measured (its own field shows no focus ring either, caret
80
- only) -- kept for real keyboard users, the same brand ring every
81
- other crisp field already shows on focus (DataTable's identical .crisp-datatable-edit:focus, e.g.). */
78
+ /* not Attio-measured (its own field shows no focus ring either, caret only) -- kept for real keyboard users, the same brand ring every other crisp field already shows on focus (DataTable's identical
79
+ .crisp-datatable-edit:focus, e.g.). */
82
80
  & .crisp-recordpage-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
83
81
  & .crisp-recordpage-edit:disabled { opacity: 0.5; cursor: not-allowed; }
84
82
  & .crisp-recordpage-edit-pending { flex-shrink: 0; color: var(--crisp-fg-muted); }
85
83
  & .crisp-recordpage-edit-error { flex-basis: 100%; margin-top: var(--crisp-space-0-5); font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
86
- /* #457: an `editor: "select"` field's at-rest chip -- same shape as
87
- RecordsTable's own `.crisp-recordstable-status[-dot]` (already measured against Attio there), but its own classes: that pair is scoped under `.crisp-recordstable`'s own recipe root, so reusing the
88
- classnames verbatim here would resolve to nothing. */
84
+ /* #457: an `editor: "select"` field's at-rest chip -- same shape as RecordsTable's own `.crisp-recordstable-status[-dot]` (already
85
+ measured against Attio there), but its own classes: that pair is
86
+ scoped under `.crisp-recordstable`'s own recipe root, so reusing the classnames verbatim here would resolve to nothing. */
89
87
  & .crisp-recordpage-status { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; }
90
88
  & .crisp-recordpage-status-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: var(--crisp-radius-full); }
91
89
  & .crisp-recordpage-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; }
@@ -93,23 +91,18 @@
93
91
  & .crisp-recordpage-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
94
92
  & .crisp-recordpage-link:hover { text-decoration: underline; }
95
93
  & .crisp-recordpage-emptystate { padding: var(--crisp-space-2) var(--crisp-space-1); font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
96
-
97
94
  & .crisp-recordpage-main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: var(--crisp-space-3) var(--crisp-space-5); }
98
-
99
95
  & .crisp-recordpage-panel { padding-top: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-4); }
100
96
  & .crisp-recordpage-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); background: var(--crisp-bg-surface); overflow: hidden; }
101
97
  & .crisp-recordpage-card-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.14px; color: var(--crisp-fg-primary); padding: var(--crisp-space-3) var(--crisp-space-3) var(--crisp-space-2); }
102
98
  & .crisp-recordpage-hl-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); overflow: hidden; }
103
- /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any
104
- column count, so the grid stays responsive (auto-fit) without per-cell
105
- nth-child border hacks that break when the column count changes. */
99
+ /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any column count, so the grid stays responsive (auto-fit) without per-cell nth-child border hacks that break when the column count changes. */
106
100
  & .crisp-recordpage-hl-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1px; background: var(--crisp-border-subtle); }
107
101
  & .crisp-recordpage-hl-cell { position: relative; min-height: 84px; padding: var(--crisp-space-2-5) var(--crisp-space-3); background: var(--crisp-bg-surface); }
108
102
  & .crisp-recordpage-hl-icon { position: absolute; top: var(--crisp-space-2-5); inset-inline-end: var(--crisp-space-3); color: var(--crisp-fg-muted); }
109
103
  & .crisp-recordpage-hl-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); margin-bottom: var(--crisp-space-2-5); padding-inline-end: var(--crisp-space-4); }
110
104
  & .crisp-recordpage-hl-value { display: flex; flex-wrap: wrap; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
111
105
  & .crisp-recordpage-hl-value--empty { color: var(--crisp-fg-muted); font-weight: var(--crisp-weight-regular); }
112
-
113
106
  & .crisp-recordpage-feed { display: flex; flex-direction: column; }
114
107
  & .crisp-recordpage-feed-item { display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2) 0; }
115
108
  & .crisp-recordpage-feed-dot { position: relative; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
@@ -118,7 +111,6 @@
118
111
  & .crisp-recordpage-feed-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); margin-top: 2px; }
119
112
  & .crisp-recordpage-feed-more { align-self: flex-start; margin-top: var(--crisp-space-1); border: none; background: none; padding: var(--crisp-space-1) 0; color: var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
120
113
  & .crisp-recordpage-feed-more:hover { text-decoration: underline; }
121
-
122
114
  & .crisp-recordpage-section-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-1); }
123
115
  & .crisp-recordpage-section-head .crisp-recordpage-section-title { padding: 0; }
124
116
  & .crisp-recordpage-section-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
@@ -126,7 +118,6 @@
126
118
  & .crisp-recordpage-addbtn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: 26px; padding: 0 var(--crisp-space-1-5); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
127
119
  & .crisp-recordpage-addbtn:hover { background: var(--crisp-bg-raised); }
128
120
  & .crisp-recordpage-addbtn svg { flex-shrink: 0; }
129
-
130
121
  & .crisp-recordpage-composer { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding: var(--crisp-space-2); border: 1px solid var(--crisp-border-default); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); margin-bottom: var(--crisp-space-2); }
131
122
  & .crisp-recordpage-composer textarea { width: 100%; min-height: 56px; resize: vertical; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
132
123
  & .crisp-recordpage-composer textarea:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); border-radius: var(--crisp-radius-md); }
@@ -136,23 +127,18 @@
136
127
  & .crisp-recordpage-btn:hover { background: var(--crisp-bg-raised); }
137
128
  & .crisp-recordpage-btn--primary { box-shadow: none; background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); }
138
129
  & .crisp-recordpage-btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
139
-
140
130
  & .crisp-recordpage-note { display: flex; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
141
131
  & .crisp-recordpage-note-body { flex: 1; min-width: 0; }
142
132
  & .crisp-recordpage-note-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-bottom: var(--crisp-space-0-5); }
143
133
  & .crisp-recordpage-note-author { font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
144
134
  & .crisp-recordpage-note-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
145
135
  & .crisp-recordpage-note-text { font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); white-space: pre-wrap; word-break: break-word; }
146
-
147
136
  & .crisp-recordpage-task { display: flex; align-items: flex-start; gap: var(--crisp-space-2); padding: var(--crisp-space-1-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
148
137
  & .crisp-recordpage-task-text { flex: 1; min-width: 0; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); padding-top: 1px; }
149
138
  & .crisp-recordpage-task[data-done] .crisp-recordpage-task-text { text-decoration: line-through; color: var(--crisp-fg-muted); }
150
-
151
139
  & .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-6) var(--crisp-space-3); border: 1px dashed var(--crisp-border-default); border-radius: var(--crisp-radius-xl); color: var(--crisp-fg-muted); font-size: var(--crisp-text-md); text-align: center; }
152
140
  & .crisp-recordpage-emptycard svg { color: var(--crisp-fg-muted); opacity: 0.7; }
153
-
154
141
  & .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
155
-
156
142
  & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
157
143
  & .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
158
144
  & .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
@@ -48,6 +48,10 @@
48
48
  & .crisp-recordpage-detailsearch { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: var(--crisp-size-chip); flex-shrink: 0; padding: 0; margin: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
49
49
  & .crisp-recordpage-detailsearch:hover, & .crisp-recordpage-detailsearch[data-active] { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
50
50
  & .crisp-recordpage-noresults { padding: var(--crisp-space-1-5) var(--crisp-space-1); font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
51
+ /* A caller-supplied `sections` grouping (#528) is not an Attio-measured
52
+ shape (its own Details column is flat) -- this heading reuses
53
+ .crisp-recordpage-section-title's own type treatment verbatim, just spaced from the group above it. */
54
+ & .crisp-recordpage-fieldsection-title { margin-top: var(--crisp-space-3); }
51
55
  & .crisp-recordpage-morevalues { display: inline-flex; align-items: center; padding: 0; margin: 0; border: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-muted); cursor: pointer; }
52
56
  & .crisp-recordpage-morevalues:hover { color: var(--crisp-fg-primary); }
53
57
  & .crisp-recordpage-viewall { flex-basis: 100%; margin-top: var(--crisp-space-0-5); border: none; background: none; padding: 0; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-family: inherit; font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-regular); cursor: pointer; text-align: start; }
@@ -62,31 +66,25 @@
62
66
  & .crisp-recordpage-field-label-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
63
67
  & .crisp-recordpage-field-value { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--crisp-space-1); padding-top: 2px; line-height: 20px; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
64
68
  & .crisp-recordpage-field-value[data-editable] { cursor: pointer; }
65
- /* the click-to-edit trigger is a real <button> (keyboard-operable),
66
- reset back to plain inline text and made a flex-wrap container of its
67
- own -- FieldValue can render several chips (tags), which still need
68
- to wrap the way they did before this had a button around them at
69
- all. It wraps only FieldValue, never the error slot beside it (see
70
- the component's own comment) -- NOT `display: contents`, which drops a button's accessible semantics in some engines. */
69
+ /* the click-to-edit trigger is a real <button> (keyboard-operable), reset back to plain inline text and made a flex-wrap container of its own -- FieldValue can render several chips (tags), which still need to wrap the way they did before this had a button around them at all. It wraps only FieldValue, never the error slot beside it (see the component's own comment) -- NOT `display: contents`, which
70
+ drops a button's accessible semantics in some engines. */
71
71
  & .crisp-recordpage-edit-trigger { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--crisp-space-1); margin: 0; padding: 0; border: none; background: none; text-align: start; font: inherit; color: inherit; cursor: pointer; }
72
72
  & .crisp-recordpage-empty { color: var(--crisp-fg-muted); }
73
-
74
- /* #431: measured live on app.attio.com's record page Details column -- clicking a field's value opens a plain, borderless field exactly
75
- filling the value's own box (no border, no background, no box-shadow), same font as the value it replaces (hence `font: inherit`, not a restated size/weight). A field row's own hover tint
76
- above already carries the "this is interactive" affordance; the
77
- pending/error treatment isn't Attio-measured -- it reuses the same tokens DataTable's identical states already use (opacity 0.5,
78
- `bgDanger`), per #385/#430. */
73
+ /* #431: measured live on app.attio.com's record page Details column --
74
+ clicking a field's value opens a plain, borderless field exactly filling the value's own box (no border, no background, no
75
+ box-shadow), same font as the value it replaces (hence `font:
76
+ inherit`, not a restated size/weight). A field row's own hover tint above already carries the "this is interactive" affordance; the pending/error treatment isn't Attio-measured -- it reuses the same
77
+ tokens DataTable's identical states already use (opacity 0.5, `bgDanger`), per #385/#430. */
79
78
  & .crisp-recordpage-edit { display: block; width: 100%; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; box-shadow: none; outline: none; font: inherit; color: inherit; }
80
- /* not Attio-measured (its own field shows no focus ring either, caret
81
- only) -- kept for real keyboard users, the same brand ring every
82
- other crisp field already shows on focus (DataTable's identical .crisp-datatable-edit:focus, e.g.). */
79
+ /* not Attio-measured (its own field shows no focus ring either, caret only) -- kept for real keyboard users, the same brand ring every other crisp field already shows on focus (DataTable's identical
80
+ .crisp-datatable-edit:focus, e.g.). */
83
81
  & .crisp-recordpage-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
84
82
  & .crisp-recordpage-edit:disabled { opacity: 0.5; cursor: not-allowed; }
85
83
  & .crisp-recordpage-edit-pending { flex-shrink: 0; color: var(--crisp-fg-muted); }
86
84
  & .crisp-recordpage-edit-error { flex-basis: 100%; margin-top: var(--crisp-space-0-5); font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
87
- /* #457: an `editor: "select"` field's at-rest chip -- same shape as
88
- RecordsTable's own `.crisp-recordstable-status[-dot]` (already measured against Attio there), but its own classes: that pair is scoped under `.crisp-recordstable`'s own recipe root, so reusing the
89
- classnames verbatim here would resolve to nothing. */
85
+ /* #457: an `editor: "select"` field's at-rest chip -- same shape as RecordsTable's own `.crisp-recordstable-status[-dot]` (already
86
+ measured against Attio there), but its own classes: that pair is
87
+ scoped under `.crisp-recordstable`'s own recipe root, so reusing the classnames verbatim here would resolve to nothing. */
90
88
  & .crisp-recordpage-status { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; }
91
89
  & .crisp-recordpage-status-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: var(--crisp-radius-full); }
92
90
  & .crisp-recordpage-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; }
@@ -94,23 +92,18 @@
94
92
  & .crisp-recordpage-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
95
93
  & .crisp-recordpage-link:hover { text-decoration: underline; }
96
94
  & .crisp-recordpage-emptystate { padding: var(--crisp-space-2) var(--crisp-space-1); font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
97
-
98
95
  & .crisp-recordpage-main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: var(--crisp-space-3) var(--crisp-space-5); }
99
-
100
96
  & .crisp-recordpage-panel { padding-top: var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-4); }
101
97
  & .crisp-recordpage-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); background: var(--crisp-bg-surface); overflow: hidden; }
102
98
  & .crisp-recordpage-card-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.14px; color: var(--crisp-fg-primary); padding: var(--crisp-space-3) var(--crisp-space-3) var(--crisp-space-2); }
103
99
  & .crisp-recordpage-hl-card { border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-2xl); overflow: hidden; }
104
- /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any
105
- column count, so the grid stays responsive (auto-fit) without per-cell
106
- nth-child border hacks that break when the column count changes. */
100
+ /* gap-as-gridlines: a 1px gap over the border colour draws dividers at any column count, so the grid stays responsive (auto-fit) without per-cell nth-child border hacks that break when the column count changes. */
107
101
  & .crisp-recordpage-hl-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1px; background: var(--crisp-border-subtle); }
108
102
  & .crisp-recordpage-hl-cell { position: relative; min-height: 84px; padding: var(--crisp-space-2-5) var(--crisp-space-3); background: var(--crisp-bg-surface); }
109
103
  & .crisp-recordpage-hl-icon { position: absolute; top: var(--crisp-space-2-5); inset-inline-end: var(--crisp-space-3); color: var(--crisp-fg-muted); }
110
104
  & .crisp-recordpage-hl-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); margin-bottom: var(--crisp-space-2-5); padding-inline-end: var(--crisp-space-4); }
111
105
  & .crisp-recordpage-hl-value { display: flex; flex-wrap: wrap; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
112
106
  & .crisp-recordpage-hl-value--empty { color: var(--crisp-fg-muted); font-weight: var(--crisp-weight-regular); }
113
-
114
107
  & .crisp-recordpage-feed { display: flex; flex-direction: column; }
115
108
  & .crisp-recordpage-feed-item { display: flex; align-items: flex-start; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2) 0; }
116
109
  & .crisp-recordpage-feed-dot { position: relative; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
@@ -119,7 +112,6 @@
119
112
  & .crisp-recordpage-feed-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); margin-top: 2px; }
120
113
  & .crisp-recordpage-feed-more { align-self: flex-start; margin-top: var(--crisp-space-1); border: none; background: none; padding: var(--crisp-space-1) 0; color: var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
121
114
  & .crisp-recordpage-feed-more:hover { text-decoration: underline; }
122
-
123
115
  & .crisp-recordpage-section-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-1); }
124
116
  & .crisp-recordpage-section-head .crisp-recordpage-section-title { padding: 0; }
125
117
  & .crisp-recordpage-section-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
@@ -127,7 +119,6 @@
127
119
  & .crisp-recordpage-addbtn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: 26px; padding: 0 var(--crisp-space-1-5); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
128
120
  & .crisp-recordpage-addbtn:hover { background: var(--crisp-bg-raised); }
129
121
  & .crisp-recordpage-addbtn svg { flex-shrink: 0; }
130
-
131
122
  & .crisp-recordpage-composer { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding: var(--crisp-space-2); border: 1px solid var(--crisp-border-default); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); margin-bottom: var(--crisp-space-2); }
132
123
  & .crisp-recordpage-composer textarea { width: 100%; min-height: 56px; resize: vertical; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
133
124
  & .crisp-recordpage-composer textarea:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); border-radius: var(--crisp-radius-md); }
@@ -137,23 +128,18 @@
137
128
  & .crisp-recordpage-btn:hover { background: var(--crisp-bg-raised); }
138
129
  & .crisp-recordpage-btn--primary { box-shadow: none; background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); }
139
130
  & .crisp-recordpage-btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
140
-
141
131
  & .crisp-recordpage-note { display: flex; gap: var(--crisp-space-2-5); padding: var(--crisp-space-2-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
142
132
  & .crisp-recordpage-note-body { flex: 1; min-width: 0; }
143
133
  & .crisp-recordpage-note-head { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-bottom: var(--crisp-space-0-5); }
144
134
  & .crisp-recordpage-note-author { font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
145
135
  & .crisp-recordpage-note-time { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
146
136
  & .crisp-recordpage-note-text { font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); white-space: pre-wrap; word-break: break-word; }
147
-
148
137
  & .crisp-recordpage-task { display: flex; align-items: flex-start; gap: var(--crisp-space-2); padding: var(--crisp-space-1-5) 0; border-top: 1px solid var(--crisp-border-subtle); }
149
138
  & .crisp-recordpage-task-text { flex: 1; min-width: 0; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); padding-top: 1px; }
150
139
  & .crisp-recordpage-task[data-done] .crisp-recordpage-task-text { text-decoration: line-through; color: var(--crisp-fg-muted); }
151
-
152
140
  & .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-6) var(--crisp-space-3); border: 1px dashed var(--crisp-border-default); border-radius: var(--crisp-radius-xl); color: var(--crisp-fg-muted); font-size: var(--crisp-text-md); text-align: center; }
153
141
  & .crisp-recordpage-emptycard svg { color: var(--crisp-fg-muted); opacity: 0.7; }
154
-
155
142
  & .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
156
-
157
143
  & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
158
144
  & .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
159
145
  & .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
@@ -0,0 +1,5 @@
1
+ .crisp-skiplink {
2
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; text-decoration: none; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary);
3
+ &:focus { position: fixed; top: var(--crisp-space-3); inset-inline-start: var(--crisp-space-3); z-index: 90; width: auto; height: auto; margin: 0; overflow: visible; clip: auto; white-space: normal; padding: var(--crisp-space-2) var(--crisp-space-3); border-radius: var(--crisp-radius-md); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); }
4
+ &:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
5
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface SkiplinkVariants {
4
+
5
+ }
6
+
7
+ export declare const skiplinkRecipe: RecipeFn<{ }>;
@@ -0,0 +1,7 @@
1
+ @layer crisp-components {
2
+ .crisp-skiplink {
3
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; text-decoration: none; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary);
4
+ &:focus { position: fixed; top: var(--crisp-space-3); inset-inline-start: var(--crisp-space-3); z-index: 90; width: auto; height: auto; margin: 0; overflow: visible; clip: auto; white-space: normal; padding: var(--crisp-space-2) var(--crisp-space-3); border-radius: var(--crisp-radius-md); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); }
5
+ &:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
6
+ }
7
+ }