@mendylanda/ui 0.3.0-alpha.9 → 0.3.1

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/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Mendy UI
2
2
 
3
- My personal collection of components I like to use. Maintained as one package and shared across projects.
3
+ React components by Mendy Landa. My collection currently includes Table and Filters, maintained in one package and shared across projects.
4
+
5
+ Use either component independently or connect them. [Documentation and live examples](https://ui.mendylanda.com).
4
6
 
5
7
  ```sh
6
8
  npm install @mendylanda/ui
@@ -12,6 +14,47 @@ Import the stylesheet once:
12
14
  import "@mendylanda/ui/styles.css";
13
15
  ```
14
16
 
17
+ ## Table
18
+
19
+ The table is exported from `@mendylanda/ui/table`.
20
+
21
+ ```tsx
22
+ import { DataTable, defineColumns, format } from "@mendylanda/ui/table";
23
+
24
+ type Project = { id: string; name: string; budget: number };
25
+ const columns = defineColumns<Project>((column) => [
26
+ column.accessor("name", { label: "Project" }),
27
+ column.accessor("budget", { label: "Budget", format: format.currency("USD") }),
28
+ ]);
29
+
30
+ export function Projects({ rows }: { rows: Project[] }) {
31
+ return <DataTable rows={rows} columns={columns} getRowId={(row) => row.id} />;
32
+ }
33
+ ```
34
+
35
+ Use `useDataTable` and `TableView` when the application already controls sorting, pagination, preferences, or selection. Data fetching and mutations stay in the application. `useResultSelection` represents explicit IDs or all matching results with exclusions. `TableSavedViews` accepts application-owned views; it does not create a backend.
36
+
37
+ Pass a filter controller once to place the shared filter bar and connect table recovery behavior:
38
+
39
+ ```tsx
40
+ <DataTable
41
+ table={table}
42
+ filters={filters}
43
+ toolbar={<ProjectActions />}
44
+ filterBar={{ showSearch: false }}
45
+ />
46
+ ```
47
+
48
+ Create the controller with `useFilters`, `useUrlFilters`, or `useControlledFilters`. Its values remain application query state. The table does not derive a predicate or fetch rows from filter definitions. Set `filterBar={false}` when controls live elsewhere, or use `TableFilters` and `TableView` with the same controller in a composed layout.
49
+
50
+ Tables use `layout="content"` by default, fitting their rows, headers, and empty content up to 65dvh. Short tables do not reserve a vertical scrollbar. An explicit `height` sets the table viewport in content layout. `layout="fill"` measures the space from the component's top to the bottom of the viewport and remeasures when the viewport resizes or content above the table changes size. `DataTable` reserves its toolbar, pagination, and footer automatically. Composed layouts can pass controls below the grid through `TableView`'s `footer` prop. Rows remain 44px unless `rowHeight="auto"` opts into measurement for wrapped or editable content. Selected rows receive the standard highlight; `isRowHighlighted` adds application state. `onRowClick`, `rowClassName`, and `renderRowDetail` cover row actions and expandable content.
51
+
52
+ Changes to table query state or the shared controller reset scroll and cell selection. Appending rows keeps the current position and selection. Empty states distinguish an empty dataset, an empty filtered result with one controller clear, and an empty later page that can return to the first page. Locked filters never get a clear action.
53
+
54
+ See the [table documentation](https://ui.mendylanda.com/docs/components/table) for composition, remote data, and persistence.
55
+
56
+ ## Filters
57
+
15
58
  Define filters in your application:
16
59
 
17
60
  ```tsx
@@ -85,42 +128,3 @@ Opening the menu shows the category list without choosing a filter. Focusing a c
85
128
  `FilterBar` and composed `FilterList` show Clear all after the chips when a removable filter is active. It clears filters and search. Search-only bars use the input’s clear control and hide the filter menu when no menu fields are available. Set `showClear={false}` to opt out. Choice search is enabled by default; use `searchable: false` for short lists. Menu selections keep the menu open, and selecting the current single choice again clears it. Chip editor popups open instantly; `editorAnimation` opts into their animation.
86
129
 
87
130
  See [defaults and app configuration](https://ui.mendylanda.com/docs/defaults) for the full behavior and [system reference](https://ui.mendylanda.com/docs/system) for query adapters, selected-label loading, grouped fields, and custom editors.
88
-
89
- ## Table (alpha)
90
-
91
- Install `@mendylanda/ui@alpha` to try the table. This API may change during the alpha.
92
-
93
- ```tsx
94
- import { DataTable, defineColumns, format } from "@mendylanda/ui/table";
95
-
96
- type Project = { id: string; name: string; budget: number };
97
- const columns = defineColumns<Project>((column) => [
98
- column.accessor("name", { label: "Project" }),
99
- column.accessor("budget", { label: "Budget", format: format.currency("USD") }),
100
- ]);
101
-
102
- export function Projects({ rows }: { rows: Project[] }) {
103
- return <DataTable rows={rows} columns={columns} getRowId={(row) => row.id} />;
104
- }
105
- ```
106
-
107
- Use `useDataTable` and `TableView` when the application already controls sorting, pagination, preferences, or selection. Data fetching and mutations stay in the application. `useResultSelection` represents explicit IDs or all matching results with exclusions. `TableSavedViews` accepts application-owned views; it does not create a backend.
108
-
109
- Pass a filter controller once to place the shared filter bar and connect table recovery behavior:
110
-
111
- ```tsx
112
- <DataTable
113
- table={table}
114
- filters={filters}
115
- toolbar={<ProjectActions />}
116
- filterBar={{ showSearch: false }}
117
- />
118
- ```
119
-
120
- Create the controller with `useFilters`, `useUrlFilters`, or `useControlledFilters`. Its values remain application query state. The table does not derive a predicate or fetch rows from filter definitions. Set `filterBar={false}` when controls live elsewhere, or use `TableFilters` and `TableView` with the same controller in a composed layout.
121
-
122
- Tables fit their rows, headers, and empty content by default, capped at 65dvh. Short tables do not reserve a vertical scrollbar. Full-page layouts can set an explicit `height`. Rows remain 44px unless `rowHeight="auto"` opts into measurement for wrapped or editable content. Selected rows receive the standard highlight; `isRowHighlighted` adds application state. `onRowClick`, `rowClassName`, and `renderRowDetail` cover row actions and expandable content.
123
-
124
- Changes to table query state or the shared controller reset scroll and cell selection. Appending rows keeps the current position and selection. Empty states distinguish an empty dataset, an empty filtered result with one controller clear, and an empty later page that can return to the first page. Locked filters never get a clear action.
125
-
126
- See the [table documentation](https://ui.mendylanda.com/docs/components/table) for composition, remote data, and persistence.
@@ -1,11 +1,13 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useDataTable } from "./use-data-table.js";
4
4
  import { TableView } from "./table-view.js";
5
5
  import { TableColumnSettings, TablePagination } from "./table-controls.js";
6
6
  import { TableFilters } from "./table-filters.js";
7
- function ControlledTable({ table, toolbar, footer, showColumnSettings = true, showPagination = false, filters, filterBar, ...view }) {
8
- return (_jsxs("div", { "data-mendy-ui": "", className: "mui-184ddc11e5f9 mui-267171770524", children: [(toolbar || showColumnSettings || (filters && filterBar !== false)) && (_jsxs("div", { className: "mui-222f930b8752 mui-faa8a23c68f6 mui-b9677bd1cb66 mui-32c92bb1fde4 mui-074569488cca", children: [_jsxs("div", { className: "mui-184ddc11e5f9 mui-7bd5bab6d7f4 mui-267171770524", children: [filters && filterBar !== false && _jsx(TableFilters, { ...filterBar, filters: filters }), !filters && toolbar] }), filters && toolbar, showColumnSettings && _jsx(TableColumnSettings, { table: table })] })), _jsx(TableView, { table: table, filters: filters, ...view }), showPagination && (_jsx(TablePagination, { table: table, loading: view.status === "loading" || view.refreshing })), footer] }));
7
+ import { TableFrame } from "./table-frame.js";
8
+ function ControlledTable({ table, toolbar, footer, showColumnSettings = true, showPagination = false, filters, filterBar, layout = "content", ...view }) {
9
+ const fill = layout === "fill";
10
+ return (_jsx(TableFrame, { layout: layout, slot: "data-table", header: (toolbar || showColumnSettings || (filters && filterBar !== false)) && (_jsxs("div", { className: "mui-222f930b8752 mui-27ead27a81df mui-faa8a23c68f6 mui-b9677bd1cb66 mui-32c92bb1fde4 mui-074569488cca", children: [_jsxs("div", { className: "mui-184ddc11e5f9 mui-7bd5bab6d7f4 mui-267171770524", children: [filters && filterBar !== false && _jsx(TableFilters, { ...filterBar, filters: filters }), !filters && toolbar] }), filters && toolbar, showColumnSettings && _jsx(TableColumnSettings, { table: table })] })), footer: showPagination || footer ? (_jsxs(_Fragment, { children: [showPagination && (_jsx("div", { className: "mui-27ead27a81df", children: _jsx(TablePagination, { table: table, loading: view.status === "loading" || view.refreshing }) })), footer] })) : undefined, children: _jsx(TableView, { table: table, filters: filters, ...view, height: fill ? "100%" : view.height }) }));
9
11
  }
10
12
  function ConfiguredTable(props) {
11
13
  const table = useDataTable(props);
@@ -1,11 +1,24 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { TableDataState } from "./table-view.js";
3
- export declare function TableInitialState({ status, hasRows, loadingState, emptyState, error, retry, hasFilters, pageIndex, clearFilters, firstPage, }: Pick<TableDataState, "status" | "error" | "retry"> & {
3
+ export declare function TableFeedbackRow({ children, columnCount, rowIndex, className, }: {
4
+ children: ReactNode;
5
+ columnCount: number;
6
+ rowIndex: number;
7
+ className?: string;
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ export declare function TableInitialState({ status, hasRows, loadingState, emptyState, error, retry, hasFilters, pageIndex, clearFilters, firstPage, columnCount, }: Pick<TableDataState, "status" | "error" | "retry"> & {
4
10
  hasRows: boolean;
11
+ columnCount: number;
5
12
  loadingState?: ReactNode;
6
13
  emptyState?: ReactNode;
7
14
  hasFilters?: boolean;
8
15
  pageIndex?: number;
9
16
  clearFilters?: () => void;
10
17
  firstPage?: () => void;
11
- }): string | number | bigint | boolean | import("react/jsx-runtime").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null;
18
+ }): import("react/jsx-runtime").JSX.Element | null;
19
+ export declare function TableLoadMore({ loadMore, columnCount, rowIndex, onLoad, }: {
20
+ loadMore: NonNullable<TableDataState["loadMore"]>;
21
+ columnCount: number;
22
+ rowIndex: number;
23
+ onLoad: () => void;
24
+ }): import("react/jsx-runtime").JSX.Element;
@@ -1,12 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Button } from "../primitives/button.js";
3
- export function TableInitialState({ status, hasRows, loadingState, emptyState, error, retry, hasFilters, pageIndex = 0, clearFilters, firstPage, }) {
3
+ export function TableFeedbackRow({ children, columnCount, rowIndex, className, }) {
4
+ return (_jsx("div", { role: "row", "aria-rowindex": rowIndex, className: className, children: _jsx("div", { role: "gridcell", "aria-colindex": 1, "aria-colspan": Math.max(1, columnCount), children: children }) }));
5
+ }
6
+ export function TableInitialState({ status, hasRows, loadingState, emptyState, error, retry, hasFilters, pageIndex = 0, clearFilters, firstPage, columnCount, }) {
4
7
  if (hasRows)
5
8
  return null;
6
9
  if (status === "loading")
7
- return loadingState ?? null;
10
+ return (_jsx(TableFeedbackRow, { columnCount: columnCount, rowIndex: 2, children: loadingState }));
8
11
  if (status === "error")
9
- return (_jsxs("div", { role: "alert", className: "mui-c432836760e3 mui-05ab12448317", children: [error ?? "Could not load rows.", retry && (_jsx(Button, { variant: "outline", size: "sm", onClick: retry, children: "Retry" }))] }));
10
- return (_jsx("div", { role: "status", className: "mui-222f930b8752 mui-302c0d124a94 mui-71556df3b421 mui-074569488cca mui-c7902d77ad80 mui-05ab12448317 mui-35f35c41d134", children: emptyState ??
11
- (pageIndex > 0 ? (_jsxs(_Fragment, { children: [_jsx("span", { children: "No results on this page." }), _jsx(Button, { variant: "outline", size: "sm", onClick: firstPage, children: "Go to first page" })] })) : hasFilters ? (_jsxs(_Fragment, { children: [_jsx("span", { children: "No results match your filters." }), clearFilters && (_jsx(Button, { variant: "outline", size: "sm", onClick: clearFilters, children: "Clear filters" }))] })) : ("No rows yet.")) }));
12
+ return (_jsx(TableFeedbackRow, { columnCount: columnCount, rowIndex: 2, children: _jsxs("div", { role: "alert", className: "mui-c432836760e3 mui-05ab12448317", children: [error ?? "Could not load rows.", retry && (_jsx(Button, { variant: "outline", size: "sm", onClick: retry, children: "Retry" }))] }) }));
13
+ return (_jsx(TableFeedbackRow, { columnCount: columnCount, rowIndex: 2, children: _jsx("div", { role: "status", className: "mui-222f930b8752 mui-302c0d124a94 mui-71556df3b421 mui-074569488cca mui-c7902d77ad80 mui-05ab12448317 mui-35f35c41d134", children: emptyState ??
14
+ (pageIndex > 0 ? (_jsxs(_Fragment, { children: [_jsx("span", { children: "No results on this page." }), _jsx(Button, { variant: "outline", size: "sm", onClick: firstPage, children: "Go to first page" })] })) : hasFilters ? (_jsxs(_Fragment, { children: [_jsx("span", { children: "No results match your filters." }), clearFilters && (_jsx(Button, { variant: "outline", size: "sm", onClick: clearFilters, children: "Clear filters" }))] })) : ("No rows yet.")) }) }));
15
+ }
16
+ export function TableLoadMore({ loadMore, columnCount, rowIndex, onLoad, }) {
17
+ return (_jsx(TableFeedbackRow, { columnCount: columnCount, rowIndex: rowIndex, className: "mui-964a9431ff49 mui-635702706586", children: _jsxs("div", { className: "mui-222f930b8752 mui-71556df3b421 mui-a503dd374cca mui-074569488cca mui-96b92a971c95 mui-c74ab393b96d mui-35f35c41d134", children: [loadMore.loading ? (_jsxs("div", { role: "status", className: "mui-222f930b8752 mui-71556df3b421 mui-074569488cca", children: [_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", className: "mui-2bd43fb57913 mui-27ead27a81df mui-2c14b55d77e6 mui-237747164b60", children: _jsx("path", { d: "M12 3v3m6.366-.366-2.12 2.12M21 12h-3m.366 6.366-2.12-2.12M12 21v-3m-6.366.366 2.12-2.12M3 12h3m-.366-6.366 2.12 2.12" }) }), _jsx("span", { children: "Loading more\u2026" })] })) : (_jsx(Button, { size: "sm", variant: "ghost", onClick: onLoad, children: loadMore.error ? "Retry loading more" : "Load more" })), loadMore.error && _jsx("span", { role: "alert", children: loadMore.error })] }) }));
12
18
  }
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react";
2
+ /** Shared sizing for the composed view and the table with built-in controls. */
3
+ export declare function TableFrame({ layout, stretch, slot, header, footer, children, }: {
4
+ layout: "content" | "fill";
5
+ stretch?: boolean;
6
+ slot?: string;
7
+ header?: ReactNode;
8
+ footer?: ReactNode;
9
+ children: ReactNode;
10
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../utils.js";
4
+ import { useFillLayout } from "./use-fill-layout.js";
5
+ /** Shared sizing for the composed view and the table with built-in controls. */
6
+ export function TableFrame({ layout, stretch = false, slot = "table-view", header, footer, children, }) {
7
+ const fill = layout === "fill";
8
+ const ref = useFillLayout(fill);
9
+ return (_jsxs("div", { ref: ref, "data-mendy-ui": "", "data-slot": slot, "data-layout": layout, className: cn("mui-184ddc11e5f9", fill ? "mui-222f930b8752 mui-410da8dfa8ac mui-302c0d124a94 mui-074569488cca" : "mui-267171770524"), style: !fill && stretch ? { height: "100%" } : undefined, children: [header && _jsx("div", { className: "mui-27ead27a81df", children: header }), _jsx("div", { className: fill ? "mui-0f2a693e93e2 mui-410da8dfa8ac mui-7bd5bab6d7f4 mui-b5985369ee35" : undefined, style: !fill && stretch ? { height: "100%" } : undefined, children: children }), footer && _jsx("div", { className: "mui-27ead27a81df mui-267171770524", children: footer })] }));
10
+ }
@@ -20,6 +20,10 @@ export interface TableViewProps<T extends object> extends TableDataState {
20
20
  label?: string;
21
21
  /** Defaults to "auto": fit the rows, capped at 65% of the viewport height. */
22
22
  height?: number | string;
23
+ /** Fill the remaining page height, or fit content by default. Fill overrides height. */
24
+ layout?: "content" | "fill";
25
+ /** Controls below the grid, included in the available height when layout is fill. */
26
+ footer?: ReactNode;
23
27
  /** Auto measures multiline rows and retains all columns to keep their height stable. */
24
28
  rowHeight?: number | "auto";
25
29
  rowClassName?: (row: T) => string | undefined;
@@ -45,4 +49,4 @@ export interface TableViewProps<T extends object> extends TableDataState {
45
49
  rowSelectionControls?: boolean;
46
50
  onCopyError?: (error: unknown) => void;
47
51
  }
48
- export declare function TableView<T extends object>({ table, label, height, rowHeight: rowHeightOption, rowClassName, renderRowDetail, className, contentClassName, emptyState, loadingState, scrollRef, status, refreshing, error, retry, loadMore, queryKey, filters, renderHeader, onRowClick, onRowActivate, isRowHighlighted, rowSelectionControls, onCopyError, }: TableViewProps<T>): import("react/jsx-runtime").JSX.Element;
52
+ export declare function TableView<T extends object>({ table, label, height, layout: layoutMode, footer, rowHeight: rowHeightOption, rowClassName, renderRowDetail, className, contentClassName, emptyState, loadingState, scrollRef, status, refreshing, error, retry, loadMore, queryKey, filters, renderHeader, onRowClick, onRowActivate, isRowHighlighted, rowSelectionControls, onCopyError, }: TableViewProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -9,11 +9,13 @@ import { useColumnWindow } from "./use-column-window.js";
9
9
  import { useViewportWidth } from "./use-viewport-width.js";
10
10
  import { tableLayout } from "./table-layout.js";
11
11
  import { TableLoadingRows } from "./table-loading.js";
12
- import { TableInitialState } from "./table-feedback.js";
12
+ import { TableInitialState, TableFeedbackRow, TableLoadMore } from "./table-feedback.js";
13
13
  import { useTableInteraction } from "./use-table-interaction.js";
14
14
  import { useTableResults } from "./use-table-results.js";
15
15
  import { useInlineRowSelection } from "./use-inline-row-selection.js";
16
- export function TableView({ table, label = "Data table", height = "auto", rowHeight: rowHeightOption = 44, rowClassName, renderRowDetail, className, contentClassName, emptyState, loadingState, scrollRef, status = "ready", refreshing, error, retry, loadMore, queryKey, filters, renderHeader, onRowClick, onRowActivate, isRowHighlighted, rowSelectionControls = true, onCopyError, }) {
16
+ import { TableFrame } from "./table-frame.js";
17
+ export function TableView({ table, label = "Data table", height = "auto", layout: layoutMode = "content", footer, rowHeight: rowHeightOption = 44, rowClassName, renderRowDetail, className, contentClassName, emptyState, loadingState, scrollRef, status = "ready", refreshing, error, retry, loadMore, queryKey, filters, renderHeader, onRowClick, onRowActivate, isRowHighlighted, rowSelectionControls = true, onCopyError, }) {
18
+ const fill = layoutMode === "fill";
17
19
  const { resultKey, hasFilters, clearFilters } = useTableResults(table, queryKey, filters);
18
20
  const autoRowHeight = rowHeightOption === "auto" || Boolean(renderRowDetail);
19
21
  const rowHeight = rowHeightOption === "auto" ? 44 : rowHeightOption;
@@ -26,6 +28,9 @@ export function TableView({ table, label = "Data table", height = "auto", rowHei
26
28
  scrollRef.current = node;
27
29
  }, [scrollRef]);
28
30
  const rows = table.getRowModel().rows;
31
+ const hasInitialFeedback = rows.length === 0;
32
+ const hasRefreshError = rows.length > 0 && status === "error";
33
+ const feedbackRowCount = Number(hasInitialFeedback) + Number(hasRefreshError) + Number(Boolean(loadMore?.available));
29
34
  const getScrollElement = useCallback(() => container.current, []);
30
35
  const estimateSize = useCallback(() => rowHeight, [rowHeight]);
31
36
  const getItemKey = useCallback((index) => rows[index]?.id ?? index, [rows]);
@@ -99,17 +104,17 @@ export function TableView({ table, label = "Data table", height = "auto", rowHei
99
104
  virtual,
100
105
  ]);
101
106
  const headersById = new Map(table.getFlatHeaders().map((header) => [header.column.id, header]));
102
- return (_jsxs("div", { "data-mendy-ui": "", className: "mui-184ddc11e5f9", children: [_jsx("div", { "aria-live": "polite", className: "mui-32fb090591d9", children: announcement }), _jsxs("div", { ref: setContainer, role: "grid", tabIndex: -1, "aria-label": label, "aria-rowcount": renderRowDetail ? -1 : rows.length + 1, "aria-colcount": layout.columns.length, "aria-busy": status === "loading" || refreshing, style: {
103
- height: height === "auto" ? undefined : height,
104
- maxHeight: height === "auto" ? "65dvh" : undefined,
105
- }, className: cn("mui-d2d9e1f13413 mui-014aadadffad mui-1a26a0d28420 mui-3fa8c572949b mui-4f1a55de40bc mui-5b272f3c5076 mui-c74ab393b96d mui-b30fc56058b6", className), onKeyDown: onKeyDown, children: [_jsx("div", { role: "row", "aria-rowindex": 1, className: "mui-964a9431ff49 mui-98599e4ee250 mui-4e8f0a87a0dc mui-222f930b8752 mui-2bf6510f1330 mui-bbe39cfb5cc6 mui-5b272f3c5076", style: { width: totalWidth }, children: columns.map((column) => (_jsxs(Fragment, { children: [columnGaps.has(column.id) && (_jsx("div", { "aria-hidden": "true", className: "mui-27ead27a81df", style: { width: columnGaps.get(column.id) } })), _jsx(TableHeaderCell, { table: table, header: headersById.get(column.id), index: columnIndexes.get(column.id), style: cellStyle(column), renderHeader: renderHeader, contentClassName: contentClassName, selectLoaded: selectLoaded }, column.id)] }, column.id))) }), _jsx(TableInitialState, { status: status, hasRows: rows.length > 0, loadingState: loadingState ?? (_jsx(TableLoadingRows, { columns: columns, columnGaps: columnGaps, width: totalWidth, rowHeight: rowHeight, cellStyle: cellStyle, count: height === "auto"
107
+ return (_jsxs(TableFrame, { layout: layoutMode, stretch: height === "100%", footer: footer, children: [_jsx("div", { "aria-live": "polite", className: "mui-32fb090591d9", children: announcement }), _jsxs("div", { ref: setContainer, role: "grid", tabIndex: -1, "aria-label": label, "aria-rowcount": renderRowDetail ? -1 : rows.length + 1 + feedbackRowCount, "aria-colcount": layout.columns.length, "aria-busy": status === "loading" || refreshing, style: {
108
+ height: fill || height === "auto" ? undefined : height,
109
+ maxHeight: !fill && height === "auto" ? "65dvh" : undefined,
110
+ }, className: cn("mui-d2d9e1f13413 mui-014aadadffad mui-1a26a0d28420 mui-3fa8c572949b mui-4f1a55de40bc mui-5b272f3c5076 mui-c74ab393b96d mui-b30fc56058b6", fill && "mui-410da8dfa8ac mui-7bd5bab6d7f4", className), onKeyDown: onKeyDown, children: [_jsx("div", { role: "row", "aria-rowindex": 1, className: "mui-964a9431ff49 mui-98599e4ee250 mui-4e8f0a87a0dc mui-222f930b8752 mui-2bf6510f1330 mui-bbe39cfb5cc6 mui-5b272f3c5076", style: { width: totalWidth }, children: columns.map((column) => (_jsxs(Fragment, { children: [columnGaps.has(column.id) && (_jsx("div", { "aria-hidden": "true", className: "mui-27ead27a81df", style: { width: columnGaps.get(column.id) } })), _jsx(TableHeaderCell, { table: table, header: headersById.get(column.id), index: columnIndexes.get(column.id), style: cellStyle(column), renderHeader: renderHeader, contentClassName: contentClassName, selectLoaded: selectLoaded }, column.id)] }, column.id))) }), _jsx(TableInitialState, { columnCount: layout.columns.length, status: status, hasRows: rows.length > 0, loadingState: loadingState ?? (_jsx(TableLoadingRows, { columns: columns, columnGaps: columnGaps, width: totalWidth, rowHeight: rowHeight, cellStyle: cellStyle, count: !fill && height === "auto"
106
111
  ? 8
107
- : Math.max(1, Math.ceil(((virtual.scrollRect?.height ?? 400) - rowHeight) / rowHeight)) })), emptyState: emptyState, hasFilters: hasFilters, pageIndex: table.state.pagination.pageIndex, clearFilters: clearFilters, firstPage: () => table.setPageIndex(0), error: error, retry: retry }), _jsx("div", { role: "rowgroup", className: "mui-d2d9e1f13413 mui-2bf6510f1330", style: { height: virtual.getTotalSize(), width: totalWidth }, children: items.map((item) => (_jsx(TableBodyRow, { row: rows[item.index], rowIndex: item.index, start: item.start, rowHeight: rowHeight, autoRowHeight: autoRowHeight, measureElement: autoRowHeight ? virtual.measureElement : undefined, rowClassName: rowClassName, renderRowDetail: renderRowDetail, detailWidth: viewportWidth ?? totalWidth, cellStyle: cellStyle, columns: columns, columnGaps: columnGaps, columnIndexes: columnIndexes, contentVersion: table.options, contentState: table.state, focusedId: focused?.id, copied: copied, contentClassName: contentClassName, onRowClick: onRowClick, onRowActivate: onRowActivate, isRowHighlighted: isRowHighlighted, inlineSelection: inlineSelection }, item.key))) }), rows.length > 0 && status === "error" && (_jsxs("div", { role: "alert", className: "mui-964a9431ff49 mui-30150dd033ab mui-5b272f3c5076 mui-094f5333853b", children: [error ?? "Could not refresh rows.", retry && (_jsx(Button, { onClick: retry, variant: "outline", size: "sm", children: "Retry" }))] })), loadMore?.available && (_jsxs("div", { className: "mui-964a9431ff49 mui-635702706586 mui-222f930b8752 mui-71556df3b421 mui-a503dd374cca mui-074569488cca mui-96b92a971c95 mui-c74ab393b96d mui-35f35c41d134", children: [loadMore.loading ? (_jsxs("div", { role: "status", className: "mui-222f930b8752 mui-71556df3b421 mui-074569488cca", children: [_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", className: "mui-2bd43fb57913 mui-27ead27a81df mui-2c14b55d77e6 mui-237747164b60", children: _jsx("path", { d: "M12 3v3m6.366-.366-2.12 2.12M21 12h-3m.366 6.366-2.12-2.12M12 21v-3m-6.366.366 2.12-2.12M3 12h3m-.366-6.366 2.12 2.12" }) }), _jsx("span", { children: "Loading more\u2026" })] })) : (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => {
108
- requested.current = null;
109
- void Promise.resolve()
110
- .then(() => loadMore.load())
111
- .catch(() => {
112
- requested.current = null;
113
- });
114
- }, children: loadMore.error ? "Retry loading more" : "Load more" })), loadMore.error && _jsx("span", { role: "alert", children: loadMore.error })] }))] })] }));
112
+ : Math.max(1, Math.ceil(((virtual.scrollRect?.height ?? 400) - rowHeight) / rowHeight)) })), emptyState: emptyState, hasFilters: hasFilters, pageIndex: table.state.pagination.pageIndex, clearFilters: clearFilters, firstPage: () => table.setPageIndex(0), error: error, retry: retry }), _jsx("div", { role: "rowgroup", className: "mui-d2d9e1f13413 mui-2bf6510f1330", style: { height: virtual.getTotalSize(), width: totalWidth }, children: items.map((item) => (_jsx(TableBodyRow, { row: rows[item.index], rowIndex: item.index, start: item.start, rowHeight: rowHeight, autoRowHeight: autoRowHeight, measureElement: autoRowHeight ? virtual.measureElement : undefined, rowClassName: rowClassName, renderRowDetail: renderRowDetail, detailWidth: viewportWidth ?? totalWidth, cellStyle: cellStyle, columns: columns, columnGaps: columnGaps, columnIndexes: columnIndexes, contentVersion: table.options, contentState: table.state, focusedId: focused?.id, copied: copied, contentClassName: contentClassName, onRowClick: onRowClick, onRowActivate: onRowActivate, isRowHighlighted: isRowHighlighted, inlineSelection: inlineSelection }, item.key))) }), hasRefreshError && (_jsx(TableFeedbackRow, { columnCount: layout.columns.length, rowIndex: rows.length + 2, className: "mui-964a9431ff49 mui-30150dd033ab", children: _jsxs("div", { role: "alert", className: "mui-5b272f3c5076 mui-094f5333853b", children: [error ?? "Could not refresh rows.", retry && (_jsx(Button, { onClick: retry, variant: "outline", size: "sm", children: "Retry" }))] }) })), loadMore?.available && (_jsx(TableLoadMore, { loadMore: loadMore, columnCount: layout.columns.length, rowIndex: rows.length + 2 + Number(hasInitialFeedback || hasRefreshError), onLoad: () => {
113
+ requested.current = null;
114
+ void Promise.resolve()
115
+ .then(() => loadMore.load())
116
+ .catch(() => {
117
+ requested.current = null;
118
+ });
119
+ } }))] })] }));
115
120
  }
@@ -0,0 +1,2 @@
1
+ /** Size the containing frame, not the grid, so controls share its available height. */
2
+ export declare function useFillLayout(enabled: boolean): (node: HTMLDivElement | null) => void;
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { useCallback, useRef } from "react";
3
+ /** Size the containing frame, not the grid, so controls share its available height. */
4
+ export function useFillLayout(enabled) {
5
+ const dispose = useRef(undefined);
6
+ return useCallback((node) => {
7
+ dispose.current?.();
8
+ dispose.current = undefined;
9
+ if (!node || !enabled)
10
+ return;
11
+ let frame = 0;
12
+ let previous = -1;
13
+ const measure = () => {
14
+ frame = 0;
15
+ let inset = 0;
16
+ let ancestor = node.parentElement;
17
+ while (ancestor) {
18
+ const style = getComputedStyle(ancestor);
19
+ inset += parseFloat(style.paddingBottom) || 0;
20
+ inset += parseFloat(style.borderBottomWidth) || 0;
21
+ ancestor = ancestor.parentElement;
22
+ }
23
+ const height = Math.max(0, window.innerHeight - node.getBoundingClientRect().top - inset);
24
+ if (Math.abs(height - previous) < 0.5)
25
+ return;
26
+ previous = height;
27
+ node.style.height = `${height}px`;
28
+ };
29
+ const schedule = () => {
30
+ if (!frame)
31
+ frame = requestAnimationFrame(measure);
32
+ };
33
+ const observer = new ResizeObserver(schedule);
34
+ const observe = () => {
35
+ observer.disconnect();
36
+ let current = node;
37
+ while (current?.parentElement) {
38
+ observer.observe(current.parentElement);
39
+ let sibling = current.previousElementSibling;
40
+ while (sibling) {
41
+ observer.observe(sibling);
42
+ sibling = sibling.previousElementSibling;
43
+ }
44
+ current = current.parentElement;
45
+ }
46
+ schedule();
47
+ };
48
+ // Newly inserted preceding siblings can move a table inside a fixed-height parent.
49
+ const mutations = new MutationObserver(observe);
50
+ let parent = node.parentElement;
51
+ while (parent) {
52
+ mutations.observe(parent, { childList: true });
53
+ parent = parent.parentElement;
54
+ }
55
+ measure();
56
+ observe();
57
+ window.addEventListener("resize", schedule);
58
+ dispose.current = () => {
59
+ observer.disconnect();
60
+ mutations.disconnect();
61
+ window.removeEventListener("resize", schedule);
62
+ cancelAnimationFrame(frame);
63
+ if (node.style.height === `${previous}px`)
64
+ node.style.removeProperty("height");
65
+ };
66
+ }, [enabled]);
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mendylanda/ui",
3
- "version": "0.3.0-alpha.9",
3
+ "version": "0.3.1",
4
4
  "description": "Mendy Landa’s reusable React components. Typed filters and tables with shared interaction behavior and customizable UI.",
5
5
  "homepage": "https://ui.mendylanda.com",
6
6
  "license": "MIT",
@@ -71,11 +71,6 @@
71
71
  "publishConfig": {
72
72
  "access": "public"
73
73
  },
74
- "scripts": {
75
- "build": "tsc -p tsconfig.build.json && node scripts/build-css.mjs",
76
- "typecheck": "tsc -p tsconfig.build.json --noEmit",
77
- "prepack": "pnpm build"
78
- },
79
74
  "dependencies": {
80
75
  "@hello-pangea/dnd": "18.0.1",
81
76
  "@radix-ui/react-checkbox": "^1.3.3",
@@ -112,5 +107,9 @@
112
107
  "nuqs": {
113
108
  "optional": true
114
109
  }
110
+ },
111
+ "scripts": {
112
+ "build": "tsc -p tsconfig.build.json && node scripts/build-css.mjs",
113
+ "typecheck": "tsc -p tsconfig.build.json --noEmit"
115
114
  }
116
- }
115
+ }