@12-apps/ui 6.66.2 → 6.67.0

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 (196) hide show
  1. package/dist/_internal/components/data-display/Avatar/Avatar.js +4 -4
  2. package/dist/_internal/components/data-display/Avatar/Avatar.view.js +2 -2
  3. package/dist/_internal/components/data-display/Carousel/Carousel.js +2 -2
  4. package/dist/_internal/components/data-display/Chart/Chart.js +3 -3
  5. package/dist/_internal/components/data-display/Chart/SpecChart.js +6 -6
  6. package/dist/_internal/components/data-display/Chart/chart-renderers.js +2 -2
  7. package/dist/_internal/components/data-display/Chart/chart-spec.js +2 -2
  8. package/dist/_internal/components/data-display/DataGrid/DataGrid.js +2 -2
  9. package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +31 -28
  10. package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +33 -30
  11. package/dist/_internal/components/data-display/DataViews/base-card.js +2 -2
  12. package/dist/_internal/components/data-display/DataViews/base-list-card-geometry.js +5 -5
  13. package/dist/_internal/components/data-display/DataViews/base-list-card-layout.js +6 -6
  14. package/dist/_internal/components/data-display/DataViews/base-list-card-slots.js +4 -4
  15. package/dist/_internal/components/data-display/DataViews/base-list-card.js +8 -8
  16. package/dist/_internal/components/data-display/DataViews/data-views-display-panel.js +4 -4
  17. package/dist/_internal/components/data-display/DataViews/data-views-display-tabs.js +3 -3
  18. package/dist/_internal/components/data-display/DataViews/data-views-empty.js +5 -3
  19. package/dist/_internal/components/data-display/DataViews/data-views-filter-panel.js +3 -3
  20. package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +7 -6
  21. package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +28 -25
  22. package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +11 -5
  23. package/dist/_internal/components/data-display/DataViews/data-views-layout-context.js +2 -2
  24. package/dist/_internal/components/data-display/DataViews/data-views-more-fields.js +8 -3
  25. package/dist/_internal/components/data-display/DataViews/data-views-more-filters.js +7 -4
  26. package/dist/_internal/components/data-display/DataViews/data-views-overflow-fields.js +18 -0
  27. package/dist/_internal/components/data-display/DataViews/data-views-overflow-fields.js.map +1 -0
  28. package/dist/_internal/components/data-display/DataViews/data-views-overflow.js +6 -2
  29. package/dist/_internal/components/data-display/DataViews/data-views-quick-chip.js +20 -0
  30. package/dist/_internal/components/data-display/DataViews/data-views-quick-chip.js.map +1 -0
  31. package/dist/_internal/components/data-display/DataViews/data-views-quick-types.js +1 -0
  32. package/dist/_internal/components/data-display/DataViews/data-views-quick-types.js.map +1 -0
  33. package/dist/_internal/components/data-display/DataViews/data-views-range-pill.js +2 -2
  34. package/dist/_internal/components/data-display/DataViews/data-views-scope-tabs.js +3 -3
  35. package/dist/_internal/components/data-display/DataViews/data-views-shell-toolbar.js +8 -7
  36. package/dist/_internal/components/data-display/DataViews/data-views-sticky-head.js +21 -0
  37. package/dist/_internal/components/data-display/DataViews/data-views-sticky-head.js.map +1 -0
  38. package/dist/_internal/components/data-display/DataViews/data-views-toolbar.js +7 -6
  39. package/dist/_internal/components/data-display/DataViews/data-views-types.js +1 -1
  40. package/dist/_internal/components/data-display/DataViews/list-card-rails.js +3 -3
  41. package/dist/_internal/components/data-display/DataViews/use-data-views-state.js +3 -3
  42. package/dist/_internal/components/data-display/DataViews/use-saved-views-controller.js +2 -2
  43. package/dist/_internal/components/data-display/index.js +19 -19
  44. package/dist/_internal/components/index.js +19 -19
  45. package/dist/_internal/user-avatar/user-avatar.js +5 -5
  46. package/dist/charts.js +11 -11
  47. package/dist/{chunk-35L5YMYQ.js → chunk-2QB4WIU5.js} +6 -6
  48. package/dist/chunk-4IE66GY2.js +89 -0
  49. package/dist/chunk-4IE66GY2.js.map +1 -0
  50. package/dist/{chunk-JYMXYSDP.js → chunk-5CAWRMDX.js} +2 -2
  51. package/dist/{chunk-L7OWFJUU.js → chunk-5RC7WAHH.js} +40 -34
  52. package/dist/chunk-5RC7WAHH.js.map +1 -0
  53. package/dist/{chunk-647DEGIU.js → chunk-5WJFCQFN.js} +7 -7
  54. package/dist/{chunk-K6KBASL2.js → chunk-72L5C74V.js} +2 -2
  55. package/dist/{chunk-TKLPPAS5.js → chunk-AHNEEFK6.js} +2 -2
  56. package/dist/chunk-AT6UKTH4.js +129 -0
  57. package/dist/chunk-AT6UKTH4.js.map +1 -0
  58. package/dist/{chunk-BI7YMVKZ.js → chunk-B2JSUFDX.js} +2 -2
  59. package/dist/{chunk-7J5JR33K.js → chunk-BBBHPK2Q.js} +2 -2
  60. package/dist/{chunk-UZ2CEJOO.js → chunk-CSJ7WNXU.js} +2 -2
  61. package/dist/{chunk-6QYKZMEE.js → chunk-CSLBRJ7N.js} +40 -25
  62. package/dist/chunk-CSLBRJ7N.js.map +1 -0
  63. package/dist/{chunk-LMIOWP6H.js → chunk-D34PZJH3.js} +2 -2
  64. package/dist/{chunk-SUZLGB5I.js → chunk-E6K2UZGR.js} +3 -3
  65. package/dist/{chunk-7XSCEHGA.js → chunk-EOQ6EKTS.js} +7 -7
  66. package/dist/chunk-GCYGVWOI.js +59 -0
  67. package/dist/chunk-GCYGVWOI.js.map +1 -0
  68. package/dist/{chunk-AMLHEKSV.js → chunk-HI7JPDD6.js} +5 -5
  69. package/dist/{chunk-O2P2KTKN.js → chunk-HLYYB4O5.js} +1 -1
  70. package/dist/chunk-HLYYB4O5.js.map +1 -0
  71. package/dist/{chunk-KARMRTTM.js → chunk-J2RUDZVJ.js} +7 -7
  72. package/dist/{chunk-HQCZT6TO.js → chunk-JVOEVTCZ.js} +5 -5
  73. package/dist/{chunk-VJB63DUM.js → chunk-K3PMMW23.js} +2 -2
  74. package/dist/{chunk-J6DYHBID.js → chunk-LIN4A2BW.js} +7 -7
  75. package/dist/{chunk-FXZUVZON.js → chunk-LWZ2K225.js} +6 -6
  76. package/dist/{chunk-6S3S2K7O.js → chunk-P3USTYIO.js} +3 -3
  77. package/dist/chunk-P3USTYIO.js.map +1 -0
  78. package/dist/{chunk-MJ6EAND6.js → chunk-Q7BGMLCX.js} +2 -2
  79. package/dist/{chunk-6ATXOJVQ.js → chunk-QADTI63U.js} +4 -4
  80. package/dist/{chunk-M6V7Y7CP.js → chunk-QAZLQYSD.js} +3 -3
  81. package/dist/chunk-QAZLQYSD.js.map +1 -0
  82. package/dist/{chunk-L6MGAPBX.js → chunk-RCZ4KL2H.js} +5 -3
  83. package/dist/chunk-RCZ4KL2H.js.map +1 -0
  84. package/dist/{chunk-YLSSQJZM.js → chunk-T7ZERP7A.js} +30 -2
  85. package/dist/chunk-T7ZERP7A.js.map +1 -0
  86. package/dist/{chunk-SB2SEGR6.js → chunk-TIM4PXWN.js} +3 -3
  87. package/dist/{chunk-Y7YKIIYS.js → chunk-TQXB2OHP.js} +28 -4
  88. package/dist/chunk-TQXB2OHP.js.map +1 -0
  89. package/dist/{chunk-3WLS7Z7D.js → chunk-UMOHHWXY.js} +3 -3
  90. package/dist/{chunk-S4JXSC4D.js → chunk-V6SRBQMN.js} +5 -5
  91. package/dist/{chunk-JY5YINUH.js → chunk-WQ32YFM5.js} +15 -7
  92. package/dist/chunk-WQ32YFM5.js.map +1 -0
  93. package/dist/{chunk-ERA3AEWU.js → chunk-WTGIW7LX.js} +12 -3
  94. package/dist/chunk-WTGIW7LX.js.map +1 -0
  95. package/dist/{chunk-2VMIHFMX.js → chunk-WVIJFS5Y.js} +9 -4
  96. package/dist/chunk-WVIJFS5Y.js.map +1 -0
  97. package/dist/{chunk-375HTCF2.js → chunk-Y2CCXB7R.js} +18 -2
  98. package/dist/chunk-Y2CCXB7R.js.map +1 -0
  99. package/dist/{chunk-33XGC4YU.js → chunk-YEGZIYVM.js} +2 -2
  100. package/dist/data-display/Avatar.js +4 -4
  101. package/dist/data-display/CardKit.js +7 -7
  102. package/dist/data-display/Carousel.js +2 -2
  103. package/dist/data-display/Chart.js +11 -11
  104. package/dist/data-display/DataGrid.js +2 -2
  105. package/dist/data-display/DataViews.js +42 -39
  106. package/dist/types/components/data-display/DataViews/DataViewsGrid.d.ts +15 -1
  107. package/dist/types/components/data-display/DataViews/DataViewsGrid.d.ts.map +1 -1
  108. package/dist/types/components/data-display/DataViews/DataViewsTableBase.d.ts +5 -1
  109. package/dist/types/components/data-display/DataViews/DataViewsTableBase.d.ts.map +1 -1
  110. package/dist/types/components/data-display/DataViews/data-views-empty.d.ts +10 -0
  111. package/dist/types/components/data-display/DataViews/data-views-empty.d.ts.map +1 -1
  112. package/dist/types/components/data-display/DataViews/data-views-grid-bodies.d.ts +2 -0
  113. package/dist/types/components/data-display/DataViews/data-views-grid-bodies.d.ts.map +1 -1
  114. package/dist/types/components/data-display/DataViews/data-views-grid-parts.d.ts +5 -1
  115. package/dist/types/components/data-display/DataViews/data-views-grid-parts.d.ts.map +1 -1
  116. package/dist/types/components/data-display/DataViews/data-views-inline-bar.d.ts +4 -1
  117. package/dist/types/components/data-display/DataViews/data-views-inline-bar.d.ts.map +1 -1
  118. package/dist/types/components/data-display/DataViews/data-views-more-fields.d.ts +10 -1
  119. package/dist/types/components/data-display/DataViews/data-views-more-fields.d.ts.map +1 -1
  120. package/dist/types/components/data-display/DataViews/data-views-more-filters.d.ts.map +1 -1
  121. package/dist/types/components/data-display/DataViews/data-views-overflow-fields.d.ts +56 -0
  122. package/dist/types/components/data-display/DataViews/data-views-overflow-fields.d.ts.map +1 -0
  123. package/dist/types/components/data-display/DataViews/data-views-overflow.d.ts +7 -5
  124. package/dist/types/components/data-display/DataViews/data-views-overflow.d.ts.map +1 -1
  125. package/dist/types/components/data-display/DataViews/data-views-quick-chip.d.ts +16 -0
  126. package/dist/types/components/data-display/DataViews/data-views-quick-chip.d.ts.map +1 -0
  127. package/dist/types/components/data-display/DataViews/data-views-quick-types.d.ts +33 -0
  128. package/dist/types/components/data-display/DataViews/data-views-quick-types.d.ts.map +1 -0
  129. package/dist/types/components/data-display/DataViews/data-views-range-types.d.ts +2 -0
  130. package/dist/types/components/data-display/DataViews/data-views-range-types.d.ts.map +1 -1
  131. package/dist/types/components/data-display/DataViews/data-views-shell-toolbar.d.ts +11 -6
  132. package/dist/types/components/data-display/DataViews/data-views-shell-toolbar.d.ts.map +1 -1
  133. package/dist/types/components/data-display/DataViews/data-views-sticky-head.d.ts +45 -0
  134. package/dist/types/components/data-display/DataViews/data-views-sticky-head.d.ts.map +1 -0
  135. package/dist/types/components/data-display/DataViews/data-views-toolbar.d.ts +6 -0
  136. package/dist/types/components/data-display/DataViews/data-views-toolbar.d.ts.map +1 -1
  137. package/dist/types/components/data-display/DataViews/data-views-types.d.ts +7 -4
  138. package/dist/types/components/data-display/DataViews/data-views-types.d.ts.map +1 -1
  139. package/dist/types/components/data-display/DataViews/index.d.ts +1 -1
  140. package/dist/types/components/data-display/DataViews/index.d.ts.map +1 -1
  141. package/dist/user-avatar.js +5 -5
  142. package/package.json +1 -1
  143. package/src/components/data-display/DataViews/DataViewsGrid.tsx +15 -0
  144. package/src/components/data-display/DataViews/DataViewsTableBase.tsx +5 -0
  145. package/src/components/data-display/DataViews/RESPONSIVE-BEHAVIOUR.md +50 -0
  146. package/src/components/data-display/DataViews/data-views-empty.tsx +25 -0
  147. package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +11 -1
  148. package/src/components/data-display/DataViews/data-views-grid-parts.tsx +27 -15
  149. package/src/components/data-display/DataViews/data-views-inline-bar.tsx +24 -1
  150. package/src/components/data-display/DataViews/data-views-more-fields.tsx +34 -1
  151. package/src/components/data-display/DataViews/data-views-more-filters.tsx +20 -4
  152. package/src/components/data-display/DataViews/data-views-overflow-fields.ts +110 -0
  153. package/src/components/data-display/DataViews/data-views-overflow.ts +58 -36
  154. package/src/components/data-display/DataViews/data-views-quick-chip.tsx +143 -0
  155. package/src/components/data-display/DataViews/data-views-quick-types.ts +33 -0
  156. package/src/components/data-display/DataViews/data-views-range-types.ts +2 -0
  157. package/src/components/data-display/DataViews/data-views-shell-toolbar.tsx +31 -23
  158. package/src/components/data-display/DataViews/data-views-sticky-head.ts +132 -0
  159. package/src/components/data-display/DataViews/data-views-toolbar.tsx +9 -0
  160. package/src/components/data-display/DataViews/data-views-types.ts +7 -11
  161. package/src/components/data-display/DataViews/index.ts +1 -0
  162. package/dist/chunk-2VMIHFMX.js.map +0 -1
  163. package/dist/chunk-375HTCF2.js.map +0 -1
  164. package/dist/chunk-6QYKZMEE.js.map +0 -1
  165. package/dist/chunk-6S3S2K7O.js.map +0 -1
  166. package/dist/chunk-ERA3AEWU.js.map +0 -1
  167. package/dist/chunk-JY5YINUH.js.map +0 -1
  168. package/dist/chunk-L6MGAPBX.js.map +0 -1
  169. package/dist/chunk-L7OWFJUU.js.map +0 -1
  170. package/dist/chunk-M6V7Y7CP.js.map +0 -1
  171. package/dist/chunk-O2P2KTKN.js.map +0 -1
  172. package/dist/chunk-Y7YKIIYS.js.map +0 -1
  173. package/dist/chunk-YLSSQJZM.js.map +0 -1
  174. /package/dist/{chunk-35L5YMYQ.js.map → chunk-2QB4WIU5.js.map} +0 -0
  175. /package/dist/{chunk-JYMXYSDP.js.map → chunk-5CAWRMDX.js.map} +0 -0
  176. /package/dist/{chunk-647DEGIU.js.map → chunk-5WJFCQFN.js.map} +0 -0
  177. /package/dist/{chunk-K6KBASL2.js.map → chunk-72L5C74V.js.map} +0 -0
  178. /package/dist/{chunk-TKLPPAS5.js.map → chunk-AHNEEFK6.js.map} +0 -0
  179. /package/dist/{chunk-BI7YMVKZ.js.map → chunk-B2JSUFDX.js.map} +0 -0
  180. /package/dist/{chunk-7J5JR33K.js.map → chunk-BBBHPK2Q.js.map} +0 -0
  181. /package/dist/{chunk-UZ2CEJOO.js.map → chunk-CSJ7WNXU.js.map} +0 -0
  182. /package/dist/{chunk-LMIOWP6H.js.map → chunk-D34PZJH3.js.map} +0 -0
  183. /package/dist/{chunk-SUZLGB5I.js.map → chunk-E6K2UZGR.js.map} +0 -0
  184. /package/dist/{chunk-7XSCEHGA.js.map → chunk-EOQ6EKTS.js.map} +0 -0
  185. /package/dist/{chunk-AMLHEKSV.js.map → chunk-HI7JPDD6.js.map} +0 -0
  186. /package/dist/{chunk-KARMRTTM.js.map → chunk-J2RUDZVJ.js.map} +0 -0
  187. /package/dist/{chunk-HQCZT6TO.js.map → chunk-JVOEVTCZ.js.map} +0 -0
  188. /package/dist/{chunk-VJB63DUM.js.map → chunk-K3PMMW23.js.map} +0 -0
  189. /package/dist/{chunk-J6DYHBID.js.map → chunk-LIN4A2BW.js.map} +0 -0
  190. /package/dist/{chunk-FXZUVZON.js.map → chunk-LWZ2K225.js.map} +0 -0
  191. /package/dist/{chunk-MJ6EAND6.js.map → chunk-Q7BGMLCX.js.map} +0 -0
  192. /package/dist/{chunk-6ATXOJVQ.js.map → chunk-QADTI63U.js.map} +0 -0
  193. /package/dist/{chunk-SB2SEGR6.js.map → chunk-TIM4PXWN.js.map} +0 -0
  194. /package/dist/{chunk-3WLS7Z7D.js.map → chunk-UMOHHWXY.js.map} +0 -0
  195. /package/dist/{chunk-S4JXSC4D.js.map → chunk-V6SRBQMN.js.map} +0 -0
  196. /package/dist/{chunk-33XGC4YU.js.map → chunk-YEGZIYVM.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState } from "react";
3
+ import { useEffect, useRef, useState } from "react";
4
4
 
5
5
  import { TableFilter } from "../../layout/TableFilter";
6
6
  import { Stack } from "../../../mui/Stack";
@@ -21,15 +21,18 @@ import { ShellToolbar } from "./data-views-shell-toolbar";
21
21
  import type { DisplayPanelView } from "./data-views-display-panel";
22
22
  import type { DataViewExport } from "./data-views-export";
23
23
  import { useDataViewsCopy } from "./data-views-copy-context";
24
- import { DataViewsEmpty } from "./data-views-empty";
24
+ import { ShellEmpty } from "./data-views-empty";
25
25
  import { DataViewsPagination } from "./data-views-pagination";
26
26
  import type { BoardConfig } from "./DataViewsBoard";
27
27
  import { type ScopeConfig } from "./data-views-scopes";
28
28
  import { ScopeTabsSlot } from "./data-views-scope-tabs";
29
+ import { SHELL_PROPS, useStickyBarTop } from "./data-views-sticky-head";
29
30
  import { togglePillValues } from "./data-views-grid-helpers";
31
+ import { toggleQuick } from "./data-views-overflow-fields";
30
32
  import type {
31
33
  DataViewCardSelection,
32
34
  FilterFieldConfig,
35
+ QuickFilterConfig,
33
36
  RangeFieldConfig,
34
37
  RangeValue,
35
38
  RowAction,
@@ -82,6 +85,8 @@ interface FilterSurfaceProps<T extends Record<string, unknown>> {
82
85
  onChangeRange: (fieldId: string, range: RangeValue) => void;
83
86
  onClearField: (fieldId: string) => void;
84
87
  onClearAll: () => void;
88
+ /** Press a quick chip (its field becomes `[value]`), or release the pressed one. */
89
+ onToggleQuick: (quick: QuickFilterConfig) => void;
85
90
  }
86
91
 
87
92
  /**
@@ -99,6 +104,7 @@ interface GridShellFiltersArgs<T extends Record<string, unknown>> {
99
104
  alwaysShowSearch: boolean;
100
105
  /** Is "Exportar" on the bar? Half the right cluster's cost when it is not. */
101
106
  hasExport: boolean;
107
+ quickFilters: QuickFilterConfig[];
102
108
  }
103
109
 
104
110
  function useGridShellFilters<T extends Record<string, unknown>>({
@@ -109,6 +115,7 @@ function useGridShellFilters<T extends Record<string, unknown>>({
109
115
  testIdPrefix,
110
116
  alwaysShowSearch,
111
117
  hasExport,
118
+ quickFilters,
112
119
  }: GridShellFiltersArgs<T>): {
113
120
  showInline: boolean;
114
121
  useModal: boolean;
@@ -125,7 +132,7 @@ function useGridShellFilters<T extends Record<string, unknown>>({
125
132
  // it can keep, and the TOOLBAR needs the same answer to drop its labels.
126
133
  // Measuring twice would let the two disagree at the crossover width.
127
134
  const split = useFilterOverflow(
128
- toOverflowFields(fields, rangeFields),
135
+ toOverflowFields(fields, rangeFields, quickFilters),
129
136
  state.pills,
130
137
  c.ranges,
131
138
  openControls > 0,
@@ -144,7 +151,11 @@ function useGridShellFilters<T extends Record<string, unknown>>({
144
151
  const useModal = false;
145
152
  const inlineVisible =
146
153
  showInline &&
147
- (alwaysShowSearch || fields.length > 0 || rangeFields.length > 0 || state.search !== "");
154
+ (alwaysShowSearch ||
155
+ fields.length > 0 ||
156
+ rangeFields.length > 0 ||
157
+ quickFilters.length > 0 ||
158
+ state.search !== "");
148
159
  const filterProps: FilterSurfaceProps<T> = {
149
160
  testIdPrefix,
150
161
  search: state.search,
@@ -159,6 +170,7 @@ function useGridShellFilters<T extends Record<string, unknown>>({
159
170
  onChangeRange: (fieldId, range) => c.patch({ ranges: { ...c.ranges, [fieldId]: range } }),
160
171
  onClearField: (fieldId) => c.patch({ pills: { ...state.pills, [fieldId]: [] } }),
161
172
  onClearAll: () => c.patch({ search: "", pills: {}, ranges: {} }),
173
+ onToggleQuick: (quick) => c.patch(({ pills }) => ({ pills: toggleQuick(pills, quick) })),
162
174
  };
163
175
  return {
164
176
  showInline,
@@ -237,6 +249,10 @@ interface GridShellProps<T extends Record<string, unknown>> {
237
249
  * every grid that ships filter fields already shows the row.
238
250
  */
239
251
  alwaysShowSearch?: boolean;
252
+ /** One-click toggle chips at the head of the bar — see `QuickFilterConfig`. */
253
+ quickFilters?: QuickFilterConfig[];
254
+ /** Pin the toolbar and the table header while the page scrolls. */
255
+ stickyToolbar?: boolean;
240
256
  }
241
257
 
242
258
  /** The scrollable content region: scope tabs, toolbar, filter bar, body, pager. */
@@ -250,22 +266,15 @@ function ShellStack<T extends Record<string, unknown>>({
250
266
  const copy = useDataViewsCopy();
251
267
  const { c, testIdPrefix, dataTestId, scopes = [], emptyState, inlineFilters = false } = props;
252
268
  const { showInline, useModal, inlineVisible, filterProps, split, onControlOpenChange } = filters;
253
- // The grid renders the FILTERED empty state itself — it is the only party
254
- // that knows a filter is applied. See {@link DataViewsEmpty}.
255
- const body = (
256
- <DataViewsEmpty
257
- filtered={c.activeFilterCount > 0 || c.state.search !== ""}
258
- onClearFilters={() => c.patch({ search: "", pills: {}, ranges: {} })}
259
- emptyState={emptyState}
260
- testIdPrefix={testIdPrefix}
261
- />
262
- );
269
+ const shellRef = useRef<HTMLDivElement>(null);
270
+ useStickyBarTop(shellRef, props.stickyToolbar === true);
271
+ const body = <ShellEmpty c={c} emptyState={emptyState} testIdPrefix={testIdPrefix} />;
263
272
  return (
264
273
  <TableFilter
265
274
  copy={copy.tableFilter} open={c.filterOpen} onOpenChange={c.setFilterOpen}
266
275
  hasActiveFilters={c.activeFilterCount > 0}
267
276
  >
268
- <Stack spacing={0} data-testid={dataTestId ? `${dataTestId}-container` : undefined}>
277
+ <Stack ref={shellRef} spacing={0} data-testid={dataTestId ? `${dataTestId}-container` : undefined} {...(props.stickyToolbar ? SHELL_PROPS : {})}>
269
278
  <GridHeaderRow title={props.title} headerActions={props.headerActions} testIdPrefix={testIdPrefix} />
270
279
  <ShellToolbar
271
280
  c={c}
@@ -291,6 +300,7 @@ function ShellStack<T extends Record<string, unknown>>({
291
300
  compactControls={showInline && split.compactControls}
292
301
  counterHidden={showInline && split.counterHidden}
293
302
  showInline={showInline}
303
+ sticky={props.stickyToolbar === true}
294
304
  />
295
305
  <ScopeTabsSlot
296
306
  scopes={scopes} scopeFieldId={props.scopeFieldId} board={props.board}
@@ -310,6 +320,7 @@ function ShellStack<T extends Record<string, unknown>>({
310
320
  dataTestId={dataTestId}
311
321
  testIdPrefix={testIdPrefix}
312
322
  emptyState={body}
323
+ stickyHead={props.stickyToolbar === true}
313
324
  />
314
325
  <DataViewsPagination c={c} testIdPrefix={testIdPrefix} />
315
326
  </TableFilter.Main>
@@ -343,6 +354,7 @@ export function GridShell<T extends Record<string, unknown>>(props: GridShellPro
343
354
  testIdPrefix,
344
355
  alwaysShowSearch,
345
356
  hasExport: props.exportConfig !== undefined,
357
+ quickFilters: props.quickFilters ?? [],
346
358
  });
347
359
  return (
348
360
  <DataViewsLayoutProvider
@@ -25,8 +25,10 @@ import { MoreFilters } from "./data-views-more-filters";
25
25
  import type { OverflowField, OverflowSplit } from "./data-views-overflow";
26
26
  import { CollapsedSearch, InlineKeyword } from "./data-views-search";
27
27
  import { PillControl } from "./data-views-category-pill";
28
+ import { isQuickActive } from "./data-views-overflow";
29
+ import { QuickChip } from "./data-views-quick-chip";
28
30
  import { RangePill } from "./data-views-range-pill";
29
- import type { RangeValue } from "./data-views-types";
31
+ import type { QuickFilterConfig, RangeValue } from "./data-views-types";
30
32
 
31
33
 
32
34
 
@@ -102,10 +104,12 @@ function InlineControl<T extends Record<string, unknown>>({
102
104
  onTogglePill,
103
105
  onChangeRange,
104
106
  onClearField,
107
+ onToggleQuick,
105
108
  onOpenChange,
106
109
  testIdPrefix,
107
110
  }: {
108
111
  field: OverflowField<T>;
112
+ onToggleQuick?: (quick: QuickFilterConfig) => void;
109
113
  onOpenChange?: (open: boolean) => void;
110
114
  pills: Record<string, string[]>;
111
115
  ranges: Record<string, RangeValue>;
@@ -114,6 +118,17 @@ function InlineControl<T extends Record<string, unknown>>({
114
118
  onClearField: (fieldId: string) => void;
115
119
  testIdPrefix: string;
116
120
  }): React.JSX.Element | null {
121
+ if (field.group === "quick" && field.quick) {
122
+ const quick = field.quick;
123
+ return (
124
+ <QuickChip
125
+ quick={quick}
126
+ active={isQuickActive(quick, pills)}
127
+ onToggle={() => onToggleQuick?.(quick)}
128
+ testIdPrefix={testIdPrefix}
129
+ />
130
+ );
131
+ }
117
132
  if (field.group === "pill" && field.pill) {
118
133
  return (
119
134
  <PillControl
@@ -202,7 +217,10 @@ export function InlineFilterControls<T extends Record<string, unknown>>({
202
217
  onClearAll,
203
218
  activeFilterCount,
204
219
  onControlOpenChange,
220
+ onToggleQuick,
205
221
  }: Omit<InlineFilterBarProps<T>, "fields" | "rangeFields"> & {
222
+ /** Presses or releases a quick chip — see `QuickFilterConfig`. */
223
+ onToggleQuick?: (quick: QuickFilterConfig) => void;
206
224
  /** How many filters are applied — decides whether "Limpar" is on the bar at all. */
207
225
  activeFilterCount: number;
208
226
  /** Reports a control opening/closing so the shell can freeze the measurement. */
@@ -250,6 +268,7 @@ export function InlineFilterControls<T extends Record<string, unknown>>({
250
268
  onTogglePill={onTogglePill}
251
269
  onChangeRange={onChangeRange}
252
270
  onClearField={onClearField}
271
+ onToggleQuick={onToggleQuick}
253
272
  onClearAll={onClearAll}
254
273
  onControlOpenChange={onControlOpenChange}
255
274
  activeFilterCount={activeFilterCount}
@@ -276,6 +295,7 @@ function FilterSlots<T extends Record<string, unknown>>({
276
295
  onTogglePill,
277
296
  onChangeRange,
278
297
  onClearField,
298
+ onToggleQuick,
279
299
  onClearAll,
280
300
  onControlOpenChange,
281
301
  activeFilterCount,
@@ -283,6 +303,7 @@ function FilterSlots<T extends Record<string, unknown>>({
283
303
  clearAllHidden,
284
304
  testIdPrefix,
285
305
  }: {
306
+ onToggleQuick?: (quick: QuickFilterConfig) => void;
286
307
  inline: OverflowField<T>[];
287
308
  overflow: OverflowField<T>[];
288
309
  pills: Record<string, string[]>;
@@ -309,6 +330,7 @@ function FilterSlots<T extends Record<string, unknown>>({
309
330
  onTogglePill={onTogglePill}
310
331
  onChangeRange={onChangeRange}
311
332
  onClearField={onClearField}
333
+ onToggleQuick={onToggleQuick}
312
334
  testIdPrefix={testIdPrefix}
313
335
  />
314
336
  ))}
@@ -322,6 +344,7 @@ function FilterSlots<T extends Record<string, unknown>>({
322
344
  ranges={ranges}
323
345
  onTogglePill={onTogglePill}
324
346
  onChangeRange={onChangeRange}
347
+ onToggleQuick={onToggleQuick}
325
348
  testIdPrefix={testIdPrefix}
326
349
  />
327
350
  {activeFilterCount > 0 && !clearAllHidden && (
@@ -21,7 +21,9 @@ import { PillControl } from "./data-views-category-pill";
21
21
  import type { OverflowField } from "./data-views-overflow";
22
22
  import { RangeBounds } from "./data-views-range-pill";
23
23
  import { isRangeSet } from "./data-views-range-values";
24
- import type { RangeValue } from "./data-views-types";
24
+ import type { QuickFilterConfig, RangeValue } from "./data-views-types";
25
+ import { QuickRow } from "./data-views-quick-chip";
26
+ import { isQuickActive } from "./data-views-overflow-fields";
25
27
  import { useDataViewsCopy } from "./data-views-copy-context";
26
28
  import { fieldRadiusPx } from "../../../tokens/field-radius";
27
29
  import { fieldHeight } from "../../../tokens/field-height";
@@ -34,6 +36,8 @@ export interface MoreFieldProps {
34
36
  ranges: Record<string, RangeValue>;
35
37
  onTogglePill: (fieldId: string, value: string, checked: boolean) => void;
36
38
  onChangeRange: (fieldId: string, range: RangeValue) => void;
39
+ /** Presses or releases a quick chip shed into the panel. */
40
+ onToggleQuick?: (quick: QuickFilterConfig) => void;
37
41
  testIdPrefix: string;
38
42
  }
39
43
 
@@ -202,6 +206,11 @@ export function fieldClearing<T extends Record<string, unknown>>({
202
206
  applied: boolean;
203
207
  clear: () => void;
204
208
  } {
209
+ if (field.group === "quick" && field.quick) {
210
+ const quick = field.quick;
211
+ const applied = isQuickActive(quick, pills);
212
+ return { applied, clear: () => onTogglePill(quick.fieldId, quick.value, false) };
213
+ }
205
214
  if (field.group === "range") {
206
215
  return {
207
216
  applied: isRangeSet(ranges[field.id] ?? {}),
@@ -216,6 +225,30 @@ export function fieldClearing<T extends Record<string, unknown>>({
216
225
  }
217
226
 
218
227
 
228
+ /**
229
+ * A quick chip shed into the panel: its own line, because label and count are
230
+ * already the whole control — no heading above it, no "Limpar" beside it.
231
+ */
232
+ export function MoreQuick({
233
+ quick,
234
+ pills,
235
+ onToggleQuick,
236
+ testIdPrefix,
237
+ }: {
238
+ quick: QuickFilterConfig;
239
+ } & Pick<MoreFieldProps, "pills" | "onToggleQuick" | "testIdPrefix">): React.JSX.Element {
240
+ return (
241
+ <Box sx={{ mb: 1, "&:last-of-type": { mb: 0 } }}>
242
+ <QuickRow
243
+ quick={quick}
244
+ active={isQuickActive(quick, pills)}
245
+ onToggle={() => onToggleQuick?.(quick)}
246
+ testIdPrefix={testIdPrefix}
247
+ />
248
+ </Box>
249
+ );
250
+ }
251
+
219
252
  /** One labelled group in the panel: the field's name, then its control. */
220
253
  export function MoreGroup<T extends Record<string, unknown>>({
221
254
  field,
@@ -16,7 +16,7 @@ import { Button } from "../../form/Button";
16
16
  import { Box } from "../../../mui/Box";
17
17
  import { Text } from "../../typography/Text";
18
18
 
19
- import { fieldClearing, MoreGroup, type MoreFieldProps } from "./data-views-more-fields";
19
+ import { fieldClearing, MoreGroup, MoreQuick, type MoreFieldProps } from "./data-views-more-fields";
20
20
  import type { OverflowField } from "./data-views-overflow";
21
21
  import { rem, sxRem } from "../../../tokens/relative";
22
22
 
@@ -182,6 +182,24 @@ function MoreFooter({
182
182
  );
183
183
  }
184
184
 
185
+ /** The panel's body: a shed quick chip as a toggle row, anything else as a labelled group. */
186
+ function MoreFields<T extends Record<string, unknown>>({
187
+ fields,
188
+ ...rest
189
+ }: MoreFiltersProps<T>): React.JSX.Element {
190
+ return (
191
+ <>
192
+ {fields.map((field) =>
193
+ field.quick ? (
194
+ <MoreQuick key={field.id} quick={field.quick} {...rest} />
195
+ ) : (
196
+ <MoreGroup key={field.id} field={field} {...rest} />
197
+ ),
198
+ )}
199
+ </>
200
+ );
201
+ }
202
+
185
203
  /** The overflow trigger + panel. Renders nothing when everything fits. */
186
204
  export function MoreFilters<T extends Record<string, unknown>>({
187
205
  fields,
@@ -249,9 +267,7 @@ export function MoreFilters<T extends Record<string, unknown>>({
249
267
  means something refused to, which is a bug to fix rather than a
250
268
  scrollbar to live with. */}
251
269
  <Box sx={{ maxHeight: sxRem(320), overflowY: "auto", overflowX: "hidden", p: 1.5, minWidth: 0 }}>
252
- {fields.map((field) => (
253
- <MoreGroup key={field.id} field={field} {...rest} />
254
- ))}
270
+ <MoreFields fields={fields} {...rest} />
255
271
  </Box>
256
272
  {anyApplied && (
257
273
  <MoreFooter onClearAll={onClearAll} testIdPrefix={rest.testIdPrefix} />
@@ -0,0 +1,110 @@
1
+ /**
2
+ * WHICH CONTROLS THE BAR DECLARES — split from `data-views-overflow` at the
3
+ * file-size gate. That module decides where each control goes; this one only
4
+ * lists them, in the order the bar renders them.
5
+ */
6
+ import type { OverflowField } from "./data-views-overflow";
7
+ import { isRangeSet } from "./data-views-overflow-costs";
8
+ import type { FilterFieldConfig, QuickFilterConfig, RangeFieldConfig, RangeValue } from "./data-views-types";
9
+
10
+ /**
11
+ * Every declared control in the order the bar renders them: quick chips first,
12
+ * then pills, then ranges.
13
+ *
14
+ * A pill field a quick chip writes to is NOT a control of its own — the chips
15
+ * stand in for it (see `QuickFilterConfig`) — so it is left out here, and only
16
+ * here: it stays in `fields`, where the active count and saved views read it.
17
+ */
18
+ export function toOverflowFields<T extends Record<string, unknown>>(
19
+ fields: FilterFieldConfig<T>[],
20
+ rangeFields: RangeFieldConfig<T>[],
21
+ quickFilters: QuickFilterConfig[] = [],
22
+ ): OverflowField<T>[] {
23
+ const chipFields = new Set(quickFilters.map((quick) => quick.fieldId));
24
+ return [
25
+ ...quickFilters.map((quick) => ({
26
+ id: `quick:${quick.id}`,
27
+ label: quick.label,
28
+ group: "quick" as const,
29
+ quick,
30
+ })),
31
+ ...fields
32
+ .filter((pill) => !chipFields.has(pill.id))
33
+ .map((pill) => ({ id: pill.id, label: pill.label, group: "pill" as const, pill, pinned: pill.inMore === true })),
34
+ ...rangeFields.map((range) => ({
35
+ id: range.id,
36
+ label: range.label,
37
+ group: "range" as const,
38
+ range,
39
+ pinned: range.inMore === true,
40
+ })),
41
+ ];
42
+ }
43
+
44
+ /**
45
+ * Is this quick chip pressed — its one value the field's WHOLE selection?
46
+ *
47
+ * Exactly, not "includes": a field holding two values (from a saved view, the
48
+ * URL or the side panel) is not what either chip applies, and reading both as
49
+ * pressed made pressing one clear the other.
50
+ */
51
+ export function isQuickActive(quick: QuickFilterConfig, pills: Record<string, string[]>): boolean {
52
+ const selected = pills[quick.fieldId] ?? [];
53
+ return selected.length === 1 && selected[0] === quick.value;
54
+ }
55
+
56
+ /**
57
+ * Is any DECLARED filter applied — a pill value or a bounded range, wherever it
58
+ * is drawn (on the bar, in "Mais", or as a chip)? Only declared ids count: the
59
+ * bar draws "Limpar" on the active count, which ignores a stale key a saved view
60
+ * still carries for a field since removed.
61
+ */
62
+ export function anyApplied<T extends Record<string, unknown>>(
63
+ all: OverflowField<T>[],
64
+ pills: Record<string, string[]>,
65
+ ranges: Record<string, RangeValue>,
66
+ ): boolean {
67
+ return all.some((field) => {
68
+ if (field.group === "range") return isRangeSet(ranges[field.id]);
69
+ const id = field.quick?.fieldId ?? field.id;
70
+ return (pills[id]?.length ?? 0) > 0;
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Quick chips stay a PREFIX of their declared order on the bar.
76
+ *
77
+ * The keep-what-fits loop skips a control that does not fit and tries the next,
78
+ * which for pills is right (any narrower one is worth the room) and for chips
79
+ * is not: "Zerado" alone on the bar, with "Estoque baixo" in "Mais", reads as a
80
+ * different bar from the one declared. So the first chip that went to "Mais"
81
+ * takes every later chip with it. Their room is NOT re-spent here — `used`
82
+ * drops, and the ladder's later passes can hand it to the furniture.
83
+ */
84
+ export function keepChipsInOrder<T extends Record<string, unknown>>(
85
+ split: { inline: OverflowField<T>[]; overflow: OverflowField<T>[]; used: number },
86
+ chips: OverflowField<T>[],
87
+ cost: (field: OverflowField<T>) => number,
88
+ ): { inline: OverflowField<T>[]; overflow: OverflowField<T>[]; used: number } {
89
+ const firstOut = chips.findIndex((chip) => split.overflow.includes(chip));
90
+ if (firstOut < 0) return split;
91
+ const evicted = chips.slice(firstOut).filter((chip) => split.inline.includes(chip));
92
+ if (evicted.length === 0) return split;
93
+ return {
94
+ inline: split.inline.filter((field) => !evicted.includes(field)),
95
+ overflow: [...chips.slice(firstOut), ...split.overflow.filter((field) => !chips.includes(field))],
96
+ used: split.used - evicted.reduce((sum, chip) => sum + cost(chip), 0),
97
+ };
98
+ }
99
+
100
+
101
+
102
+ /**
103
+ * A quick chip's press, as a pill write. Exclusive within the field: the chip's
104
+ * value REPLACES the selection, and pressing the pressed chip empties it.
105
+ */
106
+ export function toggleQuick(pills: Record<string, string[]>, quick: QuickFilterConfig): Record<string, string[]> {
107
+ const pressed = isQuickActive(quick, pills);
108
+ return { ...pills, [quick.fieldId]: pressed ? [] : [quick.value] };
109
+ }
110
+
@@ -15,7 +15,11 @@ import {
15
15
  rightClusterCost,
16
16
  type Prices,
17
17
  } from "./data-views-overflow-costs";
18
- import type { FilterFieldConfig, RangeFieldConfig, RangeValue } from "./data-views-types";
18
+ import type { FilterFieldConfig, QuickFilterConfig, RangeFieldConfig, RangeValue } from "./data-views-types";
19
+ import { anyApplied, isQuickActive, keepChipsInOrder } from "./data-views-overflow-fields";
20
+
21
+ // Declaring the controls lives in its own module (size gate); re-exported for its callers.
22
+ export { isQuickActive, toOverflowFields } from "./data-views-overflow-fields";
19
23
 
20
24
  /**
21
25
  * PROGRESSIVE COLLAPSE — which filter controls fit on the toolbar, measured.
@@ -44,14 +48,17 @@ import type { FilterFieldConfig, RangeFieldConfig, RangeValue } from "./data-vie
44
48
  * or changes a rendered row.
45
49
  */
46
50
 
47
- /** A filter control on the bar: a multi-select pill or a min/max range. */
51
+ /** A filter control on the bar: a multi-select pill, a min/max range or a quick chip. */
48
52
  export interface OverflowField<T extends Record<string, unknown>> {
49
53
  id: string;
50
54
  label: string;
51
55
  /** Which control it is — the caller renders the right one. */
52
- group: "pill" | "range";
56
+ group: "pill" | "range" | "quick";
53
57
  pill?: FilterFieldConfig<T>;
54
58
  range?: RangeFieldConfig<T>;
59
+ quick?: QuickFilterConfig;
60
+ /** Declared `inMore`: never on the bar, always behind "Mais". */
61
+ pinned?: boolean;
55
62
  }
56
63
 
57
64
  /** What the caller renders inline, and what goes behind "Mais". */
@@ -98,24 +105,13 @@ export interface OverflowSplit<T extends Record<string, unknown>> {
98
105
  barRef: React.RefObject<HTMLDivElement | null>;
99
106
  }
100
107
 
101
- /** Every declared control, pills first, in the order the bar renders them. */
102
- export function toOverflowFields<T extends Record<string, unknown>>(
103
- fields: FilterFieldConfig<T>[],
104
- rangeFields: RangeFieldConfig<T>[],
105
- ): OverflowField<T>[] {
106
- return [
107
- ...fields.map((pill) => ({ id: pill.id, label: pill.label, group: "pill" as const, pill })),
108
- ...rangeFields.map((range) => ({ id: range.id, label: range.label, group: "range" as const, range })),
109
- ];
110
- }
111
-
112
-
113
108
  /** Is this control carrying a value the operator set? */
114
109
  function isActiveField<T extends Record<string, unknown>>(
115
110
  field: OverflowField<T>,
116
111
  pills: Record<string, string[]>,
117
112
  ranges: Record<string, RangeValue>,
118
113
  ): boolean {
114
+ if (field.group === "quick" && field.quick) return isQuickActive(field.quick, pills);
119
115
  return field.group === "pill"
120
116
  ? (pills[field.id]?.length ?? 0) > 0
121
117
  : isRangeSet(ranges[field.id]);
@@ -128,6 +124,8 @@ function fieldWidth<T extends Record<string, unknown>>(
128
124
  pills: Record<string, string[]>,
129
125
  ranges: Record<string, RangeValue>,
130
126
  ): number {
127
+ // A chip is a pill with its count beside the label and no chevron.
128
+ if (field.group === "quick") return estimateWidth(theme, `${field.label} ${field.quick?.count ?? ""}`, -20);
131
129
  if (field.group === "pill") return estimateWidth(theme, pillText(field, pills[field.id] ?? []), 0);
132
130
  // A bounded range renders two number inputs (100 design px) beside its label.
133
131
  return estimateWidth(theme, field.label, isRangeSet(ranges[field.id]) ? 100 : 0);
@@ -154,13 +152,25 @@ function splitFilters<T extends Record<string, unknown>>(
154
152
  /** Whose type scale every price is read at — the bar's own. */
155
153
  theme: Theme,
156
154
  ): Split<T> {
157
- const active = all.filter((field) => isActiveField(field, pills, ranges));
158
- const idle = all.filter((field) => !isActiveField(field, pills, ranges));
155
+ // `inMore` fields never compete for the bar: they are behind "Mais" from the
156
+ // start, so "Mais" is on the bar whatever else fits.
157
+ const pinned = all.filter((field) => field.pinned);
158
+ const candidates = all.filter((field) => !field.pinned);
159
+ const chips = candidates.filter((field) => field.group === "quick");
160
+ const rest = candidates.filter((field) => field.group !== "quick");
161
+ const active = rest.filter((field) => isActiveField(field, pills, ranges));
162
+ const idle = rest.filter((field) => !isActiveField(field, pills, ranges));
159
163
  const price = pricesFor(theme);
164
+ const moreCost = compact ? price.overflowButtonCompact : price.overflowButton;
160
165
  const gap = remPx(theme, RESERVED.betweenControls);
161
166
 
162
- // "Limpar" rides the end of the cluster whenever anything is applied.
163
- const clearCost = active.length > 0 ? price.clearAll + gap : 0;
167
+ // "Limpar" rides the end of the cluster whenever anything is applied —
168
+ // ANYTHING, a pinned field and a chip's field included, since the bar draws
169
+ // it on the active count, not on what it can see. Pressing a chip can
170
+ // therefore cost the bar room; chips rank first and stay a prefix (below),
171
+ // so what leaves is an idle pill, or the chips together — never one chip
172
+ // swapped for another.
173
+ const clearCost = anyApplied(all, pills, ranges) ? price.clearAll + gap : 0;
164
174
  // APPLIED FIRST, BUT NOT EXEMPT.
165
175
  //
166
176
  // Applied controls take the visible slots ahead of idle ones — that part was
@@ -174,20 +184,30 @@ function splitFilters<T extends Record<string, unknown>>(
174
184
  // are in there (see `MoreTrigger`), which is the same signal the pill itself
175
185
  // was carrying. A control you scroll off-screen carries no signal at all.
176
186
  //
177
- // The keep-what-fits loop itself is `splitToFit` (`utility/Overflow`) — the
178
- // one part of this that is not filter-shaped, and the part a second cluster
179
- // in the design system would otherwise have had to reimplement.
180
- const split = splitToFit(all, {
181
- widthOf: (field) => fieldWidth(theme, field, pills, ranges),
182
- keyOf: (field) => field.id,
183
- gap,
184
- available: width - furniture - clearCost,
185
- // The overflow button at the width it will actually have, which on a phone
186
- // is the icon-and-badge one.
187
- overflowCost: compact ? price.overflowButtonCompact : price.overflowButton,
188
- priority: [...active, ...idle],
189
- });
190
- return { ...split, used: split.used + clearCost };
187
+ // Quick chips come before all of it, in their DECLARED order whatever is
188
+ // pressed: they are the bar's fixed vocabulary, so their slots never move.
189
+ //
190
+ // The keep-what-fits loop is `splitToFit` (`utility/Overflow`). With pinned
191
+ // fields "Mais" is always drawn, so its cost is paid up front.
192
+ const pinnedCost = pinned.length > 0 ? moreCost + gap : 0;
193
+ const widthOf = (field: OverflowField<T>): number => fieldWidth(theme, field, pills, ranges);
194
+ const split = keepChipsInOrder(
195
+ splitToFit(candidates, {
196
+ widthOf,
197
+ keyOf: (field) => field.id,
198
+ gap,
199
+ available: width - furniture - clearCost - pinnedCost,
200
+ overflowCost: pinned.length > 0 ? 0 : moreCost,
201
+ priority: [...chips, ...active, ...idle],
202
+ }),
203
+ chips,
204
+ (field) => widthOf(field) + gap,
205
+ );
206
+ return {
207
+ inline: split.inline,
208
+ overflow: [...split.overflow, ...pinned],
209
+ used: split.used + clearCost + pinnedCost,
210
+ };
191
211
  }
192
212
 
193
213
  /**
@@ -219,8 +239,8 @@ function computeSplit<T extends Record<string, unknown>>(
219
239
  // Unmeasured (SSR, or jsdom without a ResizeObserver) ⇒ degrade nothing.
220
240
  if (width === 0) {
221
241
  return {
222
- inline: all,
223
- overflow: [],
242
+ inline: all.filter((field) => !field.pinned),
243
+ overflow: all.filter((field) => field.pinned),
224
244
  compactControls: false,
225
245
  counterHidden: false,
226
246
  searchCollapsed: false,
@@ -350,7 +370,9 @@ export function useFilterOverflow<T extends Record<string, unknown>>(
350
370
  // The prices follow the theme's type scale, so the answer does too.
351
371
  const theme = useTheme();
352
372
  const signature = JSON.stringify({
353
- ids: all.map((field) => field.id),
373
+ // A chip's count and a field's `inMore` change its price or its place
374
+ // without changing its id, so both are part of the key.
375
+ ids: all.map((field) => [field.id, field.quick?.count ?? null, field.pinned === true]),
354
376
  pills,
355
377
  ranges,
356
378
  width,