@crisp-ui-kit/crisp 0.54.4 → 0.54.6
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/authshell.css +4 -0
- package/dist/authshell.layered.css +4 -0
- package/dist/chip.css +1 -1
- package/dist/chip.layered.css +1 -1
- package/dist/cjs/components/chip/chip.recipe.js +12 -0
- package/dist/cjs/components/datatable/datatable.js +28 -5
- package/dist/cjs/components/datatable/datatable.recipe.js +6 -0
- package/dist/cjs/components/personchip/personchip.js +13 -3
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +75 -2
- package/dist/cjs/components/tooltip/tooltip.js +2 -2
- package/dist/cjs/components/tooltip/tooltip.recipe.js +37 -0
- package/dist/cjs/product/authshell/authshell.js +1 -1
- package/dist/cjs/product/authshell/authshell.recipe.js +18 -0
- package/dist/cjs/product/comments/comments.js +13 -21
- package/dist/cjs/product/comments/comments.recipe.js +29 -14
- package/dist/cjs/product/recordstable/recordstable.js +12 -3
- package/dist/cjs/product/richtext/richtext.js +31 -4
- package/dist/comments.css +21 -15
- package/dist/comments.layered.css +21 -15
- package/dist/datatable.css +8 -4
- package/dist/datatable.layered.css +8 -4
- package/dist/esm/components/chip/chip.recipe.js +12 -0
- package/dist/esm/components/datatable/datatable.js +28 -5
- package/dist/esm/components/datatable/datatable.recipe.js +6 -0
- package/dist/esm/components/personchip/personchip.js +13 -3
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +76 -3
- package/dist/esm/components/tooltip/tooltip.js +2 -2
- package/dist/esm/components/tooltip/tooltip.recipe.js +37 -0
- package/dist/esm/product/authshell/authshell.js +1 -1
- package/dist/esm/product/authshell/authshell.recipe.js +18 -0
- package/dist/esm/product/comments/comments.js +13 -21
- package/dist/esm/product/comments/comments.recipe.js +29 -14
- package/dist/esm/product/recordstable/recordstable.js +12 -3
- package/dist/esm/product/richtext/richtext.js +31 -4
- package/dist/styles.css +41 -20
- package/dist/styles.layered.css +41 -20
- package/dist/tooltip.css +7 -0
- package/dist/tooltip.d.ts +2 -2
- package/dist/tooltip.layered.css +7 -0
- package/dist/types/components/chip/chip.recipe.d.ts +1 -0
- package/dist/types/components/datatable/datatable.d.ts +50 -11
- package/dist/types/components/personchip/personchip.d.ts +14 -2
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +32 -1
- package/dist/types/components/tooltip/tooltip.d.ts +16 -0
- package/dist/types/components/tooltip/tooltip.recipe.d.ts +30 -2
- package/dist/types/product/authshell/authshell.d.ts +4 -1
- package/dist/types/product/comments/comments.d.ts +1 -2
- package/dist/types/product/recordstable/recordstable.d.ts +12 -0
- package/package.json +1 -1
|
@@ -32,7 +32,7 @@ function mergeRefs(...refs) {
|
|
|
32
32
|
* carry them for the same reason it can't carry `aria-describedby` (below):
|
|
33
33
|
* a screen reader reads state off the focused element, not an ancestor.
|
|
34
34
|
*/
|
|
35
|
-
export const Tooltip = React.forwardRef(({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, asChild = false, "aria-describedby": restDescribedBy,
|
|
35
|
+
export const Tooltip = React.forwardRef(({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, asChild = false, variant = "plain", "aria-describedby": restDescribedBy,
|
|
36
36
|
// Not forwarded, even though a composer's other rest props are
|
|
37
37
|
// (below): `style` sizes a VISUAL box, and Tooltip's wrapper has none
|
|
38
38
|
// of its own -- the box that matters is whatever the consumer's own
|
|
@@ -116,7 +116,7 @@ style: _style, ...rest }, forwardedRef) => {
|
|
|
116
116
|
};
|
|
117
117
|
const portal = open &&
|
|
118
118
|
pos != null &&
|
|
119
|
-
createPortal(_jsx("div", { id: tipId, role: "tooltip", className: tooltipRecipe(), style: {
|
|
119
|
+
createPortal(_jsx("div", { id: tipId, role: "tooltip", className: tooltipRecipe({ variant }), style: {
|
|
120
120
|
left: pos.left,
|
|
121
121
|
top: pos.top,
|
|
122
122
|
transform: pos.below
|
|
@@ -11,6 +11,16 @@ import { contract as c } from "../../tokens/index.js";
|
|
|
11
11
|
// (#101112, the same "dark pill" ink NavItem's own text uses) and
|
|
12
12
|
// line100's light hex (#eeeff1) — both theme-constant, same as before,
|
|
13
13
|
// just the wrong pair.
|
|
14
|
+
//
|
|
15
|
+
// crisp#563: this `boxShadow` is Attio's PLAIN tooltip (a truncated header
|
|
16
|
+
// label, measured on "Twitter follower count") — still an uncorroborated
|
|
17
|
+
// single sample, flagged for its own re-measurement pass, not touched here.
|
|
18
|
+
// Attio has a SECOND, visually similar but structurally different tooltip
|
|
19
|
+
// for a reference-column header (icon + the referenced type's name after
|
|
20
|
+
// the label, crisp#580) with its own measured shadow — an inset ring plus
|
|
21
|
+
// two offset shadows, `variant: "reference"` below. Background, text
|
|
22
|
+
// colour, radius, padding and font are identical between the two measured
|
|
23
|
+
// samples; only the shadow differs.
|
|
14
24
|
const spec = defineSpec({
|
|
15
25
|
name: "tooltip",
|
|
16
26
|
base: {
|
|
@@ -35,7 +45,34 @@ const spec = defineSpec({
|
|
|
35
45
|
// An at-rule whose body is declarations rather than rules, so a part
|
|
36
46
|
// expresses it directly: the entry state the overlay animates from.
|
|
37
47
|
{ selector: "@starting-style", decls: { opacity: 0 } },
|
|
48
|
+
// #580: the icon + referenced-type-name row a `variant="reference"`
|
|
49
|
+
// consumer (DataTable/RecordsTable's `ThLabel`) composes into `content`.
|
|
50
|
+
// Lives here, not in the caller's inline style, so it reads its gap from
|
|
51
|
+
// the token contract like every other measured value on this surface —
|
|
52
|
+
// `content` is portaled straight into this bubble regardless of which
|
|
53
|
+
// component built the JSX, so a nested selector reaches it the same way
|
|
54
|
+
// it reaches anything else rendered inside `.crisp-tooltip`.
|
|
55
|
+
{
|
|
56
|
+
selector: "& .crisp-tooltip-reference-row",
|
|
57
|
+
decls: {
|
|
58
|
+
display: "inline-flex",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
gap: c.space["1"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
38
63
|
],
|
|
64
|
+
variants: {
|
|
65
|
+
variant: {
|
|
66
|
+
// Inherits `base`'s boxShadow untouched -- crisp#563 updates that
|
|
67
|
+
// single value when it re-measures the plain tooltip, and this
|
|
68
|
+
// variant picks the correction up for free.
|
|
69
|
+
plain: {},
|
|
70
|
+
reference: {
|
|
71
|
+
boxShadow: "rgba(0,0,0,0) 0 0 0 1px inset, rgba(28,40,64,0.18) 0 0 2px 0, rgba(0,0,0,0.04) 0 1px 3px 0",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
defaultVariants: { variant: "plain" },
|
|
39
76
|
});
|
|
40
77
|
const config = specToRecipe(spec);
|
|
41
78
|
export const tooltipRecipe = resolveRecipe(config);
|
|
@@ -13,6 +13,6 @@ export function AuthShell({ progress, logo, title, children, preview, footer, cl
|
|
|
13
13
|
const pct = hasProgress
|
|
14
14
|
? Math.round(Math.max(0, Math.min(1, progress)) * 100)
|
|
15
15
|
: 0;
|
|
16
|
-
return (_jsxs("div", { className: authshellRecipe({ className }), ...rest, children: [hasProgress && (_jsx("div", { className: "crisp-authshell-progress", role: "progressbar", "aria-valuenow": pct, "aria-valuemin": 0, "aria-valuemax": 100, children: _jsx("div", { className: "crisp-authshell-progress-fill", style: { width: `${pct}%` } }) })), logo != null && _jsx("div", { className: "crisp-authshell-logo", children: logo }), _jsxs("div", { className: "crisp-authshell-card", children: [_jsx("div", { className: "crisp-authshell-pane", children: _jsxs("div", { className: "crisp-authshell-body", children: [title != null && (_jsx("div", { className: "crisp-authshell-title", children: title })), _jsx("div", { className: "crisp-authshell-form", children: children })] }) }), preview != null && (_jsx("div", { className: "crisp-authshell-preview", children: preview }))] }), footer != null && _jsx("div", { className: "crisp-authshell-footer", children: footer })] }));
|
|
16
|
+
return (_jsxs("div", { className: authshellRecipe({ className }), "data-single-pane": preview == null ? "" : undefined, ...rest, children: [hasProgress && (_jsx("div", { className: "crisp-authshell-progress", role: "progressbar", "aria-valuenow": pct, "aria-valuemin": 0, "aria-valuemax": 100, children: _jsx("div", { className: "crisp-authshell-progress-fill", style: { width: `${pct}%` } }) })), logo != null && _jsx("div", { className: "crisp-authshell-logo", children: logo }), _jsxs("div", { className: "crisp-authshell-card", children: [_jsx("div", { className: "crisp-authshell-pane", children: _jsxs("div", { className: "crisp-authshell-body", children: [title != null && (_jsx("div", { className: "crisp-authshell-title", children: title })), _jsx("div", { className: "crisp-authshell-form", children: children })] }) }), preview != null && (_jsx("div", { className: "crisp-authshell-preview", children: preview }))] }), footer != null && _jsx("div", { className: "crisp-authshell-footer", children: footer })] }));
|
|
17
17
|
}
|
|
18
18
|
AuthShell.displayName = "AuthShell";
|
|
@@ -7,25 +7,43 @@ import { contract as c } from "../../tokens/index.js";
|
|
|
7
7
|
// from the live app, EXCEPT the 1200px preview-hiding breakpoint (called out
|
|
8
8
|
// at the container query below) — the pane drops out in JS somewhere between a
|
|
9
9
|
// measured 954px and 1393px viewport width, so 1200px is chosen, not measured.
|
|
10
|
+
//
|
|
11
|
+
// crisp#653: that two-pane card's own 692px height, border and 420px pane
|
|
12
|
+
// width were measured for a SPECIFIC screen with a real product-preview
|
|
13
|
+
// pane -- omitting `preview` (`data-single-pane`, set by the component) used
|
|
14
|
+
// to keep all three anyway, leaving a bordered box with a huge empty area
|
|
15
|
+
// under short content. Attio's own single-pane screen (`app.attio.com/login`,
|
|
16
|
+
// reachable in a fresh logged-out browser context -- no session needed) has
|
|
17
|
+
// NO card at all: every ancestor of its "Sign in" heading measured
|
|
18
|
+
// `border: 0px none`, `box-shadow: none`, `background: transparent`, a
|
|
19
|
+
// 360-364px content column (not 420px), sized to its own content and
|
|
20
|
+
// roughly centred in the viewport rather than pinned near the top.
|
|
21
|
+
// `data-single-pane` now drops the card chrome and narrows the body to
|
|
22
|
+
// match; the two-pane card is unchanged.
|
|
10
23
|
const config = defineRecipe({
|
|
11
24
|
name: "authshell",
|
|
12
25
|
base: `min-height: 100%; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; position: relative;
|
|
13
26
|
background: ${c.bgSurface}; font-family: ${c.font.sans}; font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgPrimary};
|
|
14
27
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
15
28
|
|
|
29
|
+
&[data-single-pane] { justify-content: center; }
|
|
30
|
+
|
|
16
31
|
& .crisp-authshell-progress { position: absolute; top: 0; left: 0; width: 100%; height: 2px; background: ${c.borderDefault}; }
|
|
17
32
|
& .crisp-authshell-progress-fill { height: 2px; background: ${c.fgPrimary}; border-radius: 4px; transition: width .2s; }
|
|
18
33
|
|
|
19
34
|
& .crisp-authshell-logo { margin-top: ${c.space["8"]}; height: 24px; display: flex; align-items: center; }
|
|
20
35
|
|
|
21
36
|
& .crisp-authshell-card { margin-top: ${c.space["6"]}; display: flex; height: 692px; border-radius: 20px; border: 1px solid ${c.borderSubtle}; overflow: hidden; }
|
|
37
|
+
&[data-single-pane] .crisp-authshell-card { height: auto; border: none; border-radius: 0; overflow: visible; }
|
|
22
38
|
|
|
23
39
|
& .crisp-authshell-pane { flex: 0 0 564px; padding: 72px; display: flex; flex-direction: column; box-sizing: border-box; }
|
|
40
|
+
&[data-single-pane] .crisp-authshell-pane { flex: 0 0 auto; padding: 0; }
|
|
24
41
|
|
|
25
42
|
& .crisp-authshell-preview { flex: 0 0 564px; padding: 96px 64px; background: ${c.bgSubtle}; border-inline-start: 1px solid color-mix(in srgb, ${c.fgPrimary} 5%, transparent); position: relative; overflow: hidden; box-sizing: border-box; }
|
|
26
43
|
& .crisp-authshell-preview::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 120px; background: linear-gradient(color-mix(in srgb, ${c.bgSubtle} 0%, transparent) 0%, ${c.bgSubtle} 100%); pointer-events: none; }
|
|
27
44
|
|
|
28
45
|
& .crisp-authshell-body { width: 420px; margin: 0 auto; display: flex; flex-direction: column; gap: ${c.space["8"]}; align-items: center; }
|
|
46
|
+
&[data-single-pane] .crisp-authshell-body { width: 360px; }
|
|
29
47
|
|
|
30
48
|
& .crisp-authshell-title { font-size: 20px; line-height: 24px; font-weight: ${c.weight.semibold}; letter-spacing: -0.4px; color: ${c.fgPrimary}; align-self: stretch; }
|
|
31
49
|
|
|
@@ -8,13 +8,6 @@ import { Menu, MenuCheckboxItem, MenuItem } from "../../components/menu/index.js
|
|
|
8
8
|
import { Popover } from "../../components/popover/index.js";
|
|
9
9
|
import { matchMentionTrigger, useCrispMessage, } from "../../core/index.js";
|
|
10
10
|
import { commentsRecipe } from "./comments.recipe.js";
|
|
11
|
-
/** A single literal JSX occurrence for the reply glyph, used at two sizes --
|
|
12
|
-
* so the RTL-mirroring sweep (which counts the flip-marker attribute against
|
|
13
|
-
* source-text occurrences, not runtime renders) sees one mark for one
|
|
14
|
-
* direction-bearing icon, not two. */
|
|
15
|
-
function ReplyGlyph({ size }) {
|
|
16
|
-
return (_jsx(CornerUpLeft, { "data-rtl-flip": "", size: size, strokeWidth: 2, "aria-hidden": "true" }));
|
|
17
|
-
}
|
|
18
11
|
const DEFAULT_EMOJI = [
|
|
19
12
|
"👍",
|
|
20
13
|
"👎",
|
|
@@ -121,7 +114,7 @@ export function serializeComposerField(field) {
|
|
|
121
114
|
* disabled until the field holds text. Uncontrolled — reads its own DOM on
|
|
122
115
|
* submit, the same shape `RichTextEditor` uses for its body.
|
|
123
116
|
*/
|
|
124
|
-
const Composer = React.forwardRef(function Composer({ placeholder, postLabel, mentionTrigger, renderMentionPicker,
|
|
117
|
+
const Composer = React.forwardRef(function Composer({ placeholder, postLabel, mentionTrigger, renderMentionPicker, mentionToolLabel, onSubmit, }, ref) {
|
|
125
118
|
const fieldRef = React.useRef(null);
|
|
126
119
|
const [empty, setEmpty] = React.useState(true);
|
|
127
120
|
const [mention, setMention] = React.useState(null);
|
|
@@ -257,15 +250,15 @@ const Composer = React.forwardRef(function Composer({ placeholder, postLabel, me
|
|
|
257
250
|
e.preventDefault();
|
|
258
251
|
submit();
|
|
259
252
|
}
|
|
260
|
-
} }), _jsxs("div", { className: "crisp-comments-composer-foot", children: [
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
253
|
+
} }), _jsxs("div", { className: "crisp-comments-composer-foot", children: [_jsx("div", { className: "crisp-comments-composer-tools", children: _jsx("button", { type: "button", className: "crisp-comments-tool", "aria-label": mentionToolLabel, onClick: () => {
|
|
254
|
+
const field = fieldRef.current;
|
|
255
|
+
if (!field)
|
|
256
|
+
return;
|
|
257
|
+
field.focus();
|
|
258
|
+
document.execCommand("insertText", false, mentionTrigger);
|
|
259
|
+
syncEmpty();
|
|
260
|
+
updateMention();
|
|
261
|
+
}, children: "@" }) }), _jsx("button", { type: "button", className: "crisp-comments-post", disabled: empty, onClick: submit, children: postLabel })] }), mention &&
|
|
269
262
|
renderMentionPicker?.({
|
|
270
263
|
query: mention.query,
|
|
271
264
|
anchor: mention.anchor,
|
|
@@ -274,7 +267,7 @@ const Composer = React.forwardRef(function Composer({ placeholder, postLabel, me
|
|
|
274
267
|
})] }));
|
|
275
268
|
});
|
|
276
269
|
function CommentRow({ comment, replyLabel, reactLabel, resolveLabel, moreLabel, copyLinkLabel, deleteLabel, addReactionLabel, reactionSearchLabel, emojiOptions, resolvable, resolved, resolvedLabel, onReply, onReact, onResolve, onCopyLink, onDeleteRequest, }) {
|
|
277
|
-
return (_jsxs("div", { className: "crisp-comments-row", children: [_jsx(AuthorAvatar, { author: comment.author }), _jsxs("div", { className: "crisp-comments-row-main", children: [resolvedLabel != null && (_jsxs("div", { className: "crisp-comments-resolved-label", children: [_jsx(Check, { size: 12, strokeWidth: 2.5, "aria-hidden": "true" }), resolvedLabel] })), _jsxs("div", { className: "crisp-comments-row-head", children: [_jsx("span", { className: "crisp-comments-author", children: comment.author.name }), _jsx("span", { className: "crisp-comments-time", children: comment.time })] }), _jsx("div", { className: "crisp-comments-body-text", children: comment.body }), comment.reactions && comment.reactions.length > 0 && (_jsxs("div", { className: "crisp-comments-reactions", children: [comment.reactions.map((r) => (_jsxs("button", { type: "button", className: "crisp-comments-reaction", "data-reacted": r.reactedByMe ? "" : undefined, onClick: () => onReact(r.emoji), children: [_jsx("span", { "aria-hidden": "true", children: r.emoji }), r.count] }, r.emoji))), _jsx(Popover, { align: "start", trigger: _jsx("button", { type: "button", className: "crisp-comments-reaction-add", "aria-label": addReactionLabel, children: _jsx(SmilePlus, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: _jsx(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: onReact }) })] }))] }), _jsxs("div", { className: "crisp-comments-row-actions", children: [onReply && (_jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": replyLabel, onClick: onReply, children: _jsx(
|
|
270
|
+
return (_jsxs("div", { className: "crisp-comments-row", children: [_jsx(AuthorAvatar, { author: comment.author }), _jsxs("div", { className: "crisp-comments-row-main", children: [resolvedLabel != null && (_jsxs("div", { className: "crisp-comments-resolved-label", children: [_jsx(Check, { size: 12, strokeWidth: 2.5, "aria-hidden": "true" }), resolvedLabel] })), _jsxs("div", { className: "crisp-comments-row-head", children: [_jsx("span", { className: "crisp-comments-author", children: comment.author.name }), _jsx("span", { className: "crisp-comments-time", children: comment.time })] }), _jsx("div", { className: "crisp-comments-body-text", children: comment.body }), comment.reactions && comment.reactions.length > 0 && (_jsxs("div", { className: "crisp-comments-reactions", children: [comment.reactions.map((r) => (_jsxs("button", { type: "button", className: "crisp-comments-reaction", "data-reacted": r.reactedByMe ? "" : undefined, onClick: () => onReact(r.emoji), children: [_jsx("span", { "aria-hidden": "true", children: r.emoji }), r.count] }, r.emoji))), _jsx(Popover, { align: "start", trigger: _jsx("button", { type: "button", className: "crisp-comments-reaction-add", "aria-label": addReactionLabel, children: _jsx(SmilePlus, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: _jsx(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: onReact }) })] }))] }), _jsxs("div", { className: "crisp-comments-row-actions", children: [onReply && (_jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": replyLabel, onClick: onReply, children: _jsx(CornerUpLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 2, "aria-hidden": "true" }) })), _jsx(Popover, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": reactLabel, children: _jsx(SmilePlus, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: _jsx(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: onReact }) }), resolvable && (_jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": resolveLabel, "aria-pressed": !!resolved, "data-active": resolved ? "" : undefined, onClick: onResolve, children: _jsx(Check, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })), _jsxs(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-comments-action", "aria-label": moreLabel, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [_jsx(MenuItem, { onSelect: onCopyLink, children: copyLinkLabel }), _jsx(MenuItem, { danger: true, onSelect: onDeleteRequest, children: deleteLabel })] })] })] }));
|
|
278
271
|
}
|
|
279
272
|
/**
|
|
280
273
|
* Per-record comment threads (crisp#625) — measured live on app.attio.com's
|
|
@@ -285,7 +278,7 @@ function CommentRow({ comment, replyLabel, reactLabel, resolveLabel, moreLabel,
|
|
|
285
278
|
* separate row affordance (P-051) — deliberately NOT part of this
|
|
286
279
|
* component, since it lives in a table cell this component doesn't own.
|
|
287
280
|
*/
|
|
288
|
-
export function CommentsPanel({ threads, currentUserId, subject, subjectIcon, showResolved = false, onShowResolvedChange, onlyMine = false, onOnlyMineChange, activeThreadId: activeThreadIdProp, onActiveThreadChange, onPost, onReact, onResolve, onDelete, onCopyLink, emojiOptions = DEFAULT_EMOJI, mentionTrigger = "@", renderMentionPicker, onClose, closeLabel: closeLabelProp, backLabel: backLabelProp, title: titleProp, threadTitle: threadTitleProp, breadcrumbPrefix: breadcrumbPrefixProp, composerPlaceholder: composerPlaceholderProp, replyPlaceholder: replyPlaceholderProp, postLabel: postLabelProp, replyPostLabel: replyPostLabelProp, emptyLabel, filterLabel: filterLabelProp, showResolvedLabel: showResolvedLabelProp, onlyMineLabel: onlyMineLabelProp, replyActionLabel: replyActionLabelProp, reactActionLabel: reactActionLabelProp, resolveActionLabel: resolveActionLabelProp, resolvedByYouLabel: resolvedByYouLabelProp, resolvedByLabel, moreActionsLabel: moreActionsLabelProp, copyLinkLabel: copyLinkLabelProp, deleteLabel: deleteLabelProp, addReactionLabel: addReactionLabelProp, reactionSearchLabel: reactionSearchLabelProp, deleteConfirmTitle: deleteConfirmTitleProp, deleteConfirmBody: deleteConfirmBodyProp, deleteConfirmActionLabel: deleteConfirmActionLabelProp, cancelLabel: cancelLabelProp,
|
|
281
|
+
export function CommentsPanel({ threads, currentUserId, subject, subjectIcon, showResolved = false, onShowResolvedChange, onlyMine = false, onOnlyMineChange, activeThreadId: activeThreadIdProp, onActiveThreadChange, onPost, onReact, onResolve, onDelete, onCopyLink, emojiOptions = DEFAULT_EMOJI, mentionTrigger = "@", renderMentionPicker, onClose, closeLabel: closeLabelProp, backLabel: backLabelProp, title: titleProp, threadTitle: threadTitleProp, breadcrumbPrefix: breadcrumbPrefixProp, composerPlaceholder: composerPlaceholderProp, replyPlaceholder: replyPlaceholderProp, postLabel: postLabelProp, replyPostLabel: replyPostLabelProp, emptyLabel, filterLabel: filterLabelProp, showResolvedLabel: showResolvedLabelProp, onlyMineLabel: onlyMineLabelProp, replyActionLabel: replyActionLabelProp, reactActionLabel: reactActionLabelProp, resolveActionLabel: resolveActionLabelProp, resolvedByYouLabel: resolvedByYouLabelProp, resolvedByLabel, moreActionsLabel: moreActionsLabelProp, copyLinkLabel: copyLinkLabelProp, deleteLabel: deleteLabelProp, addReactionLabel: addReactionLabelProp, reactionSearchLabel: reactionSearchLabelProp, deleteConfirmTitle: deleteConfirmTitleProp, deleteConfirmBody: deleteConfirmBodyProp, deleteConfirmActionLabel: deleteConfirmActionLabelProp, cancelLabel: cancelLabelProp, composerMentionToolLabel: composerMentionToolLabelProp, className, ...rest }) {
|
|
289
282
|
const closeLabel = useCrispMessage(closeLabelProp, "Close");
|
|
290
283
|
const backLabel = useCrispMessage(backLabelProp, "Back");
|
|
291
284
|
const title = useCrispMessage(titleProp, "All comments");
|
|
@@ -311,7 +304,6 @@ export function CommentsPanel({ threads, currentUserId, subject, subjectIcon, sh
|
|
|
311
304
|
const deleteConfirmBody = useCrispMessage(deleteConfirmBodyProp, "You're about to delete this comment. Once deleted it cannot be restored.");
|
|
312
305
|
const deleteConfirmActionLabel = useCrispMessage(deleteConfirmActionLabelProp, "Delete");
|
|
313
306
|
const cancelLabel = useCrispMessage(cancelLabelProp, "Cancel");
|
|
314
|
-
const composerReplyToolLabel = useCrispMessage(composerReplyToolLabelProp, "Reply");
|
|
315
307
|
const composerMentionToolLabel = useCrispMessage(composerMentionToolLabelProp, "Mention someone");
|
|
316
308
|
const [internalActiveId, setInternalActiveId] = React.useState(null);
|
|
317
309
|
const activeThreadId = activeThreadIdProp ?? internalActiveId;
|
|
@@ -356,7 +348,7 @@ export function CommentsPanel({ threads, currentUserId, subject, subjectIcon, sh
|
|
|
356
348
|
? resolvedByYouLabel
|
|
357
349
|
: (resolvedByLabel?.(t.resolvedBy.name) ??
|
|
358
350
|
`Resolved by ${t.resolvedBy.name}`)
|
|
359
|
-
: undefined, onReply: () => setActiveThreadId(t.id), onReact: (emoji) => onReact(t.id, t.root.id, emoji), onResolve: onResolve ? () => onResolve(t.id) : undefined, onCopyLink: () => copyLink(t.id, t.root.id), onDeleteRequest: () => setDeleteTarget({ threadId: t.id, commentId: t.root.id }) }, t.id)))) }), _jsx(Composer, { placeholder: activeThread ? replyPlaceholder : composerPlaceholder, postLabel: activeThread ? replyPostLabel : postLabel, mentionTrigger: mentionTrigger, renderMentionPicker: renderMentionPicker,
|
|
351
|
+
: undefined, onReply: () => setActiveThreadId(t.id), onReact: (emoji) => onReact(t.id, t.root.id, emoji), onResolve: onResolve ? () => onResolve(t.id) : undefined, onCopyLink: () => copyLink(t.id, t.root.id), onDeleteRequest: () => setDeleteTarget({ threadId: t.id, commentId: t.root.id }) }, t.id)))) }), _jsx(Composer, { placeholder: activeThread ? replyPlaceholder : composerPlaceholder, postLabel: activeThread ? replyPostLabel : postLabel, mentionTrigger: mentionTrigger, renderMentionPicker: renderMentionPicker, mentionToolLabel: composerMentionToolLabel, onSubmit: (text) => onPost(text, activeThread ? activeThread.id : null) }, activeThread ? activeThread.id : "root"), _jsxs(Dialog, { open: deleteTarget != null, onOpenChange: (open) => {
|
|
360
352
|
if (!open)
|
|
361
353
|
setDeleteTarget(null);
|
|
362
354
|
}, title: deleteConfirmTitle, children: [_jsx(DialogBody, { children: deleteConfirmBody }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", intent: "neutral", size: "md", onClick: () => setDeleteTarget(null), children: cancelLabel }), _jsx(Button, { type: "button", intent: "danger", size: "md", onClick: () => {
|
|
@@ -95,29 +95,44 @@ const config = defineRecipe({
|
|
|
95
95
|
RichTextEditor's own \`.crisp-rte-mention\` (crisp#428), reused here
|
|
96
96
|
rather than re-derived since it's the same atomic-chip affordance. */
|
|
97
97
|
& .crisp-comments-mention { display: inline; padding: 0 2px; border-radius: 4px; background: color-mix(in srgb, ${c.bgBrandSolid} 12%, transparent); color: ${c.bgBrandSolid}; font-weight: ${c.weight.medium}; user-select: none; }
|
|
98
|
-
|
|
98
|
+
${rtlFlipCss}`,
|
|
99
|
+
variants: {},
|
|
100
|
+
defaultVariants: {},
|
|
101
|
+
compoundVariants: [],
|
|
102
|
+
// Two pieces below intentionally live OUTSIDE the nested \`base\` above,
|
|
103
|
+
// the same reason Menu's own \`.crisp-menu-content\` does (menu.recipe.ts's
|
|
104
|
+
// own comment, crisp#357): nesting compiles to a descendant combinator
|
|
105
|
+
// scoped to \`.crisp-comments\`, which needs real DOM containment. Both of
|
|
106
|
+
// these escape it -- \`ReactionPicker\` because \`Popover\` portals its
|
|
107
|
+
// content to \`document.body\`, and \`CommentCountBadge\` because it's built
|
|
108
|
+
// to be dropped into a table cell that has no \`.crisp-comments\` ancestor
|
|
109
|
+
// at all (its own module doc comment). Nested, this shipped invisible:
|
|
110
|
+
// \`CommentCountBadge\`'s dot rendered at 0×0 and the reaction picker fell
|
|
111
|
+
// back to \`Popover\`'s own generic (unstyled-by-this-recipe) surface --
|
|
112
|
+
// caught taking this component's own screenshots, not by any gate, since
|
|
113
|
+
// \`spec-check\` reads recipe source text and both classes were right there
|
|
114
|
+
// in it.
|
|
115
|
+
globalCss: `
|
|
99
116
|
/* Reaction picker -- see \`ReactionPicker\`'s own doc comment: the exact
|
|
100
117
|
chrome wasn't independently pixel-measured this pass (screenshot
|
|
101
118
|
capture broke mid-session), so this reuses Menu's already-measured
|
|
102
119
|
floating-popover shell rather than a new value. */
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
.crisp-comments-reactionpicker { font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; box-sizing: border-box; width: 240px; padding: ${c.space["2"]}; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; box-shadow: ${e.floating}; }
|
|
121
|
+
.crisp-comments-reactionpicker-search { box-sizing: border-box; width: 100%; height: 28px; margin-bottom: ${c.space["2"]}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgRaised}; font-family: inherit; font-size: ${c.text.md}; color: ${c.fgPrimary}; }
|
|
122
|
+
.crisp-comments-reactionpicker-search::placeholder { color: color-mix(in srgb, ${c.fgPrimary} 40%, transparent); }
|
|
123
|
+
.crisp-comments-reactionpicker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: ${c.space["0.5"]}; max-height: 160px; overflow-y: auto; }
|
|
124
|
+
.crisp-comments-reactionpicker-item { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; border-radius: ${c.radius.sm}; background: none; font-family: inherit; font-size: 16px; line-height: 1; cursor: pointer; }
|
|
125
|
+
.crisp-comments-reactionpicker-item:hover { background: ${c.bgRaised}; }
|
|
109
126
|
|
|
110
127
|
/* Row badge (CommentCountBadge) -- lives in its own class family so the
|
|
111
128
|
grid lanes can drop it into a cell without pulling in the panel's
|
|
112
129
|
CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
|
|
113
130
|
icon + 12px count text, both at 55% ink, replacing the dot. */
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
defaultVariants: {},
|
|
120
|
-
compoundVariants: [],
|
|
131
|
+
.crisp-comments-badge { box-sizing: border-box; font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; display: inline-flex; align-items: center; gap: ${c.space["1"]}; border: none; background: none; padding: 0; margin: 0; cursor: pointer; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); }
|
|
132
|
+
.crisp-comments-badge-dot { box-sizing: border-box; width: 6px; height: 6px; border-radius: ${c.radius.full}; background: color-mix(in srgb, ${c.fgPrimary} 38%, transparent); }
|
|
133
|
+
.crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
|
|
134
|
+
[dir="rtl"] .crisp-comments-badge [data-rtl-flip] { transform: scaleX(-1); }
|
|
135
|
+
`,
|
|
121
136
|
});
|
|
122
137
|
export const commentsRecipe = resolveRecipe(config);
|
|
123
138
|
export default config;
|
|
@@ -37,8 +37,15 @@ export function isLabelTruncated(el) {
|
|
|
37
37
|
* viewport change, label change) via `ResizeObserver`; SSR-safe the same way
|
|
38
38
|
* `MiddleTruncate` is -- the DOM is only touched inside an effect, so the
|
|
39
39
|
* server (and the first client paint) renders the plain span.
|
|
40
|
+
*
|
|
41
|
+
* #580: a `referencedType` column (Attio's own `type: "reference"`) appends
|
|
42
|
+
* that type's icon + name after the label and switches to `Tooltip`'s
|
|
43
|
+
* `"reference"` chrome variant (Attio's second, structurally distinct
|
|
44
|
+
* tooltip — same background/colour/radius/padding/font as the plain one, a
|
|
45
|
+
* different `box-shadow`, crisp#563). Every other column stays label-only,
|
|
46
|
+
* the `"plain"` variant #534/#558 shipped.
|
|
40
47
|
*/
|
|
41
|
-
function ThLabel({ label }) {
|
|
48
|
+
function ThLabel({ label, referencedType, }) {
|
|
42
49
|
const ref = React.useRef(null);
|
|
43
50
|
const [overflowing, setOverflowing] = React.useState(false);
|
|
44
51
|
// biome-ignore lint/correctness/useExhaustiveDependencies: `label` isn't read in the body, but a rename (col.label changing with the column's own width unchanged) needs a re-measure a ResizeObserver on `el` won't fire for -- same reasoning as Tabs' own re-measure effect.
|
|
@@ -67,7 +74,9 @@ function ThLabel({ label }) {
|
|
|
67
74
|
// actually show. `open={false}` when it fits keeps the Tooltip mounted
|
|
68
75
|
// (asChild costs no extra DOM either way) but pins it permanently closed,
|
|
69
76
|
// so hovering a non-overflowing header still opens nothing.
|
|
70
|
-
|
|
77
|
+
const ReferenceIcon = referencedType?.icon;
|
|
78
|
+
const content = referencedType ? (_jsxs("span", { className: "crisp-tooltip-reference-row", children: [label, ReferenceIcon && _jsx(ReferenceIcon, { size: 16, strokeWidth: 1.75 }), referencedType.label] })) : (label);
|
|
79
|
+
return (_jsx(Tooltip, { content: content, asChild: true, variant: referencedType ? "reference" : "plain", open: overflowing ? undefined : false, children: _jsx("span", { ref: ref, className: "crisp-recordstable-th-label", children: label }) }));
|
|
71
80
|
}
|
|
72
81
|
const EDITABLE = new Set(["text", "number", "url", "currency"]);
|
|
73
82
|
// types whose cell is an always-interactive picker (not double-click-to-edit)
|
|
@@ -1173,7 +1182,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
|
|
|
1173
1182
|
setDragCol(col.key);
|
|
1174
1183
|
e.dataTransfer.effectAllowed = "move";
|
|
1175
1184
|
}
|
|
1176
|
-
: undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? _jsx(Icon, { size: 16, strokeWidth: 1.75 }) : null, _jsx(ThLabel, { label: col.label }), _jsx("span", { className: "crisp-recordstable-sort", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
|
|
1185
|
+
: undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? _jsx(Icon, { size: 16, strokeWidth: 1.75 }) : null, _jsx(ThLabel, { label: col.label, referencedType: col.referencedType }), _jsx("span", { className: "crisp-recordstable-sort", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
|
|
1177
1186
|
rootRef.current &&
|
|
1178
1187
|
createPortal(_jsxs("div", { ref: menuRef, className: "crisp-recordstable-menu crisp-recordstable-menu--fixed crisp-recordstable-menu--hdr", role: "menu", "aria-label": `${col.label} column`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: [_jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "asc"), children: [_jsx(ArrowUp, { size: 15, strokeWidth: 1.75 }), t.sortAscending] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "desc"), children: [_jsx(ArrowDown, { size: 15, strokeWidth: 1.75 }), t.sortDescending] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [_jsx(ArrowLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveLeft] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i === 0 || i >= cols.length - 1, onClick: () => moveColumn(col.key, 1), children: [_jsx(ArrowRight, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveRight] }), _jsx("div", { className: "crisp-recordstable-menu-sep" }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => {
|
|
1179
1188
|
setRenamingCol(col.key);
|
|
@@ -123,6 +123,17 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
|
|
|
123
123
|
// The Range covering a live "/query" (trigger included), mirroring the
|
|
124
124
|
// mention primitive below — only set in "slash" mode.
|
|
125
125
|
const slashRangeRef = React.useRef(null);
|
|
126
|
+
// #672: the selection the user actually had when they opened the toolbar's
|
|
127
|
+
// "Body ▾" menu — only set in "toolbar" mode. That menu's own search
|
|
128
|
+
// input steals DOM focus from the body the moment it opens, so by the
|
|
129
|
+
// time an item is picked, `window.getSelection()` no longer points at
|
|
130
|
+
// the block the user meant; without this, `run()`'s `body.focus()`
|
|
131
|
+
// resets the caret to the start of the document (the FIRST block)
|
|
132
|
+
// instead, and every conversion landed there regardless of what was
|
|
133
|
+
// selected. Cloned, since the live Range would otherwise track DOM
|
|
134
|
+
// mutations (and the selection moving to the search input) between now
|
|
135
|
+
// and when it's restored.
|
|
136
|
+
const toolbarSelectionRef = React.useRef(null);
|
|
126
137
|
const blockSearchRef = React.useRef(null);
|
|
127
138
|
const [mention, setMention] = React.useState(null);
|
|
128
139
|
// The Range covering the live "@query" text (trigger included) — kept
|
|
@@ -228,6 +239,7 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
|
|
|
228
239
|
}, [readOnly]);
|
|
229
240
|
const closeBlockMenu = React.useCallback(() => {
|
|
230
241
|
slashRangeRef.current = null;
|
|
242
|
+
toolbarSelectionRef.current = null;
|
|
231
243
|
setBlockMenu(null);
|
|
232
244
|
}, []);
|
|
233
245
|
/** Mirrors the mention-trigger detection below: only handles the caret
|
|
@@ -406,6 +418,14 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
|
|
|
406
418
|
sel?.removeAllRanges();
|
|
407
419
|
sel?.addRange(range);
|
|
408
420
|
}
|
|
421
|
+
else if (blockMenu?.mode === "toolbar" && toolbarSelectionRef.current) {
|
|
422
|
+
// #672: no trigger text to delete here (nothing was typed to open
|
|
423
|
+
// this menu) -- just put the selection back where the user actually
|
|
424
|
+
// had it, before `applyBlockItem` -> `run()` refocuses the body.
|
|
425
|
+
const sel = window.getSelection();
|
|
426
|
+
sel?.removeAllRanges();
|
|
427
|
+
sel?.addRange(toolbarSelectionRef.current);
|
|
428
|
+
}
|
|
409
429
|
applyBlockItem(item.id);
|
|
410
430
|
closeBlockMenu();
|
|
411
431
|
};
|
|
@@ -428,14 +448,21 @@ export const RichTextEditor = React.forwardRef(function RichTextEditor({ default
|
|
|
428
448
|
const toggleBlockMenuFromToolbar = () => {
|
|
429
449
|
if (!bar)
|
|
430
450
|
return;
|
|
431
|
-
setBlockMenu((cur) =>
|
|
432
|
-
|
|
433
|
-
|
|
451
|
+
setBlockMenu((cur) => {
|
|
452
|
+
if (cur?.mode === "toolbar")
|
|
453
|
+
return null;
|
|
454
|
+
// #672: capture the live selection NOW, before the menu's own search
|
|
455
|
+
// input (focused by the effect below) steals it away.
|
|
456
|
+
const sel = window.getSelection();
|
|
457
|
+
toolbarSelectionRef.current =
|
|
458
|
+
sel && sel.rangeCount > 0 ? sel.getRangeAt(0).cloneRange() : null;
|
|
459
|
+
return {
|
|
434
460
|
anchor: { x: bar.x, y: bar.y },
|
|
435
461
|
query: "",
|
|
436
462
|
highlighted: 0,
|
|
437
463
|
mode: "toolbar",
|
|
438
|
-
}
|
|
464
|
+
};
|
|
465
|
+
});
|
|
439
466
|
};
|
|
440
467
|
// biome-ignore lint/correctness/useExhaustiveDependencies: syncEmpty only reads/writes stable refs, same as run()'s own call to it just below
|
|
441
468
|
const insertMention = React.useCallback((item) => {
|
package/dist/styles.css
CHANGED
|
@@ -447,7 +447,7 @@
|
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
.crisp-chip {
|
|
450
|
-
display: inline-flex; align-items: center; gap: 3px; height: var(--crisp-size-chip); min-width: 0; padding-inline: var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap;
|
|
450
|
+
display: inline-flex; align-items: center; gap: 3px; height: var(--crisp-size-chip); min-width: 0; max-width: 100%; padding-inline: var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap;
|
|
451
451
|
}
|
|
452
452
|
.crisp-chip--truncate_true {
|
|
453
453
|
& .crisp-chip-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
|
|
@@ -679,10 +679,14 @@
|
|
|
679
679
|
& .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
|
|
680
680
|
& .crisp-datatable-num { font-variant-numeric: tabular-nums; }
|
|
681
681
|
& .crisp-datatable-muted { color: var(--crisp-fg-muted); }
|
|
682
|
-
/*
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
682
|
+
/* url/email cell types (#654) -- same measured values as RecordsTable's own .crisp-recordstable-link, which this mirrors: no underline at rest, underline on hover, own-colour text rather than a browser-blue link. */
|
|
683
|
+
& .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
684
|
+
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
685
|
+
/* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
|
|
686
|
+
hairline doubling as the drop indicator (it tints toward the brand
|
|
687
|
+
colour as the active drop target). The affordance + geometry are
|
|
688
|
+
measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
|
|
689
|
+
pointer-events implementation. */
|
|
686
690
|
& .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
|
|
687
691
|
& .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
|
|
688
692
|
& .crisp-datatable-grip:active { cursor: grabbing; }
|
|
@@ -1892,6 +1896,13 @@
|
|
|
1892
1896
|
.crisp-tooltip {
|
|
1893
1897
|
position: fixed; z-index: 70; box-sizing: border-box; pointer-events: none; max-width: 260px; padding: var(--crisp-space-1-5) var(--crisp-space-2); border-radius: var(--crisp-radius-md); background: #101112; color: #eeeff1; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); line-height: 1.45; box-shadow: 0 4px 14px -2px rgba(28,40,64,0.28), 0 0 0 1px rgba(255,255,255,0.04); opacity: 1; transition: opacity 0.12s ease;
|
|
1894
1898
|
@starting-style { opacity: 0; }
|
|
1899
|
+
& .crisp-tooltip-reference-row { display: inline-flex; align-items: center; gap: var(--crisp-space-1); }
|
|
1900
|
+
}
|
|
1901
|
+
.crisp-tooltip--variant_plain {
|
|
1902
|
+
|
|
1903
|
+
}
|
|
1904
|
+
.crisp-tooltip--variant_reference {
|
|
1905
|
+
box-shadow: rgba(0,0,0,0) 0 0 0 1px inset, rgba(28,40,64,0.18) 0 0 2px 0, rgba(0,0,0,0.04) 0 1px 3px 0;
|
|
1895
1906
|
}
|
|
1896
1907
|
|
|
1897
1908
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -1966,14 +1977,18 @@
|
|
|
1966
1977
|
|
|
1967
1978
|
.crisp-authshell {
|
|
1968
1979
|
min-height: 100%; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; position: relative; background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; color: var(--crisp-fg-primary); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
1980
|
+
&[data-single-pane] { justify-content: center; }
|
|
1969
1981
|
& .crisp-authshell-progress { position: absolute; top: 0; left: 0; width: 100%; height: 2px; background: var(--crisp-border-default); }
|
|
1970
1982
|
& .crisp-authshell-progress-fill { height: 2px; background: var(--crisp-fg-primary); border-radius: 4px; transition: width .2s; }
|
|
1971
1983
|
& .crisp-authshell-logo { margin-top: var(--crisp-space-8); height: 24px; display: flex; align-items: center; }
|
|
1972
1984
|
& .crisp-authshell-card { margin-top: var(--crisp-space-6); display: flex; height: 692px; border-radius: 20px; border: 1px solid var(--crisp-border-subtle); overflow: hidden; }
|
|
1985
|
+
&[data-single-pane] .crisp-authshell-card { height: auto; border: none; border-radius: 0; overflow: visible; }
|
|
1973
1986
|
& .crisp-authshell-pane { flex: 0 0 564px; padding: 72px; display: flex; flex-direction: column; box-sizing: border-box; }
|
|
1987
|
+
&[data-single-pane] .crisp-authshell-pane { flex: 0 0 auto; padding: 0; }
|
|
1974
1988
|
& .crisp-authshell-preview { flex: 0 0 564px; padding: 96px 64px; background: var(--crisp-bg-subtle); border-inline-start: 1px solid color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); position: relative; overflow: hidden; box-sizing: border-box; }
|
|
1975
1989
|
& .crisp-authshell-preview::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 120px; background: linear-gradient(color-mix(in srgb, var(--crisp-bg-subtle) 0%, transparent) 0%, var(--crisp-bg-subtle) 100%); pointer-events: none; }
|
|
1976
1990
|
& .crisp-authshell-body { width: 420px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--crisp-space-8); align-items: center; }
|
|
1991
|
+
&[data-single-pane] .crisp-authshell-body { width: 360px; }
|
|
1977
1992
|
& .crisp-authshell-title { font-size: 20px; line-height: 24px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.4px; color: var(--crisp-fg-primary); align-self: stretch; }
|
|
1978
1993
|
& .crisp-authshell-form { align-self: stretch; display: flex; flex-direction: column; gap: 28px; }
|
|
1979
1994
|
& .crisp-authshell-footer { margin-top: var(--crisp-space-6); display: flex; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
|
|
@@ -2090,6 +2105,27 @@
|
|
|
2090
2105
|
}
|
|
2091
2106
|
}
|
|
2092
2107
|
|
|
2108
|
+
|
|
2109
|
+
/* Reaction picker -- see `ReactionPicker`'s own doc comment: the exact
|
|
2110
|
+
chrome wasn't independently pixel-measured this pass (screenshot
|
|
2111
|
+
capture broke mid-session), so this reuses Menu's already-measured
|
|
2112
|
+
floating-popover shell rather than a new value. */
|
|
2113
|
+
.crisp-comments-reactionpicker { font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; box-sizing: border-box; width: 240px; padding: var(--crisp-space-2); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); }
|
|
2114
|
+
.crisp-comments-reactionpicker-search { box-sizing: border-box; width: 100%; height: 28px; margin-bottom: var(--crisp-space-2); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-raised); font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-primary); }
|
|
2115
|
+
.crisp-comments-reactionpicker-search::placeholder { color: color-mix(in srgb, var(--crisp-fg-primary) 40%, transparent); }
|
|
2116
|
+
.crisp-comments-reactionpicker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: var(--crisp-space-0-5); max-height: 160px; overflow-y: auto; }
|
|
2117
|
+
.crisp-comments-reactionpicker-item { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; border-radius: var(--crisp-radius-sm); background: none; font-family: inherit; font-size: 16px; line-height: 1; cursor: pointer; }
|
|
2118
|
+
.crisp-comments-reactionpicker-item:hover { background: var(--crisp-bg-raised); }
|
|
2119
|
+
|
|
2120
|
+
/* Row badge (CommentCountBadge) -- lives in its own class family so the
|
|
2121
|
+
grid lanes can drop it into a cell without pulling in the panel's
|
|
2122
|
+
CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
|
|
2123
|
+
icon + 12px count text, both at 55% ink, replacing the dot. */
|
|
2124
|
+
.crisp-comments-badge { box-sizing: border-box; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; display: inline-flex; align-items: center; gap: var(--crisp-space-1); border: none; background: none; padding: 0; margin: 0; cursor: pointer; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); }
|
|
2125
|
+
.crisp-comments-badge-dot { box-sizing: border-box; width: 6px; height: 6px; border-radius: var(--crisp-radius-full); background: color-mix(in srgb, var(--crisp-fg-primary) 38%, transparent); }
|
|
2126
|
+
.crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
|
|
2127
|
+
[dir="rtl"] .crisp-comments-badge [data-rtl-flip] { transform: scaleX(-1); }
|
|
2128
|
+
|
|
2093
2129
|
.crisp-comments {
|
|
2094
2130
|
display: flex; flex-direction: column; box-sizing: border-box; width: 320px; height: 100%; background: var(--crisp-bg-surface); border-inline-start: 1px solid var(--crisp-border-subtle); font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
2095
2131
|
& * { box-sizing: border-box; }
|
|
@@ -2145,21 +2181,6 @@
|
|
|
2145
2181
|
/* An inserted "@mention" chip in the composer -- same shape as
|
|
2146
2182
|
RichTextEditor's own `.crisp-rte-mention` (crisp#428), reused here rather than re-derived since it's the same atomic-chip affordance. */
|
|
2147
2183
|
& .crisp-comments-mention { display: inline; padding: 0 2px; border-radius: 4px; background: color-mix(in srgb, var(--crisp-bg-brand-solid) 12%, transparent); color: var(--crisp-bg-brand-solid); font-weight: var(--crisp-weight-medium); user-select: none; }
|
|
2148
|
-
|
|
2149
|
-
/* Reaction picker -- see `ReactionPicker`'s own doc comment: the exact chrome wasn't independently pixel-measured this pass (screenshot
|
|
2150
|
-
capture broke mid-session), so this reuses Menu's already-measured floating-popover shell rather than a new value. */
|
|
2151
|
-
& .crisp-comments-reactionpicker { width: 240px; padding: var(--crisp-space-2); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); }
|
|
2152
|
-
& .crisp-comments-reactionpicker-search { box-sizing: border-box; width: 100%; height: 28px; margin-bottom: var(--crisp-space-2); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-raised); font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-primary); }
|
|
2153
|
-
& .crisp-comments-reactionpicker-search::placeholder { color: color-mix(in srgb, var(--crisp-fg-primary) 40%, transparent); }
|
|
2154
|
-
& .crisp-comments-reactionpicker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: var(--crisp-space-0-5); max-height: 160px; overflow-y: auto; }
|
|
2155
|
-
& .crisp-comments-reactionpicker-item { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; border-radius: var(--crisp-radius-sm); background: none; font-family: inherit; font-size: 16px; line-height: 1; cursor: pointer; }
|
|
2156
|
-
& .crisp-comments-reactionpicker-item:hover { background: var(--crisp-bg-raised); }
|
|
2157
|
-
/* Row badge (CommentCountBadge) -- lives in its own class family so the grid lanes can drop it into a cell without pulling in the panel's
|
|
2158
|
-
CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
|
|
2159
|
-
icon + 12px count text, both at 55% ink, replacing the dot. */
|
|
2160
|
-
& .crisp-comments-badge { display: inline-flex; align-items: center; gap: var(--crisp-space-1); border: none; background: none; padding: 0; margin: 0; font-family: inherit; cursor: pointer; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); }
|
|
2161
|
-
& .crisp-comments-badge-dot { width: 6px; height: 6px; border-radius: var(--crisp-radius-full); background: color-mix(in srgb, var(--crisp-fg-primary) 38%, transparent); }
|
|
2162
|
-
& .crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
|
|
2163
2184
|
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
2164
2185
|
}
|
|
2165
2186
|
|