@crisp-ui-kit/crisp 0.57.3 → 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 +1 -1
- package/dist/button.layered.css +1 -1
- package/dist/cjs/components/button/button.recipe.js +15 -10
- package/dist/cjs/components/datatable/datatable.js +6 -3
- package/dist/cjs/components/datatable/datatable.recipe.js +35 -2
- package/dist/cjs/components/menu/menu.js +48 -8
- 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/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/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/recordpage.js +64 -5
- package/dist/cjs/product/recordpage/recordpage.recipe.js +38 -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 +40 -7
- package/dist/datatable.layered.css +40 -7
- package/dist/esm/components/button/button.recipe.js +15 -10
- package/dist/esm/components/datatable/datatable.js +6 -3
- package/dist/esm/components/datatable/datatable.recipe.js +35 -2
- package/dist/esm/components/menu/menu.js +47 -7
- 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/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/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/recordpage.js +64 -5
- package/dist/esm/product/recordpage/recordpage.recipe.js +38 -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 +36 -0
- package/dist/recordpage.layered.css +36 -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 +387 -8
- package/dist/styles.layered.css +387 -8
- 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/button/button.recipe.d.ts +2 -3
- package/dist/types/components/datatable/datatable.d.ts +8 -0
- 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/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/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 +96 -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
|
@@ -315,9 +315,24 @@ const config = defineRecipe({
|
|
|
315
315
|
Avatar's own already-shipped shape="rounded" value, not a new number.
|
|
316
316
|
shape="circle" reuses Avatar's own shape="circle" value (radius.full)
|
|
317
317
|
the same way, measured live on a real Attio person avatar. */
|
|
318
|
-
& .crisp-datatable-avatar { width: ${c.size.icon}; height: ${c.size.icon}; border-radius: 30%; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: ${c.weight.semibold}; text-transform: uppercase; color: ${c.
|
|
318
|
+
& .crisp-datatable-avatar { width: ${c.size.icon}; height: ${c.size.icon}; border-radius: 30%; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: ${c.weight.semibold}; text-transform: uppercase; color: ${c.fgOnTone}; background: ${c.bgBrandSolid}; }
|
|
319
319
|
& .crisp-datatable-avatar[data-shape="circle"] { border-radius: ${c.radius.full}; }
|
|
320
|
-
|
|
320
|
+
/* your-moon/crisp#891: every data-tone override painted ONLY a
|
|
321
|
+
background, inheriting the base rule's white fgOnBrand -- white
|
|
322
|
+
clears AA against none of these seven fills (brand 3.23:1, success
|
|
323
|
+
3.96, warning 4.12, danger 3.76, purple 3.58, cyan 2.30, orange
|
|
324
|
+
2.77). fgOnTone (ink, #101112 -- semantic.ts's own comment: "dark ink
|
|
325
|
+
clears AA on every tone in both themes") clears 4.5:1+ on every fill
|
|
326
|
+
here with a comfortable margin (brand 5.85, success 4.77/6.29,
|
|
327
|
+
danger 5.03/6.19, purple 5.27, cyan 8.23, orange 6.83, light/dark),
|
|
328
|
+
so the base rule's colour moves there and every override keeps it --
|
|
329
|
+
EXCEPT warning, whose raw fill (bgWarning) only clears 4.59:1 in
|
|
330
|
+
light, too thin a margin to rely on: it takes bgWarningSolid instead,
|
|
331
|
+
the same solid-warning-plus-ink pair Badge's own solid variant
|
|
332
|
+
already established (13.98/7.83), for the same reason (see
|
|
333
|
+
tokens/palette.ts's amberSolid comment). No background changes
|
|
334
|
+
otherwise -- the fills themselves are unaffected, only the text. */
|
|
335
|
+
& .crisp-datatable-avatar[data-tone="success"] { background: ${c.bgSuccess}; color: ${c.fgOnTone}; } & .crisp-datatable-avatar[data-tone="warning"] { background: ${c.bgWarningSolid}; color: ${c.fgOnTone}; } & .crisp-datatable-avatar[data-tone="danger"] { background: ${c.bgDanger}; color: ${c.fgOnTone}; } & .crisp-datatable-avatar[data-tone="purple"] { background: ${c.bgPurple}; color: ${c.fgOnTone}; } & .crisp-datatable-avatar[data-tone="cyan"] { background: ${c.bgCyan}; color: ${c.fgOnTone}; } & .crisp-datatable-avatar[data-tone="orange"] { background: ${c.bgOrange}; color: ${c.fgOnTone}; }
|
|
321
336
|
& .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
|
|
322
337
|
& .crisp-datatable-tags { display: flex; align-items: center; gap: ${c.space["1"]}; overflow: hidden; }
|
|
323
338
|
& .crisp-datatable-tag { display: inline-flex; align-items: center; height: ${c.size.chip}; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.chip}; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; white-space: nowrap; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${c.fgPrimary} 82%, transparent); }
|
|
@@ -328,6 +343,24 @@ const config = defineRecipe({
|
|
|
328
343
|
& .crisp-datatable-tag[data-tone="purple"] { background: color-mix(in srgb, ${c.bgPurple} 16%, transparent); color: color-mix(in srgb, ${c.bgPurple} 80%, ${c.fgPrimary}); }
|
|
329
344
|
& .crisp-datatable-tag[data-tone="cyan"] { background: color-mix(in srgb, ${c.bgCyan} 18%, transparent); color: color-mix(in srgb, ${c.bgCyan} 82%, ${c.fgPrimary}); }
|
|
330
345
|
& .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); }
|
|
346
|
+
/* your-moon/crisp#998: Attio's own 12-colour tag palette (measured
|
|
347
|
+
live, see tokens/palette.ts's tagSwatch1 comment) -- flat black
|
|
348
|
+
text, no ring, unlike the tone family above. 7px radius/10px text
|
|
349
|
+
are measured literals (no matching step in the shared scale, same
|
|
350
|
+
as Select's own .crisp-select-swatch). */
|
|
351
|
+
& .crisp-datatable-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; color: ${c.fgTagSwatch}; }
|
|
352
|
+
& .crisp-datatable-swatch[data-swatch="1"] { background: ${c.bgTagSwatch1}; }
|
|
353
|
+
& .crisp-datatable-swatch[data-swatch="2"] { background: ${c.bgTagSwatch2}; }
|
|
354
|
+
& .crisp-datatable-swatch[data-swatch="3"] { background: ${c.bgTagSwatch3}; }
|
|
355
|
+
& .crisp-datatable-swatch[data-swatch="4"] { background: ${c.bgTagSwatch4}; }
|
|
356
|
+
& .crisp-datatable-swatch[data-swatch="5"] { background: ${c.bgTagSwatch5}; }
|
|
357
|
+
& .crisp-datatable-swatch[data-swatch="6"] { background: ${c.bgTagSwatch6}; }
|
|
358
|
+
& .crisp-datatable-swatch[data-swatch="7"] { background: ${c.bgTagSwatch7}; }
|
|
359
|
+
& .crisp-datatable-swatch[data-swatch="8"] { background: ${c.bgTagSwatch8}; }
|
|
360
|
+
& .crisp-datatable-swatch[data-swatch="9"] { background: ${c.bgTagSwatch9}; }
|
|
361
|
+
& .crisp-datatable-swatch[data-swatch="10"] { background: ${c.bgTagSwatch10}; }
|
|
362
|
+
& .crisp-datatable-swatch[data-swatch="11"] { background: ${c.bgTagSwatch11}; }
|
|
363
|
+
& .crisp-datatable-swatch[data-swatch="12"] { background: ${c.bgTagSwatch12}; }
|
|
331
364
|
& .crisp-datatable-num { font-variant-numeric: tabular-nums; }
|
|
332
365
|
& .crisp-datatable-muted { color: ${c.fgMuted}; }
|
|
333
366
|
/* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
|
|
@@ -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,
|
|
@@ -141,17 +174,24 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
141
174
|
});
|
|
142
175
|
return (_jsxs("div", { ref: rootRef, className: menuRecipe({ className }), children: [triggerEl, open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("div", { ref: contentRef, className: contentClassName
|
|
143
176
|
? `crisp-menu-content ${contentClassName}`
|
|
144
|
-
: "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, onFocus: onContentFocus, 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 }) }) }))] }));
|
|
145
178
|
}
|
|
146
179
|
Menu.displayName = "Menu";
|
|
147
|
-
|
|
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) {
|
|
148
187
|
const ctx = React.useContext(MenuContext);
|
|
149
|
-
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);
|
|
150
190
|
onSelect?.();
|
|
151
191
|
if (closeOnSelect)
|
|
152
192
|
ctx?.close();
|
|
153
|
-
}, 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 })] }));
|
|
154
|
-
}
|
|
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
|
+
});
|
|
155
195
|
MenuItem.displayName = "MenuItem";
|
|
156
196
|
export function MenuCheckboxItem({ children, checked, onCheckedChange, disabled, }) {
|
|
157
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}; }
|
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;
|
|
@@ -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";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// The standalone Notes gallery -- measured live on app.attio.com's Notes
|
|
4
|
+
// page: a toolbar (leading Sort control, trailing view toggle/View
|
|
5
|
+
// settings/New note, both consumer-composed slots -- crisp doesn't own the
|
|
6
|
+
// exact button set, same reasoning as `ListPage`'s own leading/trailing), a
|
|
7
|
+
// dashed "Favorites" region, then date-grouped sections (a group header +
|
|
8
|
+
// count badge, transcribed from `ListPage`'s own already-measured values
|
|
9
|
+
// rather than re-deriving them -- this IS the same Attio pattern, applied
|
|
10
|
+
// above a card grid instead of a row list) holding a responsive card grid of
|
|
11
|
+
// `EntityCard`s.
|
|
12
|
+
//
|
|
13
|
+
// The grid itself: a single real note card measured 356px wide -- with only
|
|
14
|
+
// one note in the workspace there was no second card to read a column gap or
|
|
15
|
+
// count from, so this uses a responsive `auto-fill` grid at that measured
|
|
16
|
+
// per-card width instead of inventing a fixed column count or gap.
|
|
17
|
+
const config = defineRecipe({
|
|
18
|
+
name: "notesgallery",
|
|
19
|
+
base: `display: flex; flex-direction: column; min-height: 0; box-sizing: border-box; font-family: ${c.font.sans}; color: ${c.fgPrimary};
|
|
20
|
+
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
21
|
+
|
|
22
|
+
& .crisp-notesgallery-tabs { display: flex; align-items: center; gap: ${c.space["2"]}; height: 44px; flex-shrink: 0; padding: 0 ${c.space["3"]}; }
|
|
23
|
+
|
|
24
|
+
& .crisp-notesgallery-toolbar { display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.header}; flex-shrink: 0; padding: 0 ${c.space["3"]}; }
|
|
25
|
+
& .crisp-notesgallery-toolbar-trail { display: flex; align-items: center; gap: ${c.space["2"]}; margin-inline-start: auto; }
|
|
26
|
+
|
|
27
|
+
& .crisp-notesgallery-favorites-section { background: ${c.bgSubtle}; box-shadow: inset 0 -1px 0 ${c.borderSubtle}; }
|
|
28
|
+
& .crisp-notesgallery-favorites-label { padding: ${c.space["2"]} ${c.space["4"]} 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
|
|
29
|
+
& .crisp-notesgallery-favorites { display: flex; align-items: center; justify-content: center; min-height: 200px; padding: ${c.space["6"]}; }
|
|
30
|
+
& .crisp-notesgallery-favorites-empty { box-sizing: border-box; width: 100%; padding: ${c.space["6"]}; border: 1px dashed ${c.borderDefault}; border-radius: ${c.radius.xl}; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }
|
|
31
|
+
|
|
32
|
+
& .crisp-notesgallery-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 ${c.space["3"]} ${c.space["3"]}; }
|
|
33
|
+
& .crisp-notesgallery-group { display: flex; align-items: center; gap: ${c.space["1.5"]}; height: ${c.size.menuRow}; padding: ${c.space["2"]} ${c.space["1"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
|
|
34
|
+
& .crisp-notesgallery-group-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); font-size: ${c.text.xs}; color: ${c.fgMuted}; }
|
|
35
|
+
& .crisp-notesgallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: ${c.space["4"]}; padding-bottom: ${c.space["4"]}; }
|
|
36
|
+
|
|
37
|
+
& .crisp-notesgallery-empty { padding: ${c.space["8"]} ${c.space["3"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }`,
|
|
38
|
+
variants: {},
|
|
39
|
+
defaultVariants: {},
|
|
40
|
+
compoundVariants: [],
|
|
41
|
+
});
|
|
42
|
+
export const notesgalleryRecipe = resolveRecipe(config);
|
|
43
|
+
export default config;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./objectpagemenu.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Bell, ChevronRight, LayoutGrid, MessageSquare, MoreHorizontal, Settings, } from "lucide-react";
|
|
3
|
+
import { Button } from "../../components/button/index.js";
|
|
4
|
+
import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
|
|
5
|
+
import { useCrispMessage } from "../../core/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* The page-level "…" menu on Attio's object page: Object settings / Manage
|
|
8
|
+
* attributes / a Notifications submenu ("Connect Slack workspace") — crisp
|
|
9
|
+
* parity checklist rows G-020/P-005 (crisp#616). Live-measured on
|
|
10
|
+
* app.attio.com/techpartners/companies: 208×114 popover (12px radius, 4px
|
|
11
|
+
* padding, 32px rows), a hairline separator before Notifications, and its
|
|
12
|
+
* submenu opening flush against the item with no gap, flipping side when
|
|
13
|
+
* the measured width doesn't fit (`placeSubmenu`, core/place-floating.ts).
|
|
14
|
+
*/
|
|
15
|
+
export function ObjectPageMenu({ trigger, onObjectSettings, onManageAttributes, onConnectSlack, objectSettingsLabel, manageAttributesLabel, notificationsLabel, connectSlackLabel, "aria-label": ariaLabelProp, align = "end", }) {
|
|
16
|
+
const objectSettings = useCrispMessage(objectSettingsLabel, "Object settings");
|
|
17
|
+
const manageAttributes = useCrispMessage(manageAttributesLabel, "Manage attributes");
|
|
18
|
+
const notifications = useCrispMessage(notificationsLabel, "Notifications");
|
|
19
|
+
const connectSlack = useCrispMessage(connectSlackLabel, "Connect Slack workspace");
|
|
20
|
+
const ariaLabel = useCrispMessage(ariaLabelProp, "Show menu");
|
|
21
|
+
return (_jsxs(Menu, { align: align, "aria-label": ariaLabel, trigger: trigger ?? (_jsx(Button, { intent: "ghost", size: "md", shape: "icon", "aria-label": ariaLabel, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) })), children: [_jsx(MenuItem, { icon: _jsx(Settings, { size: 16, strokeWidth: 1.75 }), onSelect: onObjectSettings, children: objectSettings }), _jsx(MenuItem, { icon: _jsx(LayoutGrid, { size: 16, strokeWidth: 1.75 }), onSelect: onManageAttributes, children: manageAttributes }), _jsx(MenuSeparator, {}), _jsx(Menu, { placement: "submenu", "aria-label": notifications, trigger: _jsx(MenuItem, { icon: _jsx(Bell, { size: 16, strokeWidth: 1.75 }), trailingIcon: _jsx(ChevronRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75 }), closeOnSelect: false, children: notifications }), children: _jsx(MenuItem, { icon: _jsx(MessageSquare, { size: 16, strokeWidth: 1.75 }), onSelect: onConnectSlack, children: connectSlack }) })] }));
|
|
22
|
+
}
|
|
23
|
+
ObjectPageMenu.displayName = "ObjectPageMenu";
|