laif-ds 1.0.5 → 1.1.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/dist/CHANGELOG.md +23 -0
- package/dist/_virtual/index2.js +2 -5
- package/dist/_virtual/index3.js +5 -2
- package/dist/_virtual/index4.js +5 -5
- package/dist/_virtual/index5.js +5 -5
- package/dist/_virtual/index6.js +2 -2
- package/dist/_virtual/index7.js +2 -2
- package/dist/agent-docs/adoption-report.json +165 -39
- package/dist/agent-docs/components/AppCard.md +25 -21
- package/dist/agent-docs/components/AppDataGrid/columns.md +113 -0
- package/dist/agent-docs/components/AppDataGrid/dom-hooks.md +75 -0
- package/dist/agent-docs/components/AppDataGrid/examples.md +129 -0
- package/dist/agent-docs/components/AppDataGrid/filtering.md +126 -0
- package/dist/agent-docs/components/AppDataGrid/grouping.md +20 -0
- package/dist/agent-docs/components/AppDataGrid/i18n-and-accessibility.md +40 -0
- package/dist/agent-docs/components/AppDataGrid/layout.md +45 -0
- package/dist/agent-docs/components/AppDataGrid/migration-from-datatable.md +32 -0
- package/dist/agent-docs/components/AppDataGrid/saved-views.md +76 -0
- package/dist/agent-docs/components/AppDataGrid/selection.md +44 -0
- package/dist/agent-docs/components/AppDataGrid/server-and-pagination.md +73 -0
- package/dist/agent-docs/components/AppDataGrid/toolbar.md +28 -0
- package/dist/agent-docs/components/AppDataGrid.md +278 -0
- package/dist/agent-docs/components/AppEditor.md +18 -13
- package/dist/agent-docs/components/AppSelect.md +51 -0
- package/dist/agent-docs/components/DataTable.md +40 -3
- package/dist/agent-docs/components/DatePicker.md +8 -7
- package/dist/agent-docs/components/DropdownMenu.md +2 -0
- package/dist/agent-docs/components/FileUploader.md +12 -12
- package/dist/agent-docs/components/GridFilterPanel.md +128 -0
- package/dist/agent-docs/components/SidebarMenuBadge.md +87 -0
- package/dist/agent-docs/components-list.md +16 -1
- package/dist/agent-docs/manifest.json +253 -30
- package/dist/components/design-tokens.js +138 -2
- package/dist/components/ui/app-data-grid/app-data-grid.js +900 -0
- package/dist/components/ui/app-data-grid/engine/use-grid-column-resize.js +110 -0
- package/dist/components/ui/app-data-grid/engine/use-grid-engine.js +621 -0
- package/dist/components/ui/app-data-grid/engine/use-grid-infinite-loader.js +50 -0
- package/dist/components/ui/app-data-grid/engine/use-grid-virtualizer.js +90 -0
- package/dist/components/ui/app-data-grid/grid-aggregate.js +5 -0
- package/dist/components/ui/app-data-grid/grid-column-registry.js +28 -0
- package/dist/components/ui/app-data-grid/grid-columns.js +204 -0
- package/dist/components/ui/app-data-grid/grid-filter-client.js +73 -0
- package/dist/components/ui/app-data-grid/grid-filter-compile.js +33 -0
- package/dist/components/ui/app-data-grid/grid-filter-operators.js +141 -0
- package/dist/components/ui/app-data-grid/grid-grouping.js +104 -0
- package/dist/components/ui/app-data-grid/grid-header-matrix.js +112 -0
- package/dist/components/ui/app-data-grid/grid-i18n.js +208 -0
- package/dist/components/ui/app-data-grid/grid-mount-count.js +8 -0
- package/dist/components/ui/app-data-grid/grid-reducer.js +202 -0
- package/dist/components/ui/app-data-grid/grid-result-scope.js +35 -0
- package/dist/components/ui/app-data-grid/grid-tokens.js +169 -0
- package/dist/components/ui/app-data-grid/grid-view-preset.js +97 -0
- package/dist/components/ui/app-data-grid/parts/grid-actions-row.js +86 -0
- package/dist/components/ui/app-data-grid/parts/grid-cell-text.js +67 -0
- package/dist/components/ui/app-data-grid/parts/grid-column-registry-panel.js +320 -0
- package/dist/components/ui/app-data-grid/parts/grid-density-control.js +88 -0
- package/dist/components/ui/app-data-grid/parts/grid-filter-chips.js +234 -0
- package/dist/components/ui/app-data-grid/parts/grid-filter-editor.js +201 -0
- package/dist/components/ui/app-data-grid/parts/grid-filter-panel.js +397 -0
- package/dist/components/ui/app-data-grid/parts/grid-filter-row.js +277 -0
- package/dist/components/ui/app-data-grid/parts/grid-fullscreen-toggle.js +28 -0
- package/dist/components/ui/app-data-grid/parts/grid-group-row.js +135 -0
- package/dist/components/ui/app-data-grid/parts/grid-header.js +337 -0
- package/dist/components/ui/app-data-grid/parts/grid-pagination-control.js +238 -0
- package/dist/components/ui/app-data-grid/parts/grid-portal-container.js +14 -0
- package/dist/components/ui/app-data-grid/parts/grid-row.js +181 -0
- package/dist/components/ui/app-data-grid/parts/grid-scroll-area.js +185 -0
- package/dist/components/ui/app-data-grid/parts/grid-selection-bar.js +195 -0
- package/dist/components/ui/app-data-grid/parts/grid-status-surface.js +66 -0
- package/dist/components/ui/app-data-grid/parts/grid-table.js +482 -0
- package/dist/components/ui/app-data-grid/parts/grid-toolbar-slots.js +224 -0
- package/dist/components/ui/app-data-grid/use-grid-filters.js +183 -0
- package/dist/components/ui/app-select.js +334 -222
- package/dist/components/ui/button.js +7 -6
- package/dist/components/ui/date-picker.js +36 -34
- package/dist/components/ui/dropdown-menu.js +3 -3
- package/dist/components/ui/tables/data-cross-table/data-cross-table.js +6 -6
- package/dist/components/ui/tables/data-table/data-table-constants.js +36 -16
- package/dist/components/ui/tables/data-table/data-table-i18n.js +135 -1
- package/dist/css-for-template.css +26 -0
- package/dist/index.d.ts +5957 -124
- package/dist/index.js +462 -395
- package/dist/node_modules/eventemitter3/index.js +1 -1
- package/dist/node_modules/eventemitter3/index2.js +1 -1
- package/dist/node_modules/hast-util-to-jsx-runtime/lib/index.js +1 -1
- package/dist/node_modules/recharts/es6/util/Events.js +1 -1
- package/dist/node_modules/style-to-object/cjs/index.js +1 -1
- package/dist/node_modules/unified/lib/index.js +1 -1
- package/dist/node_modules/use-sync-external-store/shim/index.js +1 -1
- package/dist/styles.css +2 -1
- package/dist/styles.v3.css +1 -1
- package/package.json +13 -6
- /package/dist/agent-docs/components/{truncated-cell.md → TruncatedCell.md} +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# AppDataGrid — DOM hooks
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## DOM hooks (for tests)
|
|
6
|
+
|
|
7
|
+
Stable attributes, independent of the i18n language.
|
|
8
|
+
|
|
9
|
+
| Selector / attribute | Meaning |
|
|
10
|
+
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `[data-app-data-grid]` | Grid root. |
|
|
12
|
+
| `data-emissions` (root) | Query emissions: `0` at mount, +1 per emission, unchanged by view actions. Debounced: read in `waitFor`. |
|
|
13
|
+
| `data-render-count` (root) | React commits of the root since mount. |
|
|
14
|
+
| `data-mount-count` (root) | Mounts of a grid with this `id` / `data-testid` (stays `1` across full-screen toggles). |
|
|
15
|
+
| `data-loadmore-call-count` (root) | Load-more requests issued (infinite mode). |
|
|
16
|
+
| `data-grid-fullscreen` (root) | `"true"` / `"false"`. |
|
|
17
|
+
| `[data-grid-toolbar]` | Toolbar. |
|
|
18
|
+
| `[data-grid-search]` | Search input. |
|
|
19
|
+
| `[data-grid-viewport]` | Scroll container; its `clientHeight` equals `viewportHeight`. |
|
|
20
|
+
| `[data-grid-table]` | The `<table>`. |
|
|
21
|
+
| `[data-grid-col="<id>"]` | `<col>` of a column — count columns with this (the extra layout col is `[data-grid-col-filler]`). |
|
|
22
|
+
| `[data-grid-header="<id>"]` | Leaf `<th>` (`aria-sort`). |
|
|
23
|
+
| `[data-grid-header-group="<id>"]` | Group `<th>` (`scope="colgroup"`). |
|
|
24
|
+
| `[data-grid-header-row="<n>"]` | Header `<tr>`, 0-based. |
|
|
25
|
+
| `[data-grid-sort="<id>"]` | Sort button. |
|
|
26
|
+
| `[data-grid-sort-index]` | Multi-sort priority badge. |
|
|
27
|
+
| `[data-grid-resize-handle="<id>"]` | Resize handle (`aria-valuenow` = width). |
|
|
28
|
+
| `[data-grid-row]` | Data row, never a spacer. Use `querySelector` for the first one (the first `<tr>` may be a spacer). |
|
|
29
|
+
| `[data-grid-cell="<id>"]` | Body cell. |
|
|
30
|
+
| `[data-grid-row-header]` | Row-header cell (`<th scope="row">`). |
|
|
31
|
+
| `[data-grid-spacer]` | Virtualization spacer rows. |
|
|
32
|
+
| `[data-grid-skeleton-row]` | Skeleton row (`aria-hidden`). |
|
|
33
|
+
| `[data-grid-pinned="true"]` | Pinned cell (either side). |
|
|
34
|
+
| `[data-grid-cell-text]` | Text cell that fits. |
|
|
35
|
+
| `[data-grid-cell-expand]` | Focusable target of a truncated text cell. |
|
|
36
|
+
| `[data-grid-row-menu]` | Row-menu trigger. |
|
|
37
|
+
| `[data-grid-row-menu-item="<id>"]` | Row-menu item (`data-variant="destructive"` when destructive). |
|
|
38
|
+
| `[data-grid-live-region]` | The single status region. |
|
|
39
|
+
| `[data-grid-empty]` | Empty-outcome cell. |
|
|
40
|
+
| `[data-grid-error]` | Error-outcome cell. |
|
|
41
|
+
| `[data-grid-status-cell]` | The outcome cell, empty or error. |
|
|
42
|
+
| `[data-grid-retry]` | Retry button (absent without `onRetry`). |
|
|
43
|
+
| `[data-grid-footer]` | Totals `<tfoot>`. |
|
|
44
|
+
| `[data-grid-footer-cell="<id>"]` | Footer cell. |
|
|
45
|
+
| `[data-grid-pagination]` | Paginator. |
|
|
46
|
+
| `[data-grid-page-range]` | «Rows x-y of N». |
|
|
47
|
+
| `[data-grid-page-number="<n>"]` | Page button (1-based; current one has `aria-current="page"`). |
|
|
48
|
+
| `[data-grid-page-size]` | Wrapper of the page-size selector. |
|
|
49
|
+
| `tr[data-grid-filter-row]` | Filter row (absent while hidden). |
|
|
50
|
+
| `[data-grid-filter-cell="<id>"]` | Filter cell. |
|
|
51
|
+
| `[data-grid-filter-cell-trigger="<id>"]` | Filter cell's button. |
|
|
52
|
+
| `[data-grid-header-filtered="<id>"]` | Funnel on a filtered column's header. |
|
|
53
|
+
| `[data-grid-toolbar-filter-row-toggle]` | Filter-row toggle. |
|
|
54
|
+
| `[data-grid-toolbar-columns]` | Columns-panel trigger. |
|
|
55
|
+
| `[data-grid-fullscreen-toggle]` | Full-screen toggle. |
|
|
56
|
+
| `[data-grid-actions-row]` | Container of the `actions` prop buttons. |
|
|
57
|
+
| `[data-grid-action="<label>"]` | One `actions` command. |
|
|
58
|
+
| `[data-grid-actions-trigger]` | Collapsed trigger (two or more actions). |
|
|
59
|
+
| `[data-grid-group-row="<path>"]` | Group row (`aria-expanded`). |
|
|
60
|
+
| `[data-grid-group-toggle="<path>"]` | Group expand/collapse control. |
|
|
61
|
+
| `[data-grid-group-count]` | Group row count — read the attribute (the text has parentheses). |
|
|
62
|
+
| `[data-grid-group-select="<path>"]` | Group checkbox. |
|
|
63
|
+
| `[data-grid-select-all]` | Header checkbox. |
|
|
64
|
+
| `[data-grid-selection-bar]` | Selection bar. |
|
|
65
|
+
| `[data-grid-selection-count]` | Selected count. |
|
|
66
|
+
| `[data-grid-selection-action="<label>"]` | Bulk action. |
|
|
67
|
+
| `[data-grid-toolbar-custom-filters]` | Toolbar group of custom filters. |
|
|
68
|
+
| `[data-grid-toolbar-custom-filter="<id>"]` | One custom filter (by badge id). |
|
|
69
|
+
| `[data-grid-filter-chip-trigger="<id>"]` | A chip in `GridFilterChips`. |
|
|
70
|
+
| `[data-grid-filter-add-chip]` | «+ Filter» chip. |
|
|
71
|
+
| `[data-grid-filter-scope]` | Chips' scope clause. |
|
|
72
|
+
| `[data-grid-column-registry-item="<id>"]` | Columns-panel row. |
|
|
73
|
+
| `[data-grid-column-registry-toggle="<id>"]` | Columns-panel visibility checkbox. |
|
|
74
|
+
|
|
75
|
+
---
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# AppDataGrid — Examples
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
### Server grid with `onQueryChange`
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import {
|
|
12
|
+
AppDataGrid,
|
|
13
|
+
type GridColumnNode,
|
|
14
|
+
type GridServerState,
|
|
15
|
+
} from "laif-ds";
|
|
16
|
+
|
|
17
|
+
const initialQuery: GridServerState = {
|
|
18
|
+
pagination: { pageIndex: 0, pageSize: 25 },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const UsersGrid = ({ columns }: { columns: GridColumnNode<User>[] }) => {
|
|
22
|
+
// The grid does not emit at mount: the initial query is fetched by the consumer.
|
|
23
|
+
const [query, setQuery] = useState<GridServerState>(initialQuery);
|
|
24
|
+
const { data, isLoading, isError, refetch } = useUsersSearch(query); // your data hook
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<AppDataGrid
|
|
28
|
+
columns={columns}
|
|
29
|
+
data={data?.items ?? []}
|
|
30
|
+
getRowId={(row) => String(row.id)}
|
|
31
|
+
serverMode
|
|
32
|
+
paginated
|
|
33
|
+
serverConfig={{ totalItems: data?.total ?? 0, onStateChange: setQuery }}
|
|
34
|
+
loading={isLoading}
|
|
35
|
+
error={isError}
|
|
36
|
+
onRetry={() => void refetch()}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
// In useUsersSearch, send query.computedFilter, query.computedSorting and query.pagination.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Grouping with aggregates
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
const columns: GridColumnNode<Sale>[] = [
|
|
47
|
+
createGridStringColumn<Sale>({ accessorKey: "country", header: "Country" }),
|
|
48
|
+
createGridStringColumn<Sale>({ accessorKey: "city", header: "City" }),
|
|
49
|
+
createGridNumberColumn<Sale, number>({
|
|
50
|
+
accessorKey: "amount",
|
|
51
|
+
header: "Amount",
|
|
52
|
+
align: "right",
|
|
53
|
+
aggregatedCell: (rows) => rows.reduce((sum, r) => sum + r.amount, 0),
|
|
54
|
+
aggregate: (rows) => rows.reduce((sum, r) => sum + r.amount, 0),
|
|
55
|
+
}),
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
<AppDataGrid
|
|
59
|
+
columns={columns}
|
|
60
|
+
data={sales}
|
|
61
|
+
groupBy={["country", "city"]}
|
|
62
|
+
initialOpenGroups="first"
|
|
63
|
+
groupLabelPlacement="first-column"
|
|
64
|
+
viewportHeight={520}
|
|
65
|
+
/>;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Lifted filters with chips and the filter row
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { useMemo, useState } from "react";
|
|
72
|
+
import {
|
|
73
|
+
AppDataGrid,
|
|
74
|
+
GridFilterChips,
|
|
75
|
+
gridResultScope,
|
|
76
|
+
gridViewportHeight,
|
|
77
|
+
toGridFilterColumns,
|
|
78
|
+
useGridFilters,
|
|
79
|
+
} from "laif-ds";
|
|
80
|
+
|
|
81
|
+
export const OrdersPage = ({ orders }: { orders: Order[] }) => {
|
|
82
|
+
const filters = useGridFilters({ columns: toGridFilterColumns(columns) });
|
|
83
|
+
const [filterRowOpen, setFilterRowOpen] = useState(true); // the row starts open
|
|
84
|
+
|
|
85
|
+
// With lifted filters the consumer filters the rows (client) or sends
|
|
86
|
+
// filters.computedFilter to the server.
|
|
87
|
+
const visibleOrders = useMemo(
|
|
88
|
+
() => applyMyFilters(orders, filters.committedBadges),
|
|
89
|
+
[orders, filters.committedBadges],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<GridFilterChips
|
|
95
|
+
badges={filters.badges}
|
|
96
|
+
onRemove={filters.removeBadge}
|
|
97
|
+
onClearAll={filters.clearBadges}
|
|
98
|
+
scope={gridResultScope({
|
|
99
|
+
badges: filters.badges,
|
|
100
|
+
resultTotal: visibleOrders.length,
|
|
101
|
+
})}
|
|
102
|
+
/>
|
|
103
|
+
<AppDataGrid
|
|
104
|
+
columns={columns}
|
|
105
|
+
data={visibleOrders}
|
|
106
|
+
resultTotal={visibleOrders.length}
|
|
107
|
+
filters={filters.badges}
|
|
108
|
+
columnVisibility={filters.columnVisibility}
|
|
109
|
+
onColumnVisibilityChange={(next) => {
|
|
110
|
+
for (const [id, visible] of Object.entries(next))
|
|
111
|
+
if (filters.columnVisibility[id] !== visible)
|
|
112
|
+
filters.setColumnVisible(id, visible);
|
|
113
|
+
}}
|
|
114
|
+
onAddFilter={filters.addBadge}
|
|
115
|
+
onUpdateFilter={filters.updateBadge}
|
|
116
|
+
onRemoveFilter={filters.removeBadge}
|
|
117
|
+
onFilterRowOpenChange={setFilterRowOpen}
|
|
118
|
+
viewportHeight={gridViewportHeight({
|
|
119
|
+
rowCount: 10,
|
|
120
|
+
density: "default",
|
|
121
|
+
hasFilterRow: filterRowOpen,
|
|
122
|
+
})}
|
|
123
|
+
/>
|
|
124
|
+
</>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# AppDataGrid — Filtering
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## Filtering
|
|
6
|
+
|
|
7
|
+
### Global search
|
|
8
|
+
|
|
9
|
+
The search field filters rows (client) or feeds `filters.searchbarFilter` and `computedFilter` (server).
|
|
10
|
+
Only columns with `searchable: true` are matched. Keystrokes are debounced by `debounceTime`.
|
|
11
|
+
|
|
12
|
+
### Filter row (default, `filterMode="row"`)
|
|
13
|
+
|
|
14
|
+
A row inside `<thead>` with one filter cell per column (plus the checkbox column). Each cell is a button
|
|
15
|
+
that opens an editor popover; the editor and its operators are derived from the column's `filterType`:
|
|
16
|
+
text field, number field, boolean, date / date-time picker, single or multi list (from `options`). A
|
|
17
|
+
list column without options is shown as an incomplete declaration, never as a free text field. Cells of
|
|
18
|
+
filtered columns get an accent border; filtered headers show a funnel icon. The toolbar toggle unmounts
|
|
19
|
+
and remounts the row.
|
|
20
|
+
|
|
21
|
+
Without lifted `filters`, the grid owns the filter state and (on the client branch) filters `data`
|
|
22
|
+
itself. Client-side matching reproduces the backend operators (`like`, `starts_with`, `eq`,
|
|
23
|
+
`array_overlap`, `date_after`, …), with edge-case differences a client cannot replicate (database
|
|
24
|
+
collation/case folding of non-ASCII text, `citext` columns, decimal precision). `%` and `_` in text
|
|
25
|
+
filters behave as `LIKE` wildcards, as on the server.
|
|
26
|
+
|
|
27
|
+
Operators (`GridFilterOperator`): `like`, `n_like`, `starts_with`, `ends_with`, `eq`, `ne`, `eq_null`,
|
|
28
|
+
`n_eq_null`, `gt`, `ge`, `lt`, `le`, `checked`, `unchecked`, `date_after`, `date_before`,
|
|
29
|
+
`date_time_after`, `date_time_before`, `array_overlap`, `n_array_overlap`. A range is expressed as two
|
|
30
|
+
badges (e.g. `ge` + `le`).
|
|
31
|
+
|
|
32
|
+
### Lifted filters (`useGridFilters`)
|
|
33
|
+
|
|
34
|
+
Lift the filter state when chips, counts or a server fetch live outside the grid. The hook owns the
|
|
35
|
+
badges; the grid, `GridFilterChips` and `GridFilterPanel` read/write the same store, and the chips work
|
|
36
|
+
without a grid in the tree.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const filters = useGridFilters({
|
|
40
|
+
columns: toGridFilterColumns(leafColumns), // required, for pruning
|
|
41
|
+
initialBadges, // optional
|
|
42
|
+
initialColumnVisibility, // optional
|
|
43
|
+
debounceTime, // optional, default 200
|
|
44
|
+
onFiltersChange: (computedFilter) => refetch(computedFilter), // optional
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`GridFiltersApi`: `badges` (live, feed the grid and editors), `committedBadges` (debounced, use them to
|
|
49
|
+
filter rows), `computedFilter`, `columnVisibility`, `panelOpen`, `emissions`, `addBadge`, `addBadges`
|
|
50
|
+
(several in one mutation — use for ranges), `updateBadge`, `removeBadge`, `removeBadges`, `clearBadges`,
|
|
51
|
+
`restore(badges, columnVisibility)`, `setColumnVisible(id, visible)`, `setPanelOpen(open)`.
|
|
52
|
+
|
|
53
|
+
Rules when `filters` is passed to the grid:
|
|
54
|
+
|
|
55
|
+
- **You filter the rows.** The grid does not apply lifted badges to `data`. On the client, filter with
|
|
56
|
+
`committedBadges`; on the server, send `filters.computedFilter` (or the value received by
|
|
57
|
+
`onFiltersChange`).
|
|
58
|
+
- **The grid does not emit for them** (except in infinite mode, below). `onFiltersChange` is called
|
|
59
|
+
synchronously inside the hook's actions (keystrokes coalesced by the hook's `debounceTime`; adding,
|
|
60
|
+
removing and restoring are immediate), and only when the payload actually changes.
|
|
61
|
+
- **Pagination resets to page 0** when the badges' content changes (compared by content, so an inline
|
|
62
|
+
`filters={badges.filter(...)}` is fine).
|
|
63
|
+
- Wire `onAddFilter={filters.addBadge}`, `onUpdateFilter={filters.updateBadge}` and
|
|
64
|
+
`onRemoveFilter={filters.removeBadge}`; pass `columnVisibility={filters.columnVisibility}` with
|
|
65
|
+
`onColumnVisibilityChange` so hidden columns also prune their badges.
|
|
66
|
+
|
|
67
|
+
#### Infinite mode restarts on its own
|
|
68
|
+
|
|
69
|
+
With `infinite` on (and `paginated` off), a change in the content of lifted `filters` makes the grid
|
|
70
|
+
emit **one** `onQueryChange` with `pageIndex: 0`; later load-mores ask for 1, 2, …. Keystrokes into an
|
|
71
|
+
existing badge coalesce into one restart. It never fires at mount, and a change that matches the last
|
|
72
|
+
emission (e.g. the `restore()` after a preset) does not restart. Recommended wiring in infinite mode:
|
|
73
|
+
do **not** also refetch in `onFiltersChange`; if you do, treat `pageIndex: 0` as a replace and drop
|
|
74
|
+
superseded responses.
|
|
75
|
+
|
|
76
|
+
### Filter chips, panel and columns panel
|
|
77
|
+
|
|
78
|
+
- `GridFilterChips` — `{ badges, onRemove, onClearAll?, scope?, onAddFilter?, onEditValue?, i18n?,
|
|
79
|
+
className? }`. A row of chips (one button per badge with an edit/remove menu), an optional dashed «+
|
|
80
|
+
Filter» chip, and a scope clause («128 rows · 2 filters») when `scope` is passed. Build `scope` with
|
|
81
|
+
`gridResultScope({ badges, resultTotal, rowsInPage?, pageIndex?, pageSize?, locale? })`.
|
|
82
|
+
- `GridFilterPanel` — the filter inventory overlay used by `filterMode="panel"`; see
|
|
83
|
+
[GridFilterPanel](./GridFilterPanel.md). Mount it inside the grid through `toolbar.filters.render`,
|
|
84
|
+
otherwise it is covered in full screen.
|
|
85
|
+
- `GridColumnRegistryPanel` — the standalone columns panel (`columns`, `columnVisibility`,
|
|
86
|
+
`onColumnVisibleChange`, `columnOrder?`, `onColumnOrderChange?`, `i18n?`, `className?`). The grid
|
|
87
|
+
already renders one behind `toolbar.columns`.
|
|
88
|
+
|
|
89
|
+
### Custom filters (without a column)
|
|
90
|
+
|
|
91
|
+
A filter that cites no column («only favourites», «older than N days») is declared once as a
|
|
92
|
+
`GridFilterControl` and enters the state as a badge with `kind: "custom"`. It is counted, cleared by
|
|
93
|
+
reset, appended to `computedFilter`, and never pruned by column visibility.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import {
|
|
97
|
+
createGridCustomFilterBadge,
|
|
98
|
+
defaultGridI18n,
|
|
99
|
+
type GridFilterControl,
|
|
100
|
+
type GridFilterControlRegistry,
|
|
101
|
+
} from "laif-ds";
|
|
102
|
+
|
|
103
|
+
const olderThan: GridFilterControl = {
|
|
104
|
+
type: "olderThan", // registry key
|
|
105
|
+
Control: ({ value, onChange, label }) => (
|
|
106
|
+
<DaysInput value={value} onChange={onChange} label={label} />
|
|
107
|
+
), // rendered by YOUR app, not by the grid
|
|
108
|
+
toFragment: (value, ctx) => ({ [ctx.field]: { operator: "ge", value } }), // null = not applied
|
|
109
|
+
describe: (value) => `Older than ${String(value)} days`, // the only text source
|
|
110
|
+
};
|
|
111
|
+
const registry: GridFilterControlRegistry = { olderThan };
|
|
112
|
+
|
|
113
|
+
filters.addBadge(
|
|
114
|
+
createGridCustomFilterBadge(olderThan, 30, defaultGridI18n, "older"),
|
|
115
|
+
);
|
|
116
|
+
// To change the value: rebuild with the SAME id and call filters.updateBadge("older", badge).
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`toFragment` and `describe` are both mandatory. `createGridCustomFilterBadge(control, value, i18n, id)`
|
|
120
|
+
is the only constructor; it fills `kind`, `customKey`, `customLabel` and `customFragment`. With lifted
|
|
121
|
+
`filters` and `onRemoveFilter`, the toolbar shows a dashed group with each custom filter's full name and
|
|
122
|
+
a remove button; chips show `customLabel`. Pass `filterControls={registry}` to flag badges whose key is
|
|
123
|
+
not registered. The grid never renders `Control`: custom filters are named and removable in the grid,
|
|
124
|
+
edited where your app creates them.
|
|
125
|
+
|
|
126
|
+
---
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# AppDataGrid — Grouping
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## Grouping
|
|
6
|
+
|
|
7
|
+
Client branch only. Each `groupBy` entry is one level; group rows show label, chevron and count
|
|
8
|
+
(`data-grid-group-count`), and one cell per visible column filled by that column's `aggregatedCell`.
|
|
9
|
+
|
|
10
|
+
- Groups are ordered by first appearance (i.e. by the current sort), not alphabetically.
|
|
11
|
+
- Expanding is view state and never emits. Opening or closing a group above the viewport keeps what you
|
|
12
|
+
are reading in place; closing a group you are reading inside lands on the group row.
|
|
13
|
+
- Hiding the grouping column does not ungroup: the label moves to the first visible column.
|
|
14
|
+
- A grouped grid always has a bounded viewport (`viewportHeight`, default 480).
|
|
15
|
+
- `aria-rowcount` counts the traversable rows — data rows plus the group rows on screen — so it changes
|
|
16
|
+
as groups open and close.
|
|
17
|
+
- The `<tfoot>` total (`aggregate`) sums the data rows, not the group aggregates.
|
|
18
|
+
- Group-row checkboxes select every leaf of the group, even when it is collapsed.
|
|
19
|
+
|
|
20
|
+
---
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# AppDataGrid — i18n and accessibility
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## i18n
|
|
6
|
+
|
|
7
|
+
`GridI18n` is a superset of `DataTableI18n`. Required top-level keys: `search`, `searchPlaceholder`,
|
|
8
|
+
`clearSearch`, `reset`, `notFoundMessage`, `searchTooltipLabel`, `selectAll`, `selectRow`, and the
|
|
9
|
+
sections `filters`, `stringOperators`, `numberOperators`, `booleanOperators`, `dateOperators`,
|
|
10
|
+
`dateTimeOperators`, `listOperators`, `sorting`, `columnVisibility`, `pagination`, `tableActions`.
|
|
11
|
+
Optional sections (fall back to `defaultGridI18n`): `selectColumnHeader`, `columnResize`, `columnOrder`,
|
|
12
|
+
`loading`, `toolbar`, `table`, `grouping`, `selection`, `status`.
|
|
13
|
+
|
|
14
|
+
`table` keys include `label` («Data grid», the `<table>` name), `skipToRows` / `skipToHeader` (bypass
|
|
15
|
+
links), `expandCell`, `rowMenu`, `errorMessage`, `retry`, `totalsLabel`.
|
|
16
|
+
|
|
17
|
+
Always start from the default bag: `i18n={{ ...defaultGridI18n, ...overrides }}`. There is no bundled
|
|
18
|
+
Italian bag; `GRID_IT_LOCALE` (`"it-IT"`) is the locale constant for number formatting in
|
|
19
|
+
`gridResultScope`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Accessibility
|
|
24
|
+
|
|
25
|
+
- Native table semantics: `aria-sort` on sortable headers; `id`/`headers` chain for multi-level headers;
|
|
26
|
+
`aria-rowcount` / `aria-rowindex` over the traversable body rows (header rows excluded; `-1` while an
|
|
27
|
+
infinite source has more); `aria-colcount`.
|
|
28
|
+
- One row header per row via `rowHeader: true`; `aria-label` on the table from `i18n.table.label`; two
|
|
29
|
+
bypass links («skip to rows», «skip to headers»).
|
|
30
|
+
- **Exactly one live region** (`role="status"`, polite, atomic) announces loading, results, errors, sort,
|
|
31
|
+
filters and selection. Skeleton rows are `aria-hidden`.
|
|
32
|
+
- Truncated text cells get a focusable target with the full value; cells that fit add no tab stop.
|
|
33
|
+
Copying a data row pastes as one spreadsheet row.
|
|
34
|
+
- Full screen: `role="dialog"`, focus contained, Escape exits.
|
|
35
|
+
- Menus and popovers are non-modal and portal inside the grid root when needed (so they stay reachable
|
|
36
|
+
in full screen).
|
|
37
|
+
- Known gaps: column resize has no single-pointer alternative to dragging (WCAG 2.5.7) and the handle's
|
|
38
|
+
pointer target is 8 px wide (2.5.8); infinite loading has no keyboard trigger other than scrolling.
|
|
39
|
+
|
|
40
|
+
---
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# AppDataGrid — Layout
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## Layout: widths, pinning, virtualization, density, full screen
|
|
6
|
+
|
|
7
|
+
- **Widths and resize.** Every leaf header has a resize handle (drag, or arrow keys = 8 px on the
|
|
8
|
+
focused handle; `role="separator"` with `aria-valuenow`). Resizing never emits and commits once per
|
|
9
|
+
gesture (`onColumnWidthsChange`). Columns without a declared `width` fill the container; horizontal
|
|
10
|
+
overflow and scrollbars appear when widths exceed it.
|
|
11
|
+
- **Pinning.** `pinned: "left" | "right"` on columns. Offsets are computed from declared widths, so they
|
|
12
|
+
are correct at first paint. A pinned block is visibly sticky only under horizontal overflow.
|
|
13
|
+
- **Virtualization.** `virtualized` + a bounded viewport renders a window of rows (fewer than 60 `tr`
|
|
14
|
+
for 10,000 rows). Row heights are exact per density; switching density re-measures. Scrolling never
|
|
15
|
+
emits.
|
|
16
|
+
- **Height helpers.** `gridViewportHeight({ rowCount, density, hasFooter?, hasFilterRow?,
|
|
17
|
+
headerLevels? })` returns the scroll-box height to pass as `viewportHeight` (use the live density and
|
|
18
|
+
the filter-row state). `gridFrameHeight({ ...same, hasSelectionBar? })` returns viewport plus in-flow
|
|
19
|
+
bands, for computing a delta — it excludes toolbar, paginator, padding and border.
|
|
20
|
+
`gridFilterRowHeight` is the filter-row addend.
|
|
21
|
+
- **Density.** `density` prop; offer a switch with `GridDensityControl`:
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
const [density, setDensity] = useState<GridDensity>("default");
|
|
25
|
+
<AppDataGrid
|
|
26
|
+
columns={columns}
|
|
27
|
+
data={rows}
|
|
28
|
+
density={density}
|
|
29
|
+
toolbar={{
|
|
30
|
+
actions: {
|
|
31
|
+
render: () => (
|
|
32
|
+
<GridDensityControl density={density} onChange={setDensity} />
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
}}
|
|
36
|
+
/>;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **Full screen.** The toolbar toggle (or `initialFullscreen`) promotes the same DOM node to
|
|
40
|
+
`fixed inset-0` with a z-index above page overlays; nothing remounts or refetches and scroll position
|
|
41
|
+
is kept. The root becomes `role="dialog"` `aria-modal`, focus cycles inside it, Escape exits.
|
|
42
|
+
`requestFullscreen` is not used. If an ancestor creates a stacking context (`transform`, `filter`),
|
|
43
|
+
full screen renders inside that ancestor's context.
|
|
44
|
+
|
|
45
|
+
---
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# AppDataGrid — Migrating from DataTable
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## Migrating from `DataTable`
|
|
6
|
+
|
|
7
|
+
`DataTable` is deprecated in favour of `AppDataGrid`; both keep working side by side, so migration can
|
|
8
|
+
be done call site by call site. Most exports map by name:
|
|
9
|
+
|
|
10
|
+
| `DataTable` export | `AppDataGrid` export |
|
|
11
|
+
| -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
12
|
+
| `DataTable`, `DataTableProps` | `AppDataGrid`, `AppDataGridProps` |
|
|
13
|
+
| `createStringColumn`, `createNumberColumn`, `createBooleanColumn`, `createDateColumn`, `createDateTimeColumn` | `createGridStringColumn`, `createGridNumberColumn`, `createGridBooleanColumn`, `createGridDateColumn`, `createGridDateTimeColumn` |
|
|
14
|
+
| `createSingleSelectColumn`, `createMultiSelectColumn`, `createComputedColumn`, `createActionColumn`, `createDisplayColumn` | `createGridSingleSelectColumn`, `createGridMultiSelectColumn`, `createGridComputedColumn`, `createGridActionColumn`, `createGridDisplayColumn` |
|
|
15
|
+
| `createInitialState`, `createSorting` | `createGridInitialState`, `createGridSorting` |
|
|
16
|
+
| `ServerState`, `DataTableServerConfig`, `DataTableState` / `InitialTableState` | `GridServerState`, `GridServerConfig`, `GridInitialState` |
|
|
17
|
+
| `DataTableI18n`, `defaultDataTableI18n` | `GridI18n`, `defaultGridI18n` |
|
|
18
|
+
| `DataTableBadgeFilter`, `DataTableFilterForSearch`, `DataTableSorting`, `DataTablePagination`, `DataTableActions`, `FilterOperator`, `IColumnType` | `GridBadgeFilter`, `GridFilterForSearch`, `GridSorting`, `GridPagination`, `GridActions`, `GridFilterOperator`, `GridColumnType` |
|
|
19
|
+
|
|
20
|
+
Behaviour differences to check when migrating:
|
|
21
|
+
|
|
22
|
+
- `sortable` and `filterable` default to `true` (were `false`); `searchable` stays opt-in.
|
|
23
|
+
- `hideActionsRow` hides only the `actions` buttons, not the toolbar.
|
|
24
|
+
- `serverDebounceTime` (default 200) coalesces emissions instead of delaying the loading overlay.
|
|
25
|
+
- There is no `mode` prop: the server branch is selected by the server props; grouping is client-only.
|
|
26
|
+
- No emission at mount — perform the initial fetch yourself.
|
|
27
|
+
- Filters are in the header filter row by default; there is no bundled Italian i18n bag.
|
|
28
|
+
- Filter helpers of `DataTable` (`createFilterBadge`, `applyClientSideFilter`, `getPinnedStyle`, …) have
|
|
29
|
+
no public equivalent: filter badges are created by the grid, by `useGridFilters` or by
|
|
30
|
+
`createGridCustomFilterBadge`.
|
|
31
|
+
|
|
32
|
+
---
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# AppDataGrid — Saved views
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## Saved views (presets)
|
|
6
|
+
|
|
7
|
+
A `GridViewPreset` captures one view:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
interface GridViewPreset {
|
|
11
|
+
version: number; // GRID_VIEW_PRESET_VERSION
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
query: {
|
|
15
|
+
search: string;
|
|
16
|
+
sorting: { id: string; desc: boolean }[]; // GridSortDescriptor[]
|
|
17
|
+
filters: GridBadgeFilter[];
|
|
18
|
+
pageSize: number;
|
|
19
|
+
};
|
|
20
|
+
view: {
|
|
21
|
+
density: GridDensity;
|
|
22
|
+
columnWidths: Record<string, number>;
|
|
23
|
+
columnVisibility: Record<string, boolean>;
|
|
24
|
+
columnOrder: string[];
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- **Apply** by passing a new object to `preset` (identity-based; re-passing an equal new object applies
|
|
30
|
+
again). One application = one `onQueryChange` emission.
|
|
31
|
+
- **Capture** the view half through `onDensityChange` (only for presets — own your density control),
|
|
32
|
+
`onColumnVisibilityChange`, `onColumnWidthsChange`, `onColumnOrderChange`. The query half you build from
|
|
33
|
+
your own state (`GridServerState.computedSorting` must be mapped to `GridSortDescriptor[]` by hand).
|
|
34
|
+
- The capture callbacks do not fire on preset application; `onQueryChange` does. Save on an explicit user
|
|
35
|
+
action, not on every change.
|
|
36
|
+
- `readGridViewPreset(raw)` validates a stored payload field by field (version, id, name, every
|
|
37
|
+
sort/filter element, operator vs. its `filterType`, finite numbers, integer `pageSize` ≥ 1, widths,
|
|
38
|
+
visibility, order). It throws `GridViewPresetVersionError` (`found`, `expected`, `reason` = offending
|
|
39
|
+
field path, `undefined` only for a version mismatch). It refuses, it does not sanitise: coerce values
|
|
40
|
+
(e.g. widths stored as strings in JSON columns) before reading.
|
|
41
|
+
- Not validated: the inside of a custom badge's `customFragment` (treat presets from users as privileged
|
|
42
|
+
input), and ids that name no existing column.
|
|
43
|
+
- `createInMemoryGridViewStore(initial?)` returns a `GridViewStore` (`list`, `load`, `save`, `remove`,
|
|
44
|
+
all async) kept in memory; implement the same interface for real persistence.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
const filters = useGridFilters({ columns: toGridFilterColumns(columns) });
|
|
48
|
+
const [preset, setPreset] = useState<GridViewPreset>();
|
|
49
|
+
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
|
50
|
+
const [columnOrder, setColumnOrder] = useState<string[]>([]);
|
|
51
|
+
|
|
52
|
+
<AppDataGrid
|
|
53
|
+
columns={columns}
|
|
54
|
+
data={rows}
|
|
55
|
+
preset={preset}
|
|
56
|
+
filters={filters.badges}
|
|
57
|
+
columnVisibility={filters.columnVisibility}
|
|
58
|
+
onColumnVisibilityChange={(next) => {
|
|
59
|
+
for (const [id, visible] of Object.entries(next))
|
|
60
|
+
if (filters.columnVisibility[id] !== visible)
|
|
61
|
+
filters.setColumnVisible(id, visible);
|
|
62
|
+
}}
|
|
63
|
+
onAddFilter={filters.addBadge}
|
|
64
|
+
onUpdateFilter={filters.updateBadge}
|
|
65
|
+
onColumnWidthsChange={(next) => setColumnWidths({ ...next })}
|
|
66
|
+
onColumnOrderChange={setColumnOrder}
|
|
67
|
+
onPresetApplied={(p) => {
|
|
68
|
+
filters.restore(p.query.filters, p.view.columnVisibility);
|
|
69
|
+
// Re-seed captured state: preset application does not call the capture callbacks.
|
|
70
|
+
setColumnWidths({ ...p.view.columnWidths });
|
|
71
|
+
setColumnOrder([...p.view.columnOrder]);
|
|
72
|
+
}}
|
|
73
|
+
/>;
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# AppDataGrid — Selection
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## Selection
|
|
6
|
+
|
|
7
|
+
With `checkable`, a **selection bar** appears above the table as soon as a row is ticked (it occupies no
|
|
8
|
+
space otherwise). It shows the count («2 rows selected»), the scope («of 8 on this page»), a totals band
|
|
9
|
+
(the columns' `aggregate` over the selected rows, or `selection.summary.render()`), the bulk actions and
|
|
10
|
+
a dismiss / «Deselect all» button.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
type GridSelectionSlots =
|
|
14
|
+
| {
|
|
15
|
+
mode?: "page";
|
|
16
|
+
actions?: readonly GridSelectionAction[];
|
|
17
|
+
summary?: GridToolbarSlot;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
mode: "all";
|
|
21
|
+
actions?: readonly GridPromotableSelectionAction[];
|
|
22
|
+
summary?: GridToolbarSlot;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface GridSelectionAction {
|
|
26
|
+
id?: string; // stable key, recommended when labels can repeat
|
|
27
|
+
label: string; // visible label WITHOUT the number: the grid appends the count
|
|
28
|
+
icon?: IconName;
|
|
29
|
+
destructive?: boolean;
|
|
30
|
+
onSelect: (rowIds: readonly string[]) => void;
|
|
31
|
+
"data-testid"?: string;
|
|
32
|
+
}
|
|
33
|
+
// GridPromotableSelectionAction: same fields, but onSelectScope(scope: GridSelectionScope) instead of onSelect.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- `mode: "page"` (default): the header checkbox selects the rows of the current page.
|
|
37
|
+
- `mode: "all"`: the bar also offers «Select all N rows» (N = `resultTotal` or `serverConfig.totalItems`)
|
|
38
|
+
when the result is larger than the page. Actions then receive `{ scope, rowIds, count }`: at scope
|
|
39
|
+
`"all"`, `rowIds` are only the ids the grid holds — use `count` and your current query to act on the
|
|
40
|
+
whole result. `onSelectScope` is required there (the compiler rejects a plain `onSelect`).
|
|
41
|
+
- In full screen the bar takes its height from the viewport (one data row may be hidden).
|
|
42
|
+
- Selected rows get an accent tint on every cell plus a left bar.
|
|
43
|
+
|
|
44
|
+
---
|