@crisp-ui-kit/crisp 0.54.0 → 0.54.2

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 (127) 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/datatable/datatable.js +2 -1
  9. package/dist/cjs/components/datatable/datatable.recipe.js +4 -0
  10. package/dist/cjs/components/mentionchip/index.js +7 -0
  11. package/dist/cjs/components/mentionchip/mentionchip.js +57 -0
  12. package/dist/cjs/components/mentionchip/mentionchip.recipe.js +38 -0
  13. package/dist/cjs/components/table/table.recipe.js +15 -3
  14. package/dist/cjs/core/floating-stack.js +18 -0
  15. package/dist/cjs/core/index.js +2 -1
  16. package/dist/cjs/index.js +7 -0
  17. package/dist/cjs/product/activityfeed/activityfeed.js +38 -0
  18. package/dist/cjs/product/activityfeed/activityfeed.recipe.js +35 -0
  19. package/dist/cjs/product/activityfeed/index.js +10 -0
  20. package/dist/cjs/product/formdialog/formdialog.js +171 -0
  21. package/dist/cjs/product/formdialog/formdialog.recipe.js +26 -0
  22. package/dist/cjs/product/formdialog/index.js +8 -0
  23. package/dist/cjs/product/importdialog/importdialog.js +134 -0
  24. package/dist/cjs/product/importdialog/importdialog.recipe.js +92 -0
  25. package/dist/cjs/product/importdialog/index.js +18 -0
  26. package/dist/cjs/product/richtext/richtext.js +43 -0
  27. package/dist/cjs/product/savedviews/index.js +7 -0
  28. package/dist/cjs/product/savedviews/savedviews.js +207 -0
  29. package/dist/cjs/product/savedviews/savedviews.recipe.js +103 -0
  30. package/dist/cjs/product/taskstable/index.js +7 -0
  31. package/dist/cjs/product/taskstable/taskstable.js +27 -0
  32. package/dist/cjs/product/taskstable/taskstable.recipe.js +47 -0
  33. package/dist/cjs/product/themepicker/index.js +17 -0
  34. package/dist/cjs/product/themepicker/themepicker.js +32 -0
  35. package/dist/cjs/product/themepicker/themepicker.recipe.js +98 -0
  36. package/dist/cjs/tokens/palette.js +31 -0
  37. package/dist/cjs/tokens/semantic.js +11 -0
  38. package/dist/datatable.css +20 -21
  39. package/dist/datatable.layered.css +20 -21
  40. package/dist/esm/components/badge/badge.recipe.js +10 -0
  41. package/dist/esm/components/datatable/datatable.js +2 -1
  42. package/dist/esm/components/datatable/datatable.recipe.js +4 -0
  43. package/dist/esm/components/mentionchip/index.js +2 -0
  44. package/dist/esm/components/mentionchip/mentionchip.js +21 -0
  45. package/dist/esm/components/mentionchip/mentionchip.recipe.js +35 -0
  46. package/dist/esm/components/table/table.recipe.js +15 -3
  47. package/dist/esm/core/floating-stack.js +17 -0
  48. package/dist/esm/core/index.js +1 -1
  49. package/dist/esm/index.js +7 -0
  50. package/dist/esm/product/activityfeed/activityfeed.js +32 -0
  51. package/dist/esm/product/activityfeed/activityfeed.recipe.js +32 -0
  52. package/dist/esm/product/activityfeed/index.js +2 -0
  53. package/dist/esm/product/formdialog/formdialog.js +134 -0
  54. package/dist/esm/product/formdialog/formdialog.recipe.js +23 -0
  55. package/dist/esm/product/formdialog/index.js +2 -0
  56. package/dist/esm/product/importdialog/importdialog.js +97 -0
  57. package/dist/esm/product/importdialog/importdialog.recipe.js +89 -0
  58. package/dist/esm/product/importdialog/index.js +2 -0
  59. package/dist/esm/product/richtext/richtext.js +43 -0
  60. package/dist/esm/product/savedviews/index.js +2 -0
  61. package/dist/esm/product/savedviews/savedviews.js +171 -0
  62. package/dist/esm/product/savedviews/savedviews.recipe.js +100 -0
  63. package/dist/esm/product/taskstable/index.js +2 -0
  64. package/dist/esm/product/taskstable/taskstable.js +24 -0
  65. package/dist/esm/product/taskstable/taskstable.recipe.js +44 -0
  66. package/dist/esm/product/themepicker/index.js +1 -0
  67. package/dist/esm/product/themepicker/themepicker.js +29 -0
  68. package/dist/esm/product/themepicker/themepicker.recipe.js +95 -0
  69. package/dist/esm/tokens/palette.js +31 -0
  70. package/dist/esm/tokens/semantic.js +11 -0
  71. package/dist/formdialog.css +6 -0
  72. package/dist/formdialog.d.ts +7 -0
  73. package/dist/formdialog.layered.css +8 -0
  74. package/dist/importdialog.css +38 -0
  75. package/dist/importdialog.d.ts +7 -0
  76. package/dist/importdialog.layered.css +40 -0
  77. package/dist/mentionchip.css +3 -0
  78. package/dist/mentionchip.d.ts +7 -0
  79. package/dist/mentionchip.layered.css +5 -0
  80. package/dist/native/tokens.d.ts +1 -1
  81. package/dist/native/tokens.js +24 -4
  82. package/dist/savedviews.css +78 -0
  83. package/dist/savedviews.d.ts +7 -0
  84. package/dist/savedviews.layered.css +80 -0
  85. package/dist/styles.css +242 -27
  86. package/dist/styles.layered.css +242 -27
  87. package/dist/table.css +15 -6
  88. package/dist/table.layered.css +15 -6
  89. package/dist/taskstable.css +23 -0
  90. package/dist/taskstable.d.ts +7 -0
  91. package/dist/taskstable.layered.css +25 -0
  92. package/dist/themepicker.css +27 -0
  93. package/dist/themepicker.d.ts +7 -0
  94. package/dist/themepicker.layered.css +29 -0
  95. package/dist/tokens.css +10 -0
  96. package/dist/tokens.json +24 -4
  97. package/dist/types/components/badge/badge.d.ts +2 -0
  98. package/dist/types/components/badge/badge.recipe.d.ts +8 -0
  99. package/dist/types/components/datatable/datatable.d.ts +12 -1
  100. package/dist/types/components/mentionchip/index.d.ts +2 -0
  101. package/dist/types/components/mentionchip/mentionchip.d.ts +20 -0
  102. package/dist/types/components/mentionchip/mentionchip.recipe.d.ts +18 -0
  103. package/dist/types/core/floating-stack.d.ts +14 -0
  104. package/dist/types/core/index.d.ts +1 -1
  105. package/dist/types/index.d.ts +7 -0
  106. package/dist/types/product/activityfeed/activityfeed.d.ts +57 -0
  107. package/dist/types/product/activityfeed/activityfeed.recipe.d.ts +3 -0
  108. package/dist/types/product/activityfeed/index.d.ts +2 -0
  109. package/dist/types/product/formdialog/formdialog.d.ts +57 -0
  110. package/dist/types/product/formdialog/formdialog.recipe.d.ts +3 -0
  111. package/dist/types/product/formdialog/index.d.ts +2 -0
  112. package/dist/types/product/importdialog/importdialog.d.ts +86 -0
  113. package/dist/types/product/importdialog/importdialog.recipe.d.ts +3 -0
  114. package/dist/types/product/importdialog/index.d.ts +2 -0
  115. package/dist/types/product/savedviews/index.d.ts +2 -0
  116. package/dist/types/product/savedviews/savedviews.d.ts +55 -0
  117. package/dist/types/product/savedviews/savedviews.recipe.d.ts +3 -0
  118. package/dist/types/product/taskstable/index.d.ts +2 -0
  119. package/dist/types/product/taskstable/taskstable.d.ts +54 -0
  120. package/dist/types/product/taskstable/taskstable.recipe.d.ts +3 -0
  121. package/dist/types/product/themepicker/index.d.ts +1 -0
  122. package/dist/types/product/themepicker/themepicker.d.ts +21 -0
  123. package/dist/types/product/themepicker/themepicker.recipe.d.ts +3 -0
  124. package/dist/types/tokens/contract.d.ts +5 -0
  125. package/dist/types/tokens/palette.d.ts +20 -0
  126. package/dist/types/tokens/semantic.d.ts +11 -0
  127. package/package.json +36 -1
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SavedViews = SavedViews;
37
+ const jsx_runtime_1 = require("react/jsx-runtime");
38
+ const lucide_react_1 = require("lucide-react");
39
+ const React = __importStar(require("react"));
40
+ const menu_1 = require("../../components/menu");
41
+ const select_1 = require("../../components/select");
42
+ const core_1 = require("../../core");
43
+ const savedviews_recipe_1 = require("./savedviews.recipe");
44
+ /**
45
+ * A saved-view switcher — measured from Attio's "All Companies ▾" menu. A
46
+ * chip trigger opens a popover listing every saved view as a `role="option"`
47
+ * row (leading coloured icon, name, a hover-revealed kebab of
48
+ * rename/duplicate/delete), plus a trailing "Create new view" row. The
49
+ * active view is marked `aria-selected`/`aria-current="page"` and mirrored
50
+ * onto the trigger's own icon/label.
51
+ *
52
+ * Keyboard: while the trigger has focus and the panel is open, ↑/↓ move a
53
+ * virtual highlight (`aria-activedescendant`, the same "focus stays on the
54
+ * trigger" model `Select`/`MultiSelect` use without a search field), Enter
55
+ * chooses the highlighted row, Escape closes — deferring to a still-open
56
+ * kebab submenu first (`hasOpenFloatingLayer`), so Escape closes one layer
57
+ * at a time.
58
+ */
59
+ function SavedViews({ label, icon, views, activeId, onSelect, onCreate, onRename, onDuplicate, onDelete, onToggleFavorite, createLabel: createLabelProp, renameLabel: renameLabelProp, duplicateLabel: duplicateLabelProp, deleteLabel: deleteLabelProp, favoriteLabel: favoriteLabelProp, viewOptionsLabel: viewOptionsLabelProp, className, "aria-label": ariaLabel, ...rest }) {
60
+ const createLabel = (0, core_1.useCrispMessage)(createLabelProp, "Create new view");
61
+ const renameLabel = (0, core_1.useCrispMessage)(renameLabelProp, "Rename");
62
+ const duplicateLabel = (0, core_1.useCrispMessage)(duplicateLabelProp, "Duplicate");
63
+ const deleteLabel = (0, core_1.useCrispMessage)(deleteLabelProp, "Delete");
64
+ const favoriteLabel = (0, core_1.useCrispMessage)(favoriteLabelProp, "Add to favorites");
65
+ const viewOptionsLabel = (0, core_1.useCrispMessage)(viewOptionsLabelProp, "View options");
66
+ const autoId = React.useId();
67
+ const triggerId = `${autoId}-trigger`;
68
+ // role="combobox" (below) does not take its name from content — unlike a
69
+ // plain button, aria-activedescendant is only valid on a widget role that
70
+ // supports it (Select's/MultiSelect's own !searchable trigger is the same
71
+ // shape). aria-labelledby, resolved recursively, DOES read a referenced
72
+ // node's own text, so pointing it at the visible label span gives the
73
+ // trigger a name equal to what's on screen without a second, redundant
74
+ // aria-label prop — self-referencing (aria-labelledby={triggerId} on the
75
+ // trigger itself) does not work, since the referenced node's own
76
+ // name-from-content restriction still applies to it.
77
+ const triggerLabelId = `${autoId}-trigger-label`;
78
+ const panelId = `${autoId}-panel`;
79
+ const optionId = (i) => `${autoId}-opt-${i}`;
80
+ const [open, setOpen] = React.useState(false);
81
+ const [up, setUp] = React.useState(false);
82
+ const [pos, setPos] = React.useState({});
83
+ const [highlight, setHighlight] = React.useState(-1);
84
+ const rootRef = React.useRef(null);
85
+ const triggerRef = React.useRef(null);
86
+ const panelRef = React.useRef(null);
87
+ const activeView = views.find((v) => v.id === activeId) ?? null;
88
+ // the trailing "Create new view" row is one more enabled index, past every
89
+ // real view
90
+ const createIndex = views.length;
91
+ const openPanel = () => {
92
+ const rect = triggerRef.current?.getBoundingClientRect();
93
+ if (rect) {
94
+ const { style, up: flipUp } = (0, core_1.placeFloating)({
95
+ rect,
96
+ gap: 4,
97
+ flipBelow: 280,
98
+ anchor: triggerRef.current,
99
+ });
100
+ setUp(flipUp);
101
+ setPos(style);
102
+ }
103
+ setHighlight(-1);
104
+ setOpen(true);
105
+ };
106
+ const close = React.useCallback(() => {
107
+ setOpen(false);
108
+ triggerRef.current?.focus({ preventScroll: true });
109
+ }, []);
110
+ const commit = () => {
111
+ if (highlight === createIndex) {
112
+ onCreate();
113
+ close();
114
+ }
115
+ else {
116
+ const v = views[highlight];
117
+ if (v) {
118
+ onSelect(v.id);
119
+ close();
120
+ }
121
+ }
122
+ };
123
+ const { onKeyDown: onNavKeys } = (0, select_1.useListboxKeyboard)({
124
+ enabledIndexes: Array.from({ length: views.length + 1 }, (_, i) => i),
125
+ highlight,
126
+ setHighlight,
127
+ commit,
128
+ close,
129
+ dismiss: () => setOpen(false),
130
+ });
131
+ // Registers the portaled panel on the shared floating-layer stack (#339)
132
+ // so a kebab's own Menu (also portaled, also registered) counts as
133
+ // "inside" this layer for outside-click purposes, and so this panel's own
134
+ // Escape can defer to it below.
135
+ React.useEffect(() => {
136
+ if (!open || !panelRef.current)
137
+ return;
138
+ return (0, core_1.registerFloatingLayer)(panelRef.current);
139
+ }, [open]);
140
+ React.useEffect(() => {
141
+ if (!open)
142
+ return;
143
+ const onDown = (e) => {
144
+ const target = e.target;
145
+ if (rootRef.current?.contains(target))
146
+ return;
147
+ if (panelRef.current?.contains(target))
148
+ return;
149
+ if ((0, core_1.isInsideLaterFloatingLayer)(target, panelRef.current))
150
+ return;
151
+ setOpen(false);
152
+ };
153
+ document.addEventListener("pointerdown", onDown);
154
+ return () => document.removeEventListener("pointerdown", onDown);
155
+ }, [open]);
156
+ // Escape defers to a still-open kebab submenu (core/floating-stack.ts):
157
+ // without this, both this panel's listener AND the kebab Menu's own
158
+ // Escape listener fire on the same keydown and both close at once, which
159
+ // is the double-close bug that module's doc comment warns about. One
160
+ // Escape now closes the kebab; a second closes the panel.
161
+ //
162
+ // `hasNestedFloatingLayer`, not `hasOpenFloatingLayer`: this panel
163
+ // registers ITSELF on the stack (below), so a plain "is the stack
164
+ // non-empty" check would always be true the moment this panel is open and
165
+ // Escape would never reach `close()` — `hasNestedFloatingLayer` excludes
166
+ // this panel's own slot and only counts a layer registered after it.
167
+ React.useEffect(() => {
168
+ if (!open)
169
+ return;
170
+ const onKey = (e) => {
171
+ if (e.key !== "Escape")
172
+ return;
173
+ if ((0, core_1.hasNestedFloatingLayer)(panelRef.current))
174
+ return;
175
+ close();
176
+ };
177
+ document.addEventListener("keydown", onKey);
178
+ return () => document.removeEventListener("keydown", onKey);
179
+ }, [open, close]);
180
+ const triggerIcon = activeView ? activeView.icon : icon;
181
+ const triggerLabel = activeView ? activeView.name : label;
182
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, savedviews_recipe_1.savedviewsRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, id: triggerId, type: "button",
183
+ // combobox, not the bare default "button" role: aria-activedescendant
184
+ // (the virtual-focus highlight, same model Select/MultiSelect use
185
+ // without a search field) is only valid on a widget role that
186
+ // supports it, and combobox's aria-haspopup explicitly allows
187
+ // "dialog" — the value Attio's own trigger measures — alongside the
188
+ // more common "listbox".
189
+ role: "combobox", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? panelId : undefined, "aria-activedescendant": open && highlight >= 0 ? optionId(highlight) : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerLabelId, "data-open": open ? "" : undefined, className: "crisp-savedviews-trigger", onClick: () => (open ? setOpen(false) : openPanel()), onKeyDown: (e) => {
190
+ if (open)
191
+ onNavKeys(e);
192
+ else if (e.key === "ArrowDown" || e.key === "Enter") {
193
+ e.preventDefault();
194
+ openPanel();
195
+ }
196
+ }, children: [triggerIcon != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-savedviews-trigger-icon", children: triggerIcon })), (0, jsx_runtime_1.jsx)("span", { id: triggerLabelId, className: "crisp-savedviews-trigger-label", children: triggerLabel }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-savedviews-trigger-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsxs)("div", { ref: panelRef, id: panelId, role: "listbox", "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerLabelId, className: "crisp-savedviews-panel", "data-up": up ? "" : undefined, style: pos, children: [views.map((v, i) => {
197
+ const selected = v.id === activeId;
198
+ return ((0, jsx_runtime_1.jsxs)("div", { id: optionId(i), role: "option", "aria-selected": selected, "aria-current": selected ? "page" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-savedviews-row", onMouseEnter: () => setHighlight(i), onClick: () => {
199
+ onSelect(v.id);
200
+ close();
201
+ }, children: [v.icon != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-savedviews-row-icon", children: v.icon })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-savedviews-row-label", children: v.name }), v.shared && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-savedviews-row-shared", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Users2, { size: 12, strokeWidth: 1.75 }) })), (0, jsx_runtime_1.jsx)("div", { className: "crisp-savedviews-kebab-slot", onClick: (e) => e.stopPropagation(), children: (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-savedviews-kebab", "aria-label": viewOptionsLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 14, strokeWidth: 1.75 }) }), children: [onToggleFavorite && ((0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => onToggleFavorite(v.id), children: favoriteLabel })), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => onRename(v.id), children: renameLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => onDuplicate(v.id), children: duplicateLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { danger: true, onSelect: () => onDelete(v.id), children: deleteLabel })] }) })] }, v.id));
202
+ }), (0, jsx_runtime_1.jsxs)("div", { id: optionId(createIndex), role: "option", "aria-selected": false, "data-highlighted": highlight === createIndex ? "" : undefined, className: "crisp-savedviews-row", onMouseEnter: () => setHighlight(createIndex), onClick: () => {
203
+ onCreate();
204
+ close();
205
+ }, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-savedviews-row-icon", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 16, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-savedviews-row-label", children: createLabel })] })] }) }))] }));
206
+ }
207
+ SavedViews.displayName = "SavedViews";
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.savedviewsRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A saved-view switcher — measured live from Attio's own "All Companies ▾"
7
+ // trigger (app.attio.com/techpartners/companies): a 28px chip opens a
8
+ // popover of 32px `role="option"` rows, each revealing a 20px kebab on
9
+ // hover that opens a second, identical-chrome popover of rename/duplicate/
10
+ // delete actions. The trigger stays un-portaled (`&`-nested); the panel and
11
+ // its rows portal to document.body (#339), so they live in `globalCss`, not
12
+ // nested under `&` — the same split Menu/MultiSelect use and for the same
13
+ // reason: a descendant selector needs real DOM containment, which the
14
+ // portal breaks.
15
+ //
16
+ // Panel chrome (radius 12 / 4px padding / elevation.floating) and row height
17
+ // (32px) are the exact values Menu's own `crisp-menu-content`/`crisp-menu-item`
18
+ // already carry — reused by reference (radius.xl, space["1"], size.menuRow,
19
+ // elevation.floating), not re-measured, since it is the same overlay
20
+ // surface. The panel's measured 308.75×182 size is deliberately NOT
21
+ // hardcoded — it grows with the number of views — so width/height are left
22
+ // to content, with the same 200px floor Menu's own popover uses.
23
+ const config = (0, core_1.defineRecipe)({
24
+ name: "savedviews",
25
+ base: `
26
+ position: relative; display: inline-flex; font-family: ${tokens_1.contract.font.sans};
27
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
28
+
29
+ /* Trigger fill measured rgb(246,247,247); bgRaised's light value is
30
+ #f4f4f5 (244,244,245) — a 2-unit-per-channel difference, inside
31
+ screenshot/anti-aliasing noise, not a second real colour. Reusing the
32
+ token (Lesson 3 — tokens, not raw px/hex) gets a correct,
33
+ already-established dark-mode fill for free instead of a light-only
34
+ literal; AUTHORING.md Lesson 29 is the case where that shortcut went
35
+ wrong (a flat colour that stayed visible where the reference had
36
+ none) — this is the opposite risk (a token that already flips per
37
+ theme, applied to the closest measured match). */
38
+ & .crisp-savedviews-trigger {
39
+ box-sizing: border-box; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]};
40
+ height: ${tokens_1.contract.size.row}; margin: 0; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["1.5"]};
41
+ border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgRaised};
42
+ font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary};
43
+ cursor: pointer; white-space: nowrap; transition: background-color .12s ease;
44
+ }
45
+ & .crisp-savedviews-trigger:hover,
46
+ & .crisp-savedviews-trigger[data-open] { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, ${tokens_1.contract.bgRaised}); }
47
+ & .crisp-savedviews-trigger-icon { flex-shrink: 0; display: inline-flex; width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; }
48
+ & .crisp-savedviews-trigger-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
49
+ & .crisp-savedviews-trigger-chevron { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
50
+ `,
51
+ globalCss: `
52
+ /* pointer-events: auto (#375) — see Menu/MultiSelect's matching comment:
53
+ a modal Dialog sets pointer-events: none on document.body, and a
54
+ portaled layer outside Zag's knowledge inherits that lock. */
55
+ .crisp-savedviews-panel {
56
+ font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
57
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
58
+ min-width: 200px; max-height: 360px; overflow-y: auto; padding: ${tokens_1.contract.space["1"]};
59
+ border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover};
60
+ box-shadow: ${tokens_1.elevation.floating};
61
+ transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
62
+ }
63
+ .crisp-savedviews-panel[data-up] { transform-origin: bottom left; }
64
+ .crisp-savedviews-panel { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
65
+
66
+ .crisp-savedviews-row {
67
+ box-sizing: border-box; position: relative; display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]};
68
+ height: ${tokens_1.contract.size.menuRow}; flex-shrink: 0; padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]};
69
+ border-radius: ${tokens_1.contract.radius.md}; cursor: pointer; -webkit-user-select: none; user-select: none;
70
+ }
71
+ .crisp-savedviews-row:hover, .crisp-savedviews-row[data-highlighted] { background: ${tokens_1.contract.bgRaised}; }
72
+ .crisp-savedviews-row-icon { flex-shrink: 0; display: inline-flex; width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; color: ${tokens_1.contract.fgMuted}; }
73
+ .crisp-savedviews-row-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; }
74
+ .crisp-savedviews-row[aria-selected="true"] .crisp-savedviews-row-label { font-weight: ${tokens_1.contract.weight.semibold}; }
75
+ .crisp-savedviews-row-shared { flex-shrink: 0; display: inline-flex; color: ${tokens_1.contract.fgMuted}; }
76
+
77
+ /* The kebab's own <Menu> is wrapped in a slot div (savedviews.tsx) that
78
+ stops a click from bubbling — through the REACT tree, which a portal
79
+ does not leave, even though the Menu's content is a DOM sibling under
80
+ document.body — into the row's own onClick. The slot, not the kebab
81
+ button itself, is what the row's flex layout pushes to the trailing
82
+ edge. */
83
+ .crisp-savedviews-kebab-slot { flex-shrink: 0; margin-inline-start: auto; }
84
+ /* 20px / 4px padding is a component-intrinsic size (AUTHORING.md §4b —
85
+ "a badge's 20px" is the named example), used once here: raw, not a
86
+ scale step. */
87
+ .crisp-savedviews-kebab {
88
+ box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
89
+ width: 20px; height: 20px; padding: ${tokens_1.contract.space["1"]}; margin: 0;
90
+ border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; color: ${tokens_1.contract.fgMuted}; cursor: pointer;
91
+ opacity: 0; transition: opacity .1s ease, background-color .12s ease, color .12s ease;
92
+ }
93
+ .crisp-savedviews-row:hover .crisp-savedviews-kebab,
94
+ .crisp-savedviews-row:focus-within .crisp-savedviews-kebab,
95
+ .crisp-savedviews-kebab[data-open] { opacity: 1; }
96
+ .crisp-savedviews-kebab:hover, .crisp-savedviews-kebab[data-open] { background: ${tokens_1.contract.bgSurface}; color: ${tokens_1.contract.fgPrimary}; }
97
+ `,
98
+ variants: {},
99
+ defaultVariants: {},
100
+ compoundVariants: [],
101
+ });
102
+ exports.savedviewsRecipe = (0, core_1.resolveRecipe)(config);
103
+ exports.default = config;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskstableRecipe = exports.TasksTable = void 0;
4
+ var taskstable_1 = require("./taskstable");
5
+ Object.defineProperty(exports, "TasksTable", { enumerable: true, get: function () { return taskstable_1.TasksTable; } });
6
+ var taskstable_recipe_1 = require("./taskstable.recipe");
7
+ Object.defineProperty(exports, "taskstableRecipe", { enumerable: true, get: function () { return taskstable_recipe_1.taskstableRecipe; } });
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TasksTable = TasksTable;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const core_1 = require("../../core");
6
+ const listpage_1 = require("../listpage");
7
+ const taskstable_recipe_1 = require("./taskstable.recipe");
8
+ function Cell({ flex, className, children, }) {
9
+ return ((0, jsx_runtime_1.jsx)("span", { className: (0, core_1.cx)("crisp-taskstable-cell", className), style: { flex }, children: children }));
10
+ }
11
+ /**
12
+ * A grouped due-date table pattern — Attio's Tasks page. Composes
13
+ * `ListPage`'s toolbar and group-row chrome (both class-compatible; see
14
+ * taskstable.recipe.ts) with a typed column/cell layer: a title+checkbox
15
+ * row grouped into due-date sections, a due-date cell that goes red past
16
+ * due, and consumer-supplied record/assignee chip slots.
17
+ */
18
+ function TasksTable({ columns, groups, leading, trailing, emptyLabel: emptyLabelProp, completeLabel: completeLabelProp, incompleteLabel: incompleteLabelProp, className, ...rest }) {
19
+ const crispMessages = (0, core_1.useCrispMessages)();
20
+ const emptyLabel = emptyLabelProp ?? crispMessages["No tasks"] ?? "No tasks";
21
+ const completeLabel = (0, core_1.useCrispMessage)(completeLabelProp, "Mark complete");
22
+ const incompleteLabel = (0, core_1.useCrispMessage)(incompleteLabelProp, "Mark incomplete");
23
+ const flexOf = (key) => columns.find((col) => col.key === key)?.flex ?? 1;
24
+ const total = groups.reduce((s, g) => s + g.rows.length, 0);
25
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, core_1.cx)((0, taskstable_recipe_1.taskstableRecipe)({ className }), (0, listpage_1.listpageRecipe)()), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-listpage-toolbar", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-toolbar-lead", children: leading }), trailing != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-toolbar-trail", children: trailing }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-listpage-body", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-taskstable-colhead", children: columns.map((col) => ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-taskstable-colhead-cell", style: { flex: col.flex ?? 1 }, children: [col.icon, col.label] }, col.key))) }), total === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-taskstable-empty", children: emptyLabel })) : (groups.map((g) => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-listpage-group", children: [g.label, (0, jsx_runtime_1.jsx)("span", { className: "crisp-listpage-group-count", children: g.count })] }), g.rows.map((row) => ((0, jsx_runtime_1.jsx)(listpage_1.ListRow, { children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-taskstable-row", style: { flex: 1 }, children: [(0, jsx_runtime_1.jsxs)(Cell, { flex: flexOf("title"), children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", className: "crisp-taskstable-check", checked: !!row.done, "aria-label": row.done ? incompleteLabel : completeLabel, onChange: () => row.onToggle?.(!row.done) }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-taskstable-title", children: row.title })] }), (0, jsx_runtime_1.jsx)(Cell, { flex: flexOf("due"), children: row.due != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-taskstable-due", "data-overdue": row.due.overdue ? "" : undefined, children: row.due.label })) }), (0, jsx_runtime_1.jsx)(Cell, { flex: flexOf("record"), children: row.record }), (0, jsx_runtime_1.jsx)(Cell, { flex: flexOf("assignee"), children: row.assignee })] }) }, row.id)))] }, g.id))))] })] }));
26
+ }
27
+ TasksTable.displayName = "TasksTable";
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskstableRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A grouped due-date table — Attio's Tasks page (app.attio.com/<ws>/tasks),
7
+ // measured live and recorded on crisp#462. Composes ListPage's own toolbar
8
+ // (`.crisp-listpage-toolbar`) and group-row (`.crisp-listpage-group`) chrome
9
+ // by carrying BOTH this recipe's root class and `listpageRecipe`'s root class
10
+ // on the same element (see taskstable.tsx) -- both recipes' descendant rules
11
+ // nest under their own root class, so this is a safe, ordinary composition,
12
+ // the same way `<ListRow>` already lets a caller compose into `ListPage`'s
13
+ // group bodies. Real deviation: ListPage's own `columns` colhead slot is
14
+ // pinned to `c.size.menuRow` (32px, correct for its Reports/Sequences
15
+ // reference surfaces per #479) but Tasks/Notes independently measure a 49px
16
+ // column-header row (#462) -- ListPage's colhead can't be asked to render at
17
+ // a different height per consumer today, so this component renders its own
18
+ // colhead instead of using that slot. Flagged in the PR, not fixed here (out
19
+ // of scope: don't change ListPage for this component).
20
+ const config = (0, core_1.defineRecipe)({
21
+ name: "taskstable",
22
+ base: `display: flex; flex-direction: column; min-height: 0; box-sizing: border-box; font-family: ${tokens_1.contract.font.sans}; color: ${tokens_1.contract.fgPrimary};
23
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
24
+
25
+ & .crisp-taskstable-colhead { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-height: 49px; max-height: 49px; padding: ${tokens_1.contract.space["2.5"]} ${tokens_1.contract.space["3"]}; box-shadow: inset 0 -1px 0 ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; }
26
+ & .crisp-taskstable-colhead-cell { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; }
27
+
28
+ & .crisp-taskstable-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; }
29
+ & .crisp-taskstable-cell { display: flex; align-items: center; min-width: 0; }
30
+
31
+ & .crisp-taskstable-check { appearance: none; -webkit-appearance: none; box-sizing: border-box; flex-shrink: 0; margin: 0; width: 16px; height: 16px; border: none; border-radius: ${tokens_1.contract.radius.full}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 9%, transparent); cursor: pointer; transition: background-color .12s, box-shadow .12s; }
32
+ & .crisp-taskstable-check:hover { box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 30%, transparent); }
33
+ & .crisp-taskstable-check:checked { background: ${tokens_1.contract.bgBrandSolid} center / 10px no-repeat; box-shadow: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 6.5 5 9l4.5-5'/%3E%3C/svg%3E"); }
34
+ & .crisp-taskstable-check:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 1px; }
35
+
36
+ & .crisp-taskstable-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; }
37
+
38
+ & .crisp-taskstable-due { font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgMuted}; white-space: nowrap; }
39
+ & .crisp-taskstable-due[data-overdue] { color: ${tokens_1.contract.fgDanger}; }
40
+
41
+ & .crisp-taskstable-empty { padding: ${tokens_1.contract.space["8"]} ${tokens_1.contract.space["4"]}; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }`,
42
+ variants: {},
43
+ defaultVariants: {},
44
+ compoundVariants: [],
45
+ });
46
+ exports.taskstableRecipe = (0, core_1.resolveRecipe)(config);
47
+ exports.default = config;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./themepicker"), exports);
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ThemePicker = ThemePicker;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const radiogroup_1 = require("../../components/radiogroup");
6
+ const core_1 = require("../../core");
7
+ const themepicker_recipe_1 = require("./themepicker.recipe");
8
+ const OPTIONS = ["light", "dark", "system"];
9
+ function PreviewSwatch({ swatch }) {
10
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-themepicker-preview", "data-swatch": swatch, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-themepicker-preview-side", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-themepicker-preview-main", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-themepicker-preview-bar" })] })] }));
11
+ }
12
+ /**
13
+ * A light/dark/system theme picker — three cards, each a small preview of
14
+ * what that option looks like, radio-selectable. See the recipe's own doc
15
+ * comment for the live Attio measurement (crisp#462 gap 4) and for why the
16
+ * preview swatch colours are literal rather than `var(--crisp-*)` tokens.
17
+ */
18
+ function ThemePicker({ value, onChange, labels, className, ...rest }) {
19
+ const lightLabel = (0, core_1.useCrispMessage)(labels?.light, "Light");
20
+ const darkLabel = (0, core_1.useCrispMessage)(labels?.dark, "Dark");
21
+ const systemLabel = (0, core_1.useCrispMessage)(labels?.system, "System");
22
+ const labelFor = {
23
+ light: lightLabel,
24
+ dark: darkLabel,
25
+ system: systemLabel,
26
+ };
27
+ const { onKeyDown, groupRef } = (0, radiogroup_1.useRadioGroupKeyboard)({
28
+ select: (v) => onChange(v),
29
+ });
30
+ return ((0, jsx_runtime_1.jsx)("div", { ref: groupRef, className: (0, themepicker_recipe_1.themepickerRecipe)({ className }), role: "radiogroup", onKeyDown: onKeyDown, ...rest, children: OPTIONS.map((option) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "radio", "aria-checked": value === option, "data-value": option, className: "crisp-themepicker-option", "data-selected": value === option ? "" : undefined, onClick: () => onChange(option), children: [(0, jsx_runtime_1.jsx)(PreviewSwatch, { swatch: option }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-themepicker-label", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-themepicker-radio", "aria-hidden": "true" }), labelFor[option]] })] }, option))) }));
31
+ }
32
+ ThemePicker.displayName = "ThemePicker";
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.themepickerRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A theme picker (crisp#571) — measured live at
7
+ // app.attio.com/<ws>/settings/appearance (Settings -> Appearance -> Theme,
8
+ // its "Light" / "Dark" / "System" tiles):
9
+ // each option is a 224x152 card, 16px radius, on a flex row with an 8px
10
+ // gap. Selected carries a 1px solid brand-blue border (theme-constant
11
+ // #266df0, matches `bgBrandSolid`); unselected carries the ordinary
12
+ // hairline (`borderSubtle`). Inside, a ~214x114 mock-UI preview card sits
13
+ // on the same 12px radius / 1px hairline chrome the real Sidebar itself
14
+ // uses, with a label row below (a radio dot + 14px/500 text, `fgPrimary`
15
+ // when selected, muted when not).
16
+ //
17
+ // The preview card's OWN colours are deliberately literal, not `var(--crisp-*)`
18
+ // tokens: they must show what "Light"/"Dark" look like regardless of the
19
+ // picker's own ambient theme (a dark-mode app still needs its "Light" tile
20
+ // to render light), and crisp's token system only has one live value per
21
+ // custom property at a time -- there's no way to force a light subtree
22
+ // inside a dark ancestor without one. This is also how Attio's own tiles
23
+ // behave: their preview is a fixed illustration asset, not theme-reactive
24
+ // markup (confirmed by reading the SVG's own baked-in fills while the app
25
+ // itself was in light mode: the "Dark" tile's fills were already dark).
26
+ // The literals below are simply the EXISTING dual-theme tokens' own two
27
+ // values, spelled out per swatch rather than invented fresh: bgSidebar
28
+ // (#fbfbfb / #0e0f10), gray00/bgSurface (#ffffff / #0a0a0a), borderSubtle
29
+ // (#eeeff1 / #1c1d1f) -- each matches what Attio's own SVG measured
30
+ // (#0B0C0D-#101112 range for backgrounds, #1B1C1F for dividers) closely
31
+ // enough to treat as the same token rather than a fresh, barely-different
32
+ // one. `bgBrandSolid` (#266df0) IS referenced live since it's already
33
+ // theme-constant.
34
+ //
35
+ // "System" mirrors the OS's own effective scheme via `prefers-color-scheme`,
36
+ // not a guessed split illustration: this session's own System tile
37
+ // rendered with the SAME fills as the Light tile while the OS/browser
38
+ // reported light -- reads as "follows the OS", not a fixed half-and-half
39
+ // image there's no other evidence for.
40
+ //
41
+ // The mock-UI content inside each preview card (Attio's own hand-drawn
42
+ // sidebar/content wireframe SVG) is NOT reproduced pixel-for-pixel here --
43
+ // that illustration is decorative, not a measured design value, so this
44
+ // recipe draws a simplified schematic (a sidebar block + a few content
45
+ // bars) instead of a bespoke SVG asset.
46
+ //
47
+ // Attio's unselected label reads a black-alpha `rgba(0,0,0,0.63)`, the
48
+ // same pattern SettingsSection's own subtitle measured (crisp#522) and
49
+ // left unresolved for the same reason: no confirmed dark-mode value yet.
50
+ // `fgMuted` is used here as the closest existing (and already dual-theme)
51
+ // token rather than repeating an unmeasured-in-dark literal.
52
+ const config = (0, core_1.defineRecipe)({
53
+ name: "themepicker",
54
+ base: `display: flex; gap: ${tokens_1.contract.space["2"]}; font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
55
+ & .crisp-themepicker-option { box-sizing: border-box; display: flex; flex-direction: column; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; width: 224px; padding: ${tokens_1.contract.space["1"]}; border: 1px solid ${tokens_1.contract.borderSubtle}; border-radius: 16px; background: ${tokens_1.contract.bgSurface}; cursor: pointer; font-family: inherit; }
56
+ & .crisp-themepicker-option[data-selected] { border-color: ${tokens_1.contract.bgBrandSolid}; }
57
+
58
+ & .crisp-themepicker-preview { box-sizing: border-box; display: flex; width: 100%; height: 114px; padding: ${tokens_1.contract.space["2"]}; gap: ${tokens_1.contract.space["1.5"]}; border-radius: ${tokens_1.contract.radius.xl}; overflow: hidden; }
59
+ & .crisp-themepicker-preview-side { flex-shrink: 0; width: 28%; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["1"]}; }
60
+ & .crisp-themepicker-preview-bar { height: 4px; border-radius: ${tokens_1.contract.radius.full}; }
61
+ & .crisp-themepicker-preview-bar:nth-child(1) { width: 70%; background: ${tokens_1.contract.bgBrandSolid}; }
62
+ & .crisp-themepicker-preview-bar:nth-child(2) { width: 90%; }
63
+ & .crisp-themepicker-preview-bar:nth-child(3) { width: 60%; }
64
+ & .crisp-themepicker-preview-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["1.5"]}; padding: ${tokens_1.contract.space["1.5"]}; border-radius: ${tokens_1.contract.radius.md}; }
65
+
66
+ /* Light / System (default) swatch colours. */
67
+ & .crisp-themepicker-preview[data-swatch="light"],
68
+ & .crisp-themepicker-preview[data-swatch="system"] { border: 1px solid #eeeff1; background: #fbfbfb; }
69
+ & [data-swatch="light"] .crisp-themepicker-preview-bar:not(:nth-child(1)),
70
+ & [data-swatch="system"] .crisp-themepicker-preview-bar:not(:nth-child(1)) { background: #eeeff1; }
71
+ & [data-swatch="light"] .crisp-themepicker-preview-main,
72
+ & [data-swatch="system"] .crisp-themepicker-preview-main { background: #ffffff; }
73
+ & [data-swatch="light"] .crisp-themepicker-preview-main .crisp-themepicker-preview-bar,
74
+ & [data-swatch="system"] .crisp-themepicker-preview-main .crisp-themepicker-preview-bar { background: #eeeff1; }
75
+
76
+ /* Dark swatch colours -- always for "dark", and for "system" only
77
+ when the OS itself reports dark. */
78
+ & .crisp-themepicker-preview[data-swatch="dark"] { border: 1px solid #1c1d1f; background: #0e0f10; }
79
+ & [data-swatch="dark"] .crisp-themepicker-preview-bar:not(:nth-child(1)) { background: #1c1d1f; }
80
+ & [data-swatch="dark"] .crisp-themepicker-preview-main { background: #0a0a0a; }
81
+ & [data-swatch="dark"] .crisp-themepicker-preview-main .crisp-themepicker-preview-bar { background: #1c1d1f; }
82
+ @media (prefers-color-scheme: dark) {
83
+ & .crisp-themepicker-preview[data-swatch="system"] { border: 1px solid #1c1d1f; background: #0e0f10; }
84
+ & [data-swatch="system"] .crisp-themepicker-preview-bar:not(:nth-child(1)) { background: #1c1d1f; }
85
+ & [data-swatch="system"] .crisp-themepicker-preview-main { background: #0a0a0a; }
86
+ & [data-swatch="system"] .crisp-themepicker-preview-main .crisp-themepicker-preview-bar { background: #1c1d1f; }
87
+ }
88
+
89
+ & .crisp-themepicker-label { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
90
+ & .crisp-themepicker-option[data-selected] .crisp-themepicker-label { color: ${tokens_1.contract.fgPrimary}; }
91
+ & .crisp-themepicker-radio { flex-shrink: 0; width: 14px; height: 14px; border-radius: ${tokens_1.contract.radius.full}; border: 1px solid ${tokens_1.contract.borderDefault}; box-sizing: border-box; }
92
+ & .crisp-themepicker-option[data-selected] .crisp-themepicker-radio { border: 4px solid ${tokens_1.contract.bgBrandSolid}; }`,
93
+ variants: {},
94
+ defaultVariants: {},
95
+ compoundVariants: [],
96
+ });
97
+ exports.themepickerRecipe = (0, core_1.resolveRecipe)(config);
98
+ exports.default = config;
@@ -12,6 +12,14 @@ exports.palette = {
12
12
  gray900: { light: "#101112", dark: "#f5f5f5" },
13
13
  line200: { light: "#e4e4e7", dark: "#212224" },
14
14
  line100: { light: "#eeeff1", dark: "#1c1d1f" },
15
+ // ImportDialog's dropzone border — measured live from Attio's Import CSV
16
+ // flow (`rgb(230,231,234)`), distinct from `line200`/`line100` (#e4e4e7 /
17
+ // #eeeff1 are close but not equal, and AUTHORING's dimensional-scale rule
18
+ // against rounding a measurement to fit an existing token applies to color
19
+ // too). Dark value not independently re-measured (Attio's dark theme is
20
+ // off-limits to toggle, same restriction as `ringField`/`ringSwitch`) —
21
+ // reuses `line200`'s dark line colour rather than inventing a new one.
22
+ line150: { light: "#e6e7ea", dark: "#212224" },
15
23
  ringNeutral: { light: "transparent", dark: "#212224" },
16
24
  // Text-entry fields (Input, Textarea) — measured live on app.attio.com's
17
25
  // Settings page, a real typed field: `inset 0 0 0 1px rgba(0,0,0,.06)`,
@@ -52,6 +60,15 @@ exports.palette = {
52
60
  // #EB3B3B it uses for destructive menu TEXT. Does not flip: a filled
53
61
  // destructive control needs the same white-bearing fill in both themes.
54
62
  red700: { light: "#ba2525", dark: "#ba2525" },
63
+ // The #EB3B3B named in red700's own comment above (destructive menu text) —
64
+ // it had never actually been given a token. Independently re-measured live
65
+ // on `app.attio.com/.../tasks`, `rgb(235,59,59)`, as the overdue due-date
66
+ // text colour on the Tasks page (crisp#462) — same hex, second surface.
67
+ // Dark not independently re-measured (Attio's theme toggle is off-limits,
68
+ // same restriction noted throughout this file); reuses red600's dark value,
69
+ // the same "brightens for legibility on a dark surface" tone this red
70
+ // already sits next to, rather than inventing a third red.
71
+ red500: { light: "#eb3b3b", dark: "#ff5a5f" },
55
72
  // seed-design's field focus ring (stroke-neutral-contrast = gray-1000),
56
73
  // measured from @seed-design/css. A high-contrast neutral rather than a brand
57
74
  // hue: the ring reads as "this field has focus", not as a state colour.
@@ -100,4 +117,18 @@ exports.palette = {
100
117
  // as-is per build/spec-check.mjs's own note), this token exists only for
101
118
  // Badge's soft-neutral text, so darkening it doesn't move any other surface.
102
119
  softNeutralText: { light: "#686870", dark: "#9a9aa2" },
120
+ // Badge's purple/role-pill tone (crisp#506) -- measured live off Attio's
121
+ // own role pill ("Admin", app.attio.com/<ws>/settings/members-and-teams):
122
+ // background rgb(245,240,255), text rgb(98,56,181), ring rgb(232,221,254)
123
+ // (cross-validated against a second, independent pill sharing the
124
+ // identical background, a company's "Broadcasting" Categories tag).
125
+ // Dark mode (crisp#566) measured the same way, after switching Attio's
126
+ // own Appearance setting to Dark, reading the pill, then switching back
127
+ // to Light to restore the original state: background rgb(47,30,90),
128
+ // text rgb(216,196,255), ring rgb(69,41,125) -- verify-the-right-element
129
+ // note: the pill's own wrapper span inherits an unstyled rgb(241,242,243)
130
+ // text colour; the real styled colour sits on its nested leaf text node.
131
+ purpleTint: { light: "#f5f0ff", dark: "#2f1e5a" },
132
+ softPurpleText: { light: "#6238b5", dark: "#d8c4ff" },
133
+ purpleRing: { light: "#e8ddfe", dark: "#45297d" },
103
134
  };