@crisp-ui-kit/crisp 0.43.0 → 0.45.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 (40) hide show
  1. package/dist/chip.css +27 -0
  2. package/dist/chip.d.ts +7 -0
  3. package/dist/chip.layered.css +29 -0
  4. package/dist/cjs/components/chip/chip.js +58 -0
  5. package/dist/cjs/components/chip/chip.recipe.js +80 -0
  6. package/dist/cjs/components/chip/index.js +7 -0
  7. package/dist/cjs/components/contextmenu/contextmenu.recipe.js +6 -1
  8. package/dist/cjs/components/menu/menu.recipe.js +6 -1
  9. package/dist/cjs/components/multiselect/multiselect.recipe.js +6 -1
  10. package/dist/cjs/components/popover/popover.recipe.js +6 -1
  11. package/dist/cjs/components/select/select.recipe.js +6 -1
  12. package/dist/cjs/index.js +1 -0
  13. package/dist/cjs/product/recordpanel/recordpanel.js +15 -2
  14. package/dist/contextmenu.css +6 -1
  15. package/dist/contextmenu.layered.css +6 -1
  16. package/dist/esm/components/chip/chip.js +22 -0
  17. package/dist/esm/components/chip/chip.recipe.js +77 -0
  18. package/dist/esm/components/chip/index.js +2 -0
  19. package/dist/esm/components/contextmenu/contextmenu.recipe.js +6 -1
  20. package/dist/esm/components/menu/menu.recipe.js +6 -1
  21. package/dist/esm/components/multiselect/multiselect.recipe.js +6 -1
  22. package/dist/esm/components/popover/popover.recipe.js +6 -1
  23. package/dist/esm/components/select/select.recipe.js +6 -1
  24. package/dist/esm/index.js +1 -0
  25. package/dist/esm/product/recordpanel/recordpanel.js +15 -2
  26. package/dist/menu.css +6 -1
  27. package/dist/menu.layered.css +6 -1
  28. package/dist/multiselect.css +6 -1
  29. package/dist/multiselect.layered.css +6 -1
  30. package/dist/popover.css +6 -1
  31. package/dist/popover.layered.css +6 -1
  32. package/dist/select.css +6 -1
  33. package/dist/select.layered.css +6 -1
  34. package/dist/styles.css +58 -5
  35. package/dist/styles.layered.css +58 -5
  36. package/dist/types/components/chip/chip.d.ts +47 -0
  37. package/dist/types/components/chip/chip.recipe.d.ts +100 -0
  38. package/dist/types/components/chip/index.d.ts +2 -0
  39. package/dist/types/index.d.ts +1 -0
  40. package/package.json +6 -1
package/dist/chip.css ADDED
@@ -0,0 +1,27 @@
1
+ .crisp-chip {
2
+ display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding-inline: 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;
3
+ }
4
+ .crisp-chip--tone_neutral {
5
+ background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle);
6
+ }
7
+ .crisp-chip--tone_brand {
8
+ background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent);
9
+ }
10
+ .crisp-chip--tone_success {
11
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-success) 30%, transparent);
12
+ }
13
+ .crisp-chip--tone_warning {
14
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-warning) 30%, transparent);
15
+ }
16
+ .crisp-chip--tone_danger {
17
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
18
+ }
19
+ .crisp-chip--tone_purple {
20
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-purple) 30%, transparent);
21
+ }
22
+ .crisp-chip--tone_cyan {
23
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-cyan) 30%, transparent);
24
+ }
25
+ .crisp-chip--tone_orange {
26
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-orange) 30%, transparent);
27
+ }
package/dist/chip.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface ChipVariants {
4
+ tone?: "neutral" | "brand" | "success" | "warning" | "danger" | "purple" | "cyan" | "orange";
5
+ }
6
+
7
+ export declare const chipRecipe: RecipeFn<{ tone: { neutral: string; brand: string; success: string; warning: string; danger: string; purple: string; cyan: string; orange: string } }>;
@@ -0,0 +1,29 @@
1
+ @layer crisp-components {
2
+ .crisp-chip {
3
+ display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding-inline: 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;
4
+ }
5
+ .crisp-chip--tone_neutral {
6
+ background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle);
7
+ }
8
+ .crisp-chip--tone_brand {
9
+ background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent);
10
+ }
11
+ .crisp-chip--tone_success {
12
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-success) 30%, transparent);
13
+ }
14
+ .crisp-chip--tone_warning {
15
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-warning) 30%, transparent);
16
+ }
17
+ .crisp-chip--tone_danger {
18
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
19
+ }
20
+ .crisp-chip--tone_purple {
21
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-purple) 30%, transparent);
22
+ }
23
+ .crisp-chip--tone_cyan {
24
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-cyan) 30%, transparent);
25
+ }
26
+ .crisp-chip--tone_orange {
27
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-orange) 30%, transparent);
28
+ }
29
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Chip = void 0;
37
+ const jsx_runtime_1 = require("react/jsx-runtime");
38
+ const React = __importStar(require("react"));
39
+ const core_1 = require("../../core");
40
+ const chip_recipe_1 = require("./chip.recipe");
41
+ /**
42
+ * A small pill — the same measured chip RecordsTable, RecordPanel and
43
+ * MultiSelect already draw as `.crisp-recordstable-tag`, exported standalone
44
+ * for surfaces that need one without a table or a fixed option set (e.g. a
45
+ * free-text tag list).
46
+ *
47
+ * This ships the measured body only. Attio's removable × on a chip needs a
48
+ * live, populated multi-value editor to measure against, and none was
49
+ * reachable this round (a person's "Email addresses" field was empty on the
50
+ * only record checked, and "Categories" removes via a checkbox toggle, not
51
+ * an inline ×) — so `Chip` has no `onRemove`/remove button yet. Add one once
52
+ * that surface can be measured; don't guess the icon, its hit target, or its
53
+ * spacing from the label.
54
+ */
55
+ exports.Chip = React.forwardRef(({ tone, className, asChild, children, ...props }, ref) => {
56
+ return ((0, jsx_runtime_1.jsx)(core_1.Primitive.span, { ref: ref, asChild: asChild, className: (0, chip_recipe_1.chipRecipe)({ tone, className }), ...props, children: children }));
57
+ });
58
+ exports.Chip.displayName = "Chip";
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.spec = exports.chipRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // The same measured pill RecordsTable/RecordPanel/MultiSelect already draw
7
+ // as ".crisp-recordstable-tag" (product/recordstable/recordstable.tag.ts's
8
+ // recordTagCss): 22px tall, `padding: 0 4px`, `border-radius: chip`,
9
+ // 14px/20px at weight 500, and a 1px inset ring in the tone's colour. That
10
+ // export is raw CSS written to be spliced under another component's own
11
+ // `&` root (RecordsTable/RecordPanel nest it inside their own recipe), so it
12
+ // can't be reused verbatim here — Chip's root IS the styled element, not a
13
+ // wrapper around one. The values below are transcribed from recordTagCss,
14
+ // not re-measured; keep the two in sync by hand if either changes.
15
+ const ring = (v, pct) => `inset 0 0 0 1px color-mix(in srgb, ${v} ${pct}%, transparent)`;
16
+ const soft = (v, pct) => `color-mix(in srgb, ${v} ${pct}%, transparent)`;
17
+ const onFg = (v, pct) => `color-mix(in srgb, ${v} ${pct}%, ${tokens_1.contract.fgPrimary})`;
18
+ const spec = (0, core_1.defineSpec)({
19
+ name: "chip",
20
+ base: {
21
+ display: "inline-flex",
22
+ alignItems: "center",
23
+ height: tokens_1.contract.size.chip,
24
+ paddingInline: tokens_1.contract.space["1"],
25
+ borderRadius: tokens_1.contract.radius.chip,
26
+ fontSize: tokens_1.contract.text.lg,
27
+ lineHeight: "20px",
28
+ fontWeight: tokens_1.contract.weight.medium,
29
+ whiteSpace: "nowrap",
30
+ },
31
+ variants: {
32
+ tone: {
33
+ neutral: {
34
+ background: soft(tokens_1.contract.fgPrimary, 6),
35
+ color: soft(tokens_1.contract.fgPrimary, 82),
36
+ boxShadow: `inset 0 0 0 1px ${tokens_1.contract.borderSubtle}`,
37
+ },
38
+ brand: {
39
+ background: soft(tokens_1.contract.bgBrandSolid, 14),
40
+ color: onFg(tokens_1.contract.bgBrandSolid, 82),
41
+ boxShadow: ring(tokens_1.contract.bgBrandSolid, 30),
42
+ },
43
+ success: {
44
+ background: soft(tokens_1.contract.bgSuccess, 16),
45
+ color: onFg(tokens_1.contract.bgSuccess, 78),
46
+ boxShadow: ring(tokens_1.contract.bgSuccess, 30),
47
+ },
48
+ warning: {
49
+ background: soft(tokens_1.contract.bgWarning, 18),
50
+ color: onFg(tokens_1.contract.bgWarning, 82),
51
+ boxShadow: ring(tokens_1.contract.bgWarning, 30),
52
+ },
53
+ danger: {
54
+ background: soft(tokens_1.contract.bgDanger, 15),
55
+ color: onFg(tokens_1.contract.bgDanger, 80),
56
+ boxShadow: ring(tokens_1.contract.bgDanger, 30),
57
+ },
58
+ purple: {
59
+ background: soft(tokens_1.contract.bgPurple, 16),
60
+ color: onFg(tokens_1.contract.bgPurple, 80),
61
+ boxShadow: ring(tokens_1.contract.bgPurple, 30),
62
+ },
63
+ cyan: {
64
+ background: soft(tokens_1.contract.bgCyan, 18),
65
+ color: onFg(tokens_1.contract.bgCyan, 82),
66
+ boxShadow: ring(tokens_1.contract.bgCyan, 30),
67
+ },
68
+ orange: {
69
+ background: soft(tokens_1.contract.bgOrange, 16),
70
+ color: onFg(tokens_1.contract.bgOrange, 82),
71
+ boxShadow: ring(tokens_1.contract.bgOrange, 30),
72
+ },
73
+ },
74
+ },
75
+ defaultVariants: { tone: "neutral" },
76
+ });
77
+ exports.spec = spec;
78
+ const config = (0, core_1.specToRecipe)(spec);
79
+ exports.chipRecipe = Object.assign((0, core_1.resolveRecipe)(config), { config });
80
+ exports.default = config;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.chipRecipe = exports.Chip = void 0;
4
+ var chip_1 = require("./chip");
5
+ Object.defineProperty(exports, "Chip", { enumerable: true, get: function () { return chip_1.Chip; } });
6
+ var chip_recipe_1 = require("./chip.recipe");
7
+ Object.defineProperty(exports, "chipRecipe", { enumerable: true, get: function () { return chip_recipe_1.chipRecipe; } });
@@ -21,7 +21,12 @@ const config = (0, core_1.defineRecipe)({
21
21
  name: "contextmenu",
22
22
  base: `display: contents; font-family: ${tokens_1.contract.font.sans};`,
23
23
  globalCss: `
24
- .crisp-contextmenu-content { font-family: ${tokens_1.contract.font.sans}; position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; box-shadow: ${tokens_1.elevation.floating}; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
24
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
25
+ on document.body while open (Zag re-enables it only on layers IT
26
+ tracks), and the portaled content is outside Zag's knowledge -- it
27
+ inherited the lock, unclickable, hit-testing right through to
28
+ whatever real element sat behind it. */
29
+ .crisp-contextmenu-content { font-family: ${tokens_1.contract.font.sans}; position: fixed; z-index: 70; pointer-events: auto; min-width: 200px; box-sizing: border-box; padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; box-shadow: ${tokens_1.elevation.floating}; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
25
30
  .crisp-contextmenu-item { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; width: 100%; height: ${tokens_1.contract.size.menuRow}; box-sizing: border-box; padding: 0 ${tokens_1.contract.space["2"]}; margin: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: none; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; text-align: start; white-space: nowrap; cursor: pointer; }
26
31
  .crisp-contextmenu-item svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
27
32
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); }
@@ -26,9 +26,14 @@ const config = (0, core_1.defineRecipe)({
26
26
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
27
27
  `,
28
28
  globalCss: `
29
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
30
+ on document.body while open (Zag re-enables it only on layers IT
31
+ tracks), and the portaled content is outside Zag's knowledge -- it
32
+ inherited the lock, so every item was unclickable, hit-testing right
33
+ through to whatever real element sat behind it. */
29
34
  .crisp-menu-content {
30
35
  font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
31
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
36
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
32
37
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: ${tokens_1.contract.space["1"]};
33
38
  border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover};
34
39
  box-shadow: ${tokens_1.elevation.floating};
@@ -44,9 +44,14 @@ const config = (0, core_1.defineRecipe)({
44
44
  & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
45
45
  `,
46
46
  globalCss: `
47
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
48
+ on document.body while open (Zag re-enables it only on layers IT
49
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
50
+ inherited the lock, so every option was unclickable, hit-testing
51
+ right through to whatever real element sat behind it. */
47
52
  .crisp-multiselect-listbox {
48
53
  font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
49
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
54
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
50
55
  max-height: 300px; overflow-y: auto; padding: ${tokens_1.contract.space["1"]};
51
56
  border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover};
52
57
  box-shadow: ${tokens_1.elevation.floating};
@@ -26,7 +26,12 @@ const config = (0, core_1.defineRecipe)({
26
26
  name: "popover",
27
27
  base: `display: contents; font-family: ${tokens_1.contract.font.sans};`,
28
28
  globalCss: `
29
- .crisp-popover-content { font-family: ${tokens_1.contract.font.sans}; position: fixed; z-index: 70; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; color: ${tokens_1.contract.fgPrimary}; box-shadow: ${tokens_1.elevation.floating}; 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); } }
29
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
30
+ on document.body while open (Zag re-enables it only on layers IT
31
+ tracks), and the portaled content is outside Zag's knowledge -- it
32
+ inherited the lock, unclickable, hit-testing right through to
33
+ whatever real element sat behind it. */
34
+ .crisp-popover-content { font-family: ${tokens_1.contract.font.sans}; position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; color: ${tokens_1.contract.fgPrimary}; box-shadow: ${tokens_1.elevation.floating}; 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); } }
30
35
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
31
36
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
32
37
  `,
@@ -44,9 +44,14 @@ const config = (0, core_1.defineRecipe)({
44
44
  & .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
45
45
  `,
46
46
  globalCss: `
47
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
48
+ on document.body while open (Zag re-enables it only on layers IT
49
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
50
+ inherited the lock, so every option was unclickable, hit-testing
51
+ right through to whatever real element sat behind it. */
47
52
  .crisp-select-listbox {
48
53
  font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
49
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
54
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
50
55
  max-height: 280px; overflow-y: auto; padding: ${tokens_1.contract.space["1"]};
51
56
  border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover};
52
57
  box-shadow: ${tokens_1.elevation.floating};
package/dist/cjs/index.js CHANGED
@@ -29,6 +29,7 @@ __exportStar(require("./components/button"), exports);
29
29
  __exportStar(require("./components/callout"), exports);
30
30
  __exportStar(require("./components/card"), exports);
31
31
  __exportStar(require("./components/checkbox"), exports);
32
+ __exportStar(require("./components/chip"), exports);
32
33
  __exportStar(require("./components/columncalc"), exports);
33
34
  __exportStar(require("./components/contextmenu"), exports);
34
35
  __exportStar(require("./components/datatable"), exports);
@@ -72,6 +72,19 @@ function RecordPanel({ record, columns, onClose, className, closeLabel = "Close
72
72
  // both close on the same keypress, since neither knows about the other --
73
73
  // none of the five are built on this listener or registered anywhere it
74
74
  // can see, only on the shared floating-stack this checks instead.
75
+ //
76
+ // The listener has to run in the CAPTURE phase, not bubble (#376): Menu's
77
+ // own Escape handling is a React onKeyDown on its content div -- a bubble
78
+ // -phase synthetic handler, which fires (and, closing Menu, synchronously
79
+ // unregisters it from the floating stack) BEFORE a bubble-phase listener
80
+ // on `document` ever sees the event. `hasOpenFloatingLayer()` read AFTER
81
+ // that already reflects Menu's own reaction, not the state Escape actually
82
+ // arrived in, so it raced and lost every time -- confirmed by logging the
83
+ // registration order in a real browser, not by reasoning about it. Capture
84
+ // fires top-down, before Menu's bubble handler runs at all, so the check
85
+ // sees the state as it stood when the key was pressed. This mirrors how
86
+ // Zag's own `trackEscapeKeydown` (Dialog/Drawer's Escape handling) already
87
+ // listens on `document` in capture, for the same reason.
75
88
  React.useEffect(() => {
76
89
  if (!record)
77
90
  return;
@@ -80,8 +93,8 @@ function RecordPanel({ record, columns, onClose, className, closeLabel = "Close
80
93
  return;
81
94
  onClose();
82
95
  };
83
- document.addEventListener("keydown", onKey);
84
- return () => document.removeEventListener("keydown", onKey);
96
+ document.addEventListener("keydown", onKey, true);
97
+ return () => document.removeEventListener("keydown", onKey, true);
85
98
  }, [record, onClose]);
86
99
  if (!record)
87
100
  return null;
@@ -1,5 +1,10 @@
1
1
 
2
- .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; 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); opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
2
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
3
+ on document.body while open (Zag re-enables it only on layers IT
4
+ tracks), and the portaled content is outside Zag's knowledge -- it
5
+ inherited the lock, unclickable, hit-testing right through to
6
+ whatever real element sat behind it. */
7
+ .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; min-width: 200px; box-sizing: border-box; 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); opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
3
8
  .crisp-contextmenu-item { display: flex; align-items: center; gap: var(--crisp-space-2); width: 100%; height: var(--crisp-size-menuRow); box-sizing: border-box; padding: 0 var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); text-align: start; white-space: nowrap; cursor: pointer; }
4
9
  .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
5
10
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
@@ -1,6 +1,11 @@
1
1
  @layer crisp-components {
2
2
 
3
- .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; 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); opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
3
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
4
+ on document.body while open (Zag re-enables it only on layers IT
5
+ tracks), and the portaled content is outside Zag's knowledge -- it
6
+ inherited the lock, unclickable, hit-testing right through to
7
+ whatever real element sat behind it. */
8
+ .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; min-width: 200px; box-sizing: border-box; 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); opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
4
9
  .crisp-contextmenu-item { display: flex; align-items: center; gap: var(--crisp-space-2); width: 100%; height: var(--crisp-size-menuRow); box-sizing: border-box; padding: 0 var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); text-align: start; white-space: nowrap; cursor: pointer; }
5
10
  .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
6
11
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Primitive } from "../../core/index.js";
4
+ import { chipRecipe } from "./chip.recipe.js";
5
+ /**
6
+ * A small pill — the same measured chip RecordsTable, RecordPanel and
7
+ * MultiSelect already draw as `.crisp-recordstable-tag`, exported standalone
8
+ * for surfaces that need one without a table or a fixed option set (e.g. a
9
+ * free-text tag list).
10
+ *
11
+ * This ships the measured body only. Attio's removable × on a chip needs a
12
+ * live, populated multi-value editor to measure against, and none was
13
+ * reachable this round (a person's "Email addresses" field was empty on the
14
+ * only record checked, and "Categories" removes via a checkbox toggle, not
15
+ * an inline ×) — so `Chip` has no `onRemove`/remove button yet. Add one once
16
+ * that surface can be measured; don't guess the icon, its hit target, or its
17
+ * spacing from the label.
18
+ */
19
+ export const Chip = React.forwardRef(({ tone, className, asChild, children, ...props }, ref) => {
20
+ return (_jsx(Primitive.span, { ref: ref, asChild: asChild, className: chipRecipe({ tone, className }), ...props, children: children }));
21
+ });
22
+ Chip.displayName = "Chip";
@@ -0,0 +1,77 @@
1
+ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // The same measured pill RecordsTable/RecordPanel/MultiSelect already draw
4
+ // as ".crisp-recordstable-tag" (product/recordstable/recordstable.tag.ts's
5
+ // recordTagCss): 22px tall, `padding: 0 4px`, `border-radius: chip`,
6
+ // 14px/20px at weight 500, and a 1px inset ring in the tone's colour. That
7
+ // export is raw CSS written to be spliced under another component's own
8
+ // `&` root (RecordsTable/RecordPanel nest it inside their own recipe), so it
9
+ // can't be reused verbatim here — Chip's root IS the styled element, not a
10
+ // wrapper around one. The values below are transcribed from recordTagCss,
11
+ // not re-measured; keep the two in sync by hand if either changes.
12
+ const ring = (v, pct) => `inset 0 0 0 1px color-mix(in srgb, ${v} ${pct}%, transparent)`;
13
+ const soft = (v, pct) => `color-mix(in srgb, ${v} ${pct}%, transparent)`;
14
+ const onFg = (v, pct) => `color-mix(in srgb, ${v} ${pct}%, ${c.fgPrimary})`;
15
+ const spec = defineSpec({
16
+ name: "chip",
17
+ base: {
18
+ display: "inline-flex",
19
+ alignItems: "center",
20
+ height: c.size.chip,
21
+ paddingInline: c.space["1"],
22
+ borderRadius: c.radius.chip,
23
+ fontSize: c.text.lg,
24
+ lineHeight: "20px",
25
+ fontWeight: c.weight.medium,
26
+ whiteSpace: "nowrap",
27
+ },
28
+ variants: {
29
+ tone: {
30
+ neutral: {
31
+ background: soft(c.fgPrimary, 6),
32
+ color: soft(c.fgPrimary, 82),
33
+ boxShadow: `inset 0 0 0 1px ${c.borderSubtle}`,
34
+ },
35
+ brand: {
36
+ background: soft(c.bgBrandSolid, 14),
37
+ color: onFg(c.bgBrandSolid, 82),
38
+ boxShadow: ring(c.bgBrandSolid, 30),
39
+ },
40
+ success: {
41
+ background: soft(c.bgSuccess, 16),
42
+ color: onFg(c.bgSuccess, 78),
43
+ boxShadow: ring(c.bgSuccess, 30),
44
+ },
45
+ warning: {
46
+ background: soft(c.bgWarning, 18),
47
+ color: onFg(c.bgWarning, 82),
48
+ boxShadow: ring(c.bgWarning, 30),
49
+ },
50
+ danger: {
51
+ background: soft(c.bgDanger, 15),
52
+ color: onFg(c.bgDanger, 80),
53
+ boxShadow: ring(c.bgDanger, 30),
54
+ },
55
+ purple: {
56
+ background: soft(c.bgPurple, 16),
57
+ color: onFg(c.bgPurple, 80),
58
+ boxShadow: ring(c.bgPurple, 30),
59
+ },
60
+ cyan: {
61
+ background: soft(c.bgCyan, 18),
62
+ color: onFg(c.bgCyan, 82),
63
+ boxShadow: ring(c.bgCyan, 30),
64
+ },
65
+ orange: {
66
+ background: soft(c.bgOrange, 16),
67
+ color: onFg(c.bgOrange, 82),
68
+ boxShadow: ring(c.bgOrange, 30),
69
+ },
70
+ },
71
+ },
72
+ defaultVariants: { tone: "neutral" },
73
+ });
74
+ const config = specToRecipe(spec);
75
+ export const chipRecipe = Object.assign(resolveRecipe(config), { config });
76
+ export { spec };
77
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { Chip } from "./chip.js";
2
+ export { chipRecipe } from "./chip.recipe.js";
@@ -18,7 +18,12 @@ const config = defineRecipe({
18
18
  name: "contextmenu",
19
19
  base: `display: contents; font-family: ${c.font.sans};`,
20
20
  globalCss: `
21
- .crisp-contextmenu-content { font-family: ${c.font.sans}; position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; padding: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: ${c.bgPopover}; box-shadow: ${e.floating}; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
21
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
22
+ on document.body while open (Zag re-enables it only on layers IT
23
+ tracks), and the portaled content is outside Zag's knowledge -- it
24
+ inherited the lock, unclickable, hit-testing right through to
25
+ whatever real element sat behind it. */
26
+ .crisp-contextmenu-content { font-family: ${c.font.sans}; position: fixed; z-index: 70; pointer-events: auto; min-width: 200px; box-sizing: border-box; padding: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: ${c.bgPopover}; box-shadow: ${e.floating}; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
22
27
  .crisp-contextmenu-item { display: flex; align-items: center; gap: ${c.space["2"]}; width: 100%; height: ${c.size.menuRow}; box-sizing: border-box; padding: 0 ${c.space["2"]}; margin: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.lg}; text-align: start; white-space: nowrap; cursor: pointer; }
23
28
  .crisp-contextmenu-item svg { flex-shrink: 0; color: ${c.fgMuted}; }
24
29
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
@@ -23,9 +23,14 @@ const config = defineRecipe({
23
23
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
24
24
  `,
25
25
  globalCss: `
26
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
27
+ on document.body while open (Zag re-enables it only on layers IT
28
+ tracks), and the portaled content is outside Zag's knowledge -- it
29
+ inherited the lock, so every item was unclickable, hit-testing right
30
+ through to whatever real element sat behind it. */
26
31
  .crisp-menu-content {
27
32
  font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
28
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
33
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
29
34
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: ${c.space["1"]};
30
35
  border-radius: ${c.radius.xl}; background: ${c.bgPopover};
31
36
  box-shadow: ${e.floating};
@@ -41,9 +41,14 @@ const config = defineRecipe({
41
41
  & .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
42
42
  `,
43
43
  globalCss: `
44
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
45
+ on document.body while open (Zag re-enables it only on layers IT
46
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
47
+ inherited the lock, so every option was unclickable, hit-testing
48
+ right through to whatever real element sat behind it. */
44
49
  .crisp-multiselect-listbox {
45
50
  font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
46
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
51
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
47
52
  max-height: 300px; overflow-y: auto; padding: ${c.space["1"]};
48
53
  border-radius: ${c.radius.xl}; background: ${c.bgPopover};
49
54
  box-shadow: ${e.floating};
@@ -23,7 +23,12 @@ const config = defineRecipe({
23
23
  name: "popover",
24
24
  base: `display: contents; font-family: ${c.font.sans};`,
25
25
  globalCss: `
26
- .crisp-popover-content { font-family: ${c.font.sans}; position: fixed; z-index: 70; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: ${c.bgPopover}; color: ${c.fgPrimary}; box-shadow: ${e.floating}; 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); } }
26
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
27
+ on document.body while open (Zag re-enables it only on layers IT
28
+ tracks), and the portaled content is outside Zag's knowledge -- it
29
+ inherited the lock, unclickable, hit-testing right through to
30
+ whatever real element sat behind it. */
31
+ .crisp-popover-content { font-family: ${c.font.sans}; position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: ${c.bgPopover}; color: ${c.fgPrimary}; box-shadow: ${e.floating}; 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); } }
27
32
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
28
33
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
29
34
  `,
@@ -41,9 +41,14 @@ const config = defineRecipe({
41
41
  & .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
42
42
  `,
43
43
  globalCss: `
44
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
45
+ on document.body while open (Zag re-enables it only on layers IT
46
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
47
+ inherited the lock, so every option was unclickable, hit-testing
48
+ right through to whatever real element sat behind it. */
44
49
  .crisp-select-listbox {
45
50
  font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
46
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
51
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
47
52
  max-height: 280px; overflow-y: auto; padding: ${c.space["1"]};
48
53
  border-radius: ${c.radius.xl}; background: ${c.bgPopover};
49
54
  box-shadow: ${e.floating};
package/dist/esm/index.js CHANGED
@@ -13,6 +13,7 @@ export * from "./components/button/index.js";
13
13
  export * from "./components/callout/index.js";
14
14
  export * from "./components/card/index.js";
15
15
  export * from "./components/checkbox/index.js";
16
+ export * from "./components/chip/index.js";
16
17
  export * from "./components/columncalc/index.js";
17
18
  export * from "./components/contextmenu/index.js";
18
19
  export * from "./components/datatable/index.js";
@@ -36,6 +36,19 @@ export function RecordPanel({ record, columns, onClose, className, closeLabel =
36
36
  // both close on the same keypress, since neither knows about the other --
37
37
  // none of the five are built on this listener or registered anywhere it
38
38
  // can see, only on the shared floating-stack this checks instead.
39
+ //
40
+ // The listener has to run in the CAPTURE phase, not bubble (#376): Menu's
41
+ // own Escape handling is a React onKeyDown on its content div -- a bubble
42
+ // -phase synthetic handler, which fires (and, closing Menu, synchronously
43
+ // unregisters it from the floating stack) BEFORE a bubble-phase listener
44
+ // on `document` ever sees the event. `hasOpenFloatingLayer()` read AFTER
45
+ // that already reflects Menu's own reaction, not the state Escape actually
46
+ // arrived in, so it raced and lost every time -- confirmed by logging the
47
+ // registration order in a real browser, not by reasoning about it. Capture
48
+ // fires top-down, before Menu's bubble handler runs at all, so the check
49
+ // sees the state as it stood when the key was pressed. This mirrors how
50
+ // Zag's own `trackEscapeKeydown` (Dialog/Drawer's Escape handling) already
51
+ // listens on `document` in capture, for the same reason.
39
52
  React.useEffect(() => {
40
53
  if (!record)
41
54
  return;
@@ -44,8 +57,8 @@ export function RecordPanel({ record, columns, onClose, className, closeLabel =
44
57
  return;
45
58
  onClose();
46
59
  };
47
- document.addEventListener("keydown", onKey);
48
- return () => document.removeEventListener("keydown", onKey);
60
+ document.addEventListener("keydown", onKey, true);
61
+ return () => document.removeEventListener("keydown", onKey, true);
49
62
  }, [record, onClose]);
50
63
  if (!record)
51
64
  return null;
package/dist/menu.css CHANGED
@@ -1,7 +1,12 @@
1
1
 
2
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
3
+ on document.body while open (Zag re-enables it only on layers IT
4
+ tracks), and the portaled content is outside Zag's knowledge -- it
5
+ inherited the lock, so every item was unclickable, hit-testing right
6
+ through to whatever real element sat behind it. */
2
7
  .crisp-menu-content {
3
8
  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);
9
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
5
10
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
6
11
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
7
12
  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);
@@ -1,8 +1,13 @@
1
1
  @layer crisp-components {
2
2
 
3
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
4
+ on document.body while open (Zag re-enables it only on layers IT
5
+ tracks), and the portaled content is outside Zag's knowledge -- it
6
+ inherited the lock, so every item was unclickable, hit-testing right
7
+ through to whatever real element sat behind it. */
3
8
  .crisp-menu-content {
4
9
  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);
10
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
6
11
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
7
12
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
8
13
  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);
@@ -1,7 +1,12 @@
1
1
 
2
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
3
+ on document.body while open (Zag re-enables it only on layers IT
4
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
5
+ inherited the lock, so every option was unclickable, hit-testing
6
+ right through to whatever real element sat behind it. */
2
7
  .crisp-multiselect-listbox {
3
8
  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);
9
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
5
10
  max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1);
6
11
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
7
12
  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);
@@ -1,8 +1,13 @@
1
1
  @layer crisp-components {
2
2
 
3
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
4
+ on document.body while open (Zag re-enables it only on layers IT
5
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
6
+ inherited the lock, so every option was unclickable, hit-testing
7
+ right through to whatever real element sat behind it. */
3
8
  .crisp-multiselect-listbox {
4
9
  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);
10
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
6
11
  max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1);
7
12
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
8
13
  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);
package/dist/popover.css CHANGED
@@ -1,5 +1,10 @@
1
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); } }
2
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
3
+ on document.body while open (Zag re-enables it only on layers IT
4
+ tracks), and the portaled content is outside Zag's knowledge -- it
5
+ inherited the lock, unclickable, hit-testing right through to
6
+ whatever real element sat behind it. */
7
+ .crisp-popover-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; 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
8
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
4
9
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
5
10
 
@@ -1,6 +1,11 @@
1
1
  @layer crisp-components {
2
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); } }
3
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
4
+ on document.body while open (Zag re-enables it only on layers IT
5
+ tracks), and the portaled content is outside Zag's knowledge -- it
6
+ inherited the lock, unclickable, hit-testing right through to
7
+ whatever real element sat behind it. */
8
+ .crisp-popover-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; 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
9
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
5
10
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
6
11
 
package/dist/select.css CHANGED
@@ -1,7 +1,12 @@
1
1
 
2
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
3
+ on document.body while open (Zag re-enables it only on layers IT
4
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
5
+ inherited the lock, so every option was unclickable, hit-testing
6
+ right through to whatever real element sat behind it. */
2
7
  .crisp-select-listbox {
3
8
  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);
9
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
5
10
  max-height: 280px; overflow-y: auto; padding: var(--crisp-space-1);
6
11
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
7
12
  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);
@@ -1,8 +1,13 @@
1
1
  @layer crisp-components {
2
2
 
3
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
4
+ on document.body while open (Zag re-enables it only on layers IT
5
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
6
+ inherited the lock, so every option was unclickable, hit-testing
7
+ right through to whatever real element sat behind it. */
3
8
  .crisp-select-listbox {
4
9
  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);
10
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
6
11
  max-height: 280px; overflow-y: auto; padding: var(--crisp-space-1);
7
12
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
8
13
  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);
package/dist/styles.css CHANGED
@@ -428,6 +428,34 @@
428
428
  }
429
429
  }
430
430
 
431
+ .crisp-chip {
432
+ display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding-inline: 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;
433
+ }
434
+ .crisp-chip--tone_neutral {
435
+ background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle);
436
+ }
437
+ .crisp-chip--tone_brand {
438
+ background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent);
439
+ }
440
+ .crisp-chip--tone_success {
441
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-success) 30%, transparent);
442
+ }
443
+ .crisp-chip--tone_warning {
444
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-warning) 30%, transparent);
445
+ }
446
+ .crisp-chip--tone_danger {
447
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
448
+ }
449
+ .crisp-chip--tone_purple {
450
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-purple) 30%, transparent);
451
+ }
452
+ .crisp-chip--tone_cyan {
453
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-cyan) 30%, transparent);
454
+ }
455
+ .crisp-chip--tone_orange {
456
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-orange) 30%, transparent);
457
+ }
458
+
431
459
  .crisp-columncalc {
432
460
  display: flex; align-items: stretch; width: 100%; height: 100%; font-family: var(--crisp-font-sans); /* Menu wraps its trigger in a flex div that sizes to content, which would shrink the button back to its text. Make the wrapper fill too, so the whole cell stays clickable. */
433
461
  & .crisp-menu { width: 100%; height: 100%; }
@@ -459,7 +487,12 @@
459
487
  }
460
488
 
461
489
 
462
- .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; 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); opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
490
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
491
+ on document.body while open (Zag re-enables it only on layers IT
492
+ tracks), and the portaled content is outside Zag's knowledge -- it
493
+ inherited the lock, unclickable, hit-testing right through to
494
+ whatever real element sat behind it. */
495
+ .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; min-width: 200px; box-sizing: border-box; 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); opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
463
496
  .crisp-contextmenu-item { display: flex; align-items: center; gap: var(--crisp-space-2); width: 100%; height: var(--crisp-size-menuRow); box-sizing: border-box; padding: 0 var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); text-align: start; white-space: nowrap; cursor: pointer; }
464
497
  .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
465
498
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
@@ -985,9 +1018,14 @@
985
1018
  }
986
1019
 
987
1020
 
1021
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1022
+ on document.body while open (Zag re-enables it only on layers IT
1023
+ tracks), and the portaled content is outside Zag's knowledge -- it
1024
+ inherited the lock, so every item was unclickable, hit-testing right
1025
+ through to whatever real element sat behind it. */
988
1026
  .crisp-menu-content {
989
1027
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
990
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1028
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
991
1029
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
992
1030
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
993
1031
  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);
@@ -1031,9 +1069,14 @@
1031
1069
  }
1032
1070
 
1033
1071
 
1072
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1073
+ on document.body while open (Zag re-enables it only on layers IT
1074
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
1075
+ inherited the lock, so every option was unclickable, hit-testing
1076
+ right through to whatever real element sat behind it. */
1034
1077
  .crisp-multiselect-listbox {
1035
1078
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1036
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1079
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1037
1080
  max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1);
1038
1081
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
1039
1082
  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);
@@ -1095,7 +1138,12 @@
1095
1138
  }
1096
1139
 
1097
1140
 
1098
- .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); } }
1141
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1142
+ on document.body while open (Zag re-enables it only on layers IT
1143
+ tracks), and the portaled content is outside Zag's knowledge -- it
1144
+ inherited the lock, unclickable, hit-testing right through to
1145
+ whatever real element sat behind it. */
1146
+ .crisp-popover-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; 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); } }
1099
1147
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
1100
1148
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
1101
1149
 
@@ -1261,9 +1309,14 @@
1261
1309
  }
1262
1310
 
1263
1311
 
1312
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1313
+ on document.body while open (Zag re-enables it only on layers IT
1314
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
1315
+ inherited the lock, so every option was unclickable, hit-testing
1316
+ right through to whatever real element sat behind it. */
1264
1317
  .crisp-select-listbox {
1265
1318
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1266
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1319
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1267
1320
  max-height: 280px; overflow-y: auto; padding: var(--crisp-space-1);
1268
1321
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
1269
1322
  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);
@@ -429,6 +429,34 @@
429
429
  }
430
430
  }
431
431
 
432
+ .crisp-chip {
433
+ display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding-inline: 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;
434
+ }
435
+ .crisp-chip--tone_neutral {
436
+ background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle);
437
+ }
438
+ .crisp-chip--tone_brand {
439
+ background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent);
440
+ }
441
+ .crisp-chip--tone_success {
442
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-success) 30%, transparent);
443
+ }
444
+ .crisp-chip--tone_warning {
445
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-warning) 30%, transparent);
446
+ }
447
+ .crisp-chip--tone_danger {
448
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-danger) 30%, transparent);
449
+ }
450
+ .crisp-chip--tone_purple {
451
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-purple) 30%, transparent);
452
+ }
453
+ .crisp-chip--tone_cyan {
454
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-cyan) 30%, transparent);
455
+ }
456
+ .crisp-chip--tone_orange {
457
+ 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)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-orange) 30%, transparent);
458
+ }
459
+
432
460
  .crisp-columncalc {
433
461
  display: flex; align-items: stretch; width: 100%; height: 100%; font-family: var(--crisp-font-sans); /* Menu wraps its trigger in a flex div that sizes to content, which would shrink the button back to its text. Make the wrapper fill too, so the whole cell stays clickable. */
434
462
  & .crisp-menu { width: 100%; height: 100%; }
@@ -460,7 +488,12 @@
460
488
  }
461
489
 
462
490
 
463
- .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; 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); opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
491
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
492
+ on document.body while open (Zag re-enables it only on layers IT
493
+ tracks), and the portaled content is outside Zag's knowledge -- it
494
+ inherited the lock, unclickable, hit-testing right through to
495
+ whatever real element sat behind it. */
496
+ .crisp-contextmenu-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; min-width: 200px; box-sizing: border-box; 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); opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
464
497
  .crisp-contextmenu-item { display: flex; align-items: center; gap: var(--crisp-space-2); width: 100%; height: var(--crisp-size-menuRow); box-sizing: border-box; padding: 0 var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); text-align: start; white-space: nowrap; cursor: pointer; }
465
498
  .crisp-contextmenu-item svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
466
499
  .crisp-contextmenu-item:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
@@ -986,9 +1019,14 @@
986
1019
  }
987
1020
 
988
1021
 
1022
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1023
+ on document.body while open (Zag re-enables it only on layers IT
1024
+ tracks), and the portaled content is outside Zag's knowledge -- it
1025
+ inherited the lock, so every item was unclickable, hit-testing right
1026
+ through to whatever real element sat behind it. */
989
1027
  .crisp-menu-content {
990
1028
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
991
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1029
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
992
1030
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: var(--crisp-space-1);
993
1031
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
994
1032
  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);
@@ -1032,9 +1070,14 @@
1032
1070
  }
1033
1071
 
1034
1072
 
1073
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1074
+ on document.body while open (Zag re-enables it only on layers IT
1075
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
1076
+ inherited the lock, so every option was unclickable, hit-testing
1077
+ right through to whatever real element sat behind it. */
1035
1078
  .crisp-multiselect-listbox {
1036
1079
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1037
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1080
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1038
1081
  max-height: 300px; overflow-y: auto; padding: var(--crisp-space-1);
1039
1082
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
1040
1083
  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);
@@ -1096,7 +1139,12 @@
1096
1139
  }
1097
1140
 
1098
1141
 
1099
- .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); } }
1142
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1143
+ on document.body while open (Zag re-enables it only on layers IT
1144
+ tracks), and the portaled content is outside Zag's knowledge -- it
1145
+ inherited the lock, unclickable, hit-testing right through to
1146
+ whatever real element sat behind it. */
1147
+ .crisp-popover-content { font-family: var(--crisp-font-sans); position: fixed; z-index: 70; pointer-events: auto; 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); } }
1100
1148
  .crisp-popover-content[data-align="end"] { transform-origin: top right; }
1101
1149
  .crisp-popover-content[data-up] { transform-origin: bottom left; }
1102
1150
 
@@ -1262,9 +1310,14 @@
1262
1310
  }
1263
1311
 
1264
1312
 
1313
+ /* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
1314
+ on document.body while open (Zag re-enables it only on layers IT
1315
+ tracks), and the portaled listbox is outside Zag's knowledge -- it
1316
+ inherited the lock, so every option was unclickable, hit-testing
1317
+ right through to whatever real element sat behind it. */
1265
1318
  .crisp-select-listbox {
1266
1319
  font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1267
- position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1320
+ position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: var(--crisp-space-px);
1268
1321
  max-height: 280px; overflow-y: auto; padding: var(--crisp-space-1);
1269
1322
  border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover);
1270
1323
  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);
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+ import type { VariantProps } from "../../core";
3
+ import { chipRecipe } from "./chip.recipe";
4
+ export type ChipProps = React.ComponentPropsWithoutRef<"span"> & VariantProps<typeof chipRecipe> & {
5
+ asChild?: boolean;
6
+ };
7
+ /**
8
+ * A small pill — the same measured chip RecordsTable, RecordPanel and
9
+ * MultiSelect already draw as `.crisp-recordstable-tag`, exported standalone
10
+ * for surfaces that need one without a table or a fixed option set (e.g. a
11
+ * free-text tag list).
12
+ *
13
+ * This ships the measured body only. Attio's removable × on a chip needs a
14
+ * live, populated multi-value editor to measure against, and none was
15
+ * reachable this round (a person's "Email addresses" field was empty on the
16
+ * only record checked, and "Categories" removes via a checkbox toggle, not
17
+ * an inline ×) — so `Chip` has no `onRemove`/remove button yet. Add one once
18
+ * that surface can be measured; don't guess the icon, its hit target, or its
19
+ * spacing from the label.
20
+ */
21
+ export declare const Chip: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & VariantProps<import("../..").RecipeFn<{
22
+ readonly tone: {
23
+ readonly neutral: string;
24
+ readonly brand: string;
25
+ readonly success: string;
26
+ readonly warning: string;
27
+ readonly danger: string;
28
+ readonly purple: string;
29
+ readonly cyan: string;
30
+ readonly orange: string;
31
+ };
32
+ }> & {
33
+ config: import("../..").RecipeConfig<{
34
+ readonly tone: {
35
+ readonly neutral: string;
36
+ readonly brand: string;
37
+ readonly success: string;
38
+ readonly warning: string;
39
+ readonly danger: string;
40
+ readonly purple: string;
41
+ readonly cyan: string;
42
+ readonly orange: string;
43
+ };
44
+ }>;
45
+ }> & {
46
+ asChild?: boolean;
47
+ } & React.RefAttributes<HTMLSpanElement>>;
@@ -0,0 +1,100 @@
1
+ declare const spec: {
2
+ readonly name: "chip";
3
+ readonly base: {
4
+ readonly display: "inline-flex";
5
+ readonly alignItems: "center";
6
+ readonly height: `var(--${string})`;
7
+ readonly paddingInline: `var(--${string})`;
8
+ readonly borderRadius: `var(--${string})`;
9
+ readonly fontSize: `var(--${string})`;
10
+ readonly lineHeight: "20px";
11
+ readonly fontWeight: `var(--${string})`;
12
+ readonly whiteSpace: "nowrap";
13
+ };
14
+ readonly variants: {
15
+ readonly tone: {
16
+ readonly neutral: {
17
+ readonly background: string;
18
+ readonly color: string;
19
+ readonly boxShadow: `inset 0 0 0 1px var(--${string})`;
20
+ };
21
+ readonly brand: {
22
+ background: string;
23
+ color: string;
24
+ boxShadow: string;
25
+ };
26
+ readonly success: {
27
+ background: string;
28
+ color: string;
29
+ boxShadow: string;
30
+ };
31
+ readonly warning: {
32
+ background: string;
33
+ color: string;
34
+ boxShadow: string;
35
+ };
36
+ readonly danger: {
37
+ background: string;
38
+ color: string;
39
+ boxShadow: string;
40
+ };
41
+ readonly purple: {
42
+ background: string;
43
+ color: string;
44
+ boxShadow: string;
45
+ };
46
+ readonly cyan: {
47
+ background: string;
48
+ color: string;
49
+ boxShadow: string;
50
+ };
51
+ readonly orange: {
52
+ background: string;
53
+ color: string;
54
+ boxShadow: string;
55
+ };
56
+ };
57
+ };
58
+ readonly defaultVariants: {
59
+ readonly tone: "neutral";
60
+ };
61
+ };
62
+ declare const config: import("../..").RecipeConfig<{
63
+ readonly tone: {
64
+ readonly neutral: string;
65
+ readonly brand: string;
66
+ readonly success: string;
67
+ readonly warning: string;
68
+ readonly danger: string;
69
+ readonly purple: string;
70
+ readonly cyan: string;
71
+ readonly orange: string;
72
+ };
73
+ }>;
74
+ export declare const chipRecipe: import("../..").RecipeFn<{
75
+ readonly tone: {
76
+ readonly neutral: string;
77
+ readonly brand: string;
78
+ readonly success: string;
79
+ readonly warning: string;
80
+ readonly danger: string;
81
+ readonly purple: string;
82
+ readonly cyan: string;
83
+ readonly orange: string;
84
+ };
85
+ }> & {
86
+ config: import("../..").RecipeConfig<{
87
+ readonly tone: {
88
+ readonly neutral: string;
89
+ readonly brand: string;
90
+ readonly success: string;
91
+ readonly warning: string;
92
+ readonly danger: string;
93
+ readonly purple: string;
94
+ readonly cyan: string;
95
+ readonly orange: string;
96
+ };
97
+ }>;
98
+ };
99
+ export { spec };
100
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { Chip, type ChipProps } from "./chip";
2
+ export { chipRecipe } from "./chip.recipe";
@@ -9,6 +9,7 @@ export * from "./components/button";
9
9
  export * from "./components/callout";
10
10
  export * from "./components/card";
11
11
  export * from "./components/checkbox";
12
+ export * from "./components/chip";
12
13
  export * from "./components/columncalc";
13
14
  export * from "./components/contextmenu";
14
15
  export * from "./components/datatable";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.43.0",
3
+ "version": "0.45.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -88,6 +88,11 @@
88
88
  "import": "./dist/esm/components/checkbox/index.js",
89
89
  "require": "./dist/cjs/components/checkbox/index.js"
90
90
  },
91
+ "./chip": {
92
+ "types": "./dist/types/components/chip/index.d.ts",
93
+ "import": "./dist/esm/components/chip/index.js",
94
+ "require": "./dist/cjs/components/chip/index.js"
95
+ },
91
96
  "./columncalc": {
92
97
  "types": "./dist/types/components/columncalc/index.d.ts",
93
98
  "import": "./dist/esm/components/columncalc/index.js",