@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
|
@@ -98,29 +98,44 @@ const config = (0, core_1.defineRecipe)({
|
|
|
98
98
|
RichTextEditor's own \`.crisp-rte-mention\` (crisp#428), reused here
|
|
99
99
|
rather than re-derived since it's the same atomic-chip affordance. */
|
|
100
100
|
& .crisp-comments-mention { display: inline; padding: 0 2px; border-radius: 4px; background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 12%, transparent); color: ${tokens_1.contract.bgBrandSolid}; font-weight: ${tokens_1.contract.weight.medium}; user-select: none; }
|
|
101
|
-
|
|
101
|
+
${core_1.rtlFlipCss}`,
|
|
102
|
+
variants: {},
|
|
103
|
+
defaultVariants: {},
|
|
104
|
+
compoundVariants: [],
|
|
105
|
+
// Two pieces below intentionally live OUTSIDE the nested \`base\` above,
|
|
106
|
+
// the same reason Menu's own \`.crisp-menu-content\` does (menu.recipe.ts's
|
|
107
|
+
// own comment, crisp#357): nesting compiles to a descendant combinator
|
|
108
|
+
// scoped to \`.crisp-comments\`, which needs real DOM containment. Both of
|
|
109
|
+
// these escape it -- \`ReactionPicker\` because \`Popover\` portals its
|
|
110
|
+
// content to \`document.body\`, and \`CommentCountBadge\` because it's built
|
|
111
|
+
// to be dropped into a table cell that has no \`.crisp-comments\` ancestor
|
|
112
|
+
// at all (its own module doc comment). Nested, this shipped invisible:
|
|
113
|
+
// \`CommentCountBadge\`'s dot rendered at 0×0 and the reaction picker fell
|
|
114
|
+
// back to \`Popover\`'s own generic (unstyled-by-this-recipe) surface --
|
|
115
|
+
// caught taking this component's own screenshots, not by any gate, since
|
|
116
|
+
// \`spec-check\` reads recipe source text and both classes were right there
|
|
117
|
+
// in it.
|
|
118
|
+
globalCss: `
|
|
102
119
|
/* Reaction picker -- see \`ReactionPicker\`'s own doc comment: the exact
|
|
103
120
|
chrome wasn't independently pixel-measured this pass (screenshot
|
|
104
121
|
capture broke mid-session), so this reuses Menu's already-measured
|
|
105
122
|
floating-popover shell rather than a new value. */
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
.crisp-comments-reactionpicker { font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; box-sizing: border-box; width: 240px; padding: ${tokens_1.contract.space["2"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgSurface}; box-shadow: ${tokens_1.elevation.floating}; }
|
|
124
|
+
.crisp-comments-reactionpicker-search { box-sizing: border-box; width: 100%; height: 28px; margin-bottom: ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgRaised}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgPrimary}; }
|
|
125
|
+
.crisp-comments-reactionpicker-search::placeholder { color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 40%, transparent); }
|
|
126
|
+
.crisp-comments-reactionpicker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: ${tokens_1.contract.space["0.5"]}; max-height: 160px; overflow-y: auto; }
|
|
127
|
+
.crisp-comments-reactionpicker-item { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; font-family: inherit; font-size: 16px; line-height: 1; cursor: pointer; }
|
|
128
|
+
.crisp-comments-reactionpicker-item:hover { background: ${tokens_1.contract.bgRaised}; }
|
|
112
129
|
|
|
113
130
|
/* Row badge (CommentCountBadge) -- lives in its own class family so the
|
|
114
131
|
grid lanes can drop it into a cell without pulling in the panel's
|
|
115
132
|
CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
|
|
116
133
|
icon + 12px count text, both at 55% ink, replacing the dot. */
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
defaultVariants: {},
|
|
123
|
-
compoundVariants: [],
|
|
134
|
+
.crisp-comments-badge { box-sizing: border-box; font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; border: none; background: none; padding: 0; margin: 0; cursor: pointer; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); }
|
|
135
|
+
.crisp-comments-badge-dot { box-sizing: border-box; width: 6px; height: 6px; border-radius: ${tokens_1.contract.radius.full}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 38%, transparent); }
|
|
136
|
+
.crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
|
|
137
|
+
[dir="rtl"] .crisp-comments-badge [data-rtl-flip] { transform: scaleX(-1); }
|
|
138
|
+
`,
|
|
124
139
|
});
|
|
125
140
|
exports.commentsRecipe = (0, core_1.resolveRecipe)(config);
|
|
126
141
|
exports.default = config;
|
|
@@ -76,8 +76,15 @@ function isLabelTruncated(el) {
|
|
|
76
76
|
* viewport change, label change) via `ResizeObserver`; SSR-safe the same way
|
|
77
77
|
* `MiddleTruncate` is -- the DOM is only touched inside an effect, so the
|
|
78
78
|
* server (and the first client paint) renders the plain span.
|
|
79
|
+
*
|
|
80
|
+
* #580: a `referencedType` column (Attio's own `type: "reference"`) appends
|
|
81
|
+
* that type's icon + name after the label and switches to `Tooltip`'s
|
|
82
|
+
* `"reference"` chrome variant (Attio's second, structurally distinct
|
|
83
|
+
* tooltip — same background/colour/radius/padding/font as the plain one, a
|
|
84
|
+
* different `box-shadow`, crisp#563). Every other column stays label-only,
|
|
85
|
+
* the `"plain"` variant #534/#558 shipped.
|
|
79
86
|
*/
|
|
80
|
-
function ThLabel({ label }) {
|
|
87
|
+
function ThLabel({ label, referencedType, }) {
|
|
81
88
|
const ref = React.useRef(null);
|
|
82
89
|
const [overflowing, setOverflowing] = React.useState(false);
|
|
83
90
|
// 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.
|
|
@@ -106,7 +113,9 @@ function ThLabel({ label }) {
|
|
|
106
113
|
// actually show. `open={false}` when it fits keeps the Tooltip mounted
|
|
107
114
|
// (asChild costs no extra DOM either way) but pins it permanently closed,
|
|
108
115
|
// so hovering a non-overflowing header still opens nothing.
|
|
109
|
-
|
|
116
|
+
const ReferenceIcon = referencedType?.icon;
|
|
117
|
+
const content = referencedType ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tooltip-reference-row", children: [label, ReferenceIcon && (0, jsx_runtime_1.jsx)(ReferenceIcon, { size: 16, strokeWidth: 1.75 }), referencedType.label] })) : (label);
|
|
118
|
+
return ((0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { content: content, asChild: true, variant: referencedType ? "reference" : "plain", open: overflowing ? undefined : false, children: (0, jsx_runtime_1.jsx)("span", { ref: ref, className: "crisp-recordstable-th-label", children: label }) }));
|
|
110
119
|
}
|
|
111
120
|
const EDITABLE = new Set(["text", "number", "url", "currency"]);
|
|
112
121
|
// types whose cell is an always-interactive picker (not double-click-to-edit)
|
|
@@ -1213,7 +1222,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
|
|
|
1213
1222
|
setDragCol(col.key);
|
|
1214
1223
|
e.dataTransfer.effectAllowed = "move";
|
|
1215
1224
|
}
|
|
1216
|
-
: undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)(ThLabel, { label: col.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-sort", children: (0, jsx_runtime_1.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
|
|
1225
|
+
: undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)(ThLabel, { label: col.label, referencedType: col.referencedType }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-sort", children: (0, jsx_runtime_1.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
|
|
1217
1226
|
rootRef.current &&
|
|
1218
1227
|
(0, react_dom_1.createPortal)((0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "asc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { size: 15, strokeWidth: 1.75 }), t.sortAscending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "desc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { size: 15, strokeWidth: 1.75 }), t.sortDescending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveLeft] }), (0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveRight] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordstable-menu-sep" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => {
|
|
1219
1228
|
setRenamingCol(col.key);
|
|
@@ -159,6 +159,17 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
|
|
|
159
159
|
// The Range covering a live "/query" (trigger included), mirroring the
|
|
160
160
|
// mention primitive below — only set in "slash" mode.
|
|
161
161
|
const slashRangeRef = React.useRef(null);
|
|
162
|
+
// #672: the selection the user actually had when they opened the toolbar's
|
|
163
|
+
// "Body ▾" menu — only set in "toolbar" mode. That menu's own search
|
|
164
|
+
// input steals DOM focus from the body the moment it opens, so by the
|
|
165
|
+
// time an item is picked, `window.getSelection()` no longer points at
|
|
166
|
+
// the block the user meant; without this, `run()`'s `body.focus()`
|
|
167
|
+
// resets the caret to the start of the document (the FIRST block)
|
|
168
|
+
// instead, and every conversion landed there regardless of what was
|
|
169
|
+
// selected. Cloned, since the live Range would otherwise track DOM
|
|
170
|
+
// mutations (and the selection moving to the search input) between now
|
|
171
|
+
// and when it's restored.
|
|
172
|
+
const toolbarSelectionRef = React.useRef(null);
|
|
162
173
|
const blockSearchRef = React.useRef(null);
|
|
163
174
|
const [mention, setMention] = React.useState(null);
|
|
164
175
|
// The Range covering the live "@query" text (trigger included) — kept
|
|
@@ -264,6 +275,7 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
|
|
|
264
275
|
}, [readOnly]);
|
|
265
276
|
const closeBlockMenu = React.useCallback(() => {
|
|
266
277
|
slashRangeRef.current = null;
|
|
278
|
+
toolbarSelectionRef.current = null;
|
|
267
279
|
setBlockMenu(null);
|
|
268
280
|
}, []);
|
|
269
281
|
/** Mirrors the mention-trigger detection below: only handles the caret
|
|
@@ -442,6 +454,14 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
|
|
|
442
454
|
sel?.removeAllRanges();
|
|
443
455
|
sel?.addRange(range);
|
|
444
456
|
}
|
|
457
|
+
else if (blockMenu?.mode === "toolbar" && toolbarSelectionRef.current) {
|
|
458
|
+
// #672: no trigger text to delete here (nothing was typed to open
|
|
459
|
+
// this menu) -- just put the selection back where the user actually
|
|
460
|
+
// had it, before `applyBlockItem` -> `run()` refocuses the body.
|
|
461
|
+
const sel = window.getSelection();
|
|
462
|
+
sel?.removeAllRanges();
|
|
463
|
+
sel?.addRange(toolbarSelectionRef.current);
|
|
464
|
+
}
|
|
445
465
|
applyBlockItem(item.id);
|
|
446
466
|
closeBlockMenu();
|
|
447
467
|
};
|
|
@@ -464,14 +484,21 @@ exports.RichTextEditor = React.forwardRef(function RichTextEditor({ defaultTitle
|
|
|
464
484
|
const toggleBlockMenuFromToolbar = () => {
|
|
465
485
|
if (!bar)
|
|
466
486
|
return;
|
|
467
|
-
setBlockMenu((cur) =>
|
|
468
|
-
|
|
469
|
-
|
|
487
|
+
setBlockMenu((cur) => {
|
|
488
|
+
if (cur?.mode === "toolbar")
|
|
489
|
+
return null;
|
|
490
|
+
// #672: capture the live selection NOW, before the menu's own search
|
|
491
|
+
// input (focused by the effect below) steals it away.
|
|
492
|
+
const sel = window.getSelection();
|
|
493
|
+
toolbarSelectionRef.current =
|
|
494
|
+
sel && sel.rangeCount > 0 ? sel.getRangeAt(0).cloneRange() : null;
|
|
495
|
+
return {
|
|
470
496
|
anchor: { x: bar.x, y: bar.y },
|
|
471
497
|
query: "",
|
|
472
498
|
highlighted: 0,
|
|
473
499
|
mode: "toolbar",
|
|
474
|
-
}
|
|
500
|
+
};
|
|
501
|
+
});
|
|
475
502
|
};
|
|
476
503
|
// biome-ignore lint/correctness/useExhaustiveDependencies: syncEmpty only reads/writes stable refs, same as run()'s own call to it just below
|
|
477
504
|
const insertMention = React.useCallback((item) => {
|
package/dist/comments.css
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
/* Reaction picker -- see `ReactionPicker`'s own doc comment: the exact
|
|
3
|
+
chrome wasn't independently pixel-measured this pass (screenshot
|
|
4
|
+
capture broke mid-session), so this reuses Menu's already-measured
|
|
5
|
+
floating-popover shell rather than a new value. */
|
|
6
|
+
.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); }
|
|
7
|
+
.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); }
|
|
8
|
+
.crisp-comments-reactionpicker-search::placeholder { color: color-mix(in srgb, var(--crisp-fg-primary) 40%, transparent); }
|
|
9
|
+
.crisp-comments-reactionpicker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: var(--crisp-space-0-5); max-height: 160px; overflow-y: auto; }
|
|
10
|
+
.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; }
|
|
11
|
+
.crisp-comments-reactionpicker-item:hover { background: var(--crisp-bg-raised); }
|
|
12
|
+
|
|
13
|
+
/* Row badge (CommentCountBadge) -- lives in its own class family so the
|
|
14
|
+
grid lanes can drop it into a cell without pulling in the panel's
|
|
15
|
+
CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
|
|
16
|
+
icon + 12px count text, both at 55% ink, replacing the dot. */
|
|
17
|
+
.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); }
|
|
18
|
+
.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); }
|
|
19
|
+
.crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
|
|
20
|
+
[dir="rtl"] .crisp-comments-badge [data-rtl-flip] { transform: scaleX(-1); }
|
|
21
|
+
|
|
1
22
|
.crisp-comments {
|
|
2
23
|
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;
|
|
3
24
|
& * { box-sizing: border-box; }
|
|
@@ -53,21 +74,6 @@
|
|
|
53
74
|
/* An inserted "@mention" chip in the composer -- same shape as
|
|
54
75
|
RichTextEditor's own `.crisp-rte-mention` (crisp#428), reused here rather than re-derived since it's the same atomic-chip affordance. */
|
|
55
76
|
& .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; }
|
|
56
|
-
|
|
57
|
-
/* Reaction picker -- see `ReactionPicker`'s own doc comment: the exact chrome wasn't independently pixel-measured this pass (screenshot
|
|
58
|
-
capture broke mid-session), so this reuses Menu's already-measured floating-popover shell rather than a new value. */
|
|
59
|
-
& .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); }
|
|
60
|
-
& .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); }
|
|
61
|
-
& .crisp-comments-reactionpicker-search::placeholder { color: color-mix(in srgb, var(--crisp-fg-primary) 40%, transparent); }
|
|
62
|
-
& .crisp-comments-reactionpicker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: var(--crisp-space-0-5); max-height: 160px; overflow-y: auto; }
|
|
63
|
-
& .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; }
|
|
64
|
-
& .crisp-comments-reactionpicker-item:hover { background: var(--crisp-bg-raised); }
|
|
65
|
-
/* 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
|
|
66
|
-
CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
|
|
67
|
-
icon + 12px count text, both at 55% ink, replacing the dot. */
|
|
68
|
-
& .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); }
|
|
69
|
-
& .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); }
|
|
70
|
-
& .crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
|
|
71
77
|
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
@layer crisp-components {
|
|
2
|
+
|
|
3
|
+
/* Reaction picker -- see `ReactionPicker`'s own doc comment: the exact
|
|
4
|
+
chrome wasn't independently pixel-measured this pass (screenshot
|
|
5
|
+
capture broke mid-session), so this reuses Menu's already-measured
|
|
6
|
+
floating-popover shell rather than a new value. */
|
|
7
|
+
.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); }
|
|
8
|
+
.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); }
|
|
9
|
+
.crisp-comments-reactionpicker-search::placeholder { color: color-mix(in srgb, var(--crisp-fg-primary) 40%, transparent); }
|
|
10
|
+
.crisp-comments-reactionpicker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: var(--crisp-space-0-5); max-height: 160px; overflow-y: auto; }
|
|
11
|
+
.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; }
|
|
12
|
+
.crisp-comments-reactionpicker-item:hover { background: var(--crisp-bg-raised); }
|
|
13
|
+
|
|
14
|
+
/* Row badge (CommentCountBadge) -- lives in its own class family so the
|
|
15
|
+
grid lanes can drop it into a cell without pulling in the panel's
|
|
16
|
+
CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
|
|
17
|
+
icon + 12px count text, both at 55% ink, replacing the dot. */
|
|
18
|
+
.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); }
|
|
19
|
+
.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); }
|
|
20
|
+
.crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
|
|
21
|
+
[dir="rtl"] .crisp-comments-badge [data-rtl-flip] { transform: scaleX(-1); }
|
|
22
|
+
|
|
2
23
|
.crisp-comments {
|
|
3
24
|
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;
|
|
4
25
|
& * { box-sizing: border-box; }
|
|
@@ -54,21 +75,6 @@
|
|
|
54
75
|
/* An inserted "@mention" chip in the composer -- same shape as
|
|
55
76
|
RichTextEditor's own `.crisp-rte-mention` (crisp#428), reused here rather than re-derived since it's the same atomic-chip affordance. */
|
|
56
77
|
& .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; }
|
|
57
|
-
|
|
58
|
-
/* Reaction picker -- see `ReactionPicker`'s own doc comment: the exact chrome wasn't independently pixel-measured this pass (screenshot
|
|
59
|
-
capture broke mid-session), so this reuses Menu's already-measured floating-popover shell rather than a new value. */
|
|
60
|
-
& .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); }
|
|
61
|
-
& .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); }
|
|
62
|
-
& .crisp-comments-reactionpicker-search::placeholder { color: color-mix(in srgb, var(--crisp-fg-primary) 40%, transparent); }
|
|
63
|
-
& .crisp-comments-reactionpicker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: var(--crisp-space-0-5); max-height: 160px; overflow-y: auto; }
|
|
64
|
-
& .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; }
|
|
65
|
-
& .crisp-comments-reactionpicker-item:hover { background: var(--crisp-bg-raised); }
|
|
66
|
-
/* 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
|
|
67
|
-
CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
|
|
68
|
-
icon + 12px count text, both at 55% ink, replacing the dot. */
|
|
69
|
-
& .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); }
|
|
70
|
-
& .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); }
|
|
71
|
-
& .crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
|
|
72
78
|
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
73
79
|
}
|
|
74
80
|
|
package/dist/datatable.css
CHANGED
|
@@ -114,10 +114,14 @@
|
|
|
114
114
|
& .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)); }
|
|
115
115
|
& .crisp-datatable-num { font-variant-numeric: tabular-nums; }
|
|
116
116
|
& .crisp-datatable-muted { color: var(--crisp-fg-muted); }
|
|
117
|
-
/*
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
/* 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. */
|
|
118
|
+
& .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
119
|
+
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
120
|
+
/* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
|
|
121
|
+
hairline doubling as the drop indicator (it tints toward the brand
|
|
122
|
+
colour as the active drop target). The affordance + geometry are
|
|
123
|
+
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
|
|
124
|
+
pointer-events implementation. */
|
|
121
125
|
& .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
|
|
122
126
|
& .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; }
|
|
123
127
|
& .crisp-datatable-grip:active { cursor: grabbing; }
|
|
@@ -115,10 +115,14 @@
|
|
|
115
115
|
& .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)); }
|
|
116
116
|
& .crisp-datatable-num { font-variant-numeric: tabular-nums; }
|
|
117
117
|
& .crisp-datatable-muted { color: var(--crisp-fg-muted); }
|
|
118
|
-
/*
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
/* 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. */
|
|
119
|
+
& .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
120
|
+
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
121
|
+
/* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
|
|
122
|
+
hairline doubling as the drop indicator (it tints toward the brand
|
|
123
|
+
colour as the active drop target). The affordance + geometry are
|
|
124
|
+
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
|
|
125
|
+
pointer-events implementation. */
|
|
122
126
|
& .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
|
|
123
127
|
& .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; }
|
|
124
128
|
& .crisp-datatable-grip:active { cursor: grabbing; }
|
|
@@ -27,6 +27,18 @@ const spec = defineSpec({
|
|
|
27
27
|
// -- `inline-flex` items don't by default -- which `truncate` below
|
|
28
28
|
// depends on and any consumer constraining a Chip's width already needs.
|
|
29
29
|
minWidth: 0,
|
|
30
|
+
// Caps the chip at its containing block's width (#668) -- `min-width: 0`
|
|
31
|
+
// alone only lets it shrink inside a flex/grid row with `flex-shrink`;
|
|
32
|
+
// it does nothing in a plain block or table-cell ancestor (GRC's own
|
|
33
|
+
// `DataTable` cells, a render-uniform `<td>`, not a flex row), where the
|
|
34
|
+
// chip just grew to content width and overflowed instead. Checked
|
|
35
|
+
// Attio's own actor chip for a value to match instead of guessing one:
|
|
36
|
+
// `max-width: none` there too -- it has no pixel cap either, and stays
|
|
37
|
+
// contained only because its own ancestor chain is a real flex row the
|
|
38
|
+
// whole way down, which `Chip` alone can't assume for every consumer.
|
|
39
|
+
// `100%` is the honest equivalent: never wider than the container,
|
|
40
|
+
// whatever kind of container it is.
|
|
41
|
+
maxWidth: "100%",
|
|
30
42
|
paddingInline: c.space["1"],
|
|
31
43
|
borderRadius: c.radius.chip,
|
|
32
44
|
fontSize: c.text.lg,
|
|
@@ -26,8 +26,16 @@ export function isLabelTruncated(el) {
|
|
|
26
26
|
* viewport change, header text change) via `ResizeObserver`; SSR-safe the
|
|
27
27
|
* same way `MiddleTruncate` is -- the DOM is only touched inside an effect,
|
|
28
28
|
* so the server (and the first client paint) render the closed tooltip.
|
|
29
|
+
*
|
|
30
|
+
* #580: a `referencedType` column appends that type's icon + name after the
|
|
31
|
+
* label (Attio's own reference-column tooltip, e.g. "Primary location" +
|
|
32
|
+
* Country's icon + "Country") and uses `Tooltip`'s `"reference"` chrome
|
|
33
|
+
* variant -- Attio's second, structurally distinct tooltip (same
|
|
34
|
+
* background/colour/radius/padding/font as the plain one, a different
|
|
35
|
+
* `box-shadow`, crisp#563). Every other column stays exactly as #534/#558
|
|
36
|
+
* shipped: label-only, the `"plain"` variant.
|
|
29
37
|
*/
|
|
30
|
-
function ThLabel({ header }) {
|
|
38
|
+
function ThLabel({ header, referencedType, }) {
|
|
31
39
|
const ref = React.useRef(null);
|
|
32
40
|
const [overflowing, setOverflowing] = React.useState(false);
|
|
33
41
|
// biome-ignore lint/correctness/useExhaustiveDependencies: `header` isn't read in the body, but a rename (col.header 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.
|
|
@@ -56,7 +64,9 @@ function ThLabel({ header }) {
|
|
|
56
64
|
// actually show. `open={false}` when it fits keeps the Tooltip mounted
|
|
57
65
|
// (asChild costs no extra DOM either way) but pins it permanently closed,
|
|
58
66
|
// so hovering a non-overflowing header still opens nothing.
|
|
59
|
-
|
|
67
|
+
const ReferenceIcon = referencedType?.icon;
|
|
68
|
+
const content = referencedType ? (_jsxs("span", { className: "crisp-tooltip-reference-row", children: [header, ReferenceIcon && _jsx(ReferenceIcon, { size: 16, strokeWidth: 1.75 }), referencedType.label] })) : (header);
|
|
69
|
+
return (_jsx(Tooltip, { content: content, asChild: true, variant: referencedType ? "reference" : "plain", open: overflowing ? undefined : false, children: _jsx("span", { ref: ref, className: "crisp-datatable-th-label", children: header }) }));
|
|
60
70
|
}
|
|
61
71
|
/** A `date` input needs `yyyy-mm-dd`; cells may hold a Date, an ISO string or
|
|
62
72
|
* a display string. Returns "" when it cannot be parsed, so the field opens
|
|
@@ -252,6 +262,16 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US") {
|
|
|
252
262
|
// translation) can never silently repaint the chip
|
|
253
263
|
return (_jsx("span", { className: "crisp-datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: display || s }));
|
|
254
264
|
}
|
|
265
|
+
case "url":
|
|
266
|
+
case "email": {
|
|
267
|
+
const text = display || asText(value);
|
|
268
|
+
if (!text)
|
|
269
|
+
return empty;
|
|
270
|
+
return (_jsx("a", { className: "crisp-datatable-link", href: col.type === "url" ? `https://${text}` : `mailto:${text}`, target: col.type === "url" ? "_blank" : undefined, rel: col.type === "url" ? "noreferrer" : undefined,
|
|
271
|
+
// Same reasoning as `entity`'s `onEntityClick` link above: the
|
|
272
|
+
// link's own navigation must not also fire the row's onRowClick.
|
|
273
|
+
onClick: (ev) => ev.stopPropagation(), children: text }));
|
|
274
|
+
}
|
|
255
275
|
default: {
|
|
256
276
|
const text = display || formatCell(col.type, value, col.symbol, locale);
|
|
257
277
|
const isNum = col.type === "number" || col.type === "currency";
|
|
@@ -387,7 +407,8 @@ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
|
|
|
387
407
|
keywords: query,
|
|
388
408
|
});
|
|
389
409
|
}
|
|
390
|
-
|
|
410
|
+
const isActor = col.editor === "actor";
|
|
411
|
+
return (_jsx("span", { ref: ref, style: { display: "contents" }, children: _jsx(Select, { variant: "ghost", searchable: true, searchPlaceholder: col.refSearchPlaceholder ?? (isActor ? "Search people…" : undefined), emptyLabel: col.refEmptyLabel ?? (isActor ? "No users" : undefined), onSearchChange: setQuery, "aria-label": `Edit ${col.key}`, value: asText(value) || undefined, disabled: isPending, options: options, onValueChange: (v) => {
|
|
391
412
|
if (v === CREATE_REFERENCE_VALUE)
|
|
392
413
|
col.onCreateReference?.(query);
|
|
393
414
|
else
|
|
@@ -1162,7 +1183,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1162
1183
|
? "ascending"
|
|
1163
1184
|
: "descending"
|
|
1164
1185
|
: "none";
|
|
1165
|
-
const inner = (_jsxs("span", { className: "crisp-datatable-th-inner", children: [Icon ? _jsx(Icon, { size: 16, strokeWidth: 1.75 }) : null, _jsx(ThLabel, { header: col.header }), col.sortable ? _jsx(ChevronFor, { colKey: col.key }) : null] }));
|
|
1186
|
+
const inner = (_jsxs("span", { className: "crisp-datatable-th-inner", children: [Icon ? _jsx(Icon, { size: 16, strokeWidth: 1.75 }) : null, _jsx(ThLabel, { header: col.header, referencedType: col.referencedType }), col.sortable ? _jsx(ChevronFor, { colKey: col.key }) : null] }));
|
|
1166
1187
|
return (_jsxs(TableHead, { align: alignFor(col), "aria-sort": ariaSort, "aria-colindex": colAriaIndex(i), ...dataStickyCol(i), children: [renaming === col.key ? (_jsx("input", {
|
|
1167
1188
|
// biome-ignore lint/a11y/noAutofocus: the rename item opens this field
|
|
1168
1189
|
autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": renameColumnLabel, onBlur: (e) => {
|
|
@@ -1243,7 +1264,9 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1243
1264
|
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_jsx(Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1244
1265
|
}
|
|
1245
1266
|
if (isEditing &&
|
|
1246
|
-
(col.editor === "select" ||
|
|
1267
|
+
(col.editor === "select" ||
|
|
1268
|
+
col.editor === "reference" ||
|
|
1269
|
+
col.editor === "actor")) {
|
|
1247
1270
|
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined,
|
|
1248
1271
|
// Same as the plain text editor's own cell:
|
|
1249
1272
|
// opening/using the picker must not also fire
|
|
@@ -141,6 +141,12 @@ const config = defineRecipe({
|
|
|
141
141
|
& .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); }
|
|
142
142
|
& .crisp-datatable-num { font-variant-numeric: tabular-nums; }
|
|
143
143
|
& .crisp-datatable-muted { color: ${c.fgMuted}; }
|
|
144
|
+
/* url/email cell types (#654) -- same measured values as RecordsTable's
|
|
145
|
+
own .crisp-recordstable-link, which this mirrors: no underline at
|
|
146
|
+
rest, underline on hover, own-colour text rather than a browser-blue
|
|
147
|
+
link. */
|
|
148
|
+
& .crisp-datatable-link { color: ${c.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
149
|
+
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
144
150
|
/* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
|
|
145
151
|
a leading 20px grip column, a 16px grip icon, and the row's own
|
|
146
152
|
hairline doubling as the drop indicator (it tints toward the brand
|
|
@@ -39,9 +39,19 @@ function initials(name) {
|
|
|
39
39
|
* Composes `Chip`'s `truncate` CSS directly rather than its string-only
|
|
40
40
|
* auto-wrap (children here is an avatar plus a name, not a single string),
|
|
41
41
|
* so the name gets its own `.crisp-chip-label` span while the avatar stays
|
|
42
|
-
* fixed-size.
|
|
42
|
+
* fixed-size. `trailing` (#683) is the same idea for a consumer's own
|
|
43
|
+
* icon/badge after the name — inside the chip's own flex row rather than a
|
|
44
|
+
* second one a consumer would otherwise wrap this in, which needs its own
|
|
45
|
+
* `min-width: 0` to keep shrinking correctly and, missing that, overflows
|
|
46
|
+
* its container the same way #668 did before `Chip` itself was fixed.
|
|
43
47
|
*/
|
|
44
|
-
export function PersonChip({ name, avatar, muted, className, ...props }) {
|
|
45
|
-
return (_jsxs(Chip, { tone: "neutral", truncate: true, className: className, ...props, children: [_jsx(Avatar, { size: "sm", shape: "circle", tone: avatar ? "neutral" : hashTone(name),
|
|
48
|
+
export function PersonChip({ name, avatar, muted, trailing, className, ...props }) {
|
|
49
|
+
return (_jsxs(Chip, { tone: "neutral", truncate: true, className: className, ...props, children: [_jsx(Avatar, { size: "sm", shape: "circle", tone: avatar ? "neutral" : hashTone(name),
|
|
50
|
+
// Explicit rather than relying on the flex default (a fixed-size,
|
|
51
|
+
// non-wrapping element's own min-content floor already resists
|
|
52
|
+
// shrinking in every browser tested) — #668 asked "does the avatar
|
|
53
|
+
// stay" as its own question, so it gets its own undeniable answer
|
|
54
|
+
// rather than an implicit one.
|
|
55
|
+
style: { flexShrink: 0 }, children: avatar ? _jsx("img", { src: avatar, alt: "" }) : initials(name) }), _jsx("span", { className: "crisp-chip-label", style: muted ? { color: c.fgNeutralMuted } : undefined, children: name }), trailing != null && (_jsx("span", { className: "crisp-personchip-trailing", style: { flexShrink: 0 }, children: trailing }))] }));
|
|
46
56
|
}
|
|
47
57
|
PersonChip.displayName = "PersonChip";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, CircleHelp, GripVertical, ListFilter, MoreVertical, Plus, X, } from "lucide-react";
|
|
2
|
+
import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, CircleHelp, GripVertical, ListFilter, MoreVertical, Plus, SlidersHorizontal, X, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { isInsideLaterFloatingLayer, useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
|
|
5
5
|
import { MenuCheckboxItem, useMenuKeyboard } from "../menu/index.js";
|
|
@@ -138,7 +138,7 @@ export function applySorts(rows, sorts, get = (row, key) => row[key]) {
|
|
|
138
138
|
* this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
|
|
139
139
|
* Created by), which one shared list can't reproduce.
|
|
140
140
|
*/
|
|
141
|
-
export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, ...rest }) {
|
|
141
|
+
export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, ...rest }) {
|
|
142
142
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
143
143
|
// CrispMessagesProvider's translation of the English default below, or
|
|
144
144
|
// that default itself with no provider — unchanged from before this
|
|
@@ -161,6 +161,10 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
161
161
|
const deleteFilterLabel = useCrispMessage(deleteFilterLabelProp, "Delete filter");
|
|
162
162
|
const ascendingLabel = useCrispMessage(ascendingLabelProp, "Ascending");
|
|
163
163
|
const descendingLabel = useCrispMessage(descendingLabelProp, "Descending");
|
|
164
|
+
const viewSettingsLabel = useCrispMessage(viewSettingsLabelProp, "View settings");
|
|
165
|
+
const reorderAttributeLabel = useCrispMessage(reorderAttributeLabelProp, "Reorder attribute");
|
|
166
|
+
const addAttributeToViewLabel = useCrispMessage(addAttributeToViewLabelProp, "Add attribute to view");
|
|
167
|
+
const searchAttributesPlaceholder = useCrispMessage(searchAttributesPlaceholderProp, "Search attributes…");
|
|
164
168
|
// OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
|
|
165
169
|
// their own — a whole lookup table, not one string — so they translate
|
|
166
170
|
// straight from the provider's dictionary at the two spots they render
|
|
@@ -172,10 +176,17 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
172
176
|
// gets `columns` on the other, unchanged from before either prop existed.
|
|
173
177
|
const sortColumns = sortColumnsProp ?? columns ?? [];
|
|
174
178
|
const filterColumns = filterColumnsProp ?? columns ?? [];
|
|
179
|
+
const viewColumns = viewColumnsProp ?? columns ?? [];
|
|
175
180
|
const [sortsState, setSortsState] = React.useState(defaultSorts ?? []);
|
|
176
181
|
const [filtersState, setFiltersState] = React.useState(defaultFilters ?? []);
|
|
182
|
+
// #590: "everything shown" unless told otherwise -- lazy initializer so
|
|
183
|
+
// this only reads `viewColumns` once, at mount, the same way `defaultSorts`/
|
|
184
|
+
// `defaultFilters` above seed once and are never re-derived from a prop
|
|
185
|
+
// that may keep changing identity across renders.
|
|
186
|
+
const [shownKeysState, setShownKeysState] = React.useState(() => defaultShownKeys ?? viewColumns.map((c) => c.key));
|
|
177
187
|
const sorts = sortsProp ?? sortsState;
|
|
178
188
|
const filters = filtersProp ?? filtersState;
|
|
189
|
+
const shownKeys = shownKeysProp ?? shownKeysState;
|
|
179
190
|
const putSorts = (next) => {
|
|
180
191
|
if (sortsProp === undefined)
|
|
181
192
|
setSortsState(next);
|
|
@@ -186,11 +197,28 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
186
197
|
setFiltersState(next);
|
|
187
198
|
onFiltersChange?.(next);
|
|
188
199
|
};
|
|
200
|
+
const putShownKeys = (next) => {
|
|
201
|
+
if (shownKeysProp === undefined)
|
|
202
|
+
setShownKeysState(next);
|
|
203
|
+
onShownKeysChange?.(next);
|
|
204
|
+
};
|
|
189
205
|
const [openMenu, setOpenMenu] = React.useState(null);
|
|
190
206
|
const [dragSort, setDragSort] = React.useState(null);
|
|
191
207
|
const [filterQuery, setFilterQuery] = React.useState("");
|
|
192
208
|
// filters the "isanyof" checkbox list, same lifetime as filterQuery above
|
|
193
209
|
const [anyOfQuery, setAnyOfQuery] = React.useState("");
|
|
210
|
+
const [dragAttr, setDragAttr] = React.useState(null);
|
|
211
|
+
// #590: the popover has two screens -- the draggable shown-attribute list
|
|
212
|
+
// (default) and, behind "Add attribute to view", a search list of the
|
|
213
|
+
// rest. Measured live, 2026-09-25: clicking "View settings" opens a
|
|
214
|
+
// `role="dialog"` of dnd-kit `aria-roledescription="sortable"` rows --
|
|
215
|
+
// grip, type icon, label -- with no search box, ending in a static
|
|
216
|
+
// "Add attribute to view" row; each row also carries a "View attribute
|
|
217
|
+
// menu" kebab whose own contents weren't explored (undefined scope, not
|
|
218
|
+
// built here -- hiding an attribute is a trailing control instead, see
|
|
219
|
+
// the hide button below).
|
|
220
|
+
const [addingAttribute, setAddingAttribute] = React.useState(false);
|
|
221
|
+
const [addAttrQuery, setAddAttrQuery] = React.useState("");
|
|
194
222
|
// close any open menu on outside click (anything not inside a menu or its
|
|
195
223
|
// trigger) or Escape
|
|
196
224
|
React.useEffect(() => {
|
|
@@ -253,6 +281,27 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
253
281
|
putFilters(filters.filter((c) => c.id !== id));
|
|
254
282
|
setOpenMenu(null);
|
|
255
283
|
};
|
|
284
|
+
// #590: shown attributes, in `shownKeys`' order -- a key with no matching
|
|
285
|
+
// `viewColumns` entry (stale state after a column is removed upstream)
|
|
286
|
+
// drops out rather than rendering a blank row.
|
|
287
|
+
const shownAttrs = shownKeys
|
|
288
|
+
.map((k) => viewColumns.find((c) => c.key === k))
|
|
289
|
+
.filter((c) => c != null);
|
|
290
|
+
const hiddenAttrs = viewColumns.filter((c) => !shownKeys.includes(c.key));
|
|
291
|
+
const moveAttr = (from, to) => {
|
|
292
|
+
if (from === to)
|
|
293
|
+
return;
|
|
294
|
+
const next = [...shownKeys];
|
|
295
|
+
const [moved] = next.splice(from, 1);
|
|
296
|
+
next.splice(to, 0, moved);
|
|
297
|
+
putShownKeys(next);
|
|
298
|
+
};
|
|
299
|
+
const hideAttr = (key) => putShownKeys(shownKeys.filter((k) => k !== key));
|
|
300
|
+
const showAttr = (key) => {
|
|
301
|
+
putShownKeys([...shownKeys, key]);
|
|
302
|
+
setAddingAttribute(false);
|
|
303
|
+
setAddAttrQuery("");
|
|
304
|
+
};
|
|
256
305
|
// #594: the filter chip's kebab is a real role="menu" (crisp/#188's shared
|
|
257
306
|
// model — arrows, Home/End, typeahead, Escape), not a hand-rolled div like
|
|
258
307
|
// the sort/condition popovers above (those are role="dialog", which makes
|
|
@@ -342,7 +391,31 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
342
391
|
});
|
|
343
392
|
}, children: v }, v));
|
|
344
393
|
})] }))] }))] }, cond.id));
|
|
345
|
-
}), _jsx("span", { className: "crisp-tabletoolbar-
|
|
394
|
+
}), _jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" }), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost", onClick: () => {
|
|
395
|
+
if (openMenu === "viewsettings") {
|
|
396
|
+
setOpenMenu(null);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
setAddingAttribute(false);
|
|
400
|
+
setAddAttrQuery("");
|
|
401
|
+
setOpenMenu("viewsettings");
|
|
402
|
+
}
|
|
403
|
+
}, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [_jsx(SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel] }), openMenu === "viewsettings" &&
|
|
404
|
+
(addingAttribute ? (_jsxs("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: searchAttributesPlaceholder, "aria-label": searchAttributesPlaceholder,
|
|
405
|
+
// biome-ignore lint/a11y/noAutofocus: focus the search box on open, same as Filter's field picker
|
|
406
|
+
autoFocus: true, value: addAttrQuery, onChange: (e) => setAddAttrQuery(e.target.value) }), hiddenAttrs
|
|
407
|
+
.filter((col) => col.label
|
|
408
|
+
.toLowerCase()
|
|
409
|
+
.includes(addAttrQuery.trim().toLowerCase()))
|
|
410
|
+
.map((col) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => showAttr(col.key), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key)))] })) : (_jsxs("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
411
|
+
if (dragAttr !== null)
|
|
412
|
+
e.preventDefault();
|
|
413
|
+
}, onDrop: () => {
|
|
414
|
+
if (dragAttr !== null)
|
|
415
|
+
moveAttr(dragAttr, i);
|
|
416
|
+
setDragAttr(null);
|
|
417
|
+
}, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), _jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
|
|
418
|
+
`${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] }), _jsx("span", { className: "crisp-tabletoolbar-spacer" }), count != null && (_jsx("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
|
|
346
419
|
}
|
|
347
420
|
/**
|
|
348
421
|
* The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
|