@crisp-ui-kit/crisp 0.56.0 → 0.57.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accentpicker.css +5 -0
- package/dist/accentpicker.d.ts +7 -0
- package/dist/accentpicker.layered.css +7 -0
- package/dist/avatar.css +1 -1
- package/dist/avatar.layered.css +1 -1
- package/dist/badge.css +2 -2
- package/dist/badge.layered.css +2 -2
- package/dist/button.css +3 -3
- package/dist/button.layered.css +3 -3
- package/dist/callout.css +6 -0
- package/dist/callout.d.ts +2 -2
- package/dist/callout.layered.css +6 -0
- package/dist/cjs/components/accentpicker/accentpicker.js +66 -0
- package/dist/cjs/components/accentpicker/accentpicker.recipe.js +25 -0
- package/dist/cjs/components/accentpicker/index.js +7 -0
- package/dist/cjs/components/avatar/avatar.recipe.js +4 -1
- package/dist/cjs/components/badge/badge.recipe.js +16 -2
- package/dist/cjs/components/button/button.recipe.js +16 -6
- package/dist/cjs/components/callout/callout.js +2 -2
- package/dist/cjs/components/callout/callout.recipe.js +15 -2
- package/dist/cjs/components/input/input.js +2 -2
- package/dist/cjs/components/input/input.recipe.js +14 -0
- package/dist/cjs/components/menu/menu.js +2 -2
- package/dist/cjs/components/menu/menu.recipe.js +8 -0
- package/dist/cjs/components/multiselect/multiselect.js +23 -10
- package/dist/cjs/components/multiselect/multiselect.recipe.js +104 -65
- package/dist/cjs/components/progress/progress.js +21 -4
- package/dist/cjs/components/progress/progress.recipe.js +30 -0
- package/dist/cjs/components/select/listbox-keyboard.js +7 -1
- package/dist/cjs/components/select/select.js +24 -11
- package/dist/cjs/components/select/select.recipe.js +177 -78
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +3 -3
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
- package/dist/cjs/components/tabs/tabs.js +63 -4
- package/dist/cjs/components/tabs/tabs.recipe.js +13 -0
- package/dist/cjs/components/text/text.recipe.js +8 -7
- package/dist/cjs/components/textarea/textarea.js +3 -2
- package/dist/cjs/components/textarea/textarea.recipe.js +2 -1
- package/dist/cjs/components/textlink/textlink.js +2 -2
- package/dist/cjs/components/textlink/textlink.recipe.js +24 -3
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/product/recordpage/recordpage.js +61 -7
- package/dist/cjs/tokens/elevation.js +7 -0
- package/dist/cjs/tokens/palette.js +120 -25
- package/dist/cjs/tokens/scale.js +9 -0
- package/dist/cjs/tokens/semantic.js +42 -0
- package/dist/esm/components/accentpicker/accentpicker.js +30 -0
- package/dist/esm/components/accentpicker/accentpicker.recipe.js +22 -0
- package/dist/esm/components/accentpicker/index.js +2 -0
- package/dist/esm/components/avatar/avatar.recipe.js +4 -1
- package/dist/esm/components/badge/badge.recipe.js +16 -2
- package/dist/esm/components/button/button.recipe.js +16 -6
- package/dist/esm/components/callout/callout.js +2 -2
- package/dist/esm/components/callout/callout.recipe.js +15 -2
- package/dist/esm/components/input/input.js +3 -3
- package/dist/esm/components/input/input.recipe.js +14 -0
- package/dist/esm/components/menu/menu.js +2 -2
- package/dist/esm/components/menu/menu.recipe.js +8 -0
- package/dist/esm/components/multiselect/multiselect.js +23 -10
- package/dist/esm/components/multiselect/multiselect.recipe.js +104 -65
- package/dist/esm/components/progress/progress.js +22 -5
- package/dist/esm/components/progress/progress.recipe.js +30 -0
- package/dist/esm/components/select/listbox-keyboard.js +7 -1
- package/dist/esm/components/select/select.js +24 -11
- package/dist/esm/components/select/select.recipe.js +177 -78
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +3 -3
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +26 -1
- package/dist/esm/components/tabs/tabs.js +63 -4
- package/dist/esm/components/tabs/tabs.recipe.js +13 -0
- package/dist/esm/components/text/text.recipe.js +8 -7
- package/dist/esm/components/textarea/textarea.js +3 -2
- package/dist/esm/components/textarea/textarea.recipe.js +2 -1
- package/dist/esm/components/textlink/textlink.js +2 -2
- package/dist/esm/components/textlink/textlink.recipe.js +24 -3
- package/dist/esm/index.js +1 -0
- package/dist/esm/product/recordpage/recordpage.js +62 -8
- package/dist/esm/tokens/elevation.js +7 -0
- package/dist/esm/tokens/palette.js +120 -25
- package/dist/esm/tokens/scale.js +9 -0
- package/dist/esm/tokens/semantic.js +42 -0
- package/dist/input.css +11 -0
- package/dist/input.d.ts +2 -2
- package/dist/input.layered.css +11 -0
- package/dist/menu.css +8 -0
- package/dist/menu.layered.css +8 -0
- package/dist/multiselect.css +57 -29
- package/dist/multiselect.d.ts +3 -1
- package/dist/multiselect.layered.css +57 -29
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +82 -28
- package/dist/progress.css +3 -0
- package/dist/progress.layered.css +3 -0
- package/dist/select.css +100 -42
- package/dist/select.d.ts +3 -2
- package/dist/select.layered.css +100 -42
- package/dist/styles.css +268 -93
- package/dist/styles.layered.css +268 -93
- package/dist/tabletoolbar.css +8 -1
- package/dist/tabletoolbar.layered.css +8 -1
- package/dist/tabs.css +6 -0
- package/dist/tabs.layered.css +6 -0
- package/dist/text.css +1 -1
- package/dist/text.layered.css +1 -1
- package/dist/textarea.css +2 -1
- package/dist/textarea.layered.css +2 -1
- package/dist/textlink.css +11 -2
- package/dist/textlink.d.ts +2 -1
- package/dist/textlink.layered.css +11 -2
- package/dist/tokens.css +43 -11
- package/dist/tokens.json +82 -28
- package/dist/types/components/accentpicker/accentpicker.d.ts +23 -0
- package/dist/types/components/accentpicker/accentpicker.recipe.d.ts +3 -0
- package/dist/types/components/accentpicker/index.d.ts +2 -0
- package/dist/types/components/button/button.recipe.d.ts +2 -2
- package/dist/types/components/callout/callout.d.ts +9 -1
- package/dist/types/components/callout/callout.recipe.d.ts +12 -2
- package/dist/types/components/input/input.d.ts +14 -0
- package/dist/types/components/input/input.recipe.d.ts +3 -0
- package/dist/types/components/menu/menu.d.ts +7 -1
- package/dist/types/components/multiselect/multiselect.d.ts +25 -6
- package/dist/types/components/multiselect/multiselect.recipe.d.ts +27 -0
- package/dist/types/components/progress/progress.d.ts +16 -4
- package/dist/types/components/progress/progress.recipe.d.ts +22 -0
- package/dist/types/components/select/listbox-keyboard.d.ts +10 -1
- package/dist/types/components/select/select.d.ts +29 -6
- package/dist/types/components/select/select.recipe.d.ts +25 -10
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +14 -0
- package/dist/types/components/tabs/tabs.d.ts +9 -1
- package/dist/types/components/textarea/textarea.d.ts +16 -0
- package/dist/types/components/textlink/textlink.d.ts +8 -1
- package/dist/types/components/textlink/textlink.recipe.d.ts +10 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/product/recordpage/recordpage.d.ts +61 -1
- package/dist/types/tokens/contract.d.ts +16 -3
- package/dist/types/tokens/elevation.d.ts +7 -0
- package/dist/types/tokens/palette.d.ts +55 -11
- package/dist/types/tokens/scale.d.ts +6 -0
- package/dist/types/tokens/semantic.d.ts +33 -0
- package/package.json +6 -1
|
@@ -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: {},
|
|
@@ -7,7 +7,7 @@ import { textlinkRecipe } from "./textlink.recipe.js";
|
|
|
7
7
|
* `event.preventDefault()` pairs it with client-side routing (e.g.
|
|
8
8
|
* react-router's own `Link`).
|
|
9
9
|
*/
|
|
10
|
-
export function TextLink({ tone = "muted", className, ...rest }) {
|
|
11
|
-
return _jsx("a", { className: textlinkRecipe({ tone, className }), ...rest });
|
|
10
|
+
export function TextLink({ tone = "muted", size, className, ...rest }) {
|
|
11
|
+
return _jsx("a", { className: textlinkRecipe({ tone, size, className }), ...rest });
|
|
12
12
|
}
|
|
13
13
|
TextLink.displayName = "TextLink";
|
|
@@ -18,11 +18,32 @@ 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}; }`,
|
|
29
|
+
},
|
|
30
|
+
// crisp#874: an explicit, OPTIONAL size override -- no `defaultVariants`
|
|
31
|
+
// entry below, so omitting `size` emits no `--size_*` class at all and
|
|
32
|
+
// each tone's own hard-coded font-size (above) stands completely
|
|
33
|
+
// unchanged. Only when a caller passes `size` explicitly does this
|
|
34
|
+
// override it: `size`'s CSS block is emitted AFTER `tone`'s in the
|
|
35
|
+
// generated stylesheet (`generate-recipes.mjs` walks `variants` in
|
|
36
|
+
// object-key order), so at equal specificity it wins the cascade.
|
|
37
|
+
// Measured live (a running GRC server, `getComputedStyle`):
|
|
38
|
+
// internal/grc's two primary-column record links
|
|
39
|
+
// (routes/lists/Lists.tsx:51, routes/registers/Registers.tsx:63) are
|
|
40
|
+
// 14px/500 ambient, inherited from DataTable's own
|
|
41
|
+
// `.crisp-datatable-entity-name` -- `size="lg"` matches that context
|
|
42
|
+
// without changing `brand`'s weight/colour.
|
|
43
|
+
size: {
|
|
44
|
+
sm: `font-size: ${c.text.sm};`,
|
|
45
|
+
md: `font-size: ${c.text.md};`,
|
|
46
|
+
lg: `font-size: ${c.text.lg};`,
|
|
26
47
|
},
|
|
27
48
|
},
|
|
28
49
|
defaultVariants: { tone: "muted" },
|
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,
|
|
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,19 +35,32 @@ 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
|
-
|
|
42
|
-
|
|
50
|
+
// Tooltip copy re-measured live for #865 (was "Log a note"/"Create a
|
|
51
|
+
// task" — the field names stay, only the visible text changes to match
|
|
52
|
+
// Attio's own React-tooltip strings on these two icon buttons exactly).
|
|
53
|
+
logANote: "New note",
|
|
54
|
+
createATask: "New task",
|
|
55
|
+
runWorkflow: "Run workflow",
|
|
43
56
|
fullScreen: "Full screen",
|
|
44
57
|
exitFullScreen: "Exit full screen",
|
|
45
58
|
resizeDetails: "Resize details column",
|
|
46
59
|
composeEmail: "Compose email",
|
|
47
60
|
askAttio: "Ask AI",
|
|
48
61
|
addToList: "Add to list",
|
|
62
|
+
share: "Share",
|
|
63
|
+
comments: "Comments",
|
|
49
64
|
noLists: "This record has not been added to any lists.",
|
|
50
65
|
highlights: "Highlights",
|
|
51
66
|
recordDetails: "Record Details",
|
|
@@ -419,7 +434,7 @@ RecordDetails.displayName = "RecordDetails";
|
|
|
419
434
|
* Emails sections (Notes & Tasks are live — add and check things off). Drop it
|
|
420
435
|
* into the app's main content area.
|
|
421
436
|
*/
|
|
422
|
-
export function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, ...rest }) {
|
|
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 }) {
|
|
423
438
|
const t = useRecordPageLabels(labels);
|
|
424
439
|
const [favorite, setFavorite] = React.useState(false);
|
|
425
440
|
// Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
|
|
@@ -431,6 +446,11 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
431
446
|
const [internalTasks, setInternalTasks] = React.useState([]);
|
|
432
447
|
const notes = notesProp ?? internalNotes;
|
|
433
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" };
|
|
434
454
|
const [showAllActivity, setShowAllActivity] = React.useState(false);
|
|
435
455
|
// Drag-resizable left details column.
|
|
436
456
|
const [detailsW, setDetailsW] = React.useState(DETAILS_DEFAULT_W);
|
|
@@ -528,6 +548,15 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
528
548
|
},
|
|
529
549
|
];
|
|
530
550
|
const highlights = highlightsProp === undefined ? defaultHighlights : highlightsProp;
|
|
551
|
+
// #865: the built-in CRM icon set for the quick-action row, used when the
|
|
552
|
+
// caller omits `quickActions` -- same "caller's own list, not a merge"
|
|
553
|
+
// choice `tabs` (#379) and `highlights` (#528) already make.
|
|
554
|
+
const defaultQuickActions = [
|
|
555
|
+
{ label: t.logANote, icon: FileText },
|
|
556
|
+
{ label: t.runWorkflow, icon: Workflow },
|
|
557
|
+
{ label: t.createATask, icon: SquareCheck },
|
|
558
|
+
];
|
|
559
|
+
const quickActions = quickActionsProp ?? defaultQuickActions;
|
|
531
560
|
const ti = { size: 15, strokeWidth: 1.75 };
|
|
532
561
|
const builtinTabs = [
|
|
533
562
|
{ value: "overview", label: t.tabs.overview, icon: _jsx(LayoutGrid, { ...ti }) },
|
|
@@ -580,6 +609,31 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
580
609
|
}
|
|
581
610
|
setInternalTasks((n) => n.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
|
|
582
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
|
+
};
|
|
583
637
|
// A small, plausible activity feed derived from the record's own fields.
|
|
584
638
|
const domain = byKey.get("domain");
|
|
585
639
|
const cats = byKey.get("categories");
|
|
@@ -636,7 +690,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
636
690
|
const effectivePanel = tabsProp
|
|
637
691
|
? (active) => tabsProp.find((tb) => tb.id === active)?.render()
|
|
638
692
|
: builtinPanel;
|
|
639
|
-
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(
|
|
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) => {
|
|
640
694
|
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
641
695
|
return;
|
|
642
696
|
// The pointer drag learned its direction in #212; this did not.
|
|
@@ -648,6 +702,6 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
648
702
|
setDetailsW((w) => grow
|
|
649
703
|
? Math.min(DETAILS_MAX_W, w + 16)
|
|
650
704
|
: Math.max(DETAILS_MIN_W, w - 16));
|
|
651
|
-
} }), _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 }) })] }));
|
|
652
706
|
}
|
|
653
707
|
RecordPage.displayName = "RecordPage";
|
|
@@ -56,4 +56,11 @@ export const elevation = {
|
|
|
56
56
|
* nobody measured for them.
|
|
57
57
|
*/
|
|
58
58
|
fieldNoRing: `0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04)`,
|
|
59
|
+
/**
|
|
60
|
+
* Select's own listbox shadow (crisp/#867) — seed-design's `$shadow.s3`, a
|
|
61
|
+
* single layer, NOT `floating` above. This component follows seed, not
|
|
62
|
+
* Attio, for the same reason `DatePicker` follows seed's calendar
|
|
63
|
+
* structure: an explicit owner request to match seed-design exactly.
|
|
64
|
+
*/
|
|
65
|
+
selectFloating: `0 4px 16px ${c.shadowSelect}`,
|
|
59
66
|
};
|
|
@@ -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
|
|
@@ -44,6 +52,15 @@ export const palette = {
|
|
|
44
52
|
// both themes (Switch's own spec), so this reuses ringField's dark value
|
|
45
53
|
// rather than inventing a third "field-adjacent ring, dark" colour.
|
|
46
54
|
ringSwitch: { light: "#d9dade", dark: "#212224" },
|
|
55
|
+
// Callout's bordered/banner look (crisp#869) — measured live on Attio's
|
|
56
|
+
// Objects settings page (the "Create custom objects... Upgrade" Pro-plan
|
|
57
|
+
// banner): `border: 1px solid rgb(214,229,255)`, an opaque colour, not a
|
|
58
|
+
// composited tint of `bgBrandSolid` (checked: no single color-mix
|
|
59
|
+
// percentage matches all three channels at once). Dark value not
|
|
60
|
+
// independently re-measured (Attio's own theme toggle is off-limits) —
|
|
61
|
+
// reuses `ringField`/`ringSwitch`'s existing dark ring (#212224), the same
|
|
62
|
+
// "edge visible against a dark surface" role those already establish.
|
|
63
|
+
ringBrandSoft: { light: "#d6e5ff", dark: "#212224" },
|
|
47
64
|
tileActive: { light: "#ffffff", dark: "#101112" },
|
|
48
65
|
tileActiveShadow: { light: "rgba(0,0,0,0.04)", dark: "rgba(0,0,0,0)" },
|
|
49
66
|
// Dialog's page-dimming backdrop (crisp#677) — measured live behind the
|
|
@@ -72,25 +89,54 @@ export const palette = {
|
|
|
72
89
|
shadowPopover2: { light: "rgba(28,40,64,0.12)", dark: "rgba(0,0,0,0.24)" },
|
|
73
90
|
shadowPopover3: { light: "rgba(24,41,75,0.16)", dark: "rgba(0,0,0,0.32)" },
|
|
74
91
|
gray400: { light: "#71717a", dark: "#9a9aa2" },
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
// the
|
|
92
|
-
//
|
|
93
|
-
|
|
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" },
|
|
94
140
|
// seed-design's field focus ring (stroke-neutral-contrast = gray-1000),
|
|
95
141
|
// measured from @seed-design/css. A high-contrast neutral rather than a brand
|
|
96
142
|
// hue: the ring reads as "this field has focus", not as a state colour.
|
|
@@ -110,8 +156,25 @@ export const palette = {
|
|
|
110
156
|
neutralSubtle: { light: "#868b94", dark: "#b0b3ba" },
|
|
111
157
|
neutralMuted: { light: "#555d6d", dark: "#dcdee3" },
|
|
112
158
|
focusRing: { light: "#1e82eb", dark: "#5e98fe" },
|
|
113
|
-
|
|
114
|
-
|
|
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" },
|
|
115
178
|
orange600: { light: "#f97514", dark: "#f97514" },
|
|
116
179
|
cyan600: { light: "#00b9eb", dark: "#00b9eb" },
|
|
117
180
|
purple600: { light: "#9b69ff", dark: "#9b69ff" },
|
|
@@ -159,4 +222,36 @@ export const palette = {
|
|
|
159
222
|
purpleTint: { light: "#f5f0ff", dark: "#2f1e5a" },
|
|
160
223
|
softPurpleText: { light: "#6238b5", dark: "#d8c4ff" },
|
|
161
224
|
purpleRing: { light: "#e8ddfe", dark: "#45297d" },
|
|
225
|
+
// Select rebuilt to seed-design parity (crisp/#867), NOT Attio -- a
|
|
226
|
+
// deliberate one-component exception, the same way DatePicker already
|
|
227
|
+
// follows seed's calendar structure. Values below are resolved directly
|
|
228
|
+
// from `@seed-design/rootage`'s color.yaml (gray palette), not measured
|
|
229
|
+
// live, since there is no Attio reference for this component anymore.
|
|
230
|
+
// gray-00 / gray-200 (bg.layer-floating) -- the listbox surface. Distinct
|
|
231
|
+
// from `popoverBg` (crisp's Attio-measured popover, #fff/#17181a): the dark
|
|
232
|
+
// value differs (#1d2025), so this is its own token, not a reuse.
|
|
233
|
+
selectListboxBg: { light: "#ffffff", dark: "#1d2025" },
|
|
234
|
+
// gray-400 (stroke.neutral-weak) -- the trigger's real 1px border at rest.
|
|
235
|
+
selectTriggerStroke: { light: "#dcdee3", dark: "#393d46" },
|
|
236
|
+
// static-black/white-alpha (bg.transparent-pressed) -- the trigger/item
|
|
237
|
+
// press tint. Deliberately alpha, not a gray, so it reads the same over
|
|
238
|
+
// either surface colour.
|
|
239
|
+
selectPressedTint: { light: "#00000007", dark: "#ffffff0d" },
|
|
240
|
+
// gray-200 / gray-300 (bg.disabled) -- the trigger's disabled AND readonly
|
|
241
|
+
// fill. Distinct from `neutralWeak` (light matches, dark is #1d2025 not
|
|
242
|
+
// #2b2e35), so its own token rather than a reuse.
|
|
243
|
+
selectDisabledBg: { light: "#f3f4f5", dark: "#2b2e35" },
|
|
244
|
+
// red-700 (stroke.critical-solid) -- the trigger's invalid border. Distinct
|
|
245
|
+
// from crisp's own `bgDangerSolid`/`red700` (#ba2525, an Attio red): seed's
|
|
246
|
+
// red is a different hue entirely, and this component follows seed.
|
|
247
|
+
selectInvalidStroke: { light: "#fa342c", dark: "#ff6e60" },
|
|
248
|
+
// static-black/white-alpha (stroke.neutral-muted) -- the listbox's own
|
|
249
|
+
// divider slot (defined in select.yaml; not yet exposed on any consumer).
|
|
250
|
+
selectDivider: { light: "#00000010", dark: "#ffffff17" },
|
|
251
|
+
// gray-600 (fg.placeholder) -- distinct from `neutralSubtle` (its light/dark
|
|
252
|
+
// pair is the same two hexes, swapped), so its own token, not a reuse.
|
|
253
|
+
selectPlaceholder: { light: "#b0b3ba", dark: "#868b94" },
|
|
254
|
+
// s3 (shadow.yaml) -- the listbox's single-layer floating shadow, in place
|
|
255
|
+
// of crisp's own multi-layer `elevation.floating` for this component only.
|
|
256
|
+
selectShadow: { light: "#0000001f", dark: "#000000cc" },
|
|
162
257
|
};
|
package/dist/esm/tokens/scale.js
CHANGED
|
@@ -17,6 +17,7 @@ export const space = {
|
|
|
17
17
|
"2": "8px",
|
|
18
18
|
"2.5": "10px",
|
|
19
19
|
"3": "12px",
|
|
20
|
+
"3.5": "14px", // seed-design's $dimension.x3_5 — Select's medium trigger paddingX (crisp/#867)
|
|
20
21
|
"4": "16px",
|
|
21
22
|
"5": "20px",
|
|
22
23
|
"6": "24px",
|
|
@@ -25,12 +26,14 @@ export const space = {
|
|
|
25
26
|
// Corner radius. sm/md/lg are the three used on stacked surfaces
|
|
26
27
|
// (inputs, rows, tiles); full is the pill.
|
|
27
28
|
export const radius = {
|
|
29
|
+
xs: "4px", // seed-design's $radius.r1 — small badge / checkbox box corner
|
|
28
30
|
chip: "7px", // tag / chip corner — the single most used radius
|
|
29
31
|
sm: "6px", // chips, badges
|
|
30
32
|
md: "8px", // inputs, search box
|
|
31
33
|
lg: "9px", // rows, tiles, list items
|
|
32
34
|
xl: "12px", // overlay items (command rows)
|
|
33
35
|
"2xl": "16px", // overlay panels (command surface)
|
|
36
|
+
"3xl": "20px", // seed-design's $radius.r5 — Select's listbox corner (crisp/#867)
|
|
34
37
|
full: "999px", // pills
|
|
35
38
|
};
|
|
36
39
|
// Component sizing — recurring measured dimensions that are neither spacing nor
|
|
@@ -49,6 +52,12 @@ export const size = {
|
|
|
49
52
|
// whole cells and a column gap would cut it.
|
|
50
53
|
calendarCell: "34px", // calendar day pitch (32px day + 2px gap)
|
|
51
54
|
calendarPill: "32px", // the day highlight itself
|
|
55
|
+
// seed-design's Select, resolved from select-trigger.yaml/select-item.yaml
|
|
56
|
+
// (crisp/#867) — this component follows seed, not Attio, so these sit
|
|
57
|
+
// alongside the measured scale rather than folding into it.
|
|
58
|
+
selectTriggerMd: "40px", // $dimension.x10 — the medium (default) trigger height
|
|
59
|
+
selectTriggerLg: "52px", // $dimension.x13 — the large trigger height
|
|
60
|
+
selectIconLg: "20px", // $dimension.x5 — large trigger's chevron
|
|
52
61
|
};
|
|
53
62
|
// Typography — the measured UI runs entirely on Inter. Sizes measured in the sidebar
|
|
54
63
|
// collapse onto 11/12/13/14/16 (lg = 14px is the default body/label size);
|
|
@@ -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",
|
|
@@ -82,4 +106,22 @@ export const semantic = {
|
|
|
82
106
|
bgPurpleSoft: "purpleTint",
|
|
83
107
|
fgPurpleSoft: "softPurpleText",
|
|
84
108
|
ringPurpleSoft: "purpleRing",
|
|
109
|
+
/** Callout's bordered/banner look (crisp#869) — an opaque measured ring,
|
|
110
|
+
* not a `color-mix` tint of `bgBrandSolid` (see palette.ts's own comment). */
|
|
111
|
+
ringBrandSoft: "ringBrandSoft",
|
|
112
|
+
/** Select rebuilt to seed-design parity (crisp/#867) — see palette.ts's
|
|
113
|
+
* comment above `selectListboxBg` for why these are their own tokens
|
|
114
|
+
* rather than a reuse of an Attio-measured one. */
|
|
115
|
+
/** Seed's neutral ink (gray-1000) used as TEXT/icon colour — same palette
|
|
116
|
+
* entry as `strokeFieldFocus` (a ring), aliased separately because the
|
|
117
|
+
* role here is content colour, not a focus ring. */
|
|
118
|
+
fgSelectNeutral: "fieldFocus",
|
|
119
|
+
bgSelectListbox: "selectListboxBg",
|
|
120
|
+
strokeSelectTrigger: "selectTriggerStroke",
|
|
121
|
+
bgSelectPressed: "selectPressedTint",
|
|
122
|
+
bgSelectDisabled: "selectDisabledBg",
|
|
123
|
+
strokeSelectInvalid: "selectInvalidStroke",
|
|
124
|
+
strokeSelectDivider: "selectDivider",
|
|
125
|
+
fgSelectPlaceholder: "selectPlaceholder",
|
|
126
|
+
shadowSelect: "selectShadow",
|
|
85
127
|
};
|
package/dist/input.css
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
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); }
|
|
16
|
+
}
|
|
17
|
+
.crisp-input--size_sm {
|
|
18
|
+
height: var(--crisp-size-row); border-radius: var(--crisp-radius-md);
|
|
19
|
+
& .crisp-input-value { padding-inline: var(--crisp-space-2-5); }
|
|
9
20
|
}
|
|
10
21
|
.crisp-input--size_medium {
|
|
11
22
|
height: 32px; border-radius: var(--crisp-radius-md);
|
package/dist/input.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { RecipeFn } from "../core/types";
|
|
2
2
|
|
|
3
3
|
export interface InputVariants {
|
|
4
|
-
size?: "medium" | "large";
|
|
4
|
+
size?: "sm" | "medium" | "large";
|
|
5
5
|
invalid?: boolean;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export declare const inputRecipe: RecipeFn<{ size: { medium: string; large: string }; invalid: { true: string; false: string } }>;
|
|
8
|
+
export declare const inputRecipe: RecipeFn<{ size: { sm: string; medium: string; large: string }; invalid: { true: string; false: string } }>;
|