@umodeapporg/ui 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -87,6 +87,8 @@ const DataTableEditableCell = ({
87
87
  const error = editable.validate?.(rawValue, row) ?? null;
88
88
  const arrayValue = Array.isArray(rawValue) ? rawValue.map(String) : [];
89
89
  const stringValue = rawValue == null || Array.isArray(rawValue) ? "" : String(rawValue);
90
+ const editorOptions = editable.getOptions?.(row) ?? editable.options;
91
+ const selectOptions = Array.isArray(editorOptions) ? editorOptions : [];
90
92
  const decimalValue = editable.type === "number" && typeof rawValue === "number" ? stringValue.replace(".", resolveDecimalSeparator(i18n.language)) : stringValue;
91
93
  const renderEditor = () => {
92
94
  if (editable.renderEditor) {
@@ -102,7 +104,7 @@ const DataTableEditableCell = ({
102
104
  ControlledSelect,
103
105
  {
104
106
  className: "w-full [&_.controlled-select-container]:!min-w-0 [&_.controlled-select-input]:!min-w-0",
105
- options: Array.isArray(editable.options) ? editable.options : [],
107
+ options: selectOptions,
106
108
  selectedValue: arrayValue,
107
109
  onChangeHook: (selected) => onCellChange(row, column.id, Array.isArray(selected) ? selected : [selected]),
108
110
  multiple: true,
@@ -123,7 +125,7 @@ const DataTableEditableCell = ({
123
125
  ControlledSelect,
124
126
  {
125
127
  className: "w-full [&_.controlled-select-container]:!min-w-0 [&_.controlled-select-input]:!min-w-0",
126
- options: Array.isArray(editable.options) ? editable.options : [],
128
+ options: selectOptions,
127
129
  selectedValue: stringValue,
128
130
  onChangeHook: (selected) => onCellChange(row, column.id, selected),
129
131
  multiple: false,
@@ -1 +1 @@
1
- {"version":3,"file":"DataTableEditableCell.js","sources":["../../../../../src/components/UI/DataTable/cells/DataTableEditableCell.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport DatePicker from '@/components/DateTime/DatePicker';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport { ControlledSelect } from '@/components/Form/ControlledSelect';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { formatIsoDate } from '@/helpers/dateHelpers';\nimport { useHoverCapable } from '@/hooks/useHoverCapable';\nimport { useInlineEditor } from '@/hooks/useInlineEditor';\n\nimport { useGridCellState, useGridStoreApi } from '../grid/GridProvider';\nimport { GRID_CELL_EDITING } from '../grid/gridTypes';\nimport { useGridEditMode } from '../grid/useGridInstance';\nimport type { IDataTableColumnConfig, IDataTableRow } from '../types';\nimport { buildEditableCellKey, EDITABLE_CELL_ATTR, useDataTableCellEditing } from '../useCellEditingState';\n\n/**\n * Marks the typeable editable inputs so `keyboardNav` can walk them in DOM order.\n * Only text/number inputs opt in; date and select cells drive their own popovers\n * and fall back to native Tab traversal.\n */\nconst EDITABLE_INPUT_ATTR = 'data-datatable-editable-input';\n\ntype TEditableValue = string | number | Array<string> | null;\n\n/** Multi-select devolve um array novo a cada leitura, então `===` nunca casaria. */\nconst isSameEditableValue = (current: TEditableValue, previous: TEditableValue): boolean => {\n if (Array.isArray(current) || Array.isArray(previous)) {\n if (!Array.isArray(current) || !Array.isArray(previous)) return false;\n return current.length === previous.length && current.every((entry, index) => entry === previous[index]);\n }\n\n return current === previous;\n};\n\n/** `,` for pt-BR, `.` for en: derived from the locale itself, no language table. */\nconst resolveDecimalSeparator = (language: string): string => {\n const separator = (1.1).toLocaleString(language || 'pt-BR').replace(/\\d/g, '');\n return separator || '.';\n};\n\nconst focusNextEditableInput = (current: HTMLElement): void => {\n const inputs = Array.from(document.querySelectorAll<HTMLElement>(`[${EDITABLE_INPUT_ATTR}]`));\n const index = inputs.indexOf(current);\n if (index === -1) return;\n inputs[index + 1]?.focus();\n};\n\ninterface IDataTableEditableCellProps<T extends IDataTableRow> {\n column: IDataTableColumnConfig<T>;\n row: T;\n /** TanStack row id: present only inside a spreadsheet-enabled table. */\n gridRowId?: string;\n onCellChange: (row: T, columnId: string, value: unknown) => void;\n /** When true, pressing Enter moves focus to the next editable input (DOM order). */\n keyboardNav?: boolean;\n /** When true, the cell shows the value as text until it is clicked. */\n clickToEdit?: boolean;\n}\n\n/**\n * Controlled inline editor for a single cell. Reads the current value from the\n * host-owned row via `column.editable.getValue` and reports every edit through\n * `onCellChange`; the host re-renders with the updated row. Validation is derived\n * from the current value on each render, so no local state is kept.\n *\n * With `clickToEdit`, the cell renders the value as text and only swaps in the editor\n * once clicked, no editor is mounted before that.\n */\nconst DataTableEditableCell = <T extends IDataTableRow>({\n column,\n row,\n gridRowId,\n onCellChange,\n keyboardNav = false,\n clickToEdit = false,\n}: IDataTableEditableCellProps<T>) => {\n const { t, i18n } = useTranslation('ui');\n const hoverCapable = useHoverCapable();\n const cellEditing = useDataTableCellEditing();\n const gridStore = useGridStoreApi();\n const gridCellState = useGridCellState(gridRowId ?? '', column.id);\n const cellKey = buildEditableCellKey(row.id, column.id);\n\n // Inside a spreadsheet-enabled table the grid store owns the editing state, so\n // the legacy click-to-edit context and its document listener stay out of the way.\n const isGridEditModeOn = useGridEditMode(gridStore);\n const hasGrid = !!gridStore && !!gridRowId;\n const isGridCell = hasGrid && isGridEditModeOn;\n const isEditing = isGridCell\n ? (gridCellState & GRID_CELL_EDITING) !== 0\n : clickToEdit\n ? cellEditing?.editingCellKey === cellKey\n : true;\n const showsReadView = isGridCell || clickToEdit;\n\n const { containerRef, handleKeyDown } = useInlineEditor(\n Boolean(!isGridCell && clickToEdit && isEditing),\n () => cellEditing?.stopEditing(),\n (direction) => cellEditing?.navigate(cellKey, direction),\n );\n\n const editable = column.editable;\n const valueBeforeEditRef = useRef<string | number | Array<string> | null>(null);\n const wasEditingRef = useRef(false);\n\n // The cell is controlled by the value the host ALREADY parsed, so a half-typed\n // decimal is erased on the same keystroke: `1,` parses to the number 1 and comes\n // back as `\"1\"`, taking with it the separator just typed. The draft holds the exact\n // text while the field is focused; the host still receives every keystroke and\n // remains the source of truth.\n const [draftText, setDraftText] = useState<string | null>(null);\n\n useEffect(() => {\n if (!isEditing) setDraftText(null);\n }, [isEditing]);\n\n // Escape restores what the cell held when the editor opened. The editing model is\n // controlled (every keystroke already reached the host), so cancelling means\n // writing the snapshot back, not dropping local state.\n useEffect(() => {\n if (!isGridCell || !editable) return;\n\n if (isEditing) {\n if (wasEditingRef.current) return;\n wasEditingRef.current = true;\n valueBeforeEditRef.current = editable.getValue(row);\n\n // The key that opened the editor IS the new value, so the first keystroke is not lost.\n const typedSeed = gridStore?.getState().editingSeed;\n if (typedSeed == null) return;\n setDraftText(typedSeed);\n onCellChange(row, column.id, typedSeed);\n return;\n }\n\n if (!wasEditingRef.current) return;\n wasEditingRef.current = false;\n\n if (gridStore?.getState().editingOutcome !== 'cancel') return;\n if (isSameEditableValue(editable.getValue(row), valueBeforeEditRef.current)) return;\n\n onCellChange(row, column.id, valueBeforeEditRef.current);\n }, [isEditing, isGridCell, editable, gridStore, onCellChange, row, column.id]);\n\n if (!editable) return null;\n\n const rawValue = editable.getValue(row);\n const disabled = editable.disabled?.(row) ?? false;\n const error = editable.validate?.(rawValue, row) ?? null;\n const arrayValue = Array.isArray(rawValue) ? rawValue.map(String) : [];\n const stringValue = rawValue == null || Array.isArray(rawValue) ? '' : String(rawValue);\n\n // `String()` on a number always writes a dot, so a pt-BR user would get back a\n // separator that is not theirs. Hosts that keep the raw text (the edit buffer) are\n // untouched: only an actual number reaches this branch.\n const decimalValue =\n editable.type === 'number' && typeof rawValue === 'number'\n ? stringValue.replace('.', resolveDecimalSeparator(i18n.language))\n : stringValue;\n\n const renderEditor = () => {\n if (editable.renderEditor) {\n return editable.renderEditor({\n value: rawValue,\n onChange: (nextValue) => onCellChange(row, column.id, nextValue),\n disabled,\n label: column.header,\n });\n }\n\n if (editable.type === 'multiselect') {\n return (\n <ControlledSelect\n className=\"w-full [&_.controlled-select-container]:!min-w-0 [&_.controlled-select-input]:!min-w-0\"\n options={Array.isArray(editable.options) ? editable.options : []}\n selectedValue={arrayValue}\n onChangeHook={(selected) => onCellChange(row, column.id, Array.isArray(selected) ? selected : [selected])}\n multiple\n searchable\n clearable\n disabled={disabled}\n error={!!error}\n />\n );\n }\n\n if (editable.type === 'select') {\n return (\n // `ControlledSelect` ships hard min-widths (`min-w-52` on the field and the MT\n // container's `min-w-[200px]`) that overflow narrow table cells and spill into\n // the neighbouring column; force both to shrink so the editor fits its column\n // like the text/number editors do.\n <ControlledSelect\n className=\"w-full [&_.controlled-select-container]:!min-w-0 [&_.controlled-select-input]:!min-w-0\"\n options={Array.isArray(editable.options) ? editable.options : []}\n selectedValue={stringValue}\n onChangeHook={(selected) => onCellChange(row, column.id, selected)}\n multiple={false}\n searchable\n clearable\n disabled={disabled}\n error={!!error}\n />\n );\n }\n\n if (editable.type === 'date') {\n const parsed = stringValue ? new Date(stringValue) : undefined;\n const startValue = parsed && !Number.isNaN(parsed.getTime()) ? parsed : undefined;\n\n return (\n <DatePicker\n startValue={startValue}\n onUpdate={(value) => onCellChange(row, column.id, value ?? null)}\n disabled={disabled}\n />\n );\n }\n\n // `number` renders a text input with a decimal input mode rather than\n // `type=\"number\"`, so pt-BR comma decimals are not blocked by the browser; the\n // host parses the raw string it receives in `onCellChange`.\n return (\n <ControlledInput\n value={draftText ?? decimalValue}\n onChange={(event) => {\n setDraftText(event.target.value);\n onCellChange(row, column.id, event.target.value);\n }}\n onBlur={() => setDraftText(null)}\n onKeyDown={\n keyboardNav && !isGridCell\n ? (event) => {\n if (event.key === 'Enter') {\n event.preventDefault();\n focusNextEditableInput(event.currentTarget);\n }\n }\n : undefined\n }\n inputMode={editable.type === 'number' ? 'decimal' : undefined}\n disabled={disabled}\n error={!!error}\n errorMessage={error ?? undefined}\n aria-label={column.header}\n {...{ [EDITABLE_INPUT_ATTR]: '' }}\n // The Material-Tailwind input container ships a hard `min-w-[200px]` that\n // overflows narrow table cells; force it to shrink so the editor fits its\n // column (the cell padding then provides the gap between adjacent inputs).\n containerClassName=\"min-w-0\"\n />\n );\n };\n\n if (!hasGrid && (!showsReadView || !cellEditing)) return renderEditor();\n\n const resolveOptionLabel = (optionValue: string): string =>\n (editable.options ?? []).find((option) => String(option.value) === optionValue)?.label ?? optionValue;\n\n const resolveDisplayText = (): string => {\n if (editable.displayValue) return editable.displayValue(row);\n if (editable.type === 'multiselect') return arrayValue.map(resolveOptionLabel).join(', ');\n if (stringValue === '') return '';\n if (editable.type === 'select') return resolveOptionLabel(stringValue);\n if (editable.type === 'date') return formatIsoDate(stringValue);\n return decimalValue;\n };\n\n const displayText = resolveDisplayText();\n\n // Uma coluna com renderização própria (chip de etapa, status, thumbnail) mantém\n // esse visual no modo de leitura: só o editor aberto troca a célula por um input.\n const displayContent =\n !editable.displayValue && column.render ? (\n column.render(row)\n ) : (\n <span className={classNames('min-w-0 flex-1 truncate', displayText === '' && 'text-foreground-faint')}>\n {displayText === '' ? '-' : displayText}\n </span>\n );\n\n // Com o grid montado mas o modo de edição desligado, a célula é só o valor: sem\n // lápis, sem realce e sem clique, para não prometer uma edição que não abre.\n const showsEditAffordance = !disabled && (!hasGrid || isGridEditModeOn);\n\n const readCell = (\n <div\n className={classNames(\n // A mesma altura do editor (h-10), para a linha não pular quando a célula é aberta.\n 'flex min-h-10 w-full items-center gap-1 rounded-md border border-transparent px-2 py-1.5 text-sm',\n disabled && 'text-foreground-faint',\n !disabled && !showsEditAffordance && 'text-foreground',\n showsEditAffordance &&\n 'group cursor-pointer text-foreground transition-colors hover:border-brand-border hover:bg-brand-soft/50',\n error && 'border-danger-border',\n )}\n >\n <div className=\"min-w-0 flex-1 truncate\">{displayContent}</div>\n {showsEditAffordance && (\n // On touch there is no hover, so a hover-only pencil means the cell never\n // advertises that it can be edited. Keep it visible (dimmed) there instead.\n <MaterialSymbol\n icon=\"edit\"\n size={13}\n className={classNames(\n 'shrink-0 text-brand-emphasis transition-opacity',\n hoverCapable ? 'opacity-0 group-hover:opacity-100' : 'opacity-60',\n )}\n />\n )}\n </div>\n );\n\n // Fora do modo de edição a tabela é de leitura: nenhum editor fica montado.\n if (hasGrid && !isGridEditModeOn) return readCell;\n\n // The `<td>` owns the gestures that open the editor: a click, Enter, F2 or typing.\n if (isGridCell) return isEditing ? renderEditor() : readCell;\n\n if (!cellEditing) return renderEditor();\n\n return (\n <div ref={containerRef} onKeyDown={handleKeyDown} {...{ [EDITABLE_CELL_ATTR]: cellKey }}>\n {isEditing ? (\n renderEditor()\n ) : disabled ? (\n readCell\n ) : (\n <ControlledTooltip content={t('dataTable.editCell')}>\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => cellEditing.startEditing(cellKey)}\n onKeyDown={(event) => {\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n cellEditing.startEditing(cellKey);\n }}\n className=\"rounded-md focus:outline-none focus-visible:ring-1 focus-visible:ring-brand-border\"\n >\n {readCell}\n </div>\n </ControlledTooltip>\n )}\n </div>\n );\n};\n\nexport default DataTableEditableCell;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DataTableEditableCell.js","sources":["../../../../../src/components/UI/DataTable/cells/DataTableEditableCell.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport DatePicker from '@/components/DateTime/DatePicker';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport { ControlledSelect } from '@/components/Form/ControlledSelect';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { formatIsoDate } from '@/helpers/dateHelpers';\nimport { useHoverCapable } from '@/hooks/useHoverCapable';\nimport { useInlineEditor } from '@/hooks/useInlineEditor';\n\nimport { useGridCellState, useGridStoreApi } from '../grid/GridProvider';\nimport { GRID_CELL_EDITING } from '../grid/gridTypes';\nimport { useGridEditMode } from '../grid/useGridInstance';\nimport type { IDataTableColumnConfig, IDataTableRow } from '../types';\nimport { buildEditableCellKey, EDITABLE_CELL_ATTR, useDataTableCellEditing } from '../useCellEditingState';\n\n/**\n * Marks the typeable editable inputs so `keyboardNav` can walk them in DOM order.\n * Only text/number inputs opt in; date and select cells drive their own popovers\n * and fall back to native Tab traversal.\n */\nconst EDITABLE_INPUT_ATTR = 'data-datatable-editable-input';\n\ntype TEditableValue = string | number | Array<string> | null;\n\n/** Multi-select devolve um array novo a cada leitura, então `===` nunca casaria. */\nconst isSameEditableValue = (current: TEditableValue, previous: TEditableValue): boolean => {\n if (Array.isArray(current) || Array.isArray(previous)) {\n if (!Array.isArray(current) || !Array.isArray(previous)) return false;\n return current.length === previous.length && current.every((entry, index) => entry === previous[index]);\n }\n\n return current === previous;\n};\n\n/** `,` for pt-BR, `.` for en: derived from the locale itself, no language table. */\nconst resolveDecimalSeparator = (language: string): string => {\n const separator = (1.1).toLocaleString(language || 'pt-BR').replace(/\\d/g, '');\n return separator || '.';\n};\n\nconst focusNextEditableInput = (current: HTMLElement): void => {\n const inputs = Array.from(document.querySelectorAll<HTMLElement>(`[${EDITABLE_INPUT_ATTR}]`));\n const index = inputs.indexOf(current);\n if (index === -1) return;\n inputs[index + 1]?.focus();\n};\n\ninterface IDataTableEditableCellProps<T extends IDataTableRow> {\n column: IDataTableColumnConfig<T>;\n row: T;\n /** TanStack row id: present only inside a spreadsheet-enabled table. */\n gridRowId?: string;\n onCellChange: (row: T, columnId: string, value: unknown) => void;\n /** When true, pressing Enter moves focus to the next editable input (DOM order). */\n keyboardNav?: boolean;\n /** When true, the cell shows the value as text until it is clicked. */\n clickToEdit?: boolean;\n}\n\n/**\n * Controlled inline editor for a single cell. Reads the current value from the\n * host-owned row via `column.editable.getValue` and reports every edit through\n * `onCellChange`; the host re-renders with the updated row. Validation is derived\n * from the current value on each render, so no local state is kept.\n *\n * With `clickToEdit`, the cell renders the value as text and only swaps in the editor\n * once clicked, no editor is mounted before that.\n */\nconst DataTableEditableCell = <T extends IDataTableRow>({\n column,\n row,\n gridRowId,\n onCellChange,\n keyboardNav = false,\n clickToEdit = false,\n}: IDataTableEditableCellProps<T>) => {\n const { t, i18n } = useTranslation('ui');\n const hoverCapable = useHoverCapable();\n const cellEditing = useDataTableCellEditing();\n const gridStore = useGridStoreApi();\n const gridCellState = useGridCellState(gridRowId ?? '', column.id);\n const cellKey = buildEditableCellKey(row.id, column.id);\n\n // Inside a spreadsheet-enabled table the grid store owns the editing state, so\n // the legacy click-to-edit context and its document listener stay out of the way.\n const isGridEditModeOn = useGridEditMode(gridStore);\n const hasGrid = !!gridStore && !!gridRowId;\n const isGridCell = hasGrid && isGridEditModeOn;\n const isEditing = isGridCell\n ? (gridCellState & GRID_CELL_EDITING) !== 0\n : clickToEdit\n ? cellEditing?.editingCellKey === cellKey\n : true;\n const showsReadView = isGridCell || clickToEdit;\n\n const { containerRef, handleKeyDown } = useInlineEditor(\n Boolean(!isGridCell && clickToEdit && isEditing),\n () => cellEditing?.stopEditing(),\n (direction) => cellEditing?.navigate(cellKey, direction),\n );\n\n const editable = column.editable;\n const valueBeforeEditRef = useRef<string | number | Array<string> | null>(null);\n const wasEditingRef = useRef(false);\n\n // The cell is controlled by the value the host ALREADY parsed, so a half-typed\n // decimal is erased on the same keystroke: `1,` parses to the number 1 and comes\n // back as `\"1\"`, taking with it the separator just typed. The draft holds the exact\n // text while the field is focused; the host still receives every keystroke and\n // remains the source of truth.\n const [draftText, setDraftText] = useState<string | null>(null);\n\n useEffect(() => {\n if (!isEditing) setDraftText(null);\n }, [isEditing]);\n\n // Escape restores what the cell held when the editor opened. The editing model is\n // controlled (every keystroke already reached the host), so cancelling means\n // writing the snapshot back, not dropping local state.\n useEffect(() => {\n if (!isGridCell || !editable) return;\n\n if (isEditing) {\n if (wasEditingRef.current) return;\n wasEditingRef.current = true;\n valueBeforeEditRef.current = editable.getValue(row);\n\n // The key that opened the editor IS the new value, so the first keystroke is not lost.\n const typedSeed = gridStore?.getState().editingSeed;\n if (typedSeed == null) return;\n setDraftText(typedSeed);\n onCellChange(row, column.id, typedSeed);\n return;\n }\n\n if (!wasEditingRef.current) return;\n wasEditingRef.current = false;\n\n if (gridStore?.getState().editingOutcome !== 'cancel') return;\n if (isSameEditableValue(editable.getValue(row), valueBeforeEditRef.current)) return;\n\n onCellChange(row, column.id, valueBeforeEditRef.current);\n }, [isEditing, isGridCell, editable, gridStore, onCellChange, row, column.id]);\n\n if (!editable) return null;\n\n const rawValue = editable.getValue(row);\n const disabled = editable.disabled?.(row) ?? false;\n const error = editable.validate?.(rawValue, row) ?? null;\n const arrayValue = Array.isArray(rawValue) ? rawValue.map(String) : [];\n const stringValue = rawValue == null || Array.isArray(rawValue) ? '' : String(rawValue);\n const editorOptions = editable.getOptions?.(row) ?? editable.options;\n const selectOptions = Array.isArray(editorOptions) ? editorOptions : [];\n\n // `String()` on a number always writes a dot, so a pt-BR user would get back a\n // separator that is not theirs. Hosts that keep the raw text (the edit buffer) are\n // untouched: only an actual number reaches this branch.\n const decimalValue =\n editable.type === 'number' && typeof rawValue === 'number'\n ? stringValue.replace('.', resolveDecimalSeparator(i18n.language))\n : stringValue;\n\n const renderEditor = () => {\n if (editable.renderEditor) {\n return editable.renderEditor({\n value: rawValue,\n onChange: (nextValue) => onCellChange(row, column.id, nextValue),\n disabled,\n label: column.header,\n });\n }\n\n if (editable.type === 'multiselect') {\n return (\n <ControlledSelect\n className=\"w-full [&_.controlled-select-container]:!min-w-0 [&_.controlled-select-input]:!min-w-0\"\n options={selectOptions}\n selectedValue={arrayValue}\n onChangeHook={(selected) => onCellChange(row, column.id, Array.isArray(selected) ? selected : [selected])}\n multiple\n searchable\n clearable\n disabled={disabled}\n error={!!error}\n />\n );\n }\n\n if (editable.type === 'select') {\n return (\n // `ControlledSelect` ships hard min-widths (`min-w-52` on the field and the MT\n // container's `min-w-[200px]`) that overflow narrow table cells and spill into\n // the neighbouring column; force both to shrink so the editor fits its column\n // like the text/number editors do.\n <ControlledSelect\n className=\"w-full [&_.controlled-select-container]:!min-w-0 [&_.controlled-select-input]:!min-w-0\"\n options={selectOptions}\n selectedValue={stringValue}\n onChangeHook={(selected) => onCellChange(row, column.id, selected)}\n multiple={false}\n searchable\n clearable\n disabled={disabled}\n error={!!error}\n />\n );\n }\n\n if (editable.type === 'date') {\n const parsed = stringValue ? new Date(stringValue) : undefined;\n const startValue = parsed && !Number.isNaN(parsed.getTime()) ? parsed : undefined;\n\n return (\n <DatePicker\n startValue={startValue}\n onUpdate={(value) => onCellChange(row, column.id, value ?? null)}\n disabled={disabled}\n />\n );\n }\n\n // `number` renders a text input with a decimal input mode rather than\n // `type=\"number\"`, so pt-BR comma decimals are not blocked by the browser; the\n // host parses the raw string it receives in `onCellChange`.\n return (\n <ControlledInput\n value={draftText ?? decimalValue}\n onChange={(event) => {\n setDraftText(event.target.value);\n onCellChange(row, column.id, event.target.value);\n }}\n onBlur={() => setDraftText(null)}\n onKeyDown={\n keyboardNav && !isGridCell\n ? (event) => {\n if (event.key === 'Enter') {\n event.preventDefault();\n focusNextEditableInput(event.currentTarget);\n }\n }\n : undefined\n }\n inputMode={editable.type === 'number' ? 'decimal' : undefined}\n disabled={disabled}\n error={!!error}\n errorMessage={error ?? undefined}\n aria-label={column.header}\n {...{ [EDITABLE_INPUT_ATTR]: '' }}\n // The Material-Tailwind input container ships a hard `min-w-[200px]` that\n // overflows narrow table cells; force it to shrink so the editor fits its\n // column (the cell padding then provides the gap between adjacent inputs).\n containerClassName=\"min-w-0\"\n />\n );\n };\n\n if (!hasGrid && (!showsReadView || !cellEditing)) return renderEditor();\n\n const resolveOptionLabel = (optionValue: string): string =>\n (editable.options ?? []).find((option) => String(option.value) === optionValue)?.label ?? optionValue;\n\n const resolveDisplayText = (): string => {\n if (editable.displayValue) return editable.displayValue(row);\n if (editable.type === 'multiselect') return arrayValue.map(resolveOptionLabel).join(', ');\n if (stringValue === '') return '';\n if (editable.type === 'select') return resolveOptionLabel(stringValue);\n if (editable.type === 'date') return formatIsoDate(stringValue);\n return decimalValue;\n };\n\n const displayText = resolveDisplayText();\n\n // Uma coluna com renderização própria (chip de etapa, status, thumbnail) mantém\n // esse visual no modo de leitura: só o editor aberto troca a célula por um input.\n const displayContent =\n !editable.displayValue && column.render ? (\n column.render(row)\n ) : (\n <span className={classNames('min-w-0 flex-1 truncate', displayText === '' && 'text-foreground-faint')}>\n {displayText === '' ? '-' : displayText}\n </span>\n );\n\n // Com o grid montado mas o modo de edição desligado, a célula é só o valor: sem\n // lápis, sem realce e sem clique, para não prometer uma edição que não abre.\n const showsEditAffordance = !disabled && (!hasGrid || isGridEditModeOn);\n\n const readCell = (\n <div\n className={classNames(\n // A mesma altura do editor (h-10), para a linha não pular quando a célula é aberta.\n 'flex min-h-10 w-full items-center gap-1 rounded-md border border-transparent px-2 py-1.5 text-sm',\n disabled && 'text-foreground-faint',\n !disabled && !showsEditAffordance && 'text-foreground',\n showsEditAffordance &&\n 'group cursor-pointer text-foreground transition-colors hover:border-brand-border hover:bg-brand-soft/50',\n error && 'border-danger-border',\n )}\n >\n <div className=\"min-w-0 flex-1 truncate\">{displayContent}</div>\n {showsEditAffordance && (\n // On touch there is no hover, so a hover-only pencil means the cell never\n // advertises that it can be edited. Keep it visible (dimmed) there instead.\n <MaterialSymbol\n icon=\"edit\"\n size={13}\n className={classNames(\n 'shrink-0 text-brand-emphasis transition-opacity',\n hoverCapable ? 'opacity-0 group-hover:opacity-100' : 'opacity-60',\n )}\n />\n )}\n </div>\n );\n\n // Fora do modo de edição a tabela é de leitura: nenhum editor fica montado.\n if (hasGrid && !isGridEditModeOn) return readCell;\n\n // The `<td>` owns the gestures that open the editor: a click, Enter, F2 or typing.\n if (isGridCell) return isEditing ? renderEditor() : readCell;\n\n if (!cellEditing) return renderEditor();\n\n return (\n <div ref={containerRef} onKeyDown={handleKeyDown} {...{ [EDITABLE_CELL_ATTR]: cellKey }}>\n {isEditing ? (\n renderEditor()\n ) : disabled ? (\n readCell\n ) : (\n <ControlledTooltip content={t('dataTable.editCell')}>\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => cellEditing.startEditing(cellKey)}\n onKeyDown={(event) => {\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n cellEditing.startEditing(cellKey);\n }}\n className=\"rounded-md focus:outline-none focus-visible:ring-1 focus-visible:ring-brand-border\"\n >\n {readCell}\n </div>\n </ControlledTooltip>\n )}\n </div>\n );\n};\n\nexport default DataTableEditableCell;\n"],"names":[],"mappings":"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"}
@@ -138,6 +138,12 @@ export interface IDataTableEditableConfig<T extends IDataTableRow> {
138
138
  type: TDataTableEditableType;
139
139
  /** Options for `type: 'select'` / `'multiselect'`. Ignored by the other input types. */
140
140
  options?: Array<IFilterOption>;
141
+ /**
142
+ * Narrows the choices the editor offers on one row (a field whose options depend on another
143
+ * column of the same row). `options` still names the stored value, so a value outside the
144
+ * narrowed list keeps its label. Returning `undefined` falls back to `options`.
145
+ */
146
+ getOptions?: (row: T) => Array<IFilterOption> | undefined;
141
147
  /**
142
148
  * Editor próprio, para o que os tipos acima não alcançam (uma lista paginada
143
149
  * vinda de uma biblioteca, por exemplo). Quando existe, ele substitui o editor
@@ -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 * 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
+ {"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 * Narrows the choices the editor offers on one row (a field whose options depend on another\n * column of the same row). `options` still names the stored value, so a value outside the\n * narrowed list keeps its label. Returning `undefined` falls back to `options`.\n */\n getOptions?: (row: T) => Array<IFilterOption> | undefined;\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":"AAwbO,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;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umodeapporg/ui",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": [