@crisp-ui-kit/crisp 0.54.7 → 0.54.9
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.
- package/dist/authshell.css +5 -1
- package/dist/authshell.layered.css +5 -1
- package/dist/badge.css +5 -5
- package/dist/badge.layered.css +5 -5
- package/dist/board.css +36 -8
- package/dist/board.layered.css +36 -8
- package/dist/button.css +3 -1
- package/dist/button.layered.css +3 -1
- package/dist/cjs/components/badge/badge.recipe.js +28 -5
- package/dist/cjs/components/button/button.js +33 -2
- package/dist/cjs/components/button/button.recipe.js +31 -5
- package/dist/cjs/components/contextmenu/contextmenu.js +2 -2
- package/dist/cjs/components/contextmenu/contextmenu.recipe.js +24 -0
- package/dist/cjs/components/datatable/datatable.js +590 -41
- package/dist/cjs/components/datatable/datatable.recipe.js +118 -5
- package/dist/cjs/components/dialog/dialog.js +72 -5
- package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +365 -49
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +78 -0
- package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
- package/dist/cjs/components/tooltip/tooltip.js +23 -9
- package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
- package/dist/cjs/product/authshell/authshell.js +14 -1
- package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
- package/dist/cjs/product/board/board.js +175 -46
- package/dist/cjs/product/board/board.recipe.js +56 -4
- package/dist/cjs/product/comments/comments.js +34 -12
- package/dist/cjs/product/comments/comments.recipe.js +31 -7
- package/dist/cjs/product/formdialog/formdialog.js +10 -4
- package/dist/cjs/product/navitem/navitem.js +7 -2
- package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
- package/dist/cjs/product/objectsettings/index.js +1 -2
- package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
- package/dist/cjs/product/recordstable/recordstable.js +1 -17
- package/dist/cjs/product/richtext/richtext.js +76 -1
- package/dist/cjs/product/savedviews/savedviews.js +55 -24
- package/dist/cjs/product/savedviews/savedviews.recipe.js +27 -0
- package/dist/cjs/product/sidebar/sidebar.js +68 -15
- package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
- package/dist/cjs/tokens/palette.js +32 -1
- package/dist/cjs/tokens/semantic.js +11 -0
- package/dist/comments.css +50 -14
- package/dist/comments.layered.css +50 -14
- package/dist/contextmenu.css +24 -0
- package/dist/contextmenu.layered.css +24 -0
- package/dist/datatable.css +101 -37
- package/dist/datatable.layered.css +101 -37
- package/dist/dialog.css +13 -1
- package/dist/dialog.layered.css +13 -1
- package/dist/esm/components/badge/badge.recipe.js +28 -5
- package/dist/esm/components/button/button.js +34 -3
- package/dist/esm/components/button/button.recipe.js +31 -5
- package/dist/esm/components/contextmenu/contextmenu.js +2 -2
- package/dist/esm/components/contextmenu/contextmenu.recipe.js +24 -0
- package/dist/esm/components/datatable/datatable.js +590 -42
- package/dist/esm/components/datatable/datatable.recipe.js +119 -6
- package/dist/esm/components/dialog/dialog.js +72 -5
- package/dist/esm/components/dialog/dialog.recipe.js +15 -1
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +366 -50
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +80 -2
- package/dist/esm/components/tabs/tabs.recipe.js +11 -2
- package/dist/esm/components/tooltip/tooltip.js +23 -9
- package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
- package/dist/esm/product/authshell/authshell.js +15 -2
- package/dist/esm/product/authshell/authshell.recipe.js +13 -1
- package/dist/esm/product/authshell/index.js +1 -1
- package/dist/esm/product/board/board.js +176 -47
- package/dist/esm/product/board/board.recipe.js +56 -4
- package/dist/esm/product/comments/comments.js +35 -13
- package/dist/esm/product/comments/comments.recipe.js +31 -7
- package/dist/esm/product/formdialog/formdialog.js +10 -4
- package/dist/esm/product/navitem/index.js +1 -1
- package/dist/esm/product/navitem/navitem.js +7 -2
- package/dist/esm/product/navitem/navitem.recipe.js +16 -0
- package/dist/esm/product/objectsettings/index.js +1 -1
- package/dist/esm/product/objectsettings/objectsettings.js +2 -12
- package/dist/esm/product/recordstable/recordstable.js +2 -18
- package/dist/esm/product/richtext/richtext.js +76 -1
- package/dist/esm/product/savedviews/savedviews.js +57 -26
- package/dist/esm/product/savedviews/savedviews.recipe.js +27 -0
- package/dist/esm/product/sidebar/sidebar.js +69 -16
- package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
- package/dist/esm/tokens/palette.js +32 -1
- package/dist/esm/tokens/semantic.js +11 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +16 -2
- package/dist/navitem.css +7 -0
- package/dist/navitem.layered.css +7 -0
- package/dist/recordstable.css +34 -32
- package/dist/recordstable.layered.css +34 -32
- package/dist/savedviews.css +27 -0
- package/dist/savedviews.layered.css +27 -0
- package/dist/sidebar.css +6 -1
- package/dist/sidebar.layered.css +6 -1
- package/dist/styles.css +369 -106
- package/dist/styles.layered.css +369 -106
- package/dist/tabletoolbar.css +47 -3
- package/dist/tabletoolbar.layered.css +47 -3
- package/dist/tabs.css +1 -1
- package/dist/tabs.layered.css +1 -1
- package/dist/tokens.css +9 -1
- package/dist/tokens.json +16 -2
- package/dist/tooltip.css +1 -1
- package/dist/tooltip.layered.css +1 -1
- package/dist/types/components/badge/badge.recipe.d.ts +5 -1
- package/dist/types/components/button/button.recipe.d.ts +18 -5
- package/dist/types/components/contextmenu/contextmenu.d.ts +8 -1
- package/dist/types/components/datatable/datatable.d.ts +98 -1
- package/dist/types/components/dialog/dialog.d.ts +25 -2
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +113 -3
- package/dist/types/components/tooltip/tooltip.d.ts +12 -4
- package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
- package/dist/types/product/authshell/authshell.d.ts +26 -2
- package/dist/types/product/authshell/index.d.ts +1 -1
- package/dist/types/product/board/board.d.ts +53 -6
- package/dist/types/product/comments/comments.d.ts +16 -3
- package/dist/types/product/formdialog/formdialog.d.ts +2 -1
- package/dist/types/product/navitem/index.d.ts +1 -1
- package/dist/types/product/navitem/navitem.d.ts +23 -0
- package/dist/types/product/objectsettings/index.d.ts +1 -1
- package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
- package/dist/types/product/recordstable/recordstable.d.ts +0 -1
- package/dist/types/product/savedviews/savedviews.d.ts +45 -8
- package/dist/types/product/sidebar/sidebar.d.ts +33 -5
- package/dist/types/tokens/contract.d.ts +4 -0
- package/dist/types/tokens/palette.d.ts +13 -1
- package/dist/types/tokens/semantic.d.ts +11 -0
- package/package.json +1 -1
|
@@ -1,32 +1,47 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronDown, MoreVertical, Plus, Users2 } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { Menu, MenuItem } from "../../components/menu/index.js";
|
|
4
|
+
import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
|
|
5
5
|
import { useListboxKeyboard } from "../../components/select/index.js";
|
|
6
6
|
import { FloatingPortal, hasNestedFloatingLayer, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
|
|
7
7
|
import { savedviewsRecipe } from "./savedviews.recipe.js";
|
|
8
|
+
// #618: the same fallback Select's own `optionText` gives a non-string
|
|
9
|
+
// `label` — `keywords` first, then the plain string itself, then (rather
|
|
10
|
+
// than becoming permanently unmatchable) the view's own `id`.
|
|
11
|
+
function viewText(v) {
|
|
12
|
+
if (v.keywords)
|
|
13
|
+
return v.keywords;
|
|
14
|
+
return typeof v.name === "string" ? v.name : v.id;
|
|
15
|
+
}
|
|
8
16
|
/**
|
|
9
17
|
* A saved-view switcher — measured from Attio's "All Companies ▾" menu. A
|
|
10
18
|
* chip trigger opens a popover listing every saved view as a `role="option"`
|
|
11
19
|
* row (leading coloured icon, name, a hover-revealed kebab of
|
|
12
20
|
* rename/duplicate/delete), plus a trailing "Create new view" row. The
|
|
13
21
|
* active view is marked `aria-selected`/`aria-current="page"` and mirrored
|
|
14
|
-
* onto the trigger's own icon/label.
|
|
22
|
+
* onto the trigger's own icon/label. The panel opens on a "Search views..."
|
|
23
|
+
* box (#618, P-007) — real DOM focus moves there (`requestAnimationFrame`,
|
|
24
|
+
* same race `Select`'s own `searchable` variant already guards against),
|
|
25
|
+
* not virtual focus on the trigger.
|
|
15
26
|
*
|
|
16
|
-
* Keyboard: while the
|
|
17
|
-
* virtual highlight (`aria-activedescendant
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
27
|
+
* Keyboard: while the panel is open, focus lives on the search input — ↑/↓
|
|
28
|
+
* move a virtual highlight (`aria-activedescendant`) over the (possibly
|
|
29
|
+
* filtered) view list, Enter chooses the highlighted row, Escape closes,
|
|
30
|
+
* deferring to a still-open kebab submenu first (`hasOpenFloatingLayer`),
|
|
31
|
+
* so Escape closes one layer at a time — the same split `Select`'s own
|
|
32
|
+
* `searchable` variant already uses between its trigger and its own search
|
|
33
|
+
* input.
|
|
22
34
|
*/
|
|
23
|
-
export 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 }) {
|
|
35
|
+
export function SavedViews({ label, icon, views, activeId, onSelect, onCreate, onRename, onDuplicate, onDelete, onToggleFavorite, createLabel: createLabelProp, renameLabel: renameLabelProp, duplicateLabel: duplicateLabelProp, deleteLabel: deleteLabelProp, favoriteLabel: favoriteLabelProp, viewOptionsLabel: viewOptionsLabelProp, searchPlaceholder: searchPlaceholderProp, searchLabel: searchLabelProp, emptyLabel: emptyLabelProp, className, "aria-label": ariaLabel, ...rest }) {
|
|
24
36
|
const createLabel = useCrispMessage(createLabelProp, "Create new view");
|
|
25
37
|
const renameLabel = useCrispMessage(renameLabelProp, "Rename");
|
|
26
38
|
const duplicateLabel = useCrispMessage(duplicateLabelProp, "Duplicate");
|
|
27
39
|
const deleteLabel = useCrispMessage(deleteLabelProp, "Delete");
|
|
28
40
|
const favoriteLabel = useCrispMessage(favoriteLabelProp, "Add to favorites");
|
|
29
41
|
const viewOptionsLabel = useCrispMessage(viewOptionsLabelProp, "View options");
|
|
42
|
+
const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search views...");
|
|
43
|
+
const searchLabel = useCrispMessage(searchLabelProp, "Filter views");
|
|
44
|
+
const emptyLabel = useCrispMessage(emptyLabelProp, "No results");
|
|
30
45
|
const autoId = React.useId();
|
|
31
46
|
const triggerId = `${autoId}-trigger`;
|
|
32
47
|
// role="combobox" (below) does not take its name from content — unlike a
|
|
@@ -45,13 +60,22 @@ export function SavedViews({ label, icon, views, activeId, onSelect, onCreate, o
|
|
|
45
60
|
const [up, setUp] = React.useState(false);
|
|
46
61
|
const [pos, setPos] = React.useState({});
|
|
47
62
|
const [highlight, setHighlight] = React.useState(-1);
|
|
63
|
+
// #618: the panel's own "Search views..." box — real DOM focus lives here
|
|
64
|
+
// while open, unlike the virtual-focus-on-trigger model this component
|
|
65
|
+
// used before the search box existed.
|
|
66
|
+
const [query, setQuery] = React.useState("");
|
|
48
67
|
const rootRef = React.useRef(null);
|
|
49
68
|
const triggerRef = React.useRef(null);
|
|
50
69
|
const panelRef = React.useRef(null);
|
|
70
|
+
const searchRef = React.useRef(null);
|
|
51
71
|
const activeView = views.find((v) => v.id === activeId) ?? null;
|
|
72
|
+
const q = query.trim().toLowerCase();
|
|
73
|
+
const filteredViews = q
|
|
74
|
+
? views.filter((v) => viewText(v).toLowerCase().includes(q))
|
|
75
|
+
: views;
|
|
52
76
|
// the trailing "Create new view" row is one more enabled index, past every
|
|
53
|
-
//
|
|
54
|
-
const createIndex =
|
|
77
|
+
// (possibly filtered) view -- it never itself filters out
|
|
78
|
+
const createIndex = filteredViews.length;
|
|
55
79
|
const openPanel = () => {
|
|
56
80
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
57
81
|
if (rect) {
|
|
@@ -64,8 +88,14 @@ export function SavedViews({ label, icon, views, activeId, onSelect, onCreate, o
|
|
|
64
88
|
setUp(flipUp);
|
|
65
89
|
setPos(style);
|
|
66
90
|
}
|
|
91
|
+
setQuery("");
|
|
67
92
|
setHighlight(-1);
|
|
68
93
|
setOpen(true);
|
|
94
|
+
// Same race Select's own `searchable` variant already guards against
|
|
95
|
+
// (its own comment: calling focus synchronously here loses to the
|
|
96
|
+
// portal's own mount timing) -- a `requestAnimationFrame` after the
|
|
97
|
+
// state update that renders the input wins it.
|
|
98
|
+
requestAnimationFrame(() => searchRef.current?.focus({ preventScroll: true }));
|
|
69
99
|
};
|
|
70
100
|
const close = React.useCallback(() => {
|
|
71
101
|
setOpen(false);
|
|
@@ -77,7 +107,7 @@ export function SavedViews({ label, icon, views, activeId, onSelect, onCreate, o
|
|
|
77
107
|
close();
|
|
78
108
|
}
|
|
79
109
|
else {
|
|
80
|
-
const v =
|
|
110
|
+
const v = filteredViews[highlight];
|
|
81
111
|
if (v) {
|
|
82
112
|
onSelect(v.id);
|
|
83
113
|
close();
|
|
@@ -85,7 +115,7 @@ export function SavedViews({ label, icon, views, activeId, onSelect, onCreate, o
|
|
|
85
115
|
}
|
|
86
116
|
};
|
|
87
117
|
const { onKeyDown: onNavKeys } = useListboxKeyboard({
|
|
88
|
-
enabledIndexes: Array.from({ length:
|
|
118
|
+
enabledIndexes: Array.from({ length: filteredViews.length + 1 }, (_, i) => i),
|
|
89
119
|
highlight,
|
|
90
120
|
setHighlight,
|
|
91
121
|
commit,
|
|
@@ -144,25 +174,26 @@ export function SavedViews({ label, icon, views, activeId, onSelect, onCreate, o
|
|
|
144
174
|
const triggerIcon = activeView ? activeView.icon : icon;
|
|
145
175
|
const triggerLabel = activeView ? activeView.name : label;
|
|
146
176
|
return (_jsxs("div", { ref: rootRef, className: savedviewsRecipe({ className }), ...rest, children: [_jsxs("button", { ref: triggerRef, id: triggerId, type: "button",
|
|
147
|
-
// combobox
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (open)
|
|
155
|
-
onNavKeys(e);
|
|
156
|
-
else if (e.key === "ArrowDown" || e.key === "Enter") {
|
|
177
|
+
// combobox: aria-haspopup explicitly allows "dialog" — the value
|
|
178
|
+
// Attio's own trigger measures — alongside the more common
|
|
179
|
+
// "listbox". aria-activedescendant lives on the search input (#618)
|
|
180
|
+
// now that real focus moves there while open, not here — the same
|
|
181
|
+
// split Select's own `searchable` variant already uses.
|
|
182
|
+
role: "combobox", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? panelId : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerLabelId, "data-open": open ? "" : undefined, className: "crisp-savedviews-trigger", onClick: () => (open ? setOpen(false) : openPanel()), onKeyDown: (e) => {
|
|
183
|
+
// navigation is handled on the search input while open (#618)
|
|
184
|
+
if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
157
185
|
e.preventDefault();
|
|
158
186
|
openPanel();
|
|
159
187
|
}
|
|
160
|
-
}, children: [triggerIcon != null && (_jsx("span", { className: "crisp-savedviews-trigger-icon", children: triggerIcon })), _jsx("span", { id: triggerLabelId, className: "crisp-savedviews-trigger-label", children: triggerLabel }), _jsx(ChevronDown, { className: "crisp-savedviews-trigger-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _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: [
|
|
188
|
+
}, children: [triggerIcon != null && (_jsx("span", { className: "crisp-savedviews-trigger-icon", children: triggerIcon })), _jsx("span", { id: triggerLabelId, className: "crisp-savedviews-trigger-label", children: triggerLabel }), _jsx(ChevronDown, { className: "crisp-savedviews-trigger-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _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: [_jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchLabel, "aria-controls": panelId, "aria-activedescendant": highlight >= 0 ? optionId(highlight) : undefined, className: "crisp-savedviews-search", value: query, onChange: (e) => {
|
|
189
|
+
setQuery(e.target.value);
|
|
190
|
+
setHighlight(-1);
|
|
191
|
+
}, onKeyDown: onNavKeys }), _jsx("div", { className: "crisp-savedviews-search-sep", "aria-hidden": "true" }), filteredViews.length === 0 && (_jsx("div", { className: "crisp-savedviews-empty", children: emptyLabel })), filteredViews.map((v, i) => {
|
|
161
192
|
const selected = v.id === activeId;
|
|
162
193
|
return (_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: () => {
|
|
163
194
|
onSelect(v.id);
|
|
164
195
|
close();
|
|
165
|
-
}, children: [v.icon != null && (_jsx("span", { className: "crisp-savedviews-row-icon", children: v.icon })), _jsx("span", { className: "crisp-savedviews-row-label", children: v.name }), v.shared && (_jsx("span", { className: "crisp-savedviews-row-shared", "aria-hidden": "true", children: _jsx(Users2, { size: 12, strokeWidth: 1.75 }) })), _jsx("div", { className: "crisp-savedviews-kebab-slot", onClick: (e) => e.stopPropagation(), children: _jsxs(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-savedviews-kebab", "aria-label": viewOptionsLabel, children: _jsx(MoreVertical, { size: 14, strokeWidth: 1.75 }) }), children: [onToggleFavorite && (_jsx(MenuItem, { onSelect: () => onToggleFavorite(v.id), children: favoriteLabel })), _jsx(MenuItem, { onSelect: () => onRename(v.id), children: renameLabel }), _jsx(MenuItem, { onSelect: () => onDuplicate(v.id), children: duplicateLabel }), _jsx(MenuItem, { danger: true, onSelect: () => onDelete(v.id), children: deleteLabel })] }) })] }, v.id));
|
|
196
|
+
}, children: [v.icon != null && (_jsx("span", { className: "crisp-savedviews-row-icon", children: v.icon })), _jsx("span", { className: "crisp-savedviews-row-label", children: v.name }), v.shared && (_jsx("span", { className: "crisp-savedviews-row-shared", "aria-hidden": "true", children: _jsx(Users2, { size: 12, strokeWidth: 1.75 }) })), _jsx("div", { className: "crisp-savedviews-kebab-slot", onClick: (e) => e.stopPropagation(), children: _jsxs(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-savedviews-kebab", "aria-label": viewOptionsLabel, children: _jsx(MoreVertical, { size: 14, strokeWidth: 1.75 }) }), children: [onToggleFavorite && (_jsx(MenuItem, { onSelect: () => onToggleFavorite(v.id), children: favoriteLabel })), _jsx(MenuItem, { onSelect: () => onRename(v.id), children: renameLabel }), _jsx(MenuItem, { onSelect: () => onDuplicate(v.id), children: duplicateLabel }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { danger: true, onSelect: () => onDelete(v.id), children: deleteLabel }), (v.createdBy != null || v.createdAt != null) && (_jsxs(_Fragment, { children: [_jsx(MenuSeparator, {}), _jsxs("div", { className: "crisp-savedviews-kebab-footer", children: [v.createdBy != null && (_jsx("div", { className: "crisp-savedviews-kebab-footer-line", children: v.createdBy })), v.createdAt != null && (_jsx("div", { className: "crisp-savedviews-kebab-footer-line", children: v.createdAt }))] })] }))] }) })] }, v.id));
|
|
166
197
|
}), _jsxs("div", { id: optionId(createIndex), role: "option", "aria-selected": false, "data-highlighted": highlight === createIndex ? "" : undefined, className: "crisp-savedviews-row", onMouseEnter: () => setHighlight(createIndex), onClick: () => {
|
|
167
198
|
onCreate();
|
|
168
199
|
close();
|
|
@@ -60,6 +60,21 @@ const config = defineRecipe({
|
|
|
60
60
|
.crisp-savedviews-panel[data-up] { transform-origin: bottom left; }
|
|
61
61
|
.crisp-savedviews-panel { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
62
62
|
|
|
63
|
+
/* #618 (P-007): the panel's own "Search views..." box — measured live:
|
|
64
|
+
32px tall, 6px/8px padding, NO border/background/leading icon of its
|
|
65
|
+
own (unlike Select's own bordered searchable-input pill), 14px text.
|
|
66
|
+
A hairline divider (borderSubtle, 1px, 3px top margin) separates it
|
|
67
|
+
from the view list, matching the divider Attio's own kebab menu uses
|
|
68
|
+
between its action items and the "Created by" footer below. */
|
|
69
|
+
.crisp-savedviews-search {
|
|
70
|
+
box-sizing: border-box; display: block; width: 100%; height: 32px; flex-shrink: 0;
|
|
71
|
+
margin: 0; padding: ${c.space["1.5"]} ${c.space["2"]}; border: none; background: none;
|
|
72
|
+
font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; outline: none;
|
|
73
|
+
}
|
|
74
|
+
.crisp-savedviews-search::placeholder { color: ${c.fgMuted}; }
|
|
75
|
+
.crisp-savedviews-search-sep { height: 1px; flex-shrink: 0; margin: 3px 0 0; background: ${c.borderSubtle}; }
|
|
76
|
+
.crisp-savedviews-empty { padding: ${c.space["1.5"]} ${c.space["2"]}; font-size: ${c.text.lg}; color: ${c.fgMuted}; }
|
|
77
|
+
|
|
63
78
|
.crisp-savedviews-row {
|
|
64
79
|
box-sizing: border-box; position: relative; display: flex; align-items: center; gap: ${c.space["1.5"]};
|
|
65
80
|
height: ${c.size.menuRow}; flex-shrink: 0; padding: ${c.space["1.5"]} ${c.space["2"]};
|
|
@@ -91,6 +106,18 @@ const config = defineRecipe({
|
|
|
91
106
|
.crisp-savedviews-row:focus-within .crisp-savedviews-kebab,
|
|
92
107
|
.crisp-savedviews-kebab[data-open] { opacity: 1; }
|
|
93
108
|
.crisp-savedviews-kebab:hover, .crisp-savedviews-kebab[data-open] { background: ${c.bgSurface}; color: ${c.fgPrimary}; }
|
|
109
|
+
|
|
110
|
+
/* #618 (P-008): the kebab menu's own "Created by X" / date footer —
|
|
111
|
+
measured live: 2 stacked lines, 6px/8px padding, 2px gap between
|
|
112
|
+
them, 12px/500 text, BOTH lines the same color-mix 50% muted (a
|
|
113
|
+
distinct value from the 63% mix #592's chip breadcrumb uses —
|
|
114
|
+
independently measured, not assumed to match). */
|
|
115
|
+
.crisp-savedviews-kebab-footer {
|
|
116
|
+
display: flex; flex-direction: column; gap: 2px;
|
|
117
|
+
padding: ${c.space["1.5"]} ${c.space["2"]};
|
|
118
|
+
font-size: ${c.text.sm}; font-weight: ${c.weight.medium};
|
|
119
|
+
color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent);
|
|
120
|
+
}
|
|
94
121
|
`,
|
|
95
122
|
variants: {},
|
|
96
123
|
defaultVariants: {},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Building2, ChartNoAxesColumn, ChevronDown, CircleDollarSign, FileText, House, Inbox, LayoutGrid, Mail, MessageSquare, PanelLeft, Phone, Search, Send, SquareCheck, User, UserPlus, Users, Waypoints, } from "lucide-react";
|
|
2
|
+
import { Building2, ChartNoAxesColumn, ChevronDown, CircleDollarSign, FileText, House, Inbox, LayoutGrid, Mail, MessageSquare, PackagePlus, PanelLeft, Phone, Search, Send, Settings, SquareCheck, User, UserPlus, Users, Waypoints, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Avatar } from "../../components/avatar/index.js";
|
|
5
5
|
import { Badge } from "../../components/badge/index.js";
|
|
@@ -44,42 +44,83 @@ export const ATTIO_SIDEBAR_WORKSPACE = {
|
|
|
44
44
|
avatarLabel: "T",
|
|
45
45
|
avatarTone: "cyan",
|
|
46
46
|
};
|
|
47
|
-
/** The measured main nav — the default `items` prop.
|
|
47
|
+
/** The measured main nav — the default `items` prop.
|
|
48
|
+
*
|
|
49
|
+
* `shortcut` is set only where a live hover produced Attio's "G then
|
|
50
|
+
* <letter>" tooltip (crisp#633, P-108) -- Calls/Sequences/Workflows showed
|
|
51
|
+
* no tooltip at all after a 1.2s hover, tried repeatedly, so they're left
|
|
52
|
+
* without one rather than guessed. */
|
|
48
53
|
export const ATTIO_SIDEBAR_ITEMS = [
|
|
49
|
-
{
|
|
54
|
+
{
|
|
55
|
+
label: "Home",
|
|
56
|
+
icon: IconHome,
|
|
57
|
+
shortcut: { description: "Go to home page", keys: ["g", "h"] },
|
|
58
|
+
},
|
|
50
59
|
{
|
|
51
60
|
label: "Tasks",
|
|
52
61
|
icon: IconTasks,
|
|
53
62
|
active: true,
|
|
54
63
|
trailing: (_jsx(Badge, { tone: "brand", variant: "solid", size: "small", children: "1" })),
|
|
64
|
+
shortcut: { description: "Go to tasks page", keys: ["g", "t"] },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: "Notes",
|
|
68
|
+
icon: IconNotes,
|
|
69
|
+
shortcut: { description: "Go to all notes page", keys: ["g", "n"] },
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: "Emails",
|
|
73
|
+
icon: IconEmails,
|
|
74
|
+
shortcut: { description: "Go to email drafts page", keys: ["g", "e"] },
|
|
55
75
|
},
|
|
56
|
-
{ label: "Notes", icon: IconNotes },
|
|
57
|
-
{ label: "Emails", icon: IconEmails },
|
|
58
76
|
{ label: "Calls", icon: IconCalls },
|
|
59
|
-
{
|
|
77
|
+
{
|
|
78
|
+
label: "Reports",
|
|
79
|
+
icon: IconReports,
|
|
80
|
+
shortcut: { description: "Go to reports page", keys: ["g", "d"] },
|
|
81
|
+
},
|
|
60
82
|
{ label: "Sequences", icon: IconSequences },
|
|
61
83
|
{ label: "Workflows", icon: IconWorkflows },
|
|
62
84
|
];
|
|
63
|
-
/** The measured Records section — the default `recordsSection` prop.
|
|
85
|
+
/** The measured Records section — the default `recordsSection` prop. Same
|
|
86
|
+
* `shortcut` caveat as `ATTIO_SIDEBAR_ITEMS`: Deals/Users/Workspaces showed
|
|
87
|
+
* no hover tooltip. `actions` is Attio's own gear + add glyph (P-109),
|
|
88
|
+
* measured aria-labels "Record options" / "Create new objects or enable
|
|
89
|
+
* standard objects". */
|
|
64
90
|
export const ATTIO_SIDEBAR_RECORDS_SECTION = {
|
|
65
91
|
label: "Records",
|
|
66
92
|
items: [
|
|
67
|
-
{
|
|
68
|
-
|
|
93
|
+
{
|
|
94
|
+
label: "Companies",
|
|
95
|
+
tone: "brand",
|
|
96
|
+
glyph: GlyphCompanies,
|
|
97
|
+
shortcut: { description: "Go to all companies page", keys: ["g", "c"] },
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: "People",
|
|
101
|
+
tone: "brand",
|
|
102
|
+
glyph: GlyphPeople,
|
|
103
|
+
shortcut: { description: "Go to all people page", keys: ["g", "p"] },
|
|
104
|
+
},
|
|
69
105
|
{ label: "Deals", tone: "orange", glyph: GlyphDeals },
|
|
70
106
|
{ label: "Users", tone: "cyan", glyph: GlyphUsers },
|
|
71
107
|
{ label: "Workspaces", tone: "purple", glyph: GlyphWorkspaces },
|
|
72
108
|
],
|
|
109
|
+
actions: defaultSectionActions("Record options", "Create new objects or enable standard objects"),
|
|
73
110
|
};
|
|
74
|
-
/** The measured Lists section — the default `listsSection` prop.
|
|
111
|
+
/** The measured Lists section — the default `listsSection` prop. `actions`
|
|
112
|
+
* per P-109, measured aria-labels "List options" / "Add list". */
|
|
75
113
|
export const ATTIO_SIDEBAR_LISTS_SECTION = {
|
|
76
114
|
label: "Lists",
|
|
77
115
|
newListLabel: "+ New list",
|
|
116
|
+
actions: defaultSectionActions("List options", "Add list"),
|
|
78
117
|
};
|
|
79
|
-
/** The measured Chats section — the default `chatsSection` prop.
|
|
118
|
+
/** The measured Chats section — the default `chatsSection` prop. `actions`
|
|
119
|
+
* per P-109, measured aria-labels "Chat options" / "New chat". */
|
|
80
120
|
export const ATTIO_SIDEBAR_CHATS_SECTION = {
|
|
81
121
|
label: "Chats",
|
|
82
122
|
items: [{ label: "In-app notification for deal wins", icon: IconChat }],
|
|
123
|
+
actions: defaultSectionActions("Chat options", "New chat"),
|
|
83
124
|
};
|
|
84
125
|
/** The measured footer copy — the default `footer` prop. */
|
|
85
126
|
export const ATTIO_SIDEBAR_FOOTER = {
|
|
@@ -88,8 +129,20 @@ export const ATTIO_SIDEBAR_FOOTER = {
|
|
|
88
129
|
trialText: "13 days left on trial!",
|
|
89
130
|
trialActionLabel: "Keep Pro",
|
|
90
131
|
};
|
|
91
|
-
function SectionHeader({ label, open, onToggle, }) {
|
|
92
|
-
return (_jsxs("button", { type: "button", className: "crisp-sidebar-section-header", "data-collapsed": open ? undefined : "", "aria-expanded": open, onClick: onToggle, children: [_jsx(IconChevronMini, {}), _jsx("span", { children: label })] }));
|
|
132
|
+
function SectionHeader({ label, open, onToggle, actions, }) {
|
|
133
|
+
return (_jsxs("div", { className: "crisp-sidebar-section-header-row", children: [_jsxs("button", { type: "button", className: "crisp-sidebar-section-header", "data-collapsed": open ? undefined : "", "aria-expanded": open, onClick: onToggle, children: [_jsx(IconChevronMini, {}), _jsx("span", { children: label })] }), actions != null && (_jsx("div", { className: "crisp-sidebar-section-actions", children: actions }))] }));
|
|
134
|
+
}
|
|
135
|
+
/** Attio's own two hover-only header actions (crisp#633, P-109), measured on
|
|
136
|
+
* the Records/Lists/Chats section headers: a gear ("<Section> options") and
|
|
137
|
+
* an add glyph -- re-measured on all three, the add icon is the SAME glyph
|
|
138
|
+
* (`icon-defs/cab3218f7a08cd13.svg`) reused everywhere, not a per-section
|
|
139
|
+
* one, so `PackagePlus` covers Lists/Chats too, not just Records' own
|
|
140
|
+
* "Create new objects" link. Both are decorative defaults with no handler,
|
|
141
|
+
* the same trade a consumer already makes with `footer`'s default
|
|
142
|
+
* "Getting started"/"Keep Pro" buttons — a consumer wanting the real
|
|
143
|
+
* behaviour passes its own `actions` node instead. */
|
|
144
|
+
function defaultSectionActions(optionsLabel, addLabel) {
|
|
145
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "crisp-sidebar-section-action", "aria-label": optionsLabel, children: _jsx(Settings, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx("button", { type: "button", className: "crisp-sidebar-section-action", "aria-label": addLabel, children: _jsx(PackagePlus, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }));
|
|
93
146
|
}
|
|
94
147
|
export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = ATTIO_SIDEBAR_WORKSPACE, workspaceMenu, items = ATTIO_SIDEBAR_ITEMS, recordsSection = ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = ATTIO_SIDEBAR_CHATS_SECTION, groups, footer = ATTIO_SIDEBAR_FOOTER, commandItems, commandOpen: commandOpenProp, defaultCommandOpen, onCommandOpenChange, commandShortcut = true, "aria-label": ariaLabel = "Main", ...props }, ref) => {
|
|
95
148
|
// Seeded once, on mount, from whatever groups start collapsed — not
|
|
@@ -138,14 +191,14 @@ export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse,
|
|
|
138
191
|
});
|
|
139
192
|
}, []);
|
|
140
193
|
const workspaceTrigger = (_jsxs("button", { type: "button", className: "crisp-sidebar-ws", children: [_jsx(Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), _jsx("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), _jsx(IconChevronDown, {})] }));
|
|
141
|
-
return (_jsxs(Primitive.nav, { ref: ref, asChild: asChild, "aria-label": ariaLabel, className: sidebarRecipe({ className }), ...props, children: [_jsxs("div", { className: "crisp-sidebar-ws-row", children: [workspaceMenu ? (_jsx(Menu, { trigger: workspaceTrigger, className: "crisp-sidebar-ws-menuroot", contentClassName: "crisp-sidebar-ws-menu-content", children: workspaceMenu })) : (workspaceTrigger), _jsx(Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: _jsx(IconCollapsePanel, {}) })] }), _jsxs("div", { className: "crisp-sidebar-searchrow", children: [_jsxs("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [_jsx(IconSearch, {}), _jsx("span", { className: "crisp-sidebar-search-label", children: searchLabel }), _jsx("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), _jsx(Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: _jsx(IconInbox, {}) })] }), _jsx("div", { className: "crisp-sidebar-divider" }), _jsx("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing, href, onSelect }) => (_jsx(NavItem, { href: href ?? "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, onClick: onSelect, children: label }, href ?? label))) }), groups ? (
|
|
194
|
+
return (_jsxs(Primitive.nav, { ref: ref, asChild: asChild, "aria-label": ariaLabel, className: sidebarRecipe({ className }), ...props, children: [_jsxs("div", { className: "crisp-sidebar-ws-row", children: [workspaceMenu ? (_jsx(Menu, { trigger: workspaceTrigger, className: "crisp-sidebar-ws-menuroot", contentClassName: "crisp-sidebar-ws-menu-content", children: workspaceMenu })) : (workspaceTrigger), _jsx(Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: _jsx(IconCollapsePanel, {}) })] }), _jsxs("div", { className: "crisp-sidebar-searchrow", children: [_jsxs("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [_jsx(IconSearch, {}), _jsx("span", { className: "crisp-sidebar-search-label", children: searchLabel }), _jsx("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), _jsx(Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: _jsx(IconInbox, {}) })] }), _jsx("div", { className: "crisp-sidebar-divider" }), _jsx("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing, href, onSelect, shortcut, }) => (_jsx(NavItem, { href: href ?? "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, onClick: onSelect, shortcut: shortcut, children: label }, href ?? label))) }), groups ? (
|
|
142
195
|
// #325: navigation as data, reusing the exact section markup/
|
|
143
196
|
// recipe above — SectionHeader + .crisp-sidebar-section — instead
|
|
144
197
|
// of the three fixed Attio sections.
|
|
145
198
|
groups.map((group) => {
|
|
146
199
|
const groupKey = group.key ?? group.label;
|
|
147
|
-
return (_jsxs(React.Fragment, { children: [_jsx(SectionHeader, { label: group.label, open: !collapsedSections.has(groupKey), onToggle: () => toggleSection(groupKey) }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has(groupKey) ? "" : undefined, children: group.items.map((item) => (_jsx(NavItem, { href: item.href ?? "#", active: item.active, icon: item.icon ? _jsx(item.icon, {}) : undefined, trailing: item.count, onClick: item.onSelect, children: item.label }, item.href ?? item.label))) })] }, groupKey));
|
|
148
|
-
})) : (_jsxs(_Fragment, { children: [_jsx(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => (_jsx(NavItem, { href: "#", icon: _jsx(Avatar, { size: "sm", shape: "rounded", tone: tone, children: _jsx(Glyph, {}) }), children: label }, label))) }), _jsx(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: _jsx("button", { type: "button", className: "crisp-sidebar-newlist", children: listsSection.newListLabel }) }), _jsx(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon, active, trailing, href, onSelect, }) => (_jsx(NavItem, { href: href ?? "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, onClick: onSelect, children: label }, href ?? label))) })] })), footer !== false &&
|
|
200
|
+
return (_jsxs(React.Fragment, { children: [_jsx(SectionHeader, { label: group.label, open: !collapsedSections.has(groupKey), onToggle: () => toggleSection(groupKey), actions: group.actions }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has(groupKey) ? "" : undefined, children: group.items.map((item) => (_jsx(NavItem, { href: item.href ?? "#", active: item.active, icon: item.icon ? _jsx(item.icon, {}) : undefined, trailing: item.count, onClick: item.onSelect, shortcut: item.shortcut, children: item.label }, item.href ?? item.label))) })] }, groupKey));
|
|
201
|
+
})) : (_jsxs(_Fragment, { children: [_jsx(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records"), actions: recordsSection.actions }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph, shortcut }) => (_jsx(NavItem, { href: "#", icon: _jsx(Avatar, { size: "sm", shape: "rounded", tone: tone, children: _jsx(Glyph, {}) }), shortcut: shortcut, children: label }, label))) }), _jsx(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists"), actions: listsSection.actions }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: _jsx("button", { type: "button", className: "crisp-sidebar-newlist", children: listsSection.newListLabel }) }), _jsx(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats"), actions: chatsSection.actions }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon, active, trailing, href, onSelect, shortcut, }) => (_jsx(NavItem, { href: href ?? "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, onClick: onSelect, shortcut: shortcut, children: label }, href ?? label))) })] })), footer !== false &&
|
|
149
202
|
(isSidebarFooterCopy(footer) ? (_jsxs("div", { className: "crisp-sidebar-footer", children: [_jsx(Button, { intent: "neutral", size: "sm", className: "crisp-sidebar-pill", children: footer.gettingStartedLabel }), _jsx(NavItem, { href: "#", icon: _jsx(IconInvite, {}), children: footer.inviteLabel }), _jsx("div", { className: "crisp-sidebar-divider" }), _jsxs("div", { className: "crisp-sidebar-trial", children: [_jsx("span", { children: footer.trialText }), _jsx(Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] })) : (_jsx("div", { className: "crisp-sidebar-footer", children: footer }))), _jsx(Command, { open: commandOpen, onOpenChange: setCommandOpen, items: commandItems })] }));
|
|
150
203
|
});
|
|
151
204
|
Sidebar.displayName = "Sidebar";
|
|
@@ -16,9 +16,14 @@ const config = defineRecipe({
|
|
|
16
16
|
; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overflow-y: auto; & .crisp-sidebar-section { display: flex; flex-direction: column; gap: ${c.space.px}; margin-top: ${c.space["2"]}; }
|
|
17
17
|
& .crisp-sidebar-section[data-collapsed] { display: none; }
|
|
18
18
|
& .crisp-sidebar-label { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; padding: ${c.space["1.5"]} ${c.space["2"]} ${c.space["0.5"]}; }
|
|
19
|
-
& .crisp-sidebar-section-header { display: flex; align-items: center;
|
|
19
|
+
& .crisp-sidebar-section-header-row { display: flex; align-items: center; }
|
|
20
|
+
& .crisp-sidebar-section-header { display: flex; align-items: center; gap: ${c.space["1"]}; flex: 1; min-width: 0; margin: 0; border: none; background: none; padding: ${c.space["1.5"]} ${c.space["2"]} ${c.space["0.5"]}; font-family: ${c.font.sans}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; cursor: pointer; }
|
|
20
21
|
& .crisp-sidebar-section-header svg { flex-shrink: 0; transition: transform .15s ease; }
|
|
21
22
|
& .crisp-sidebar-section-header[data-collapsed] svg { transform: rotate(-90deg); }
|
|
23
|
+
& .crisp-sidebar-section-actions { display: flex; align-items: center; gap: ${c.space["1"]}; flex-shrink: 0; padding-inline-end: ${c.space["1"]}; opacity: 0; transition: opacity .12s ease; }
|
|
24
|
+
& .crisp-sidebar-section-header-row:hover .crisp-sidebar-section-actions, & .crisp-sidebar-section-header-row:focus-within .crisp-sidebar-section-actions { opacity: 1; }
|
|
25
|
+
& .crisp-sidebar-section-action { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: ${c.space["5"]}; height: ${c.space["5"]}; margin: 0; padding: 0; border: none; border-radius: ${c.radius.sm}; background: transparent; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); font-family: inherit; cursor: pointer; text-decoration: none; transition: background-color 0.14s, color 0.14s; }
|
|
26
|
+
& .crisp-sidebar-section-action:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
|
|
22
27
|
& .crisp-sidebar-ws-row { display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.header}; padding: 0 ${c.space["2"]}; }
|
|
23
28
|
& .crisp-sidebar-ws-menuroot { display: flex; flex: 1; min-width: 0; }
|
|
24
29
|
& .crisp-sidebar-ws { box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["2"]}; flex: 1; min-width: 0; height: 100%; margin: 0; padding: 0; border: none; background: none; border-radius: ${c.radius.lg}; font-family: inherit; font-size: inherit; color: inherit; text-align: start; cursor: pointer; transition: background-color 0.16s; }
|
|
@@ -17,6 +17,15 @@ export const palette = {
|
|
|
17
17
|
// off-limits to toggle, same restriction as `ringField`/`ringSwitch`) —
|
|
18
18
|
// reuses `line200`'s dark line colour rather than inventing a new one.
|
|
19
19
|
line150: { light: "#e6e7ea", dark: "#212224" },
|
|
20
|
+
// The filter chip's field/value segments while the condition is
|
|
21
|
+
// incomplete (no value typed yet) — measured live on Attio's own filter
|
|
22
|
+
// chip (`rgb(246,247,247)`), lighter than `gray50` (#f4f4f5, close but not
|
|
23
|
+
// equal). The chip's operator/kebab segments stay `gray00` in the same
|
|
24
|
+
// state; only the two text-content segments carry this muted fill. Dark
|
|
25
|
+
// value not independently re-measured (Attio's dark theme is off-limits to
|
|
26
|
+
// toggle) — reuses `gray50`'s dark value, the nearest existing "muted
|
|
27
|
+
// surface" tone, rather than inventing a third dark gray.
|
|
28
|
+
gray25: { light: "#f6f7f7", dark: "#101112" },
|
|
20
29
|
ringNeutral: { light: "transparent", dark: "#212224" },
|
|
21
30
|
// Text-entry fields (Input, Textarea) — measured live on app.attio.com's
|
|
22
31
|
// Settings page, a real typed field: `inset 0 0 0 1px rgba(0,0,0,.06)`,
|
|
@@ -37,6 +46,22 @@ export const palette = {
|
|
|
37
46
|
ringSwitch: { light: "#d9dade", dark: "#212224" },
|
|
38
47
|
tileActive: { light: "#ffffff", dark: "#101112" },
|
|
39
48
|
tileActiveShadow: { light: "rgba(0,0,0,0.04)", dark: "rgba(0,0,0,0)" },
|
|
49
|
+
// Dialog's page-dimming backdrop (crisp#677) — measured live behind the
|
|
50
|
+
// Create-attribute FormDialog, both themes: `rgba(0,0,0,.25)` light,
|
|
51
|
+
// `rgba(0,0,0,.55)` dark. `Dialog`'s own backdrop had shipped fully
|
|
52
|
+
// `transparent` (AUTHORING's Lesson 14 measured the command palette has
|
|
53
|
+
// NO dark backdrop and that got generalized to every `Dialog`, which this
|
|
54
|
+
// corrects — the palette is a different surface from a plain content
|
|
55
|
+
// dialog and really does dim the page).
|
|
56
|
+
bgScrim: { light: "rgba(0,0,0,0.25)", dark: "rgba(0,0,0,0.55)" },
|
|
57
|
+
// The discard-confirm's description line (crisp#677, `Dialog`'s new
|
|
58
|
+
// `variant="alert"`) — measured live, both themes, over `popoverBg`: a
|
|
59
|
+
// theme-relative alpha rather than a flat gray, so it stays legible if
|
|
60
|
+
// `popoverBg` itself ever shifts.
|
|
61
|
+
fgAlertDescription: {
|
|
62
|
+
light: "rgba(0,0,0,0.63)",
|
|
63
|
+
dark: "rgba(255,255,255,0.6)",
|
|
64
|
+
},
|
|
40
65
|
overlayFrost: { light: "#ffffff", dark: "#1c1d1f" },
|
|
41
66
|
shadowOverlay1: { light: "rgba(28,40,64,0.12)", dark: "rgba(0,0,0,0.48)" },
|
|
42
67
|
shadowOverlay2: { light: "rgba(28,40,64,0.16)", dark: "rgba(0,0,0,0.64)" },
|
|
@@ -113,7 +138,13 @@ export const palette = {
|
|
|
113
138
|
// fg-muted/bg-raised themselves (system-wide, measured from Attio, kept
|
|
114
139
|
// as-is per build/spec-check.mjs's own note), this token exists only for
|
|
115
140
|
// Badge's soft-neutral text, so darkening it doesn't move any other surface.
|
|
116
|
-
|
|
141
|
+
// crisp#696: re-measured live (Standard/System pills, object settings) at
|
|
142
|
+
// rgb(80,81,85) -- darker than the #686870 above, which only ever cleared
|
|
143
|
+
// AA, never matched the reference. Only the light side moves: dark
|
|
144
|
+
// (#9a9aa2) already passed and Attio's own dark theme stays off-limits to
|
|
145
|
+
// toggle, the same restriction this file notes throughout (ringField,
|
|
146
|
+
// ringSwitch, line150).
|
|
147
|
+
softNeutralText: { light: "#505155", dark: "#9a9aa2" },
|
|
117
148
|
// Badge's purple/role-pill tone (crisp#506) -- measured live off Attio's
|
|
118
149
|
// own role pill ("Admin", app.attio.com/<ws>/settings/members-and-teams):
|
|
119
150
|
// background rgb(245,240,255), text rgb(98,56,181), ring rgb(232,221,254)
|
|
@@ -8,15 +8,26 @@ export const semantic = {
|
|
|
8
8
|
fgOnTone: "ink",
|
|
9
9
|
bgSurface: "gray00",
|
|
10
10
|
bgRaised: "gray50",
|
|
11
|
+
/** A surface lighter than `bgRaised` for content that isn't actionable
|
|
12
|
+
* yet — the filter chip's field/value segments before the condition has
|
|
13
|
+
* a value (see palette.ts's `gray25`). */
|
|
14
|
+
bgSurfaceMuted: "gray25",
|
|
11
15
|
borderDefault: "line200",
|
|
12
16
|
borderSubtle: "line100",
|
|
13
17
|
/** ImportDialog's dashed dropzone border — see palette.ts's `line150`. */
|
|
14
18
|
borderDropzone: "line150",
|
|
19
|
+
/** A hairline between adjacent same-height segments (the filter chip's
|
|
20
|
+
* field/operator/value/kebab boxes) — same measured hex as
|
|
21
|
+
* `borderDropzone` (`line150`), aliased under its own name because the
|
|
22
|
+
* two consumers share nothing but the colour. */
|
|
23
|
+
borderDivider: "line150",
|
|
15
24
|
ringNeutral: "ringNeutral",
|
|
16
25
|
ringField: "ringField",
|
|
17
26
|
ringSwitch: "ringSwitch",
|
|
18
27
|
bgTileActive: "tileActive",
|
|
19
28
|
shadowTileActive: "tileActiveShadow",
|
|
29
|
+
bgScrim: "bgScrim",
|
|
30
|
+
fgAlertDescription: "fgAlertDescription",
|
|
20
31
|
bgOverlayFrost: "overlayFrost",
|
|
21
32
|
shadowOverlay1: "shadowOverlay1",
|
|
22
33
|
shadowOverlay2: "shadowOverlay2",
|
package/dist/native/tokens.d.ts
CHANGED
|
@@ -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" | "borderDropzone" | "ringNeutral" | "ringField" | "ringSwitch" | "bgTileActive" | "shadowTileActive" | "bgOverlayFrost" | "shadowOverlay1" | "shadowOverlay2" | "shadowOverlay3" | "bgPopover" | "ringPopover" | "shadowPopover1" | "shadowPopover2" | "shadowPopover3" | "fgPrimary" | "fgMuted" | "bgDanger" | "bgDangerSolid" | "fgDanger" | "strokeFieldFocus" | "fgDisabled" | "bgNeutralWeak" | "bgNeutralInverted" | "fgOnNeutralInverted" | "fgNeutralSubtle" | "fgNeutralMuted" | "strokeFocusRing" | "bgSuccess" | "bgWarning" | "bgOrange" | "bgCyan" | "bgPurple" | "bgSidebar" | "bgSubtle" | "fgBrandSoft" | "fgSuccessSoft" | "fgWarningSoft" | "fgDangerSoft" | "fgNeutralSoft" | "bgPurpleSoft" | "fgPurpleSoft" | "ringPurpleSoft", string>;
|
|
5
|
+
export type ColorTokens = Record<"bgBrandSolid" | "bgBrandSolidPressed" | "fgOnBrand" | "fgOnTone" | "bgSurface" | "bgRaised" | "bgSurfaceMuted" | "borderDefault" | "borderSubtle" | "borderDropzone" | "borderDivider" | "ringNeutral" | "ringField" | "ringSwitch" | "bgTileActive" | "shadowTileActive" | "bgScrim" | "fgAlertDescription" | "bgOverlayFrost" | "shadowOverlay1" | "shadowOverlay2" | "shadowOverlay3" | "bgPopover" | "ringPopover" | "shadowPopover1" | "shadowPopover2" | "shadowPopover3" | "fgPrimary" | "fgMuted" | "bgDanger" | "bgDangerSolid" | "fgDanger" | "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>>;
|
package/dist/native/tokens.js
CHANGED
|
@@ -10,14 +10,18 @@ export const tokens = {
|
|
|
10
10
|
"fgOnTone": "#101112",
|
|
11
11
|
"bgSurface": "#ffffff",
|
|
12
12
|
"bgRaised": "#f4f4f5",
|
|
13
|
+
"bgSurfaceMuted": "#f6f7f7",
|
|
13
14
|
"borderDefault": "#e4e4e7",
|
|
14
15
|
"borderSubtle": "#eeeff1",
|
|
15
16
|
"borderDropzone": "#e6e7ea",
|
|
17
|
+
"borderDivider": "#e6e7ea",
|
|
16
18
|
"ringNeutral": "transparent",
|
|
17
19
|
"ringField": "rgba(0,0,0,0.06)",
|
|
18
20
|
"ringSwitch": "#d9dade",
|
|
19
21
|
"bgTileActive": "#ffffff",
|
|
20
22
|
"shadowTileActive": "rgba(0,0,0,0.04)",
|
|
23
|
+
"bgScrim": "rgba(0,0,0,0.25)",
|
|
24
|
+
"fgAlertDescription": "rgba(0,0,0,0.63)",
|
|
21
25
|
"bgOverlayFrost": "#ffffff",
|
|
22
26
|
"shadowOverlay1": "rgba(28,40,64,0.12)",
|
|
23
27
|
"shadowOverlay2": "rgba(28,40,64,0.16)",
|
|
@@ -51,7 +55,7 @@ export const tokens = {
|
|
|
51
55
|
"fgSuccessSoft": "#186b3d",
|
|
52
56
|
"fgWarningSoft": "#80520d",
|
|
53
57
|
"fgDangerSoft": "#a33539",
|
|
54
|
-
"fgNeutralSoft": "#
|
|
58
|
+
"fgNeutralSoft": "#505155",
|
|
55
59
|
"bgPurpleSoft": "#f5f0ff",
|
|
56
60
|
"fgPurpleSoft": "#6238b5",
|
|
57
61
|
"ringPurpleSoft": "#e8ddfe"
|
|
@@ -63,14 +67,18 @@ export const tokens = {
|
|
|
63
67
|
"fgOnTone": "#101112",
|
|
64
68
|
"bgSurface": "#0a0a0a",
|
|
65
69
|
"bgRaised": "#101112",
|
|
70
|
+
"bgSurfaceMuted": "#101112",
|
|
66
71
|
"borderDefault": "#212224",
|
|
67
72
|
"borderSubtle": "#1c1d1f",
|
|
68
73
|
"borderDropzone": "#212224",
|
|
74
|
+
"borderDivider": "#212224",
|
|
69
75
|
"ringNeutral": "#212224",
|
|
70
76
|
"ringField": "#212224",
|
|
71
77
|
"ringSwitch": "#212224",
|
|
72
78
|
"bgTileActive": "#101112",
|
|
73
79
|
"shadowTileActive": "rgba(0,0,0,0)",
|
|
80
|
+
"bgScrim": "rgba(0,0,0,0.55)",
|
|
81
|
+
"fgAlertDescription": "rgba(255,255,255,0.6)",
|
|
74
82
|
"bgOverlayFrost": "#1c1d1f",
|
|
75
83
|
"shadowOverlay1": "rgba(0,0,0,0.48)",
|
|
76
84
|
"shadowOverlay2": "rgba(0,0,0,0.64)",
|
|
@@ -120,11 +128,14 @@ export const tokens = {
|
|
|
120
128
|
"line200": "#e4e4e7",
|
|
121
129
|
"line100": "#eeeff1",
|
|
122
130
|
"line150": "#e6e7ea",
|
|
131
|
+
"gray25": "#f6f7f7",
|
|
123
132
|
"ringNeutral": "transparent",
|
|
124
133
|
"ringField": "rgba(0,0,0,0.06)",
|
|
125
134
|
"ringSwitch": "#d9dade",
|
|
126
135
|
"tileActive": "#ffffff",
|
|
127
136
|
"tileActiveShadow": "rgba(0,0,0,0.04)",
|
|
137
|
+
"bgScrim": "rgba(0,0,0,0.25)",
|
|
138
|
+
"fgAlertDescription": "rgba(0,0,0,0.63)",
|
|
128
139
|
"overlayFrost": "#ffffff",
|
|
129
140
|
"shadowOverlay1": "rgba(28,40,64,0.12)",
|
|
130
141
|
"shadowOverlay2": "rgba(28,40,64,0.16)",
|
|
@@ -158,7 +169,7 @@ export const tokens = {
|
|
|
158
169
|
"softSuccessText": "#186b3d",
|
|
159
170
|
"softWarningText": "#80520d",
|
|
160
171
|
"softDangerText": "#a33539",
|
|
161
|
-
"softNeutralText": "#
|
|
172
|
+
"softNeutralText": "#505155",
|
|
162
173
|
"purpleTint": "#f5f0ff",
|
|
163
174
|
"softPurpleText": "#6238b5",
|
|
164
175
|
"purpleRing": "#e8ddfe"
|
|
@@ -172,11 +183,14 @@ export const tokens = {
|
|
|
172
183
|
"line200": "#212224",
|
|
173
184
|
"line100": "#1c1d1f",
|
|
174
185
|
"line150": "#212224",
|
|
186
|
+
"gray25": "#101112",
|
|
175
187
|
"ringNeutral": "#212224",
|
|
176
188
|
"ringField": "#212224",
|
|
177
189
|
"ringSwitch": "#212224",
|
|
178
190
|
"tileActive": "#101112",
|
|
179
191
|
"tileActiveShadow": "rgba(0,0,0,0)",
|
|
192
|
+
"bgScrim": "rgba(0,0,0,0.55)",
|
|
193
|
+
"fgAlertDescription": "rgba(255,255,255,0.6)",
|
|
180
194
|
"overlayFrost": "#1c1d1f",
|
|
181
195
|
"shadowOverlay1": "rgba(0,0,0,0.48)",
|
|
182
196
|
"shadowOverlay2": "rgba(0,0,0,0.64)",
|
package/dist/navitem.css
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
.crisp-navitem-shortcut { display: flex; align-items: baseline; justify-content: space-between; gap: var(--crisp-space-3); }
|
|
3
|
+
.crisp-navitem-shortcut-desc { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); line-height: 16px; white-space: nowrap; }
|
|
4
|
+
.crisp-navitem-shortcut-chord { display: inline-flex; align-items: center; gap: var(--crisp-space-1); flex-shrink: 0; }
|
|
5
|
+
.crisp-navitem-shortcut-then { font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: #898a8d; }
|
|
6
|
+
.crisp-navitem-shortcut-chord .crisp-kbd { text-transform: uppercase; font-weight: var(--crisp-weight-regular); color: #a2a4a7; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2); }
|
|
7
|
+
|
|
1
8
|
.crisp-navitem {
|
|
2
9
|
box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1-5); height: 28px; padding-inline: var(--crisp-space-2); padding-inline-end: var(--crisp-space-1); border-radius: var(--crisp-radius-lg); font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; line-height: 20px; color: var(--crisp-fg-primary); text-decoration: none; cursor: pointer; user-select: none; width: 100%; transition: background-color 0.16s;
|
|
3
10
|
& .crisp-navitem-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
package/dist/navitem.layered.css
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
@layer crisp-components {
|
|
2
|
+
|
|
3
|
+
.crisp-navitem-shortcut { display: flex; align-items: baseline; justify-content: space-between; gap: var(--crisp-space-3); }
|
|
4
|
+
.crisp-navitem-shortcut-desc { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); line-height: 16px; white-space: nowrap; }
|
|
5
|
+
.crisp-navitem-shortcut-chord { display: inline-flex; align-items: center; gap: var(--crisp-space-1); flex-shrink: 0; }
|
|
6
|
+
.crisp-navitem-shortcut-then { font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: #898a8d; }
|
|
7
|
+
.crisp-navitem-shortcut-chord .crisp-kbd { text-transform: uppercase; font-weight: var(--crisp-weight-regular); color: #a2a4a7; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2); }
|
|
8
|
+
|
|
2
9
|
.crisp-navitem {
|
|
3
10
|
box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1-5); height: 28px; padding-inline: var(--crisp-space-2); padding-inline-end: var(--crisp-space-1); border-radius: var(--crisp-radius-lg); font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; line-height: 20px; color: var(--crisp-fg-primary); text-decoration: none; cursor: pointer; user-select: none; width: 100%; transition: background-color 0.16s;
|
|
4
11
|
& .crisp-navitem-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|