@crisp-ui-kit/crisp 0.57.2 → 0.57.4
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/assigneepicker.css +13 -0
- package/dist/assigneepicker.d.ts +7 -0
- package/dist/assigneepicker.layered.css +15 -0
- package/dist/button.css +3 -0
- package/dist/button.d.ts +2 -2
- package/dist/button.layered.css +3 -0
- package/dist/chip.css +1 -1
- package/dist/chip.layered.css +1 -1
- package/dist/cjs/components/breadcrumb/breadcrumb.js +1 -1
- package/dist/cjs/components/button/button.recipe.js +33 -0
- package/dist/cjs/components/chip/chip.recipe.js +3 -1
- package/dist/cjs/components/datatable/datatable.js +6 -3
- package/dist/cjs/components/datatable/datatable.recipe.js +45 -6
- package/dist/cjs/components/datepickerfield/datepickerfield.js +2 -2
- package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +16 -3
- package/dist/cjs/components/menu/menu.js +65 -9
- package/dist/cjs/components/multiselect/multiselect.js +3 -0
- package/dist/cjs/components/multiselect/multiselect.recipe.js +18 -0
- package/dist/cjs/components/select/select.js +2 -2
- package/dist/cjs/components/select/select.recipe.js +19 -0
- package/dist/cjs/components/table/table.recipe.js +7 -1
- package/dist/cjs/core/floating-portal.js +7 -4
- package/dist/cjs/core/index.js +2 -1
- package/dist/cjs/core/place-floating.js +38 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/product/assigneepicker/assigneepicker.js +127 -0
- package/dist/cjs/product/assigneepicker/assigneepicker.recipe.js +36 -0
- package/dist/cjs/product/assigneepicker/index.js +7 -0
- package/dist/cjs/product/inbox/inbox.js +11 -3
- package/dist/cjs/product/notesgallery/index.js +7 -0
- package/dist/cjs/product/notesgallery/notesgallery.js +80 -0
- package/dist/cjs/product/notesgallery/notesgallery.recipe.js +46 -0
- package/dist/cjs/product/objectpagemenu/index.js +17 -0
- package/dist/cjs/product/objectpagemenu/objectpagemenu.js +26 -0
- package/dist/cjs/product/objectpagemenu/objectpagemenu.recipe.js +30 -0
- package/dist/cjs/product/objectsettings/objectsettings.js +107 -14
- package/dist/cjs/product/recordpage/index.js +2 -1
- package/dist/cjs/product/recordpage/recordpage.js +112 -7
- package/dist/cjs/product/recordpage/recordpage.recipe.js +63 -0
- package/dist/cjs/product/recordpicker/index.js +7 -0
- package/dist/cjs/product/recordpicker/recordpicker.js +100 -0
- package/dist/cjs/product/recordpicker/recordpicker.recipe.js +36 -0
- package/dist/cjs/product/savedviews/index.js +2 -1
- package/dist/cjs/product/savedviews/savedviews.js +22 -0
- package/dist/cjs/product/savedviews/savedviews.recipe.js +47 -0
- package/dist/cjs/product/share/index.js +7 -0
- package/dist/cjs/product/share/share.js +179 -0
- package/dist/cjs/product/share/share.recipe.js +114 -0
- package/dist/cjs/product/shortcutssheet/index.js +17 -0
- package/dist/cjs/product/shortcutssheet/shortcutssheet.js +125 -0
- package/dist/cjs/product/shortcutssheet/shortcutssheet.recipe.js +50 -0
- package/dist/cjs/product/taskeditpopover/index.js +7 -0
- package/dist/cjs/product/taskeditpopover/taskeditpopover.js +159 -0
- package/dist/cjs/product/taskeditpopover/taskeditpopover.recipe.js +68 -0
- package/dist/cjs/product/workspaceswitchermenu/index.js +17 -0
- package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.js +38 -0
- package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +34 -0
- package/dist/cjs/tokens/palette.js +27 -0
- package/dist/cjs/tokens/semantic.js +20 -0
- package/dist/datatable.css +50 -11
- package/dist/datatable.layered.css +50 -11
- package/dist/datepickerfield.css +8 -2
- package/dist/datepickerfield.d.ts +2 -2
- package/dist/datepickerfield.layered.css +8 -2
- package/dist/esm/components/breadcrumb/breadcrumb.js +1 -1
- package/dist/esm/components/button/button.recipe.js +33 -0
- package/dist/esm/components/chip/chip.recipe.js +3 -1
- package/dist/esm/components/datatable/datatable.js +6 -3
- package/dist/esm/components/datatable/datatable.recipe.js +45 -6
- package/dist/esm/components/datepickerfield/datepickerfield.js +2 -2
- package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +16 -3
- package/dist/esm/components/menu/menu.js +64 -8
- package/dist/esm/components/multiselect/multiselect.js +3 -0
- package/dist/esm/components/multiselect/multiselect.recipe.js +18 -0
- package/dist/esm/components/select/select.js +2 -2
- package/dist/esm/components/select/select.recipe.js +19 -0
- package/dist/esm/components/table/table.recipe.js +7 -1
- package/dist/esm/core/floating-portal.js +7 -4
- package/dist/esm/core/index.js +1 -1
- package/dist/esm/core/place-floating.js +37 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/product/assigneepicker/assigneepicker.js +91 -0
- package/dist/esm/product/assigneepicker/assigneepicker.recipe.js +33 -0
- package/dist/esm/product/assigneepicker/index.js +2 -0
- package/dist/esm/product/inbox/inbox.js +11 -3
- package/dist/esm/product/notesgallery/index.js +2 -0
- package/dist/esm/product/notesgallery/notesgallery.js +44 -0
- package/dist/esm/product/notesgallery/notesgallery.recipe.js +43 -0
- package/dist/esm/product/objectpagemenu/index.js +1 -0
- package/dist/esm/product/objectpagemenu/objectpagemenu.js +23 -0
- package/dist/esm/product/objectpagemenu/objectpagemenu.recipe.js +27 -0
- package/dist/esm/product/objectsettings/objectsettings.js +106 -13
- package/dist/esm/product/recordpage/index.js +1 -1
- package/dist/esm/product/recordpage/recordpage.js +112 -8
- package/dist/esm/product/recordpage/recordpage.recipe.js +63 -0
- package/dist/esm/product/recordpicker/index.js +2 -0
- package/dist/esm/product/recordpicker/recordpicker.js +64 -0
- package/dist/esm/product/recordpicker/recordpicker.recipe.js +33 -0
- package/dist/esm/product/savedviews/index.js +1 -1
- package/dist/esm/product/savedviews/savedviews.js +22 -1
- package/dist/esm/product/savedviews/savedviews.recipe.js +47 -0
- package/dist/esm/product/share/index.js +2 -0
- package/dist/esm/product/share/share.js +143 -0
- package/dist/esm/product/share/share.recipe.js +111 -0
- package/dist/esm/product/shortcutssheet/index.js +1 -0
- package/dist/esm/product/shortcutssheet/shortcutssheet.js +88 -0
- package/dist/esm/product/shortcutssheet/shortcutssheet.recipe.js +47 -0
- package/dist/esm/product/taskeditpopover/index.js +2 -0
- package/dist/esm/product/taskeditpopover/taskeditpopover.js +123 -0
- package/dist/esm/product/taskeditpopover/taskeditpopover.recipe.js +65 -0
- package/dist/esm/product/workspaceswitchermenu/index.js +1 -0
- package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.js +35 -0
- package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +31 -0
- package/dist/esm/tokens/palette.js +27 -0
- package/dist/esm/tokens/semantic.js +20 -0
- package/dist/multiselect.css +18 -0
- package/dist/multiselect.layered.css +18 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +56 -4
- package/dist/notesgallery.css +15 -0
- package/dist/notesgallery.d.ts +7 -0
- package/dist/notesgallery.layered.css +17 -0
- package/dist/objectpagemenu.css +3 -0
- package/dist/objectpagemenu.d.ts +7 -0
- package/dist/objectpagemenu.layered.css +5 -0
- package/dist/recordpage.css +59 -0
- package/dist/recordpage.layered.css +59 -0
- package/dist/recordpicker.css +13 -0
- package/dist/recordpicker.d.ts +7 -0
- package/dist/recordpicker.layered.css +15 -0
- package/dist/savedviews.css +47 -0
- package/dist/savedviews.layered.css +47 -0
- package/dist/select.css +19 -0
- package/dist/select.layered.css +19 -0
- package/dist/share.css +84 -0
- package/dist/share.d.ts +7 -0
- package/dist/share.layered.css +86 -0
- package/dist/shortcutssheet.css +24 -0
- package/dist/shortcutssheet.d.ts +7 -0
- package/dist/shortcutssheet.layered.css +26 -0
- package/dist/styles.css +438 -16
- package/dist/styles.layered.css +438 -16
- package/dist/table.css +7 -2
- package/dist/table.layered.css +7 -2
- package/dist/taskeditpopover.css +36 -0
- package/dist/taskeditpopover.d.ts +7 -0
- package/dist/taskeditpopover.layered.css +38 -0
- package/dist/tokens.css +26 -0
- package/dist/tokens.json +56 -4
- package/dist/types/components/breadcrumb/breadcrumb.d.ts +9 -1
- package/dist/types/components/button/button.d.ts +2 -0
- package/dist/types/components/button/button.recipe.d.ts +12 -0
- package/dist/types/components/chip/chip.recipe.d.ts +1 -1
- package/dist/types/components/datatable/datatable.d.ts +8 -0
- package/dist/types/components/datepickerfield/datepickerfield.d.ts +4 -1
- package/dist/types/components/datepickerfield/datepickerfield.recipe.d.ts +18 -3
- package/dist/types/components/menu/menu.d.ts +50 -7
- package/dist/types/components/multiselect/multiselect.d.ts +9 -0
- package/dist/types/components/select/select.d.ts +10 -0
- package/dist/types/core/floating-portal.d.ts +6 -5
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/core/place-floating.d.ts +37 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/product/assigneepicker/assigneepicker.d.ts +36 -0
- package/dist/types/product/assigneepicker/assigneepicker.recipe.d.ts +3 -0
- package/dist/types/product/assigneepicker/index.d.ts +2 -0
- package/dist/types/product/inbox/inbox.d.ts +13 -1
- package/dist/types/product/notesgallery/index.d.ts +2 -0
- package/dist/types/product/notesgallery/notesgallery.d.ts +54 -0
- package/dist/types/product/notesgallery/notesgallery.recipe.d.ts +3 -0
- package/dist/types/product/objectpagemenu/index.d.ts +1 -0
- package/dist/types/product/objectpagemenu/objectpagemenu.d.ts +40 -0
- package/dist/types/product/objectpagemenu/objectpagemenu.recipe.d.ts +17 -0
- package/dist/types/product/objectsettings/objectsettings.d.ts +71 -15
- package/dist/types/product/recordpage/index.d.ts +1 -1
- package/dist/types/product/recordpage/recordpage.d.ts +142 -1
- package/dist/types/product/recordpicker/index.d.ts +2 -0
- package/dist/types/product/recordpicker/recordpicker.d.ts +38 -0
- package/dist/types/product/recordpicker/recordpicker.recipe.d.ts +3 -0
- package/dist/types/product/savedviews/index.d.ts +1 -1
- package/dist/types/product/savedviews/savedviews.d.ts +36 -0
- package/dist/types/product/share/index.d.ts +2 -0
- package/dist/types/product/share/share.d.ts +85 -0
- package/dist/types/product/share/share.recipe.d.ts +3 -0
- package/dist/types/product/shortcutssheet/index.d.ts +1 -0
- package/dist/types/product/shortcutssheet/shortcutssheet.d.ts +49 -0
- package/dist/types/product/shortcutssheet/shortcutssheet.recipe.d.ts +3 -0
- package/dist/types/product/taskeditpopover/index.d.ts +2 -0
- package/dist/types/product/taskeditpopover/taskeditpopover.d.ts +67 -0
- package/dist/types/product/taskeditpopover/taskeditpopover.recipe.d.ts +3 -0
- package/dist/types/product/workspaceswitchermenu/index.d.ts +1 -0
- package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.d.ts +54 -0
- package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.recipe.d.ts +19 -0
- package/dist/types/tokens/contract.d.ts +13 -0
- package/dist/types/tokens/palette.d.ts +52 -0
- package/dist/types/tokens/semantic.d.ts +20 -0
- package/dist/workspaceswitchermenu.css +4 -0
- package/dist/workspaceswitchermenu.d.ts +7 -0
- package/dist/workspaceswitchermenu.layered.css +6 -0
- package/package.json +41 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { FloatingPortal, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, } from "../../core/index.js";
|
|
4
|
+
import { FloatingPortal, isInsideLaterFloatingLayer, placeFloating, placeSubmenu, registerFloatingLayer, } from "../../core/index.js";
|
|
5
5
|
import { menuRecipe } from "./menu.recipe.js";
|
|
6
6
|
import { useMenuKeyboard } from "./menu-keyboard.js";
|
|
7
7
|
const MenuContext = React.createContext(null);
|
|
@@ -12,7 +12,7 @@ const MenuContext = React.createContext(null);
|
|
|
12
12
|
* typeahead, Enter/Space select, Esc closes and returns focus to the trigger);
|
|
13
13
|
* closes on outside click. Flips up / right-aligns when space is tight.
|
|
14
14
|
*/
|
|
15
|
-
export function Menu({ trigger, children, align = "start", open: openProp, defaultOpen, onOpenChange, className, contentClassName, "aria-label": ariaLabel, }) {
|
|
15
|
+
export function Menu({ trigger, children, align = "start", placement = "menu", open: openProp, defaultOpen, onOpenChange, className, contentClassName, "aria-label": ariaLabel, }) {
|
|
16
16
|
// The surface and the button that opens it were two unconnected nodes: the
|
|
17
17
|
// menu/dialog had no id, so nothing could point at it, and no accessible
|
|
18
18
|
// name, so a screen reader announced a bare "menu"/"dialog". Labelling the
|
|
@@ -28,6 +28,7 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
28
28
|
onOpenChange?.(next);
|
|
29
29
|
}, [openProp, onOpenChange]);
|
|
30
30
|
const [up, setUp] = React.useState(false);
|
|
31
|
+
const [submenuAlign, setSubmenuAlign] = React.useState("start");
|
|
31
32
|
const [pos, setPos] = React.useState({});
|
|
32
33
|
const rootRef = React.useRef(null);
|
|
33
34
|
const triggerRef = React.useRef(null);
|
|
@@ -39,6 +40,38 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
39
40
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
40
41
|
if (!rect)
|
|
41
42
|
return;
|
|
43
|
+
if (placement === "submenu") {
|
|
44
|
+
// The content div is already in the DOM by this point (it's part of
|
|
45
|
+
// the same render this effect runs after, just not yet painted) so its
|
|
46
|
+
// real rendered size/padding are measurable, not guessed.
|
|
47
|
+
const content = contentRef.current;
|
|
48
|
+
const size = content?.getBoundingClientRect() ?? {
|
|
49
|
+
width: 200,
|
|
50
|
+
height: 0,
|
|
51
|
+
};
|
|
52
|
+
const padTop = content
|
|
53
|
+
? Number.parseFloat(getComputedStyle(content).paddingTop) || 0
|
|
54
|
+
: 0;
|
|
55
|
+
const { style, align: side, up: flipUp, } = placeSubmenu({
|
|
56
|
+
// Not a `{...rect}` spread: DOMRect's numeric fields are accessors
|
|
57
|
+
// on its prototype, not own enumerable properties, so a spread
|
|
58
|
+
// silently drops everything except the two overridden below.
|
|
59
|
+
rect: {
|
|
60
|
+
left: rect.left,
|
|
61
|
+
right: rect.right,
|
|
62
|
+
top: rect.top - padTop,
|
|
63
|
+
bottom: rect.bottom - padTop,
|
|
64
|
+
},
|
|
65
|
+
size,
|
|
66
|
+
// Measured flush against the item, no gap (crisp#616).
|
|
67
|
+
gap: 0,
|
|
68
|
+
anchor: triggerRef.current,
|
|
69
|
+
});
|
|
70
|
+
setSubmenuAlign(side);
|
|
71
|
+
setUp(flipUp);
|
|
72
|
+
setPos(style);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
42
75
|
const { style, up: flipUp } = placeFloating({
|
|
43
76
|
rect,
|
|
44
77
|
align,
|
|
@@ -104,12 +137,28 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
104
137
|
document.addEventListener("keydown", onKey);
|
|
105
138
|
return () => document.removeEventListener("keydown", onKey);
|
|
106
139
|
}, [open, close]);
|
|
107
|
-
const { onKeyDown: onContentKeyDown } = useMenuKeyboard({
|
|
140
|
+
const { onKeyDown: onContentKeyDown, items } = useMenuKeyboard({
|
|
108
141
|
open,
|
|
109
142
|
close,
|
|
110
143
|
dismiss: () => setOpen(false),
|
|
111
144
|
contentRef,
|
|
112
145
|
});
|
|
146
|
+
// Roving tabindex (crisp#964): every item hardcodes tabIndex={-1} for the
|
|
147
|
+
// arrow-key model above, which means none of them is ever a real Tab stop
|
|
148
|
+
// on its own -- kept in sync with wherever focus actually lands, same as
|
|
149
|
+
// MenuSheet's identical fix (#966).
|
|
150
|
+
const onContentFocus = React.useCallback((e) => {
|
|
151
|
+
for (const el of items())
|
|
152
|
+
el.tabIndex = -1;
|
|
153
|
+
e.target.tabIndex = 0;
|
|
154
|
+
}, [items]);
|
|
155
|
+
React.useEffect(() => {
|
|
156
|
+
if (!open)
|
|
157
|
+
return;
|
|
158
|
+
const [first] = items();
|
|
159
|
+
if (first)
|
|
160
|
+
first.tabIndex = 0;
|
|
161
|
+
}, [open, items]);
|
|
113
162
|
const triggerEl = React.cloneElement(trigger, {
|
|
114
163
|
ref: triggerRef,
|
|
115
164
|
id: triggerId,
|
|
@@ -125,17 +174,24 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
125
174
|
});
|
|
126
175
|
return (_jsxs("div", { ref: rootRef, className: menuRecipe({ className }), children: [triggerEl, open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("div", { ref: contentRef, className: contentClassName
|
|
127
176
|
? `crisp-menu-content ${contentClassName}`
|
|
128
|
-
: "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: _jsx(MenuContext.Provider, { value: { close }, children: children }) }) }))] }));
|
|
177
|
+
: "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": placement === "submenu" ? submenuAlign : align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, onFocus: onContentFocus, children: _jsx(MenuContext.Provider, { value: { close }, children: children }) }) }))] }));
|
|
129
178
|
}
|
|
130
179
|
Menu.displayName = "Menu";
|
|
131
|
-
|
|
180
|
+
/**
|
|
181
|
+
* A row inside a `Menu`. Forwards its ref and spreads unrecognised button
|
|
182
|
+
* props (crisp#616) so it can double as another `Menu`'s own `trigger` —
|
|
183
|
+
* `Menu` clones its trigger with `ref`/`onClick`/`aria-*`, which a plain
|
|
184
|
+
* function component would silently drop.
|
|
185
|
+
*/
|
|
186
|
+
export const MenuItem = React.forwardRef(function MenuItem({ children, icon, shortcut, count, trailingIcon, danger, disabled, onSelect, closeOnSelect = true, onClick, ...rest }, ref) {
|
|
132
187
|
const ctx = React.useContext(MenuContext);
|
|
133
|
-
return (_jsxs("button", { type: "button", role: "menuitem", tabIndex: -1, disabled: disabled, "data-disabled": disabled ? "" : undefined, className: `crisp-menu-item${danger ? " crisp-menu-item--danger" : ""}`, onClick: () => {
|
|
188
|
+
return (_jsxs("button", { ref: ref, type: "button", role: "menuitem", tabIndex: -1, disabled: disabled, "data-disabled": disabled ? "" : undefined, className: `crisp-menu-item${danger ? " crisp-menu-item--danger" : ""}`, onClick: (e) => {
|
|
189
|
+
onClick?.(e);
|
|
134
190
|
onSelect?.();
|
|
135
191
|
if (closeOnSelect)
|
|
136
192
|
ctx?.close();
|
|
137
|
-
}, children: [icon, _jsx("span", { className: "crisp-menu-item-label", children: children }), count != null && _jsx("span", { className: "crisp-menu-item-count", children: count }), shortcut && _jsx("span", { className: "crisp-menu-item-shortcut", children: shortcut })] }));
|
|
138
|
-
}
|
|
193
|
+
}, ...rest, children: [icon, _jsx("span", { className: "crisp-menu-item-label", children: children }), count != null && (_jsx("span", { className: "crisp-menu-item-count", children: count })), shortcut && (_jsx("span", { className: "crisp-menu-item-shortcut", children: shortcut })), trailingIcon] }));
|
|
194
|
+
});
|
|
139
195
|
MenuItem.displayName = "MenuItem";
|
|
140
196
|
export function MenuCheckboxItem({ children, checked, onCheckedChange, disabled, }) {
|
|
141
197
|
return (_jsxs("button", { type: "button", role: "menuitemcheckbox", "aria-checked": checked, tabIndex: -1, disabled: disabled, "data-disabled": disabled ? "" : undefined, className: "crisp-menu-item", onClick: () => onCheckedChange?.(!checked), children: [checked ? (_jsx(Check, { className: "crisp-menu-item-check", size: 15, strokeWidth: 2 })) : (_jsx("span", { className: "crisp-menu-item-check--placeholder" })), _jsx("span", { className: "crisp-menu-item-label", children: children })] }));
|
|
@@ -21,6 +21,9 @@ function toneFor(t) {
|
|
|
21
21
|
return TONES[h % TONES.length];
|
|
22
22
|
}
|
|
23
23
|
function Chip({ opt }) {
|
|
24
|
+
if (opt.swatch) {
|
|
25
|
+
return (_jsx("span", { className: "crisp-multiselect-swatch", "data-swatch": opt.swatch, children: opt.label }));
|
|
26
|
+
}
|
|
24
27
|
return (_jsx("span", { className: "crisp-recordstable-tag", "data-tone": opt.tone ?? toneFor(opt.value), children: opt.label }));
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
@@ -104,6 +104,24 @@ const config = defineRecipe({
|
|
|
104
104
|
.crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, ${c.bgPurple} 16%, transparent); color: color-mix(in srgb, ${c.bgPurple} 80%, ${c.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgPurple} 30%, transparent); }
|
|
105
105
|
.crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, ${c.bgCyan} 18%, transparent); color: color-mix(in srgb, ${c.bgCyan} 82%, ${c.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgCyan} 30%, transparent); }
|
|
106
106
|
.crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgOrange} 30%, transparent); }
|
|
107
|
+
/* your-moon/crisp#998: Attio's own 12-colour tag palette (measured live,
|
|
108
|
+
see tokens/palette.ts's tagSwatch1 comment) -- flat black text, no
|
|
109
|
+
ring/border, unlike the tone family above. 7px radius/10px text are
|
|
110
|
+
measured literals, no matching step in the shared scale (same as
|
|
111
|
+
Select's own .crisp-select-swatch). */
|
|
112
|
+
.crisp-multiselect-swatch { display: inline-flex; align-items: center; height: ${c.size.chip}; padding: 0 ${c.space["1"]}; border-radius: 7px; font-size: 10px; font-weight: ${c.weight.regular}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: ${c.fgTagSwatch}; }
|
|
113
|
+
.crisp-multiselect-swatch[data-swatch="1"] { background: ${c.bgTagSwatch1}; }
|
|
114
|
+
.crisp-multiselect-swatch[data-swatch="2"] { background: ${c.bgTagSwatch2}; }
|
|
115
|
+
.crisp-multiselect-swatch[data-swatch="3"] { background: ${c.bgTagSwatch3}; }
|
|
116
|
+
.crisp-multiselect-swatch[data-swatch="4"] { background: ${c.bgTagSwatch4}; }
|
|
117
|
+
.crisp-multiselect-swatch[data-swatch="5"] { background: ${c.bgTagSwatch5}; }
|
|
118
|
+
.crisp-multiselect-swatch[data-swatch="6"] { background: ${c.bgTagSwatch6}; }
|
|
119
|
+
.crisp-multiselect-swatch[data-swatch="7"] { background: ${c.bgTagSwatch7}; }
|
|
120
|
+
.crisp-multiselect-swatch[data-swatch="8"] { background: ${c.bgTagSwatch8}; }
|
|
121
|
+
.crisp-multiselect-swatch[data-swatch="9"] { background: ${c.bgTagSwatch9}; }
|
|
122
|
+
.crisp-multiselect-swatch[data-swatch="10"] { background: ${c.bgTagSwatch10}; }
|
|
123
|
+
.crisp-multiselect-swatch[data-swatch="11"] { background: ${c.bgTagSwatch11}; }
|
|
124
|
+
.crisp-multiselect-swatch[data-swatch="12"] { background: ${c.bgTagSwatch12}; }
|
|
107
125
|
`,
|
|
108
126
|
variants: {
|
|
109
127
|
// Owner's follow-up after #867/#879/#882 (crisp/#889): SIZE is Attio's
|
|
@@ -181,7 +181,7 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
181
181
|
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, size, invalid, className }), ...rest, children: [name && (selected || required) && (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
182
182
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
183
183
|
// validation, which is the only reason this input is not hidden
|
|
184
|
-
onChange: () => { } })), _jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "crisp-select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-select-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
184
|
+
onChange: () => { } })), _jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "crisp-select-value", children: selectedOption ? (selectedOption.swatch ? (_jsx("span", { className: "crisp-select-swatch", "data-swatch": selectedOption.swatch, children: selectedOption.triggerLabel ?? selectedOption.label })) : (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] }))) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-select-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
185
185
|
setQuery(e.target.value);
|
|
186
186
|
setHighlight(0);
|
|
187
187
|
onSearchChange?.(e.target.value);
|
|
@@ -192,7 +192,7 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
192
192
|
// heading, not one each.
|
|
193
193
|
const showGroupLabel = opt.group !== undefined &&
|
|
194
194
|
opt.group !== filtered[i - 1]?.group;
|
|
195
|
-
return (_jsxs(React.Fragment, { children: [showGroupLabel && (_jsx("div", { className: "crisp-select-label", children: opt.group })), _jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), opt.description !== undefined ? (_jsxs("span", { className: "crisp-select-option-body", children: [_jsx("span", { className: "crisp-select-option-label", children: opt.label }), _jsx("span", { className: "crisp-select-option-description", children: opt.description })] })) : (_jsx("span", { className: "crisp-select-option-label", children: opt.label })), isSel && (_jsx(Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] })] }, opt.value));
|
|
195
|
+
return (_jsxs(React.Fragment, { children: [showGroupLabel && (_jsx("div", { className: "crisp-select-label", children: opt.group })), _jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [!opt.swatch && opt.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), opt.description !== undefined ? (_jsxs("span", { className: "crisp-select-option-body", children: [_jsx("span", { className: "crisp-select-option-label", children: opt.swatch ? (_jsx("span", { className: "crisp-select-swatch", "data-swatch": opt.swatch, children: opt.label })) : (opt.label) }), _jsx("span", { className: "crisp-select-option-description", children: opt.description })] })) : (_jsx("span", { className: "crisp-select-option-label", children: opt.swatch ? (_jsx("span", { className: "crisp-select-swatch", "data-swatch": opt.swatch, children: opt.label })) : (opt.label) })), isSel && (_jsx(Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] })] }, opt.value));
|
|
196
196
|
}))] }) }))] }));
|
|
197
197
|
}
|
|
198
198
|
Select.displayName = "Select";
|
|
@@ -134,6 +134,25 @@ const config = defineRecipe({
|
|
|
134
134
|
.crisp-select-option[data-disabled] .crisp-select-option-description,
|
|
135
135
|
.crisp-select-option[data-disabled] .crisp-select-option-check { color: ${c.fgDisabled}; }
|
|
136
136
|
.crisp-select-option-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${c.radius.full}; margin-inline-end: ${c.space["2"]}; }
|
|
137
|
+
/* your-moon/crisp#998: Attio's own 12-colour tag palette (measured live,
|
|
138
|
+
see tokens/palette.ts's tagSwatch1 comment) -- the label itself as a
|
|
139
|
+
filled pill, not a dot beside it. 7px radius/10px text have no
|
|
140
|
+
matching step in the shared scale (radius.xs is 4px, .sm is 6px;
|
|
141
|
+
text.xs is 11px), so both are the measured literals, same as
|
|
142
|
+
DataTable's own avatar font-size (#891). */
|
|
143
|
+
.crisp-select-swatch { display: inline-flex; align-items: center; height: ${c.size.chip}; padding: 0 ${c.space["1"]}; border-radius: 7px; font-size: 10px; font-weight: ${c.weight.regular}; color: ${c.fgTagSwatch}; }
|
|
144
|
+
.crisp-select-swatch[data-swatch="1"] { background: ${c.bgTagSwatch1}; }
|
|
145
|
+
.crisp-select-swatch[data-swatch="2"] { background: ${c.bgTagSwatch2}; }
|
|
146
|
+
.crisp-select-swatch[data-swatch="3"] { background: ${c.bgTagSwatch3}; }
|
|
147
|
+
.crisp-select-swatch[data-swatch="4"] { background: ${c.bgTagSwatch4}; }
|
|
148
|
+
.crisp-select-swatch[data-swatch="5"] { background: ${c.bgTagSwatch5}; }
|
|
149
|
+
.crisp-select-swatch[data-swatch="6"] { background: ${c.bgTagSwatch6}; }
|
|
150
|
+
.crisp-select-swatch[data-swatch="7"] { background: ${c.bgTagSwatch7}; }
|
|
151
|
+
.crisp-select-swatch[data-swatch="8"] { background: ${c.bgTagSwatch8}; }
|
|
152
|
+
.crisp-select-swatch[data-swatch="9"] { background: ${c.bgTagSwatch9}; }
|
|
153
|
+
.crisp-select-swatch[data-swatch="10"] { background: ${c.bgTagSwatch10}; }
|
|
154
|
+
.crisp-select-swatch[data-swatch="11"] { background: ${c.bgTagSwatch11}; }
|
|
155
|
+
.crisp-select-swatch[data-swatch="12"] { background: ${c.bgTagSwatch12}; }
|
|
137
156
|
.crisp-select-option-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: ${c.space["0.5"]}; }
|
|
138
157
|
.crisp-select-option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
139
158
|
.crisp-select-option-description { min-width: 0; overflow: hidden; text-overflow: ellipsis; color: ${c.fgNeutralSubtle}; }
|
|
@@ -21,7 +21,13 @@ const config = defineRecipe({
|
|
|
21
21
|
(padding: 0 12px) and independently on a body cell's text inset
|
|
22
22
|
(getBoundingClientRect delta from the cell edge). Matches
|
|
23
23
|
ColumnCalc's own already-correct 12px trigger inset. */
|
|
24
|
-
|
|
24
|
+
/* #719: overflow: clip, not hidden -- both render the ellipsis
|
|
25
|
+
identically, but hidden still honours a programmatic scrollLeft
|
|
26
|
+
assignment (a screenshot tool, a browser extension) even though it
|
|
27
|
+
already blocks user-driven wheel/scrollbar input; clip leaves no
|
|
28
|
+
scroll position for anything to move, so the truncated text can
|
|
29
|
+
never be panned out of view. */
|
|
30
|
+
& th, & td { position: relative; height: ${c.size.tableRow}; padding: 0 ${c.space["3"]}; border-bottom: 1px solid ${c.borderSubtle}; border-inline-end: 1px solid ${c.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: clip; text-overflow: ellipsis; }
|
|
25
31
|
/* #554: sticky on the vertical axis, mirroring RecordsTable's own
|
|
26
32
|
\`.crisp-recordstable-th\`. z-index 2 sits above a plain body cell (auto)
|
|
27
33
|
and below a sticky-column corner cell (DataTable's \`th[data-sticky]\`,
|
|
@@ -16,10 +16,11 @@ import { createPortal } from "react-dom";
|
|
|
16
16
|
* `Dialog` was rendering offset by the dialog's own box instead of anchored
|
|
17
17
|
* to its trigger.
|
|
18
18
|
*
|
|
19
|
-
* Every caller renders this conditionally on its own `open` state
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* Every caller renders this conditionally on its own `open` state. A fixture
|
|
20
|
+
* CAN set that true at mount (crisp#964 needs exactly that, to give Menu's
|
|
21
|
+
* roving-tabindex guard something to inspect), so this checks for `window`
|
|
22
|
+
* itself rather than assuming the caller never does — the same guard
|
|
23
|
+
* `@zag-js/react`'s own `Portal` uses for the identical reason.
|
|
23
24
|
*
|
|
24
25
|
* `sourceRef` is the caller's own root — still in its original DOM position,
|
|
25
26
|
* never portaled — used to carry a `data-theme` set on some ancestor of it
|
|
@@ -35,6 +36,8 @@ import { createPortal } from "react-dom";
|
|
|
35
36
|
* element carrying that attribute.
|
|
36
37
|
*/
|
|
37
38
|
export function FloatingPortal({ children, sourceRef, }) {
|
|
39
|
+
if (typeof window === "undefined")
|
|
40
|
+
return children;
|
|
38
41
|
const theme = sourceRef.current
|
|
39
42
|
?.closest("[data-theme]")
|
|
40
43
|
?.getAttribute("data-theme");
|
package/dist/esm/core/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { hasNestedFloatingLayer, hasOpenFloatingLayer, isInsideLaterFloatingLaye
|
|
|
9
9
|
export { formValueStyle } from "./form-value.js";
|
|
10
10
|
export { CrispMessagesProvider, useCrispComponentDefaults, useCrispMessage, useCrispMessages, } from "./i18n.js";
|
|
11
11
|
export { matchMentionTrigger, } from "./mention.js";
|
|
12
|
-
export { placeFloating, } from "./place-floating.js";
|
|
12
|
+
export { placeFloating, placeSubmenu, } from "./place-floating.js";
|
|
13
13
|
export { Primitive } from "./primitive.js";
|
|
14
14
|
export { resolveRecipe } from "./resolve.js";
|
|
15
15
|
export * from "./rtl.js";
|
|
@@ -33,3 +33,40 @@ export function placeFloating({ rect, align = "start", gap, flipBelow, anchor, }
|
|
|
33
33
|
style.left = rect.left;
|
|
34
34
|
return { style, up };
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Where a cascading submenu goes, BESIDE the item that opens it — not below
|
|
38
|
+
* it, the way `placeFloating`'s own dropdown does. Opens toward the inline
|
|
39
|
+
* end by default (further into the reading direction) and flips to the
|
|
40
|
+
* inline start when the surface's measured width doesn't fit there.
|
|
41
|
+
*
|
|
42
|
+
* Live-measured on Attio's own object-page "…" menu (crisp#616): the
|
|
43
|
+
* Notifications submenu sits with NO gap against the item's opposite edge
|
|
44
|
+
* (its own edge lines up exactly with the item's), so callers pass `gap: 0`
|
|
45
|
+
* for this specific cascade rather than the 4px a dropdown uses against its
|
|
46
|
+
* trigger.
|
|
47
|
+
*/
|
|
48
|
+
export function placeSubmenu({ rect, size, gap, anchor, }) {
|
|
49
|
+
const rtl = isRtlElement(anchor);
|
|
50
|
+
// "end" here is the inline-end direction FROM the item, e.g. further
|
|
51
|
+
// right in LTR — the room available there is measured from whichever
|
|
52
|
+
// physical edge that is.
|
|
53
|
+
const roomTowardEnd = rtl ? rect.left : window.innerWidth - rect.right;
|
|
54
|
+
const opensEnd = roomTowardEnd >= size.width + gap;
|
|
55
|
+
const align = opensEnd ? "start" : "end";
|
|
56
|
+
const style = {};
|
|
57
|
+
// Same formula as `placeFloating`'s own `toRight`, applied to the opposite
|
|
58
|
+
// geometry: there `align` picks which of the TRIGGER's edges the layer's
|
|
59
|
+
// matching edge lines up with; here it picks which side of the ITEM the
|
|
60
|
+
// layer cascades toward.
|
|
61
|
+
const anchorRight = align === "end" ? !rtl : rtl;
|
|
62
|
+
if (anchorRight)
|
|
63
|
+
style.right = window.innerWidth - rect.left + gap;
|
|
64
|
+
else
|
|
65
|
+
style.left = rect.right + gap;
|
|
66
|
+
const up = rect.top + size.height > window.innerHeight;
|
|
67
|
+
if (up)
|
|
68
|
+
style.bottom = Math.max(0, window.innerHeight - rect.bottom);
|
|
69
|
+
else
|
|
70
|
+
style.top = rect.top;
|
|
71
|
+
return { style, align, up };
|
|
72
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -76,6 +76,7 @@ export * from "./core/index.js";
|
|
|
76
76
|
// the generic primitives (Table, DataTable, Button, …) to build your own UI from.
|
|
77
77
|
export * from "./product/activityfeed/index.js";
|
|
78
78
|
export * from "./product/appshell/index.js";
|
|
79
|
+
export * from "./product/assigneepicker/index.js";
|
|
79
80
|
export * from "./product/authshell/index.js";
|
|
80
81
|
export * from "./product/board/index.js";
|
|
81
82
|
export * from "./product/command/index.js";
|
|
@@ -89,21 +90,28 @@ export * from "./product/inbox/index.js";
|
|
|
89
90
|
export * from "./product/listpage/index.js";
|
|
90
91
|
export * from "./product/memberssettings/index.js";
|
|
91
92
|
export * from "./product/navitem/index.js";
|
|
93
|
+
export * from "./product/notesgallery/index.js";
|
|
92
94
|
export * from "./product/notificationssettings/index.js";
|
|
95
|
+
export * from "./product/objectpagemenu/index.js";
|
|
93
96
|
export * from "./product/objectsettings/index.js";
|
|
94
97
|
export * from "./product/onboarding/index.js";
|
|
95
98
|
export * from "./product/recordpage/index.js";
|
|
96
99
|
export * from "./product/recordpanel/index.js";
|
|
100
|
+
export * from "./product/recordpicker/index.js";
|
|
97
101
|
export * from "./product/richtext/index.js";
|
|
98
102
|
export * from "./product/savedviews/index.js";
|
|
99
103
|
export * from "./product/settingssection/index.js";
|
|
100
104
|
export * from "./product/settingsshell/index.js";
|
|
105
|
+
export * from "./product/share/index.js";
|
|
106
|
+
export * from "./product/shortcutssheet/index.js";
|
|
101
107
|
export * from "./product/sidebar/index.js";
|
|
102
108
|
export * from "./product/statlist/index.js";
|
|
109
|
+
export * from "./product/taskeditpopover/index.js";
|
|
103
110
|
export * from "./product/tasklist/index.js";
|
|
104
111
|
export * from "./product/themepicker/index.js";
|
|
105
112
|
export * from "./product/trialbanner/index.js";
|
|
106
113
|
export * from "./product/viewtoggle/index.js";
|
|
107
114
|
export * from "./product/workspacegeneralsettings/index.js";
|
|
115
|
+
export * from "./product/workspaceswitchermenu/index.js";
|
|
108
116
|
// design tokens + recipe core (for building custom components)
|
|
109
117
|
export * from "./tokens/index.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Search, X } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Avatar } from "../../components/avatar/index.js";
|
|
5
|
+
import { PersonChip } from "../../components/personchip/index.js";
|
|
6
|
+
import { useListboxKeyboard } from "../../components/select/index.js";
|
|
7
|
+
import { useCrispMessage, useCrispMessages } from "../../core/index.js";
|
|
8
|
+
import { assigneepickerRecipe } from "./assigneepicker.recipe.js";
|
|
9
|
+
// Same five hashed avatar tones as `PersonChip`'s own (transcribed, not
|
|
10
|
+
// imported -- PersonChip's are private to that file, and this is the same
|
|
11
|
+
// one-line pure function `personchip.tsx` itself transcribes from
|
|
12
|
+
// `datatable.tsx`'s `TAG_TONES` rather than sharing an import; see its own
|
|
13
|
+
// comment for why that's fine here).
|
|
14
|
+
const HASH_TONES = ["brand", "orange", "cyan", "purple", "green"];
|
|
15
|
+
function hashTone(name) {
|
|
16
|
+
let h = 0;
|
|
17
|
+
for (let i = 0; i < name.length; i++)
|
|
18
|
+
h = (h * 31 + name.charCodeAt(i)) >>> 0;
|
|
19
|
+
return HASH_TONES[h % HASH_TONES.length];
|
|
20
|
+
}
|
|
21
|
+
function initials(name) {
|
|
22
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
23
|
+
if (parts.length === 0)
|
|
24
|
+
return "?";
|
|
25
|
+
return (parts[0][0] + (parts[1]?.[0] ?? "")).toUpperCase();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The picker behind a task's "Assigned to" chip -- measured live on
|
|
29
|
+
* app.attio.com's Tasks page ("Edit task" -> "Assigned to You" -> its own
|
|
30
|
+
* sub-popover, `Popover`-sized at the same measured 350px): a "Find a
|
|
31
|
+
* user..." search, the current assignee as a removable pill, then a plain
|
|
32
|
+
* option list, or "No users" when nothing matches. Attio's task assignment
|
|
33
|
+
* is single-user (one pill, replaced by picking another), unlike a
|
|
34
|
+
* multi-value field.
|
|
35
|
+
*/
|
|
36
|
+
export function AssigneePicker({ users, assignee = null, onAssigneeChange, query: queryProp, onQueryChange, searchLabel: searchLabelProp, removeLabel: removeLabelProp, emptyLabel: emptyLabelProp, className, ...rest }) {
|
|
37
|
+
const searchLabel = useCrispMessage(searchLabelProp, "Find a user...");
|
|
38
|
+
const removeLabel = useCrispMessage(removeLabelProp, "Remove assignee");
|
|
39
|
+
// crisp/#471: `emptyLabel` is typed `React.ReactNode`, so it resolves by
|
|
40
|
+
// hand rather than through `useCrispMessage`, which is string-only --
|
|
41
|
+
// same reasoning as `Inbox`'s own `emptyTitle`/`emptySubtitle`.
|
|
42
|
+
const crispMessages = useCrispMessages();
|
|
43
|
+
const emptyLabel = emptyLabelProp ?? crispMessages["No users"] ?? "No users";
|
|
44
|
+
const [internalQuery, setInternalQuery] = React.useState("");
|
|
45
|
+
const query = queryProp ?? internalQuery;
|
|
46
|
+
const setQuery = (q) => {
|
|
47
|
+
if (queryProp === undefined)
|
|
48
|
+
setInternalQuery(q);
|
|
49
|
+
onQueryChange?.(q);
|
|
50
|
+
};
|
|
51
|
+
const [highlight, setHighlight] = React.useState(0);
|
|
52
|
+
const autoId = React.useId();
|
|
53
|
+
const listboxId = `${autoId}-listbox`;
|
|
54
|
+
const optionId = (i) => `${autoId}-opt-${i}`;
|
|
55
|
+
const q = query.trim().toLowerCase();
|
|
56
|
+
const candidates = users.filter((u) => u.id !== assignee?.id);
|
|
57
|
+
const filtered = q
|
|
58
|
+
? candidates.filter((u) => u.name.toLowerCase().includes(q))
|
|
59
|
+
: candidates;
|
|
60
|
+
const select = (u) => {
|
|
61
|
+
onAssigneeChange?.(u);
|
|
62
|
+
setQuery("");
|
|
63
|
+
setHighlight(0);
|
|
64
|
+
};
|
|
65
|
+
// crisp's shared popup-listbox model (#198), same as Select/MultiSelect --
|
|
66
|
+
// arrows, Home/End and wrapping, instead of a second hand-rolled one.
|
|
67
|
+
const { onKeyDown } = useListboxKeyboard({
|
|
68
|
+
enabledIndexes: filtered.map((_, i) => i),
|
|
69
|
+
highlight,
|
|
70
|
+
setHighlight,
|
|
71
|
+
commit: () => {
|
|
72
|
+
const u = filtered[highlight];
|
|
73
|
+
if (u)
|
|
74
|
+
select(u);
|
|
75
|
+
},
|
|
76
|
+
// Escape/Tab are for the enclosing Popover to handle (it owns open/close
|
|
77
|
+
// here, this picker has no floating state of its own).
|
|
78
|
+
close: () => { },
|
|
79
|
+
dismiss: () => { },
|
|
80
|
+
});
|
|
81
|
+
const activeId = highlight >= 0 && highlight < filtered.length
|
|
82
|
+
? optionId(highlight)
|
|
83
|
+
: undefined;
|
|
84
|
+
return (_jsxs("div", { className: assigneepickerRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-assigneepicker-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { type: "text", className: "crisp-assigneepicker-search-input", placeholder: searchLabel, value: query, onChange: (e) => {
|
|
85
|
+
setQuery(e.target.value);
|
|
86
|
+
setHighlight(0);
|
|
87
|
+
}, onKeyDown: onKeyDown, role: "combobox", "aria-expanded": "true", "aria-controls": listboxId, "aria-activedescendant": activeId,
|
|
88
|
+
// biome-ignore lint/a11y/noAutofocus: focus the search field the moment its enclosing popover opens
|
|
89
|
+
autoFocus: true })] }), assignee != null && (_jsx("div", { className: "crisp-assigneepicker-pill", children: _jsx(PersonChip, { name: assignee.name, avatar: assignee.avatar, trailing: _jsx("button", { type: "button", className: "crisp-assigneepicker-remove", "aria-label": removeLabel, onClick: () => onAssigneeChange?.(null), children: _jsx(X, { size: 10, strokeWidth: 2 }) }) }) })), filtered.length === 0 ? (_jsx("div", { className: "crisp-assigneepicker-empty", children: emptyLabel })) : (_jsx("div", { className: "crisp-assigneepicker-list", role: "listbox", id: listboxId, children: filtered.map((u, i) => (_jsxs("button", { type: "button", id: optionId(i), role: "option", tabIndex: -1, "aria-selected": i === highlight, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-assigneepicker-option", onMouseEnter: () => setHighlight(i), onClick: () => select(u), children: [_jsx(Avatar, { size: "sm", shape: "circle", tone: u.avatar ? "neutral" : hashTone(u.name), style: { flexShrink: 0, marginInlineEnd: 8 }, children: u.avatar ? _jsx("img", { src: u.avatar, alt: "" }) : initials(u.name) }), u.name] }, u.id))) }))] }));
|
|
90
|
+
}
|
|
91
|
+
AssigneePicker.displayName = "AssigneePicker";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// The assignee picker behind a task's "Assigned to" chip -- measured live on
|
|
4
|
+
// app.attio.com's Tasks page (Edit task -> "Assigned to You" -> its own
|
|
5
|
+
// sub-popover): a borderless 14px/500 "Find a user..." search row, the
|
|
6
|
+
// current assignee as a removable pill below it, then a plain option list
|
|
7
|
+
// (32px rows, radius 8, the same measured hover crisp's other listboxes
|
|
8
|
+
// share -- see menu.recipe.ts's own comment) or a centred "No users" line.
|
|
9
|
+
// The popover shell itself (radius/shadow/width) is `Popover`'s own --
|
|
10
|
+
// this is only the 350px-wide content Attio's own picker measured at.
|
|
11
|
+
const config = defineRecipe({
|
|
12
|
+
name: "assigneepicker",
|
|
13
|
+
base: `display: flex; flex-direction: column; box-sizing: border-box; width: 350px; font-family: ${c.font.sans};
|
|
14
|
+
|
|
15
|
+
& .crisp-assigneepicker-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 36px; padding: 0 ${c.space["3"]}; color: ${c.fgMuted}; }
|
|
16
|
+
& .crisp-assigneepicker-search svg { flex-shrink: 0; }
|
|
17
|
+
& .crisp-assigneepicker-search-input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; line-height: 20px; color: ${c.fgPrimary}; }
|
|
18
|
+
& .crisp-assigneepicker-search-input::placeholder { color: ${c.fgMuted}; }
|
|
19
|
+
|
|
20
|
+
& .crisp-assigneepicker-pill { display: flex; padding: 0 ${c.space["3"]} ${c.space["2"]}; }
|
|
21
|
+
& .crisp-assigneepicker-remove { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 14px; height: 14px; margin: 0; padding: 0; border: none; background: none; color: inherit; font-family: inherit; cursor: pointer; }
|
|
22
|
+
|
|
23
|
+
& .crisp-assigneepicker-empty { padding: ${c.space["4"]} ${c.space["3"]}; text-align: center; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
24
|
+
|
|
25
|
+
& .crisp-assigneepicker-list { display: flex; flex-direction: column; margin: 0; padding: ${c.space["1"]}; list-style: none; }
|
|
26
|
+
& .crisp-assigneepicker-option { display: flex; align-items: center; width: 100%; box-sizing: border-box; height: 32px; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: none; font-family: inherit; text-align: start; cursor: pointer; }
|
|
27
|
+
& .crisp-assigneepicker-option:hover, & .crisp-assigneepicker-option[data-highlighted] { background: ${c.bgRaised}; }`,
|
|
28
|
+
variants: {},
|
|
29
|
+
defaultVariants: {},
|
|
30
|
+
compoundVariants: [],
|
|
31
|
+
});
|
|
32
|
+
export const assigneepickerRecipe = resolveRecipe(config);
|
|
33
|
+
export default config;
|
|
@@ -18,10 +18,18 @@ export function Inbox({ notifications = [], title: titleProp, tabs, activeTab, o
|
|
|
18
18
|
// own `empty`.
|
|
19
19
|
const crispMessages = useCrispMessages();
|
|
20
20
|
const title = titleProp ?? crispMessages.Notifications ?? "Notifications";
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// A tab's own empty copy wins over the instance-level prop, which wins
|
|
22
|
+
// over the built-in default — same precedence as `title`/`emptyAction`.
|
|
23
|
+
const activeTabDef = tabs?.find((t) => t.id === activeTab);
|
|
24
|
+
const emptyTitle = activeTabDef?.emptyTitle ??
|
|
25
|
+
emptyTitleProp ??
|
|
26
|
+
crispMessages["No notifications"] ??
|
|
27
|
+
"No notifications";
|
|
28
|
+
const emptySubtitle = activeTabDef?.emptySubtitle ??
|
|
29
|
+
emptySubtitleProp ??
|
|
23
30
|
crispMessages["Notifications such as mentions and important updates will be displayed here."] ??
|
|
24
31
|
"Notifications such as mentions and important updates will be displayed here.";
|
|
32
|
+
const resolvedEmptyAction = activeTabDef?.emptyAction ?? emptyAction;
|
|
25
33
|
const tabsLabel = useCrispMessage(tabsLabelProp, "Notification views");
|
|
26
34
|
// crisp's shared tablist model, the one `Tabs` runs (#195). Hand-rolling a
|
|
27
35
|
// second one is how Inbox ended up with no arrow keys, no roving tabindex
|
|
@@ -32,6 +40,6 @@ export function Inbox({ notifications = [], title: titleProp, tabs, activeTab, o
|
|
|
32
40
|
select: (id) => onTabChange?.(id),
|
|
33
41
|
listRef,
|
|
34
42
|
});
|
|
35
|
-
return (_jsxs("div", { className: inboxRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-inbox-head", children: [_jsx("div", { className: "crisp-inbox-title", children: title }), tabs && tabs.length > 0 && (_jsx("div", { ref: listRef, className: "crisp-inbox-tabs", role: "tablist", "aria-label": tabsLabel, onKeyDown: onKeyDown, children: tabs.map((t) => (_jsxs("button", { type: "button", role: "tab", "aria-selected": t.id === activeTab, tabIndex: tabIndexFor(t.id), "data-value": t.id, disabled: t.disabled, className: "crisp-inbox-tab", "data-active": t.id === activeTab ? "" : undefined, onClick: () => onTabChange?.(t.id), children: [t.label, t.count != null && (_jsxs("span", { className: "crisp-inbox-tab-count", children: ["(", t.count, ")"] }))] }, t.id))) }))] }), notifications.length === 0 ? (_jsxs("div", { className: "crisp-inbox-empty", children: [_jsx("span", { className: "crisp-inbox-empty-icon", children: _jsx(Bell, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-inbox-empty-title", children: emptyTitle }), emptySubtitle != null && (_jsx("div", { className: "crisp-inbox-empty-sub", children: emptySubtitle })),
|
|
43
|
+
return (_jsxs("div", { className: inboxRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-inbox-head", children: [_jsx("div", { className: "crisp-inbox-title", children: title }), tabs && tabs.length > 0 && (_jsx("div", { ref: listRef, className: "crisp-inbox-tabs", role: "tablist", "aria-label": tabsLabel, onKeyDown: onKeyDown, children: tabs.map((t) => (_jsxs("button", { type: "button", role: "tab", "aria-selected": t.id === activeTab, tabIndex: tabIndexFor(t.id), "data-value": t.id, disabled: t.disabled, className: "crisp-inbox-tab", "data-active": t.id === activeTab ? "" : undefined, onClick: () => onTabChange?.(t.id), children: [t.label, t.count != null && (_jsxs("span", { className: "crisp-inbox-tab-count", children: ["(", t.count, ")"] }))] }, t.id))) }))] }), notifications.length === 0 ? (_jsxs("div", { className: "crisp-inbox-empty", children: [_jsx("span", { className: "crisp-inbox-empty-icon", children: _jsx(Bell, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-inbox-empty-title", children: emptyTitle }), emptySubtitle != null && (_jsx("div", { className: "crisp-inbox-empty-sub", children: emptySubtitle })), resolvedEmptyAction != null && (_jsx("div", { className: "crisp-inbox-empty-action", children: resolvedEmptyAction }))] })) : (_jsx("div", { className: "crisp-inbox-list", children: notifications.map((n) => (_jsxs("button", { type: "button", className: "crisp-inbox-row", onClick: n.onClick, children: [n.avatar != null && (_jsx("span", { className: "crisp-inbox-row-avatar", children: n.avatar })), _jsxs("span", { className: "crisp-inbox-row-body", children: [_jsx("span", { className: "crisp-inbox-row-msg", children: n.message }), n.time != null && (_jsx("span", { className: "crisp-inbox-row-time", children: n.time }))] }), n.unread && _jsx("span", { className: "crisp-inbox-row-unread" })] }, n.id))) }))] }));
|
|
36
44
|
}
|
|
37
45
|
Inbox.displayName = "Inbox";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Tabs } from "../../components/tabs/index.js";
|
|
4
|
+
import { useCrispMessage, useCrispMessages } from "../../core/index.js";
|
|
5
|
+
import { EntityCard } from "../entitycard/index.js";
|
|
6
|
+
import { notesgalleryRecipe } from "./notesgallery.recipe.js";
|
|
7
|
+
function NoteCard({ note }) {
|
|
8
|
+
return (_jsx(EntityCard, { author: note.linkedRecord, title: note.title, excerpt: note.excerpt, footer: note.author, date: note.date, onClick: note.onClick }));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The standalone Notes gallery -- measured live on app.attio.com's Notes
|
|
12
|
+
* page: "Notes N"/"Templates N" sub-tabs, a toolbar (Sort/View
|
|
13
|
+
* toggle/settings/New note, all consumer-composed), a dashed "Favorites"
|
|
14
|
+
* region, and date-grouped card sections. The card itself is `EntityCard`
|
|
15
|
+
* (crisp#474's own "measured from a Notes grid" component) — this shell
|
|
16
|
+
* only adds the gallery chrome around it.
|
|
17
|
+
*/
|
|
18
|
+
export function NotesGallery({ notesCount, templatesCount, activeTab: activeTabProp, defaultActiveTab = "notes", onActiveTabChange, notesLabel: notesLabelProp, templatesLabel: templatesLabelProp, leading, trailing, favoritesLabel: favoritesLabelProp, favorites, favoritesEmptyLabel: favoritesEmptyLabelProp, groups, emptyLabel: emptyLabelProp, className, ...rest }) {
|
|
19
|
+
const notesLabel = useCrispMessage(notesLabelProp, "Notes");
|
|
20
|
+
const templatesLabel = useCrispMessage(templatesLabelProp, "Templates");
|
|
21
|
+
const favoritesLabel = useCrispMessage(favoritesLabelProp, "Favorites");
|
|
22
|
+
const favoritesEmptyLabel = useCrispMessage(favoritesEmptyLabelProp, "Notes that you favorite will appear here");
|
|
23
|
+
// crisp/#471: `emptyLabel` is typed `React.ReactNode`, so it resolves by
|
|
24
|
+
// hand rather than through `useCrispMessage`, which is string-only.
|
|
25
|
+
const crispMessages = useCrispMessages();
|
|
26
|
+
const emptyLabel = emptyLabelProp ?? crispMessages["No notes"] ?? "No notes";
|
|
27
|
+
const [internalTab, setInternalTab] = React.useState(defaultActiveTab);
|
|
28
|
+
const activeTab = activeTabProp ?? internalTab;
|
|
29
|
+
const setActiveTab = (v) => {
|
|
30
|
+
if (activeTabProp === undefined)
|
|
31
|
+
setInternalTab(v);
|
|
32
|
+
onActiveTabChange?.(v);
|
|
33
|
+
};
|
|
34
|
+
const total = groups.reduce((s, g) => s + g.notes.length, 0);
|
|
35
|
+
return (_jsxs("div", { className: notesgalleryRecipe({ className }), ...rest, children: [_jsx("div", { className: "crisp-notesgallery-tabs", children: _jsx(Tabs, { tabs: [
|
|
36
|
+
{ value: "notes", label: notesLabel, badge: notesCount },
|
|
37
|
+
{
|
|
38
|
+
value: "templates",
|
|
39
|
+
label: templatesLabel,
|
|
40
|
+
badge: templatesCount,
|
|
41
|
+
},
|
|
42
|
+
], value: activeTab, onValueChange: setActiveTab }) }), _jsxs("div", { className: "crisp-notesgallery-toolbar", children: [leading, trailing != null && (_jsx("div", { className: "crisp-notesgallery-toolbar-trail", children: trailing }))] }), _jsxs("div", { className: "crisp-notesgallery-favorites-section", children: [_jsx("div", { className: "crisp-notesgallery-favorites-label", children: favoritesLabel }), _jsx("div", { className: "crisp-notesgallery-favorites", children: favorites != null && favorites.length > 0 ? (_jsx("div", { className: "crisp-notesgallery-grid", children: favorites.map((n) => (_jsx(NoteCard, { note: n }, n.id))) })) : (_jsx("div", { className: "crisp-notesgallery-favorites-empty", children: favoritesEmptyLabel })) })] }), _jsx("div", { className: "crisp-notesgallery-body", children: total === 0 ? (_jsx("div", { className: "crisp-notesgallery-empty", children: emptyLabel })) : (groups.map((g) => (_jsxs("div", { children: [_jsxs("div", { className: "crisp-notesgallery-group", children: [g.label, _jsx("span", { className: "crisp-notesgallery-group-count", children: g.count })] }), _jsx("div", { className: "crisp-notesgallery-grid", children: g.notes.map((n) => (_jsx(NoteCard, { note: n }, n.id))) })] }, g.id)))) })] }));
|
|
43
|
+
}
|
|
44
|
+
NotesGallery.displayName = "NotesGallery";
|