aplance-ui 1.5.2 → 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 (29) hide show
  1. package/README.md +1 -1
  2. package/dist/api.d.ts +2 -1
  3. package/dist/api.js +103 -68
  4. package/dist/api.js.map +1 -1
  5. package/dist/frameworks/vite-react/vite.config.ts.template +1 -1
  6. package/dist/index.js +103 -68
  7. package/dist/index.js.map +1 -1
  8. package/package.json +12 -6
  9. package/registry/components/primitives/modal/modal.module.css +9 -3
  10. package/registry/components/primitives/popover/popover-context.ts +2 -0
  11. package/registry/components/primitives/popover/popover.tsx +47 -2
  12. package/registry/components/ui/chart/chart.tsx +51 -30
  13. package/registry/components/ui/chart/types.ts +5 -1
  14. package/registry/components/ui/data-table/checkbox-column.tsx +12 -8
  15. package/registry/components/ui/data-table/data-table.module.css +41 -19
  16. package/registry/components/ui/data-table/data-table.tsx +1285 -1238
  17. package/registry/components/ui/data-table/filter-bar.tsx +56 -13
  18. package/registry/components/ui/data-table/filter-modal.tsx +16 -82
  19. package/registry/components/ui/data-table/filter-sheet.tsx +97 -0
  20. package/registry/components/ui/data-table/filters.tsx +21 -21
  21. package/registry/components/ui/data-table/pagination-bar.tsx +4 -6
  22. package/registry/components/ui/data-table/types.ts +149 -80
  23. package/registry/components/ui/data-table/useDataTable.tsx +35 -16
  24. package/registry/components/ui/data-table/utils.ts +691 -471
  25. package/registry/components/ui/input-fields/date-picker/calendar.tsx +7 -15
  26. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +1 -1
  27. package/registry/components/ui/input-fields/date-picker/types.ts +3 -3
  28. package/registry/components/ui/input-fields/emoji-picker/emoji-palette.tsx +1 -1
  29. package/registry/registry.json +16 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aplance-ui",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "description": "The aplance-ui CLI - scaffold the design system and copy components into your project as source.",
5
5
  "keywords": [
6
6
  "react",
@@ -50,14 +50,20 @@
50
50
  "lint": "eslint src",
51
51
  "prepublishOnly": "npm run build && npm run typecheck && npm run lint"
52
52
  },
53
+ "engines": {
54
+ "node": ">=20.12.0"
55
+ },
53
56
  "dependencies": {
54
- "@clack/prompts": "^0.8.2",
55
- "commander": "^12.1.0",
57
+ "@clack/prompts": "^1.9.0",
58
+ "commander": "^14.0.3",
56
59
  "picocolors": "^1.1.1"
57
60
  },
58
61
  "devDependencies": {
59
- "@types/node": "^24.10.1",
60
- "tsup": "^8.3.5",
61
- "typescript": "~5.9.3"
62
+ "@eslint/js": "^10.0.1",
63
+ "@types/node": "^24.12.2",
64
+ "eslint": "^10.15.0",
65
+ "tsup": "^9.0.0",
66
+ "typescript": "~5.9.3",
67
+ "typescript-eslint": "^8.72.2"
62
68
  }
63
69
  }
@@ -120,9 +120,13 @@
120
120
  flex-direction: column;
121
121
  max-height: calc(100dvh - 2 * var(--spacing-6));
122
122
 
123
- /* Edge placements fill the viewport - inside-scroll should reach the edges */
123
+ /* A sheet stops short of the far edge, so the page behind it still shows
124
+ and a tap there closes it: --modal-sheet-max-height, 80dvh by default.
125
+ Drawers and full placements fill the viewport. */
124
126
  &[data-placement='bottom'],
125
- &[data-placement='top'],
127
+ &[data-placement='top'] {
128
+ max-height: var(--modal-sheet-max-height, 80dvh);
129
+ }
126
130
  &[data-placement='left'],
127
131
  &[data-placement='right'],
128
132
  &[data-placement='full'] {
@@ -390,7 +394,9 @@
390
394
  max-height: calc(100dvh - 2 * var(--spacing-6));
391
395
  }
392
396
  .scroll_inside[data-mobile-placement='bottom'],
393
- .scroll_inside[data-mobile-placement='top'],
397
+ .scroll_inside[data-mobile-placement='top'] {
398
+ max-height: var(--modal-sheet-max-height, 80dvh);
399
+ }
394
400
  .scroll_inside[data-mobile-placement='left'],
395
401
  .scroll_inside[data-mobile-placement='right'],
396
402
  .scroll_inside[data-mobile-placement='full'] {
@@ -17,6 +17,8 @@ export type PopoverContextType = {
17
17
  availableWidth?: number;
18
18
  availableHeight?: number;
19
19
  autoPosition: boolean;
20
+ /** Above the trigger's layer (e.g. a modal), or null for the popover token. */
21
+ menuZIndex: number | null;
20
22
  hoverTimeoutRef: RefObject<ReturnType<typeof setTimeout> | null>;
21
23
  renderOnPortal: boolean;
22
24
  mounted: boolean;
@@ -41,6 +41,37 @@ const useIsomorphicLayoutEffect =
41
41
  const isVerticalPosition = (position: Position): boolean =>
42
42
  position === "top" || position === "bottom";
43
43
 
44
+ /**
45
+ * The z-index a portaled menu needs to show above the layer its trigger lives
46
+ * in: a modal's overlay, a sheet, an app's own fixed panel. The menu portals
47
+ * to <body>, where it competes with the trigger's outermost positioned
48
+ * ancestor that has a z-index. When that layer sits at or above
49
+ * `--z-index-popover`, the menu goes one level above it; null keeps the token
50
+ * (a trigger on the page itself, or under a lower layer like a sticky header).
51
+ */
52
+ const zIndexAboveLayer = (trigger: HTMLElement): number | null => {
53
+ let layer: number | null = null;
54
+ for (
55
+ let node: HTMLElement | null = trigger;
56
+ node && node !== document.body;
57
+ node = node.parentElement
58
+ ) {
59
+ const style = getComputedStyle(node);
60
+ const zIndex = Number.parseInt(style.zIndex, 10);
61
+ if (style.position !== "static" && Number.isFinite(zIndex)) layer = zIndex;
62
+ }
63
+ if (layer === null) return null;
64
+ const popoverLevel = Number.parseInt(
65
+ getComputedStyle(document.documentElement).getPropertyValue(
66
+ "--z-index-popover",
67
+ ),
68
+ 10,
69
+ );
70
+ return Number.isFinite(popoverLevel) && layer < popoverLevel
71
+ ? null
72
+ : layer + 1;
73
+ };
74
+
44
75
  const clamp = (value: number, min: number, max: number): number =>
45
76
  Math.min(Math.max(value, min), Math.max(min, max));
46
77
 
@@ -120,6 +151,7 @@ export const Popover: FC<PopoverProps> = ({
120
151
  width?: number;
121
152
  height?: number;
122
153
  }>({});
154
+ const [menuZIndex, setMenuZIndex] = useState<number | null>(null);
123
155
 
124
156
  // Calculate Menu Position
125
157
  const calculatePosition = useCallback(() => {
@@ -224,8 +256,19 @@ export const Popover: FC<PopoverProps> = ({
224
256
  width: availableWidth,
225
257
  height: availableHeight,
226
258
  });
259
+ // An in-place menu already sits inside its trigger's layer.
260
+ setMenuZIndex(
261
+ renderOnPortal ? zIndexAboveLayer(popoverRef.current) : null,
262
+ );
227
263
  setIsMeasured(true);
228
- }, [position, align, offset, shouldAutoPosition, collisionPadding]);
264
+ }, [
265
+ position,
266
+ align,
267
+ offset,
268
+ shouldAutoPosition,
269
+ collisionPadding,
270
+ renderOnPortal,
271
+ ]);
229
272
 
230
273
  // Open / Close Handling
231
274
  // Visibility (opacity / pointer-events) is derived from `isOpen && isMeasured`
@@ -352,6 +395,7 @@ export const Popover: FC<PopoverProps> = ({
352
395
  availableWidth: availableSize.width,
353
396
  availableHeight: availableSize.height,
354
397
  autoPosition: shouldAutoPosition,
398
+ menuZIndex,
355
399
  mounted,
356
400
  }}
357
401
  >
@@ -484,6 +528,7 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
484
528
  availableWidth,
485
529
  availableHeight,
486
530
  autoPosition,
531
+ menuZIndex,
487
532
  } = usePopover();
488
533
 
489
534
  const handleMouseEnter = () => {
@@ -544,7 +589,7 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
544
589
  position: "fixed",
545
590
  top: coords.top,
546
591
  left: coords.left,
547
- zIndex: "var(--z-index-popover)",
592
+ zIndex: menuZIndex ?? "var(--z-index-popover)",
548
593
  ...(constrainedMaxWidth !== undefined && { maxWidth: constrainedMaxWidth }),
549
594
  ...(autoPosition &&
550
595
  availableHeight !== undefined && { maxHeight: availableHeight }),
@@ -6,7 +6,6 @@ import {
6
6
  Bar,
7
7
  BarChart as RechartsBarChart,
8
8
  CartesianGrid,
9
- Cell,
10
9
  ComposedChart as RechartsComposedChart,
11
10
  Line,
12
11
  LineChart as RechartsLineChart,
@@ -15,14 +14,19 @@ import {
15
14
  PolarAngleAxis,
16
15
  RadialBar,
17
16
  RadialBarChart as RechartsRadialBarChart,
17
+ Rectangle,
18
18
  ReferenceLine,
19
19
  ResponsiveContainer,
20
20
  Scatter as RechartsScatter,
21
21
  ScatterChart as RechartsScatterChart,
22
+ Sector,
22
23
  Tooltip,
23
24
  XAxis,
24
25
  YAxis,
25
26
  ZAxis,
27
+ type BarShapeProps,
28
+ type PieSectorShapeProps,
29
+ type RadialBarSectorProps,
26
30
  } from "recharts";
27
31
  import type { ReactNode } from "react";
28
32
  import s from "./chart.module.css";
@@ -428,6 +432,27 @@ function ChartTooltip({
428
432
  );
429
433
  }
430
434
 
435
+ // ─── Per-item colours ─────────────────────────────────────────────────────────
436
+ // Recharts 3 deprecates <Cell> (removed in 4.0): each bar or sector takes its
437
+ // own colour through `shape`, from the `__color` stamped on its datum here.
438
+
439
+ const itemColor = (payload: unknown, fallback: string | undefined) => {
440
+ const color = (payload as { __color?: unknown } | null | undefined)?.__color;
441
+ return typeof color === "string" ? color : fallback;
442
+ };
443
+
444
+ const renderColoredBar = (props: BarShapeProps) => (
445
+ <Rectangle {...props} fill={itemColor(props.payload, props.fill)} />
446
+ );
447
+
448
+ const renderColoredSector = (props: PieSectorShapeProps) => (
449
+ <Sector {...props} fill={itemColor(props.payload, props.fill)} />
450
+ );
451
+
452
+ const renderColoredRadialBar = (props: RadialBarSectorProps) => (
453
+ <Sector {...props} fill={itemColor(props.payload, props.fill)} />
454
+ );
455
+
431
456
  // ─── Shared legend ────────────────────────────────────────────────────────────
432
457
 
433
458
  function ChartLegend({ items }: { items: LegendItem[] }) {
@@ -444,8 +469,8 @@ function ChartLegend({ items }: { items: LegendItem[] }) {
444
469
  }
445
470
 
446
471
  // ─── Cartesian frame ──────────────────────────────────────────────────────────
447
- // Returns an array so Recharts' React.Children.toArray scan finds XAxis/YAxis
448
- // directly - Fragments are opaque to that scan, arrays are not.
472
+ // Grid, axes, tooltip and reference lines shared by every cartesian chart,
473
+ // returned as a keyed array of chart children.
449
474
 
450
475
  const renderCartesianFrame = ({
451
476
  categoryKey,
@@ -465,6 +490,18 @@ const renderCartesianFrame = ({
465
490
  orientation?: "vertical" | "horizontal";
466
491
  }): ReactNode[] => {
467
492
  const isHorizontal = orientation === "horizontal";
493
+ const showLeftYAxis = options.showYAxis && axisConfig.showLeftAxis;
494
+ const showRightYAxis =
495
+ options.showYAxis && axisConfig.showRightAxis && !isHorizontal;
496
+ // The rendered Y axis that the grid and reference lines measure against.
497
+ // Recharts 3 drops a grid or reference line (a vertical one included) whose
498
+ // yAxisId names no rendered axis, and the axes here use named ids. With the
499
+ // Y axis hidden, everything shares Recharts' default axis instead.
500
+ const frameYAxisId = showLeftYAxis
501
+ ? "left"
502
+ : showRightYAxis
503
+ ? "right"
504
+ : undefined;
468
505
 
469
506
  const items: ReactNode[] = [
470
507
  options.showGrid ? (
@@ -474,6 +511,7 @@ const renderCartesianFrame = ({
474
511
  vertical={isHorizontal}
475
512
  horizontal={!isHorizontal}
476
513
  stroke={palette.grid}
514
+ yAxisId={frameYAxisId}
477
515
  />
478
516
  ) : null,
479
517
 
@@ -505,7 +543,7 @@ const renderCartesianFrame = ({
505
543
  ) : null,
506
544
 
507
545
  // Left Y axis - value axis (vertical) or category axis (horizontal)
508
- options.showYAxis && axisConfig.showLeftAxis ? (
546
+ showLeftYAxis ? (
509
547
  isHorizontal ? (
510
548
  <YAxis
511
549
  key="y-axis-left"
@@ -535,7 +573,7 @@ const renderCartesianFrame = ({
535
573
  ) : null,
536
574
 
537
575
  // Right Y axis - only for vertical orientation with dual-axis series
538
- options.showYAxis && axisConfig.showRightAxis && !isHorizontal ? (
576
+ showRightYAxis ? (
539
577
  <YAxis
540
578
  key="y-axis-right"
541
579
  yAxisId="right"
@@ -569,12 +607,8 @@ const renderCartesianFrame = ({
569
607
  items.push(
570
608
  <ReferenceLine
571
609
  key={`rl-${i}`}
572
- {...(rl.axis === "x"
573
- ? { x: rl.value }
574
- : {
575
- y: rl.value,
576
- yAxisId: options.showYAxis ? (rl.yAxisId ?? "left") : undefined,
577
- })}
610
+ {...(rl.axis === "x" ? { x: rl.value } : { y: rl.value })}
611
+ yAxisId={options.showYAxis ? (rl.yAxisId ?? frameYAxisId) : undefined}
578
612
  stroke={resolveColorToken(rl.color, palette.muted)}
579
613
  strokeDasharray={rl.strokeDasharray ?? "4 4"}
580
614
  strokeWidth={1}
@@ -689,15 +723,8 @@ function BarChartRenderer({
689
723
  fill={item.color}
690
724
  stackId={item.stackId}
691
725
  yAxisId={options.showYAxis ? item.yAxisId : undefined}
692
- >
693
- {usesCategoryColorMap &&
694
- chartData.map((entry, index) => {
695
- const key = toLookupKey(entry[categoryKey]);
696
- const fill =
697
- typeof entry.__color === "string" ? entry.__color : item.color;
698
- return <Cell key={`${item.key}-${key}-${index}`} fill={fill} />;
699
- })}
700
- </Bar>
726
+ shape={usesCategoryColorMap ? renderColoredBar : undefined}
727
+ />
701
728
  ))}
702
729
  </RechartsBarChart>
703
730
  </ResponsiveContainer>
@@ -1093,11 +1120,8 @@ function DonutChartRenderer({
1093
1120
  nameKey={nameKey}
1094
1121
  stroke="var(--color-background)"
1095
1122
  strokeWidth={2}
1096
- >
1097
- {legendItems.map((item, index) => (
1098
- <Cell key={`${item.key}-${index}`} fill={item.color} />
1099
- ))}
1100
- </Pie>
1123
+ shape={renderColoredSector}
1124
+ />
1101
1125
  <Tooltip
1102
1126
  content={
1103
1127
  <ChartTooltip
@@ -1183,11 +1207,8 @@ function RadialBarChartRenderer({
1183
1207
  dataKey="__value"
1184
1208
  cornerRadius={6}
1185
1209
  background={{ fill: palette.grid }}
1186
- >
1187
- {chartData.map((item, index) => (
1188
- <Cell key={index} fill={item.__color} />
1189
- ))}
1190
- </RadialBar>
1210
+ shape={renderColoredRadialBar}
1211
+ />
1191
1212
  <Tooltip
1192
1213
  content={
1193
1214
  <ChartTooltip
@@ -23,7 +23,11 @@ export type ReferenceLineConfig = {
23
23
  /** CSS token (e.g. "--color-chart-secondary") or raw CSS color. */
24
24
  color?: string;
25
25
  strokeDasharray?: string;
26
- /** Which Y axis to attach to when axis is "y". Default: "left". */
26
+ /**
27
+ * Which Y axis the line measures against: a "y" line sits on its scale, an
28
+ * "x" line spans its height. Default: the left axis, or the right one when
29
+ * only right-axis series are shown.
30
+ */
27
31
  yAxisId?: "left" | "right";
28
32
  };
29
33
 
@@ -1,10 +1,12 @@
1
- import type { ColumnDef } from "@tanstack/react-table";
2
1
  import { Input } from "../input-fields/input/input";
2
+ import type { DataTableColumnDef } from "./types";
3
+ import { SELECT_COLUMN_ID } from "./utils";
3
4
  import s from "./data-table.module.css";
4
5
 
5
6
  /**
6
7
  * Pre-built select-all / per-row checkbox column using `<Input type="checkbox">`.
7
- * Handles indeterminate state on the header checkbox.
8
+ * Handles indeterminate state on the header checkbox. Shift-click a row's
9
+ * checkbox to select (or clear) every row between it and the last one clicked.
8
10
  *
9
11
  * @example
10
12
  * const cols = useMemo(
@@ -14,9 +16,9 @@ import s from "./data-table.module.css";
14
16
  */
15
17
  export function checkboxColumn<TData extends { id: string }>(
16
18
  editingRowId?: string | null,
17
- ): ColumnDef<TData> {
19
+ ): DataTableColumnDef<TData, unknown> {
18
20
  return {
19
- id: "select",
21
+ id: SELECT_COLUMN_ID,
20
22
  // Compact fixed width keeps checkbox cells visually balanced while
21
23
  // reserving enough room for focus ring rendering.
22
24
  size: 42,
@@ -30,19 +32,21 @@ export function checkboxColumn<TData extends { id: string }>(
30
32
  align: "center",
31
33
  truncate: false,
32
34
  },
35
+ // The header box selects the rows on screen: the current page.
33
36
  header: ({ table }) => (
34
37
  <Input
35
38
  type="checkbox"
36
- checked={table.getIsAllRowsSelected()}
39
+ checked={table.getIsAllPageRowsSelected()}
37
40
  className={s.selection_checkbox}
38
41
  disabled={editingRowId != null}
39
42
  ref={(el: HTMLInputElement | null) => {
40
43
  if (el)
41
44
  el.indeterminate =
42
- table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected();
45
+ table.getIsSomePageRowsSelected() &&
46
+ !table.getIsAllPageRowsSelected();
43
47
  }}
44
- onChange={table.getToggleAllRowsSelectedHandler()}
45
- aria-label="Select all rows"
48
+ onChange={table.getToggleAllPageRowsSelectedHandler()}
49
+ aria-label="Select all rows on this page"
46
50
  />
47
51
  ),
48
52
  cell: ({ row }) => (
@@ -277,18 +277,36 @@
277
277
  overflow: visible;
278
278
  }
279
279
  }
280
+
281
+ /* Footer row (columns with a `footer`, e.g. totals): sticks to the bottom
282
+ of the scrolling body so it stays in view. Its top line is an inset
283
+ shadow, so it never doubles the last row's line. */
284
+ tfoot td {
285
+ position: sticky;
286
+ bottom: 0;
287
+ z-index: 1;
288
+ padding: var(--spacing-5) var(--spacing-6);
289
+ font-size: var(--font-size-sm);
290
+ font-weight: var(--font-weight-semibold);
291
+ color: var(--color-foreground);
292
+ vertical-align: middle;
293
+ box-shadow: inset 0 var(--border-1) 0 var(--color-border);
294
+ /* explicit background so the rows scrolling under it stay hidden */
295
+ background: var(--color-muted);
296
+ }
280
297
  }
281
298
 
282
299
  /* ─────────────────────────────────────────────
283
300
  Vertical column borders (showVerticalBorders prop)
284
- Drawn on the LEFT of every cell except the first, so we never get a
301
+ Drawn on the START side of every cell except the first, so we never get a
285
302
  double border at the table's outer edge.
286
303
  ───────────────────────────────────────────── */
287
304
  .table_vertical_borders {
288
- /* Only add vertical border if the previous cell is not the last left-pinned cell. Prevents double border with pinned columns. */
289
- thead th:not(.col_pinned_last.col_pinned_left) + th,
290
- tbody td:not(.col_pinned_last.col_pinned_left) + td {
291
- border-left: var(--border-1) solid var(--color-border);
305
+ /* Skip the cell after the start group's divider column, which already draws that line. */
306
+ thead th:not(.col_pinned_last.col_pinned_start) + th,
307
+ tbody td:not(.col_pinned_last.col_pinned_start) + td,
308
+ tfoot td:not(.col_pinned_last.col_pinned_start) + td {
309
+ border-inline-start: var(--border-1) solid var(--color-border);
292
310
  }
293
311
  }
294
312
 
@@ -472,13 +490,14 @@
472
490
 
473
491
  /* ─────────────────────────────────────────────
474
492
  Column pinning - sticky offsets are written inline by JS
475
- The hairline divider lives only on the LAST column of each pinning group
476
- (`col_pinned_last`) so adjacent pinned columns don't get double dividers.
493
+ The hairline divider lives only on the pinned column that meets the
494
+ scrolling center (`col_pinned_last`: the last one counted inward from its
495
+ edge), so adjacent pinned columns don't get double dividers.
477
496
  ───────────────────────────────────────────── */
478
- .table th.col_pinned_left,
479
- .table th.col_pinned_right,
480
- .table td.col_pinned_left,
481
- .table td.col_pinned_right {
497
+ .table th.col_pinned_start,
498
+ .table th.col_pinned_end,
499
+ .table td.col_pinned_start,
500
+ .table td.col_pinned_end {
482
501
  position: sticky;
483
502
  z-index: 1;
484
503
 
@@ -486,19 +505,22 @@
486
505
  background-color: var(--color-muted);
487
506
  }
488
507
 
489
- .table thead th.col_pinned_left,
490
- .table thead th.col_pinned_right {
508
+ /* Pinned header and footer cells stick on both axes: above everything else. */
509
+ .table thead th.col_pinned_start,
510
+ .table thead th.col_pinned_end,
511
+ .table tfoot td.col_pinned_start,
512
+ .table tfoot td.col_pinned_end {
491
513
  z-index: 2;
492
514
  }
493
515
 
494
- /* Pinned left columns: add a right border only to the last pinned cell to act as a divider. */
495
- .col_pinned_last.col_pinned_left {
496
- border-right: var(--border-1) solid var(--color-border);
516
+ /* Start group: the divider on its inner (end) side. */
517
+ .col_pinned_last.col_pinned_start {
518
+ border-inline-end: var(--border-1) solid var(--color-border);
497
519
  }
498
520
 
499
- /* Pinned right columns: add a left border only to the last pinned cell to act as a divider. */
500
- .col_pinned_last.col_pinned_right {
501
- border-left: var(--border-1) solid var(--color-border);
521
+ /* End group: the divider on its inner (start) side. */
522
+ .col_pinned_last.col_pinned_end {
523
+ border-inline-start: var(--border-1) solid var(--color-border);
502
524
  }
503
525
 
504
526
  /* Keep interactive cell popovers above sticky neighbors when they are rendered