@crisp-ui-kit/crisp 0.7.1 → 0.7.3
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/cjs/components/datepicker/datepicker.recipe.js +22 -20
- package/dist/cjs/components/multiselect/multiselect.js +12 -3
- package/dist/cjs/components/select/select.js +13 -2
- package/dist/cjs/tokens/scale.js +6 -2
- package/dist/datepicker.css +17 -17
- package/dist/esm/components/datepicker/datepicker.recipe.js +22 -20
- package/dist/esm/components/multiselect/multiselect.js +12 -3
- package/dist/esm/components/select/select.js +13 -2
- package/dist/esm/tokens/scale.js +6 -2
- package/dist/native/tokens.js +2 -2
- package/dist/styles.css +19 -19
- package/dist/tokens.css +2 -2
- package/dist/tokens.json +2 -2
- package/dist/types/tokens/scale.d.ts +2 -2
- package/package.json +1 -1
|
@@ -3,9 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.datepickerRecipe = void 0;
|
|
4
4
|
const core_1 = require("../../core");
|
|
5
5
|
const tokens_1 = require("../../tokens");
|
|
6
|
-
// seed-design's calendar
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// seed-design's calendar structure on Attio's measured scale. The behaviour,
|
|
7
|
+
// markup and range model are seed's; every visual value is measured from Attio's
|
|
8
|
+
// own date picker (app.attio.com, task due-date field): a 32px day on a 34px
|
|
9
|
+
// pitch, an 8px-radius highlight rather than a circle, 14/20 medium numerals and
|
|
10
|
+
// the brand blue for the selection.
|
|
9
11
|
//
|
|
10
12
|
// The pill and the range bridge are ::before layers behind the numeral
|
|
11
13
|
// (`isolation: isolate` on the cell keeps their z-index local), which is what
|
|
@@ -20,18 +22,18 @@ const config = (0, core_1.defineRecipe)({
|
|
|
20
22
|
name: "datepicker",
|
|
21
23
|
base: `
|
|
22
24
|
position: relative; box-sizing: border-box; width: 100%; min-width: 0;
|
|
23
|
-
font-family: ${tokens_1.contract.font.sans}; color: ${tokens_1.contract.
|
|
25
|
+
font-family: ${tokens_1.contract.font.sans}; color: ${tokens_1.contract.fgPrimary}; background: ${tokens_1.contract.bgSurface};
|
|
24
26
|
|
|
25
27
|
& .datepicker-header {
|
|
26
28
|
display: flex; align-items: center; justify-content: space-between;
|
|
27
|
-
height: ${tokens_1.contract.size.
|
|
29
|
+
height: ${tokens_1.contract.size.menuRow};
|
|
28
30
|
}
|
|
29
31
|
& .datepicker-header-label {
|
|
30
32
|
display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]};
|
|
31
33
|
height: 100%; min-width: 0; border: 0; padding: 0 ${tokens_1.contract.space["1"]};
|
|
32
34
|
border-radius: ${tokens_1.contract.radius.md}; background: transparent; cursor: pointer;
|
|
33
35
|
color: ${tokens_1.contract.fgPrimary}; font-family: inherit;
|
|
34
|
-
font-size: ${tokens_1.contract.text.
|
|
36
|
+
font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium};
|
|
35
37
|
white-space: nowrap;
|
|
36
38
|
outline: 2px solid transparent; outline-offset: -2px;
|
|
37
39
|
transition: outline-color .2s ease;
|
|
@@ -60,16 +62,16 @@ const config = (0, core_1.defineRecipe)({
|
|
|
60
62
|
& .datepicker-weekday {
|
|
61
63
|
display: flex; align-items: center; justify-content: center;
|
|
62
64
|
height: ${tokens_1.contract.size.calendarCell}; color: ${tokens_1.contract.fgNeutralSubtle};
|
|
63
|
-
font-size: ${tokens_1.contract.text.
|
|
65
|
+
font-size: ${tokens_1.contract.text.sm}; line-height: 16px; font-weight: ${tokens_1.contract.weight.medium};
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
& .datepicker-months { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; min-width: 0; }
|
|
67
69
|
& .datepicker-months[data-wheel-open] { visibility: hidden; }
|
|
68
70
|
& .datepicker-month { min-width: 0; }
|
|
69
71
|
& .datepicker-month-label {
|
|
70
|
-
display: flex; align-items: center; height:
|
|
71
|
-
color: ${tokens_1.contract.fgPrimary}; font-size: ${tokens_1.contract.text.
|
|
72
|
-
font-weight: ${tokens_1.contract.weight.
|
|
72
|
+
display: flex; align-items: center; height: 24px;
|
|
73
|
+
color: ${tokens_1.contract.fgPrimary}; font-size: ${tokens_1.contract.text.lg}; line-height: 20px;
|
|
74
|
+
font-weight: ${tokens_1.contract.weight.medium};
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
& .datepicker-grid { display: grid; min-width: 0; }
|
|
@@ -108,27 +110,27 @@ const config = (0, core_1.defineRecipe)({
|
|
|
108
110
|
min-height: ${tokens_1.contract.size.calendarCell}; min-width: 0;
|
|
109
111
|
align-items: flex-start; justify-content: center;
|
|
110
112
|
border: 0; padding: 0; background: transparent; cursor: pointer;
|
|
111
|
-
color: ${tokens_1.contract.
|
|
113
|
+
color: ${tokens_1.contract.fgPrimary}; font: inherit; outline: none;
|
|
112
114
|
isolation: isolate;
|
|
113
115
|
}
|
|
114
116
|
/* the round day highlight, behind the numeral */
|
|
115
117
|
& .datepicker-date::before {
|
|
116
118
|
content: ""; position: absolute; z-index: -1; top: ${pillInset}; left: 50%;
|
|
117
119
|
width: ${tokens_1.contract.size.calendarPill}; height: ${tokens_1.contract.size.calendarPill};
|
|
118
|
-
border-radius: ${tokens_1.contract.radius.
|
|
120
|
+
border-radius: ${tokens_1.contract.radius.md};
|
|
119
121
|
background: transparent; transform: translateX(-50%);
|
|
120
122
|
}
|
|
121
123
|
& .datepicker-date[data-today]::before { background: ${tokens_1.contract.bgNeutralWeak}; }
|
|
122
|
-
& .datepicker-date[data-selected]::before { background: ${tokens_1.contract.
|
|
123
|
-
& .datepicker-date[data-selected] { color: ${tokens_1.contract.
|
|
124
|
-
& .datepicker-date[data-in-range] { color: ${tokens_1.contract.
|
|
124
|
+
& .datepicker-date[data-selected]::before { background: ${tokens_1.contract.bgBrandSolid}; }
|
|
125
|
+
& .datepicker-date[data-selected] { color: ${tokens_1.contract.fgOnBrand}; }
|
|
126
|
+
& .datepicker-date[data-in-range] { color: ${tokens_1.contract.fgPrimary}; }
|
|
125
127
|
& .datepicker-date[data-in-range]::before { background: transparent; }
|
|
126
128
|
& .datepicker-date[data-unavailable] { color: ${tokens_1.contract.fgDisabled}; }
|
|
127
129
|
& .datepicker-date:disabled { color: ${tokens_1.contract.fgDisabled}; cursor: not-allowed; }
|
|
128
130
|
& .datepicker-date:focus-visible::after {
|
|
129
131
|
content: ""; position: absolute; top: ${pillInset}; left: 50%;
|
|
130
132
|
width: ${tokens_1.contract.size.calendarPill}; height: ${tokens_1.contract.size.calendarPill};
|
|
131
|
-
transform: translateX(-50%); border-radius: ${tokens_1.contract.radius.
|
|
133
|
+
transform: translateX(-50%); border-radius: ${tokens_1.contract.radius.md};
|
|
132
134
|
outline: 2px solid ${tokens_1.contract.strokeFocusRing}; outline-offset: -2px;
|
|
133
135
|
}
|
|
134
136
|
|
|
@@ -137,7 +139,7 @@ const config = (0, core_1.defineRecipe)({
|
|
|
137
139
|
min-height: ${tokens_1.contract.size.calendarCell}; min-width: 0; box-sizing: border-box;
|
|
138
140
|
flex-direction: column; align-items: center; gap: 3px;
|
|
139
141
|
padding-top: ${pillInset}; color: inherit;
|
|
140
|
-
font-size: ${tokens_1.contract.text.
|
|
142
|
+
font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium};
|
|
141
143
|
font-variant-numeric: tabular-nums;
|
|
142
144
|
}
|
|
143
145
|
/* The numeral is its own 42px flex box so it centres on the 42px pill.
|
|
@@ -152,7 +154,7 @@ const config = (0, core_1.defineRecipe)({
|
|
|
152
154
|
/* the year/month wheel, laid over the grid it replaces */
|
|
153
155
|
& .datepicker-wheel-container {
|
|
154
156
|
position: absolute; z-index: 1; inset-inline: 0;
|
|
155
|
-
top: ${tokens_1.contract.size.
|
|
157
|
+
top: ${tokens_1.contract.size.menuRow}; bottom: 0;
|
|
156
158
|
display: flex; align-items: center; overflow: hidden;
|
|
157
159
|
background: ${tokens_1.contract.bgSurface};
|
|
158
160
|
}
|
|
@@ -175,8 +177,8 @@ const config = (0, core_1.defineRecipe)({
|
|
|
175
177
|
visibleRange: {
|
|
176
178
|
month: ``,
|
|
177
179
|
twoMonths: `& .datepicker-months { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: ${tokens_1.contract.space["6"]}; }
|
|
178
|
-
& .datepicker-two-month-header { display: flex; align-items: center; justify-content: space-between; height: ${tokens_1.contract.size.
|
|
179
|
-
& .datepicker-two-month-label { color: ${tokens_1.contract.fgPrimary}; font-size: ${tokens_1.contract.text.
|
|
180
|
+
& .datepicker-two-month-header { display: flex; align-items: center; justify-content: space-between; height: ${tokens_1.contract.size.menuRow}; }
|
|
181
|
+
& .datepicker-two-month-label { color: ${tokens_1.contract.fgPrimary}; font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium}; }`,
|
|
180
182
|
week: ``,
|
|
181
183
|
continuous: `& .datepicker-weekday-row { position: sticky; top: 0; }`,
|
|
182
184
|
},
|
|
@@ -64,6 +64,12 @@ function Chip({ opt }) {
|
|
|
64
64
|
* click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
|
|
65
65
|
*/
|
|
66
66
|
function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
67
|
+
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
68
|
+
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
69
|
+
// styling hook only -- a screen reader cannot see it.
|
|
70
|
+
const autoId = React.useId();
|
|
71
|
+
const listboxId = `${autoId}-listbox`;
|
|
72
|
+
const optionId = (i) => `${autoId}-opt-${i}`;
|
|
67
73
|
const [open, setOpen] = React.useState(false);
|
|
68
74
|
const [up, setUp] = React.useState(false);
|
|
69
75
|
const [pos, setPos] = React.useState({});
|
|
@@ -105,6 +111,9 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
|
|
|
105
111
|
setOpen(false);
|
|
106
112
|
triggerRef.current?.focus({ preventScroll: true });
|
|
107
113
|
};
|
|
114
|
+
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
115
|
+
? optionId(highlight)
|
|
116
|
+
: undefined;
|
|
108
117
|
const toggle = (v) => {
|
|
109
118
|
const next = selectedSet.has(v)
|
|
110
119
|
? value.filter((x) => x !== v)
|
|
@@ -143,19 +152,19 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
|
|
|
143
152
|
document.addEventListener("pointerdown", onDown);
|
|
144
153
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
145
154
|
}, [open]);
|
|
146
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
155
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
147
156
|
if (open && !searchable)
|
|
148
157
|
onNavKeys(e);
|
|
149
158
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
150
159
|
e.preventDefault();
|
|
151
160
|
openMenu();
|
|
152
161
|
}
|
|
153
|
-
}, children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => (0, jsx_runtime_1.jsx)(Chip, { opt: o }, o.value))) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "multiselect-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => {
|
|
162
|
+
}, children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => (0, jsx_runtime_1.jsx)(Chip, { opt: o }, o.value))) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { id: listboxId, className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "multiselect-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
154
163
|
setQuery(e.target.value);
|
|
155
164
|
setHighlight(0);
|
|
156
165
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "multiselect-empty", children: "No results" })) : (filtered.map((opt, i) => {
|
|
157
166
|
const sel = selectedSet.has(opt.value);
|
|
158
|
-
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "option", "aria-selected": sel, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-option-chip", children: (0, jsx_runtime_1.jsx)(Chip, { opt: opt }) }), (0, jsx_runtime_1.jsx)("span", { className: "multiselect-check", children: sel && (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
|
|
167
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-option-chip", children: (0, jsx_runtime_1.jsx)(Chip, { opt: opt }) }), (0, jsx_runtime_1.jsx)("span", { className: "multiselect-check", children: sel && (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
|
|
159
168
|
}))] }))] }));
|
|
160
169
|
}
|
|
161
170
|
MultiSelect.displayName = "MultiSelect";
|
|
@@ -51,6 +51,14 @@ function optionText(o) {
|
|
|
51
51
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
52
52
|
*/
|
|
53
53
|
function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
54
|
+
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
55
|
+
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
56
|
+
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
57
|
+
// ids below have to resolve to the element the highlight is actually drawn on.
|
|
58
|
+
const autoId = React.useId();
|
|
59
|
+
const baseId = id ?? autoId;
|
|
60
|
+
const listboxId = `${baseId}-listbox`;
|
|
61
|
+
const optionId = (i) => `${baseId}-opt-${i}`;
|
|
54
62
|
const [internal, setInternal] = React.useState(defaultValue);
|
|
55
63
|
const selected = value ?? internal;
|
|
56
64
|
const [open, setOpen] = React.useState(false);
|
|
@@ -70,6 +78,9 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
|
|
|
70
78
|
const enabledIndexes = filtered
|
|
71
79
|
.map((o, i) => (o.disabled ? -1 : i))
|
|
72
80
|
.filter((i) => i >= 0);
|
|
81
|
+
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
82
|
+
? optionId(highlight)
|
|
83
|
+
: undefined;
|
|
73
84
|
const openMenu = () => {
|
|
74
85
|
if (disabled)
|
|
75
86
|
return;
|
|
@@ -169,12 +180,12 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
|
|
|
169
180
|
document.addEventListener("pointerdown", onDown);
|
|
170
181
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
171
182
|
}, [open]);
|
|
172
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => {
|
|
183
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [(0, jsx_runtime_1.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, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { id: listboxId, className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
173
184
|
setQuery(e.target.value);
|
|
174
185
|
setHighlight(0);
|
|
175
186
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "select-empty", children: "No results" })) : (filtered.map((opt, i) => {
|
|
176
187
|
const isSel = opt.value === selected;
|
|
177
|
-
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "option", "aria-selected": isSel, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
|
|
188
|
+
return ((0, jsx_runtime_1.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: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
|
|
178
189
|
}))] }))] }));
|
|
179
190
|
}
|
|
180
191
|
Select.displayName = "Select";
|
package/dist/cjs/tokens/scale.js
CHANGED
|
@@ -46,8 +46,12 @@ exports.size = {
|
|
|
46
46
|
tableRow: "36px", // data-grid row height
|
|
47
47
|
tableHeader: "40px", // data-grid header row height
|
|
48
48
|
header: "48px", // workspace-switcher header height
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// Attio's date picker, measured: a 32px day with a 2px gap on both axes, so
|
|
50
|
+
// the pitch is 34. The gap is expressed as the cell being 2px taller than the
|
|
51
|
+
// highlight rather than as a grid gap, because crisp's range bridge spans
|
|
52
|
+
// whole cells and a column gap would cut it.
|
|
53
|
+
calendarCell: "34px", // calendar day pitch (32px day + 2px gap)
|
|
54
|
+
calendarPill: "32px", // the day highlight itself
|
|
51
55
|
};
|
|
52
56
|
// Typography — Attio's UI runs entirely on Inter. Sizes measured in the sidebar
|
|
53
57
|
// collapse onto 11/12/13/14/16 (lg = 14px is the default body/label size);
|
package/dist/datepicker.css
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
.datepicker {
|
|
2
2
|
|
|
3
3
|
position: relative; box-sizing: border-box; width: 100%; min-width: 0;
|
|
4
|
-
font-family: var(--font-sans); color: var(--fg-
|
|
4
|
+
font-family: var(--font-sans); color: var(--fg-primary); background: var(--bg-surface);
|
|
5
5
|
|
|
6
6
|
& .datepicker-header {
|
|
7
7
|
display: flex; align-items: center; justify-content: space-between;
|
|
8
|
-
height: var(--size-
|
|
8
|
+
height: var(--size-menuRow);
|
|
9
9
|
}
|
|
10
10
|
& .datepicker-header-label {
|
|
11
11
|
display: inline-flex; align-items: center; gap: var(--space-1);
|
|
12
12
|
height: 100%; min-width: 0; border: 0; padding: 0 var(--space-1);
|
|
13
13
|
border-radius: var(--radius-md); background: transparent; cursor: pointer;
|
|
14
14
|
color: var(--fg-primary); font-family: inherit;
|
|
15
|
-
font-size: var(--text-
|
|
15
|
+
font-size: var(--text-lg); line-height: 20px; font-weight: var(--weight-medium);
|
|
16
16
|
white-space: nowrap;
|
|
17
17
|
outline: 2px solid transparent; outline-offset: -2px;
|
|
18
18
|
transition: outline-color .2s ease;
|
|
@@ -41,16 +41,16 @@
|
|
|
41
41
|
& .datepicker-weekday {
|
|
42
42
|
display: flex; align-items: center; justify-content: center;
|
|
43
43
|
height: var(--size-calendarCell); color: var(--fg-neutral-subtle);
|
|
44
|
-
font-size: var(--text-
|
|
44
|
+
font-size: var(--text-sm); line-height: 16px; font-weight: var(--weight-medium);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
& .datepicker-months { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; min-width: 0; }
|
|
48
48
|
& .datepicker-months[data-wheel-open] { visibility: hidden; }
|
|
49
49
|
& .datepicker-month { min-width: 0; }
|
|
50
50
|
& .datepicker-month-label {
|
|
51
|
-
display: flex; align-items: center; height:
|
|
52
|
-
color: var(--fg-primary); font-size: var(--text-
|
|
53
|
-
font-weight: var(--weight-
|
|
51
|
+
display: flex; align-items: center; height: 24px;
|
|
52
|
+
color: var(--fg-primary); font-size: var(--text-lg); line-height: 20px;
|
|
53
|
+
font-weight: var(--weight-medium);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
& .datepicker-grid { display: grid; min-width: 0; }
|
|
@@ -89,27 +89,27 @@
|
|
|
89
89
|
min-height: var(--size-calendarCell); min-width: 0;
|
|
90
90
|
align-items: flex-start; justify-content: center;
|
|
91
91
|
border: 0; padding: 0; background: transparent; cursor: pointer;
|
|
92
|
-
color: var(--fg-
|
|
92
|
+
color: var(--fg-primary); font: inherit; outline: none;
|
|
93
93
|
isolation: isolate;
|
|
94
94
|
}
|
|
95
95
|
/* the round day highlight, behind the numeral */
|
|
96
96
|
& .datepicker-date::before {
|
|
97
97
|
content: ""; position: absolute; z-index: -1; top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); left: 50%;
|
|
98
98
|
width: var(--size-calendarPill); height: var(--size-calendarPill);
|
|
99
|
-
border-radius: var(--radius-
|
|
99
|
+
border-radius: var(--radius-md);
|
|
100
100
|
background: transparent; transform: translateX(-50%);
|
|
101
101
|
}
|
|
102
102
|
& .datepicker-date[data-today]::before { background: var(--bg-neutral-weak); }
|
|
103
|
-
& .datepicker-date[data-selected]::before { background: var(--bg-
|
|
104
|
-
& .datepicker-date[data-selected] { color: var(--fg-on-
|
|
105
|
-
& .datepicker-date[data-in-range] { color: var(--fg-
|
|
103
|
+
& .datepicker-date[data-selected]::before { background: var(--bg-brand-solid); }
|
|
104
|
+
& .datepicker-date[data-selected] { color: var(--fg-on-brand); }
|
|
105
|
+
& .datepicker-date[data-in-range] { color: var(--fg-primary); }
|
|
106
106
|
& .datepicker-date[data-in-range]::before { background: transparent; }
|
|
107
107
|
& .datepicker-date[data-unavailable] { color: var(--fg-disabled); }
|
|
108
108
|
& .datepicker-date:disabled { color: var(--fg-disabled); cursor: not-allowed; }
|
|
109
109
|
& .datepicker-date:focus-visible::after {
|
|
110
110
|
content: ""; position: absolute; top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); left: 50%;
|
|
111
111
|
width: var(--size-calendarPill); height: var(--size-calendarPill);
|
|
112
|
-
transform: translateX(-50%); border-radius: var(--radius-
|
|
112
|
+
transform: translateX(-50%); border-radius: var(--radius-md);
|
|
113
113
|
outline: 2px solid var(--stroke-focus-ring); outline-offset: -2px;
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
min-height: var(--size-calendarCell); min-width: 0; box-sizing: border-box;
|
|
119
119
|
flex-direction: column; align-items: center; gap: 3px;
|
|
120
120
|
padding-top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); color: inherit;
|
|
121
|
-
font-size: var(--text-
|
|
121
|
+
font-size: var(--text-lg); line-height: 20px; font-weight: var(--weight-medium);
|
|
122
122
|
font-variant-numeric: tabular-nums;
|
|
123
123
|
}
|
|
124
124
|
/* The numeral is its own 42px flex box so it centres on the 42px pill.
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
/* the year/month wheel, laid over the grid it replaces */
|
|
134
134
|
& .datepicker-wheel-container {
|
|
135
135
|
position: absolute; z-index: 1; inset-inline: 0;
|
|
136
|
-
top: var(--size-
|
|
136
|
+
top: var(--size-menuRow); bottom: 0;
|
|
137
137
|
display: flex; align-items: center; overflow: hidden;
|
|
138
138
|
background: var(--bg-surface);
|
|
139
139
|
}
|
|
@@ -158,8 +158,8 @@
|
|
|
158
158
|
}
|
|
159
159
|
.datepicker--visibleRange_twoMonths {
|
|
160
160
|
& .datepicker-months { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--space-6); }
|
|
161
|
-
& .datepicker-two-month-header { display: flex; align-items: center; justify-content: space-between; height: var(--size-
|
|
162
|
-
& .datepicker-two-month-label { color: var(--fg-primary); font-size: var(--text-
|
|
161
|
+
& .datepicker-two-month-header { display: flex; align-items: center; justify-content: space-between; height: var(--size-menuRow); }
|
|
162
|
+
& .datepicker-two-month-label { color: var(--fg-primary); font-size: var(--text-lg); line-height: 20px; font-weight: var(--weight-medium); }
|
|
163
163
|
}
|
|
164
164
|
.datepicker--visibleRange_week {
|
|
165
165
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
2
|
import { contract as c } from "../../tokens/index.js";
|
|
3
|
-
// seed-design's calendar
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// seed-design's calendar structure on Attio's measured scale. The behaviour,
|
|
4
|
+
// markup and range model are seed's; every visual value is measured from Attio's
|
|
5
|
+
// own date picker (app.attio.com, task due-date field): a 32px day on a 34px
|
|
6
|
+
// pitch, an 8px-radius highlight rather than a circle, 14/20 medium numerals and
|
|
7
|
+
// the brand blue for the selection.
|
|
6
8
|
//
|
|
7
9
|
// The pill and the range bridge are ::before layers behind the numeral
|
|
8
10
|
// (`isolation: isolate` on the cell keeps their z-index local), which is what
|
|
@@ -17,18 +19,18 @@ const config = defineRecipe({
|
|
|
17
19
|
name: "datepicker",
|
|
18
20
|
base: `
|
|
19
21
|
position: relative; box-sizing: border-box; width: 100%; min-width: 0;
|
|
20
|
-
font-family: ${c.font.sans}; color: ${c.
|
|
22
|
+
font-family: ${c.font.sans}; color: ${c.fgPrimary}; background: ${c.bgSurface};
|
|
21
23
|
|
|
22
24
|
& .datepicker-header {
|
|
23
25
|
display: flex; align-items: center; justify-content: space-between;
|
|
24
|
-
height: ${c.size.
|
|
26
|
+
height: ${c.size.menuRow};
|
|
25
27
|
}
|
|
26
28
|
& .datepicker-header-label {
|
|
27
29
|
display: inline-flex; align-items: center; gap: ${c.space["1"]};
|
|
28
30
|
height: 100%; min-width: 0; border: 0; padding: 0 ${c.space["1"]};
|
|
29
31
|
border-radius: ${c.radius.md}; background: transparent; cursor: pointer;
|
|
30
32
|
color: ${c.fgPrimary}; font-family: inherit;
|
|
31
|
-
font-size: ${c.text.
|
|
33
|
+
font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium};
|
|
32
34
|
white-space: nowrap;
|
|
33
35
|
outline: 2px solid transparent; outline-offset: -2px;
|
|
34
36
|
transition: outline-color .2s ease;
|
|
@@ -57,16 +59,16 @@ const config = defineRecipe({
|
|
|
57
59
|
& .datepicker-weekday {
|
|
58
60
|
display: flex; align-items: center; justify-content: center;
|
|
59
61
|
height: ${c.size.calendarCell}; color: ${c.fgNeutralSubtle};
|
|
60
|
-
font-size: ${c.text.
|
|
62
|
+
font-size: ${c.text.sm}; line-height: 16px; font-weight: ${c.weight.medium};
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
& .datepicker-months { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; min-width: 0; }
|
|
64
66
|
& .datepicker-months[data-wheel-open] { visibility: hidden; }
|
|
65
67
|
& .datepicker-month { min-width: 0; }
|
|
66
68
|
& .datepicker-month-label {
|
|
67
|
-
display: flex; align-items: center; height:
|
|
68
|
-
color: ${c.fgPrimary}; font-size: ${c.text.
|
|
69
|
-
font-weight: ${c.weight.
|
|
69
|
+
display: flex; align-items: center; height: 24px;
|
|
70
|
+
color: ${c.fgPrimary}; font-size: ${c.text.lg}; line-height: 20px;
|
|
71
|
+
font-weight: ${c.weight.medium};
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
& .datepicker-grid { display: grid; min-width: 0; }
|
|
@@ -105,27 +107,27 @@ const config = defineRecipe({
|
|
|
105
107
|
min-height: ${c.size.calendarCell}; min-width: 0;
|
|
106
108
|
align-items: flex-start; justify-content: center;
|
|
107
109
|
border: 0; padding: 0; background: transparent; cursor: pointer;
|
|
108
|
-
color: ${c.
|
|
110
|
+
color: ${c.fgPrimary}; font: inherit; outline: none;
|
|
109
111
|
isolation: isolate;
|
|
110
112
|
}
|
|
111
113
|
/* the round day highlight, behind the numeral */
|
|
112
114
|
& .datepicker-date::before {
|
|
113
115
|
content: ""; position: absolute; z-index: -1; top: ${pillInset}; left: 50%;
|
|
114
116
|
width: ${c.size.calendarPill}; height: ${c.size.calendarPill};
|
|
115
|
-
border-radius: ${c.radius.
|
|
117
|
+
border-radius: ${c.radius.md};
|
|
116
118
|
background: transparent; transform: translateX(-50%);
|
|
117
119
|
}
|
|
118
120
|
& .datepicker-date[data-today]::before { background: ${c.bgNeutralWeak}; }
|
|
119
|
-
& .datepicker-date[data-selected]::before { background: ${c.
|
|
120
|
-
& .datepicker-date[data-selected] { color: ${c.
|
|
121
|
-
& .datepicker-date[data-in-range] { color: ${c.
|
|
121
|
+
& .datepicker-date[data-selected]::before { background: ${c.bgBrandSolid}; }
|
|
122
|
+
& .datepicker-date[data-selected] { color: ${c.fgOnBrand}; }
|
|
123
|
+
& .datepicker-date[data-in-range] { color: ${c.fgPrimary}; }
|
|
122
124
|
& .datepicker-date[data-in-range]::before { background: transparent; }
|
|
123
125
|
& .datepicker-date[data-unavailable] { color: ${c.fgDisabled}; }
|
|
124
126
|
& .datepicker-date:disabled { color: ${c.fgDisabled}; cursor: not-allowed; }
|
|
125
127
|
& .datepicker-date:focus-visible::after {
|
|
126
128
|
content: ""; position: absolute; top: ${pillInset}; left: 50%;
|
|
127
129
|
width: ${c.size.calendarPill}; height: ${c.size.calendarPill};
|
|
128
|
-
transform: translateX(-50%); border-radius: ${c.radius.
|
|
130
|
+
transform: translateX(-50%); border-radius: ${c.radius.md};
|
|
129
131
|
outline: 2px solid ${c.strokeFocusRing}; outline-offset: -2px;
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -134,7 +136,7 @@ const config = defineRecipe({
|
|
|
134
136
|
min-height: ${c.size.calendarCell}; min-width: 0; box-sizing: border-box;
|
|
135
137
|
flex-direction: column; align-items: center; gap: 3px;
|
|
136
138
|
padding-top: ${pillInset}; color: inherit;
|
|
137
|
-
font-size: ${c.text.
|
|
139
|
+
font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium};
|
|
138
140
|
font-variant-numeric: tabular-nums;
|
|
139
141
|
}
|
|
140
142
|
/* The numeral is its own 42px flex box so it centres on the 42px pill.
|
|
@@ -149,7 +151,7 @@ const config = defineRecipe({
|
|
|
149
151
|
/* the year/month wheel, laid over the grid it replaces */
|
|
150
152
|
& .datepicker-wheel-container {
|
|
151
153
|
position: absolute; z-index: 1; inset-inline: 0;
|
|
152
|
-
top: ${c.size.
|
|
154
|
+
top: ${c.size.menuRow}; bottom: 0;
|
|
153
155
|
display: flex; align-items: center; overflow: hidden;
|
|
154
156
|
background: ${c.bgSurface};
|
|
155
157
|
}
|
|
@@ -172,8 +174,8 @@ const config = defineRecipe({
|
|
|
172
174
|
visibleRange: {
|
|
173
175
|
month: ``,
|
|
174
176
|
twoMonths: `& .datepicker-months { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: ${c.space["6"]}; }
|
|
175
|
-
& .datepicker-two-month-header { display: flex; align-items: center; justify-content: space-between; height: ${c.size.
|
|
176
|
-
& .datepicker-two-month-label { color: ${c.fgPrimary}; font-size: ${c.text.
|
|
177
|
+
& .datepicker-two-month-header { display: flex; align-items: center; justify-content: space-between; height: ${c.size.menuRow}; }
|
|
178
|
+
& .datepicker-two-month-label { color: ${c.fgPrimary}; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; }`,
|
|
177
179
|
week: ``,
|
|
178
180
|
continuous: `& .datepicker-weekday-row { position: sticky; top: 0; }`,
|
|
179
181
|
},
|
|
@@ -28,6 +28,12 @@ function Chip({ opt }) {
|
|
|
28
28
|
* click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
|
|
29
29
|
*/
|
|
30
30
|
export function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
31
|
+
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
32
|
+
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
33
|
+
// styling hook only -- a screen reader cannot see it.
|
|
34
|
+
const autoId = React.useId();
|
|
35
|
+
const listboxId = `${autoId}-listbox`;
|
|
36
|
+
const optionId = (i) => `${autoId}-opt-${i}`;
|
|
31
37
|
const [open, setOpen] = React.useState(false);
|
|
32
38
|
const [up, setUp] = React.useState(false);
|
|
33
39
|
const [pos, setPos] = React.useState({});
|
|
@@ -69,6 +75,9 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
69
75
|
setOpen(false);
|
|
70
76
|
triggerRef.current?.focus({ preventScroll: true });
|
|
71
77
|
};
|
|
78
|
+
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
79
|
+
? optionId(highlight)
|
|
80
|
+
: undefined;
|
|
72
81
|
const toggle = (v) => {
|
|
73
82
|
const next = selectedSet.has(v)
|
|
74
83
|
? value.filter((x) => x !== v)
|
|
@@ -107,19 +116,19 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
107
116
|
document.addEventListener("pointerdown", onDown);
|
|
108
117
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
109
118
|
}, [open]);
|
|
110
|
-
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
119
|
+
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
111
120
|
if (open && !searchable)
|
|
112
121
|
onNavKeys(e);
|
|
113
122
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
114
123
|
e.preventDefault();
|
|
115
124
|
openMenu();
|
|
116
125
|
}
|
|
117
|
-
}, children: [_jsx("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => {
|
|
126
|
+
}, children: [_jsx("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { id: listboxId, className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "multiselect-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) => {
|
|
118
127
|
setQuery(e.target.value);
|
|
119
128
|
setHighlight(0);
|
|
120
129
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "multiselect-empty", children: "No results" })) : (filtered.map((opt, i) => {
|
|
121
130
|
const sel = selectedSet.has(opt.value);
|
|
122
|
-
return (_jsxs("button", { type: "button", role: "option", "aria-selected": sel, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [_jsx("span", { className: "multiselect-option-chip", children: _jsx(Chip, { opt: opt }) }), _jsx("span", { className: "multiselect-check", children: sel && _jsx(Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
|
|
131
|
+
return (_jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [_jsx("span", { className: "multiselect-option-chip", children: _jsx(Chip, { opt: opt }) }), _jsx("span", { className: "multiselect-check", children: sel && _jsx(Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
|
|
123
132
|
}))] }))] }));
|
|
124
133
|
}
|
|
125
134
|
MultiSelect.displayName = "MultiSelect";
|
|
@@ -15,6 +15,14 @@ function optionText(o) {
|
|
|
15
15
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
16
16
|
*/
|
|
17
17
|
export function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
18
|
+
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
19
|
+
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
20
|
+
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
21
|
+
// ids below have to resolve to the element the highlight is actually drawn on.
|
|
22
|
+
const autoId = React.useId();
|
|
23
|
+
const baseId = id ?? autoId;
|
|
24
|
+
const listboxId = `${baseId}-listbox`;
|
|
25
|
+
const optionId = (i) => `${baseId}-opt-${i}`;
|
|
18
26
|
const [internal, setInternal] = React.useState(defaultValue);
|
|
19
27
|
const selected = value ?? internal;
|
|
20
28
|
const [open, setOpen] = React.useState(false);
|
|
@@ -34,6 +42,9 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
|
|
|
34
42
|
const enabledIndexes = filtered
|
|
35
43
|
.map((o, i) => (o.disabled ? -1 : i))
|
|
36
44
|
.filter((i) => i >= 0);
|
|
45
|
+
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
46
|
+
? optionId(highlight)
|
|
47
|
+
: undefined;
|
|
37
48
|
const openMenu = () => {
|
|
38
49
|
if (disabled)
|
|
39
50
|
return;
|
|
@@ -133,12 +144,12 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
|
|
|
133
144
|
document.addEventListener("pointerdown", onDown);
|
|
134
145
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
135
146
|
}, [open]);
|
|
136
|
-
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : (_jsx("span", { className: "select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "select-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => {
|
|
147
|
+
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [_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, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : (_jsx("span", { className: "select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { id: listboxId, className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "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) => {
|
|
137
148
|
setQuery(e.target.value);
|
|
138
149
|
setHighlight(0);
|
|
139
150
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "select-empty", children: "No results" })) : (filtered.map((opt, i) => {
|
|
140
151
|
const isSel = opt.value === selected;
|
|
141
|
-
return (_jsxs("button", { type: "button", role: "option", "aria-selected": isSel, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
|
|
152
|
+
return (_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: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
|
|
142
153
|
}))] }))] }));
|
|
143
154
|
}
|
|
144
155
|
Select.displayName = "Select";
|
package/dist/esm/tokens/scale.js
CHANGED
|
@@ -43,8 +43,12 @@ export const size = {
|
|
|
43
43
|
tableRow: "36px", // data-grid row height
|
|
44
44
|
tableHeader: "40px", // data-grid header row height
|
|
45
45
|
header: "48px", // workspace-switcher header height
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// Attio's date picker, measured: a 32px day with a 2px gap on both axes, so
|
|
47
|
+
// the pitch is 34. The gap is expressed as the cell being 2px taller than the
|
|
48
|
+
// highlight rather than as a grid gap, because crisp's range bridge spans
|
|
49
|
+
// whole cells and a column gap would cut it.
|
|
50
|
+
calendarCell: "34px", // calendar day pitch (32px day + 2px gap)
|
|
51
|
+
calendarPill: "32px", // the day highlight itself
|
|
48
52
|
};
|
|
49
53
|
// Typography — Attio's UI runs entirely on Inter. Sizes measured in the sidebar
|
|
50
54
|
// collapse onto 11/12/13/14/16 (lg = 14px is the default body/label size);
|
package/dist/native/tokens.js
CHANGED
package/dist/styles.css
CHANGED
|
@@ -108,8 +108,8 @@
|
|
|
108
108
|
--size-tableRow: 36px;
|
|
109
109
|
--size-tableHeader: 40px;
|
|
110
110
|
--size-header: 48px;
|
|
111
|
-
--size-calendarCell:
|
|
112
|
-
--size-calendarPill:
|
|
111
|
+
--size-calendarCell: 34px;
|
|
112
|
+
--size-calendarPill: 32px;
|
|
113
113
|
--text-xs: 11px;
|
|
114
114
|
--text-sm: 12px;
|
|
115
115
|
--text-md: 13px;
|
|
@@ -571,18 +571,18 @@
|
|
|
571
571
|
.datepicker {
|
|
572
572
|
|
|
573
573
|
position: relative; box-sizing: border-box; width: 100%; min-width: 0;
|
|
574
|
-
font-family: var(--font-sans); color: var(--fg-
|
|
574
|
+
font-family: var(--font-sans); color: var(--fg-primary); background: var(--bg-surface);
|
|
575
575
|
|
|
576
576
|
& .datepicker-header {
|
|
577
577
|
display: flex; align-items: center; justify-content: space-between;
|
|
578
|
-
height: var(--size-
|
|
578
|
+
height: var(--size-menuRow);
|
|
579
579
|
}
|
|
580
580
|
& .datepicker-header-label {
|
|
581
581
|
display: inline-flex; align-items: center; gap: var(--space-1);
|
|
582
582
|
height: 100%; min-width: 0; border: 0; padding: 0 var(--space-1);
|
|
583
583
|
border-radius: var(--radius-md); background: transparent; cursor: pointer;
|
|
584
584
|
color: var(--fg-primary); font-family: inherit;
|
|
585
|
-
font-size: var(--text-
|
|
585
|
+
font-size: var(--text-lg); line-height: 20px; font-weight: var(--weight-medium);
|
|
586
586
|
white-space: nowrap;
|
|
587
587
|
outline: 2px solid transparent; outline-offset: -2px;
|
|
588
588
|
transition: outline-color .2s ease;
|
|
@@ -611,16 +611,16 @@
|
|
|
611
611
|
& .datepicker-weekday {
|
|
612
612
|
display: flex; align-items: center; justify-content: center;
|
|
613
613
|
height: var(--size-calendarCell); color: var(--fg-neutral-subtle);
|
|
614
|
-
font-size: var(--text-
|
|
614
|
+
font-size: var(--text-sm); line-height: 16px; font-weight: var(--weight-medium);
|
|
615
615
|
}
|
|
616
616
|
|
|
617
617
|
& .datepicker-months { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; min-width: 0; }
|
|
618
618
|
& .datepicker-months[data-wheel-open] { visibility: hidden; }
|
|
619
619
|
& .datepicker-month { min-width: 0; }
|
|
620
620
|
& .datepicker-month-label {
|
|
621
|
-
display: flex; align-items: center; height:
|
|
622
|
-
color: var(--fg-primary); font-size: var(--text-
|
|
623
|
-
font-weight: var(--weight-
|
|
621
|
+
display: flex; align-items: center; height: 24px;
|
|
622
|
+
color: var(--fg-primary); font-size: var(--text-lg); line-height: 20px;
|
|
623
|
+
font-weight: var(--weight-medium);
|
|
624
624
|
}
|
|
625
625
|
|
|
626
626
|
& .datepicker-grid { display: grid; min-width: 0; }
|
|
@@ -659,27 +659,27 @@
|
|
|
659
659
|
min-height: var(--size-calendarCell); min-width: 0;
|
|
660
660
|
align-items: flex-start; justify-content: center;
|
|
661
661
|
border: 0; padding: 0; background: transparent; cursor: pointer;
|
|
662
|
-
color: var(--fg-
|
|
662
|
+
color: var(--fg-primary); font: inherit; outline: none;
|
|
663
663
|
isolation: isolate;
|
|
664
664
|
}
|
|
665
665
|
/* the round day highlight, behind the numeral */
|
|
666
666
|
& .datepicker-date::before {
|
|
667
667
|
content: ""; position: absolute; z-index: -1; top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); left: 50%;
|
|
668
668
|
width: var(--size-calendarPill); height: var(--size-calendarPill);
|
|
669
|
-
border-radius: var(--radius-
|
|
669
|
+
border-radius: var(--radius-md);
|
|
670
670
|
background: transparent; transform: translateX(-50%);
|
|
671
671
|
}
|
|
672
672
|
& .datepicker-date[data-today]::before { background: var(--bg-neutral-weak); }
|
|
673
|
-
& .datepicker-date[data-selected]::before { background: var(--bg-
|
|
674
|
-
& .datepicker-date[data-selected] { color: var(--fg-on-
|
|
675
|
-
& .datepicker-date[data-in-range] { color: var(--fg-
|
|
673
|
+
& .datepicker-date[data-selected]::before { background: var(--bg-brand-solid); }
|
|
674
|
+
& .datepicker-date[data-selected] { color: var(--fg-on-brand); }
|
|
675
|
+
& .datepicker-date[data-in-range] { color: var(--fg-primary); }
|
|
676
676
|
& .datepicker-date[data-in-range]::before { background: transparent; }
|
|
677
677
|
& .datepicker-date[data-unavailable] { color: var(--fg-disabled); }
|
|
678
678
|
& .datepicker-date:disabled { color: var(--fg-disabled); cursor: not-allowed; }
|
|
679
679
|
& .datepicker-date:focus-visible::after {
|
|
680
680
|
content: ""; position: absolute; top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); left: 50%;
|
|
681
681
|
width: var(--size-calendarPill); height: var(--size-calendarPill);
|
|
682
|
-
transform: translateX(-50%); border-radius: var(--radius-
|
|
682
|
+
transform: translateX(-50%); border-radius: var(--radius-md);
|
|
683
683
|
outline: 2px solid var(--stroke-focus-ring); outline-offset: -2px;
|
|
684
684
|
}
|
|
685
685
|
|
|
@@ -688,7 +688,7 @@
|
|
|
688
688
|
min-height: var(--size-calendarCell); min-width: 0; box-sizing: border-box;
|
|
689
689
|
flex-direction: column; align-items: center; gap: 3px;
|
|
690
690
|
padding-top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); color: inherit;
|
|
691
|
-
font-size: var(--text-
|
|
691
|
+
font-size: var(--text-lg); line-height: 20px; font-weight: var(--weight-medium);
|
|
692
692
|
font-variant-numeric: tabular-nums;
|
|
693
693
|
}
|
|
694
694
|
/* The numeral is its own 42px flex box so it centres on the 42px pill.
|
|
@@ -703,7 +703,7 @@
|
|
|
703
703
|
/* the year/month wheel, laid over the grid it replaces */
|
|
704
704
|
& .datepicker-wheel-container {
|
|
705
705
|
position: absolute; z-index: 1; inset-inline: 0;
|
|
706
|
-
top: var(--size-
|
|
706
|
+
top: var(--size-menuRow); bottom: 0;
|
|
707
707
|
display: flex; align-items: center; overflow: hidden;
|
|
708
708
|
background: var(--bg-surface);
|
|
709
709
|
}
|
|
@@ -728,8 +728,8 @@
|
|
|
728
728
|
}
|
|
729
729
|
.datepicker--visibleRange_twoMonths {
|
|
730
730
|
& .datepicker-months { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--space-6); }
|
|
731
|
-
& .datepicker-two-month-header { display: flex; align-items: center; justify-content: space-between; height: var(--size-
|
|
732
|
-
& .datepicker-two-month-label { color: var(--fg-primary); font-size: var(--text-
|
|
731
|
+
& .datepicker-two-month-header { display: flex; align-items: center; justify-content: space-between; height: var(--size-menuRow); }
|
|
732
|
+
& .datepicker-two-month-label { color: var(--fg-primary); font-size: var(--text-lg); line-height: 20px; font-weight: var(--weight-medium); }
|
|
733
733
|
}
|
|
734
734
|
.datepicker--visibleRange_week {
|
|
735
735
|
|
package/dist/tokens.css
CHANGED
|
@@ -108,8 +108,8 @@
|
|
|
108
108
|
--size-tableRow: 36px;
|
|
109
109
|
--size-tableHeader: 40px;
|
|
110
110
|
--size-header: 48px;
|
|
111
|
-
--size-calendarCell:
|
|
112
|
-
--size-calendarPill:
|
|
111
|
+
--size-calendarCell: 34px;
|
|
112
|
+
--size-calendarPill: 32px;
|
|
113
113
|
--text-xs: 11px;
|
|
114
114
|
--text-sm: 12px;
|
|
115
115
|
--text-md: 13px;
|
package/dist/tokens.json
CHANGED
|
@@ -28,8 +28,8 @@ export declare const size: {
|
|
|
28
28
|
readonly tableRow: "36px";
|
|
29
29
|
readonly tableHeader: "40px";
|
|
30
30
|
readonly header: "48px";
|
|
31
|
-
readonly calendarCell: "
|
|
32
|
-
readonly calendarPill: "
|
|
31
|
+
readonly calendarCell: "34px";
|
|
32
|
+
readonly calendarPill: "32px";
|
|
33
33
|
};
|
|
34
34
|
export declare const font: {
|
|
35
35
|
readonly sans: '"Inter", system-ui, -apple-system, "Segoe UI", sans-serif';
|