@crisp-ui-kit/crisp 0.44.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chip.css +27 -0
- package/dist/chip.d.ts +7 -0
- package/dist/chip.layered.css +29 -0
- package/dist/cjs/components/chip/chip.js +58 -0
- package/dist/cjs/components/chip/chip.recipe.js +80 -0
- package/dist/cjs/components/chip/index.js +7 -0
- package/dist/cjs/components/menu/menu.js +18 -0
- package/dist/cjs/components/multiselect/multiselect.js +14 -2
- package/dist/cjs/components/popover/popover.js +17 -0
- package/dist/cjs/components/select/select.js +20 -2
- package/dist/cjs/components/tabs/tabs.recipe.js +14 -7
- package/dist/cjs/index.js +1 -0
- package/dist/esm/components/chip/chip.js +22 -0
- package/dist/esm/components/chip/chip.recipe.js +77 -0
- package/dist/esm/components/chip/index.js +2 -0
- package/dist/esm/components/menu/menu.js +18 -0
- package/dist/esm/components/multiselect/multiselect.js +14 -2
- package/dist/esm/components/popover/popover.js +17 -0
- package/dist/esm/components/select/select.js +20 -2
- package/dist/esm/components/tabs/tabs.recipe.js +14 -7
- package/dist/esm/index.js +1 -0
- package/dist/styles.css +31 -2
- package/dist/styles.layered.css +31 -2
- package/dist/tabs.css +3 -2
- package/dist/tabs.layered.css +3 -2
- package/dist/types/components/chip/chip.d.ts +47 -0
- package/dist/types/components/chip/chip.recipe.d.ts +100 -0
- package/dist/types/components/chip/index.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- 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; } });
|
|
@@ -126,6 +126,24 @@ function Menu({ trigger, children, align = "start", open: openProp, defaultOpen,
|
|
|
126
126
|
document.addEventListener("pointerdown", onDown);
|
|
127
127
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
128
128
|
}, [open, setOpen]);
|
|
129
|
+
// A document-level fallback for Escape (#376 come back). The content's own
|
|
130
|
+
// Escape handling (`useMenuKeyboard`'s onKeyDown, below) only fires once
|
|
131
|
+
// focus is actually inside `.crisp-menu-content` -- but focus lands there
|
|
132
|
+
// via a `requestAnimationFrame` scheduled on open (menu-keyboard.ts), not
|
|
133
|
+
// synchronously. An Escape pressed before that frame paints reaches
|
|
134
|
+
// whatever still has focus (the trigger button), which is outside the
|
|
135
|
+
// content div's subtree, so `onContentKeyDown` never sees it and nothing
|
|
136
|
+
// closes. This listener doesn't depend on focus location, so it closes
|
|
137
|
+
// regardless -- the same pattern `ContextMenu` already uses for the
|
|
138
|
+
// identical reason (its own comment: "Escape is handled twice on
|
|
139
|
+
// purpose").
|
|
140
|
+
React.useEffect(() => {
|
|
141
|
+
if (!open)
|
|
142
|
+
return;
|
|
143
|
+
const onKey = (e) => e.key === "Escape" && close();
|
|
144
|
+
document.addEventListener("keydown", onKey);
|
|
145
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
146
|
+
}, [open, close]);
|
|
129
147
|
const { onKeyDown: onContentKeyDown } = (0, menu_keyboard_1.useMenuKeyboard)({
|
|
130
148
|
open,
|
|
131
149
|
close,
|
|
@@ -125,10 +125,10 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
125
125
|
if (searchable)
|
|
126
126
|
requestAnimationFrame(() => searchRef.current?.focus({ preventScroll: true }));
|
|
127
127
|
};
|
|
128
|
-
const close = () => {
|
|
128
|
+
const close = React.useCallback(() => {
|
|
129
129
|
setOpen(false);
|
|
130
130
|
triggerRef.current?.focus({ preventScroll: true });
|
|
131
|
-
};
|
|
131
|
+
}, []);
|
|
132
132
|
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
133
133
|
? optionId(highlight)
|
|
134
134
|
: undefined;
|
|
@@ -182,6 +182,18 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
182
182
|
document.addEventListener("pointerdown", onDown);
|
|
183
183
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
184
184
|
}, [open]);
|
|
185
|
+
// A document-level fallback for Escape (#376 come back) -- see Select's
|
|
186
|
+
// matching comment: the listbox's own Escape handling depends on focus
|
|
187
|
+
// already being inside it, which a requestAnimationFrame moves there
|
|
188
|
+
// asynchronously after open, so an Escape pressed before that frame
|
|
189
|
+
// paints is missed entirely. This closes regardless of focus location.
|
|
190
|
+
React.useEffect(() => {
|
|
191
|
+
if (!open)
|
|
192
|
+
return;
|
|
193
|
+
const onKey = (e) => e.key === "Escape" && close();
|
|
194
|
+
document.addEventListener("keydown", onKey);
|
|
195
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
196
|
+
}, [open, close]);
|
|
185
197
|
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ invalid, className }), ...rest, children: [name &&
|
|
186
198
|
(value.length > 0 ? value : required ? [""] : []).map((v) => ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, id: id, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
187
199
|
if (open && !searchable)
|
|
@@ -109,6 +109,23 @@ function Popover({ trigger, children, align = "start", open: openProp, defaultOp
|
|
|
109
109
|
document.addEventListener("pointerdown", onDown);
|
|
110
110
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
111
111
|
}, [open, setOpen]);
|
|
112
|
+
// A document-level fallback for Escape (#376 come back). The content's own
|
|
113
|
+
// Escape handling (its `onKeyDown`, below) only fires when focus is
|
|
114
|
+
// already inside `.crisp-popover-content` -- but Popover, unlike Menu,
|
|
115
|
+
// never moves focus there on open at all, so that handler only ever fires
|
|
116
|
+
// if a consumer's own content happens to contain something already
|
|
117
|
+
// focused. In the common case (trigger stays focused after the click that
|
|
118
|
+
// opened it) Escape reached nothing and did nothing. This listener
|
|
119
|
+
// doesn't depend on focus location, so it closes regardless -- the same
|
|
120
|
+
// pattern `ContextMenu` already uses for the identical reason (its own
|
|
121
|
+
// comment: "Escape is handled twice on purpose").
|
|
122
|
+
React.useEffect(() => {
|
|
123
|
+
if (!open)
|
|
124
|
+
return;
|
|
125
|
+
const onKey = (e) => e.key === "Escape" && setOpen(false);
|
|
126
|
+
document.addEventListener("keydown", onKey);
|
|
127
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
128
|
+
}, [open, setOpen]);
|
|
112
129
|
const triggerEl = React.cloneElement(trigger, {
|
|
113
130
|
ref: triggerRef,
|
|
114
131
|
id: triggerId,
|
|
@@ -117,10 +117,10 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
117
117
|
if (searchable)
|
|
118
118
|
requestAnimationFrame(() => searchRef.current?.focus({ preventScroll: true }));
|
|
119
119
|
};
|
|
120
|
-
const close = () => {
|
|
120
|
+
const close = React.useCallback(() => {
|
|
121
121
|
setOpen(false);
|
|
122
122
|
triggerRef.current?.focus({ preventScroll: true });
|
|
123
|
-
};
|
|
123
|
+
}, []);
|
|
124
124
|
const pick = (opt) => {
|
|
125
125
|
if (!opt || opt.disabled)
|
|
126
126
|
return;
|
|
@@ -177,6 +177,24 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
177
177
|
document.addEventListener("pointerdown", onDown);
|
|
178
178
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
179
179
|
}, [open]);
|
|
180
|
+
// A document-level fallback for Escape (#376 come back): the listbox's own
|
|
181
|
+
// Escape handling lives on `useListboxKeyboard`'s `onKeyDown`, which only
|
|
182
|
+
// fires once focus is actually inside the listbox -- but focus lands there
|
|
183
|
+
// via a `requestAnimationFrame` scheduled on open (below), not
|
|
184
|
+
// synchronously. Escape pressed before that frame paints reaches whatever
|
|
185
|
+
// still has focus (the trigger, or the search input before ITS OWN focus
|
|
186
|
+
// lands), which is outside the listbox's own subtree, so its handler never
|
|
187
|
+
// sees the keydown at all and nothing closes. This listener doesn't depend
|
|
188
|
+
// on focus location, so it closes regardless -- the same pattern
|
|
189
|
+
// `ContextMenu` already uses for the identical reason (its own comment:
|
|
190
|
+
// "Escape is handled twice on purpose").
|
|
191
|
+
React.useEffect(() => {
|
|
192
|
+
if (!open)
|
|
193
|
+
return;
|
|
194
|
+
const onKey = (e) => e.key === "Escape" && close();
|
|
195
|
+
document.addEventListener("keydown", onKey);
|
|
196
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
197
|
+
}, [open, close]);
|
|
180
198
|
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, invalid, className }), ...rest, children: [name && (selected || required) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
181
199
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
182
200
|
// validation, which is the only reason this input is not hidden
|
|
@@ -4,18 +4,25 @@ exports.tabsRecipe = void 0;
|
|
|
4
4
|
const core_1 = require("../../core");
|
|
5
5
|
const tokens_1 = require("../../tokens");
|
|
6
6
|
// Record-view tabs: a row of segmented tab buttons over a 1px baseline.
|
|
7
|
-
// The
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// The list itself is the 36px bar — the 28px tab row plus the 8px the
|
|
8
|
+
// baseline needs, reserved as bottom padding so it's real box height and not
|
|
9
|
+
// paint that trailing content can sit on top of. The bar carries 12px inline
|
|
10
|
+
// padding too, and draws its own full-width `::after` baseline in
|
|
11
|
+
// borderSubtle at its bottom edge, spanning that padding. The active tab is
|
|
12
|
+
// a light pill — bg #fbfbfb + inset ring borderSubtle, r8 — with a 1px ink
|
|
13
|
+
// underline that sits on the baseline (painted above it) and slides along it;
|
|
14
|
+
// inactive tabs are muted and fill to a subtle pill on hover. Each tab carries
|
|
15
|
+
// a 16px leading icon and an optional count badge. Measured from the live
|
|
16
|
+
// app: bar `padding: 0 12px 8px`, 28px pills, 4/8 padding, 6px icon-label
|
|
17
|
+
// gap, 14/500 labels, 10px badges (rgba(0,0,0,.04)).
|
|
12
18
|
const config = (0, core_1.defineRecipe)({
|
|
13
19
|
name: "tabs",
|
|
14
20
|
base: `
|
|
15
21
|
display: flex; flex-direction: column; min-height: 0; font-family: ${tokens_1.contract.font.sans};
|
|
16
22
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
17
23
|
|
|
18
|
-
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.row}; flex-shrink: 0; }
|
|
24
|
+
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; box-sizing: content-box; height: ${tokens_1.contract.size.row}; padding: 0 ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]}; flex-shrink: 0; }
|
|
25
|
+
& .crisp-tabs-list::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: ${tokens_1.contract.borderSubtle}; }
|
|
19
26
|
& .crisp-tabs-leading { display: inline-flex; align-items: center; margin-inline-end: ${tokens_1.contract.space["1"]}; }
|
|
20
27
|
& .crisp-tabs-tab { position: relative; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 28px; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; background: none; box-shadow: inset 0 0 0 1px transparent; border-radius: ${tokens_1.contract.radius.md}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; letter-spacing: -0.14px; color: ${tokens_1.contract.fgMuted}; cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease, box-shadow .12s ease; }
|
|
21
28
|
& .crisp-tabs-tab svg { flex-shrink: 0; opacity: 0.85; }
|
|
@@ -24,7 +31,7 @@ const config = (0, core_1.defineRecipe)({
|
|
|
24
31
|
& .crisp-tabs-tab:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 1px; }
|
|
25
32
|
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); font-size: ${tokens_1.contract.text.xs}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
|
|
26
33
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${tokens_1.contract.fgPrimary}; }
|
|
27
|
-
& .crisp-tabs-indicator { position: absolute; bottom:
|
|
34
|
+
& .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: ${tokens_1.contract.fgPrimary}; border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
|
|
28
35
|
& .crisp-tabs-panel { flex: 1; min-height: 0; }
|
|
29
36
|
& .crisp-tabs-panel:focus-visible { outline: none; }
|
|
30
37
|
`,
|
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);
|
|
@@ -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;
|
|
@@ -86,6 +86,24 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
86
86
|
document.addEventListener("pointerdown", onDown);
|
|
87
87
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
88
88
|
}, [open, setOpen]);
|
|
89
|
+
// A document-level fallback for Escape (#376 come back). The content's own
|
|
90
|
+
// Escape handling (`useMenuKeyboard`'s onKeyDown, below) only fires once
|
|
91
|
+
// focus is actually inside `.crisp-menu-content` -- but focus lands there
|
|
92
|
+
// via a `requestAnimationFrame` scheduled on open (menu-keyboard.ts), not
|
|
93
|
+
// synchronously. An Escape pressed before that frame paints reaches
|
|
94
|
+
// whatever still has focus (the trigger button), which is outside the
|
|
95
|
+
// content div's subtree, so `onContentKeyDown` never sees it and nothing
|
|
96
|
+
// closes. This listener doesn't depend on focus location, so it closes
|
|
97
|
+
// regardless -- the same pattern `ContextMenu` already uses for the
|
|
98
|
+
// identical reason (its own comment: "Escape is handled twice on
|
|
99
|
+
// purpose").
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
if (!open)
|
|
102
|
+
return;
|
|
103
|
+
const onKey = (e) => e.key === "Escape" && close();
|
|
104
|
+
document.addEventListener("keydown", onKey);
|
|
105
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
106
|
+
}, [open, close]);
|
|
89
107
|
const { onKeyDown: onContentKeyDown } = useMenuKeyboard({
|
|
90
108
|
open,
|
|
91
109
|
close,
|
|
@@ -89,10 +89,10 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
89
89
|
if (searchable)
|
|
90
90
|
requestAnimationFrame(() => searchRef.current?.focus({ preventScroll: true }));
|
|
91
91
|
};
|
|
92
|
-
const close = () => {
|
|
92
|
+
const close = React.useCallback(() => {
|
|
93
93
|
setOpen(false);
|
|
94
94
|
triggerRef.current?.focus({ preventScroll: true });
|
|
95
|
-
};
|
|
95
|
+
}, []);
|
|
96
96
|
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
97
97
|
? optionId(highlight)
|
|
98
98
|
: undefined;
|
|
@@ -146,6 +146,18 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
146
146
|
document.addEventListener("pointerdown", onDown);
|
|
147
147
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
148
148
|
}, [open]);
|
|
149
|
+
// A document-level fallback for Escape (#376 come back) -- see Select's
|
|
150
|
+
// matching comment: the listbox's own Escape handling depends on focus
|
|
151
|
+
// already being inside it, which a requestAnimationFrame moves there
|
|
152
|
+
// asynchronously after open, so an Escape pressed before that frame
|
|
153
|
+
// paints is missed entirely. This closes regardless of focus location.
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
if (!open)
|
|
156
|
+
return;
|
|
157
|
+
const onKey = (e) => e.key === "Escape" && close();
|
|
158
|
+
document.addEventListener("keydown", onKey);
|
|
159
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
160
|
+
}, [open, close]);
|
|
149
161
|
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ invalid, className }), ...rest, children: [name &&
|
|
150
162
|
(value.length > 0 ? value : required ? [""] : []).map((v) => (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), _jsxs("button", { ref: triggerRef, id: id, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
151
163
|
if (open && !searchable)
|
|
@@ -73,6 +73,23 @@ export function Popover({ trigger, children, align = "start", open: openProp, de
|
|
|
73
73
|
document.addEventListener("pointerdown", onDown);
|
|
74
74
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
75
75
|
}, [open, setOpen]);
|
|
76
|
+
// A document-level fallback for Escape (#376 come back). The content's own
|
|
77
|
+
// Escape handling (its `onKeyDown`, below) only fires when focus is
|
|
78
|
+
// already inside `.crisp-popover-content` -- but Popover, unlike Menu,
|
|
79
|
+
// never moves focus there on open at all, so that handler only ever fires
|
|
80
|
+
// if a consumer's own content happens to contain something already
|
|
81
|
+
// focused. In the common case (trigger stays focused after the click that
|
|
82
|
+
// opened it) Escape reached nothing and did nothing. This listener
|
|
83
|
+
// doesn't depend on focus location, so it closes regardless -- the same
|
|
84
|
+
// pattern `ContextMenu` already uses for the identical reason (its own
|
|
85
|
+
// comment: "Escape is handled twice on purpose").
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
if (!open)
|
|
88
|
+
return;
|
|
89
|
+
const onKey = (e) => e.key === "Escape" && setOpen(false);
|
|
90
|
+
document.addEventListener("keydown", onKey);
|
|
91
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
92
|
+
}, [open, setOpen]);
|
|
76
93
|
const triggerEl = React.cloneElement(trigger, {
|
|
77
94
|
ref: triggerRef,
|
|
78
95
|
id: triggerId,
|
|
@@ -81,10 +81,10 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
81
81
|
if (searchable)
|
|
82
82
|
requestAnimationFrame(() => searchRef.current?.focus({ preventScroll: true }));
|
|
83
83
|
};
|
|
84
|
-
const close = () => {
|
|
84
|
+
const close = React.useCallback(() => {
|
|
85
85
|
setOpen(false);
|
|
86
86
|
triggerRef.current?.focus({ preventScroll: true });
|
|
87
|
-
};
|
|
87
|
+
}, []);
|
|
88
88
|
const pick = (opt) => {
|
|
89
89
|
if (!opt || opt.disabled)
|
|
90
90
|
return;
|
|
@@ -141,6 +141,24 @@ 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
|
+
// A document-level fallback for Escape (#376 come back): the listbox's own
|
|
145
|
+
// Escape handling lives on `useListboxKeyboard`'s `onKeyDown`, which only
|
|
146
|
+
// fires once focus is actually inside the listbox -- but focus lands there
|
|
147
|
+
// via a `requestAnimationFrame` scheduled on open (below), not
|
|
148
|
+
// synchronously. Escape pressed before that frame paints reaches whatever
|
|
149
|
+
// still has focus (the trigger, or the search input before ITS OWN focus
|
|
150
|
+
// lands), which is outside the listbox's own subtree, so its handler never
|
|
151
|
+
// sees the keydown at all and nothing closes. This listener doesn't depend
|
|
152
|
+
// on focus location, so it closes regardless -- the same pattern
|
|
153
|
+
// `ContextMenu` already uses for the identical reason (its own comment:
|
|
154
|
+
// "Escape is handled twice on purpose").
|
|
155
|
+
React.useEffect(() => {
|
|
156
|
+
if (!open)
|
|
157
|
+
return;
|
|
158
|
+
const onKey = (e) => e.key === "Escape" && close();
|
|
159
|
+
document.addEventListener("keydown", onKey);
|
|
160
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
161
|
+
}, [open, close]);
|
|
144
162
|
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
163
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
146
164
|
// validation, which is the only reason this input is not hidden
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
2
|
import { contract as c } from "../../tokens/index.js";
|
|
3
3
|
// Record-view tabs: a row of segmented tab buttons over a 1px baseline.
|
|
4
|
-
// The
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// The list itself is the 36px bar — the 28px tab row plus the 8px the
|
|
5
|
+
// baseline needs, reserved as bottom padding so it's real box height and not
|
|
6
|
+
// paint that trailing content can sit on top of. The bar carries 12px inline
|
|
7
|
+
// padding too, and draws its own full-width `::after` baseline in
|
|
8
|
+
// borderSubtle at its bottom edge, spanning that padding. The active tab is
|
|
9
|
+
// a light pill — bg #fbfbfb + inset ring borderSubtle, r8 — with a 1px ink
|
|
10
|
+
// underline that sits on the baseline (painted above it) and slides along it;
|
|
11
|
+
// inactive tabs are muted and fill to a subtle pill on hover. Each tab carries
|
|
12
|
+
// a 16px leading icon and an optional count badge. Measured from the live
|
|
13
|
+
// app: bar `padding: 0 12px 8px`, 28px pills, 4/8 padding, 6px icon-label
|
|
14
|
+
// gap, 14/500 labels, 10px badges (rgba(0,0,0,.04)).
|
|
9
15
|
const config = defineRecipe({
|
|
10
16
|
name: "tabs",
|
|
11
17
|
base: `
|
|
12
18
|
display: flex; flex-direction: column; min-height: 0; font-family: ${c.font.sans};
|
|
13
19
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
14
20
|
|
|
15
|
-
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.row}; flex-shrink: 0; }
|
|
21
|
+
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: ${c.space["1"]}; box-sizing: content-box; height: ${c.size.row}; padding: 0 ${c.space["3"]} ${c.space["2"]}; flex-shrink: 0; }
|
|
22
|
+
& .crisp-tabs-list::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: ${c.borderSubtle}; }
|
|
16
23
|
& .crisp-tabs-leading { display: inline-flex; align-items: center; margin-inline-end: ${c.space["1"]}; }
|
|
17
24
|
& .crisp-tabs-tab { position: relative; display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 28px; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; box-shadow: inset 0 0 0 1px transparent; border-radius: ${c.radius.md}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.14px; color: ${c.fgMuted}; cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease, box-shadow .12s ease; }
|
|
18
25
|
& .crisp-tabs-tab svg { flex-shrink: 0; opacity: 0.85; }
|
|
@@ -21,7 +28,7 @@ const config = defineRecipe({
|
|
|
21
28
|
& .crisp-tabs-tab:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 1px; }
|
|
22
29
|
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); font-size: ${c.text.xs}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
|
|
23
30
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${c.fgPrimary}; }
|
|
24
|
-
& .crisp-tabs-indicator { position: absolute; bottom:
|
|
31
|
+
& .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: ${c.fgPrimary}; border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
|
|
25
32
|
& .crisp-tabs-panel { flex: 1; min-height: 0; }
|
|
26
33
|
& .crisp-tabs-panel:focus-visible { outline: none; }
|
|
27
34
|
`,
|
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";
|
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%; }
|
|
@@ -1597,7 +1625,8 @@
|
|
|
1597
1625
|
|
|
1598
1626
|
.crisp-tabs {
|
|
1599
1627
|
display: flex; flex-direction: column; min-height: 0; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
1600
|
-
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); flex-shrink: 0; }
|
|
1628
|
+
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: var(--crisp-space-1); box-sizing: content-box; height: var(--crisp-size-row); padding: 0 var(--crisp-space-3) var(--crisp-space-2); flex-shrink: 0; }
|
|
1629
|
+
& .crisp-tabs-list::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--crisp-border-subtle); }
|
|
1601
1630
|
& .crisp-tabs-leading { display: inline-flex; align-items: center; margin-inline-end: var(--crisp-space-1); }
|
|
1602
1631
|
& .crisp-tabs-tab { position: relative; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: 28px; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; box-shadow: inset 0 0 0 1px transparent; border-radius: var(--crisp-radius-md); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: var(--crisp-fg-muted); cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease, box-shadow .12s ease; }
|
|
1603
1632
|
& .crisp-tabs-tab svg { flex-shrink: 0; opacity: 0.85; }
|
|
@@ -1606,7 +1635,7 @@
|
|
|
1606
1635
|
& .crisp-tabs-tab:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
|
|
1607
1636
|
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
|
|
1608
1637
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: var(--crisp-fg-primary); }
|
|
1609
|
-
& .crisp-tabs-indicator { position: absolute; bottom:
|
|
1638
|
+
& .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: var(--crisp-fg-primary); border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
|
|
1610
1639
|
& .crisp-tabs-panel { flex: 1; min-height: 0; }
|
|
1611
1640
|
& .crisp-tabs-panel:focus-visible { outline: none; }
|
|
1612
1641
|
}
|
package/dist/styles.layered.css
CHANGED
|
@@ -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%; }
|
|
@@ -1598,7 +1626,8 @@
|
|
|
1598
1626
|
|
|
1599
1627
|
.crisp-tabs {
|
|
1600
1628
|
display: flex; flex-direction: column; min-height: 0; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
1601
|
-
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); flex-shrink: 0; }
|
|
1629
|
+
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: var(--crisp-space-1); box-sizing: content-box; height: var(--crisp-size-row); padding: 0 var(--crisp-space-3) var(--crisp-space-2); flex-shrink: 0; }
|
|
1630
|
+
& .crisp-tabs-list::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--crisp-border-subtle); }
|
|
1602
1631
|
& .crisp-tabs-leading { display: inline-flex; align-items: center; margin-inline-end: var(--crisp-space-1); }
|
|
1603
1632
|
& .crisp-tabs-tab { position: relative; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: 28px; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; box-shadow: inset 0 0 0 1px transparent; border-radius: var(--crisp-radius-md); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: var(--crisp-fg-muted); cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease, box-shadow .12s ease; }
|
|
1604
1633
|
& .crisp-tabs-tab svg { flex-shrink: 0; opacity: 0.85; }
|
|
@@ -1607,7 +1636,7 @@
|
|
|
1607
1636
|
& .crisp-tabs-tab:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
|
|
1608
1637
|
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
|
|
1609
1638
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: var(--crisp-fg-primary); }
|
|
1610
|
-
& .crisp-tabs-indicator { position: absolute; bottom:
|
|
1639
|
+
& .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: var(--crisp-fg-primary); border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
|
|
1611
1640
|
& .crisp-tabs-panel { flex: 1; min-height: 0; }
|
|
1612
1641
|
& .crisp-tabs-panel:focus-visible { outline: none; }
|
|
1613
1642
|
}
|
package/dist/tabs.css
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
.crisp-tabs {
|
|
2
2
|
display: flex; flex-direction: column; min-height: 0; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
3
|
-
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); flex-shrink: 0; }
|
|
3
|
+
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: var(--crisp-space-1); box-sizing: content-box; height: var(--crisp-size-row); padding: 0 var(--crisp-space-3) var(--crisp-space-2); flex-shrink: 0; }
|
|
4
|
+
& .crisp-tabs-list::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--crisp-border-subtle); }
|
|
4
5
|
& .crisp-tabs-leading { display: inline-flex; align-items: center; margin-inline-end: var(--crisp-space-1); }
|
|
5
6
|
& .crisp-tabs-tab { position: relative; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: 28px; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; box-shadow: inset 0 0 0 1px transparent; border-radius: var(--crisp-radius-md); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: var(--crisp-fg-muted); cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease, box-shadow .12s ease; }
|
|
6
7
|
& .crisp-tabs-tab svg { flex-shrink: 0; opacity: 0.85; }
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
& .crisp-tabs-tab:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
|
|
10
11
|
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
|
|
11
12
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: var(--crisp-fg-primary); }
|
|
12
|
-
& .crisp-tabs-indicator { position: absolute; bottom:
|
|
13
|
+
& .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: var(--crisp-fg-primary); border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
|
|
13
14
|
& .crisp-tabs-panel { flex: 1; min-height: 0; }
|
|
14
15
|
& .crisp-tabs-panel:focus-visible { outline: none; }
|
|
15
16
|
}
|
package/dist/tabs.layered.css
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
@layer crisp-components {
|
|
2
2
|
.crisp-tabs {
|
|
3
3
|
display: flex; flex-direction: column; min-height: 0; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
4
|
-
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); flex-shrink: 0; }
|
|
4
|
+
& .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: var(--crisp-space-1); box-sizing: content-box; height: var(--crisp-size-row); padding: 0 var(--crisp-space-3) var(--crisp-space-2); flex-shrink: 0; }
|
|
5
|
+
& .crisp-tabs-list::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--crisp-border-subtle); }
|
|
5
6
|
& .crisp-tabs-leading { display: inline-flex; align-items: center; margin-inline-end: var(--crisp-space-1); }
|
|
6
7
|
& .crisp-tabs-tab { position: relative; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: 28px; margin: 0; padding: 0 var(--crisp-space-2); border: none; background: none; box-shadow: inset 0 0 0 1px transparent; border-radius: var(--crisp-radius-md); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: var(--crisp-fg-muted); cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease, box-shadow .12s ease; }
|
|
7
8
|
& .crisp-tabs-tab svg { flex-shrink: 0; opacity: 0.85; }
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
& .crisp-tabs-tab:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
|
|
11
12
|
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); font-size: var(--crisp-text-xs); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
|
|
12
13
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: var(--crisp-fg-primary); }
|
|
13
|
-
& .crisp-tabs-indicator { position: absolute; bottom:
|
|
14
|
+
& .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: var(--crisp-fg-primary); border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
|
|
14
15
|
& .crisp-tabs-panel { flex: 1; min-height: 0; }
|
|
15
16
|
& .crisp-tabs-panel:focus-visible { outline: none; }
|
|
16
17
|
}
|
|
@@ -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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.46.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",
|