@aglyn/shared-ui-jsx 1.0.0-beta.230 → 1.0.0-beta.232
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/lib/components/copy-field.component.d.ts +25 -0
- package/src/lib/components/copy-field.component.js +77 -0
- package/src/lib/components/copy-field.component.js.map +1 -0
- package/src/lib/components/download-cost-badge.component.d.ts +37 -0
- package/src/lib/components/download-cost-badge.component.js +89 -0
- package/src/lib/components/download-cost-badge.component.js.map +1 -0
- package/src/lib/components/file-drop-zone.component.d.ts +48 -0
- package/src/lib/components/file-drop-zone.component.js +173 -0
- package/src/lib/components/file-drop-zone.component.js.map +1 -0
- package/src/lib/components/list-query-notices.component.d.ts +1 -18
- package/src/lib/components/list-query-notices.component.js +1 -11
- package/src/lib/components/list-query-notices.component.js.map +1 -1
- package/src/lib/components/list-table.component.d.ts +12 -0
- package/src/lib/components/list-table.component.js +30 -4
- package/src/lib/components/list-table.component.js.map +1 -1
- package/src/lib/components/option-card-grid.component.d.ts +46 -0
- package/src/lib/components/option-card-grid.component.js +139 -0
- package/src/lib/components/option-card-grid.component.js.map +1 -0
- package/src/lib/components/searchable-virtual-list.component.d.ts +72 -0
- package/src/lib/components/searchable-virtual-list.component.js +212 -0
- package/src/lib/components/searchable-virtual-list.component.js.map +1 -0
- package/src/lib/components/site-live-notice.component.d.ts +39 -0
- package/src/lib/components/site-live-notice.component.js +95 -0
- package/src/lib/components/site-live-notice.component.js.map +1 -0
- package/src/lib/components/status-chip.component.d.ts +26 -0
- package/src/lib/components/status-chip.component.js +48 -0
- package/src/lib/components/status-chip.component.js.map +1 -0
- package/src/lib/const/list-filter-sentence.d.ts +1 -24
- package/src/lib/const/list-filter-sentence.js +3 -105
- package/src/lib/const/list-filter-sentence.js.map +1 -1
- package/src/lib/const/list-filter.d.ts +1 -219
- package/src/lib/const/list-filter.js +4 -282
- package/src/lib/const/list-filter.js.map +1 -1
- package/src/lib/const/list-grid-filter.d.ts +4 -25
- package/src/lib/const/list-grid-filter.js +20 -44
- package/src/lib/const/list-grid-filter.js.map +1 -1
- package/src/lib/const/list-query-plan.d.ts +1 -154
- package/src/lib/const/list-query-plan.js +4 -610
- package/src/lib/const/list-query-plan.js.map +1 -1
- package/src/lib/hooks/use-client-pagination.d.ts +25 -0
- package/src/lib/hooks/use-client-pagination.js +65 -0
- package/src/lib/hooks/use-client-pagination.js.map +1 -0
- package/src/lib/hooks/use-in-view.d.ts +16 -0
- package/src/lib/hooks/use-in-view.js +66 -0
- package/src/lib/hooks/use-in-view.js.map +1 -0
- package/src/lib/hooks/use-list-column-sort.d.ts +57 -0
- package/src/lib/hooks/use-list-column-sort.js +156 -0
- package/src/lib/hooks/use-list-column-sort.js.map +1 -0
- package/src/lib/hooks/use-viewport-fill.d.ts +39 -0
- package/src/lib/hooks/use-viewport-fill.js +64 -0
- package/src/lib/hooks/use-viewport-fill.js.map +1 -0
|
@@ -21,6 +21,7 @@ import { AppLink } from "./app-link.js";
|
|
|
21
21
|
import { MdiIcon } from "./mdi-icon/mdi-icon.js";
|
|
22
22
|
import { DataTableComponent } from "./data-table.component.js";
|
|
23
23
|
import { IconButton, Stack, Tooltip } from "@mui/material";
|
|
24
|
+
import { LIST_PAGE_SORT_DESCRIPTION } from "@aglyn/shared-util-tools/list-query/list-column-sort";
|
|
24
25
|
import RowActionsMenu from "./row-actions-menu.component.js";
|
|
25
26
|
import { TABLE_PAGE_SIZE_DEFAULT, TABLE_PAGE_SIZE_OPTIONS, TABLE_ROWS_PER_PAGE_LABEL } from "../const/table-pagination.js";
|
|
26
27
|
/**
|
|
@@ -166,6 +167,24 @@ function ListQuickButton(props) {
|
|
|
166
167
|
renderCell: ({ row })=>render(row)
|
|
167
168
|
};
|
|
168
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* The columns as `columnSort` sorts them: the hook's columns sortable in its
|
|
172
|
+
* directions, a page-sorted header described as such, every other column not
|
|
173
|
+
* sortable. The actions column never sorts.
|
|
174
|
+
*/ function columnsSortedBy(columns, columnSort) {
|
|
175
|
+
return columns == null ? void 0 : columns.map((column)=>{
|
|
176
|
+
const sorted = column.field === LIST_ACTIONS_FIELD ? undefined : columnSort.columns[column.field];
|
|
177
|
+
if (!sorted) return _extends({}, column, {
|
|
178
|
+
sortable: false
|
|
179
|
+
});
|
|
180
|
+
return _extends({}, column, {
|
|
181
|
+
sortable: true,
|
|
182
|
+
sortingOrder: sorted.sortingOrder
|
|
183
|
+
}, sorted.mode === 'page' ? {
|
|
184
|
+
description: column.description ? `${column.description} ${LIST_PAGE_SORT_DESCRIPTION}.` : `${LIST_PAGE_SORT_DESCRIPTION}.`
|
|
185
|
+
} : {});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
169
188
|
const NO_ROWS = [];
|
|
170
189
|
/** No column chooser and no Export: the columns and rows are drawn elsewhere. */ const TOOLBAR_ONLY_PROPS = {
|
|
171
190
|
disableColumnSelector: true
|
|
@@ -206,7 +225,7 @@ const NO_ROWS = [];
|
|
|
206
225
|
* reader actually compares.
|
|
207
226
|
*/ export function ListTable(props) {
|
|
208
227
|
var _ref;
|
|
209
|
-
const { onOpen, sx, initialState, hideFooter, rows, selectable, quickFilter, slotProps, toolbarOnly = false, slots } = props, rest = _object_without_properties_loose(props, [
|
|
228
|
+
const { onOpen, sx, initialState, hideFooter, rows, selectable, quickFilter, slotProps, toolbarOnly = false, slots, columnSort } = props, rest = _object_without_properties_loose(props, [
|
|
210
229
|
"onOpen",
|
|
211
230
|
"sx",
|
|
212
231
|
"initialState",
|
|
@@ -216,8 +235,15 @@ const NO_ROWS = [];
|
|
|
216
235
|
"quickFilter",
|
|
217
236
|
"slotProps",
|
|
218
237
|
"toolbarOnly",
|
|
219
|
-
"slots"
|
|
238
|
+
"slots",
|
|
239
|
+
"columnSort"
|
|
220
240
|
]);
|
|
241
|
+
const sortProps = columnSort ? {
|
|
242
|
+
columns: columnsSortedBy(rest.columns, columnSort),
|
|
243
|
+
sortingMode: columnSort.sortingMode,
|
|
244
|
+
sortModel: columnSort.sortModel,
|
|
245
|
+
onSortModelChange: columnSort.onSortModelChange
|
|
246
|
+
} : {};
|
|
221
247
|
const showQuickFilter = quickFilter != null ? quickFilter : rest.filterMode !== 'server';
|
|
222
248
|
/*==========================================
|
|
223
249
|
* SELECTION IS OPT-IN, AND NAVIGATION-ONLY IS THE DEFAULT.
|
|
@@ -420,10 +446,10 @@ const NO_ROWS = [];
|
|
|
420
446
|
// A toolbar-only grid holds no rows: the reader sees them some other
|
|
421
447
|
// way, and rows laid out under a collapsed body would be drawn for no
|
|
422
448
|
// one.
|
|
423
|
-
rows: toolbarOnly ? NO_ROWS : rows,
|
|
449
|
+
rows: toolbarOnly ? NO_ROWS : columnSort ? columnSort.rows : rows,
|
|
424
450
|
hideFooter: footerHidden,
|
|
425
451
|
slots: toolbarOnly ? _extends({}, slots, TOOLBAR_ONLY_SLOTS) : slots
|
|
426
|
-
}, toolbarOnly ? TOOLBAR_ONLY_PROPS : {}, rest, unpaged));
|
|
452
|
+
}, toolbarOnly ? TOOLBAR_ONLY_PROPS : {}, rest, sortProps, unpaged));
|
|
427
453
|
}
|
|
428
454
|
ListTable.displayName = 'ListTable';
|
|
429
455
|
export default ListTable;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/list-table.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { AppLink } from './app-link'\nimport { MdiIcon } from './mdi-icon/mdi-icon'\nimport {\n DataTableComponent,\n type DataTableProps,\n} from './data-table.component'\nimport { IconButton, Stack, Tooltip } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport type { ReactNode } from 'react'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from './row-actions-menu.component'\nimport {\n TABLE_PAGE_SIZE_DEFAULT,\n TABLE_PAGE_SIZE_OPTIONS,\n TABLE_ROWS_PER_PAGE_LABEL,\n} from '../const/table-pagination'\n\n/**\n * ONE row grammar for every artifact list (AGL-2501).\n *\n *\n * They did. Three of the four are `DataTableComponent`, but each declared its\n * own column set, its own action arrangement and its own row-click, and the\n * fourth — screens — is a hand-rolled tree because it has to be: drag to\n * reorder, expand to nest, no grid can do that. So \"the same table\" cannot\n * literally be one component for all four, and pretending otherwise would mean\n * either losing the hierarchy or bolting a tree onto a grid.\n *\n * What IS shared is the row GRAMMAR, and that is what actually differed on\n * screen. Every artifact row now reads the same way:\n *\n * ```\n * [ drag · expand ] name id description updated created [ ⇗ ⋮ ]\n * screens only quick menu\n * ```\n *\n * - **The row opens the detail view.** Clicking anywhere that is not a control\n * navigates. It is the primary action on every one of these lists and it was\n * previously a link buried in one column.\n * - **Rows are NOT selectable.** The grid lists shipped with selection on, so\n * a click both navigated AND tinted the row, and the footer counted \"1 row\n * selected\" for a selection nothing could act on. A selection with no bulk\n * action is a state the reader has to dismiss and cannot use.\n * - **Exactly one quick action, then the menu.** Everything else moves behind\n * the overflow — a delete sitting inline is one mis-click from the row's own\n * open handler, which is the argument AGL-701 already made for components and\n * never carried to the rest.\n *\n * The quick action is the one thing worth a direct click on that kind of\n * artifact: **Open live page** for a screen, **Preview** for anything that has\n * no address of its own. It is the only per-kind variation in the trailing\n * cluster, and it is chosen by the caller rather than inferred here.\n *\n * A screen is the only artifact of the four with a route the public can reach,\n * so it is the only one whose quick action says \"live\". Layouts, components\n * and templates render inside something else and have no address to open, so\n * theirs is a **Preview** into the console's own canvas render.\n *\n * The quick action is ALSO the menu's first entry — see\n * {@link quickActionMenuItem}. The icon alone is a glyph with a tooltip; the\n * menu is where every action on the row is spelled out, and an action missing\n * from it reads as an action the row does not have.\n */\n\n/**\n * The single trailing icon beside the overflow menu.\n *\n * Three shapes, because the destinations genuinely differ: an EXTERNAL live\n * page (new tab), an IN-APP preview route, and a plain handler for the rare\n * action that opens a dialog. `unavailableReason` is the fourth state and the\n * one most easily got wrong — a screen with no single live page (a collection\n * template renders under routes it does not own) must still show the control,\n * disabled, saying why. Hiding it would read as the feature being absent.\n */\nexport interface ListQuickAction {\n /** `mdi` icon path. */\n icon: string\n /** Accessible name; also the tooltip when the action is unavailable. */\n label: string\n /** A live, external URL — opened in a new tab. */\n href?: string\n /** An in-app route — a normal client navigation. */\n to?: string\n onClick?: () => void\n /**\n * Why this row cannot use the action. Renders it DISABLED with the reason as\n * its tooltip, rather than removing it — an absent control and an\n * inapplicable one look identical, and only one of them is honest.\n */\n unavailableReason?: string\n}\n\nexport interface ListRowActionsProps {\n /** The artifact's name — every control is labelled with it, because these\n * Repeat once per row and \"More actions\" alone says nothing about which. */\n label: string\n quick?: ListQuickAction | null\n items: RowActionsMenuItem[]\n}\n\n/**\n * The quick action, restated as the menu's first entry.\n *\n * The icon and the menu are two ways into ONE action, not two features: the\n * icon is a bare glyph, so a reader who does not recognize it has the overflow\n * as the place every action is named. Deriving the item from the same\n * {@link ListQuickAction} is what keeps the two in step — availability,\n * destination and the reason it is refused are read once and cannot drift.\n */\nfunction quickActionMenuItem(\n quick: ListQuickAction,\n): RowActionsMenuItem {\n return {\n key: 'quick',\n label: quick.label,\n icon: <MdiIcon path={quick.icon} size={0.8} />,\n // `href` is off-site and opens a new tab; `to` is an in-app route.\n href: quick.href ?? quick.to,\n external: Boolean(quick.href),\n onClick: quick.href || quick.to ? undefined : quick.onClick,\n disabled: Boolean(quick.unavailableReason),\n disabledReason: quick.unavailableReason,\n }\n}\n\n/** The trailing cluster: one quick action, then the overflow menu. */\nexport function ListRowActions(props: ListRowActionsProps) {\n const { label, quick, items } = props\n const menuItems = quick ? [quickActionMenuItem(quick), ...items] : items\n return (\n <Stack\n direction=\"row\"\n spacing={0.5}\n /*\n `height: '100%'` is what actually centres these (AGL-2501). A DataGrid\n cell is a flex box, but `renderCell` content is auto-height inside it —\n so at the taller `TABLE_ROW_HEIGHT` these icons ride at the top of the\n row while the text beside them is centred. `alignItems` alone cannot\n fix it: there is no spare height to distribute until the child claims\n the row.\n */\n sx={{\n height: '100%',\n alignItems: 'center',\n justifyContent: 'flex-end',\n }}\n // The row's own click opens the detail view; without this every control\n // in here would navigate out from under the thing it just opened.\n onClick={(event) => event.stopPropagation()}\n >\n {quick ? <ListQuickButton label={label} action={quick} /> : null}\n <RowActionsMenu label={label} items={menuItems} />\n </Stack>\n )\n}\nListRowActions.displayName = 'ListRowActions'\n\nfunction ListQuickButton(props: {\n label: string\n action: ListQuickAction\n}) {\n const { label, action } = props\n const icon = <MdiIcon path={action.icon} size={0.8} />\n const name = `${action.label} — ${label}`\n\n if (action.unavailableReason) {\n return (\n <Tooltip title={action.unavailableReason}>\n {/* span: a disabled button emits no events, so the tooltip needs a\n wrapper that does. */}\n <span>\n <IconButton size=\"small\" aria-label={name} disabled>\n {icon}\n </IconButton>\n </span>\n </Tooltip>\n )\n }\n if (action.href) {\n return (\n <Tooltip title={action.label}>\n <AppLink\n componentVariant=\"icon-button\"\n size=\"small\"\n aria-label={name}\n href={action.href}\n target=\"_blank\"\n rel=\"noreferrer\"\n >\n {icon}\n </AppLink>\n </Tooltip>\n )\n }\n if (action.to) {\n return (\n <Tooltip title={action.label}>\n <AppLink\n componentVariant=\"icon-button\"\n size=\"small\"\n aria-label={name}\n href={action.to}\n >\n {icon}\n </AppLink>\n </Tooltip>\n )\n }\n return (\n <Tooltip title={action.label}>\n <IconButton size=\"small\" aria-label={name} onClick={action.onClick}>\n {icon}\n </IconButton>\n </Tooltip>\n )\n}\n\n/** The field `listActionsColumn` draws, which a row click never opens through. */\nexport const LIST_ACTIONS_FIELD = 'actions'\n\n/**\n * The trailing ACTIONS column, identical on every grid list.\n *\n * Deliberately NOT MUI's `type: 'actions'`. That type renders each\n * `GridActionsCellItem` as its own icon and moves the ones marked `showInMenu`\n * into a menu it owns — which is how the three lists ended up with two, three\n * and five inline icons respectively, each a different width, and how the\n * screens table (which cannot use it at all) grew a second implementation.\n * One `renderCell` gives every list the same cluster and the same width.\n */\nexport function listActionsColumn(\n render: (row: any) => ReactNode,\n options: { width?: number } = {},\n): GridColDef {\n return {\n field: LIST_ACTIONS_FIELD,\n headerName: 'Actions',\n width: options.width ?? 110,\n align: 'right',\n headerAlign: 'right',\n sortable: false,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n renderCell: ({ row }: any) => render(row),\n }\n}\n\n/**\n * Row selection, opted into by a list that has something to DO with a\n * selection (AGL-2595).\n *\n * Controlled, and in the caller's vocabulary — an array of `$id`s in, an\n * array out — rather than MUI's own model. The grid's model is a\n * `{ type, ids: Set }` whose `exclude` form appears the moment the header\n * checkbox is ticked, and a caller that had to fold \"everything except\n * these\" back into \"these\" would do it wrong on the page boundary; the\n * table holds the rows and does the fold once, here.\n */\nexport interface ListTableSelection {\n /** The `$id`s currently selected; ids not in `rows` are ignored. */\n selected: readonly string[]\n /** Every change, as the full new selection. */\n onChange: (ids: string[]) => void\n}\n\nexport interface ListTableProps extends DataTableProps {\n /** Row click → the artifact's detail view. */\n onOpen?: (id: string, row: any) => void\n /**\n * The toolbar's free-text search box.\n *\n * Defaults to shown where the GRID answers it, which is client filtering.\n * Under `filterMode=\"server\"` the grid applies no filter at all and the\n * model goes to the caller, so the box defaults to hidden: a list whose\n * handler reads only the column filter would otherwise offer a search that\n * narrows nothing, which reads as \"no match\" rather than \"not supported\".\n * Pass it on a server-filtered list whose handler reads `quickFilterValues`.\n */\n quickFilter?: boolean\n /**\n * Present → a checkbox column and a controlled selection. Absent → the\n * navigation-only grid every list had before, checkbox column and all.\n * The row click keeps opening the record either way: a checkbox is the\n * only thing that selects, so the two gestures cannot collide.\n */\n selectable?: ListTableSelection\n /**\n * The toolbar, its Filters panel and its search, and none of the rows\n * (AGL-3327).\n *\n * For a list the reader can also see some other way — the media library's\n * thumbnail grid — that must filter exactly as its table view does. The\n * panel, the quick search and Export all live inside the grid, bound to\n * its API, so a second, hand-built toolbar for the other view would be a\n * second filter UI to keep in step with this one. Instead the grid is\n * drawn with its body collapsed and no rows: the toolbar stays, and the\n * panel still opens under it (its anchor is kept, at the top of the\n * collapsed body). The columns chooser and Export go — there are no\n * columns on screen to choose, and the rows are the other view's to draw\n * and to export.\n */\n toolbarOnly?: boolean\n}\n\nconst NO_ROWS = [] as const\n\n/** No column chooser and no Export: the columns and rows are drawn elsewhere. */\nconst TOOLBAR_ONLY_PROPS = { disableColumnSelector: true }\n\n/** Nothing, for an overlay a toolbar-only grid has no body to draw in. */\nconst NoOverlay = () => null\n\n/** A toolbar-only grid's overlays: its body is collapsed, so they are too. */\nconst TOOLBAR_ONLY_SLOTS = {\n noRowsOverlay: NoOverlay,\n noResultsOverlay: NoOverlay,\n loadingOverlay: NoOverlay,\n}\n\n/**\n * The body of a toolbar-only grid, collapsed to nothing.\n *\n * Everything in the main container goes except the panel anchor, which the\n * Filters panel positions itself against: the anchor sits at the top of the\n * collapsed body, which is directly under the toolbar, where the panel opens\n * on every other list. The body keeps no height, so the virtualized rows it\n * would draw into have none to fill.\n */\nconst TOOLBAR_ONLY_SX = {\n '& .MuiDataGrid-main': {\n flex: '0 0 0px',\n height: 0,\n minHeight: 0,\n overflow: 'visible',\n },\n '& .MuiDataGrid-main > :not([data-id=\"gridPanelAnchor\"])': {\n display: 'none',\n },\n '& [data-id=\"gridPanelAnchor\"]': { top: 0 },\n} as const\n\n/**\n * The grid every FLAT artifact list uses — layouts, components, templates.\n *\n * Screens is the deliberate exception and keeps its own tree; it adopts\n * {@link ListRowActions} so the two still read alike, which is the part a\n * reader actually compares.\n */\nexport function ListTable(props: ListTableProps) {\n const {\n onOpen,\n sx,\n initialState,\n hideFooter,\n rows,\n selectable,\n quickFilter,\n slotProps,\n toolbarOnly = false,\n slots,\n ...rest\n } = props\n const showQuickFilter = quickFilter ?? rest.filterMode !== 'server'\n /*==========================================\n * SELECTION IS OPT-IN, AND NAVIGATION-ONLY IS THE DEFAULT.\n *\n * Without `selectable`, the three properties that make a row a NAVIGATION\n * target rather than a selectable record: `disableRowSelectionOnClick`\n * alone is not enough — it stops the click selecting, and leaves the\n * checkbox column and the \"n rows selected\" footer behind.\n *\n * With it, the grid's model is built from the caller's id list on every\n * render and folded back into one on every change. MUI's model since v8 is\n * `{ type: 'include' | 'exclude', ids }`, and `exclude` is what the header\n * checkbox produces — \"everything but these\" — so the fold has to consult\n * the rows the grid was given, which is why it lives here and not in the\n * caller. `disableRowSelectionOnClick` stays on in this branch too: a row\n * click still opens the record, and only the checkbox selects.\n *=========================================*/\n const selection = selectable\n ? {\n rowSelection: true,\n checkboxSelection: true,\n rowSelectionModel: {\n type: 'include' as const,\n ids: new Set<string>(selectable.selected),\n },\n onRowSelectionModelChange: (model: {\n type: 'include' | 'exclude'\n ids: Set<unknown>\n }) => {\n const all = (rows ?? []).map((row: any) => String(row.$id))\n selectable.onChange(\n model.type === 'include'\n ? all.filter((id) => model.ids.has(id))\n : all.filter((id) => !model.ids.has(id)),\n )\n },\n }\n : { rowSelection: false, checkboxSelection: false }\n /*==========================================\n * A HIDDEN FOOTER STILL PAGED THE ROWS.\n *\n * `hideFooter` is how a caller says \"something else owns the page\" — a\n * server-paged staff list, a cursor feed, a card whose `ListPagination`\n * sits under the grid. It hides the CONTROL and nothing else: the free\n * DataGrid always paginates, so the grid went on slicing at the shared\n * default and rows eleven onward were drawn by nothing and reachable by\n * nothing. The outer footer then described the rows it was HANDED, so it\n * reported a page two rows longer than the one on screen and offered no\n * way to see the difference.\n *\n * Eight lists pass `hideFooter`, and every one of them was losing rows past\n * the tenth. It went unnoticed because a list has to hold more than ten\n * rows to show a symptom, and most of these do not yet.\n *\n * So when the footer is hidden the grid takes ONE page as tall as the rows\n * it was given. CONTROLLED rather than `initialState`, because a\n * server-paged list hands over a different number of rows on every page and\n * initial state is read once. A floor of one because MUI rejects a page\n * size of zero, and an empty grid has nothing to slice anyway.\n *=========================================*/\n const rowCount = toolbarOnly ? 0 : (rows?.length ?? 0)\n const footerHidden = hideFooter || toolbarOnly\n const unpaged = footerHidden\n ? {\n paginationModel: { page: 0, pageSize: Math.max(rowCount, 1) },\n // A controlled model with no handler logs a MUI warning, and the\n // grid must not move off page zero in any case: the page is not\n // this component's to change while somebody else owns the footer.\n onPaginationModelChange: () => undefined,\n // Keeps the size out of the \"not preset in pageSizeOptions\" warning.\n // Nothing renders it — the footer carrying the menu is hidden.\n pageSizeOptions: [Math.max(rowCount, 1)],\n }\n : {}\n return (\n <DataTableComponent\n getRowId={(row: any) => row.$id}\n {...selection}\n disableRowSelectionOnClick\n onRowClick={\n onOpen\n ? ({ id, row }, event) => {\n /*\n * A press anywhere in the actions cell is aimed at the actions.\n * `ListRowActions` stops its own clicks, but the cell is wider\n * and taller than the cluster, and a press on the padding\n * beside the menu would otherwise open the record from under\n * the reader.\n */\n const target = event.target as Element | null\n if (target?.closest?.(`[data-field=\"${LIST_ACTIONS_FIELD}\"]`)) return\n onOpen(String(id), row)\n }\n : undefined\n }\n /*\n * ENTER OPENS IT TOO. The grid moves focus cell by cell with the arrow\n * keys, and a row a mouse can open must open from the keyboard as\n * well. Only when the CELL itself holds focus: Enter on a link or a\n * button inside one is that control's, and neither the actions cell\n * nor the grid's own columns (the selection checkbox) open the record.\n */\n onCellKeyDown={\n onOpen\n ? (params, event) => {\n if (event.key !== 'Enter') return\n if (params.field === LIST_ACTIONS_FIELD || params.field.startsWith('__')) return\n if (event.target !== event.currentTarget) return\n event.defaultMuiPrevented = true\n onOpen(String(params.id), params.row)\n }\n : undefined\n }\n /*\n * The list GROWS; the PAGE scrolls.\n *\n * `DataTableComponent` gives its wrapper a fixed `height: 400`, which\n * is right for a grid sitting in a pane of its own and wrong for a\n * card in a column of cards: the table got its own scrollbar, the card\n * stopped at 400px whatever it held, and the last row was cut through\n * the middle. Two scroll regions on one page is the thing to avoid —\n * a reader scrolling the page expects the page to move.\n *\n * `autoHeight` sizes the grid to its rows; clearing the wrapper's\n * height is what lets the card follow it. Caller `sx` is spread after,\n * so a list that genuinely wants a viewport can still take one.\n */\n autoHeight\n sx={[\n { height: 'auto' },\n { '& .MuiDataGrid-row': { cursor: onOpen ? 'pointer' : 'default' } },\n /*\n A cell whose content is taller than one line sets the row's height,\n and every OTHER cell then draws its content at the top of it — so a\n two-line date column leaves the counts beside it riding high while\n `ListRowActions`, which claims `height: '100%'` for itself, sits\n centred. One row, three vertical positions.\n\n Centring the cell itself fixes every column at once, including the\n ones a caller renders with a bare `<Typography>`; `alignItems`\n alone cannot, because an auto-height child has no spare height to\n distribute until something claims the row. `justifyContent` is\n deliberately not set here — a column's own `align` is what decides\n its horizontal placement, and overriding it would silently undo\n every right-aligned figure.\n */\n {\n '& .MuiDataGrid-cell': {\n display: 'flex',\n alignItems: 'center',\n },\n },\n ...(toolbarOnly ? [TOOLBAR_ONLY_SX] : []),\n ...(Array.isArray(sx) ? sx : sx ? [sx] : []),\n ]}\n /*\n * The column menu can hide a column; only the toolbar can bring it\n * back.\n *\n * Every column here is hideable, `Manage columns` lives in the toolbar,\n * and the toolbar was off — so hiding the last column left a grid with\n * no columns and no control that could restore one. Not a rare misuse\n * either: the per-column menu offers `Hide column` on every column, and\n * the way out was never on screen.\n *\n * The Actions column is the reason the grid was not left completely\n * blank — `listActionsColumn` marks it `hideable: false` — and \"one\n * unhideable column\" is not a recovery path.\n */\n showToolbar\n pagination\n // The console's ONE footer (AGL-2501): same options, same default, same\n // label.\n pageSizeOptions={TABLE_PAGE_SIZE_OPTIONS}\n /*\n * MERGED, not replaced. `initialState` used to ride in through `rest`,\n * so a caller that set anything in it — a column visibility model, a\n * sort — silently dropped the shared page size along with it. The two\n * keys a caller is likely to set are spread individually so that a page\n * size and a hidden column can coexist.\n */\n initialState={{\n ...initialState,\n pagination: {\n paginationModel: { pageSize: TABLE_PAGE_SIZE_DEFAULT },\n ...initialState?.pagination,\n },\n columns: { ...initialState?.columns },\n }}\n /*\n * MERGED with the caller's, for the reason `initialState` is: a caller\n * setting one slot's props must not drop the shared footer label or the\n * search box's filter-mode default along with the object they live in.\n */\n slotProps={{\n ...slotProps,\n toolbar: {\n showQuickFilter,\n ...slotProps?.toolbar,\n ...(toolbarOnly\n ? {\n csvOptions: { disableToolbarButton: true },\n printOptions: { disableToolbarButton: true },\n }\n : {}),\n },\n pagination: {\n labelRowsPerPage: TABLE_ROWS_PER_PAGE_LABEL,\n ...slotProps?.pagination,\n },\n }}\n // A toolbar-only grid holds no rows: the reader sees them some other\n // way, and rows laid out under a collapsed body would be drawn for no\n // one.\n rows={toolbarOnly ? NO_ROWS : rows}\n hideFooter={footerHidden}\n slots={toolbarOnly ? { ...slots, ...TOOLBAR_ONLY_SLOTS } : slots}\n {...(toolbarOnly ? TOOLBAR_ONLY_PROPS : {})}\n {...rest}\n /*\n * AFTER `rest`, deliberately. \"The footer is hidden, so every row I was\n * handed is on screen\" is an invariant of this component, not a default\n * a call site may talk it out of — a caller that could re-slice here\n * would reintroduce exactly the rows nobody can reach.\n */\n {...unpaged}\n />\n )\n}\nListTable.displayName = 'ListTable'\n\nexport default ListTable\n"],"names":["AppLink","MdiIcon","DataTableComponent","IconButton","Stack","Tooltip","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","TABLE_PAGE_SIZE_OPTIONS","TABLE_ROWS_PER_PAGE_LABEL","quickActionMenuItem","quick","key","label","icon","path","size","href","to","external","Boolean","onClick","undefined","disabled","unavailableReason","disabledReason","ListRowActions","props","items","menuItems","direction","spacing","sx","height","alignItems","justifyContent","event","stopPropagation","ListQuickButton","action","displayName","name","title","span","aria-label","componentVariant","target","rel","LIST_ACTIONS_FIELD","listActionsColumn","render","options","field","headerName","width","align","headerAlign","sortable","filterable","hideable","disableColumnMenu","renderCell","row","NO_ROWS","TOOLBAR_ONLY_PROPS","disableColumnSelector","NoOverlay","TOOLBAR_ONLY_SLOTS","noRowsOverlay","noResultsOverlay","loadingOverlay","TOOLBAR_ONLY_SX","flex","minHeight","overflow","display","top","ListTable","onOpen","initialState","hideFooter","rows","selectable","quickFilter","slotProps","toolbarOnly","slots","rest","showQuickFilter","filterMode","selection","rowSelection","checkboxSelection","rowSelectionModel","type","ids","Set","selected","onRowSelectionModelChange","model","all","map","String","$id","onChange","filter","id","has","rowCount","length","footerHidden","unpaged","paginationModel","page","pageSize","Math","max","onPaginationModelChange","pageSizeOptions","getRowId","disableRowSelectionOnClick","onRowClick","closest","onCellKeyDown","params","startsWith","currentTarget","defaultMuiPrevented","autoHeight","cursor","Array","isArray","showToolbar","pagination","columns","toolbar","csvOptions","disableToolbarButton","printOptions","labelRowsPerPage"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/list-table.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { AppLink } from './app-link'\nimport { MdiIcon } from './mdi-icon/mdi-icon'\nimport {\n DataTableComponent,\n type DataTableProps,\n} from './data-table.component'\nimport { IconButton, Stack, Tooltip } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport type { ReactNode } from 'react'\nimport { LIST_PAGE_SORT_DESCRIPTION } from '@aglyn/shared-util-tools/list-query/list-column-sort'\nimport type { ListColumnSort } from '../hooks/use-list-column-sort'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from './row-actions-menu.component'\nimport {\n TABLE_PAGE_SIZE_DEFAULT,\n TABLE_PAGE_SIZE_OPTIONS,\n TABLE_ROWS_PER_PAGE_LABEL,\n} from '../const/table-pagination'\n\n/**\n * ONE row grammar for every artifact list (AGL-2501).\n *\n *\n * They did. Three of the four are `DataTableComponent`, but each declared its\n * own column set, its own action arrangement and its own row-click, and the\n * fourth — screens — is a hand-rolled tree because it has to be: drag to\n * reorder, expand to nest, no grid can do that. So \"the same table\" cannot\n * literally be one component for all four, and pretending otherwise would mean\n * either losing the hierarchy or bolting a tree onto a grid.\n *\n * What IS shared is the row GRAMMAR, and that is what actually differed on\n * screen. Every artifact row now reads the same way:\n *\n * ```\n * [ drag · expand ] name id description updated created [ ⇗ ⋮ ]\n * screens only quick menu\n * ```\n *\n * - **The row opens the detail view.** Clicking anywhere that is not a control\n * navigates. It is the primary action on every one of these lists and it was\n * previously a link buried in one column.\n * - **Rows are NOT selectable.** The grid lists shipped with selection on, so\n * a click both navigated AND tinted the row, and the footer counted \"1 row\n * selected\" for a selection nothing could act on. A selection with no bulk\n * action is a state the reader has to dismiss and cannot use.\n * - **Exactly one quick action, then the menu.** Everything else moves behind\n * the overflow — a delete sitting inline is one mis-click from the row's own\n * open handler, which is the argument AGL-701 already made for components and\n * never carried to the rest.\n *\n * The quick action is the one thing worth a direct click on that kind of\n * artifact: **Open live page** for a screen, **Preview** for anything that has\n * no address of its own. It is the only per-kind variation in the trailing\n * cluster, and it is chosen by the caller rather than inferred here.\n *\n * A screen is the only artifact of the four with a route the public can reach,\n * so it is the only one whose quick action says \"live\". Layouts, components\n * and templates render inside something else and have no address to open, so\n * theirs is a **Preview** into the console's own canvas render.\n *\n * The quick action is ALSO the menu's first entry — see\n * {@link quickActionMenuItem}. The icon alone is a glyph with a tooltip; the\n * menu is where every action on the row is spelled out, and an action missing\n * from it reads as an action the row does not have.\n */\n\n/**\n * The single trailing icon beside the overflow menu.\n *\n * Three shapes, because the destinations genuinely differ: an EXTERNAL live\n * page (new tab), an IN-APP preview route, and a plain handler for the rare\n * action that opens a dialog. `unavailableReason` is the fourth state and the\n * one most easily got wrong — a screen with no single live page (a collection\n * template renders under routes it does not own) must still show the control,\n * disabled, saying why. Hiding it would read as the feature being absent.\n */\nexport interface ListQuickAction {\n /** `mdi` icon path. */\n icon: string\n /** Accessible name; also the tooltip when the action is unavailable. */\n label: string\n /** A live, external URL — opened in a new tab. */\n href?: string\n /** An in-app route — a normal client navigation. */\n to?: string\n onClick?: () => void\n /**\n * Why this row cannot use the action. Renders it DISABLED with the reason as\n * its tooltip, rather than removing it — an absent control and an\n * inapplicable one look identical, and only one of them is honest.\n */\n unavailableReason?: string\n}\n\nexport interface ListRowActionsProps {\n /** The artifact's name — every control is labelled with it, because these\n * Repeat once per row and \"More actions\" alone says nothing about which. */\n label: string\n quick?: ListQuickAction | null\n items: RowActionsMenuItem[]\n}\n\n/**\n * The quick action, restated as the menu's first entry.\n *\n * The icon and the menu are two ways into ONE action, not two features: the\n * icon is a bare glyph, so a reader who does not recognize it has the overflow\n * as the place every action is named. Deriving the item from the same\n * {@link ListQuickAction} is what keeps the two in step — availability,\n * destination and the reason it is refused are read once and cannot drift.\n */\nfunction quickActionMenuItem(\n quick: ListQuickAction,\n): RowActionsMenuItem {\n return {\n key: 'quick',\n label: quick.label,\n icon: <MdiIcon path={quick.icon} size={0.8} />,\n // `href` is off-site and opens a new tab; `to` is an in-app route.\n href: quick.href ?? quick.to,\n external: Boolean(quick.href),\n onClick: quick.href || quick.to ? undefined : quick.onClick,\n disabled: Boolean(quick.unavailableReason),\n disabledReason: quick.unavailableReason,\n }\n}\n\n/** The trailing cluster: one quick action, then the overflow menu. */\nexport function ListRowActions(props: ListRowActionsProps) {\n const { label, quick, items } = props\n const menuItems = quick ? [quickActionMenuItem(quick), ...items] : items\n return (\n <Stack\n direction=\"row\"\n spacing={0.5}\n /*\n `height: '100%'` is what actually centres these (AGL-2501). A DataGrid\n cell is a flex box, but `renderCell` content is auto-height inside it —\n so at the taller `TABLE_ROW_HEIGHT` these icons ride at the top of the\n row while the text beside them is centred. `alignItems` alone cannot\n fix it: there is no spare height to distribute until the child claims\n the row.\n */\n sx={{\n height: '100%',\n alignItems: 'center',\n justifyContent: 'flex-end',\n }}\n // The row's own click opens the detail view; without this every control\n // in here would navigate out from under the thing it just opened.\n onClick={(event) => event.stopPropagation()}\n >\n {quick ? <ListQuickButton label={label} action={quick} /> : null}\n <RowActionsMenu label={label} items={menuItems} />\n </Stack>\n )\n}\nListRowActions.displayName = 'ListRowActions'\n\nfunction ListQuickButton(props: {\n label: string\n action: ListQuickAction\n}) {\n const { label, action } = props\n const icon = <MdiIcon path={action.icon} size={0.8} />\n const name = `${action.label} — ${label}`\n\n if (action.unavailableReason) {\n return (\n <Tooltip title={action.unavailableReason}>\n {/* span: a disabled button emits no events, so the tooltip needs a\n wrapper that does. */}\n <span>\n <IconButton size=\"small\" aria-label={name} disabled>\n {icon}\n </IconButton>\n </span>\n </Tooltip>\n )\n }\n if (action.href) {\n return (\n <Tooltip title={action.label}>\n <AppLink\n componentVariant=\"icon-button\"\n size=\"small\"\n aria-label={name}\n href={action.href}\n target=\"_blank\"\n rel=\"noreferrer\"\n >\n {icon}\n </AppLink>\n </Tooltip>\n )\n }\n if (action.to) {\n return (\n <Tooltip title={action.label}>\n <AppLink\n componentVariant=\"icon-button\"\n size=\"small\"\n aria-label={name}\n href={action.to}\n >\n {icon}\n </AppLink>\n </Tooltip>\n )\n }\n return (\n <Tooltip title={action.label}>\n <IconButton size=\"small\" aria-label={name} onClick={action.onClick}>\n {icon}\n </IconButton>\n </Tooltip>\n )\n}\n\n/** The field `listActionsColumn` draws, which a row click never opens through. */\nexport const LIST_ACTIONS_FIELD = 'actions'\n\n/**\n * The trailing ACTIONS column, identical on every grid list.\n *\n * Deliberately NOT MUI's `type: 'actions'`. That type renders each\n * `GridActionsCellItem` as its own icon and moves the ones marked `showInMenu`\n * into a menu it owns — which is how the three lists ended up with two, three\n * and five inline icons respectively, each a different width, and how the\n * screens table (which cannot use it at all) grew a second implementation.\n * One `renderCell` gives every list the same cluster and the same width.\n */\nexport function listActionsColumn(\n render: (row: any) => ReactNode,\n options: { width?: number } = {},\n): GridColDef {\n return {\n field: LIST_ACTIONS_FIELD,\n headerName: 'Actions',\n width: options.width ?? 110,\n align: 'right',\n headerAlign: 'right',\n sortable: false,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n renderCell: ({ row }: any) => render(row),\n }\n}\n\n/**\n * Row selection, opted into by a list that has something to DO with a\n * selection (AGL-2595).\n *\n * Controlled, and in the caller's vocabulary — an array of `$id`s in, an\n * array out — rather than MUI's own model. The grid's model is a\n * `{ type, ids: Set }` whose `exclude` form appears the moment the header\n * checkbox is ticked, and a caller that had to fold \"everything except\n * these\" back into \"these\" would do it wrong on the page boundary; the\n * table holds the rows and does the fold once, here.\n */\nexport interface ListTableSelection {\n /** The `$id`s currently selected; ids not in `rows` are ignored. */\n selected: readonly string[]\n /** Every change, as the full new selection. */\n onChange: (ids: string[]) => void\n}\n\nexport interface ListTableProps extends DataTableProps {\n /** Row click → the artifact's detail view. */\n onOpen?: (id: string, row: any) => void\n /**\n * The toolbar's free-text search box.\n *\n * Defaults to shown where the GRID answers it, which is client filtering.\n * Under `filterMode=\"server\"` the grid applies no filter at all and the\n * model goes to the caller, so the box defaults to hidden: a list whose\n * handler reads only the column filter would otherwise offer a search that\n * narrows nothing, which reads as \"no match\" rather than \"not supported\".\n * Pass it on a server-filtered list whose handler reads `quickFilterValues`.\n */\n quickFilter?: boolean\n /**\n * Present → a checkbox column and a controlled selection. Absent → the\n * navigation-only grid every list had before, checkbox column and all.\n * The row click keeps opening the record either way: a checkbox is the\n * only thing that selects, so the two gestures cannot collide.\n */\n selectable?: ListTableSelection\n /**\n * The toolbar, its Filters panel and its search, and none of the rows\n * (AGL-3327).\n *\n * For a list the reader can also see some other way — the media library's\n * thumbnail grid — that must filter exactly as its table view does. The\n * panel, the quick search and Export all live inside the grid, bound to\n * its API, so a second, hand-built toolbar for the other view would be a\n * second filter UI to keep in step with this one. Instead the grid is\n * drawn with its body collapsed and no rows: the toolbar stays, and the\n * panel still opens under it (its anchor is kept, at the top of the\n * collapsed body). The columns chooser and Export go — there are no\n * columns on screen to choose, and the rows are the other view's to draw\n * and to export.\n */\n toolbarOnly?: boolean\n /**\n * The list's header sorts (AGL-3680), from `useListColumnSort`.\n *\n * Present, the table draws `columnSort.rows`, sorts nothing itself\n * (`sortingMode=\"server\"`), and decides every column's `sortable` from it:\n * a column the hook names sorts — a page-sorted one says so on its header\n * — and every other column does not. Pass this rather than\n * `disableColumnSorting`, which `specs/list-tables-sort-by-their-headers`\n * refuses on any new caller.\n */\n columnSort?: ListColumnSort\n}\n\n/**\n * The columns as `columnSort` sorts them: the hook's columns sortable in its\n * directions, a page-sorted header described as such, every other column not\n * sortable. The actions column never sorts.\n */\nfunction columnsSortedBy(\n columns: readonly GridColDef[] | undefined,\n columnSort: ListColumnSort,\n): GridColDef[] | undefined {\n return columns?.map((column) => {\n const sorted = column.field === LIST_ACTIONS_FIELD ? undefined : columnSort.columns[column.field]\n if (!sorted) return { ...column, sortable: false }\n return {\n ...column,\n sortable: true,\n sortingOrder: sorted.sortingOrder,\n ...(sorted.mode === 'page'\n ? {\n description: column.description\n ? `${column.description} ${LIST_PAGE_SORT_DESCRIPTION}.`\n : `${LIST_PAGE_SORT_DESCRIPTION}.`,\n }\n : {}),\n }\n })\n}\n\nconst NO_ROWS = [] as const\n\n/** No column chooser and no Export: the columns and rows are drawn elsewhere. */\nconst TOOLBAR_ONLY_PROPS = { disableColumnSelector: true }\n\n/** Nothing, for an overlay a toolbar-only grid has no body to draw in. */\nconst NoOverlay = () => null\n\n/** A toolbar-only grid's overlays: its body is collapsed, so they are too. */\nconst TOOLBAR_ONLY_SLOTS = {\n noRowsOverlay: NoOverlay,\n noResultsOverlay: NoOverlay,\n loadingOverlay: NoOverlay,\n}\n\n/**\n * The body of a toolbar-only grid, collapsed to nothing.\n *\n * Everything in the main container goes except the panel anchor, which the\n * Filters panel positions itself against: the anchor sits at the top of the\n * collapsed body, which is directly under the toolbar, where the panel opens\n * on every other list. The body keeps no height, so the virtualized rows it\n * would draw into have none to fill.\n */\nconst TOOLBAR_ONLY_SX = {\n '& .MuiDataGrid-main': {\n flex: '0 0 0px',\n height: 0,\n minHeight: 0,\n overflow: 'visible',\n },\n '& .MuiDataGrid-main > :not([data-id=\"gridPanelAnchor\"])': {\n display: 'none',\n },\n '& [data-id=\"gridPanelAnchor\"]': { top: 0 },\n} as const\n\n/**\n * The grid every FLAT artifact list uses — layouts, components, templates.\n *\n * Screens is the deliberate exception and keeps its own tree; it adopts\n * {@link ListRowActions} so the two still read alike, which is the part a\n * reader actually compares.\n */\nexport function ListTable(props: ListTableProps) {\n const {\n onOpen,\n sx,\n initialState,\n hideFooter,\n rows,\n selectable,\n quickFilter,\n slotProps,\n toolbarOnly = false,\n slots,\n columnSort,\n ...rest\n } = props\n const sortProps = columnSort\n ? {\n columns: columnsSortedBy(rest.columns, columnSort),\n sortingMode: columnSort.sortingMode,\n sortModel: columnSort.sortModel,\n onSortModelChange: columnSort.onSortModelChange,\n }\n : {}\n const showQuickFilter = quickFilter ?? rest.filterMode !== 'server'\n /*==========================================\n * SELECTION IS OPT-IN, AND NAVIGATION-ONLY IS THE DEFAULT.\n *\n * Without `selectable`, the three properties that make a row a NAVIGATION\n * target rather than a selectable record: `disableRowSelectionOnClick`\n * alone is not enough — it stops the click selecting, and leaves the\n * checkbox column and the \"n rows selected\" footer behind.\n *\n * With it, the grid's model is built from the caller's id list on every\n * render and folded back into one on every change. MUI's model since v8 is\n * `{ type: 'include' | 'exclude', ids }`, and `exclude` is what the header\n * checkbox produces — \"everything but these\" — so the fold has to consult\n * the rows the grid was given, which is why it lives here and not in the\n * caller. `disableRowSelectionOnClick` stays on in this branch too: a row\n * click still opens the record, and only the checkbox selects.\n *=========================================*/\n const selection = selectable\n ? {\n rowSelection: true,\n checkboxSelection: true,\n rowSelectionModel: {\n type: 'include' as const,\n ids: new Set<string>(selectable.selected),\n },\n onRowSelectionModelChange: (model: {\n type: 'include' | 'exclude'\n ids: Set<unknown>\n }) => {\n const all = (rows ?? []).map((row: any) => String(row.$id))\n selectable.onChange(\n model.type === 'include'\n ? all.filter((id) => model.ids.has(id))\n : all.filter((id) => !model.ids.has(id)),\n )\n },\n }\n : { rowSelection: false, checkboxSelection: false }\n /*==========================================\n * A HIDDEN FOOTER STILL PAGED THE ROWS.\n *\n * `hideFooter` is how a caller says \"something else owns the page\" — a\n * server-paged staff list, a cursor feed, a card whose `ListPagination`\n * sits under the grid. It hides the CONTROL and nothing else: the free\n * DataGrid always paginates, so the grid went on slicing at the shared\n * default and rows eleven onward were drawn by nothing and reachable by\n * nothing. The outer footer then described the rows it was HANDED, so it\n * reported a page two rows longer than the one on screen and offered no\n * way to see the difference.\n *\n * Eight lists pass `hideFooter`, and every one of them was losing rows past\n * the tenth. It went unnoticed because a list has to hold more than ten\n * rows to show a symptom, and most of these do not yet.\n *\n * So when the footer is hidden the grid takes ONE page as tall as the rows\n * it was given. CONTROLLED rather than `initialState`, because a\n * server-paged list hands over a different number of rows on every page and\n * initial state is read once. A floor of one because MUI rejects a page\n * size of zero, and an empty grid has nothing to slice anyway.\n *=========================================*/\n const rowCount = toolbarOnly ? 0 : (rows?.length ?? 0)\n const footerHidden = hideFooter || toolbarOnly\n const unpaged = footerHidden\n ? {\n paginationModel: { page: 0, pageSize: Math.max(rowCount, 1) },\n // A controlled model with no handler logs a MUI warning, and the\n // grid must not move off page zero in any case: the page is not\n // this component's to change while somebody else owns the footer.\n onPaginationModelChange: () => undefined,\n // Keeps the size out of the \"not preset in pageSizeOptions\" warning.\n // Nothing renders it — the footer carrying the menu is hidden.\n pageSizeOptions: [Math.max(rowCount, 1)],\n }\n : {}\n return (\n <DataTableComponent\n getRowId={(row: any) => row.$id}\n {...selection}\n disableRowSelectionOnClick\n onRowClick={\n onOpen\n ? ({ id, row }, event) => {\n /*\n * A press anywhere in the actions cell is aimed at the actions.\n * `ListRowActions` stops its own clicks, but the cell is wider\n * and taller than the cluster, and a press on the padding\n * beside the menu would otherwise open the record from under\n * the reader.\n */\n const target = event.target as Element | null\n if (target?.closest?.(`[data-field=\"${LIST_ACTIONS_FIELD}\"]`)) return\n onOpen(String(id), row)\n }\n : undefined\n }\n /*\n * ENTER OPENS IT TOO. The grid moves focus cell by cell with the arrow\n * keys, and a row a mouse can open must open from the keyboard as\n * well. Only when the CELL itself holds focus: Enter on a link or a\n * button inside one is that control's, and neither the actions cell\n * nor the grid's own columns (the selection checkbox) open the record.\n */\n onCellKeyDown={\n onOpen\n ? (params, event) => {\n if (event.key !== 'Enter') return\n if (params.field === LIST_ACTIONS_FIELD || params.field.startsWith('__')) return\n if (event.target !== event.currentTarget) return\n event.defaultMuiPrevented = true\n onOpen(String(params.id), params.row)\n }\n : undefined\n }\n /*\n * The list GROWS; the PAGE scrolls.\n *\n * `DataTableComponent` gives its wrapper a fixed `height: 400`, which\n * is right for a grid sitting in a pane of its own and wrong for a\n * card in a column of cards: the table got its own scrollbar, the card\n * stopped at 400px whatever it held, and the last row was cut through\n * the middle. Two scroll regions on one page is the thing to avoid —\n * a reader scrolling the page expects the page to move.\n *\n * `autoHeight` sizes the grid to its rows; clearing the wrapper's\n * height is what lets the card follow it. Caller `sx` is spread after,\n * so a list that genuinely wants a viewport can still take one.\n */\n autoHeight\n sx={[\n { height: 'auto' },\n { '& .MuiDataGrid-row': { cursor: onOpen ? 'pointer' : 'default' } },\n /*\n A cell whose content is taller than one line sets the row's height,\n and every OTHER cell then draws its content at the top of it — so a\n two-line date column leaves the counts beside it riding high while\n `ListRowActions`, which claims `height: '100%'` for itself, sits\n centred. One row, three vertical positions.\n\n Centring the cell itself fixes every column at once, including the\n ones a caller renders with a bare `<Typography>`; `alignItems`\n alone cannot, because an auto-height child has no spare height to\n distribute until something claims the row. `justifyContent` is\n deliberately not set here — a column's own `align` is what decides\n its horizontal placement, and overriding it would silently undo\n every right-aligned figure.\n */\n {\n '& .MuiDataGrid-cell': {\n display: 'flex',\n alignItems: 'center',\n },\n },\n ...(toolbarOnly ? [TOOLBAR_ONLY_SX] : []),\n ...(Array.isArray(sx) ? sx : sx ? [sx] : []),\n ]}\n /*\n * The column menu can hide a column; only the toolbar can bring it\n * back.\n *\n * Every column here is hideable, `Manage columns` lives in the toolbar,\n * and the toolbar was off — so hiding the last column left a grid with\n * no columns and no control that could restore one. Not a rare misuse\n * either: the per-column menu offers `Hide column` on every column, and\n * the way out was never on screen.\n *\n * The Actions column is the reason the grid was not left completely\n * blank — `listActionsColumn` marks it `hideable: false` — and \"one\n * unhideable column\" is not a recovery path.\n */\n showToolbar\n pagination\n // The console's ONE footer (AGL-2501): same options, same default, same\n // label.\n pageSizeOptions={TABLE_PAGE_SIZE_OPTIONS}\n /*\n * MERGED, not replaced. `initialState` used to ride in through `rest`,\n * so a caller that set anything in it — a column visibility model, a\n * sort — silently dropped the shared page size along with it. The two\n * keys a caller is likely to set are spread individually so that a page\n * size and a hidden column can coexist.\n */\n initialState={{\n ...initialState,\n pagination: {\n paginationModel: { pageSize: TABLE_PAGE_SIZE_DEFAULT },\n ...initialState?.pagination,\n },\n columns: { ...initialState?.columns },\n }}\n /*\n * MERGED with the caller's, for the reason `initialState` is: a caller\n * setting one slot's props must not drop the shared footer label or the\n * search box's filter-mode default along with the object they live in.\n */\n slotProps={{\n ...slotProps,\n toolbar: {\n showQuickFilter,\n ...slotProps?.toolbar,\n ...(toolbarOnly\n ? {\n csvOptions: { disableToolbarButton: true },\n printOptions: { disableToolbarButton: true },\n }\n : {}),\n },\n pagination: {\n labelRowsPerPage: TABLE_ROWS_PER_PAGE_LABEL,\n ...slotProps?.pagination,\n },\n }}\n // A toolbar-only grid holds no rows: the reader sees them some other\n // way, and rows laid out under a collapsed body would be drawn for no\n // one.\n rows={toolbarOnly ? NO_ROWS : columnSort ? (columnSort.rows as any[]) : rows}\n hideFooter={footerHidden}\n slots={toolbarOnly ? { ...slots, ...TOOLBAR_ONLY_SLOTS } : slots}\n {...(toolbarOnly ? TOOLBAR_ONLY_PROPS : {})}\n {...rest}\n {...sortProps}\n /*\n * AFTER `rest`, deliberately. \"The footer is hidden, so every row I was\n * handed is on screen\" is an invariant of this component, not a default\n * a call site may talk it out of — a caller that could re-slice here\n * would reintroduce exactly the rows nobody can reach.\n */\n {...unpaged}\n />\n )\n}\nListTable.displayName = 'ListTable'\n\nexport default ListTable\n"],"names":["AppLink","MdiIcon","DataTableComponent","IconButton","Stack","Tooltip","LIST_PAGE_SORT_DESCRIPTION","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","TABLE_PAGE_SIZE_OPTIONS","TABLE_ROWS_PER_PAGE_LABEL","quickActionMenuItem","quick","key","label","icon","path","size","href","to","external","Boolean","onClick","undefined","disabled","unavailableReason","disabledReason","ListRowActions","props","items","menuItems","direction","spacing","sx","height","alignItems","justifyContent","event","stopPropagation","ListQuickButton","action","displayName","name","title","span","aria-label","componentVariant","target","rel","LIST_ACTIONS_FIELD","listActionsColumn","render","options","field","headerName","width","align","headerAlign","sortable","filterable","hideable","disableColumnMenu","renderCell","row","columnsSortedBy","columns","columnSort","map","column","sorted","sortingOrder","mode","description","NO_ROWS","TOOLBAR_ONLY_PROPS","disableColumnSelector","NoOverlay","TOOLBAR_ONLY_SLOTS","noRowsOverlay","noResultsOverlay","loadingOverlay","TOOLBAR_ONLY_SX","flex","minHeight","overflow","display","top","ListTable","onOpen","initialState","hideFooter","rows","selectable","quickFilter","slotProps","toolbarOnly","slots","rest","sortProps","sortingMode","sortModel","onSortModelChange","showQuickFilter","filterMode","selection","rowSelection","checkboxSelection","rowSelectionModel","type","ids","Set","selected","onRowSelectionModelChange","model","all","String","$id","onChange","filter","id","has","rowCount","length","footerHidden","unpaged","paginationModel","page","pageSize","Math","max","onPaginationModelChange","pageSizeOptions","getRowId","disableRowSelectionOnClick","onRowClick","closest","onCellKeyDown","params","startsWith","currentTarget","defaultMuiPrevented","autoHeight","cursor","Array","isArray","showToolbar","pagination","toolbar","csvOptions","disableToolbarButton","printOptions","labelRowsPerPage"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/** One choice in an {@link OptionCardGrid}. */
|
|
18
|
+
export interface OptionCard<T extends string = string> {
|
|
19
|
+
id: T;
|
|
20
|
+
title: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
/** An SVG path, such as an MDI icon's. */
|
|
23
|
+
icon?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface OptionCardGridProps<T extends string = string> {
|
|
26
|
+
/** What the group is, for assistive technology. */
|
|
27
|
+
label: string;
|
|
28
|
+
options: readonly OptionCard<T>[];
|
|
29
|
+
/** The selected option, or `null` for none. */
|
|
30
|
+
value: T | null;
|
|
31
|
+
onChange: (value: T) => void;
|
|
32
|
+
/** Columns at each breakpoint. */
|
|
33
|
+
columns?: {
|
|
34
|
+
xs?: number;
|
|
35
|
+
sm?: number;
|
|
36
|
+
md?: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A grid of cards to pick one from (AGL-3660): a radio group drawn as cards,
|
|
41
|
+
* each with an icon, a title and a line under it. The selected card carries
|
|
42
|
+
* the primary color and a check; the arrow keys move between cards and pick,
|
|
43
|
+
* as a radio group's do, and only the picked card is in the tab order.
|
|
44
|
+
*/
|
|
45
|
+
export declare function OptionCardGrid<T extends string = string>({ label, options, value, onChange, columns, }: OptionCardGridProps<T>): import("react").JSX.Element;
|
|
46
|
+
export default OptionCardGrid;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { Box, Card, CardActionArea, Stack, Typography } from "@mui/material";
|
|
18
|
+
import { alpha } from "@mui/material/styles";
|
|
19
|
+
import { useRef } from "react";
|
|
20
|
+
import { MdiIcon } from "./mdi-icon/mdi-icon.js";
|
|
21
|
+
/**
|
|
22
|
+
* A grid of cards to pick one from (AGL-3660): a radio group drawn as cards,
|
|
23
|
+
* each with an icon, a title and a line under it. The selected card carries
|
|
24
|
+
* the primary color and a check; the arrow keys move between cards and pick,
|
|
25
|
+
* as a radio group's do, and only the picked card is in the tab order.
|
|
26
|
+
*/ export function OptionCardGrid({ label, options, value, onChange, columns = {
|
|
27
|
+
xs: 1,
|
|
28
|
+
sm: 2,
|
|
29
|
+
md: 3
|
|
30
|
+
} }) {
|
|
31
|
+
var _columns_xs;
|
|
32
|
+
const refs = useRef([]);
|
|
33
|
+
const selectedIndex = options.findIndex((option)=>option.id === value);
|
|
34
|
+
const focusable = selectedIndex === -1 ? 0 : selectedIndex;
|
|
35
|
+
const onKeyDown = (event, index)=>{
|
|
36
|
+
var _refs_current_next;
|
|
37
|
+
const step = event.key === 'ArrowRight' || event.key === 'ArrowDown' ? 1 : event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 0;
|
|
38
|
+
if (!step) return;
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
const next = (index + step + options.length) % options.length;
|
|
41
|
+
(_refs_current_next = refs.current[next]) == null ? void 0 : _refs_current_next.focus();
|
|
42
|
+
onChange(options[next].id);
|
|
43
|
+
};
|
|
44
|
+
const template = (count)=>count ? `repeat(${count}, minmax(0, 1fr))` : undefined;
|
|
45
|
+
return /*#__PURE__*/ _jsx(Box, {
|
|
46
|
+
role: "radiogroup",
|
|
47
|
+
"aria-label": label,
|
|
48
|
+
sx: {
|
|
49
|
+
display: 'grid',
|
|
50
|
+
gap: 1.5,
|
|
51
|
+
gridTemplateColumns: {
|
|
52
|
+
xs: template((_columns_xs = columns.xs) != null ? _columns_xs : 1),
|
|
53
|
+
sm: template(columns.sm),
|
|
54
|
+
md: template(columns.md)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
children: options.map((option, index)=>{
|
|
58
|
+
const selected = option.id === value;
|
|
59
|
+
return /*#__PURE__*/ _jsx(Card, {
|
|
60
|
+
variant: "outlined",
|
|
61
|
+
sx: (theme)=>({
|
|
62
|
+
borderRadius: 2,
|
|
63
|
+
borderColor: selected ? 'primary.main' : 'divider',
|
|
64
|
+
bgcolor: selected ? alpha(theme.palette.primary.main, 0.08) : 'transparent'
|
|
65
|
+
}),
|
|
66
|
+
children: /*#__PURE__*/ _jsx(CardActionArea, {
|
|
67
|
+
ref: (node)=>{
|
|
68
|
+
refs.current[index] = node;
|
|
69
|
+
},
|
|
70
|
+
role: "radio",
|
|
71
|
+
"aria-checked": selected,
|
|
72
|
+
"aria-label": option.description ? `${option.title}: ${option.description}` : option.title,
|
|
73
|
+
tabIndex: index === focusable ? 0 : -1,
|
|
74
|
+
onClick: ()=>onChange(option.id),
|
|
75
|
+
onKeyDown: (event)=>onKeyDown(event, index),
|
|
76
|
+
sx: {
|
|
77
|
+
p: 2,
|
|
78
|
+
height: '100%',
|
|
79
|
+
minHeight: 44,
|
|
80
|
+
display: 'flex',
|
|
81
|
+
alignItems: 'flex-start'
|
|
82
|
+
},
|
|
83
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
84
|
+
direction: "row",
|
|
85
|
+
spacing: 1.5,
|
|
86
|
+
sx: {
|
|
87
|
+
alignItems: 'flex-start',
|
|
88
|
+
width: '100%'
|
|
89
|
+
},
|
|
90
|
+
children: [
|
|
91
|
+
option.icon && /*#__PURE__*/ _jsx(Box, {
|
|
92
|
+
sx: {
|
|
93
|
+
color: selected ? 'primary.main' : 'text.secondary',
|
|
94
|
+
display: 'flex',
|
|
95
|
+
pt: 0.25
|
|
96
|
+
},
|
|
97
|
+
"aria-hidden": true,
|
|
98
|
+
children: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
99
|
+
path: option.icon,
|
|
100
|
+
sx: {
|
|
101
|
+
fontSize: 22
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}),
|
|
105
|
+
/*#__PURE__*/ _jsxs(Box, {
|
|
106
|
+
sx: {
|
|
107
|
+
flex: 1,
|
|
108
|
+
minWidth: 0
|
|
109
|
+
},
|
|
110
|
+
children: [
|
|
111
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
112
|
+
variant: "subtitle2",
|
|
113
|
+
component: "span",
|
|
114
|
+
sx: {
|
|
115
|
+
display: 'block'
|
|
116
|
+
},
|
|
117
|
+
children: option.title
|
|
118
|
+
}),
|
|
119
|
+
option.description && /*#__PURE__*/ _jsx(Typography, {
|
|
120
|
+
variant: "body2",
|
|
121
|
+
color: "text.secondary",
|
|
122
|
+
component: "span",
|
|
123
|
+
sx: {
|
|
124
|
+
display: 'block'
|
|
125
|
+
},
|
|
126
|
+
children: option.description
|
|
127
|
+
})
|
|
128
|
+
]
|
|
129
|
+
})
|
|
130
|
+
]
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
}, option.id);
|
|
134
|
+
})
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
export default OptionCardGrid;
|
|
138
|
+
|
|
139
|
+
//# sourceMappingURL=option-card-grid.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/option-card-grid.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Box, Card, CardActionArea, Stack, Typography } from '@mui/material'\nimport { alpha, type Theme } from '@mui/material/styles'\nimport { useRef, type KeyboardEvent } from 'react'\nimport { MdiIcon } from './mdi-icon/mdi-icon'\n\n/** One choice in an {@link OptionCardGrid}. */\nexport interface OptionCard<T extends string = string> {\n id: T\n title: string\n description?: string\n /** An SVG path, such as an MDI icon's. */\n icon?: string\n}\n\nexport interface OptionCardGridProps<T extends string = string> {\n /** What the group is, for assistive technology. */\n label: string\n options: readonly OptionCard<T>[]\n /** The selected option, or `null` for none. */\n value: T | null\n onChange: (value: T) => void\n /** Columns at each breakpoint. */\n columns?: { xs?: number; sm?: number; md?: number }\n}\n\n/**\n * A grid of cards to pick one from (AGL-3660): a radio group drawn as cards,\n * each with an icon, a title and a line under it. The selected card carries\n * the primary color and a check; the arrow keys move between cards and pick,\n * as a radio group's do, and only the picked card is in the tab order.\n */\nexport function OptionCardGrid<T extends string = string>({\n label,\n options,\n value,\n onChange,\n columns = { xs: 1, sm: 2, md: 3 },\n}: OptionCardGridProps<T>) {\n const refs = useRef<Array<HTMLButtonElement | null>>([])\n const selectedIndex = options.findIndex((option) => option.id === value)\n const focusable = selectedIndex === -1 ? 0 : selectedIndex\n\n const onKeyDown = (event: KeyboardEvent, index: number) => {\n const step =\n event.key === 'ArrowRight' || event.key === 'ArrowDown'\n ? 1\n : event.key === 'ArrowLeft' || event.key === 'ArrowUp'\n ? -1\n : 0\n if (!step) return\n event.preventDefault()\n const next = (index + step + options.length) % options.length\n refs.current[next]?.focus()\n onChange(options[next].id)\n }\n\n const template = (count?: number) => (count ? `repeat(${count}, minmax(0, 1fr))` : undefined)\n return (\n <Box\n role=\"radiogroup\"\n aria-label={label}\n sx={{\n display: 'grid',\n gap: 1.5,\n gridTemplateColumns: {\n xs: template(columns.xs ?? 1),\n sm: template(columns.sm),\n md: template(columns.md),\n },\n }}\n >\n {options.map((option, index) => {\n const selected = option.id === value\n return (\n <Card\n key={option.id}\n variant=\"outlined\"\n sx={(theme: Theme) => ({\n borderRadius: 2,\n borderColor: selected ? 'primary.main' : 'divider',\n bgcolor: selected ? alpha(theme.palette.primary.main, 0.08) : 'transparent',\n })}\n >\n <CardActionArea\n ref={(node: HTMLButtonElement | null) => {\n refs.current[index] = node\n }}\n role=\"radio\"\n aria-checked={selected}\n aria-label={option.description ? `${option.title}: ${option.description}` : option.title}\n tabIndex={index === focusable ? 0 : -1}\n onClick={() => onChange(option.id)}\n onKeyDown={(event) => onKeyDown(event, index)}\n sx={{ p: 2, height: '100%', minHeight: 44, display: 'flex', alignItems: 'flex-start' }}\n >\n <Stack direction=\"row\" spacing={1.5} sx={{ alignItems: 'flex-start', width: '100%' }}>\n {option.icon && (\n <Box sx={{ color: selected ? 'primary.main' : 'text.secondary', display: 'flex', pt: 0.25 }} aria-hidden>\n <MdiIcon path={option.icon} sx={{ fontSize: 22 }} />\n </Box>\n )}\n <Box sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"subtitle2\" component=\"span\" sx={{ display: 'block' }}>\n {option.title}\n </Typography>\n {option.description && (\n <Typography variant=\"body2\" color=\"text.secondary\" component=\"span\" sx={{ display: 'block' }}>\n {option.description}\n </Typography>\n )}\n </Box>\n </Stack>\n </CardActionArea>\n </Card>\n )\n })}\n </Box>\n )\n}\n\nexport default OptionCardGrid\n"],"names":["Box","Card","CardActionArea","Stack","Typography","alpha","useRef","MdiIcon","OptionCardGrid","label","options","value","onChange","columns","xs","sm","md","refs","selectedIndex","findIndex","option","id","focusable","onKeyDown","event","index","step","key","preventDefault","next","length","current","focus","template","count","undefined","role","aria-label","sx","display","gap","gridTemplateColumns","map","selected","variant","theme","borderRadius","borderColor","bgcolor","palette","primary","main","ref","node","aria-checked","description","title","tabIndex","onClick","p","height","minHeight","alignItems","direction","spacing","width","icon","color","pt","aria-hidden","path","fontSize","flex","minWidth","component"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,GAAG,EAAEC,IAAI,EAAEC,cAAc,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAC5E,SAASC,KAAK,QAAoB,uBAAsB;AACxD,SAASC,MAAM,QAA4B,QAAO;AAClD,SAASC,OAAO,QAAQ,yBAAqB;AAsB7C;;;;;CAKC,GACD,OAAO,SAASC,eAA0C,EACxDC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,QAAQ,EACRC,UAAU;IAAEC,IAAI;IAAGC,IAAI;IAAGC,IAAI;AAAE,CAAC,EACV;QA4BFH;IA3BrB,MAAMI,OAAOX,OAAwC,EAAE;IACvD,MAAMY,gBAAgBR,QAAQS,SAAS,CAAC,CAACC,SAAWA,OAAOC,EAAE,KAAKV;IAClE,MAAMW,YAAYJ,kBAAkB,CAAC,IAAI,IAAIA;IAE7C,MAAMK,YAAY,CAACC,OAAsBC;YAUvCR;QATA,MAAMS,OACJF,MAAMG,GAAG,KAAK,gBAAgBH,MAAMG,GAAG,KAAK,cACxC,IACAH,MAAMG,GAAG,KAAK,eAAeH,MAAMG,GAAG,KAAK,YACzC,CAAC,IACD;QACR,IAAI,CAACD,MAAM;QACXF,MAAMI,cAAc;QACpB,MAAMC,OAAO,AAACJ,CAAAA,QAAQC,OAAOhB,QAAQoB,MAAM,AAAD,IAAKpB,QAAQoB,MAAM;SAC7Db,qBAAAA,KAAKc,OAAO,CAACF,KAAK,qBAAlBZ,mBAAoBe,KAAK;QACzBpB,SAASF,OAAO,CAACmB,KAAK,CAACR,EAAE;IAC3B;IAEA,MAAMY,WAAW,CAACC,QAAoBA,QAAQ,CAAC,OAAO,EAAEA,MAAM,iBAAiB,CAAC,GAAGC;IACnF,qBACE,KAACnC;QACCoC,MAAK;QACLC,cAAY5B;QACZ6B,IAAI;YACFC,SAAS;YACTC,KAAK;YACLC,qBAAqB;gBACnB3B,IAAImB,UAASpB,cAAAA,QAAQC,EAAE,YAAVD,cAAc;gBAC3BE,IAAIkB,SAASpB,QAAQE,EAAE;gBACvBC,IAAIiB,SAASpB,QAAQG,EAAE;YACzB;QACF;kBAECN,QAAQgC,GAAG,CAAC,CAACtB,QAAQK;YACpB,MAAMkB,WAAWvB,OAAOC,EAAE,KAAKV;YAC/B,qBACE,KAACV;gBAEC2C,SAAQ;gBACRN,IAAI,CAACO,QAAkB,CAAA;wBACrBC,cAAc;wBACdC,aAAaJ,WAAW,iBAAiB;wBACzCK,SAASL,WAAWtC,MAAMwC,MAAMI,OAAO,CAACC,OAAO,CAACC,IAAI,EAAE,QAAQ;oBAChE,CAAA;0BAEA,cAAA,KAACjD;oBACCkD,KAAK,CAACC;wBACJpC,KAAKc,OAAO,CAACN,MAAM,GAAG4B;oBACxB;oBACAjB,MAAK;oBACLkB,gBAAcX;oBACdN,cAAYjB,OAAOmC,WAAW,GAAG,GAAGnC,OAAOoC,KAAK,CAAC,EAAE,EAAEpC,OAAOmC,WAAW,EAAE,GAAGnC,OAAOoC,KAAK;oBACxFC,UAAUhC,UAAUH,YAAY,IAAI,CAAC;oBACrCoC,SAAS,IAAM9C,SAASQ,OAAOC,EAAE;oBACjCE,WAAW,CAACC,QAAUD,UAAUC,OAAOC;oBACvCa,IAAI;wBAAEqB,GAAG;wBAAGC,QAAQ;wBAAQC,WAAW;wBAAItB,SAAS;wBAAQuB,YAAY;oBAAa;8BAErF,cAAA,MAAC3D;wBAAM4D,WAAU;wBAAMC,SAAS;wBAAK1B,IAAI;4BAAEwB,YAAY;4BAAcG,OAAO;wBAAO;;4BAChF7C,OAAO8C,IAAI,kBACV,KAAClE;gCAAIsC,IAAI;oCAAE6B,OAAOxB,WAAW,iBAAiB;oCAAkBJ,SAAS;oCAAQ6B,IAAI;gCAAK;gCAAGC,aAAW;0CACtG,cAAA,KAAC9D;oCAAQ+D,MAAMlD,OAAO8C,IAAI;oCAAE5B,IAAI;wCAAEiC,UAAU;oCAAG;;;0CAGnD,MAACvE;gCAAIsC,IAAI;oCAAEkC,MAAM;oCAAGC,UAAU;gCAAE;;kDAC9B,KAACrE;wCAAWwC,SAAQ;wCAAY8B,WAAU;wCAAOpC,IAAI;4CAAEC,SAAS;wCAAQ;kDACrEnB,OAAOoC,KAAK;;oCAEdpC,OAAOmC,WAAW,kBACjB,KAACnD;wCAAWwC,SAAQ;wCAAQuB,OAAM;wCAAiBO,WAAU;wCAAOpC,IAAI;4CAAEC,SAAS;wCAAQ;kDACxFnB,OAAOmC,WAAW;;;;;;;eAhCxBnC,OAAOC,EAAE;QAwCpB;;AAGN;AAEA,eAAeb,eAAc"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** One choice of a list's filter chips. */
|
|
3
|
+
export interface SearchableVirtualListFilterOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SearchableVirtualListProps<T> {
|
|
8
|
+
/** The rows to show, already searched and filtered by the caller. */
|
|
9
|
+
items: readonly T[];
|
|
10
|
+
itemKey: (item: T, index: number) => string;
|
|
11
|
+
renderItem: (item: T, index: number) => ReactNode;
|
|
12
|
+
/** The search box; the caller decides what a word matches. */
|
|
13
|
+
search: {
|
|
14
|
+
value: string;
|
|
15
|
+
onChange: (value: string) => void;
|
|
16
|
+
label: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* One row of chips that narrows the list to one choice, with an "All" chip
|
|
21
|
+
* first. Absent, the list has no chips.
|
|
22
|
+
*/
|
|
23
|
+
filters?: {
|
|
24
|
+
label: string;
|
|
25
|
+
options: readonly SearchableVirtualListFilterOption[];
|
|
26
|
+
/** The chosen option's value, or `''` for all. */
|
|
27
|
+
value: string;
|
|
28
|
+
onChange: (value: string) => void;
|
|
29
|
+
allLabel?: string;
|
|
30
|
+
};
|
|
31
|
+
/** Scrolls with the rows, above them: a pinned section, a heading. */
|
|
32
|
+
header?: ReactNode;
|
|
33
|
+
/** A line under the controls, such as how many rows match. */
|
|
34
|
+
summary?: ReactNode;
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
/** Placeholder rows while loading. */
|
|
37
|
+
loadingRows?: number;
|
|
38
|
+
/** The height of a placeholder row, in px. */
|
|
39
|
+
loadingRowHeight?: number;
|
|
40
|
+
error?: {
|
|
41
|
+
message: string;
|
|
42
|
+
onRetry?: () => void;
|
|
43
|
+
} | null;
|
|
44
|
+
/** What an empty list says; shown when `items` is empty and nothing is loading. */
|
|
45
|
+
empty: {
|
|
46
|
+
label: ReactNode;
|
|
47
|
+
description?: ReactNode;
|
|
48
|
+
action?: ReactNode;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The list's height. Absent, it fills its parent, which must give it one:
|
|
52
|
+
* a flex column with a height, a dialog's content.
|
|
53
|
+
*/
|
|
54
|
+
height?: number | string;
|
|
55
|
+
/** The gap between rows, in theme spacing units. */
|
|
56
|
+
gap?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A long list a person searches, narrows by a row of chips and scrolls, of
|
|
60
|
+
* which only the rows on screen are drawn (AGL-3656, first for the font
|
|
61
|
+
* picker's 1,950 families): the search box, the chips, the list, and its
|
|
62
|
+
* loading, failed and empty states, in the console's own components.
|
|
63
|
+
*
|
|
64
|
+
* The caller owns the words and the chosen chip and hands in the rows they
|
|
65
|
+
* leave, so the list never decides what a search matches. NOT in the barrel:
|
|
66
|
+
* subpath-import it (`@aglyn/shared-ui-jsx/components/searchable-virtual-list.component`).
|
|
67
|
+
*/
|
|
68
|
+
export declare function SearchableVirtualList<T>(props: SearchableVirtualListProps<T>): import("react").JSX.Element;
|
|
69
|
+
export declare namespace SearchableVirtualList {
|
|
70
|
+
var displayName: string;
|
|
71
|
+
}
|
|
72
|
+
export default SearchableVirtualList;
|