@crisp-ui-kit/crisp 0.55.6 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/callout.css +6 -0
- package/dist/callout.d.ts +2 -2
- package/dist/callout.layered.css +6 -0
- package/dist/cjs/components/callout/callout.js +2 -2
- package/dist/cjs/components/callout/callout.recipe.js +15 -2
- package/dist/cjs/components/input/input.recipe.js +6 -0
- package/dist/cjs/components/menu/menu.js +2 -2
- package/dist/cjs/components/menu/menu.recipe.js +8 -0
- package/dist/cjs/components/multiselect/multiselect.js +23 -10
- package/dist/cjs/components/multiselect/multiselect.recipe.js +95 -65
- package/dist/cjs/components/select/listbox-keyboard.js +7 -1
- package/dist/cjs/components/select/select.js +24 -11
- package/dist/cjs/components/select/select.recipe.js +150 -78
- package/dist/cjs/components/statusdot/index.js +7 -0
- package/dist/cjs/components/statusdot/statusdot.js +15 -0
- package/dist/cjs/components/statusdot/statusdot.recipe.js +34 -0
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +3 -3
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
- package/dist/cjs/components/tabs/tabs.js +63 -4
- package/dist/cjs/components/tabs/tabs.recipe.js +13 -0
- package/dist/cjs/components/text/text.recipe.js +10 -0
- package/dist/cjs/components/textlink/textlink.js +2 -2
- package/dist/cjs/components/textlink/textlink.recipe.js +18 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/product/recordpage/recordpage.js +17 -4
- package/dist/cjs/tokens/elevation.js +7 -0
- package/dist/cjs/tokens/palette.js +41 -0
- package/dist/cjs/tokens/scale.js +16 -1
- package/dist/cjs/tokens/semantic.js +18 -0
- package/dist/esm/components/callout/callout.js +2 -2
- package/dist/esm/components/callout/callout.recipe.js +15 -2
- package/dist/esm/components/input/input.recipe.js +6 -0
- package/dist/esm/components/menu/menu.js +2 -2
- package/dist/esm/components/menu/menu.recipe.js +8 -0
- package/dist/esm/components/multiselect/multiselect.js +23 -10
- package/dist/esm/components/multiselect/multiselect.recipe.js +95 -65
- package/dist/esm/components/select/listbox-keyboard.js +7 -1
- package/dist/esm/components/select/select.js +24 -11
- package/dist/esm/components/select/select.recipe.js +150 -78
- package/dist/esm/components/statusdot/index.js +2 -0
- package/dist/esm/components/statusdot/statusdot.js +12 -0
- package/dist/esm/components/statusdot/statusdot.recipe.js +31 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +3 -3
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
- package/dist/esm/components/tabs/tabs.js +63 -4
- package/dist/esm/components/tabs/tabs.recipe.js +13 -0
- package/dist/esm/components/text/text.recipe.js +10 -0
- package/dist/esm/components/textlink/textlink.js +2 -2
- package/dist/esm/components/textlink/textlink.recipe.js +18 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/product/recordpage/recordpage.js +18 -5
- package/dist/esm/tokens/elevation.js +7 -0
- package/dist/esm/tokens/palette.js +41 -0
- package/dist/esm/tokens/scale.js +16 -1
- package/dist/esm/tokens/semantic.js +18 -0
- package/dist/input.css +4 -0
- package/dist/input.d.ts +2 -2
- package/dist/input.layered.css +4 -0
- package/dist/menu.css +8 -0
- package/dist/menu.layered.css +8 -0
- package/dist/multiselect.css +53 -29
- package/dist/multiselect.d.ts +3 -1
- package/dist/multiselect.layered.css +53 -29
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +50 -7
- package/dist/select.css +80 -42
- package/dist/select.d.ts +3 -2
- package/dist/select.layered.css +80 -42
- package/dist/statusdot.css +3 -0
- package/dist/statusdot.d.ts +7 -0
- package/dist/statusdot.layered.css +5 -0
- package/dist/styles.css +238 -73
- package/dist/styles.layered.css +238 -73
- package/dist/tabletoolbar.css +8 -1
- package/dist/tabletoolbar.layered.css +8 -1
- package/dist/tabs.css +6 -0
- package/dist/tabs.layered.css +6 -0
- package/dist/text.css +6 -0
- package/dist/text.d.ts +2 -2
- package/dist/text.layered.css +6 -0
- package/dist/textlink.css +9 -0
- package/dist/textlink.d.ts +2 -1
- package/dist/textlink.layered.css +9 -0
- package/dist/tokens.css +54 -1
- package/dist/tokens.json +50 -7
- package/dist/types/components/callout/callout.d.ts +9 -1
- package/dist/types/components/callout/callout.recipe.d.ts +12 -2
- package/dist/types/components/input/input.d.ts +2 -0
- package/dist/types/components/input/input.recipe.d.ts +3 -0
- package/dist/types/components/menu/menu.d.ts +7 -1
- package/dist/types/components/multiselect/multiselect.d.ts +24 -6
- package/dist/types/components/multiselect/multiselect.recipe.d.ts +24 -0
- package/dist/types/components/select/listbox-keyboard.d.ts +10 -1
- package/dist/types/components/select/select.d.ts +28 -6
- package/dist/types/components/select/select.recipe.d.ts +22 -10
- package/dist/types/components/statusdot/index.d.ts +2 -0
- package/dist/types/components/statusdot/statusdot.d.ts +21 -0
- package/dist/types/components/statusdot/statusdot.recipe.d.ts +15 -0
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +14 -0
- package/dist/types/components/tabs/tabs.d.ts +9 -1
- package/dist/types/components/text/text.d.ts +4 -0
- package/dist/types/components/text/text.recipe.d.ts +12 -0
- package/dist/types/components/textlink/textlink.d.ts +8 -1
- package/dist/types/components/textlink/textlink.recipe.d.ts +10 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/product/recordpage/recordpage.d.ts +25 -1
- package/dist/types/tokens/contract.d.ts +13 -3
- package/dist/types/tokens/elevation.d.ts +7 -0
- package/dist/types/tokens/palette.d.ts +36 -0
- package/dist/types/tokens/scale.d.ts +6 -1
- package/dist/types/tokens/semantic.d.ts +18 -0
- package/package.json +6 -1
|
@@ -76,8 +76,12 @@ exports.recordPageLabels = {
|
|
|
76
76
|
copyRecordLink: "Copy record link",
|
|
77
77
|
deleteRecord: "Delete record",
|
|
78
78
|
linkRecords: "Link records",
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// Tooltip copy re-measured live for #865 (was "Log a note"/"Create a
|
|
80
|
+
// task" — the field names stay, only the visible text changes to match
|
|
81
|
+
// Attio's own React-tooltip strings on these two icon buttons exactly).
|
|
82
|
+
logANote: "New note",
|
|
83
|
+
createATask: "New task",
|
|
84
|
+
runWorkflow: "Run workflow",
|
|
81
85
|
fullScreen: "Full screen",
|
|
82
86
|
exitFullScreen: "Exit full screen",
|
|
83
87
|
resizeDetails: "Resize details column",
|
|
@@ -457,7 +461,7 @@ RecordDetails.displayName = "RecordDetails";
|
|
|
457
461
|
* Emails sections (Notes & Tasks are live — add and check things off). Drop it
|
|
458
462
|
* into the app's main content area.
|
|
459
463
|
*/
|
|
460
|
-
function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, ...rest }) {
|
|
464
|
+
function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, quickActions: quickActionsProp, ...rest }) {
|
|
461
465
|
const t = useRecordPageLabels(labels);
|
|
462
466
|
const [favorite, setFavorite] = React.useState(false);
|
|
463
467
|
// Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
|
|
@@ -566,6 +570,15 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
566
570
|
},
|
|
567
571
|
];
|
|
568
572
|
const highlights = highlightsProp === undefined ? defaultHighlights : highlightsProp;
|
|
573
|
+
// #865: the built-in CRM icon set for the quick-action row, used when the
|
|
574
|
+
// caller omits `quickActions` -- same "caller's own list, not a merge"
|
|
575
|
+
// choice `tabs` (#379) and `highlights` (#528) already make.
|
|
576
|
+
const defaultQuickActions = [
|
|
577
|
+
{ label: t.logANote, icon: lucide_react_1.FileText },
|
|
578
|
+
{ label: t.runWorkflow, icon: lucide_react_1.Workflow },
|
|
579
|
+
{ label: t.createATask, icon: lucide_react_1.SquareCheck },
|
|
580
|
+
];
|
|
581
|
+
const quickActions = quickActionsProp ?? defaultQuickActions;
|
|
569
582
|
const ti = { size: 15, strokeWidth: 1.75 };
|
|
570
583
|
const builtinTabs = [
|
|
571
584
|
{ value: "overview", label: t.tabs.overview, icon: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { ...ti }) },
|
|
@@ -674,7 +687,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
674
687
|
const effectivePanel = tabsProp
|
|
675
688
|
? (active) => tabsProp.find((tb) => tb.id === active)?.render()
|
|
676
689
|
: builtinPanel;
|
|
677
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpage_recipe_1.recordpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-topbar", children: [(0, jsx_runtime_1.jsxs)("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-name", children: name })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Minimize2, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Maximize2, { size: 15, strokeWidth: 1.75 })) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.recordActions, trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), (0, jsx_runtime_1.jsx)(menu_1.MenuSeparator, {}), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-body", children: [(0, jsx_runtime_1.jsxs)("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-entity-name", children: name }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-quickactions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), (0, jsx_runtime_1.
|
|
690
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpage_recipe_1.recordpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-topbar", children: [(0, jsx_runtime_1.jsxs)("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-name", children: name })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Minimize2, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Maximize2, { size: 15, strokeWidth: 1.75 })) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.recordActions, trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), (0, jsx_runtime_1.jsx)(menu_1.MenuSeparator, {}), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-body", children: [(0, jsx_runtime_1.jsxs)("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-entity-name", children: name }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-quickactions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 16, strokeWidth: 1.75 }), t.addToList] }), quickActions.map((a, i) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": a.label, onClick: a.onSelect, children: (0, jsx_runtime_1.jsx)(a.icon, { size: 15, strokeWidth: 1.75 }) }, i)))] }), (0, jsx_runtime_1.jsx)(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.lists, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-addbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": DETAILS_MIN_W, "aria-valuemax": DETAILS_MAX_W, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
|
|
678
691
|
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
679
692
|
return;
|
|
680
693
|
// The pointer drag learned its direction in #212; this did not.
|
|
@@ -59,4 +59,11 @@ exports.elevation = {
|
|
|
59
59
|
* nobody measured for them.
|
|
60
60
|
*/
|
|
61
61
|
fieldNoRing: `0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04)`,
|
|
62
|
+
/**
|
|
63
|
+
* Select's own listbox shadow (crisp/#867) — seed-design's `$shadow.s3`, a
|
|
64
|
+
* single layer, NOT `floating` above. This component follows seed, not
|
|
65
|
+
* Attio, for the same reason `DatePicker` follows seed's calendar
|
|
66
|
+
* structure: an explicit owner request to match seed-design exactly.
|
|
67
|
+
*/
|
|
68
|
+
selectFloating: `0 4px 16px ${contract_1.contract.shadowSelect}`,
|
|
62
69
|
};
|
|
@@ -47,6 +47,15 @@ exports.palette = {
|
|
|
47
47
|
// both themes (Switch's own spec), so this reuses ringField's dark value
|
|
48
48
|
// rather than inventing a third "field-adjacent ring, dark" colour.
|
|
49
49
|
ringSwitch: { light: "#d9dade", dark: "#212224" },
|
|
50
|
+
// Callout's bordered/banner look (crisp#869) — measured live on Attio's
|
|
51
|
+
// Objects settings page (the "Create custom objects... Upgrade" Pro-plan
|
|
52
|
+
// banner): `border: 1px solid rgb(214,229,255)`, an opaque colour, not a
|
|
53
|
+
// composited tint of `bgBrandSolid` (checked: no single color-mix
|
|
54
|
+
// percentage matches all three channels at once). Dark value not
|
|
55
|
+
// independently re-measured (Attio's own theme toggle is off-limits) —
|
|
56
|
+
// reuses `ringField`/`ringSwitch`'s existing dark ring (#212224), the same
|
|
57
|
+
// "edge visible against a dark surface" role those already establish.
|
|
58
|
+
ringBrandSoft: { light: "#d6e5ff", dark: "#212224" },
|
|
50
59
|
tileActive: { light: "#ffffff", dark: "#101112" },
|
|
51
60
|
tileActiveShadow: { light: "rgba(0,0,0,0.04)", dark: "rgba(0,0,0,0)" },
|
|
52
61
|
// Dialog's page-dimming backdrop (crisp#677) — measured live behind the
|
|
@@ -162,4 +171,36 @@ exports.palette = {
|
|
|
162
171
|
purpleTint: { light: "#f5f0ff", dark: "#2f1e5a" },
|
|
163
172
|
softPurpleText: { light: "#6238b5", dark: "#d8c4ff" },
|
|
164
173
|
purpleRing: { light: "#e8ddfe", dark: "#45297d" },
|
|
174
|
+
// Select rebuilt to seed-design parity (crisp/#867), NOT Attio -- a
|
|
175
|
+
// deliberate one-component exception, the same way DatePicker already
|
|
176
|
+
// follows seed's calendar structure. Values below are resolved directly
|
|
177
|
+
// from `@seed-design/rootage`'s color.yaml (gray palette), not measured
|
|
178
|
+
// live, since there is no Attio reference for this component anymore.
|
|
179
|
+
// gray-00 / gray-200 (bg.layer-floating) -- the listbox surface. Distinct
|
|
180
|
+
// from `popoverBg` (crisp's Attio-measured popover, #fff/#17181a): the dark
|
|
181
|
+
// value differs (#1d2025), so this is its own token, not a reuse.
|
|
182
|
+
selectListboxBg: { light: "#ffffff", dark: "#1d2025" },
|
|
183
|
+
// gray-400 (stroke.neutral-weak) -- the trigger's real 1px border at rest.
|
|
184
|
+
selectTriggerStroke: { light: "#dcdee3", dark: "#393d46" },
|
|
185
|
+
// static-black/white-alpha (bg.transparent-pressed) -- the trigger/item
|
|
186
|
+
// press tint. Deliberately alpha, not a gray, so it reads the same over
|
|
187
|
+
// either surface colour.
|
|
188
|
+
selectPressedTint: { light: "#00000007", dark: "#ffffff0d" },
|
|
189
|
+
// gray-200 / gray-300 (bg.disabled) -- the trigger's disabled AND readonly
|
|
190
|
+
// fill. Distinct from `neutralWeak` (light matches, dark is #1d2025 not
|
|
191
|
+
// #2b2e35), so its own token rather than a reuse.
|
|
192
|
+
selectDisabledBg: { light: "#f3f4f5", dark: "#2b2e35" },
|
|
193
|
+
// red-700 (stroke.critical-solid) -- the trigger's invalid border. Distinct
|
|
194
|
+
// from crisp's own `bgDangerSolid`/`red700` (#ba2525, an Attio red): seed's
|
|
195
|
+
// red is a different hue entirely, and this component follows seed.
|
|
196
|
+
selectInvalidStroke: { light: "#fa342c", dark: "#ff6e60" },
|
|
197
|
+
// static-black/white-alpha (stroke.neutral-muted) -- the listbox's own
|
|
198
|
+
// divider slot (defined in select.yaml; not yet exposed on any consumer).
|
|
199
|
+
selectDivider: { light: "#00000010", dark: "#ffffff17" },
|
|
200
|
+
// gray-600 (fg.placeholder) -- distinct from `neutralSubtle` (its light/dark
|
|
201
|
+
// pair is the same two hexes, swapped), so its own token, not a reuse.
|
|
202
|
+
selectPlaceholder: { light: "#b0b3ba", dark: "#868b94" },
|
|
203
|
+
// s3 (shadow.yaml) -- the listbox's single-layer floating shadow, in place
|
|
204
|
+
// of crisp's own multi-layer `elevation.floating` for this component only.
|
|
205
|
+
selectShadow: { light: "#0000001f", dark: "#000000cc" },
|
|
165
206
|
};
|
package/dist/cjs/tokens/scale.js
CHANGED
|
@@ -20,6 +20,7 @@ exports.space = {
|
|
|
20
20
|
"2": "8px",
|
|
21
21
|
"2.5": "10px",
|
|
22
22
|
"3": "12px",
|
|
23
|
+
"3.5": "14px", // seed-design's $dimension.x3_5 — Select's medium trigger paddingX (crisp/#867)
|
|
23
24
|
"4": "16px",
|
|
24
25
|
"5": "20px",
|
|
25
26
|
"6": "24px",
|
|
@@ -34,6 +35,7 @@ exports.radius = {
|
|
|
34
35
|
lg: "9px", // rows, tiles, list items
|
|
35
36
|
xl: "12px", // overlay items (command rows)
|
|
36
37
|
"2xl": "16px", // overlay panels (command surface)
|
|
38
|
+
"3xl": "20px", // seed-design's $radius.r5 — Select's listbox corner (crisp/#867)
|
|
37
39
|
full: "999px", // pills
|
|
38
40
|
};
|
|
39
41
|
// Component sizing — recurring measured dimensions that are neither spacing nor
|
|
@@ -52,12 +54,25 @@ exports.size = {
|
|
|
52
54
|
// whole cells and a column gap would cut it.
|
|
53
55
|
calendarCell: "34px", // calendar day pitch (32px day + 2px gap)
|
|
54
56
|
calendarPill: "32px", // the day highlight itself
|
|
57
|
+
// seed-design's Select, resolved from select-trigger.yaml/select-item.yaml
|
|
58
|
+
// (crisp/#867) — this component follows seed, not Attio, so these sit
|
|
59
|
+
// alongside the measured scale rather than folding into it.
|
|
60
|
+
selectTriggerMd: "40px", // $dimension.x10 — the medium (default) trigger height
|
|
61
|
+
selectTriggerLg: "52px", // $dimension.x13 — the large trigger height
|
|
62
|
+
selectIconLg: "20px", // $dimension.x5 — large trigger's chevron
|
|
55
63
|
};
|
|
56
64
|
// Typography — the measured UI runs entirely on Inter. Sizes measured in the sidebar
|
|
57
65
|
// collapse onto 11/12/13/14/16 (lg = 14px is the default body/label size);
|
|
58
66
|
// weights are 400/500/600 with 500 the UI default.
|
|
67
|
+
//
|
|
68
|
+
// `"VK Sans Display"` leads the stack: an owner override (your-moon/crisp#857),
|
|
69
|
+
// not a remeasure of Attio. It only ever matches an INSTALLED copy of the font
|
|
70
|
+
// via the `local()`-only `@font-face` rules in dist/tokens.css (see
|
|
71
|
+
// build/generate-fonts.mjs for why — VK Sans Display's license has no
|
|
72
|
+
// redistribution grant, so crisp never ships the font files). Everywhere it
|
|
73
|
+
// isn't installed, this falls straight through to Inter, unchanged.
|
|
59
74
|
exports.font = {
|
|
60
|
-
sans: '"Inter", system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
75
|
+
sans: '"VK Sans Display", "Inter", system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
61
76
|
};
|
|
62
77
|
exports.text = {
|
|
63
78
|
xs: "11px",
|
|
@@ -85,4 +85,22 @@ exports.semantic = {
|
|
|
85
85
|
bgPurpleSoft: "purpleTint",
|
|
86
86
|
fgPurpleSoft: "softPurpleText",
|
|
87
87
|
ringPurpleSoft: "purpleRing",
|
|
88
|
+
/** Callout's bordered/banner look (crisp#869) — an opaque measured ring,
|
|
89
|
+
* not a `color-mix` tint of `bgBrandSolid` (see palette.ts's own comment). */
|
|
90
|
+
ringBrandSoft: "ringBrandSoft",
|
|
91
|
+
/** Select rebuilt to seed-design parity (crisp/#867) — see palette.ts's
|
|
92
|
+
* comment above `selectListboxBg` for why these are their own tokens
|
|
93
|
+
* rather than a reuse of an Attio-measured one. */
|
|
94
|
+
/** Seed's neutral ink (gray-1000) used as TEXT/icon colour — same palette
|
|
95
|
+
* entry as `strokeFieldFocus` (a ring), aliased separately because the
|
|
96
|
+
* role here is content colour, not a focus ring. */
|
|
97
|
+
fgSelectNeutral: "fieldFocus",
|
|
98
|
+
bgSelectListbox: "selectListboxBg",
|
|
99
|
+
strokeSelectTrigger: "selectTriggerStroke",
|
|
100
|
+
bgSelectPressed: "selectPressedTint",
|
|
101
|
+
bgSelectDisabled: "selectDisabledBg",
|
|
102
|
+
strokeSelectInvalid: "selectInvalidStroke",
|
|
103
|
+
strokeSelectDivider: "selectDivider",
|
|
104
|
+
fgSelectPlaceholder: "selectPlaceholder",
|
|
105
|
+
shadowSelect: "selectShadow",
|
|
88
106
|
};
|
|
@@ -12,11 +12,11 @@ const DEFAULT_ICONS = {
|
|
|
12
12
|
* An inline callout / banner — measured from info notices. `tone` tints
|
|
13
13
|
* the background and icon; pass a `title` and/or body `children`.
|
|
14
14
|
*/
|
|
15
|
-
export function Callout({ tone = "neutral", icon, title, children, action, className, ...rest }) {
|
|
15
|
+
export function Callout({ tone = "neutral", icon, title, children, action, bordered, className, ...rest }) {
|
|
16
16
|
const Fallback = DEFAULT_ICONS[tone];
|
|
17
17
|
const iconNode = icon === undefined
|
|
18
18
|
? Fallback && _jsx(Fallback, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" })
|
|
19
19
|
: icon;
|
|
20
|
-
return (_jsxs("div", { className: calloutRecipe({ className }), ...rest, "data-tone": tone, role: "note", children: [iconNode && _jsx("span", { className: "crisp-callout-icon", children: iconNode }), _jsxs("div", { className: "crisp-callout-body", children: [title != null && _jsx("div", { className: "crisp-callout-title", children: title }), children != null && (_jsx("div", { className: "crisp-callout-desc", children: children }))] }), action && _jsx("div", { className: "crisp-callout-action", children: action })] }));
|
|
20
|
+
return (_jsxs("div", { className: calloutRecipe({ bordered, className }), ...rest, "data-tone": tone, role: "note", children: [iconNode && _jsx("span", { className: "crisp-callout-icon", children: iconNode }), _jsxs("div", { className: "crisp-callout-body", children: [title != null && _jsx("div", { className: "crisp-callout-title", children: title }), children != null && (_jsx("div", { className: "crisp-callout-desc", children: children }))] }), action && _jsx("div", { className: "crisp-callout-action", children: action })] }));
|
|
21
21
|
}
|
|
22
22
|
Callout.displayName = "Callout";
|
|
@@ -18,8 +18,21 @@ const config = defineRecipe({
|
|
|
18
18
|
&[data-tone="warning"] .crisp-callout-icon { color: ${c.bgWarning}; }
|
|
19
19
|
&[data-tone="danger"] { background: color-mix(in srgb, ${c.bgDanger} 10%, transparent); }
|
|
20
20
|
&[data-tone="danger"] .crisp-callout-icon { color: ${c.bgDanger}; }`,
|
|
21
|
-
variants: {
|
|
22
|
-
|
|
21
|
+
variants: {
|
|
22
|
+
// crisp#869: a page-level banner look, measured live on Attio's Objects
|
|
23
|
+
// settings page (the "Create custom objects... Upgrade" Pro-plan
|
|
24
|
+
// nudge) — a real 1px opaque border, 12px radius, 8px/12px padding, and
|
|
25
|
+
// a 14%-brand-tinted background (the same `color-mix` ratio Badge's own
|
|
26
|
+
// soft-brand variant already uses). Only this one brand/info-toned
|
|
27
|
+
// instance was found live; `bordered` doesn't attempt a per-tone border
|
|
28
|
+
// colour for success/warning/danger — no such Attio banner was found to
|
|
29
|
+
// measure one against.
|
|
30
|
+
bordered: {
|
|
31
|
+
true: `border: 1px solid ${c.ringBrandSoft}; border-radius: ${c.radius.xl}; padding: ${c.space["2"]} ${c.space["3"]}; background: color-mix(in srgb, ${c.bgBrandSolid} 14%, transparent);`,
|
|
32
|
+
false: "",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: { bordered: false },
|
|
23
36
|
compoundVariants: [],
|
|
24
37
|
});
|
|
25
38
|
export const calloutRecipe = resolveRecipe(config);
|
|
@@ -41,6 +41,12 @@ const config = defineRecipe({
|
|
|
41
41
|
`,
|
|
42
42
|
variants: {
|
|
43
43
|
size: {
|
|
44
|
+
// crisp#878: not a fresh click-measurement -- reuses `TableToolbar`'s
|
|
45
|
+
// own compact search box shape (`tabletoolbar.chrome.ts`'s `-search`
|
|
46
|
+
// rule), already corroborated against Attio, at the same height as
|
|
47
|
+
// `c.size.row` ("nav item · search · control height"). Same radius and
|
|
48
|
+
// padding `medium` already uses, just 4px shorter.
|
|
49
|
+
sm: `height: ${c.size.row}; border-radius: ${c.radius.md}; & .crisp-input-value { padding-inline: ${c.space["2.5"]}; }`,
|
|
44
50
|
medium: `height: 32px; border-radius: ${c.radius.md}; & .crisp-input-value { padding-inline: ${c.space["2.5"]}; }`,
|
|
45
51
|
large: `height: 36px; border-radius: 10px; & .crisp-input-value { padding-inline: ${c.space["3"]}; }`,
|
|
46
52
|
},
|
|
@@ -128,13 +128,13 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
128
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 }) }) }))] }));
|
|
129
129
|
}
|
|
130
130
|
Menu.displayName = "Menu";
|
|
131
|
-
export function MenuItem({ children, icon, shortcut, danger, disabled, onSelect, closeOnSelect = true, }) {
|
|
131
|
+
export function MenuItem({ children, icon, shortcut, count, danger, disabled, onSelect, closeOnSelect = true, }) {
|
|
132
132
|
const ctx = React.useContext(MenuContext);
|
|
133
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: () => {
|
|
134
134
|
onSelect?.();
|
|
135
135
|
if (closeOnSelect)
|
|
136
136
|
ctx?.close();
|
|
137
|
-
}, children: [icon, _jsx("span", { className: "crisp-menu-item-label", children: children }), shortcut && _jsx("span", { className: "crisp-menu-item-shortcut", children: shortcut })] }));
|
|
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
138
|
}
|
|
139
139
|
MenuItem.displayName = "MenuItem";
|
|
140
140
|
export function MenuCheckboxItem({ children, checked, onCheckedChange, disabled, }) {
|
|
@@ -54,6 +54,14 @@ const config = defineRecipe({
|
|
|
54
54
|
.crisp-menu-item--danger svg { color: ${c.bgDanger}; }
|
|
55
55
|
.crisp-menu-item--danger:hover, .crisp-menu-item--danger:focus-visible, .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, ${c.bgDanger} 10%, transparent); }
|
|
56
56
|
.crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
57
|
+
/* crisp#875: same measured neutral chip TableToolbar's own ToolbarTool
|
|
58
|
+
count prop uses (re-confirmed on two live Attio surfaces this round:
|
|
59
|
+
the Sort tool's own "+N" overflow, a Kanban board's stage-column
|
|
60
|
+
count) -- a small badge, not bare text. */
|
|
61
|
+
.crisp-menu-item-count { flex-shrink: 0; margin-inline-start: ${c.space["1.5"]}; display: inline-flex; align-items: center; height: 16px; box-sizing: border-box;
|
|
62
|
+
padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent);
|
|
63
|
+
box-shadow: inset 0 0 0 1px ${c.borderSubtle}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft};
|
|
64
|
+
font-variant-numeric: tabular-nums; }
|
|
57
65
|
.crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: ${c.space["3"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; letter-spacing: 0.02em; }
|
|
58
66
|
.crisp-menu-item-check { flex-shrink: 0; color: ${c.bgBrandSolid}; }
|
|
59
67
|
.crisp-menu-item-check--placeholder { width: 15px; }
|
|
@@ -24,16 +24,18 @@ function Chip({ opt }) {
|
|
|
24
24
|
return (_jsx("span", { className: "crisp-recordstable-tag", "data-tone": opt.tone ?? toneFor(opt.value), children: opt.label }));
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* A multi-select / tag editor
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* A multi-select / tag editor, its trigger/listbox/item chrome built to the
|
|
28
|
+
* same seed-design parity as `Select` (crisp/#882, following #867/#879) —
|
|
29
|
+
* its own chip/checkbox picker behaviour is unchanged. The outlined trigger
|
|
30
|
+
* shows the selected values as coloured chips; it opens a popover with a
|
|
31
|
+
* search field over rows of "[chip] … [checkbox]" that toggle on click (the
|
|
32
|
+
* menu stays open). Keyboard: ↑/↓ move, Enter/Space toggle, Esc closes.
|
|
31
33
|
*/
|
|
32
34
|
export function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, required,
|
|
33
35
|
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
34
36
|
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
35
37
|
// above this control named nothing (#247). Select already did this.
|
|
36
|
-
id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, ...rest }) {
|
|
38
|
+
id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, size = "medium", variant = "default", className, "aria-label": ariaLabel, ...rest }) {
|
|
37
39
|
// crisp/#471: same resolution order as Select's own.
|
|
38
40
|
const placeholder = useCrispMessage(placeholderProp, "Select…");
|
|
39
41
|
const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search…");
|
|
@@ -70,8 +72,12 @@ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPl
|
|
|
70
72
|
const filtered = searchable && q
|
|
71
73
|
? options.filter((o) => o.label.toLowerCase().includes(q))
|
|
72
74
|
: options;
|
|
75
|
+
// seed-design's `interactive` (crisp/#882, matching Select): disabled AND
|
|
76
|
+
// readOnly both refuse to open, but only `disabled` greys the text and
|
|
77
|
+
// removes the button from the tab order.
|
|
78
|
+
const interactive = !disabled && !readOnly;
|
|
73
79
|
const openMenu = () => {
|
|
74
|
-
if (
|
|
80
|
+
if (!interactive)
|
|
75
81
|
return;
|
|
76
82
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
77
83
|
if (rect) {
|
|
@@ -80,7 +86,7 @@ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPl
|
|
|
80
86
|
// direction that is (#210)
|
|
81
87
|
const { style, up: flipUp } = placeFloating({
|
|
82
88
|
rect,
|
|
83
|
-
gap: 4
|
|
89
|
+
gap: 8, // seed-design's gutter (crisp/#882, matching Select; was 4)
|
|
84
90
|
flipBelow: 300,
|
|
85
91
|
anchor: triggerRef.current,
|
|
86
92
|
});
|
|
@@ -125,6 +131,10 @@ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPl
|
|
|
125
131
|
close,
|
|
126
132
|
// Tab is already moving focus out; `close` would drag it back to the trigger
|
|
127
133
|
dismiss: () => setOpen(false),
|
|
134
|
+
// Space also toggles the highlighted option (crisp/#882, matching
|
|
135
|
+
// Select's Enter/Space parity) when the trigger itself owns navigation.
|
|
136
|
+
// Never when searchable -- see Select's own comment on this option.
|
|
137
|
+
commitOnSpace: !searchable,
|
|
128
138
|
});
|
|
129
139
|
// Registers the (now-portaled) listbox on the shared floating-layer stack
|
|
130
140
|
// (#339) so this doesn't get closed out from under itself when nested
|
|
@@ -162,11 +172,14 @@ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPl
|
|
|
162
172
|
document.addEventListener("keydown", onKey);
|
|
163
173
|
return () => document.removeEventListener("keydown", onKey);
|
|
164
174
|
}, [open, close]);
|
|
165
|
-
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ invalid, className }), ...rest, children: [name &&
|
|
166
|
-
(value.length > 0 ? value : required ? [""] : []).map((v) => (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), _jsxs("button", { ref: triggerRef, id: id, type: "button", 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, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
175
|
+
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ size, variant, invalid, className }), ...rest, children: [name &&
|
|
176
|
+
(value.length > 0 ? value : required ? [""] : []).map((v) => (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), _jsxs("button", { ref: triggerRef, id: id, type: "button", 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-multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
177
|
+
if (!interactive)
|
|
178
|
+
return;
|
|
167
179
|
if (open && !searchable)
|
|
168
180
|
onNavKeys(e);
|
|
169
|
-
else if (!open &&
|
|
181
|
+
else if (!open &&
|
|
182
|
+
(e.key === "ArrowDown" || e.key === "Enter" || e.key === " ")) {
|
|
170
183
|
e.preventDefault();
|
|
171
184
|
openMenu();
|
|
172
185
|
}
|
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
2
|
import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// (
|
|
24
|
-
// that helper is written `& .crisp-recordstable-tag { ... }` for nesting
|
|
25
|
-
// under a recipe's `base`, which doesn't compile to anything inside a flat
|
|
26
|
-
// `globalCss` block. Keep the two in sync by hand (#380 added the measured
|
|
27
|
-
// `box-shadow: inset 0 0 0 1px <tone ring colour>` here to match).
|
|
3
|
+
// MultiSelect reads as the same family as Select (crisp/#882, following
|
|
4
|
+
// #867/#879): the trigger/listbox/item chrome below reuses Select's
|
|
5
|
+
// seed-design tokens verbatim (`strokeSelectTrigger`, `bgSelectPressed`,
|
|
6
|
+
// `bgSelectDisabled`, `strokeSelectInvalid`, `bgSelectListbox`,
|
|
7
|
+
// `selectFloating`, `fgSelectNeutral`, `fgSelectPlaceholder`, `radius["3xl"]`,
|
|
8
|
+
// `size.selectTriggerMd/Lg`) rather than resolving a second seed spec --
|
|
9
|
+
// seed-design has no multi-value combobox of its own (`packages/rootage/
|
|
10
|
+
// components/` has no `combobox`/`multiselect`; `chip.yaml`/`tag-group*.yaml`
|
|
11
|
+
// are a different, standalone pill pattern, not a field's multi-value
|
|
12
|
+
// trigger). What stays untouched: the `.crisp-recordstable-tag` chip
|
|
13
|
+
// styling (shared with RecordsTable/DataTable elsewhere), the checkbox-style
|
|
14
|
+
// selected indicator, and the toggle-without-closing interaction.
|
|
15
|
+
//
|
|
16
|
+
// The search row's geometry (36px height, 6px gap, no divider) is an
|
|
17
|
+
// Attio measurement (#481) `build/spec-check.mjs` guards directly -- it is
|
|
18
|
+
// NOT part of the seed-parity change and must not move. Only its text/icon
|
|
19
|
+
// colours follow the rest of this file onto the shared tokens.
|
|
20
|
+
//
|
|
21
|
+
// The listbox portals to `document.body` (#339), so its rules live in
|
|
22
|
+
// `globalCss`, not nested under `&` -- see Select's own recipe for the full
|
|
23
|
+
// reasoning (identical here).
|
|
28
24
|
const config = defineRecipe({
|
|
29
25
|
name: "multiselect",
|
|
30
26
|
base: `
|
|
@@ -32,53 +28,63 @@ const config = defineRecipe({
|
|
|
32
28
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
33
29
|
|
|
34
30
|
& .crisp-multiselect-trigger {
|
|
35
|
-
box-sizing: border-box; display: flex; align-items: center;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
transition: background-color .12s ease, box-shadow .12s ease;
|
|
31
|
+
box-sizing: border-box; display: flex; align-items: center; width: 100%; min-width: 0; margin: 0;
|
|
32
|
+
border: 1px solid ${c.strokeSelectTrigger}; background: transparent; cursor: pointer; text-align: start;
|
|
33
|
+
font-family: inherit; color: ${c.fgSelectNeutral};
|
|
34
|
+
transition: background-color .15s cubic-bezier(.35,0,.35,1), border-color .1s cubic-bezier(.35,0,.35,1), border-width .1s cubic-bezier(.35,0,.35,1);
|
|
40
35
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
& .crisp-multiselect-trigger:disabled {
|
|
36
|
+
/* Same pressed/disabled/readonly/focus model as Select's trigger -- see
|
|
37
|
+
that recipe's comment for why hover and an open/focus ring are gone. */
|
|
38
|
+
& .crisp-multiselect-trigger:active:not(:disabled):not([aria-readonly="true"]) { background: ${c.bgSelectPressed}; }
|
|
39
|
+
& .crisp-multiselect-trigger:focus-visible { outline: 2px solid ${c.strokeFocusRing}; outline-offset: -2px; }
|
|
40
|
+
& .crisp-multiselect-trigger:disabled { background: ${c.bgSelectDisabled}; cursor: not-allowed; }
|
|
41
|
+
& .crisp-multiselect-trigger:disabled .crisp-multiselect-placeholder,
|
|
42
|
+
& .crisp-multiselect-trigger:disabled .crisp-multiselect-chevron { color: ${c.fgDisabled}; }
|
|
43
|
+
& .crisp-multiselect-trigger[aria-readonly="true"] { background: ${c.bgSelectDisabled}; cursor: default; }
|
|
44
44
|
& .crisp-multiselect-chips { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: ${c.space["1"]}; }
|
|
45
|
-
& .crisp-multiselect-placeholder { color: ${c.
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
& .crisp-multiselect-placeholder { color: ${c.fgSelectPlaceholder}; }
|
|
46
|
+
/* closeRotateDuration/openRotateDuration, same asymmetric timing as
|
|
47
|
+
Select's own chevron (see that recipe's comment). */
|
|
48
|
+
& .crisp-multiselect-chevron { flex-shrink: 0; color: ${c.fgNeutralMuted}; transition: transform .1s cubic-bezier(.35,0,.35,1); }
|
|
49
|
+
& .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); transition-duration: .15s; }
|
|
48
50
|
`,
|
|
49
51
|
globalCss: `
|
|
50
|
-
/* pointer-events: auto (#375)
|
|
51
|
-
on document.body while open (Zag re-enables it only on layers IT
|
|
52
|
-
tracks), and the portaled listbox is outside Zag's knowledge -- it
|
|
53
|
-
inherited the lock, so every option was unclickable, hit-testing
|
|
54
|
-
right through to whatever real element sat behind it. */
|
|
52
|
+
/* pointer-events: auto (#375) -- see Select's matching comment. */
|
|
55
53
|
.crisp-multiselect-listbox {
|
|
56
54
|
font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
57
|
-
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space
|
|
58
|
-
max-height:
|
|
59
|
-
border-radius: ${c.radius
|
|
60
|
-
box-shadow: ${e.
|
|
61
|
-
transform-origin: top left; opacity: 1; transform: none;
|
|
55
|
+
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space["2"]};
|
|
56
|
+
max-height: 480px; overflow-y: auto; padding: ${c.space["2"]};
|
|
57
|
+
border-radius: ${c.radius["3xl"]}; background: ${c.bgSelectListbox};
|
|
58
|
+
box-shadow: ${e.selectFloating};
|
|
59
|
+
transform-origin: top left; opacity: 1; transform: none;
|
|
60
|
+
transition: opacity .15s cubic-bezier(0,0,.15,1), transform .15s cubic-bezier(0,0,.15,1);
|
|
62
61
|
}
|
|
63
62
|
.crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
|
|
64
|
-
.crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.
|
|
65
|
-
|
|
63
|
+
.crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
|
|
64
|
+
/* LOCKED geometry (#481, guarded by build/spec-check.mjs): height 36px,
|
|
65
|
+
gap space["1.5"] (6px), no border-bottom -- an Attio measurement, not
|
|
66
|
+
part of this seed-parity pass. Only colours below follow Select. */
|
|
67
|
+
.crisp-multiselect-search { display: flex; align-items: center; gap: ${c.space["1.5"]}; height: 36px; flex-shrink: 0; margin: calc(-1 * ${c.space["2"]}) calc(-1 * ${c.space["2"]}) ${c.space["2"]}; padding: ${c.space["2"]} ${c.space["3"]}; color: ${c.fgNeutralMuted}; }
|
|
66
68
|
.crisp-multiselect-search svg { flex-shrink: 0; }
|
|
67
|
-
.crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.
|
|
68
|
-
.crisp-multiselect-search input::placeholder { color: ${c.
|
|
69
|
-
/*
|
|
70
|
-
|
|
71
|
-
same "small muted heading above a run of rows" treatment. */
|
|
72
|
-
.crisp-multiselect-label { padding: ${c.space["1.5"]} ${c.space["2"]} ${c.space["0.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
|
|
69
|
+
.crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgSelectNeutral}; }
|
|
70
|
+
.crisp-multiselect-search input::placeholder { color: ${c.fgSelectPlaceholder}; }
|
|
71
|
+
/* Same groupLabel treatment as Select's (13px/regular/neutral-subtle). */
|
|
72
|
+
.crisp-multiselect-label { padding: ${c.space["2"]} ${c.space["4"]} 0; font-size: ${c.text.md}; line-height: 18px; font-weight: ${c.weight.regular}; color: ${c.fgNeutralSubtle}; }
|
|
73
73
|
.crisp-multiselect-option {
|
|
74
|
-
box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["2"]};
|
|
75
|
-
padding:
|
|
74
|
+
box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0;
|
|
75
|
+
padding: ${c.space["2.5"]} ${c.space["4"]}; margin: 0; border: none; background: none; border-radius: ${c.radius.xl};
|
|
76
|
+
font-family: inherit; text-align: start; cursor: pointer;
|
|
77
|
+
transition: background-color .15s cubic-bezier(.35,0,.35,1);
|
|
76
78
|
}
|
|
79
|
+
/* Keyboard/hover highlight keeps crisp's own visible neutral fill, same
|
|
80
|
+
reasoning as Select's matching rule (seed's own pressed tint is too
|
|
81
|
+
faint for a keyboard-navigation indicator). */
|
|
77
82
|
.crisp-multiselect-option[data-highlighted] { background: ${c.bgRaised}; }
|
|
83
|
+
.crisp-multiselect-option:active { background: ${c.bgSelectPressed}; }
|
|
78
84
|
.crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
|
|
79
85
|
.crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: ${c.radius.sm}; box-shadow: inset 0 0 0 1.5px ${c.borderDefault}; color: transparent; }
|
|
80
86
|
.crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: ${c.bgBrandSolid}; box-shadow: none; color: #fff; }
|
|
81
|
-
.crisp-multiselect-empty { padding: ${c.space["3"]} ${c.space["
|
|
87
|
+
.crisp-multiselect-empty { padding: ${c.space["3"]} ${c.space["4"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgNeutralSubtle}; }
|
|
82
88
|
|
|
83
89
|
.crisp-recordstable-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; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${c.fgPrimary} 82%, transparent); box-shadow: inset 0 0 0 1px ${c.borderSubtle}; }
|
|
84
90
|
.crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, ${c.bgBrandSolid} 14%, transparent); color: color-mix(in srgb, ${c.bgBrandSolid} 82%, ${c.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgBrandSolid} 30%, transparent); }
|
|
@@ -90,18 +96,42 @@ const config = defineRecipe({
|
|
|
90
96
|
.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); }
|
|
91
97
|
`,
|
|
92
98
|
variants: {
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
& .crisp-multiselect-trigger:
|
|
98
|
-
& .crisp-multiselect-
|
|
99
|
-
|
|
99
|
+
// seed's two trigger sizes, same tokens as Select's. medium is the
|
|
100
|
+
// default -- closest to crisp's previous single size.
|
|
101
|
+
size: {
|
|
102
|
+
medium: `
|
|
103
|
+
& .crisp-multiselect-trigger { min-height: ${c.size.selectTriggerMd}; border-radius: ${c.radius.md}; gap: ${c.space["2"]}; padding: ${c.space["1"]} ${c.space["3.5"]}; }
|
|
104
|
+
& .crisp-multiselect-chevron { width: ${c.size.icon}; height: ${c.size.icon}; }
|
|
105
|
+
`,
|
|
106
|
+
large: `
|
|
107
|
+
& .crisp-multiselect-trigger { min-height: ${c.size.selectTriggerLg}; border-radius: ${c.radius.xl}; gap: ${c.space["2.5"]}; padding: ${c.space["1.5"]} ${c.space["4"]}; }
|
|
108
|
+
& .crisp-multiselect-chevron { width: ${c.size.selectIconLg}; height: ${c.size.selectIconLg}; }
|
|
100
109
|
`,
|
|
110
|
+
},
|
|
111
|
+
// Real border width+colour change, same as Select's -- see that
|
|
112
|
+
// recipe's comment.
|
|
113
|
+
invalid: {
|
|
114
|
+
true: `& .crisp-multiselect-trigger { border-width: 2px; border-color: ${c.strokeSelectInvalid}; }`,
|
|
101
115
|
false: ``,
|
|
102
116
|
},
|
|
117
|
+
// ghost: fills whatever row it's embedded in instead of seed's fixed
|
|
118
|
+
// trigger height -- RecordsTable's TagsCell embeds a MultiSelect inline
|
|
119
|
+
// in a 36px table row, the exact regression #879 caught for Select's
|
|
120
|
+
// own datatable consumers. Not a seed state; crisp's own escape hatch.
|
|
121
|
+
//
|
|
122
|
+
// Declared LAST on purpose, same reasoning as Select's recipe: same-
|
|
123
|
+
// specificity CSS rules resolve by source order, and ghost's
|
|
124
|
+
// `min-height: 0; height: 100%` below must come after `size`'s fixed
|
|
125
|
+
// min-height to win the cascade.
|
|
126
|
+
variant: {
|
|
127
|
+
default: "",
|
|
128
|
+
ghost: `
|
|
129
|
+
& .crisp-multiselect-trigger { min-height: 0; height: 100%; border-color: transparent; }
|
|
130
|
+
& .crisp-multiselect-trigger:hover:not(:disabled) { background: ${c.bgSelectPressed}; }
|
|
131
|
+
`,
|
|
132
|
+
},
|
|
103
133
|
},
|
|
104
|
-
defaultVariants: { invalid: false },
|
|
134
|
+
defaultVariants: { size: "medium", invalid: false, variant: "default" },
|
|
105
135
|
compoundVariants: [],
|
|
106
136
|
});
|
|
107
137
|
export const multiselectRecipe = Object.assign(resolveRecipe(config), {
|