@umodeapporg/ui 0.10.0 → 0.11.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.
@@ -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 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":"AAsaO,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\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;"}
@@ -0,0 +1,30 @@
1
+ import { KeyboardEvent } from 'react';
2
+ import { IDataTableColumnConfig, IDataTableFillChange, IDataTableRow } from './types';
3
+ export interface IDataTableRestoredCell<T extends IDataTableRow> {
4
+ row: T;
5
+ columnId: string;
6
+ }
7
+ interface IUseDataTableUndoHistoryParams<T extends IDataTableRow> {
8
+ enabled: boolean;
9
+ rows: Array<T>;
10
+ columns: Array<IDataTableColumnConfig<T>>;
11
+ onCellChange?: (row: T, columnId: string, value: unknown) => void;
12
+ onFillCells?: (changes: Array<IDataTableFillChange<T>>) => void;
13
+ }
14
+ /**
15
+ * Undo/redo of the edits made through the table. Every write already goes through
16
+ * `editing.onCellChange` or `spreadsheet.fill.onFillCells`, and the column knows the
17
+ * value the cell held (`editable.getValue`), so undoing is writing that value back
18
+ * through the same handler: the host persists it like any other edit.
19
+ *
20
+ * - keystrokes on the same cell within a short window become one step;
21
+ * - a fill, a bulk edit or any writes fired in the same tick become one step;
22
+ * - a write that brings a cell back to the value before its last step (Escape on
23
+ * an editor) removes that step instead of stacking a second one.
24
+ */
25
+ export declare function useDataTableUndoHistory<T extends IDataTableRow>({ enabled, rows, columns, onCellChange, onFillCells, }: IUseDataTableUndoHistoryParams<T>): {
26
+ recordedOnCellChange: (row: T, columnId: string, value: unknown) => void;
27
+ recordedOnFillCells: (changes: Array<IDataTableFillChange<T>>) => void;
28
+ handleUndoShortcut: (event: KeyboardEvent<HTMLElement>) => IDataTableRestoredCell<T> | null;
29
+ };
30
+ export {};
@@ -0,0 +1,130 @@
1
+ import { useRef, useCallback } from "react";
2
+ import { isTextEntryTarget } from "./grid/useGridKeyboard.js";
3
+ const TYPING_MERGE_WINDOW_MS = 1e3;
4
+ const resolveUndoDirection = (event) => {
5
+ if (!(event.ctrlKey || event.metaKey) || event.altKey) return null;
6
+ const pressedKey = event.key.toLowerCase();
7
+ if (pressedKey === "z") return event.shiftKey ? "redo" : "undo";
8
+ if (pressedKey === "y" && event.ctrlKey && !event.shiftKey) return "redo";
9
+ return null;
10
+ };
11
+ const areCellValuesEqual = (left, right) => JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
12
+ function useDataTableUndoHistory({
13
+ enabled,
14
+ rows,
15
+ columns,
16
+ onCellChange,
17
+ onFillCells
18
+ }) {
19
+ const enabledRef = useRef(enabled);
20
+ enabledRef.current = enabled;
21
+ const rowsRef = useRef(rows);
22
+ rowsRef.current = rows;
23
+ const columnsRef = useRef(columns);
24
+ columnsRef.current = columns;
25
+ const onCellChangeRef = useRef(onCellChange);
26
+ onCellChangeRef.current = onCellChange;
27
+ const onFillCellsRef = useRef(onFillCells);
28
+ onFillCellsRef.current = onFillCells;
29
+ const undoStackRef = useRef([]);
30
+ const redoStackRef = useRef([]);
31
+ const sameTickEntryRef = useRef(null);
32
+ const readCellValue = useCallback(
33
+ (row, columnId) => (columnsRef.current ?? []).find((column) => column.id === columnId)?.editable?.getValue(row) ?? null,
34
+ []
35
+ );
36
+ const recordCellChanges = useCallback(
37
+ (changes) => {
38
+ if (!enabledRef.current) return;
39
+ const changedCells = (changes ?? []).map(({ row, columnId, value }) => ({
40
+ row,
41
+ columnId,
42
+ previousValue: readCellValue(row, columnId),
43
+ nextValue: value
44
+ })).filter((cell) => !areCellValuesEqual(cell.previousValue, cell.nextValue));
45
+ if (changedCells.length === 0) return;
46
+ redoStackRef.current = [];
47
+ const now = Date.now();
48
+ const sameTickEntry = sameTickEntryRef.current;
49
+ if (sameTickEntry) {
50
+ sameTickEntry.cells.push(...changedCells);
51
+ return;
52
+ }
53
+ const lastEntry = undoStackRef.current[undoStackRef.current.length - 1];
54
+ const lastCell = lastEntry?.cells.length === 1 ? lastEntry.cells[0] : void 0;
55
+ const onlyChangedCell = changedCells.length === 1 ? changedCells[0] : void 0;
56
+ if (lastEntry && lastCell && onlyChangedCell && lastCell.row.id === onlyChangedCell.row.id && lastCell.columnId === onlyChangedCell.columnId) {
57
+ if (areCellValuesEqual(onlyChangedCell.nextValue, lastCell.previousValue)) {
58
+ undoStackRef.current = undoStackRef.current.slice(0, -1);
59
+ return;
60
+ }
61
+ if (now - lastEntry.recordedAt < TYPING_MERGE_WINDOW_MS) {
62
+ lastCell.nextValue = onlyChangedCell.nextValue;
63
+ lastEntry.recordedAt = now;
64
+ return;
65
+ }
66
+ }
67
+ const entry = { cells: changedCells, recordedAt: now };
68
+ undoStackRef.current = [...undoStackRef.current, entry].slice(-100);
69
+ sameTickEntryRef.current = entry;
70
+ queueMicrotask(() => {
71
+ sameTickEntryRef.current = null;
72
+ });
73
+ },
74
+ [readCellValue]
75
+ );
76
+ const recordedOnCellChange = useCallback(
77
+ (row, columnId, value) => {
78
+ recordCellChanges([{ row, columnId, value }]);
79
+ onCellChangeRef.current?.(row, columnId, value);
80
+ },
81
+ [recordCellChanges]
82
+ );
83
+ const recordedOnFillCells = useCallback(
84
+ (changes) => {
85
+ recordCellChanges(changes);
86
+ onFillCellsRef.current?.(changes);
87
+ },
88
+ [recordCellChanges]
89
+ );
90
+ const replayLastEntry = useCallback((direction) => {
91
+ const sourceStackRef = direction === "undo" ? undoStackRef : redoStackRef;
92
+ const targetStackRef = direction === "undo" ? redoStackRef : undoStackRef;
93
+ const entry = sourceStackRef.current[sourceStackRef.current.length - 1];
94
+ if (!entry) return null;
95
+ const cellWrites = entry.cells.map((cell) => ({
96
+ row: (rowsRef.current ?? []).find((candidate) => candidate.id === cell.row.id) ?? cell.row,
97
+ columnId: cell.columnId,
98
+ value: direction === "undo" ? cell.previousValue : cell.nextValue
99
+ }));
100
+ if (cellWrites.length > 1 && onFillCellsRef.current) {
101
+ onFillCellsRef.current(cellWrites);
102
+ } else {
103
+ cellWrites.forEach((write) => onCellChangeRef.current?.(write.row, write.columnId, write.value));
104
+ }
105
+ sourceStackRef.current = sourceStackRef.current.slice(0, -1);
106
+ targetStackRef.current = [...targetStackRef.current, entry].slice(-100);
107
+ sameTickEntryRef.current = null;
108
+ const firstWrite = cellWrites[0];
109
+ return firstWrite ? { row: firstWrite.row, columnId: firstWrite.columnId } : null;
110
+ }, []);
111
+ const handleUndoShortcut = useCallback(
112
+ (event) => {
113
+ if (!enabledRef.current || event.defaultPrevented) return null;
114
+ if (!(event.target instanceof Node) || !event.currentTarget.contains(event.target)) return null;
115
+ const direction = resolveUndoDirection(event);
116
+ if (!direction || isTextEntryTarget(event.target)) return null;
117
+ const restoredCell = replayLastEntry(direction);
118
+ if (!restoredCell) return null;
119
+ event.preventDefault();
120
+ event.stopPropagation();
121
+ return restoredCell;
122
+ },
123
+ [replayLastEntry]
124
+ );
125
+ return { recordedOnCellChange, recordedOnFillCells, handleUndoShortcut };
126
+ }
127
+ export {
128
+ useDataTableUndoHistory
129
+ };
130
+ //# sourceMappingURL=useDataTableUndoHistory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDataTableUndoHistory.js","sources":["../../../../src/components/UI/DataTable/useDataTableUndoHistory.ts"],"sourcesContent":["import { type KeyboardEvent, useCallback, useRef } from 'react';\n\nimport { isTextEntryTarget } from './grid/useGridKeyboard';\nimport type { IDataTableColumnConfig, IDataTableFillChange, IDataTableRow } from './types';\n\nconst MAX_UNDO_ENTRIES = 100;\nconst TYPING_MERGE_WINDOW_MS = 1000;\n\ntype TDataTableUndoDirection = 'undo' | 'redo';\n\ninterface IDataTableUndoCell<T extends IDataTableRow> {\n row: T;\n columnId: string;\n previousValue: unknown;\n nextValue: unknown;\n}\n\ninterface IDataTableUndoEntry<T extends IDataTableRow> {\n cells: Array<IDataTableUndoCell<T>>;\n recordedAt: number;\n}\n\nexport interface IDataTableRestoredCell<T extends IDataTableRow> {\n row: T;\n columnId: string;\n}\n\ninterface IUseDataTableUndoHistoryParams<T extends IDataTableRow> {\n enabled: boolean;\n rows: Array<T>;\n columns: Array<IDataTableColumnConfig<T>>;\n onCellChange?: (row: T, columnId: string, value: unknown) => void;\n onFillCells?: (changes: Array<IDataTableFillChange<T>>) => void;\n}\n\nconst resolveUndoDirection = (event: KeyboardEvent): TDataTableUndoDirection | null => {\n if (!(event.ctrlKey || event.metaKey) || event.altKey) return null;\n const pressedKey = event.key.toLowerCase();\n if (pressedKey === 'z') return event.shiftKey ? 'redo' : 'undo';\n if (pressedKey === 'y' && event.ctrlKey && !event.shiftKey) return 'redo';\n return null;\n};\n\nconst areCellValuesEqual = (left: unknown, right: unknown): boolean =>\n JSON.stringify(left ?? null) === JSON.stringify(right ?? null);\n\n/**\n * Undo/redo of the edits made through the table. Every write already goes through\n * `editing.onCellChange` or `spreadsheet.fill.onFillCells`, and the column knows the\n * value the cell held (`editable.getValue`), so undoing is writing that value back\n * through the same handler: the host persists it like any other edit.\n *\n * - keystrokes on the same cell within a short window become one step;\n * - a fill, a bulk edit or any writes fired in the same tick become one step;\n * - a write that brings a cell back to the value before its last step (Escape on\n * an editor) removes that step instead of stacking a second one.\n */\nexport function useDataTableUndoHistory<T extends IDataTableRow>({\n enabled,\n rows,\n columns,\n onCellChange,\n onFillCells,\n}: IUseDataTableUndoHistoryParams<T>) {\n const enabledRef = useRef(enabled);\n enabledRef.current = enabled;\n const rowsRef = useRef(rows);\n rowsRef.current = rows;\n const columnsRef = useRef(columns);\n columnsRef.current = columns;\n const onCellChangeRef = useRef(onCellChange);\n onCellChangeRef.current = onCellChange;\n const onFillCellsRef = useRef(onFillCells);\n onFillCellsRef.current = onFillCells;\n\n const undoStackRef = useRef<Array<IDataTableUndoEntry<T>>>([]);\n const redoStackRef = useRef<Array<IDataTableUndoEntry<T>>>([]);\n const sameTickEntryRef = useRef<IDataTableUndoEntry<T> | null>(null);\n\n const readCellValue = useCallback(\n (row: T, columnId: string): unknown =>\n (columnsRef.current ?? []).find((column) => column.id === columnId)?.editable?.getValue(row) ?? null,\n [],\n );\n\n const recordCellChanges = useCallback(\n (changes: Array<IDataTableFillChange<T>>) => {\n if (!enabledRef.current) return;\n\n const changedCells = (changes ?? [])\n .map<IDataTableUndoCell<T>>(({ row, columnId, value }) => ({\n row,\n columnId,\n previousValue: readCellValue(row, columnId),\n nextValue: value,\n }))\n .filter((cell) => !areCellValuesEqual(cell.previousValue, cell.nextValue));\n if (changedCells.length === 0) return;\n\n redoStackRef.current = [];\n const now = Date.now();\n\n const sameTickEntry = sameTickEntryRef.current;\n if (sameTickEntry) {\n sameTickEntry.cells.push(...changedCells);\n return;\n }\n\n const lastEntry = undoStackRef.current[undoStackRef.current.length - 1];\n const lastCell = lastEntry?.cells.length === 1 ? lastEntry.cells[0] : undefined;\n const onlyChangedCell = changedCells.length === 1 ? changedCells[0] : undefined;\n\n if (\n lastEntry &&\n lastCell &&\n onlyChangedCell &&\n lastCell.row.id === onlyChangedCell.row.id &&\n lastCell.columnId === onlyChangedCell.columnId\n ) {\n if (areCellValuesEqual(onlyChangedCell.nextValue, lastCell.previousValue)) {\n undoStackRef.current = undoStackRef.current.slice(0, -1);\n return;\n }\n if (now - lastEntry.recordedAt < TYPING_MERGE_WINDOW_MS) {\n lastCell.nextValue = onlyChangedCell.nextValue;\n lastEntry.recordedAt = now;\n return;\n }\n }\n\n const entry: IDataTableUndoEntry<T> = { cells: changedCells, recordedAt: now };\n undoStackRef.current = [...undoStackRef.current, entry].slice(-MAX_UNDO_ENTRIES);\n sameTickEntryRef.current = entry;\n queueMicrotask(() => {\n sameTickEntryRef.current = null;\n });\n },\n [readCellValue],\n );\n\n const recordedOnCellChange = useCallback(\n (row: T, columnId: string, value: unknown) => {\n recordCellChanges([{ row, columnId, value }]);\n onCellChangeRef.current?.(row, columnId, value);\n },\n [recordCellChanges],\n );\n\n const recordedOnFillCells = useCallback(\n (changes: Array<IDataTableFillChange<T>>) => {\n recordCellChanges(changes);\n onFillCellsRef.current?.(changes);\n },\n [recordCellChanges],\n );\n\n const replayLastEntry = useCallback((direction: TDataTableUndoDirection): IDataTableRestoredCell<T> | null => {\n const sourceStackRef = direction === 'undo' ? undoStackRef : redoStackRef;\n const targetStackRef = direction === 'undo' ? redoStackRef : undoStackRef;\n const entry = sourceStackRef.current[sourceStackRef.current.length - 1];\n if (!entry) return null;\n\n const cellWrites = entry.cells.map<IDataTableFillChange<T>>((cell) => ({\n row: (rowsRef.current ?? []).find((candidate) => candidate.id === cell.row.id) ?? cell.row,\n columnId: cell.columnId,\n value: direction === 'undo' ? cell.previousValue : cell.nextValue,\n }));\n\n if (cellWrites.length > 1 && onFillCellsRef.current) {\n onFillCellsRef.current(cellWrites);\n } else {\n cellWrites.forEach((write) => onCellChangeRef.current?.(write.row, write.columnId, write.value));\n }\n\n sourceStackRef.current = sourceStackRef.current.slice(0, -1);\n targetStackRef.current = [...targetStackRef.current, entry].slice(-MAX_UNDO_ENTRIES);\n sameTickEntryRef.current = null;\n\n const firstWrite = cellWrites[0];\n return firstWrite ? { row: firstWrite.row, columnId: firstWrite.columnId } : null;\n }, []);\n\n /**\n * Handles Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z (or Ctrl+Y) pressed inside the table.\n * A focused text control keeps its own native undo, and with nothing to replay the\n * event is left untouched so an outer surface can still handle it.\n */\n const handleUndoShortcut = useCallback(\n (event: KeyboardEvent<HTMLElement>): IDataTableRestoredCell<T> | null => {\n if (!enabledRef.current || event.defaultPrevented) return null;\n if (!(event.target instanceof Node) || !event.currentTarget.contains(event.target)) return null;\n\n const direction = resolveUndoDirection(event);\n if (!direction || isTextEntryTarget(event.target)) return null;\n\n const restoredCell = replayLastEntry(direction);\n if (!restoredCell) return null;\n\n event.preventDefault();\n event.stopPropagation();\n return restoredCell;\n },\n [replayLastEntry],\n );\n\n return { recordedOnCellChange, recordedOnFillCells, handleUndoShortcut };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export type TIconFontState = 'loading' | 'ready' | 'unavailable';
2
+ export declare const ICON_FONT_FAMILY = "Material Symbols Outlined";
3
+ /**
4
+ * Starts the icon font download at boot and mirrors its state on `<html data-icon-font>`.
5
+ * base.css hides the ligature text while the state is `loading` or `unavailable`, so a
6
+ * blocked or failed font leaves empty glyph boxes instead of icon names spread over the
7
+ * layout. Resolves to null when there is no DOM or the host registered no icon face.
8
+ */
9
+ export declare const watchIconFont: () => Promise<TIconFontState | null>;
@@ -0,0 +1,37 @@
1
+ import { uiLogger } from "./uiLogger.js";
2
+ const ICON_FONT_FAMILY = "Material Symbols Outlined";
3
+ const ICON_FONT_STATE_ATTRIBUTE = "data-icon-font";
4
+ const normalizeFontFamily = (family) => family.replace(/["']/g, "").trim();
5
+ const findIconFontFaces = () => {
6
+ const iconFontFaces = [];
7
+ document.fonts.forEach((fontFace) => {
8
+ if (normalizeFontFamily(fontFace.family) === ICON_FONT_FAMILY) iconFontFaces.push(fontFace);
9
+ });
10
+ return iconFontFaces;
11
+ };
12
+ const watchIconFont = async () => {
13
+ if (typeof document === "undefined" || !document.fonts) return null;
14
+ const iconFontFaces = findIconFontFaces();
15
+ if (iconFontFaces.length === 0) return null;
16
+ const rootElement = document.documentElement;
17
+ const isAlreadyLoaded = iconFontFaces.every((fontFace) => fontFace.status === "loaded");
18
+ if (isAlreadyLoaded) {
19
+ rootElement.setAttribute(ICON_FONT_STATE_ATTRIBUTE, "ready");
20
+ return "ready";
21
+ }
22
+ rootElement.setAttribute(ICON_FONT_STATE_ATTRIBUTE, "loading");
23
+ try {
24
+ await Promise.all(iconFontFaces.map((fontFace) => fontFace.load()));
25
+ rootElement.setAttribute(ICON_FONT_STATE_ATTRIBUTE, "ready");
26
+ return "ready";
27
+ } catch (error) {
28
+ rootElement.setAttribute(ICON_FONT_STATE_ATTRIBUTE, "unavailable");
29
+ uiLogger.warn(`[iconFont] ${ICON_FONT_FAMILY} could not be loaded; icons are hidden`, error);
30
+ return "unavailable";
31
+ }
32
+ };
33
+ export {
34
+ ICON_FONT_FAMILY,
35
+ watchIconFont
36
+ };
37
+ //# sourceMappingURL=iconFont.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iconFont.js","sources":["../../src/helpers/iconFont.ts"],"sourcesContent":["import { uiLogger } from './uiLogger';\n\nexport type TIconFontState = 'loading' | 'ready' | 'unavailable';\n\nexport const ICON_FONT_FAMILY = 'Material Symbols Outlined';\n\nconst ICON_FONT_STATE_ATTRIBUTE = 'data-icon-font';\n\nconst normalizeFontFamily = (family: string): string => family.replace(/[\"']/g, '').trim();\n\nconst findIconFontFaces = (): Array<FontFace> => {\n const iconFontFaces: Array<FontFace> = [];\n document.fonts.forEach((fontFace) => {\n if (normalizeFontFamily(fontFace.family) === ICON_FONT_FAMILY) iconFontFaces.push(fontFace);\n });\n return iconFontFaces;\n};\n\n/**\n * Starts the icon font download at boot and mirrors its state on `<html data-icon-font>`.\n * base.css hides the ligature text while the state is `loading` or `unavailable`, so a\n * blocked or failed font leaves empty glyph boxes instead of icon names spread over the\n * layout. Resolves to null when there is no DOM or the host registered no icon face.\n */\nexport const watchIconFont = async (): Promise<TIconFontState | null> => {\n if (typeof document === 'undefined' || !document.fonts) return null;\n\n const iconFontFaces = findIconFontFaces();\n if (iconFontFaces.length === 0) return null;\n\n const rootElement = document.documentElement;\n const isAlreadyLoaded = iconFontFaces.every((fontFace) => fontFace.status === 'loaded');\n if (isAlreadyLoaded) {\n rootElement.setAttribute(ICON_FONT_STATE_ATTRIBUTE, 'ready');\n return 'ready';\n }\n\n rootElement.setAttribute(ICON_FONT_STATE_ATTRIBUTE, 'loading');\n try {\n await Promise.all(iconFontFaces.map((fontFace) => fontFace.load()));\n rootElement.setAttribute(ICON_FONT_STATE_ATTRIBUTE, 'ready');\n return 'ready';\n } catch (error) {\n rootElement.setAttribute(ICON_FONT_STATE_ATTRIBUTE, 'unavailable');\n uiLogger.warn(`[iconFont] ${ICON_FONT_FAMILY} could not be loaded; icons are hidden`, error);\n return 'unavailable';\n }\n};\n"],"names":[],"mappings":";AAIO,MAAM,mBAAmB;AAEhC,MAAM,4BAA4B;AAElC,MAAM,sBAAsB,CAAC,WAA2B,OAAO,QAAQ,SAAS,EAAE,EAAE,KAAA;AAEpF,MAAM,oBAAoB,MAAuB;AAC/C,QAAM,gBAAiC,CAAA;AACvC,WAAS,MAAM,QAAQ,CAAC,aAAa;AACnC,QAAI,oBAAoB,SAAS,MAAM,MAAM,iBAAkB,eAAc,KAAK,QAAQ;AAAA,EAC5F,CAAC;AACD,SAAO;AACT;AAQO,MAAM,gBAAgB,YAA4C;AACvE,MAAI,OAAO,aAAa,eAAe,CAAC,SAAS,MAAO,QAAO;AAE/D,QAAM,gBAAgB,kBAAA;AACtB,MAAI,cAAc,WAAW,EAAG,QAAO;AAEvC,QAAM,cAAc,SAAS;AAC7B,QAAM,kBAAkB,cAAc,MAAM,CAAC,aAAa,SAAS,WAAW,QAAQ;AACtF,MAAI,iBAAiB;AACnB,gBAAY,aAAa,2BAA2B,OAAO;AAC3D,WAAO;AAAA,EACT;AAEA,cAAY,aAAa,2BAA2B,SAAS;AAC7D,MAAI;AACF,UAAM,QAAQ,IAAI,cAAc,IAAI,CAAC,aAAa,SAAS,KAAA,CAAM,CAAC;AAClE,gBAAY,aAAa,2BAA2B,OAAO;AAC3D,WAAO;AAAA,EACT,SAAS,OAAO;AACd,gBAAY,aAAa,2BAA2B,aAAa;AACjE,aAAS,KAAK,cAAc,gBAAgB,0CAA0C,KAAK;AAC3F,WAAO;AAAA,EACT;AACF;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umodeapporg/ui",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "dist",
11
11
  "src/assets/styles",
12
+ "src/assets/fonts",
12
13
  "tailwind.preset.js",
13
14
  "tailwind-plugins"
14
15
  ],
@@ -0,0 +1,92 @@
1
+ Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+ -----------------------------------------------------------
8
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
9
+ -----------------------------------------------------------
10
+
11
+ PREAMBLE
12
+ The goals of the Open Font License (OFL) are to stimulate worldwide
13
+ development of collaborative font projects, to support the font creation
14
+ efforts of academic and linguistic communities, and to provide a free and
15
+ open framework in which fonts may be shared and improved in partnership
16
+ with others.
17
+
18
+ The OFL allows the licensed fonts to be used, studied, modified and
19
+ redistributed freely as long as they are not sold by themselves. The
20
+ fonts, including any derivative works, can be bundled, embedded,
21
+ redistributed and/or sold with any software provided that any reserved
22
+ names are not used by derivative works. The fonts and derivatives,
23
+ however, cannot be released under any other type of license. The
24
+ requirement for fonts to remain under this license does not apply
25
+ to any document created using the fonts or their derivatives.
26
+
27
+ DEFINITIONS
28
+ "Font Software" refers to the set of files released by the Copyright
29
+ Holder(s) under this license and clearly marked as such. This may
30
+ include source files, build scripts and documentation.
31
+
32
+ "Reserved Font Name" refers to any names specified as such after the
33
+ copyright statement(s).
34
+
35
+ "Original Version" refers to the collection of Font Software components as
36
+ distributed by the Copyright Holder(s).
37
+
38
+ "Modified Version" refers to any derivative made by adding to, deleting,
39
+ or substituting -- in part or in whole -- any of the components of the
40
+ Original Version, by changing formats or by porting the Font Software to a
41
+ new environment.
42
+
43
+ "Author" refers to any designer, engineer, programmer, technical
44
+ writer or other person who contributed to the Font Software.
45
+
46
+ PERMISSION AND CONDITIONS
47
+ Permission is hereby granted, free of charge, to any person obtaining
48
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
49
+ redistribute, and sell modified and unmodified copies of the Font
50
+ Software, subject to the following conditions:
51
+
52
+ 1) Neither the Font Software nor any of its individual components,
53
+ in Original or Modified Versions, may be sold by itself.
54
+
55
+ 2) Original or Modified Versions of the Font Software may be bundled,
56
+ redistributed and/or sold with any software, provided that each copy
57
+ contains the above copyright notice and this license. These can be
58
+ included either as stand-alone text files, human-readable headers or
59
+ in the appropriate machine-readable metadata fields within text or
60
+ binary files as long as those fields can be easily viewed by the user.
61
+
62
+ 3) No Modified Version of the Font Software may use the Reserved Font
63
+ Name(s) unless explicit written permission is granted by the corresponding
64
+ Copyright Holder. This restriction only applies to the primary font name as
65
+ presented to the users.
66
+
67
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
68
+ Software shall not be used to promote, endorse or advertise any
69
+ Modified Version, except to acknowledge the contribution(s) of the
70
+ Copyright Holder(s) and the Author(s) or with their explicit written
71
+ permission.
72
+
73
+ 5) The Font Software, modified or unmodified, in part or in whole,
74
+ must be distributed entirely under this license, and must not be
75
+ distributed under any other license. The requirement for fonts to
76
+ remain under this license does not apply to any document created
77
+ using the Font Software.
78
+
79
+ TERMINATION
80
+ This license becomes null and void if any of the above conditions are
81
+ not met.
82
+
83
+ DISCLAIMER
84
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
85
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
86
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
87
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
88
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
89
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
90
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
91
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
92
+ OTHER DEALINGS IN THE FONT SOFTWARE.