@crisp-ui-kit/crisp 0.56.0 → 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/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/textlink/textlink.js +2 -2
- package/dist/cjs/components/textlink/textlink.recipe.js +18 -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 +8 -0
- 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/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/textlink/textlink.js +2 -2
- package/dist/esm/components/textlink/textlink.recipe.js +18 -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 +8 -0
- 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 +49 -6
- package/dist/select.css +80 -42
- package/dist/select.d.ts +3 -2
- package/dist/select.layered.css +80 -42
- package/dist/styles.css +199 -72
- package/dist/styles.layered.css +199 -72
- 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/textlink.css +9 -0
- package/dist/textlink.d.ts +2 -1
- package/dist/textlink.layered.css +9 -0
- package/dist/tokens.css +25 -0
- package/dist/tokens.json +49 -6
- 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/tabletoolbar/tabletoolbar.d.ts +14 -0
- package/dist/types/components/tabs/tabs.d.ts +9 -1
- package/dist/types/components/textlink/textlink.d.ts +8 -1
- package/dist/types/components/textlink/textlink.recipe.d.ts +10 -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 +5 -0
- package/dist/types/tokens/semantic.d.ts +18 -0
- package/package.json +1 -1
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
2
|
import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// it
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
3
|
+
// seed-design's Select, resolved value-for-value from
|
|
4
|
+
// `@seed-design/rootage`'s select.yaml / select-trigger.yaml / select-item.yaml
|
|
5
|
+
// (crisp/#867) — a deliberate, owner-requested exception to "measure Attio
|
|
6
|
+
// first": this ONE component follows seed-design, not Attio, the same way
|
|
7
|
+
// DatePicker already follows seed's calendar structure. Every px/colour below
|
|
8
|
+
// traces to a resolved seed token (see the new entries in tokens/scale.ts,
|
|
9
|
+
// tokens/palette.ts, tokens/semantic.ts and tokens/elevation.ts, each
|
|
10
|
+
// commented with the seed token it came from) — nothing here is measured
|
|
11
|
+
// live, because there is no live Attio reference for this shape any more.
|
|
12
|
+
//
|
|
13
|
+
// crisp keeps its own `searchable` filter field and `triggerLabel` (#846):
|
|
14
|
+
// seed has no search of its own, and the owner asked to keep both. The
|
|
15
|
+
// listbox stays portaled to `document.body` for the same reason #339
|
|
16
|
+
// established (no ancestor's transform/backdrop-filter can capture its
|
|
17
|
+
// position) — see the sibling comment on `globalCss` below for why its rules
|
|
18
|
+
// live there rather than nested under `&`.
|
|
19
|
+
//
|
|
20
|
+
// Two things seed has that this recipe deliberately does NOT reproduce:
|
|
21
|
+
// - A JS-driven exit animation (seed's `exitDuration`/`exitScale`/
|
|
22
|
+
// `exitOpacity`, played before unmount). Every other crisp overlay
|
|
23
|
+
// (Dialog, Menu, ContextMenu, Popover) unmounts synchronously on close and
|
|
24
|
+
// only animates the ENTER transition via `@starting-style` — adding a
|
|
25
|
+
// delayed-unmount mechanism just for Select would be a new architectural
|
|
26
|
+
// pattern with no precedent anywhere else in the repo, for one component.
|
|
27
|
+
// The enter animation (scale 0.95→1, opacity 0→1, seed's own curve) IS
|
|
28
|
+
// reproduced below.
|
|
29
|
+
// - A visible `divider` slot: `select.yaml` defines one, but nothing in
|
|
30
|
+
// crisp or GRC groups options with an explicit separator today, and
|
|
31
|
+
// AUTHORING's rule against building what nothing consumes applies to CSS
|
|
32
|
+
// too. The resolved value is in `~/grc-parity/seed-select-spec.md` for
|
|
33
|
+
// whoever adds it.
|
|
22
34
|
const config = defineRecipe({
|
|
23
35
|
name: "select",
|
|
24
36
|
base: `
|
|
@@ -26,87 +38,147 @@ const config = defineRecipe({
|
|
|
26
38
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
27
39
|
|
|
28
40
|
& .crisp-select-trigger {
|
|
29
|
-
box-sizing: border-box; display: inline-flex; align-items: center;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
transition: background-color .12s ease, box-shadow .12s ease;
|
|
41
|
+
box-sizing: border-box; display: inline-flex; align-items: center; width: 100%; min-width: 0; margin: 0;
|
|
42
|
+
border: 1px solid ${c.strokeSelectTrigger}; background: transparent; cursor: pointer;
|
|
43
|
+
font-family: inherit; color: ${c.fgSelectNeutral};
|
|
44
|
+
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);
|
|
34
45
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
/* seed defines no hover state for this trigger -- only pressed, invalid,
|
|
47
|
+
disabled and readonly. The scale-down is scoped to the CONTENT
|
|
48
|
+
(seed's scaleScope: content), not the button itself, so the border
|
|
49
|
+
never moves. */
|
|
50
|
+
/* Only the value scales, not the chevron: the chevron's own transform is
|
|
51
|
+
its open/close rotation, and a scale would fight it for one shared
|
|
52
|
+
CSS property when both are true at once (pressing while open). */
|
|
53
|
+
& .crisp-select-trigger:active:not(:disabled):not([aria-readonly="true"]) { background: ${c.bgSelectPressed}; }
|
|
54
|
+
& .crisp-select-trigger:active:not(:disabled):not([aria-readonly="true"]) .crisp-select-value { transform: scale(0.98); }
|
|
55
|
+
& .crisp-select-trigger:focus-visible { outline: 2px solid ${c.strokeFocusRing}; outline-offset: -2px; }
|
|
56
|
+
& .crisp-select-trigger:disabled { background: ${c.bgSelectDisabled}; cursor: not-allowed; }
|
|
57
|
+
& .crisp-select-trigger:disabled .crisp-select-value,
|
|
58
|
+
& .crisp-select-trigger:disabled .crisp-select-placeholder,
|
|
59
|
+
& .crisp-select-trigger:disabled .crisp-select-chevron { color: ${c.fgDisabled}; }
|
|
60
|
+
/* readonly: same fill as disabled, but the value/placeholder text stays
|
|
61
|
+
normal ink -- it reads as "answered and locked", not "answered and
|
|
62
|
+
greyed out" (select-trigger.yaml's readonly state only overrides
|
|
63
|
+
root.color; value/placeholder keep their enabled colours). */
|
|
64
|
+
& .crisp-select-trigger[aria-readonly="true"] { background: ${c.bgSelectDisabled}; cursor: default; }
|
|
65
|
+
& .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); }
|
|
66
|
+
& .crisp-select-placeholder { color: ${c.fgSelectPlaceholder}; }
|
|
67
|
+
/* closeRotateDuration/closeRotateTimingFunction: 100ms, seed's "easing"
|
|
68
|
+
curve. Overridden to 150ms on open (openRotateDuration) below --
|
|
69
|
+
same curve both ways, only the duration differs. */
|
|
70
|
+
& .crisp-select-chevron { flex-shrink: 0; color: ${c.fgNeutralMuted}; transition: transform .1s cubic-bezier(.35,0,.35,1); }
|
|
71
|
+
& .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); transition-duration: .15s; }
|
|
42
72
|
`,
|
|
43
73
|
globalCss: `
|
|
44
74
|
/* pointer-events: auto (#375): a modal Dialog sets pointer-events: none
|
|
45
|
-
on document.body while open
|
|
46
|
-
|
|
47
|
-
inherited the lock, so every option was unclickable, hit-testing
|
|
48
|
-
right through to whatever real element sat behind it. */
|
|
75
|
+
on document.body while open, and the portaled listbox is outside its
|
|
76
|
+
knowledge -- it inherited the lock without this. */
|
|
49
77
|
.crisp-select-listbox {
|
|
50
78
|
font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
51
|
-
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space
|
|
52
|
-
max-height:
|
|
53
|
-
border-radius: ${c.radius
|
|
54
|
-
box-shadow: ${e.
|
|
55
|
-
transform-origin: top left; opacity: 1; transform: none;
|
|
79
|
+
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space["2"]};
|
|
80
|
+
max-height: 480px; overflow-y: auto; padding: ${c.space["2"]};
|
|
81
|
+
border-radius: ${c.radius["3xl"]}; background: ${c.bgSelectListbox};
|
|
82
|
+
box-shadow: ${e.selectFloating};
|
|
83
|
+
transform-origin: top left; opacity: 1; transform: none;
|
|
84
|
+
transition: opacity .15s cubic-bezier(0,0,.15,1), transform .15s cubic-bezier(0,0,.15,1);
|
|
56
85
|
}
|
|
57
|
-
.crisp-select-listbox[data-up] { transform-origin: bottom; }
|
|
58
|
-
.crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.
|
|
59
|
-
.crisp-select-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["
|
|
86
|
+
.crisp-select-listbox[data-up] { transform-origin: bottom left; }
|
|
87
|
+
.crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
|
|
88
|
+
.crisp-select-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["2"]}) calc(-1 * ${c.space["2"]}) ${c.space["2"]}; padding: 0 ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgNeutralMuted}; }
|
|
60
89
|
.crisp-select-search svg { flex-shrink: 0; }
|
|
61
|
-
.crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.
|
|
62
|
-
.crisp-select-search input::placeholder { color: ${c.
|
|
63
|
-
.crisp-select-empty { padding: ${c.space["3"]} ${c.space["
|
|
64
|
-
/*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.crisp-select-label { padding: ${c.space["1.5"]} ${c.space["2"]} ${c.space["0.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
|
|
90
|
+
.crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgSelectNeutral}; }
|
|
91
|
+
.crisp-select-search input::placeholder { color: ${c.fgSelectPlaceholder}; }
|
|
92
|
+
.crisp-select-empty { padding: ${c.space["3"]} ${c.space["4"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgNeutralSubtle}; }
|
|
93
|
+
/* select.yaml's groupLabel -- font-size/line-height/weight are the same
|
|
94
|
+
across sizes, only paddingY differs (medium 8px, large 10px). */
|
|
95
|
+
.crisp-select-label { padding: ${c.space["2"]} ${c.space["4"]} 0; font-size: ${c.text.md}; line-height: 18px; font-weight: ${c.weight.regular}; color: ${c.fgNeutralSubtle}; }
|
|
68
96
|
.crisp-select-option {
|
|
69
|
-
box-sizing: border-box; display: flex; align-items: center;
|
|
70
|
-
padding:
|
|
71
|
-
font-family: inherit;
|
|
97
|
+
box-sizing: border-box; display: flex; align-items: center; flex-shrink: 0;
|
|
98
|
+
padding: ${c.space["2.5"]} ${c.space["4"]}; margin-inline: 0; border: none; background: none; border-radius: ${c.radius.xl};
|
|
99
|
+
font-family: inherit; color: ${c.fgSelectNeutral}; text-align: start; cursor: pointer; white-space: nowrap;
|
|
100
|
+
transition: background-color .15s cubic-bezier(.35,0,.35,1), margin-inline .15s cubic-bezier(.35,0,.35,1);
|
|
72
101
|
}
|
|
102
|
+
/* Keyboard/hover highlight keeps crisp's own visible neutral fill --
|
|
103
|
+
seed's spec has no "highlighted" state at all (only pressed/selected/
|
|
104
|
+
disabled), and its actual pressed tint (below) is a ~3% black hint,
|
|
105
|
+
too faint to serve as a keyboard-navigation indicator. */
|
|
73
106
|
.crisp-select-option[data-highlighted] { background: ${c.bgRaised}; }
|
|
74
|
-
.crisp-select-option[data-disabled] {
|
|
75
|
-
.crisp-select-option-
|
|
107
|
+
.crisp-select-option:active:not([data-disabled]) { background: ${c.bgSelectPressed}; margin-inline: ${c.space["2"]}; }
|
|
108
|
+
.crisp-select-option[data-disabled] { cursor: not-allowed; }
|
|
109
|
+
.crisp-select-option[data-disabled] .crisp-select-option-label,
|
|
110
|
+
.crisp-select-option[data-disabled] .crisp-select-option-description,
|
|
111
|
+
.crisp-select-option[data-disabled] .crisp-select-option-check { color: ${c.fgDisabled}; }
|
|
112
|
+
.crisp-select-option-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${c.radius.full}; margin-inline-end: ${c.space["2"]}; }
|
|
113
|
+
.crisp-select-option-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: ${c.space["0.5"]}; }
|
|
76
114
|
.crisp-select-option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
77
|
-
.crisp-select-option-
|
|
115
|
+
.crisp-select-option-description { min-width: 0; overflow: hidden; text-overflow: ellipsis; color: ${c.fgNeutralSubtle}; }
|
|
116
|
+
.crisp-select-option-check { flex-shrink: 0; color: ${c.fgSelectNeutral}; margin-inline-start: ${c.space["2"]}; }
|
|
78
117
|
`,
|
|
79
118
|
variants: {
|
|
80
|
-
//
|
|
81
|
-
// inline selects (e.g. the sort configurator).
|
|
82
|
-
variant: {
|
|
83
|
-
default: "",
|
|
84
|
-
ghost: `
|
|
85
|
-
& .crisp-select-trigger { background: transparent; box-shadow: none; }
|
|
86
|
-
& .crisp-select-trigger:hover { background: ${c.bgRaised}; box-shadow: none; }
|
|
87
|
-
& .crisp-select-trigger[data-open], & .crisp-select-trigger:focus-visible { background: transparent; box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
|
|
88
|
-
`,
|
|
89
|
-
},
|
|
90
|
-
// full (default): fills its container, as before. auto: sizes to content.
|
|
119
|
+
// full (default): fills its container. auto: sizes to content.
|
|
91
120
|
width: {
|
|
92
121
|
full: `width: 100%;`,
|
|
93
122
|
auto: `width: auto;`,
|
|
94
123
|
},
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
& .crisp-select-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
// seed's two trigger/item sizes (select-trigger.yaml / select-item.yaml).
|
|
125
|
+
// medium is the default -- closest to crisp's previous single size.
|
|
126
|
+
size: {
|
|
127
|
+
medium: `
|
|
128
|
+
& .crisp-select-trigger { height: ${c.size.selectTriggerMd}; border-radius: ${c.radius.md}; gap: ${c.space["2"]}; padding-inline: ${c.space["3.5"]}; }
|
|
129
|
+
& .crisp-select-value, & .crisp-select-placeholder { font-size: ${c.text.lg}; line-height: 19px; }
|
|
130
|
+
& .crisp-select-chevron { width: ${c.size.icon}; height: ${c.size.icon}; }
|
|
131
|
+
.crisp-select-option { gap: ${c.space["2"]}; }
|
|
132
|
+
.crisp-select-option-label { font-size: ${c.text.lg}; line-height: 19px; }
|
|
133
|
+
.crisp-select-option-description { font-size: ${c.text.sm}; line-height: 16px; }
|
|
134
|
+
.crisp-select-option-check { width: ${c.space["3"]}; height: ${c.space["3"]}; }
|
|
135
|
+
.crisp-select-label { padding-top: ${c.space["2"]}; }
|
|
105
136
|
`,
|
|
137
|
+
large: `
|
|
138
|
+
& .crisp-select-trigger { height: ${c.size.selectTriggerLg}; border-radius: ${c.radius.xl}; gap: ${c.space["2.5"]}; padding-inline: ${c.space["4"]}; }
|
|
139
|
+
& .crisp-select-value, & .crisp-select-placeholder { font-size: ${c.text.xl}; line-height: 22px; }
|
|
140
|
+
& .crisp-select-chevron { width: ${c.size.selectIconLg}; height: ${c.size.selectIconLg}; }
|
|
141
|
+
.crisp-select-option { gap: ${c.space["3"]}; }
|
|
142
|
+
.crisp-select-option-label { font-size: ${c.text.xl}; line-height: 22px; }
|
|
143
|
+
.crisp-select-option-description { font-size: ${c.text.md}; line-height: 18px; }
|
|
144
|
+
.crisp-select-option-check { width: ${c.space["3.5"]}; height: ${c.space["3.5"]}; }
|
|
145
|
+
.crisp-select-label { padding-top: ${c.space["2.5"]}; }
|
|
146
|
+
`,
|
|
147
|
+
},
|
|
148
|
+
// Real border width+colour change (seed: strokeWidth 1px→2px,
|
|
149
|
+
// strokeColor neutral-weak→critical-solid), not a box-shadow ring --
|
|
150
|
+
// crisp's own field/switch invalid rings don't apply here since this
|
|
151
|
+
// component's whole border model changed to match seed's real stroke.
|
|
152
|
+
invalid: {
|
|
153
|
+
true: `& .crisp-select-trigger { border-width: 2px; border-color: ${c.strokeSelectInvalid}; }`,
|
|
106
154
|
false: ``,
|
|
107
155
|
},
|
|
156
|
+
// ghost: borderless at rest, a subtle fill on hover -- inline selects
|
|
157
|
+
// (e.g. the sort configurator, and datatable's own cell editors, which
|
|
158
|
+
// embed at whatever height their row already is, not seed's fixed
|
|
159
|
+
// trigger height). Not a seed state; kept from crisp's existing API
|
|
160
|
+
// surface, layered on top of the seed base.
|
|
161
|
+
//
|
|
162
|
+
// Declared LAST on purpose: `resolveRecipe` emits one CSS rule per
|
|
163
|
+
// variant, in the order this object lists them, and same-specificity
|
|
164
|
+
// rules resolve by source order -- ghost's `height: 100%` below must
|
|
165
|
+
// come after `size`'s fixed height to win the cascade. Move `variant`
|
|
166
|
+
// earlier and every ghost consumer's trigger silently grows to 40/52px
|
|
167
|
+
// inside its 36px row again.
|
|
168
|
+
variant: {
|
|
169
|
+
default: "",
|
|
170
|
+
ghost: `
|
|
171
|
+
& .crisp-select-trigger { height: 100%; border-color: transparent; }
|
|
172
|
+
& .crisp-select-trigger:hover:not(:disabled) { background: ${c.bgSelectPressed}; }
|
|
173
|
+
`,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
defaultVariants: {
|
|
177
|
+
width: "full",
|
|
178
|
+
size: "medium",
|
|
179
|
+
invalid: false,
|
|
180
|
+
variant: "default",
|
|
108
181
|
},
|
|
109
|
-
defaultVariants: { variant: "default", width: "full", invalid: false },
|
|
110
182
|
compoundVariants: [],
|
|
111
183
|
});
|
|
112
184
|
export const selectRecipe = Object.assign(resolveRecipe(config), { config });
|
|
@@ -733,7 +733,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
733
733
|
: sorts.length
|
|
734
734
|
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortField}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
735
735
|
: sortLabel;
|
|
736
|
-
return (_jsx("div", { ref: rootRef, className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortTriggerLabel || !sorts.length ? (sortedLabel) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-tabletoolbar-sorted-prefix", children: crispMessages["Sorted by"] ?? "Sorted by" }), " ", _jsx("span", { className: "crisp-tabletoolbar-sorted-field", children: sortField }), sorts.length > 1 && (_jsxs("span", { className: "crisp-tabletoolbar-sorted-count", children: ["
|
|
736
|
+
return (_jsx("div", { ref: rootRef, className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortTriggerLabel || !sorts.length ? (sortedLabel) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-tabletoolbar-sorted-prefix", children: crispMessages["Sorted by"] ?? "Sorted by" }), " ", _jsx("span", { className: "crisp-tabletoolbar-sorted-field", children: sortField }), sorts.length > 1 && (_jsxs("span", { className: "crisp-tabletoolbar-sorted-count", children: ["+", sorts.length - 1] }))] }))] }), openMenu === "sort" && (_jsxs("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
737
737
|
if (dragSort !== null)
|
|
738
738
|
e.preventDefault();
|
|
739
739
|
}, onDrop: () => {
|
|
@@ -928,12 +928,12 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
928
928
|
* contents` so it contributes no box of its own — the same technique
|
|
929
929
|
* `TableToolbar`'s own root already uses to disappear from layout.
|
|
930
930
|
*/
|
|
931
|
-
export const ToolbarTool = React.forwardRef(({ active, ghost, className, ...rest }, ref) => (_jsx("span", { className: tabletoolbarRecipe(), children:
|
|
931
|
+
export const ToolbarTool = React.forwardRef(({ active, ghost, count, className, children, ...rest }, ref) => (_jsx("span", { className: tabletoolbarRecipe(), children: _jsxs("button", { ref: ref, type: "button", className: [
|
|
932
932
|
"crisp-tabletoolbar-tool",
|
|
933
933
|
active && "crisp-tabletoolbar-tool--active",
|
|
934
934
|
ghost && "crisp-tabletoolbar-tool--ghost",
|
|
935
935
|
className,
|
|
936
936
|
]
|
|
937
937
|
.filter(Boolean)
|
|
938
|
-
.join(" "), ...rest }) })));
|
|
938
|
+
.join(" "), ...rest, children: [children, count != null && (_jsx("span", { className: "crisp-tabletoolbar-tool-count", children: count }))] }) })));
|
|
939
939
|
ToolbarTool.displayName = "ToolbarTool";
|
|
@@ -104,9 +104,33 @@ const importExportChrome = `
|
|
|
104
104
|
// vs 14px) and its own mid-grey, distinct from the "Sorted by" muted tone.
|
|
105
105
|
// TableToolbar-only, same reasoning as `filterChipChrome` above (RecordsTable
|
|
106
106
|
// keeps its own older Sort label markup, not touched here).
|
|
107
|
+
// crisp#876: re-measured live -- the "+N" isn't just smaller/greyer text,
|
|
108
|
+
// it's a real chip: `background: rgba(0,0,0,.04)`, `box-shadow: inset 0 0 0
|
|
109
|
+
// 1px rgb(238,239,241)` (borderSubtle), `border-radius: 6px`, `padding: 0
|
|
110
|
+
// 4px`, 16px tall, colour `rgb(80,81,85)` -- bit-for-bit crisp's own Badge
|
|
111
|
+
// `tone="neutral" size="small"` shape, so this reuses those exact tokens
|
|
112
|
+
// (`fgNeutralSoft`, `borderSubtle`, `radius.sm`, `space["1"]`) rather than
|
|
113
|
+
// re-deriving new ones or leaving the old 73%-mix colour approximation.
|
|
107
114
|
const sortedLabelChrome = `
|
|
108
115
|
& .crisp-tabletoolbar-sorted-prefix { color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
109
|
-
& .crisp-tabletoolbar-sorted-count {
|
|
116
|
+
& .crisp-tabletoolbar-sorted-count { display: inline-flex; align-items: center; height: 16px; box-sizing: border-box;
|
|
117
|
+
padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent);
|
|
118
|
+
box-shadow: inset 0 0 0 1px ${c.borderSubtle}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft}; }`;
|
|
119
|
+
// crisp#875: a general count-badge slot for `ToolbarTool` (a screen's own
|
|
120
|
+
// quick filter, e.g. "My tasks •5") -- the SAME measured shape as
|
|
121
|
+
// `sorted-count` above, independently re-confirmed on a second live Attio
|
|
122
|
+
// surface this round (a Kanban board's own stage-column count, "Idea 0":
|
|
123
|
+
// identical `data-variant="tertiary" data-size="16"` chip, same bg/ring/
|
|
124
|
+
// radius/padding/colour, plus `font-variant-numeric: tabular-nums` for
|
|
125
|
+
// digit alignment past a single character). Same reasoning as
|
|
126
|
+
// search/menu-input's own duplicated ring formula elsewhere in this file:
|
|
127
|
+
// a distinct class, not a shared one, so `sorted-count`'s own meaning
|
|
128
|
+
// doesn't get diluted by an unrelated consumer.
|
|
129
|
+
const toolCountChrome = `
|
|
130
|
+
& .crisp-tabletoolbar-tool-count { display: inline-flex; align-items: center; height: 16px; box-sizing: border-box;
|
|
131
|
+
padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent);
|
|
132
|
+
box-shadow: inset 0 0 0 1px ${c.borderSubtle}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft};
|
|
133
|
+
font-variant-numeric: tabular-nums; }`;
|
|
110
134
|
// The table toolbar, measured: 28px controls, the Sort/Filter buttons and
|
|
111
135
|
// the menus they open, sort-condition rows, and the filter chips. The chrome
|
|
112
136
|
// itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
|
|
@@ -126,6 +150,7 @@ ${toolbarChromeBody("tabletoolbar")}
|
|
|
126
150
|
${filterChipChrome}
|
|
127
151
|
${importExportChrome}
|
|
128
152
|
${sortedLabelChrome}
|
|
153
|
+
${toolCountChrome}
|
|
129
154
|
${rtlFlipCss}
|
|
130
155
|
`,
|
|
131
156
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
2
3
|
import * as React from "react";
|
|
4
|
+
import { Menu, MenuItem } from "../menu/index.js";
|
|
3
5
|
import { tabsRecipe } from "./tabs.recipe.js";
|
|
4
6
|
import { useTablistKeyboard } from "./tabs-keyboard.js";
|
|
5
7
|
/**
|
|
@@ -8,7 +10,7 @@ import { useTablistKeyboard } from "./tabs-keyboard.js";
|
|
|
8
10
|
* Home/End jump); activation follows focus. Controlled via
|
|
9
11
|
* `value`/`onValueChange`, or uncontrolled from `defaultValue`.
|
|
10
12
|
*/
|
|
11
|
-
export function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, className, fill, "aria-label": ariaLabel, ...rest }) {
|
|
13
|
+
export function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, moreLabel = (n) => `+${n} more`, moreMenuLabel = "More tabs", className, fill, "aria-label": ariaLabel, ...rest }) {
|
|
12
14
|
// Scoped to this instance: `tab-${value}` and `panel-${value}` are global,
|
|
13
15
|
// so two tab bars sharing a value mint the same id twice and every
|
|
14
16
|
// aria-labelledby resolves to the first one (#227).
|
|
@@ -41,16 +43,73 @@ export function Tabs({ tabs, value, defaultValue, onValueChange, leading, childr
|
|
|
41
43
|
ro.observe(list);
|
|
42
44
|
return () => ro.disconnect();
|
|
43
45
|
}, [active, tabs]);
|
|
46
|
+
// crisp#863 -- responsive "+N more" overflow, measured live: Attio
|
|
47
|
+
// collapses the TRAILING tabs into a single trigger once the strip runs
|
|
48
|
+
// out of room, rather than letting the row wrap or overrun. Every tab
|
|
49
|
+
// (and the "+N more" trigger itself) stays permanently mounted -- an
|
|
50
|
+
// overflowed one gets `data-tabs-overflow-hidden` (position: absolute;
|
|
51
|
+
// visibility: hidden in the recipe), which removes it from the flex row
|
|
52
|
+
// WITHOUT ever making its `offsetWidth` unmeasurable the way `display:
|
|
53
|
+
// none` would. That's what lets this recompute purely from cached
|
|
54
|
+
// per-tab widths on every resize, with no flash: a naive
|
|
55
|
+
// measure-everything-visible-first pass would show the full unclipped
|
|
56
|
+
// row for one frame before collapsing it.
|
|
57
|
+
const [hiddenFrom, setHiddenFrom] = React.useState(null);
|
|
58
|
+
const moreRef = React.useRef(null);
|
|
59
|
+
React.useLayoutEffect(() => {
|
|
60
|
+
const list = listRef.current;
|
|
61
|
+
if (!list || tabs.length === 0)
|
|
62
|
+
return;
|
|
63
|
+
const recompute = () => {
|
|
64
|
+
const containerWidth = list.clientWidth;
|
|
65
|
+
// Not yet laid out (e.g. this render, or the test environment, which
|
|
66
|
+
// never computes real box metrics) -- never collapse everything on a
|
|
67
|
+
// 0-width read, that's a false "nothing fits" rather than "not
|
|
68
|
+
// measurable yet".
|
|
69
|
+
if (containerWidth <= 0)
|
|
70
|
+
return;
|
|
71
|
+
const els = tabs.map((t) => list.querySelector(`[data-value="${CSS.escape(t.value)}"]`));
|
|
72
|
+
if (els.some((el) => !el))
|
|
73
|
+
return;
|
|
74
|
+
const gap = 4; // ${c.space["1"]}, the list's own gap (recipe.ts)
|
|
75
|
+
const moreWidth = moreRef.current?.offsetWidth ?? 0;
|
|
76
|
+
let used = 0;
|
|
77
|
+
let cutoff = tabs.length;
|
|
78
|
+
for (let i = 0; i < els.length; i++) {
|
|
79
|
+
const w = (els[i]?.offsetWidth ?? 0) + (i > 0 ? gap : 0);
|
|
80
|
+
const reserve = i < tabs.length - 1 ? moreWidth + gap : 0;
|
|
81
|
+
if (used + w + reserve > containerWidth) {
|
|
82
|
+
cutoff = i;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
used += w;
|
|
86
|
+
}
|
|
87
|
+
// The active tab is never allowed to fall into the overflow menu --
|
|
88
|
+
// not itself measured live (Attio's own fixture never exercised
|
|
89
|
+
// navigating INTO an overflowed tab first), but "the tab you're on
|
|
90
|
+
// stays visible" is the safe, unsurprising default for any tab UI.
|
|
91
|
+
const activeIndex = tabs.findIndex((t) => t.value === active);
|
|
92
|
+
if (activeIndex >= 0 && activeIndex >= cutoff)
|
|
93
|
+
cutoff = activeIndex + 1;
|
|
94
|
+
setHiddenFrom(cutoff >= tabs.length ? null : cutoff);
|
|
95
|
+
};
|
|
96
|
+
recompute();
|
|
97
|
+
const ro = new ResizeObserver(recompute);
|
|
98
|
+
ro.observe(list);
|
|
99
|
+
return () => ro.disconnect();
|
|
100
|
+
}, [tabs, active]);
|
|
101
|
+
const hiddenTabs = hiddenFrom == null ? [] : tabs.slice(hiddenFrom);
|
|
44
102
|
const { onKeyDown, tabIndexFor } = useTablistKeyboard({
|
|
45
103
|
tabs,
|
|
46
104
|
active,
|
|
47
105
|
select,
|
|
48
106
|
listRef,
|
|
49
107
|
});
|
|
50
|
-
return (_jsxs("div", { className: tabsRecipe({ fill, className }), ...rest, children: [_jsxs("div", { ref: listRef, className: "crisp-tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && _jsx("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t) => {
|
|
108
|
+
return (_jsxs("div", { className: tabsRecipe({ fill, className }), ...rest, children: [_jsxs("div", { ref: listRef, className: "crisp-tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && _jsx("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t, i) => {
|
|
51
109
|
const selected = t.value === active;
|
|
52
|
-
|
|
53
|
-
|
|
110
|
+
const overflowHidden = hiddenFrom != null && i >= hiddenFrom;
|
|
111
|
+
return (_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, _jsx("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && (_jsx("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value));
|
|
112
|
+
}), _jsx("span", { ref: moreRef, className: "crisp-tabs-more", "data-tabs-overflow-hidden": hiddenTabs.length === 0 ? "" : undefined, children: _jsx(Menu, { "aria-label": moreMenuLabel, trigger: _jsxs("button", { type: "button", className: "crisp-tabs-tab", children: [_jsx("span", { className: "tabs-tab-label", children: moreLabel(hiddenTabs.length) }), _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), children: hiddenTabs.map((t) => (_jsxs(MenuItem, { icon: t.icon, onSelect: () => select(t.value), children: [t.label, t.badge != null && (_jsx("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value))) }) }), indicator && (_jsx("span", { className: "crisp-tabs-indicator", style: {
|
|
54
113
|
width: indicator.width,
|
|
55
114
|
transform: `translateX(${indicator.left}px)`,
|
|
56
115
|
} }))] }), children && (_jsx("div", { className: "crisp-tabs-panel", role: "tabpanel", id: panelId(active), "aria-labelledby": tabId(active),
|
|
@@ -54,6 +54,19 @@ const config = defineRecipe({
|
|
|
54
54
|
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 14px; height: 14px; padding: 0 ${c.space["1"]}; border-radius: 4px; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); box-shadow: inset 0 0 0 1px ${c.borderSubtle}; font-size: 10px; line-height: 14px; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft}; }
|
|
55
55
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${c.fgPrimary}; }
|
|
56
56
|
& .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; }
|
|
57
|
+
/* crisp#863: the "+N more" overflow trigger, measured live -- its
|
|
58
|
+
computed style is identical to a plain .crisp-tabs-tab (same pill,
|
|
59
|
+
same 6px icon/label gap the tab class already gives via
|
|
60
|
+
space["1.5"]), and the dropdown it opens is crisp's own Menu/
|
|
61
|
+
MenuItem chrome (already matches Attio's popover, nothing new to
|
|
62
|
+
style there). Only the wrapper needs a rule: an overflowed tab (or
|
|
63
|
+
the trigger itself, when nothing is hidden) is taken out of the
|
|
64
|
+
flex row and out of the a11y tree without ever making its own
|
|
65
|
+
offsetWidth unmeasurable the way display:none would -- that's what
|
|
66
|
+
lets the tabs.tsx effect recompute the cutoff from cached widths on
|
|
67
|
+
every resize with no flash of an unclipped row. */
|
|
68
|
+
& .crisp-tabs-more { display: inline-flex; }
|
|
69
|
+
& [data-tabs-overflow-hidden] { position: absolute; visibility: hidden; pointer-events: none; }
|
|
57
70
|
& .crisp-tabs-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; padding-top: ${c.space["8"]}; }
|
|
58
71
|
& .crisp-tabs-panel:focus-visible { outline: none; }
|
|
59
72
|
`,
|
|
@@ -7,7 +7,7 @@ import { textlinkRecipe } from "./textlink.recipe.js";
|
|
|
7
7
|
* `event.preventDefault()` pairs it with client-side routing (e.g.
|
|
8
8
|
* react-router's own `Link`).
|
|
9
9
|
*/
|
|
10
|
-
export function TextLink({ tone = "muted", className, ...rest }) {
|
|
11
|
-
return _jsx("a", { className: textlinkRecipe({ tone, className }), ...rest });
|
|
10
|
+
export function TextLink({ tone = "muted", size, className, ...rest }) {
|
|
11
|
+
return _jsx("a", { className: textlinkRecipe({ tone, size, className }), ...rest });
|
|
12
12
|
}
|
|
13
13
|
TextLink.displayName = "TextLink";
|
|
@@ -24,6 +24,24 @@ const config = defineRecipe({
|
|
|
24
24
|
// that only goes brand on hover.
|
|
25
25
|
muted: `font-size: ${c.text.md}; font-weight: ${c.weight.regular}; color: ${c.fgMuted}; &:hover { color: ${c.bgBrandSolid}; }`,
|
|
26
26
|
},
|
|
27
|
+
// crisp#874: an explicit, OPTIONAL size override -- no `defaultVariants`
|
|
28
|
+
// entry below, so omitting `size` emits no `--size_*` class at all and
|
|
29
|
+
// each tone's own hard-coded font-size (above) stands completely
|
|
30
|
+
// unchanged. Only when a caller passes `size` explicitly does this
|
|
31
|
+
// override it: `size`'s CSS block is emitted AFTER `tone`'s in the
|
|
32
|
+
// generated stylesheet (`generate-recipes.mjs` walks `variants` in
|
|
33
|
+
// object-key order), so at equal specificity it wins the cascade.
|
|
34
|
+
// Measured live (a running GRC server, `getComputedStyle`):
|
|
35
|
+
// internal/grc's two primary-column record links
|
|
36
|
+
// (routes/lists/Lists.tsx:51, routes/registers/Registers.tsx:63) are
|
|
37
|
+
// 14px/500 ambient, inherited from DataTable's own
|
|
38
|
+
// `.crisp-datatable-entity-name` -- `size="lg"` matches that context
|
|
39
|
+
// without changing `brand`'s weight/colour.
|
|
40
|
+
size: {
|
|
41
|
+
sm: `font-size: ${c.text.sm};`,
|
|
42
|
+
md: `font-size: ${c.text.md};`,
|
|
43
|
+
lg: `font-size: ${c.text.lg};`,
|
|
44
|
+
},
|
|
27
45
|
},
|
|
28
46
|
defaultVariants: { tone: "muted" },
|
|
29
47
|
compoundVariants: [],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Activity as ActivityIcon, CalendarClock, ChevronDown, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, Mail, Maximize2, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search,
|
|
2
|
+
import { Activity as ActivityIcon, CalendarClock, ChevronDown, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, Mail, Maximize2, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search, Sparkles, SquareCheck, Star, Trash2, Users, UsersRound, Workflow, X, Zap, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Avatar } from "../../components/avatar/index.js";
|
|
5
5
|
import { Checkbox } from "../../components/checkbox/index.js";
|
|
@@ -38,8 +38,12 @@ export const recordPageLabels = {
|
|
|
38
38
|
copyRecordLink: "Copy record link",
|
|
39
39
|
deleteRecord: "Delete record",
|
|
40
40
|
linkRecords: "Link records",
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Tooltip copy re-measured live for #865 (was "Log a note"/"Create a
|
|
42
|
+
// task" — the field names stay, only the visible text changes to match
|
|
43
|
+
// Attio's own React-tooltip strings on these two icon buttons exactly).
|
|
44
|
+
logANote: "New note",
|
|
45
|
+
createATask: "New task",
|
|
46
|
+
runWorkflow: "Run workflow",
|
|
43
47
|
fullScreen: "Full screen",
|
|
44
48
|
exitFullScreen: "Exit full screen",
|
|
45
49
|
resizeDetails: "Resize details column",
|
|
@@ -419,7 +423,7 @@ RecordDetails.displayName = "RecordDetails";
|
|
|
419
423
|
* Emails sections (Notes & Tasks are live — add and check things off). Drop it
|
|
420
424
|
* into the app's main content area.
|
|
421
425
|
*/
|
|
422
|
-
export function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, ...rest }) {
|
|
426
|
+
export function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, quickActions: quickActionsProp, ...rest }) {
|
|
423
427
|
const t = useRecordPageLabels(labels);
|
|
424
428
|
const [favorite, setFavorite] = React.useState(false);
|
|
425
429
|
// Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
|
|
@@ -528,6 +532,15 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
528
532
|
},
|
|
529
533
|
];
|
|
530
534
|
const highlights = highlightsProp === undefined ? defaultHighlights : highlightsProp;
|
|
535
|
+
// #865: the built-in CRM icon set for the quick-action row, used when the
|
|
536
|
+
// caller omits `quickActions` -- same "caller's own list, not a merge"
|
|
537
|
+
// choice `tabs` (#379) and `highlights` (#528) already make.
|
|
538
|
+
const defaultQuickActions = [
|
|
539
|
+
{ label: t.logANote, icon: FileText },
|
|
540
|
+
{ label: t.runWorkflow, icon: Workflow },
|
|
541
|
+
{ label: t.createATask, icon: SquareCheck },
|
|
542
|
+
];
|
|
543
|
+
const quickActions = quickActionsProp ?? defaultQuickActions;
|
|
531
544
|
const ti = { size: 15, strokeWidth: 1.75 };
|
|
532
545
|
const builtinTabs = [
|
|
533
546
|
{ value: "overview", label: t.tabs.overview, icon: _jsx(LayoutGrid, { ...ti }) },
|
|
@@ -636,7 +649,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
636
649
|
const effectivePanel = tabsProp
|
|
637
650
|
? (active) => tabsProp.find((tb) => tb.id === active)?.render()
|
|
638
651
|
: builtinPanel;
|
|
639
|
-
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }),
|
|
652
|
+
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Plus, { size: 16, strokeWidth: 1.75 }), t.addToList] }), quickActions.map((a, i) => (_jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": a.label, onClick: a.onSelect, children: _jsx(a.icon, { size: 15, strokeWidth: 1.75 }) }, i)))] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": DETAILS_MIN_W, "aria-valuemax": DETAILS_MAX_W, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
|
|
640
653
|
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
641
654
|
return;
|
|
642
655
|
// The pointer drag learned its direction in #212; this did not.
|
|
@@ -56,4 +56,11 @@ export const elevation = {
|
|
|
56
56
|
* nobody measured for them.
|
|
57
57
|
*/
|
|
58
58
|
fieldNoRing: `0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04)`,
|
|
59
|
+
/**
|
|
60
|
+
* Select's own listbox shadow (crisp/#867) — seed-design's `$shadow.s3`, a
|
|
61
|
+
* single layer, NOT `floating` above. This component follows seed, not
|
|
62
|
+
* Attio, for the same reason `DatePicker` follows seed's calendar
|
|
63
|
+
* structure: an explicit owner request to match seed-design exactly.
|
|
64
|
+
*/
|
|
65
|
+
selectFloating: `0 4px 16px ${c.shadowSelect}`,
|
|
59
66
|
};
|