@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
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* NOT for `WheelPicker`: that is an always-visible multi-column wheel that
|
|
12
12
|
* commits on move, so Enter, Escape and Tab have no meaning in it.
|
|
13
13
|
*/
|
|
14
|
-
export function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, close, dismiss = close, }) {
|
|
14
|
+
export function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, close, dismiss = close, commitOnSpace = false, }) {
|
|
15
15
|
const move = (dir) => {
|
|
16
16
|
if (enabledIndexes.length === 0)
|
|
17
17
|
return;
|
|
@@ -50,6 +50,12 @@ export function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, co
|
|
|
50
50
|
e.preventDefault();
|
|
51
51
|
commit();
|
|
52
52
|
break;
|
|
53
|
+
case " ":
|
|
54
|
+
if (!commitOnSpace)
|
|
55
|
+
break;
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
commit();
|
|
58
|
+
break;
|
|
53
59
|
case "Escape":
|
|
54
60
|
e.preventDefault();
|
|
55
61
|
close();
|
|
@@ -10,13 +10,16 @@ function optionText(o) {
|
|
|
10
10
|
return typeof o.label === "string" ? o.label : o.value;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* A select / dropdown
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* A select / dropdown, built to seed-design's Select (crisp/#867) rather than
|
|
14
|
+
* measured from Attio — a deliberate one-component exception, the same way
|
|
15
|
+
* `DatePicker` follows seed's calendar structure — with an optional
|
|
16
|
+
* Linear-style search seed itself has no equivalent of. A `role="combobox"`
|
|
17
|
+
* trigger opens a `role="listbox"`; pass `searchable` to add a filter field
|
|
18
|
+
* at the top (with a "No results" empty state). Full keyboard model — ↑/↓
|
|
19
|
+
* move, Enter/Space pick, Esc close, Home/End jump, opens upward when tight.
|
|
20
|
+
* Controlled or uncontrolled.
|
|
18
21
|
*/
|
|
19
|
-
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
22
|
+
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", size = "medium", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
20
23
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
21
24
|
// CrispMessagesProvider's translation of the English default, or that
|
|
22
25
|
// default itself with no provider — unchanged from before this existed.
|
|
@@ -63,8 +66,12 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
63
66
|
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
64
67
|
? optionId(highlight)
|
|
65
68
|
: undefined;
|
|
69
|
+
// seed-design's `interactive` (crisp/#867): disabled AND readOnly both
|
|
70
|
+
// refuse to open, but only `disabled` removes the button from the tab
|
|
71
|
+
// order and greys its text — readOnly stays focusable, full-strength ink.
|
|
72
|
+
const interactive = !disabled && !readOnly;
|
|
66
73
|
const openMenu = () => {
|
|
67
|
-
if (
|
|
74
|
+
if (!interactive)
|
|
68
75
|
return;
|
|
69
76
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
70
77
|
if (rect) {
|
|
@@ -73,7 +80,7 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
73
80
|
// direction that is (#210)
|
|
74
81
|
const { style, up: flipUp } = placeFloating({
|
|
75
82
|
rect,
|
|
76
|
-
gap: 4
|
|
83
|
+
gap: 8, // seed-design's select.yaml `gutter` (crisp/#867; was 4)
|
|
77
84
|
flipBelow: 260,
|
|
78
85
|
anchor: triggerRef.current,
|
|
79
86
|
});
|
|
@@ -107,8 +114,14 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
107
114
|
commit: () => pick(filtered[highlight]),
|
|
108
115
|
close,
|
|
109
116
|
dismiss: () => setOpen(false),
|
|
117
|
+
// Space also commits when the trigger itself owns navigation (crisp/#867,
|
|
118
|
+
// matching seed-design). Never on the search input's own wiring below —
|
|
119
|
+
// that one omits this option, so Space stays a normal character there.
|
|
120
|
+
commitOnSpace: !searchable,
|
|
110
121
|
});
|
|
111
122
|
const onTriggerKeyDown = (e) => {
|
|
123
|
+
if (!interactive)
|
|
124
|
+
return;
|
|
112
125
|
if (!open) {
|
|
113
126
|
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
|
114
127
|
e.preventDefault();
|
|
@@ -165,10 +178,10 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
165
178
|
document.addEventListener("keydown", onKey);
|
|
166
179
|
return () => document.removeEventListener("keydown", onKey);
|
|
167
180
|
}, [open, close]);
|
|
168
|
-
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, invalid, className }), ...rest, children: [name && (selected || required) && (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
181
|
+
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, size, invalid, className }), ...rest, children: [name && (selected || required) && (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
169
182
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
170
183
|
// validation, which is the only reason this input is not hidden
|
|
171
|
-
onChange: () => { } })), _jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "crisp-select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-select-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
184
|
+
onChange: () => { } })), _jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "crisp-select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-select-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
172
185
|
setQuery(e.target.value);
|
|
173
186
|
setHighlight(0);
|
|
174
187
|
onSearchChange?.(e.target.value);
|
|
@@ -179,7 +192,7 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
179
192
|
// heading, not one each.
|
|
180
193
|
const showGroupLabel = opt.group !== undefined &&
|
|
181
194
|
opt.group !== filtered[i - 1]?.group;
|
|
182
|
-
return (_jsxs(React.Fragment, { children: [showGroupLabel && (_jsx("div", { className: "crisp-select-label", children: opt.group })), _jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "crisp-select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] })] }, opt.value));
|
|
195
|
+
return (_jsxs(React.Fragment, { children: [showGroupLabel && (_jsx("div", { className: "crisp-select-label", children: opt.group })), _jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), opt.description !== undefined ? (_jsxs("span", { className: "crisp-select-option-body", children: [_jsx("span", { className: "crisp-select-option-label", children: opt.label }), _jsx("span", { className: "crisp-select-option-description", children: opt.description })] })) : (_jsx("span", { className: "crisp-select-option-label", children: opt.label })), isSel && (_jsx(Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] })] }, opt.value));
|
|
183
196
|
}))] }) }))] }));
|
|
184
197
|
}
|
|
185
198
|
Select.displayName = "Select";
|
|
@@ -1,24 +1,36 @@
|
|
|
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
|
-
// it
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
3
|
+
// seed-design's Select, resolved value-for-value from
|
|
4
|
+
// `@seed-design/rootage`'s select.yaml / select-trigger.yaml / select-item.yaml
|
|
5
|
+
// (crisp/#867) — a deliberate, owner-requested exception to "measure Attio
|
|
6
|
+
// first": this ONE component follows seed-design, not Attio, the same way
|
|
7
|
+
// DatePicker already follows seed's calendar structure. Every px/colour below
|
|
8
|
+
// traces to a resolved seed token (see the new entries in tokens/scale.ts,
|
|
9
|
+
// tokens/palette.ts, tokens/semantic.ts and tokens/elevation.ts, each
|
|
10
|
+
// commented with the seed token it came from) — nothing here is measured
|
|
11
|
+
// live, because there is no live Attio reference for this shape any more.
|
|
12
|
+
//
|
|
13
|
+
// crisp keeps its own `searchable` filter field and `triggerLabel` (#846):
|
|
14
|
+
// seed has no search of its own, and the owner asked to keep both. The
|
|
15
|
+
// listbox stays portaled to `document.body` for the same reason #339
|
|
16
|
+
// established (no ancestor's transform/backdrop-filter can capture its
|
|
17
|
+
// position) — see the sibling comment on `globalCss` below for why its rules
|
|
18
|
+
// live there rather than nested under `&`.
|
|
19
|
+
//
|
|
20
|
+
// Two things seed has that this recipe deliberately does NOT reproduce:
|
|
21
|
+
// - A JS-driven exit animation (seed's `exitDuration`/`exitScale`/
|
|
22
|
+
// `exitOpacity`, played before unmount). Every other crisp overlay
|
|
23
|
+
// (Dialog, Menu, ContextMenu, Popover) unmounts synchronously on close and
|
|
24
|
+
// only animates the ENTER transition via `@starting-style` — adding a
|
|
25
|
+
// delayed-unmount mechanism just for Select would be a new architectural
|
|
26
|
+
// pattern with no precedent anywhere else in the repo, for one component.
|
|
27
|
+
// The enter animation (scale 0.95→1, opacity 0→1, seed's own curve) IS
|
|
28
|
+
// reproduced below.
|
|
29
|
+
// - A visible `divider` slot: `select.yaml` defines one, but nothing in
|
|
30
|
+
// crisp or GRC groups options with an explicit separator today, and
|
|
31
|
+
// AUTHORING's rule against building what nothing consumes applies to CSS
|
|
32
|
+
// too. The resolved value is in `~/grc-parity/seed-select-spec.md` for
|
|
33
|
+
// whoever adds it.
|
|
22
34
|
const config = defineRecipe({
|
|
23
35
|
name: "select",
|
|
24
36
|
base: `
|
|
@@ -26,87 +38,147 @@ const config = defineRecipe({
|
|
|
26
38
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
27
39
|
|
|
28
40
|
& .crisp-select-trigger {
|
|
29
|
-
box-sizing: border-box; display: inline-flex; align-items: center;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
transition: background-color .12s ease, box-shadow .12s ease;
|
|
41
|
+
box-sizing: border-box; display: inline-flex; align-items: center; width: 100%; min-width: 0; margin: 0;
|
|
42
|
+
border: 1px solid ${c.strokeSelectTrigger}; background: transparent; cursor: pointer;
|
|
43
|
+
font-family: inherit; color: ${c.fgSelectNeutral};
|
|
44
|
+
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);
|
|
34
45
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
/* seed defines no hover state for this trigger -- only pressed, invalid,
|
|
47
|
+
disabled and readonly. The scale-down is scoped to the CONTENT
|
|
48
|
+
(seed's scaleScope: content), not the button itself, so the border
|
|
49
|
+
never moves. */
|
|
50
|
+
/* Only the value scales, not the chevron: the chevron's own transform is
|
|
51
|
+
its open/close rotation, and a scale would fight it for one shared
|
|
52
|
+
CSS property when both are true at once (pressing while open). */
|
|
53
|
+
& .crisp-select-trigger:active:not(:disabled):not([aria-readonly="true"]) { background: ${c.bgSelectPressed}; }
|
|
54
|
+
& .crisp-select-trigger:active:not(:disabled):not([aria-readonly="true"]) .crisp-select-value { transform: scale(0.98); }
|
|
55
|
+
& .crisp-select-trigger:focus-visible { outline: 2px solid ${c.strokeFocusRing}; outline-offset: -2px; }
|
|
56
|
+
& .crisp-select-trigger:disabled { background: ${c.bgSelectDisabled}; cursor: not-allowed; }
|
|
57
|
+
& .crisp-select-trigger:disabled .crisp-select-value,
|
|
58
|
+
& .crisp-select-trigger:disabled .crisp-select-placeholder,
|
|
59
|
+
& .crisp-select-trigger:disabled .crisp-select-chevron { color: ${c.fgDisabled}; }
|
|
60
|
+
/* readonly: same fill as disabled, but the value/placeholder text stays
|
|
61
|
+
normal ink -- it reads as "answered and locked", not "answered and
|
|
62
|
+
greyed out" (select-trigger.yaml's readonly state only overrides
|
|
63
|
+
root.color; value/placeholder keep their enabled colours). */
|
|
64
|
+
& .crisp-select-trigger[aria-readonly="true"] { background: ${c.bgSelectDisabled}; cursor: default; }
|
|
65
|
+
& .crisp-select-value { flex: 1; min-width: 0; display: inline-flex; align-items: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; transition: transform .15s cubic-bezier(0,0,.15,1); }
|
|
66
|
+
& .crisp-select-placeholder { color: ${c.fgSelectPlaceholder}; }
|
|
67
|
+
/* closeRotateDuration/closeRotateTimingFunction: 100ms, seed's "easing"
|
|
68
|
+
curve. Overridden to 150ms on open (openRotateDuration) below --
|
|
69
|
+
same curve both ways, only the duration differs. */
|
|
70
|
+
& .crisp-select-chevron { flex-shrink: 0; color: ${c.fgNeutralMuted}; transition: transform .1s cubic-bezier(.35,0,.35,1); }
|
|
71
|
+
& .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); transition-duration: .15s; }
|
|
42
72
|
`,
|
|
43
73
|
globalCss: `
|
|
44
74
|
/* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
|
|
45
|
-
on document.body while open
|
|
46
|
-
|
|
47
|
-
inherited the lock, so every option was unclickable, hit-testing
|
|
48
|
-
right through to whatever real element sat behind it. */
|
|
75
|
+
on document.body while open, and the portaled listbox is outside its
|
|
76
|
+
knowledge -- it inherited the lock without this. */
|
|
49
77
|
.crisp-select-listbox {
|
|
50
78
|
font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
51
|
-
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space
|
|
52
|
-
max-height:
|
|
53
|
-
border-radius: ${c.radius
|
|
54
|
-
box-shadow: ${e.
|
|
55
|
-
transform-origin: top left; opacity: 1; transform: none;
|
|
79
|
+
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space["2"]};
|
|
80
|
+
max-height: 480px; overflow-y: auto; padding: ${c.space["2"]};
|
|
81
|
+
border-radius: ${c.radius["3xl"]}; background: ${c.bgSelectListbox};
|
|
82
|
+
box-shadow: ${e.selectFloating};
|
|
83
|
+
transform-origin: top left; opacity: 1; transform: none;
|
|
84
|
+
transition: opacity .15s cubic-bezier(0,0,.15,1), transform .15s cubic-bezier(0,0,.15,1);
|
|
56
85
|
}
|
|
57
|
-
.crisp-select-listbox[data-up] { transform-origin: bottom; }
|
|
58
|
-
.crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.
|
|
59
|
-
.crisp-select-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["
|
|
86
|
+
.crisp-select-listbox[data-up] { transform-origin: bottom left; }
|
|
87
|
+
.crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
|
|
88
|
+
.crisp-select-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["2"]}) calc(-1 * ${c.space["2"]}) ${c.space["2"]}; padding: 0 ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgNeutralMuted}; }
|
|
60
89
|
.crisp-select-search svg { flex-shrink: 0; }
|
|
61
|
-
.crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.
|
|
62
|
-
.crisp-select-search input::placeholder { color: ${c.
|
|
63
|
-
.crisp-select-empty { padding: ${c.space["3"]} ${c.space["
|
|
64
|
-
/*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.crisp-select-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}; }
|
|
90
|
+
.crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgSelectNeutral}; }
|
|
91
|
+
.crisp-select-search input::placeholder { color: ${c.fgSelectPlaceholder}; }
|
|
92
|
+
.crisp-select-empty { padding: ${c.space["3"]} ${c.space["4"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgNeutralSubtle}; }
|
|
93
|
+
/* select.yaml's groupLabel -- font-size/line-height/weight are the same
|
|
94
|
+
across sizes, only paddingY differs (medium 8px, large 10px). */
|
|
95
|
+
.crisp-select-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}; }
|
|
68
96
|
.crisp-select-option {
|
|
69
|
-
box-sizing: border-box; display: flex; align-items: center;
|
|
70
|
-
padding:
|
|
71
|
-
font-family: inherit;
|
|
97
|
+
box-sizing: border-box; display: flex; align-items: center; flex-shrink: 0;
|
|
98
|
+
padding: ${c.space["2.5"]} ${c.space["4"]}; margin-inline: 0; border: none; background: none; border-radius: ${c.radius.xl};
|
|
99
|
+
font-family: inherit; color: ${c.fgSelectNeutral}; text-align: start; cursor: pointer; white-space: nowrap;
|
|
100
|
+
transition: background-color .15s cubic-bezier(.35,0,.35,1), margin-inline .15s cubic-bezier(.35,0,.35,1);
|
|
72
101
|
}
|
|
102
|
+
/* Keyboard/hover highlight keeps crisp's own visible neutral fill --
|
|
103
|
+
seed's spec has no "highlighted" state at all (only pressed/selected/
|
|
104
|
+
disabled), and its actual pressed tint (below) is a ~3% black hint,
|
|
105
|
+
too faint to serve as a keyboard-navigation indicator. */
|
|
73
106
|
.crisp-select-option[data-highlighted] { background: ${c.bgRaised}; }
|
|
74
|
-
.crisp-select-option[data-disabled] {
|
|
75
|
-
.crisp-select-option-
|
|
107
|
+
.crisp-select-option:active:not([data-disabled]) { background: ${c.bgSelectPressed}; margin-inline: ${c.space["2"]}; }
|
|
108
|
+
.crisp-select-option[data-disabled] { cursor: not-allowed; }
|
|
109
|
+
.crisp-select-option[data-disabled] .crisp-select-option-label,
|
|
110
|
+
.crisp-select-option[data-disabled] .crisp-select-option-description,
|
|
111
|
+
.crisp-select-option[data-disabled] .crisp-select-option-check { color: ${c.fgDisabled}; }
|
|
112
|
+
.crisp-select-option-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${c.radius.full}; margin-inline-end: ${c.space["2"]}; }
|
|
113
|
+
.crisp-select-option-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: ${c.space["0.5"]}; }
|
|
76
114
|
.crisp-select-option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
77
|
-
.crisp-select-option-
|
|
115
|
+
.crisp-select-option-description { min-width: 0; overflow: hidden; text-overflow: ellipsis; color: ${c.fgNeutralSubtle}; }
|
|
116
|
+
.crisp-select-option-check { flex-shrink: 0; color: ${c.fgSelectNeutral}; margin-inline-start: ${c.space["2"]}; }
|
|
78
117
|
`,
|
|
79
118
|
variants: {
|
|
80
|
-
//
|
|
81
|
-
// inline selects (e.g. the sort configurator).
|
|
82
|
-
variant: {
|
|
83
|
-
default: "",
|
|
84
|
-
ghost: `
|
|
85
|
-
& .crisp-select-trigger { background: transparent; box-shadow: none; }
|
|
86
|
-
& .crisp-select-trigger:hover { background: ${c.bgRaised}; box-shadow: none; }
|
|
87
|
-
& .crisp-select-trigger[data-open], & .crisp-select-trigger:focus-visible { background: transparent; box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
|
|
88
|
-
`,
|
|
89
|
-
},
|
|
90
|
-
// full (default): fills its container, as before. auto: sizes to content.
|
|
119
|
+
// full (default): fills its container. auto: sizes to content.
|
|
91
120
|
width: {
|
|
92
121
|
full: `width: 100%;`,
|
|
93
122
|
auto: `width: auto;`,
|
|
94
123
|
},
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
& .crisp-select-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
// seed's two trigger/item sizes (select-trigger.yaml / select-item.yaml).
|
|
125
|
+
// medium is the default -- closest to crisp's previous single size.
|
|
126
|
+
size: {
|
|
127
|
+
medium: `
|
|
128
|
+
& .crisp-select-trigger { height: ${c.size.selectTriggerMd}; border-radius: ${c.radius.md}; gap: ${c.space["2"]}; padding-inline: ${c.space["3.5"]}; }
|
|
129
|
+
& .crisp-select-value, & .crisp-select-placeholder { font-size: ${c.text.lg}; line-height: 19px; }
|
|
130
|
+
& .crisp-select-chevron { width: ${c.size.icon}; height: ${c.size.icon}; }
|
|
131
|
+
.crisp-select-option { gap: ${c.space["2"]}; }
|
|
132
|
+
.crisp-select-option-label { font-size: ${c.text.lg}; line-height: 19px; }
|
|
133
|
+
.crisp-select-option-description { font-size: ${c.text.sm}; line-height: 16px; }
|
|
134
|
+
.crisp-select-option-check { width: ${c.space["3"]}; height: ${c.space["3"]}; }
|
|
135
|
+
.crisp-select-label { padding-top: ${c.space["2"]}; }
|
|
105
136
|
`,
|
|
137
|
+
large: `
|
|
138
|
+
& .crisp-select-trigger { height: ${c.size.selectTriggerLg}; border-radius: ${c.radius.xl}; gap: ${c.space["2.5"]}; padding-inline: ${c.space["4"]}; }
|
|
139
|
+
& .crisp-select-value, & .crisp-select-placeholder { font-size: ${c.text.xl}; line-height: 22px; }
|
|
140
|
+
& .crisp-select-chevron { width: ${c.size.selectIconLg}; height: ${c.size.selectIconLg}; }
|
|
141
|
+
.crisp-select-option { gap: ${c.space["3"]}; }
|
|
142
|
+
.crisp-select-option-label { font-size: ${c.text.xl}; line-height: 22px; }
|
|
143
|
+
.crisp-select-option-description { font-size: ${c.text.md}; line-height: 18px; }
|
|
144
|
+
.crisp-select-option-check { width: ${c.space["3.5"]}; height: ${c.space["3.5"]}; }
|
|
145
|
+
.crisp-select-label { padding-top: ${c.space["2.5"]}; }
|
|
146
|
+
`,
|
|
147
|
+
},
|
|
148
|
+
// Real border width+colour change (seed: strokeWidth 1px→2px,
|
|
149
|
+
// strokeColor neutral-weak→critical-solid), not a box-shadow ring --
|
|
150
|
+
// crisp's own field/switch invalid rings don't apply here since this
|
|
151
|
+
// component's whole border model changed to match seed's real stroke.
|
|
152
|
+
invalid: {
|
|
153
|
+
true: `& .crisp-select-trigger { border-width: 2px; border-color: ${c.strokeSelectInvalid}; }`,
|
|
106
154
|
false: ``,
|
|
107
155
|
},
|
|
156
|
+
// ghost: borderless at rest, a subtle fill on hover -- inline selects
|
|
157
|
+
// (e.g. the sort configurator, and datatable's own cell editors, which
|
|
158
|
+
// embed at whatever height their row already is, not seed's fixed
|
|
159
|
+
// trigger height). Not a seed state; kept from crisp's existing API
|
|
160
|
+
// surface, layered on top of the seed base.
|
|
161
|
+
//
|
|
162
|
+
// Declared LAST on purpose: `resolveRecipe` emits one CSS rule per
|
|
163
|
+
// variant, in the order this object lists them, and same-specificity
|
|
164
|
+
// rules resolve by source order -- ghost's `height: 100%` below must
|
|
165
|
+
// come after `size`'s fixed height to win the cascade. Move `variant`
|
|
166
|
+
// earlier and every ghost consumer's trigger silently grows to 40/52px
|
|
167
|
+
// inside its 36px row again.
|
|
168
|
+
variant: {
|
|
169
|
+
default: "",
|
|
170
|
+
ghost: `
|
|
171
|
+
& .crisp-select-trigger { height: 100%; border-color: transparent; }
|
|
172
|
+
& .crisp-select-trigger:hover:not(:disabled) { background: ${c.bgSelectPressed}; }
|
|
173
|
+
`,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
defaultVariants: {
|
|
177
|
+
width: "full",
|
|
178
|
+
size: "medium",
|
|
179
|
+
invalid: false,
|
|
180
|
+
variant: "default",
|
|
108
181
|
},
|
|
109
|
-
defaultVariants: { variant: "default", width: "full", invalid: false },
|
|
110
182
|
compoundVariants: [],
|
|
111
183
|
});
|
|
112
184
|
export const selectRecipe = Object.assign(resolveRecipe(config), { config });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { statusdotRecipe } from "./statusdot.recipe.js";
|
|
3
|
+
/**
|
|
4
|
+
* A standalone coloured status/stage dot — a stage-column header's own
|
|
5
|
+
* colour swatch, a status field's leading indicator. Not a Badge: no pill,
|
|
6
|
+
* no text pairing required. Pass `children` (e.g. a label) alongside it in
|
|
7
|
+
* your own flex row; this renders only the dot.
|
|
8
|
+
*/
|
|
9
|
+
export function StatusDot({ color, className, style, ...rest }) {
|
|
10
|
+
return (_jsx("span", { className: statusdotRecipe({ className }), style: { backgroundColor: color, ...style }, ...rest }));
|
|
11
|
+
}
|
|
12
|
+
StatusDot.displayName = "StatusDot";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A standalone status/stage dot — measured live on Attio's Deals Pipeline
|
|
4
|
+
// board (crisp#859): each stage-column header (Lead / In Progress / Won /
|
|
5
|
+
// Lost) renders a free-standing 12x12 circle before its label, no border, no
|
|
6
|
+
// box-shadow. Distinct from Badge's own `dot` prop, which is a smaller 6px
|
|
7
|
+
// dot rendered INSIDE a text pill in `currentColor` (tied to the badge's own
|
|
8
|
+
// tone). This one stands on its own next to plain text.
|
|
9
|
+
//
|
|
10
|
+
// Its fill is a per-instance prop, not a fixed tone variant: the four
|
|
11
|
+
// stage colours measured live (blue #266df0, pink #fa4b94, green #00d17e,
|
|
12
|
+
// red #ff5454) don't match crisp's existing success/warning/danger tokens
|
|
13
|
+
// (green600 #1f9d57, red600 #e5484d) at all — Attio lets a workspace assign
|
|
14
|
+
// an arbitrary colour per stage from its own picker, so this component takes
|
|
15
|
+
// a raw CSS colour rather than guessing at the rest of that picker's palette
|
|
16
|
+
// from four samples.
|
|
17
|
+
const spec = defineSpec({
|
|
18
|
+
name: "statusdot",
|
|
19
|
+
base: {
|
|
20
|
+
display: "inline-block",
|
|
21
|
+
boxSizing: "border-box",
|
|
22
|
+
flexShrink: 0,
|
|
23
|
+
width: "12px",
|
|
24
|
+
height: "12px",
|
|
25
|
+
borderRadius: c.radius.full,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const config = specToRecipe(spec);
|
|
29
|
+
export const statusdotRecipe = resolveRecipe(config);
|
|
30
|
+
export { spec };
|
|
31
|
+
export default config;
|
|
@@ -733,7 +733,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
733
733
|
: sorts.length
|
|
734
734
|
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortField}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
735
735
|
: sortLabel;
|
|
736
|
-
return (_jsx("div", { ref: rootRef, className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : " crisp-tabletoolbar-tool--dashed"}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortTriggerLabel || !sorts.length ? (sortedLabel) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-tabletoolbar-sorted-prefix", children: crispMessages["Sorted by"] ?? "Sorted by" }), " ", _jsx("span", { className: "crisp-tabletoolbar-sorted-field", children: sortField }), sorts.length > 1 && (_jsxs("span", { className: "crisp-tabletoolbar-sorted-count", children: ["
|
|
736
|
+
return (_jsx("div", { ref: rootRef, className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : " crisp-tabletoolbar-tool--dashed"}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortTriggerLabel || !sorts.length ? (sortedLabel) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-tabletoolbar-sorted-prefix", children: crispMessages["Sorted by"] ?? "Sorted by" }), " ", _jsx("span", { className: "crisp-tabletoolbar-sorted-field", children: sortField }), sorts.length > 1 && (_jsxs("span", { className: "crisp-tabletoolbar-sorted-count", children: ["+", sorts.length - 1] }))] }))] }), openMenu === "sort" && (_jsxs("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
737
737
|
if (dragSort !== null)
|
|
738
738
|
e.preventDefault();
|
|
739
739
|
}, onDrop: () => {
|
|
@@ -928,12 +928,12 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
928
928
|
* contents` so it contributes no box of its own — the same technique
|
|
929
929
|
* `TableToolbar`'s own root already uses to disappear from layout.
|
|
930
930
|
*/
|
|
931
|
-
export const ToolbarTool = React.forwardRef(({ active, ghost, className, ...rest }, ref) => (_jsx("span", { className: tabletoolbarRecipe(), children:
|
|
931
|
+
export const ToolbarTool = React.forwardRef(({ active, ghost, count, className, children, ...rest }, ref) => (_jsx("span", { className: tabletoolbarRecipe(), children: _jsxs("button", { ref: ref, type: "button", className: [
|
|
932
932
|
"crisp-tabletoolbar-tool",
|
|
933
933
|
active && "crisp-tabletoolbar-tool--active",
|
|
934
934
|
ghost && "crisp-tabletoolbar-tool--ghost",
|
|
935
935
|
className,
|
|
936
936
|
]
|
|
937
937
|
.filter(Boolean)
|
|
938
|
-
.join(" "), ...rest }) })));
|
|
938
|
+
.join(" "), ...rest, children: [children, count != null && (_jsx("span", { className: "crisp-tabletoolbar-tool-count", children: count }))] }) })));
|
|
939
939
|
ToolbarTool.displayName = "ToolbarTool";
|
|
@@ -104,9 +104,33 @@ const importExportChrome = `
|
|
|
104
104
|
// vs 14px) and its own mid-grey, distinct from the "Sorted by" muted tone.
|
|
105
105
|
// TableToolbar-only, same reasoning as `filterChipChrome` above (RecordsTable
|
|
106
106
|
// keeps its own older Sort label markup, not touched here).
|
|
107
|
+
// crisp#876: re-measured live -- the "+N" isn't just smaller/greyer text,
|
|
108
|
+
// it's a real chip: `background: rgba(0,0,0,.04)`, `box-shadow: inset 0 0 0
|
|
109
|
+
// 1px rgb(238,239,241)` (borderSubtle), `border-radius: 6px`, `padding: 0
|
|
110
|
+
// 4px`, 16px tall, colour `rgb(80,81,85)` -- bit-for-bit crisp's own Badge
|
|
111
|
+
// `tone="neutral" size="small"` shape, so this reuses those exact tokens
|
|
112
|
+
// (`fgNeutralSoft`, `borderSubtle`, `radius.sm`, `space["1"]`) rather than
|
|
113
|
+
// re-deriving new ones or leaving the old 73%-mix colour approximation.
|
|
107
114
|
const sortedLabelChrome = `
|
|
108
115
|
& .crisp-tabletoolbar-sorted-prefix { color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
109
|
-
& .crisp-tabletoolbar-sorted-count {
|
|
116
|
+
& .crisp-tabletoolbar-sorted-count { display: inline-flex; align-items: center; height: 16px; box-sizing: border-box;
|
|
117
|
+
padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent);
|
|
118
|
+
box-shadow: inset 0 0 0 1px ${c.borderSubtle}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft}; }`;
|
|
119
|
+
// crisp#875: a general count-badge slot for `ToolbarTool` (a screen's own
|
|
120
|
+
// quick filter, e.g. "My tasks •5") -- the SAME measured shape as
|
|
121
|
+
// `sorted-count` above, independently re-confirmed on a second live Attio
|
|
122
|
+
// surface this round (a Kanban board's own stage-column count, "Idea 0":
|
|
123
|
+
// identical `data-variant="tertiary" data-size="16"` chip, same bg/ring/
|
|
124
|
+
// radius/padding/colour, plus `font-variant-numeric: tabular-nums` for
|
|
125
|
+
// digit alignment past a single character). Same reasoning as
|
|
126
|
+
// search/menu-input's own duplicated ring formula elsewhere in this file:
|
|
127
|
+
// a distinct class, not a shared one, so `sorted-count`'s own meaning
|
|
128
|
+
// doesn't get diluted by an unrelated consumer.
|
|
129
|
+
const toolCountChrome = `
|
|
130
|
+
& .crisp-tabletoolbar-tool-count { display: inline-flex; align-items: center; height: 16px; box-sizing: border-box;
|
|
131
|
+
padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent);
|
|
132
|
+
box-shadow: inset 0 0 0 1px ${c.borderSubtle}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft};
|
|
133
|
+
font-variant-numeric: tabular-nums; }`;
|
|
110
134
|
// The table toolbar, measured: 28px controls, the Sort/Filter buttons and
|
|
111
135
|
// the menus they open, sort-condition rows, and the filter chips. The chrome
|
|
112
136
|
// itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
|
|
@@ -126,6 +150,7 @@ ${toolbarChromeBody("tabletoolbar")}
|
|
|
126
150
|
${filterChipChrome}
|
|
127
151
|
${importExportChrome}
|
|
128
152
|
${sortedLabelChrome}
|
|
153
|
+
${toolCountChrome}
|
|
129
154
|
${rtlFlipCss}
|
|
130
155
|
`,
|
|
131
156
|
});
|