@crisp-ui-kit/crisp 0.40.0 → 0.42.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.
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ContextMenu = ContextMenu;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
+ const react_slot_1 = require("@radix-ui/react-slot");
38
39
  const React = __importStar(require("react"));
39
40
  const core_1 = require("../../core");
40
41
  const menu_1 = require("../menu");
@@ -44,7 +45,7 @@ const contextmenu_recipe_1 = require("./contextmenu.recipe");
44
45
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
45
46
  * edges), closing on select, outside pointer-down, and Escape.
46
47
  */
47
- function ContextMenu({ children, items, menuLabel = "Context menu", className, }) {
48
+ function ContextMenu({ children, items, menuLabel = "Context menu", className, asChild = false, }) {
48
49
  const rootRef = React.useRef(null);
49
50
  const menuRef = React.useRef(null);
50
51
  const returnFocusTo = React.useRef(null);
@@ -109,7 +110,7 @@ function ContextMenu({ children, items, menuLabel = "Context menu", className, }
109
110
  document.removeEventListener("keydown", onKey);
110
111
  };
111
112
  }, [pos, close]);
112
- return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, contextmenu_recipe_1.contextmenuRecipe)({ className }), children: [(0, jsx_runtime_1.jsx)("div", { onContextMenu: open, children: children }), pos && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-contextmenu-sep" }, i)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
113
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [asChild ? ((0, jsx_runtime_1.jsx)(react_slot_1.Slot, { ref: rootRef, className: className, onContextMenu: open, children: children })) : ((0, jsx_runtime_1.jsx)("div", { ref: rootRef, className: (0, contextmenu_recipe_1.contextmenuRecipe)({ className }), children: (0, jsx_runtime_1.jsx)("div", { onContextMenu: open, children: children }) })), pos && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-contextmenu-sep" }, i)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
113
114
  ? "crisp-contextmenu-item crisp-contextmenu-item--danger"
114
115
  : "crisp-contextmenu-item", tabIndex: -1, onClick: () => {
115
116
  item.onSelect?.();
@@ -59,9 +59,14 @@ const config = (0, core_1.defineRecipe)({
59
59
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
60
60
  position: relative; z-index: 2; background: ${tokens_1.contract.bgSurface};
61
61
  }
62
+ /* fgNeutralMuted, not seed's fgNeutralSubtle (#367): 3.42:1 in light
63
+ against bgSurface, fails AA for this 12px text. Not an Attio
64
+ measurement -- accessibility wins over the seed reference.
65
+ neutralMuted clears it, 6.62:1 light / 14.71:1 dark (dark was
66
+ already fine at 9.43:1). */
62
67
  & .crisp-datepicker-weekday {
63
68
  display: flex; align-items: center; justify-content: center;
64
- height: ${tokens_1.contract.size.calendarCell}; color: ${tokens_1.contract.fgNeutralSubtle};
69
+ height: ${tokens_1.contract.size.calendarCell}; color: ${tokens_1.contract.fgNeutralMuted};
65
70
  font-size: ${tokens_1.contract.text.sm}; line-height: 16px; font-weight: ${tokens_1.contract.weight.medium};
66
71
  }
67
72
 
@@ -37,7 +37,7 @@ exports.Primitive = void 0;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const react_slot_1 = require("@radix-ui/react-slot");
39
39
  const React = __importStar(require("react"));
40
- const NODES = ["button", "div", "span", "a", "input", "p"];
40
+ const NODES = ["button", "div", "span", "a", "input", "p", "nav"];
41
41
  exports.Primitive = NODES.reduce((acc, node) => {
42
42
  const Component = React.forwardRef(({ asChild, ...props }, ref) => {
43
43
  const Comp = asChild ? react_slot_1.Slot : node;
@@ -117,7 +117,7 @@ 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, groups, footer = exports.ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
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, commandItems, commandOpen: commandOpenProp, defaultCommandOpen, onCommandOpenChange, commandShortcut = true, "aria-label": ariaLabel = "Main", ...props }, ref) => {
121
121
  // Seeded once, on mount, from whatever groups start collapsed — not
122
122
  // reactive to `groups` changing later, matching every other piece of
123
123
  // this component's state (recordsSection/listsSection/chatsSection have
@@ -125,8 +125,22 @@ exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, coll
125
125
  const [collapsedSections, setCollapsedSections] = React.useState(() => new Set(groups
126
126
  ?.filter((g) => g.defaultCollapsed)
127
127
  .map((g) => g.key ?? g.label)));
128
- const [commandOpen, setCommandOpen] = React.useState(false);
128
+ // Controlled/uncontrolled triad (#364), same shape as Popover/Menu's own
129
+ // `open` -- an app driving a single ⌘K listener needs to be able to set
130
+ // `commandOpen` itself, not just read it.
131
+ const [internalCommandOpen, setInternalCommandOpen] = React.useState(defaultCommandOpen ?? false);
132
+ const commandOpen = commandOpenProp ?? internalCommandOpen;
133
+ const setCommandOpen = React.useCallback((next) => {
134
+ if (commandOpenProp === undefined)
135
+ setInternalCommandOpen(next);
136
+ onCommandOpenChange?.(next);
137
+ }, [commandOpenProp, onCommandOpenChange]);
129
138
  React.useEffect(() => {
139
+ // #364: skip attaching entirely when the app owns its own ⌘K listener
140
+ // -- two listeners on the same key both firing opened two overlapping
141
+ // command palettes.
142
+ if (!commandShortcut)
143
+ return;
130
144
  function handleKeyDown(event) {
131
145
  if ((event.metaKey || event.ctrlKey) &&
132
146
  event.key.toLowerCase() === "k") {
@@ -136,7 +150,7 @@ exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, coll
136
150
  }
137
151
  document.addEventListener("keydown", handleKeyDown);
138
152
  return () => document.removeEventListener("keydown", handleKeyDown);
139
- }, []);
153
+ }, [commandShortcut, setCommandOpen]);
140
154
  const toggleSection = React.useCallback((key) => {
141
155
  setCollapsedSections((prev) => {
142
156
  const next = new Set(prev);
@@ -149,13 +163,13 @@ exports.Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, coll
149
163
  return next;
150
164
  });
151
165
  }, []);
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 ? (
166
+ return ((0, jsx_runtime_1.jsxs)(core_1.Primitive.nav, { ref: ref, asChild: asChild, "aria-label": ariaLabel, 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
167
  // #325: navigation as data, reusing the exact section markup/
154
168
  // recipe above — SectionHeader + .crisp-sidebar-section — instead
155
169
  // of the three fixed Attio sections.
156
170
  groups.map((group) => {
157
171
  const groupKey = group.key ?? group.label;
158
172
  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 })] }));
173
+ })) : ((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, items: commandItems })] }));
160
174
  });
161
175
  exports.Sidebar.displayName = "Sidebar";
@@ -11,47 +11,47 @@
11
11
  & .crisp-datepicker-nav-button:focus-visible { outline-color: var(--crisp-stroke-focus-ring); }
12
12
  & .crisp-datepicker-nav-button:disabled { opacity: .4; cursor: not-allowed; }
13
13
  & .crisp-datepicker-weekday-row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); position: relative; z-index: 2; background: var(--crisp-bg-surface); }
14
- & .crisp-datepicker-weekday { display: flex; align-items: center; justify-content: center; height: var(--crisp-size-calendarCell); color: var(--crisp-fg-neutral-subtle); font-size: var(--crisp-text-sm); line-height: 16px; font-weight: var(--crisp-weight-medium); }
14
+ /* fgNeutralMuted, not seed's fgNeutralSubtle (#367): 3.42:1 in light
15
+ against bgSurface, fails AA for this 12px text. Not an Attio
16
+ measurement -- accessibility wins over the seed reference.
17
+ neutralMuted clears it, 6.62:1 light / 14.71:1 dark (dark was
18
+ already fine at 9.43:1). */
19
+ & .crisp-datepicker-weekday {
20
+ display: flex; align-items: center; justify-content: center;
21
+ height: var(--crisp-size-calendarCell); color: var(--crisp-fg-neutral-muted);
22
+ font-size: var(--crisp-text-sm); line-height: 16px; font-weight: var(--crisp-weight-medium);
23
+ }
24
+
15
25
  & .crisp-datepicker-months { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; min-width: 0; }
16
26
  & .crisp-datepicker-months[data-wheel-open] { visibility: hidden; }
17
27
  & .crisp-datepicker-month { min-width: 0; }
18
- & .crisp-datepicker-month-label { display: flex; align-items: center; height: 24px; color: var(--crisp-fg-primary); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); }
28
+ & .crisp-datepicker-month-label {
29
+ display: flex; align-items: center; height: 24px;
30
+ color: var(--crisp-fg-primary); font-size: var(--crisp-text-lg); line-height: 20px;
31
+ font-weight: var(--crisp-weight-medium);
32
+ }
33
+
19
34
  & .crisp-datepicker-grid { display: grid; min-width: 0; }
20
35
  & .crisp-datepicker-week-row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
21
- & .crisp-datepicker-cell { position: relative; min-width: 0; min-height: var(--crisp-size-calendarCell); isolation: isolate; }
22
- & .crisp-datepicker-empty-cell { min-height: var(--crisp-size-calendarCell); }
23
- /* The bridge joining days inside a completed range. The endpoints do NOT carry data-in-range -- only the interior days do -- so these are three independent rules, not a base plus two modifiers. A modifier that sets only inset-inline-start never generates a pseudo element on an endpoint, which leaves a half-cell of white between the endpoint's pill and the run of grey beside it. Each rule declares its
24
- own content. */
25
- & .crisp-datepicker-cell[data-range-complete][data-in-range]::before {
26
- content: ""; position: absolute; z-index: -1; inset-inline: 0;
27
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
28
- }
29
- & .crisp-datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
30
- content: ""; position: absolute; z-index: -1;
31
- inset-inline-start: 50%; inset-inline-end: 0;
32
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
33
- }
34
- & .crisp-datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
35
- content: ""; position: absolute; z-index: -1;
36
- inset-inline-start: 0; inset-inline-end: 50%;
37
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
38
- }
39
36
 
40
- & .crisp-datepicker-date {
41
- position: relative; display: flex; width: 100%; height: 100%;
42
- min-height: var(--crisp-size-calendarCell); min-width: 0;
43
- align-items: flex-start; justify-content: center;
44
- border: 0; padding: 0; background: transparent; cursor: pointer;
45
- color: var(--crisp-fg-primary); font: inherit; outline: none;
37
+ & .crisp-datepicker-cell {
38
+ position: relative; min-width: 0; min-height: var(--crisp-size-calendarCell);
46
39
  isolation: isolate;
47
40
  }
41
+ & .crisp-datepicker-empty-cell { min-height: var(--crisp-size-calendarCell); }
42
+
43
+ /* The bridge joining days inside a completed range.
44
+ The endpoints do NOT carry data-in-range -- only the interior days do --
45
+ so these are three independent rules, not a base plus two modifiers. A
46
+ modifier that sets only inset-inline-start never generates a pseudo
47
+ element on an endpoint, which leaves a half-cell of white between the
48
+ endpoint's pill and the run of grey beside it. Each rule declares its own content. */
49
+ & .crisp-datepicker-cell[data-range-complete][data-in-range]::before { content: ""; position: absolute; z-index: -1; inset-inline: 0; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
50
+ & .crisp-datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before { content: ""; position: absolute; z-index: -1; inset-inline-start: 50%; inset-inline-end: 0; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
51
+ & .crisp-datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before { content: ""; position: absolute; z-index: -1; inset-inline-start: 0; inset-inline-end: 50%; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
52
+ & .crisp-datepicker-date { position: relative; display: flex; width: 100%; height: 100%; min-height: var(--crisp-size-calendarCell); min-width: 0; align-items: flex-start; justify-content: center; border: 0; padding: 0; background: transparent; cursor: pointer; color: var(--crisp-fg-primary); font: inherit; outline: none; isolation: isolate; }
48
53
  /* the round day highlight, behind the numeral */
49
- & .crisp-datepicker-date::before {
50
- content: ""; position: absolute; z-index: -1; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%;
51
- width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill);
52
- border-radius: var(--crisp-radius-md);
53
- background: transparent; transform: translateX(-50%);
54
- }
54
+ & .crisp-datepicker-date::before { content: ""; position: absolute; z-index: -1; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%; width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill); border-radius: var(--crisp-radius-md); background: transparent; transform: translateX(-50%); }
55
55
  & .crisp-datepicker-date[data-today]::before { background: var(--crisp-bg-neutral-weak); }
56
56
  & .crisp-datepicker-date[data-selected]::before { background: var(--crisp-bg-brand-solid); }
57
57
  & .crisp-datepicker-date[data-selected] { color: var(--crisp-fg-on-brand); }
@@ -59,34 +59,35 @@
59
59
  & .crisp-datepicker-date[data-in-range]::before { background: transparent; }
60
60
  & .crisp-datepicker-date[data-unavailable] { color: var(--crisp-fg-disabled); }
61
61
  & .crisp-datepicker-date:disabled { color: var(--crisp-fg-disabled); cursor: not-allowed; }
62
- & .crisp-datepicker-date:focus-visible::after {
63
- content: ""; position: absolute; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%;
64
- width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill);
65
- transform: translateX(-50%); border-radius: var(--crisp-radius-md);
66
- outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px;
62
+ & .crisp-datepicker-date:focus-visible::after { content: ""; position: absolute; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%; width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill); transform: translateX(-50%); border-radius: var(--crisp-radius-md); outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px; }
63
+ & .crisp-datepicker-date-content { position: relative; z-index: 1; display: flex; width: 100%; min-height: var(--crisp-size-calendarCell); min-width: 0; box-sizing: border-box; flex-direction: column; align-items: center; gap: 3px; padding-top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); color: inherit; font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); font-variant-numeric: tabular-nums; }
64
+ /* The numeral is its own 42px flex box so it centres on the 42px pill. Without this the content column top-aligns the glyph and the pill reads as offset — the cell is 48px, the pill 42px inset 3px, and only a 42px-tall numeral box shares the pill's centre line. */
65
+ & .crisp-datepicker-date-content > [data-date-picker-day] {
66
+ display: flex; min-height: var(--crisp-size-calendarPill); flex-shrink: 0;
67
+ align-items: center; justify-content: center;
67
68
  }
68
69
 
69
- & .crisp-datepicker-date-content {
70
- position: relative; z-index: 1; display: flex; width: 100%;
71
- min-height: var(--crisp-size-calendarCell); min-width: 0; box-sizing: border-box;
72
- flex-direction: column; align-items: center; gap: 3px;
73
- padding-top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); color: inherit;
74
- font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium);
75
- font-variant-numeric: tabular-nums;
76
- }
77
- /* The numeral is its own 42px flex box so it centres on the 42px pill.
78
- Without this the content column top-aligns the glyph and the pill reads
79
- as offset — the cell is 48px, the pill 42px inset 3px, and only a
80
- 42px-tall numeral box shares the pill's centre line. */
81
- & .crisp-datepicker-date-content > [data-date-picker-day] { display: flex; min-height: var(--crisp-size-calendarPill); flex-shrink: 0; align-items: center; justify-content: center; }
82
70
  /* the year/month wheel, laid over the grid it replaces */
83
- & .crisp-datepicker-wheel-container { position: absolute; z-index: 1; inset-inline: 0; top: var(--crisp-size-menuRow); bottom: 0; display: flex; align-items: center; overflow: hidden; background: var(--crisp-bg-surface); }
71
+ & .crisp-datepicker-wheel-container {
72
+ position: absolute; z-index: 1; inset-inline: 0;
73
+ top: var(--crisp-size-menuRow); bottom: 0;
74
+ display: flex; align-items: center; overflow: hidden;
75
+ background: var(--crisp-bg-surface);
76
+ }
84
77
  & .crisp-datepicker-wheel { width: 100%; }
85
- & .crisp-datepicker-continuous-scroll { height: 100%; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none; }
78
+
79
+ & .crisp-datepicker-continuous-scroll {
80
+ height: 100%; min-height: 0; overflow-y: auto;
81
+ overscroll-behavior: contain; scrollbar-width: none;
82
+ }
86
83
  & .crisp-datepicker-continuous-scroll::-webkit-scrollbar { display: none; }
87
84
  & .crisp-datepicker-continuous-content { display: flex; flex-direction: column; }
85
+
88
86
  /* announced, never drawn */
89
- & .crisp-datepicker-live-region { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
87
+ & .crisp-datepicker-live-region {
88
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
89
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
90
+ }
90
91
  }
91
92
  .crisp-datepicker--visibleRange_month {
92
93
 
@@ -12,47 +12,47 @@
12
12
  & .crisp-datepicker-nav-button:focus-visible { outline-color: var(--crisp-stroke-focus-ring); }
13
13
  & .crisp-datepicker-nav-button:disabled { opacity: .4; cursor: not-allowed; }
14
14
  & .crisp-datepicker-weekday-row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); position: relative; z-index: 2; background: var(--crisp-bg-surface); }
15
- & .crisp-datepicker-weekday { display: flex; align-items: center; justify-content: center; height: var(--crisp-size-calendarCell); color: var(--crisp-fg-neutral-subtle); font-size: var(--crisp-text-sm); line-height: 16px; font-weight: var(--crisp-weight-medium); }
15
+ /* fgNeutralMuted, not seed's fgNeutralSubtle (#367): 3.42:1 in light
16
+ against bgSurface, fails AA for this 12px text. Not an Attio
17
+ measurement -- accessibility wins over the seed reference.
18
+ neutralMuted clears it, 6.62:1 light / 14.71:1 dark (dark was
19
+ already fine at 9.43:1). */
20
+ & .crisp-datepicker-weekday {
21
+ display: flex; align-items: center; justify-content: center;
22
+ height: var(--crisp-size-calendarCell); color: var(--crisp-fg-neutral-muted);
23
+ font-size: var(--crisp-text-sm); line-height: 16px; font-weight: var(--crisp-weight-medium);
24
+ }
25
+
16
26
  & .crisp-datepicker-months { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; min-width: 0; }
17
27
  & .crisp-datepicker-months[data-wheel-open] { visibility: hidden; }
18
28
  & .crisp-datepicker-month { min-width: 0; }
19
- & .crisp-datepicker-month-label { display: flex; align-items: center; height: 24px; color: var(--crisp-fg-primary); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); }
29
+ & .crisp-datepicker-month-label {
30
+ display: flex; align-items: center; height: 24px;
31
+ color: var(--crisp-fg-primary); font-size: var(--crisp-text-lg); line-height: 20px;
32
+ font-weight: var(--crisp-weight-medium);
33
+ }
34
+
20
35
  & .crisp-datepicker-grid { display: grid; min-width: 0; }
21
36
  & .crisp-datepicker-week-row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
22
- & .crisp-datepicker-cell { position: relative; min-width: 0; min-height: var(--crisp-size-calendarCell); isolation: isolate; }
23
- & .crisp-datepicker-empty-cell { min-height: var(--crisp-size-calendarCell); }
24
- /* The bridge joining days inside a completed range. The endpoints do NOT carry data-in-range -- only the interior days do -- so these are three independent rules, not a base plus two modifiers. A modifier that sets only inset-inline-start never generates a pseudo element on an endpoint, which leaves a half-cell of white between the endpoint's pill and the run of grey beside it. Each rule declares its
25
- own content. */
26
- & .crisp-datepicker-cell[data-range-complete][data-in-range]::before {
27
- content: ""; position: absolute; z-index: -1; inset-inline: 0;
28
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
29
- }
30
- & .crisp-datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
31
- content: ""; position: absolute; z-index: -1;
32
- inset-inline-start: 50%; inset-inline-end: 0;
33
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
34
- }
35
- & .crisp-datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
36
- content: ""; position: absolute; z-index: -1;
37
- inset-inline-start: 0; inset-inline-end: 50%;
38
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
39
- }
40
37
 
41
- & .crisp-datepicker-date {
42
- position: relative; display: flex; width: 100%; height: 100%;
43
- min-height: var(--crisp-size-calendarCell); min-width: 0;
44
- align-items: flex-start; justify-content: center;
45
- border: 0; padding: 0; background: transparent; cursor: pointer;
46
- color: var(--crisp-fg-primary); font: inherit; outline: none;
38
+ & .crisp-datepicker-cell {
39
+ position: relative; min-width: 0; min-height: var(--crisp-size-calendarCell);
47
40
  isolation: isolate;
48
41
  }
42
+ & .crisp-datepicker-empty-cell { min-height: var(--crisp-size-calendarCell); }
43
+
44
+ /* The bridge joining days inside a completed range.
45
+ The endpoints do NOT carry data-in-range -- only the interior days do --
46
+ so these are three independent rules, not a base plus two modifiers. A
47
+ modifier that sets only inset-inline-start never generates a pseudo
48
+ element on an endpoint, which leaves a half-cell of white between the
49
+ endpoint's pill and the run of grey beside it. Each rule declares its own content. */
50
+ & .crisp-datepicker-cell[data-range-complete][data-in-range]::before { content: ""; position: absolute; z-index: -1; inset-inline: 0; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
51
+ & .crisp-datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before { content: ""; position: absolute; z-index: -1; inset-inline-start: 50%; inset-inline-end: 0; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
52
+ & .crisp-datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before { content: ""; position: absolute; z-index: -1; inset-inline-start: 0; inset-inline-end: 50%; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
53
+ & .crisp-datepicker-date { position: relative; display: flex; width: 100%; height: 100%; min-height: var(--crisp-size-calendarCell); min-width: 0; align-items: flex-start; justify-content: center; border: 0; padding: 0; background: transparent; cursor: pointer; color: var(--crisp-fg-primary); font: inherit; outline: none; isolation: isolate; }
49
54
  /* the round day highlight, behind the numeral */
50
- & .crisp-datepicker-date::before {
51
- content: ""; position: absolute; z-index: -1; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%;
52
- width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill);
53
- border-radius: var(--crisp-radius-md);
54
- background: transparent; transform: translateX(-50%);
55
- }
55
+ & .crisp-datepicker-date::before { content: ""; position: absolute; z-index: -1; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%; width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill); border-radius: var(--crisp-radius-md); background: transparent; transform: translateX(-50%); }
56
56
  & .crisp-datepicker-date[data-today]::before { background: var(--crisp-bg-neutral-weak); }
57
57
  & .crisp-datepicker-date[data-selected]::before { background: var(--crisp-bg-brand-solid); }
58
58
  & .crisp-datepicker-date[data-selected] { color: var(--crisp-fg-on-brand); }
@@ -60,34 +60,35 @@
60
60
  & .crisp-datepicker-date[data-in-range]::before { background: transparent; }
61
61
  & .crisp-datepicker-date[data-unavailable] { color: var(--crisp-fg-disabled); }
62
62
  & .crisp-datepicker-date:disabled { color: var(--crisp-fg-disabled); cursor: not-allowed; }
63
- & .crisp-datepicker-date:focus-visible::after {
64
- content: ""; position: absolute; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%;
65
- width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill);
66
- transform: translateX(-50%); border-radius: var(--crisp-radius-md);
67
- outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px;
63
+ & .crisp-datepicker-date:focus-visible::after { content: ""; position: absolute; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%; width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill); transform: translateX(-50%); border-radius: var(--crisp-radius-md); outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px; }
64
+ & .crisp-datepicker-date-content { position: relative; z-index: 1; display: flex; width: 100%; min-height: var(--crisp-size-calendarCell); min-width: 0; box-sizing: border-box; flex-direction: column; align-items: center; gap: 3px; padding-top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); color: inherit; font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); font-variant-numeric: tabular-nums; }
65
+ /* The numeral is its own 42px flex box so it centres on the 42px pill. Without this the content column top-aligns the glyph and the pill reads as offset — the cell is 48px, the pill 42px inset 3px, and only a 42px-tall numeral box shares the pill's centre line. */
66
+ & .crisp-datepicker-date-content > [data-date-picker-day] {
67
+ display: flex; min-height: var(--crisp-size-calendarPill); flex-shrink: 0;
68
+ align-items: center; justify-content: center;
68
69
  }
69
70
 
70
- & .crisp-datepicker-date-content {
71
- position: relative; z-index: 1; display: flex; width: 100%;
72
- min-height: var(--crisp-size-calendarCell); min-width: 0; box-sizing: border-box;
73
- flex-direction: column; align-items: center; gap: 3px;
74
- padding-top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); color: inherit;
75
- font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium);
76
- font-variant-numeric: tabular-nums;
77
- }
78
- /* The numeral is its own 42px flex box so it centres on the 42px pill.
79
- Without this the content column top-aligns the glyph and the pill reads
80
- as offset — the cell is 48px, the pill 42px inset 3px, and only a
81
- 42px-tall numeral box shares the pill's centre line. */
82
- & .crisp-datepicker-date-content > [data-date-picker-day] { display: flex; min-height: var(--crisp-size-calendarPill); flex-shrink: 0; align-items: center; justify-content: center; }
83
71
  /* the year/month wheel, laid over the grid it replaces */
84
- & .crisp-datepicker-wheel-container { position: absolute; z-index: 1; inset-inline: 0; top: var(--crisp-size-menuRow); bottom: 0; display: flex; align-items: center; overflow: hidden; background: var(--crisp-bg-surface); }
72
+ & .crisp-datepicker-wheel-container {
73
+ position: absolute; z-index: 1; inset-inline: 0;
74
+ top: var(--crisp-size-menuRow); bottom: 0;
75
+ display: flex; align-items: center; overflow: hidden;
76
+ background: var(--crisp-bg-surface);
77
+ }
85
78
  & .crisp-datepicker-wheel { width: 100%; }
86
- & .crisp-datepicker-continuous-scroll { height: 100%; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none; }
79
+
80
+ & .crisp-datepicker-continuous-scroll {
81
+ height: 100%; min-height: 0; overflow-y: auto;
82
+ overscroll-behavior: contain; scrollbar-width: none;
83
+ }
87
84
  & .crisp-datepicker-continuous-scroll::-webkit-scrollbar { display: none; }
88
85
  & .crisp-datepicker-continuous-content { display: flex; flex-direction: column; }
86
+
89
87
  /* announced, never drawn */
90
- & .crisp-datepicker-live-region { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
88
+ & .crisp-datepicker-live-region {
89
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
90
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
91
+ }
91
92
  }
92
93
  .crisp-datepicker--visibleRange_month {
93
94
 
@@ -1,4 +1,5 @@
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
+ import { Slot } from "@radix-ui/react-slot";
2
3
  import * as React from "react";
3
4
  import { FloatingPortal, isInsideLaterFloatingLayer, registerFloatingLayer, } from "../../core/index.js";
4
5
  import { useMenuKeyboard } from "../menu/index.js";
@@ -8,7 +9,7 @@ import { contextmenuRecipe } from "./contextmenu.recipe.js";
8
9
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
9
10
  * edges), closing on select, outside pointer-down, and Escape.
10
11
  */
11
- export function ContextMenu({ children, items, menuLabel = "Context menu", className, }) {
12
+ export function ContextMenu({ children, items, menuLabel = "Context menu", className, asChild = false, }) {
12
13
  const rootRef = React.useRef(null);
13
14
  const menuRef = React.useRef(null);
14
15
  const returnFocusTo = React.useRef(null);
@@ -73,7 +74,7 @@ export function ContextMenu({ children, items, menuLabel = "Context menu", class
73
74
  document.removeEventListener("keydown", onKey);
74
75
  };
75
76
  }, [pos, close]);
76
- return (_jsxs("div", { ref: rootRef, className: contextmenuRecipe({ className }), children: [_jsx("div", { onContextMenu: open, children: children }), pos && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? (_jsx("hr", { className: "crisp-contextmenu-sep" }, i)) : (_jsxs("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
77
+ return (_jsxs(_Fragment, { children: [asChild ? (_jsx(Slot, { ref: rootRef, className: className, onContextMenu: open, children: children })) : (_jsx("div", { ref: rootRef, className: contextmenuRecipe({ className }), children: _jsx("div", { onContextMenu: open, children: children }) })), pos && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? (_jsx("hr", { className: "crisp-contextmenu-sep" }, i)) : (_jsxs("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
77
78
  ? "crisp-contextmenu-item crisp-contextmenu-item--danger"
78
79
  : "crisp-contextmenu-item", tabIndex: -1, onClick: () => {
79
80
  item.onSelect?.();
@@ -56,9 +56,14 @@ const config = defineRecipe({
56
56
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
57
57
  position: relative; z-index: 2; background: ${c.bgSurface};
58
58
  }
59
+ /* fgNeutralMuted, not seed's fgNeutralSubtle (#367): 3.42:1 in light
60
+ against bgSurface, fails AA for this 12px text. Not an Attio
61
+ measurement -- accessibility wins over the seed reference.
62
+ neutralMuted clears it, 6.62:1 light / 14.71:1 dark (dark was
63
+ already fine at 9.43:1). */
59
64
  & .crisp-datepicker-weekday {
60
65
  display: flex; align-items: center; justify-content: center;
61
- height: ${c.size.calendarCell}; color: ${c.fgNeutralSubtle};
66
+ height: ${c.size.calendarCell}; color: ${c.fgNeutralMuted};
62
67
  font-size: ${c.text.sm}; line-height: 16px; font-weight: ${c.weight.medium};
63
68
  }
64
69
 
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Slot } from "@radix-ui/react-slot";
3
3
  import * as React from "react";
4
- const NODES = ["button", "div", "span", "a", "input", "p"];
4
+ const NODES = ["button", "div", "span", "a", "input", "p", "nav"];
5
5
  export const Primitive = NODES.reduce((acc, node) => {
6
6
  const Component = React.forwardRef(({ asChild, ...props }, ref) => {
7
7
  const Comp = asChild ? Slot : node;
@@ -81,7 +81,7 @@ 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, groups, footer = ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
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, commandItems, commandOpen: commandOpenProp, defaultCommandOpen, onCommandOpenChange, commandShortcut = true, "aria-label": ariaLabel = "Main", ...props }, ref) => {
85
85
  // Seeded once, on mount, from whatever groups start collapsed — not
86
86
  // reactive to `groups` changing later, matching every other piece of
87
87
  // this component's state (recordsSection/listsSection/chatsSection have
@@ -89,8 +89,22 @@ export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse,
89
89
  const [collapsedSections, setCollapsedSections] = React.useState(() => new Set(groups
90
90
  ?.filter((g) => g.defaultCollapsed)
91
91
  .map((g) => g.key ?? g.label)));
92
- const [commandOpen, setCommandOpen] = React.useState(false);
92
+ // Controlled/uncontrolled triad (#364), same shape as Popover/Menu's own
93
+ // `open` -- an app driving a single ⌘K listener needs to be able to set
94
+ // `commandOpen` itself, not just read it.
95
+ const [internalCommandOpen, setInternalCommandOpen] = React.useState(defaultCommandOpen ?? false);
96
+ const commandOpen = commandOpenProp ?? internalCommandOpen;
97
+ const setCommandOpen = React.useCallback((next) => {
98
+ if (commandOpenProp === undefined)
99
+ setInternalCommandOpen(next);
100
+ onCommandOpenChange?.(next);
101
+ }, [commandOpenProp, onCommandOpenChange]);
93
102
  React.useEffect(() => {
103
+ // #364: skip attaching entirely when the app owns its own ⌘K listener
104
+ // -- two listeners on the same key both firing opened two overlapping
105
+ // command palettes.
106
+ if (!commandShortcut)
107
+ return;
94
108
  function handleKeyDown(event) {
95
109
  if ((event.metaKey || event.ctrlKey) &&
96
110
  event.key.toLowerCase() === "k") {
@@ -100,7 +114,7 @@ export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse,
100
114
  }
101
115
  document.addEventListener("keydown", handleKeyDown);
102
116
  return () => document.removeEventListener("keydown", handleKeyDown);
103
- }, []);
117
+ }, [commandShortcut, setCommandOpen]);
104
118
  const toggleSection = React.useCallback((key) => {
105
119
  setCollapsedSections((prev) => {
106
120
  const next = new Set(prev);
@@ -113,13 +127,13 @@ export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse,
113
127
  return next;
114
128
  });
115
129
  }, []);
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 ? (
130
+ return (_jsxs(Primitive.nav, { ref: ref, asChild: asChild, "aria-label": ariaLabel, 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
131
  // #325: navigation as data, reusing the exact section markup/
118
132
  // recipe above — SectionHeader + .crisp-sidebar-section — instead
119
133
  // of the three fixed Attio sections.
120
134
  groups.map((group) => {
121
135
  const groupKey = group.key ?? group.label;
122
136
  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 })] }));
137
+ })) : (_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, items: commandItems })] }));
124
138
  });
125
139
  Sidebar.displayName = "Sidebar";
package/dist/styles.css CHANGED
@@ -601,47 +601,47 @@
601
601
  & .crisp-datepicker-nav-button:focus-visible { outline-color: var(--crisp-stroke-focus-ring); }
602
602
  & .crisp-datepicker-nav-button:disabled { opacity: .4; cursor: not-allowed; }
603
603
  & .crisp-datepicker-weekday-row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); position: relative; z-index: 2; background: var(--crisp-bg-surface); }
604
- & .crisp-datepicker-weekday { display: flex; align-items: center; justify-content: center; height: var(--crisp-size-calendarCell); color: var(--crisp-fg-neutral-subtle); font-size: var(--crisp-text-sm); line-height: 16px; font-weight: var(--crisp-weight-medium); }
604
+ /* fgNeutralMuted, not seed's fgNeutralSubtle (#367): 3.42:1 in light
605
+ against bgSurface, fails AA for this 12px text. Not an Attio
606
+ measurement -- accessibility wins over the seed reference.
607
+ neutralMuted clears it, 6.62:1 light / 14.71:1 dark (dark was
608
+ already fine at 9.43:1). */
609
+ & .crisp-datepicker-weekday {
610
+ display: flex; align-items: center; justify-content: center;
611
+ height: var(--crisp-size-calendarCell); color: var(--crisp-fg-neutral-muted);
612
+ font-size: var(--crisp-text-sm); line-height: 16px; font-weight: var(--crisp-weight-medium);
613
+ }
614
+
605
615
  & .crisp-datepicker-months { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; min-width: 0; }
606
616
  & .crisp-datepicker-months[data-wheel-open] { visibility: hidden; }
607
617
  & .crisp-datepicker-month { min-width: 0; }
608
- & .crisp-datepicker-month-label { display: flex; align-items: center; height: 24px; color: var(--crisp-fg-primary); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); }
618
+ & .crisp-datepicker-month-label {
619
+ display: flex; align-items: center; height: 24px;
620
+ color: var(--crisp-fg-primary); font-size: var(--crisp-text-lg); line-height: 20px;
621
+ font-weight: var(--crisp-weight-medium);
622
+ }
623
+
609
624
  & .crisp-datepicker-grid { display: grid; min-width: 0; }
610
625
  & .crisp-datepicker-week-row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
611
- & .crisp-datepicker-cell { position: relative; min-width: 0; min-height: var(--crisp-size-calendarCell); isolation: isolate; }
612
- & .crisp-datepicker-empty-cell { min-height: var(--crisp-size-calendarCell); }
613
- /* The bridge joining days inside a completed range. The endpoints do NOT carry data-in-range -- only the interior days do -- so these are three independent rules, not a base plus two modifiers. A modifier that sets only inset-inline-start never generates a pseudo element on an endpoint, which leaves a half-cell of white between the endpoint's pill and the run of grey beside it. Each rule declares its
614
- own content. */
615
- & .crisp-datepicker-cell[data-range-complete][data-in-range]::before {
616
- content: ""; position: absolute; z-index: -1; inset-inline: 0;
617
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
618
- }
619
- & .crisp-datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
620
- content: ""; position: absolute; z-index: -1;
621
- inset-inline-start: 50%; inset-inline-end: 0;
622
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
623
- }
624
- & .crisp-datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
625
- content: ""; position: absolute; z-index: -1;
626
- inset-inline-start: 0; inset-inline-end: 50%;
627
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
628
- }
629
626
 
630
- & .crisp-datepicker-date {
631
- position: relative; display: flex; width: 100%; height: 100%;
632
- min-height: var(--crisp-size-calendarCell); min-width: 0;
633
- align-items: flex-start; justify-content: center;
634
- border: 0; padding: 0; background: transparent; cursor: pointer;
635
- color: var(--crisp-fg-primary); font: inherit; outline: none;
627
+ & .crisp-datepicker-cell {
628
+ position: relative; min-width: 0; min-height: var(--crisp-size-calendarCell);
636
629
  isolation: isolate;
637
630
  }
631
+ & .crisp-datepicker-empty-cell { min-height: var(--crisp-size-calendarCell); }
632
+
633
+ /* The bridge joining days inside a completed range.
634
+ The endpoints do NOT carry data-in-range -- only the interior days do --
635
+ so these are three independent rules, not a base plus two modifiers. A
636
+ modifier that sets only inset-inline-start never generates a pseudo
637
+ element on an endpoint, which leaves a half-cell of white between the
638
+ endpoint's pill and the run of grey beside it. Each rule declares its own content. */
639
+ & .crisp-datepicker-cell[data-range-complete][data-in-range]::before { content: ""; position: absolute; z-index: -1; inset-inline: 0; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
640
+ & .crisp-datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before { content: ""; position: absolute; z-index: -1; inset-inline-start: 50%; inset-inline-end: 0; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
641
+ & .crisp-datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before { content: ""; position: absolute; z-index: -1; inset-inline-start: 0; inset-inline-end: 50%; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
642
+ & .crisp-datepicker-date { position: relative; display: flex; width: 100%; height: 100%; min-height: var(--crisp-size-calendarCell); min-width: 0; align-items: flex-start; justify-content: center; border: 0; padding: 0; background: transparent; cursor: pointer; color: var(--crisp-fg-primary); font: inherit; outline: none; isolation: isolate; }
638
643
  /* the round day highlight, behind the numeral */
639
- & .crisp-datepicker-date::before {
640
- content: ""; position: absolute; z-index: -1; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%;
641
- width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill);
642
- border-radius: var(--crisp-radius-md);
643
- background: transparent; transform: translateX(-50%);
644
- }
644
+ & .crisp-datepicker-date::before { content: ""; position: absolute; z-index: -1; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%; width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill); border-radius: var(--crisp-radius-md); background: transparent; transform: translateX(-50%); }
645
645
  & .crisp-datepicker-date[data-today]::before { background: var(--crisp-bg-neutral-weak); }
646
646
  & .crisp-datepicker-date[data-selected]::before { background: var(--crisp-bg-brand-solid); }
647
647
  & .crisp-datepicker-date[data-selected] { color: var(--crisp-fg-on-brand); }
@@ -649,34 +649,35 @@
649
649
  & .crisp-datepicker-date[data-in-range]::before { background: transparent; }
650
650
  & .crisp-datepicker-date[data-unavailable] { color: var(--crisp-fg-disabled); }
651
651
  & .crisp-datepicker-date:disabled { color: var(--crisp-fg-disabled); cursor: not-allowed; }
652
- & .crisp-datepicker-date:focus-visible::after {
653
- content: ""; position: absolute; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%;
654
- width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill);
655
- transform: translateX(-50%); border-radius: var(--crisp-radius-md);
656
- outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px;
652
+ & .crisp-datepicker-date:focus-visible::after { content: ""; position: absolute; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%; width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill); transform: translateX(-50%); border-radius: var(--crisp-radius-md); outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px; }
653
+ & .crisp-datepicker-date-content { position: relative; z-index: 1; display: flex; width: 100%; min-height: var(--crisp-size-calendarCell); min-width: 0; box-sizing: border-box; flex-direction: column; align-items: center; gap: 3px; padding-top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); color: inherit; font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); font-variant-numeric: tabular-nums; }
654
+ /* The numeral is its own 42px flex box so it centres on the 42px pill. Without this the content column top-aligns the glyph and the pill reads as offset — the cell is 48px, the pill 42px inset 3px, and only a 42px-tall numeral box shares the pill's centre line. */
655
+ & .crisp-datepicker-date-content > [data-date-picker-day] {
656
+ display: flex; min-height: var(--crisp-size-calendarPill); flex-shrink: 0;
657
+ align-items: center; justify-content: center;
657
658
  }
658
659
 
659
- & .crisp-datepicker-date-content {
660
- position: relative; z-index: 1; display: flex; width: 100%;
661
- min-height: var(--crisp-size-calendarCell); min-width: 0; box-sizing: border-box;
662
- flex-direction: column; align-items: center; gap: 3px;
663
- padding-top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); color: inherit;
664
- font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium);
665
- font-variant-numeric: tabular-nums;
666
- }
667
- /* The numeral is its own 42px flex box so it centres on the 42px pill.
668
- Without this the content column top-aligns the glyph and the pill reads
669
- as offset — the cell is 48px, the pill 42px inset 3px, and only a
670
- 42px-tall numeral box shares the pill's centre line. */
671
- & .crisp-datepicker-date-content > [data-date-picker-day] { display: flex; min-height: var(--crisp-size-calendarPill); flex-shrink: 0; align-items: center; justify-content: center; }
672
660
  /* the year/month wheel, laid over the grid it replaces */
673
- & .crisp-datepicker-wheel-container { position: absolute; z-index: 1; inset-inline: 0; top: var(--crisp-size-menuRow); bottom: 0; display: flex; align-items: center; overflow: hidden; background: var(--crisp-bg-surface); }
661
+ & .crisp-datepicker-wheel-container {
662
+ position: absolute; z-index: 1; inset-inline: 0;
663
+ top: var(--crisp-size-menuRow); bottom: 0;
664
+ display: flex; align-items: center; overflow: hidden;
665
+ background: var(--crisp-bg-surface);
666
+ }
674
667
  & .crisp-datepicker-wheel { width: 100%; }
675
- & .crisp-datepicker-continuous-scroll { height: 100%; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none; }
668
+
669
+ & .crisp-datepicker-continuous-scroll {
670
+ height: 100%; min-height: 0; overflow-y: auto;
671
+ overscroll-behavior: contain; scrollbar-width: none;
672
+ }
676
673
  & .crisp-datepicker-continuous-scroll::-webkit-scrollbar { display: none; }
677
674
  & .crisp-datepicker-continuous-content { display: flex; flex-direction: column; }
675
+
678
676
  /* announced, never drawn */
679
- & .crisp-datepicker-live-region { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
677
+ & .crisp-datepicker-live-region {
678
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
679
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
680
+ }
680
681
  }
681
682
  .crisp-datepicker--visibleRange_month {
682
683
 
@@ -602,47 +602,47 @@
602
602
  & .crisp-datepicker-nav-button:focus-visible { outline-color: var(--crisp-stroke-focus-ring); }
603
603
  & .crisp-datepicker-nav-button:disabled { opacity: .4; cursor: not-allowed; }
604
604
  & .crisp-datepicker-weekday-row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); position: relative; z-index: 2; background: var(--crisp-bg-surface); }
605
- & .crisp-datepicker-weekday { display: flex; align-items: center; justify-content: center; height: var(--crisp-size-calendarCell); color: var(--crisp-fg-neutral-subtle); font-size: var(--crisp-text-sm); line-height: 16px; font-weight: var(--crisp-weight-medium); }
605
+ /* fgNeutralMuted, not seed's fgNeutralSubtle (#367): 3.42:1 in light
606
+ against bgSurface, fails AA for this 12px text. Not an Attio
607
+ measurement -- accessibility wins over the seed reference.
608
+ neutralMuted clears it, 6.62:1 light / 14.71:1 dark (dark was
609
+ already fine at 9.43:1). */
610
+ & .crisp-datepicker-weekday {
611
+ display: flex; align-items: center; justify-content: center;
612
+ height: var(--crisp-size-calendarCell); color: var(--crisp-fg-neutral-muted);
613
+ font-size: var(--crisp-text-sm); line-height: 16px; font-weight: var(--crisp-weight-medium);
614
+ }
615
+
606
616
  & .crisp-datepicker-months { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; min-width: 0; }
607
617
  & .crisp-datepicker-months[data-wheel-open] { visibility: hidden; }
608
618
  & .crisp-datepicker-month { min-width: 0; }
609
- & .crisp-datepicker-month-label { display: flex; align-items: center; height: 24px; color: var(--crisp-fg-primary); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); }
619
+ & .crisp-datepicker-month-label {
620
+ display: flex; align-items: center; height: 24px;
621
+ color: var(--crisp-fg-primary); font-size: var(--crisp-text-lg); line-height: 20px;
622
+ font-weight: var(--crisp-weight-medium);
623
+ }
624
+
610
625
  & .crisp-datepicker-grid { display: grid; min-width: 0; }
611
626
  & .crisp-datepicker-week-row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
612
- & .crisp-datepicker-cell { position: relative; min-width: 0; min-height: var(--crisp-size-calendarCell); isolation: isolate; }
613
- & .crisp-datepicker-empty-cell { min-height: var(--crisp-size-calendarCell); }
614
- /* The bridge joining days inside a completed range. The endpoints do NOT carry data-in-range -- only the interior days do -- so these are three independent rules, not a base plus two modifiers. A modifier that sets only inset-inline-start never generates a pseudo element on an endpoint, which leaves a half-cell of white between the endpoint's pill and the run of grey beside it. Each rule declares its
615
- own content. */
616
- & .crisp-datepicker-cell[data-range-complete][data-in-range]::before {
617
- content: ""; position: absolute; z-index: -1; inset-inline: 0;
618
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
619
- }
620
- & .crisp-datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
621
- content: ""; position: absolute; z-index: -1;
622
- inset-inline-start: 50%; inset-inline-end: 0;
623
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
624
- }
625
- & .crisp-datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
626
- content: ""; position: absolute; z-index: -1;
627
- inset-inline-start: 0; inset-inline-end: 50%;
628
- top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak);
629
- }
630
627
 
631
- & .crisp-datepicker-date {
632
- position: relative; display: flex; width: 100%; height: 100%;
633
- min-height: var(--crisp-size-calendarCell); min-width: 0;
634
- align-items: flex-start; justify-content: center;
635
- border: 0; padding: 0; background: transparent; cursor: pointer;
636
- color: var(--crisp-fg-primary); font: inherit; outline: none;
628
+ & .crisp-datepicker-cell {
629
+ position: relative; min-width: 0; min-height: var(--crisp-size-calendarCell);
637
630
  isolation: isolate;
638
631
  }
632
+ & .crisp-datepicker-empty-cell { min-height: var(--crisp-size-calendarCell); }
633
+
634
+ /* The bridge joining days inside a completed range.
635
+ The endpoints do NOT carry data-in-range -- only the interior days do --
636
+ so these are three independent rules, not a base plus two modifiers. A
637
+ modifier that sets only inset-inline-start never generates a pseudo
638
+ element on an endpoint, which leaves a half-cell of white between the
639
+ endpoint's pill and the run of grey beside it. Each rule declares its own content. */
640
+ & .crisp-datepicker-cell[data-range-complete][data-in-range]::before { content: ""; position: absolute; z-index: -1; inset-inline: 0; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
641
+ & .crisp-datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before { content: ""; position: absolute; z-index: -1; inset-inline-start: 50%; inset-inline-end: 0; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
642
+ & .crisp-datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before { content: ""; position: absolute; z-index: -1; inset-inline-start: 0; inset-inline-end: 50%; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); height: var(--crisp-size-calendarPill); background: var(--crisp-bg-neutral-weak); }
643
+ & .crisp-datepicker-date { position: relative; display: flex; width: 100%; height: 100%; min-height: var(--crisp-size-calendarCell); min-width: 0; align-items: flex-start; justify-content: center; border: 0; padding: 0; background: transparent; cursor: pointer; color: var(--crisp-fg-primary); font: inherit; outline: none; isolation: isolate; }
639
644
  /* the round day highlight, behind the numeral */
640
- & .crisp-datepicker-date::before {
641
- content: ""; position: absolute; z-index: -1; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%;
642
- width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill);
643
- border-radius: var(--crisp-radius-md);
644
- background: transparent; transform: translateX(-50%);
645
- }
645
+ & .crisp-datepicker-date::before { content: ""; position: absolute; z-index: -1; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%; width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill); border-radius: var(--crisp-radius-md); background: transparent; transform: translateX(-50%); }
646
646
  & .crisp-datepicker-date[data-today]::before { background: var(--crisp-bg-neutral-weak); }
647
647
  & .crisp-datepicker-date[data-selected]::before { background: var(--crisp-bg-brand-solid); }
648
648
  & .crisp-datepicker-date[data-selected] { color: var(--crisp-fg-on-brand); }
@@ -650,34 +650,35 @@
650
650
  & .crisp-datepicker-date[data-in-range]::before { background: transparent; }
651
651
  & .crisp-datepicker-date[data-unavailable] { color: var(--crisp-fg-disabled); }
652
652
  & .crisp-datepicker-date:disabled { color: var(--crisp-fg-disabled); cursor: not-allowed; }
653
- & .crisp-datepicker-date:focus-visible::after {
654
- content: ""; position: absolute; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%;
655
- width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill);
656
- transform: translateX(-50%); border-radius: var(--crisp-radius-md);
657
- outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px;
653
+ & .crisp-datepicker-date:focus-visible::after { content: ""; position: absolute; top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); left: 50%; width: var(--crisp-size-calendarPill); height: var(--crisp-size-calendarPill); transform: translateX(-50%); border-radius: var(--crisp-radius-md); outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px; }
654
+ & .crisp-datepicker-date-content { position: relative; z-index: 1; display: flex; width: 100%; min-height: var(--crisp-size-calendarCell); min-width: 0; box-sizing: border-box; flex-direction: column; align-items: center; gap: 3px; padding-top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); color: inherit; font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); font-variant-numeric: tabular-nums; }
655
+ /* The numeral is its own 42px flex box so it centres on the 42px pill. Without this the content column top-aligns the glyph and the pill reads as offset — the cell is 48px, the pill 42px inset 3px, and only a 42px-tall numeral box shares the pill's centre line. */
656
+ & .crisp-datepicker-date-content > [data-date-picker-day] {
657
+ display: flex; min-height: var(--crisp-size-calendarPill); flex-shrink: 0;
658
+ align-items: center; justify-content: center;
658
659
  }
659
660
 
660
- & .crisp-datepicker-date-content {
661
- position: relative; z-index: 1; display: flex; width: 100%;
662
- min-height: var(--crisp-size-calendarCell); min-width: 0; box-sizing: border-box;
663
- flex-direction: column; align-items: center; gap: 3px;
664
- padding-top: calc((var(--crisp-size-calendarCell) - var(--crisp-size-calendarPill)) / 2); color: inherit;
665
- font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium);
666
- font-variant-numeric: tabular-nums;
667
- }
668
- /* The numeral is its own 42px flex box so it centres on the 42px pill.
669
- Without this the content column top-aligns the glyph and the pill reads
670
- as offset — the cell is 48px, the pill 42px inset 3px, and only a
671
- 42px-tall numeral box shares the pill's centre line. */
672
- & .crisp-datepicker-date-content > [data-date-picker-day] { display: flex; min-height: var(--crisp-size-calendarPill); flex-shrink: 0; align-items: center; justify-content: center; }
673
661
  /* the year/month wheel, laid over the grid it replaces */
674
- & .crisp-datepicker-wheel-container { position: absolute; z-index: 1; inset-inline: 0; top: var(--crisp-size-menuRow); bottom: 0; display: flex; align-items: center; overflow: hidden; background: var(--crisp-bg-surface); }
662
+ & .crisp-datepicker-wheel-container {
663
+ position: absolute; z-index: 1; inset-inline: 0;
664
+ top: var(--crisp-size-menuRow); bottom: 0;
665
+ display: flex; align-items: center; overflow: hidden;
666
+ background: var(--crisp-bg-surface);
667
+ }
675
668
  & .crisp-datepicker-wheel { width: 100%; }
676
- & .crisp-datepicker-continuous-scroll { height: 100%; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none; }
669
+
670
+ & .crisp-datepicker-continuous-scroll {
671
+ height: 100%; min-height: 0; overflow-y: auto;
672
+ overscroll-behavior: contain; scrollbar-width: none;
673
+ }
677
674
  & .crisp-datepicker-continuous-scroll::-webkit-scrollbar { display: none; }
678
675
  & .crisp-datepicker-continuous-content { display: flex; flex-direction: column; }
676
+
679
677
  /* announced, never drawn */
680
- & .crisp-datepicker-live-region { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
678
+ & .crisp-datepicker-live-region {
679
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
680
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
681
+ }
681
682
  }
682
683
  .crisp-datepicker--visibleRange_month {
683
684
 
@@ -17,13 +17,23 @@ export type ContextMenuProps = {
17
17
  /** Accessible name for the menu surface. */
18
18
  menuLabel?: string;
19
19
  className?: string;
20
+ /**
21
+ * Render `onContextMenu` and the trigger ref directly onto `children`
22
+ * instead of wrapping it in a `<div>` (#340) — needed when `children` is
23
+ * an element that cannot have a wrapping element inserted around it, such
24
+ * as a `<tr>` inside a `<tbody>` (the browser's HTML parser hoists the row
25
+ * out of the wrapping `<div>`, breaking both the layout and the
26
+ * right-click target). `children` must then be a single valid React
27
+ * element, same as any other `asChild` in crisp (`core/primitive.tsx`).
28
+ */
29
+ asChild?: boolean;
20
30
  };
21
31
  /**
22
32
  * A right-click context menu — the popover surface positioned at the cursor.
23
33
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
24
34
  * edges), closing on select, outside pointer-down, and Escape.
25
35
  */
26
- export declare function ContextMenu({ children, items, menuLabel, className, }: ContextMenuProps): React.JSX.Element;
36
+ export declare function ContextMenu({ children, items, menuLabel, className, asChild, }: ContextMenuProps): React.JSX.Element;
27
37
  export declare namespace ContextMenu {
28
38
  var displayName: string;
29
39
  }
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- declare const NODES: readonly ["button", "div", "span", "a", "input", "p"];
2
+ declare const NODES: readonly ["button", "div", "span", "a", "input", "p", "nav"];
3
3
  type NodeName = (typeof NODES)[number];
4
4
  type PrimitiveProps<E extends NodeName> = React.ComponentPropsWithoutRef<E> & {
5
5
  asChild?: boolean;
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import type { AvatarProps } from "../../components/avatar";
3
+ import { type CommandItem } from "../command";
3
4
  type IconProps = {
4
5
  className?: string;
5
6
  };
@@ -115,6 +116,27 @@ export type SidebarProps = React.ComponentPropsWithoutRef<"nav"> & {
115
116
  groups?: SidebarNavGroup[];
116
117
  /** Footer copy (onboarding pill, invite row, trial banner). */
117
118
  footer?: SidebarFooter;
119
+ /**
120
+ * Command-palette items, forwarded to the internal `Command`'s own
121
+ * `items` prop. Omit for Command's built-in Attio set (#364: GRC's
122
+ * commands aren't Attio's, and there was no way to reach them).
123
+ */
124
+ commandItems?: CommandItem[];
125
+ /** Controlled open state for the command palette. Omit to let Sidebar
126
+ * manage it (see `defaultCommandOpen`). */
127
+ commandOpen?: boolean;
128
+ /** Initially open, when uncontrolled. */
129
+ defaultCommandOpen?: boolean;
130
+ onCommandOpenChange?: (open: boolean) => void;
131
+ /**
132
+ * Whether Sidebar attaches its own ⌘K/Ctrl+K document listener to open
133
+ * the command palette (default `true`). Set `false` when the app already
134
+ * owns a single global ⌘K listener — e.g. to route the same shortcut to
135
+ * more than Sidebar's palette elsewhere in the page — and drives
136
+ * `commandOpen` itself; two listeners on the same key both firing opened
137
+ * two overlapping instances (#364).
138
+ */
139
+ commandShortcut?: boolean;
118
140
  };
119
141
  /** The measured workspace switcher — the default `workspace` prop. */
120
142
  export declare const ATTIO_SIDEBAR_WORKSPACE: SidebarWorkspace;
@@ -160,5 +182,26 @@ export declare const Sidebar: React.ForwardRefExoticComponent<Omit<React.Detaile
160
182
  groups?: SidebarNavGroup[];
161
183
  /** Footer copy (onboarding pill, invite row, trial banner). */
162
184
  footer?: SidebarFooter;
163
- } & React.RefAttributes<HTMLDivElement>>;
185
+ /**
186
+ * Command-palette items, forwarded to the internal `Command`'s own
187
+ * `items` prop. Omit for Command's built-in Attio set (#364: GRC's
188
+ * commands aren't Attio's, and there was no way to reach them).
189
+ */
190
+ commandItems?: CommandItem[];
191
+ /** Controlled open state for the command palette. Omit to let Sidebar
192
+ * manage it (see `defaultCommandOpen`). */
193
+ commandOpen?: boolean;
194
+ /** Initially open, when uncontrolled. */
195
+ defaultCommandOpen?: boolean;
196
+ onCommandOpenChange?: (open: boolean) => void;
197
+ /**
198
+ * Whether Sidebar attaches its own ⌘K/Ctrl+K document listener to open
199
+ * the command palette (default `true`). Set `false` when the app already
200
+ * owns a single global ⌘K listener — e.g. to route the same shortcut to
201
+ * more than Sidebar's palette elsewhere in the page — and drives
202
+ * `commandOpen` itself; two listeners on the same key both firing opened
203
+ * two overlapping instances (#364).
204
+ */
205
+ commandShortcut?: boolean;
206
+ } & React.RefAttributes<HTMLElement>>;
164
207
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",