@12-apps/ui 1.5.1 → 1.6.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 (39) hide show
  1. package/package.json +5 -5
  2. package/src/components/data-display/DataViews/DataViewsBoard.tsx +327 -0
  3. package/src/components/data-display/DataViews/DataViewsGrid.tsx +80 -77
  4. package/src/components/data-display/DataViews/DataViewsTableBase.tsx +178 -230
  5. package/src/components/data-display/DataViews/DeleteViewDialog.tsx +83 -0
  6. package/src/components/data-display/DataViews/SaveViewModal.tsx +140 -52
  7. package/src/components/data-display/DataViews/data-views-columns-tab.tsx +240 -0
  8. package/src/components/data-display/DataViews/data-views-columns.ts +129 -0
  9. package/src/components/data-display/DataViews/data-views-dirty.ts +62 -0
  10. package/src/components/data-display/DataViews/data-views-display-panel.tsx +328 -0
  11. package/src/components/data-display/DataViews/data-views-display-tabs.tsx +328 -0
  12. package/src/components/data-display/DataViews/data-views-empty.tsx +91 -0
  13. package/src/components/data-display/DataViews/data-views-export.tsx +260 -0
  14. package/src/components/data-display/DataViews/data-views-glyphs.tsx +103 -0
  15. package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +340 -0
  16. package/src/components/data-display/DataViews/data-views-grid-parts.tsx +220 -242
  17. package/src/components/data-display/DataViews/data-views-inline-bar.tsx +187 -81
  18. package/src/components/data-display/DataViews/data-views-layout-context.tsx +215 -79
  19. package/src/components/data-display/DataViews/data-views-more-filters.tsx +300 -0
  20. package/src/components/data-display/DataViews/data-views-overflow.ts +302 -0
  21. package/src/components/data-display/DataViews/data-views-preview.ts +11 -3
  22. package/src/components/data-display/DataViews/data-views-range-pill.tsx +162 -92
  23. package/src/components/data-display/DataViews/data-views-range-values.ts +69 -0
  24. package/src/components/data-display/DataViews/data-views-scopes.tsx +356 -0
  25. package/src/components/data-display/DataViews/data-views-search.tsx +156 -0
  26. package/src/components/data-display/DataViews/data-views-select-all-strip.tsx +105 -0
  27. package/src/components/data-display/DataViews/data-views-selection.ts +50 -0
  28. package/src/components/data-display/DataViews/data-views-server-query.ts +75 -0
  29. package/src/components/data-display/DataViews/data-views-shell-toolbar.tsx +87 -0
  30. package/src/components/data-display/DataViews/data-views-table-parts.tsx +5 -0
  31. package/src/components/data-display/DataViews/data-views-toolbar.tsx +63 -48
  32. package/src/components/data-display/DataViews/data-views-types.ts +88 -9
  33. package/src/components/data-display/DataViews/data-views-view-nav.tsx +378 -0
  34. package/src/components/data-display/DataViews/index.ts +14 -1
  35. package/src/components/data-display/DataViews/use-data-views-state.ts +158 -134
  36. package/src/components/data-display/DataViews/use-saved-views-controller.ts +171 -0
  37. package/src/components/layout/ContentToolbar/ContentToolbar.tsx +86 -30
  38. package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +25 -0
  39. package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +6 -1
@@ -98,6 +98,58 @@ function SelectionCluster({
98
98
  );
99
99
  }
100
100
 
101
+ /**
102
+ * The BROWSING half of the toolbar: search + filters on the left, the page's
103
+ * own controls on the right. Split out of {@link ContentToolbar} so that
104
+ * function stays inside the complexity gate — it is layout only, and the
105
+ * decision of whether to show it at all is made by the caller.
106
+ */
107
+ function BrowsingClusters({
108
+ leadingControls,
109
+ rightControls,
110
+ hasSelection,
111
+ edgeAlign,
112
+ }: {
113
+ leadingControls?: React.ReactNode;
114
+ rightControls: React.ReactNode;
115
+ hasSelection: boolean;
116
+ edgeAlign: boolean;
117
+ }): React.JSX.Element {
118
+ return (
119
+ <>
120
+ {/* minWidth:0 lets this cluster be the one that gives up width as the row
121
+ * tightens, which is what the filter overflow measures against. */}
122
+ {leadingControls !== undefined && (
123
+ <Box sx={{ display: 'flex', alignItems: 'center', flexGrow: 1, minWidth: 0 }}>
124
+ {leadingControls}
125
+ </Box>
126
+ )}
127
+ {/* ml:auto keeps the controls right-aligned even when the cluster wraps to
128
+ * its own row (where justify-content:space-between would otherwise snap a
129
+ * lone item to the left); it's a no-op on a shared row. */}
130
+ <Box
131
+ sx={{
132
+ display: 'flex',
133
+ alignItems: 'center',
134
+ // Same single-line rule as the row itself: this cluster wrapping
135
+ // INTERNALLY buys a second line just as surely as the row wrapping
136
+ // does. It bit at exactly 900px, the `md` breakpoint where the "N de
137
+ // N" counter appears and pushes Exibir/Exportar over the edge.
138
+ flexWrap: leadingControls === undefined ? 'wrap' : 'nowrap',
139
+ // Tight (5px) when browsing so the controls fit one line beside the
140
+ // checkbox; roomier (14px) in selection mode where they get their own row.
141
+ gap: hasSelection ? '14px' : '5px',
142
+ ml: 'auto',
143
+ mr: edgeAlign ? -1.5 : 0,
144
+ '& > *': { flexShrink: 0 },
145
+ }}
146
+ >
147
+ {rightControls}
148
+ </Box>
149
+ </>
150
+ );
151
+ }
152
+
101
153
  /**
102
154
  * Shared toolbar for content pages (Favorites, Personal Space, Recents, …). The
103
155
  * left cluster owns selection chrome — **Select All**, and once items are
@@ -133,52 +185,56 @@ export function ContentToolbar({
133
185
  selectAll,
134
186
  clearSelection,
135
187
  rightControls,
188
+ leadingControls,
136
189
  actions,
137
190
  selectAllTestId,
138
191
  clearAllTestId,
139
192
  edgeAlign = false,
193
+ exclusiveSelection = false,
140
194
  }: ContentToolbarProps): React.JSX.Element {
195
+ // Under `exclusiveSelection` the two states are alternatives, not neighbours:
196
+ // browsing shows the controls and no checkbox, selecting shows the checkbox
197
+ // and no controls. Anything else double-books the row.
198
+ const showSelection = !exclusiveSelection || hasSelection;
199
+ const showControls = !exclusiveSelection || !hasSelection;
141
200
  return (
142
- // Wrap the two clusters onto separate rows when they can't share one line
143
- // (narrow screens / phones) instead of letting the controls collide.
201
+ // Two clusters on one line. They may wrap onto separate rows ONLY for a
202
+ // toolbar without `leadingControls` — one carrying search + filters is
203
+ // driven by a measured collapse ladder (see `useFilterOverflow`), and a
204
+ // wrap there is the ladder having failed: it would silently buy a second
205
+ // row instead of shedding a control into "Mais", which is the one outcome
206
+ // the ladder exists to prevent.
144
207
  <Box
145
208
  sx={{
146
209
  display: 'flex',
147
210
  alignItems: 'center',
148
211
  justifyContent: 'space-between',
149
212
  gap: 2,
150
- flexWrap: 'wrap',
213
+ flexWrap: leadingControls === undefined ? 'wrap' : 'nowrap',
151
214
  rowGap: 1,
152
215
  }}
153
216
  >
154
- <SelectionCluster
155
- hasSelection={hasSelection}
156
- selectedCount={selectedCount}
157
- selectAll={selectAll}
158
- clearSelection={clearSelection}
159
- actions={actions}
160
- selectAllTestId={selectAllTestId}
161
- clearAllTestId={clearAllTestId}
162
- edgeAlign={edgeAlign}
163
- />
217
+ {showSelection && (
218
+ <SelectionCluster
219
+ hasSelection={hasSelection}
220
+ selectedCount={selectedCount}
221
+ selectAll={selectAll}
222
+ clearSelection={clearSelection}
223
+ actions={actions}
224
+ selectAllTestId={selectAllTestId}
225
+ clearAllTestId={clearAllTestId}
226
+ edgeAlign={edgeAlign}
227
+ />
228
+ )}
164
229
 
165
- {/* ml:auto keeps the controls right-aligned even when the cluster wraps to
166
- * its own row (where justify-content:space-between would otherwise snap a
167
- * lone item to the left); it's a no-op on a shared row. */}
168
- <Box
169
- sx={{
170
- display: 'flex',
171
- alignItems: 'center',
172
- flexWrap: 'wrap',
173
- // Tight (5px) when browsing so the controls fit one line beside the
174
- // checkbox; roomier (14px) in selection mode where they get their own row.
175
- gap: hasSelection ? '14px' : '5px',
176
- ml: 'auto',
177
- mr: edgeAlign ? -1.5 : 0,
178
- }}
179
- >
180
- {rightControls}
181
- </Box>
230
+ {showControls && (
231
+ <BrowsingClusters
232
+ leadingControls={leadingControls}
233
+ rightControls={rightControls}
234
+ hasSelection={hasSelection}
235
+ edgeAlign={edgeAlign}
236
+ />
237
+ )}
182
238
  </Box>
183
239
  );
184
240
  }
@@ -14,6 +14,22 @@ export interface ContentToolbarProps {
14
14
  clearSelection: () => void;
15
15
  /** Page-specific right-aligned controls (ViewSelector, SortByDropdown, …). */
16
16
  rightControls: ReactNode;
17
+ /**
18
+ * Controls that share the toolbar's own line, left-aligned after the
19
+ * selection cluster — the search box and the filter controls, which belong on
20
+ * the toolbar rather than on a row of their own. Left unset the toolbar keeps
21
+ * its two-cluster shape.
22
+ */
23
+ leadingControls?: ReactNode;
24
+ /**
25
+ * Selection OWNS the toolbar line instead of sharing it. Nothing selected ⇒
26
+ * no Select All at all (the row is search + filters + controls); something
27
+ * selected ⇒ those give way entirely and only the selection cluster remains.
28
+ *
29
+ * Off by default: a toolbar that already shows Select All permanently keeps
30
+ * doing so, so this cannot move a control on a consumer that did not ask.
31
+ */
32
+ exclusiveSelection?: boolean;
17
33
  /**
18
34
  * Selection actions slot, rendered after the count when items are selected
19
35
  * (e.g. a "Delete" / "Send email" control). Kept generic — the consumer owns
@@ -134,6 +150,15 @@ export interface MultiSelectDropdownProps<TValue extends string = string> {
134
150
  */
135
151
  layout?: 'inline' | 'stacked' | 'pill';
136
152
  'data-testid'?: string;
153
+ /**
154
+ * Notified when the menu opens and closes.
155
+ *
156
+ * For a toolbar whose LAYOUT is measured: applying a filter changes this
157
+ * control's label and therefore its width, and re-measuring mid-interaction
158
+ * moves the row under the operator's cursor. A caller that measures uses this
159
+ * to hold still while the menu is open.
160
+ */
161
+ onOpenChange?: (open: boolean) => void;
137
162
  }
138
163
 
139
164
  /* ── FilterTrigger ───────────────────────────────────────────────────────── */
@@ -67,6 +67,7 @@ export function MultiSelectDropdown<TValue extends string = string>({
67
67
  noResultsLabel = 'No results',
68
68
  layout = 'inline',
69
69
  'data-testid': testId,
70
+ onOpenChange,
70
71
  }: MultiSelectDropdownProps<TValue>): React.JSX.Element {
71
72
  const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
72
73
  const [query, setQuery] = useState('');
@@ -76,6 +77,7 @@ export function MultiSelectDropdown<TValue extends string = string>({
76
77
  const close = (): void => {
77
78
  setAnchorEl(null);
78
79
  setQuery('');
80
+ onOpenChange?.(false);
79
81
  };
80
82
  const triggerLabel = buildTriggerLabel(selected, options, allLabel);
81
83
  // Auto-enable search for long lists (e.g. many categories) unless overridden.
@@ -83,7 +85,10 @@ export function MultiSelectDropdown<TValue extends string = string>({
83
85
  const anyExtraChecked = extraOptions?.some((option) => option.checked) ?? false;
84
86
  const clearDisabled = selected.size === 0 && !anyExtraChecked;
85
87
 
86
- const openMenu = (event: React.MouseEvent<HTMLElement>): void => setAnchorEl(event.currentTarget);
88
+ const openMenu = (event: React.MouseEvent<HTMLElement>): void => {
89
+ setAnchorEl(event.currentTarget);
90
+ onOpenChange?.(true);
91
+ };
87
92
  const triggerProps = { triggerLabel, open, onOpen: openMenu, testId };
88
93
 
89
94
  const renderTrigger = (): React.JSX.Element => {