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