@crisp-ui-kit/crisp 0.57.2 → 0.57.3
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/button.css +3 -0
- package/dist/button.d.ts +2 -2
- package/dist/button.layered.css +3 -0
- package/dist/chip.css +1 -1
- package/dist/chip.layered.css +1 -1
- package/dist/cjs/components/breadcrumb/breadcrumb.js +1 -1
- package/dist/cjs/components/button/button.recipe.js +28 -0
- package/dist/cjs/components/chip/chip.recipe.js +3 -1
- package/dist/cjs/components/datatable/datatable.recipe.js +10 -4
- package/dist/cjs/components/datepickerfield/datepickerfield.js +2 -2
- package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +16 -3
- package/dist/cjs/components/menu/menu.js +18 -2
- package/dist/cjs/components/table/table.recipe.js +7 -1
- package/dist/cjs/core/floating-portal.js +7 -4
- package/dist/cjs/product/inbox/inbox.js +11 -3
- package/dist/cjs/product/recordpage/index.js +2 -1
- package/dist/cjs/product/recordpage/recordpage.js +49 -3
- package/dist/cjs/product/recordpage/recordpage.recipe.js +25 -0
- package/dist/datatable.css +10 -4
- package/dist/datatable.layered.css +10 -4
- package/dist/datepickerfield.css +8 -2
- package/dist/datepickerfield.d.ts +2 -2
- package/dist/datepickerfield.layered.css +8 -2
- package/dist/esm/components/breadcrumb/breadcrumb.js +1 -1
- package/dist/esm/components/button/button.recipe.js +28 -0
- package/dist/esm/components/chip/chip.recipe.js +3 -1
- package/dist/esm/components/datatable/datatable.recipe.js +10 -4
- package/dist/esm/components/datepickerfield/datepickerfield.js +2 -2
- package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +16 -3
- package/dist/esm/components/menu/menu.js +18 -2
- package/dist/esm/components/table/table.recipe.js +7 -1
- package/dist/esm/core/floating-portal.js +7 -4
- package/dist/esm/product/inbox/inbox.js +11 -3
- package/dist/esm/product/recordpage/index.js +1 -1
- package/dist/esm/product/recordpage/recordpage.js +49 -4
- package/dist/esm/product/recordpage/recordpage.recipe.js +25 -0
- package/dist/recordpage.css +23 -0
- package/dist/recordpage.layered.css +23 -0
- package/dist/styles.css +52 -9
- package/dist/styles.layered.css +52 -9
- package/dist/table.css +7 -2
- package/dist/table.layered.css +7 -2
- package/dist/types/components/breadcrumb/breadcrumb.d.ts +9 -1
- package/dist/types/components/button/button.d.ts +2 -0
- package/dist/types/components/button/button.recipe.d.ts +13 -0
- package/dist/types/components/chip/chip.recipe.d.ts +1 -1
- package/dist/types/components/datepickerfield/datepickerfield.d.ts +4 -1
- package/dist/types/components/datepickerfield/datepickerfield.recipe.d.ts +18 -3
- package/dist/types/core/floating-portal.d.ts +6 -5
- package/dist/types/product/inbox/inbox.d.ts +13 -1
- package/dist/types/product/recordpage/index.d.ts +1 -1
- package/dist/types/product/recordpage/recordpage.d.ts +46 -0
- package/package.json +1 -1
package/dist/datepickerfield.css
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
.crisp-datepickerfield {
|
|
2
2
|
display: inline-flex; font-family: var(--crisp-font-sans);
|
|
3
|
-
& .crisp-datepickerfield-box { display: inline-flex; align-items: center; box-sizing: border-box; height: 32px; margin: 0; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); }
|
|
3
|
+
& .crisp-datepickerfield-box { display: inline-flex; align-items: center; width: 100%; box-sizing: border-box; height: 32px; margin: 0; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); }
|
|
4
4
|
& .crisp-datepickerfield-box[data-ghost] { background: transparent; box-shadow: none; }
|
|
5
5
|
& .crisp-datepickerfield-box[data-ghost]:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
|
|
6
|
-
& .crisp-datepickerfield-trigger { display: inline-flex; align-items: center; gap: var(--crisp-space-2); height: 100%; padding: 0 var(--crisp-space-2-5); margin: 0; border: none; background: none; color: inherit; font-family: inherit; font-size: inherit; white-space: nowrap; cursor: pointer; }
|
|
6
|
+
& .crisp-datepickerfield-trigger { flex: 1; min-width: 0; display: inline-flex; align-items: center; gap: var(--crisp-space-2); height: 100%; padding: 0 var(--crisp-space-2-5); margin: 0; border: none; background: none; color: inherit; font-family: inherit; font-size: inherit; white-space: nowrap; cursor: pointer; }
|
|
7
7
|
& .crisp-datepickerfield-box[data-placeholder] .crisp-datepickerfield-trigger { color: var(--crisp-fg-muted); }
|
|
8
8
|
& .crisp-datepickerfield-trigger svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
|
|
9
9
|
/* Reuses TableToolbar's measured filter-chip remove button
|
|
@@ -18,3 +18,9 @@
|
|
|
18
18
|
& .crisp-datepickerfield-preset { height: var(--crisp-size-chip); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-chip); background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-sm); cursor: pointer; }
|
|
19
19
|
& .crisp-datepickerfield-preset:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); }
|
|
20
20
|
}
|
|
21
|
+
.crisp-datepickerfield--width_full {
|
|
22
|
+
width: 100%;
|
|
23
|
+
}
|
|
24
|
+
.crisp-datepickerfield--width_auto {
|
|
25
|
+
width: auto;
|
|
26
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RecipeFn } from "../core/types";
|
|
2
2
|
|
|
3
3
|
export interface DatepickerfieldVariants {
|
|
4
|
-
|
|
4
|
+
width?: "full" | "auto";
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export declare const datepickerfieldRecipe: RecipeFn<{
|
|
7
|
+
export declare const datepickerfieldRecipe: RecipeFn<{ width: { full: string; auto: string } }>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
@layer crisp-components {
|
|
2
2
|
.crisp-datepickerfield {
|
|
3
3
|
display: inline-flex; font-family: var(--crisp-font-sans);
|
|
4
|
-
& .crisp-datepickerfield-box { display: inline-flex; align-items: center; box-sizing: border-box; height: 32px; margin: 0; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); }
|
|
4
|
+
& .crisp-datepickerfield-box { display: inline-flex; align-items: center; width: 100%; box-sizing: border-box; height: 32px; margin: 0; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); }
|
|
5
5
|
& .crisp-datepickerfield-box[data-ghost] { background: transparent; box-shadow: none; }
|
|
6
6
|
& .crisp-datepickerfield-box[data-ghost]:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
|
|
7
|
-
& .crisp-datepickerfield-trigger { display: inline-flex; align-items: center; gap: var(--crisp-space-2); height: 100%; padding: 0 var(--crisp-space-2-5); margin: 0; border: none; background: none; color: inherit; font-family: inherit; font-size: inherit; white-space: nowrap; cursor: pointer; }
|
|
7
|
+
& .crisp-datepickerfield-trigger { flex: 1; min-width: 0; display: inline-flex; align-items: center; gap: var(--crisp-space-2); height: 100%; padding: 0 var(--crisp-space-2-5); margin: 0; border: none; background: none; color: inherit; font-family: inherit; font-size: inherit; white-space: nowrap; cursor: pointer; }
|
|
8
8
|
& .crisp-datepickerfield-box[data-placeholder] .crisp-datepickerfield-trigger { color: var(--crisp-fg-muted); }
|
|
9
9
|
& .crisp-datepickerfield-trigger svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
|
|
10
10
|
/* Reuses TableToolbar's measured filter-chip remove button
|
|
@@ -19,4 +19,10 @@
|
|
|
19
19
|
& .crisp-datepickerfield-preset { height: var(--crisp-size-chip); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-chip); background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-sm); cursor: pointer; }
|
|
20
20
|
& .crisp-datepickerfield-preset:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); }
|
|
21
21
|
}
|
|
22
|
+
.crisp-datepickerfield--width_full {
|
|
23
|
+
width: 100%;
|
|
24
|
+
}
|
|
25
|
+
.crisp-datepickerfield--width_auto {
|
|
26
|
+
width: auto;
|
|
27
|
+
}
|
|
22
28
|
}
|
|
@@ -19,7 +19,7 @@ export function Breadcrumb({ items, separator, "aria-label": ariaLabelProp, clas
|
|
|
19
19
|
return (_jsx("nav", { "aria-label": ariaLabel, className: breadcrumbRecipe({ className }), ...rest, children: items.map((item, i) => {
|
|
20
20
|
const last = i === items.length - 1;
|
|
21
21
|
const content = (_jsxs(_Fragment, { children: [item.icon, item.label] }));
|
|
22
|
-
return (_jsxs(React.Fragment, { children: [last ? (_jsx("span", { className: "crisp-breadcrumb-item", "aria-current": "page", children: content })) : item.href ? (_jsx("a", { className: "crisp-breadcrumb-item", href: item.href, children: content })) : (_jsx("button", { type: "button", className: "crisp-breadcrumb-item", onClick: item.onClick, children: content })), !last && _jsx("span", { className: "crisp-breadcrumb-sep", children: sep })] }, i));
|
|
22
|
+
return (_jsxs(React.Fragment, { children: [last ? (_jsx("span", { className: "crisp-breadcrumb-item", "aria-current": "page", children: content })) : item.href ? (_jsx("a", { className: "crisp-breadcrumb-item", href: item.href, onClick: item.onClick, children: content })) : (_jsx("button", { type: "button", className: "crisp-breadcrumb-item", onClick: item.onClick, children: content })), !last && _jsx("span", { className: "crisp-breadcrumb-sep", children: sep })] }, i));
|
|
23
23
|
}) }));
|
|
24
24
|
}
|
|
25
25
|
Breadcrumb.displayName = "Breadcrumb";
|
|
@@ -121,6 +121,34 @@ const spec = defineSpec({
|
|
|
121
121
|
fontSize: c.text.md,
|
|
122
122
|
borderRadius: c.radius.md,
|
|
123
123
|
},
|
|
124
|
+
// crisp#688: Attio's own 20px small button, split out of #682 rather
|
|
125
|
+
// than redefining `sm` -- 72 existing `size="sm"` call sites (sidebar,
|
|
126
|
+
// comments, recordpage, workspacegeneralsettings, personchip,
|
|
127
|
+
// datatable) already rely on sm's 24px, unverified against `xs`'s own
|
|
128
|
+
// context. Box measured live on app.attio.com Home/Tasks ("View all",
|
|
129
|
+
// "Today, <date>", the date-stepper's Previous/Next-day icon
|
|
130
|
+
// buttons): 20px height, 6px radius, 6px inline padding on text
|
|
131
|
+
// buttons (a 20x20 icon-only button centers a 16px icon the same way
|
|
132
|
+
// shape="icon" already does for every other size, so it needs no
|
|
133
|
+
// extra rule), 4px gap between multiple children.
|
|
134
|
+
//
|
|
135
|
+
// The label itself is NOT 14px/#682's tracking -- getComputedStyle on
|
|
136
|
+
// the live button (fonts loaded, DPR 2, zero zoom/transform anywhere
|
|
137
|
+
// in the ancestor chain) reads 10px/regular/15px line-height/normal
|
|
138
|
+
// tracking, independently matching `recordpanel.recipe.ts`'s own
|
|
139
|
+
// "View all" measurement (10px/400) elsewhere in this codebase. 10px
|
|
140
|
+
// sits below the text scale's 11px floor, so it's a literal here, the
|
|
141
|
+
// same as recordpanel's.
|
|
142
|
+
xs: {
|
|
143
|
+
height: "20px",
|
|
144
|
+
paddingInline: c.space["1.5"],
|
|
145
|
+
fontSize: "10px",
|
|
146
|
+
fontWeight: c.weight.regular,
|
|
147
|
+
lineHeight: "15px",
|
|
148
|
+
letterSpacing: "normal",
|
|
149
|
+
borderRadius: c.radius.sm,
|
|
150
|
+
gap: c.space["1"],
|
|
151
|
+
},
|
|
124
152
|
md: {
|
|
125
153
|
decls: {
|
|
126
154
|
height: "28px",
|
|
@@ -73,7 +73,9 @@ const spec = defineSpec({
|
|
|
73
73
|
selector: "& .crisp-chip-label",
|
|
74
74
|
decls: {
|
|
75
75
|
minWidth: 0,
|
|
76
|
-
|
|
76
|
+
// #719: `clip`, not `hidden` -- immune to a programmatic
|
|
77
|
+
// `scrollLeft` assignment; renders the ellipsis identically.
|
|
78
|
+
overflow: "clip",
|
|
77
79
|
textOverflow: "ellipsis",
|
|
78
80
|
whiteSpace: "nowrap",
|
|
79
81
|
display: "block",
|
|
@@ -113,7 +113,13 @@ const config = defineRecipe({
|
|
|
113
113
|
& .crisp-datatable-group-count { margin-inline-start: ${c.space["1.5"]}; color: ${c.fgMuted}; font-variant-numeric: tabular-nums; }
|
|
114
114
|
& .crisp-datatable-th-rename { width: 100%; min-width: 0; height: ${c.size.row}; margin: 0; padding: 0 ${c.space["1"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; outline: none; }
|
|
115
115
|
& .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
|
|
116
|
-
|
|
116
|
+
/* #719: overflow: clip, not hidden, on every truncation rule below --
|
|
117
|
+
hidden still honours a programmatic scrollLeft assignment (found via
|
|
118
|
+
a screenshot tool that pans anything with scrollWidth > clientWidth),
|
|
119
|
+
blanking or fragmenting the visible text even though real user
|
|
120
|
+
scrolling was already blocked. clip renders the ellipsis identically
|
|
121
|
+
and leaves no scroll position to move. */
|
|
122
|
+
& .crisp-datatable-th-label { overflow: clip; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
117
123
|
/* #465: 12px per the comment above (and Attio's measured header cell
|
|
118
124
|
padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
|
|
119
125
|
own documented value. */
|
|
@@ -200,7 +206,7 @@ const config = defineRecipe({
|
|
|
200
206
|
/* reference picker: a leading 16px avatar per option, plus a trailing
|
|
201
207
|
"Create X" row when the consumer supplies one (#449) -- same measured
|
|
202
208
|
values as RecordsTable's own .crisp-recordstable-refoption*. */
|
|
203
|
-
& .crisp-datatable-refoption { display: flex; align-items: center; gap: ${c.space["2"]}; min-width: 0; overflow:
|
|
209
|
+
& .crisp-datatable-refoption { display: flex; align-items: center; gap: ${c.space["2"]}; min-width: 0; overflow: clip; text-overflow: ellipsis; }
|
|
204
210
|
& .crisp-datatable-refoption-avatar { flex-shrink: 0; }
|
|
205
211
|
& .crisp-datatable-refoption--create { color: ${c.fgMuted}; }
|
|
206
212
|
& .crisp-datatable-empty { padding: ${c.space["8"]} ${c.space["3"]}; text-align: center; color: ${c.fgMuted}; font-size: ${c.text.lg}; }
|
|
@@ -286,7 +292,7 @@ const config = defineRecipe({
|
|
|
286
292
|
(#833/#835/#848) that happened to apply the same 500 to every
|
|
287
293
|
primary-column body cell regardless of type. Made explicit here so
|
|
288
294
|
fixing that leak doesn't also (wrongly) drop entity names to 400. */
|
|
289
|
-
& .crisp-datatable-entity-name { overflow:
|
|
295
|
+
& .crisp-datatable-entity-name { overflow: clip; text-overflow: ellipsis; white-space: nowrap; font-weight: ${c.weight.medium}; text-decoration: underline; text-decoration-color: color-mix(in srgb, ${c.fgPrimary} 9%, transparent); }
|
|
290
296
|
/* #893: shared by the button (onEntityClick, a different record) and
|
|
291
297
|
the anchor (href, the row's own record) -- was button-only, which
|
|
292
298
|
silently left a real <a> unstyled the day #893 added one. */
|
|
@@ -332,7 +338,7 @@ const config = defineRecipe({
|
|
|
332
338
|
own .crisp-recordstable-link, which this mirrors: no underline at
|
|
333
339
|
rest, underline on hover, own-colour text rather than a browser-blue
|
|
334
340
|
link. */
|
|
335
|
-
& .crisp-datatable-link { color: ${c.fgPrimary}; text-decoration: none; overflow:
|
|
341
|
+
& .crisp-datatable-link { color: ${c.fgPrimary}; text-decoration: none; overflow: clip; text-overflow: ellipsis; }
|
|
336
342
|
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
337
343
|
/* phone cell type/editor (#765) -- measured live: the dial-code prefix
|
|
338
344
|
is a muted rgba(0,0,0,0.63) at 14px, matching every other muted-text
|
|
@@ -22,7 +22,7 @@ const fromDate = (d) => ({
|
|
|
22
22
|
* popover. This is what `DatePicker` used to be; `DatePicker` is now
|
|
23
23
|
* seed-design's bare calendar, and this composes it.
|
|
24
24
|
*/
|
|
25
|
-
export function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder: placeholderProp, variant = "default", format = defaultFormat, clearable = true, clearLabel: clearLabelProp, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
|
|
25
|
+
export function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder: placeholderProp, variant = "default", width = "full", format = defaultFormat, clearable = true, clearLabel: clearLabelProp, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
|
|
26
26
|
// crisp/#471: same resolution order as Select's own.
|
|
27
27
|
const placeholder = useCrispMessage(placeholderProp, "Set date…");
|
|
28
28
|
const clearLabel = useCrispMessage(clearLabelProp, "Clear date");
|
|
@@ -43,7 +43,7 @@ export function DatePickerField({ value, defaultValue, onValueChange, onChange,
|
|
|
43
43
|
onValueChange?.(null);
|
|
44
44
|
onChange?.(null);
|
|
45
45
|
};
|
|
46
|
-
return (_jsx("div", { className: datepickerfieldRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-datepickerfield-box", "data-ghost": variant === "ghost" ? "" : undefined, "data-placeholder": selected ? undefined : "", children: [_jsx(Popover, { open: open, onOpenChange: setOpen, "aria-label": ariaLabel, trigger: _jsxs("button", { type: "button", className: "crisp-datepickerfield-trigger", "aria-label": ariaLabel, children: [_jsx(CalendarDays, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), selected ? format(selected) : placeholder] }), children: _jsxs("div", { className: "crisp-datepickerfield-panel", children: [_jsx(DatePicker
|
|
46
|
+
return (_jsx("div", { className: datepickerfieldRecipe({ width, className }), ...rest, children: _jsxs("div", { className: "crisp-datepickerfield-box", "data-ghost": variant === "ghost" ? "" : undefined, "data-placeholder": selected ? undefined : "", children: [_jsx(Popover, { open: open, onOpenChange: setOpen, "aria-label": ariaLabel, trigger: _jsxs("button", { type: "button", className: "crisp-datepickerfield-trigger", "aria-label": ariaLabel, children: [_jsx(CalendarDays, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), selected ? format(selected) : placeholder] }), children: _jsxs("div", { className: "crisp-datepickerfield-panel", children: [_jsx(DatePicker
|
|
47
47
|
// DatePickerProps is a union over selectionMode, and TypeScript
|
|
48
48
|
// cannot narrow it through a spread: it tries every branch and
|
|
49
49
|
// reports the range one as missing props. The field is always
|
|
@@ -10,7 +10,7 @@ const config = defineRecipe({
|
|
|
10
10
|
display: inline-flex; font-family: ${c.font.sans};
|
|
11
11
|
|
|
12
12
|
& .crisp-datepickerfield-box {
|
|
13
|
-
display: inline-flex; align-items: center;
|
|
13
|
+
display: inline-flex; align-items: center; width: 100%;
|
|
14
14
|
box-sizing: border-box; height: 32px; margin: 0;
|
|
15
15
|
border-radius: ${c.radius.md}; background: transparent;
|
|
16
16
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 7%, transparent);
|
|
@@ -19,6 +19,7 @@ const config = defineRecipe({
|
|
|
19
19
|
& .crisp-datepickerfield-box[data-ghost] { background: transparent; box-shadow: none; }
|
|
20
20
|
& .crisp-datepickerfield-box[data-ghost]:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); }
|
|
21
21
|
& .crisp-datepickerfield-trigger {
|
|
22
|
+
flex: 1; min-width: 0;
|
|
22
23
|
display: inline-flex; align-items: center; gap: ${c.space["2"]};
|
|
23
24
|
height: 100%; padding: 0 ${c.space["2.5"]}; margin: 0;
|
|
24
25
|
border: none; background: none; color: inherit; font-family: inherit;
|
|
@@ -48,8 +49,20 @@ const config = defineRecipe({
|
|
|
48
49
|
}
|
|
49
50
|
& .crisp-datepickerfield-preset:hover { background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); }
|
|
50
51
|
`,
|
|
51
|
-
variants: {
|
|
52
|
-
|
|
52
|
+
variants: {
|
|
53
|
+
// crisp#980: full (default) fills its container, matching Select's own
|
|
54
|
+
// width variant/default -- so a form composing DatePickerField alongside
|
|
55
|
+
// Input/Select via FormField gets one consistent column width across
|
|
56
|
+
// every field type, not just some of them. auto sizes to content, the
|
|
57
|
+
// behaviour every consumer got unconditionally before this.
|
|
58
|
+
width: {
|
|
59
|
+
full: `width: 100%;`,
|
|
60
|
+
auto: `width: auto;`,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
defaultVariants: {
|
|
64
|
+
width: "full",
|
|
65
|
+
},
|
|
53
66
|
compoundVariants: [],
|
|
54
67
|
});
|
|
55
68
|
export const datepickerfieldRecipe = Object.assign(resolveRecipe(config), {
|
|
@@ -104,12 +104,28 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
104
104
|
document.addEventListener("keydown", onKey);
|
|
105
105
|
return () => document.removeEventListener("keydown", onKey);
|
|
106
106
|
}, [open, close]);
|
|
107
|
-
const { onKeyDown: onContentKeyDown } = useMenuKeyboard({
|
|
107
|
+
const { onKeyDown: onContentKeyDown, items } = useMenuKeyboard({
|
|
108
108
|
open,
|
|
109
109
|
close,
|
|
110
110
|
dismiss: () => setOpen(false),
|
|
111
111
|
contentRef,
|
|
112
112
|
});
|
|
113
|
+
// Roving tabindex (crisp#964): every item hardcodes tabIndex={-1} for the
|
|
114
|
+
// arrow-key model above, which means none of them is ever a real Tab stop
|
|
115
|
+
// on its own -- kept in sync with wherever focus actually lands, same as
|
|
116
|
+
// MenuSheet's identical fix (#966).
|
|
117
|
+
const onContentFocus = React.useCallback((e) => {
|
|
118
|
+
for (const el of items())
|
|
119
|
+
el.tabIndex = -1;
|
|
120
|
+
e.target.tabIndex = 0;
|
|
121
|
+
}, [items]);
|
|
122
|
+
React.useEffect(() => {
|
|
123
|
+
if (!open)
|
|
124
|
+
return;
|
|
125
|
+
const [first] = items();
|
|
126
|
+
if (first)
|
|
127
|
+
first.tabIndex = 0;
|
|
128
|
+
}, [open, items]);
|
|
113
129
|
const triggerEl = React.cloneElement(trigger, {
|
|
114
130
|
ref: triggerRef,
|
|
115
131
|
id: triggerId,
|
|
@@ -125,7 +141,7 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
125
141
|
});
|
|
126
142
|
return (_jsxs("div", { ref: rootRef, className: menuRecipe({ className }), children: [triggerEl, open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("div", { ref: contentRef, className: contentClassName
|
|
127
143
|
? `crisp-menu-content ${contentClassName}`
|
|
128
|
-
: "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: _jsx(MenuContext.Provider, { value: { close }, children: children }) }) }))] }));
|
|
144
|
+
: "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, onFocus: onContentFocus, children: _jsx(MenuContext.Provider, { value: { close }, children: children }) }) }))] }));
|
|
129
145
|
}
|
|
130
146
|
Menu.displayName = "Menu";
|
|
131
147
|
export function MenuItem({ children, icon, shortcut, count, danger, disabled, onSelect, closeOnSelect = true, }) {
|
|
@@ -21,7 +21,13 @@ const config = defineRecipe({
|
|
|
21
21
|
(padding: 0 12px) and independently on a body cell's text inset
|
|
22
22
|
(getBoundingClientRect delta from the cell edge). Matches
|
|
23
23
|
ColumnCalc's own already-correct 12px trigger inset. */
|
|
24
|
-
|
|
24
|
+
/* #719: overflow: clip, not hidden -- both render the ellipsis
|
|
25
|
+
identically, but hidden still honours a programmatic scrollLeft
|
|
26
|
+
assignment (a screenshot tool, a browser extension) even though it
|
|
27
|
+
already blocks user-driven wheel/scrollbar input; clip leaves no
|
|
28
|
+
scroll position for anything to move, so the truncated text can
|
|
29
|
+
never be panned out of view. */
|
|
30
|
+
& th, & td { position: relative; height: ${c.size.tableRow}; padding: 0 ${c.space["3"]}; border-bottom: 1px solid ${c.borderSubtle}; border-inline-end: 1px solid ${c.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: clip; text-overflow: ellipsis; }
|
|
25
31
|
/* #554: sticky on the vertical axis, mirroring RecordsTable's own
|
|
26
32
|
\`.crisp-recordstable-th\`. z-index 2 sits above a plain body cell (auto)
|
|
27
33
|
and below a sticky-column corner cell (DataTable's \`th[data-sticky]\`,
|
|
@@ -16,10 +16,11 @@ import { createPortal } from "react-dom";
|
|
|
16
16
|
* `Dialog` was rendering offset by the dialog's own box instead of anchored
|
|
17
17
|
* to its trigger.
|
|
18
18
|
*
|
|
19
|
-
* Every caller renders this conditionally on its own `open` state
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* Every caller renders this conditionally on its own `open` state. A fixture
|
|
20
|
+
* CAN set that true at mount (crisp#964 needs exactly that, to give Menu's
|
|
21
|
+
* roving-tabindex guard something to inspect), so this checks for `window`
|
|
22
|
+
* itself rather than assuming the caller never does — the same guard
|
|
23
|
+
* `@zag-js/react`'s own `Portal` uses for the identical reason.
|
|
23
24
|
*
|
|
24
25
|
* `sourceRef` is the caller's own root — still in its original DOM position,
|
|
25
26
|
* never portaled — used to carry a `data-theme` set on some ancestor of it
|
|
@@ -35,6 +36,8 @@ import { createPortal } from "react-dom";
|
|
|
35
36
|
* element carrying that attribute.
|
|
36
37
|
*/
|
|
37
38
|
export function FloatingPortal({ children, sourceRef, }) {
|
|
39
|
+
if (typeof window === "undefined")
|
|
40
|
+
return children;
|
|
38
41
|
const theme = sourceRef.current
|
|
39
42
|
?.closest("[data-theme]")
|
|
40
43
|
?.getAttribute("data-theme");
|
|
@@ -18,10 +18,18 @@ export function Inbox({ notifications = [], title: titleProp, tabs, activeTab, o
|
|
|
18
18
|
// own `empty`.
|
|
19
19
|
const crispMessages = useCrispMessages();
|
|
20
20
|
const title = titleProp ?? crispMessages.Notifications ?? "Notifications";
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// A tab's own empty copy wins over the instance-level prop, which wins
|
|
22
|
+
// over the built-in default — same precedence as `title`/`emptyAction`.
|
|
23
|
+
const activeTabDef = tabs?.find((t) => t.id === activeTab);
|
|
24
|
+
const emptyTitle = activeTabDef?.emptyTitle ??
|
|
25
|
+
emptyTitleProp ??
|
|
26
|
+
crispMessages["No notifications"] ??
|
|
27
|
+
"No notifications";
|
|
28
|
+
const emptySubtitle = activeTabDef?.emptySubtitle ??
|
|
29
|
+
emptySubtitleProp ??
|
|
23
30
|
crispMessages["Notifications such as mentions and important updates will be displayed here."] ??
|
|
24
31
|
"Notifications such as mentions and important updates will be displayed here.";
|
|
32
|
+
const resolvedEmptyAction = activeTabDef?.emptyAction ?? emptyAction;
|
|
25
33
|
const tabsLabel = useCrispMessage(tabsLabelProp, "Notification views");
|
|
26
34
|
// crisp's shared tablist model, the one `Tabs` runs (#195). Hand-rolling a
|
|
27
35
|
// second one is how Inbox ended up with no arrow keys, no roving tabindex
|
|
@@ -32,6 +40,6 @@ export function Inbox({ notifications = [], title: titleProp, tabs, activeTab, o
|
|
|
32
40
|
select: (id) => onTabChange?.(id),
|
|
33
41
|
listRef,
|
|
34
42
|
});
|
|
35
|
-
return (_jsxs("div", { className: inboxRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-inbox-head", children: [_jsx("div", { className: "crisp-inbox-title", children: title }), tabs && tabs.length > 0 && (_jsx("div", { ref: listRef, className: "crisp-inbox-tabs", role: "tablist", "aria-label": tabsLabel, onKeyDown: onKeyDown, children: tabs.map((t) => (_jsxs("button", { type: "button", role: "tab", "aria-selected": t.id === activeTab, tabIndex: tabIndexFor(t.id), "data-value": t.id, disabled: t.disabled, className: "crisp-inbox-tab", "data-active": t.id === activeTab ? "" : undefined, onClick: () => onTabChange?.(t.id), children: [t.label, t.count != null && (_jsxs("span", { className: "crisp-inbox-tab-count", children: ["(", t.count, ")"] }))] }, t.id))) }))] }), notifications.length === 0 ? (_jsxs("div", { className: "crisp-inbox-empty", children: [_jsx("span", { className: "crisp-inbox-empty-icon", children: _jsx(Bell, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-inbox-empty-title", children: emptyTitle }), emptySubtitle != null && (_jsx("div", { className: "crisp-inbox-empty-sub", children: emptySubtitle })),
|
|
43
|
+
return (_jsxs("div", { className: inboxRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-inbox-head", children: [_jsx("div", { className: "crisp-inbox-title", children: title }), tabs && tabs.length > 0 && (_jsx("div", { ref: listRef, className: "crisp-inbox-tabs", role: "tablist", "aria-label": tabsLabel, onKeyDown: onKeyDown, children: tabs.map((t) => (_jsxs("button", { type: "button", role: "tab", "aria-selected": t.id === activeTab, tabIndex: tabIndexFor(t.id), "data-value": t.id, disabled: t.disabled, className: "crisp-inbox-tab", "data-active": t.id === activeTab ? "" : undefined, onClick: () => onTabChange?.(t.id), children: [t.label, t.count != null && (_jsxs("span", { className: "crisp-inbox-tab-count", children: ["(", t.count, ")"] }))] }, t.id))) }))] }), notifications.length === 0 ? (_jsxs("div", { className: "crisp-inbox-empty", children: [_jsx("span", { className: "crisp-inbox-empty-icon", children: _jsx(Bell, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-inbox-empty-title", children: emptyTitle }), emptySubtitle != null && (_jsx("div", { className: "crisp-inbox-empty-sub", children: emptySubtitle })), resolvedEmptyAction != null && (_jsx("div", { className: "crisp-inbox-empty-action", children: resolvedEmptyAction }))] })) : (_jsx("div", { className: "crisp-inbox-list", children: notifications.map((n) => (_jsxs("button", { type: "button", className: "crisp-inbox-row", onClick: n.onClick, children: [n.avatar != null && (_jsx("span", { className: "crisp-inbox-row-avatar", children: n.avatar })), _jsxs("span", { className: "crisp-inbox-row-body", children: [_jsx("span", { className: "crisp-inbox-row-msg", children: n.message }), n.time != null && (_jsx("span", { className: "crisp-inbox-row-time", children: n.time }))] }), n.unread && _jsx("span", { className: "crisp-inbox-row-unread" })] }, n.id))) }))] }));
|
|
36
44
|
}
|
|
37
45
|
Inbox.displayName = "Inbox";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { tagTone, } from "./column.js";
|
|
2
2
|
export { editableInputType, useEditableField, } from "./editable-field.js";
|
|
3
|
-
export { RecordDetails, RecordPage, recordPageLabels, } from "./recordpage.js";
|
|
3
|
+
export { RecordDetails, RecordPage, RecordPageMain, recordPageLabels, } from "./recordpage.js";
|
|
4
4
|
export { recordpageRecipe } from "./recordpage.recipe.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Activity as ActivityIcon, CalendarClock, ChevronDown, ChevronUp, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, Mail, Maximize2, Merge, MessageSquare, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search, Settings, Sparkles, SquareCheck, Star, Trash2, Users, UsersRound, Workflow, X, Zap, } from "lucide-react";
|
|
2
|
+
import { Activity as ActivityIcon, CalendarClock, ChevronDown, ChevronUp, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, ListFilter, Mail, Maximize2, Merge, MessageSquare, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search, Settings, 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
|
+
import { Button } from "../../components/button/index.js";
|
|
5
6
|
import { Checkbox } from "../../components/checkbox/index.js";
|
|
6
7
|
import { Drawer } from "../../components/drawer/index.js";
|
|
7
8
|
import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
|
|
@@ -62,6 +63,22 @@ export const recordPageLabels = {
|
|
|
62
63
|
share: "Share",
|
|
63
64
|
comments: "Comments",
|
|
64
65
|
noLists: "This record has not been added to any lists.",
|
|
66
|
+
filters: "Filters",
|
|
67
|
+
addMeeting: "Add meeting",
|
|
68
|
+
noFiles: "No files",
|
|
69
|
+
filesHint: "Drag a file here or choose one from your computer.",
|
|
70
|
+
newFolder: "New folder",
|
|
71
|
+
uploadFile: "Upload file",
|
|
72
|
+
fileMoreActions: "More file actions",
|
|
73
|
+
noDealsFound: "No Deals found",
|
|
74
|
+
noDealsHint: "There are no related records for this relationship.",
|
|
75
|
+
newDeal: "New Deal",
|
|
76
|
+
noEmails: "No emails",
|
|
77
|
+
noEmailsHint: "This record doesn't have any emails, or they may be hidden due to permissions.",
|
|
78
|
+
configureMailbox: "Configure mailbox",
|
|
79
|
+
upgradeToViewCalls: "Upgrade to view calls",
|
|
80
|
+
callsUpgradeHint: "No call recordings yet! Upgrade to Pro to start recording calls.",
|
|
81
|
+
upgrade: "Upgrade",
|
|
65
82
|
highlights: "Highlights",
|
|
66
83
|
recordDetails: "Record Details",
|
|
67
84
|
searchFields: "Search fields",
|
|
@@ -477,6 +494,28 @@ export function RecordDetails({ record, columns, sections, onEdit, labels, class
|
|
|
477
494
|
} }))] }));
|
|
478
495
|
}
|
|
479
496
|
RecordDetails.displayName = "RecordDetails";
|
|
497
|
+
/**
|
|
498
|
+
* The main/tab-content column's own box, on its own (#750) — the same
|
|
499
|
+
* "standalone piece" `RecordDetails` already is for the Details column,
|
|
500
|
+
* mirrored to the other side of the layout. `RecordPage` composes this
|
|
501
|
+
* internally for its own tabbed body (below) — this exact box, not a second
|
|
502
|
+
* copy — so a consumer building a custom record page (its own tab set, its
|
|
503
|
+
* own menu, `RecordDetails` for the left column, but not the full
|
|
504
|
+
* `RecordPage`, which hardcodes the CRM tab set) gets the same
|
|
505
|
+
* Attio-measured inset: `flex: 1; min-width: 0; display: flex;
|
|
506
|
+
* flex-direction: column; min-height: 0; overflow-y: auto; padding: 12px
|
|
507
|
+
* 20px` (`recordpage.recipe.ts`'s own `.crisp-recordpage-main`) — without
|
|
508
|
+
* reaching for that internal class name directly, the only option before
|
|
509
|
+
* this existed.
|
|
510
|
+
*
|
|
511
|
+
* Needs the same single `.crisp-recordpage`-scoped ancestor `RecordDetails`
|
|
512
|
+
* already documents — wrap both in `recordpageRecipe()`, the way
|
|
513
|
+
* `RecordPage`'s own root does.
|
|
514
|
+
*/
|
|
515
|
+
export function RecordPageMain({ className, children, ...rest }) {
|
|
516
|
+
return (_jsx("main", { className: cx("crisp-recordpage-main", className), ...rest, children: children }));
|
|
517
|
+
}
|
|
518
|
+
RecordPageMain.displayName = "RecordPageMain";
|
|
480
519
|
/**
|
|
481
520
|
* The full-page company record view — the page that opens when you click into a
|
|
482
521
|
* record (as opposed to the peek [RecordPanel]). A top breadcrumb bar with
|
|
@@ -717,13 +756,19 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
717
756
|
case "overview":
|
|
718
757
|
return (_jsxs("div", { className: "crisp-recordpage-panel", children: [highlights !== false && (_jsxs("section", { children: [_jsx("div", { className: "crisp-recordpage-section-title", children: t.highlights }), _jsx("div", { className: "crisp-recordpage-hl-card", children: _jsx("div", { className: "crisp-recordpage-hl-grid", children: highlights.map((h) => (_jsxs("div", { className: "crisp-recordpage-hl-cell", children: [h.icon && (_jsx(h.icon, { className: "crisp-recordpage-hl-icon", size: 15, strokeWidth: 1.75 })), _jsx("div", { className: "crisp-recordpage-hl-label", children: h.label }), _jsx("div", { className: "crisp-recordpage-hl-value", children: h.value ?? (_jsx("span", { className: "crisp-recordpage-hl-value--empty", children: h.empty })) })] }, h.label))) }) })] })), activityFeed, _jsxs("section", { children: [_jsx(SectionHead, { title: t.tabs.emails, count: 0 }), emailsEmpty] }), _jsx(NotesSection, { notes: notes, onAdd: addNote, labels: t }), _jsx(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t })] }));
|
|
719
758
|
case "activity":
|
|
720
|
-
return _jsx("div", { className: "crisp-recordpage-panel", children:
|
|
759
|
+
return (_jsx("div", { className: "crisp-recordpage-panel", children: _jsxs("section", { children: [_jsx(SectionHead, { title: t.tabs.activity, action: _jsxs("span", { className: "crisp-recordpage-tabtoolbar", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action", children: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75 }), t.filters] }), _jsxs("button", { type: "button", className: "crisp-recordpage-action", children: [_jsx(Plus, { size: 14, strokeWidth: 1.75 }), t.addMeeting] })] }) }), _jsx("div", { className: "crisp-recordpage-feed", children: activity.map((a) => (_jsxs("div", { className: "crisp-recordpage-feed-item", children: [_jsx("span", { className: "crisp-recordpage-feed-dot", children: _jsx(ActivityIcon, { size: 13, strokeWidth: 2 }) }), _jsxs("div", { className: "crisp-recordpage-feed-body", children: [a.text, _jsx("div", { className: "crisp-recordpage-feed-time", children: a.time })] })] }, a.id))) })] }) }));
|
|
760
|
+
case "files":
|
|
761
|
+
return (_jsx("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: _jsxs("div", { className: "crisp-recordpage-illustrated", children: [_jsx("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: _jsx(Folder, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-recordpage-illustrated-title", children: t.noFiles }), _jsx("div", { className: "crisp-recordpage-illustrated-desc", children: t.filesHint }), _jsxs("div", { className: "crisp-recordpage-illustrated-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action", children: [_jsx(Plus, { size: 14, strokeWidth: 1.75 }), t.newFolder] }), _jsxs("button", { type: "button", className: "crisp-recordpage-action", children: [_jsx(Plus, { size: 14, strokeWidth: 1.75 }), t.uploadFile] }), _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.fileMoreActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) })] })] }) }));
|
|
762
|
+
case "deals":
|
|
763
|
+
return (_jsx("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: _jsxs("div", { className: "crisp-recordpage-illustrated", children: [_jsx("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: _jsx(Merge, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-recordpage-illustrated-title", children: t.noDealsFound }), _jsx("div", { className: "crisp-recordpage-illustrated-desc", children: t.noDealsHint }), _jsx("div", { className: "crisp-recordpage-illustrated-actions", children: _jsxs(Button, { intent: "primary", size: "md", children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newDeal] }) })] }) }));
|
|
721
764
|
case "notes":
|
|
722
765
|
return (_jsx("div", { className: "crisp-recordpage-panel", children: _jsx(NotesSection, { notes: notes, onAdd: addNote, labels: t }) }));
|
|
723
766
|
case "tasks":
|
|
724
767
|
return (_jsx("div", { className: "crisp-recordpage-panel", children: _jsx(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t }) }));
|
|
725
768
|
case "emails":
|
|
726
|
-
return _jsx("div", { className: "crisp-recordpage-panel", children:
|
|
769
|
+
return (_jsxs("div", { className: "crisp-recordpage-panel", children: [_jsx(SectionHead, { title: t.tabs.emails }), _jsx("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: _jsxs("div", { className: "crisp-recordpage-illustrated", children: [_jsx("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: _jsx(Mail, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-recordpage-illustrated-title", children: t.noEmails }), _jsx("div", { className: "crisp-recordpage-illustrated-desc", children: t.noEmailsHint }), _jsx("div", { className: "crisp-recordpage-illustrated-actions", children: _jsx(Button, { intent: "primary", size: "md", children: t.configureMailbox }) })] }) })] }));
|
|
770
|
+
case "calls":
|
|
771
|
+
return (_jsx("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: _jsxs("div", { className: "crisp-recordpage-illustrated", children: [_jsx("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: _jsx(Phone, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-recordpage-illustrated-title crisp-recordpage-illustrated-title--lg", children: t.upgradeToViewCalls }), _jsx("div", { className: "crisp-recordpage-illustrated-desc", children: t.callsUpgradeHint }), _jsx("div", { className: "crisp-recordpage-illustrated-actions", children: _jsxs(Button, { intent: "primary", size: "md", children: [_jsx(Zap, { size: 15, strokeWidth: 1.75 }), t.upgrade] }) })] }) }));
|
|
727
772
|
default:
|
|
728
773
|
return (_jsx("div", { className: "crisp-recordpage-panel", children: _jsx(EmptyCard, { icon: _jsx(Folder, { size: 22, strokeWidth: 1.5 }), children: t.nothingHereYet }) }));
|
|
729
774
|
}
|
|
@@ -753,6 +798,6 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
753
798
|
setDetailsW((w) => grow
|
|
754
799
|
? Math.min(DETAILS_MAX_W, w + 16)
|
|
755
800
|
: Math.max(DETAILS_MIN_W, w - 16));
|
|
756
|
-
} }), _jsx(
|
|
801
|
+
} }), _jsx(RecordPageMain, { children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] }), _jsx(Drawer, { open: commentsOpen, onOpenChange: setCommentsOpen, "aria-label": t.comments, style: { width: 320 }, children: _jsx(CommentsPanel, { threads: comments, currentUserId: me.id, onPost: postComment, onClose: () => setCommentsOpen(false), role: undefined }) })] }));
|
|
757
802
|
}
|
|
758
803
|
RecordPage.displayName = "RecordPage";
|
|
@@ -226,6 +226,31 @@ const config = defineRecipe({
|
|
|
226
226
|
& .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: ${c.space["1.5"]}; padding: ${c.space["6"]} ${c.space["3"]}; border: 1px dashed ${c.borderDefault}; border-radius: ${c.radius.xl}; color: ${c.fgMuted}; font-size: ${c.text.md}; text-align: center; }
|
|
227
227
|
& .crisp-recordpage-emptycard svg { color: ${c.fgMuted}; opacity: 0.7; }
|
|
228
228
|
|
|
229
|
+
/* #622: the Activity tab's own toolbar -- "Filters" + "+ Add meeting",
|
|
230
|
+
both the same measured button chrome as the topbar's own action
|
|
231
|
+
buttons (28px, e.field shadow, 14/500 label). */
|
|
232
|
+
& .crisp-recordpage-tabtoolbar { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; }
|
|
233
|
+
|
|
234
|
+
/* #622: Files tab's empty state -- measured live on Attio's own record
|
|
235
|
+
page (a company with no files uploaded). The 138x138 dotted-grid
|
|
236
|
+
backdrop is the same measured tile EmptyState's records variant uses
|
|
237
|
+
(crisp/#603), duplicated here rather than composed through that
|
|
238
|
+
component because this surface's title (16/600) and body (14/500,
|
|
239
|
+
63%-mixed fgPrimary, not the flat fgMuted EmptyState uses) measure to
|
|
240
|
+
their own values, matching neither of that component's existing
|
|
241
|
+
variants. */
|
|
242
|
+
& .crisp-recordpage-panel--center { flex: 1; align-items: center; justify-content: center; }
|
|
243
|
+
& .crisp-recordpage-illustrated { display: flex; flex-direction: column; align-items: center; gap: ${c.space["1.5"]}; text-align: center; }
|
|
244
|
+
& .crisp-recordpage-illustrated-icon { display: flex; align-items: center; justify-content: center; width: 138px; height: 138px; margin-bottom: ${c.space["2"]}; color: color-mix(in srgb, ${c.fgPrimary} 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, ${c.fgPrimary} 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }
|
|
245
|
+
& .crisp-recordpage-illustrated-title { font-size: ${c.text.xl}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
|
|
246
|
+
/* #988: the Calls tab's own gated state measures a larger 20/24 title
|
|
247
|
+
than Files/Deals' 16/600 -- Attio's own "empty view" pattern (the
|
|
248
|
+
same 20/600 EmptyState's page/records variants already carry, not a
|
|
249
|
+
one-off). */
|
|
250
|
+
& .crisp-recordpage-illustrated-title--lg { font-size: 20px; line-height: 24px; }
|
|
251
|
+
& .crisp-recordpage-illustrated-desc { max-width: 320px; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
252
|
+
& .crisp-recordpage-illustrated-actions { display: inline-flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: ${c.space["1.5"]}; margin-top: ${c.space["2"]}; }
|
|
253
|
+
|
|
229
254
|
& .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
|
|
230
255
|
|
|
231
256
|
& .crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${c.size.chip}; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.chip}; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; white-space: nowrap; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${c.fgPrimary} 82%, transparent); }
|
package/dist/recordpage.css
CHANGED
|
@@ -193,6 +193,29 @@
|
|
|
193
193
|
& .crisp-recordpage-task[data-done] .crisp-recordpage-task-text { text-decoration: line-through; color: var(--crisp-fg-muted); }
|
|
194
194
|
& .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-6) var(--crisp-space-3); border: 1px dashed var(--crisp-border-default); border-radius: var(--crisp-radius-xl); color: var(--crisp-fg-muted); font-size: var(--crisp-text-md); text-align: center; }
|
|
195
195
|
& .crisp-recordpage-emptycard svg { color: var(--crisp-fg-muted); opacity: 0.7; }
|
|
196
|
+
/* #622: the Activity tab's own toolbar -- "Filters" + "+ Add meeting",
|
|
197
|
+
both the same measured button chrome as the topbar's own action
|
|
198
|
+
buttons (28px, e.field shadow, 14/500 label). */
|
|
199
|
+
& .crisp-recordpage-tabtoolbar { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); }
|
|
200
|
+
/* #622: Files tab's empty state -- measured live on Attio's own record
|
|
201
|
+
page (a company with no files uploaded). The 138x138 dotted-grid
|
|
202
|
+
backdrop is the same measured tile EmptyState's records variant uses
|
|
203
|
+
(crisp/#603), duplicated here rather than composed through that
|
|
204
|
+
component because this surface's title (16/600) and body (14/500,
|
|
205
|
+
63%-mixed fgPrimary, not the flat fgMuted EmptyState uses) measure to
|
|
206
|
+
their own values, matching neither of that component's existing
|
|
207
|
+
variants. */
|
|
208
|
+
& .crisp-recordpage-panel--center { flex: 1; align-items: center; justify-content: center; }
|
|
209
|
+
& .crisp-recordpage-illustrated { display: flex; flex-direction: column; align-items: center; gap: var(--crisp-space-1-5); text-align: center; }
|
|
210
|
+
& .crisp-recordpage-illustrated-icon { display: flex; align-items: center; justify-content: center; width: 138px; height: 138px; margin-bottom: var(--crisp-space-2); color: color-mix(in srgb, var(--crisp-fg-primary) 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, var(--crisp-fg-primary) 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }
|
|
211
|
+
& .crisp-recordpage-illustrated-title { font-size: var(--crisp-text-xl); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
|
|
212
|
+
/* #988: the Calls tab's own gated state measures a larger 20/24 title
|
|
213
|
+
than Files/Deals' 16/600 -- Attio's own "empty view" pattern (the
|
|
214
|
+
same 20/600 EmptyState's page/records variants already carry, not a
|
|
215
|
+
one-off). */
|
|
216
|
+
& .crisp-recordpage-illustrated-title--lg { font-size: 20px; line-height: 24px; }
|
|
217
|
+
& .crisp-recordpage-illustrated-desc { max-width: 320px; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
|
|
218
|
+
& .crisp-recordpage-illustrated-actions { display: inline-flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: var(--crisp-space-1-5); margin-top: var(--crisp-space-2); }
|
|
196
219
|
& .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
|
|
197
220
|
& .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
|
|
198
221
|
& .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
|
|
@@ -194,6 +194,29 @@
|
|
|
194
194
|
& .crisp-recordpage-task[data-done] .crisp-recordpage-task-text { text-decoration: line-through; color: var(--crisp-fg-muted); }
|
|
195
195
|
& .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-6) var(--crisp-space-3); border: 1px dashed var(--crisp-border-default); border-radius: var(--crisp-radius-xl); color: var(--crisp-fg-muted); font-size: var(--crisp-text-md); text-align: center; }
|
|
196
196
|
& .crisp-recordpage-emptycard svg { color: var(--crisp-fg-muted); opacity: 0.7; }
|
|
197
|
+
/* #622: the Activity tab's own toolbar -- "Filters" + "+ Add meeting",
|
|
198
|
+
both the same measured button chrome as the topbar's own action
|
|
199
|
+
buttons (28px, e.field shadow, 14/500 label). */
|
|
200
|
+
& .crisp-recordpage-tabtoolbar { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); }
|
|
201
|
+
/* #622: Files tab's empty state -- measured live on Attio's own record
|
|
202
|
+
page (a company with no files uploaded). The 138x138 dotted-grid
|
|
203
|
+
backdrop is the same measured tile EmptyState's records variant uses
|
|
204
|
+
(crisp/#603), duplicated here rather than composed through that
|
|
205
|
+
component because this surface's title (16/600) and body (14/500,
|
|
206
|
+
63%-mixed fgPrimary, not the flat fgMuted EmptyState uses) measure to
|
|
207
|
+
their own values, matching neither of that component's existing
|
|
208
|
+
variants. */
|
|
209
|
+
& .crisp-recordpage-panel--center { flex: 1; align-items: center; justify-content: center; }
|
|
210
|
+
& .crisp-recordpage-illustrated { display: flex; flex-direction: column; align-items: center; gap: var(--crisp-space-1-5); text-align: center; }
|
|
211
|
+
& .crisp-recordpage-illustrated-icon { display: flex; align-items: center; justify-content: center; width: 138px; height: 138px; margin-bottom: var(--crisp-space-2); color: color-mix(in srgb, var(--crisp-fg-primary) 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, var(--crisp-fg-primary) 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }
|
|
212
|
+
& .crisp-recordpage-illustrated-title { font-size: var(--crisp-text-xl); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
|
|
213
|
+
/* #988: the Calls tab's own gated state measures a larger 20/24 title
|
|
214
|
+
than Files/Deals' 16/600 -- Attio's own "empty view" pattern (the
|
|
215
|
+
same 20/600 EmptyState's page/records variants already carry, not a
|
|
216
|
+
one-off). */
|
|
217
|
+
& .crisp-recordpage-illustrated-title--lg { font-size: 20px; line-height: 24px; }
|
|
218
|
+
& .crisp-recordpage-illustrated-desc { max-width: 320px; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
|
|
219
|
+
& .crisp-recordpage-illustrated-actions { display: inline-flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: var(--crisp-space-1-5); margin-top: var(--crisp-space-2); }
|
|
197
220
|
& .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
|
|
198
221
|
& .crisp-recordstable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
|
|
199
222
|
& .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); }
|