@crisp-ui-kit/crisp 0.54.0 → 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 (38) 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/index.js +1 -0
  9. package/dist/cjs/product/activityfeed/activityfeed.js +38 -0
  10. package/dist/cjs/product/activityfeed/activityfeed.recipe.js +35 -0
  11. package/dist/cjs/product/activityfeed/index.js +10 -0
  12. package/dist/cjs/product/richtext/richtext.js +43 -0
  13. package/dist/cjs/tokens/palette.js +14 -0
  14. package/dist/cjs/tokens/semantic.js +5 -0
  15. package/dist/esm/components/badge/badge.recipe.js +10 -0
  16. package/dist/esm/index.js +1 -0
  17. package/dist/esm/product/activityfeed/activityfeed.js +32 -0
  18. package/dist/esm/product/activityfeed/activityfeed.recipe.js +32 -0
  19. package/dist/esm/product/activityfeed/index.js +2 -0
  20. package/dist/esm/product/richtext/richtext.js +43 -0
  21. package/dist/esm/tokens/palette.js +14 -0
  22. package/dist/esm/tokens/semantic.js +5 -0
  23. package/dist/native/tokens.d.ts +1 -1
  24. package/dist/native/tokens.js +16 -4
  25. package/dist/styles.css +22 -0
  26. package/dist/styles.layered.css +22 -0
  27. package/dist/tokens.css +6 -0
  28. package/dist/tokens.json +16 -4
  29. package/dist/types/components/badge/badge.d.ts +2 -0
  30. package/dist/types/components/badge/badge.recipe.d.ts +8 -0
  31. package/dist/types/index.d.ts +1 -0
  32. package/dist/types/product/activityfeed/activityfeed.d.ts +57 -0
  33. package/dist/types/product/activityfeed/activityfeed.recipe.d.ts +3 -0
  34. package/dist/types/product/activityfeed/index.d.ts +2 -0
  35. package/dist/types/tokens/contract.d.ts +3 -0
  36. package/dist/types/tokens/palette.d.ts +12 -0
  37. package/dist/types/tokens/semantic.d.ts +5 -0
  38. package/package.json +6 -1
@@ -0,0 +1,12 @@
1
+ .crisp-activityfeed {
2
+ box-sizing: border-box; max-width: 767px; margin-inline: auto; padding: var(--crisp-space-6); display: flex; flex-direction: column; gap: var(--crisp-space-4);
3
+ & .crisp-activityfeed-row { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding-bottom: var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
4
+ & .crisp-activityfeed-row:last-of-type { border-bottom: none; padding-bottom: 0; }
5
+ & .crisp-activityfeed-row-head { display: flex; align-items: center; gap: var(--crisp-space-2); }
6
+ & .crisp-activityfeed-row-spacer { flex: 1; }
7
+ & .crisp-activityfeed-row-when { font-size: var(--crisp-text-xs); color: var(--crisp-fg-muted); white-space: nowrap; }
8
+ & .crisp-activityfeed-diff { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--crisp-space-1); }
9
+ & .crisp-activityfeed-diff-item { padding-inline-start: var(--crisp-space-8); font-size: var(--crisp-text-md); }
10
+ & .crisp-activityfeed-diff-field { color: var(--crisp-fg-muted); }
11
+ & .crisp-activityfeed-diff-before { color: var(--crisp-fg-muted); text-decoration: line-through; }
12
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface ActivityfeedVariants {
4
+
5
+ }
6
+
7
+ export declare const activityfeedRecipe: RecipeFn<{ }>;
@@ -0,0 +1,14 @@
1
+ @layer crisp-components {
2
+ .crisp-activityfeed {
3
+ box-sizing: border-box; max-width: 767px; margin-inline: auto; padding: var(--crisp-space-6); display: flex; flex-direction: column; gap: var(--crisp-space-4);
4
+ & .crisp-activityfeed-row { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding-bottom: var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
5
+ & .crisp-activityfeed-row:last-of-type { border-bottom: none; padding-bottom: 0; }
6
+ & .crisp-activityfeed-row-head { display: flex; align-items: center; gap: var(--crisp-space-2); }
7
+ & .crisp-activityfeed-row-spacer { flex: 1; }
8
+ & .crisp-activityfeed-row-when { font-size: var(--crisp-text-xs); color: var(--crisp-fg-muted); white-space: nowrap; }
9
+ & .crisp-activityfeed-diff { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--crisp-space-1); }
10
+ & .crisp-activityfeed-diff-item { padding-inline-start: var(--crisp-space-8); font-size: var(--crisp-text-md); }
11
+ & .crisp-activityfeed-diff-field { color: var(--crisp-fg-muted); }
12
+ & .crisp-activityfeed-diff-before { color: var(--crisp-fg-muted); text-decoration: line-through; }
13
+ }
14
+ }
package/dist/badge.css CHANGED
@@ -17,6 +17,9 @@
17
17
  .crisp-badge--tone_danger {
18
18
  background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-fg-danger-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
19
19
  }
20
+ .crisp-badge--tone_purple {
21
+ background: var(--crisp-bg-purple-soft); color: var(--crisp-fg-purple-soft); box-shadow: inset 0 0 0 1px var(--crisp-ring-purple-soft);
22
+ }
20
23
  .crisp-badge--size_small {
21
24
  height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
22
25
  }
package/dist/badge.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { RecipeFn } from "../core/types";
2
2
 
3
3
  export interface BadgeVariants {
4
- tone?: "neutral" | "brand" | "success" | "warning" | "danger";
4
+ tone?: "neutral" | "brand" | "success" | "warning" | "danger" | "purple";
5
5
  size?: "small" | "medium" | "large";
6
6
  variant?: "soft" | "solid";
7
7
  }
8
8
 
9
- export declare const badgeRecipe: RecipeFn<{ tone: { neutral: string; brand: string; success: string; warning: string; danger: string }; size: { small: string; medium: string; large: string }; variant: { soft: string; solid: string } }>;
9
+ export declare const badgeRecipe: RecipeFn<{ tone: { neutral: string; brand: string; success: string; warning: string; danger: string; purple: string }; size: { small: string; medium: string; large: string }; variant: { soft: string; solid: string } }>;
@@ -18,6 +18,9 @@
18
18
  .crisp-badge--tone_danger {
19
19
  background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-fg-danger-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
20
20
  }
21
+ .crisp-badge--tone_purple {
22
+ background: var(--crisp-bg-purple-soft); color: var(--crisp-fg-purple-soft); box-shadow: inset 0 0 0 1px var(--crisp-ring-purple-soft);
23
+ }
21
24
  .crisp-badge--size_small {
22
25
  height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
23
26
  }
@@ -63,6 +63,16 @@ const spec = (0, core_1.defineSpec)({
63
63
  color: tokens_1.contract.fgDangerSoft,
64
64
  boxShadow: ring(tokens_1.contract.bgDanger, 30),
65
65
  },
66
+ // crisp#506: a distinct purple used for role-style labels (Attio's own
67
+ // "Admin" role pill), not a status/severity colour -- the other five
68
+ // tones don't cover this hue. Unlike those, these are the measured
69
+ // *solid* values directly (not a color-mix tint of a base accent):
70
+ // Attio's own pill background reads a flat, opaque rgb(), no alpha.
71
+ purple: {
72
+ background: tokens_1.contract.bgPurpleSoft,
73
+ color: tokens_1.contract.fgPurpleSoft,
74
+ boxShadow: `inset 0 0 0 1px ${tokens_1.contract.ringPurpleSoft}`,
75
+ },
66
76
  },
67
77
  size: {
68
78
  small: {
package/dist/cjs/index.js CHANGED
@@ -81,6 +81,7 @@ __exportStar(require("./core"), exports);
81
81
  // product screens (Sidebar, RecordsTable, RecordPage, Inbox, …). Use them as-is
82
82
  // or as composition references for your own data; `components/` above holds
83
83
  // the generic primitives (Table, DataTable, Button, …) to build your own UI from.
84
+ __exportStar(require("./product/activityfeed"), exports);
84
85
  __exportStar(require("./product/appshell"), exports);
85
86
  __exportStar(require("./product/authshell"), exports);
86
87
  __exportStar(require("./product/board"), exports);
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActivityFeed = ActivityFeed;
4
+ exports.ActivityFeedRow = ActivityFeedRow;
5
+ exports.ActivityFeedDiffItem = ActivityFeedDiffItem;
6
+ exports.ActivityFeedDiffList = ActivityFeedDiffList;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const activityfeed_recipe_1 = require("./activityfeed.recipe");
9
+ /**
10
+ * A record's own event timeline — a vertically stacked, bordered list of
11
+ * `ActivityFeedRow`s, capped at a readable content width. Built for
12
+ * internal/grc's per-record "Activity" tab (crisp#462 SHELL/forms gap): it
13
+ * replaces a hand-rolled `<div className="mx-auto max-w-3xl space-y-4
14
+ * p-6">`. No live Attio re-measurement behind its own chrome this pass (the
15
+ * CDP bridge was unreachable when this was built) — see the recipe's own
16
+ * doc comment for which already-measured crisp values it reuses instead of
17
+ * a new guess.
18
+ */
19
+ function ActivityFeed({ className, children, ...rest }) {
20
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, activityfeed_recipe_1.activityfeedRecipe)({ className }), ...rest, children: children }));
21
+ }
22
+ ActivityFeed.displayName = "ActivityFeed";
23
+ /** One event row: actor + action + timestamp, with an optional diff list
24
+ * underneath. The last row in an `ActivityFeed` draws no divider. */
25
+ function ActivityFeedRow({ actor, action, when, children, }) {
26
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-activityfeed-row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-activityfeed-row-head", children: [actor, action, (0, jsx_runtime_1.jsx)("span", { className: "crisp-activityfeed-row-spacer" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-activityfeed-row-when", children: when })] }), children] }));
27
+ }
28
+ ActivityFeedRow.displayName = "ActivityFeedRow";
29
+ /** One changed-field line under an `ActivityFeedRow` — "Field: before →
30
+ * after", or "Field: set to after" when there is no known prior value. */
31
+ function ActivityFeedDiffItem({ field, before, after, }) {
32
+ return ((0, jsx_runtime_1.jsxs)("li", { className: "crisp-activityfeed-diff-item", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-activityfeed-diff-field", children: field }), " ", before !== undefined ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-activityfeed-diff-before", children: before }), " ", (0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", children: "\u2192" }), " ", (0, jsx_runtime_1.jsx)("span", { children: after })] })) : ((0, jsx_runtime_1.jsx)("span", { children: after }))] }));
33
+ }
34
+ ActivityFeedDiffItem.displayName = "ActivityFeedDiffItem";
35
+ function ActivityFeedDiffList({ children, }) {
36
+ return (0, jsx_runtime_1.jsx)("ul", { className: "crisp-activityfeed-diff", children: children });
37
+ }
38
+ ActivityFeedDiffList.displayName = "ActivityFeedDiffList";
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.activityfeedRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A record's own event timeline (crisp#462 gap: internal/grc's ActivityTab
7
+ // hand-rolled this as a bare `<div className="mx-auto max-w-3xl space-y-4
8
+ // p-6">` list of bordered rows). No live Attio re-measurement behind these
9
+ // values this pass -- the CDP bridge to the owner's Vivaldi was unreachable
10
+ // when this was built (documented in the component's own doc comment) --
11
+ // so this reuses already-measured crisp values throughout rather than a new
12
+ // guess: `space["6"]` (24px) padding is RecordPanel's own content padding,
13
+ // the row's bottom border reuses `borderSubtle` (the same divider every
14
+ // other bordered row in this codebase draws with), and the 767px content
15
+ // cap matches SettingsSection's own measured content width.
16
+ const config = (0, core_1.defineRecipe)({
17
+ name: "activityfeed",
18
+ base: `box-sizing: border-box; max-width: 767px; margin-inline: auto; padding: ${tokens_1.contract.space["6"]};
19
+ display: flex; flex-direction: column; gap: ${tokens_1.contract.space["4"]};
20
+ & .crisp-activityfeed-row { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]}; padding-bottom: ${tokens_1.contract.space["4"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
21
+ & .crisp-activityfeed-row:last-of-type { border-bottom: none; padding-bottom: 0; }
22
+ & .crisp-activityfeed-row-head { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; }
23
+ & .crisp-activityfeed-row-spacer { flex: 1; }
24
+ & .crisp-activityfeed-row-when { font-size: ${tokens_1.contract.text.xs}; color: ${tokens_1.contract.fgMuted}; white-space: nowrap; }
25
+ & .crisp-activityfeed-diff { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["1"]}; }
26
+ & .crisp-activityfeed-diff-item { padding-inline-start: ${tokens_1.contract.space["8"]}; font-size: ${tokens_1.contract.text.md}; }
27
+ & .crisp-activityfeed-diff-field { color: ${tokens_1.contract.fgMuted}; }
28
+ & .crisp-activityfeed-diff-before { color: ${tokens_1.contract.fgMuted}; text-decoration: line-through; }
29
+ `,
30
+ variants: {},
31
+ defaultVariants: {},
32
+ compoundVariants: [],
33
+ });
34
+ exports.activityfeedRecipe = (0, core_1.resolveRecipe)(config);
35
+ exports.default = config;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.activityfeedRecipe = exports.ActivityFeedRow = exports.ActivityFeedDiffList = exports.ActivityFeedDiffItem = exports.ActivityFeed = void 0;
4
+ var activityfeed_1 = require("./activityfeed");
5
+ Object.defineProperty(exports, "ActivityFeed", { enumerable: true, get: function () { return activityfeed_1.ActivityFeed; } });
6
+ Object.defineProperty(exports, "ActivityFeedDiffItem", { enumerable: true, get: function () { return activityfeed_1.ActivityFeedDiffItem; } });
7
+ Object.defineProperty(exports, "ActivityFeedDiffList", { enumerable: true, get: function () { return activityfeed_1.ActivityFeedDiffList; } });
8
+ Object.defineProperty(exports, "ActivityFeedRow", { enumerable: true, get: function () { return activityfeed_1.ActivityFeedRow; } });
9
+ var activityfeed_recipe_1 = require("./activityfeed.recipe");
10
+ Object.defineProperty(exports, "activityfeedRecipe", { enumerable: true, get: function () { return activityfeed_recipe_1.activityfeedRecipe; } });
@@ -183,6 +183,14 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
183
183
  // (documented React behaviour), so a plain onFocus/onBlur pair on this
184
184
  // same wrapper sees the portal's contents correctly.
185
185
  const mentionPickerHasFocusRef = React.useRef(false);
186
+ // A caret position `insertMention` just established, to be re-asserted
187
+ // the next time `body` regains focus (#514 fixed the picker stealing
188
+ // focus on OPEN; this is the same "caret resets to offset 0" mechanism
189
+ // on SELECT — see insertMention's own comment for why one synchronous
190
+ // `sel.addRange` isn't enough). One-shot: consumed and cleared by the
191
+ // very next focus or input, whichever comes first, so a stale pointer
192
+ // can never later hijack an unrelated focus.
193
+ const pendingCaretRef = React.useRef(null);
186
194
  const syncEmpty = (el) => {
187
195
  if (el)
188
196
  el.toggleAttribute("data-empty", (el.textContent ?? "") === "");
@@ -494,6 +502,20 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
494
502
  after.collapse(true);
495
503
  sel.removeAllRanges();
496
504
  sel.addRange(after);
505
+ // `select`ing a mention is usually the LAST thing the consumer's
506
+ // picker does before it unmounts (ActorPicker-shaped consumers call
507
+ // `close` right after `select`, same as this file's own doc example).
508
+ // A picker built on a dialog primitive (crisp's own `Command`
509
+ // included) commonly restores focus to whatever had it before the
510
+ // dialog opened as part of ITS OWN unmount cleanup — a plain
511
+ // `.focus()` with no Range of its own, which collapses a
512
+ // contentEditable's caret to offset 0. That cleanup runs after this
513
+ // function returns (the picker only unmounts once `closeMention`
514
+ // below flips `mention` to null and React commits it), so the
515
+ // Selection set two lines up can already be correct here and still
516
+ // get clobbered a tick later. Re-assert it the next time `body`
517
+ // actually regains focus, whenever that turns out to be.
518
+ pendingCaretRef.current = { node: space, offset: 1 };
497
519
  closeMention();
498
520
  syncEmpty(bodyRef.current);
499
521
  emit();
@@ -558,10 +580,31 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
558
580
  } })), (0, jsx_runtime_1.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
559
581
  ? undefined
560
582
  : () => {
583
+ // A real keystroke landing here means `body` already has
584
+ // focus on its own terms — any caret a prior mention
585
+ // selection was waiting to reassert (see insertMention) is
586
+ // moot now and must not later jump the caret backward on
587
+ // some unrelated future focus.
588
+ pendingCaretRef.current = null;
561
589
  syncEmpty(bodyRef.current);
562
590
  updateSlashTrigger();
563
591
  updateMention();
564
592
  emit();
593
+ }, onFocus: readOnly
594
+ ? undefined
595
+ : () => {
596
+ const pending = pendingCaretRef.current;
597
+ pendingCaretRef.current = null;
598
+ if (!pending?.node.isConnected)
599
+ return;
600
+ const sel = window.getSelection();
601
+ if (!sel)
602
+ return;
603
+ const range = document.createRange();
604
+ range.setStart(pending.node, pending.offset);
605
+ range.collapse(true);
606
+ sel.removeAllRanges();
607
+ sel.addRange(range);
565
608
  }, onMouseUp: readOnly
566
609
  ? undefined
567
610
  : () => {
@@ -100,4 +100,18 @@ exports.palette = {
100
100
  // as-is per build/spec-check.mjs's own note), this token exists only for
101
101
  // Badge's soft-neutral text, so darkening it doesn't move any other surface.
102
102
  softNeutralText: { light: "#686870", dark: "#9a9aa2" },
103
+ // Badge's purple/role-pill tone (crisp#506) -- measured live off Attio's
104
+ // own role pill ("Admin", app.attio.com/<ws>/settings/workspace/members):
105
+ // background rgb(245,240,255), text rgb(98,56,181). The ring re-uses a
106
+ // second, independent measurement from the same session (a company's
107
+ // Categories tag pill sharing the identical background, "Broadcasting",
108
+ // app.attio.com/<ws>/companies) at rgb(232,221,254) -- same hue family,
109
+ // cross-validated rather than guessed. Dark mode was NOT measured (no
110
+ // live Attio session was reachable when this tone was added) -- these
111
+ // three light values are reused as-is for dark rather than an invented
112
+ // "brightened" guess (Lesson: don't add unmeasured "robustness"). Needs a
113
+ // real dark-mode measurement before this note can be removed.
114
+ purpleTint: { light: "#f5f0ff", dark: "#f5f0ff" },
115
+ softPurpleText: { light: "#6238b5", dark: "#6238b5" },
116
+ purpleRing: { light: "#e8ddfe", dark: "#e8ddfe" },
103
117
  };
@@ -63,4 +63,9 @@ exports.semantic = {
63
63
  fgWarningSoft: "softWarningText",
64
64
  fgDangerSoft: "softDangerText",
65
65
  fgNeutralSoft: "softNeutralText",
66
+ /** Badge's purple/role-pill tone (crisp#506) — see palette.ts's comment
67
+ * on `purpleTint`/`softPurpleText`/`purpleRing` for the measurement. */
68
+ bgPurpleSoft: "purpleTint",
69
+ fgPurpleSoft: "softPurpleText",
70
+ ringPurpleSoft: "purpleRing",
66
71
  };
@@ -60,6 +60,16 @@ const spec = defineSpec({
60
60
  color: c.fgDangerSoft,
61
61
  boxShadow: ring(c.bgDanger, 30),
62
62
  },
63
+ // crisp#506: a distinct purple used for role-style labels (Attio's own
64
+ // "Admin" role pill), not a status/severity colour -- the other five
65
+ // tones don't cover this hue. Unlike those, these are the measured
66
+ // *solid* values directly (not a color-mix tint of a base accent):
67
+ // Attio's own pill background reads a flat, opaque rgb(), no alpha.
68
+ purple: {
69
+ background: c.bgPurpleSoft,
70
+ color: c.fgPurpleSoft,
71
+ boxShadow: `inset 0 0 0 1px ${c.ringPurpleSoft}`,
72
+ },
63
73
  },
64
74
  size: {
65
75
  small: {
package/dist/esm/index.js CHANGED
@@ -65,6 +65,7 @@ export * from "./core/index.js";
65
65
  // product screens (Sidebar, RecordsTable, RecordPage, Inbox, …). Use them as-is
66
66
  // or as composition references for your own data; `components/` above holds
67
67
  // the generic primitives (Table, DataTable, Button, …) to build your own UI from.
68
+ export * from "./product/activityfeed/index.js";
68
69
  export * from "./product/appshell/index.js";
69
70
  export * from "./product/authshell/index.js";
70
71
  export * from "./product/board/index.js";
@@ -0,0 +1,32 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { activityfeedRecipe } from "./activityfeed.recipe.js";
3
+ /**
4
+ * A record's own event timeline — a vertically stacked, bordered list of
5
+ * `ActivityFeedRow`s, capped at a readable content width. Built for
6
+ * internal/grc's per-record "Activity" tab (crisp#462 SHELL/forms gap): it
7
+ * replaces a hand-rolled `<div className="mx-auto max-w-3xl space-y-4
8
+ * p-6">`. No live Attio re-measurement behind its own chrome this pass (the
9
+ * CDP bridge was unreachable when this was built) — see the recipe's own
10
+ * doc comment for which already-measured crisp values it reuses instead of
11
+ * a new guess.
12
+ */
13
+ export function ActivityFeed({ className, children, ...rest }) {
14
+ return (_jsx("div", { className: activityfeedRecipe({ className }), ...rest, children: children }));
15
+ }
16
+ ActivityFeed.displayName = "ActivityFeed";
17
+ /** One event row: actor + action + timestamp, with an optional diff list
18
+ * underneath. The last row in an `ActivityFeed` draws no divider. */
19
+ export function ActivityFeedRow({ actor, action, when, children, }) {
20
+ return (_jsxs("div", { className: "crisp-activityfeed-row", children: [_jsxs("div", { className: "crisp-activityfeed-row-head", children: [actor, action, _jsx("span", { className: "crisp-activityfeed-row-spacer" }), _jsx("span", { className: "crisp-activityfeed-row-when", children: when })] }), children] }));
21
+ }
22
+ ActivityFeedRow.displayName = "ActivityFeedRow";
23
+ /** One changed-field line under an `ActivityFeedRow` — "Field: before →
24
+ * after", or "Field: set to after" when there is no known prior value. */
25
+ export function ActivityFeedDiffItem({ field, before, after, }) {
26
+ return (_jsxs("li", { className: "crisp-activityfeed-diff-item", children: [_jsx("span", { className: "crisp-activityfeed-diff-field", children: field }), " ", before !== undefined ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-activityfeed-diff-before", children: before }), " ", _jsx("span", { "aria-hidden": "true", children: "\u2192" }), " ", _jsx("span", { children: after })] })) : (_jsx("span", { children: after }))] }));
27
+ }
28
+ ActivityFeedDiffItem.displayName = "ActivityFeedDiffItem";
29
+ export function ActivityFeedDiffList({ children, }) {
30
+ return _jsx("ul", { className: "crisp-activityfeed-diff", children: children });
31
+ }
32
+ ActivityFeedDiffList.displayName = "ActivityFeedDiffList";
@@ -0,0 +1,32 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A record's own event timeline (crisp#462 gap: internal/grc's ActivityTab
4
+ // hand-rolled this as a bare `<div className="mx-auto max-w-3xl space-y-4
5
+ // p-6">` list of bordered rows). No live Attio re-measurement behind these
6
+ // values this pass -- the CDP bridge to the owner's Vivaldi was unreachable
7
+ // when this was built (documented in the component's own doc comment) --
8
+ // so this reuses already-measured crisp values throughout rather than a new
9
+ // guess: `space["6"]` (24px) padding is RecordPanel's own content padding,
10
+ // the row's bottom border reuses `borderSubtle` (the same divider every
11
+ // other bordered row in this codebase draws with), and the 767px content
12
+ // cap matches SettingsSection's own measured content width.
13
+ const config = defineRecipe({
14
+ name: "activityfeed",
15
+ base: `box-sizing: border-box; max-width: 767px; margin-inline: auto; padding: ${c.space["6"]};
16
+ display: flex; flex-direction: column; gap: ${c.space["4"]};
17
+ & .crisp-activityfeed-row { display: flex; flex-direction: column; gap: ${c.space["2"]}; padding-bottom: ${c.space["4"]}; border-bottom: 1px solid ${c.borderSubtle}; }
18
+ & .crisp-activityfeed-row:last-of-type { border-bottom: none; padding-bottom: 0; }
19
+ & .crisp-activityfeed-row-head { display: flex; align-items: center; gap: ${c.space["2"]}; }
20
+ & .crisp-activityfeed-row-spacer { flex: 1; }
21
+ & .crisp-activityfeed-row-when { font-size: ${c.text.xs}; color: ${c.fgMuted}; white-space: nowrap; }
22
+ & .crisp-activityfeed-diff { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: ${c.space["1"]}; }
23
+ & .crisp-activityfeed-diff-item { padding-inline-start: ${c.space["8"]}; font-size: ${c.text.md}; }
24
+ & .crisp-activityfeed-diff-field { color: ${c.fgMuted}; }
25
+ & .crisp-activityfeed-diff-before { color: ${c.fgMuted}; text-decoration: line-through; }
26
+ `,
27
+ variants: {},
28
+ defaultVariants: {},
29
+ compoundVariants: [],
30
+ });
31
+ export const activityfeedRecipe = resolveRecipe(config);
32
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { ActivityFeed, ActivityFeedDiffItem, ActivityFeedDiffList, ActivityFeedRow, } from "./activityfeed.js";
2
+ export { activityfeedRecipe } from "./activityfeed.recipe.js";
@@ -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
  : () => {
@@ -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
  };
@@ -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": {
package/dist/styles.css CHANGED
@@ -45,6 +45,9 @@
45
45
  --crisp-fg-warning-soft: #80520d;
46
46
  --crisp-fg-danger-soft: #a33539;
47
47
  --crisp-fg-neutral-soft: #686870;
48
+ --crisp-bg-purple-soft: #f5f0ff;
49
+ --crisp-fg-purple-soft: #6238b5;
50
+ --crisp-ring-purple-soft: #e8ddfe;
48
51
  }
49
52
 
50
53
  [data-theme="dark"] {
@@ -94,6 +97,9 @@
94
97
  --crisp-fg-warning-soft: #e0982a;
95
98
  --crisp-fg-danger-soft: #ff5a5f;
96
99
  --crisp-fg-neutral-soft: #9a9aa2;
100
+ --crisp-bg-purple-soft: #f5f0ff;
101
+ --crisp-fg-purple-soft: #6238b5;
102
+ --crisp-ring-purple-soft: #e8ddfe;
97
103
  }
98
104
 
99
105
  :root {
@@ -224,6 +230,9 @@
224
230
  .crisp-badge--tone_danger {
225
231
  background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-fg-danger-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
226
232
  }
233
+ .crisp-badge--tone_purple {
234
+ background: var(--crisp-bg-purple-soft); color: var(--crisp-fg-purple-soft); box-shadow: inset 0 0 0 1px var(--crisp-ring-purple-soft);
235
+ }
227
236
  .crisp-badge--size_small {
228
237
  height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
229
238
  }
@@ -1869,6 +1878,19 @@
1869
1878
  & .crisp-wheelpicker-item { height: 44px; min-height: 44px; font-size: 26px; line-height: 35px; }
1870
1879
  }
1871
1880
 
1881
+ .crisp-activityfeed {
1882
+ box-sizing: border-box; max-width: 767px; margin-inline: auto; padding: var(--crisp-space-6); display: flex; flex-direction: column; gap: var(--crisp-space-4);
1883
+ & .crisp-activityfeed-row { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding-bottom: var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
1884
+ & .crisp-activityfeed-row:last-of-type { border-bottom: none; padding-bottom: 0; }
1885
+ & .crisp-activityfeed-row-head { display: flex; align-items: center; gap: var(--crisp-space-2); }
1886
+ & .crisp-activityfeed-row-spacer { flex: 1; }
1887
+ & .crisp-activityfeed-row-when { font-size: var(--crisp-text-xs); color: var(--crisp-fg-muted); white-space: nowrap; }
1888
+ & .crisp-activityfeed-diff { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--crisp-space-1); }
1889
+ & .crisp-activityfeed-diff-item { padding-inline-start: var(--crisp-space-8); font-size: var(--crisp-text-md); }
1890
+ & .crisp-activityfeed-diff-field { color: var(--crisp-fg-muted); }
1891
+ & .crisp-activityfeed-diff-before { color: var(--crisp-fg-muted); text-decoration: line-through; }
1892
+ }
1893
+
1872
1894
  .crisp-appshell {
1873
1895
  display: flex; height: 100%; width: 100%; overflow: hidden; background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1874
1896
  & .crisp-appshell-side { position: relative; flex-shrink: 0; overflow: hidden; }
@@ -45,6 +45,9 @@
45
45
  --crisp-fg-warning-soft: #80520d;
46
46
  --crisp-fg-danger-soft: #a33539;
47
47
  --crisp-fg-neutral-soft: #686870;
48
+ --crisp-bg-purple-soft: #f5f0ff;
49
+ --crisp-fg-purple-soft: #6238b5;
50
+ --crisp-ring-purple-soft: #e8ddfe;
48
51
  }
49
52
 
50
53
  [data-theme="dark"] {
@@ -94,6 +97,9 @@
94
97
  --crisp-fg-warning-soft: #e0982a;
95
98
  --crisp-fg-danger-soft: #ff5a5f;
96
99
  --crisp-fg-neutral-soft: #9a9aa2;
100
+ --crisp-bg-purple-soft: #f5f0ff;
101
+ --crisp-fg-purple-soft: #6238b5;
102
+ --crisp-ring-purple-soft: #e8ddfe;
97
103
  }
98
104
 
99
105
  :root {
@@ -225,6 +231,9 @@
225
231
  .crisp-badge--tone_danger {
226
232
  background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-fg-danger-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
227
233
  }
234
+ .crisp-badge--tone_purple {
235
+ background: var(--crisp-bg-purple-soft); color: var(--crisp-fg-purple-soft); box-shadow: inset 0 0 0 1px var(--crisp-ring-purple-soft);
236
+ }
228
237
  .crisp-badge--size_small {
229
238
  height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
230
239
  }
@@ -1870,6 +1879,19 @@
1870
1879
  & .crisp-wheelpicker-item { height: 44px; min-height: 44px; font-size: 26px; line-height: 35px; }
1871
1880
  }
1872
1881
 
1882
+ .crisp-activityfeed {
1883
+ box-sizing: border-box; max-width: 767px; margin-inline: auto; padding: var(--crisp-space-6); display: flex; flex-direction: column; gap: var(--crisp-space-4);
1884
+ & .crisp-activityfeed-row { display: flex; flex-direction: column; gap: var(--crisp-space-2); padding-bottom: var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
1885
+ & .crisp-activityfeed-row:last-of-type { border-bottom: none; padding-bottom: 0; }
1886
+ & .crisp-activityfeed-row-head { display: flex; align-items: center; gap: var(--crisp-space-2); }
1887
+ & .crisp-activityfeed-row-spacer { flex: 1; }
1888
+ & .crisp-activityfeed-row-when { font-size: var(--crisp-text-xs); color: var(--crisp-fg-muted); white-space: nowrap; }
1889
+ & .crisp-activityfeed-diff { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--crisp-space-1); }
1890
+ & .crisp-activityfeed-diff-item { padding-inline-start: var(--crisp-space-8); font-size: var(--crisp-text-md); }
1891
+ & .crisp-activityfeed-diff-field { color: var(--crisp-fg-muted); }
1892
+ & .crisp-activityfeed-diff-before { color: var(--crisp-fg-muted); text-decoration: line-through; }
1893
+ }
1894
+
1873
1895
  .crisp-appshell {
1874
1896
  display: flex; height: 100%; width: 100%; overflow: hidden; background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1875
1897
  & .crisp-appshell-side { position: relative; flex-shrink: 0; overflow: hidden; }
package/dist/tokens.css CHANGED
@@ -45,6 +45,9 @@
45
45
  --crisp-fg-warning-soft: #80520d;
46
46
  --crisp-fg-danger-soft: #a33539;
47
47
  --crisp-fg-neutral-soft: #686870;
48
+ --crisp-bg-purple-soft: #f5f0ff;
49
+ --crisp-fg-purple-soft: #6238b5;
50
+ --crisp-ring-purple-soft: #e8ddfe;
48
51
  }
49
52
 
50
53
  [data-theme="dark"] {
@@ -94,6 +97,9 @@
94
97
  --crisp-fg-warning-soft: #e0982a;
95
98
  --crisp-fg-danger-soft: #ff5a5f;
96
99
  --crisp-fg-neutral-soft: #9a9aa2;
100
+ --crisp-bg-purple-soft: #f5f0ff;
101
+ --crisp-fg-purple-soft: #6238b5;
102
+ --crisp-ring-purple-soft: #e8ddfe;
97
103
  }
98
104
 
99
105
  :root {
package/dist/tokens.json CHANGED
@@ -46,7 +46,10 @@
46
46
  "fgSuccessSoft": "#186b3d",
47
47
  "fgWarningSoft": "#80520d",
48
48
  "fgDangerSoft": "#a33539",
49
- "fgNeutralSoft": "#686870"
49
+ "fgNeutralSoft": "#686870",
50
+ "bgPurpleSoft": "#f5f0ff",
51
+ "fgPurpleSoft": "#6238b5",
52
+ "ringPurpleSoft": "#e8ddfe"
50
53
  },
51
54
  "dark": {
52
55
  "bgBrandSolid": "#266df0",
@@ -94,7 +97,10 @@
94
97
  "fgSuccessSoft": "#2ea968",
95
98
  "fgWarningSoft": "#e0982a",
96
99
  "fgDangerSoft": "#ff5a5f",
97
- "fgNeutralSoft": "#9a9aa2"
100
+ "fgNeutralSoft": "#9a9aa2",
101
+ "bgPurpleSoft": "#f5f0ff",
102
+ "fgPurpleSoft": "#6238b5",
103
+ "ringPurpleSoft": "#e8ddfe"
98
104
  }
99
105
  },
100
106
  "palette": {
@@ -143,7 +149,10 @@
143
149
  "softSuccessText": "#186b3d",
144
150
  "softWarningText": "#80520d",
145
151
  "softDangerText": "#a33539",
146
- "softNeutralText": "#686870"
152
+ "softNeutralText": "#686870",
153
+ "purpleTint": "#f5f0ff",
154
+ "softPurpleText": "#6238b5",
155
+ "purpleRing": "#e8ddfe"
147
156
  },
148
157
  "dark": {
149
158
  "gray00": "#0a0a0a",
@@ -190,7 +199,10 @@
190
199
  "softSuccessText": "#2ea968",
191
200
  "softWarningText": "#e0982a",
192
201
  "softDangerText": "#ff5a5f",
193
- "softNeutralText": "#9a9aa2"
202
+ "softNeutralText": "#9a9aa2",
203
+ "purpleTint": "#f5f0ff",
204
+ "softPurpleText": "#6238b5",
205
+ "purpleRing": "#e8ddfe"
194
206
  }
195
207
  },
196
208
  "space": {
@@ -13,6 +13,7 @@ export declare const Badge: React.ForwardRefExoticComponent<Omit<React.DetailedH
13
13
  readonly success: string;
14
14
  readonly warning: string;
15
15
  readonly danger: string;
16
+ readonly purple: string;
16
17
  };
17
18
  readonly size: {
18
19
  readonly small: string;
@@ -31,6 +32,7 @@ export declare const Badge: React.ForwardRefExoticComponent<Omit<React.DetailedH
31
32
  readonly success: string;
32
33
  readonly warning: string;
33
34
  readonly danger: string;
35
+ readonly purple: string;
34
36
  };
35
37
  readonly size: {
36
38
  readonly small: string;
@@ -48,6 +48,11 @@ declare const spec: {
48
48
  color: `var(--${string})`;
49
49
  boxShadow: string;
50
50
  };
51
+ readonly purple: {
52
+ readonly background: `var(--${string})`;
53
+ readonly color: `var(--${string})`;
54
+ readonly boxShadow: `inset 0 0 0 1px var(--${string})`;
55
+ };
51
56
  };
52
57
  readonly size: {
53
58
  readonly small: {
@@ -128,6 +133,7 @@ declare const config: import("../..").RecipeConfig<{
128
133
  readonly success: string;
129
134
  readonly warning: string;
130
135
  readonly danger: string;
136
+ readonly purple: string;
131
137
  };
132
138
  readonly size: {
133
139
  readonly small: string;
@@ -146,6 +152,7 @@ export declare const badgeRecipe: import("../..").RecipeFn<{
146
152
  readonly success: string;
147
153
  readonly warning: string;
148
154
  readonly danger: string;
155
+ readonly purple: string;
149
156
  };
150
157
  readonly size: {
151
158
  readonly small: string;
@@ -164,6 +171,7 @@ export declare const badgeRecipe: import("../..").RecipeFn<{
164
171
  readonly success: string;
165
172
  readonly warning: string;
166
173
  readonly danger: string;
174
+ readonly purple: string;
167
175
  };
168
176
  readonly size: {
169
177
  readonly small: string;
@@ -57,6 +57,7 @@ export * from "./components/tooltip";
57
57
  export * from "./components/visuallyhidden";
58
58
  export * from "./components/wheelpicker";
59
59
  export * from "./core";
60
+ export * from "./product/activityfeed";
60
61
  export * from "./product/appshell";
61
62
  export * from "./product/authshell";
62
63
  export * from "./product/board";
@@ -0,0 +1,57 @@
1
+ import type * as React from "react";
2
+ export type ActivityFeedProps = React.ComponentPropsWithoutRef<"div"> & {
3
+ className?: string;
4
+ };
5
+ /**
6
+ * A record's own event timeline — a vertically stacked, bordered list of
7
+ * `ActivityFeedRow`s, capped at a readable content width. Built for
8
+ * internal/grc's per-record "Activity" tab (crisp#462 SHELL/forms gap): it
9
+ * replaces a hand-rolled `<div className="mx-auto max-w-3xl space-y-4
10
+ * p-6">`. No live Attio re-measurement behind its own chrome this pass (the
11
+ * CDP bridge was unreachable when this was built) — see the recipe's own
12
+ * doc comment for which already-measured crisp values it reuses instead of
13
+ * a new guess.
14
+ */
15
+ export declare function ActivityFeed({ className, children, ...rest }: ActivityFeedProps): React.JSX.Element;
16
+ export declare namespace ActivityFeed {
17
+ var displayName: string;
18
+ }
19
+ export type ActivityFeedRowProps = {
20
+ /** Who did it — a chip, an avatar+name, whatever the consumer's identity
21
+ * treatment is. crisp renders no actor chip of its own. */
22
+ actor: React.ReactNode;
23
+ /** What happened — a tag/badge naming the action. */
24
+ action: React.ReactNode;
25
+ /** When — already formatted for display. */
26
+ when: React.ReactNode;
27
+ /** An optional field-level diff list under the row (see
28
+ * `ActivityFeedDiffItem`). */
29
+ children?: React.ReactNode;
30
+ };
31
+ /** One event row: actor + action + timestamp, with an optional diff list
32
+ * underneath. The last row in an `ActivityFeed` draws no divider. */
33
+ export declare function ActivityFeedRow({ actor, action, when, children, }: ActivityFeedRowProps): React.JSX.Element;
34
+ export declare namespace ActivityFeedRow {
35
+ var displayName: string;
36
+ }
37
+ export type ActivityFeedDiffItemProps = {
38
+ /** The changed field's label. */
39
+ field: React.ReactNode;
40
+ /** The prior value, already formatted — omit for a "set to" (no known
41
+ * prior value) row. */
42
+ before?: React.ReactNode;
43
+ /** The new value, already formatted. */
44
+ after: React.ReactNode;
45
+ };
46
+ /** One changed-field line under an `ActivityFeedRow` — "Field: before →
47
+ * after", or "Field: set to after" when there is no known prior value. */
48
+ export declare function ActivityFeedDiffItem({ field, before, after, }: ActivityFeedDiffItemProps): React.JSX.Element;
49
+ export declare namespace ActivityFeedDiffItem {
50
+ var displayName: string;
51
+ }
52
+ export declare function ActivityFeedDiffList({ children, }: {
53
+ children: React.ReactNode;
54
+ }): React.JSX.Element;
55
+ export declare namespace ActivityFeedDiffList {
56
+ var displayName: string;
57
+ }
@@ -0,0 +1,3 @@
1
+ declare const config: import("../..").RecipeConfig<{}>;
2
+ export declare const activityfeedRecipe: import("../..").RecipeFn<{}>;
3
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { ActivityFeed, ActivityFeedDiffItem, type ActivityFeedDiffItemProps, ActivityFeedDiffList, type ActivityFeedProps, ActivityFeedRow, type ActivityFeedRowProps, } from "./activityfeed";
2
+ export { activityfeedRecipe } from "./activityfeed.recipe";
@@ -47,6 +47,9 @@ export declare const contract: MapLeafNodes<{
47
47
  fgWarningSoft: "softWarningText";
48
48
  fgDangerSoft: "softDangerText";
49
49
  fgNeutralSoft: "softNeutralText";
50
+ bgPurpleSoft: "purpleTint";
51
+ fgPurpleSoft: "softPurpleText";
52
+ ringPurpleSoft: "purpleRing";
50
53
  }, `var(--${string})`> & {
51
54
  space: Record<"0.5" | "1" | "1.5" | "2" | "2.5" | "3" | "4" | "5" | "6" | "8" | "px", `var(--${string})`>;
52
55
  radius: Record<"2xl" | "chip" | "full" | "lg" | "md" | "sm" | "xl", `var(--${string})`>;
@@ -179,5 +179,17 @@ export declare const palette: {
179
179
  readonly light: "#686870";
180
180
  readonly dark: "#9a9aa2";
181
181
  };
182
+ readonly purpleTint: {
183
+ readonly light: "#f5f0ff";
184
+ readonly dark: "#f5f0ff";
185
+ };
186
+ readonly softPurpleText: {
187
+ readonly light: "#6238b5";
188
+ readonly dark: "#6238b5";
189
+ };
190
+ readonly purpleRing: {
191
+ readonly light: "#e8ddfe";
192
+ readonly dark: "#e8ddfe";
193
+ };
182
194
  };
183
195
  export type PaletteKey = keyof typeof palette;
@@ -55,5 +55,10 @@ export declare const semantic: {
55
55
  fgWarningSoft: "softWarningText";
56
56
  fgDangerSoft: "softDangerText";
57
57
  fgNeutralSoft: "softNeutralText";
58
+ /** Badge's purple/role-pill tone (crisp#506) — see palette.ts's comment
59
+ * on `purpleTint`/`softPurpleText`/`purpleRing` for the measurement. */
60
+ bgPurpleSoft: "purpleTint";
61
+ fgPurpleSoft: "softPurpleText";
62
+ ringPurpleSoft: "purpleRing";
58
63
  };
59
64
  export type Tokens = typeof semantic;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.54.0",
3
+ "version": "0.54.1",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -323,6 +323,11 @@
323
323
  "import": "./dist/esm/components/wheelpicker/index.js",
324
324
  "require": "./dist/cjs/components/wheelpicker/index.js"
325
325
  },
326
+ "./activityfeed": {
327
+ "types": "./dist/types/product/activityfeed/index.d.ts",
328
+ "import": "./dist/esm/product/activityfeed/index.js",
329
+ "require": "./dist/cjs/product/activityfeed/index.js"
330
+ },
326
331
  "./appshell": {
327
332
  "types": "./dist/types/product/appshell/index.d.ts",
328
333
  "import": "./dist/esm/product/appshell/index.js",