@umodeapporg/ui 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/UI/DataTable/types.d.ts +6 -0
- package/dist/components/UI/DataTable/types.js.map +1 -1
- package/dist/components/UI/DataTable/useDataTableInstance.js +32 -2
- package/dist/components/UI/DataTable/useDataTableInstance.js.map +1 -1
- package/dist/components/UI/ImageZoomOverlay.js +1 -1
- package/dist/components/UI/ImageZoomOverlay.js.map +1 -1
- package/package.json +1 -1
|
@@ -278,6 +278,12 @@ interface IDataTableExpansionConfigBase<T extends IDataTableRow> {
|
|
|
278
278
|
* column (a "pending approvals" count) rather than to the row identity.
|
|
279
279
|
*/
|
|
280
280
|
triggerColumnId?: string;
|
|
281
|
+
/**
|
|
282
|
+
* Which rows start expanded: a boolean for every row or a predicate per row. A row follows it
|
|
283
|
+
* whenever the rows or the predicate change, until the user toggles that row by hand; from then
|
|
284
|
+
* on the row keeps the user's choice. Applies to top-level rows.
|
|
285
|
+
*/
|
|
286
|
+
defaultExpanded?: boolean | ((row: T) => boolean);
|
|
281
287
|
}
|
|
282
288
|
/**
|
|
283
289
|
* Master/detail expansion: child rows render indented under the parent using
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sources":["../../../../src/components/UI/DataTable/types.ts"],"sourcesContent":["import type { ReactNode } from 'react';\n\nimport type { IFilterGroup, TFilterCombinator, TFilterField } from '@/components/Search/filters';\nimport type { TUiCapability } from '@/components/UI/access/UiAccessContext';\nimport type { SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport type { IFilterOption } from '@/interfaces/IFilterOption';\n\nimport type { IGridNavigationConfig } from './grid/gridTypes';\n\nexport type { IGridNavigationConfig, TGridSelectionMode } from './grid/gridTypes';\n\nexport interface IDataTableRow {\n id: string;\n}\n\nexport type TDataTableSortDirection = 'asc' | 'desc';\n\nexport interface IDataTableSortState {\n sortBy: string;\n sortDir: TDataTableSortDirection;\n}\n\nexport type TDataTableStatusTone = 'active' | 'inactive';\n\nexport interface IDataTableStatusValue {\n label: string;\n tone: TDataTableStatusTone;\n}\n\nexport type TDataTableOriginTone = 'erp' | 'custom' | 'official' | 'legacy' | 'default';\n\nexport interface IDataTableOriginValue {\n label: string;\n tone: TDataTableOriginTone;\n}\n\nexport interface IDataTableTagValue {\n label: string;\n className?: string;\n chipIcon?: SymbolCodepoints;\n}\n\nexport interface IDataTableColorValue {\n hex: string | null;\n label?: string;\n}\n\nexport interface IDataTableThumbnailValue {\n src: string | null;\n label?: string;\n}\n\nexport interface IDataTableActionItem<T extends IDataTableRow> {\n key: string;\n label: string | ((row: T) => string);\n icon?: SymbolCodepoints | ((row: T) => SymbolCodepoints);\n /**\n * Optional numeric badge rendered next to the icon (e.g. how many users the\n * action navigates to). Return `null`/`undefined` to render the icon alone.\n */\n count?: (row: T) => number | null | undefined;\n danger?: boolean;\n hidden?: (row: T) => boolean;\n disabled?: (row: T) => boolean;\n /** Missing capability disables the action and replaces its tooltip with the denial reason. */\n capability?: TUiCapability | Array<TUiCapability>;\n /**\n * Renders the action as a real `ControlledSwitch` instead of an icon button, returning\n * the current on/off state of the row. `onClick` still fires on toggle; `icon` is ignored.\n */\n toggle?: (row: T) => boolean;\n onClick: (row: T) => void;\n}\n\nexport type TDataTableCellType =\n | 'text'\n | 'code'\n | 'color'\n | 'thumbnail'\n | 'badgeStatus'\n | 'tag'\n | 'origin'\n | 'count'\n | 'date'\n | 'commitment'\n | 'actions'\n | 'custom';\n\n/**\n * A column config describes how to render one cell type from an entity row.\n * `accessor` maps a row to the value the cell renderer consumes; the concrete\n * value shape depends on `type` (see the cell renderers). `render` allows a\n * fully custom cell when no catalog renderer fits.\n */\nexport interface IDataTableColumnConfig<T extends IDataTableRow> {\n id: string;\n header: string;\n /** Optional tooltip shown on hover over the column header label. */\n headerTooltip?: string;\n /**\n * Optional banded header. Contiguous VISIBLE columns sharing the same label\n * render one spanning cell in an extra row above the header row; columns\n * without a label get an empty band cell. The band follows the current column\n * order, so it is meant for tables with fixed ordering (editor grids).\n */\n headerGroupLabel?: string;\n type: TDataTableCellType;\n headerAlign?: 'left' | 'center' | 'right';\n cellAlign?: 'left' | 'center' | 'right';\n /**\n * Escape hatch for exceptional column widths. Prefer the built-in rules per\n * `type` (actions fixed, badge compact, default min-w-[7.5rem]).\n */\n widthClassName?: string;\n sortable?: boolean;\n /**\n * Whether the end-user may hide this column via the \"Colunas\" toggle. A column\n * is hideable unless this is explicitly `false`. Identity/first columns and the\n * `actions` column must set `hideable: false` so the table never goes empty.\n */\n hideable?: boolean;\n /** Column starts hidden (still toggleable back on via the \"Colunas\" control). */\n defaultHidden?: boolean;\n /**\n * Whether the end-user may drag-resize this column. Defaults to `true`. The\n * `actions` column is never resizable regardless of this flag. Minimum width\n * is max(type seed, header label width); resizing only ever widens it.\n */\n resizable?: boolean;\n /** Backend sort field. Used by server mode. Required when `sortable` in server mode. */\n sortField?: string;\n /**\n * Entity property this column maps to in the backend export catalog. Columns\n * that declare it are offered to the export as `fields`, so the generated file\n * carries the columns the user left visible, in the order they arranged them.\n * A column with no equivalent in the catalog simply omits it.\n */\n exportKey?: string;\n accessor?: (row: T) => unknown;\n /**\n * Client mode: the sortable/globally-filterable primitive for this column.\n * Required for sortable columns whose `accessor` returns an object (e.g.\n * badgeStatus/origem/tag). Falls back to `accessor` when it returns a\n * primitive, else to an empty string.\n */\n sortAccessor?: (row: T) => string | number | boolean;\n render?: (row: T) => ReactNode;\n /** For the `actions` cell type. */\n actions?: Array<IDataTableActionItem<T>>;\n /**\n * Additive, opt-in inline editing for this column. When set (and the table has\n * `editing.onCellChange`), the cell renders a controlled editor instead of the\n * read-only renderer. Omit to keep the column read-only.\n */\n editable?: IDataTableEditableConfig<T>;\n}\n\n/**\n * Inline editing descriptor for one column (additive, opt-in). Turns the cell\n * into a live editor rendered with the project's controlled inputs. This is a\n * CONTROLLED model: the host owns the row data and re-renders after each change,\n * mirroring the existing `onRowsChange` pattern of editor grids. Columns without\n * this field render read-only exactly as before.\n */\nexport type TDataTableEditableType = 'text' | 'number' | 'date' | 'select' | 'multiselect';\n\nexport interface IDataTableEditableConfig<T extends IDataTableRow> {\n type: TDataTableEditableType;\n /** Options for `type: 'select'` / `'multiselect'`. Ignored by the other input types. */\n options?: Array<IFilterOption>;\n /**\n * Editor próprio, para o que os tipos acima não alcançam (uma lista paginada\n * vinda de uma biblioteca, por exemplo). Quando existe, ele substitui o editor\n * derivado de `type`, que segue valendo para leitura, preenchimento e validação.\n */\n renderEditor?: (params: {\n value: unknown;\n onChange: (value: unknown) => void;\n disabled?: boolean;\n label?: string;\n }) => ReactNode;\n /** Current cell value read from the (host-owned) row. */\n getValue: (row: T) => string | number | Array<string> | null;\n /** Disable editing for a specific row (renders a disabled input). */\n disabled?: (row: T) => boolean;\n /** Returns an inline error message for the current value, or `null` when valid. */\n validate?: (value: unknown, row: T) => string | null;\n /**\n * Texto exibido no modo de leitura de `editing.clickToEdit`. Por padrão o valor é derivado do\n * tipo do editor (o rótulo da opção nos selects, a data formatada, o valor cru nos demais).\n */\n displayValue?: (row: T) => string;\n /** Drag-to-fill from this column. Defaults to `true` when the table enables `spreadsheet.fill`. */\n fillable?: boolean;\n /**\n * Forces one fill strategy instead of detecting it from the source values\n * (`constant`, `numericSeries`, `dateSeries`).\n */\n fillStrategy?: string;\n}\n\nexport interface IDataTableFillChange<T extends IDataTableRow> {\n row: T;\n columnId: string;\n value: unknown;\n}\n\nexport interface IDataTableFillConfig<T extends IDataTableRow> {\n /**\n * Applies a whole fill in one call: the host decides how to persist it (a batch\n * request, one queued mutation per cell, an optimistic update). Without it the\n * table falls back to `editing.onCellChange` per written cell.\n */\n onFillCells?: (changes: Array<IDataTableFillChange<T>>) => void;\n /** Upper bound of cells one drag may write. Defaults to 500. */\n maxCells?: number;\n}\n\nexport interface IDataTableSpreadsheetConfig<T extends IDataTableRow> extends IGridNavigationConfig {\n /** Enables the fill handle on editable columns. Omit for navigation only. */\n fill?: IDataTableFillConfig<T>;\n}\n\n/**\n * Add/remove row affordances for an editable table. Only rendered when the\n * corresponding handler is configured: the \"add row\" button appears in the\n * footer when `onAddRow` is set, and a trailing delete control per row when\n * `onRemoveRow` is set (`canRemove` hides it per row).\n */\nexport interface IDataTableEditingRowsConfig<T extends IDataTableRow> {\n canAdd?: boolean;\n onAddRow?: () => void;\n canRemove?: (row: T) => boolean;\n onRemoveRow?: (row: T) => void;\n}\n\n/**\n * Root editing config. `onCellChange` is fired for every editable cell edit with\n * the row, the column id and the new value; the host translates it into its own\n * state update (controlled). Adding this object never changes a table that has no\n * `editable` columns and no `rows` handlers.\n */\nexport interface IDataTableEditingConfig<T extends IDataTableRow> {\n onCellChange: (row: T, columnId: string, value: unknown) => void;\n /**\n * Opt-in: pressing Enter inside a typeable editable cell (text/number) moves\n * focus to the next editable input in DOM order. Native Tab still traverses all\n * cells; date/select cells keep their own popover Enter behaviour.\n */\n keyboardNav?: boolean;\n /**\n * Opt-in: as células editáveis exibem o valor como texto e só viram o editor ao serem clicadas\n * (Tab / Shift+Tab andam entre elas, Esc e o clique fora encerram). Sem a flag, todas as células\n * continuam sendo editores sempre visíveis.\n */\n clickToEdit?: boolean;\n /**\n * Ctrl/Cmd+Z reverts the last edit made through the table and Ctrl/Cmd+Shift+Z\n * (or Ctrl+Y) reapplies it, by writing the previous value back through\n * `onCellChange` / `spreadsheet.fill.onFillCells`. On by default; `false` opts out.\n */\n undo?: boolean;\n rows?: IDataTableEditingRowsConfig<T>;\n}\n\nexport interface IDataTableDetailField {\n label: string;\n value: ReactNode;\n}\n\nexport interface IDataTableDetailConfig<T extends IDataTableRow> {\n title: (row: T) => string;\n code?: (row: T) => string | null;\n status?: (row: T) => IDataTableStatusValue | null;\n color?: (row: T) => IDataTableColorValue | null;\n thumbnail?: (row: T) => IDataTableThumbnailValue | null;\n fields: (row: T) => Array<IDataTableDetailField>;\n /**\n * Full-width block rendered under the two-column `fields`, for content that does not fit a\n * label/value pair (a child list, a nested table). Omit for a fields-only detail.\n */\n content?: (row: T) => ReactNode;\n}\n\nexport interface IDataTableTab<T extends IDataTableRow = IDataTableRow> {\n value: string;\n label: string;\n count?: number;\n /**\n * Client mode: rows are filtered by the active tab's predicate BEFORE search,\n * sort and pagination. Omit (or return true) for an \"all\" tab.\n */\n clientPredicate?: (row: T) => boolean;\n}\n\n/**\n * How the table processes filter/sort/pagination.\n * - `server` (default): the host owns round-trips; the table renders the rows\n * it is given and reports intent via `onSearch` / `onSortChange` / `pagination`.\n * - `client`: the table receives ALL rows once and does filter/sort/pagination\n * in memory (TanStack row models). No round-trips, instant, animated.\n */\nexport type TDataTableDataMode = 'server' | 'client';\n\n/** Body layout the user can switch between via the toolbar toggle. */\nexport type TDataTableViewMode = 'list' | 'cards' | 'grouped' | 'kanban';\n\n/**\n * Shared expand affordance. Chevron visibility is driven by `canExpand` (or a\n * mode-specific default). `onExpandChange` fires after the toggle. Use it for\n * lazy fetches in either mode.\n */\ninterface IDataTableExpansionConfigBase<T extends IDataTableRow> {\n canExpand?: (row: T) => boolean;\n onExpandChange?: (row: T, expanded: boolean) => void;\n /**\n * Column that carries the expand chevron and toggles the row when its cell is clicked.\n * Defaults to the first column. Use it when the expansion belongs to one specific\n * column (a \"pending approvals\" count) rather than to the row identity.\n */\n triggerColumnId?: string;\n}\n\n/**\n * Master/detail expansion: child rows render indented under the parent using\n * the same columns. Children can be loaded lazily: `getSubRows` returns\n * `undefined` until they arrive, `canExpand` decides the chevron from a count,\n * and `isLoadingChildren` drives a loading placeholder while the fetch runs.\n *\n * `mode` defaults to `'subRows'` when omitted so existing Libraries configs\n * keep working without changes.\n */\nexport interface IDataTableSubRowsExpansionConfig<T extends IDataTableRow> extends IDataTableExpansionConfigBase<T> {\n mode?: 'subRows';\n getSubRows: (row: T) => Array<T> | undefined;\n isLoadingChildren?: (row: T) => boolean;\n}\n\n/**\n * Helpers passed to panel `renderExpandedContent` so the panel can sync with\n * the table's expand state (e.g. a \"Collapse\" button inside an inline form).\n */\nexport interface IDataTableExpandedContentContext {\n collapse: () => void;\n}\n\n/**\n * Panel expansion: the row expands into arbitrary React content (forms, editors,\n * rich panels) spanning the full table width. XOR with subRows. Do not combine\n * `getSubRows` and `renderExpandedContent` on the same config.\n *\n * When `canExpand` is omitted, every row is expandable. Use `isLoadingContent`\n * for a lazy-load spinner in the panel slot.\n */\nexport interface IDataTablePanelExpansionConfig<T extends IDataTableRow> extends IDataTableExpansionConfigBase<T> {\n mode: 'panel';\n renderExpandedContent: (row: T, context: IDataTableExpandedContentContext) => ReactNode;\n isLoadingContent?: (row: T) => boolean;\n}\n\n/**\n * Master/detail expansion with child-specific columns (AG-Grid style): the row\n * expands into a nested detail table whose columns describe a DIFFERENT row\n * type (`C`) than the parent (`T`). The detail table renders in the full-width\n * expanded slot with its own header row and the standard cell renderer catalog,\n * but has no toolbar, pagination or sorting. The detail table carries its OWN\n * \"Colunas\" menu (visibility + drag reordering), independent from the parent's\n * and persisted under `${entityKey}:detail`; `defaultHidden` detail columns\n * start hidden and can be toggled back on there.\n *\n * Children can be loaded lazily: `getDetailRows` returns `undefined` until they\n * arrive and a spinner shows while it does (or while `isLoadingChildren` is\n * true). Use `onExpandChange` from the base config to trigger the fetch.\n */\nexport interface IDataTableDetailTableExpansionConfig<\n T extends IDataTableRow,\n C extends IDataTableRow = IDataTableRow,\n> extends IDataTableExpansionConfigBase<T> {\n mode: 'detailTable';\n /** Columns for the detail (child) rows, independent from the parent columns. A function form receives the parent row, for details whose columns vary per parent (dynamic columns skip the persisted show/hide and order state). */\n columns: Array<IDataTableColumnConfig<C>> | ((parentRow: T) => Array<IDataTableColumnConfig<C>>);\n /** Detail rows of a parent. Return `undefined` while they are still loading. */\n getDetailRows: (row: T) => Array<C> | undefined;\n isLoadingChildren?: (row: T) => boolean;\n onDetailRowClick?: (detailRow: C, parentRow: T) => void;\n detailRowIsDimmed?: (detailRow: C) => boolean;\n /** Shown when a parent expands to zero detail rows. Defaults to the shared empty label. */\n emptyMessage?: string;\n}\n\n/**\n * Master/detail expansion into a FULL nested `DataTable`: the row expands into a\n * self-contained `DataTable` driven by its own `childConfig` (its own toolbar,\n * search, pagination, cell catalog, etc.) for a child row type `C`. Unlike\n * `detailTable` (a lightweight header+rows panel), this reuses the whole\n * component so the nested table has every DataTable capability.\n *\n * Children can be loaded lazily: `getChildRows` returns `undefined` until they\n * arrive, `loadChildren` is invoked on expand to trigger the fetch, and\n * `isLoadingChildren` drives the nested table's own loading state.\n */\nexport interface IDataTableTableExpansionConfig<\n T extends IDataTableRow,\n C extends IDataTableRow = IDataTableRow,\n> extends IDataTableExpansionConfigBase<T> {\n mode: 'table';\n /** Full config for the nested child table. */\n childConfig: IDataTableConfig<C>;\n /** Child rows of a parent. Return `undefined` while they are still loading / not fetched. */\n getChildRows: (row: T) => Array<C> | undefined;\n /** Lazy fetch invoked on expand when `getChildRows` returns `undefined`. */\n loadChildren?: (row: T) => Promise<void>;\n isLoadingChildren?: (row: T) => boolean;\n}\n\n/**\n * Row expansion config. Discriminated by `mode`:\n * - `'subRows'` (default): nested table rows via `getSubRows`, same columns\n * - `'panel'`: custom React content via `renderExpandedContent`\n * - `'detailTable'`: nested detail table with its own columns for a child row type\n * - `'table'`: a full nested `DataTable` driven by `childConfig` for a child row type\n */\nexport type TDataTableExpansionConfig<T extends IDataTableRow, C extends IDataTableRow = IDataTableRow> =\n | IDataTableSubRowsExpansionConfig<T>\n | IDataTablePanelExpansionConfig<T>\n | IDataTableDetailTableExpansionConfig<T, C>\n | IDataTableTableExpansionConfig<T, C>;\n\nexport const isDataTablePanelExpansion = <T extends IDataTableRow, C extends IDataTableRow>(\n expansion: TDataTableExpansionConfig<T, C>,\n): expansion is IDataTablePanelExpansionConfig<T> => expansion.mode === 'panel';\n\nexport const isDataTableDetailTableExpansion = <T extends IDataTableRow, C extends IDataTableRow>(\n expansion: TDataTableExpansionConfig<T, C>,\n): expansion is IDataTableDetailTableExpansionConfig<T, C> => expansion.mode === 'detailTable';\n\nexport const isDataTableTableExpansion = <T extends IDataTableRow, C extends IDataTableRow>(\n expansion: TDataTableExpansionConfig<T, C>,\n): expansion is IDataTableTableExpansionConfig<T, C> => expansion.mode === 'table';\n\nexport const isDataTableSubRowsExpansion = <T extends IDataTableRow, C extends IDataTableRow>(\n expansion: TDataTableExpansionConfig<T, C>,\n): expansion is IDataTableSubRowsExpansionConfig<T> => expansion.mode === undefined || expansion.mode === 'subRows';\n\n/**\n * Id of the synthetic trailing actions column injected when `editing.rows`\n * declares an `onRemoveRow` handler. Reserved so hosts do not collide with it.\n */\nexport const DATA_TABLE_REMOVE_COLUMN_ID = '__dataTableRemoveRow__';\n\n/**\n * A table filter field is a shared `TFilterField` (the 13 kinds understood by\n * `FiltersPanel`) plus how its value reaches the data: `predicate` for CLIENT\n * mode, `toServerParams` for SERVER mode. The rendering contract lives in\n * `common/components/Search/filters`, so a new field kind is available to every\n * table without touching the DataTable.\n */\nexport interface IDataTableFilterBinding<T extends IDataTableRow> {\n /**\n * CLIENT mode: in-memory row test. Only called when the field is active.\n * Optional so a field can be server-only.\n */\n predicate?: (row: T, value: unknown) => boolean;\n /**\n * SERVER mode: maps the active value to backend query params, merged into the\n * fetch. Optional so a field can be client-only.\n */\n toServerParams?: (value: unknown) => Record<string, unknown>;\n}\n\nexport type TDataTableFilterField<T extends IDataTableRow> = TFilterField & IDataTableFilterBinding<T>;\n\nexport interface IDataTableFilterOption extends IFilterOption {}\n\nexport interface IDataTableFiltersConfig<T extends IDataTableRow> {\n /** Drawer title. Defaults to the shared \"Filtros\" label. */\n title?: string;\n subtitle?: string;\n /** Collapsible sections. A single group renders without a section header. */\n groups?: Array<IFilterGroup>;\n fields: Array<TDataTableFilterField<T>>;\n /** How active fields combine in CLIENT mode. Defaults to AND. */\n combinator?: TFilterCombinator;\n}\n\n/**\n * The applied value of every filter field, keyed by field `key`. The shape per\n * field follows its `kind` (see `emptyValueForKind`).\n */\nexport type TDataTableFilterValues = Record<string, unknown>;\n\n/**\n * Root entity config that drives the whole table. Adding a new library later\n * means writing one of these objects, not touching the table implementation.\n */\nexport interface IDataTableConfig<T extends IDataTableRow, C extends IDataTableRow = IDataTableRow> {\n entityKey: string;\n /** Rows shown by the list-view loading skeleton. Defaults to 8; lower it for short config tables. */\n skeletonRows?: number;\n dataMode?: TDataTableDataMode;\n columns: Array<IDataTableColumnConfig<T>>;\n /**\n * Additive, opt-in inline editing. Enables `editable` columns (via\n * `onCellChange`) and add/remove-row affordances (via `rows`). Omit for a\n * read-only table — existing configs behave identically.\n */\n editing?: IDataTableEditingConfig<T>;\n /**\n * Spreadsheet layer: active cell, keyboard navigation, range selection and the\n * fill handle on editable columns. It is ON by default in every table; pass a\n * config to narrow it, or `false` to opt out entirely.\n */\n spreadsheet?: IDataTableSpreadsheetConfig<T> | false;\n detail?: IDataTableDetailConfig<T>;\n onRowClick?: (row: T) => void;\n rowIsDimmed?: (row: T) => boolean;\n /**\n * Whether the toolbar search box renders. Defaults to `true`. Set `false` to\n * suppress the search input entirely (e.g. a small/nested table); the toolbar\n * still shows if it has other slots (tabs, view toggle, custom slots).\n */\n searchable?: boolean;\n /**\n * Pinned footer row. Called with the currently-visible rows (outside\n * pagination) and returns the cells to render, keyed by `columnId`. The cells\n * align to their columns like a normal row and the footer is styled distinctly\n * (top border, semibold). Omit for no footer.\n */\n summaryRow?: (visibleRows: Array<T>) => Array<{ columnId: string; content: ReactNode }>;\n /**\n * Segmented toolbar tabs. In client mode the active tab's `clientPredicate`\n * filters rows before search/sort/pagination. First tab is the default.\n */\n tabs?: Array<IDataTableTab<T>>;\n /**\n * Client-side, config-driven filters rendered in a \"Filtrar\" drawer. Each\n * field maps to a drawer input and provides a `predicate` applied in memory\n * before search/sort/pagination. Render the control with `DataTableFilters`.\n */\n filters?: IDataTableFiltersConfig<T>;\n /**\n * Client mode only. Returns the concatenated searchable text for a row so the\n * global filter can match fields that are not even columns (label, description,\n * derived labels). Matching is accent/case-insensitive.\n */\n getSearchableText?: (row: T) => string;\n /**\n * How one row is named outside its own line: today, the pick list of the bulk\n * editor. Without it the first `text` column answers, which is right for most\n * tables and wrong wherever the identity lives in a `custom` column.\n */\n getRowLabel?: (row: T) => string;\n /**\n * Row expansion. When present, the first cell gets a chevron for expandable\n * rows. Use `mode: 'subRows'` (default) for nested table rows, `mode: 'panel'`\n * for custom expanded content under the row, or `mode: 'detailTable'` for a\n * nested detail table with its own columns for a child row type `C`.\n */\n expansion?: TDataTableExpansionConfig<T, C>;\n /**\n * Column that absorbs the leftover horizontal width under `table-fixed`. Defaults to the last\n * visible non-actions, non-badge, non-first column. Name it when the natural candidate is a\n * fixed-size cell (a grid of size columns) and the slack should land on the trailing column.\n */\n flexColumnId?: string;\n /**\n * Card renderer for the current page rows. When present, the toolbar shows a\n * Lista/Cards view toggle; the Cards view renders `renderCard` for every\n * visible (searched/filtered/paginated) row, so both views share one source,\n * toolbar, filters and pagination. Omit for a list-only table.\n */\n renderCard?: (row: T) => ReactNode;\n /** Extra classes for the flex-wrap cards container (spacing/basis overrides). */\n cardGridClassName?: string;\n /**\n * Width class of one skeleton card, so the loading grid matches the real cards\n * when the host renders them at a size other than the default.\n */\n cardSkeletonClassName?: string;\n /**\n * Grouped view. When present, the toolbar view toggle offers a third \"grouping\"\n * option and the body delegates entirely to this renderer while it is active\n * (the host owns fetching, accordions and empty states).\n */\n renderGroupedBody?: () => ReactNode;\n /**\n * Kanban view. Same contract as `renderGroupedBody`: the host owns the lanes,\n * their fetching and their empty states, and the table only yields the body.\n */\n renderKanbanBody?: () => ReactNode;\n}\n"],"names":[],"mappings":"AA4aO,MAAM,4BAA4B,CACvC,cACmD,UAAU,SAAS;AAEjE,MAAM,kCAAkC,CAC7C,cAC4D,UAAU,SAAS;AAE1E,MAAM,4BAA4B,CACvC,cACsD,UAAU,SAAS;AAEpE,MAAM,8BAA8B,CACzC,cACqD,UAAU,SAAS,UAAa,UAAU,SAAS;AAMnG,MAAM,8BAA8B;"}
|
|
1
|
+
{"version":3,"file":"types.js","sources":["../../../../src/components/UI/DataTable/types.ts"],"sourcesContent":["import type { ReactNode } from 'react';\n\nimport type { IFilterGroup, TFilterCombinator, TFilterField } from '@/components/Search/filters';\nimport type { TUiCapability } from '@/components/UI/access/UiAccessContext';\nimport type { SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport type { IFilterOption } from '@/interfaces/IFilterOption';\n\nimport type { IGridNavigationConfig } from './grid/gridTypes';\n\nexport type { IGridNavigationConfig, TGridSelectionMode } from './grid/gridTypes';\n\nexport interface IDataTableRow {\n id: string;\n}\n\nexport type TDataTableSortDirection = 'asc' | 'desc';\n\nexport interface IDataTableSortState {\n sortBy: string;\n sortDir: TDataTableSortDirection;\n}\n\nexport type TDataTableStatusTone = 'active' | 'inactive';\n\nexport interface IDataTableStatusValue {\n label: string;\n tone: TDataTableStatusTone;\n}\n\nexport type TDataTableOriginTone = 'erp' | 'custom' | 'official' | 'legacy' | 'default';\n\nexport interface IDataTableOriginValue {\n label: string;\n tone: TDataTableOriginTone;\n}\n\nexport interface IDataTableTagValue {\n label: string;\n className?: string;\n chipIcon?: SymbolCodepoints;\n}\n\nexport interface IDataTableColorValue {\n hex: string | null;\n label?: string;\n}\n\nexport interface IDataTableThumbnailValue {\n src: string | null;\n label?: string;\n}\n\nexport interface IDataTableActionItem<T extends IDataTableRow> {\n key: string;\n label: string | ((row: T) => string);\n icon?: SymbolCodepoints | ((row: T) => SymbolCodepoints);\n /**\n * Optional numeric badge rendered next to the icon (e.g. how many users the\n * action navigates to). Return `null`/`undefined` to render the icon alone.\n */\n count?: (row: T) => number | null | undefined;\n danger?: boolean;\n hidden?: (row: T) => boolean;\n disabled?: (row: T) => boolean;\n /** Missing capability disables the action and replaces its tooltip with the denial reason. */\n capability?: TUiCapability | Array<TUiCapability>;\n /**\n * Renders the action as a real `ControlledSwitch` instead of an icon button, returning\n * the current on/off state of the row. `onClick` still fires on toggle; `icon` is ignored.\n */\n toggle?: (row: T) => boolean;\n onClick: (row: T) => void;\n}\n\nexport type TDataTableCellType =\n | 'text'\n | 'code'\n | 'color'\n | 'thumbnail'\n | 'badgeStatus'\n | 'tag'\n | 'origin'\n | 'count'\n | 'date'\n | 'commitment'\n | 'actions'\n | 'custom';\n\n/**\n * A column config describes how to render one cell type from an entity row.\n * `accessor` maps a row to the value the cell renderer consumes; the concrete\n * value shape depends on `type` (see the cell renderers). `render` allows a\n * fully custom cell when no catalog renderer fits.\n */\nexport interface IDataTableColumnConfig<T extends IDataTableRow> {\n id: string;\n header: string;\n /** Optional tooltip shown on hover over the column header label. */\n headerTooltip?: string;\n /**\n * Optional banded header. Contiguous VISIBLE columns sharing the same label\n * render one spanning cell in an extra row above the header row; columns\n * without a label get an empty band cell. The band follows the current column\n * order, so it is meant for tables with fixed ordering (editor grids).\n */\n headerGroupLabel?: string;\n type: TDataTableCellType;\n headerAlign?: 'left' | 'center' | 'right';\n cellAlign?: 'left' | 'center' | 'right';\n /**\n * Escape hatch for exceptional column widths. Prefer the built-in rules per\n * `type` (actions fixed, badge compact, default min-w-[7.5rem]).\n */\n widthClassName?: string;\n sortable?: boolean;\n /**\n * Whether the end-user may hide this column via the \"Colunas\" toggle. A column\n * is hideable unless this is explicitly `false`. Identity/first columns and the\n * `actions` column must set `hideable: false` so the table never goes empty.\n */\n hideable?: boolean;\n /** Column starts hidden (still toggleable back on via the \"Colunas\" control). */\n defaultHidden?: boolean;\n /**\n * Whether the end-user may drag-resize this column. Defaults to `true`. The\n * `actions` column is never resizable regardless of this flag. Minimum width\n * is max(type seed, header label width); resizing only ever widens it.\n */\n resizable?: boolean;\n /** Backend sort field. Used by server mode. Required when `sortable` in server mode. */\n sortField?: string;\n /**\n * Entity property this column maps to in the backend export catalog. Columns\n * that declare it are offered to the export as `fields`, so the generated file\n * carries the columns the user left visible, in the order they arranged them.\n * A column with no equivalent in the catalog simply omits it.\n */\n exportKey?: string;\n accessor?: (row: T) => unknown;\n /**\n * Client mode: the sortable/globally-filterable primitive for this column.\n * Required for sortable columns whose `accessor` returns an object (e.g.\n * badgeStatus/origem/tag). Falls back to `accessor` when it returns a\n * primitive, else to an empty string.\n */\n sortAccessor?: (row: T) => string | number | boolean;\n render?: (row: T) => ReactNode;\n /** For the `actions` cell type. */\n actions?: Array<IDataTableActionItem<T>>;\n /**\n * Additive, opt-in inline editing for this column. When set (and the table has\n * `editing.onCellChange`), the cell renders a controlled editor instead of the\n * read-only renderer. Omit to keep the column read-only.\n */\n editable?: IDataTableEditableConfig<T>;\n}\n\n/**\n * Inline editing descriptor for one column (additive, opt-in). Turns the cell\n * into a live editor rendered with the project's controlled inputs. This is a\n * CONTROLLED model: the host owns the row data and re-renders after each change,\n * mirroring the existing `onRowsChange` pattern of editor grids. Columns without\n * this field render read-only exactly as before.\n */\nexport type TDataTableEditableType = 'text' | 'number' | 'date' | 'select' | 'multiselect';\n\nexport interface IDataTableEditableConfig<T extends IDataTableRow> {\n type: TDataTableEditableType;\n /** Options for `type: 'select'` / `'multiselect'`. Ignored by the other input types. */\n options?: Array<IFilterOption>;\n /**\n * Editor próprio, para o que os tipos acima não alcançam (uma lista paginada\n * vinda de uma biblioteca, por exemplo). Quando existe, ele substitui o editor\n * derivado de `type`, que segue valendo para leitura, preenchimento e validação.\n */\n renderEditor?: (params: {\n value: unknown;\n onChange: (value: unknown) => void;\n disabled?: boolean;\n label?: string;\n }) => ReactNode;\n /** Current cell value read from the (host-owned) row. */\n getValue: (row: T) => string | number | Array<string> | null;\n /** Disable editing for a specific row (renders a disabled input). */\n disabled?: (row: T) => boolean;\n /** Returns an inline error message for the current value, or `null` when valid. */\n validate?: (value: unknown, row: T) => string | null;\n /**\n * Texto exibido no modo de leitura de `editing.clickToEdit`. Por padrão o valor é derivado do\n * tipo do editor (o rótulo da opção nos selects, a data formatada, o valor cru nos demais).\n */\n displayValue?: (row: T) => string;\n /** Drag-to-fill from this column. Defaults to `true` when the table enables `spreadsheet.fill`. */\n fillable?: boolean;\n /**\n * Forces one fill strategy instead of detecting it from the source values\n * (`constant`, `numericSeries`, `dateSeries`).\n */\n fillStrategy?: string;\n}\n\nexport interface IDataTableFillChange<T extends IDataTableRow> {\n row: T;\n columnId: string;\n value: unknown;\n}\n\nexport interface IDataTableFillConfig<T extends IDataTableRow> {\n /**\n * Applies a whole fill in one call: the host decides how to persist it (a batch\n * request, one queued mutation per cell, an optimistic update). Without it the\n * table falls back to `editing.onCellChange` per written cell.\n */\n onFillCells?: (changes: Array<IDataTableFillChange<T>>) => void;\n /** Upper bound of cells one drag may write. Defaults to 500. */\n maxCells?: number;\n}\n\nexport interface IDataTableSpreadsheetConfig<T extends IDataTableRow> extends IGridNavigationConfig {\n /** Enables the fill handle on editable columns. Omit for navigation only. */\n fill?: IDataTableFillConfig<T>;\n}\n\n/**\n * Add/remove row affordances for an editable table. Only rendered when the\n * corresponding handler is configured: the \"add row\" button appears in the\n * footer when `onAddRow` is set, and a trailing delete control per row when\n * `onRemoveRow` is set (`canRemove` hides it per row).\n */\nexport interface IDataTableEditingRowsConfig<T extends IDataTableRow> {\n canAdd?: boolean;\n onAddRow?: () => void;\n canRemove?: (row: T) => boolean;\n onRemoveRow?: (row: T) => void;\n}\n\n/**\n * Root editing config. `onCellChange` is fired for every editable cell edit with\n * the row, the column id and the new value; the host translates it into its own\n * state update (controlled). Adding this object never changes a table that has no\n * `editable` columns and no `rows` handlers.\n */\nexport interface IDataTableEditingConfig<T extends IDataTableRow> {\n onCellChange: (row: T, columnId: string, value: unknown) => void;\n /**\n * Opt-in: pressing Enter inside a typeable editable cell (text/number) moves\n * focus to the next editable input in DOM order. Native Tab still traverses all\n * cells; date/select cells keep their own popover Enter behaviour.\n */\n keyboardNav?: boolean;\n /**\n * Opt-in: as células editáveis exibem o valor como texto e só viram o editor ao serem clicadas\n * (Tab / Shift+Tab andam entre elas, Esc e o clique fora encerram). Sem a flag, todas as células\n * continuam sendo editores sempre visíveis.\n */\n clickToEdit?: boolean;\n /**\n * Ctrl/Cmd+Z reverts the last edit made through the table and Ctrl/Cmd+Shift+Z\n * (or Ctrl+Y) reapplies it, by writing the previous value back through\n * `onCellChange` / `spreadsheet.fill.onFillCells`. On by default; `false` opts out.\n */\n undo?: boolean;\n rows?: IDataTableEditingRowsConfig<T>;\n}\n\nexport interface IDataTableDetailField {\n label: string;\n value: ReactNode;\n}\n\nexport interface IDataTableDetailConfig<T extends IDataTableRow> {\n title: (row: T) => string;\n code?: (row: T) => string | null;\n status?: (row: T) => IDataTableStatusValue | null;\n color?: (row: T) => IDataTableColorValue | null;\n thumbnail?: (row: T) => IDataTableThumbnailValue | null;\n fields: (row: T) => Array<IDataTableDetailField>;\n /**\n * Full-width block rendered under the two-column `fields`, for content that does not fit a\n * label/value pair (a child list, a nested table). Omit for a fields-only detail.\n */\n content?: (row: T) => ReactNode;\n}\n\nexport interface IDataTableTab<T extends IDataTableRow = IDataTableRow> {\n value: string;\n label: string;\n count?: number;\n /**\n * Client mode: rows are filtered by the active tab's predicate BEFORE search,\n * sort and pagination. Omit (or return true) for an \"all\" tab.\n */\n clientPredicate?: (row: T) => boolean;\n}\n\n/**\n * How the table processes filter/sort/pagination.\n * - `server` (default): the host owns round-trips; the table renders the rows\n * it is given and reports intent via `onSearch` / `onSortChange` / `pagination`.\n * - `client`: the table receives ALL rows once and does filter/sort/pagination\n * in memory (TanStack row models). No round-trips, instant, animated.\n */\nexport type TDataTableDataMode = 'server' | 'client';\n\n/** Body layout the user can switch between via the toolbar toggle. */\nexport type TDataTableViewMode = 'list' | 'cards' | 'grouped' | 'kanban';\n\n/**\n * Shared expand affordance. Chevron visibility is driven by `canExpand` (or a\n * mode-specific default). `onExpandChange` fires after the toggle. Use it for\n * lazy fetches in either mode.\n */\ninterface IDataTableExpansionConfigBase<T extends IDataTableRow> {\n canExpand?: (row: T) => boolean;\n onExpandChange?: (row: T, expanded: boolean) => void;\n /**\n * Column that carries the expand chevron and toggles the row when its cell is clicked.\n * Defaults to the first column. Use it when the expansion belongs to one specific\n * column (a \"pending approvals\" count) rather than to the row identity.\n */\n triggerColumnId?: string;\n /**\n * Which rows start expanded: a boolean for every row or a predicate per row. A row follows it\n * whenever the rows or the predicate change, until the user toggles that row by hand; from then\n * on the row keeps the user's choice. Applies to top-level rows.\n */\n defaultExpanded?: boolean | ((row: T) => boolean);\n}\n\n/**\n * Master/detail expansion: child rows render indented under the parent using\n * the same columns. Children can be loaded lazily: `getSubRows` returns\n * `undefined` until they arrive, `canExpand` decides the chevron from a count,\n * and `isLoadingChildren` drives a loading placeholder while the fetch runs.\n *\n * `mode` defaults to `'subRows'` when omitted so existing Libraries configs\n * keep working without changes.\n */\nexport interface IDataTableSubRowsExpansionConfig<T extends IDataTableRow> extends IDataTableExpansionConfigBase<T> {\n mode?: 'subRows';\n getSubRows: (row: T) => Array<T> | undefined;\n isLoadingChildren?: (row: T) => boolean;\n}\n\n/**\n * Helpers passed to panel `renderExpandedContent` so the panel can sync with\n * the table's expand state (e.g. a \"Collapse\" button inside an inline form).\n */\nexport interface IDataTableExpandedContentContext {\n collapse: () => void;\n}\n\n/**\n * Panel expansion: the row expands into arbitrary React content (forms, editors,\n * rich panels) spanning the full table width. XOR with subRows. Do not combine\n * `getSubRows` and `renderExpandedContent` on the same config.\n *\n * When `canExpand` is omitted, every row is expandable. Use `isLoadingContent`\n * for a lazy-load spinner in the panel slot.\n */\nexport interface IDataTablePanelExpansionConfig<T extends IDataTableRow> extends IDataTableExpansionConfigBase<T> {\n mode: 'panel';\n renderExpandedContent: (row: T, context: IDataTableExpandedContentContext) => ReactNode;\n isLoadingContent?: (row: T) => boolean;\n}\n\n/**\n * Master/detail expansion with child-specific columns (AG-Grid style): the row\n * expands into a nested detail table whose columns describe a DIFFERENT row\n * type (`C`) than the parent (`T`). The detail table renders in the full-width\n * expanded slot with its own header row and the standard cell renderer catalog,\n * but has no toolbar, pagination or sorting. The detail table carries its OWN\n * \"Colunas\" menu (visibility + drag reordering), independent from the parent's\n * and persisted under `${entityKey}:detail`; `defaultHidden` detail columns\n * start hidden and can be toggled back on there.\n *\n * Children can be loaded lazily: `getDetailRows` returns `undefined` until they\n * arrive and a spinner shows while it does (or while `isLoadingChildren` is\n * true). Use `onExpandChange` from the base config to trigger the fetch.\n */\nexport interface IDataTableDetailTableExpansionConfig<\n T extends IDataTableRow,\n C extends IDataTableRow = IDataTableRow,\n> extends IDataTableExpansionConfigBase<T> {\n mode: 'detailTable';\n /** Columns for the detail (child) rows, independent from the parent columns. A function form receives the parent row, for details whose columns vary per parent (dynamic columns skip the persisted show/hide and order state). */\n columns: Array<IDataTableColumnConfig<C>> | ((parentRow: T) => Array<IDataTableColumnConfig<C>>);\n /** Detail rows of a parent. Return `undefined` while they are still loading. */\n getDetailRows: (row: T) => Array<C> | undefined;\n isLoadingChildren?: (row: T) => boolean;\n onDetailRowClick?: (detailRow: C, parentRow: T) => void;\n detailRowIsDimmed?: (detailRow: C) => boolean;\n /** Shown when a parent expands to zero detail rows. Defaults to the shared empty label. */\n emptyMessage?: string;\n}\n\n/**\n * Master/detail expansion into a FULL nested `DataTable`: the row expands into a\n * self-contained `DataTable` driven by its own `childConfig` (its own toolbar,\n * search, pagination, cell catalog, etc.) for a child row type `C`. Unlike\n * `detailTable` (a lightweight header+rows panel), this reuses the whole\n * component so the nested table has every DataTable capability.\n *\n * Children can be loaded lazily: `getChildRows` returns `undefined` until they\n * arrive, `loadChildren` is invoked on expand to trigger the fetch, and\n * `isLoadingChildren` drives the nested table's own loading state.\n */\nexport interface IDataTableTableExpansionConfig<\n T extends IDataTableRow,\n C extends IDataTableRow = IDataTableRow,\n> extends IDataTableExpansionConfigBase<T> {\n mode: 'table';\n /** Full config for the nested child table. */\n childConfig: IDataTableConfig<C>;\n /** Child rows of a parent. Return `undefined` while they are still loading / not fetched. */\n getChildRows: (row: T) => Array<C> | undefined;\n /** Lazy fetch invoked on expand when `getChildRows` returns `undefined`. */\n loadChildren?: (row: T) => Promise<void>;\n isLoadingChildren?: (row: T) => boolean;\n}\n\n/**\n * Row expansion config. Discriminated by `mode`:\n * - `'subRows'` (default): nested table rows via `getSubRows`, same columns\n * - `'panel'`: custom React content via `renderExpandedContent`\n * - `'detailTable'`: nested detail table with its own columns for a child row type\n * - `'table'`: a full nested `DataTable` driven by `childConfig` for a child row type\n */\nexport type TDataTableExpansionConfig<T extends IDataTableRow, C extends IDataTableRow = IDataTableRow> =\n | IDataTableSubRowsExpansionConfig<T>\n | IDataTablePanelExpansionConfig<T>\n | IDataTableDetailTableExpansionConfig<T, C>\n | IDataTableTableExpansionConfig<T, C>;\n\nexport const isDataTablePanelExpansion = <T extends IDataTableRow, C extends IDataTableRow>(\n expansion: TDataTableExpansionConfig<T, C>,\n): expansion is IDataTablePanelExpansionConfig<T> => expansion.mode === 'panel';\n\nexport const isDataTableDetailTableExpansion = <T extends IDataTableRow, C extends IDataTableRow>(\n expansion: TDataTableExpansionConfig<T, C>,\n): expansion is IDataTableDetailTableExpansionConfig<T, C> => expansion.mode === 'detailTable';\n\nexport const isDataTableTableExpansion = <T extends IDataTableRow, C extends IDataTableRow>(\n expansion: TDataTableExpansionConfig<T, C>,\n): expansion is IDataTableTableExpansionConfig<T, C> => expansion.mode === 'table';\n\nexport const isDataTableSubRowsExpansion = <T extends IDataTableRow, C extends IDataTableRow>(\n expansion: TDataTableExpansionConfig<T, C>,\n): expansion is IDataTableSubRowsExpansionConfig<T> => expansion.mode === undefined || expansion.mode === 'subRows';\n\n/**\n * Id of the synthetic trailing actions column injected when `editing.rows`\n * declares an `onRemoveRow` handler. Reserved so hosts do not collide with it.\n */\nexport const DATA_TABLE_REMOVE_COLUMN_ID = '__dataTableRemoveRow__';\n\n/**\n * A table filter field is a shared `TFilterField` (the 13 kinds understood by\n * `FiltersPanel`) plus how its value reaches the data: `predicate` for CLIENT\n * mode, `toServerParams` for SERVER mode. The rendering contract lives in\n * `common/components/Search/filters`, so a new field kind is available to every\n * table without touching the DataTable.\n */\nexport interface IDataTableFilterBinding<T extends IDataTableRow> {\n /**\n * CLIENT mode: in-memory row test. Only called when the field is active.\n * Optional so a field can be server-only.\n */\n predicate?: (row: T, value: unknown) => boolean;\n /**\n * SERVER mode: maps the active value to backend query params, merged into the\n * fetch. Optional so a field can be client-only.\n */\n toServerParams?: (value: unknown) => Record<string, unknown>;\n}\n\nexport type TDataTableFilterField<T extends IDataTableRow> = TFilterField & IDataTableFilterBinding<T>;\n\nexport interface IDataTableFilterOption extends IFilterOption {}\n\nexport interface IDataTableFiltersConfig<T extends IDataTableRow> {\n /** Drawer title. Defaults to the shared \"Filtros\" label. */\n title?: string;\n subtitle?: string;\n /** Collapsible sections. A single group renders without a section header. */\n groups?: Array<IFilterGroup>;\n fields: Array<TDataTableFilterField<T>>;\n /** How active fields combine in CLIENT mode. Defaults to AND. */\n combinator?: TFilterCombinator;\n}\n\n/**\n * The applied value of every filter field, keyed by field `key`. The shape per\n * field follows its `kind` (see `emptyValueForKind`).\n */\nexport type TDataTableFilterValues = Record<string, unknown>;\n\n/**\n * Root entity config that drives the whole table. Adding a new library later\n * means writing one of these objects, not touching the table implementation.\n */\nexport interface IDataTableConfig<T extends IDataTableRow, C extends IDataTableRow = IDataTableRow> {\n entityKey: string;\n /** Rows shown by the list-view loading skeleton. Defaults to 8; lower it for short config tables. */\n skeletonRows?: number;\n dataMode?: TDataTableDataMode;\n columns: Array<IDataTableColumnConfig<T>>;\n /**\n * Additive, opt-in inline editing. Enables `editable` columns (via\n * `onCellChange`) and add/remove-row affordances (via `rows`). Omit for a\n * read-only table — existing configs behave identically.\n */\n editing?: IDataTableEditingConfig<T>;\n /**\n * Spreadsheet layer: active cell, keyboard navigation, range selection and the\n * fill handle on editable columns. It is ON by default in every table; pass a\n * config to narrow it, or `false` to opt out entirely.\n */\n spreadsheet?: IDataTableSpreadsheetConfig<T> | false;\n detail?: IDataTableDetailConfig<T>;\n onRowClick?: (row: T) => void;\n rowIsDimmed?: (row: T) => boolean;\n /**\n * Whether the toolbar search box renders. Defaults to `true`. Set `false` to\n * suppress the search input entirely (e.g. a small/nested table); the toolbar\n * still shows if it has other slots (tabs, view toggle, custom slots).\n */\n searchable?: boolean;\n /**\n * Pinned footer row. Called with the currently-visible rows (outside\n * pagination) and returns the cells to render, keyed by `columnId`. The cells\n * align to their columns like a normal row and the footer is styled distinctly\n * (top border, semibold). Omit for no footer.\n */\n summaryRow?: (visibleRows: Array<T>) => Array<{ columnId: string; content: ReactNode }>;\n /**\n * Segmented toolbar tabs. In client mode the active tab's `clientPredicate`\n * filters rows before search/sort/pagination. First tab is the default.\n */\n tabs?: Array<IDataTableTab<T>>;\n /**\n * Client-side, config-driven filters rendered in a \"Filtrar\" drawer. Each\n * field maps to a drawer input and provides a `predicate` applied in memory\n * before search/sort/pagination. Render the control with `DataTableFilters`.\n */\n filters?: IDataTableFiltersConfig<T>;\n /**\n * Client mode only. Returns the concatenated searchable text for a row so the\n * global filter can match fields that are not even columns (label, description,\n * derived labels). Matching is accent/case-insensitive.\n */\n getSearchableText?: (row: T) => string;\n /**\n * How one row is named outside its own line: today, the pick list of the bulk\n * editor. Without it the first `text` column answers, which is right for most\n * tables and wrong wherever the identity lives in a `custom` column.\n */\n getRowLabel?: (row: T) => string;\n /**\n * Row expansion. When present, the first cell gets a chevron for expandable\n * rows. Use `mode: 'subRows'` (default) for nested table rows, `mode: 'panel'`\n * for custom expanded content under the row, or `mode: 'detailTable'` for a\n * nested detail table with its own columns for a child row type `C`.\n */\n expansion?: TDataTableExpansionConfig<T, C>;\n /**\n * Column that absorbs the leftover horizontal width under `table-fixed`. Defaults to the last\n * visible non-actions, non-badge, non-first column. Name it when the natural candidate is a\n * fixed-size cell (a grid of size columns) and the slack should land on the trailing column.\n */\n flexColumnId?: string;\n /**\n * Card renderer for the current page rows. When present, the toolbar shows a\n * Lista/Cards view toggle; the Cards view renders `renderCard` for every\n * visible (searched/filtered/paginated) row, so both views share one source,\n * toolbar, filters and pagination. Omit for a list-only table.\n */\n renderCard?: (row: T) => ReactNode;\n /** Extra classes for the flex-wrap cards container (spacing/basis overrides). */\n cardGridClassName?: string;\n /**\n * Width class of one skeleton card, so the loading grid matches the real cards\n * when the host renders them at a size other than the default.\n */\n cardSkeletonClassName?: string;\n /**\n * Grouped view. When present, the toolbar view toggle offers a third \"grouping\"\n * option and the body delegates entirely to this renderer while it is active\n * (the host owns fetching, accordions and empty states).\n */\n renderGroupedBody?: () => ReactNode;\n /**\n * Kanban view. Same contract as `renderGroupedBody`: the host owns the lanes,\n * their fetching and their empty states, and the table only yields the body.\n */\n renderKanbanBody?: () => ReactNode;\n}\n"],"names":[],"mappings":"AAkbO,MAAM,4BAA4B,CACvC,cACmD,UAAU,SAAS;AAEjE,MAAM,kCAAkC,CAC7C,cAC4D,UAAU,SAAS;AAE1E,MAAM,4BAA4B,CACvC,cACsD,UAAU,SAAS;AAEpE,MAAM,8BAA8B,CACzC,cACqD,UAAU,SAAS,UAAa,UAAU,SAAS;AAMnG,MAAM,8BAA8B;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useMemo, useRef, useEffect } from "react";
|
|
2
|
+
import { useState, useMemo, useRef, useEffect, useCallback } from "react";
|
|
3
3
|
import { normalizeText } from "../../../helpers/stringHelpers.js";
|
|
4
4
|
import { createColumnHelper, useReactTable, getExpandedRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel, getCoreRowModel } from "@tanstack/react-table";
|
|
5
5
|
import DataTableActionsCell from "./cells/DataTableActionsCell.js";
|
|
@@ -100,6 +100,36 @@ const useDataTableInstance = ({
|
|
|
100
100
|
const [expanded, setExpanded] = useState({});
|
|
101
101
|
const expansion = config.expansion;
|
|
102
102
|
const hasExpansion = !!expansion;
|
|
103
|
+
const defaultExpanded = expansion?.defaultExpanded;
|
|
104
|
+
const userToggledRowIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
105
|
+
const handleExpandedChange = useCallback((updater) => {
|
|
106
|
+
setExpanded((previous) => {
|
|
107
|
+
const next = typeof updater === "function" ? updater(previous) : updater;
|
|
108
|
+
if (previous !== true && next !== true) {
|
|
109
|
+
for (const rowId of /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(next)])) {
|
|
110
|
+
if (Boolean(previous[rowId]) !== Boolean(next[rowId])) userToggledRowIdsRef.current.add(rowId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return next;
|
|
114
|
+
});
|
|
115
|
+
}, []);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!hasExpansion || defaultExpanded === void 0) return;
|
|
118
|
+
setExpanded((previous) => {
|
|
119
|
+
if (previous === true) return previous;
|
|
120
|
+
let next = null;
|
|
121
|
+
for (const row of tabFilteredRows) {
|
|
122
|
+
const rowId = row.id;
|
|
123
|
+
if (!rowId || userToggledRowIdsRef.current.has(rowId)) continue;
|
|
124
|
+
const shouldExpand = typeof defaultExpanded === "function" ? defaultExpanded(row) : defaultExpanded;
|
|
125
|
+
if (Boolean(previous[rowId]) === shouldExpand) continue;
|
|
126
|
+
next = next ?? { ...previous };
|
|
127
|
+
if (shouldExpand) next[rowId] = true;
|
|
128
|
+
else delete next[rowId];
|
|
129
|
+
}
|
|
130
|
+
return next ?? previous;
|
|
131
|
+
});
|
|
132
|
+
}, [hasExpansion, defaultExpanded, tabFilteredRows]);
|
|
103
133
|
const isPanelExpansion = !!expansion && isDataTablePanelExpansion(expansion);
|
|
104
134
|
const isDetailTableExpansion = !!expansion && isDataTableDetailTableExpansion(expansion);
|
|
105
135
|
const isTableExpansion = !!expansion && isDataTableTableExpansion(expansion);
|
|
@@ -155,7 +185,7 @@ const useDataTableInstance = ({
|
|
|
155
185
|
onSortingChange: isClientMode ? setClientSorting : void 0,
|
|
156
186
|
onGlobalFilterChange: isClientMode ? setGlobalFilter : void 0,
|
|
157
187
|
onPaginationChange: isClientMode ? setClientPagination : void 0,
|
|
158
|
-
onExpandedChange: hasExpansion ?
|
|
188
|
+
onExpandedChange: hasExpansion ? handleExpandedChange : void 0,
|
|
159
189
|
globalFilterFn: isClientMode ? globalFilterFn : void 0,
|
|
160
190
|
// Panel and detailTable modes have no nested rows in the row model. Omit
|
|
161
191
|
// getSubRows so TanStack never looks for children. SubRows mode keeps the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDataTableInstance.js","sources":["../../../../src/components/UI/DataTable/useDataTableInstance.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { normalizeText } from '@/helpers/stringHelpers';\nimport {\n type ColumnSizingState,\n createColumnHelper,\n type ExpandedState,\n type FilterFn,\n getCoreRowModel,\n getExpandedRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n type PaginationState,\n type SortingState,\n type Updater,\n useReactTable,\n type VisibilityState,\n} from '@tanstack/react-table';\n\nimport DataTableActionsCell from './cells/DataTableActionsCell';\nimport DataTableCell from './cells/DataTableCell';\nimport { computeColumnContentWidths, resolveColumnMinWidthPx } from './dataTableColumnWidths';\nimport {\n DATA_TABLE_CROSSFADE_DIP_OPACITY,\n DATA_TABLE_OPACITY_TRANSITION_MS,\n DATA_TABLE_UPDATING_BAR_DELAY_MS,\n DATA_TABLE_UPDATING_OPACITY,\n dataTableOpacityTransitionClass,\n prefersReducedMotion,\n} from './dataTableMotion';\nimport {\n type IDataTableColumnConfig,\n type IDataTableConfig,\n type IDataTableRow,\n type IDataTableSortState,\n isDataTableDetailTableExpansion,\n isDataTablePanelExpansion,\n isDataTableSubRowsExpansion,\n isDataTableTableExpansion,\n type TDataTableSortDirection,\n} from './types';\nimport { useFlipRows } from './useFlipRows';\n\nconst CLIENT_SEARCH_DEBOUNCE_MS = 150;\n\nexport interface IDataTableClientPaginationState {\n page: number;\n pageSize: number;\n}\n\nexport interface IDataTablePagination {\n pageSize: number;\n numberOfPages: number;\n currentPage: number;\n setCurrentPage: (page: number) => void;\n onPageSizeChange?: (pageSize: number) => void;\n /** Total de registros. Presente, a barra mostra o intervalo em vez do número da página. */\n totalItems?: number;\n}\n\ninterface IUseDataTableInstanceParams<T extends IDataTableRow, C extends IDataTableRow = IDataTableRow> {\n config: IDataTableConfig<T, C>;\n rows: Array<T>;\n loading: boolean;\n sort?: IDataTableSortState | null;\n onSortChange?: (sort: IDataTableSortState | null) => void;\n onSearch?: (value: string) => void;\n initialSearch?: string;\n onSearchTermChange?: (value: string) => void;\n pagination?: IDataTablePagination;\n /** Client mode: seeds the internal page/pageSize (1-based page), e.g. from the URL. */\n initialClientPagination?: IDataTableClientPaginationState;\n /** Client mode: reports every page/pageSize change so the host can persist it (URL, storage). */\n onClientPaginationChange?: (pagination: IDataTableClientPaginationState) => void;\n columnVisibility: VisibilityState;\n onColumnVisibilityChange: (updater: Updater<VisibilityState>) => void;\n columnOrder: Array<string>;\n onColumnOrderChange: (updater: Updater<Array<string>>) => void;\n columnSizing: ColumnSizingState;\n onColumnSizingChange: (updater: Updater<ColumnSizingState>) => void;\n onFilteredCountChange?: (count: number) => void;\n}\n\nexport const useDataTableInstance = <T extends IDataTableRow, C extends IDataTableRow = IDataTableRow>({\n config,\n rows,\n loading,\n sort,\n onSortChange,\n onSearch,\n initialSearch = '',\n onSearchTermChange,\n pagination,\n initialClientPagination,\n onClientPaginationChange,\n columnVisibility,\n onColumnVisibilityChange,\n columnOrder,\n onColumnOrderChange,\n columnSizing,\n onColumnSizingChange,\n onFilteredCountChange,\n}: IUseDataTableInstanceParams<T, C>) => {\n const isClientMode = config.dataMode === 'client';\n\n const tabs = config.tabs;\n const [activeTab, setActiveTab] = useState(() => tabs?.[0]?.value ?? '');\n\n // Active tab predicate filters rows BEFORE search/sort/pagination (client mode).\n const tabFilteredRows = useMemo(() => {\n if (!isClientMode || !tabs?.length) return rows;\n const predicate = tabs.find((tab) => tab.value === activeTab)?.clientPredicate;\n if (!predicate) return rows;\n\n return rows.filter(predicate);\n }, [isClientMode, tabs, activeTab, rows]);\n\n const columnHelper = useMemo(() => createColumnHelper<T>(), []);\n\n // Actions and badge columns are sized from the current rows (visible action\n // count / widest chip) so the actions column scales with its buttons and badge\n // labels are never clipped. Other columns fall back to the config-only seed.\n // Both are derived in one memo so the hook signature stays unchanged.\n // Row-derived widths (actions/badge columns) kept in their own memo and read by the\n // column defs through a ref, so editing a cell (which changes the rows) does NOT\n // rebuild the column defs. Rebuilding them hands TanStack a new columns array every\n // keystroke and tears down / remounts every cell, dropping input focus.\n const columnContentWidths = useMemo(\n () => computeColumnContentWidths(config.columns, tabFilteredRows),\n [config.columns, tabFilteredRows],\n );\n const columnContentWidthsRef = useRef(columnContentWidths);\n columnContentWidthsRef.current = columnContentWidths;\n\n const columns = useMemo(() => {\n const contentWidths = columnContentWidthsRef.current;\n\n const columnDefs = config.columns.map((columnConfig, columnIndex) => {\n const accessorFn = (row: T): string | number | boolean => {\n if (columnConfig.sortAccessor) return columnConfig.sortAccessor(row);\n\n const value = columnConfig.accessor?.(row);\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n\n return '';\n };\n\n const isFirstColumn = columnIndex === 0;\n const columnWidth =\n contentWidths.get(columnConfig.id) ??\n resolveColumnMinWidthPx(columnConfig, {\n isFirstColumn,\n hasColumnToggle: isFirstColumn,\n });\n\n return columnHelper.accessor(accessorFn, {\n id: columnConfig.id,\n header: columnConfig.header,\n enableSorting: !!columnConfig.sortable,\n size: columnWidth,\n minSize: columnWidth,\n enableResizing: columnConfig.type !== 'actions' && columnConfig.resizable !== false,\n cell: ({ row }) =>\n columnConfig.type === 'actions' && columnConfig.actions ? (\n <DataTableActionsCell row={row.original} actions={columnConfig.actions} />\n ) : (\n <DataTableCell\n column={columnConfig}\n row={row.original}\n gridRowId={row.id}\n onCellChange={config.editing?.onCellChange}\n keyboardNav={config.editing?.keyboardNav}\n clickToEdit={config.editing?.clickToEdit}\n />\n ),\n });\n });\n\n return columnDefs;\n }, [config.columns, config.editing, columnHelper]);\n\n // ----- Client-mode internal state -----\n const [clientSorting, setClientSorting] = useState<SortingState>([]);\n const [globalFilter, setGlobalFilter] = useState(initialSearch);\n const [clientPagination, setClientPagination] = useState<PaginationState>(() => ({\n pageIndex: Math.max(0, (initialClientPagination?.page ?? 1) - 1),\n pageSize: initialClientPagination?.pageSize ?? 20,\n }));\n\n const onClientPaginationChangeRef = useRef(onClientPaginationChange);\n onClientPaginationChangeRef.current = onClientPaginationChange;\n\n useEffect(() => {\n onClientPaginationChangeRef.current?.({\n page: clientPagination.pageIndex + 1,\n pageSize: clientPagination.pageSize,\n });\n }, [clientPagination.pageIndex, clientPagination.pageSize]);\n const [expanded, setExpanded] = useState<ExpandedState>({});\n\n const expansion = config.expansion;\n const hasExpansion = !!expansion;\n const isPanelExpansion = !!expansion && isDataTablePanelExpansion(expansion);\n const isDetailTableExpansion = !!expansion && isDataTableDetailTableExpansion(expansion);\n const isTableExpansion = !!expansion && isDataTableTableExpansion(expansion);\n const subRowsExpansion = expansion && isDataTableSubRowsExpansion(expansion) ? expansion : null;\n\n const getSearchableText = config.getSearchableText;\n\n const globalFilterFn: FilterFn<T> = useMemo(\n () => (row, _columnId, filterValue) => {\n const term = normalizeText(String(filterValue ?? '')).trim();\n if (!term) return true;\n\n const haystack = getSearchableText ? getSearchableText(row.original) : '';\n\n return normalizeText(haystack).includes(term);\n },\n [getSearchableText],\n );\n\n // ----- Server-mode external sort mapping -----\n const serverSortingState: SortingState = useMemo(() => {\n if (!sort) return [];\n const columnForField = config.columns.find((column) => column.sortField === sort.sortBy);\n if (!columnForField) return [];\n\n return [{ id: columnForField.id, desc: sort.sortDir === 'desc' }];\n }, [sort, config.columns]);\n\n const table = useReactTable({\n data: tabFilteredRows,\n columns,\n // The row id IS the entity id (prefixed by the parent for nested rows), not the\n // default positional index: the spreadsheet layer and the commit queue address\n // cells by it, so it has to survive sorting, filtering and pagination.\n getRowId: (row, index, parent) => {\n const ownId = row.id ?? String(index);\n return parent ? `${parent.id}.${ownId}` : ownId;\n },\n state: isClientMode\n ? {\n sorting: clientSorting,\n globalFilter,\n pagination: clientPagination,\n columnVisibility,\n columnOrder,\n columnSizing,\n expanded,\n }\n : { sorting: serverSortingState, columnVisibility, columnOrder, columnSizing, expanded },\n manualSorting: !isClientMode,\n manualFiltering: !isClientMode,\n manualPagination: !isClientMode,\n // A host that persists the page (URL) expects an in-place row update to keep it;\n // TanStack would otherwise jump back to page 1 whenever `rows` changes reference.\n autoResetPageIndex: onClientPaginationChange ? false : undefined,\n enableColumnResizing: true,\n // `onChange`: columns follow the cursor live during the drag (fluid, ag-grid\n // style) rather than snapping only on release.\n columnResizeMode: 'onChange',\n onColumnVisibilityChange,\n onColumnOrderChange,\n onColumnSizingChange,\n onSortingChange: isClientMode ? setClientSorting : undefined,\n onGlobalFilterChange: isClientMode ? setGlobalFilter : undefined,\n onPaginationChange: isClientMode ? setClientPagination : undefined,\n onExpandedChange: hasExpansion ? setExpanded : undefined,\n globalFilterFn: isClientMode ? globalFilterFn : undefined,\n // Panel and detailTable modes have no nested rows in the row model. Omit\n // getSubRows so TanStack never looks for children. SubRows mode keeps the\n // existing lazy-children path.\n getSubRows: subRowsExpansion?.getSubRows,\n getRowCanExpand: expansion\n ? (row) => {\n if (expansion.canExpand) return expansion.canExpand(row.original);\n // Panel/detailTable/table: every row is expandable unless canExpand says otherwise.\n if (isPanelExpansion || isDetailTableExpansion || isTableExpansion) return true;\n return row.subRows.length > 0;\n }\n : undefined,\n // Client mode keeps children glued to their parent (not counted toward the\n // page size). The pagination row model re-inserts them. Server mode has NO\n // pagination row model, so `false` would leave `getExpandedRowModel` without\n // anything to flatten the children and they would never render; `true` makes\n // the expanded model flatten them itself (server already paginates parents).\n // Panel and detailTable modes have no child rows, so the flag is irrelevant.\n paginateExpandedRows: !isClientMode,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: isClientMode ? getFilteredRowModel() : undefined,\n getSortedRowModel: isClientMode ? getSortedRowModel() : undefined,\n getPaginationRowModel: isClientMode ? getPaginationRowModel() : undefined,\n getExpandedRowModel: hasExpansion ? getExpandedRowModel() : undefined,\n });\n\n const visibleRows = table.getRowModel().rows;\n const orderedRowIds = useMemo(() => visibleRows.map((row) => row.id).join('|'), [visibleRows]);\n // SubRows expansion uses a slower slide so the subgroup open/close reads clearly.\n // Panel and detailTable expansion only toggle a content row. Keep the default\n // FLIP timing for those.\n const flipDurationMs = subRowsExpansion ? 420 : undefined;\n const { registerRow, beginExit: beginRowsExit } = useFlipRows(\n useMemo(() => orderedRowIds.split('|'), [orderedRowIds]),\n flipDurationMs,\n );\n\n const filteredCount = isClientMode ? table.getFilteredRowModel().rows.length : rows.length;\n\n useEffect(() => {\n onFilteredCountChange?.(filteredCount);\n }, [filteredCount, onFilteredCountChange]);\n\n // In server mode, header click round-trips through the host. In client mode\n // TanStack drives the sorting state directly (cycle: asc -> desc -> none).\n const handleHeaderSortClick = (columnConfig: IDataTableColumnConfig<T>) => {\n if (!columnConfig.sortable) return;\n\n if (isClientMode) {\n table.getColumn(columnConfig.id)?.toggleSorting(undefined, false);\n return;\n }\n\n if (!columnConfig.sortField || !onSortChange) return;\n\n const isActive = sort?.sortBy === columnConfig.sortField;\n let nextDir: TDataTableSortDirection = 'asc';\n\n if (isActive && sort?.sortDir === 'asc') nextDir = 'desc';\n else if (isActive && sort?.sortDir === 'desc') {\n onSortChange(null);\n return;\n }\n\n onSortChange({ sortBy: columnConfig.sortField, sortDir: nextDir });\n };\n\n const resolveSortDirection = (columnConfig: IDataTableColumnConfig<T>): false | 'asc' | 'desc' => {\n if (isClientMode) {\n const direction = table.getColumn(columnConfig.id)?.getIsSorted();\n return direction ?? false;\n }\n\n if (sort?.sortBy !== columnConfig.sortField) return false;\n return sort?.sortDir ?? 'asc';\n };\n\n const handleSearchInput = (value: string) => {\n onSearchTermChange?.(value);\n\n if (isClientMode) {\n setGlobalFilter(value);\n table.setPageIndex(0);\n return;\n }\n\n onSearch?.(value);\n };\n\n const handleTabChange = (value: string) => {\n setActiveTab(value);\n if (isClientMode) table.setPageIndex(0);\n };\n\n // Skeleton is shown ONLY on the very first load (no rows yet). Filter/sort/\n // pagination never swap the loaded table for skeletons. That is the \"blink\".\n const showSkeleton = loading && rows.length === 0;\n // Server mode keeps rows visible during a refetch with a subtle updating state.\n const isUpdating = !isClientMode && loading && rows.length > 0;\n\n const [showUpdatingBar, setShowUpdatingBar] = useState(false);\n const [contentOpacity, setContentOpacity] = useState(1);\n const wasUpdatingRef = useRef(false);\n\n useEffect(() => {\n if (!isUpdating) {\n setShowUpdatingBar(false);\n return;\n }\n\n if (prefersReducedMotion()) return;\n\n const timer = window.setTimeout(() => setShowUpdatingBar(true), DATA_TABLE_UPDATING_BAR_DELAY_MS);\n return () => window.clearTimeout(timer);\n }, [isUpdating]);\n\n useEffect(() => {\n const reducedMotion = prefersReducedMotion();\n\n if (wasUpdatingRef.current && !isUpdating && !isClientMode) {\n if (reducedMotion) {\n setContentOpacity(1);\n } else {\n setContentOpacity(DATA_TABLE_CROSSFADE_DIP_OPACITY);\n const frame = window.requestAnimationFrame(() => setContentOpacity(1));\n wasUpdatingRef.current = isUpdating;\n return () => window.cancelAnimationFrame(frame);\n }\n }\n\n wasUpdatingRef.current = isUpdating;\n }, [isUpdating, isClientMode]);\n\n const contentOpacityStyle = useMemo(() => {\n if (prefersReducedMotion()) return undefined;\n\n const opacity = isUpdating ? DATA_TABLE_UPDATING_OPACITY : contentOpacity;\n\n return {\n opacity,\n transition: `opacity ${DATA_TABLE_OPACITY_TRANSITION_MS}ms ease-enter`,\n };\n }, [isUpdating, contentOpacity]);\n\n const clientPaginationProps: IDataTablePagination | undefined = isClientMode\n ? {\n pageSize: clientPagination.pageSize,\n numberOfPages: table.getPageCount(),\n currentPage: clientPagination.pageIndex + 1,\n totalItems: table.getFilteredRowModel().rows.length,\n setCurrentPage: (page: number) => table.setPageIndex(page - 1),\n onPageSizeChange: (size: number) => {\n table.setPageSize(size);\n table.setPageIndex(0);\n },\n }\n : pagination;\n\n const hasRows = visibleRows.length > 0;\n\n return {\n table,\n isClientMode,\n columnContentWidths,\n registerRow,\n beginRowsExit,\n visibleRows,\n orderedRowIds,\n filteredCount,\n activeTab,\n tabs,\n handleTabChange,\n handleSearchInput,\n handleHeaderSortClick,\n resolveSortDirection,\n clientPaginationProps,\n isUpdating,\n showUpdatingBar,\n contentOpacityStyle,\n dataTableOpacityTransitionClass,\n showSkeleton,\n hasRows,\n clientSearchDebounceMs: CLIENT_SEARCH_DEBOUNCE_MS,\n };\n};\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useDataTableInstance.js","sources":["../../../../src/components/UI/DataTable/useDataTableInstance.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { normalizeText } from '@/helpers/stringHelpers';\nimport {\n type ColumnSizingState,\n createColumnHelper,\n type ExpandedState,\n type FilterFn,\n getCoreRowModel,\n getExpandedRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n type PaginationState,\n type SortingState,\n type Updater,\n useReactTable,\n type VisibilityState,\n} from '@tanstack/react-table';\n\nimport DataTableActionsCell from './cells/DataTableActionsCell';\nimport DataTableCell from './cells/DataTableCell';\nimport { computeColumnContentWidths, resolveColumnMinWidthPx } from './dataTableColumnWidths';\nimport {\n DATA_TABLE_CROSSFADE_DIP_OPACITY,\n DATA_TABLE_OPACITY_TRANSITION_MS,\n DATA_TABLE_UPDATING_BAR_DELAY_MS,\n DATA_TABLE_UPDATING_OPACITY,\n dataTableOpacityTransitionClass,\n prefersReducedMotion,\n} from './dataTableMotion';\nimport {\n type IDataTableColumnConfig,\n type IDataTableConfig,\n type IDataTableRow,\n type IDataTableSortState,\n isDataTableDetailTableExpansion,\n isDataTablePanelExpansion,\n isDataTableSubRowsExpansion,\n isDataTableTableExpansion,\n type TDataTableSortDirection,\n} from './types';\nimport { useFlipRows } from './useFlipRows';\n\nconst CLIENT_SEARCH_DEBOUNCE_MS = 150;\n\nexport interface IDataTableClientPaginationState {\n page: number;\n pageSize: number;\n}\n\nexport interface IDataTablePagination {\n pageSize: number;\n numberOfPages: number;\n currentPage: number;\n setCurrentPage: (page: number) => void;\n onPageSizeChange?: (pageSize: number) => void;\n /** Total de registros. Presente, a barra mostra o intervalo em vez do número da página. */\n totalItems?: number;\n}\n\ninterface IUseDataTableInstanceParams<T extends IDataTableRow, C extends IDataTableRow = IDataTableRow> {\n config: IDataTableConfig<T, C>;\n rows: Array<T>;\n loading: boolean;\n sort?: IDataTableSortState | null;\n onSortChange?: (sort: IDataTableSortState | null) => void;\n onSearch?: (value: string) => void;\n initialSearch?: string;\n onSearchTermChange?: (value: string) => void;\n pagination?: IDataTablePagination;\n /** Client mode: seeds the internal page/pageSize (1-based page), e.g. from the URL. */\n initialClientPagination?: IDataTableClientPaginationState;\n /** Client mode: reports every page/pageSize change so the host can persist it (URL, storage). */\n onClientPaginationChange?: (pagination: IDataTableClientPaginationState) => void;\n columnVisibility: VisibilityState;\n onColumnVisibilityChange: (updater: Updater<VisibilityState>) => void;\n columnOrder: Array<string>;\n onColumnOrderChange: (updater: Updater<Array<string>>) => void;\n columnSizing: ColumnSizingState;\n onColumnSizingChange: (updater: Updater<ColumnSizingState>) => void;\n onFilteredCountChange?: (count: number) => void;\n}\n\nexport const useDataTableInstance = <T extends IDataTableRow, C extends IDataTableRow = IDataTableRow>({\n config,\n rows,\n loading,\n sort,\n onSortChange,\n onSearch,\n initialSearch = '',\n onSearchTermChange,\n pagination,\n initialClientPagination,\n onClientPaginationChange,\n columnVisibility,\n onColumnVisibilityChange,\n columnOrder,\n onColumnOrderChange,\n columnSizing,\n onColumnSizingChange,\n onFilteredCountChange,\n}: IUseDataTableInstanceParams<T, C>) => {\n const isClientMode = config.dataMode === 'client';\n\n const tabs = config.tabs;\n const [activeTab, setActiveTab] = useState(() => tabs?.[0]?.value ?? '');\n\n // Active tab predicate filters rows BEFORE search/sort/pagination (client mode).\n const tabFilteredRows = useMemo(() => {\n if (!isClientMode || !tabs?.length) return rows;\n const predicate = tabs.find((tab) => tab.value === activeTab)?.clientPredicate;\n if (!predicate) return rows;\n\n return rows.filter(predicate);\n }, [isClientMode, tabs, activeTab, rows]);\n\n const columnHelper = useMemo(() => createColumnHelper<T>(), []);\n\n // Actions and badge columns are sized from the current rows (visible action\n // count / widest chip) so the actions column scales with its buttons and badge\n // labels are never clipped. Other columns fall back to the config-only seed.\n // Both are derived in one memo so the hook signature stays unchanged.\n // Row-derived widths (actions/badge columns) kept in their own memo and read by the\n // column defs through a ref, so editing a cell (which changes the rows) does NOT\n // rebuild the column defs. Rebuilding them hands TanStack a new columns array every\n // keystroke and tears down / remounts every cell, dropping input focus.\n const columnContentWidths = useMemo(\n () => computeColumnContentWidths(config.columns, tabFilteredRows),\n [config.columns, tabFilteredRows],\n );\n const columnContentWidthsRef = useRef(columnContentWidths);\n columnContentWidthsRef.current = columnContentWidths;\n\n const columns = useMemo(() => {\n const contentWidths = columnContentWidthsRef.current;\n\n const columnDefs = config.columns.map((columnConfig, columnIndex) => {\n const accessorFn = (row: T): string | number | boolean => {\n if (columnConfig.sortAccessor) return columnConfig.sortAccessor(row);\n\n const value = columnConfig.accessor?.(row);\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n\n return '';\n };\n\n const isFirstColumn = columnIndex === 0;\n const columnWidth =\n contentWidths.get(columnConfig.id) ??\n resolveColumnMinWidthPx(columnConfig, {\n isFirstColumn,\n hasColumnToggle: isFirstColumn,\n });\n\n return columnHelper.accessor(accessorFn, {\n id: columnConfig.id,\n header: columnConfig.header,\n enableSorting: !!columnConfig.sortable,\n size: columnWidth,\n minSize: columnWidth,\n enableResizing: columnConfig.type !== 'actions' && columnConfig.resizable !== false,\n cell: ({ row }) =>\n columnConfig.type === 'actions' && columnConfig.actions ? (\n <DataTableActionsCell row={row.original} actions={columnConfig.actions} />\n ) : (\n <DataTableCell\n column={columnConfig}\n row={row.original}\n gridRowId={row.id}\n onCellChange={config.editing?.onCellChange}\n keyboardNav={config.editing?.keyboardNav}\n clickToEdit={config.editing?.clickToEdit}\n />\n ),\n });\n });\n\n return columnDefs;\n }, [config.columns, config.editing, columnHelper]);\n\n // ----- Client-mode internal state -----\n const [clientSorting, setClientSorting] = useState<SortingState>([]);\n const [globalFilter, setGlobalFilter] = useState(initialSearch);\n const [clientPagination, setClientPagination] = useState<PaginationState>(() => ({\n pageIndex: Math.max(0, (initialClientPagination?.page ?? 1) - 1),\n pageSize: initialClientPagination?.pageSize ?? 20,\n }));\n\n const onClientPaginationChangeRef = useRef(onClientPaginationChange);\n onClientPaginationChangeRef.current = onClientPaginationChange;\n\n useEffect(() => {\n onClientPaginationChangeRef.current?.({\n page: clientPagination.pageIndex + 1,\n pageSize: clientPagination.pageSize,\n });\n }, [clientPagination.pageIndex, clientPagination.pageSize]);\n const [expanded, setExpanded] = useState<ExpandedState>({});\n\n const expansion = config.expansion;\n const hasExpansion = !!expansion;\n const defaultExpanded = expansion?.defaultExpanded;\n\n // Rows the user opened or closed by hand keep that choice; `defaultExpanded` only drives the\n // rest, so a default that resolves later (a count that arrives async) never undoes a click.\n const userToggledRowIdsRef = useRef<Set<string>>(new Set());\n\n const handleExpandedChange = useCallback((updater: Updater<ExpandedState>) => {\n setExpanded((previous) => {\n const next = typeof updater === 'function' ? updater(previous) : updater;\n\n if (previous !== true && next !== true) {\n for (const rowId of new Set([...Object.keys(previous), ...Object.keys(next)])) {\n if (Boolean(previous[rowId]) !== Boolean(next[rowId])) userToggledRowIdsRef.current.add(rowId);\n }\n }\n\n return next;\n });\n }, []);\n\n useEffect(() => {\n if (!hasExpansion || defaultExpanded === undefined) return;\n\n setExpanded((previous) => {\n if (previous === true) return previous;\n\n let next: Record<string, boolean> | null = null;\n\n for (const row of tabFilteredRows) {\n const rowId = row.id;\n if (!rowId || userToggledRowIdsRef.current.has(rowId)) continue;\n\n const shouldExpand = typeof defaultExpanded === 'function' ? defaultExpanded(row) : defaultExpanded;\n if (Boolean(previous[rowId]) === shouldExpand) continue;\n\n next = next ?? { ...previous };\n if (shouldExpand) next[rowId] = true;\n else delete next[rowId];\n }\n\n return next ?? previous;\n });\n }, [hasExpansion, defaultExpanded, tabFilteredRows]);\n\n const isPanelExpansion = !!expansion && isDataTablePanelExpansion(expansion);\n const isDetailTableExpansion = !!expansion && isDataTableDetailTableExpansion(expansion);\n const isTableExpansion = !!expansion && isDataTableTableExpansion(expansion);\n const subRowsExpansion = expansion && isDataTableSubRowsExpansion(expansion) ? expansion : null;\n\n const getSearchableText = config.getSearchableText;\n\n const globalFilterFn: FilterFn<T> = useMemo(\n () => (row, _columnId, filterValue) => {\n const term = normalizeText(String(filterValue ?? '')).trim();\n if (!term) return true;\n\n const haystack = getSearchableText ? getSearchableText(row.original) : '';\n\n return normalizeText(haystack).includes(term);\n },\n [getSearchableText],\n );\n\n // ----- Server-mode external sort mapping -----\n const serverSortingState: SortingState = useMemo(() => {\n if (!sort) return [];\n const columnForField = config.columns.find((column) => column.sortField === sort.sortBy);\n if (!columnForField) return [];\n\n return [{ id: columnForField.id, desc: sort.sortDir === 'desc' }];\n }, [sort, config.columns]);\n\n const table = useReactTable({\n data: tabFilteredRows,\n columns,\n // The row id IS the entity id (prefixed by the parent for nested rows), not the\n // default positional index: the spreadsheet layer and the commit queue address\n // cells by it, so it has to survive sorting, filtering and pagination.\n getRowId: (row, index, parent) => {\n const ownId = row.id ?? String(index);\n return parent ? `${parent.id}.${ownId}` : ownId;\n },\n state: isClientMode\n ? {\n sorting: clientSorting,\n globalFilter,\n pagination: clientPagination,\n columnVisibility,\n columnOrder,\n columnSizing,\n expanded,\n }\n : { sorting: serverSortingState, columnVisibility, columnOrder, columnSizing, expanded },\n manualSorting: !isClientMode,\n manualFiltering: !isClientMode,\n manualPagination: !isClientMode,\n // A host that persists the page (URL) expects an in-place row update to keep it;\n // TanStack would otherwise jump back to page 1 whenever `rows` changes reference.\n autoResetPageIndex: onClientPaginationChange ? false : undefined,\n enableColumnResizing: true,\n // `onChange`: columns follow the cursor live during the drag (fluid, ag-grid\n // style) rather than snapping only on release.\n columnResizeMode: 'onChange',\n onColumnVisibilityChange,\n onColumnOrderChange,\n onColumnSizingChange,\n onSortingChange: isClientMode ? setClientSorting : undefined,\n onGlobalFilterChange: isClientMode ? setGlobalFilter : undefined,\n onPaginationChange: isClientMode ? setClientPagination : undefined,\n onExpandedChange: hasExpansion ? handleExpandedChange : undefined,\n globalFilterFn: isClientMode ? globalFilterFn : undefined,\n // Panel and detailTable modes have no nested rows in the row model. Omit\n // getSubRows so TanStack never looks for children. SubRows mode keeps the\n // existing lazy-children path.\n getSubRows: subRowsExpansion?.getSubRows,\n getRowCanExpand: expansion\n ? (row) => {\n if (expansion.canExpand) return expansion.canExpand(row.original);\n // Panel/detailTable/table: every row is expandable unless canExpand says otherwise.\n if (isPanelExpansion || isDetailTableExpansion || isTableExpansion) return true;\n return row.subRows.length > 0;\n }\n : undefined,\n // Client mode keeps children glued to their parent (not counted toward the\n // page size). The pagination row model re-inserts them. Server mode has NO\n // pagination row model, so `false` would leave `getExpandedRowModel` without\n // anything to flatten the children and they would never render; `true` makes\n // the expanded model flatten them itself (server already paginates parents).\n // Panel and detailTable modes have no child rows, so the flag is irrelevant.\n paginateExpandedRows: !isClientMode,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: isClientMode ? getFilteredRowModel() : undefined,\n getSortedRowModel: isClientMode ? getSortedRowModel() : undefined,\n getPaginationRowModel: isClientMode ? getPaginationRowModel() : undefined,\n getExpandedRowModel: hasExpansion ? getExpandedRowModel() : undefined,\n });\n\n const visibleRows = table.getRowModel().rows;\n const orderedRowIds = useMemo(() => visibleRows.map((row) => row.id).join('|'), [visibleRows]);\n // SubRows expansion uses a slower slide so the subgroup open/close reads clearly.\n // Panel and detailTable expansion only toggle a content row. Keep the default\n // FLIP timing for those.\n const flipDurationMs = subRowsExpansion ? 420 : undefined;\n const { registerRow, beginExit: beginRowsExit } = useFlipRows(\n useMemo(() => orderedRowIds.split('|'), [orderedRowIds]),\n flipDurationMs,\n );\n\n const filteredCount = isClientMode ? table.getFilteredRowModel().rows.length : rows.length;\n\n useEffect(() => {\n onFilteredCountChange?.(filteredCount);\n }, [filteredCount, onFilteredCountChange]);\n\n // In server mode, header click round-trips through the host. In client mode\n // TanStack drives the sorting state directly (cycle: asc -> desc -> none).\n const handleHeaderSortClick = (columnConfig: IDataTableColumnConfig<T>) => {\n if (!columnConfig.sortable) return;\n\n if (isClientMode) {\n table.getColumn(columnConfig.id)?.toggleSorting(undefined, false);\n return;\n }\n\n if (!columnConfig.sortField || !onSortChange) return;\n\n const isActive = sort?.sortBy === columnConfig.sortField;\n let nextDir: TDataTableSortDirection = 'asc';\n\n if (isActive && sort?.sortDir === 'asc') nextDir = 'desc';\n else if (isActive && sort?.sortDir === 'desc') {\n onSortChange(null);\n return;\n }\n\n onSortChange({ sortBy: columnConfig.sortField, sortDir: nextDir });\n };\n\n const resolveSortDirection = (columnConfig: IDataTableColumnConfig<T>): false | 'asc' | 'desc' => {\n if (isClientMode) {\n const direction = table.getColumn(columnConfig.id)?.getIsSorted();\n return direction ?? false;\n }\n\n if (sort?.sortBy !== columnConfig.sortField) return false;\n return sort?.sortDir ?? 'asc';\n };\n\n const handleSearchInput = (value: string) => {\n onSearchTermChange?.(value);\n\n if (isClientMode) {\n setGlobalFilter(value);\n table.setPageIndex(0);\n return;\n }\n\n onSearch?.(value);\n };\n\n const handleTabChange = (value: string) => {\n setActiveTab(value);\n if (isClientMode) table.setPageIndex(0);\n };\n\n // Skeleton is shown ONLY on the very first load (no rows yet). Filter/sort/\n // pagination never swap the loaded table for skeletons. That is the \"blink\".\n const showSkeleton = loading && rows.length === 0;\n // Server mode keeps rows visible during a refetch with a subtle updating state.\n const isUpdating = !isClientMode && loading && rows.length > 0;\n\n const [showUpdatingBar, setShowUpdatingBar] = useState(false);\n const [contentOpacity, setContentOpacity] = useState(1);\n const wasUpdatingRef = useRef(false);\n\n useEffect(() => {\n if (!isUpdating) {\n setShowUpdatingBar(false);\n return;\n }\n\n if (prefersReducedMotion()) return;\n\n const timer = window.setTimeout(() => setShowUpdatingBar(true), DATA_TABLE_UPDATING_BAR_DELAY_MS);\n return () => window.clearTimeout(timer);\n }, [isUpdating]);\n\n useEffect(() => {\n const reducedMotion = prefersReducedMotion();\n\n if (wasUpdatingRef.current && !isUpdating && !isClientMode) {\n if (reducedMotion) {\n setContentOpacity(1);\n } else {\n setContentOpacity(DATA_TABLE_CROSSFADE_DIP_OPACITY);\n const frame = window.requestAnimationFrame(() => setContentOpacity(1));\n wasUpdatingRef.current = isUpdating;\n return () => window.cancelAnimationFrame(frame);\n }\n }\n\n wasUpdatingRef.current = isUpdating;\n }, [isUpdating, isClientMode]);\n\n const contentOpacityStyle = useMemo(() => {\n if (prefersReducedMotion()) return undefined;\n\n const opacity = isUpdating ? DATA_TABLE_UPDATING_OPACITY : contentOpacity;\n\n return {\n opacity,\n transition: `opacity ${DATA_TABLE_OPACITY_TRANSITION_MS}ms ease-enter`,\n };\n }, [isUpdating, contentOpacity]);\n\n const clientPaginationProps: IDataTablePagination | undefined = isClientMode\n ? {\n pageSize: clientPagination.pageSize,\n numberOfPages: table.getPageCount(),\n currentPage: clientPagination.pageIndex + 1,\n totalItems: table.getFilteredRowModel().rows.length,\n setCurrentPage: (page: number) => table.setPageIndex(page - 1),\n onPageSizeChange: (size: number) => {\n table.setPageSize(size);\n table.setPageIndex(0);\n },\n }\n : pagination;\n\n const hasRows = visibleRows.length > 0;\n\n return {\n table,\n isClientMode,\n columnContentWidths,\n registerRow,\n beginRowsExit,\n visibleRows,\n orderedRowIds,\n filteredCount,\n activeTab,\n tabs,\n handleTabChange,\n handleSearchInput,\n handleHeaderSortClick,\n resolveSortDirection,\n clientPaginationProps,\n isUpdating,\n showUpdatingBar,\n contentOpacityStyle,\n dataTableOpacityTransitionClass,\n showSkeleton,\n hasRows,\n clientSearchDebounceMs: CLIENT_SEARCH_DEBOUNCE_MS,\n };\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -28,7 +28,7 @@ const ImageZoomOverlay = ({ src, onClose }) => {
|
|
|
28
28
|
role: "dialog",
|
|
29
29
|
"aria-modal": true,
|
|
30
30
|
"aria-label": t("imageZoom.title"),
|
|
31
|
-
className: "fixed inset-0 z-[
|
|
31
|
+
className: "fixed inset-0 z-[9999] flex items-center justify-center p-6 glass-scrim",
|
|
32
32
|
children: [
|
|
33
33
|
/* @__PURE__ */ jsx(ControlledTooltip, { content: t("imageZoom.close"), className: "z-[9999]", children: /* @__PURE__ */ jsx(
|
|
34
34
|
ControlledButton,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageZoomOverlay.js","sources":["../../../src/components/UI/ImageZoomOverlay.tsx"],"sourcesContent":["import React, { useEffect } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { getPortalHost } from '@/helpers/portalHost';\n\ninterface IImageZoomOverlayProps {\n src: string;\n onClose: () => void;\n}\n\n// Visualização ampliada em tela cheia (portal). Fecha com Esc, clique fora ou\n// no botão de fechar.\nconst ImageZoomOverlay: React.FC<IImageZoomOverlayProps> = ({ src, onClose }) => {\n const { t } = useTranslation('ui');\n\n useEffect(() => {\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== 'Escape') return;\n // Na captura e barrando a propagação: o drawer que abriu esta imagem também escuta Esc no\n // document, e sem isso uma tecla fecharia a imagem E o drawer atrás dela.\n event.stopPropagation();\n onClose();\n };\n document.addEventListener('keydown', onKeyDown, true);\n return () => document.removeEventListener('keydown', onKeyDown, true);\n }, [onClose]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <div\n onClick={onClose}\n // `role=\"dialog\"` não é enfeite: é como `isPressOnLayeredSurface` reconhece esta camada. Sem\n // ele, o drawer por baixo lê o clique no X como \"clicou fora de mim\" e se fecha junto.\n role=\"dialog\"\n aria-modal={true}\n aria-label={t('imageZoom.title')}\n className=\"fixed inset-0 z-[
|
|
1
|
+
{"version":3,"file":"ImageZoomOverlay.js","sources":["../../../src/components/UI/ImageZoomOverlay.tsx"],"sourcesContent":["import React, { useEffect } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { getPortalHost } from '@/helpers/portalHost';\n\ninterface IImageZoomOverlayProps {\n src: string;\n onClose: () => void;\n}\n\n// Visualização ampliada em tela cheia (portal). Fecha com Esc, clique fora ou\n// no botão de fechar.\nconst ImageZoomOverlay: React.FC<IImageZoomOverlayProps> = ({ src, onClose }) => {\n const { t } = useTranslation('ui');\n\n useEffect(() => {\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== 'Escape') return;\n // Na captura e barrando a propagação: o drawer que abriu esta imagem também escuta Esc no\n // document, e sem isso uma tecla fecharia a imagem E o drawer atrás dela.\n event.stopPropagation();\n onClose();\n };\n document.addEventListener('keydown', onKeyDown, true);\n return () => document.removeEventListener('keydown', onKeyDown, true);\n }, [onClose]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <div\n onClick={onClose}\n // `role=\"dialog\"` não é enfeite: é como `isPressOnLayeredSurface` reconhece esta camada. Sem\n // ele, o drawer por baixo lê o clique no X como \"clicou fora de mim\" e se fecha junto.\n role=\"dialog\"\n aria-modal={true}\n aria-label={t('imageZoom.title')}\n // Same layer as ControlledDialog: mounted after the dialog that opened it, DOM order puts\n // the zoom on top. Any lower value hides it behind the dialog's scrim.\n className=\"fixed inset-0 z-[9999] flex items-center justify-center p-6 glass-scrim\"\n >\n <ControlledTooltip content={t('imageZoom.close')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n onClick={onClose}\n className=\"absolute right-4 top-4 flex items-center justify-center rounded-full bg-glass/10 p-2 text-white transition-interactive duration-base ease-enter hover:bg-glass/20 active:scale-90\"\n >\n <MaterialSymbol icon=\"close\" size={24} />\n </ControlledButton>\n </ControlledTooltip>\n <div onClick={(event) => event.stopPropagation()} className=\"flex max-h-full max-w-full\">\n <ControlledImage src={src} className=\"max-h-[90vh] max-w-[90vw] object-contain\" />\n </div>\n </div>,\n portalHost,\n );\n};\n\nexport default ImageZoomOverlay;\n"],"names":[],"mappings":";;;;;;;;;AAiBA,MAAM,mBAAqD,CAAC,EAAE,KAAK,cAAc;AAC/E,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,YAAU,MAAM;AACd,UAAM,YAAY,CAAC,UAA+B;AAChD,UAAI,MAAM,QAAQ,SAAU;AAG5B,YAAM,gBAAA;AACN,cAAA;AAAA,IACF;AACA,aAAS,iBAAiB,WAAW,WAAW,IAAI;AACpD,WAAO,MAAM,SAAS,oBAAoB,WAAW,WAAW,IAAI;AAAA,EACtE,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,aAAa,cAAA;AACnB,MAAI,eAAe,KAAM,QAAO;AAEhC,SAAO;AAAA,IACL;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAS;AAAA,QAGT,MAAK;AAAA,QACL,cAAY;AAAA,QACZ,cAAY,EAAE,iBAAiB;AAAA,QAG/B,WAAU;AAAA,QAEV,UAAA;AAAA,UAAA,oBAAC,qBAAkB,SAAS,EAAE,iBAAiB,GAAG,WAAU,YAC1D,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,mBAAiB;AAAA,cACjB,SAAS;AAAA,cACT,WAAU;AAAA,cAEV,UAAA,oBAAC,gBAAA,EAAe,MAAK,SAAQ,MAAM,GAAA,CAAI;AAAA,YAAA;AAAA,UAAA,GAE3C;AAAA,UACA,oBAAC,OAAA,EAAI,SAAS,CAAC,UAAU,MAAM,mBAAmB,WAAU,8BAC1D,UAAA,oBAAC,iBAAA,EAAgB,KAAU,WAAU,4CAA2C,EAAA,CAClF;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAEF;AAAA,EAAA;AAEJ;"}
|