@crisp-ui-kit/crisp 0.37.0 → 0.38.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/badge.css +5 -5
- package/dist/badge.layered.css +5 -5
- package/dist/cjs/components/badge/badge.recipe.js +11 -5
- package/dist/cjs/components/fileupload/fileupload.js +2 -2
- package/dist/cjs/components/multiselect/multiselect.js +3 -3
- package/dist/cjs/components/multiselect/multiselect.recipe.js +13 -2
- package/dist/cjs/components/select/select.js +3 -3
- package/dist/cjs/components/select/select.recipe.js +14 -1
- package/dist/cjs/tokens/palette.js +23 -0
- package/dist/cjs/tokens/semantic.js +7 -0
- package/dist/esm/components/badge/badge.recipe.js +11 -5
- package/dist/esm/components/fileupload/fileupload.js +2 -2
- package/dist/esm/components/multiselect/multiselect.js +3 -3
- package/dist/esm/components/multiselect/multiselect.recipe.js +13 -2
- package/dist/esm/components/select/select.js +3 -3
- package/dist/esm/components/select/select.recipe.js +14 -1
- package/dist/esm/tokens/palette.js +23 -0
- package/dist/esm/tokens/semantic.js +7 -0
- package/dist/multiselect.css +6 -0
- package/dist/multiselect.d.ts +2 -2
- package/dist/multiselect.layered.css +6 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +24 -4
- package/dist/select.css +6 -0
- package/dist/select.d.ts +2 -1
- package/dist/select.layered.css +6 -0
- package/dist/styles.css +27 -5
- package/dist/styles.layered.css +27 -5
- package/dist/tokens.css +10 -0
- package/dist/tokens.json +24 -4
- package/dist/types/components/fileupload/fileupload.d.ts +1 -1
- package/dist/types/components/multiselect/multiselect.d.ts +3 -1
- package/dist/types/components/multiselect/multiselect.recipe.d.ts +18 -3
- package/dist/types/components/select/select.d.ts +4 -1
- package/dist/types/components/select/select.recipe.d.ts +12 -0
- package/dist/types/product/sidebar/index.d.ts +1 -1
- package/dist/types/tokens/contract.d.ts +5 -0
- package/dist/types/tokens/palette.d.ts +20 -0
- package/dist/types/tokens/semantic.d.ts +7 -0
- package/package.json +1 -1
package/dist/badge.css
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
& .crisp-badge-dot { width: 6px; height: 6px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: currentColor; }
|
|
4
4
|
}
|
|
5
5
|
.crisp-badge--tone_neutral {
|
|
6
|
-
background: var(--crisp-bg-raised); color: var(--crisp-fg-
|
|
6
|
+
background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-soft); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
7
7
|
}
|
|
8
8
|
.crisp-badge--tone_brand {
|
|
9
|
-
background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-
|
|
9
|
+
background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-fg-brand-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent);
|
|
10
10
|
}
|
|
11
11
|
.crisp-badge--tone_success {
|
|
12
|
-
background: color-mix(in srgb, var(--crisp-bg-success) 14%, transparent); color: var(--crisp-
|
|
12
|
+
background: color-mix(in srgb, var(--crisp-bg-success) 14%, transparent); color: var(--crisp-fg-success-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-success) 30%, transparent);
|
|
13
13
|
}
|
|
14
14
|
.crisp-badge--tone_warning {
|
|
15
|
-
background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); color: var(--crisp-
|
|
15
|
+
background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); color: var(--crisp-fg-warning-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-warning) 32%, transparent);
|
|
16
16
|
}
|
|
17
17
|
.crisp-badge--tone_danger {
|
|
18
|
-
background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-
|
|
18
|
+
background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-fg-danger-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
|
|
19
19
|
}
|
|
20
20
|
.crisp-badge--size_small {
|
|
21
21
|
height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
|
package/dist/badge.layered.css
CHANGED
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
& .crisp-badge-dot { width: 6px; height: 6px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: currentColor; }
|
|
5
5
|
}
|
|
6
6
|
.crisp-badge--tone_neutral {
|
|
7
|
-
background: var(--crisp-bg-raised); color: var(--crisp-fg-
|
|
7
|
+
background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-soft); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
8
8
|
}
|
|
9
9
|
.crisp-badge--tone_brand {
|
|
10
|
-
background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-
|
|
10
|
+
background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-fg-brand-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent);
|
|
11
11
|
}
|
|
12
12
|
.crisp-badge--tone_success {
|
|
13
|
-
background: color-mix(in srgb, var(--crisp-bg-success) 14%, transparent); color: var(--crisp-
|
|
13
|
+
background: color-mix(in srgb, var(--crisp-bg-success) 14%, transparent); color: var(--crisp-fg-success-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-success) 30%, transparent);
|
|
14
14
|
}
|
|
15
15
|
.crisp-badge--tone_warning {
|
|
16
|
-
background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); color: var(--crisp-
|
|
16
|
+
background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); color: var(--crisp-fg-warning-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-warning) 32%, transparent);
|
|
17
17
|
}
|
|
18
18
|
.crisp-badge--tone_danger {
|
|
19
|
-
background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-
|
|
19
|
+
background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-fg-danger-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
|
|
20
20
|
}
|
|
21
21
|
.crisp-badge--size_small {
|
|
22
22
|
height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
|
|
@@ -32,29 +32,35 @@ const spec = (0, core_1.defineSpec)({
|
|
|
32
32
|
],
|
|
33
33
|
variants: {
|
|
34
34
|
tone: {
|
|
35
|
+
// #323: the soft text colour is a dedicated fg*Soft token, not the
|
|
36
|
+
// tone's own accent (fgMuted/bgBrandSolid/bgSuccess/bgWarning/bgDanger)
|
|
37
|
+
// — reusing the accent as soft text is exactly what measured below AA
|
|
38
|
+
// (2.97-4.40 in light; brand also failed in dark, 3.83). Background and
|
|
39
|
+
// ring are unchanged: they were never the problem, and neither is a
|
|
40
|
+
// measured Attio value that moving would help — only the text is.
|
|
35
41
|
neutral: {
|
|
36
42
|
background: tokens_1.contract.bgRaised,
|
|
37
|
-
color: tokens_1.contract.
|
|
43
|
+
color: tokens_1.contract.fgNeutralSoft,
|
|
38
44
|
boxShadow: `inset 0 0 0 1px ${tokens_1.contract.borderDefault}`,
|
|
39
45
|
},
|
|
40
46
|
brand: {
|
|
41
47
|
background: soft(tokens_1.contract.bgBrandSolid, 14),
|
|
42
|
-
color: tokens_1.contract.
|
|
48
|
+
color: tokens_1.contract.fgBrandSoft,
|
|
43
49
|
boxShadow: ring(tokens_1.contract.bgBrandSolid, 30),
|
|
44
50
|
},
|
|
45
51
|
success: {
|
|
46
52
|
background: soft(tokens_1.contract.bgSuccess, 14),
|
|
47
|
-
color: tokens_1.contract.
|
|
53
|
+
color: tokens_1.contract.fgSuccessSoft,
|
|
48
54
|
boxShadow: ring(tokens_1.contract.bgSuccess, 30),
|
|
49
55
|
},
|
|
50
56
|
warning: {
|
|
51
57
|
background: soft(tokens_1.contract.bgWarning, 16),
|
|
52
|
-
color: tokens_1.contract.
|
|
58
|
+
color: tokens_1.contract.fgWarningSoft,
|
|
53
59
|
boxShadow: ring(tokens_1.contract.bgWarning, 32),
|
|
54
60
|
},
|
|
55
61
|
danger: {
|
|
56
62
|
background: soft(tokens_1.contract.bgDanger, 14),
|
|
57
|
-
color: tokens_1.contract.
|
|
63
|
+
color: tokens_1.contract.fgDangerSoft,
|
|
58
64
|
boxShadow: ring(tokens_1.contract.bgDanger, 30),
|
|
59
65
|
},
|
|
60
66
|
},
|
|
@@ -73,7 +73,7 @@ function formatSize(bytes) {
|
|
|
73
73
|
* is theirs by construction. Controlled or uncontrolled, the same way every
|
|
74
74
|
* other crisp component is.
|
|
75
75
|
*/
|
|
76
|
-
function FileUpload({ className, labels, "aria-label": ariaLabel, accept, disabled: disabledProp, required, invalid: invalidProp, readOnly, maxFiles, maxFileSize, minFileSize, name, validate, preventDocumentDrop, onFileReject, onFileAccept, acceptedFileEntries: acceptedFileEntriesProp, defaultAcceptedFileEntries, onAcceptedFileEntriesChange, ...rest }) {
|
|
76
|
+
function FileUpload({ className, labels, "aria-label": ariaLabel, id, accept, disabled: disabledProp, required, invalid: invalidProp, readOnly, maxFiles, maxFileSize, minFileSize, name, validate, preventDocumentDrop, onFileReject, onFileAccept, acceptedFileEntries: acceptedFileEntriesProp, defaultAcceptedFileEntries, onAcceptedFileEntriesChange, ...rest }) {
|
|
77
77
|
const props = {
|
|
78
78
|
accept,
|
|
79
79
|
disabled: disabledProp,
|
|
@@ -124,7 +124,7 @@ function FileUpload({ className, labels, "aria-label": ariaLabel, accept, disabl
|
|
|
124
124
|
// focus should land next.
|
|
125
125
|
const rootRef = React.useRef(null);
|
|
126
126
|
const focusAfterRemoval = (0, core_1.useFocusAfterRemoval)(rootRef, ".crisp-fileupload-remove", dropzoneRef);
|
|
127
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, ...rest, className: (0, fileupload_recipe_1.fileuploadRecipe)({ className }), children: [(0, jsx_runtime_1.jsxs)("button", { ...dropzoneProps, ref: dropzoneRef, type: "button", className: "crisp-fileupload-zone", tabIndex: disabled ? -1 : 0, "aria-label": ariaLabel ?? t.hint, disabled: disabled, "data-dragging": dragging ? "" : undefined, "data-disabled": disabled ? "" : undefined, "data-invalid": invalid ? "" : undefined, onClick: () => openFilePicker(), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-title", children: t.title }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-hint", children: t.hint })] }), (0, jsx_runtime_1.jsx)("input", { ...hiddenInputProps, ref: inputRef, className: "crisp-fileupload-input" }), acceptedFileEntries.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: "crisp-fileupload-list", children: acceptedFileEntries.map((entry, i) => ((0, jsx_runtime_1.jsxs)("li", { className: "crisp-fileupload-item", "data-status": entry.status, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-item-name", children: entry.file.name }), entry.status === "uploading" && entry.progress != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-progress", role: "progressbar", "aria-valuenow": Math.round(entry.progress), "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": entry.file.name, children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-progress-fill", style: { width: `${entry.progress}%` } }) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-item-size", children: formatSize(entry.file.size) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-fileupload-remove", "aria-label": `${t.remove}: ${entry.file.name}`, onClick: () => {
|
|
127
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, ...rest, className: (0, fileupload_recipe_1.fileuploadRecipe)({ className }), children: [(0, jsx_runtime_1.jsxs)("button", { ...dropzoneProps, ref: dropzoneRef, type: "button", className: "crisp-fileupload-zone", tabIndex: disabled ? -1 : 0, "aria-label": ariaLabel ?? t.hint, disabled: disabled, "data-dragging": dragging ? "" : undefined, "data-disabled": disabled ? "" : undefined, "data-invalid": invalid ? "" : undefined, onClick: () => openFilePicker(), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-title", children: t.title }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-hint", children: t.hint })] }), (0, jsx_runtime_1.jsx)("input", { ...hiddenInputProps, id: id ?? hiddenInputProps.id, ref: inputRef, className: "crisp-fileupload-input" }), acceptedFileEntries.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: "crisp-fileupload-list", children: acceptedFileEntries.map((entry, i) => ((0, jsx_runtime_1.jsxs)("li", { className: "crisp-fileupload-item", "data-status": entry.status, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-item-name", children: entry.file.name }), entry.status === "uploading" && entry.progress != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-progress", role: "progressbar", "aria-valuenow": Math.round(entry.progress), "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": entry.file.name, children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-progress-fill", style: { width: `${entry.progress}%` } }) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-fileupload-item-size", children: formatSize(entry.file.size) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-fileupload-remove", "aria-label": `${t.remove}: ${entry.file.name}`, onClick: () => {
|
|
128
128
|
focusAfterRemoval(i);
|
|
129
129
|
removeFileEntry(entry.id);
|
|
130
130
|
}, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2 }) })] }, entry.id))) })), (0, jsx_runtime_1.jsx)("ul", { className: "crisp-fileupload-errors", "aria-live": "polite", children: rejections.map((r) => ((0, jsx_runtime_1.jsx)("li", { className: "crisp-fileupload-error", children: `${r.file.name}: ${r.errors.map((e) => t.errors[e] ?? e).join(" ")}` }, r.file.name))) })] }));
|
|
@@ -69,7 +69,7 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, n
|
|
|
69
69
|
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
70
70
|
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
71
71
|
// above this control named nothing (#247). Select already did this.
|
|
72
|
-
id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
72
|
+
id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, invalid, className, "aria-label": ariaLabel, ...rest }) {
|
|
73
73
|
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
74
74
|
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
75
75
|
// styling hook only -- a screen reader cannot see it.
|
|
@@ -182,8 +182,8 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
182
182
|
document.addEventListener("pointerdown", onDown);
|
|
183
183
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
184
184
|
}, [open]);
|
|
185
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [name &&
|
|
186
|
-
(value.length > 0 ? value : required ? [""] : []).map((v) => ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, id: id, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
185
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ invalid, className }), ...rest, children: [name &&
|
|
186
|
+
(value.length > 0 ? value : required ? [""] : []).map((v) => ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, id: id, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
187
187
|
if (open && !searchable)
|
|
188
188
|
onNavKeys(e);
|
|
189
189
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
@@ -62,8 +62,19 @@ const config = (0, core_1.defineRecipe)({
|
|
|
62
62
|
& .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, ${tokens_1.contract.bgCyan} 18%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgCyan} 82%, ${tokens_1.contract.fgPrimary}); }
|
|
63
63
|
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${tokens_1.contract.bgOrange} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgOrange} 82%, ${tokens_1.contract.fgPrimary}); }
|
|
64
64
|
`,
|
|
65
|
-
variants: {
|
|
66
|
-
|
|
65
|
+
variants: {
|
|
66
|
+
// Same ring Select's `invalid` draws — see that recipe's comment.
|
|
67
|
+
invalid: {
|
|
68
|
+
true: `
|
|
69
|
+
& .crisp-multiselect-trigger,
|
|
70
|
+
& .crisp-multiselect-trigger:hover,
|
|
71
|
+
& .crisp-multiselect-trigger[data-open],
|
|
72
|
+
& .crisp-multiselect-trigger:focus-visible { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgDanger}; }
|
|
73
|
+
`,
|
|
74
|
+
false: ``,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
defaultVariants: { invalid: false },
|
|
67
78
|
compoundVariants: [],
|
|
68
79
|
});
|
|
69
80
|
exports.multiselectRecipe = Object.assign((0, core_1.resolveRecipe)(config), {
|
|
@@ -52,7 +52,7 @@ function optionText(o) {
|
|
|
52
52
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
53
53
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
54
54
|
*/
|
|
55
|
-
function Select({ options, value, defaultValue, onValueChange, name, required, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
55
|
+
function Select({ options, value, defaultValue, onValueChange, name, required, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
56
56
|
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
57
57
|
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
58
58
|
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
@@ -177,10 +177,10 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
177
177
|
document.addEventListener("pointerdown", onDown);
|
|
178
178
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
179
179
|
}, [open]);
|
|
180
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [name && (selected || required) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
180
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, invalid, className }), ...rest, children: [name && (selected || required) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
181
181
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
182
182
|
// validation, which is the only reason this input is not hidden
|
|
183
|
-
onChange: () => { } })), (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: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-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) => {
|
|
183
|
+
onChange: () => { } })), (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, "aria-invalid": invalid === true || undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-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) => {
|
|
184
184
|
setQuery(e.target.value);
|
|
185
185
|
setHighlight(0);
|
|
186
186
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
@@ -73,8 +73,21 @@ const config = (0, core_1.defineRecipe)({
|
|
|
73
73
|
full: `width: 100%;`,
|
|
74
74
|
auto: `width: auto;`,
|
|
75
75
|
},
|
|
76
|
+
// Same ring Input's `invalid` draws, adapted to Select's box-shadow-ring
|
|
77
|
+
// trigger (a real <button>, so it needs no ::after overlay the way
|
|
78
|
+
// Input/Textarea do). Outranks hover/open/focus, same reasoning as
|
|
79
|
+
// Input: a field being corrected stays visibly wrong while it is.
|
|
80
|
+
invalid: {
|
|
81
|
+
true: `
|
|
82
|
+
& .crisp-select-trigger,
|
|
83
|
+
& .crisp-select-trigger:hover,
|
|
84
|
+
& .crisp-select-trigger[data-open],
|
|
85
|
+
& .crisp-select-trigger:focus-visible { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgDanger}; }
|
|
86
|
+
`,
|
|
87
|
+
false: ``,
|
|
88
|
+
},
|
|
76
89
|
},
|
|
77
|
-
defaultVariants: { variant: "default", width: "full" },
|
|
90
|
+
defaultVariants: { variant: "default", width: "full", invalid: false },
|
|
78
91
|
compoundVariants: [],
|
|
79
92
|
});
|
|
80
93
|
exports.selectRecipe = Object.assign((0, core_1.resolveRecipe)(config), { config });
|
|
@@ -60,4 +60,27 @@ exports.palette = {
|
|
|
60
60
|
cyan600: { light: "#00b9eb", dark: "#00b9eb" },
|
|
61
61
|
purple600: { light: "#9b69ff", dark: "#9b69ff" },
|
|
62
62
|
graySidebar: { light: "#fbfbfb", dark: "#0e0f10" },
|
|
63
|
+
// Soft Badge's text colour (#323): the *600 tones above, used directly as
|
|
64
|
+
// soft-badge text, fail AA on their own soft tint in light mode (2.97-3.87)
|
|
65
|
+
// and brand fails in dark too (3.83) -- everything else already brightens
|
|
66
|
+
// enough in dark to pass (measured: 5.60-6.77). Attio's own soft tag pills
|
|
67
|
+
// (a company's Categories field, app.attio.com, 2026-09-23) don't reuse the
|
|
68
|
+
// accent colour as text either: they shift it toward black (light) or white
|
|
69
|
+
// (dark) at roughly the same relative luminance regardless of hue --
|
|
70
|
+
// measured 5 different category colours in dark mode (L 0.61-0.75) and two
|
|
71
|
+
// in light (L 0.08-0.10), a tight enough cluster to generalise the SHIFT,
|
|
72
|
+
// not the exact colour, to crisp's own four accent hues. Computed to clear
|
|
73
|
+
// AA with the same margin Attio's pills measured at (5.5-6.5:1) against
|
|
74
|
+
// crisp's EXISTING soft-tint background -- the tint itself is unchanged.
|
|
75
|
+
// Dark values that already passed keep their *600 dark value unchanged.
|
|
76
|
+
softBrandText: { light: "#2057bd", dark: "#568df3" },
|
|
77
|
+
softSuccessText: { light: "#186b3d", dark: "#2ea968" },
|
|
78
|
+
softWarningText: { light: "#80520d", dark: "#e0982a" },
|
|
79
|
+
softDangerText: { light: "#a33539", dark: "#ff5a5f" },
|
|
80
|
+
// Same reasoning, for Badge's neutral tone: fg-muted on bg-raised measures
|
|
81
|
+
// 4.40 in light (just under AA) and 6.77 in dark (already fine). Unlike
|
|
82
|
+
// fg-muted/bg-raised themselves (system-wide, measured from Attio, kept
|
|
83
|
+
// as-is per build/spec-check.mjs's own note), this token exists only for
|
|
84
|
+
// Badge's soft-neutral text, so darkening it doesn't move any other surface.
|
|
85
|
+
softNeutralText: { light: "#686870", dark: "#9a9aa2" },
|
|
63
86
|
};
|
|
@@ -54,4 +54,11 @@ exports.semantic = {
|
|
|
54
54
|
// #fbfbfb — the subtle raised fill; the active tab pill uses the same
|
|
55
55
|
// value as the sidebar, so it shares the palette entry under a neutral name.
|
|
56
56
|
bgSubtle: "graySidebar",
|
|
57
|
+
/** Soft Badge's text colour (#323) — see palette.ts's comment on the
|
|
58
|
+
* soft*Text entries for the Attio measurement this is derived from. */
|
|
59
|
+
fgBrandSoft: "softBrandText",
|
|
60
|
+
fgSuccessSoft: "softSuccessText",
|
|
61
|
+
fgWarningSoft: "softWarningText",
|
|
62
|
+
fgDangerSoft: "softDangerText",
|
|
63
|
+
fgNeutralSoft: "softNeutralText",
|
|
57
64
|
};
|
|
@@ -29,29 +29,35 @@ const spec = defineSpec({
|
|
|
29
29
|
],
|
|
30
30
|
variants: {
|
|
31
31
|
tone: {
|
|
32
|
+
// #323: the soft text colour is a dedicated fg*Soft token, not the
|
|
33
|
+
// tone's own accent (fgMuted/bgBrandSolid/bgSuccess/bgWarning/bgDanger)
|
|
34
|
+
// — reusing the accent as soft text is exactly what measured below AA
|
|
35
|
+
// (2.97-4.40 in light; brand also failed in dark, 3.83). Background and
|
|
36
|
+
// ring are unchanged: they were never the problem, and neither is a
|
|
37
|
+
// measured Attio value that moving would help — only the text is.
|
|
32
38
|
neutral: {
|
|
33
39
|
background: c.bgRaised,
|
|
34
|
-
color: c.
|
|
40
|
+
color: c.fgNeutralSoft,
|
|
35
41
|
boxShadow: `inset 0 0 0 1px ${c.borderDefault}`,
|
|
36
42
|
},
|
|
37
43
|
brand: {
|
|
38
44
|
background: soft(c.bgBrandSolid, 14),
|
|
39
|
-
color: c.
|
|
45
|
+
color: c.fgBrandSoft,
|
|
40
46
|
boxShadow: ring(c.bgBrandSolid, 30),
|
|
41
47
|
},
|
|
42
48
|
success: {
|
|
43
49
|
background: soft(c.bgSuccess, 14),
|
|
44
|
-
color: c.
|
|
50
|
+
color: c.fgSuccessSoft,
|
|
45
51
|
boxShadow: ring(c.bgSuccess, 30),
|
|
46
52
|
},
|
|
47
53
|
warning: {
|
|
48
54
|
background: soft(c.bgWarning, 16),
|
|
49
|
-
color: c.
|
|
55
|
+
color: c.fgWarningSoft,
|
|
50
56
|
boxShadow: ring(c.bgWarning, 32),
|
|
51
57
|
},
|
|
52
58
|
danger: {
|
|
53
59
|
background: soft(c.bgDanger, 14),
|
|
54
|
-
color: c.
|
|
60
|
+
color: c.fgDangerSoft,
|
|
55
61
|
boxShadow: ring(c.bgDanger, 30),
|
|
56
62
|
},
|
|
57
63
|
},
|
|
@@ -36,7 +36,7 @@ function formatSize(bytes) {
|
|
|
36
36
|
* is theirs by construction. Controlled or uncontrolled, the same way every
|
|
37
37
|
* other crisp component is.
|
|
38
38
|
*/
|
|
39
|
-
export function FileUpload({ className, labels, "aria-label": ariaLabel, accept, disabled: disabledProp, required, invalid: invalidProp, readOnly, maxFiles, maxFileSize, minFileSize, name, validate, preventDocumentDrop, onFileReject, onFileAccept, acceptedFileEntries: acceptedFileEntriesProp, defaultAcceptedFileEntries, onAcceptedFileEntriesChange, ...rest }) {
|
|
39
|
+
export function FileUpload({ className, labels, "aria-label": ariaLabel, id, accept, disabled: disabledProp, required, invalid: invalidProp, readOnly, maxFiles, maxFileSize, minFileSize, name, validate, preventDocumentDrop, onFileReject, onFileAccept, acceptedFileEntries: acceptedFileEntriesProp, defaultAcceptedFileEntries, onAcceptedFileEntriesChange, ...rest }) {
|
|
40
40
|
const props = {
|
|
41
41
|
accept,
|
|
42
42
|
disabled: disabledProp,
|
|
@@ -87,7 +87,7 @@ export function FileUpload({ className, labels, "aria-label": ariaLabel, accept,
|
|
|
87
87
|
// focus should land next.
|
|
88
88
|
const rootRef = React.useRef(null);
|
|
89
89
|
const focusAfterRemoval = useFocusAfterRemoval(rootRef, ".crisp-fileupload-remove", dropzoneRef);
|
|
90
|
-
return (_jsxs("div", { ref: rootRef, ...rest, className: fileuploadRecipe({ className }), children: [_jsxs("button", { ...dropzoneProps, ref: dropzoneRef, type: "button", className: "crisp-fileupload-zone", tabIndex: disabled ? -1 : 0, "aria-label": ariaLabel ?? t.hint, disabled: disabled, "data-dragging": dragging ? "" : undefined, "data-disabled": disabled ? "" : undefined, "data-invalid": invalid ? "" : undefined, onClick: () => openFilePicker(), children: [_jsx("span", { className: "crisp-fileupload-title", children: t.title }), _jsx("span", { className: "crisp-fileupload-hint", children: t.hint })] }), _jsx("input", { ...hiddenInputProps, ref: inputRef, className: "crisp-fileupload-input" }), acceptedFileEntries.length > 0 && (_jsx("ul", { className: "crisp-fileupload-list", children: acceptedFileEntries.map((entry, i) => (_jsxs("li", { className: "crisp-fileupload-item", "data-status": entry.status, children: [_jsx("span", { className: "crisp-fileupload-item-name", children: entry.file.name }), entry.status === "uploading" && entry.progress != null && (_jsx("span", { className: "crisp-fileupload-progress", role: "progressbar", "aria-valuenow": Math.round(entry.progress), "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": entry.file.name, children: _jsx("span", { className: "crisp-fileupload-progress-fill", style: { width: `${entry.progress}%` } }) })), _jsx("span", { className: "crisp-fileupload-item-size", children: formatSize(entry.file.size) }), _jsx("button", { type: "button", className: "crisp-fileupload-remove", "aria-label": `${t.remove}: ${entry.file.name}`, onClick: () => {
|
|
90
|
+
return (_jsxs("div", { ref: rootRef, ...rest, className: fileuploadRecipe({ className }), children: [_jsxs("button", { ...dropzoneProps, ref: dropzoneRef, type: "button", className: "crisp-fileupload-zone", tabIndex: disabled ? -1 : 0, "aria-label": ariaLabel ?? t.hint, disabled: disabled, "data-dragging": dragging ? "" : undefined, "data-disabled": disabled ? "" : undefined, "data-invalid": invalid ? "" : undefined, onClick: () => openFilePicker(), children: [_jsx("span", { className: "crisp-fileupload-title", children: t.title }), _jsx("span", { className: "crisp-fileupload-hint", children: t.hint })] }), _jsx("input", { ...hiddenInputProps, id: id ?? hiddenInputProps.id, ref: inputRef, className: "crisp-fileupload-input" }), acceptedFileEntries.length > 0 && (_jsx("ul", { className: "crisp-fileupload-list", children: acceptedFileEntries.map((entry, i) => (_jsxs("li", { className: "crisp-fileupload-item", "data-status": entry.status, children: [_jsx("span", { className: "crisp-fileupload-item-name", children: entry.file.name }), entry.status === "uploading" && entry.progress != null && (_jsx("span", { className: "crisp-fileupload-progress", role: "progressbar", "aria-valuenow": Math.round(entry.progress), "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": entry.file.name, children: _jsx("span", { className: "crisp-fileupload-progress-fill", style: { width: `${entry.progress}%` } }) })), _jsx("span", { className: "crisp-fileupload-item-size", children: formatSize(entry.file.size) }), _jsx("button", { type: "button", className: "crisp-fileupload-remove", "aria-label": `${t.remove}: ${entry.file.name}`, onClick: () => {
|
|
91
91
|
focusAfterRemoval(i);
|
|
92
92
|
removeFileEntry(entry.id);
|
|
93
93
|
}, children: _jsx(X, { size: 13, strokeWidth: 2 }) })] }, entry.id))) })), _jsx("ul", { className: "crisp-fileupload-errors", "aria-live": "polite", children: rejections.map((r) => (_jsx("li", { className: "crisp-fileupload-error", children: `${r.file.name}: ${r.errors.map((e) => t.errors[e] ?? e).join(" ")}` }, r.file.name))) })] }));
|
|
@@ -33,7 +33,7 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
33
33
|
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
34
34
|
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
35
35
|
// above this control named nothing (#247). Select already did this.
|
|
36
|
-
id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
36
|
+
id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, invalid, className, "aria-label": ariaLabel, ...rest }) {
|
|
37
37
|
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
38
38
|
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
39
39
|
// styling hook only -- a screen reader cannot see it.
|
|
@@ -146,8 +146,8 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
146
146
|
document.addEventListener("pointerdown", onDown);
|
|
147
147
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
148
148
|
}, [open]);
|
|
149
|
-
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [name &&
|
|
150
|
-
(value.length > 0 ? value : required ? [""] : []).map((v) => (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), _jsxs("button", { ref: triggerRef, id: id, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
149
|
+
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ invalid, className }), ...rest, children: [name &&
|
|
150
|
+
(value.length > 0 ? value : required ? [""] : []).map((v) => (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), _jsxs("button", { ref: triggerRef, id: id, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
151
151
|
if (open && !searchable)
|
|
152
152
|
onNavKeys(e);
|
|
153
153
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
@@ -59,8 +59,19 @@ const config = defineRecipe({
|
|
|
59
59
|
& .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, ${c.bgCyan} 18%, transparent); color: color-mix(in srgb, ${c.bgCyan} 82%, ${c.fgPrimary}); }
|
|
60
60
|
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); }
|
|
61
61
|
`,
|
|
62
|
-
variants: {
|
|
63
|
-
|
|
62
|
+
variants: {
|
|
63
|
+
// Same ring Select's `invalid` draws — see that recipe's comment.
|
|
64
|
+
invalid: {
|
|
65
|
+
true: `
|
|
66
|
+
& .crisp-multiselect-trigger,
|
|
67
|
+
& .crisp-multiselect-trigger:hover,
|
|
68
|
+
& .crisp-multiselect-trigger[data-open],
|
|
69
|
+
& .crisp-multiselect-trigger:focus-visible { box-shadow: inset 0 0 0 1px ${c.bgDanger}; }
|
|
70
|
+
`,
|
|
71
|
+
false: ``,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
defaultVariants: { invalid: false },
|
|
64
75
|
compoundVariants: [],
|
|
65
76
|
});
|
|
66
77
|
export const multiselectRecipe = Object.assign(resolveRecipe(config), {
|
|
@@ -16,7 +16,7 @@ function optionText(o) {
|
|
|
16
16
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
17
17
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
18
18
|
*/
|
|
19
|
-
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
19
|
+
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
20
20
|
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
21
21
|
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
22
22
|
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
@@ -141,10 +141,10 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
141
141
|
document.addEventListener("pointerdown", onDown);
|
|
142
142
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
143
143
|
}, [open]);
|
|
144
|
-
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [name && (selected || required) && (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
144
|
+
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",
|
|
145
145
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
146
146
|
// validation, which is the only reason this input is not hidden
|
|
147
|
-
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, 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.label] })) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { 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) => {
|
|
147
|
+
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.label] })) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { 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) => {
|
|
148
148
|
setQuery(e.target.value);
|
|
149
149
|
setHighlight(0);
|
|
150
150
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
@@ -70,8 +70,21 @@ const config = defineRecipe({
|
|
|
70
70
|
full: `width: 100%;`,
|
|
71
71
|
auto: `width: auto;`,
|
|
72
72
|
},
|
|
73
|
+
// Same ring Input's `invalid` draws, adapted to Select's box-shadow-ring
|
|
74
|
+
// trigger (a real <button>, so it needs no ::after overlay the way
|
|
75
|
+
// Input/Textarea do). Outranks hover/open/focus, same reasoning as
|
|
76
|
+
// Input: a field being corrected stays visibly wrong while it is.
|
|
77
|
+
invalid: {
|
|
78
|
+
true: `
|
|
79
|
+
& .crisp-select-trigger,
|
|
80
|
+
& .crisp-select-trigger:hover,
|
|
81
|
+
& .crisp-select-trigger[data-open],
|
|
82
|
+
& .crisp-select-trigger:focus-visible { box-shadow: inset 0 0 0 1px ${c.bgDanger}; }
|
|
83
|
+
`,
|
|
84
|
+
false: ``,
|
|
85
|
+
},
|
|
73
86
|
},
|
|
74
|
-
defaultVariants: { variant: "default", width: "full" },
|
|
87
|
+
defaultVariants: { variant: "default", width: "full", invalid: false },
|
|
75
88
|
compoundVariants: [],
|
|
76
89
|
});
|
|
77
90
|
export const selectRecipe = Object.assign(resolveRecipe(config), { config });
|
|
@@ -57,4 +57,27 @@ export const palette = {
|
|
|
57
57
|
cyan600: { light: "#00b9eb", dark: "#00b9eb" },
|
|
58
58
|
purple600: { light: "#9b69ff", dark: "#9b69ff" },
|
|
59
59
|
graySidebar: { light: "#fbfbfb", dark: "#0e0f10" },
|
|
60
|
+
// Soft Badge's text colour (#323): the *600 tones above, used directly as
|
|
61
|
+
// soft-badge text, fail AA on their own soft tint in light mode (2.97-3.87)
|
|
62
|
+
// and brand fails in dark too (3.83) -- everything else already brightens
|
|
63
|
+
// enough in dark to pass (measured: 5.60-6.77). Attio's own soft tag pills
|
|
64
|
+
// (a company's Categories field, app.attio.com, 2026-09-23) don't reuse the
|
|
65
|
+
// accent colour as text either: they shift it toward black (light) or white
|
|
66
|
+
// (dark) at roughly the same relative luminance regardless of hue --
|
|
67
|
+
// measured 5 different category colours in dark mode (L 0.61-0.75) and two
|
|
68
|
+
// in light (L 0.08-0.10), a tight enough cluster to generalise the SHIFT,
|
|
69
|
+
// not the exact colour, to crisp's own four accent hues. Computed to clear
|
|
70
|
+
// AA with the same margin Attio's pills measured at (5.5-6.5:1) against
|
|
71
|
+
// crisp's EXISTING soft-tint background -- the tint itself is unchanged.
|
|
72
|
+
// Dark values that already passed keep their *600 dark value unchanged.
|
|
73
|
+
softBrandText: { light: "#2057bd", dark: "#568df3" },
|
|
74
|
+
softSuccessText: { light: "#186b3d", dark: "#2ea968" },
|
|
75
|
+
softWarningText: { light: "#80520d", dark: "#e0982a" },
|
|
76
|
+
softDangerText: { light: "#a33539", dark: "#ff5a5f" },
|
|
77
|
+
// Same reasoning, for Badge's neutral tone: fg-muted on bg-raised measures
|
|
78
|
+
// 4.40 in light (just under AA) and 6.77 in dark (already fine). Unlike
|
|
79
|
+
// fg-muted/bg-raised themselves (system-wide, measured from Attio, kept
|
|
80
|
+
// as-is per build/spec-check.mjs's own note), this token exists only for
|
|
81
|
+
// Badge's soft-neutral text, so darkening it doesn't move any other surface.
|
|
82
|
+
softNeutralText: { light: "#686870", dark: "#9a9aa2" },
|
|
60
83
|
};
|
|
@@ -51,4 +51,11 @@ export const semantic = {
|
|
|
51
51
|
// #fbfbfb — the subtle raised fill; the active tab pill uses the same
|
|
52
52
|
// value as the sidebar, so it shares the palette entry under a neutral name.
|
|
53
53
|
bgSubtle: "graySidebar",
|
|
54
|
+
/** Soft Badge's text colour (#323) — see palette.ts's comment on the
|
|
55
|
+
* soft*Text entries for the Attio measurement this is derived from. */
|
|
56
|
+
fgBrandSoft: "softBrandText",
|
|
57
|
+
fgSuccessSoft: "softSuccessText",
|
|
58
|
+
fgWarningSoft: "softWarningText",
|
|
59
|
+
fgDangerSoft: "softDangerText",
|
|
60
|
+
fgNeutralSoft: "softNeutralText",
|
|
54
61
|
};
|
package/dist/multiselect.css
CHANGED
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
& .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, var(--crisp-bg-cyan) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-cyan) 82%, var(--crisp-fg-primary)); }
|
|
31
31
|
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
|
|
32
32
|
}
|
|
33
|
+
.crisp-multiselect--invalid_true {
|
|
34
|
+
& .crisp-multiselect-trigger, & .crisp-multiselect-trigger:hover, & .crisp-multiselect-trigger[data-open], & .crisp-multiselect-trigger:focus-visible { box-shadow: inset 0 0 0 1px var(--crisp-bg-danger); }
|
|
35
|
+
}
|
|
36
|
+
.crisp-multiselect--invalid_false {
|
|
37
|
+
|
|
38
|
+
}
|
|
33
39
|
|
|
34
40
|
@media (prefers-reduced-motion: reduce) {
|
|
35
41
|
.crisp-multiselect, .crisp-multiselect * {
|
package/dist/multiselect.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RecipeFn } from "../core/types";
|
|
2
2
|
|
|
3
3
|
export interface MultiselectVariants {
|
|
4
|
-
|
|
4
|
+
invalid?: boolean;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export declare const multiselectRecipe: RecipeFn<{
|
|
7
|
+
export declare const multiselectRecipe: RecipeFn<{ invalid: { true: string; false: string } }>;
|
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
& .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, var(--crisp-bg-cyan) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-cyan) 82%, var(--crisp-fg-primary)); }
|
|
32
32
|
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
|
|
33
33
|
}
|
|
34
|
+
.crisp-multiselect--invalid_true {
|
|
35
|
+
& .crisp-multiselect-trigger, & .crisp-multiselect-trigger:hover, & .crisp-multiselect-trigger[data-open], & .crisp-multiselect-trigger:focus-visible { box-shadow: inset 0 0 0 1px var(--crisp-bg-danger); }
|
|
36
|
+
}
|
|
37
|
+
.crisp-multiselect--invalid_false {
|
|
38
|
+
|
|
39
|
+
}
|
|
34
40
|
|
|
35
41
|
@media (prefers-reduced-motion: reduce) {
|
|
36
42
|
.crisp-multiselect, .crisp-multiselect * {
|
package/dist/native/tokens.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The same tokens the CSS custom properties carry, resolved per theme, with
|
|
3
3
|
// dimensions as numbers for React Native and Lynx style objects.
|
|
4
4
|
export type Theme = "light" | "dark";
|
|
5
|
-
export type ColorTokens = Record<"bgBrandSolid" | "bgBrandSolidPressed" | "fgOnBrand" | "fgOnTone" | "bgSurface" | "bgRaised" | "borderDefault" | "borderSubtle" | "ringNeutral" | "bgTileActive" | "shadowTileActive" | "bgOverlayFrost" | "shadowOverlay1" | "shadowOverlay2" | "shadowOverlay3" | "bgPopover" | "ringPopover" | "shadowPopover1" | "shadowPopover2" | "shadowPopover3" | "fgPrimary" | "fgMuted" | "bgDanger" | "bgDangerSolid" | "strokeFieldFocus" | "fgDisabled" | "bgNeutralWeak" | "bgNeutralInverted" | "fgOnNeutralInverted" | "fgNeutralSubtle" | "fgNeutralMuted" | "strokeFocusRing" | "bgSuccess" | "bgWarning" | "bgOrange" | "bgCyan" | "bgPurple" | "bgSidebar" | "bgSubtle", string>;
|
|
5
|
+
export type ColorTokens = Record<"bgBrandSolid" | "bgBrandSolidPressed" | "fgOnBrand" | "fgOnTone" | "bgSurface" | "bgRaised" | "borderDefault" | "borderSubtle" | "ringNeutral" | "bgTileActive" | "shadowTileActive" | "bgOverlayFrost" | "shadowOverlay1" | "shadowOverlay2" | "shadowOverlay3" | "bgPopover" | "ringPopover" | "shadowPopover1" | "shadowPopover2" | "shadowPopover3" | "fgPrimary" | "fgMuted" | "bgDanger" | "bgDangerSolid" | "strokeFieldFocus" | "fgDisabled" | "bgNeutralWeak" | "bgNeutralInverted" | "fgOnNeutralInverted" | "fgNeutralSubtle" | "fgNeutralMuted" | "strokeFocusRing" | "bgSuccess" | "bgWarning" | "bgOrange" | "bgCyan" | "bgPurple" | "bgSidebar" | "bgSubtle" | "fgBrandSoft" | "fgSuccessSoft" | "fgWarningSoft" | "fgDangerSoft" | "fgNeutralSoft", string>;
|
|
6
6
|
export declare const tokens: {
|
|
7
7
|
color: Record<Theme, ColorTokens>;
|
|
8
8
|
palette: Record<Theme, Record<string, string>>;
|
package/dist/native/tokens.js
CHANGED
|
@@ -42,7 +42,12 @@ export const tokens = {
|
|
|
42
42
|
"bgCyan": "#00b9eb",
|
|
43
43
|
"bgPurple": "#9b69ff",
|
|
44
44
|
"bgSidebar": "#fbfbfb",
|
|
45
|
-
"bgSubtle": "#fbfbfb"
|
|
45
|
+
"bgSubtle": "#fbfbfb",
|
|
46
|
+
"fgBrandSoft": "#2057bd",
|
|
47
|
+
"fgSuccessSoft": "#186b3d",
|
|
48
|
+
"fgWarningSoft": "#80520d",
|
|
49
|
+
"fgDangerSoft": "#a33539",
|
|
50
|
+
"fgNeutralSoft": "#686870"
|
|
46
51
|
},
|
|
47
52
|
"dark": {
|
|
48
53
|
"bgBrandSolid": "#266df0",
|
|
@@ -83,7 +88,12 @@ export const tokens = {
|
|
|
83
88
|
"bgCyan": "#00b9eb",
|
|
84
89
|
"bgPurple": "#9b69ff",
|
|
85
90
|
"bgSidebar": "#0e0f10",
|
|
86
|
-
"bgSubtle": "#0e0f10"
|
|
91
|
+
"bgSubtle": "#0e0f10",
|
|
92
|
+
"fgBrandSoft": "#568df3",
|
|
93
|
+
"fgSuccessSoft": "#2ea968",
|
|
94
|
+
"fgWarningSoft": "#e0982a",
|
|
95
|
+
"fgDangerSoft": "#ff5a5f",
|
|
96
|
+
"fgNeutralSoft": "#9a9aa2"
|
|
87
97
|
}
|
|
88
98
|
},
|
|
89
99
|
"palette": {
|
|
@@ -125,7 +135,12 @@ export const tokens = {
|
|
|
125
135
|
"orange600": "#f97514",
|
|
126
136
|
"cyan600": "#00b9eb",
|
|
127
137
|
"purple600": "#9b69ff",
|
|
128
|
-
"graySidebar": "#fbfbfb"
|
|
138
|
+
"graySidebar": "#fbfbfb",
|
|
139
|
+
"softBrandText": "#2057bd",
|
|
140
|
+
"softSuccessText": "#186b3d",
|
|
141
|
+
"softWarningText": "#80520d",
|
|
142
|
+
"softDangerText": "#a33539",
|
|
143
|
+
"softNeutralText": "#686870"
|
|
129
144
|
},
|
|
130
145
|
"dark": {
|
|
131
146
|
"gray00": "#0a0a0a",
|
|
@@ -165,7 +180,12 @@ export const tokens = {
|
|
|
165
180
|
"orange600": "#f97514",
|
|
166
181
|
"cyan600": "#00b9eb",
|
|
167
182
|
"purple600": "#9b69ff",
|
|
168
|
-
"graySidebar": "#0e0f10"
|
|
183
|
+
"graySidebar": "#0e0f10",
|
|
184
|
+
"softBrandText": "#568df3",
|
|
185
|
+
"softSuccessText": "#2ea968",
|
|
186
|
+
"softWarningText": "#e0982a",
|
|
187
|
+
"softDangerText": "#ff5a5f",
|
|
188
|
+
"softNeutralText": "#9a9aa2"
|
|
169
189
|
}
|
|
170
190
|
},
|
|
171
191
|
"space": {
|
package/dist/select.css
CHANGED
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
.crisp-select--width_auto {
|
|
38
38
|
width: auto;
|
|
39
39
|
}
|
|
40
|
+
.crisp-select--invalid_true {
|
|
41
|
+
& .crisp-select-trigger, & .crisp-select-trigger:hover, & .crisp-select-trigger[data-open], & .crisp-select-trigger:focus-visible { box-shadow: inset 0 0 0 1px var(--crisp-bg-danger); }
|
|
42
|
+
}
|
|
43
|
+
.crisp-select--invalid_false {
|
|
44
|
+
|
|
45
|
+
}
|
|
40
46
|
|
|
41
47
|
@media (prefers-reduced-motion: reduce) {
|
|
42
48
|
.crisp-select, .crisp-select * {
|
package/dist/select.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { RecipeFn } from "../core/types";
|
|
|
3
3
|
export interface SelectVariants {
|
|
4
4
|
variant?: "default" | "ghost";
|
|
5
5
|
width?: "full" | "auto";
|
|
6
|
+
invalid?: boolean;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
export declare const selectRecipe: RecipeFn<{ variant: { default: string; ghost: string }; width: { full: string; auto: string } }>;
|
|
9
|
+
export declare const selectRecipe: RecipeFn<{ variant: { default: string; ghost: string }; width: { full: string; auto: string }; invalid: { true: string; false: string } }>;
|
package/dist/select.layered.css
CHANGED
|
@@ -38,6 +38,12 @@
|
|
|
38
38
|
.crisp-select--width_auto {
|
|
39
39
|
width: auto;
|
|
40
40
|
}
|
|
41
|
+
.crisp-select--invalid_true {
|
|
42
|
+
& .crisp-select-trigger, & .crisp-select-trigger:hover, & .crisp-select-trigger[data-open], & .crisp-select-trigger:focus-visible { box-shadow: inset 0 0 0 1px var(--crisp-bg-danger); }
|
|
43
|
+
}
|
|
44
|
+
.crisp-select--invalid_false {
|
|
45
|
+
|
|
46
|
+
}
|
|
41
47
|
|
|
42
48
|
@media (prefers-reduced-motion: reduce) {
|
|
43
49
|
.crisp-select, .crisp-select * {
|
package/dist/styles.css
CHANGED
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
--crisp-bg-purple: #9b69ff;
|
|
39
39
|
--crisp-bg-sidebar: #fbfbfb;
|
|
40
40
|
--crisp-bg-subtle: #fbfbfb;
|
|
41
|
+
--crisp-fg-brand-soft: #2057bd;
|
|
42
|
+
--crisp-fg-success-soft: #186b3d;
|
|
43
|
+
--crisp-fg-warning-soft: #80520d;
|
|
44
|
+
--crisp-fg-danger-soft: #a33539;
|
|
45
|
+
--crisp-fg-neutral-soft: #686870;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
[data-theme="dark"] {
|
|
@@ -80,6 +85,11 @@
|
|
|
80
85
|
--crisp-bg-purple: #9b69ff;
|
|
81
86
|
--crisp-bg-sidebar: #0e0f10;
|
|
82
87
|
--crisp-bg-subtle: #0e0f10;
|
|
88
|
+
--crisp-fg-brand-soft: #568df3;
|
|
89
|
+
--crisp-fg-success-soft: #2ea968;
|
|
90
|
+
--crisp-fg-warning-soft: #e0982a;
|
|
91
|
+
--crisp-fg-danger-soft: #ff5a5f;
|
|
92
|
+
--crisp-fg-neutral-soft: #9a9aa2;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
:root {
|
|
@@ -196,19 +206,19 @@
|
|
|
196
206
|
& .crisp-badge-dot { width: 6px; height: 6px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: currentColor; }
|
|
197
207
|
}
|
|
198
208
|
.crisp-badge--tone_neutral {
|
|
199
|
-
background: var(--crisp-bg-raised); color: var(--crisp-fg-
|
|
209
|
+
background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-soft); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
200
210
|
}
|
|
201
211
|
.crisp-badge--tone_brand {
|
|
202
|
-
background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-
|
|
212
|
+
background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-fg-brand-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent);
|
|
203
213
|
}
|
|
204
214
|
.crisp-badge--tone_success {
|
|
205
|
-
background: color-mix(in srgb, var(--crisp-bg-success) 14%, transparent); color: var(--crisp-
|
|
215
|
+
background: color-mix(in srgb, var(--crisp-bg-success) 14%, transparent); color: var(--crisp-fg-success-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-success) 30%, transparent);
|
|
206
216
|
}
|
|
207
217
|
.crisp-badge--tone_warning {
|
|
208
|
-
background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); color: var(--crisp-
|
|
218
|
+
background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); color: var(--crisp-fg-warning-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-warning) 32%, transparent);
|
|
209
219
|
}
|
|
210
220
|
.crisp-badge--tone_danger {
|
|
211
|
-
background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-
|
|
221
|
+
background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-fg-danger-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
|
|
212
222
|
}
|
|
213
223
|
.crisp-badge--size_small {
|
|
214
224
|
height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
|
|
@@ -1034,6 +1044,12 @@
|
|
|
1034
1044
|
& .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, var(--crisp-bg-cyan) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-cyan) 82%, var(--crisp-fg-primary)); }
|
|
1035
1045
|
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
|
|
1036
1046
|
}
|
|
1047
|
+
.crisp-multiselect--invalid_true {
|
|
1048
|
+
& .crisp-multiselect-trigger, & .crisp-multiselect-trigger:hover, & .crisp-multiselect-trigger[data-open], & .crisp-multiselect-trigger:focus-visible { box-shadow: inset 0 0 0 1px var(--crisp-bg-danger); }
|
|
1049
|
+
}
|
|
1050
|
+
.crisp-multiselect--invalid_false {
|
|
1051
|
+
|
|
1052
|
+
}
|
|
1037
1053
|
|
|
1038
1054
|
@media (prefers-reduced-motion: reduce) {
|
|
1039
1055
|
.crisp-multiselect, .crisp-multiselect * {
|
|
@@ -1246,6 +1262,12 @@
|
|
|
1246
1262
|
.crisp-select--width_auto {
|
|
1247
1263
|
width: auto;
|
|
1248
1264
|
}
|
|
1265
|
+
.crisp-select--invalid_true {
|
|
1266
|
+
& .crisp-select-trigger, & .crisp-select-trigger:hover, & .crisp-select-trigger[data-open], & .crisp-select-trigger:focus-visible { box-shadow: inset 0 0 0 1px var(--crisp-bg-danger); }
|
|
1267
|
+
}
|
|
1268
|
+
.crisp-select--invalid_false {
|
|
1269
|
+
|
|
1270
|
+
}
|
|
1249
1271
|
|
|
1250
1272
|
@media (prefers-reduced-motion: reduce) {
|
|
1251
1273
|
.crisp-select, .crisp-select * {
|
package/dist/styles.layered.css
CHANGED
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
--crisp-bg-purple: #9b69ff;
|
|
39
39
|
--crisp-bg-sidebar: #fbfbfb;
|
|
40
40
|
--crisp-bg-subtle: #fbfbfb;
|
|
41
|
+
--crisp-fg-brand-soft: #2057bd;
|
|
42
|
+
--crisp-fg-success-soft: #186b3d;
|
|
43
|
+
--crisp-fg-warning-soft: #80520d;
|
|
44
|
+
--crisp-fg-danger-soft: #a33539;
|
|
45
|
+
--crisp-fg-neutral-soft: #686870;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
[data-theme="dark"] {
|
|
@@ -80,6 +85,11 @@
|
|
|
80
85
|
--crisp-bg-purple: #9b69ff;
|
|
81
86
|
--crisp-bg-sidebar: #0e0f10;
|
|
82
87
|
--crisp-bg-subtle: #0e0f10;
|
|
88
|
+
--crisp-fg-brand-soft: #568df3;
|
|
89
|
+
--crisp-fg-success-soft: #2ea968;
|
|
90
|
+
--crisp-fg-warning-soft: #e0982a;
|
|
91
|
+
--crisp-fg-danger-soft: #ff5a5f;
|
|
92
|
+
--crisp-fg-neutral-soft: #9a9aa2;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
:root {
|
|
@@ -197,19 +207,19 @@
|
|
|
197
207
|
& .crisp-badge-dot { width: 6px; height: 6px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: currentColor; }
|
|
198
208
|
}
|
|
199
209
|
.crisp-badge--tone_neutral {
|
|
200
|
-
background: var(--crisp-bg-raised); color: var(--crisp-fg-
|
|
210
|
+
background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-soft); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
|
|
201
211
|
}
|
|
202
212
|
.crisp-badge--tone_brand {
|
|
203
|
-
background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-
|
|
213
|
+
background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-fg-brand-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent);
|
|
204
214
|
}
|
|
205
215
|
.crisp-badge--tone_success {
|
|
206
|
-
background: color-mix(in srgb, var(--crisp-bg-success) 14%, transparent); color: var(--crisp-
|
|
216
|
+
background: color-mix(in srgb, var(--crisp-bg-success) 14%, transparent); color: var(--crisp-fg-success-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-success) 30%, transparent);
|
|
207
217
|
}
|
|
208
218
|
.crisp-badge--tone_warning {
|
|
209
|
-
background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); color: var(--crisp-
|
|
219
|
+
background: color-mix(in srgb, var(--crisp-bg-warning) 16%, transparent); color: var(--crisp-fg-warning-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-warning) 32%, transparent);
|
|
210
220
|
}
|
|
211
221
|
.crisp-badge--tone_danger {
|
|
212
|
-
background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-
|
|
222
|
+
background: color-mix(in srgb, var(--crisp-bg-danger) 14%, transparent); color: var(--crisp-fg-danger-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
|
|
213
223
|
}
|
|
214
224
|
.crisp-badge--size_small {
|
|
215
225
|
height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
|
|
@@ -1035,6 +1045,12 @@
|
|
|
1035
1045
|
& .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, var(--crisp-bg-cyan) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-cyan) 82%, var(--crisp-fg-primary)); }
|
|
1036
1046
|
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
|
|
1037
1047
|
}
|
|
1048
|
+
.crisp-multiselect--invalid_true {
|
|
1049
|
+
& .crisp-multiselect-trigger, & .crisp-multiselect-trigger:hover, & .crisp-multiselect-trigger[data-open], & .crisp-multiselect-trigger:focus-visible { box-shadow: inset 0 0 0 1px var(--crisp-bg-danger); }
|
|
1050
|
+
}
|
|
1051
|
+
.crisp-multiselect--invalid_false {
|
|
1052
|
+
|
|
1053
|
+
}
|
|
1038
1054
|
|
|
1039
1055
|
@media (prefers-reduced-motion: reduce) {
|
|
1040
1056
|
.crisp-multiselect, .crisp-multiselect * {
|
|
@@ -1247,6 +1263,12 @@
|
|
|
1247
1263
|
.crisp-select--width_auto {
|
|
1248
1264
|
width: auto;
|
|
1249
1265
|
}
|
|
1266
|
+
.crisp-select--invalid_true {
|
|
1267
|
+
& .crisp-select-trigger, & .crisp-select-trigger:hover, & .crisp-select-trigger[data-open], & .crisp-select-trigger:focus-visible { box-shadow: inset 0 0 0 1px var(--crisp-bg-danger); }
|
|
1268
|
+
}
|
|
1269
|
+
.crisp-select--invalid_false {
|
|
1270
|
+
|
|
1271
|
+
}
|
|
1250
1272
|
|
|
1251
1273
|
@media (prefers-reduced-motion: reduce) {
|
|
1252
1274
|
.crisp-select, .crisp-select * {
|
package/dist/tokens.css
CHANGED
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
--crisp-bg-purple: #9b69ff;
|
|
39
39
|
--crisp-bg-sidebar: #fbfbfb;
|
|
40
40
|
--crisp-bg-subtle: #fbfbfb;
|
|
41
|
+
--crisp-fg-brand-soft: #2057bd;
|
|
42
|
+
--crisp-fg-success-soft: #186b3d;
|
|
43
|
+
--crisp-fg-warning-soft: #80520d;
|
|
44
|
+
--crisp-fg-danger-soft: #a33539;
|
|
45
|
+
--crisp-fg-neutral-soft: #686870;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
[data-theme="dark"] {
|
|
@@ -80,6 +85,11 @@
|
|
|
80
85
|
--crisp-bg-purple: #9b69ff;
|
|
81
86
|
--crisp-bg-sidebar: #0e0f10;
|
|
82
87
|
--crisp-bg-subtle: #0e0f10;
|
|
88
|
+
--crisp-fg-brand-soft: #568df3;
|
|
89
|
+
--crisp-fg-success-soft: #2ea968;
|
|
90
|
+
--crisp-fg-warning-soft: #e0982a;
|
|
91
|
+
--crisp-fg-danger-soft: #ff5a5f;
|
|
92
|
+
--crisp-fg-neutral-soft: #9a9aa2;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
:root {
|
package/dist/tokens.json
CHANGED
|
@@ -39,7 +39,12 @@
|
|
|
39
39
|
"bgCyan": "#00b9eb",
|
|
40
40
|
"bgPurple": "#9b69ff",
|
|
41
41
|
"bgSidebar": "#fbfbfb",
|
|
42
|
-
"bgSubtle": "#fbfbfb"
|
|
42
|
+
"bgSubtle": "#fbfbfb",
|
|
43
|
+
"fgBrandSoft": "#2057bd",
|
|
44
|
+
"fgSuccessSoft": "#186b3d",
|
|
45
|
+
"fgWarningSoft": "#80520d",
|
|
46
|
+
"fgDangerSoft": "#a33539",
|
|
47
|
+
"fgNeutralSoft": "#686870"
|
|
43
48
|
},
|
|
44
49
|
"dark": {
|
|
45
50
|
"bgBrandSolid": "#266df0",
|
|
@@ -80,7 +85,12 @@
|
|
|
80
85
|
"bgCyan": "#00b9eb",
|
|
81
86
|
"bgPurple": "#9b69ff",
|
|
82
87
|
"bgSidebar": "#0e0f10",
|
|
83
|
-
"bgSubtle": "#0e0f10"
|
|
88
|
+
"bgSubtle": "#0e0f10",
|
|
89
|
+
"fgBrandSoft": "#568df3",
|
|
90
|
+
"fgSuccessSoft": "#2ea968",
|
|
91
|
+
"fgWarningSoft": "#e0982a",
|
|
92
|
+
"fgDangerSoft": "#ff5a5f",
|
|
93
|
+
"fgNeutralSoft": "#9a9aa2"
|
|
84
94
|
}
|
|
85
95
|
},
|
|
86
96
|
"palette": {
|
|
@@ -122,7 +132,12 @@
|
|
|
122
132
|
"orange600": "#f97514",
|
|
123
133
|
"cyan600": "#00b9eb",
|
|
124
134
|
"purple600": "#9b69ff",
|
|
125
|
-
"graySidebar": "#fbfbfb"
|
|
135
|
+
"graySidebar": "#fbfbfb",
|
|
136
|
+
"softBrandText": "#2057bd",
|
|
137
|
+
"softSuccessText": "#186b3d",
|
|
138
|
+
"softWarningText": "#80520d",
|
|
139
|
+
"softDangerText": "#a33539",
|
|
140
|
+
"softNeutralText": "#686870"
|
|
126
141
|
},
|
|
127
142
|
"dark": {
|
|
128
143
|
"gray00": "#0a0a0a",
|
|
@@ -162,7 +177,12 @@
|
|
|
162
177
|
"orange600": "#f97514",
|
|
163
178
|
"cyan600": "#00b9eb",
|
|
164
179
|
"purple600": "#9b69ff",
|
|
165
|
-
"graySidebar": "#0e0f10"
|
|
180
|
+
"graySidebar": "#0e0f10",
|
|
181
|
+
"softBrandText": "#568df3",
|
|
182
|
+
"softSuccessText": "#2ea968",
|
|
183
|
+
"softWarningText": "#e0982a",
|
|
184
|
+
"softDangerText": "#ff5a5f",
|
|
185
|
+
"softNeutralText": "#9a9aa2"
|
|
166
186
|
}
|
|
167
187
|
},
|
|
168
188
|
"space": {
|
|
@@ -21,4 +21,4 @@ export type FileUploadProps = UseFileUploadProps & Omit<React.ComponentPropsWith
|
|
|
21
21
|
* is theirs by construction. Controlled or uncontrolled, the same way every
|
|
22
22
|
* other crisp component is.
|
|
23
23
|
*/
|
|
24
|
-
export declare function FileUpload({ className, labels, "aria-label": ariaLabel, accept, disabled: disabledProp, required, invalid: invalidProp, readOnly, maxFiles, maxFileSize, minFileSize, name, validate, preventDocumentDrop, onFileReject, onFileAccept, acceptedFileEntries: acceptedFileEntriesProp, defaultAcceptedFileEntries, onAcceptedFileEntriesChange, ...rest }: FileUploadProps): React.JSX.Element;
|
|
24
|
+
export declare function FileUpload({ className, labels, "aria-label": ariaLabel, id, accept, disabled: disabledProp, required, invalid: invalidProp, readOnly, maxFiles, maxFileSize, minFileSize, name, validate, preventDocumentDrop, onFileReject, onFileAccept, acceptedFileEntries: acceptedFileEntriesProp, defaultAcceptedFileEntries, onAcceptedFileEntriesChange, ...rest }: FileUploadProps): React.JSX.Element;
|
|
@@ -37,6 +37,8 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
|
|
|
37
37
|
/** Shown when the search matches nothing. */
|
|
38
38
|
emptyLabel?: string;
|
|
39
39
|
disabled?: boolean;
|
|
40
|
+
/** Red ring + `aria-invalid`, matching `Input`'s and `Select`'s. */
|
|
41
|
+
invalid?: boolean;
|
|
40
42
|
className?: string;
|
|
41
43
|
"aria-label"?: string;
|
|
42
44
|
};
|
|
@@ -46,7 +48,7 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
|
|
|
46
48
|
* popover with a search field over rows of "[chip] … [checkbox]" that toggle on
|
|
47
49
|
* click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
|
|
48
50
|
*/
|
|
49
|
-
export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, required, id, placeholder, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
|
|
51
|
+
export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, required, id, placeholder, searchable, searchPlaceholder, emptyLabel, disabled, invalid, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
|
|
50
52
|
export declare namespace MultiSelect {
|
|
51
53
|
var displayName: string;
|
|
52
54
|
}
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
declare const config: import("../..").RecipeConfig<{
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
declare const config: import("../..").RecipeConfig<{
|
|
2
|
+
invalid: {
|
|
3
|
+
true: string;
|
|
4
|
+
false: string;
|
|
5
|
+
};
|
|
6
|
+
}>;
|
|
7
|
+
export declare const multiselectRecipe: import("../..").RecipeFn<{
|
|
8
|
+
invalid: {
|
|
9
|
+
true: string;
|
|
10
|
+
false: string;
|
|
11
|
+
};
|
|
12
|
+
}> & {
|
|
13
|
+
config: import("../..").RecipeConfig<{
|
|
14
|
+
invalid: {
|
|
15
|
+
true: string;
|
|
16
|
+
false: string;
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
4
19
|
};
|
|
5
20
|
export default config;
|
|
@@ -42,6 +42,9 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
|
|
|
42
42
|
/** Shown when the search matches nothing. */
|
|
43
43
|
emptyLabel?: string;
|
|
44
44
|
disabled?: boolean;
|
|
45
|
+
/** Red ring + `aria-invalid`, matching `Input`'s. Outranks the open/focus
|
|
46
|
+
* ring — a field being corrected stays visibly wrong while it is. */
|
|
47
|
+
invalid?: boolean;
|
|
45
48
|
className?: string;
|
|
46
49
|
"aria-label"?: string;
|
|
47
50
|
id?: string;
|
|
@@ -53,7 +56,7 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
|
|
|
53
56
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
54
57
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
55
58
|
*/
|
|
56
|
-
export declare function Select({ options, value, defaultValue, onValueChange, name, required, placeholder, variant, width, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
|
|
59
|
+
export declare function Select({ options, value, defaultValue, onValueChange, name, required, placeholder, variant, width, searchable, searchPlaceholder, emptyLabel, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
|
|
57
60
|
export declare namespace Select {
|
|
58
61
|
var displayName: string;
|
|
59
62
|
}
|
|
@@ -7,6 +7,10 @@ declare const config: import("../..").RecipeConfig<{
|
|
|
7
7
|
full: string;
|
|
8
8
|
auto: string;
|
|
9
9
|
};
|
|
10
|
+
invalid: {
|
|
11
|
+
true: string;
|
|
12
|
+
false: string;
|
|
13
|
+
};
|
|
10
14
|
}>;
|
|
11
15
|
export declare const selectRecipe: import("../..").RecipeFn<{
|
|
12
16
|
variant: {
|
|
@@ -17,6 +21,10 @@ export declare const selectRecipe: import("../..").RecipeFn<{
|
|
|
17
21
|
full: string;
|
|
18
22
|
auto: string;
|
|
19
23
|
};
|
|
24
|
+
invalid: {
|
|
25
|
+
true: string;
|
|
26
|
+
false: string;
|
|
27
|
+
};
|
|
20
28
|
}> & {
|
|
21
29
|
config: import("../..").RecipeConfig<{
|
|
22
30
|
variant: {
|
|
@@ -27,6 +35,10 @@ export declare const selectRecipe: import("../..").RecipeFn<{
|
|
|
27
35
|
full: string;
|
|
28
36
|
auto: string;
|
|
29
37
|
};
|
|
38
|
+
invalid: {
|
|
39
|
+
true: string;
|
|
40
|
+
false: string;
|
|
41
|
+
};
|
|
30
42
|
}>;
|
|
31
43
|
};
|
|
32
44
|
export default config;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { ATTIO_SIDEBAR_CHATS_SECTION, ATTIO_SIDEBAR_FOOTER, ATTIO_SIDEBAR_ITEMS, ATTIO_SIDEBAR_LISTS_SECTION, ATTIO_SIDEBAR_RECORDS_SECTION, ATTIO_SIDEBAR_WORKSPACE, Sidebar, type SidebarFooter, type SidebarListsSection, type SidebarNavItem, type SidebarProps, type SidebarRecordItem, type SidebarRecordsSection, type SidebarSection, type SidebarWorkspace, } from "./sidebar";
|
|
1
|
+
export { ATTIO_SIDEBAR_CHATS_SECTION, ATTIO_SIDEBAR_FOOTER, ATTIO_SIDEBAR_ITEMS, ATTIO_SIDEBAR_LISTS_SECTION, ATTIO_SIDEBAR_RECORDS_SECTION, ATTIO_SIDEBAR_WORKSPACE, Sidebar, type SidebarFooter, type SidebarListsSection, type SidebarNavGroup, type SidebarNavGroupItem, type SidebarNavItem, type SidebarProps, type SidebarRecordItem, type SidebarRecordsSection, type SidebarSection, type SidebarWorkspace, } from "./sidebar";
|
|
2
2
|
export { sidebarRecipe } from "./sidebar.recipe";
|
|
@@ -40,6 +40,11 @@ export declare const contract: MapLeafNodes<{
|
|
|
40
40
|
bgPurple: "purple600";
|
|
41
41
|
bgSidebar: "graySidebar";
|
|
42
42
|
bgSubtle: "graySidebar";
|
|
43
|
+
fgBrandSoft: "softBrandText";
|
|
44
|
+
fgSuccessSoft: "softSuccessText";
|
|
45
|
+
fgWarningSoft: "softWarningText";
|
|
46
|
+
fgDangerSoft: "softDangerText";
|
|
47
|
+
fgNeutralSoft: "softNeutralText";
|
|
43
48
|
}, `var(--${string})`> & {
|
|
44
49
|
space: Record<"0.5" | "1" | "1.5" | "2" | "2.5" | "3" | "4" | "5" | "6" | "8" | "px", `var(--${string})`>;
|
|
45
50
|
radius: Record<"2xl" | "chip" | "full" | "lg" | "md" | "sm" | "xl", `var(--${string})`>;
|
|
@@ -151,5 +151,25 @@ export declare const palette: {
|
|
|
151
151
|
readonly light: "#fbfbfb";
|
|
152
152
|
readonly dark: "#0e0f10";
|
|
153
153
|
};
|
|
154
|
+
readonly softBrandText: {
|
|
155
|
+
readonly light: "#2057bd";
|
|
156
|
+
readonly dark: "#568df3";
|
|
157
|
+
};
|
|
158
|
+
readonly softSuccessText: {
|
|
159
|
+
readonly light: "#186b3d";
|
|
160
|
+
readonly dark: "#2ea968";
|
|
161
|
+
};
|
|
162
|
+
readonly softWarningText: {
|
|
163
|
+
readonly light: "#80520d";
|
|
164
|
+
readonly dark: "#e0982a";
|
|
165
|
+
};
|
|
166
|
+
readonly softDangerText: {
|
|
167
|
+
readonly light: "#a33539";
|
|
168
|
+
readonly dark: "#ff5a5f";
|
|
169
|
+
};
|
|
170
|
+
readonly softNeutralText: {
|
|
171
|
+
readonly light: "#686870";
|
|
172
|
+
readonly dark: "#9a9aa2";
|
|
173
|
+
};
|
|
154
174
|
};
|
|
155
175
|
export type PaletteKey = keyof typeof palette;
|
|
@@ -46,5 +46,12 @@ export declare const semantic: {
|
|
|
46
46
|
bgPurple: "purple600";
|
|
47
47
|
bgSidebar: "graySidebar";
|
|
48
48
|
bgSubtle: "graySidebar";
|
|
49
|
+
/** Soft Badge's text colour (#323) — see palette.ts's comment on the
|
|
50
|
+
* soft*Text entries for the Attio measurement this is derived from. */
|
|
51
|
+
fgBrandSoft: "softBrandText";
|
|
52
|
+
fgSuccessSoft: "softSuccessText";
|
|
53
|
+
fgWarningSoft: "softWarningText";
|
|
54
|
+
fgDangerSoft: "softDangerText";
|
|
55
|
+
fgNeutralSoft: "softNeutralText";
|
|
49
56
|
};
|
|
50
57
|
export type Tokens = typeof semantic;
|