@crisp-ui-kit/crisp 0.37.0 → 0.39.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.
Files changed (61) hide show
  1. package/dist/badge.css +5 -5
  2. package/dist/badge.layered.css +5 -5
  3. package/dist/cjs/components/badge/badge.recipe.js +11 -5
  4. package/dist/cjs/components/contextmenu/contextmenu.js +1 -1
  5. package/dist/cjs/components/contextmenu/contextmenu.recipe.js +22 -10
  6. package/dist/cjs/components/fileupload/fileupload.js +2 -2
  7. package/dist/cjs/components/menu/menu.js +1 -1
  8. package/dist/cjs/components/menu/menu.recipe.js +32 -20
  9. package/dist/cjs/components/multiselect/multiselect.js +4 -4
  10. package/dist/cjs/components/multiselect/multiselect.recipe.js +50 -24
  11. package/dist/cjs/components/popover/popover.js +1 -1
  12. package/dist/cjs/components/popover/popover.recipe.js +19 -4
  13. package/dist/cjs/components/select/select.js +3 -3
  14. package/dist/cjs/components/select/select.recipe.js +42 -16
  15. package/dist/cjs/core/floating-portal.js +55 -2
  16. package/dist/cjs/tokens/palette.js +23 -0
  17. package/dist/cjs/tokens/semantic.js +7 -0
  18. package/dist/contextmenu.css +11 -9
  19. package/dist/contextmenu.layered.css +11 -9
  20. package/dist/esm/components/badge/badge.recipe.js +11 -5
  21. package/dist/esm/components/contextmenu/contextmenu.js +1 -1
  22. package/dist/esm/components/contextmenu/contextmenu.recipe.js +22 -10
  23. package/dist/esm/components/fileupload/fileupload.js +2 -2
  24. package/dist/esm/components/menu/menu.js +1 -1
  25. package/dist/esm/components/menu/menu.recipe.js +32 -20
  26. package/dist/esm/components/multiselect/multiselect.js +4 -4
  27. package/dist/esm/components/multiselect/multiselect.recipe.js +50 -24
  28. package/dist/esm/components/popover/popover.js +1 -1
  29. package/dist/esm/components/popover/popover.recipe.js +19 -4
  30. package/dist/esm/components/select/select.js +3 -3
  31. package/dist/esm/components/select/select.recipe.js +42 -16
  32. package/dist/esm/core/floating-portal.js +22 -2
  33. package/dist/esm/tokens/palette.js +23 -0
  34. package/dist/esm/tokens/semantic.js +7 -0
  35. package/dist/menu.css +34 -19
  36. package/dist/menu.layered.css +34 -19
  37. package/dist/multiselect.css +40 -21
  38. package/dist/multiselect.d.ts +2 -2
  39. package/dist/multiselect.layered.css +40 -21
  40. package/dist/native/tokens.d.ts +1 -1
  41. package/dist/native/tokens.js +24 -4
  42. package/dist/popover.css +5 -3
  43. package/dist/popover.layered.css +5 -3
  44. package/dist/select.css +33 -14
  45. package/dist/select.d.ts +2 -1
  46. package/dist/select.layered.css +33 -14
  47. package/dist/styles.css +138 -71
  48. package/dist/styles.layered.css +138 -71
  49. package/dist/tokens.css +10 -0
  50. package/dist/tokens.json +24 -4
  51. package/dist/types/components/fileupload/fileupload.d.ts +1 -1
  52. package/dist/types/components/multiselect/multiselect.d.ts +3 -1
  53. package/dist/types/components/multiselect/multiselect.recipe.d.ts +18 -3
  54. package/dist/types/components/select/select.d.ts +4 -1
  55. package/dist/types/components/select/select.recipe.d.ts +12 -0
  56. package/dist/types/core/floating-portal.d.ts +17 -3
  57. package/dist/types/product/sidebar/index.d.ts +1 -1
  58. package/dist/types/tokens/contract.d.ts +5 -0
  59. package/dist/types/tokens/palette.d.ts +20 -0
  60. package/dist/types/tokens/semantic.d.ts +7 -0
  61. package/package.json +3 -3
@@ -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, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-select-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
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) => {
@@ -8,6 +8,17 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
8
8
  // (Dialog's own measured surface sets both) can capture its position: fixed
9
9
  // box; z-index 70 keeps it above Dialog/Drawer's z-index 60/61 layers, since
10
10
  // it's no longer nested inside whichever one opened it.
11
+ // The listbox and everything inside it live in `globalCss` below, not
12
+ // nested under `&` (#357): nesting compiles to a real descendant combinator
13
+ // scoped to the recipe's root class, which needs actual DOM containment —
14
+ // exactly what the portal to `document.body` breaks. Nested, these rules
15
+ // matched nothing once the listbox moved and it rendered completely
16
+ // unstyled. The trigger and its own parts stay nested under `&` — the
17
+ // trigger is NOT portaled, it stays where it was authored. `font-family`/
18
+ // smoothing are restated on the listbox's own rule (its children pick font
19
+ // up via their own `font-family: inherit`, which still works — that
20
+ // inheritance is intact WITHIN the portaled subtree, only the link back to
21
+ // the trigger-holding root broke).
11
22
  const config = defineRecipe({
12
23
  name: "select",
13
24
  base: `
@@ -28,31 +39,33 @@ const config = defineRecipe({
28
39
  & .crisp-select-placeholder { color: ${c.fgMuted}; }
29
40
  & .crisp-select-chevron { flex-shrink: 0; color: ${c.fgMuted}; transition: transform .15s ease; }
30
41
  & .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
31
-
32
- & .crisp-select-listbox {
42
+ `,
43
+ globalCss: `
44
+ .crisp-select-listbox {
45
+ font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
33
46
  position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
34
47
  max-height: 280px; overflow-y: auto; padding: ${c.space["1"]};
35
48
  border-radius: ${c.radius.xl}; background: ${c.bgPopover};
36
49
  box-shadow: ${e.floating};
37
50
  transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
38
51
  }
39
- & .crisp-select-listbox[data-up] { transform-origin: bottom; }
40
- & .crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
41
- & .crisp-select-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["1"]}) calc(-1 * ${c.space["1"]}) ${c.space["1"]}; padding: 0 ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgMuted}; }
42
- & .crisp-select-search svg { flex-shrink: 0; }
43
- & .crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
44
- & .crisp-select-search input::placeholder { color: ${c.fgMuted}; }
45
- & .crisp-select-empty { padding: ${c.space["3"]} ${c.space["2"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }
46
- & .crisp-select-option {
52
+ .crisp-select-listbox[data-up] { transform-origin: bottom; }
53
+ .crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
54
+ .crisp-select-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["1"]}) calc(-1 * ${c.space["1"]}) ${c.space["1"]}; padding: 0 ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgMuted}; }
55
+ .crisp-select-search svg { flex-shrink: 0; }
56
+ .crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
57
+ .crisp-select-search input::placeholder { color: ${c.fgMuted}; }
58
+ .crisp-select-empty { padding: ${c.space["3"]} ${c.space["2"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }
59
+ .crisp-select-option {
47
60
  box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.menuRow}; flex-shrink: 0;
48
61
  padding: 0 ${c.space["2"]}; margin: 0; border: none; background: none; border-radius: ${c.radius.md};
49
62
  font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: start; cursor: pointer; white-space: nowrap;
50
63
  }
51
- & .crisp-select-option[data-highlighted] { background: ${c.bgRaised}; }
52
- & .crisp-select-option[data-disabled] { opacity: 0.5; cursor: not-allowed; }
53
- & .crisp-select-option-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${c.radius.full}; }
54
- & .crisp-select-option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
55
- & .crisp-select-option-check { flex-shrink: 0; color: ${c.bgBrandSolid}; }
64
+ .crisp-select-option[data-highlighted] { background: ${c.bgRaised}; }
65
+ .crisp-select-option[data-disabled] { opacity: 0.5; cursor: not-allowed; }
66
+ .crisp-select-option-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${c.radius.full}; }
67
+ .crisp-select-option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
68
+ .crisp-select-option-check { flex-shrink: 0; color: ${c.bgBrandSolid}; }
56
69
  `,
57
70
  variants: {
58
71
  // ghost: borderless at rest (transparent), reveals a subtle fill on hover —
@@ -70,8 +83,21 @@ const config = defineRecipe({
70
83
  full: `width: 100%;`,
71
84
  auto: `width: auto;`,
72
85
  },
86
+ // Same ring Input's `invalid` draws, adapted to Select's box-shadow-ring
87
+ // trigger (a real <button>, so it needs no ::after overlay the way
88
+ // Input/Textarea do). Outranks hover/open/focus, same reasoning as
89
+ // Input: a field being corrected stays visibly wrong while it is.
90
+ invalid: {
91
+ true: `
92
+ & .crisp-select-trigger,
93
+ & .crisp-select-trigger:hover,
94
+ & .crisp-select-trigger[data-open],
95
+ & .crisp-select-trigger:focus-visible { box-shadow: inset 0 0 0 1px ${c.bgDanger}; }
96
+ `,
97
+ false: ``,
98
+ },
73
99
  },
74
- defaultVariants: { variant: "default", width: "full" },
100
+ defaultVariants: { variant: "default", width: "full", invalid: false },
75
101
  compoundVariants: [],
76
102
  });
77
103
  export const selectRecipe = Object.assign(resolveRecipe(config), { config });
@@ -1,3 +1,4 @@
1
+ import * as React from "react";
1
2
  import { createPortal } from "react-dom";
2
3
  /**
3
4
  * Portals a floating layer (a `position: fixed` listbox/menu/popover
@@ -19,7 +20,26 @@ import { createPortal } from "react-dom";
19
20
  * starts `false` and is driven only by client interaction — never a prop a
20
21
  * fixture sets true at mount (unlike Dialog/Drawer/Command) — so this never
21
22
  * touches `document` during SSR, the same safety Tooltip already had.
23
+ *
24
+ * `sourceRef` is the caller's own root — still in its original DOM position,
25
+ * never portaled — used to carry a `data-theme` set on some ancestor of it
26
+ * onto the portaled content (#357). `[data-theme="dark"]` is a normal
27
+ * attribute selector: it needs the themed element to be an actual DOM
28
+ * ancestor, which a consumer that scopes theming below `<html>`/`<body>`
29
+ * (crisp's own visual-test harness does exactly this, per case) no longer
30
+ * is once the content moves to `document.body` — the popover rendered in
31
+ * the PAGE's theme instead of the SUBTREE's. Re-declaring the attribute
32
+ * directly on the portaled root re-establishes it for CSS attribute
33
+ * selectors AND for `var()` resolution, since a custom property set under
34
+ * `[data-theme="dark"] { --crisp-x: ... }` only reaches descendants of an
35
+ * element carrying that attribute.
22
36
  */
23
- export function FloatingPortal({ children, }) {
24
- return createPortal(children, document.body);
37
+ export function FloatingPortal({ children, sourceRef, }) {
38
+ const theme = sourceRef.current
39
+ ?.closest("[data-theme]")
40
+ ?.getAttribute("data-theme");
41
+ const content = theme
42
+ ? React.cloneElement(children, { "data-theme": theme })
43
+ : children;
44
+ return createPortal(content, document.body);
25
45
  }
@@ -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/menu.css CHANGED
@@ -1,24 +1,39 @@
1
+
2
+ .crisp-menu-content {
3
+ font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
+ position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
5
+ min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
6
+ border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
7
+ box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3);
8
+ transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
9
+ }
10
+ .crisp-menu-content[data-align="end"] { transform-origin: top right; }
11
+ .crisp-menu-content[data-up] { transform-origin: bottom left; }
12
+ .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
13
+ .crisp-menu-content { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
14
+
15
+ .crisp-menu-item {
16
+ box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; height: var(--crisp-size-menuRow); flex-shrink: 0;
17
+ margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; border-radius: var(--crisp-radius-md);
18
+ font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); text-align: start; cursor: pointer; white-space: nowrap;
19
+ }
20
+ .crisp-menu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
21
+ .crisp-menu-item:focus { outline: none; }
22
+ .crisp-menu-item:hover:not([data-disabled]), .crisp-menu-item:focus-visible:not([data-disabled]), .crisp-menu-item[data-highlighted]:not([data-disabled]) { background: var(--crisp-bg-raised); }
23
+ .crisp-menu-item[data-disabled] { opacity: 0.5; cursor: not-allowed; }
24
+ .crisp-menu-item--danger { color: var(--crisp-bg-danger); }
25
+ .crisp-menu-item--danger svg { color: var(--crisp-bg-danger); }
26
+ .crisp-menu-item--danger:hover, .crisp-menu-item--danger:focus-visible, .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
27
+ .crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
28
+ .crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: var(--crisp-space-3); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); letter-spacing: 0.02em; }
29
+ .crisp-menu-item-check { flex-shrink: 0; color: var(--crisp-bg-brand-solid); }
30
+ .crisp-menu-item-check--placeholder { width: 15px; }
31
+
32
+ .crisp-menu-sep { height: 1px; flex-shrink: 0; border: none; margin: var(--crisp-space-1) calc(-1 * var(--crisp-space-1)); background: var(--crisp-border-subtle); }
33
+ .crisp-menu-label { padding: var(--crisp-space-1-5) var(--crisp-space-2) var(--crisp-space-0-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
34
+
1
35
  .crisp-menu {
2
36
  position: relative; display: inline-flex; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
- & .crisp-menu-content { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; }
4
- & .crisp-menu-content[data-align="end"] { transform-origin: top right; }
5
- & .crisp-menu-content[data-up] { transform-origin: bottom left; }
6
- & .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
7
- & .crisp-menu-content { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
8
- & .crisp-menu-item { box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; height: var(--crisp-size-menuRow); flex-shrink: 0; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; border-radius: var(--crisp-radius-md); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); text-align: start; cursor: pointer; white-space: nowrap; }
9
- & .crisp-menu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
10
- & .crisp-menu-item:focus { outline: none; }
11
- & .crisp-menu-item:hover:not([data-disabled]), & .crisp-menu-item:focus-visible:not([data-disabled]), & .crisp-menu-item[data-highlighted]:not([data-disabled]) { background: var(--crisp-bg-raised); }
12
- & .crisp-menu-item[data-disabled] { opacity: 0.5; cursor: not-allowed; }
13
- & .crisp-menu-item--danger { color: var(--crisp-bg-danger); }
14
- & .crisp-menu-item--danger svg { color: var(--crisp-bg-danger); }
15
- & .crisp-menu-item--danger:hover, & .crisp-menu-item--danger:focus-visible, & .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
16
- & .crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
17
- & .crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: var(--crisp-space-3); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); letter-spacing: 0.02em; }
18
- & .crisp-menu-item-check { flex-shrink: 0; color: var(--crisp-bg-brand-solid); }
19
- & .crisp-menu-item-check--placeholder { width: 15px; }
20
- & .crisp-menu-sep { height: 1px; flex-shrink: 0; border: none; margin: var(--crisp-space-1) calc(-1 * var(--crisp-space-1)); background: var(--crisp-border-subtle); }
21
- & .crisp-menu-label { padding: var(--crisp-space-1-5) var(--crisp-space-2) var(--crisp-space-0-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
22
37
  }
23
38
 
24
39
  @media (prefers-reduced-motion: reduce) {
@@ -1,25 +1,40 @@
1
1
  @layer crisp-components {
2
+
3
+ .crisp-menu-content {
4
+ font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
5
+ position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
6
+ min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
7
+ border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
8
+ box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3);
9
+ transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
10
+ }
11
+ .crisp-menu-content[data-align="end"] { transform-origin: top right; }
12
+ .crisp-menu-content[data-up] { transform-origin: bottom left; }
13
+ .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
14
+ .crisp-menu-content { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
15
+
16
+ .crisp-menu-item {
17
+ box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; height: var(--crisp-size-menuRow); flex-shrink: 0;
18
+ margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; border-radius: var(--crisp-radius-md);
19
+ font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); text-align: start; cursor: pointer; white-space: nowrap;
20
+ }
21
+ .crisp-menu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
22
+ .crisp-menu-item:focus { outline: none; }
23
+ .crisp-menu-item:hover:not([data-disabled]), .crisp-menu-item:focus-visible:not([data-disabled]), .crisp-menu-item[data-highlighted]:not([data-disabled]) { background: var(--crisp-bg-raised); }
24
+ .crisp-menu-item[data-disabled] { opacity: 0.5; cursor: not-allowed; }
25
+ .crisp-menu-item--danger { color: var(--crisp-bg-danger); }
26
+ .crisp-menu-item--danger svg { color: var(--crisp-bg-danger); }
27
+ .crisp-menu-item--danger:hover, .crisp-menu-item--danger:focus-visible, .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
28
+ .crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
29
+ .crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: var(--crisp-space-3); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); letter-spacing: 0.02em; }
30
+ .crisp-menu-item-check { flex-shrink: 0; color: var(--crisp-bg-brand-solid); }
31
+ .crisp-menu-item-check--placeholder { width: 15px; }
32
+
33
+ .crisp-menu-sep { height: 1px; flex-shrink: 0; border: none; margin: var(--crisp-space-1) calc(-1 * var(--crisp-space-1)); background: var(--crisp-border-subtle); }
34
+ .crisp-menu-label { padding: var(--crisp-space-1-5) var(--crisp-space-2) var(--crisp-space-0-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
35
+
2
36
  .crisp-menu {
3
37
  position: relative; display: inline-flex; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
- & .crisp-menu-content { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; }
5
- & .crisp-menu-content[data-align="end"] { transform-origin: top right; }
6
- & .crisp-menu-content[data-up] { transform-origin: bottom left; }
7
- & .crisp-menu-content[data-up][data-align="end"] { transform-origin: bottom right; }
8
- & .crisp-menu-content { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
9
- & .crisp-menu-item { box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; height: var(--crisp-size-menuRow); flex-shrink: 0; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; border-radius: var(--crisp-radius-md); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); text-align: start; cursor: pointer; white-space: nowrap; }
10
- & .crisp-menu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
11
- & .crisp-menu-item:focus { outline: none; }
12
- & .crisp-menu-item:hover:not([data-disabled]), & .crisp-menu-item:focus-visible:not([data-disabled]), & .crisp-menu-item[data-highlighted]:not([data-disabled]) { background: var(--crisp-bg-raised); }
13
- & .crisp-menu-item[data-disabled] { opacity: 0.5; cursor: not-allowed; }
14
- & .crisp-menu-item--danger { color: var(--crisp-bg-danger); }
15
- & .crisp-menu-item--danger svg { color: var(--crisp-bg-danger); }
16
- & .crisp-menu-item--danger:hover, & .crisp-menu-item--danger:focus-visible, & .crisp-menu-item--danger[data-highlighted] { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
17
- & .crisp-menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
18
- & .crisp-menu-item-shortcut { flex-shrink: 0; margin-inline-start: var(--crisp-space-3); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); letter-spacing: 0.02em; }
19
- & .crisp-menu-item-check { flex-shrink: 0; color: var(--crisp-bg-brand-solid); }
20
- & .crisp-menu-item-check--placeholder { width: 15px; }
21
- & .crisp-menu-sep { height: 1px; flex-shrink: 0; border: none; margin: var(--crisp-space-1) calc(-1 * var(--crisp-space-1)); background: var(--crisp-border-subtle); }
22
- & .crisp-menu-label { padding: var(--crisp-space-1-5) var(--crisp-space-2) var(--crisp-space-0-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
23
38
  }
24
39
 
25
40
  @media (prefers-reduced-motion: reduce) {
@@ -1,3 +1,37 @@
1
+
2
+ .crisp-multiselect-listbox {
3
+ font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
+ position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
5
+ max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1);
6
+ border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
7
+ box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3);
8
+ transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
9
+ }
10
+ .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
11
+ .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
12
+ .crisp-multiselect-search { display: flex; align-items: center; gap: var(--crisp-space-2); height: 34px; flex-shrink: 0; margin: calc(-1 * var(--crisp-space-1)) calc(-1 * var(--crisp-space-1)) var(--crisp-space-1); padding: 0 var(--crisp-space-2-5); border-bottom: 1px solid var(--crisp-border-subtle); color: var(--crisp-fg-muted); }
13
+ .crisp-multiselect-search svg { flex-shrink: 0; }
14
+ .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
15
+ .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
16
+ .crisp-multiselect-option {
17
+ box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
18
+ padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md); font-family: inherit; cursor: pointer; text-align: start;
19
+ }
20
+ .crisp-multiselect-option[data-highlighted] { background: var(--crisp-bg-raised); }
21
+ .crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
22
+ .crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: var(--crisp-radius-sm); box-shadow: inset 0 0 0 1.5px var(--crisp-border-default); color: transparent; }
23
+ .crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: var(--crisp-bg-brand-solid); box-shadow: none; color: #fff; }
24
+ .crisp-multiselect-empty { padding: var(--crisp-space-3) var(--crisp-space-2); text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
25
+
26
+ .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
27
+ .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
28
+ .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
29
+ .crisp-recordstable-tag[data-tone="warning"] { background: color-mix(in srgb, var(--crisp-bg-warning) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-warning) 82%, var(--crisp-fg-primary)); }
30
+ .crisp-recordstable-tag[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 15%, transparent); color: color-mix(in srgb, var(--crisp-bg-danger) 80%, var(--crisp-fg-primary)); }
31
+ .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, var(--crisp-bg-purple) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-purple) 80%, var(--crisp-fg-primary)); }
32
+ .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)); }
33
+ .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)); }
34
+
1
35
  .crisp-multiselect {
2
36
  position: relative; display: inline-flex; width: 100%; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
37
  & .crisp-multiselect-trigger { box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; min-width: 0; min-height: var(--crisp-size-menuRow); margin: 0; padding: var(--crisp-space-0-5) var(--crisp-space-1-5) var(--crisp-space-0-5) var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); cursor: pointer; text-align: start; transition: background-color .12s ease, box-shadow .12s ease; }
@@ -8,27 +42,12 @@
8
42
  & .crisp-multiselect-placeholder { color: var(--crisp-fg-muted); }
9
43
  & .crisp-multiselect-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
10
44
  & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
11
- & .crisp-multiselect-listbox { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; }
12
- & .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
13
- & .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
14
- & .crisp-multiselect-search { display: flex; align-items: center; gap: var(--crisp-space-2); height: 34px; flex-shrink: 0; margin: calc(-1 * var(--crisp-space-1)) calc(-1 * var(--crisp-space-1)) var(--crisp-space-1); padding: 0 var(--crisp-space-2-5); border-bottom: 1px solid var(--crisp-border-subtle); color: var(--crisp-fg-muted); }
15
- & .crisp-multiselect-search svg { flex-shrink: 0; }
16
- & .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
17
- & .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
18
- & .crisp-multiselect-option { box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0; padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md); font-family: inherit; cursor: pointer; text-align: start; }
19
- & .crisp-multiselect-option[data-highlighted] { background: var(--crisp-bg-raised); }
20
- & .crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
21
- & .crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: var(--crisp-radius-sm); box-shadow: inset 0 0 0 1.5px var(--crisp-border-default); color: transparent; }
22
- & .crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: var(--crisp-bg-brand-solid); box-shadow: none; color: #fff; }
23
- & .crisp-multiselect-empty { padding: var(--crisp-space-3) var(--crisp-space-2); text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
24
- & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
25
- & .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
26
- & .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
27
- & .crisp-recordstable-tag[data-tone="warning"] { background: color-mix(in srgb, var(--crisp-bg-warning) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-warning) 82%, var(--crisp-fg-primary)); }
28
- & .crisp-recordstable-tag[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 15%, transparent); color: color-mix(in srgb, var(--crisp-bg-danger) 80%, var(--crisp-fg-primary)); }
29
- & .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, var(--crisp-bg-purple) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-purple) 80%, var(--crisp-fg-primary)); }
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
- & .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)); }
45
+ }
46
+ .crisp-multiselect--invalid_true {
47
+ & .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); }
48
+ }
49
+ .crisp-multiselect--invalid_false {
50
+
32
51
  }
33
52
 
34
53
  @media (prefers-reduced-motion: reduce) {
@@ -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 } }>;
@@ -1,4 +1,38 @@
1
1
  @layer crisp-components {
2
+
3
+ .crisp-multiselect-listbox {
4
+ font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
5
+ position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
6
+ max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1);
7
+ border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
8
+ box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3);
9
+ transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
10
+ }
11
+ .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
12
+ .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
13
+ .crisp-multiselect-search { display: flex; align-items: center; gap: var(--crisp-space-2); height: 34px; flex-shrink: 0; margin: calc(-1 * var(--crisp-space-1)) calc(-1 * var(--crisp-space-1)) var(--crisp-space-1); padding: 0 var(--crisp-space-2-5); border-bottom: 1px solid var(--crisp-border-subtle); color: var(--crisp-fg-muted); }
14
+ .crisp-multiselect-search svg { flex-shrink: 0; }
15
+ .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
16
+ .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
17
+ .crisp-multiselect-option {
18
+ box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0;
19
+ padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md); font-family: inherit; cursor: pointer; text-align: start;
20
+ }
21
+ .crisp-multiselect-option[data-highlighted] { background: var(--crisp-bg-raised); }
22
+ .crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
23
+ .crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: var(--crisp-radius-sm); box-shadow: inset 0 0 0 1.5px var(--crisp-border-default); color: transparent; }
24
+ .crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: var(--crisp-bg-brand-solid); box-shadow: none; color: #fff; }
25
+ .crisp-multiselect-empty { padding: var(--crisp-space-3) var(--crisp-space-2); text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
26
+
27
+ .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
28
+ .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
29
+ .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
30
+ .crisp-recordstable-tag[data-tone="warning"] { background: color-mix(in srgb, var(--crisp-bg-warning) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-warning) 82%, var(--crisp-fg-primary)); }
31
+ .crisp-recordstable-tag[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 15%, transparent); color: color-mix(in srgb, var(--crisp-bg-danger) 80%, var(--crisp-fg-primary)); }
32
+ .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, var(--crisp-bg-purple) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-purple) 80%, var(--crisp-fg-primary)); }
33
+ .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)); }
34
+ .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)); }
35
+
2
36
  .crisp-multiselect {
3
37
  position: relative; display: inline-flex; width: 100%; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
38
  & .crisp-multiselect-trigger { box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; min-width: 0; min-height: var(--crisp-size-menuRow); margin: 0; padding: var(--crisp-space-0-5) var(--crisp-space-1-5) var(--crisp-space-0-5) var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); cursor: pointer; text-align: start; transition: background-color .12s ease, box-shadow .12s ease; }
@@ -9,27 +43,12 @@
9
43
  & .crisp-multiselect-placeholder { color: var(--crisp-fg-muted); }
10
44
  & .crisp-multiselect-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
11
45
  & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
12
- & .crisp-multiselect-listbox { position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px); max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; }
13
- & .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
14
- & .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
15
- & .crisp-multiselect-search { display: flex; align-items: center; gap: var(--crisp-space-2); height: 34px; flex-shrink: 0; margin: calc(-1 * var(--crisp-space-1)) calc(-1 * var(--crisp-space-1)) var(--crisp-space-1); padding: 0 var(--crisp-space-2-5); border-bottom: 1px solid var(--crisp-border-subtle); color: var(--crisp-fg-muted); }
16
- & .crisp-multiselect-search svg { flex-shrink: 0; }
17
- & .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
18
- & .crisp-multiselect-search input::placeholder { color: var(--crisp-fg-muted); }
19
- & .crisp-multiselect-option { box-sizing: border-box; display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); flex-shrink: 0; padding: 0 var(--crisp-space-2); margin: 0; border: none; background: none; border-radius: var(--crisp-radius-md); font-family: inherit; cursor: pointer; text-align: start; }
20
- & .crisp-multiselect-option[data-highlighted] { background: var(--crisp-bg-raised); }
21
- & .crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
22
- & .crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: var(--crisp-radius-sm); box-shadow: inset 0 0 0 1.5px var(--crisp-border-default); color: transparent; }
23
- & .crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: var(--crisp-bg-brand-solid); box-shadow: none; color: #fff; }
24
- & .crisp-multiselect-empty { padding: var(--crisp-space-3) var(--crisp-space-2); text-align: center; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
25
- & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
26
- & .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
27
- & .crisp-recordstable-tag[data-tone="success"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 78%, var(--crisp-fg-primary)); }
28
- & .crisp-recordstable-tag[data-tone="warning"] { background: color-mix(in srgb, var(--crisp-bg-warning) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-warning) 82%, var(--crisp-fg-primary)); }
29
- & .crisp-recordstable-tag[data-tone="danger"] { background: color-mix(in srgb, var(--crisp-bg-danger) 15%, transparent); color: color-mix(in srgb, var(--crisp-bg-danger) 80%, var(--crisp-fg-primary)); }
30
- & .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, var(--crisp-bg-purple) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-purple) 80%, var(--crisp-fg-primary)); }
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
- & .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)); }
46
+ }
47
+ .crisp-multiselect--invalid_true {
48
+ & .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); }
49
+ }
50
+ .crisp-multiselect--invalid_false {
51
+
33
52
  }
34
53
 
35
54
  @media (prefers-reduced-motion: reduce) {
@@ -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>>;
@@ -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/popover.css CHANGED
@@ -1,8 +1,10 @@
1
+
2
+ .crisp-popover-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); color: var(--crisp-fg-primary); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transform: none; transition: opacity 0.08s ease, transform 0.08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
3
+ .crisp-popover-content[data-align="end"] { transform-origin: top right; }
4
+ .crisp-popover-content[data-up] { transform-origin: bottom left; }
5
+
1
6
  .crisp-popover {
2
7
  display: contents; font-family: var(--crisp-font-sans);
3
- & .crisp-popover-content { position: fixed; z-index: 70; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); color: var(--crisp-fg-primary); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transform: none; transition: opacity 0.08s ease, transform 0.08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
4
- & .crisp-popover-content[data-align="end"] { transform-origin: top right; }
5
- & .crisp-popover-content[data-up] { transform-origin: bottom left; }
6
8
  }
7
9
 
8
10
  @media (prefers-reduced-motion: reduce) {
@@ -1,9 +1,11 @@
1
1
  @layer crisp-components {
2
+
3
+ .crisp-popover-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); color: var(--crisp-fg-primary); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transform: none; transition: opacity 0.08s ease, transform 0.08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
4
+ .crisp-popover-content[data-align="end"] { transform-origin: top right; }
5
+ .crisp-popover-content[data-up] { transform-origin: bottom left; }
6
+
2
7
  .crisp-popover {
3
8
  display: contents; font-family: var(--crisp-font-sans);
4
- & .crisp-popover-content { position: fixed; z-index: 70; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); color: var(--crisp-fg-primary); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transform: none; transition: opacity 0.08s ease, transform 0.08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
5
- & .crisp-popover-content[data-align="end"] { transform-origin: top right; }
6
- & .crisp-popover-content[data-up] { transform-origin: bottom left; }
7
9
  }
8
10
 
9
11
  @media (prefers-reduced-motion: reduce) {