@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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
2
3
|
import * as React from "react";
|
|
4
|
+
import { Menu, MenuItem } from "../menu/index.js";
|
|
3
5
|
import { tabsRecipe } from "./tabs.recipe.js";
|
|
4
6
|
import { useTablistKeyboard } from "./tabs-keyboard.js";
|
|
5
7
|
/**
|
|
@@ -8,7 +10,7 @@ import { useTablistKeyboard } from "./tabs-keyboard.js";
|
|
|
8
10
|
* Home/End jump); activation follows focus. Controlled via
|
|
9
11
|
* `value`/`onValueChange`, or uncontrolled from `defaultValue`.
|
|
10
12
|
*/
|
|
11
|
-
export function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, className, fill, "aria-label": ariaLabel, ...rest }) {
|
|
13
|
+
export function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, moreLabel = (n) => `+${n} more`, moreMenuLabel = "More tabs", className, fill, "aria-label": ariaLabel, ...rest }) {
|
|
12
14
|
// Scoped to this instance: `tab-${value}` and `panel-${value}` are global,
|
|
13
15
|
// so two tab bars sharing a value mint the same id twice and every
|
|
14
16
|
// aria-labelledby resolves to the first one (#227).
|
|
@@ -41,16 +43,73 @@ export function Tabs({ tabs, value, defaultValue, onValueChange, leading, childr
|
|
|
41
43
|
ro.observe(list);
|
|
42
44
|
return () => ro.disconnect();
|
|
43
45
|
}, [active, tabs]);
|
|
46
|
+
// crisp#863 -- responsive "+N more" overflow, measured live: Attio
|
|
47
|
+
// collapses the TRAILING tabs into a single trigger once the strip runs
|
|
48
|
+
// out of room, rather than letting the row wrap or overrun. Every tab
|
|
49
|
+
// (and the "+N more" trigger itself) stays permanently mounted -- an
|
|
50
|
+
// overflowed one gets `data-tabs-overflow-hidden` (position: absolute;
|
|
51
|
+
// visibility: hidden in the recipe), which removes it from the flex row
|
|
52
|
+
// WITHOUT ever making its `offsetWidth` unmeasurable the way `display:
|
|
53
|
+
// none` would. That's what lets this recompute purely from cached
|
|
54
|
+
// per-tab widths on every resize, with no flash: a naive
|
|
55
|
+
// measure-everything-visible-first pass would show the full unclipped
|
|
56
|
+
// row for one frame before collapsing it.
|
|
57
|
+
const [hiddenFrom, setHiddenFrom] = React.useState(null);
|
|
58
|
+
const moreRef = React.useRef(null);
|
|
59
|
+
React.useLayoutEffect(() => {
|
|
60
|
+
const list = listRef.current;
|
|
61
|
+
if (!list || tabs.length === 0)
|
|
62
|
+
return;
|
|
63
|
+
const recompute = () => {
|
|
64
|
+
const containerWidth = list.clientWidth;
|
|
65
|
+
// Not yet laid out (e.g. this render, or the test environment, which
|
|
66
|
+
// never computes real box metrics) -- never collapse everything on a
|
|
67
|
+
// 0-width read, that's a false "nothing fits" rather than "not
|
|
68
|
+
// measurable yet".
|
|
69
|
+
if (containerWidth <= 0)
|
|
70
|
+
return;
|
|
71
|
+
const els = tabs.map((t) => list.querySelector(`[data-value="${CSS.escape(t.value)}"]`));
|
|
72
|
+
if (els.some((el) => !el))
|
|
73
|
+
return;
|
|
74
|
+
const gap = 4; // ${c.space["1"]}, the list's own gap (recipe.ts)
|
|
75
|
+
const moreWidth = moreRef.current?.offsetWidth ?? 0;
|
|
76
|
+
let used = 0;
|
|
77
|
+
let cutoff = tabs.length;
|
|
78
|
+
for (let i = 0; i < els.length; i++) {
|
|
79
|
+
const w = (els[i]?.offsetWidth ?? 0) + (i > 0 ? gap : 0);
|
|
80
|
+
const reserve = i < tabs.length - 1 ? moreWidth + gap : 0;
|
|
81
|
+
if (used + w + reserve > containerWidth) {
|
|
82
|
+
cutoff = i;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
used += w;
|
|
86
|
+
}
|
|
87
|
+
// The active tab is never allowed to fall into the overflow menu --
|
|
88
|
+
// not itself measured live (Attio's own fixture never exercised
|
|
89
|
+
// navigating INTO an overflowed tab first), but "the tab you're on
|
|
90
|
+
// stays visible" is the safe, unsurprising default for any tab UI.
|
|
91
|
+
const activeIndex = tabs.findIndex((t) => t.value === active);
|
|
92
|
+
if (activeIndex >= 0 && activeIndex >= cutoff)
|
|
93
|
+
cutoff = activeIndex + 1;
|
|
94
|
+
setHiddenFrom(cutoff >= tabs.length ? null : cutoff);
|
|
95
|
+
};
|
|
96
|
+
recompute();
|
|
97
|
+
const ro = new ResizeObserver(recompute);
|
|
98
|
+
ro.observe(list);
|
|
99
|
+
return () => ro.disconnect();
|
|
100
|
+
}, [tabs, active]);
|
|
101
|
+
const hiddenTabs = hiddenFrom == null ? [] : tabs.slice(hiddenFrom);
|
|
44
102
|
const { onKeyDown, tabIndexFor } = useTablistKeyboard({
|
|
45
103
|
tabs,
|
|
46
104
|
active,
|
|
47
105
|
select,
|
|
48
106
|
listRef,
|
|
49
107
|
});
|
|
50
|
-
return (_jsxs("div", { className: tabsRecipe({ fill, className }), ...rest, children: [_jsxs("div", { ref: listRef, className: "crisp-tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && _jsx("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t) => {
|
|
108
|
+
return (_jsxs("div", { className: tabsRecipe({ fill, className }), ...rest, children: [_jsxs("div", { ref: listRef, className: "crisp-tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && _jsx("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t, i) => {
|
|
51
109
|
const selected = t.value === active;
|
|
52
|
-
|
|
53
|
-
|
|
110
|
+
const overflowHidden = hiddenFrom != null && i >= hiddenFrom;
|
|
111
|
+
return (_jsxs("button", { type: "button", role: "tab", id: tabId(t.value), "aria-selected": selected, "aria-controls": children && selected ? panelId(t.value) : undefined, tabIndex: overflowHidden ? -1 : tabIndexFor(t.value), "data-value": t.value, "data-selected": selected ? "" : undefined, "data-tabs-overflow-hidden": overflowHidden ? "" : undefined, disabled: t.disabled, className: "crisp-tabs-tab", onClick: () => select(t.value), children: [t.icon, _jsx("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && (_jsx("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value));
|
|
112
|
+
}), _jsx("span", { ref: moreRef, className: "crisp-tabs-more", "data-tabs-overflow-hidden": hiddenTabs.length === 0 ? "" : undefined, children: _jsx(Menu, { "aria-label": moreMenuLabel, trigger: _jsxs("button", { type: "button", className: "crisp-tabs-tab", children: [_jsx("span", { className: "tabs-tab-label", children: moreLabel(hiddenTabs.length) }), _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), children: hiddenTabs.map((t) => (_jsxs(MenuItem, { icon: t.icon, onSelect: () => select(t.value), children: [t.label, t.badge != null && (_jsx("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value))) }) }), indicator && (_jsx("span", { className: "crisp-tabs-indicator", style: {
|
|
54
113
|
width: indicator.width,
|
|
55
114
|
transform: `translateX(${indicator.left}px)`,
|
|
56
115
|
} }))] }), children && (_jsx("div", { className: "crisp-tabs-panel", role: "tabpanel", id: panelId(active), "aria-labelledby": tabId(active),
|
|
@@ -54,6 +54,19 @@ const config = defineRecipe({
|
|
|
54
54
|
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 14px; height: 14px; padding: 0 ${c.space["1"]}; border-radius: 4px; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); box-shadow: inset 0 0 0 1px ${c.borderSubtle}; font-size: 10px; line-height: 14px; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft}; }
|
|
55
55
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${c.fgPrimary}; }
|
|
56
56
|
& .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: ${c.fgPrimary}; border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
|
|
57
|
+
/* crisp#863: the "+N more" overflow trigger, measured live -- its
|
|
58
|
+
computed style is identical to a plain .crisp-tabs-tab (same pill,
|
|
59
|
+
same 6px icon/label gap the tab class already gives via
|
|
60
|
+
space["1.5"]), and the dropdown it opens is crisp's own Menu/
|
|
61
|
+
MenuItem chrome (already matches Attio's popover, nothing new to
|
|
62
|
+
style there). Only the wrapper needs a rule: an overflowed tab (or
|
|
63
|
+
the trigger itself, when nothing is hidden) is taken out of the
|
|
64
|
+
flex row and out of the a11y tree without ever making its own
|
|
65
|
+
offsetWidth unmeasurable the way display:none would -- that's what
|
|
66
|
+
lets the tabs.tsx effect recompute the cutoff from cached widths on
|
|
67
|
+
every resize with no flash of an unclipped row. */
|
|
68
|
+
& .crisp-tabs-more { display: inline-flex; }
|
|
69
|
+
& [data-tabs-overflow-hidden] { position: absolute; visibility: hidden; pointer-events: none; }
|
|
57
70
|
& .crisp-tabs-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; padding-top: ${c.space["8"]}; }
|
|
58
71
|
& .crisp-tabs-panel:focus-visible { outline: none; }
|
|
59
72
|
`,
|
|
@@ -31,6 +31,16 @@ const spec = defineSpec({
|
|
|
31
31
|
tone: {
|
|
32
32
|
default: { color: c.fgPrimary },
|
|
33
33
|
muted: { color: c.fgNeutralMuted },
|
|
34
|
+
// #858: no new measurement -- both reuse tokens already measured
|
|
35
|
+
// against Attio for the same colours in a different arrangement.
|
|
36
|
+
// `brand` is `textlink`'s own reused brand-text colour (`bgBrandSolid`,
|
|
37
|
+
// the same alias GRC's own `--color-fg-brand` resolves to). `danger`
|
|
38
|
+
// is `fgDanger` ("solid critical/destructive TEXT... an overdue
|
|
39
|
+
// due-date, a destructive menu item's label" -- see its own doc in
|
|
40
|
+
// tokens/semantic.ts), not `fgDangerSoft` (that one's a soft Badge
|
|
41
|
+
// pill's text, a different measured hex).
|
|
42
|
+
brand: { color: c.bgBrandSolid },
|
|
43
|
+
danger: { color: c.fgDanger },
|
|
34
44
|
},
|
|
35
45
|
},
|
|
36
46
|
defaultVariants: { tone: "default" },
|
|
@@ -7,7 +7,7 @@ import { textlinkRecipe } from "./textlink.recipe.js";
|
|
|
7
7
|
* `event.preventDefault()` pairs it with client-side routing (e.g.
|
|
8
8
|
* react-router's own `Link`).
|
|
9
9
|
*/
|
|
10
|
-
export function TextLink({ tone = "muted", className, ...rest }) {
|
|
11
|
-
return _jsx("a", { className: textlinkRecipe({ tone, className }), ...rest });
|
|
10
|
+
export function TextLink({ tone = "muted", size, className, ...rest }) {
|
|
11
|
+
return _jsx("a", { className: textlinkRecipe({ tone, size, className }), ...rest });
|
|
12
12
|
}
|
|
13
13
|
TextLink.displayName = "TextLink";
|
|
@@ -24,6 +24,24 @@ const config = defineRecipe({
|
|
|
24
24
|
// that only goes brand on hover.
|
|
25
25
|
muted: `font-size: ${c.text.md}; font-weight: ${c.weight.regular}; color: ${c.fgMuted}; &:hover { color: ${c.bgBrandSolid}; }`,
|
|
26
26
|
},
|
|
27
|
+
// crisp#874: an explicit, OPTIONAL size override -- no `defaultVariants`
|
|
28
|
+
// entry below, so omitting `size` emits no `--size_*` class at all and
|
|
29
|
+
// each tone's own hard-coded font-size (above) stands completely
|
|
30
|
+
// unchanged. Only when a caller passes `size` explicitly does this
|
|
31
|
+
// override it: `size`'s CSS block is emitted AFTER `tone`'s in the
|
|
32
|
+
// generated stylesheet (`generate-recipes.mjs` walks `variants` in
|
|
33
|
+
// object-key order), so at equal specificity it wins the cascade.
|
|
34
|
+
// Measured live (a running GRC server, `getComputedStyle`):
|
|
35
|
+
// internal/grc's two primary-column record links
|
|
36
|
+
// (routes/lists/Lists.tsx:51, routes/registers/Registers.tsx:63) are
|
|
37
|
+
// 14px/500 ambient, inherited from DataTable's own
|
|
38
|
+
// `.crisp-datatable-entity-name` -- `size="lg"` matches that context
|
|
39
|
+
// without changing `brand`'s weight/colour.
|
|
40
|
+
size: {
|
|
41
|
+
sm: `font-size: ${c.text.sm};`,
|
|
42
|
+
md: `font-size: ${c.text.md};`,
|
|
43
|
+
lg: `font-size: ${c.text.lg};`,
|
|
44
|
+
},
|
|
27
45
|
},
|
|
28
46
|
defaultVariants: { tone: "muted" },
|
|
29
47
|
compoundVariants: [],
|
package/dist/esm/index.js
CHANGED
|
@@ -51,6 +51,7 @@ export * from "./components/slider/index.js";
|
|
|
51
51
|
export * from "./components/spinner/index.js";
|
|
52
52
|
export * from "./components/stagebar/index.js";
|
|
53
53
|
export * from "./components/statcard/index.js";
|
|
54
|
+
export * from "./components/statusdot/index.js";
|
|
54
55
|
export * from "./components/switch/index.js";
|
|
55
56
|
export * from "./components/table/index.js";
|
|
56
57
|
export * from "./components/tabletoolbar/index.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Activity as ActivityIcon, CalendarClock, ChevronDown, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, Mail, Maximize2, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search,
|
|
2
|
+
import { Activity as ActivityIcon, CalendarClock, ChevronDown, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, Mail, Maximize2, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search, Sparkles, SquareCheck, Star, Trash2, Users, UsersRound, Workflow, X, Zap, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Avatar } from "../../components/avatar/index.js";
|
|
5
5
|
import { Checkbox } from "../../components/checkbox/index.js";
|
|
@@ -38,8 +38,12 @@ export const recordPageLabels = {
|
|
|
38
38
|
copyRecordLink: "Copy record link",
|
|
39
39
|
deleteRecord: "Delete record",
|
|
40
40
|
linkRecords: "Link records",
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Tooltip copy re-measured live for #865 (was "Log a note"/"Create a
|
|
42
|
+
// task" — the field names stay, only the visible text changes to match
|
|
43
|
+
// Attio's own React-tooltip strings on these two icon buttons exactly).
|
|
44
|
+
logANote: "New note",
|
|
45
|
+
createATask: "New task",
|
|
46
|
+
runWorkflow: "Run workflow",
|
|
43
47
|
fullScreen: "Full screen",
|
|
44
48
|
exitFullScreen: "Exit full screen",
|
|
45
49
|
resizeDetails: "Resize details column",
|
|
@@ -419,7 +423,7 @@ RecordDetails.displayName = "RecordDetails";
|
|
|
419
423
|
* Emails sections (Notes & Tasks are live — add and check things off). Drop it
|
|
420
424
|
* into the app's main content area.
|
|
421
425
|
*/
|
|
422
|
-
export 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 }) {
|
|
426
|
+
export 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 }) {
|
|
423
427
|
const t = useRecordPageLabels(labels);
|
|
424
428
|
const [favorite, setFavorite] = React.useState(false);
|
|
425
429
|
// Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
|
|
@@ -528,6 +532,15 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
528
532
|
},
|
|
529
533
|
];
|
|
530
534
|
const highlights = highlightsProp === undefined ? defaultHighlights : highlightsProp;
|
|
535
|
+
// #865: the built-in CRM icon set for the quick-action row, used when the
|
|
536
|
+
// caller omits `quickActions` -- same "caller's own list, not a merge"
|
|
537
|
+
// choice `tabs` (#379) and `highlights` (#528) already make.
|
|
538
|
+
const defaultQuickActions = [
|
|
539
|
+
{ label: t.logANote, icon: FileText },
|
|
540
|
+
{ label: t.runWorkflow, icon: Workflow },
|
|
541
|
+
{ label: t.createATask, icon: SquareCheck },
|
|
542
|
+
];
|
|
543
|
+
const quickActions = quickActionsProp ?? defaultQuickActions;
|
|
531
544
|
const ti = { size: 15, strokeWidth: 1.75 };
|
|
532
545
|
const builtinTabs = [
|
|
533
546
|
{ value: "overview", label: t.tabs.overview, icon: _jsx(LayoutGrid, { ...ti }) },
|
|
@@ -636,7 +649,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
636
649
|
const effectivePanel = tabsProp
|
|
637
650
|
? (active) => tabsProp.find((tb) => tb.id === active)?.render()
|
|
638
651
|
: builtinPanel;
|
|
639
|
-
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _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: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }),
|
|
652
|
+
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _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: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Plus, { size: 16, strokeWidth: 1.75 }), t.addToList] }), quickActions.map((a, i) => (_jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": a.label, onClick: a.onSelect, children: _jsx(a.icon, { size: 15, strokeWidth: 1.75 }) }, i)))] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _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) => {
|
|
640
653
|
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
641
654
|
return;
|
|
642
655
|
// The pointer drag learned its direction in #212; this did not.
|
|
@@ -56,4 +56,11 @@ export const elevation = {
|
|
|
56
56
|
* nobody measured for them.
|
|
57
57
|
*/
|
|
58
58
|
fieldNoRing: `0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04)`,
|
|
59
|
+
/**
|
|
60
|
+
* Select's own listbox shadow (crisp/#867) — seed-design's `$shadow.s3`, a
|
|
61
|
+
* single layer, NOT `floating` above. This component follows seed, not
|
|
62
|
+
* Attio, for the same reason `DatePicker` follows seed's calendar
|
|
63
|
+
* structure: an explicit owner request to match seed-design exactly.
|
|
64
|
+
*/
|
|
65
|
+
selectFloating: `0 4px 16px ${c.shadowSelect}`,
|
|
59
66
|
};
|
|
@@ -44,6 +44,15 @@ export const palette = {
|
|
|
44
44
|
// both themes (Switch's own spec), so this reuses ringField's dark value
|
|
45
45
|
// rather than inventing a third "field-adjacent ring, dark" colour.
|
|
46
46
|
ringSwitch: { light: "#d9dade", dark: "#212224" },
|
|
47
|
+
// Callout's bordered/banner look (crisp#869) — measured live on Attio's
|
|
48
|
+
// Objects settings page (the "Create custom objects... Upgrade" Pro-plan
|
|
49
|
+
// banner): `border: 1px solid rgb(214,229,255)`, an opaque colour, not a
|
|
50
|
+
// composited tint of `bgBrandSolid` (checked: no single color-mix
|
|
51
|
+
// percentage matches all three channels at once). Dark value not
|
|
52
|
+
// independently re-measured (Attio's own theme toggle is off-limits) —
|
|
53
|
+
// reuses `ringField`/`ringSwitch`'s existing dark ring (#212224), the same
|
|
54
|
+
// "edge visible against a dark surface" role those already establish.
|
|
55
|
+
ringBrandSoft: { light: "#d6e5ff", dark: "#212224" },
|
|
47
56
|
tileActive: { light: "#ffffff", dark: "#101112" },
|
|
48
57
|
tileActiveShadow: { light: "rgba(0,0,0,0.04)", dark: "rgba(0,0,0,0)" },
|
|
49
58
|
// Dialog's page-dimming backdrop (crisp#677) — measured live behind the
|
|
@@ -159,4 +168,36 @@ export const palette = {
|
|
|
159
168
|
purpleTint: { light: "#f5f0ff", dark: "#2f1e5a" },
|
|
160
169
|
softPurpleText: { light: "#6238b5", dark: "#d8c4ff" },
|
|
161
170
|
purpleRing: { light: "#e8ddfe", dark: "#45297d" },
|
|
171
|
+
// Select rebuilt to seed-design parity (crisp/#867), NOT Attio -- a
|
|
172
|
+
// deliberate one-component exception, the same way DatePicker already
|
|
173
|
+
// follows seed's calendar structure. Values below are resolved directly
|
|
174
|
+
// from `@seed-design/rootage`'s color.yaml (gray palette), not measured
|
|
175
|
+
// live, since there is no Attio reference for this component anymore.
|
|
176
|
+
// gray-00 / gray-200 (bg.layer-floating) -- the listbox surface. Distinct
|
|
177
|
+
// from `popoverBg` (crisp's Attio-measured popover, #fff/#17181a): the dark
|
|
178
|
+
// value differs (#1d2025), so this is its own token, not a reuse.
|
|
179
|
+
selectListboxBg: { light: "#ffffff", dark: "#1d2025" },
|
|
180
|
+
// gray-400 (stroke.neutral-weak) -- the trigger's real 1px border at rest.
|
|
181
|
+
selectTriggerStroke: { light: "#dcdee3", dark: "#393d46" },
|
|
182
|
+
// static-black/white-alpha (bg.transparent-pressed) -- the trigger/item
|
|
183
|
+
// press tint. Deliberately alpha, not a gray, so it reads the same over
|
|
184
|
+
// either surface colour.
|
|
185
|
+
selectPressedTint: { light: "#00000007", dark: "#ffffff0d" },
|
|
186
|
+
// gray-200 / gray-300 (bg.disabled) -- the trigger's disabled AND readonly
|
|
187
|
+
// fill. Distinct from `neutralWeak` (light matches, dark is #1d2025 not
|
|
188
|
+
// #2b2e35), so its own token rather than a reuse.
|
|
189
|
+
selectDisabledBg: { light: "#f3f4f5", dark: "#2b2e35" },
|
|
190
|
+
// red-700 (stroke.critical-solid) -- the trigger's invalid border. Distinct
|
|
191
|
+
// from crisp's own `bgDangerSolid`/`red700` (#ba2525, an Attio red): seed's
|
|
192
|
+
// red is a different hue entirely, and this component follows seed.
|
|
193
|
+
selectInvalidStroke: { light: "#fa342c", dark: "#ff6e60" },
|
|
194
|
+
// static-black/white-alpha (stroke.neutral-muted) -- the listbox's own
|
|
195
|
+
// divider slot (defined in select.yaml; not yet exposed on any consumer).
|
|
196
|
+
selectDivider: { light: "#00000010", dark: "#ffffff17" },
|
|
197
|
+
// gray-600 (fg.placeholder) -- distinct from `neutralSubtle` (its light/dark
|
|
198
|
+
// pair is the same two hexes, swapped), so its own token, not a reuse.
|
|
199
|
+
selectPlaceholder: { light: "#b0b3ba", dark: "#868b94" },
|
|
200
|
+
// s3 (shadow.yaml) -- the listbox's single-layer floating shadow, in place
|
|
201
|
+
// of crisp's own multi-layer `elevation.floating` for this component only.
|
|
202
|
+
selectShadow: { light: "#0000001f", dark: "#000000cc" },
|
|
162
203
|
};
|
package/dist/esm/tokens/scale.js
CHANGED
|
@@ -17,6 +17,7 @@ export const space = {
|
|
|
17
17
|
"2": "8px",
|
|
18
18
|
"2.5": "10px",
|
|
19
19
|
"3": "12px",
|
|
20
|
+
"3.5": "14px", // seed-design's $dimension.x3_5 — Select's medium trigger paddingX (crisp/#867)
|
|
20
21
|
"4": "16px",
|
|
21
22
|
"5": "20px",
|
|
22
23
|
"6": "24px",
|
|
@@ -31,6 +32,7 @@ export const radius = {
|
|
|
31
32
|
lg: "9px", // rows, tiles, list items
|
|
32
33
|
xl: "12px", // overlay items (command rows)
|
|
33
34
|
"2xl": "16px", // overlay panels (command surface)
|
|
35
|
+
"3xl": "20px", // seed-design's $radius.r5 — Select's listbox corner (crisp/#867)
|
|
34
36
|
full: "999px", // pills
|
|
35
37
|
};
|
|
36
38
|
// Component sizing — recurring measured dimensions that are neither spacing nor
|
|
@@ -49,12 +51,25 @@ export const size = {
|
|
|
49
51
|
// whole cells and a column gap would cut it.
|
|
50
52
|
calendarCell: "34px", // calendar day pitch (32px day + 2px gap)
|
|
51
53
|
calendarPill: "32px", // the day highlight itself
|
|
54
|
+
// seed-design's Select, resolved from select-trigger.yaml/select-item.yaml
|
|
55
|
+
// (crisp/#867) — this component follows seed, not Attio, so these sit
|
|
56
|
+
// alongside the measured scale rather than folding into it.
|
|
57
|
+
selectTriggerMd: "40px", // $dimension.x10 — the medium (default) trigger height
|
|
58
|
+
selectTriggerLg: "52px", // $dimension.x13 — the large trigger height
|
|
59
|
+
selectIconLg: "20px", // $dimension.x5 — large trigger's chevron
|
|
52
60
|
};
|
|
53
61
|
// Typography — the measured UI runs entirely on Inter. Sizes measured in the sidebar
|
|
54
62
|
// collapse onto 11/12/13/14/16 (lg = 14px is the default body/label size);
|
|
55
63
|
// weights are 400/500/600 with 500 the UI default.
|
|
64
|
+
//
|
|
65
|
+
// `"VK Sans Display"` leads the stack: an owner override (your-moon/crisp#857),
|
|
66
|
+
// not a remeasure of Attio. It only ever matches an INSTALLED copy of the font
|
|
67
|
+
// via the `local()`-only `@font-face` rules in dist/tokens.css (see
|
|
68
|
+
// build/generate-fonts.mjs for why — VK Sans Display's license has no
|
|
69
|
+
// redistribution grant, so crisp never ships the font files). Everywhere it
|
|
70
|
+
// isn't installed, this falls straight through to Inter, unchanged.
|
|
56
71
|
export const font = {
|
|
57
|
-
sans: '"Inter", system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
72
|
+
sans: '"VK Sans Display", "Inter", system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
58
73
|
};
|
|
59
74
|
export const text = {
|
|
60
75
|
xs: "11px",
|
|
@@ -82,4 +82,22 @@ export const semantic = {
|
|
|
82
82
|
bgPurpleSoft: "purpleTint",
|
|
83
83
|
fgPurpleSoft: "softPurpleText",
|
|
84
84
|
ringPurpleSoft: "purpleRing",
|
|
85
|
+
/** Callout's bordered/banner look (crisp#869) — an opaque measured ring,
|
|
86
|
+
* not a `color-mix` tint of `bgBrandSolid` (see palette.ts's own comment). */
|
|
87
|
+
ringBrandSoft: "ringBrandSoft",
|
|
88
|
+
/** Select rebuilt to seed-design parity (crisp/#867) — see palette.ts's
|
|
89
|
+
* comment above `selectListboxBg` for why these are their own tokens
|
|
90
|
+
* rather than a reuse of an Attio-measured one. */
|
|
91
|
+
/** Seed's neutral ink (gray-1000) used as TEXT/icon colour — same palette
|
|
92
|
+
* entry as `strokeFieldFocus` (a ring), aliased separately because the
|
|
93
|
+
* role here is content colour, not a focus ring. */
|
|
94
|
+
fgSelectNeutral: "fieldFocus",
|
|
95
|
+
bgSelectListbox: "selectListboxBg",
|
|
96
|
+
strokeSelectTrigger: "selectTriggerStroke",
|
|
97
|
+
bgSelectPressed: "selectPressedTint",
|
|
98
|
+
bgSelectDisabled: "selectDisabledBg",
|
|
99
|
+
strokeSelectInvalid: "selectInvalidStroke",
|
|
100
|
+
strokeSelectDivider: "selectDivider",
|
|
101
|
+
fgSelectPlaceholder: "selectPlaceholder",
|
|
102
|
+
shadowSelect: "selectShadow",
|
|
85
103
|
};
|
package/dist/input.css
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
& .crisp-input-value:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
8
8
|
&:has(.crisp-input-value:disabled) { opacity: 1; }
|
|
9
9
|
}
|
|
10
|
+
.crisp-input--size_sm {
|
|
11
|
+
height: var(--crisp-size-row); border-radius: var(--crisp-radius-md);
|
|
12
|
+
& .crisp-input-value { padding-inline: var(--crisp-space-2-5); }
|
|
13
|
+
}
|
|
10
14
|
.crisp-input--size_medium {
|
|
11
15
|
height: 32px; border-radius: var(--crisp-radius-md);
|
|
12
16
|
& .crisp-input-value { padding-inline: var(--crisp-space-2-5); }
|
package/dist/input.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { RecipeFn } from "../core/types";
|
|
2
2
|
|
|
3
3
|
export interface InputVariants {
|
|
4
|
-
size?: "medium" | "large";
|
|
4
|
+
size?: "sm" | "medium" | "large";
|
|
5
5
|
invalid?: boolean;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export declare const inputRecipe: RecipeFn<{ size: { medium: string; large: string }; invalid: { true: string; false: string } }>;
|
|
8
|
+
export declare const inputRecipe: RecipeFn<{ size: { sm: string; medium: string; large: string }; invalid: { true: string; false: string } }>;
|
package/dist/input.layered.css
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
& .crisp-input-value:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
9
9
|
&:has(.crisp-input-value:disabled) { opacity: 1; }
|
|
10
10
|
}
|
|
11
|
+
.crisp-input--size_sm {
|
|
12
|
+
height: var(--crisp-size-row); border-radius: var(--crisp-radius-md);
|
|
13
|
+
& .crisp-input-value { padding-inline: var(--crisp-space-2-5); }
|
|
14
|
+
}
|
|
11
15
|
.crisp-input--size_medium {
|
|
12
16
|
height: 32px; border-radius: var(--crisp-radius-md);
|
|
13
17
|
& .crisp-input-value { padding-inline: var(--crisp-space-2-5); }
|
package/dist/menu.css
CHANGED
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
.crisp-menu-item--danger svg { color: var(--crisp-bg-danger); }
|
|
31
31
|
.crisp-menu-item--danger:hover, .crisp-menu-item--danger:focus-visible, .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
|
|
32
32
|
.crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
33
|
+
/* crisp#875: same measured neutral chip TableToolbar's own ToolbarTool
|
|
34
|
+
count prop uses (re-confirmed on two live Attio surfaces this round:
|
|
35
|
+
the Sort tool's own "+N" overflow, a Kanban board's stage-column
|
|
36
|
+
count) -- a small badge, not bare text. */
|
|
37
|
+
.crisp-menu-item-count { flex-shrink: 0; margin-inline-start: var(--crisp-space-1-5); display: inline-flex; align-items: center; height: 16px; box-sizing: border-box;
|
|
38
|
+
padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent);
|
|
39
|
+
box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-neutral-soft);
|
|
40
|
+
font-variant-numeric: tabular-nums; }
|
|
33
41
|
.crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: var(--crisp-space-3); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); letter-spacing: 0.02em; }
|
|
34
42
|
.crisp-menu-item-check { flex-shrink: 0; color: var(--crisp-bg-brand-solid); }
|
|
35
43
|
.crisp-menu-item-check--placeholder { width: 15px; }
|
package/dist/menu.layered.css
CHANGED
|
@@ -31,6 +31,14 @@
|
|
|
31
31
|
.crisp-menu-item--danger svg { color: var(--crisp-bg-danger); }
|
|
32
32
|
.crisp-menu-item--danger:hover, .crisp-menu-item--danger:focus-visible, .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
|
|
33
33
|
.crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
34
|
+
/* crisp#875: same measured neutral chip TableToolbar's own ToolbarTool
|
|
35
|
+
count prop uses (re-confirmed on two live Attio surfaces this round:
|
|
36
|
+
the Sort tool's own "+N" overflow, a Kanban board's stage-column
|
|
37
|
+
count) -- a small badge, not bare text. */
|
|
38
|
+
.crisp-menu-item-count { flex-shrink: 0; margin-inline-start: var(--crisp-space-1-5); display: inline-flex; align-items: center; height: 16px; box-sizing: border-box;
|
|
39
|
+
padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent);
|
|
40
|
+
box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-neutral-soft);
|
|
41
|
+
font-variant-numeric: tabular-nums; }
|
|
34
42
|
.crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: var(--crisp-space-3); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); letter-spacing: 0.02em; }
|
|
35
43
|
.crisp-menu-item-check { flex-shrink: 0; color: var(--crisp-bg-brand-solid); }
|
|
36
44
|
.crisp-menu-item-check--placeholder { width: 15px; }
|
package/dist/multiselect.css
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
1
|
|
|
2
|
-
/* pointer-events: auto (#375)
|
|
3
|
-
on document.body while open (Zag re-enables it only on layers IT
|
|
4
|
-
tracks), and the portaled listbox is outside Zag's knowledge -- it
|
|
5
|
-
inherited the lock, so every option was unclickable, hit-testing
|
|
6
|
-
right through to whatever real element sat behind it. */
|
|
2
|
+
/* pointer-events: auto (#375) -- see Select's matching comment. */
|
|
7
3
|
.crisp-multiselect-listbox {
|
|
8
4
|
font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
9
|
-
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-
|
|
10
|
-
max-height:
|
|
11
|
-
border-radius: var(--crisp-radius-
|
|
12
|
-
box-shadow:
|
|
13
|
-
transform-origin: top left; opacity: 1; transform: none;
|
|
5
|
+
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-2);
|
|
6
|
+
max-height: 480px; overflow-y: auto; padding: var(--crisp-space-2);
|
|
7
|
+
border-radius: var(--crisp-radius-3xl); background: var(--crisp-bg-select-listbox);
|
|
8
|
+
box-shadow: 0 4px 16px var(--crisp-shadow-select);
|
|
9
|
+
transform-origin: top left; opacity: 1; transform: none;
|
|
10
|
+
transition: opacity .15s cubic-bezier(0,0,.15,1), transform .15s cubic-bezier(0,0,.15,1);
|
|
14
11
|
}
|
|
15
12
|
.crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
|
|
16
|
-
.crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.
|
|
17
|
-
|
|
13
|
+
.crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
|
|
14
|
+
/* LOCKED geometry (#481, guarded by build/spec-check.mjs): height 36px,
|
|
15
|
+
gap space["1.5"] (6px), no border-bottom -- an Attio measurement, not
|
|
16
|
+
part of this seed-parity pass. Only colours below follow Select. */
|
|
17
|
+
.crisp-multiselect-search { display: flex; align-items: center; gap: var(--crisp-space-1-5); height: 36px; flex-shrink: 0; margin: calc(-1 * var(--crisp-space-2)) calc(-1 * var(--crisp-space-2)) var(--crisp-space-2); padding: var(--crisp-space-2) var(--crisp-space-3); color: var(--crisp-fg-neutral-muted); }
|
|
18
18
|
.crisp-multiselect-search svg { flex-shrink: 0; }
|
|
19
|
-
.crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-
|
|
20
|
-
.crisp-multiselect-search input::placeholder { color: var(--crisp-fg-
|
|
21
|
-
/*
|
|
22
|
-
|
|
23
|
-
same "small muted heading above a run of rows" treatment. */
|
|
24
|
-
.crisp-multiselect-label { padding: var(--crisp-space-1-5) var(--crisp-space-2) var(--crisp-space-0-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
|
|
19
|
+
.crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-select-neutral); }
|
|
20
|
+
.crisp-multiselect-search input::placeholder { color: var(--crisp-fg-select-placeholder); }
|
|
21
|
+
/* Same groupLabel treatment as Select's (13px/regular/neutral-subtle). */
|
|
22
|
+
.crisp-multiselect-label { padding: var(--crisp-space-2) var(--crisp-space-4) 0; font-size: var(--crisp-text-md); line-height: 18px; font-weight: var(--crisp-weight-regular); color: var(--crisp-fg-neutral-subtle); }
|
|
25
23
|
.crisp-multiselect-option {
|
|
26
|
-
box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2);
|
|
27
|
-
padding:
|
|
24
|
+
box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); flex-shrink: 0;
|
|
25
|
+
padding: var(--crisp-space-2-5) var(--crisp-space-4); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-xl);
|
|
26
|
+
font-family: inherit; text-align: start; cursor: pointer;
|
|
27
|
+
transition: background-color .15s cubic-bezier(.35,0,.35,1);
|
|
28
28
|
}
|
|
29
|
+
/* Keyboard/hover highlight keeps crisp's own visible neutral fill, same
|
|
30
|
+
reasoning as Select's matching rule (seed's own pressed tint is too
|
|
31
|
+
faint for a keyboard-navigation indicator). */
|
|
29
32
|
.crisp-multiselect-option[data-highlighted] { background: var(--crisp-bg-raised); }
|
|
33
|
+
.crisp-multiselect-option:active { background: var(--crisp-bg-select-pressed); }
|
|
30
34
|
.crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
|
|
31
35
|
.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: var(--crisp-radius-sm); box-shadow: inset 0 0 0 1.5px var(--crisp-border-default); color: transparent; }
|
|
32
36
|
.crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: var(--crisp-bg-brand-solid); box-shadow: none; color: #fff; }
|
|
33
|
-
.crisp-multiselect-empty { padding: var(--crisp-space-3) var(--crisp-space-
|
|
37
|
+
.crisp-multiselect-empty { padding: var(--crisp-space-3) var(--crisp-space-4); text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-neutral-subtle); }
|
|
34
38
|
|
|
35
39
|
.crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); }
|
|
36
40
|
.crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent); }
|
|
@@ -43,21 +47,41 @@
|
|
|
43
47
|
|
|
44
48
|
.crisp-multiselect {
|
|
45
49
|
position: relative; display: inline-flex; width: 100%; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
46
|
-
& .crisp-multiselect-trigger { box-sizing: border-box; display: flex; align-items: center;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
& .crisp-multiselect-trigger:disabled {
|
|
50
|
+
& .crisp-multiselect-trigger { box-sizing: border-box; display: flex; align-items: center; width: 100%; min-width: 0; margin: 0; border: 1px solid var(--crisp-stroke-select-trigger); background: transparent; cursor: pointer; text-align: start; font-family: inherit; color: var(--crisp-fg-select-neutral); 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); }
|
|
51
|
+
/* Same pressed/disabled/readonly/focus model as Select's trigger -- see
|
|
52
|
+
that recipe's comment for why hover and an open/focus ring are gone. */
|
|
53
|
+
& .crisp-multiselect-trigger:active:not(:disabled):not([aria-readonly="true"]) { background: var(--crisp-bg-select-pressed); }
|
|
54
|
+
& .crisp-multiselect-trigger:focus-visible { outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px; }
|
|
55
|
+
& .crisp-multiselect-trigger:disabled { background: var(--crisp-bg-select-disabled); cursor: not-allowed; }
|
|
56
|
+
& .crisp-multiselect-trigger:disabled .crisp-multiselect-placeholder, & .crisp-multiselect-trigger:disabled .crisp-multiselect-chevron { color: var(--crisp-fg-disabled); }
|
|
57
|
+
& .crisp-multiselect-trigger[aria-readonly="true"] { background: var(--crisp-bg-select-disabled); cursor: default; }
|
|
50
58
|
& .crisp-multiselect-chips { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--crisp-space-1); }
|
|
51
|
-
& .crisp-multiselect-placeholder { color: var(--crisp-fg-
|
|
52
|
-
|
|
53
|
-
& .crisp-multiselect-
|
|
59
|
+
& .crisp-multiselect-placeholder { color: var(--crisp-fg-select-placeholder); }
|
|
60
|
+
/* closeRotateDuration/openRotateDuration, same asymmetric timing as Select's own chevron (see that recipe's comment). */
|
|
61
|
+
& .crisp-multiselect-chevron { flex-shrink: 0; color: var(--crisp-fg-neutral-muted); transition: transform .1s cubic-bezier(.35,0,.35,1); }
|
|
62
|
+
& .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); transition-duration: .15s; }
|
|
63
|
+
}
|
|
64
|
+
.crisp-multiselect--size_medium {
|
|
65
|
+
& .crisp-multiselect-trigger { min-height: var(--crisp-size-selectTriggerMd); border-radius: var(--crisp-radius-md); gap: var(--crisp-space-2); padding: var(--crisp-space-1) var(--crisp-space-3-5); }
|
|
66
|
+
& .crisp-multiselect-chevron { width: var(--crisp-size-icon); height: var(--crisp-size-icon); }
|
|
67
|
+
}
|
|
68
|
+
.crisp-multiselect--size_large {
|
|
69
|
+
& .crisp-multiselect-trigger { min-height: var(--crisp-size-selectTriggerLg); border-radius: var(--crisp-radius-xl); gap: var(--crisp-space-2-5); padding: var(--crisp-space-1-5) var(--crisp-space-4); }
|
|
70
|
+
& .crisp-multiselect-chevron { width: var(--crisp-size-selectIconLg); height: var(--crisp-size-selectIconLg); }
|
|
54
71
|
}
|
|
55
72
|
.crisp-multiselect--invalid_true {
|
|
56
|
-
& .crisp-multiselect-trigger
|
|
73
|
+
& .crisp-multiselect-trigger { border-width: 2px; border-color: var(--crisp-stroke-select-invalid); }
|
|
57
74
|
}
|
|
58
75
|
.crisp-multiselect--invalid_false {
|
|
59
76
|
|
|
60
77
|
}
|
|
78
|
+
.crisp-multiselect--variant_default {
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
.crisp-multiselect--variant_ghost {
|
|
82
|
+
& .crisp-multiselect-trigger { min-height: 0; height: 100%; border-color: transparent; }
|
|
83
|
+
& .crisp-multiselect-trigger:hover:not(:disabled) { background: var(--crisp-bg-select-pressed); }
|
|
84
|
+
}
|
|
61
85
|
|
|
62
86
|
@media (prefers-reduced-motion: reduce) {
|
|
63
87
|
.crisp-multiselect, .crisp-multiselect * {
|
package/dist/multiselect.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { RecipeFn } from "../core/types";
|
|
2
2
|
|
|
3
3
|
export interface MultiselectVariants {
|
|
4
|
+
size?: "medium" | "large";
|
|
4
5
|
invalid?: boolean;
|
|
6
|
+
variant?: "default" | "ghost";
|
|
5
7
|
}
|
|
6
8
|
|
|
7
|
-
export declare const multiselectRecipe: RecipeFn<{ invalid: { true: string; false: string } }>;
|
|
9
|
+
export declare const multiselectRecipe: RecipeFn<{ size: { medium: string; large: string }; invalid: { true: string; false: string }; variant: { default: string; ghost: string } }>;
|