@crisp-ui-kit/crisp 0.57.0 → 0.57.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accentpicker.css +5 -0
- package/dist/accentpicker.d.ts +7 -0
- package/dist/accentpicker.layered.css +7 -0
- package/dist/avatar.css +1 -1
- package/dist/avatar.layered.css +1 -1
- package/dist/badge.css +2 -2
- package/dist/badge.layered.css +2 -2
- package/dist/button.css +3 -3
- package/dist/button.layered.css +3 -3
- package/dist/cjs/components/accentpicker/accentpicker.js +66 -0
- package/dist/cjs/components/accentpicker/accentpicker.recipe.js +25 -0
- package/dist/cjs/components/accentpicker/index.js +7 -0
- package/dist/cjs/components/avatar/avatar.recipe.js +4 -1
- package/dist/cjs/components/badge/badge.recipe.js +16 -2
- package/dist/cjs/components/button/button.recipe.js +16 -6
- package/dist/cjs/components/input/input.js +2 -2
- package/dist/cjs/components/input/input.recipe.js +8 -0
- package/dist/cjs/components/multiselect/multiselect.js +1 -1
- package/dist/cjs/components/multiselect/multiselect.recipe.js +12 -3
- package/dist/cjs/components/progress/progress.js +21 -4
- package/dist/cjs/components/progress/progress.recipe.js +30 -0
- package/dist/cjs/components/select/select.js +2 -2
- package/dist/cjs/components/select/select.recipe.js +32 -5
- package/dist/cjs/components/text/text.recipe.js +8 -7
- package/dist/cjs/components/textarea/textarea.js +3 -2
- package/dist/cjs/components/textarea/textarea.recipe.js +2 -1
- package/dist/cjs/components/textlink/textlink.recipe.js +6 -3
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/product/recordpage/recordpage.js +46 -5
- package/dist/cjs/tokens/palette.js +79 -25
- package/dist/cjs/tokens/scale.js +1 -0
- package/dist/cjs/tokens/semantic.js +24 -0
- package/dist/esm/components/accentpicker/accentpicker.js +30 -0
- package/dist/esm/components/accentpicker/accentpicker.recipe.js +22 -0
- package/dist/esm/components/accentpicker/index.js +2 -0
- package/dist/esm/components/avatar/avatar.recipe.js +4 -1
- package/dist/esm/components/badge/badge.recipe.js +16 -2
- package/dist/esm/components/button/button.recipe.js +16 -6
- package/dist/esm/components/input/input.js +3 -3
- package/dist/esm/components/input/input.recipe.js +8 -0
- package/dist/esm/components/multiselect/multiselect.js +1 -1
- package/dist/esm/components/multiselect/multiselect.recipe.js +12 -3
- package/dist/esm/components/progress/progress.js +22 -5
- package/dist/esm/components/progress/progress.recipe.js +30 -0
- package/dist/esm/components/select/select.js +2 -2
- package/dist/esm/components/select/select.recipe.js +32 -5
- package/dist/esm/components/text/text.recipe.js +8 -7
- package/dist/esm/components/textarea/textarea.js +3 -2
- package/dist/esm/components/textarea/textarea.recipe.js +2 -1
- package/dist/esm/components/textlink/textlink.recipe.js +6 -3
- package/dist/esm/index.js +1 -0
- package/dist/esm/product/recordpage/recordpage.js +47 -6
- package/dist/esm/tokens/palette.js +79 -25
- package/dist/esm/tokens/scale.js +1 -0
- package/dist/esm/tokens/semantic.js +24 -0
- package/dist/input.css +7 -0
- package/dist/input.layered.css +7 -0
- package/dist/multiselect.css +4 -0
- package/dist/multiselect.d.ts +2 -2
- package/dist/multiselect.layered.css +4 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +33 -22
- package/dist/progress.css +3 -0
- package/dist/progress.layered.css +3 -0
- package/dist/select.css +22 -2
- package/dist/select.d.ts +2 -2
- package/dist/select.layered.css +22 -2
- package/dist/styles.css +71 -23
- package/dist/styles.layered.css +71 -23
- package/dist/text.css +1 -1
- package/dist/text.layered.css +1 -1
- package/dist/textarea.css +2 -1
- package/dist/textarea.layered.css +2 -1
- package/dist/textlink.css +2 -2
- package/dist/textlink.layered.css +2 -2
- package/dist/tokens.css +18 -11
- package/dist/tokens.json +33 -22
- package/dist/types/components/accentpicker/accentpicker.d.ts +23 -0
- package/dist/types/components/accentpicker/accentpicker.recipe.d.ts +3 -0
- package/dist/types/components/accentpicker/index.d.ts +2 -0
- package/dist/types/components/button/button.recipe.d.ts +2 -2
- package/dist/types/components/input/input.d.ts +12 -0
- package/dist/types/components/multiselect/multiselect.d.ts +4 -3
- package/dist/types/components/multiselect/multiselect.recipe.d.ts +3 -0
- package/dist/types/components/progress/progress.d.ts +16 -4
- package/dist/types/components/progress/progress.recipe.d.ts +22 -0
- package/dist/types/components/select/select.d.ts +6 -5
- package/dist/types/components/select/select.recipe.d.ts +3 -0
- package/dist/types/components/textarea/textarea.d.ts +16 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/product/recordpage/recordpage.d.ts +37 -1
- package/dist/types/tokens/contract.d.ts +4 -1
- package/dist/types/tokens/palette.d.ts +19 -11
- package/dist/types/tokens/scale.d.ts +1 -0
- package/dist/types/tokens/semantic.d.ts +15 -0
- package/package.json +6 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { progressRecipe } from "./progress.recipe.js";
|
|
3
3
|
/**
|
|
4
|
-
* A progress bar — a continuous brand fill,
|
|
5
|
-
* `segments` is set (the "Getting started 3/6" pattern)
|
|
6
|
-
* `
|
|
4
|
+
* A progress bar — a continuous brand fill, a segmented row of pills when
|
|
5
|
+
* `segments` is set (the "Getting started 3/6" pattern), or a small ring
|
|
6
|
+
* (`variant="circular"`, a character-counter's own progress). Reports its
|
|
7
|
+
* state via `role="progressbar"` and the ARIA value attributes.
|
|
7
8
|
*/
|
|
8
|
-
export function Progress({ value, max = 100, segments, "aria-label": ariaLabel, className, ...rest }) {
|
|
9
|
+
export function Progress({ value, max = 100, segments, variant = "linear", size = 18, "aria-label": ariaLabel, className, ...rest }) {
|
|
9
10
|
const pct = Math.max(0, Math.min(100, (value / max) * 100));
|
|
10
11
|
const aria = {
|
|
11
12
|
role: "progressbar",
|
|
@@ -14,6 +15,22 @@ export function Progress({ value, max = 100, segments, "aria-label": ariaLabel,
|
|
|
14
15
|
"aria-valuemax": max,
|
|
15
16
|
"aria-label": ariaLabel,
|
|
16
17
|
};
|
|
18
|
+
if (variant === "circular") {
|
|
19
|
+
// Measured live: an 18x18 SVG, r=7.4, stroke-width 1.6 (a 0.6px margin on
|
|
20
|
+
// each side) — a two-circle ring (a soft-tinted track + a brand arc),
|
|
21
|
+
// the same track tint Badge/Callout's own `soft(bgBrandSolid, 14%)`
|
|
22
|
+
// formula already gives.
|
|
23
|
+
const strokeWidth = size * (1.6 / 18);
|
|
24
|
+
const r = size / 2 - strokeWidth;
|
|
25
|
+
const circumference = 2 * Math.PI * r;
|
|
26
|
+
const offset = circumference * (1 - pct / 100);
|
|
27
|
+
// The measured sample was at value=0 (a full, invisible offset either
|
|
28
|
+
// way), so the arc's start point couldn't be read off that one DOM
|
|
29
|
+
// snapshot -- 12 o'clock is the conventional start for this shape and
|
|
30
|
+
// isn't itself in tension with anything measured, but isn't independently
|
|
31
|
+
// confirmed at a nonzero value either.
|
|
32
|
+
return (_jsx("div", { ...aria, className: progressRecipe({ className }), ...rest, "data-variant": "circular", children: _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", children: [_jsx("circle", { className: "crisp-progress-circle-track", r: r, cx: size / 2, cy: size / 2, strokeWidth: strokeWidth, fill: "transparent" }), _jsx("circle", { className: "crisp-progress-circle-fill", r: r, cx: size / 2, cy: size / 2, strokeWidth: strokeWidth, fill: "transparent", strokeDasharray: circumference, strokeDashoffset: offset, strokeLinecap: "round", transform: `rotate(-90 ${size / 2} ${size / 2})` })] }) }));
|
|
33
|
+
}
|
|
17
34
|
if (segments && segments > 0) {
|
|
18
35
|
const filled = Math.round((pct / 100) * segments);
|
|
19
36
|
return (_jsx("div", { ...aria, className: progressRecipe({ className }), ...rest, "data-segmented": "", children: Array.from({ length: segments }, (_, i) => (_jsx("span", { className: "crisp-progress-seg", "data-filled": i < filled ? "" : undefined }, i))) }));
|
|
@@ -49,6 +49,36 @@ const spec = defineSpec({
|
|
|
49
49
|
selector: "&[data-segmented] .crisp-progress-seg[data-filled]",
|
|
50
50
|
decls: { background: c.bgBrandSolid },
|
|
51
51
|
},
|
|
52
|
+
// crisp#634: measured live on Attio's own Settings > Profile
|
|
53
|
+
// character-counter ring — the track tint is the same
|
|
54
|
+
// `soft(bgBrandSolid, 14%)` formula Badge/Callout already use (measured
|
|
55
|
+
// rgb(229,238,255), within the same noise this codebase already treats
|
|
56
|
+
// as equivalent), the arc is the plain brand fill.
|
|
57
|
+
{
|
|
58
|
+
selector: '&[data-variant="circular"]',
|
|
59
|
+
decls: {
|
|
60
|
+
display: "inline-flex",
|
|
61
|
+
height: "auto",
|
|
62
|
+
borderRadius: "0",
|
|
63
|
+
background: "transparent",
|
|
64
|
+
overflow: "visible",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
slot: "circleTrack",
|
|
69
|
+
selector: "& .crisp-progress-circle-track",
|
|
70
|
+
decls: {
|
|
71
|
+
stroke: `color-mix(in srgb, ${c.bgBrandSolid} 14%, transparent)`,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
slot: "circleFill",
|
|
76
|
+
selector: "& .crisp-progress-circle-fill",
|
|
77
|
+
decls: {
|
|
78
|
+
stroke: c.bgBrandSolid,
|
|
79
|
+
transition: "stroke-dashoffset 0.25s ease",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
52
82
|
],
|
|
53
83
|
});
|
|
54
84
|
const config = specToRecipe(spec);
|
|
@@ -19,7 +19,7 @@ function optionText(o) {
|
|
|
19
19
|
* move, Enter/Space pick, Esc close, Home/End jump, opens upward when tight.
|
|
20
20
|
* Controlled or uncontrolled.
|
|
21
21
|
*/
|
|
22
|
-
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", size = "
|
|
22
|
+
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", size = "small", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
23
23
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
24
24
|
// CrispMessagesProvider's translation of the English default, or that
|
|
25
25
|
// default itself with no provider — unchanged from before this existed.
|
|
@@ -181,7 +181,7 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
181
181
|
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, size, invalid, className }), ...rest, children: [name && (selected || required) && (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
182
182
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
183
183
|
// validation, which is the only reason this input is not hidden
|
|
184
|
-
onChange: () => { } })), _jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "crisp-select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-select-search", children: [_jsx(Search, { size:
|
|
184
|
+
onChange: () => { } })), _jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "crisp-select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-select-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
185
185
|
setQuery(e.target.value);
|
|
186
186
|
setHighlight(0);
|
|
187
187
|
onSearchChange?.(e.target.value);
|
|
@@ -85,9 +85,19 @@ const config = defineRecipe({
|
|
|
85
85
|
}
|
|
86
86
|
.crisp-select-listbox[data-up] { transform-origin: bottom left; }
|
|
87
87
|
.crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
|
|
88
|
-
|
|
88
|
+
/* Composed from @seed-design/rootage's text-input.yaml
|
|
89
|
+
(variant=outline,size=medium) -- seed's Select has no search field of
|
|
90
|
+
its own to resolve this from (see the file header), so this reuses
|
|
91
|
+
the SAME tokens #867 already resolved for the trigger at this size:
|
|
92
|
+
minHeight x10 (${c.size.selectTriggerMd}), paddingX x3_5
|
|
93
|
+
(${c.space["3.5"]}), gap x2 (${c.space["2"]}), cornerRadius r2
|
|
94
|
+
(${c.radius.md}), strokeColor stroke.neutral-weak
|
|
95
|
+
(${c.strokeSelectTrigger}) -- a real bordered field sitting flush
|
|
96
|
+
inside the listbox's own 8px padding, replacing the old bare
|
|
97
|
+
34px/no-border/bleeds-to-the-edge strip (crisp/#904). */
|
|
98
|
+
.crisp-select-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.selectTriggerMd}; flex-shrink: 0; padding: 0 ${c.space["3.5"]}; border: 1px solid ${c.strokeSelectTrigger}; border-radius: ${c.radius.md}; color: ${c.fgNeutralMuted}; }
|
|
89
99
|
.crisp-select-search svg { flex-shrink: 0; }
|
|
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}; }
|
|
100
|
+
.crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; line-height: 19px; color: ${c.fgSelectNeutral}; }
|
|
91
101
|
.crisp-select-search input::placeholder { color: ${c.fgSelectPlaceholder}; }
|
|
92
102
|
.crisp-select-empty { padding: ${c.space["3"]} ${c.space["4"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgNeutralSubtle}; }
|
|
93
103
|
/* select.yaml's groupLabel -- font-size/line-height/weight are the same
|
|
@@ -121,9 +131,26 @@ const config = defineRecipe({
|
|
|
121
131
|
full: `width: 100%;`,
|
|
122
132
|
auto: `width: auto;`,
|
|
123
133
|
},
|
|
124
|
-
//
|
|
125
|
-
//
|
|
134
|
+
// Owner's follow-up after #867/#879 (crisp/#889): the LOOK stays seed's
|
|
135
|
+
// (border, listbox, radius, shadow, items, states, all unchanged below),
|
|
136
|
+
// but the SIZE is Attio's again -- a 40px trigger next to a 32px `Input`
|
|
137
|
+
// in the same form visibly mismatched (measured live in GRC's own "New
|
|
138
|
+
// employee" dialog). `small` is the new default: Attio's original
|
|
139
|
+
// 32px `c.size.menuRow`, `Input`'s own radius/padding/line-height so the
|
|
140
|
+
// two line up exactly, not just approximately. `medium`/`large` (seed's
|
|
141
|
+
// own 40/52px) stay, opt-in. Item rows are IDENTICAL across all three --
|
|
142
|
+
// only the trigger differs; only it was ever the mismatch.
|
|
126
143
|
size: {
|
|
144
|
+
small: `
|
|
145
|
+
& .crisp-select-trigger { height: ${c.size.menuRow}; border-radius: ${c.radius.md}; gap: ${c.space["1.5"]}; padding-inline: ${c.space["2.5"]}; }
|
|
146
|
+
& .crisp-select-value, & .crisp-select-placeholder { font-size: ${c.text.lg}; line-height: 20px; }
|
|
147
|
+
& .crisp-select-chevron { width: ${c.size.icon}; height: ${c.size.icon}; }
|
|
148
|
+
.crisp-select-option { gap: ${c.space["2"]}; }
|
|
149
|
+
.crisp-select-option-label { font-size: ${c.text.lg}; line-height: 19px; }
|
|
150
|
+
.crisp-select-option-description { font-size: ${c.text.sm}; line-height: 16px; }
|
|
151
|
+
.crisp-select-option-check { width: ${c.space["3"]}; height: ${c.space["3"]}; }
|
|
152
|
+
.crisp-select-label { padding-top: ${c.space["2"]}; }
|
|
153
|
+
`,
|
|
127
154
|
medium: `
|
|
128
155
|
& .crisp-select-trigger { height: ${c.size.selectTriggerMd}; border-radius: ${c.radius.md}; gap: ${c.space["2"]}; padding-inline: ${c.space["3.5"]}; }
|
|
129
156
|
& .crisp-select-value, & .crisp-select-placeholder { font-size: ${c.text.lg}; line-height: 19px; }
|
|
@@ -175,7 +202,7 @@ const config = defineRecipe({
|
|
|
175
202
|
},
|
|
176
203
|
defaultVariants: {
|
|
177
204
|
width: "full",
|
|
178
|
-
size: "
|
|
205
|
+
size: "small",
|
|
179
206
|
invalid: false,
|
|
180
207
|
variant: "default",
|
|
181
208
|
},
|
|
@@ -33,13 +33,14 @@ const spec = defineSpec({
|
|
|
33
33
|
muted: { color: c.fgNeutralMuted },
|
|
34
34
|
// #858: no new measurement -- both reuse tokens already measured
|
|
35
35
|
// against Attio for the same colours in a different arrangement.
|
|
36
|
-
// `brand` is `textlink`'s own reused brand-text colour
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
36
|
+
// `brand` is `textlink`'s own reused brand-text colour, now
|
|
37
|
+
// `fgBrandContrast` (your-moon/crisp#897: the plain accent,
|
|
38
|
+
// `bgBrandSolid`, only clears 3.23:1 as text on a white page).
|
|
39
|
+
// `danger` is `fgDanger` ("solid critical/destructive TEXT... an
|
|
40
|
+
// overdue due-date, a destructive menu item's label" -- see its own
|
|
41
|
+
// doc in tokens/semantic.ts), not `fgDangerSoft` (that one's a soft
|
|
42
|
+
// Badge pill's text, a different measured hex).
|
|
43
|
+
brand: { color: c.fgBrandContrast },
|
|
43
44
|
danger: { color: c.fgDanger },
|
|
44
45
|
},
|
|
45
46
|
},
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { Progress } from "../progress/index.js";
|
|
3
4
|
import { textareaRecipe } from "./textarea.recipe.js";
|
|
4
5
|
/**
|
|
5
6
|
* A multiline text field — measured from settings. Forwards every native
|
|
6
7
|
* `<textarea>` prop; pass `showCount` with `maxLength` to render a live
|
|
7
8
|
* character counter (which turns red if the value exceeds the limit).
|
|
8
9
|
*/
|
|
9
|
-
export const Textarea = React.forwardRef(({ showCount, maxLength, className, wrapperClassName, value, defaultValue, onChange,
|
|
10
|
+
export const Textarea = React.forwardRef(({ showCount, showProgress, maxLength, className, wrapperClassName, value, defaultValue, onChange,
|
|
10
11
|
// `style` is pulled out and put on the ROOT, the way Input does it.
|
|
11
12
|
// Everything else belongs on the <textarea> -- id, name, form
|
|
12
13
|
// association, aria -- but `style` sizes the visual box, and the box is
|
|
@@ -22,6 +23,6 @@ style, ...props }, ref) => {
|
|
|
22
23
|
if (value === undefined)
|
|
23
24
|
setCount(e.target.value.length);
|
|
24
25
|
onChange?.(e);
|
|
25
|
-
}, ...props }) }), counted && (_jsxs("span", { className: "crisp-textarea-count", "data-over": len > (maxLength ?? 0) ? "" : undefined, "aria-hidden": "true", children: [len, " / ", maxLength] }))] }));
|
|
26
|
+
}, ...props }) }), counted && (_jsxs("span", { className: "crisp-textarea-count", "data-over": len > (maxLength ?? 0) ? "" : undefined, "aria-hidden": "true", children: [showProgress && (_jsx(Progress, { variant: "circular", value: len, max: maxLength ?? 0, className: "crisp-textarea-progress" })), len, " / ", maxLength] }))] }));
|
|
26
27
|
});
|
|
27
28
|
Textarea.displayName = "Textarea";
|
|
@@ -46,8 +46,9 @@ const config = defineRecipe({
|
|
|
46
46
|
&[data-invalid] .crisp-textarea-wrap::after { border-color: ${c.bgDanger}; }
|
|
47
47
|
&[data-invalid] .crisp-textarea-wrap:focus-within::after { border-color: ${c.bgDanger}; }
|
|
48
48
|
&[data-counted] .crisp-textarea-field { padding-bottom: ${c.space["5"]}; }
|
|
49
|
-
& .crisp-textarea-count { position: absolute; inset-inline-end: 14px; bottom: ${c.space["2.5"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; pointer-events: none; }
|
|
49
|
+
& .crisp-textarea-count { position: absolute; inset-inline-end: 14px; bottom: ${c.space["2.5"]}; display: flex; align-items: center; gap: ${c.space["1"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; pointer-events: none; }
|
|
50
50
|
& .crisp-textarea-count[data-over] { color: ${c.bgDanger}; }
|
|
51
|
+
& .crisp-textarea-progress { flex-shrink: 0; }
|
|
51
52
|
`,
|
|
52
53
|
variants: {},
|
|
53
54
|
defaultVariants: {},
|
|
@@ -18,11 +18,14 @@ const config = defineRecipe({
|
|
|
18
18
|
variants: {
|
|
19
19
|
tone: {
|
|
20
20
|
// The bold, always-brand-coloured trailing link a Card's own `action`
|
|
21
|
-
// slot uses ("View all →", "All registers →").
|
|
22
|
-
|
|
21
|
+
// slot uses ("View all →", "All registers →"). your-moon/crisp#897:
|
|
22
|
+
// `fgBrandContrast`, not `bgBrandSolid` (#0298d9) directly -- as TEXT
|
|
23
|
+
// on a white page the accent itself only clears 3.23:1; the darker
|
|
24
|
+
// contrast-safe sibling clears 4.5:1+.
|
|
25
|
+
brand: `font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgBrandContrast};`,
|
|
23
26
|
// A quieter in-context reference (a sample record under a finding)
|
|
24
27
|
// that only goes brand on hover.
|
|
25
|
-
muted: `font-size: ${c.text.md}; font-weight: ${c.weight.regular}; color: ${c.fgMuted}; &:hover { color: ${c.
|
|
28
|
+
muted: `font-size: ${c.text.md}; font-weight: ${c.weight.regular}; color: ${c.fgMuted}; &:hover { color: ${c.fgBrandContrast}; }`,
|
|
26
29
|
},
|
|
27
30
|
// crisp#874: an explicit, OPTIONAL size override -- no `defaultVariants`
|
|
28
31
|
// entry below, so omitting `size` emits no `--size_*` class at all and
|
package/dist/esm/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// component, the design tokens, and the recipe core. Consumers also import the
|
|
3
3
|
// stylesheet once: import "@crisp-ui-kit/crisp/styles.css";
|
|
4
4
|
// primitives (components/)
|
|
5
|
+
export * from "./components/accentpicker/index.js";
|
|
5
6
|
export * from "./components/accordion/index.js";
|
|
6
7
|
export * from "./components/avatar/index.js";
|
|
7
8
|
export * from "./components/avatargroup/index.js";
|
|
@@ -1,14 +1,16 @@
|
|
|
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, Sparkles, SquareCheck, Star, Trash2, Users, UsersRound, Workflow, X, Zap, } from "lucide-react";
|
|
2
|
+
import { Activity as ActivityIcon, CalendarClock, ChevronDown, 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";
|
|
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";
|
|
6
|
+
import { Drawer } from "../../components/drawer/index.js";
|
|
6
7
|
import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
|
|
7
8
|
import { Select } from "../../components/select/index.js";
|
|
8
9
|
import { Spinner } from "../../components/spinner/index.js";
|
|
9
10
|
import { Switch } from "../../components/switch/index.js";
|
|
10
11
|
import { Tabs } from "../../components/tabs/index.js";
|
|
11
12
|
import { cx, isRtlElement, useCrispComponentDefaults } from "../../core/index.js";
|
|
13
|
+
import { CommentsPanel, } from "../comments/index.js";
|
|
12
14
|
import { tagTone } from "./column.js";
|
|
13
15
|
import { editableInputType, useEditableField } from "./editable-field.js";
|
|
14
16
|
import { recordpageRecipe } from "./recordpage.recipe.js";
|
|
@@ -33,9 +35,16 @@ export const recordPageLabels = {
|
|
|
33
35
|
breadcrumb: "Breadcrumb",
|
|
34
36
|
editRecord: "Edit record",
|
|
35
37
|
recordActions: "Record actions",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// US spelling, matching Attio's own live text exactly (#621) — this string
|
|
39
|
+
// is shared with the entity header's favourite-star aria-label, so both
|
|
40
|
+
// read "favorites" now, not just the menu.
|
|
41
|
+
addToFavourites: "Add to favorites",
|
|
42
|
+
removeFromFavourites: "Remove from favorites",
|
|
38
43
|
copyRecordLink: "Copy record link",
|
|
44
|
+
configurePage: "Configure page",
|
|
45
|
+
copyPageUrl: "Copy page URL",
|
|
46
|
+
copyRecordId: "Copy record ID",
|
|
47
|
+
mergeRecords: "Merge records",
|
|
39
48
|
deleteRecord: "Delete record",
|
|
40
49
|
linkRecords: "Link records",
|
|
41
50
|
// Tooltip copy re-measured live for #865 (was "Log a note"/"Create a
|
|
@@ -50,6 +59,8 @@ export const recordPageLabels = {
|
|
|
50
59
|
composeEmail: "Compose email",
|
|
51
60
|
askAttio: "Ask AI",
|
|
52
61
|
addToList: "Add to list",
|
|
62
|
+
share: "Share",
|
|
63
|
+
comments: "Comments",
|
|
53
64
|
noLists: "This record has not been added to any lists.",
|
|
54
65
|
highlights: "Highlights",
|
|
55
66
|
recordDetails: "Record Details",
|
|
@@ -423,7 +434,7 @@ RecordDetails.displayName = "RecordDetails";
|
|
|
423
434
|
* Emails sections (Notes & Tasks are live — add and check things off). Drop it
|
|
424
435
|
* into the app's main content area.
|
|
425
436
|
*/
|
|
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 }) {
|
|
437
|
+
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, comments: commentsProp, onPostComment, currentUser, onShare, ...rest }) {
|
|
427
438
|
const t = useRecordPageLabels(labels);
|
|
428
439
|
const [favorite, setFavorite] = React.useState(false);
|
|
429
440
|
// Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
|
|
@@ -435,6 +446,11 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
435
446
|
const [internalTasks, setInternalTasks] = React.useState([]);
|
|
436
447
|
const notes = notesProp ?? internalNotes;
|
|
437
448
|
const tasks = tasksProp ?? internalTasks;
|
|
449
|
+
// Comments follow the same controlled/self-contained duality (#864).
|
|
450
|
+
const [internalComments, setInternalComments] = React.useState([]);
|
|
451
|
+
const comments = commentsProp ?? internalComments;
|
|
452
|
+
const [commentsOpen, setCommentsOpen] = React.useState(false);
|
|
453
|
+
const me = currentUser ?? { id: "me", name: "You" };
|
|
438
454
|
const [showAllActivity, setShowAllActivity] = React.useState(false);
|
|
439
455
|
// Drag-resizable left details column.
|
|
440
456
|
const [detailsW, setDetailsW] = React.useState(DETAILS_DEFAULT_W);
|
|
@@ -593,6 +609,31 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
593
609
|
}
|
|
594
610
|
setInternalTasks((n) => n.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
|
|
595
611
|
};
|
|
612
|
+
const postComment = (text, threadId) => {
|
|
613
|
+
if (commentsProp) {
|
|
614
|
+
onPostComment?.(text, threadId);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (threadId == null) {
|
|
618
|
+
setInternalComments((prev) => [
|
|
619
|
+
{
|
|
620
|
+
id: nextId(),
|
|
621
|
+
root: { id: nextId(), author: me, time: t.justNow, body: text },
|
|
622
|
+
},
|
|
623
|
+
...prev,
|
|
624
|
+
]);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
setInternalComments((prev) => prev.map((th) => th.id === threadId
|
|
628
|
+
? {
|
|
629
|
+
...th,
|
|
630
|
+
replies: [
|
|
631
|
+
...(th.replies ?? []),
|
|
632
|
+
{ id: nextId(), author: me, time: t.justNow, body: text },
|
|
633
|
+
],
|
|
634
|
+
}
|
|
635
|
+
: th));
|
|
636
|
+
};
|
|
596
637
|
// A small, plausible activity feed derived from the record's own fields.
|
|
597
638
|
const domain = byKey.get("domain");
|
|
598
639
|
const cats = byKey.get("categories");
|
|
@@ -649,7 +690,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
649
690
|
const effectivePanel = tabsProp
|
|
650
691
|
? (active) => tabsProp.find((tb) => tb.id === active)?.render()
|
|
651
692
|
: builtinPanel;
|
|
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(
|
|
693
|
+
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: [_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", onClick: onShare, children: t.share }), _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: () => setCommentsOpen(true), "aria-label": t.comments, children: _jsx(MessageSquare, { size: 15, strokeWidth: 1.75 }) }), _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(Merge, { size: 15, strokeWidth: 1.75 }), children: t.mergeRecords }), _jsx(MenuItem, { icon: _jsx(Settings, { size: 15, strokeWidth: 1.75 }), children: t.configurePage }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyPageUrl }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyRecordId }), _jsx(MenuSeparator, {}), _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, 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) => {
|
|
653
694
|
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
654
695
|
return;
|
|
655
696
|
// The pointer drag learned its direction in #212; this did not.
|
|
@@ -661,6 +702,6 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
661
702
|
setDetailsW((w) => grow
|
|
662
703
|
? Math.min(DETAILS_MAX_W, w + 16)
|
|
663
704
|
: Math.max(DETAILS_MIN_W, w - 16));
|
|
664
|
-
} }), _jsx("main", { className: "crisp-recordpage-main", children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] })] }));
|
|
705
|
+
} }), _jsx("main", { className: "crisp-recordpage-main", 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 }) })] }));
|
|
665
706
|
}
|
|
666
707
|
RecordPage.displayName = "RecordPage";
|
|
@@ -5,9 +5,14 @@ export const palette = {
|
|
|
5
5
|
// on a bright tone fill. Same value as gray900's light side (the measured
|
|
6
6
|
// near-black), named separately because its job is not "primary foreground".
|
|
7
7
|
ink: { light: "#101112", dark: "#101112" },
|
|
8
|
-
|
|
8
|
+
// seed-identity migration (your-moon/crisp#890): seed's bg.layer-default-
|
|
9
|
+
// pressed / gray-100. Light side only — dark is untouched, unmeasured
|
|
10
|
+
// Attio territory this migration doesn't scope into.
|
|
11
|
+
gray50: { light: "#f7f8f9", dark: "#101112" },
|
|
9
12
|
gray900: { light: "#101112", dark: "#f5f5f5" },
|
|
10
|
-
|
|
13
|
+
// seed-identity migration (your-moon/crisp#890): seed's stroke.neutral-
|
|
14
|
+
// weak / gray-400. Light side only, same scoping as `gray50` above.
|
|
15
|
+
line200: { light: "#dcdee3", dark: "#212224" },
|
|
11
16
|
line100: { light: "#eeeff1", dark: "#1c1d1f" },
|
|
12
17
|
// ImportDialog's dropzone border — measured live from Attio's Import CSV
|
|
13
18
|
// flow (`rgb(230,231,234)`), distinct from `line200`/`line100` (#e4e4e7 /
|
|
@@ -19,8 +24,11 @@ export const palette = {
|
|
|
19
24
|
line150: { light: "#e6e7ea", dark: "#212224" },
|
|
20
25
|
// The filter chip's field/value segments while the condition is
|
|
21
26
|
// incomplete (no value typed yet) — measured live on Attio's own filter
|
|
22
|
-
// chip (`rgb(246,247,247)`),
|
|
23
|
-
//
|
|
27
|
+
// chip (`rgb(246,247,247)`), close to but not equal to `gray50`
|
|
28
|
+
// (`#f7f8f9` since the seed-identity migration, your-moon/crisp#890 — the
|
|
29
|
+
// two used to differ the other direction, gray25 lighter; the relative
|
|
30
|
+
// order isn't the point, staying off `gray50` while remaining a
|
|
31
|
+
// near-neighbour is). The chip's operator/kebab segments stay `gray00` in the same
|
|
24
32
|
// state; only the two text-content segments carry this muted fill. Dark
|
|
25
33
|
// value not independently re-measured (Attio's dark theme is off-limits to
|
|
26
34
|
// toggle) — reuses `gray50`'s dark value, the nearest existing "muted
|
|
@@ -81,25 +89,54 @@ export const palette = {
|
|
|
81
89
|
shadowPopover2: { light: "rgba(28,40,64,0.12)", dark: "rgba(0,0,0,0.24)" },
|
|
82
90
|
shadowPopover3: { light: "rgba(24,41,75,0.16)", dark: "rgba(0,0,0,0.32)" },
|
|
83
91
|
gray400: { light: "#71717a", dark: "#9a9aa2" },
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
// the
|
|
101
|
-
//
|
|
102
|
-
|
|
92
|
+
// seed-identity migration (your-moon/crisp#890, "Attio structure, seed
|
|
93
|
+
// look"): the owner's TechPartners primary (#0298D9, sourced from the
|
|
94
|
+
// official logo SVG + the live site's own compiled CSS — see
|
|
95
|
+
// ~/grc-parity/seed-identity-spec.md §1) replaces the old measured-from-
|
|
96
|
+
// Attio brand blue, built as seed's own carrot-scale RELATIVE lightness
|
|
97
|
+
// shape around that exact hex at step 600 ("Method A" in the spec — the
|
|
98
|
+
// verified logo hex stays literally in the scale, at the step that's
|
|
99
|
+
// actually rendered, rather than a mathematically tidier neighbour).
|
|
100
|
+
// Does not flip with theme, matching this token's own prior precedent.
|
|
101
|
+
brand600: { light: "#0298d9", dark: "#0298d9" },
|
|
102
|
+
// Method A's raw carrot-shape output (L 37.1%) — the hover/pressed-darken
|
|
103
|
+
// step of the ACCENT colour above, same role brand700 has always had.
|
|
104
|
+
// Not tuned for any particular contrast target; see `brandContrast` below
|
|
105
|
+
// for the token that carries that job.
|
|
106
|
+
brand700: { light: "#0283bb", dark: "#0283bb" },
|
|
107
|
+
// your-moon/crisp#897: #0298d9 (the verified brand hex, kept exactly as
|
|
108
|
+
// the ACCENT everywhere else — focus rings, selected states, icons,
|
|
109
|
+
// tints) only clears 3.23:1 against white, short of the 4.5:1 a SOLID
|
|
110
|
+
// fill carrying white text needs (Button primary, Badge solid+brand,
|
|
111
|
+
// Avatar's brand tone), or brand-coloured TEXT sitting directly on a white
|
|
112
|
+
// page (TextLink/Text's brand tone). This is that darker, AA-safe step —
|
|
113
|
+
// same hue (198°), L dropped to 34% — clearing 4.88:1 with white. An
|
|
114
|
+
// earlier round of this fix used dark ink text instead of finding this
|
|
115
|
+
// value; that read off-brand (seed's own solid buttons use white) and got
|
|
116
|
+
// reverted in favour of this.
|
|
117
|
+
brandContrast: { light: "#0278ac", dark: "#0278ac" },
|
|
118
|
+
// seed's red-700 (fg.critical) — the accent used for plain danger TEXT
|
|
119
|
+
// (an overdue due-date, a destructive menu label) and for tinting
|
|
120
|
+
// (Chip/Badge's soft-danger `color-mix()` inputs). NOT used for a solid
|
|
121
|
+
// fill paired with white text — see `red700` below for why that split
|
|
122
|
+
// matters here specifically.
|
|
123
|
+
red600: { light: "#fa342c", dark: "#ff5a5f" },
|
|
124
|
+
// seed's red-800 (bg.critical-solid-**pressed**), not red-700
|
|
125
|
+
// (bg.critical-solid-enabled, `red600` above) — deliberately the darker
|
|
126
|
+
// of the two. `red700` is crisp's SOLID destructive fill (Button danger,
|
|
127
|
+
// Badge solid+danger), which pairs with white text and, on Button,
|
|
128
|
+
// DARKENS further on hover/active via a brightness filter (not a second
|
|
129
|
+
// token, per this token's own original comment below this migration note
|
|
130
|
+
// — that invariant is why a dark-enough red matters at all). Seed's own
|
|
131
|
+
// red-700 only clears 3.76:1 with white (measured) — short of AA even
|
|
132
|
+
// before any hover darkening — so reusing crisp's existing "needs to
|
|
133
|
+
// survive getting darker" role means red-800 (5.68:1 at rest, improving
|
|
134
|
+
// through hover/active) is the correct step, not seed's literal
|
|
135
|
+
// "enabled" one. `red500` below is pure text again (no fill/contrast
|
|
136
|
+
// constraint), so it stays on the
|
|
137
|
+
// lighter red-700, matching seed exactly.
|
|
138
|
+
red700: { light: "#ca1d13", dark: "#ba2525" },
|
|
139
|
+
red500: { light: "#fa342c", dark: "#ff5a5f" },
|
|
103
140
|
// seed-design's field focus ring (stroke-neutral-contrast = gray-1000),
|
|
104
141
|
// measured from @seed-design/css. A high-contrast neutral rather than a brand
|
|
105
142
|
// hue: the ring reads as "this field has focus", not as a state colour.
|
|
@@ -119,8 +156,25 @@ export const palette = {
|
|
|
119
156
|
neutralSubtle: { light: "#868b94", dark: "#b0b3ba" },
|
|
120
157
|
neutralMuted: { light: "#555d6d", dark: "#dcdee3" },
|
|
121
158
|
focusRing: { light: "#1e82eb", dark: "#5e98fe" },
|
|
122
|
-
|
|
123
|
-
|
|
159
|
+
// seed's fg.positive / bg.positive-solid (green-700).
|
|
160
|
+
green600: { light: "#079171", dark: "#2ea968" },
|
|
161
|
+
// seed's fg.warning / stroke.warning-solid (yellow-700) — the ACCENT used
|
|
162
|
+
// for tinting (Badge/Chip's `soft()`/`ring()` colour-mix formulas), not
|
|
163
|
+
// seed's actual solid-fill colour, which is a much paler yellow at a
|
|
164
|
+
// different scale step — see `amberSolid` below for why the two need to
|
|
165
|
+
// stay separate tokens.
|
|
166
|
+
amber600: { light: "#9b7821", dark: "#e0982a" },
|
|
167
|
+
// seed's bg.warning-solid (yellow-300) — genuinely NOT amber600 tinted
|
|
168
|
+
// lighter: seed's warning-solid is its own pale scale step, distinct from
|
|
169
|
+
// the darker accent every other tone's solid fill derives from (see the
|
|
170
|
+
// spec's Badge section). Badge's `variant="solid" tone="warning"` uses
|
|
171
|
+
// this directly, unmixed, the same way `red700`'s solid destructive fill
|
|
172
|
+
// is used directly rather than computed from a tint. Dark side is NOT
|
|
173
|
+
// independently sourced (this is a new token; no prior Attio measurement
|
|
174
|
+
// exists to preserve) — reuses amber600's own dark value as the nearest
|
|
175
|
+
// existing "warning, dark theme" tone, same convention this file uses
|
|
176
|
+
// throughout for an unmeasured dark side (ringSwitch, line150, gray25).
|
|
177
|
+
amberSolid: { light: "#fbdc65", dark: "#e0982a" },
|
|
124
178
|
orange600: { light: "#f97514", dark: "#f97514" },
|
|
125
179
|
cyan600: { light: "#00b9eb", dark: "#00b9eb" },
|
|
126
180
|
purple600: { light: "#9b69ff", dark: "#9b69ff" },
|
package/dist/esm/tokens/scale.js
CHANGED
|
@@ -26,6 +26,7 @@ export const space = {
|
|
|
26
26
|
// Corner radius. sm/md/lg are the three used on stacked surfaces
|
|
27
27
|
// (inputs, rows, tiles); full is the pill.
|
|
28
28
|
export const radius = {
|
|
29
|
+
xs: "4px", // seed-design's $radius.r1 — small badge / checkbox box corner
|
|
29
30
|
chip: "7px", // tag / chip corner — the single most used radius
|
|
30
31
|
sm: "6px", // chips, badges
|
|
31
32
|
md: "8px", // inputs, search box
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
export const semantic = {
|
|
2
2
|
bgBrandSolid: "brand600",
|
|
3
3
|
bgBrandSolidPressed: "brand700",
|
|
4
|
+
/** your-moon/crisp#897: the AA-safe darker sibling of `bgBrandSolid` — a
|
|
5
|
+
* SOLID fill carrying white text (Button primary, Badge solid+brand,
|
|
6
|
+
* Avatar's brand tone). `bgBrandSolid` itself stays the exact brand
|
|
7
|
+
* accent (focus rings, selected states, icons, tints), unpaired with
|
|
8
|
+
* white text anywhere that matters for contrast. */
|
|
9
|
+
bgBrandContrast: "brandContrast",
|
|
10
|
+
/** your-moon/crisp#897: brand-coloured TEXT sitting on a white page
|
|
11
|
+
* (TextLink/Text's `brand` tone) — same value as `bgBrandContrast`,
|
|
12
|
+
* named separately because one's a background and one's a foreground,
|
|
13
|
+
* matching every other fg/bg pair in this file. */
|
|
14
|
+
fgBrandContrast: "brandContrast",
|
|
4
15
|
fgOnBrand: "white",
|
|
5
16
|
// text on a solid success/warning/danger fill. Those tone tokens BRIGHTEN in
|
|
6
17
|
// dark (they are tuned for tinted backgrounds and coloured text), so white on
|
|
@@ -38,6 +49,15 @@ export const semantic = {
|
|
|
38
49
|
shadowPopover2: "shadowPopover2",
|
|
39
50
|
shadowPopover3: "shadowPopover3",
|
|
40
51
|
fgPrimary: "gray900",
|
|
52
|
+
// seed-identity migration (your-moon/crisp#890): NOT touched here, on
|
|
53
|
+
// purpose. seed's closest analog (fg.neutral-subtle, #868b94) is lighter
|
|
54
|
+
// than crisp's current #71717a, and Badge's neutral-SOLID variant reuses
|
|
55
|
+
// this exact token as a BACKGROUND paired with white text
|
|
56
|
+
// (`badge.recipe.ts`'s compound variant) — #868b94 drops that pairing to
|
|
57
|
+
// 3.42:1, below AA. Retargeting `fgMuted` and fixing that one background
|
|
58
|
+
// choice belong together in Badge's own upcoming look PR, not split
|
|
59
|
+
// across two PRs where the token change alone would ship a known
|
|
60
|
+
// contrast regression.
|
|
41
61
|
fgMuted: "gray400",
|
|
42
62
|
bgDanger: "red600",
|
|
43
63
|
/** Fill for a SOLID destructive control that carries white text. */
|
|
@@ -63,6 +83,10 @@ export const semantic = {
|
|
|
63
83
|
strokeFocusRing: "focusRing",
|
|
64
84
|
bgSuccess: "green600",
|
|
65
85
|
bgWarning: "amber600",
|
|
86
|
+
/** A SOLID warning fill (Badge's `variant="solid" tone="warning"`) — seed's
|
|
87
|
+
* own warning-solid is a distinct, paler yellow, not `bgWarning` tinted
|
|
88
|
+
* lighter. See `amberSolid` in palette.ts. */
|
|
89
|
+
bgWarningSolid: "amberSolid",
|
|
66
90
|
bgOrange: "orange600",
|
|
67
91
|
bgCyan: "cyan600",
|
|
68
92
|
bgPurple: "purple600",
|
package/dist/input.css
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
& .crisp-input-value::placeholder { color: var(--crisp-fg-muted); }
|
|
7
7
|
& .crisp-input-value:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
8
8
|
&:has(.crisp-input-value:disabled) { opacity: 1; }
|
|
9
|
+
/* crisp#634: measured live on Attio's own Settings > Profile read-only
|
|
10
|
+
primary-email field -- a tinted field, not just muted text, driven off
|
|
11
|
+
the native `readOnly` attribute. `ringField` (rgba(0,0,0,.06)) is an
|
|
12
|
+
exact match for the measured ring; the 2%-fgPrimary tint reuses the
|
|
13
|
+
same color-mix idiom every other soft crisp surface already uses. */
|
|
14
|
+
&[data-readonly] { background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); box-shadow: inset 0 0 0 1px var(--crisp-ring-field); }
|
|
15
|
+
& .crisp-input-suffix { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); }
|
|
9
16
|
}
|
|
10
17
|
.crisp-input--size_sm {
|
|
11
18
|
height: var(--crisp-size-row); border-radius: var(--crisp-radius-md);
|