@crisp-ui-kit/crisp 0.55.6 → 0.57.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/callout.css +6 -0
- package/dist/callout.d.ts +2 -2
- package/dist/callout.layered.css +6 -0
- 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.recipe.js +6 -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 +95 -65
- 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 +150 -78
- package/dist/cjs/components/statusdot/index.js +7 -0
- package/dist/cjs/components/statusdot/statusdot.js +15 -0
- package/dist/cjs/components/statusdot/statusdot.recipe.js +34 -0
- 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 +10 -0
- package/dist/cjs/components/textlink/textlink.js +2 -2
- package/dist/cjs/components/textlink/textlink.recipe.js +18 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/product/recordpage/recordpage.js +17 -4
- package/dist/cjs/tokens/elevation.js +7 -0
- package/dist/cjs/tokens/palette.js +41 -0
- package/dist/cjs/tokens/scale.js +16 -1
- package/dist/cjs/tokens/semantic.js +18 -0
- 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.recipe.js +6 -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 +95 -65
- 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 +150 -78
- package/dist/esm/components/statusdot/index.js +2 -0
- package/dist/esm/components/statusdot/statusdot.js +12 -0
- package/dist/esm/components/statusdot/statusdot.recipe.js +31 -0
- 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 +10 -0
- package/dist/esm/components/textlink/textlink.js +2 -2
- package/dist/esm/components/textlink/textlink.recipe.js +18 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/product/recordpage/recordpage.js +18 -5
- package/dist/esm/tokens/elevation.js +7 -0
- package/dist/esm/tokens/palette.js +41 -0
- package/dist/esm/tokens/scale.js +16 -1
- package/dist/esm/tokens/semantic.js +18 -0
- package/dist/input.css +4 -0
- package/dist/input.d.ts +2 -2
- package/dist/input.layered.css +4 -0
- package/dist/menu.css +8 -0
- package/dist/menu.layered.css +8 -0
- package/dist/multiselect.css +53 -29
- package/dist/multiselect.d.ts +3 -1
- package/dist/multiselect.layered.css +53 -29
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +50 -7
- package/dist/select.css +80 -42
- package/dist/select.d.ts +3 -2
- package/dist/select.layered.css +80 -42
- package/dist/statusdot.css +3 -0
- package/dist/statusdot.d.ts +7 -0
- package/dist/statusdot.layered.css +5 -0
- package/dist/styles.css +238 -73
- package/dist/styles.layered.css +238 -73
- 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 +6 -0
- package/dist/text.d.ts +2 -2
- package/dist/text.layered.css +6 -0
- package/dist/textlink.css +9 -0
- package/dist/textlink.d.ts +2 -1
- package/dist/textlink.layered.css +9 -0
- package/dist/tokens.css +54 -1
- package/dist/tokens.json +50 -7
- 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 +2 -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 +24 -6
- package/dist/types/components/multiselect/multiselect.recipe.d.ts +24 -0
- package/dist/types/components/select/listbox-keyboard.d.ts +10 -1
- package/dist/types/components/select/select.d.ts +28 -6
- package/dist/types/components/select/select.recipe.d.ts +22 -10
- package/dist/types/components/statusdot/index.d.ts +2 -0
- package/dist/types/components/statusdot/statusdot.d.ts +21 -0
- package/dist/types/components/statusdot/statusdot.recipe.d.ts +15 -0
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +14 -0
- package/dist/types/components/tabs/tabs.d.ts +9 -1
- package/dist/types/components/text/text.d.ts +4 -0
- package/dist/types/components/text/text.recipe.d.ts +12 -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 +25 -1
- package/dist/types/tokens/contract.d.ts +13 -3
- package/dist/types/tokens/elevation.d.ts +7 -0
- package/dist/types/tokens/palette.d.ts +36 -0
- package/dist/types/tokens/scale.d.ts +6 -1
- package/dist/types/tokens/semantic.d.ts +18 -0
- package/package.json +6 -1
|
@@ -3,25 +3,37 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.selectRecipe = void 0;
|
|
4
4
|
const core_1 = require("../../core");
|
|
5
5
|
const tokens_1 = require("../../tokens");
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// it
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
6
|
+
// seed-design's Select, resolved value-for-value from
|
|
7
|
+
// `@seed-design/rootage`'s select.yaml / select-trigger.yaml / select-item.yaml
|
|
8
|
+
// (crisp/#867) — a deliberate, owner-requested exception to "measure Attio
|
|
9
|
+
// first": this ONE component follows seed-design, not Attio, the same way
|
|
10
|
+
// DatePicker already follows seed's calendar structure. Every px/colour below
|
|
11
|
+
// traces to a resolved seed token (see the new entries in tokens/scale.ts,
|
|
12
|
+
// tokens/palette.ts, tokens/semantic.ts and tokens/elevation.ts, each
|
|
13
|
+
// commented with the seed token it came from) — nothing here is measured
|
|
14
|
+
// live, because there is no live Attio reference for this shape any more.
|
|
15
|
+
//
|
|
16
|
+
// crisp keeps its own `searchable` filter field and `triggerLabel` (#846):
|
|
17
|
+
// seed has no search of its own, and the owner asked to keep both. The
|
|
18
|
+
// listbox stays portaled to `document.body` for the same reason #339
|
|
19
|
+
// established (no ancestor's transform/backdrop-filter can capture its
|
|
20
|
+
// position) — see the sibling comment on `globalCss` below for why its rules
|
|
21
|
+
// live there rather than nested under `&`.
|
|
22
|
+
//
|
|
23
|
+
// Two things seed has that this recipe deliberately does NOT reproduce:
|
|
24
|
+
// - A JS-driven exit animation (seed's `exitDuration`/`exitScale`/
|
|
25
|
+
// `exitOpacity`, played before unmount). Every other crisp overlay
|
|
26
|
+
// (Dialog, Menu, ContextMenu, Popover) unmounts synchronously on close and
|
|
27
|
+
// only animates the ENTER transition via `@starting-style` — adding a
|
|
28
|
+
// delayed-unmount mechanism just for Select would be a new architectural
|
|
29
|
+
// pattern with no precedent anywhere else in the repo, for one component.
|
|
30
|
+
// The enter animation (scale 0.95→1, opacity 0→1, seed's own curve) IS
|
|
31
|
+
// reproduced below.
|
|
32
|
+
// - A visible `divider` slot: `select.yaml` defines one, but nothing in
|
|
33
|
+
// crisp or GRC groups options with an explicit separator today, and
|
|
34
|
+
// AUTHORING's rule against building what nothing consumes applies to CSS
|
|
35
|
+
// too. The resolved value is in `~/grc-parity/seed-select-spec.md` for
|
|
36
|
+
// whoever adds it.
|
|
25
37
|
const config = (0, core_1.defineRecipe)({
|
|
26
38
|
name: "select",
|
|
27
39
|
base: `
|
|
@@ -29,87 +41,147 @@ const config = (0, core_1.defineRecipe)({
|
|
|
29
41
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
30
42
|
|
|
31
43
|
& .crisp-select-trigger {
|
|
32
|
-
box-sizing: border-box; display: inline-flex; align-items: center;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
transition: background-color .12s ease, box-shadow .12s ease;
|
|
44
|
+
box-sizing: border-box; display: inline-flex; align-items: center; width: 100%; min-width: 0; margin: 0;
|
|
45
|
+
border: 1px solid ${tokens_1.contract.strokeSelectTrigger}; background: transparent; cursor: pointer;
|
|
46
|
+
font-family: inherit; color: ${tokens_1.contract.fgSelectNeutral};
|
|
47
|
+
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);
|
|
37
48
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
/* seed defines no hover state for this trigger -- only pressed, invalid,
|
|
50
|
+
disabled and readonly. The scale-down is scoped to the CONTENT
|
|
51
|
+
(seed's scaleScope: content), not the button itself, so the border
|
|
52
|
+
never moves. */
|
|
53
|
+
/* Only the value scales, not the chevron: the chevron's own transform is
|
|
54
|
+
its open/close rotation, and a scale would fight it for one shared
|
|
55
|
+
CSS property when both are true at once (pressing while open). */
|
|
56
|
+
& .crisp-select-trigger:active:not(:disabled):not([aria-readonly="true"]) { background: ${tokens_1.contract.bgSelectPressed}; }
|
|
57
|
+
& .crisp-select-trigger:active:not(:disabled):not([aria-readonly="true"]) .crisp-select-value { transform: scale(0.98); }
|
|
58
|
+
& .crisp-select-trigger:focus-visible { outline: 2px solid ${tokens_1.contract.strokeFocusRing}; outline-offset: -2px; }
|
|
59
|
+
& .crisp-select-trigger:disabled { background: ${tokens_1.contract.bgSelectDisabled}; cursor: not-allowed; }
|
|
60
|
+
& .crisp-select-trigger:disabled .crisp-select-value,
|
|
61
|
+
& .crisp-select-trigger:disabled .crisp-select-placeholder,
|
|
62
|
+
& .crisp-select-trigger:disabled .crisp-select-chevron { color: ${tokens_1.contract.fgDisabled}; }
|
|
63
|
+
/* readonly: same fill as disabled, but the value/placeholder text stays
|
|
64
|
+
normal ink -- it reads as "answered and locked", not "answered and
|
|
65
|
+
greyed out" (select-trigger.yaml's readonly state only overrides
|
|
66
|
+
root.color; value/placeholder keep their enabled colours). */
|
|
67
|
+
& .crisp-select-trigger[aria-readonly="true"] { background: ${tokens_1.contract.bgSelectDisabled}; cursor: default; }
|
|
68
|
+
& .crisp-select-value { flex: 1; min-width: 0; display: inline-flex; align-items: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; transition: transform .15s cubic-bezier(0,0,.15,1); }
|
|
69
|
+
& .crisp-select-placeholder { color: ${tokens_1.contract.fgSelectPlaceholder}; }
|
|
70
|
+
/* closeRotateDuration/closeRotateTimingFunction: 100ms, seed's "easing"
|
|
71
|
+
curve. Overridden to 150ms on open (openRotateDuration) below --
|
|
72
|
+
same curve both ways, only the duration differs. */
|
|
73
|
+
& .crisp-select-chevron { flex-shrink: 0; color: ${tokens_1.contract.fgNeutralMuted}; transition: transform .1s cubic-bezier(.35,0,.35,1); }
|
|
74
|
+
& .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); transition-duration: .15s; }
|
|
45
75
|
`,
|
|
46
76
|
globalCss: `
|
|
47
77
|
/* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
|
|
48
|
-
on document.body while open
|
|
49
|
-
|
|
50
|
-
inherited the lock, so every option was unclickable, hit-testing
|
|
51
|
-
right through to whatever real element sat behind it. */
|
|
78
|
+
on document.body while open, and the portaled listbox is outside its
|
|
79
|
+
knowledge -- it inherited the lock without this. */
|
|
52
80
|
.crisp-select-listbox {
|
|
53
81
|
font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
54
|
-
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space
|
|
55
|
-
max-height:
|
|
56
|
-
border-radius: ${tokens_1.contract.radius
|
|
57
|
-
box-shadow: ${tokens_1.elevation.
|
|
58
|
-
transform-origin: top left; opacity: 1; transform: none;
|
|
82
|
+
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]};
|
|
83
|
+
max-height: 480px; overflow-y: auto; padding: ${tokens_1.contract.space["2"]};
|
|
84
|
+
border-radius: ${tokens_1.contract.radius["3xl"]}; background: ${tokens_1.contract.bgSelectListbox};
|
|
85
|
+
box-shadow: ${tokens_1.elevation.selectFloating};
|
|
86
|
+
transform-origin: top left; opacity: 1; transform: none;
|
|
87
|
+
transition: opacity .15s cubic-bezier(0,0,.15,1), transform .15s cubic-bezier(0,0,.15,1);
|
|
59
88
|
}
|
|
60
|
-
.crisp-select-listbox[data-up] { transform-origin: bottom; }
|
|
61
|
-
.crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.
|
|
62
|
-
.crisp-select-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${tokens_1.contract.space["
|
|
89
|
+
.crisp-select-listbox[data-up] { transform-origin: bottom left; }
|
|
90
|
+
.crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
|
|
91
|
+
.crisp-select-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${tokens_1.contract.space["2"]}) calc(-1 * ${tokens_1.contract.space["2"]}) ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["2.5"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; color: ${tokens_1.contract.fgNeutralMuted}; }
|
|
63
92
|
.crisp-select-search svg { flex-shrink: 0; }
|
|
64
|
-
.crisp-select-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.
|
|
65
|
-
.crisp-select-search input::placeholder { color: ${tokens_1.contract.
|
|
66
|
-
.crisp-select-empty { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["
|
|
67
|
-
/*
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.crisp-select-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}; }
|
|
93
|
+
.crisp-select-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}; }
|
|
94
|
+
.crisp-select-search input::placeholder { color: ${tokens_1.contract.fgSelectPlaceholder}; }
|
|
95
|
+
.crisp-select-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}; }
|
|
96
|
+
/* select.yaml's groupLabel -- font-size/line-height/weight are the same
|
|
97
|
+
across sizes, only paddingY differs (medium 8px, large 10px). */
|
|
98
|
+
.crisp-select-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}; }
|
|
71
99
|
.crisp-select-option {
|
|
72
|
-
box-sizing: border-box; display: flex; align-items: center;
|
|
73
|
-
padding:
|
|
74
|
-
font-family: inherit;
|
|
100
|
+
box-sizing: border-box; display: flex; align-items: center; flex-shrink: 0;
|
|
101
|
+
padding: ${tokens_1.contract.space["2.5"]} ${tokens_1.contract.space["4"]}; margin-inline: 0; border: none; background: none; border-radius: ${tokens_1.contract.radius.xl};
|
|
102
|
+
font-family: inherit; color: ${tokens_1.contract.fgSelectNeutral}; text-align: start; cursor: pointer; white-space: nowrap;
|
|
103
|
+
transition: background-color .15s cubic-bezier(.35,0,.35,1), margin-inline .15s cubic-bezier(.35,0,.35,1);
|
|
75
104
|
}
|
|
105
|
+
/* Keyboard/hover highlight keeps crisp's own visible neutral fill --
|
|
106
|
+
seed's spec has no "highlighted" state at all (only pressed/selected/
|
|
107
|
+
disabled), and its actual pressed tint (below) is a ~3% black hint,
|
|
108
|
+
too faint to serve as a keyboard-navigation indicator. */
|
|
76
109
|
.crisp-select-option[data-highlighted] { background: ${tokens_1.contract.bgRaised}; }
|
|
77
|
-
.crisp-select-option[data-disabled] {
|
|
78
|
-
.crisp-select-option-
|
|
110
|
+
.crisp-select-option:active:not([data-disabled]) { background: ${tokens_1.contract.bgSelectPressed}; margin-inline: ${tokens_1.contract.space["2"]}; }
|
|
111
|
+
.crisp-select-option[data-disabled] { cursor: not-allowed; }
|
|
112
|
+
.crisp-select-option[data-disabled] .crisp-select-option-label,
|
|
113
|
+
.crisp-select-option[data-disabled] .crisp-select-option-description,
|
|
114
|
+
.crisp-select-option[data-disabled] .crisp-select-option-check { color: ${tokens_1.contract.fgDisabled}; }
|
|
115
|
+
.crisp-select-option-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${tokens_1.contract.radius.full}; margin-inline-end: ${tokens_1.contract.space["2"]}; }
|
|
116
|
+
.crisp-select-option-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["0.5"]}; }
|
|
79
117
|
.crisp-select-option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
80
|
-
.crisp-select-option-
|
|
118
|
+
.crisp-select-option-description { min-width: 0; overflow: hidden; text-overflow: ellipsis; color: ${tokens_1.contract.fgNeutralSubtle}; }
|
|
119
|
+
.crisp-select-option-check { flex-shrink: 0; color: ${tokens_1.contract.fgSelectNeutral}; margin-inline-start: ${tokens_1.contract.space["2"]}; }
|
|
81
120
|
`,
|
|
82
121
|
variants: {
|
|
83
|
-
//
|
|
84
|
-
// inline selects (e.g. the sort configurator).
|
|
85
|
-
variant: {
|
|
86
|
-
default: "",
|
|
87
|
-
ghost: `
|
|
88
|
-
& .crisp-select-trigger { background: transparent; box-shadow: none; }
|
|
89
|
-
& .crisp-select-trigger:hover { background: ${tokens_1.contract.bgRaised}; box-shadow: none; }
|
|
90
|
-
& .crisp-select-trigger[data-open], & .crisp-select-trigger:focus-visible { background: transparent; box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; }
|
|
91
|
-
`,
|
|
92
|
-
},
|
|
93
|
-
// full (default): fills its container, as before. auto: sizes to content.
|
|
122
|
+
// full (default): fills its container. auto: sizes to content.
|
|
94
123
|
width: {
|
|
95
124
|
full: `width: 100%;`,
|
|
96
125
|
auto: `width: auto;`,
|
|
97
126
|
},
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
& .crisp-select-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
// seed's two trigger/item sizes (select-trigger.yaml / select-item.yaml).
|
|
128
|
+
// medium is the default -- closest to crisp's previous single size.
|
|
129
|
+
size: {
|
|
130
|
+
medium: `
|
|
131
|
+
& .crisp-select-trigger { height: ${tokens_1.contract.size.selectTriggerMd}; border-radius: ${tokens_1.contract.radius.md}; gap: ${tokens_1.contract.space["2"]}; padding-inline: ${tokens_1.contract.space["3.5"]}; }
|
|
132
|
+
& .crisp-select-value, & .crisp-select-placeholder { font-size: ${tokens_1.contract.text.lg}; line-height: 19px; }
|
|
133
|
+
& .crisp-select-chevron { width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; }
|
|
134
|
+
.crisp-select-option { gap: ${tokens_1.contract.space["2"]}; }
|
|
135
|
+
.crisp-select-option-label { font-size: ${tokens_1.contract.text.lg}; line-height: 19px; }
|
|
136
|
+
.crisp-select-option-description { font-size: ${tokens_1.contract.text.sm}; line-height: 16px; }
|
|
137
|
+
.crisp-select-option-check { width: ${tokens_1.contract.space["3"]}; height: ${tokens_1.contract.space["3"]}; }
|
|
138
|
+
.crisp-select-label { padding-top: ${tokens_1.contract.space["2"]}; }
|
|
108
139
|
`,
|
|
140
|
+
large: `
|
|
141
|
+
& .crisp-select-trigger { height: ${tokens_1.contract.size.selectTriggerLg}; border-radius: ${tokens_1.contract.radius.xl}; gap: ${tokens_1.contract.space["2.5"]}; padding-inline: ${tokens_1.contract.space["4"]}; }
|
|
142
|
+
& .crisp-select-value, & .crisp-select-placeholder { font-size: ${tokens_1.contract.text.xl}; line-height: 22px; }
|
|
143
|
+
& .crisp-select-chevron { width: ${tokens_1.contract.size.selectIconLg}; height: ${tokens_1.contract.size.selectIconLg}; }
|
|
144
|
+
.crisp-select-option { gap: ${tokens_1.contract.space["3"]}; }
|
|
145
|
+
.crisp-select-option-label { font-size: ${tokens_1.contract.text.xl}; line-height: 22px; }
|
|
146
|
+
.crisp-select-option-description { font-size: ${tokens_1.contract.text.md}; line-height: 18px; }
|
|
147
|
+
.crisp-select-option-check { width: ${tokens_1.contract.space["3.5"]}; height: ${tokens_1.contract.space["3.5"]}; }
|
|
148
|
+
.crisp-select-label { padding-top: ${tokens_1.contract.space["2.5"]}; }
|
|
149
|
+
`,
|
|
150
|
+
},
|
|
151
|
+
// Real border width+colour change (seed: strokeWidth 1px→2px,
|
|
152
|
+
// strokeColor neutral-weak→critical-solid), not a box-shadow ring --
|
|
153
|
+
// crisp's own field/switch invalid rings don't apply here since this
|
|
154
|
+
// component's whole border model changed to match seed's real stroke.
|
|
155
|
+
invalid: {
|
|
156
|
+
true: `& .crisp-select-trigger { border-width: 2px; border-color: ${tokens_1.contract.strokeSelectInvalid}; }`,
|
|
109
157
|
false: ``,
|
|
110
158
|
},
|
|
159
|
+
// ghost: borderless at rest, a subtle fill on hover -- inline selects
|
|
160
|
+
// (e.g. the sort configurator, and datatable's own cell editors, which
|
|
161
|
+
// embed at whatever height their row already is, not seed's fixed
|
|
162
|
+
// trigger height). Not a seed state; kept from crisp's existing API
|
|
163
|
+
// surface, layered on top of the seed base.
|
|
164
|
+
//
|
|
165
|
+
// Declared LAST on purpose: `resolveRecipe` emits one CSS rule per
|
|
166
|
+
// variant, in the order this object lists them, and same-specificity
|
|
167
|
+
// rules resolve by source order -- ghost's `height: 100%` below must
|
|
168
|
+
// come after `size`'s fixed height to win the cascade. Move `variant`
|
|
169
|
+
// earlier and every ghost consumer's trigger silently grows to 40/52px
|
|
170
|
+
// inside its 36px row again.
|
|
171
|
+
variant: {
|
|
172
|
+
default: "",
|
|
173
|
+
ghost: `
|
|
174
|
+
& .crisp-select-trigger { height: 100%; border-color: transparent; }
|
|
175
|
+
& .crisp-select-trigger:hover:not(:disabled) { background: ${tokens_1.contract.bgSelectPressed}; }
|
|
176
|
+
`,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
defaultVariants: {
|
|
180
|
+
width: "full",
|
|
181
|
+
size: "medium",
|
|
182
|
+
invalid: false,
|
|
183
|
+
variant: "default",
|
|
111
184
|
},
|
|
112
|
-
defaultVariants: { variant: "default", width: "full", invalid: false },
|
|
113
185
|
compoundVariants: [],
|
|
114
186
|
});
|
|
115
187
|
exports.selectRecipe = Object.assign((0, core_1.resolveRecipe)(config), { config });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.statusdotRecipe = exports.StatusDot = void 0;
|
|
4
|
+
var statusdot_1 = require("./statusdot");
|
|
5
|
+
Object.defineProperty(exports, "StatusDot", { enumerable: true, get: function () { return statusdot_1.StatusDot; } });
|
|
6
|
+
var statusdot_recipe_1 = require("./statusdot.recipe");
|
|
7
|
+
Object.defineProperty(exports, "statusdotRecipe", { enumerable: true, get: function () { return statusdot_recipe_1.statusdotRecipe; } });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StatusDot = StatusDot;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const statusdot_recipe_1 = require("./statusdot.recipe");
|
|
6
|
+
/**
|
|
7
|
+
* A standalone coloured status/stage dot — a stage-column header's own
|
|
8
|
+
* colour swatch, a status field's leading indicator. Not a Badge: no pill,
|
|
9
|
+
* no text pairing required. Pass `children` (e.g. a label) alongside it in
|
|
10
|
+
* your own flex row; this renders only the dot.
|
|
11
|
+
*/
|
|
12
|
+
function StatusDot({ color, className, style, ...rest }) {
|
|
13
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: (0, statusdot_recipe_1.statusdotRecipe)({ className }), style: { backgroundColor: color, ...style }, ...rest }));
|
|
14
|
+
}
|
|
15
|
+
StatusDot.displayName = "StatusDot";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.spec = exports.statusdotRecipe = void 0;
|
|
4
|
+
const core_1 = require("../../core");
|
|
5
|
+
const tokens_1 = require("../../tokens");
|
|
6
|
+
// A standalone status/stage dot — measured live on Attio's Deals Pipeline
|
|
7
|
+
// board (crisp#859): each stage-column header (Lead / In Progress / Won /
|
|
8
|
+
// Lost) renders a free-standing 12x12 circle before its label, no border, no
|
|
9
|
+
// box-shadow. Distinct from Badge's own `dot` prop, which is a smaller 6px
|
|
10
|
+
// dot rendered INSIDE a text pill in `currentColor` (tied to the badge's own
|
|
11
|
+
// tone). This one stands on its own next to plain text.
|
|
12
|
+
//
|
|
13
|
+
// Its fill is a per-instance prop, not a fixed tone variant: the four
|
|
14
|
+
// stage colours measured live (blue #266df0, pink #fa4b94, green #00d17e,
|
|
15
|
+
// red #ff5454) don't match crisp's existing success/warning/danger tokens
|
|
16
|
+
// (green600 #1f9d57, red600 #e5484d) at all — Attio lets a workspace assign
|
|
17
|
+
// an arbitrary colour per stage from its own picker, so this component takes
|
|
18
|
+
// a raw CSS colour rather than guessing at the rest of that picker's palette
|
|
19
|
+
// from four samples.
|
|
20
|
+
const spec = (0, core_1.defineSpec)({
|
|
21
|
+
name: "statusdot",
|
|
22
|
+
base: {
|
|
23
|
+
display: "inline-block",
|
|
24
|
+
boxSizing: "border-box",
|
|
25
|
+
flexShrink: 0,
|
|
26
|
+
width: "12px",
|
|
27
|
+
height: "12px",
|
|
28
|
+
borderRadius: tokens_1.contract.radius.full,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
exports.spec = spec;
|
|
32
|
+
const config = (0, core_1.specToRecipe)(spec);
|
|
33
|
+
exports.statusdotRecipe = (0, core_1.resolveRecipe)(config);
|
|
34
|
+
exports.default = config;
|
|
@@ -772,7 +772,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
772
772
|
: sorts.length
|
|
773
773
|
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortField}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
774
774
|
: sortLabel;
|
|
775
|
-
return ((0, jsx_runtime_1.jsx)("div", { ref: rootRef, className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : " crisp-tabletoolbar-tool--dashed"}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortTriggerLabel || !sorts.length ? (sortedLabel) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-sorted-prefix", children: crispMessages["Sorted by"] ?? "Sorted by" }), " ", (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-sorted-field", children: sortField }), sorts.length > 1 && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sorted-count", children: ["
|
|
775
|
+
return ((0, jsx_runtime_1.jsx)("div", { ref: rootRef, className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : " crisp-tabletoolbar-tool--dashed"}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortTriggerLabel || !sorts.length ? (sortedLabel) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-sorted-prefix", children: crispMessages["Sorted by"] ?? "Sorted by" }), " ", (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-sorted-field", children: sortField }), sorts.length > 1 && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sorted-count", children: ["+", sorts.length - 1] }))] }))] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
776
776
|
if (dragSort !== null)
|
|
777
777
|
e.preventDefault();
|
|
778
778
|
}, onDrop: () => {
|
|
@@ -967,12 +967,12 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
967
967
|
* contents` so it contributes no box of its own — the same technique
|
|
968
968
|
* `TableToolbar`'s own root already uses to disappear from layout.
|
|
969
969
|
*/
|
|
970
|
-
exports.ToolbarTool = React.forwardRef(({ active, ghost, className, ...rest }, ref) => ((0, jsx_runtime_1.jsx)("span", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)(), children: (0, jsx_runtime_1.
|
|
970
|
+
exports.ToolbarTool = React.forwardRef(({ active, ghost, count, className, children, ...rest }, ref) => ((0, jsx_runtime_1.jsx)("span", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)(), children: (0, jsx_runtime_1.jsxs)("button", { ref: ref, type: "button", className: [
|
|
971
971
|
"crisp-tabletoolbar-tool",
|
|
972
972
|
active && "crisp-tabletoolbar-tool--active",
|
|
973
973
|
ghost && "crisp-tabletoolbar-tool--ghost",
|
|
974
974
|
className,
|
|
975
975
|
]
|
|
976
976
|
.filter(Boolean)
|
|
977
|
-
.join(" "), ...rest }) })));
|
|
977
|
+
.join(" "), ...rest, children: [children, count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-tool-count", children: count }))] }) })));
|
|
978
978
|
exports.ToolbarTool.displayName = "ToolbarTool";
|
|
@@ -107,9 +107,33 @@ const importExportChrome = `
|
|
|
107
107
|
// vs 14px) and its own mid-grey, distinct from the "Sorted by" muted tone.
|
|
108
108
|
// TableToolbar-only, same reasoning as `filterChipChrome` above (RecordsTable
|
|
109
109
|
// keeps its own older Sort label markup, not touched here).
|
|
110
|
+
// crisp#876: re-measured live -- the "+N" isn't just smaller/greyer text,
|
|
111
|
+
// it's a real chip: `background: rgba(0,0,0,.04)`, `box-shadow: inset 0 0 0
|
|
112
|
+
// 1px rgb(238,239,241)` (borderSubtle), `border-radius: 6px`, `padding: 0
|
|
113
|
+
// 4px`, 16px tall, colour `rgb(80,81,85)` -- bit-for-bit crisp's own Badge
|
|
114
|
+
// `tone="neutral" size="small"` shape, so this reuses those exact tokens
|
|
115
|
+
// (`fgNeutralSoft`, `borderSubtle`, `radius.sm`, `space["1"]`) rather than
|
|
116
|
+
// re-deriving new ones or leaving the old 73%-mix colour approximation.
|
|
110
117
|
const sortedLabelChrome = `
|
|
111
118
|
& .crisp-tabletoolbar-sorted-prefix { color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
|
|
112
|
-
& .crisp-tabletoolbar-sorted-count {
|
|
119
|
+
& .crisp-tabletoolbar-sorted-count { display: inline-flex; align-items: center; height: 16px; box-sizing: border-box;
|
|
120
|
+
padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent);
|
|
121
|
+
box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgNeutralSoft}; }`;
|
|
122
|
+
// crisp#875: a general count-badge slot for `ToolbarTool` (a screen's own
|
|
123
|
+
// quick filter, e.g. "My tasks •5") -- the SAME measured shape as
|
|
124
|
+
// `sorted-count` above, independently re-confirmed on a second live Attio
|
|
125
|
+
// surface this round (a Kanban board's own stage-column count, "Idea 0":
|
|
126
|
+
// identical `data-variant="tertiary" data-size="16"` chip, same bg/ring/
|
|
127
|
+
// radius/padding/colour, plus `font-variant-numeric: tabular-nums` for
|
|
128
|
+
// digit alignment past a single character). Same reasoning as
|
|
129
|
+
// search/menu-input's own duplicated ring formula elsewhere in this file:
|
|
130
|
+
// a distinct class, not a shared one, so `sorted-count`'s own meaning
|
|
131
|
+
// doesn't get diluted by an unrelated consumer.
|
|
132
|
+
const toolCountChrome = `
|
|
133
|
+
& .crisp-tabletoolbar-tool-count { display: inline-flex; align-items: center; height: 16px; box-sizing: border-box;
|
|
134
|
+
padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent);
|
|
135
|
+
box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgNeutralSoft};
|
|
136
|
+
font-variant-numeric: tabular-nums; }`;
|
|
113
137
|
// The table toolbar, measured: 28px controls, the Sort/Filter buttons and
|
|
114
138
|
// the menus they open, sort-condition rows, and the filter chips. The chrome
|
|
115
139
|
// itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
|
|
@@ -129,6 +153,7 @@ ${(0, tabletoolbar_chrome_1.toolbarChromeBody)("tabletoolbar")}
|
|
|
129
153
|
${filterChipChrome}
|
|
130
154
|
${importExportChrome}
|
|
131
155
|
${sortedLabelChrome}
|
|
156
|
+
${toolCountChrome}
|
|
132
157
|
${core_1.rtlFlipCss}
|
|
133
158
|
`,
|
|
134
159
|
});
|
|
@@ -35,7 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.Tabs = Tabs;
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
|
+
const lucide_react_1 = require("lucide-react");
|
|
38
39
|
const React = __importStar(require("react"));
|
|
40
|
+
const menu_1 = require("../menu");
|
|
39
41
|
const tabs_recipe_1 = require("./tabs.recipe");
|
|
40
42
|
const tabs_keyboard_1 = require("./tabs-keyboard");
|
|
41
43
|
/**
|
|
@@ -44,7 +46,7 @@ const tabs_keyboard_1 = require("./tabs-keyboard");
|
|
|
44
46
|
* Home/End jump); activation follows focus. Controlled via
|
|
45
47
|
* `value`/`onValueChange`, or uncontrolled from `defaultValue`.
|
|
46
48
|
*/
|
|
47
|
-
function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, className, fill, "aria-label": ariaLabel, ...rest }) {
|
|
49
|
+
function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, moreLabel = (n) => `+${n} more`, moreMenuLabel = "More tabs", className, fill, "aria-label": ariaLabel, ...rest }) {
|
|
48
50
|
// Scoped to this instance: `tab-${value}` and `panel-${value}` are global,
|
|
49
51
|
// so two tab bars sharing a value mint the same id twice and every
|
|
50
52
|
// aria-labelledby resolves to the first one (#227).
|
|
@@ -77,16 +79,73 @@ function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, cla
|
|
|
77
79
|
ro.observe(list);
|
|
78
80
|
return () => ro.disconnect();
|
|
79
81
|
}, [active, tabs]);
|
|
82
|
+
// crisp#863 -- responsive "+N more" overflow, measured live: Attio
|
|
83
|
+
// collapses the TRAILING tabs into a single trigger once the strip runs
|
|
84
|
+
// out of room, rather than letting the row wrap or overrun. Every tab
|
|
85
|
+
// (and the "+N more" trigger itself) stays permanently mounted -- an
|
|
86
|
+
// overflowed one gets `data-tabs-overflow-hidden` (position: absolute;
|
|
87
|
+
// visibility: hidden in the recipe), which removes it from the flex row
|
|
88
|
+
// WITHOUT ever making its `offsetWidth` unmeasurable the way `display:
|
|
89
|
+
// none` would. That's what lets this recompute purely from cached
|
|
90
|
+
// per-tab widths on every resize, with no flash: a naive
|
|
91
|
+
// measure-everything-visible-first pass would show the full unclipped
|
|
92
|
+
// row for one frame before collapsing it.
|
|
93
|
+
const [hiddenFrom, setHiddenFrom] = React.useState(null);
|
|
94
|
+
const moreRef = React.useRef(null);
|
|
95
|
+
React.useLayoutEffect(() => {
|
|
96
|
+
const list = listRef.current;
|
|
97
|
+
if (!list || tabs.length === 0)
|
|
98
|
+
return;
|
|
99
|
+
const recompute = () => {
|
|
100
|
+
const containerWidth = list.clientWidth;
|
|
101
|
+
// Not yet laid out (e.g. this render, or the test environment, which
|
|
102
|
+
// never computes real box metrics) -- never collapse everything on a
|
|
103
|
+
// 0-width read, that's a false "nothing fits" rather than "not
|
|
104
|
+
// measurable yet".
|
|
105
|
+
if (containerWidth <= 0)
|
|
106
|
+
return;
|
|
107
|
+
const els = tabs.map((t) => list.querySelector(`[data-value="${CSS.escape(t.value)}"]`));
|
|
108
|
+
if (els.some((el) => !el))
|
|
109
|
+
return;
|
|
110
|
+
const gap = 4; // ${c.space["1"]}, the list's own gap (recipe.ts)
|
|
111
|
+
const moreWidth = moreRef.current?.offsetWidth ?? 0;
|
|
112
|
+
let used = 0;
|
|
113
|
+
let cutoff = tabs.length;
|
|
114
|
+
for (let i = 0; i < els.length; i++) {
|
|
115
|
+
const w = (els[i]?.offsetWidth ?? 0) + (i > 0 ? gap : 0);
|
|
116
|
+
const reserve = i < tabs.length - 1 ? moreWidth + gap : 0;
|
|
117
|
+
if (used + w + reserve > containerWidth) {
|
|
118
|
+
cutoff = i;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
used += w;
|
|
122
|
+
}
|
|
123
|
+
// The active tab is never allowed to fall into the overflow menu --
|
|
124
|
+
// not itself measured live (Attio's own fixture never exercised
|
|
125
|
+
// navigating INTO an overflowed tab first), but "the tab you're on
|
|
126
|
+
// stays visible" is the safe, unsurprising default for any tab UI.
|
|
127
|
+
const activeIndex = tabs.findIndex((t) => t.value === active);
|
|
128
|
+
if (activeIndex >= 0 && activeIndex >= cutoff)
|
|
129
|
+
cutoff = activeIndex + 1;
|
|
130
|
+
setHiddenFrom(cutoff >= tabs.length ? null : cutoff);
|
|
131
|
+
};
|
|
132
|
+
recompute();
|
|
133
|
+
const ro = new ResizeObserver(recompute);
|
|
134
|
+
ro.observe(list);
|
|
135
|
+
return () => ro.disconnect();
|
|
136
|
+
}, [tabs, active]);
|
|
137
|
+
const hiddenTabs = hiddenFrom == null ? [] : tabs.slice(hiddenFrom);
|
|
80
138
|
const { onKeyDown, tabIndexFor } = (0, tabs_keyboard_1.useTablistKeyboard)({
|
|
81
139
|
tabs,
|
|
82
140
|
active,
|
|
83
141
|
select,
|
|
84
142
|
listRef,
|
|
85
143
|
});
|
|
86
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, tabs_recipe_1.tabsRecipe)({ fill, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { ref: listRef, className: "crisp-tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t) => {
|
|
144
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, tabs_recipe_1.tabsRecipe)({ fill, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { ref: listRef, className: "crisp-tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t, i) => {
|
|
87
145
|
const selected = t.value === active;
|
|
88
|
-
|
|
89
|
-
|
|
146
|
+
const overflowHidden = hiddenFrom != null && i >= hiddenFrom;
|
|
147
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "tab", id: tabId(t.value), "aria-selected": selected, "aria-controls": children && selected ? panelId(t.value) : undefined, tabIndex: overflowHidden ? -1 : tabIndexFor(t.value), "data-value": t.value, "data-selected": selected ? "" : undefined, "data-tabs-overflow-hidden": overflowHidden ? "" : undefined, disabled: t.disabled, className: "crisp-tabs-tab", onClick: () => select(t.value), children: [t.icon, (0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value));
|
|
148
|
+
}), (0, jsx_runtime_1.jsx)("span", { ref: moreRef, className: "crisp-tabs-more", "data-tabs-overflow-hidden": hiddenTabs.length === 0 ? "" : undefined, children: (0, jsx_runtime_1.jsx)(menu_1.Menu, { "aria-label": moreMenuLabel, trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabs-tab", children: [(0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-label", children: moreLabel(hiddenTabs.length) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), children: hiddenTabs.map((t) => ((0, jsx_runtime_1.jsxs)(menu_1.MenuItem, { icon: t.icon, onSelect: () => select(t.value), children: [t.label, t.badge != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value))) }) }), indicator && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-indicator", style: {
|
|
90
149
|
width: indicator.width,
|
|
91
150
|
transform: `translateX(${indicator.left}px)`,
|
|
92
151
|
} }))] }), children && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabs-panel", role: "tabpanel", id: panelId(active), "aria-labelledby": tabId(active),
|
|
@@ -57,6 +57,19 @@ const config = (0, core_1.defineRecipe)({
|
|
|
57
57
|
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 14px; height: 14px; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: 4px; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; font-size: 10px; line-height: 14px; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgNeutralSoft}; }
|
|
58
58
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${tokens_1.contract.fgPrimary}; }
|
|
59
59
|
& .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; }
|
|
60
|
+
/* crisp#863: the "+N more" overflow trigger, measured live -- its
|
|
61
|
+
computed style is identical to a plain .crisp-tabs-tab (same pill,
|
|
62
|
+
same 6px icon/label gap the tab class already gives via
|
|
63
|
+
space["1.5"]), and the dropdown it opens is crisp's own Menu/
|
|
64
|
+
MenuItem chrome (already matches Attio's popover, nothing new to
|
|
65
|
+
style there). Only the wrapper needs a rule: an overflowed tab (or
|
|
66
|
+
the trigger itself, when nothing is hidden) is taken out of the
|
|
67
|
+
flex row and out of the a11y tree without ever making its own
|
|
68
|
+
offsetWidth unmeasurable the way display:none would -- that's what
|
|
69
|
+
lets the tabs.tsx effect recompute the cutoff from cached widths on
|
|
70
|
+
every resize with no flash of an unclipped row. */
|
|
71
|
+
& .crisp-tabs-more { display: inline-flex; }
|
|
72
|
+
& [data-tabs-overflow-hidden] { position: absolute; visibility: hidden; pointer-events: none; }
|
|
60
73
|
& .crisp-tabs-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; padding-top: ${tokens_1.contract.space["8"]}; }
|
|
61
74
|
& .crisp-tabs-panel:focus-visible { outline: none; }
|
|
62
75
|
`,
|
|
@@ -34,6 +34,16 @@ const spec = (0, core_1.defineSpec)({
|
|
|
34
34
|
tone: {
|
|
35
35
|
default: { color: tokens_1.contract.fgPrimary },
|
|
36
36
|
muted: { color: tokens_1.contract.fgNeutralMuted },
|
|
37
|
+
// #858: no new measurement -- both reuse tokens already measured
|
|
38
|
+
// against Attio for the same colours in a different arrangement.
|
|
39
|
+
// `brand` is `textlink`'s own reused brand-text colour (`bgBrandSolid`,
|
|
40
|
+
// the same alias GRC's own `--color-fg-brand` resolves to). `danger`
|
|
41
|
+
// is `fgDanger` ("solid critical/destructive TEXT... an overdue
|
|
42
|
+
// due-date, a destructive menu item's label" -- see its own doc in
|
|
43
|
+
// tokens/semantic.ts), not `fgDangerSoft` (that one's a soft Badge
|
|
44
|
+
// pill's text, a different measured hex).
|
|
45
|
+
brand: { color: tokens_1.contract.bgBrandSolid },
|
|
46
|
+
danger: { color: tokens_1.contract.fgDanger },
|
|
37
47
|
},
|
|
38
48
|
},
|
|
39
49
|
defaultVariants: { tone: "default" },
|
|
@@ -10,7 +10,7 @@ const textlink_recipe_1 = require("./textlink.recipe");
|
|
|
10
10
|
* `event.preventDefault()` pairs it with client-side routing (e.g.
|
|
11
11
|
* react-router's own `Link`).
|
|
12
12
|
*/
|
|
13
|
-
function TextLink({ tone = "muted", className, ...rest }) {
|
|
14
|
-
return (0, jsx_runtime_1.jsx)("a", { className: (0, textlink_recipe_1.textlinkRecipe)({ tone, className }), ...rest });
|
|
13
|
+
function TextLink({ tone = "muted", size, className, ...rest }) {
|
|
14
|
+
return (0, jsx_runtime_1.jsx)("a", { className: (0, textlink_recipe_1.textlinkRecipe)({ tone, size, className }), ...rest });
|
|
15
15
|
}
|
|
16
16
|
TextLink.displayName = "TextLink";
|
|
@@ -27,6 +27,24 @@ const config = (0, core_1.defineRecipe)({
|
|
|
27
27
|
// that only goes brand on hover.
|
|
28
28
|
muted: `font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.regular}; color: ${tokens_1.contract.fgMuted}; &:hover { color: ${tokens_1.contract.bgBrandSolid}; }`,
|
|
29
29
|
},
|
|
30
|
+
// crisp#874: an explicit, OPTIONAL size override -- no `defaultVariants`
|
|
31
|
+
// entry below, so omitting `size` emits no `--size_*` class at all and
|
|
32
|
+
// each tone's own hard-coded font-size (above) stands completely
|
|
33
|
+
// unchanged. Only when a caller passes `size` explicitly does this
|
|
34
|
+
// override it: `size`'s CSS block is emitted AFTER `tone`'s in the
|
|
35
|
+
// generated stylesheet (`generate-recipes.mjs` walks `variants` in
|
|
36
|
+
// object-key order), so at equal specificity it wins the cascade.
|
|
37
|
+
// Measured live (a running GRC server, `getComputedStyle`):
|
|
38
|
+
// internal/grc's two primary-column record links
|
|
39
|
+
// (routes/lists/Lists.tsx:51, routes/registers/Registers.tsx:63) are
|
|
40
|
+
// 14px/500 ambient, inherited from DataTable's own
|
|
41
|
+
// `.crisp-datatable-entity-name` -- `size="lg"` matches that context
|
|
42
|
+
// without changing `brand`'s weight/colour.
|
|
43
|
+
size: {
|
|
44
|
+
sm: `font-size: ${tokens_1.contract.text.sm};`,
|
|
45
|
+
md: `font-size: ${tokens_1.contract.text.md};`,
|
|
46
|
+
lg: `font-size: ${tokens_1.contract.text.lg};`,
|
|
47
|
+
},
|
|
30
48
|
},
|
|
31
49
|
defaultVariants: { tone: "muted" },
|
|
32
50
|
compoundVariants: [],
|
package/dist/cjs/index.js
CHANGED
|
@@ -67,6 +67,7 @@ __exportStar(require("./components/slider"), exports);
|
|
|
67
67
|
__exportStar(require("./components/spinner"), exports);
|
|
68
68
|
__exportStar(require("./components/stagebar"), exports);
|
|
69
69
|
__exportStar(require("./components/statcard"), exports);
|
|
70
|
+
__exportStar(require("./components/statusdot"), exports);
|
|
70
71
|
__exportStar(require("./components/switch"), exports);
|
|
71
72
|
__exportStar(require("./components/table"), exports);
|
|
72
73
|
__exportStar(require("./components/tabletoolbar"), exports);
|