@crisp-ui-kit/crisp 0.35.0 → 0.36.0
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/bulkbar.css +3 -0
- package/dist/bulkbar.layered.css +3 -0
- package/dist/cjs/components/bulkbar/bulkbar.js +1 -1
- package/dist/cjs/components/bulkbar/bulkbar.recipe.js +6 -0
- package/dist/cjs/product/recordpanel/recordpanel.js +4 -2
- package/dist/cjs/product/recordpanel/recordpanel.recipe.js +8 -0
- package/dist/cjs/product/sidebar/sidebar.js +16 -3
- package/dist/esm/components/bulkbar/bulkbar.js +1 -1
- package/dist/esm/components/bulkbar/bulkbar.recipe.js +6 -0
- package/dist/esm/product/recordpanel/recordpanel.js +4 -2
- package/dist/esm/product/recordpanel/recordpanel.recipe.js +8 -0
- package/dist/esm/product/sidebar/sidebar.js +17 -4
- package/dist/recordpanel.css +6 -0
- package/dist/recordpanel.layered.css +6 -0
- package/dist/styles.css +9 -0
- package/dist/styles.layered.css +9 -0
- package/dist/types/product/recordpanel/recordpanel.d.ts +11 -1
- package/dist/types/product/sidebar/sidebar.d.ts +56 -0
- package/package.json +1 -1
package/dist/bulkbar.css
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
& .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
6
6
|
& .crisp-bulkbar-count:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); color: var(--crisp-fg-primary); }
|
|
7
7
|
& .crisp-bulkbar-count svg { flex-shrink: 0; }
|
|
8
|
+
/* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
|
|
9
|
+
-- read, never drawn. Same technique as Slider's thumblabel. */
|
|
10
|
+
& .crisp-bulkbar-clearlabel { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
8
11
|
& .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
9
12
|
& .crisp-bulkbar-btn:hover { background: var(--crisp-bg-raised); }
|
|
10
13
|
& .crisp-bulkbar-btn--danger { color: var(--crisp-bg-danger); }
|
package/dist/bulkbar.layered.css
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
& .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
7
7
|
& .crisp-bulkbar-count:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); color: var(--crisp-fg-primary); }
|
|
8
8
|
& .crisp-bulkbar-count svg { flex-shrink: 0; }
|
|
9
|
+
/* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
|
|
10
|
+
-- read, never drawn. Same technique as Slider's thumblabel. */
|
|
11
|
+
& .crisp-bulkbar-clearlabel { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
9
12
|
& .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
10
13
|
& .crisp-bulkbar-btn:hover { background: var(--crisp-bg-raised); }
|
|
11
14
|
& .crisp-bulkbar-btn--danger { color: var(--crisp-bg-danger); }
|
|
@@ -45,7 +45,7 @@ const bulkbar_recipe_1 = require("./bulkbar.recipe");
|
|
|
45
45
|
* (e.g. `count > 0 && <BulkBar …/>`). Measured from a records table.
|
|
46
46
|
*/
|
|
47
47
|
function BulkBar({ count, onClear, countLabel = "selected", countText = (count, label) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
|
|
48
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: (0, bulkbar_recipe_1.bulkbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-bulkbar-bar", children: [count != null && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
|
|
48
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: (0, bulkbar_recipe_1.bulkbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-bulkbar-bar", children: [count != null && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), (0, jsx_runtime_1.jsx)("span", { className: "crisp-bulkbar-clearlabel", children: clearLabel }), (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
|
|
49
49
|
}
|
|
50
50
|
BulkBar.displayName = "BulkBar";
|
|
51
51
|
/** A styled action button for use inside a `<BulkBar>` (or as a `<Menu>` trigger). */
|
|
@@ -18,6 +18,12 @@ const config = (0, core_1.defineRecipe)({
|
|
|
18
18
|
& .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.menuRow}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
19
19
|
& .crisp-bulkbar-count:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); color: ${tokens_1.contract.fgPrimary}; }
|
|
20
20
|
& .crisp-bulkbar-count svg { flex-shrink: 0; }
|
|
21
|
+
/* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
|
|
22
|
+
-- read, never drawn. Same technique as Slider's thumblabel. */
|
|
23
|
+
& .crisp-bulkbar-clearlabel {
|
|
24
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
25
|
+
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
|
|
26
|
+
}
|
|
21
27
|
& .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.menuRow}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
22
28
|
& .crisp-bulkbar-btn:hover { background: ${tokens_1.contract.bgRaised}; }
|
|
23
29
|
& .crisp-bulkbar-btn--danger { color: ${tokens_1.contract.bgDanger}; }
|
|
@@ -62,7 +62,8 @@ function FieldValue({ col, row, emptyLabel, }) {
|
|
|
62
62
|
* relative` container (e.g. the app main area); it slides in from the right and
|
|
63
63
|
* overlays the content.
|
|
64
64
|
*/
|
|
65
|
-
function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", detailsLabel = "Details", emptyLabel = "Empty", ...rest }) {
|
|
65
|
+
function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", title, detailsLabel = "Details", emptyLabel = "Empty", footer, ...rest }) {
|
|
66
|
+
const titleId = React.useId();
|
|
66
67
|
// close on Escape while open
|
|
67
68
|
React.useEffect(() => {
|
|
68
69
|
if (!record)
|
|
@@ -78,6 +79,7 @@ function RecordPanel({ record, columns, onClose, className, closeLabel = "Close
|
|
|
78
79
|
? record[entityCol.key]
|
|
79
80
|
: undefined;
|
|
80
81
|
const fields = columns.filter((c) => c.type !== "entity");
|
|
81
|
-
|
|
82
|
+
const heading = title ?? entity?.name ?? "Record";
|
|
83
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpanel_recipe_1.recordpanelRecipe)({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-header", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpanel-fav", src: entity.avatar, alt: "" })) : entity ? ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "md", shape: "rounded", tone: "brand", children: entity.name.charAt(0) })) : null, (0, jsx_runtime_1.jsx)("span", { id: titleId, className: "crisp-recordpanel-title", children: heading }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 18, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-body", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-sectionlabel", children: detailsLabel }), fields.map((col) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-field-value", children: (0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, emptyLabel: emptyLabel }) })] }, col.key)))] }), footer != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
|
|
82
84
|
}
|
|
83
85
|
RecordPanel.displayName = "RecordPanel";
|
|
@@ -39,6 +39,14 @@ const config = (0, core_1.defineRecipe)({
|
|
|
39
39
|
${recordstable_tag_1.recordTagCss}
|
|
40
40
|
& .crisp-recordpanel-link { color: ${tokens_1.contract.bgBrandSolid}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
41
41
|
& .crisp-recordpanel-link:hover { text-decoration: underline; }
|
|
42
|
+
|
|
43
|
+
/* #329: an optional slot for record actions (e.g. Edit). Reuses the
|
|
44
|
+
header's own measured values rather than inventing new ones -- the
|
|
45
|
+
same horizontal inset (space.3), the same hairline border token
|
|
46
|
+
(borderSubtle, flipped to border-top since this sits at the opposite
|
|
47
|
+
edge), and the header/body gap (space.2) for its own vertical padding
|
|
48
|
+
and for spacing between whatever it's given. */
|
|
49
|
+
& .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; border-top: 1px solid ${tokens_1.contract.borderSubtle}; }
|
|
42
50
|
`,
|
|
43
51
|
variants: {},
|
|
44
52
|
defaultVariants: {},
|
|
@@ -117,8 +117,14 @@ exports.ATTIO_SIDEBAR_FOOTER = {
|
|
|
117
117
|
function SectionHeader({ label, open, onToggle, }) {
|
|
118
118
|
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-section-header", "data-collapsed": open ? undefined : "", "aria-expanded": open, onClick: onToggle, children: [(0, jsx_runtime_1.jsx)(IconChevronMini, {}), (0, jsx_runtime_1.jsx)("span", { children: label })] }));
|
|
119
119
|
}
|
|
120
|
-
exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = exports.ATTIO_SIDEBAR_WORKSPACE, items = exports.ATTIO_SIDEBAR_ITEMS, recordsSection = exports.ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = exports.ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = exports.ATTIO_SIDEBAR_CHATS_SECTION, footer = exports.ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
|
|
121
|
-
|
|
120
|
+
exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = exports.ATTIO_SIDEBAR_WORKSPACE, items = exports.ATTIO_SIDEBAR_ITEMS, recordsSection = exports.ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = exports.ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = exports.ATTIO_SIDEBAR_CHATS_SECTION, groups, footer = exports.ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
|
|
121
|
+
// Seeded once, on mount, from whatever groups start collapsed — not
|
|
122
|
+
// reactive to `groups` changing later, matching every other piece of
|
|
123
|
+
// this component's state (recordsSection/listsSection/chatsSection have
|
|
124
|
+
// never supported a starting-collapsed prop at all).
|
|
125
|
+
const [collapsedSections, setCollapsedSections] = React.useState(() => new Set(groups
|
|
126
|
+
?.filter((g) => g.defaultCollapsed)
|
|
127
|
+
.map((g) => g.key ?? g.label)));
|
|
122
128
|
const [commandOpen, setCommandOpen] = React.useState(false);
|
|
123
129
|
React.useEffect(() => {
|
|
124
130
|
function handleKeyDown(event) {
|
|
@@ -143,6 +149,13 @@ exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, coll
|
|
|
143
149
|
return next;
|
|
144
150
|
});
|
|
145
151
|
}, []);
|
|
146
|
-
return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.div, { ref: ref, asChild: asChild, className: (0, sidebar_recipe_1.sidebarRecipe)({ className }), ...props, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-ws", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), (0, jsx_runtime_1.jsx)(IconChevronDown, {}), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: (0, jsx_runtime_1.jsx)(IconCollapsePanel, {}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-searchrow", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-label", children: searchLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: (0, jsx_runtime_1.jsx)(IconInbox, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, children: label }, label))) }),
|
|
152
|
+
return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.div, { ref: ref, asChild: asChild, className: (0, sidebar_recipe_1.sidebarRecipe)({ className }), ...props, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-ws", children: [(0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), (0, jsx_runtime_1.jsx)(IconChevronDown, {}), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: (0, jsx_runtime_1.jsx)(IconCollapsePanel, {}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-searchrow", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-label", children: searchLabel }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: (0, jsx_runtime_1.jsx)(IconInbox, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", active: active, icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), trailing: trailing, children: label }, label))) }), groups ? (
|
|
153
|
+
// #325: navigation as data, reusing the exact section markup/
|
|
154
|
+
// recipe above — SectionHeader + .crisp-sidebar-section — instead
|
|
155
|
+
// of the three fixed Attio sections.
|
|
156
|
+
groups.map((group) => {
|
|
157
|
+
const groupKey = group.key ?? group.label;
|
|
158
|
+
return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [(0, jsx_runtime_1.jsx)(SectionHeader, { label: group.label, open: !collapsedSections.has(groupKey), onToggle: () => toggleSection(groupKey) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has(groupKey) ? "" : undefined, children: group.items.map((item) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: item.href ?? "#", active: item.active, icon: item.icon ? (0, jsx_runtime_1.jsx)(item.icon, {}) : undefined, trailing: item.count, onClick: item.onSelect, children: item.label }, item.href ?? item.label))) })] }, groupKey));
|
|
159
|
+
})) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", shape: "rounded", tone: tone, children: (0, jsx_runtime_1.jsx)(Glyph, {}) }), children: label }, label))) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-sidebar-newlist", children: listsSection.newListLabel }) }), (0, jsx_runtime_1.jsx)(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon }) => ((0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(ItemIcon, {}), children: label }, label))) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-footer", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", className: "crisp-sidebar-pill", children: footer.gettingStartedLabel }), (0, jsx_runtime_1.jsx)(navitem_1.NavItem, { href: "#", icon: (0, jsx_runtime_1.jsx)(IconInvite, {}), children: footer.inviteLabel }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-sidebar-divider" }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-sidebar-trial", children: [(0, jsx_runtime_1.jsx)("span", { children: footer.trialText }), (0, jsx_runtime_1.jsx)(button_1.Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] }), (0, jsx_runtime_1.jsx)(command_1.Command, { open: commandOpen, onOpenChange: setCommandOpen })] }));
|
|
147
160
|
});
|
|
148
161
|
exports.Sidebar.displayName = "Sidebar";
|
|
@@ -8,7 +8,7 @@ import { bulkbarRecipe } from "./bulkbar.recipe.js";
|
|
|
8
8
|
* (e.g. `count > 0 && <BulkBar …/>`). Measured from a records table.
|
|
9
9
|
*/
|
|
10
10
|
export function BulkBar({ count, onClear, countLabel = "selected", countText = (count, label) => (_jsxs(_Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
|
|
11
|
-
return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-bulkbar-bar", children: [count != null && (_jsxs("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), _jsx(X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
|
|
11
|
+
return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-bulkbar-bar", children: [count != null && (_jsxs("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), _jsx("span", { className: "crisp-bulkbar-clearlabel", children: clearLabel }), _jsx(X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
|
|
12
12
|
}
|
|
13
13
|
BulkBar.displayName = "BulkBar";
|
|
14
14
|
/** A styled action button for use inside a `<BulkBar>` (or as a `<Menu>` trigger). */
|
|
@@ -15,6 +15,12 @@ const config = defineRecipe({
|
|
|
15
15
|
& .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; margin: 0; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
16
16
|
& .crisp-bulkbar-count:hover { background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); color: ${c.fgPrimary}; }
|
|
17
17
|
& .crisp-bulkbar-count svg { flex-shrink: 0; }
|
|
18
|
+
/* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
|
|
19
|
+
-- read, never drawn. Same technique as Slider's thumblabel. */
|
|
20
|
+
& .crisp-bulkbar-clearlabel {
|
|
21
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
22
|
+
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
|
|
23
|
+
}
|
|
18
24
|
& .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: transparent; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
19
25
|
& .crisp-bulkbar-btn:hover { background: ${c.bgRaised}; }
|
|
20
26
|
& .crisp-bulkbar-btn--danger { color: ${c.bgDanger}; }
|
|
@@ -26,7 +26,8 @@ function FieldValue({ col, row, emptyLabel, }) {
|
|
|
26
26
|
* relative` container (e.g. the app main area); it slides in from the right and
|
|
27
27
|
* overlays the content.
|
|
28
28
|
*/
|
|
29
|
-
export function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", detailsLabel = "Details", emptyLabel = "Empty", ...rest }) {
|
|
29
|
+
export function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", title, detailsLabel = "Details", emptyLabel = "Empty", footer, ...rest }) {
|
|
30
|
+
const titleId = React.useId();
|
|
30
31
|
// close on Escape while open
|
|
31
32
|
React.useEffect(() => {
|
|
32
33
|
if (!record)
|
|
@@ -42,6 +43,7 @@ export function RecordPanel({ record, columns, onClose, className, closeLabel =
|
|
|
42
43
|
? record[entityCol.key]
|
|
43
44
|
: undefined;
|
|
44
45
|
const fields = columns.filter((c) => c.type !== "entity");
|
|
45
|
-
|
|
46
|
+
const heading = title ?? entity?.name ?? "Record";
|
|
47
|
+
return (_jsxs("div", { className: recordpanelRecipe({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [_jsxs("div", { className: "crisp-recordpanel-header", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpanel-fav", src: entity.avatar, alt: "" })) : entity ? (_jsx(Avatar, { size: "md", shape: "rounded", tone: "brand", children: entity.name.charAt(0) })) : null, _jsx("span", { id: titleId, className: "crisp-recordpanel-title", children: heading }), _jsx("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 18, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpanel-body", children: [_jsx("div", { className: "crisp-recordpanel-sectionlabel", children: detailsLabel }), fields.map((col) => (_jsxs("div", { className: "crisp-recordpanel-field", children: [_jsxs("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? _jsx(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), _jsx("span", { className: "crisp-recordpanel-field-value", children: _jsx(FieldValue, { col: col, row: record, emptyLabel: emptyLabel }) })] }, col.key)))] }), footer != null && (_jsx("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
|
|
46
48
|
}
|
|
47
49
|
RecordPanel.displayName = "RecordPanel";
|
|
@@ -36,6 +36,14 @@ const config = defineRecipe({
|
|
|
36
36
|
${recordTagCss}
|
|
37
37
|
& .crisp-recordpanel-link { color: ${c.bgBrandSolid}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
38
38
|
& .crisp-recordpanel-link:hover { text-decoration: underline; }
|
|
39
|
+
|
|
40
|
+
/* #329: an optional slot for record actions (e.g. Edit). Reuses the
|
|
41
|
+
header's own measured values rather than inventing new ones -- the
|
|
42
|
+
same horizontal inset (space.3), the same hairline border token
|
|
43
|
+
(borderSubtle, flipped to border-top since this sits at the opposite
|
|
44
|
+
edge), and the header/body gap (space.2) for its own vertical padding
|
|
45
|
+
and for spacing between whatever it's given. */
|
|
46
|
+
& .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: ${c.space["2"]}; padding: ${c.space["2"]} ${c.space["3"]}; border-top: 1px solid ${c.borderSubtle}; }
|
|
39
47
|
`,
|
|
40
48
|
variants: {},
|
|
41
49
|
defaultVariants: {},
|
|
@@ -1,4 +1,4 @@
|
|
|
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 { Building2, ChartNoAxesColumn, ChevronDown, CircleDollarSign, FileText, House, Inbox, LayoutGrid, Mail, MessageSquare, PanelLeft, Phone, Search, Send, SquareCheck, User, UserPlus, Users, Waypoints, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Avatar } from "../../components/avatar/index.js";
|
|
@@ -81,8 +81,14 @@ export const ATTIO_SIDEBAR_FOOTER = {
|
|
|
81
81
|
function SectionHeader({ label, open, onToggle, }) {
|
|
82
82
|
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 })] }));
|
|
83
83
|
}
|
|
84
|
-
export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = ATTIO_SIDEBAR_WORKSPACE, items = ATTIO_SIDEBAR_ITEMS, recordsSection = ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = ATTIO_SIDEBAR_CHATS_SECTION, footer = ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
|
|
85
|
-
|
|
84
|
+
export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = ATTIO_SIDEBAR_WORKSPACE, items = ATTIO_SIDEBAR_ITEMS, recordsSection = ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = ATTIO_SIDEBAR_CHATS_SECTION, groups, footer = ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
|
|
85
|
+
// Seeded once, on mount, from whatever groups start collapsed — not
|
|
86
|
+
// reactive to `groups` changing later, matching every other piece of
|
|
87
|
+
// this component's state (recordsSection/listsSection/chatsSection have
|
|
88
|
+
// never supported a starting-collapsed prop at all).
|
|
89
|
+
const [collapsedSections, setCollapsedSections] = React.useState(() => new Set(groups
|
|
90
|
+
?.filter((g) => g.defaultCollapsed)
|
|
91
|
+
.map((g) => g.key ?? g.label)));
|
|
86
92
|
const [commandOpen, setCommandOpen] = React.useState(false);
|
|
87
93
|
React.useEffect(() => {
|
|
88
94
|
function handleKeyDown(event) {
|
|
@@ -107,6 +113,13 @@ export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse,
|
|
|
107
113
|
return next;
|
|
108
114
|
});
|
|
109
115
|
}, []);
|
|
110
|
-
return (_jsxs(Primitive.div, { ref: ref, asChild: asChild, className: sidebarRecipe({ className }), ...props, children: [_jsxs("div", { 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, {}), _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 }) => (_jsx(NavItem, { href: "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, children: label }, label))) }),
|
|
116
|
+
return (_jsxs(Primitive.div, { ref: ref, asChild: asChild, className: sidebarRecipe({ className }), ...props, children: [_jsxs("div", { 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, {}), _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 }) => (_jsx(NavItem, { href: "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, children: label }, label))) }), groups ? (
|
|
117
|
+
// #325: navigation as data, reusing the exact section markup/
|
|
118
|
+
// recipe above — SectionHeader + .crisp-sidebar-section — instead
|
|
119
|
+
// of the three fixed Attio sections.
|
|
120
|
+
groups.map((group) => {
|
|
121
|
+
const groupKey = group.key ?? group.label;
|
|
122
|
+
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));
|
|
123
|
+
})) : (_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 }) => (_jsx(NavItem, { href: "#", icon: _jsx(ItemIcon, {}), children: label }, label))) })] })), _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(Command, { open: commandOpen, onOpenChange: setCommandOpen })] }));
|
|
111
124
|
});
|
|
112
125
|
Sidebar.displayName = "Sidebar";
|
package/dist/recordpanel.css
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-orange) 30%, transparent); }
|
|
26
26
|
& .crisp-recordpanel-link { color: var(--crisp-bg-brand-solid); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
27
27
|
& .crisp-recordpanel-link:hover { text-decoration: underline; }
|
|
28
|
+
/* #329: an optional slot for record actions (e.g. Edit). Reuses the header's own measured values rather than inventing new ones -- the
|
|
29
|
+
same horizontal inset (space.3), the same hairline border token
|
|
30
|
+
(borderSubtle, flipped to border-top since this sits at the opposite
|
|
31
|
+
edge), and the header/body gap (space.2) for its own vertical padding
|
|
32
|
+
and for spacing between whatever it's given. */
|
|
33
|
+
& .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: var(--crisp-space-2); padding: var(--crisp-space-2) var(--crisp-space-3); border-top: 1px solid var(--crisp-border-subtle); }
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-orange) 30%, transparent); }
|
|
27
27
|
& .crisp-recordpanel-link { color: var(--crisp-bg-brand-solid); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
28
28
|
& .crisp-recordpanel-link:hover { text-decoration: underline; }
|
|
29
|
+
/* #329: an optional slot for record actions (e.g. Edit). Reuses the header's own measured values rather than inventing new ones -- the
|
|
30
|
+
same horizontal inset (space.3), the same hairline border token
|
|
31
|
+
(borderSubtle, flipped to border-top since this sits at the opposite
|
|
32
|
+
edge), and the header/body gap (space.2) for its own vertical padding
|
|
33
|
+
and for spacing between whatever it's given. */
|
|
34
|
+
& .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: var(--crisp-space-2); padding: var(--crisp-space-2) var(--crisp-space-3); border-top: 1px solid var(--crisp-border-subtle); }
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
@media (prefers-reduced-motion: reduce) {
|
package/dist/styles.css
CHANGED
|
@@ -283,6 +283,9 @@
|
|
|
283
283
|
& .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
284
284
|
& .crisp-bulkbar-count:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); color: var(--crisp-fg-primary); }
|
|
285
285
|
& .crisp-bulkbar-count svg { flex-shrink: 0; }
|
|
286
|
+
/* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
|
|
287
|
+
-- read, never drawn. Same technique as Slider's thumblabel. */
|
|
288
|
+
& .crisp-bulkbar-clearlabel { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
286
289
|
& .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
287
290
|
& .crisp-bulkbar-btn:hover { background: var(--crisp-bg-raised); }
|
|
288
291
|
& .crisp-bulkbar-btn--danger { color: var(--crisp-bg-danger); }
|
|
@@ -2062,6 +2065,12 @@
|
|
|
2062
2065
|
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-orange) 30%, transparent); }
|
|
2063
2066
|
& .crisp-recordpanel-link { color: var(--crisp-bg-brand-solid); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
2064
2067
|
& .crisp-recordpanel-link:hover { text-decoration: underline; }
|
|
2068
|
+
/* #329: an optional slot for record actions (e.g. Edit). Reuses the header's own measured values rather than inventing new ones -- the
|
|
2069
|
+
same horizontal inset (space.3), the same hairline border token
|
|
2070
|
+
(borderSubtle, flipped to border-top since this sits at the opposite
|
|
2071
|
+
edge), and the header/body gap (space.2) for its own vertical padding
|
|
2072
|
+
and for spacing between whatever it's given. */
|
|
2073
|
+
& .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: var(--crisp-space-2); padding: var(--crisp-space-2) var(--crisp-space-3); border-top: 1px solid var(--crisp-border-subtle); }
|
|
2065
2074
|
}
|
|
2066
2075
|
|
|
2067
2076
|
@media (prefers-reduced-motion: reduce) {
|
package/dist/styles.layered.css
CHANGED
|
@@ -284,6 +284,9 @@
|
|
|
284
284
|
& .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
285
285
|
& .crisp-bulkbar-count:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); color: var(--crisp-fg-primary); }
|
|
286
286
|
& .crisp-bulkbar-count svg { flex-shrink: 0; }
|
|
287
|
+
/* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
|
|
288
|
+
-- read, never drawn. Same technique as Slider's thumblabel. */
|
|
289
|
+
& .crisp-bulkbar-clearlabel { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
287
290
|
& .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
288
291
|
& .crisp-bulkbar-btn:hover { background: var(--crisp-bg-raised); }
|
|
289
292
|
& .crisp-bulkbar-btn--danger { color: var(--crisp-bg-danger); }
|
|
@@ -2063,6 +2066,12 @@
|
|
|
2063
2066
|
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-orange) 30%, transparent); }
|
|
2064
2067
|
& .crisp-recordpanel-link { color: var(--crisp-bg-brand-solid); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
2065
2068
|
& .crisp-recordpanel-link:hover { text-decoration: underline; }
|
|
2069
|
+
/* #329: an optional slot for record actions (e.g. Edit). Reuses the header's own measured values rather than inventing new ones -- the
|
|
2070
|
+
same horizontal inset (space.3), the same hairline border token
|
|
2071
|
+
(borderSubtle, flipped to border-top since this sits at the opposite
|
|
2072
|
+
edge), and the header/body gap (space.2) for its own vertical padding
|
|
2073
|
+
and for spacing between whatever it's given. */
|
|
2074
|
+
& .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: var(--crisp-space-2); padding: var(--crisp-space-2) var(--crisp-space-3); border-top: 1px solid var(--crisp-border-subtle); }
|
|
2066
2075
|
}
|
|
2067
2076
|
|
|
2068
2077
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -4,10 +4,20 @@ export type RecordPanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "chil
|
|
|
4
4
|
/** The record to show; render nothing when null. */
|
|
5
5
|
/** Label for the close control. */
|
|
6
6
|
closeLabel?: string;
|
|
7
|
+
/**
|
|
8
|
+
* The panel's heading, and so its accessible name (via `aria-labelledby`,
|
|
9
|
+
* not a stringified `aria-label` — it also drives the visible title, which
|
|
10
|
+
* can be any node). Defaults to the entity column's name, same as before
|
|
11
|
+
* this prop existed; pass it to title the panel by something else, e.g.
|
|
12
|
+
* "{Collection} record".
|
|
13
|
+
*/
|
|
14
|
+
title?: React.ReactNode;
|
|
7
15
|
/** Heading above the field list. */
|
|
8
16
|
detailsLabel?: React.ReactNode;
|
|
9
17
|
/** Shown in place of a field that has no value. */
|
|
10
18
|
emptyLabel?: React.ReactNode;
|
|
19
|
+
/** Rendered below the field list, e.g. an Edit button. Omit for no footer. */
|
|
20
|
+
footer?: React.ReactNode;
|
|
11
21
|
record: Row | null;
|
|
12
22
|
columns: Column[];
|
|
13
23
|
onClose: () => void;
|
|
@@ -19,7 +29,7 @@ export type RecordPanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "chil
|
|
|
19
29
|
* relative` container (e.g. the app main area); it slides in from the right and
|
|
20
30
|
* overlays the content.
|
|
21
31
|
*/
|
|
22
|
-
export declare function RecordPanel({ record, columns, onClose, className, closeLabel, detailsLabel, emptyLabel, ...rest }: RecordPanelProps): React.JSX.Element | null;
|
|
32
|
+
export declare function RecordPanel({ record, columns, onClose, className, closeLabel, title, detailsLabel, emptyLabel, footer, ...rest }: RecordPanelProps): React.JSX.Element | null;
|
|
23
33
|
export declare namespace RecordPanel {
|
|
24
34
|
var displayName: string;
|
|
25
35
|
}
|
|
@@ -40,6 +40,42 @@ export type SidebarListsSection = {
|
|
|
40
40
|
label: string;
|
|
41
41
|
newListLabel: string;
|
|
42
42
|
};
|
|
43
|
+
/** A row in a `groups` section (#325). */
|
|
44
|
+
export type SidebarNavGroupItem = {
|
|
45
|
+
label: string;
|
|
46
|
+
/** 16px icon component, rendered `<Icon />`. Optional — not every group's
|
|
47
|
+
* rows carry one (unlike the main nav's `SidebarNavItem`, where it's
|
|
48
|
+
* required). */
|
|
49
|
+
icon?: React.ComponentType<IconProps>;
|
|
50
|
+
/** Either this or `onSelect` — a plain link, or a handler for anything
|
|
51
|
+
* that isn't real navigation (opening a panel, selecting a view). Both
|
|
52
|
+
* may be set; neither is required to make the row *something* clickable
|
|
53
|
+
* (it still renders as a link, defaulting `href` to "#"). */
|
|
54
|
+
href?: string;
|
|
55
|
+
onSelect?: () => void;
|
|
56
|
+
active?: boolean;
|
|
57
|
+
/** Trailing content — a count, a badge, anything `NavItem`'s own
|
|
58
|
+
* `trailing` slot already takes. */
|
|
59
|
+
count?: React.ReactNode;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* A named, collapsible group of `SidebarNavGroupItem` rows — navigation as
|
|
63
|
+
* data, for a consumer whose sections aren't Attio's Records/Lists/Chats
|
|
64
|
+
* (#325). Pass `groups` to `Sidebar` and it replaces those three sections
|
|
65
|
+
* entirely; a group's `items` can come from runtime data (a re-render with a
|
|
66
|
+
* new array is all a group needs to pick up new rows).
|
|
67
|
+
*/
|
|
68
|
+
export type SidebarNavGroup = {
|
|
69
|
+
/** Identity for this group's own collapse state across renders. Falls
|
|
70
|
+
* back to `label` — set it explicitly if two groups could ever share a
|
|
71
|
+
* label, or if a group's label itself changes at runtime. */
|
|
72
|
+
key?: string;
|
|
73
|
+
label: string;
|
|
74
|
+
items: SidebarNavGroupItem[];
|
|
75
|
+
/** Starts this group collapsed (default: expanded, matching every
|
|
76
|
+
* existing section). Read once, on mount. */
|
|
77
|
+
defaultCollapsed?: boolean;
|
|
78
|
+
};
|
|
43
79
|
/** Footer copy: the onboarding pill, the invite row, and the trial banner. */
|
|
44
80
|
export type SidebarFooter = {
|
|
45
81
|
gettingStartedLabel: string;
|
|
@@ -67,6 +103,16 @@ export type SidebarProps = React.ComponentPropsWithoutRef<"nav"> & {
|
|
|
67
103
|
listsSection?: SidebarListsSection;
|
|
68
104
|
/** The Chats section (label + nav-item rows). */
|
|
69
105
|
chatsSection?: SidebarSection;
|
|
106
|
+
/**
|
|
107
|
+
* Navigation as data (#325): any number of named, collapsible groups,
|
|
108
|
+
* reusing the same section markup/recipe as Records/Lists/Chats. When
|
|
109
|
+
* given, it REPLACES those three sections entirely — a consumer whose
|
|
110
|
+
* sections aren't Attio's (internal/grc's Activity, Governance, Identity,
|
|
111
|
+
* Organization, System Administration, plus user-built registers) passes
|
|
112
|
+
* `groups` instead of the three fixed props. Omit it and `<Sidebar />`
|
|
113
|
+
* renders exactly as before this prop existed.
|
|
114
|
+
*/
|
|
115
|
+
groups?: SidebarNavGroup[];
|
|
70
116
|
/** Footer copy (onboarding pill, invite row, trial banner). */
|
|
71
117
|
footer?: SidebarFooter;
|
|
72
118
|
};
|
|
@@ -102,6 +148,16 @@ export declare const Sidebar: React.ForwardRefExoticComponent<Omit<React.Detaile
|
|
|
102
148
|
listsSection?: SidebarListsSection;
|
|
103
149
|
/** The Chats section (label + nav-item rows). */
|
|
104
150
|
chatsSection?: SidebarSection;
|
|
151
|
+
/**
|
|
152
|
+
* Navigation as data (#325): any number of named, collapsible groups,
|
|
153
|
+
* reusing the same section markup/recipe as Records/Lists/Chats. When
|
|
154
|
+
* given, it REPLACES those three sections entirely — a consumer whose
|
|
155
|
+
* sections aren't Attio's (internal/grc's Activity, Governance, Identity,
|
|
156
|
+
* Organization, System Administration, plus user-built registers) passes
|
|
157
|
+
* `groups` instead of the three fixed props. Omit it and `<Sidebar />`
|
|
158
|
+
* renders exactly as before this prop existed.
|
|
159
|
+
*/
|
|
160
|
+
groups?: SidebarNavGroup[];
|
|
105
161
|
/** Footer copy (onboarding pill, invite row, trial banner). */
|
|
106
162
|
footer?: SidebarFooter;
|
|
107
163
|
} & React.RefAttributes<HTMLDivElement>>;
|