@crisp-ui-kit/crisp 0.57.1 → 0.57.2

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 (252) hide show
  1. package/dist/authshell.css +9 -4
  2. package/dist/authshell.layered.css +9 -4
  3. package/dist/badge.css +3 -3
  4. package/dist/badge.layered.css +3 -3
  5. package/dist/board.css +28 -8
  6. package/dist/board.layered.css +28 -8
  7. package/dist/bottomsheet.css +26 -0
  8. package/dist/bottomsheet.d.ts +7 -0
  9. package/dist/bottomsheet.layered.css +28 -0
  10. package/dist/button.css +7 -6
  11. package/dist/button.layered.css +7 -6
  12. package/dist/checkbox.css +7 -3
  13. package/dist/checkbox.layered.css +7 -3
  14. package/dist/chip.css +15 -0
  15. package/dist/chip.d.ts +3 -1
  16. package/dist/chip.layered.css +15 -0
  17. package/dist/chipgroup.css +5 -0
  18. package/dist/chipgroup.d.ts +7 -0
  19. package/dist/chipgroup.layered.css +7 -0
  20. package/dist/cjs/components/badge/badge.recipe.js +22 -8
  21. package/dist/cjs/components/bottomsheet/bottomsheet.js +90 -0
  22. package/dist/cjs/components/bottomsheet/bottomsheet.recipe.js +77 -0
  23. package/dist/cjs/components/bottomsheet/index.js +9 -0
  24. package/dist/cjs/components/button/button.recipe.js +39 -15
  25. package/dist/cjs/components/checkbox/checkbox.recipe.js +61 -9
  26. package/dist/cjs/components/chip/chip.js +14 -2
  27. package/dist/cjs/components/chip/chip.recipe.js +69 -1
  28. package/dist/cjs/components/chipgroup/chipgroup.js +51 -0
  29. package/dist/cjs/components/chipgroup/chipgroup.recipe.js +36 -0
  30. package/dist/cjs/components/chipgroup/index.js +7 -0
  31. package/dist/cjs/components/datatable/datatable.js +5 -1
  32. package/dist/cjs/components/datatable/datatable.recipe.js +11 -2
  33. package/dist/cjs/components/dialog/dialog.recipe.js +21 -10
  34. package/dist/cjs/components/emptystate/emptystate.recipe.js +7 -2
  35. package/dist/cjs/components/fileupload/fileupload.js +21 -4
  36. package/dist/cjs/components/input/index.js +2 -1
  37. package/dist/cjs/components/input/input.js +31 -1
  38. package/dist/cjs/components/input/input.recipe.js +16 -9
  39. package/dist/cjs/components/menu/menu.recipe.js +21 -7
  40. package/dist/cjs/components/menusheet/index.js +9 -0
  41. package/dist/cjs/components/menusheet/menusheet.js +133 -0
  42. package/dist/cjs/components/menusheet/menusheet.recipe.js +103 -0
  43. package/dist/cjs/components/multiselect/multiselect.js +1 -1
  44. package/dist/cjs/components/multiselect/multiselect.recipe.js +33 -9
  45. package/dist/cjs/components/select/select.js +1 -1
  46. package/dist/cjs/components/select/select.recipe.js +38 -12
  47. package/dist/cjs/components/snackbar/index.js +8 -0
  48. package/dist/cjs/components/snackbar/snackbar.js +121 -0
  49. package/dist/cjs/components/snackbar/snackbar.recipe.js +71 -0
  50. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  51. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +37 -12
  52. package/dist/cjs/components/tabs/tabs.js +10 -4
  53. package/dist/cjs/components/tabs/tabs.recipe.js +23 -6
  54. package/dist/cjs/components/textarea/textarea.recipe.js +11 -8
  55. package/dist/cjs/index.js +7 -0
  56. package/dist/cjs/product/authshell/authshell.recipe.js +5 -1
  57. package/dist/cjs/product/command/command.recipe.js +4 -1
  58. package/dist/cjs/product/comments/comments.js +7 -2
  59. package/dist/cjs/product/comments/comments.recipe.js +25 -7
  60. package/dist/cjs/product/memberssettings/index.js +6 -0
  61. package/dist/cjs/product/memberssettings/memberssettings.js +99 -0
  62. package/dist/cjs/product/memberssettings/memberssettings.recipe.js +83 -0
  63. package/dist/cjs/product/notificationssettings/index.js +5 -0
  64. package/dist/cjs/product/notificationssettings/notificationssettings.js +19 -0
  65. package/dist/cjs/product/notificationssettings/notificationssettings.recipe.js +44 -0
  66. package/dist/cjs/product/recordpage/recordpage.js +57 -6
  67. package/dist/cjs/product/recordpage/recordpage.recipe.js +43 -6
  68. package/dist/cjs/product/workspacegeneralsettings/index.js +5 -0
  69. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.js +21 -0
  70. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +51 -0
  71. package/dist/cjs/tokens/contract.js +1 -0
  72. package/dist/cjs/tokens/elevation.js +8 -5
  73. package/dist/cjs/tokens/palette.js +28 -8
  74. package/dist/cjs/tokens/scale.js +44 -2
  75. package/dist/cjs/tokens/semantic.js +2 -0
  76. package/dist/columncalc.css +15 -7
  77. package/dist/columncalc.layered.css +15 -7
  78. package/dist/command.css +23 -19
  79. package/dist/command.layered.css +23 -19
  80. package/dist/comments.css +28 -19
  81. package/dist/comments.d.ts +2 -2
  82. package/dist/comments.layered.css +28 -19
  83. package/dist/datatable.css +161 -67
  84. package/dist/datatable.layered.css +161 -67
  85. package/dist/datepicker.css +13 -41
  86. package/dist/datepicker.layered.css +13 -41
  87. package/dist/datepickerfield.css +4 -1
  88. package/dist/datepickerfield.layered.css +4 -1
  89. package/dist/dialog.css +12 -7
  90. package/dist/dialog.layered.css +12 -7
  91. package/dist/emptystate.css +5 -3
  92. package/dist/emptystate.layered.css +5 -3
  93. package/dist/esm/components/badge/badge.recipe.js +22 -8
  94. package/dist/esm/components/bottomsheet/bottomsheet.js +52 -0
  95. package/dist/esm/components/bottomsheet/bottomsheet.recipe.js +74 -0
  96. package/dist/esm/components/bottomsheet/index.js +2 -0
  97. package/dist/esm/components/button/button.recipe.js +40 -16
  98. package/dist/esm/components/checkbox/checkbox.recipe.js +61 -9
  99. package/dist/esm/components/chip/chip.js +14 -2
  100. package/dist/esm/components/chip/chip.recipe.js +69 -1
  101. package/dist/esm/components/chipgroup/chipgroup.js +15 -0
  102. package/dist/esm/components/chipgroup/chipgroup.recipe.js +33 -0
  103. package/dist/esm/components/chipgroup/index.js +2 -0
  104. package/dist/esm/components/datatable/datatable.js +5 -1
  105. package/dist/esm/components/datatable/datatable.recipe.js +11 -2
  106. package/dist/esm/components/dialog/dialog.recipe.js +22 -11
  107. package/dist/esm/components/emptystate/emptystate.recipe.js +7 -2
  108. package/dist/esm/components/fileupload/fileupload.js +20 -5
  109. package/dist/esm/components/input/index.js +1 -1
  110. package/dist/esm/components/input/input.js +30 -0
  111. package/dist/esm/components/input/input.recipe.js +17 -10
  112. package/dist/esm/components/menu/menu.recipe.js +21 -7
  113. package/dist/esm/components/menusheet/index.js +2 -0
  114. package/dist/esm/components/menusheet/menusheet.js +95 -0
  115. package/dist/esm/components/menusheet/menusheet.recipe.js +100 -0
  116. package/dist/esm/components/multiselect/multiselect.js +1 -1
  117. package/dist/esm/components/multiselect/multiselect.recipe.js +33 -9
  118. package/dist/esm/components/select/select.js +1 -1
  119. package/dist/esm/components/select/select.recipe.js +38 -12
  120. package/dist/esm/components/snackbar/index.js +2 -0
  121. package/dist/esm/components/snackbar/snackbar.js +84 -0
  122. package/dist/esm/components/snackbar/snackbar.recipe.js +68 -0
  123. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  124. package/dist/esm/components/tabletoolbar/tabletoolbar.js +37 -12
  125. package/dist/esm/components/tabs/tabs.js +10 -4
  126. package/dist/esm/components/tabs/tabs.recipe.js +23 -6
  127. package/dist/esm/components/textarea/textarea.recipe.js +12 -9
  128. package/dist/esm/index.js +7 -0
  129. package/dist/esm/product/authshell/authshell.recipe.js +5 -1
  130. package/dist/esm/product/command/command.recipe.js +4 -1
  131. package/dist/esm/product/comments/comments.js +7 -2
  132. package/dist/esm/product/comments/comments.recipe.js +25 -7
  133. package/dist/esm/product/memberssettings/index.js +1 -0
  134. package/dist/esm/product/memberssettings/memberssettings.js +62 -0
  135. package/dist/esm/product/memberssettings/memberssettings.recipe.js +80 -0
  136. package/dist/esm/product/notificationssettings/index.js +1 -0
  137. package/dist/esm/product/notificationssettings/notificationssettings.js +16 -0
  138. package/dist/esm/product/notificationssettings/notificationssettings.recipe.js +41 -0
  139. package/dist/esm/product/recordpage/recordpage.js +58 -7
  140. package/dist/esm/product/recordpage/recordpage.recipe.js +43 -6
  141. package/dist/esm/product/workspacegeneralsettings/index.js +1 -0
  142. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.js +18 -0
  143. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +48 -0
  144. package/dist/esm/tokens/contract.js +2 -1
  145. package/dist/esm/tokens/elevation.js +8 -5
  146. package/dist/esm/tokens/palette.js +28 -8
  147. package/dist/esm/tokens/scale.js +43 -1
  148. package/dist/esm/tokens/semantic.js +2 -0
  149. package/dist/fileupload.css +3 -1
  150. package/dist/fileupload.layered.css +3 -1
  151. package/dist/input.css +6 -4
  152. package/dist/input.layered.css +6 -4
  153. package/dist/listpage.css +3 -1
  154. package/dist/listpage.layered.css +3 -1
  155. package/dist/memberssettings.css +67 -0
  156. package/dist/memberssettings.d.ts +7 -0
  157. package/dist/memberssettings.layered.css +69 -0
  158. package/dist/menu.css +2 -2
  159. package/dist/menu.layered.css +2 -2
  160. package/dist/menusheet.css +36 -0
  161. package/dist/menusheet.d.ts +7 -0
  162. package/dist/menusheet.layered.css +38 -0
  163. package/dist/multiselect.css +13 -6
  164. package/dist/multiselect.layered.css +13 -6
  165. package/dist/native/tokens.d.ts +2 -1
  166. package/dist/native/tokens.js +20 -3
  167. package/dist/notificationssettings.css +22 -0
  168. package/dist/notificationssettings.d.ts +7 -0
  169. package/dist/notificationssettings.layered.css +24 -0
  170. package/dist/objectsettings.css +4 -4
  171. package/dist/objectsettings.layered.css +4 -4
  172. package/dist/recordpage.css +67 -23
  173. package/dist/recordpage.layered.css +67 -23
  174. package/dist/recordpanel.css +35 -16
  175. package/dist/recordpanel.layered.css +35 -16
  176. package/dist/resizablepanel.css +8 -5
  177. package/dist/resizablepanel.layered.css +8 -5
  178. package/dist/richtext.css +24 -8
  179. package/dist/richtext.layered.css +24 -8
  180. package/dist/savedviews.css +9 -2
  181. package/dist/savedviews.layered.css +9 -2
  182. package/dist/select.css +25 -5
  183. package/dist/select.layered.css +25 -5
  184. package/dist/settingsshell.css +2 -3
  185. package/dist/settingsshell.layered.css +2 -3
  186. package/dist/slider.css +6 -27
  187. package/dist/slider.layered.css +6 -27
  188. package/dist/snackbar.css +24 -0
  189. package/dist/snackbar.d.ts +7 -0
  190. package/dist/snackbar.layered.css +26 -0
  191. package/dist/statcard.css +3 -1
  192. package/dist/statcard.layered.css +3 -1
  193. package/dist/styles.css +859 -345
  194. package/dist/styles.layered.css +859 -345
  195. package/dist/table.css +28 -10
  196. package/dist/table.layered.css +28 -10
  197. package/dist/tabletoolbar.css +35 -24
  198. package/dist/tabletoolbar.layered.css +35 -24
  199. package/dist/tabs.css +18 -4
  200. package/dist/tabs.layered.css +18 -4
  201. package/dist/textarea.css +2 -6
  202. package/dist/textarea.layered.css +2 -6
  203. package/dist/themepicker.css +2 -1
  204. package/dist/themepicker.layered.css +2 -1
  205. package/dist/tokens.css +13 -2
  206. package/dist/tokens.json +20 -3
  207. package/dist/types/components/bottomsheet/bottomsheet.d.ts +35 -0
  208. package/dist/types/components/bottomsheet/bottomsheet.recipe.d.ts +3 -0
  209. package/dist/types/components/bottomsheet/index.d.ts +2 -0
  210. package/dist/types/components/button/button.recipe.d.ts +11 -6
  211. package/dist/types/components/checkbox/checkbox.recipe.d.ts +28 -5
  212. package/dist/types/components/chip/chip.d.ts +22 -0
  213. package/dist/types/components/chip/chip.recipe.d.ts +48 -0
  214. package/dist/types/components/chipgroup/chipgroup.d.ts +14 -0
  215. package/dist/types/components/chipgroup/chipgroup.recipe.d.ts +5 -0
  216. package/dist/types/components/chipgroup/index.d.ts +2 -0
  217. package/dist/types/components/datatable/datatable.d.ts +19 -0
  218. package/dist/types/components/fileupload/fileupload.d.ts +26 -0
  219. package/dist/types/components/input/index.d.ts +1 -1
  220. package/dist/types/components/input/input.d.ts +72 -0
  221. package/dist/types/components/menusheet/index.d.ts +2 -0
  222. package/dist/types/components/menusheet/menusheet.d.ts +42 -0
  223. package/dist/types/components/menusheet/menusheet.recipe.d.ts +3 -0
  224. package/dist/types/components/snackbar/index.d.ts +2 -0
  225. package/dist/types/components/snackbar/snackbar.d.ts +36 -0
  226. package/dist/types/components/snackbar/snackbar.recipe.d.ts +5 -0
  227. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +28 -1
  228. package/dist/types/components/tabs/tabs.d.ts +11 -1
  229. package/dist/types/index.d.ts +7 -0
  230. package/dist/types/product/comments/comments.d.ts +9 -3
  231. package/dist/types/product/comments/comments.recipe.d.ts +12 -2
  232. package/dist/types/product/memberssettings/index.d.ts +1 -0
  233. package/dist/types/product/memberssettings/memberssettings.d.ts +85 -0
  234. package/dist/types/product/memberssettings/memberssettings.recipe.d.ts +3 -0
  235. package/dist/types/product/notificationssettings/index.d.ts +1 -0
  236. package/dist/types/product/notificationssettings/notificationssettings.d.ts +37 -0
  237. package/dist/types/product/notificationssettings/notificationssettings.recipe.d.ts +3 -0
  238. package/dist/types/product/recordpage/recordpage.d.ts +53 -2
  239. package/dist/types/product/workspacegeneralsettings/index.d.ts +1 -0
  240. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.d.ts +48 -0
  241. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.recipe.d.ts +3 -0
  242. package/dist/types/tokens/contract.d.ts +4 -1
  243. package/dist/types/tokens/elevation.d.ts +8 -5
  244. package/dist/types/tokens/palette.d.ts +9 -1
  245. package/dist/types/tokens/scale.d.ts +11 -1
  246. package/dist/types/tokens/semantic.d.ts +2 -0
  247. package/dist/wheelpicker.css +4 -2
  248. package/dist/wheelpicker.layered.css +4 -2
  249. package/dist/workspacegeneralsettings.css +30 -0
  250. package/dist/workspacegeneralsettings.d.ts +7 -0
  251. package/dist/workspacegeneralsettings.layered.css +32 -0
  252. package/package.json +36 -1
@@ -16,10 +16,14 @@ const tokens_1 = require("../../tokens");
16
16
  // styling (shared with RecordsTable/DataTable elsewhere), the checkbox-style
17
17
  // selected indicator, and the toggle-without-closing interaction.
18
18
  //
19
- // The search row's geometry (36px height, 6px gap, no divider) is an
20
- // Attio measurement (#481) `build/spec-check.mjs` guards directly -- it is
21
- // NOT part of the seed-parity change and must not move. Only its text/icon
22
- // colours follow the rest of this file onto the shared tokens.
19
+ // The search row now follows seed too (owner decision, 2026-09-26: Select
20
+ // AND MultiSelect both follow seed-design, superseding #481's old Attio
21
+ // lock on this row's geometry -- crisp/#904's MultiSelect follow-up). It
22
+ // reads the exact same composed @seed-design/rootage `text-input.yaml`
23
+ // (variant=outline,size=medium) values #913 resolved for Select's own
24
+ // search row -- see that recipe's matching comment for the full citation.
25
+ // `build/spec-check.mjs`'s guard was rewritten to assert these seed values
26
+ // instead of the old 36px Attio measurement.
23
27
  //
24
28
  // The listbox portals to `document.body` (#339), so its rules live in
25
29
  // `globalCss`, not nested under `&` -- see Select's own recipe for the full
@@ -64,13 +68,19 @@ const config = (0, core_1.defineRecipe)({
64
68
  }
65
69
  .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
66
70
  .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
67
- /* LOCKED geometry (#481, guarded by build/spec-check.mjs): height 36px,
68
- gap space["1.5"] (6px), no border-bottom -- an Attio measurement, not
69
- part of this seed-parity pass. Only colours below follow Select. */
70
- .crisp-multiselect-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 36px; flex-shrink: 0; margin: calc(-1 * ${tokens_1.contract.space["2"]}) calc(-1 * ${tokens_1.contract.space["2"]}) ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; color: ${tokens_1.contract.fgNeutralMuted}; }
71
+ /* Seed geometry (build/spec-check.mjs guards these four properties
72
+ directly), superseding #481's old Attio lock -- see this file's
73
+ header comment and Select's matching .crisp-select-search comment
74
+ for the full citation. An ordinary flex child of the listbox, so its
75
+ own 8px gap provides the inset/seam on every side, same as Select. */
76
+ .crisp-multiselect-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.selectTriggerMd}; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3.5"]}; border: 1px solid ${tokens_1.contract.strokeSelectTrigger}; border-radius: ${tokens_1.contract.radius.md}; color: ${tokens_1.contract.fgNeutralMuted}; }
71
77
  .crisp-multiselect-search svg { flex-shrink: 0; }
72
- .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgSelectNeutral}; }
78
+ .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; line-height: 19px; color: ${tokens_1.contract.fgSelectNeutral}; }
73
79
  .crisp-multiselect-search input::placeholder { color: ${tokens_1.contract.fgSelectPlaceholder}; }
80
+ /* Same header/body seam as Select's own .crisp-select-divider -- see
81
+ that recipe's comment for the seed source (select.yaml's divider
82
+ slot, stroke.neutral-muted). */
83
+ .crisp-multiselect-divider { flex-shrink: 0; height: 1px; margin-inline: ${tokens_1.contract.space["4"]}; background: ${tokens_1.contract.strokeSelectDivider}; }
74
84
  /* Same groupLabel treatment as Select's (13px/regular/neutral-subtle). */
75
85
  .crisp-multiselect-label { padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["4"]} 0; font-size: ${tokens_1.contract.text.md}; line-height: 18px; font-weight: ${tokens_1.contract.weight.regular}; color: ${tokens_1.contract.fgNeutralSubtle}; }
76
86
  .crisp-multiselect-option {
@@ -106,6 +116,20 @@ const config = (0, core_1.defineRecipe)({
106
116
  // default: `c.size.menuRow` (32px) and the original, pre-#882
107
117
  // Attio-measured asymmetric padding for a chip-wrapping trigger.
108
118
  // `medium`/`large` (seed's own 40/52px) stay, opt-in.
119
+ //
120
+ // crisp/#904 (MultiSelect follow-up, briefly moved then reverted, same
121
+ // ruling as Select's own): `small`'s `gap` moved from `space["1"]`
122
+ // (4px) to `space["2"]` (8px), matching `medium`/`large`'s seed gap --
123
+ // team-lead's review ruling on Select's matching change reversed it:
124
+ // padding AND gap are SPACING, and the owner's direction is "sizes/
125
+ // spacing from Attio, look from seed", not seed's literal gap number.
126
+ // Back to `space["1"]` (4px), the original pre-#882 Attio-measured
127
+ // value. `padding` was never touched either way: the pre-#882,
128
+ // Attio-measured asymmetric padding a chip-wrapping trigger needs
129
+ // (chips sit close to the start edge, the chevron gets more room at
130
+ // the end) -- seed has no multi-value combobox of its own to source a
131
+ // replacement number from (see this file's header comment) even if
132
+ // the ruling had gone the other way.
109
133
  size: {
110
134
  small: `
111
135
  & .crisp-multiselect-trigger { min-height: ${tokens_1.contract.size.menuRow}; border-radius: ${tokens_1.contract.radius.md}; gap: ${tokens_1.contract.space["1"]}; padding: ${tokens_1.contract.space["0.5"]} ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["0.5"]} ${tokens_1.contract.space["2"]}; }
@@ -221,7 +221,7 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
221
221
  setQuery(e.target.value);
222
222
  setHighlight(0);
223
223
  onSearchChange?.(e.target.value);
224
- }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
224
+ }, onKeyDown: onNavKeys })] })), searchable && (0, jsx_runtime_1.jsx)("div", { className: "crisp-select-divider" }), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
225
225
  const isSel = opt.value === selected;
226
226
  // #343: a header renders once per run of same-`group`
227
227
  // options, so consecutive rows sharing a group render one
@@ -29,11 +29,11 @@ const tokens_1 = require("../../tokens");
29
29
  // pattern with no precedent anywhere else in the repo, for one component.
30
30
  // The enter animation (scale 0.95→1, opacity 0→1, seed's own curve) IS
31
31
  // reproduced below.
32
- // - A visible `divider` slot: `select.yaml` defines one, but nothing in
33
- // crisp or GRC groups options with an explicit separator today, and
34
- // AUTHORING's rule against building what nothing consumes applies to CSS
35
- // too. The resolved value is in `~/grc-parity/seed-select-spec.md` for
36
- // whoever adds it.
32
+ // - A general-purpose `divider` slot between arbitrary option groups:
33
+ // nothing in crisp or GRC groups options with an explicit separator
34
+ // today, and AUTHORING's rule against building what nothing consumes
35
+ // applies to CSS too. (`select.yaml`'s resolved value IS used below, just
36
+ // for one specific seam — see `.crisp-select-divider`'s own comment.)
37
37
  const config = (0, core_1.defineRecipe)({
38
38
  name: "select",
39
39
  base: `
@@ -102,6 +102,20 @@ const config = (0, core_1.defineRecipe)({
102
102
  .crisp-select-search svg { flex-shrink: 0; }
103
103
  .crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; line-height: 19px; color: ${tokens_1.contract.fgSelectNeutral}; }
104
104
  .crisp-select-search input::placeholder { color: ${tokens_1.contract.fgSelectPlaceholder}; }
105
+ /* select.yaml's own divider slot (marginX x4, height 1px, colour
106
+ stroke.neutral-muted -- described upstream as "the line marking a
107
+ boundary between semantic units: between sections, between content
108
+ and an action area, between a header and a body" -- exactly the
109
+ search-field/option-list boundary crisp#904's review flagged as
110
+ reading too tight). Not the general-purpose separator #867 declined
111
+ to build (nothing groups options with one) -- scoped to this one
112
+ specific header/body seam, which now exists because searchable
113
+ does. Sits as an ordinary flex child, so the listbox's own 8px gap
114
+ still applies on both sides of it -- total visual seam is
115
+ gap+1px+gap (17px), deliberately more than the 8px between two
116
+ plain option rows, so it reads as a section break, not a wider list
117
+ gap. */
118
+ .crisp-select-divider { flex-shrink: 0; height: 1px; margin-inline: ${tokens_1.contract.space["4"]}; background: ${tokens_1.contract.strokeSelectDivider}; }
105
119
  .crisp-select-empty { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["4"]}; text-align: center; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgNeutralSubtle}; }
106
120
  /* select.yaml's groupLabel -- font-size/line-height/weight are the same
107
121
  across sizes, only paddingY differs (medium 8px, large 10px). */
@@ -136,13 +150,25 @@ const config = (0, core_1.defineRecipe)({
136
150
  },
137
151
  // Owner's follow-up after #867/#879 (crisp/#889): the LOOK stays seed's
138
152
  // (border, listbox, radius, shadow, items, states, all unchanged below),
139
- // but the SIZE is Attio's again -- a 40px trigger next to a 32px `Input`
140
- // in the same form visibly mismatched (measured live in GRC's own "New
141
- // employee" dialog). `small` is the new default: Attio's original
142
- // 32px `c.size.menuRow`, `Input`'s own radius/padding/line-height so the
143
- // two line up exactly, not just approximately. `medium`/`large` (seed's
144
- // own 40/52px) stay, opt-in. Item rows are IDENTICAL across all three --
145
- // only the trigger differs; only it was ever the mismatch.
153
+ // but the HEIGHT is Attio's again -- a 40px trigger next to a 32px
154
+ // `Input` in the same form visibly mismatched (measured live in GRC's
155
+ // own "New employee" dialog). `small` is the new default: Attio's
156
+ // original 32px `c.size.menuRow`. `medium`/`large` (seed's own 40/52px)
157
+ // stay, opt-in. Item rows are IDENTICAL across all three -- only the
158
+ // trigger differs; only it was ever the mismatch.
159
+ //
160
+ // crisp/#904 (follow-up, then reverted): `small`'s gap/padding-inline
161
+ // briefly moved to seed's literal `select-trigger.yaml` `size=medium`
162
+ // values (8px/14px), on the theory that even the 32px trigger should
163
+ // read seed's numbers verbatim. Team-lead ruling on that PR's review:
164
+ // padding and gap are SPACING, and the owner's actual direction is
165
+ // "sizes/spacing from Attio, look from seed" -- Select and Input sit
166
+ // side by side in every form, so spacing follows Input's measured
167
+ // values here, same as `medium`/`large` below follow seed's. Reverted
168
+ // to `space["1.5"]`/`space["2.5"]` (6px/10px, `Input`'s own gap/padding
169
+ // at this row height) -- everything else `small` already got from seed
170
+ // (fill/stroke/border/radius/chevron/colours/states, see this file's
171
+ // other comments) is unaffected and stays seed's.
146
172
  size: {
147
173
  small: `
148
174
  & .crisp-select-trigger { height: ${tokens_1.contract.size.menuRow}; border-radius: ${tokens_1.contract.radius.md}; gap: ${tokens_1.contract.space["1.5"]}; padding-inline: ${tokens_1.contract.space["2.5"]}; }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.snackbarRecipe = exports.useSnackbar = exports.SnackbarProvider = void 0;
4
+ var snackbar_1 = require("./snackbar");
5
+ Object.defineProperty(exports, "SnackbarProvider", { enumerable: true, get: function () { return snackbar_1.SnackbarProvider; } });
6
+ Object.defineProperty(exports, "useSnackbar", { enumerable: true, get: function () { return snackbar_1.useSnackbar; } });
7
+ var snackbar_recipe_1 = require("./snackbar.recipe");
8
+ Object.defineProperty(exports, "snackbarRecipe", { enumerable: true, get: function () { return snackbar_recipe_1.snackbarRecipe; } });
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SnackbarProvider = SnackbarProvider;
37
+ exports.useSnackbar = useSnackbar;
38
+ const jsx_runtime_1 = require("react/jsx-runtime");
39
+ const lucide_react_1 = require("lucide-react");
40
+ const React = __importStar(require("react"));
41
+ const core_1 = require("../../core");
42
+ const snackbar_recipe_1 = require("./snackbar.recipe");
43
+ const SnackbarContext = React.createContext(null);
44
+ const ICONS = {
45
+ positive: lucide_react_1.CircleCheck,
46
+ critical: lucide_react_1.CircleAlert,
47
+ };
48
+ function SnackbarPill({ item, onClose, }) {
49
+ const Icon = ICONS[item.variant ?? "default"];
50
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-snackbar", "data-variant": item.variant ?? "default", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-snackbar-content", children: [Icon != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-snackbar-icon", children: (0, jsx_runtime_1.jsx)(Icon, { size: 24, strokeWidth: 1.75, "aria-hidden": "true" }) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-snackbar-message", children: item.message })] }), item.action && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-snackbar-action", onClick: () => {
51
+ item.action?.onClick();
52
+ onClose();
53
+ }, children: item.action.label }))] }));
54
+ }
55
+ /**
56
+ * Wrap your app in a `SnackbarProvider`, then call `useSnackbar().show({...})`
57
+ * from anywhere to queue a bottom-anchored snackbar. Only one shows at a time
58
+ * (seed-design's mobile pattern, crisp#911) -- a second call queues behind the
59
+ * first and shows once it dismisses. Auto-dismisses after `duration` (default
60
+ * 4s; 0 keeps it until `dismiss()` or its action fires -- there is no close
61
+ * button, matching seed's own anatomy, which has none).
62
+ */
63
+ function SnackbarProvider({ children, duration = 4000, regionLabel: regionLabelProp, }) {
64
+ const regionLabel = (0, core_1.useCrispMessage)(regionLabelProp, "Notifications");
65
+ const [queue, setQueue] = React.useState([]);
66
+ const idRef = React.useRef(0);
67
+ const timer = React.useRef(null);
68
+ const regionRef = React.useRef(null);
69
+ const cameFrom = React.useRef(null);
70
+ const focusAfterDismiss = (0, core_1.useFocusAfterRemoval)(regionRef, ".crisp-snackbar-action", cameFrom);
71
+ React.useEffect(() => {
72
+ const onFocusIn = (e) => {
73
+ const target = e.target;
74
+ if (!target || regionRef.current?.contains(target))
75
+ return;
76
+ cameFrom.current = target;
77
+ };
78
+ document.addEventListener("focusin", onFocusIn);
79
+ return () => document.removeEventListener("focusin", onFocusIn);
80
+ }, []);
81
+ const dismiss = React.useCallback((id) => {
82
+ setQueue((list) => list.filter((s) => s.id !== id));
83
+ if (timer.current) {
84
+ clearTimeout(timer.current);
85
+ timer.current = null;
86
+ }
87
+ }, []);
88
+ const show = React.useCallback((o) => {
89
+ idRef.current += 1;
90
+ const id = idRef.current;
91
+ setQueue((list) => [...list, { ...o, id }]);
92
+ return id;
93
+ }, []);
94
+ // Only the FRONT of the queue is ever mounted, so its own auto-dismiss
95
+ // timer starts when it becomes visible, not when `show` was called.
96
+ const current = queue[0];
97
+ React.useEffect(() => {
98
+ if (!current)
99
+ return;
100
+ const d = current.duration ?? duration;
101
+ if (d <= 0)
102
+ return;
103
+ timer.current = setTimeout(() => dismiss(current.id), d);
104
+ return () => {
105
+ if (timer.current)
106
+ clearTimeout(timer.current);
107
+ };
108
+ }, [current, duration, dismiss]);
109
+ const api = React.useMemo(() => ({ show, dismiss }), [show, dismiss]);
110
+ return ((0, jsx_runtime_1.jsxs)(SnackbarContext.Provider, { value: api, children: [children, (0, jsx_runtime_1.jsx)("div", { className: (0, snackbar_recipe_1.snackbarRecipe)(), children: (0, jsx_runtime_1.jsx)("div", { ref: regionRef, className: "crisp-snackbar-region", role: "region", "aria-label": regionLabel, "aria-live": "polite", "aria-atomic": "false", children: current && ((0, jsx_runtime_1.jsx)(SnackbarPill, { item: current, onClose: () => {
111
+ focusAfterDismiss(0);
112
+ dismiss(current.id);
113
+ } }, current.id)) }) })] }));
114
+ }
115
+ SnackbarProvider.displayName = "SnackbarProvider";
116
+ function useSnackbar() {
117
+ const ctx = React.useContext(SnackbarContext);
118
+ if (!ctx)
119
+ throw new Error("useSnackbar must be used within a SnackbarProvider");
120
+ return ctx;
121
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.snackbarRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A mobile-pattern snackbar — daangn/seed-design's
7
+ // packages/rootage/components/snackbar.yaml (dev@e1c486b904297f7b6a8f17b8523f732c37015d60),
8
+ // not Attio (no such component lives there; crisp#911). A single dark
9
+ // `bgNeutralInverted` pill, bottom-anchored, holding an optional tone-coloured
10
+ // prefix icon, a message, and an optional bold action. Every numeric value
11
+ // below cites its yaml line; sizes/radii already equal existing crisp tokens
12
+ // (crisp's space/radius scale was itself derived 2px-granular, same as
13
+ // seed's `dimension`/`radius` scales, so no new tokens were needed).
14
+ //
15
+ // Deviation from the spec's literal `actionButton.color: $color.fg.brand`
16
+ // (snackbar.yaml:128, seed's carrot-600 `#ff6600`): crisp's OWN brand token
17
+ // is TechPartners' verified blue (`bgBrandSolid`, `#0298d9`), not seed's
18
+ // orange (the identity migration takes seed's STRUCTURE, crisp's own colour
19
+ // -- `~/grc-parity/seed-identity-spec.md`). Computed contrast of
20
+ // `bgBrandSolid` on `bgNeutralInverted` (light theme: `#0298d9` on `#2a3038`)
21
+ // is 4.11:1 -- below AA's 4.5:1 for 14px text (bold 14px does not clear the
22
+ // large-text 3:1 threshold, which needs 18.66px). crisp has no brand step
23
+ // bright enough on this dark surface yet, so the action reads in
24
+ // `fgOnNeutralInverted` (the same token the message already uses) at bold
25
+ // weight instead -- distinguished by weight, not colour, until a dedicated
26
+ // token is measured.
27
+ const config = (0, core_1.defineRecipe)({
28
+ name: "snackbar",
29
+ base: `
30
+ & .crisp-snackbar-region {
31
+ position: fixed; z-index: 80; inset-inline: 0; bottom: 0;
32
+ display: flex; justify-content: center;
33
+ padding: ${tokens_1.contract.space["2"]}; /* snackbar.yaml:95-96 region.paddingX/paddingY = $dimension.x2 (8px) */
34
+ pointer-events: none;
35
+ }
36
+ & .crisp-snackbar {
37
+ pointer-events: auto; box-sizing: border-box;
38
+ display: flex; align-items: center; gap: ${tokens_1.contract.space["2.5"]}; /* snackbar.yaml:116 content.gap = $dimension.x2_5 (10px), reused as the icon+message-to-action gap */
39
+ min-height: 44px; max-width: 464px; /* snackbar.yaml:102-103 */
40
+ width: max-content;
41
+ padding: ${tokens_1.contract.space["2.5"]}; /* snackbar.yaml:104-105 root.paddingX/paddingY = $dimension.x2_5 (10px) */
42
+ border-radius: ${tokens_1.contract.radius.md}; /* snackbar.yaml:101 root.cornerRadius = $radius.r2 (8px) */
43
+ background: ${tokens_1.contract.bgNeutralInverted}; /* snackbar.yaml:100 root.color = $color.bg.neutral-inverted */
44
+ font-family: ${tokens_1.contract.font.sans};
45
+ opacity: 1; transform: scale(1);
46
+ transition: opacity 150ms cubic-bezier(0,0,.15,1), transform 150ms cubic-bezier(0,0,.15,1); /* snackbar.yaml:108-109 enterDuration/enterTimingFunction = $duration.d3 (150ms) / $timing-function.enter */
47
+ @starting-style { opacity: 0; transform: scale(0.8); } /* snackbar.yaml:106-107 enterOpacity/enterScale */
48
+ }
49
+ & .crisp-snackbar-content { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: ${tokens_1.contract.space["0.5"]}; } /* snackbar.yaml:124 prefixIcon.paddingRight = $dimension.x0_5 (2px), used as the icon-to-message gap */
50
+ & .crisp-snackbar-icon { flex-shrink: 0; display: inline-flex; }
51
+ & .crisp-snackbar[data-variant="positive"] .crisp-snackbar-icon { color: ${tokens_1.contract.bgSuccess}; } /* snackbar.yaml:138-139 variant=positive prefixIcon.color = $color.fg.positive */
52
+ & .crisp-snackbar[data-variant="critical"] .crisp-snackbar-icon { color: ${tokens_1.contract.bgDanger}; } /* snackbar.yaml:142-143 variant=critical prefixIcon.color = $color.fg.critical */
53
+ & .crisp-snackbar-message { flex: 1 1 auto; min-width: 0; color: ${tokens_1.contract.fgOnNeutralInverted}; font-size: ${tokens_1.contract.text.lg}; line-height: 19px; font-weight: ${tokens_1.contract.weight.regular}; } /* snackbar.yaml:117-121: message.color = $color.fg.neutral-inverted, fontSize/lineHeight = $font-size.t4/$line-height.t4 (14px/19px), fontWeight = regular */
54
+ & .crisp-snackbar-action {
55
+ flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
56
+ min-height: 44px; padding: 0 ${tokens_1.contract.space["2"]}; margin: -12px calc(-1 * ${tokens_1.contract.space["2"]}); /* snackbar.yaml:126-127 targetPaddingX/targetMinHeight = $dimension.x2 (8px, doubled here as horizontal padding)/44px -- a larger invisible tap target than the pill's own 44px content row, via negative margin so it doesn't grow the pill visually */
57
+ border: none; border-radius: ${tokens_1.contract.radius.md}; background: none;
58
+ color: ${tokens_1.contract.fgOnNeutralInverted}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; line-height: 19px; font-weight: ${tokens_1.contract.weight.bold}; /* fontSize/lineHeight = t4/line-height.t4; fontWeight = bold (snackbar.yaml:129-131); colour deviation documented above */
59
+ cursor: pointer;
60
+ transition: transform 150ms cubic-bezier(0,0,.15,1);
61
+ }
62
+ & .crisp-snackbar-action:active { transform: scale(0.98); } /* snackbar.yaml:133-134 pressed.actionButton.scaleScope = self */
63
+ `,
64
+ variants: {},
65
+ defaultVariants: {},
66
+ compoundVariants: [],
67
+ });
68
+ exports.snackbarRecipe = Object.assign((0, core_1.resolveRecipe)(config), {
69
+ config,
70
+ });
71
+ exports.default = config;
@@ -179,6 +179,11 @@ const toolbarChromeBody = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-menu-he
179
179
  & .${namespace_1.CLASS_PREFIX}${p}-fchip-x:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 10%, transparent); color: ${tokens_1.contract.fgPrimary}; }
180
180
  & .${namespace_1.CLASS_PREFIX}${p}-spacer { flex: 1; }
181
181
  & .${namespace_1.CLASS_PREFIX}${p}-count { font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
182
- & .${namespace_1.CLASS_PREFIX}${p}-new { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.row}; padding: 0 ${tokens_1.contract.space["2.5"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgBrandSolid}; color: ${tokens_1.contract.fgOnBrand}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; cursor: pointer; transition: background-color .12s; }
183
- & .${namespace_1.CLASS_PREFIX}${p}-new:hover { background: ${tokens_1.contract.bgBrandSolidPressed}; }`;
182
+ /* your-moon/crisp#953: bgBrandContrast, not bgBrandSolid --
183
+ the accent (#0298d9) only clears 3.23:1 with white; the darker sibling
184
+ clears 4.5:1+, same fix already applied to Button/Badge/Avatar (#897).
185
+ Hover is a brightness filter on the new fill, not bgBrandSolidPressed
186
+ (that token stays tuned for the lighter accent's own consumers). */
187
+ & .${namespace_1.CLASS_PREFIX}${p}-new { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.row}; padding: 0 ${tokens_1.contract.space["2.5"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgBrandContrast}; color: ${tokens_1.contract.fgOnBrand}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; cursor: pointer; transition: filter .12s; }
188
+ & .${namespace_1.CLASS_PREFIX}${p}-new:hover { filter: brightness(0.96); }`;
184
189
  exports.toolbarChromeBody = toolbarChromeBody;
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.ToolbarTool = void 0;
36
+ exports.ToolbarTool = exports.toolbarOperatorLabels = void 0;
37
37
  exports.applyFilters = applyFilters;
38
38
  exports.applySorts = applySorts;
39
39
  exports.TableToolbar = TableToolbar;
@@ -83,7 +83,7 @@ function ViewBarSlot({ target, children, }) {
83
83
  // file, not per call site, and three independently attributed tags for
84
84
  // the same name over-counts against that (it expects one per name).
85
85
  const RtlChevronRight = ({ size, className, }) => ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronRight, { "data-rtl-flip": "", size: size, strokeWidth: 1.75, className: className }));
86
- const OP_LABELS = {
86
+ exports.toolbarOperatorLabels = {
87
87
  contains: "contains",
88
88
  doesnotcontain: "does not contain",
89
89
  startswith: "starts with",
@@ -104,6 +104,33 @@ const OP_LABELS = {
104
104
  before: "before",
105
105
  after: "after",
106
106
  };
107
+ /**
108
+ * crisp/#962: resolves each operator word through four layers, explicit
109
+ * prop winning, same "prop beats provider beats built-in" order as every
110
+ * other label in this file — `operatorLabels` prop, then a
111
+ * `CrispMessagesProvider`'s `componentDefaults.tableToolbar` (the new,
112
+ * discoverable, typed override), then the OLD whole-`CrispMessages`-
113
+ * dictionary-keyed-by-English-string lookup this file already had (kept
114
+ * so a consumer who already added entries there keeps working, unchanged),
115
+ * then the hardcoded English word. Not `useCrispMessage` in a loop over 13
116
+ * keys — that breaks the rules of hooks the same reason `useCrispMessages`
117
+ * itself exists (see that hook's own doc comment).
118
+ */
119
+ function useToolbarOperatorLabels(operatorLabels, crispMessages) {
120
+ const providerLabels = (0, core_1.useCrispComponentDefaults)("tableToolbar");
121
+ return React.useMemo(() => {
122
+ const resolved = {};
123
+ for (const op of Object.keys(exports.toolbarOperatorLabels)) {
124
+ const builtIn = exports.toolbarOperatorLabels[op];
125
+ resolved[op] =
126
+ operatorLabels?.[op] ??
127
+ providerLabels?.[op] ??
128
+ crispMessages[builtIn] ??
129
+ builtIn;
130
+ }
131
+ return resolved;
132
+ }, [operatorLabels, providerLabels, crispMessages]);
133
+ }
107
134
  // #619: order and label set match Attio's own text-filter operator picker
108
135
  // verbatim (measured live, per RecordsTable's own #578 port of the same
109
136
  // set: contains · does not contain · starts with · ends with · is · is not ·
@@ -368,7 +395,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
368
395
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
369
396
  * Created by), which one shared list can't reproduce.
370
397
  */
371
- function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewSettingsRef, importExportRef, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
398
+ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, operatorLabels, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewSettingsRef, importExportRef, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
372
399
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
373
400
  // CrispMessagesProvider's translation of the English default below, or
374
401
  // that default itself with no provider — unchanged from before this
@@ -401,12 +428,12 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
401
428
  const exportExcelLabel = (0, core_1.useCrispMessage)(exportExcelLabelProp, "Export view as Excel");
402
429
  const learnImportExportLabel = (0, core_1.useCrispMessage)(learnImportExportLabelProp, "Learn about imports and exports");
403
430
  const backLabel = (0, core_1.useCrispMessage)(backLabelProp, "Back");
404
- // OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
405
- // their own — a whole lookup table, not one string — so they translate
406
- // straight from the provider's dictionary at the two spots they render
407
- // (#628, #673 below), same fallback order as every resolved constant
408
- // above.
409
431
  const crispMessages = (0, core_1.useCrispMessages)();
432
+ // crisp/#962: `operatorLabels` prop + `componentDefaults.tableToolbar`,
433
+ // layered over the old whole-dictionary lookup — see
434
+ // `useToolbarOperatorLabels`'s own doc comment for the full resolution
435
+ // order.
436
+ const opLabels = useToolbarOperatorLabels(operatorLabels, crispMessages);
410
437
  // Each side falls back to the shared `columns` independently — a caller
411
438
  // that only overrides one (Filter's own Record/Created by, say) still
412
439
  // gets `columns` on the other, unchanged from before either prop existed.
@@ -842,9 +869,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
842
869
  return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fcond", "data-complete": complete ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--field", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": fieldOpen, onClick: () => {
843
870
  setFieldPickerPath([]);
844
871
  setOpenMenu(fieldOpen ? null : `ffield:${cond.id}`);
845
- }, children: [col?.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 14, strokeWidth: 1.75 }) : null, colPath.length > 1 ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path", children: colPath.map((c, i) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [i > 0 && ((0, jsx_runtime_1.jsx)(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : (col?.label)] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--op", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": opOpen, onClick: () => setOpenMenu(opOpen ? null : `fop:${cond.id}`), children: cond.op
846
- ? (crispMessages[OP_LABELS[cond.op]] ?? OP_LABELS[cond.op])
847
- : selectConditionLabel }), showValueSeg && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--val", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": valOpen, onClick: () => setOpenMenu(valOpen ? null : `fval:${cond.id}`), children: cond.op === "isanyof"
872
+ }, children: [col?.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 14, strokeWidth: 1.75 }) : null, colPath.length > 1 ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path", children: colPath.map((c, i) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [i > 0 && ((0, jsx_runtime_1.jsx)(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : (col?.label)] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--op", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": opOpen, onClick: () => setOpenMenu(opOpen ? null : `fop:${cond.id}`), children: cond.op ? opLabels[cond.op] : selectConditionLabel }), showValueSeg && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--val", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": valOpen, onClick: () => setOpenMenu(valOpen ? null : `fval:${cond.id}`), children: cond.op === "isanyof"
848
873
  ? cond.values?.length
849
874
  ? isAnyOfSummary(cond.values)
850
875
  : filterValuePlaceholder
@@ -852,7 +877,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
852
877
  ? col?.type === "date"
853
878
  ? formatFilterDate(cond.value)
854
879
  : cond.value
855
- : filterValuePlaceholder })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, (key) => changeFilterField(cond.id, key), filterFieldPlaceholder, filterFieldLabel) })), opOpen && ((0, jsx_runtime_1.jsx)("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
880
+ : filterValuePlaceholder })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, (key) => changeFilterField(cond.id, key), filterFieldPlaceholder, filterFieldLabel) })), opOpen && ((0, jsx_runtime_1.jsx)("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [opLabels[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
856
881
  // biome-ignore lint/a11y/noAutofocus: focus the search field
857
882
  autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
858
883
  .filter((v) => v
@@ -46,7 +46,7 @@ const tabs_keyboard_1 = require("./tabs-keyboard");
46
46
  * Home/End jump); activation follows focus. Controlled via
47
47
  * `value`/`onValueChange`, or uncontrolled from `defaultValue`.
48
48
  */
49
- function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, moreLabel = (n) => `+${n} more`, moreMenuLabel = "More tabs", className, fill, "aria-label": ariaLabel, ...rest }) {
49
+ function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, moreLabel = (n) => `+${n} more`, moreMenuLabel = "More tabs", overflow = "collapse", className, fill, "aria-label": ariaLabel, ...rest }) {
50
50
  // Scoped to this instance: `tab-${value}` and `panel-${value}` are global,
51
51
  // so two tab bars sharing a value mint the same id twice and every
52
52
  // aria-labelledby resolves to the first one (#227).
@@ -93,6 +93,12 @@ function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, mor
93
93
  const [hiddenFrom, setHiddenFrom] = React.useState(null);
94
94
  const moreRef = React.useRef(null);
95
95
  React.useLayoutEffect(() => {
96
+ // crisp#908: "scroll" never collapses anything -- the row scrolls
97
+ // instead, so there is nothing to measure or hide.
98
+ if (overflow !== "collapse") {
99
+ setHiddenFrom(null);
100
+ return;
101
+ }
96
102
  const list = listRef.current;
97
103
  if (!list || tabs.length === 0)
98
104
  return;
@@ -133,7 +139,7 @@ function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, mor
133
139
  const ro = new ResizeObserver(recompute);
134
140
  ro.observe(list);
135
141
  return () => ro.disconnect();
136
- }, [tabs, active]);
142
+ }, [tabs, active, overflow]);
137
143
  const hiddenTabs = hiddenFrom == null ? [] : tabs.slice(hiddenFrom);
138
144
  const { onKeyDown, tabIndexFor } = (0, tabs_keyboard_1.useTablistKeyboard)({
139
145
  tabs,
@@ -141,11 +147,11 @@ function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, mor
141
147
  select,
142
148
  listRef,
143
149
  });
144
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, tabs_recipe_1.tabsRecipe)({ fill, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { ref: listRef, className: "crisp-tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t, i) => {
150
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, tabs_recipe_1.tabsRecipe)({ fill, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { ref: listRef, className: "crisp-tabs-list", "data-overflow": overflow, role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t, i) => {
145
151
  const selected = t.value === active;
146
152
  const overflowHidden = hiddenFrom != null && i >= hiddenFrom;
147
153
  return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "tab", id: tabId(t.value), "aria-selected": selected, "aria-controls": children && selected ? panelId(t.value) : undefined, tabIndex: overflowHidden ? -1 : tabIndexFor(t.value), "data-value": t.value, "data-selected": selected ? "" : undefined, "data-tabs-overflow-hidden": overflowHidden ? "" : undefined, disabled: t.disabled, className: "crisp-tabs-tab", onClick: () => select(t.value), children: [t.icon, (0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value));
148
- }), (0, jsx_runtime_1.jsx)("span", { ref: moreRef, className: "crisp-tabs-more", "data-tabs-overflow-hidden": hiddenTabs.length === 0 ? "" : undefined, children: (0, jsx_runtime_1.jsx)(menu_1.Menu, { "aria-label": moreMenuLabel, trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabs-tab", children: [(0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-label", children: moreLabel(hiddenTabs.length) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), children: hiddenTabs.map((t) => ((0, jsx_runtime_1.jsxs)(menu_1.MenuItem, { icon: t.icon, onSelect: () => select(t.value), children: [t.label, t.badge != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value))) }) }), indicator && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-indicator", style: {
154
+ }), overflow === "collapse" && ((0, jsx_runtime_1.jsx)("span", { ref: moreRef, className: "crisp-tabs-more", "data-tabs-overflow-hidden": hiddenTabs.length === 0 ? "" : undefined, children: (0, jsx_runtime_1.jsx)(menu_1.Menu, { "aria-label": moreMenuLabel, trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabs-tab", children: [(0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-label", children: moreLabel(hiddenTabs.length) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), children: hiddenTabs.map((t) => ((0, jsx_runtime_1.jsxs)(menu_1.MenuItem, { icon: t.icon, onSelect: () => select(t.value), children: [t.label, t.badge != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value))) }) })), indicator && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabs-indicator", style: {
149
155
  width: indicator.width,
150
156
  transform: `translateX(${indicator.left}px)`,
151
157
  } }))] }), children && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabs-panel", role: "tabpanel", id: panelId(active), "aria-labelledby": tabId(active),
@@ -8,10 +8,20 @@ const tokens_1 = require("../../tokens");
8
8
  // baseline needs, reserved as bottom padding so it's real box height and not
9
9
  // paint that trailing content can sit on top of. The bar carries 12px inline
10
10
  // padding too, and draws its own full-width `::after` baseline in
11
- // borderSubtle at its bottom edge, spanning that padding. The active tab is
12
- // a light pill — bg #fbfbfb + inset ring borderSubtle, r8 — with a 1px ink
13
- // underline that sits on the baseline (painted above it) and slides along it;
14
- // inactive tabs are muted and fill to a subtle pill on hover. Each tab carries
11
+ // borderSubtle at its bottom edge, spanning that padding.
12
+ //
13
+ // your-moon/crisp#934: the selected tab used to be a light
14
+ // filled pill (bg #fbfbfb + inset ring borderSubtle, r8) with a 1px ink
15
+ // underline on top of it. Owner-directed: underline tabs, no fill pill --
16
+ // seed-design's own `tab.yaml` has no `root.color` property at all, at any
17
+ // state (selected/hover/pressed); selection is carried purely by the label
18
+ // darkening (muted -> ink, unchanged, already matched seed) plus the
19
+ // existing underline (already `fgPrimary`, "same colour as selected text,
20
+ // not brand" -- also already matched seed). Only the selected pill's own
21
+ // fill/ring is removed here; unselected tabs keep their existing subtle
22
+ // hover tint (a desktop hover affordance seed's spec doesn't need to
23
+ // consider one way or the other, and the owner didn't ask to remove).
24
+ // Inactive tabs are muted and fill to a subtle pill on hover. Each tab carries
15
25
  // a 16px leading icon and an optional count badge. Measured from the live
16
26
  // app: bar `padding: 0 12px 8px`, 28px pills, 4/8 padding, 6px icon-label
17
27
  // gap, 14/500 labels. The count badge (crisp#697, re-measured against a
@@ -48,11 +58,18 @@ const config = (0, core_1.defineRecipe)({
48
58
 
49
59
  & .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; box-sizing: content-box; height: ${tokens_1.contract.size.row}; padding: 0 ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]}; flex-shrink: 0; }
50
60
  & .crisp-tabs-list::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: ${tokens_1.contract.borderSubtle}; }
61
+ /* crisp#908: an alternative to the "+N more" collapse above (crisp#863) --
62
+ a consumer opts into a single scrolling row instead (Naver/Kakao's own
63
+ pattern, docs/study/korean/), never both at once. No new visual value:
64
+ reuses the same list gap/tab geometry, only adds overflow-x + snap. */
65
+ & .crisp-tabs-list[data-overflow="scroll"] { overflow-x: auto; flex-wrap: nowrap; scroll-snap-type: x mandatory; scrollbar-width: none; }
66
+ & .crisp-tabs-list[data-overflow="scroll"]::-webkit-scrollbar { display: none; }
67
+ & .crisp-tabs-list[data-overflow="scroll"] .crisp-tabs-tab { flex-shrink: 0; scroll-snap-align: start; }
51
68
  & .crisp-tabs-leading { display: inline-flex; align-items: center; margin-inline-end: ${tokens_1.contract.space["1"]}; }
52
- & .crisp-tabs-tab { position: relative; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 28px; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; background: none; box-shadow: inset 0 0 0 1px transparent; border-radius: ${tokens_1.contract.radius.md}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; letter-spacing: -0.14px; color: ${tokens_1.contract.fgMuted}; cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease, box-shadow .12s ease; }
69
+ & .crisp-tabs-tab { position: relative; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 28px; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; background: none; border-radius: ${tokens_1.contract.radius.md}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; letter-spacing: -0.14px; color: ${tokens_1.contract.fgMuted}; cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease; }
53
70
  & .crisp-tabs-tab svg { flex-shrink: 0; opacity: 0.85; }
54
71
  & .crisp-tabs-tab:hover:not([data-selected]) { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); color: ${tokens_1.contract.fgPrimary}; }
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}; }
72
+ & .crisp-tabs-tab[data-selected] { color: ${tokens_1.contract.fgPrimary}; }
56
73
  & .crisp-tabs-tab:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 1px; }
57
74
  & .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}; }
58
75
  & .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${tokens_1.contract.fgPrimary}; }