@12-apps/ui 3.2.0 → 3.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/ui",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "license": "MIT",
5
5
  "packageManager": "pnpm@9.0.0",
6
6
  "type": "module",
@@ -93,7 +93,7 @@ export const Chart: React.FC<ChartProps> = (props) => {
93
93
  <ChartContainer
94
94
  responsive={props.responsive ?? true}
95
95
  width={props.width ?? '100%'}
96
- height={sizeStyles.height}
96
+ height={props.fillHeight === true ? '100%' : sizeStyles.height}
97
97
  data-testid={`${dataTestId}-container`}
98
98
  >
99
99
  {renderChartByType(props, theme)}
@@ -83,6 +83,16 @@ export interface ChartProps {
83
83
  maxCategoryTicks?: number;
84
84
  /** Category tick labels longer than this are truncated with an ellipsis. Default 12. */
85
85
  tickLabelMaxChars?: number;
86
+ /**
87
+ * Take the height of the box this chart is placed in, rather than the size
88
+ * preset's fixed one.
89
+ *
90
+ * For a host that has already sized the chart's container — a dashboard cell
91
+ * with a chosen height, say. The preset is a sensible default precisely
92
+ * because most callers have NOT sized anything, so this is opt-in: without
93
+ * it a chart in a box with no definite height would collapse to nothing.
94
+ */
95
+ fillHeight?: boolean;
86
96
  /** Bar corner radius in px (top corners only; stacked segments stay square). Default 3. */
87
97
  barRadius?: number;
88
98
  /**
@@ -15,7 +15,16 @@ export const ChartTooltip = Tooltip;
15
15
  export const ChartLegend = Legend;
16
16
 
17
17
  export interface ChartContainerProps {
18
- height: number;
18
+ /**
19
+ * A number of pixels, or a PERCENTAGE of the box this container is given.
20
+ *
21
+ * The difference is not cosmetic: Recharts sizes the chart from the number
22
+ * when it is one and only MEASURES its own box when it is a percentage
23
+ * (`calculateChartDimensions`). So a container stretched by its parent's
24
+ * layout goes on drawing a 300px chart inside a 600px box — visibly, as
25
+ * blank space under the plot — until the height is handed over as a percent.
26
+ */
27
+ height: number | `${number}%`;
19
28
  width?: number | string;
20
29
  responsive?: boolean;
21
30
  children: React.ReactElement;
@@ -12,6 +12,8 @@ export interface SpecChartProps {
12
12
  spec: ChartSpec;
13
13
  data: ChartDataPoint[];
14
14
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
15
+ /** Take the height of the box this chart is placed in — see `ChartProps`. */
16
+ fillHeight?: boolean;
15
17
  loading?: boolean;
16
18
  className?: string;
17
19
  onClick?: (point: ChartDataPoint) => void;
@@ -27,6 +29,7 @@ export const SpecChart: React.FC<SpecChartProps> = ({
27
29
  spec,
28
30
  data,
29
31
  size,
32
+ fillHeight,
30
33
  loading,
31
34
  className,
32
35
  onClick,
@@ -45,6 +48,7 @@ export const SpecChart: React.FC<SpecChartProps> = ({
45
48
  <Chart
46
49
  {...chartProps}
47
50
  size={size}
51
+ fillHeight={fillHeight}
48
52
  loading={loading}
49
53
  className={className}
50
54
  onClick={onClick}
@@ -2,6 +2,8 @@
2
2
 
3
3
  import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
4
4
  import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
5
+ import useMediaQuery from "@mui/material/useMediaQuery";
6
+ import type { Theme } from "@mui/material/styles";
5
7
 
6
8
  import type { SortFieldDefinition } from "../../layout/ContentToolbar";
7
9
  import { Box } from "../../../mui/Box";
@@ -286,9 +288,44 @@ const LAYOUT_TILE_LABELS: Record<DataViewsLayout, string> = {
286
288
  * `data-views-glyphs.tsx` for why. A layout this table cannot render is simply
287
289
  * not offered, and the panel says why rather than leaving a gap.
288
290
  */
291
+ /**
292
+ * The three density tiles — or, on a phone, a line saying why there are none:
293
+ * the card grid is `1fr` there, so all three would draw one card per row. Says
294
+ * so rather than vanishing, for the same reason as the board note below it.
295
+ */
296
+ function DensityTiles({ testIdPrefix }: { testIdPrefix: string }): React.JSX.Element {
297
+ const { layout, density, setDensity } = useDataViewsLayout();
298
+ // Matches the card grid's own `xs: "1fr"` track, so the control and the thing
299
+ // it controls agree on where density stops meaning anything.
300
+ const isOneColumn = useMediaQuery((t: Theme) => t.breakpoints.down("sm"));
301
+ if (layout === "cards" && isOneColumn) {
302
+ return (
303
+ <Text variant="caption" as="p">
304
+ <Box component="span" sx={{ px: 0.5, pt: 0.5, display: "block", color: "text.disabled" }}>
305
+ Um card por linha nesta largura — a densidade volta em telas maiores.
306
+ </Box>
307
+ </Text>
308
+ );
309
+ }
310
+ return (
311
+ <Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 0.75, mt: 0.5 }}>
312
+ {densityTiles(layout).map((tile) => (
313
+ <Tile
314
+ key={tile.value}
315
+ active={density === tile.value}
316
+ label={tile.label}
317
+ onClick={() => setDensity(tile.value)}
318
+ testId={`${testIdPrefix}-density-${tile.value}`}
319
+ >
320
+ {densityGlyph(layout, tile, density === tile.value)}
321
+ </Tile>
322
+ ))}
323
+ </Box>
324
+ );
325
+ }
326
+
289
327
  export function DisplayTab({ testIdPrefix }: { testIdPrefix: string }): React.JSX.Element {
290
- const { layout, setLayout, canUseCards, canUseList, canUseBoard, density, setDensity } =
291
- useDataViewsLayout();
328
+ const { layout, setLayout, canUseCards, canUseList, canUseBoard } = useDataViewsLayout();
292
329
  const available: DataViewsLayout[] = [
293
330
  "table",
294
331
  ...(canUseList ? (["list"] as const) : []),
@@ -336,19 +373,7 @@ export function DisplayTab({ testIdPrefix }: { testIdPrefix: string }): React.JS
336
373
  {DENSITY_HEADINGS[layout]}
337
374
  </Box>
338
375
  </Text>
339
- <Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 0.75, mt: 0.5 }}>
340
- {densityTiles(layout).map((tile) => (
341
- <Tile
342
- key={tile.value}
343
- active={density === tile.value}
344
- label={tile.label}
345
- onClick={() => setDensity(tile.value)}
346
- testId={`${testIdPrefix}-density-${tile.value}`}
347
- >
348
- {densityGlyph(layout, tile, density === tile.value)}
349
- </Tile>
350
- ))}
351
- </Box>
376
+ <DensityTiles testIdPrefix={testIdPrefix} />
352
377
  {!canUseBoard && (
353
378
  <Text variant="caption" as="p">
354
379
  <Box component="span" sx={{ px: 0.5, pt: 1.25, display: "block", color: "text.disabled" }}>
@@ -6,14 +6,14 @@ import { DataGrid, type GridColumn, type GridSort } from "../DataGrid";
6
6
  import { Box } from "../../../mui/Box";
7
7
 
8
8
  import {
9
- cardMinWidthForZoom,
9
+ cardTargetWidthFor,
10
10
  cardScaleForZoom,
11
11
  DENSITY_BOARD_SCALE,
12
- DENSITY_CARD_COLUMNS,
13
12
  DENSITY_ROW_PADDING,
14
13
  useDataViewsLayout,
15
14
  type DataViewsDensity,
16
15
  } from "./data-views-layout-context";
16
+ import { cardGridTracks } from "./data-views-grid-helpers";
17
17
  import { DataViewsBoard, type BoardConfig } from "./DataViewsBoard";
18
18
  import { SelectAllStrip } from "./data-views-select-all-strip";
19
19
  import { ListCardGroup, type ListGroupConfig } from "./list-card-rails";
@@ -90,9 +90,8 @@ interface CardBodyProps<T extends Record<string, unknown>> {
90
90
  getRowId: (row: T) => string | number;
91
91
  selectedIds: Set<string | number>;
92
92
  onToggleId: (id: string | number) => void;
93
- minCardWidth: number;
94
- /** Cap on cards per row, from the density preference. */
95
- maxColumns: number;
93
+ /** The width each card asks for; `auto-fill` turns it into a column count. */
94
+ targetCardWidth: number;
96
95
  /** Content scale (padding + type) handed to each card, from the zoom slider. */
97
96
  cardScale: number;
98
97
  dataTestId?: string;
@@ -112,8 +111,7 @@ function CardBody<T extends Record<string, unknown>>({
112
111
  getRowId,
113
112
  selectedIds,
114
113
  onToggleId,
115
- minCardWidth,
116
- maxColumns,
114
+ targetCardWidth,
117
115
  cardScale,
118
116
  dataTestId,
119
117
  emptyState,
@@ -129,12 +127,7 @@ function CardBody<T extends Record<string, unknown>>({
129
127
  // Fixed inter-card gap — deliberately NOT scaled by the zoom slider, so
130
128
  // only the cards grow while the space between them stays constant.
131
129
  gap: 1.5,
132
- // `auto-fill` still decides how many FIT; density caps how many are
133
- // ALLOWED, so "Poucos" reads as bigger cards rather than more of them.
134
- gridTemplateColumns: {
135
- xs: "1fr",
136
- sm: `repeat(auto-fill, minmax(max(${minCardWidth}px, calc((100% - ${(maxColumns - 1) * 12}px) / ${maxColumns})), 1fr))`,
137
- },
130
+ ...cardGridTracks(targetCardWidth),
138
131
  }}
139
132
  data-testid={dataTestId ? `${dataTestId}-cards` : "data-views-cards"}
140
133
  >
@@ -353,8 +346,7 @@ function headerlessBody<T extends Record<string, unknown>>(
353
346
  getRowId={getRowId}
354
347
  selectedIds={c.selectedIds}
355
348
  onToggleId={c.toggleId}
356
- minCardWidth={cardMinWidthForZoom(zoom)}
357
- maxColumns={DENSITY_CARD_COLUMNS[density]}
349
+ targetCardWidth={cardTargetWidthFor(zoom, density)}
358
350
  cardScale={cardScaleForZoom(zoom)}
359
351
  dataTestId={dataTestId}
360
352
  emptyState={emptyState}
@@ -246,3 +246,24 @@ export function togglePillValues(current: string[] | undefined, value: string, c
246
246
  const list = current ?? [];
247
247
  return checked ? [...list, value] : list.filter((v) => v !== value);
248
248
  }
249
+
250
+ /**
251
+ * The card grid's `grid-template-columns`, as an sx fragment.
252
+ *
253
+ * One rule at every width: each card asks for `targetWidth` and `auto-fill`
254
+ * decides how many fit. Density moves the target, so it changes the count at
255
+ * every size — which the previous column-count cap could not do, because its
256
+ * track floor bottomed out at a constant shared by all three densities.
257
+ *
258
+ * Below `sm` this is a single column, and no target changes that. That is why
259
+ * the density control hides itself there rather than offering three settings
260
+ * that all draw the same thing.
261
+ */
262
+ export function cardGridTracks(targetWidth: number) {
263
+ return {
264
+ gridTemplateColumns: {
265
+ xs: "1fr",
266
+ sm: `repeat(auto-fill, minmax(${targetWidth}px, 1fr))`,
267
+ },
268
+ };
269
+ }
@@ -81,13 +81,32 @@ export const DENSITY_ROW_PADDING: Record<DataViewsDensity, number> = {
81
81
  comfortable: 1.5,
82
82
  };
83
83
 
84
- /** Card grid columns per density — how many cards land on one row. */
85
- export const DENSITY_CARD_COLUMNS: Record<DataViewsDensity, number> = {
86
- compact: 5,
87
- cozy: 4,
88
- comfortable: 3,
84
+ /**
85
+ * The card WIDTH each density aims for, as a multiple of the base card width.
86
+ *
87
+ * Density used to cap the column COUNT, with a track floor of
88
+ * `max(baseWidth, 100% / count)`. That base is a constant, so wherever it won
89
+ * the arithmetic every density produced an identical track — "Muitos" and
90
+ * "Medio" both drew 3 cards of 279px at md — and where the percentage won
91
+ * instead, on a wide screen, the count was pinned and each card grew to a share
92
+ * of the viewport: ~430px of card holding one chip and two lines of text.
93
+ *
94
+ * One width per density fixes both, because the number `auto-fill` divides by
95
+ * now differs per density at every size. The spread is chosen so the three land
96
+ * on DIFFERENT column counts across the range rather than by eye — 4/3/2 at md,
97
+ * 6/4/3 at lg, 7/5/4 at xl, 13/9/8 at 2560.
98
+ */
99
+ export const DENSITY_CARD_SCALE: Record<DataViewsDensity, number> = {
100
+ compact: 0.84,
101
+ cozy: 1.15,
102
+ comfortable: 1.4,
89
103
  };
90
104
 
105
+ /** The width a card aims for: the base width, scaled by zoom, then by density. */
106
+ export function cardTargetWidthFor(zoom: number, density: DataViewsDensity): number {
107
+ return Math.round(cardMinWidthForZoom(zoom) * DENSITY_CARD_SCALE[density]);
108
+ }
109
+
91
110
  /**
92
111
  * The BOARD's column-width multiplier per density.
93
112
  *