@crisp-ui-kit/crisp 0.56.0 → 0.57.1
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/accentpicker.css +5 -0
- package/dist/accentpicker.d.ts +7 -0
- package/dist/accentpicker.layered.css +7 -0
- package/dist/avatar.css +1 -1
- package/dist/avatar.layered.css +1 -1
- package/dist/badge.css +2 -2
- package/dist/badge.layered.css +2 -2
- package/dist/button.css +3 -3
- package/dist/button.layered.css +3 -3
- package/dist/callout.css +6 -0
- package/dist/callout.d.ts +2 -2
- package/dist/callout.layered.css +6 -0
- package/dist/cjs/components/accentpicker/accentpicker.js +66 -0
- package/dist/cjs/components/accentpicker/accentpicker.recipe.js +25 -0
- package/dist/cjs/components/accentpicker/index.js +7 -0
- package/dist/cjs/components/avatar/avatar.recipe.js +4 -1
- package/dist/cjs/components/badge/badge.recipe.js +16 -2
- package/dist/cjs/components/button/button.recipe.js +16 -6
- package/dist/cjs/components/callout/callout.js +2 -2
- package/dist/cjs/components/callout/callout.recipe.js +15 -2
- package/dist/cjs/components/input/input.js +2 -2
- package/dist/cjs/components/input/input.recipe.js +14 -0
- package/dist/cjs/components/menu/menu.js +2 -2
- package/dist/cjs/components/menu/menu.recipe.js +8 -0
- package/dist/cjs/components/multiselect/multiselect.js +23 -10
- package/dist/cjs/components/multiselect/multiselect.recipe.js +104 -65
- package/dist/cjs/components/progress/progress.js +21 -4
- package/dist/cjs/components/progress/progress.recipe.js +30 -0
- package/dist/cjs/components/select/listbox-keyboard.js +7 -1
- package/dist/cjs/components/select/select.js +24 -11
- package/dist/cjs/components/select/select.recipe.js +177 -78
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +3 -3
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
- package/dist/cjs/components/tabs/tabs.js +63 -4
- package/dist/cjs/components/tabs/tabs.recipe.js +13 -0
- package/dist/cjs/components/text/text.recipe.js +8 -7
- package/dist/cjs/components/textarea/textarea.js +3 -2
- package/dist/cjs/components/textarea/textarea.recipe.js +2 -1
- package/dist/cjs/components/textlink/textlink.js +2 -2
- package/dist/cjs/components/textlink/textlink.recipe.js +24 -3
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/product/recordpage/recordpage.js +61 -7
- package/dist/cjs/tokens/elevation.js +7 -0
- package/dist/cjs/tokens/palette.js +120 -25
- package/dist/cjs/tokens/scale.js +9 -0
- package/dist/cjs/tokens/semantic.js +42 -0
- package/dist/esm/components/accentpicker/accentpicker.js +30 -0
- package/dist/esm/components/accentpicker/accentpicker.recipe.js +22 -0
- package/dist/esm/components/accentpicker/index.js +2 -0
- package/dist/esm/components/avatar/avatar.recipe.js +4 -1
- package/dist/esm/components/badge/badge.recipe.js +16 -2
- package/dist/esm/components/button/button.recipe.js +16 -6
- package/dist/esm/components/callout/callout.js +2 -2
- package/dist/esm/components/callout/callout.recipe.js +15 -2
- package/dist/esm/components/input/input.js +3 -3
- package/dist/esm/components/input/input.recipe.js +14 -0
- package/dist/esm/components/menu/menu.js +2 -2
- package/dist/esm/components/menu/menu.recipe.js +8 -0
- package/dist/esm/components/multiselect/multiselect.js +23 -10
- package/dist/esm/components/multiselect/multiselect.recipe.js +104 -65
- package/dist/esm/components/progress/progress.js +22 -5
- package/dist/esm/components/progress/progress.recipe.js +30 -0
- package/dist/esm/components/select/listbox-keyboard.js +7 -1
- package/dist/esm/components/select/select.js +24 -11
- package/dist/esm/components/select/select.recipe.js +177 -78
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +3 -3
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
- package/dist/esm/components/tabs/tabs.js +63 -4
- package/dist/esm/components/tabs/tabs.recipe.js +13 -0
- package/dist/esm/components/text/text.recipe.js +8 -7
- package/dist/esm/components/textarea/textarea.js +3 -2
- package/dist/esm/components/textarea/textarea.recipe.js +2 -1
- package/dist/esm/components/textlink/textlink.js +2 -2
- package/dist/esm/components/textlink/textlink.recipe.js +24 -3
- package/dist/esm/index.js +1 -0
- package/dist/esm/product/recordpage/recordpage.js +62 -8
- package/dist/esm/tokens/elevation.js +7 -0
- package/dist/esm/tokens/palette.js +120 -25
- package/dist/esm/tokens/scale.js +9 -0
- package/dist/esm/tokens/semantic.js +42 -0
- package/dist/input.css +11 -0
- package/dist/input.d.ts +2 -2
- package/dist/input.layered.css +11 -0
- package/dist/menu.css +8 -0
- package/dist/menu.layered.css +8 -0
- package/dist/multiselect.css +57 -29
- package/dist/multiselect.d.ts +3 -1
- package/dist/multiselect.layered.css +57 -29
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +82 -28
- package/dist/progress.css +3 -0
- package/dist/progress.layered.css +3 -0
- package/dist/select.css +100 -42
- package/dist/select.d.ts +3 -2
- package/dist/select.layered.css +100 -42
- package/dist/styles.css +268 -93
- package/dist/styles.layered.css +268 -93
- package/dist/tabletoolbar.css +8 -1
- package/dist/tabletoolbar.layered.css +8 -1
- package/dist/tabs.css +6 -0
- package/dist/tabs.layered.css +6 -0
- package/dist/text.css +1 -1
- package/dist/text.layered.css +1 -1
- package/dist/textarea.css +2 -1
- package/dist/textarea.layered.css +2 -1
- package/dist/textlink.css +11 -2
- package/dist/textlink.d.ts +2 -1
- package/dist/textlink.layered.css +11 -2
- package/dist/tokens.css +43 -11
- package/dist/tokens.json +82 -28
- package/dist/types/components/accentpicker/accentpicker.d.ts +23 -0
- package/dist/types/components/accentpicker/accentpicker.recipe.d.ts +3 -0
- package/dist/types/components/accentpicker/index.d.ts +2 -0
- package/dist/types/components/button/button.recipe.d.ts +2 -2
- package/dist/types/components/callout/callout.d.ts +9 -1
- package/dist/types/components/callout/callout.recipe.d.ts +12 -2
- package/dist/types/components/input/input.d.ts +14 -0
- package/dist/types/components/input/input.recipe.d.ts +3 -0
- package/dist/types/components/menu/menu.d.ts +7 -1
- package/dist/types/components/multiselect/multiselect.d.ts +25 -6
- package/dist/types/components/multiselect/multiselect.recipe.d.ts +27 -0
- package/dist/types/components/progress/progress.d.ts +16 -4
- package/dist/types/components/progress/progress.recipe.d.ts +22 -0
- package/dist/types/components/select/listbox-keyboard.d.ts +10 -1
- package/dist/types/components/select/select.d.ts +29 -6
- package/dist/types/components/select/select.recipe.d.ts +25 -10
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +14 -0
- package/dist/types/components/tabs/tabs.d.ts +9 -1
- package/dist/types/components/textarea/textarea.d.ts +16 -0
- package/dist/types/components/textlink/textlink.d.ts +8 -1
- package/dist/types/components/textlink/textlink.recipe.d.ts +10 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/product/recordpage/recordpage.d.ts +61 -1
- package/dist/types/tokens/contract.d.ts +16 -3
- package/dist/types/tokens/elevation.d.ts +7 -0
- package/dist/types/tokens/palette.d.ts +55 -11
- package/dist/types/tokens/scale.d.ts +6 -0
- package/dist/types/tokens/semantic.d.ts +33 -0
- package/package.json +6 -1
|
@@ -3,31 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.multiselectRecipe = void 0;
|
|
4
4
|
const core_1 = require("../../core");
|
|
5
5
|
const tokens_1 = require("../../tokens");
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// (
|
|
27
|
-
// that helper is written `& .crisp-recordstable-tag { ... }` for nesting
|
|
28
|
-
// under a recipe's `base`, which doesn't compile to anything inside a flat
|
|
29
|
-
// `globalCss` block. Keep the two in sync by hand (#380 added the measured
|
|
30
|
-
// `box-shadow: inset 0 0 0 1px <tone ring colour>` here to match).
|
|
6
|
+
// MultiSelect reads as the same family as Select (crisp/#882, following
|
|
7
|
+
// #867/#879): the trigger/listbox/item chrome below reuses Select's
|
|
8
|
+
// seed-design tokens verbatim (`strokeSelectTrigger`, `bgSelectPressed`,
|
|
9
|
+
// `bgSelectDisabled`, `strokeSelectInvalid`, `bgSelectListbox`,
|
|
10
|
+
// `selectFloating`, `fgSelectNeutral`, `fgSelectPlaceholder`, `radius["3xl"]`,
|
|
11
|
+
// `size.selectTriggerMd/Lg`) rather than resolving a second seed spec --
|
|
12
|
+
// seed-design has no multi-value combobox of its own (`packages/rootage/
|
|
13
|
+
// components/` has no `combobox`/`multiselect`; `chip.yaml`/`tag-group*.yaml`
|
|
14
|
+
// are a different, standalone pill pattern, not a field's multi-value
|
|
15
|
+
// trigger). What stays untouched: the `.crisp-recordstable-tag` chip
|
|
16
|
+
// styling (shared with RecordsTable/DataTable elsewhere), the checkbox-style
|
|
17
|
+
// selected indicator, and the toggle-without-closing interaction.
|
|
18
|
+
//
|
|
19
|
+
// The search row's geometry (36px height, 6px gap, no divider) is an
|
|
20
|
+
// Attio measurement (#481) `build/spec-check.mjs` guards directly -- it is
|
|
21
|
+
// NOT part of the seed-parity change and must not move. Only its text/icon
|
|
22
|
+
// colours follow the rest of this file onto the shared tokens.
|
|
23
|
+
//
|
|
24
|
+
// The listbox portals to `document.body` (#339), so its rules live in
|
|
25
|
+
// `globalCss`, not nested under `&` -- see Select's own recipe for the full
|
|
26
|
+
// reasoning (identical here).
|
|
31
27
|
const config = (0, core_1.defineRecipe)({
|
|
32
28
|
name: "multiselect",
|
|
33
29
|
base: `
|
|
@@ -35,53 +31,63 @@ const config = (0, core_1.defineRecipe)({
|
|
|
35
31
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
36
32
|
|
|
37
33
|
& .crisp-multiselect-trigger {
|
|
38
|
-
box-sizing: border-box; display: flex; align-items: center;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
transition: background-color .12s ease, box-shadow .12s ease;
|
|
34
|
+
box-sizing: border-box; display: flex; align-items: center; width: 100%; min-width: 0; margin: 0;
|
|
35
|
+
border: 1px solid ${tokens_1.contract.strokeSelectTrigger}; background: transparent; cursor: pointer; text-align: start;
|
|
36
|
+
font-family: inherit; color: ${tokens_1.contract.fgSelectNeutral};
|
|
37
|
+
transition: background-color .15s cubic-bezier(.35,0,.35,1), border-color .1s cubic-bezier(.35,0,.35,1), border-width .1s cubic-bezier(.35,0,.35,1);
|
|
43
38
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
& .crisp-multiselect-trigger:disabled {
|
|
39
|
+
/* Same pressed/disabled/readonly/focus model as Select's trigger -- see
|
|
40
|
+
that recipe's comment for why hover and an open/focus ring are gone. */
|
|
41
|
+
& .crisp-multiselect-trigger:active:not(:disabled):not([aria-readonly="true"]) { background: ${tokens_1.contract.bgSelectPressed}; }
|
|
42
|
+
& .crisp-multiselect-trigger:focus-visible { outline: 2px solid ${tokens_1.contract.strokeFocusRing}; outline-offset: -2px; }
|
|
43
|
+
& .crisp-multiselect-trigger:disabled { background: ${tokens_1.contract.bgSelectDisabled}; cursor: not-allowed; }
|
|
44
|
+
& .crisp-multiselect-trigger:disabled .crisp-multiselect-placeholder,
|
|
45
|
+
& .crisp-multiselect-trigger:disabled .crisp-multiselect-chevron { color: ${tokens_1.contract.fgDisabled}; }
|
|
46
|
+
& .crisp-multiselect-trigger[aria-readonly="true"] { background: ${tokens_1.contract.bgSelectDisabled}; cursor: default; }
|
|
47
47
|
& .crisp-multiselect-chips { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; }
|
|
48
|
-
& .crisp-multiselect-placeholder { color: ${tokens_1.contract.
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
& .crisp-multiselect-placeholder { color: ${tokens_1.contract.fgSelectPlaceholder}; }
|
|
49
|
+
/* closeRotateDuration/openRotateDuration, same asymmetric timing as
|
|
50
|
+
Select's own chevron (see that recipe's comment). */
|
|
51
|
+
& .crisp-multiselect-chevron { flex-shrink: 0; color: ${tokens_1.contract.fgNeutralMuted}; transition: transform .1s cubic-bezier(.35,0,.35,1); }
|
|
52
|
+
& .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); transition-duration: .15s; }
|
|
51
53
|
`,
|
|
52
54
|
globalCss: `
|
|
53
|
-
/* pointer-events: auto (#375)
|
|
54
|
-
on document.body while open (Zag re-enables it only on layers IT
|
|
55
|
-
tracks), and the portaled listbox is outside Zag's knowledge -- it
|
|
56
|
-
inherited the lock, so every option was unclickable, hit-testing
|
|
57
|
-
right through to whatever real element sat behind it. */
|
|
55
|
+
/* pointer-events: auto (#375) -- see Select's matching comment. */
|
|
58
56
|
.crisp-multiselect-listbox {
|
|
59
57
|
font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
60
|
-
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space
|
|
61
|
-
max-height:
|
|
62
|
-
border-radius: ${tokens_1.contract.radius
|
|
63
|
-
box-shadow: ${tokens_1.elevation.
|
|
64
|
-
transform-origin: top left; opacity: 1; transform: none;
|
|
58
|
+
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]};
|
|
59
|
+
max-height: 480px; overflow-y: auto; padding: ${tokens_1.contract.space["2"]};
|
|
60
|
+
border-radius: ${tokens_1.contract.radius["3xl"]}; background: ${tokens_1.contract.bgSelectListbox};
|
|
61
|
+
box-shadow: ${tokens_1.elevation.selectFloating};
|
|
62
|
+
transform-origin: top left; opacity: 1; transform: none;
|
|
63
|
+
transition: opacity .15s cubic-bezier(0,0,.15,1), transform .15s cubic-bezier(0,0,.15,1);
|
|
65
64
|
}
|
|
66
65
|
.crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
|
|
67
|
-
.crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.
|
|
68
|
-
|
|
66
|
+
.crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
|
|
67
|
+
/* LOCKED geometry (#481, guarded by build/spec-check.mjs): height 36px,
|
|
68
|
+
gap space["1.5"] (6px), no border-bottom -- an Attio measurement, not
|
|
69
|
+
part of this seed-parity pass. Only colours below follow Select. */
|
|
70
|
+
.crisp-multiselect-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 36px; flex-shrink: 0; margin: calc(-1 * ${tokens_1.contract.space["2"]}) calc(-1 * ${tokens_1.contract.space["2"]}) ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; color: ${tokens_1.contract.fgNeutralMuted}; }
|
|
69
71
|
.crisp-multiselect-search svg { flex-shrink: 0; }
|
|
70
|
-
.crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.
|
|
71
|
-
.crisp-multiselect-search input::placeholder { color: ${tokens_1.contract.
|
|
72
|
-
/*
|
|
73
|
-
|
|
74
|
-
same "small muted heading above a run of rows" treatment. */
|
|
75
|
-
.crisp-multiselect-label { padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["0.5"]}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
|
|
72
|
+
.crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgSelectNeutral}; }
|
|
73
|
+
.crisp-multiselect-search input::placeholder { color: ${tokens_1.contract.fgSelectPlaceholder}; }
|
|
74
|
+
/* Same groupLabel treatment as Select's (13px/regular/neutral-subtle). */
|
|
75
|
+
.crisp-multiselect-label { padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["4"]} 0; font-size: ${tokens_1.contract.text.md}; line-height: 18px; font-weight: ${tokens_1.contract.weight.regular}; color: ${tokens_1.contract.fgNeutralSubtle}; }
|
|
76
76
|
.crisp-multiselect-option {
|
|
77
|
-
box-sizing: border-box; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]};
|
|
78
|
-
padding:
|
|
77
|
+
box-sizing: border-box; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex-shrink: 0;
|
|
78
|
+
padding: ${tokens_1.contract.space["2.5"]} ${tokens_1.contract.space["4"]}; margin: 0; border: none; background: none; border-radius: ${tokens_1.contract.radius.xl};
|
|
79
|
+
font-family: inherit; text-align: start; cursor: pointer;
|
|
80
|
+
transition: background-color .15s cubic-bezier(.35,0,.35,1);
|
|
79
81
|
}
|
|
82
|
+
/* Keyboard/hover highlight keeps crisp's own visible neutral fill, same
|
|
83
|
+
reasoning as Select's matching rule (seed's own pressed tint is too
|
|
84
|
+
faint for a keyboard-navigation indicator). */
|
|
80
85
|
.crisp-multiselect-option[data-highlighted] { background: ${tokens_1.contract.bgRaised}; }
|
|
86
|
+
.crisp-multiselect-option:active { background: ${tokens_1.contract.bgSelectPressed}; }
|
|
81
87
|
.crisp-multiselect-option-chip { flex: 0 1 auto; min-width: 0; overflow: hidden; }
|
|
82
88
|
.crisp-multiselect-check { flex-shrink: 0; margin-inline-start: auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: ${tokens_1.contract.radius.sm}; box-shadow: inset 0 0 0 1.5px ${tokens_1.contract.borderDefault}; color: transparent; }
|
|
83
89
|
.crisp-multiselect-option[aria-selected="true"] .crisp-multiselect-check { background: ${tokens_1.contract.bgBrandSolid}; box-shadow: none; color: #fff; }
|
|
84
|
-
.crisp-multiselect-empty { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["
|
|
90
|
+
.crisp-multiselect-empty { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["4"]}; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgNeutralSubtle}; }
|
|
85
91
|
|
|
86
92
|
.crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.chip}; font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 82%, transparent); box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; }
|
|
87
93
|
.crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 14%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 82%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 30%, transparent); }
|
|
@@ -93,18 +99,51 @@ const config = (0, core_1.defineRecipe)({
|
|
|
93
99
|
.crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${tokens_1.contract.bgOrange} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgOrange} 82%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgOrange} 30%, transparent); }
|
|
94
100
|
`,
|
|
95
101
|
variants: {
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
// Owner's follow-up after #867/#879/#882 (crisp/#889): SIZE is Attio's
|
|
103
|
+
// again, LOOK stays seed's -- see Select's own recipe comment for the
|
|
104
|
+
// full reasoning (measured live in GRC's "New employee" dialog: a 40px
|
|
105
|
+
// trigger next to a 32px `Input` visibly mismatched). `small` is the new
|
|
106
|
+
// default: `c.size.menuRow` (32px) and the original, pre-#882
|
|
107
|
+
// Attio-measured asymmetric padding for a chip-wrapping trigger.
|
|
108
|
+
// `medium`/`large` (seed's own 40/52px) stay, opt-in.
|
|
109
|
+
size: {
|
|
110
|
+
small: `
|
|
111
|
+
& .crisp-multiselect-trigger { min-height: ${tokens_1.contract.size.menuRow}; border-radius: ${tokens_1.contract.radius.md}; gap: ${tokens_1.contract.space["1"]}; padding: ${tokens_1.contract.space["0.5"]} ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["0.5"]} ${tokens_1.contract.space["2"]}; }
|
|
112
|
+
& .crisp-multiselect-chevron { width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; }
|
|
113
|
+
`,
|
|
114
|
+
medium: `
|
|
115
|
+
& .crisp-multiselect-trigger { min-height: ${tokens_1.contract.size.selectTriggerMd}; border-radius: ${tokens_1.contract.radius.md}; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["3.5"]}; }
|
|
116
|
+
& .crisp-multiselect-chevron { width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; }
|
|
103
117
|
`,
|
|
118
|
+
large: `
|
|
119
|
+
& .crisp-multiselect-trigger { min-height: ${tokens_1.contract.size.selectTriggerLg}; border-radius: ${tokens_1.contract.radius.xl}; gap: ${tokens_1.contract.space["2.5"]}; padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["4"]}; }
|
|
120
|
+
& .crisp-multiselect-chevron { width: ${tokens_1.contract.size.selectIconLg}; height: ${tokens_1.contract.size.selectIconLg}; }
|
|
121
|
+
`,
|
|
122
|
+
},
|
|
123
|
+
// Real border width+colour change, same as Select's -- see that
|
|
124
|
+
// recipe's comment.
|
|
125
|
+
invalid: {
|
|
126
|
+
true: `& .crisp-multiselect-trigger { border-width: 2px; border-color: ${tokens_1.contract.strokeSelectInvalid}; }`,
|
|
104
127
|
false: ``,
|
|
105
128
|
},
|
|
129
|
+
// ghost: fills whatever row it's embedded in instead of seed's fixed
|
|
130
|
+
// trigger height -- RecordsTable's TagsCell embeds a MultiSelect inline
|
|
131
|
+
// in a 36px table row, the exact regression #879 caught for Select's
|
|
132
|
+
// own datatable consumers. Not a seed state; crisp's own escape hatch.
|
|
133
|
+
//
|
|
134
|
+
// Declared LAST on purpose, same reasoning as Select's recipe: same-
|
|
135
|
+
// specificity CSS rules resolve by source order, and ghost's
|
|
136
|
+
// `min-height: 0; height: 100%` below must come after `size`'s fixed
|
|
137
|
+
// min-height to win the cascade.
|
|
138
|
+
variant: {
|
|
139
|
+
default: "",
|
|
140
|
+
ghost: `
|
|
141
|
+
& .crisp-multiselect-trigger { min-height: 0; height: 100%; border-color: transparent; }
|
|
142
|
+
& .crisp-multiselect-trigger:hover:not(:disabled) { background: ${tokens_1.contract.bgSelectPressed}; }
|
|
143
|
+
`,
|
|
144
|
+
},
|
|
106
145
|
},
|
|
107
|
-
defaultVariants: { invalid: false },
|
|
146
|
+
defaultVariants: { size: "small", invalid: false, variant: "default" },
|
|
108
147
|
compoundVariants: [],
|
|
109
148
|
});
|
|
110
149
|
exports.multiselectRecipe = Object.assign((0, core_1.resolveRecipe)(config), {
|
|
@@ -4,11 +4,12 @@ exports.Progress = Progress;
|
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const progress_recipe_1 = require("./progress.recipe");
|
|
6
6
|
/**
|
|
7
|
-
* A progress bar — a continuous brand fill,
|
|
8
|
-
* `segments` is set (the "Getting started 3/6" pattern)
|
|
9
|
-
* `
|
|
7
|
+
* A progress bar — a continuous brand fill, a segmented row of pills when
|
|
8
|
+
* `segments` is set (the "Getting started 3/6" pattern), or a small ring
|
|
9
|
+
* (`variant="circular"`, a character-counter's own progress). Reports its
|
|
10
|
+
* state via `role="progressbar"` and the ARIA value attributes.
|
|
10
11
|
*/
|
|
11
|
-
function Progress({ value, max = 100, segments, "aria-label": ariaLabel, className, ...rest }) {
|
|
12
|
+
function Progress({ value, max = 100, segments, variant = "linear", size = 18, "aria-label": ariaLabel, className, ...rest }) {
|
|
12
13
|
const pct = Math.max(0, Math.min(100, (value / max) * 100));
|
|
13
14
|
const aria = {
|
|
14
15
|
role: "progressbar",
|
|
@@ -17,6 +18,22 @@ function Progress({ value, max = 100, segments, "aria-label": ariaLabel, classNa
|
|
|
17
18
|
"aria-valuemax": max,
|
|
18
19
|
"aria-label": ariaLabel,
|
|
19
20
|
};
|
|
21
|
+
if (variant === "circular") {
|
|
22
|
+
// Measured live: an 18x18 SVG, r=7.4, stroke-width 1.6 (a 0.6px margin on
|
|
23
|
+
// each side) — a two-circle ring (a soft-tinted track + a brand arc),
|
|
24
|
+
// the same track tint Badge/Callout's own `soft(bgBrandSolid, 14%)`
|
|
25
|
+
// formula already gives.
|
|
26
|
+
const strokeWidth = size * (1.6 / 18);
|
|
27
|
+
const r = size / 2 - strokeWidth;
|
|
28
|
+
const circumference = 2 * Math.PI * r;
|
|
29
|
+
const offset = circumference * (1 - pct / 100);
|
|
30
|
+
// The measured sample was at value=0 (a full, invisible offset either
|
|
31
|
+
// way), so the arc's start point couldn't be read off that one DOM
|
|
32
|
+
// snapshot -- 12 o'clock is the conventional start for this shape and
|
|
33
|
+
// isn't itself in tension with anything measured, but isn't independently
|
|
34
|
+
// confirmed at a nonzero value either.
|
|
35
|
+
return ((0, jsx_runtime_1.jsx)("div", { ...aria, className: (0, progress_recipe_1.progressRecipe)({ className }), ...rest, "data-variant": "circular", children: (0, jsx_runtime_1.jsxs)("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("circle", { className: "crisp-progress-circle-track", r: r, cx: size / 2, cy: size / 2, strokeWidth: strokeWidth, fill: "transparent" }), (0, jsx_runtime_1.jsx)("circle", { className: "crisp-progress-circle-fill", r: r, cx: size / 2, cy: size / 2, strokeWidth: strokeWidth, fill: "transparent", strokeDasharray: circumference, strokeDashoffset: offset, strokeLinecap: "round", transform: `rotate(-90 ${size / 2} ${size / 2})` })] }) }));
|
|
36
|
+
}
|
|
20
37
|
if (segments && segments > 0) {
|
|
21
38
|
const filled = Math.round((pct / 100) * segments);
|
|
22
39
|
return ((0, jsx_runtime_1.jsx)("div", { ...aria, className: (0, progress_recipe_1.progressRecipe)({ className }), ...rest, "data-segmented": "", children: Array.from({ length: segments }, (_, i) => ((0, jsx_runtime_1.jsx)("span", { className: "crisp-progress-seg", "data-filled": i < filled ? "" : undefined }, i))) }));
|
|
@@ -52,6 +52,36 @@ const spec = (0, core_1.defineSpec)({
|
|
|
52
52
|
selector: "&[data-segmented] .crisp-progress-seg[data-filled]",
|
|
53
53
|
decls: { background: tokens_1.contract.bgBrandSolid },
|
|
54
54
|
},
|
|
55
|
+
// crisp#634: measured live on Attio's own Settings > Profile
|
|
56
|
+
// character-counter ring — the track tint is the same
|
|
57
|
+
// `soft(bgBrandSolid, 14%)` formula Badge/Callout already use (measured
|
|
58
|
+
// rgb(229,238,255), within the same noise this codebase already treats
|
|
59
|
+
// as equivalent), the arc is the plain brand fill.
|
|
60
|
+
{
|
|
61
|
+
selector: '&[data-variant="circular"]',
|
|
62
|
+
decls: {
|
|
63
|
+
display: "inline-flex",
|
|
64
|
+
height: "auto",
|
|
65
|
+
borderRadius: "0",
|
|
66
|
+
background: "transparent",
|
|
67
|
+
overflow: "visible",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
slot: "circleTrack",
|
|
72
|
+
selector: "& .crisp-progress-circle-track",
|
|
73
|
+
decls: {
|
|
74
|
+
stroke: `color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 14%, transparent)`,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
slot: "circleFill",
|
|
79
|
+
selector: "& .crisp-progress-circle-fill",
|
|
80
|
+
decls: {
|
|
81
|
+
stroke: tokens_1.contract.bgBrandSolid,
|
|
82
|
+
transition: "stroke-dashoffset 0.25s ease",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
55
85
|
],
|
|
56
86
|
});
|
|
57
87
|
exports.spec = spec;
|
|
@@ -14,7 +14,7 @@ exports.useListboxKeyboard = useListboxKeyboard;
|
|
|
14
14
|
* NOT for `WheelPicker`: that is an always-visible multi-column wheel that
|
|
15
15
|
* commits on move, so Enter, Escape and Tab have no meaning in it.
|
|
16
16
|
*/
|
|
17
|
-
function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, close, dismiss = close, }) {
|
|
17
|
+
function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, close, dismiss = close, commitOnSpace = false, }) {
|
|
18
18
|
const move = (dir) => {
|
|
19
19
|
if (enabledIndexes.length === 0)
|
|
20
20
|
return;
|
|
@@ -53,6 +53,12 @@ function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, c
|
|
|
53
53
|
e.preventDefault();
|
|
54
54
|
commit();
|
|
55
55
|
break;
|
|
56
|
+
case " ":
|
|
57
|
+
if (!commitOnSpace)
|
|
58
|
+
break;
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
commit();
|
|
61
|
+
break;
|
|
56
62
|
case "Escape":
|
|
57
63
|
e.preventDefault();
|
|
58
64
|
close();
|
|
@@ -46,13 +46,16 @@ function optionText(o) {
|
|
|
46
46
|
return typeof o.label === "string" ? o.label : o.value;
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
* A select / dropdown
|
|
50
|
-
*
|
|
51
|
-
* `
|
|
52
|
-
*
|
|
53
|
-
*
|
|
49
|
+
* A select / dropdown, built to seed-design's Select (crisp/#867) rather than
|
|
50
|
+
* measured from Attio — a deliberate one-component exception, the same way
|
|
51
|
+
* `DatePicker` follows seed's calendar structure — with an optional
|
|
52
|
+
* Linear-style search seed itself has no equivalent of. A `role="combobox"`
|
|
53
|
+
* trigger opens a `role="listbox"`; pass `searchable` to add a filter field
|
|
54
|
+
* at the top (with a "No results" empty state). Full keyboard model — ↑/↓
|
|
55
|
+
* move, Enter/Space pick, Esc close, Home/End jump, opens upward when tight.
|
|
56
|
+
* Controlled or uncontrolled.
|
|
54
57
|
*/
|
|
55
|
-
function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
58
|
+
function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", size = "small", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
56
59
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
57
60
|
// CrispMessagesProvider's translation of the English default, or that
|
|
58
61
|
// default itself with no provider — unchanged from before this existed.
|
|
@@ -99,8 +102,12 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
99
102
|
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
100
103
|
? optionId(highlight)
|
|
101
104
|
: undefined;
|
|
105
|
+
// seed-design's `interactive` (crisp/#867): disabled AND readOnly both
|
|
106
|
+
// refuse to open, but only `disabled` removes the button from the tab
|
|
107
|
+
// order and greys its text — readOnly stays focusable, full-strength ink.
|
|
108
|
+
const interactive = !disabled && !readOnly;
|
|
102
109
|
const openMenu = () => {
|
|
103
|
-
if (
|
|
110
|
+
if (!interactive)
|
|
104
111
|
return;
|
|
105
112
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
106
113
|
if (rect) {
|
|
@@ -109,7 +116,7 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
109
116
|
// direction that is (#210)
|
|
110
117
|
const { style, up: flipUp } = (0, core_1.placeFloating)({
|
|
111
118
|
rect,
|
|
112
|
-
gap: 4
|
|
119
|
+
gap: 8, // seed-design's select.yaml `gutter` (crisp/#867; was 4)
|
|
113
120
|
flipBelow: 260,
|
|
114
121
|
anchor: triggerRef.current,
|
|
115
122
|
});
|
|
@@ -143,8 +150,14 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
143
150
|
commit: () => pick(filtered[highlight]),
|
|
144
151
|
close,
|
|
145
152
|
dismiss: () => setOpen(false),
|
|
153
|
+
// Space also commits when the trigger itself owns navigation (crisp/#867,
|
|
154
|
+
// matching seed-design). Never on the search input's own wiring below —
|
|
155
|
+
// that one omits this option, so Space stays a normal character there.
|
|
156
|
+
commitOnSpace: !searchable,
|
|
146
157
|
});
|
|
147
158
|
const onTriggerKeyDown = (e) => {
|
|
159
|
+
if (!interactive)
|
|
160
|
+
return;
|
|
148
161
|
if (!open) {
|
|
149
162
|
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
|
150
163
|
e.preventDefault();
|
|
@@ -201,10 +214,10 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
201
214
|
document.addEventListener("keydown", onKey);
|
|
202
215
|
return () => document.removeEventListener("keydown", onKey);
|
|
203
216
|
}, [open, close]);
|
|
204
|
-
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",
|
|
217
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, size, 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",
|
|
205
218
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
206
219
|
// validation, which is the only reason this input is not hidden
|
|
207
|
-
onChange: () => { } })), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size:
|
|
220
|
+
onChange: () => { } })), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 16, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
208
221
|
setQuery(e.target.value);
|
|
209
222
|
setHighlight(0);
|
|
210
223
|
onSearchChange?.(e.target.value);
|
|
@@ -215,7 +228,7 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
215
228
|
// heading, not one each.
|
|
216
229
|
const showGroupLabel = opt.group !== undefined &&
|
|
217
230
|
opt.group !== filtered[i - 1]?.group;
|
|
218
|
-
return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [showGroupLabel && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-select-label", children: opt.group })), (0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] })] }, opt.value));
|
|
231
|
+
return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [showGroupLabel && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-select-label", children: opt.group })), (0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), opt.description !== undefined ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-select-option-body", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-label", children: opt.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-description", children: opt.description })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-label", children: opt.label })), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] })] }, opt.value));
|
|
219
232
|
}))] }) }))] }));
|
|
220
233
|
}
|
|
221
234
|
Select.displayName = "Select";
|