@crisp-ui-kit/crisp 0.54.7 → 0.54.9
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 +5 -1
- package/dist/authshell.layered.css +5 -1
- package/dist/badge.css +5 -5
- package/dist/badge.layered.css +5 -5
- package/dist/board.css +36 -8
- package/dist/board.layered.css +36 -8
- package/dist/button.css +3 -1
- package/dist/button.layered.css +3 -1
- package/dist/cjs/components/badge/badge.recipe.js +28 -5
- package/dist/cjs/components/button/button.js +33 -2
- package/dist/cjs/components/button/button.recipe.js +31 -5
- package/dist/cjs/components/contextmenu/contextmenu.js +2 -2
- package/dist/cjs/components/contextmenu/contextmenu.recipe.js +24 -0
- package/dist/cjs/components/datatable/datatable.js +590 -41
- package/dist/cjs/components/datatable/datatable.recipe.js +118 -5
- package/dist/cjs/components/dialog/dialog.js +72 -5
- package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +365 -49
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +78 -0
- package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
- package/dist/cjs/components/tooltip/tooltip.js +23 -9
- package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
- package/dist/cjs/product/authshell/authshell.js +14 -1
- package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
- package/dist/cjs/product/board/board.js +175 -46
- package/dist/cjs/product/board/board.recipe.js +56 -4
- package/dist/cjs/product/comments/comments.js +34 -12
- package/dist/cjs/product/comments/comments.recipe.js +31 -7
- package/dist/cjs/product/formdialog/formdialog.js +10 -4
- package/dist/cjs/product/navitem/navitem.js +7 -2
- package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
- package/dist/cjs/product/objectsettings/index.js +1 -2
- package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
- package/dist/cjs/product/recordstable/recordstable.js +1 -17
- package/dist/cjs/product/richtext/richtext.js +76 -1
- package/dist/cjs/product/savedviews/savedviews.js +55 -24
- package/dist/cjs/product/savedviews/savedviews.recipe.js +27 -0
- package/dist/cjs/product/sidebar/sidebar.js +68 -15
- package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
- package/dist/cjs/tokens/palette.js +32 -1
- package/dist/cjs/tokens/semantic.js +11 -0
- package/dist/comments.css +50 -14
- package/dist/comments.layered.css +50 -14
- package/dist/contextmenu.css +24 -0
- package/dist/contextmenu.layered.css +24 -0
- package/dist/datatable.css +101 -37
- package/dist/datatable.layered.css +101 -37
- package/dist/dialog.css +13 -1
- package/dist/dialog.layered.css +13 -1
- package/dist/esm/components/badge/badge.recipe.js +28 -5
- package/dist/esm/components/button/button.js +34 -3
- package/dist/esm/components/button/button.recipe.js +31 -5
- package/dist/esm/components/contextmenu/contextmenu.js +2 -2
- package/dist/esm/components/contextmenu/contextmenu.recipe.js +24 -0
- package/dist/esm/components/datatable/datatable.js +590 -42
- package/dist/esm/components/datatable/datatable.recipe.js +119 -6
- package/dist/esm/components/dialog/dialog.js +72 -5
- package/dist/esm/components/dialog/dialog.recipe.js +15 -1
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +366 -50
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +80 -2
- package/dist/esm/components/tabs/tabs.recipe.js +11 -2
- package/dist/esm/components/tooltip/tooltip.js +23 -9
- package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
- package/dist/esm/product/authshell/authshell.js +15 -2
- package/dist/esm/product/authshell/authshell.recipe.js +13 -1
- package/dist/esm/product/authshell/index.js +1 -1
- package/dist/esm/product/board/board.js +176 -47
- package/dist/esm/product/board/board.recipe.js +56 -4
- package/dist/esm/product/comments/comments.js +35 -13
- package/dist/esm/product/comments/comments.recipe.js +31 -7
- package/dist/esm/product/formdialog/formdialog.js +10 -4
- package/dist/esm/product/navitem/index.js +1 -1
- package/dist/esm/product/navitem/navitem.js +7 -2
- package/dist/esm/product/navitem/navitem.recipe.js +16 -0
- package/dist/esm/product/objectsettings/index.js +1 -1
- package/dist/esm/product/objectsettings/objectsettings.js +2 -12
- package/dist/esm/product/recordstable/recordstable.js +2 -18
- package/dist/esm/product/richtext/richtext.js +76 -1
- package/dist/esm/product/savedviews/savedviews.js +57 -26
- package/dist/esm/product/savedviews/savedviews.recipe.js +27 -0
- package/dist/esm/product/sidebar/sidebar.js +69 -16
- package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
- package/dist/esm/tokens/palette.js +32 -1
- package/dist/esm/tokens/semantic.js +11 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +16 -2
- package/dist/navitem.css +7 -0
- package/dist/navitem.layered.css +7 -0
- package/dist/recordstable.css +34 -32
- package/dist/recordstable.layered.css +34 -32
- package/dist/savedviews.css +27 -0
- package/dist/savedviews.layered.css +27 -0
- package/dist/sidebar.css +6 -1
- package/dist/sidebar.layered.css +6 -1
- package/dist/styles.css +369 -106
- package/dist/styles.layered.css +369 -106
- package/dist/tabletoolbar.css +47 -3
- package/dist/tabletoolbar.layered.css +47 -3
- package/dist/tabs.css +1 -1
- package/dist/tabs.layered.css +1 -1
- package/dist/tokens.css +9 -1
- package/dist/tokens.json +16 -2
- package/dist/tooltip.css +1 -1
- package/dist/tooltip.layered.css +1 -1
- package/dist/types/components/badge/badge.recipe.d.ts +5 -1
- package/dist/types/components/button/button.recipe.d.ts +18 -5
- package/dist/types/components/contextmenu/contextmenu.d.ts +8 -1
- package/dist/types/components/datatable/datatable.d.ts +98 -1
- package/dist/types/components/dialog/dialog.d.ts +25 -2
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +113 -3
- package/dist/types/components/tooltip/tooltip.d.ts +12 -4
- package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
- package/dist/types/product/authshell/authshell.d.ts +26 -2
- package/dist/types/product/authshell/index.d.ts +1 -1
- package/dist/types/product/board/board.d.ts +53 -6
- package/dist/types/product/comments/comments.d.ts +16 -3
- package/dist/types/product/formdialog/formdialog.d.ts +2 -1
- package/dist/types/product/navitem/index.d.ts +1 -1
- package/dist/types/product/navitem/navitem.d.ts +23 -0
- package/dist/types/product/objectsettings/index.d.ts +1 -1
- package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
- package/dist/types/product/recordstable/recordstable.d.ts +0 -1
- package/dist/types/product/savedviews/savedviews.d.ts +45 -8
- package/dist/types/product/sidebar/sidebar.d.ts +33 -5
- package/dist/types/tokens/contract.d.ts +4 -0
- package/dist/types/tokens/palette.d.ts +13 -1
- package/dist/types/tokens/semantic.d.ts +11 -0
- package/package.json +1 -1
|
@@ -1,6 +1,81 @@
|
|
|
1
|
-
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
-
import { contract as c } from "../../tokens/index.js";
|
|
1
|
+
import { defineRecipe, resolveRecipe, rtlFlipCss } from "../../core/index.js";
|
|
2
|
+
import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
3
3
|
import { toolbarChromeBody, toolbarChromeHead } from "./tabletoolbar.chrome.js";
|
|
4
|
+
// The filter condition chip, TableToolbar-only (crisp/#462 follow-up,
|
|
5
|
+
// re-measured live 2026-09-25). Deliberately NOT added to the shared
|
|
6
|
+
// `tabletoolbar.chrome.ts` functions: those also render under RecordsTable's
|
|
7
|
+
// own `crisp-recordstable-` prefix, whose still-tested (84 cases) chip
|
|
8
|
+
// predates this re-measurement and keeps its original `.fchip`/`.fchip-main`
|
|
9
|
+
// markup (one combined field+op+value button, plus a small floating kebab)
|
|
10
|
+
// until RecordsTable is rebuilt onto DataTable + TableToolbar and inherits
|
|
11
|
+
// this one instead. New, disjoint class names here so nothing shared changes
|
|
12
|
+
// meaning under the older markup.
|
|
13
|
+
//
|
|
14
|
+
// Measured on Attio's own chip (`app.attio.com/<ws>/companies`, a text-field
|
|
15
|
+
// condition): 4 independently-clickable, edge-to-edge segments, each 28px
|
|
16
|
+
// tall with its own `inset 0 0 0 1px` ring — field (radius 8/0/0/8), operator
|
|
17
|
+
// (flat), value (flat), kebab (radius 0/8/8/0). Background is NOT fixed per
|
|
18
|
+
// segment: the field and value segments (the two that show the condition's
|
|
19
|
+
// own text) read `rgb(246,247,247)` while the condition has no value yet and
|
|
20
|
+
// flip to white the instant it does (typing a value, or picking an operator
|
|
21
|
+
// like "empty" that needs none, both trigger it); the operator and kebab
|
|
22
|
+
// segments (pure controls, no text of the condition's own) stay white
|
|
23
|
+
// throughout. Click targets, confirmed live: the field segment reopens the
|
|
24
|
+
// attribute picker and replaces this condition's field in place (not a
|
|
25
|
+
// second chip); the operator segment opens an operator-only menu (closing it
|
|
26
|
+
// after a pick, then auto-opening the value popover, focused, when that
|
|
27
|
+
// operator needs one — "empty"/"not empty" just close); the value segment
|
|
28
|
+
// opens a value-only popover/input, independent of the operator menu (Attio
|
|
29
|
+
// never combines the two, unlike crisp's older single dialog).
|
|
30
|
+
const filterChipChrome = `
|
|
31
|
+
& .crisp-tabletoolbar-fcond { display: inline-flex; align-items: center; height: ${c.size.row}; font-size: ${c.text.md}; color: ${c.fgPrimary}; white-space: nowrap; }
|
|
32
|
+
& .crisp-tabletoolbar-fcond-seg { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 100%; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: ${c.bgSurfaceMuted}; box-shadow: inset 0 0 0 1px ${c.borderDivider}; font-family: inherit; font-size: inherit; color: inherit; cursor: pointer; }
|
|
33
|
+
& .crisp-tabletoolbar-fcond-seg svg { flex-shrink: 0; }
|
|
34
|
+
& .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--field,
|
|
35
|
+
& .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--val { background: ${c.bgSurface}; }
|
|
36
|
+
& .crisp-tabletoolbar-fcond-seg--field { border-radius: ${c.radius.md} 0 0 ${c.radius.md}; font-weight: ${c.weight.medium}; max-width: 200px; }
|
|
37
|
+
& .crisp-tabletoolbar-fcond-seg--field svg { color: ${c.fgMuted}; }
|
|
38
|
+
/* #592: a nested pick's field segment ("Team › Twitter follower
|
|
39
|
+
count") — measured live: BOTH path segments the same colour (chip
|
|
40
|
+
context only; the picker's own menu rows keep the parent muted, see
|
|
41
|
+
tabletoolbar.chrome.ts), each independently truncated. Attio's own
|
|
42
|
+
layout gives the leaf segment priority (it stays fully readable
|
|
43
|
+
longer, the parent truncates first) via a two-part min-content
|
|
44
|
+
measurement this reproduces qualitatively with a flex-shrink
|
|
45
|
+
differential rather than byte-for-byte (the exact px split Attio
|
|
46
|
+
computes is content-length-dependent, not a fixed value) — not
|
|
47
|
+
independently re-verified at other path-segment lengths. */
|
|
48
|
+
& .crisp-tabletoolbar-fcond-path { display: inline-flex; align-items: center; gap: 2px; min-width: 0; max-width: 178px; }
|
|
49
|
+
& .crisp-tabletoolbar-fcond-path-seg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
|
|
50
|
+
& .crisp-tabletoolbar-fcond-path-seg:first-child { flex: 0 1 auto; }
|
|
51
|
+
& .crisp-tabletoolbar-fcond-path-sep { flex-shrink: 0; color: inherit; }
|
|
52
|
+
& .crisp-tabletoolbar-fcond-seg--op { background: ${c.bgSurface}; color: ${c.fgMuted}; }
|
|
53
|
+
& .crisp-tabletoolbar-fcond-seg--val { font-weight: ${c.weight.medium}; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
|
|
54
|
+
& .crisp-tabletoolbar-fcond-seg--kebab { background: ${c.bgSurface}; justify-content: center; width: 28px; padding: 0; border-radius: 0 ${c.radius.md} ${c.radius.md} 0; color: ${c.fgMuted}; }
|
|
55
|
+
& .crisp-tabletoolbar-fcond-seg--kebab:hover { color: ${c.fgPrimary}; }
|
|
56
|
+
& .crisp-tabletoolbar-menu--val { min-width: 134px; }
|
|
57
|
+
& .crisp-tabletoolbar-datepop { width: 300px; }
|
|
58
|
+
& .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${c.space["1"]}; margin-top: ${c.space["2"]}; padding: 0 ${c.space["1"]}; }
|
|
59
|
+
& .crisp-tabletoolbar-datepop-preset { height: ${c.size.chip}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.chip}; background: ${c.bgRaised}; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.sm}; cursor: pointer; }
|
|
60
|
+
& .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); }`;
|
|
61
|
+
// #595: the Import/Export trigger — measured live, `app.attio.com`'s own
|
|
62
|
+
// view bar: `28px` tall, `radius.md` (8px), background `bgSurfaceMuted`
|
|
63
|
+
// (`rgb(246,247,247)`, exact match) and `elevation.field`'s exact 3-layer
|
|
64
|
+
// shadow (`inset 0 0 0 1px transparent, 0 0 2px rgba(28,40,64,.18), 0 1px 3px
|
|
65
|
+
// rgba(0,0,0,.04)`) — a "raised chrome button" distinct from Sort/Filter/View
|
|
66
|
+
// settings' plain ringed `.tool` (no background, no elevation shadow), and
|
|
67
|
+
// the only trigger of the four with a trailing chevron (2 icons total,
|
|
68
|
+
// confirmed live). Overrides `.tool`'s own base background/shadow by cascade
|
|
69
|
+
// order (this block is appended after `toolbarChromeBody`, which defines
|
|
70
|
+
// them), the same way `.tool--ghost` already does. `border`/`font-family`/
|
|
71
|
+
// `cursor` are repeated here too, even though `.tool`'s own base rule (in
|
|
72
|
+
// the shared `tabletoolbar.chrome.ts`) already sets them on every button
|
|
73
|
+
// carrying this modifier — `spec-check.mjs`'s button-reset audit only reads
|
|
74
|
+
// each component's own `<name>.recipe.ts` file verbatim, so a reset living
|
|
75
|
+
// behind `chrome.ts`'s function call is invisible to it; harmless repeats,
|
|
76
|
+
// not a real gap.
|
|
77
|
+
const importExportChrome = `
|
|
78
|
+
& .crisp-tabletoolbar-tool--raised { border: none; background: ${c.bgSurfaceMuted}; box-shadow: ${e.field}; font-family: inherit; cursor: pointer; }`;
|
|
4
79
|
// The table toolbar, measured: 28px controls, the Sort/Filter buttons and
|
|
5
80
|
// the menus they open, sort-condition rows, and the filter chips. The chrome
|
|
6
81
|
// itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
|
|
@@ -17,6 +92,9 @@ const config = defineRecipe({
|
|
|
17
92
|
& * { box-sizing: border-box; }
|
|
18
93
|
${toolbarChromeHead("tabletoolbar")}
|
|
19
94
|
${toolbarChromeBody("tabletoolbar")}
|
|
95
|
+
${filterChipChrome}
|
|
96
|
+
${importExportChrome}
|
|
97
|
+
${rtlFlipCss}
|
|
20
98
|
`,
|
|
21
99
|
});
|
|
22
100
|
export const tabletoolbarRecipe = resolveRecipe(config);
|
|
@@ -11,7 +11,16 @@ import { contract as c } from "../../tokens/index.js";
|
|
|
11
11
|
// inactive tabs are muted and fill to a subtle pill on hover. Each tab carries
|
|
12
12
|
// a 16px leading icon and an optional count badge. Measured from the live
|
|
13
13
|
// app: bar `padding: 0 12px 8px`, 28px pills, 4/8 padding, 6px icon-label
|
|
14
|
-
// gap, 14/500 labels
|
|
14
|
+
// gap, 14/500 labels. The count badge (crisp#697, re-measured against a
|
|
15
|
+
// record's tab counts and a settings "Attributes 35" count) is 14px
|
|
16
|
+
// tall/wide-min, radius 4px (no scale token lands there -- a recipe-local
|
|
17
|
+
// literal, same precedent as this file's own icon/gap literals), 10/500/14
|
|
18
|
+
// text on a 4% `fgPrimary` tint (`rgba(0,0,0,.04)`) with a measured
|
|
19
|
+
// `inset 0 0 0 1px borderSubtle` ring, text `fgNeutralSoft` (rgb(80,81,85),
|
|
20
|
+
// the same token Badge's neutral tone uses, #696) -- was 16px/r6/11px/
|
|
21
|
+
// `fgMuted`/6% tint with no ring at all. The selected tab's own badge
|
|
22
|
+
// colour (`fgPrimary`) wasn't measured (the live Overview tab carries no
|
|
23
|
+
// count) and stays as-is.
|
|
15
24
|
//
|
|
16
25
|
// The panel's own `padding-top` (`c.space["8"]`, 32px) is measured
|
|
17
26
|
// separately, live, on a company record page (Overview tab): the gap from
|
|
@@ -42,7 +51,7 @@ const config = defineRecipe({
|
|
|
42
51
|
& .crisp-tabs-tab:hover:not([data-selected]) { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgPrimary}; }
|
|
43
52
|
& .crisp-tabs-tab[data-selected] { color: ${c.fgPrimary}; background: ${c.bgSubtle}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; }
|
|
44
53
|
& .crisp-tabs-tab:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 1px; }
|
|
45
|
-
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width:
|
|
54
|
+
& .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 14px; height: 14px; padding: 0 ${c.space["1"]}; border-radius: 4px; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); box-shadow: inset 0 0 0 1px ${c.borderSubtle}; font-size: 10px; line-height: 14px; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft}; }
|
|
46
55
|
& .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${c.fgPrimary}; }
|
|
47
56
|
& .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: ${c.fgPrimary}; border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
|
|
48
57
|
& .crisp-tabs-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; padding-top: ${c.space["8"]}; }
|
|
@@ -67,12 +67,24 @@ style: _style, ...rest }, forwardedRef) => {
|
|
|
67
67
|
if (!el)
|
|
68
68
|
return;
|
|
69
69
|
const r = el.getBoundingClientRect();
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
const placement = side === "right"
|
|
71
|
+
? "right"
|
|
72
|
+
: side === "bottom" || r.top < 48
|
|
73
|
+
? "bottom"
|
|
74
|
+
: "top";
|
|
75
|
+
setPos(placement === "right"
|
|
76
|
+
? {
|
|
77
|
+
// Measured (crisp#633): the bubble's left edge sits 8px past
|
|
78
|
+
// the trigger's right edge, vertically centred on it.
|
|
79
|
+
left: Math.round(r.right + 8),
|
|
80
|
+
top: Math.round(r.top + r.height / 2),
|
|
81
|
+
placement,
|
|
82
|
+
}
|
|
83
|
+
: {
|
|
84
|
+
left: Math.round(r.left + r.width / 2),
|
|
85
|
+
top: Math.round(placement === "bottom" ? r.bottom + 6 : r.top - 6),
|
|
86
|
+
placement,
|
|
87
|
+
});
|
|
76
88
|
}, [open, side]);
|
|
77
89
|
const openAfterDelay = () => {
|
|
78
90
|
clearTimeout(timer.current);
|
|
@@ -119,9 +131,11 @@ style: _style, ...rest }, forwardedRef) => {
|
|
|
119
131
|
createPortal(_jsx("div", { id: tipId, role: "tooltip", className: tooltipRecipe({ variant }), style: {
|
|
120
132
|
left: pos.left,
|
|
121
133
|
top: pos.top,
|
|
122
|
-
transform: pos.
|
|
123
|
-
? "translate(-50
|
|
124
|
-
:
|
|
134
|
+
transform: pos.placement === "right"
|
|
135
|
+
? "translate(0, -50%)"
|
|
136
|
+
: pos.placement === "bottom"
|
|
137
|
+
? "translate(-50%, 0)"
|
|
138
|
+
: "translate(-50%, -100%)",
|
|
125
139
|
}, children: content }), document.body);
|
|
126
140
|
if (asChild) {
|
|
127
141
|
// No wrapper at all: the child carries the hover/focus/Escape
|
|
@@ -13,14 +13,15 @@ import { contract as c } from "../../tokens/index.js";
|
|
|
13
13
|
// just the wrong pair.
|
|
14
14
|
//
|
|
15
15
|
// crisp#563: this `boxShadow` is Attio's PLAIN tooltip (a truncated header
|
|
16
|
-
// label, measured on "Twitter follower count")
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
16
|
+
// label, measured on "Twitter follower count"). Re-measured live and
|
|
17
|
+
// corroborated against a second sample (hovering the same field a second
|
|
18
|
+
// time) per AUTHORING.md Lesson 22 — the value below is an inset ring plus
|
|
19
|
+
// two offset shadows, not the single offset shadow this recipe carried
|
|
20
|
+
// before. Attio has a SECOND, visually similar but structurally different
|
|
21
|
+
// tooltip for a reference-column header (icon + the referenced type's name
|
|
22
|
+
// after the label, crisp#580) with its own measured shadow, `variant:
|
|
23
|
+
// "reference"` below. Background, text colour, radius, padding and font are
|
|
24
|
+
// identical between the two measured samples; only the shadow differs.
|
|
24
25
|
const spec = defineSpec({
|
|
25
26
|
name: "tooltip",
|
|
26
27
|
base: {
|
|
@@ -37,7 +38,7 @@ const spec = defineSpec({
|
|
|
37
38
|
fontSize: c.text.sm,
|
|
38
39
|
fontWeight: c.weight.medium,
|
|
39
40
|
lineHeight: "1.45",
|
|
40
|
-
boxShadow: "0
|
|
41
|
+
boxShadow: "rgb(33,34,36) 0 0 0 1px inset, rgba(0,0,0,0.04) 0 0 2px 0, rgba(0,0,0,0.12) 0 1px 3px 0",
|
|
41
42
|
opacity: 1,
|
|
42
43
|
transition: "opacity 0.12s ease",
|
|
43
44
|
},
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { isValidElement } from "react";
|
|
2
3
|
import { authshellRecipe } from "./authshell.recipe.js";
|
|
4
|
+
/** A plain `{ icon, title }` object (not a React element) — the shape
|
|
5
|
+
* `logo` renders as `AuthShell`'s own icon-badge + wordmark layout, as
|
|
6
|
+
* opposed to a caller-supplied node it just centers as-is. Same test
|
|
7
|
+
* `sidebar.tsx`'s `isSidebarFooterCopy` already uses for its own "plain
|
|
8
|
+
* shape vs. any node" prop. */
|
|
9
|
+
function isAuthShellBrand(value) {
|
|
10
|
+
return (typeof value === "object" &&
|
|
11
|
+
value !== null &&
|
|
12
|
+
!isValidElement(value) &&
|
|
13
|
+
"icon" in value &&
|
|
14
|
+
"title" in value);
|
|
15
|
+
}
|
|
3
16
|
/**
|
|
4
17
|
* The welcome/auth layout — measured live.
|
|
5
18
|
* A centered page frame: an optional top progress bar, a logo, a two-pane
|
|
@@ -13,6 +26,6 @@ export function AuthShell({ progress, logo, title, children, preview, footer, cl
|
|
|
13
26
|
const pct = hasProgress
|
|
14
27
|
? Math.round(Math.max(0, Math.min(1, progress)) * 100)
|
|
15
28
|
: 0;
|
|
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 })] }));
|
|
29
|
+
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: isAuthShellBrand(logo) ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "crisp-authshell-logo-badge", children: logo.icon }), _jsxs("div", { className: "crisp-authshell-logo-text", children: [_jsx("div", { className: "crisp-authshell-logo-title", children: logo.title }), logo.subtitle != null && (_jsx("div", { className: "crisp-authshell-logo-subtitle", children: logo.subtitle }))] })] })) : (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
30
|
}
|
|
18
31
|
AuthShell.displayName = "AuthShell";
|
|
@@ -20,6 +20,14 @@ import { contract as c } from "../../tokens/index.js";
|
|
|
20
20
|
// roughly centred in the viewport rather than pinned near the top.
|
|
21
21
|
// `data-single-pane` now drops the card chrome and narrows the body to
|
|
22
22
|
// match; the two-pane card is unchanged.
|
|
23
|
+
//
|
|
24
|
+
// your-moon/crisp#722: `.crisp-authshell-logo` is `min-height: 24px`, not
|
|
25
|
+
// `height` -- Attio's own mark (a single flat 96x24 SVG, measured live on
|
|
26
|
+
// the logged-out `/login` page) fits exactly at 24px, but a fixed height
|
|
27
|
+
// clipped any consumer logo taller than that. Found via internal/grc's real
|
|
28
|
+
// lockup (an icon badge beside a two-line TECH/tagline wordmark, ~36px
|
|
29
|
+
// tall), silently cut down to a bare one-line span rather than reported as
|
|
30
|
+
// not fitting.
|
|
23
31
|
const config = defineRecipe({
|
|
24
32
|
name: "authshell",
|
|
25
33
|
base: `min-height: 100%; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; position: relative;
|
|
@@ -31,7 +39,11 @@ const config = defineRecipe({
|
|
|
31
39
|
& .crisp-authshell-progress { position: absolute; top: 0; left: 0; width: 100%; height: 2px; background: ${c.borderDefault}; }
|
|
32
40
|
& .crisp-authshell-progress-fill { height: 2px; background: ${c.fgPrimary}; border-radius: 4px; transition: width .2s; }
|
|
33
41
|
|
|
34
|
-
& .crisp-authshell-logo { margin-top: ${c.space["8"]}; height: 24px; display: flex; align-items: center; }
|
|
42
|
+
& .crisp-authshell-logo { margin-top: ${c.space["8"]}; min-height: 24px; display: flex; align-items: center; gap: ${c.space["2.5"]}; }
|
|
43
|
+
& .crisp-authshell-logo-badge { flex-shrink: 0; box-sizing: border-box; width: 32px; height: 32px; border-radius: ${c.radius.md}; background: ${c.bgBrandSolid}; color: #fff; display: flex; align-items: center; justify-content: center; }
|
|
44
|
+
& .crisp-authshell-logo-text { display: flex; flex-direction: column; line-height: 1.2; }
|
|
45
|
+
& .crisp-authshell-logo-title { font-size: ${c.text.xl}; font-weight: ${c.weight.bold}; letter-spacing: -0.2px; color: ${c.fgPrimary}; }
|
|
46
|
+
& .crisp-authshell-logo-subtitle { font-size: ${c.text.sm}; color: ${c.fgMuted}; }
|
|
35
47
|
|
|
36
48
|
& .crisp-authshell-card { margin-top: ${c.space["6"]}; display: flex; height: 692px; border-radius: 20px; border: 1px solid ${c.borderSubtle}; overflow: hidden; }
|
|
37
49
|
&[data-single-pane] .crisp-authshell-card { height: auto; border: none; border-radius: 0; overflow: visible; }
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { AuthShell } from "./authshell.js";
|
|
1
|
+
export { AuthShell, } from "./authshell.js";
|
|
2
2
|
export { authshellRecipe } from "./authshell.recipe.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Check } from "lucide-react";
|
|
2
|
+
import { Check, Columns2, Columns3, Plus, Rows3, Square } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Menu, MenuItem } from "../../components/menu/index.js";
|
|
5
|
+
import { useRadioGroupKeyboard } from "../../components/radiogroup/index.js";
|
|
5
6
|
import { isRtlElement, useCrispMessage, useCrispMessages } from "../../core/index.js";
|
|
6
7
|
import { boardRecipe } from "./board.recipe.js";
|
|
7
8
|
const BOARD_AGG_ORDER = [
|
|
@@ -55,16 +56,50 @@ function computeBoardAgg(agg, values) {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
/**
|
|
58
|
-
* A kanban board with
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
59
|
+
* A kanban board with pointer-events drag-and-drop between columns (#674 —
|
|
60
|
+
* Attio's own mechanism, confirmed live: mousedown, several mousemoves,
|
|
61
|
+
* mouseup, no `[draggable]` anywhere). Dropping a card on a column reparents
|
|
62
|
+
* it via `onCardsChange` (the parent owns the cards, so the move is a
|
|
63
|
+
* controlled state update). Measured from a deals pipeline.
|
|
62
64
|
*/
|
|
63
|
-
export function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLabel: newCardLabelProp, getCardLabel, announceMove: announceMoveProp, calc, className, ...rest }) {
|
|
65
|
+
export function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLabel: newCardLabelProp, getCardLabel, announceMove: announceMoveProp, calc, density = false, columnDensity: columnDensityProp, onColumnDensityChange, cardDensity: cardDensityProp, onCardDensityChange, collapseColumnsLabel: collapseColumnsLabelProp, expandColumnsLabel: expandColumnsLabelProp, collapseCardsLabel: collapseCardsLabelProp, expandCardsLabel: expandCardsLabelProp, columnDensityGroupLabel: columnDensityGroupLabelProp, cardDensityGroupLabel: cardDensityGroupLabelProp, onAddStage, addStageLabel: addStageLabelProp, className, ...rest }) {
|
|
64
66
|
// crisp/#471: same resolution order as every other component's own —
|
|
65
67
|
// explicit prop, then a CrispMessagesProvider's translation of the English
|
|
66
68
|
// default, then that default itself.
|
|
67
69
|
const newCardLabel = useCrispMessage(newCardLabelProp, "New");
|
|
70
|
+
const collapseColumnsLabel = useCrispMessage(collapseColumnsLabelProp, "Collapse columns");
|
|
71
|
+
const expandColumnsLabel = useCrispMessage(expandColumnsLabelProp, "Expand columns");
|
|
72
|
+
const collapseCardsLabel = useCrispMessage(collapseCardsLabelProp, "Collapse cards");
|
|
73
|
+
const expandCardsLabel = useCrispMessage(expandCardsLabelProp, "Expand cards");
|
|
74
|
+
const columnDensityGroupLabel = useCrispMessage(columnDensityGroupLabelProp, "Column density");
|
|
75
|
+
const cardDensityGroupLabel = useCrispMessage(cardDensityGroupLabelProp, "Card density");
|
|
76
|
+
const addStageLabel = useCrispMessage(addStageLabelProp, "Add stage");
|
|
77
|
+
// #718: same controlled/uncontrolled shape as `sort`/`selectedIds`
|
|
78
|
+
// elsewhere in crisp — explicit prop wins; otherwise this manages its own
|
|
79
|
+
// state, seeded "expand" (Attio's own board opens expanded by default).
|
|
80
|
+
const [internalColumnDensity, setInternalColumnDensity] = React.useState("expand");
|
|
81
|
+
const [internalCardDensity, setInternalCardDensity] = React.useState("expand");
|
|
82
|
+
const columnDensity = columnDensityProp ?? internalColumnDensity;
|
|
83
|
+
const cardDensity = cardDensityProp ?? internalCardDensity;
|
|
84
|
+
const setColumnDensity = (next) => {
|
|
85
|
+
onColumnDensityChange?.(next);
|
|
86
|
+
if (columnDensityProp === undefined)
|
|
87
|
+
setInternalColumnDensity(next);
|
|
88
|
+
};
|
|
89
|
+
const setCardDensity = (next) => {
|
|
90
|
+
onCardDensityChange?.(next);
|
|
91
|
+
if (cardDensityProp === undefined)
|
|
92
|
+
setInternalCardDensity(next);
|
|
93
|
+
};
|
|
94
|
+
// crisp's shared radio-group keyboard model (#200) — the same
|
|
95
|
+
// ←/→/↑/↓/Home/End-with-roving-tabindex model `Segmented`/`RadioGroup`
|
|
96
|
+
// already use, not a bespoke per-button handler (see that hook's own
|
|
97
|
+
// comment on the bug a per-button closure caused once already). Two
|
|
98
|
+
// independent hook instances: each density pair is its own radio group.
|
|
99
|
+
const { onKeyDown: onColumnDensityKeyDown, groupRef: columnDensityGroupRef } = useRadioGroupKeyboard({
|
|
100
|
+
select: (v) => setColumnDensity(v),
|
|
101
|
+
});
|
|
102
|
+
const { onKeyDown: onCardDensityKeyDown, groupRef: cardDensityGroupRef } = useRadioGroupKeyboard({ select: (v) => setCardDensity(v) });
|
|
68
103
|
// #604: which BoardAgg each column's footer shows, keyed by column id.
|
|
69
104
|
// Unset (undefined) means the column still shows the "+ Add calculation"
|
|
70
105
|
// trigger, exactly like the table footer's own per-column state.
|
|
@@ -76,11 +111,37 @@ export function Board({ columns, cards, onCardsChange, renderCard, onNewCard, ne
|
|
|
76
111
|
: `${crispMessages["Moved to"] ?? "Moved to"} ${column}`);
|
|
77
112
|
const [dragId, setDragId] = React.useState(null);
|
|
78
113
|
const [overCol, setOverCol] = React.useState(null);
|
|
114
|
+
const [ghost, setGhost] = React.useState(null);
|
|
79
115
|
const [announcement, setAnnouncement] = React.useState("");
|
|
80
116
|
// the card a keyboard move just sent to another column; React unmounts and
|
|
81
117
|
// remounts it under the new parent, so focus has to be put back by hand
|
|
82
118
|
const [refocusId, setRefocusId] = React.useState(null);
|
|
83
119
|
const rootRef = React.useRef(null);
|
|
120
|
+
const colRefs = React.useRef(new Map());
|
|
121
|
+
// #674: which column a client point is over, read off each column's own
|
|
122
|
+
// rect at drag time rather than `elementFromPoint` -- the fixed-position
|
|
123
|
+
// ghost sits on top of everything (deliberately, so it never itself
|
|
124
|
+
// becomes the hit) and this is the one MEASURED interaction a test has a
|
|
125
|
+
// real handle on (`elementFromPoint` needs an actual layout engine bun
|
|
126
|
+
// test's happy-dom doesn't run).
|
|
127
|
+
//
|
|
128
|
+
// X only, not a full box: `.crisp-board`'s columns are a single
|
|
129
|
+
// non-wrapping row (`display: flex`, no `flex-wrap`), so X alone already
|
|
130
|
+
// identifies the column unambiguously. Checking Y against each column's
|
|
131
|
+
// OWN rect looked more precise but was actually a bug caught driving this
|
|
132
|
+
// in a real headed browser (Playwright), not by the tests passing --
|
|
133
|
+
// `align-items: flex-start` on the row means a short/empty column's own
|
|
134
|
+
// box ends where its content does, well short of a taller neighbour's, so
|
|
135
|
+
// a drop below that point silently found no column at all even while
|
|
136
|
+
// visually still inside the board.
|
|
137
|
+
const columnAt = (x) => {
|
|
138
|
+
for (const [id, el] of colRefs.current) {
|
|
139
|
+
const r = el.getBoundingClientRect();
|
|
140
|
+
if (x >= r.left && x <= r.right)
|
|
141
|
+
return id;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
};
|
|
84
145
|
const move = (cardId, toColumn) => {
|
|
85
146
|
const card = cards.find((c) => c.id === cardId);
|
|
86
147
|
if (!card || card.columnId === toColumn)
|
|
@@ -96,8 +157,77 @@ export function Board({ columns, cards, onCardsChange, renderCard, onNewCard, ne
|
|
|
96
157
|
setRefocusId(null);
|
|
97
158
|
}, [refocusId]);
|
|
98
159
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
160
|
+
* #674: Attio's own board drag is driven by pointer events (mousedown ->
|
|
161
|
+
* several mousemoves -> mouseup) -- confirmed live, no `[draggable]`
|
|
162
|
+
* anywhere -- not native HTML5 `draggable`/`onDragStart`, which is
|
|
163
|
+
* pointer-only by specification (no touch/pen support) and fires its own
|
|
164
|
+
* separate, browser-chrome-owned drag session crisp never controlled: no
|
|
165
|
+
* custom drag image, no way to redraw the dragged card while it moves.
|
|
166
|
+
* Pointer events give the board its own tracked drag state instead, the
|
|
167
|
+
* same primitive `resizeHandle`/`recordstable`'s own column-resize already
|
|
168
|
+
* use elsewhere in crisp.
|
|
169
|
+
*
|
|
170
|
+
* A drag doesn't "start" the instant the pointer goes down on a card --
|
|
171
|
+
* only once it has moved past a small threshold (4px). Below that, the
|
|
172
|
+
* gesture reads as a click/tap that never happened here (the card carries
|
|
173
|
+
* no click handler of its own today, but a future one would otherwise
|
|
174
|
+
* fire through a phantom near-zero-distance "drag").
|
|
175
|
+
*/
|
|
176
|
+
const dragStateRef = React.useRef(null);
|
|
177
|
+
const DRAG_THRESHOLD = 4;
|
|
178
|
+
const onCardPointerDown = (e, card) => {
|
|
179
|
+
if (!e.isPrimary)
|
|
180
|
+
return;
|
|
181
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
182
|
+
dragStateRef.current = {
|
|
183
|
+
id: card.id,
|
|
184
|
+
pointerId: e.pointerId,
|
|
185
|
+
startX: e.clientX,
|
|
186
|
+
startY: e.clientY,
|
|
187
|
+
active: false,
|
|
188
|
+
grabDx: e.clientX - r.left,
|
|
189
|
+
grabDy: e.clientY - r.top,
|
|
190
|
+
width: r.width,
|
|
191
|
+
height: r.height,
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const onCardPointerMove = (e) => {
|
|
195
|
+
const st = dragStateRef.current;
|
|
196
|
+
if (!st || st.pointerId !== e.pointerId)
|
|
197
|
+
return;
|
|
198
|
+
if (!st.active) {
|
|
199
|
+
const dx = e.clientX - st.startX;
|
|
200
|
+
const dy = e.clientY - st.startY;
|
|
201
|
+
if (Math.hypot(dx, dy) < DRAG_THRESHOLD)
|
|
202
|
+
return;
|
|
203
|
+
st.active = true;
|
|
204
|
+
setDragId(st.id);
|
|
205
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
206
|
+
}
|
|
207
|
+
setGhost({
|
|
208
|
+
x: e.clientX - st.grabDx,
|
|
209
|
+
y: e.clientY - st.grabDy,
|
|
210
|
+
width: st.width,
|
|
211
|
+
height: st.height,
|
|
212
|
+
});
|
|
213
|
+
setOverCol(columnAt(e.clientX));
|
|
214
|
+
};
|
|
215
|
+
const endCardDrag = (e, commit) => {
|
|
216
|
+
const st = dragStateRef.current;
|
|
217
|
+
if (!st || st.pointerId !== e.pointerId)
|
|
218
|
+
return;
|
|
219
|
+
if (commit && st.active && overCol)
|
|
220
|
+
move(st.id, overCol);
|
|
221
|
+
dragStateRef.current = null;
|
|
222
|
+
setDragId(null);
|
|
223
|
+
setOverCol(null);
|
|
224
|
+
setGhost(null);
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Neither drag mechanism the board has used (the old native HTML5 kind,
|
|
228
|
+
* this file's own pointer-events one now) is reachable from a keyboard, so
|
|
229
|
+
* the board's primary action needs its own key model or it has none at
|
|
230
|
+
* all.
|
|
101
231
|
*
|
|
102
232
|
* Ctrl/Cmd + Left/Right rather than plain arrows: a screen reader owns the
|
|
103
233
|
* bare arrow keys in browse mode, and a board scrolls horizontally, so plain
|
|
@@ -113,44 +243,43 @@ export function Board({ columns, cards, onCardsChange, renderCard, onNewCard, ne
|
|
|
113
243
|
setRefocusId(card.id);
|
|
114
244
|
return true;
|
|
115
245
|
};
|
|
116
|
-
return (_jsxs("div", { ref: rootRef, className: boardRecipe({ className }), ...rest, children: [columns.map((col) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}), _jsx("div", { className: "crisp-board-live", role: "status", "aria-live": "polite", children: announcement })] }));
|
|
246
|
+
return (_jsxs("div", { ref: rootRef, className: boardRecipe({ className }), ...rest, children: [density && (_jsxs("div", { className: "crisp-board-toolbar", children: [_jsxs("div", { ref: columnDensityGroupRef, className: "crisp-board-density", role: "radiogroup", "aria-label": columnDensityGroupLabel, onKeyDown: onColumnDensityKeyDown, children: [_jsx("button", { type: "button", role: "radio", "data-value": "collapse", "aria-checked": columnDensity === "collapse", "aria-label": collapseColumnsLabel, "data-state": columnDensity === "collapse" ? "on" : "off", onClick: () => setColumnDensity("collapse"), children: _jsx(Columns3, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx("button", { type: "button", role: "radio", "data-value": "expand", "aria-checked": columnDensity === "expand", "aria-label": expandColumnsLabel, "data-state": columnDensity === "expand" ? "on" : "off", onClick: () => setColumnDensity("expand"), children: _jsx(Columns2, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), _jsxs("div", { ref: cardDensityGroupRef, className: "crisp-board-density", role: "radiogroup", "aria-label": cardDensityGroupLabel, onKeyDown: onCardDensityKeyDown, children: [_jsx("button", { type: "button", role: "radio", "data-value": "collapse", "aria-checked": cardDensity === "collapse", "aria-label": collapseCardsLabel, "data-state": cardDensity === "collapse" ? "on" : "off", onClick: () => setCardDensity("collapse"), children: _jsx(Rows3, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx("button", { type: "button", role: "radio", "data-value": "expand", "aria-checked": cardDensity === "expand", "aria-label": expandCardsLabel, "data-state": cardDensity === "expand" ? "on" : "off", onClick: () => setCardDensity("expand"), children: _jsx(Square, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] })] })), _jsxs("div", { className: "crisp-board-row", children: [columns.map((col) => {
|
|
247
|
+
const colCards = cards.filter((c) => c.columnId === col.id);
|
|
248
|
+
return (_jsxs("div", { ref: (el) => {
|
|
249
|
+
if (el)
|
|
250
|
+
colRefs.current.set(col.id, el);
|
|
251
|
+
else
|
|
252
|
+
colRefs.current.delete(col.id);
|
|
253
|
+
}, className: "crisp-board-col", "data-over": overCol === col.id ? "" : undefined, "data-density": columnDensity === "collapse" ? "collapse" : undefined, children: [_jsxs("div", { className: "crisp-board-col-header", children: [col.accent && (_jsx("span", { className: "crisp-board-col-dot", style: { background: col.accent } })), _jsx("span", { className: "crisp-board-col-title", children: col.title }), _jsx("span", { className: "crisp-board-col-count", children: colCards.length }), onNewCard && columnDensity !== "collapse" && (_jsx("button", { type: "button", className: "crisp-board-col-add", "aria-label": `${crispMessages["Add to"] ?? "Add to"} ${col.title}`, onClick: () => onNewCard(col.id), children: "+" }))] }), _jsxs("div", { className: "crisp-board-col-body", role: "list", "aria-label": col.title, children: [onNewCard && (_jsxs("button", { type: "button", className: "crisp-board-newcard", onClick: () => onNewCard(col.id), children: ["+ ", newCardLabel] })), colCards.map((card) => (_jsx("div", { className: "crisp-board-card", role: "listitem",
|
|
254
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: the rule assumes a listitem cannot be operated. This one carries the board's only keyboard path to move a card, so it has to be reachable by Tab
|
|
255
|
+
tabIndex: 0, "aria-label": getCardLabel?.(card), "data-card-id": card.id, "data-dragging": dragId === card.id ? "" : undefined, "data-density": cardDensity === "collapse" ? "collapse" : undefined, onPointerDown: (e) => onCardPointerDown(e, card), onPointerMove: onCardPointerMove, onPointerUp: (e) => endCardDrag(e, true), onPointerCancel: (e) => endCardDrag(e, false), onKeyDown: (e) => {
|
|
256
|
+
if (!(e.ctrlKey || e.metaKey))
|
|
257
|
+
return;
|
|
258
|
+
// the columns are a flex row, so under RTL the next one
|
|
259
|
+
// is to the LEFT and the keys swap (#255)
|
|
260
|
+
const rtl = isRtlElement(e.currentTarget);
|
|
261
|
+
const forwardKey = rtl ? "ArrowLeft" : "ArrowRight";
|
|
262
|
+
const backKey = rtl ? "ArrowRight" : "ArrowLeft";
|
|
263
|
+
const delta = e.key === backKey ? -1 : e.key === forwardKey ? 1 : 0;
|
|
264
|
+
// only swallow the key when the move actually happens, so
|
|
265
|
+
// Ctrl+Right at the last column still reaches the browser
|
|
266
|
+
if (delta && moveByKey(card, delta))
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
}, children: renderCard(card, cardDensity) }, card.id)))] }), calc && (_jsx("div", { className: "crisp-board-col-footer", children: _jsx(Menu, { align: "start", trigger: _jsx("button", { type: "button", className: "crisp-board-col-calc", "data-empty": colAgg[col.id] ? undefined : "", children: colAgg[col.id] ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-board-col-calc-label", children: crispMessages[AGG_LABEL[colAgg[col.id]]] ??
|
|
269
|
+
AGG_LABEL[colAgg[col.id]] }), _jsx("span", { className: "crisp-board-col-calc-value", children: computeBoardAgg(colAgg[col.id], colCards.map((c) => calc.getValue(c))) })] })) : ((crispMessages["+ Add calculation"] ??
|
|
270
|
+
"+ Add calculation")) }), children: BOARD_AGG_ORDER.map((a) => (_jsxs(MenuItem, { onSelect: () => setColAgg((prev) => ({ ...prev, [col.id]: a })), children: [crispMessages[AGG_LABEL[a]] ?? AGG_LABEL[a], colAgg[col.id] === a ? (_jsx(Check, { size: 15, strokeWidth: 1.75 })) : null] }, a))) }) }))] }, col.id));
|
|
271
|
+
}), onAddStage && (_jsx("button", { type: "button", className: "crisp-board-addstage", "aria-label": addStageLabel, onClick: onAddStage, children: _jsx(Plus, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }))] }), dragId &&
|
|
272
|
+
ghost &&
|
|
273
|
+
(() => {
|
|
274
|
+
const draggedCard = cards.find((c) => c.id === dragId);
|
|
275
|
+
if (!draggedCard)
|
|
276
|
+
return null;
|
|
277
|
+
return (_jsx("div", { className: "crisp-board-card crisp-board-ghost", style: {
|
|
278
|
+
left: ghost.x,
|
|
279
|
+
top: ghost.y,
|
|
280
|
+
width: ghost.width,
|
|
281
|
+
height: ghost.height,
|
|
282
|
+
}, children: renderCard(draggedCard, cardDensity) }));
|
|
283
|
+
})(), _jsx("div", { className: "crisp-board-live", role: "status", "aria-live": "assertive", children: announcement })] }));
|
|
155
284
|
}
|
|
156
285
|
Board.displayName = "Board";
|