@ryuzaki13/react-foundation-lib 1.2.5 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/array/index.js +1 -1
- package/dist/chunks/{array-BBSjoTRy.js → array-D-_yMMUK.js} +2 -2
- package/dist/chunks/{array-BBSjoTRy.js.map → array-D-_yMMUK.js.map} +1 -1
- package/dist/chunks/{formatters--mhLir82.js → formatters-ShRrKmg6.js} +67 -71
- package/dist/chunks/{formatters--mhLir82.js.map → formatters-ShRrKmg6.js.map} +1 -1
- package/dist/chunks/{odata-service-DnPlNGpU.js → odata-service-B5w4YBao.js} +13 -17
- package/dist/chunks/{odata-service-DnPlNGpU.js.map → odata-service-B5w4YBao.js.map} +1 -1
- package/dist/chunks/{validate-nWY_Mla5.js → validate-BzjCBUqb.js} +14 -18
- package/dist/chunks/{validate-nWY_Mla5.js.map → validate-BzjCBUqb.js.map} +1 -1
- package/dist/error-report/index.js +2 -2
- package/dist/excel/index.js +3 -3
- package/dist/excel/index.js.map +1 -1
- package/dist/formatters/index.js +4 -4
- package/dist/hooks/index.js +82 -81
- package/dist/hooks/index.js.map +1 -1
- package/dist/media/index.js +2 -1
- package/dist/media/index.js.map +1 -1
- package/dist/number-scale/index.js +1 -1
- package/dist/number-scale/index.js.map +1 -1
- package/dist/odata-service/index.js +1 -1
- package/dist/pwa/index.js +1 -1
- package/dist/pwa/index.js.map +1 -1
- package/dist/pwa/sw-cache-policy.js +3 -3
- package/dist/pwa/sw-cache-policy.js.map +1 -1
- package/dist/query-client/index.js +5 -5
- package/dist/query-client/index.js.map +1 -1
- package/dist/range-output/index.js +1 -1
- package/dist/range-output/index.js.map +1 -1
- package/dist/table/index.js +3 -3
- package/dist/table/index.js.map +1 -1
- package/dist/tree-table/index.js +1 -1
- package/package.json +33 -33
- package/src/docs/browserAndReact.mdx +150 -0
- package/src/docs/filesAndExcel.mdx +163 -0
- package/src/docs/formatters-runtime.mdx +455 -0
- package/src/docs/formatters.mdx +363 -0
- package/src/docs/odataAndTables.mdx +178 -0
- package/src/docs/pureUtilities.mdx +96 -0
- package/src/docs/runtimeAndState.mdx +155 -0
- package/src/foundationLib.mdx +105 -0
- package/src/file/file.mdx +0 -6
- package/src/formatters/date/date.mdx +0 -6
- package/src/formatters/number/number.mdx +0 -6
- package/src/formatters/valueState/valueState.mdx +0 -6
- package/src/formulas/formulas.mdx +0 -6
- package/src/pwa/pwa.mdx +0 -6
- package/src/query-client/queryClient.mdx +0 -6
package/dist/table/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/table/columnLayout.ts","../../src/table/columnOrder.ts","../../src/table/columnSizing.ts","../../src/table/odataAdapter.ts","../../src/table/selection.ts","../../src/table/useTableRowSelection.ts","../../src/table/utils.ts"],"sourcesContent":["import { appendMissingIds, filterAndDeduplicateIds } from \"../array\";\n\nexport type TableColumnLayoutItem<TColumnId extends string = string, TWidth = number> = {\n\tid: TColumnId;\n\twidth: TWidth;\n};\n\nexport type ResolveTableColumnOrderArgs<TColumnId extends string = string> = {\n\tids: readonly TColumnId[];\n\torder?: readonly string[];\n};\n\nexport type BuildTableColumnLayoutArgs<TColumnId extends string = string, TWidth = number> = {\n\tids: readonly TColumnId[];\n\tgetWidth: (id: TColumnId) => TWidth;\n};\n\n/**\n * Возвращает порядок колонок как в AnalyticalTable: сначала сохранённый order,\n * затем новые или отсутствующие в order id. Ширины при этом остаются привязаны\n * к columnId, а не к визуальной позиции.\n */\nexport function resolveTableColumnOrder<TColumnId extends string>({ ids, order }: ResolveTableColumnOrderArgs<TColumnId>): TColumnId[] {\n\treturn appendMissingIds(filterAndDeduplicateIds(order, ids), ids);\n}\n\n/**\n * Строит layout колонок из уже вычисленного порядка и функции ширины по id.\n */\nexport function buildTableColumnLayout<TColumnId extends string, TWidth>({\n\tids,\n\tgetWidth\n}: BuildTableColumnLayoutArgs<TColumnId, TWidth>): TableColumnLayoutItem<TColumnId, TWidth>[] {\n\treturn ids.map((id) => ({\n\t\tid,\n\t\twidth: getWidth(id)\n\t}));\n}\n","import { arraysEqual } from \"../array\";\n\nexport type ReorderTableHeaderColumnsArgs = {\n\torder: readonly string[];\n\theaderIds: readonly string[];\n\tlockedIds?: readonly string[];\n\tpinnedIds?: readonly string[];\n\tactiveId: string;\n\toverId: string;\n};\n\nexport type TableColumnOrderState = string[];\n\nexport function normalizeTableColumnOrder(order: readonly string[] | undefined): TableColumnOrderState {\n\treturn Array.from(new Set((order ?? []).filter((columnId) => columnId.trim().length > 0)));\n}\n\nfunction moveItem<T>(items: readonly T[], fromIndex: number, toIndex: number): T[] {\n\tconst nextItems = [...items];\n\tconst [item] = nextItems.splice(fromIndex, 1);\n\n\tif (item === undefined) {\n\t\treturn nextItems;\n\t}\n\n\tnextItems.splice(toIndex, 0, item);\n\treturn nextItems;\n}\n\n/**\n * Переставляет только верхнеуровневые колонки табличной шапки.\n *\n * Ограничения:\n * - дочерние колонки групп не являются самостоятельными draggable-элементами;\n * - locked-колонки остаются в фиксированном порядке;\n * - pinned-колонки не переносятся между закреплённой и обычной зонами.\n */\nexport function reorderTableHeaderColumns(args: ReorderTableHeaderColumnsArgs): string[] {\n\tconst { activeId, overId, order } = args;\n\tif (!activeId || !overId || activeId === overId) return [...order];\n\n\tconst headerSet = new Set(args.headerIds);\n\tif (!headerSet.has(activeId) || !headerSet.has(overId)) return [...order];\n\n\tconst lockedSet = new Set(args.lockedIds ?? []);\n\tif (lockedSet.has(activeId) || lockedSet.has(overId)) return [...order];\n\n\tconst pinnedSet = new Set(args.pinnedIds ?? []);\n\tif (pinnedSet.has(activeId) || pinnedSet.has(overId)) return [...order];\n\n\tconst nextOrder = [...order];\n\tif (!nextOrder.includes(activeId)) {\n\t\tnextOrder.push(activeId);\n\t}\n\tif (!nextOrder.includes(overId)) {\n\t\tnextOrder.push(overId);\n\t}\n\n\tconst fromIndex = nextOrder.indexOf(activeId);\n\tconst toIndex = nextOrder.indexOf(overId);\n\tif (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return nextOrder;\n\n\treturn moveItem(nextOrder, fromIndex, toIndex);\n}\n\nexport function resolveReorderedTableHeaderColumns(args: ReorderTableHeaderColumnsArgs): string[] | null {\n\tconst nextOrder = reorderTableHeaderColumns(args);\n\n\treturn arraysEqual(nextOrder, args.order) ? null : nextOrder;\n}\n","export type TableColumnSizingState = Record<string, number>;\n\nexport const DEFAULT_TABLE_COLUMN_MIN_WIDTH = 60;\n\nexport function normalizeTableColumnWidth(px: number, minWidth: number = DEFAULT_TABLE_COLUMN_MIN_WIDTH): number {\n\tconst normalizedMinWidth = Number.isFinite(minWidth) && minWidth > 0 ? Math.floor(minWidth) : DEFAULT_TABLE_COLUMN_MIN_WIDTH;\n\tconst normalizedWidth = Number.isFinite(px) ? Math.floor(px) : normalizedMinWidth;\n\n\treturn Math.max(normalizedMinWidth, normalizedWidth);\n}\n\nexport function normalizeTableColumnSizing(\n\tstate: Readonly<Record<string, number>> | undefined,\n\tminWidth: number = DEFAULT_TABLE_COLUMN_MIN_WIDTH\n): TableColumnSizingState {\n\tconst nextState: TableColumnSizingState = {};\n\n\tfor (const [columnId, width] of Object.entries(state ?? {})) {\n\t\tif (typeof columnId !== \"string\" || columnId.trim().length === 0 || !Number.isFinite(width)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tnextState[columnId] = normalizeTableColumnWidth(width, minWidth);\n\t}\n\n\treturn nextState;\n}\n\nexport function patchTableColumnWidth(\n\tstate: Readonly<Record<string, number>>,\n\tcolumnId: string,\n\twidth: number,\n\tminWidth: number = DEFAULT_TABLE_COLUMN_MIN_WIDTH\n): TableColumnSizingState {\n\tif (columnId.trim().length === 0) {\n\t\treturn normalizeTableColumnSizing(state, minWidth);\n\t}\n\n\treturn {\n\t\t...normalizeTableColumnSizing(state, minWidth),\n\t\t[columnId]: normalizeTableColumnWidth(width, minWidth)\n\t};\n}\n\nexport function removeTableColumnWidth(state: Readonly<Record<string, number>>, columnId: string): TableColumnSizingState {\n\tconst nextState: TableColumnSizingState = {};\n\n\tfor (const [key, width] of Object.entries(state)) {\n\t\tif (key !== columnId) {\n\t\t\tnextState[key] = width;\n\t\t}\n\t}\n\n\treturn nextState;\n}\n","import type { FormattersPipelineTypedValueContext } from \"../formatters/pipeline\";\nimport type { EntityColumnProperty } from \"../odata-service\";\nimport type { TableColumnAlign, TableColumnDef, TableColumnFormattingMetaInput } from \"./types\";\n\n/**\n * Опции генерации колонок по OData metadata.\n */\nexport interface CreateTableColumnsFromODataMetadataOptions {\n\t/**\n\t * Позволяет переопределить заголовок колонки.\n\t */\n\tresolveHeader?: (column: EntityColumnProperty) => string;\n\t/**\n\t * Позволяет сразу навесить formatting-конфиг на generated column.\n\t *\n\t * `id`, `role` и `type` будут автоматически получены из metadata.\n\t */\n\tresolveFormatting?: (column: EntityColumnProperty) => TableColumnFormattingMetaInput | undefined;\n}\n\n/**\n * Опции обогащения уже существующих колонок данными OData metadata.\n */\nexport interface EnrichTableColumnsWithODataFormattingOptions {\n\t/**\n\t * Позволяет подменить стратегию поиска metadata по колонке.\n\t */\n\tresolveMetadataColumn?: (args: {\n\t\tcolumnId: string;\n\t\tmetadataColumnsById: ReadonlyMap<string, EntityColumnProperty>;\n\t}) => EntityColumnProperty | undefined;\n}\n\n/**\n * Опции построения стартовой карты видимости колонок по OData metadata.\n */\nexport interface CreateTableColumnVisibilityFromODataMetadataOptions {\n\t/**\n\t * Позволяет явно переопределить стартовую видимость колонки.\n\t */\n\tresolveVisible?: (column: EntityColumnProperty) => boolean;\n}\n\n/**\n * Возвращает typed-formatting контекст по metadata OData-колонки.\n */\nexport function resolveTableColumnFormattingContextFromODataColumn(column: EntityColumnProperty): FormattersPipelineTypedValueContext {\n\tconst { role, type } = column;\n\tconst forceMeasure = type === \"decimal\" || type === \"float\" || type === \"double\" || type === \"int\" || type === \"byte\";\n\n\treturn {\n\t\trole: forceMeasure ? \"measure\" : role,\n\t\ttype: type\n\t};\n}\n\n/**\n * Создаёт базовое выравнивание по семантике OData-колонки.\n *\n * NOTE: \"не аналитические\" все поля возвращают как \"dimension\"...\n */\nfunction resolveTableColumnAlignFromODataColumn(columnContext: FormattersPipelineTypedValueContext): TableColumnAlign {\n\treturn columnContext.role === \"measure\" ? \"right\" : \"left\";\n}\n\n/**\n * Определяет видимость metadata-колонки по умолчанию.\n *\n * Кодовые поля скрываются только если у них есть явная text-пара,\n * чтобы экран по умолчанию показывал человекочитаемое значение.\n */\nfunction resolveMetadataColumnVisibleByDefault(\n\tcolumn: EntityColumnProperty,\n\tmetadataColumnsById: ReadonlyMap<string, EntityColumnProperty>\n): boolean {\n\tif (column.semanticType !== \"code\" || !column.linkedColumnId) {\n\t\treturn true;\n\t}\n\n\tconst linkedColumn = metadataColumnsById.get(column.linkedColumnId);\n\n\treturn linkedColumn?.semanticType !== \"text\";\n}\n\n/**\n * Определяет стабильный идентификатор колонки для сопоставления с metadata.\n */\nexport function resolveStableColumnId<TData extends object>(column: TableColumnDef<TData>): string | undefined {\n\tif (typeof column.id === \"string\" && column.id.trim()) {\n\t\treturn column.id;\n\t}\n\n\tif (\"accessorKey\" in column && typeof column.accessorKey === \"string\" && column.accessorKey.trim()) {\n\t\treturn column.accessorKey;\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Рекурсивно обходит только leaf-колонки.\n */\nfunction mapLeafTableColumns<TData extends object>(\n\tcolumns: readonly TableColumnDef<TData>[],\n\tmapper: (column: TableColumnDef<TData>) => TableColumnDef<TData>\n): TableColumnDef<TData>[] {\n\treturn columns.map((column) => {\n\t\tif (\"columns\" in column && Array.isArray(column.columns) && column.columns.length > 0) {\n\t\t\treturn {\n\t\t\t\t...column,\n\t\t\t\tcolumns: mapLeafTableColumns(column.columns as TableColumnDef<TData>[], mapper)\n\t\t\t};\n\t\t}\n\n\t\treturn mapper(column);\n\t});\n}\n\n/**\n * Строит стартовую карту видимости колонок из metadata OData-сущности.\n *\n * Возвращает visibility-state для TanStack Table, не удаляя сами колонки\n * из результата build-режима.\n */\nexport function createTableColumnVisibilityFromODataMetadata(\n\tmetadataColumns: readonly EntityColumnProperty[],\n\toptions?: CreateTableColumnVisibilityFromODataMetadataOptions\n): Record<string, boolean> {\n\tconst metadataColumnsById = new Map(metadataColumns.map((column) => [column.id, column]));\n\n\treturn Object.fromEntries(\n\t\tmetadataColumns.map((column) => {\n\t\t\tconst visible = options?.resolveVisible?.(column) ?? resolveMetadataColumnVisibleByDefault(column, metadataColumnsById);\n\n\t\t\treturn [column.id, visible];\n\t\t})\n\t);\n}\n\n/**\n * Создаёт базовые generic-колонки из metadata OData-сущности.\n */\nexport function createTableColumnsFromODataMetadata<TData extends object = Record<string, unknown>>(\n\tmetadataColumns: readonly EntityColumnProperty[],\n\toptions?: CreateTableColumnsFromODataMetadataOptions\n): TableColumnDef<TData>[] {\n\treturn metadataColumns.map((metadataColumn) => {\n\t\t// NOTE: Ячейка сама определит выравнивание, `align` только для переопределения\n\t\tconst columnContext = resolveTableColumnFormattingContextFromODataColumn(metadataColumn);\n\t\tconst align = resolveTableColumnAlignFromODataColumn(columnContext);\n\t\tconst resolvedFormatting = options?.resolveFormatting?.(metadataColumn);\n\n\t\treturn {\n\t\t\tid: metadataColumn.id,\n\t\t\taccessorKey: metadataColumn.id,\n\t\t\theader: options?.resolveHeader?.(metadataColumn) ?? metadataColumn.label ?? metadataColumn.id,\n\t\t\tmeta: {\n\t\t\t\talign,\n\t\t\t\tformatting: {\n\t\t\t\t\t...resolvedFormatting,\n\t\t\t\t\tid: metadataColumn.id,\n\t\t\t\t\trole: columnContext.role,\n\t\t\t\t\ttype: columnContext.type\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}) as TableColumnDef<TData>[];\n}\n\n/**\n * Дозаполняет уже существующие колонки typed-formatting контекстом из OData metadata.\n */\nexport function enrichTableColumnsWithODataFormatting<TData extends object>(\n\tcolumns: readonly TableColumnDef<TData>[],\n\tmetadataColumns: readonly EntityColumnProperty[],\n\toptions?: EnrichTableColumnsWithODataFormattingOptions\n): TableColumnDef<TData>[] {\n\tconst metadataColumnsById = new Map(metadataColumns.map((column) => [column.id, column]));\n\n\treturn mapLeafTableColumns(columns, (column) => {\n\t\tconst columnId = resolveStableColumnId(column);\n\t\tif (!columnId) {\n\t\t\treturn column;\n\t\t}\n\n\t\tconst metadataColumn =\n\t\t\toptions?.resolveMetadataColumn?.({\n\t\t\t\tcolumnId,\n\t\t\t\tmetadataColumnsById\n\t\t\t}) ?? metadataColumnsById.get(columnId);\n\n\t\tif (!metadataColumn) {\n\t\t\treturn column;\n\t\t}\n\n\t\tif (column.meta?.formatting?.role && column.meta.formatting.type) {\n\t\t\treturn column;\n\t\t}\n\t\tconst columnContext = resolveTableColumnFormattingContextFromODataColumn(metadataColumn);\n\n\t\treturn {\n\t\t\t...column,\n\t\t\tmeta: {\n\t\t\t\t...column.meta,\n\t\t\t\tformatting: {\n\t\t\t\t\t...column.meta?.formatting,\n\t\t\t\t\tid: columnId,\n\t\t\t\t\trole: columnContext.role,\n\t\t\t\t\ttype: columnContext.type\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t});\n}\n","import { RowSelectionState } from \"@tanstack/react-table\";\n\nimport { TableSelectionMode } from \"./types\";\n\n/**\n * Возвращает только активные идентификаторы выбранных строк.\n */\nfunction getSelectedRowIds(selection: RowSelectionState): string[] {\n\treturn Object.keys(selection).filter((rowId) => selection[rowId]);\n}\n\n/**\n * Нормализует состояние выбора относительно списка доступных строк и текущего режима.\n */\nexport function pruneTableRowSelection(\n\tselection: RowSelectionState,\n\tavailableRowIds: readonly string[],\n\tselectionMode: TableSelectionMode\n): RowSelectionState {\n\tif (selectionMode === \"none\") {\n\t\treturn {};\n\t}\n\n\tconst availableRowIdSet = new Set(availableRowIds);\n\tconst selectedRowIds = getSelectedRowIds(selection).filter((rowId) => availableRowIdSet.has(rowId));\n\n\tif (selectionMode === \"single\") {\n\t\tconst lastSelectedRowId = selectedRowIds.at(-1);\n\n\t\treturn lastSelectedRowId ? { [lastSelectedRowId]: true } : {};\n\t}\n\n\treturn Object.fromEntries(selectedRowIds.map((rowId) => [rowId, true]));\n}\n\n/**\n * Вычисляет следующее состояние выбора после активации строки пользователем.\n *\n * Семантика:\n * - `none` не хранит выбор;\n * - `single` всегда оставляет только одну строку и не снимает выбор повторным кликом;\n * - `multi` переключает строку как toggle.\n */\nexport function toggleTableRowSelection(selection: RowSelectionState, rowId: string, selectionMode: TableSelectionMode): RowSelectionState {\n\tif (selectionMode === \"none\") {\n\t\treturn {};\n\t}\n\n\tif (selectionMode === \"single\") {\n\t\treturn selection[rowId] ? selection : { [rowId]: true };\n\t}\n\n\tif (selection[rowId]) {\n\t\tconst nextSelection = { ...selection };\n\n\t\tdelete nextSelection[rowId];\n\n\t\treturn nextSelection;\n\t}\n\n\treturn {\n\t\t...selection,\n\t\t[rowId]: true\n\t};\n}\n","import { useCallback, useEffect, useEffectEvent, useRef, useState } from \"react\";\n\nimport { RowSelectionState } from \"@tanstack/react-table\";\n\nimport { pruneTableRowSelection, toggleTableRowSelection } from \"./selection\";\nimport { TableSelectionMode } from \"./types\";\n\ninterface UseTableRowSelectionParams<TData extends object> {\n\tavailableRowIds: readonly string[];\n\trowById: Map<string, TData>;\n\tselectionMode: TableSelectionMode;\n\tselectedRowIds?: readonly string[];\n\tonRowSelectionChange?: (rows: TData[]) => void;\n}\n\n/**\n * Общая модель выбора строк для плоской и древовидной таблиц.\n */\nexport function useTableRowSelection<TData extends object>({\n\tavailableRowIds,\n\trowById,\n\tselectionMode,\n\tselectedRowIds,\n\tonRowSelectionChange\n}: UseTableRowSelectionParams<TData>) {\n\tconst [rowSelection, setRowSelection] = useState<RowSelectionState>({});\n\tconst shouldSkipNextSelectionEmitRef = useRef(false);\n\n\t/**\n\t * Сравнивает состояния выбора без лишних перерисовок.\n\t */\n\tconst areSelectionsEqual = useCallback((left: RowSelectionState, right: RowSelectionState) => {\n\t\tconst leftRowIds = Object.keys(left).filter((rowId) => left[rowId]);\n\t\tconst rightRowIds = Object.keys(right).filter((rowId) => right[rowId]);\n\n\t\tif (leftRowIds.length !== rightRowIds.length) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn leftRowIds.every((rowId) => right[rowId]);\n\n\t\t/*\n\n Более оптимизированная версия, но для текущего случая может быть практически незначительной\n\n let leftCount = 0;\n\t\tlet rightCount = 0;\n\n\t\tfor (const rowId of Object.keys(left)) {\n\t\t\tif (!left[rowId]) continue;\n\t\t\tleftCount += 1;\n\t\t\tif (!right[rowId]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\tfor (const rowId of Object.keys(right)) {\n\t\t\tif (right[rowId]) {\n\t\t\t\trightCount += 1;\n\t\t\t}\n\t\t}\n\n\t\tif (leftCount !== rightCount) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\n */\n\t}, []);\n\n\tconst syncRowSelectionState = useEffectEvent((nextAvailableRowIds: readonly string[]) => {\n\t\tsetRowSelection((currentSelection) => pruneTableRowSelection(currentSelection, nextAvailableRowIds, selectionMode));\n\t});\n\n\tconst syncControlledRowSelectionState = useEffectEvent(\n\t\t(nextSelectedRowIds: readonly string[], nextAvailableRowIds: readonly string[]) => {\n\t\t\tconst nextSelection = pruneTableRowSelection(\n\t\t\t\tObject.fromEntries(nextSelectedRowIds.map((rowId) => [rowId, true])),\n\t\t\t\tnextAvailableRowIds,\n\t\t\t\tselectionMode\n\t\t\t);\n\n\t\t\tsetRowSelection((currentSelection) => {\n\t\t\t\tif (areSelectionsEqual(currentSelection, nextSelection)) {\n\t\t\t\t\treturn currentSelection;\n\t\t\t\t}\n\n\t\t\t\t// Пропускаем следующий emit, потому что это синхронизация от внешнего controlled-состояния,\n\t\t\t\t// а не пользовательское изменение выбора внутри таблицы.\n\t\t\t\tshouldSkipNextSelectionEmitRef.current = true;\n\n\t\t\t\treturn nextSelection;\n\t\t\t});\n\t\t}\n\t);\n\n\tconst emitRowSelectionChange = useEffectEvent((currentSelection: RowSelectionState) => {\n\t\tif (!onRowSelectionChange) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst selectedRows: TData[] = [];\n\n\t\tfor (const rowId of Object.keys(currentSelection)) {\n\t\t\tif (!currentSelection[rowId]) continue;\n\n\t\t\tconst row = rowById.get(rowId);\n\t\t\tif (row) {\n\t\t\t\tselectedRows.push(row);\n\t\t\t}\n\t\t}\n\n\t\tonRowSelectionChange(selectedRows);\n\t});\n\n\tuseEffect(() => {\n\t\tif (selectedRowIds !== undefined) {\n\t\t\tsyncControlledRowSelectionState(selectedRowIds, availableRowIds);\n\t\t\treturn;\n\t\t}\n\n\t\tsyncRowSelectionState(availableRowIds);\n\t}, [availableRowIds, selectedRowIds, selectionMode]);\n\n\tuseEffect(() => {\n\t\tif (shouldSkipNextSelectionEmitRef.current) {\n\t\t\tshouldSkipNextSelectionEmitRef.current = false;\n\t\t\treturn;\n\t\t}\n\n\t\temitRowSelectionChange(rowSelection);\n\t}, [rowSelection]);\n\n\t/**\n\t * Применяет изменение выбора при активации конкретной строки.\n\t */\n\tconst activateRowSelection = useCallback(\n\t\t(rowId: string, canSelect: boolean) => {\n\t\t\tif (selectionMode === \"none\" || !canSelect) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetRowSelection((currentSelection) => toggleTableRowSelection(currentSelection, rowId, selectionMode));\n\t\t},\n\t\t[selectionMode]\n\t);\n\n\treturn {\n\t\trowSelection,\n\t\tsetRowSelection,\n\t\tactivateRowSelection\n\t};\n}\n","import { TableColumnDef, TableColumnFormattingMeta, TableColumnMeta } from \"./types\";\n\n/**\n * Возвращает метаданные колонки таблицы.\n */\nexport function getTableColumnMeta<TData extends object>(column: TableColumnDef<TData>): TableColumnMeta | undefined {\n\treturn column.meta;\n}\n\n/**\n * Возвращает форматирующий блок метаданных колонки.\n */\nexport function getTableColumnFormattingMeta<TData extends object>(column: TableColumnDef<TData>): TableColumnFormattingMeta | undefined {\n\treturn column.meta?.formatting;\n}\n\n/**\n * Нормализует размер таблицы.\n *\n * Числовое значение трактуется как `em`, чтобы разметка масштабировалась вместе со шрифтом.\n */\nexport function resolveTableLength(length: string | number): string {\n\tif (typeof length === \"number\") {\n\t\treturn `${length}em`;\n\t}\n\n\treturn length;\n}\n\n/**\n * Определяет, пришёл ли клик из интерактивного элемента внутри таблицы.\n */\nexport function isTableInteractiveElement(target: EventTarget | null, extraSelector?: string): boolean {\n\tif (!(target instanceof HTMLElement)) {\n\t\treturn false;\n\t}\n\n\tconst baseSelector = 'button, a, input, select, textarea, [role=\"button\"], [role=\"link\"]';\n\tconst selector = extraSelector ? `${baseSelector}, ${extraSelector}` : baseSelector;\n\n\treturn Boolean(target.closest(selector));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/table/columnLayout.ts","../../src/table/columnOrder.ts","../../src/table/columnSizing.ts","../../src/table/odataAdapter.ts","../../src/table/selection.ts","../../src/table/useTableRowSelection.ts","../../src/table/utils.ts"],"sourcesContent":["import { appendMissingIds, filterAndDeduplicateIds } from \"../array\";\n\nexport type TableColumnLayoutItem<TColumnId extends string = string, TWidth = number> = {\n\tid: TColumnId;\n\twidth: TWidth;\n};\n\nexport type ResolveTableColumnOrderArgs<TColumnId extends string = string> = {\n\tids: readonly TColumnId[];\n\torder?: readonly string[];\n};\n\nexport type BuildTableColumnLayoutArgs<TColumnId extends string = string, TWidth = number> = {\n\tids: readonly TColumnId[];\n\tgetWidth: (id: TColumnId) => TWidth;\n};\n\n/**\n * Возвращает порядок колонок как в AnalyticalTable: сначала сохранённый order,\n * затем новые или отсутствующие в order id. Ширины при этом остаются привязаны\n * к columnId, а не к визуальной позиции.\n */\nexport function resolveTableColumnOrder<TColumnId extends string>({ ids, order }: ResolveTableColumnOrderArgs<TColumnId>): TColumnId[] {\n\treturn appendMissingIds(filterAndDeduplicateIds(order, ids), ids);\n}\n\n/**\n * Строит layout колонок из уже вычисленного порядка и функции ширины по id.\n */\nexport function buildTableColumnLayout<TColumnId extends string, TWidth>({\n\tids,\n\tgetWidth\n}: BuildTableColumnLayoutArgs<TColumnId, TWidth>): TableColumnLayoutItem<TColumnId, TWidth>[] {\n\treturn ids.map((id) => ({\n\t\tid,\n\t\twidth: getWidth(id)\n\t}));\n}\n","import { arraysEqual } from \"../array\";\n\nexport type ReorderTableHeaderColumnsArgs = {\n\torder: readonly string[];\n\theaderIds: readonly string[];\n\tlockedIds?: readonly string[];\n\tpinnedIds?: readonly string[];\n\tactiveId: string;\n\toverId: string;\n};\n\nexport type TableColumnOrderState = string[];\n\nexport function normalizeTableColumnOrder(order: readonly string[] | undefined): TableColumnOrderState {\n\treturn Array.from(new Set((order ?? []).filter((columnId) => columnId.trim().length > 0)));\n}\n\nfunction moveItem<T>(items: readonly T[], fromIndex: number, toIndex: number): T[] {\n\tconst nextItems = [...items];\n\tconst [item] = nextItems.splice(fromIndex, 1);\n\n\tif (item === undefined) {\n\t\treturn nextItems;\n\t}\n\n\tnextItems.splice(toIndex, 0, item);\n\treturn nextItems;\n}\n\n/**\n * Переставляет только верхнеуровневые колонки табличной шапки.\n *\n * Ограничения:\n * - дочерние колонки групп не являются самостоятельными draggable-элементами;\n * - locked-колонки остаются в фиксированном порядке;\n * - pinned-колонки не переносятся между закреплённой и обычной зонами.\n */\nexport function reorderTableHeaderColumns(args: ReorderTableHeaderColumnsArgs): string[] {\n\tconst { activeId, overId, order } = args;\n\tif (!activeId || !overId || activeId === overId) return [...order];\n\n\tconst headerSet = new Set(args.headerIds);\n\tif (!headerSet.has(activeId) || !headerSet.has(overId)) return [...order];\n\n\tconst lockedSet = new Set(args.lockedIds ?? []);\n\tif (lockedSet.has(activeId) || lockedSet.has(overId)) return [...order];\n\n\tconst pinnedSet = new Set(args.pinnedIds ?? []);\n\tif (pinnedSet.has(activeId) || pinnedSet.has(overId)) return [...order];\n\n\tconst nextOrder = [...order];\n\tif (!nextOrder.includes(activeId)) {\n\t\tnextOrder.push(activeId);\n\t}\n\tif (!nextOrder.includes(overId)) {\n\t\tnextOrder.push(overId);\n\t}\n\n\tconst fromIndex = nextOrder.indexOf(activeId);\n\tconst toIndex = nextOrder.indexOf(overId);\n\tif (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return nextOrder;\n\n\treturn moveItem(nextOrder, fromIndex, toIndex);\n}\n\nexport function resolveReorderedTableHeaderColumns(args: ReorderTableHeaderColumnsArgs): string[] | null {\n\tconst nextOrder = reorderTableHeaderColumns(args);\n\n\treturn arraysEqual(nextOrder, args.order) ? null : nextOrder;\n}\n","export type TableColumnSizingState = Record<string, number>;\n\nexport const DEFAULT_TABLE_COLUMN_MIN_WIDTH = 60;\n\nexport function normalizeTableColumnWidth(px: number, minWidth: number = DEFAULT_TABLE_COLUMN_MIN_WIDTH): number {\n\tconst normalizedMinWidth = Number.isFinite(minWidth) && minWidth > 0 ? Math.floor(minWidth) : DEFAULT_TABLE_COLUMN_MIN_WIDTH;\n\tconst normalizedWidth = Number.isFinite(px) ? Math.floor(px) : normalizedMinWidth;\n\n\treturn Math.max(normalizedMinWidth, normalizedWidth);\n}\n\nexport function normalizeTableColumnSizing(\n\tstate: Readonly<Record<string, number>> | undefined,\n\tminWidth: number = DEFAULT_TABLE_COLUMN_MIN_WIDTH\n): TableColumnSizingState {\n\tconst nextState: TableColumnSizingState = {};\n\n\tfor (const [columnId, width] of Object.entries(state ?? {})) {\n\t\tif (typeof columnId !== \"string\" || columnId.trim().length === 0 || !Number.isFinite(width)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tnextState[columnId] = normalizeTableColumnWidth(width, minWidth);\n\t}\n\n\treturn nextState;\n}\n\nexport function patchTableColumnWidth(\n\tstate: Readonly<Record<string, number>>,\n\tcolumnId: string,\n\twidth: number,\n\tminWidth: number = DEFAULT_TABLE_COLUMN_MIN_WIDTH\n): TableColumnSizingState {\n\tif (columnId.trim().length === 0) {\n\t\treturn normalizeTableColumnSizing(state, minWidth);\n\t}\n\n\treturn {\n\t\t...normalizeTableColumnSizing(state, minWidth),\n\t\t[columnId]: normalizeTableColumnWidth(width, minWidth)\n\t};\n}\n\nexport function removeTableColumnWidth(state: Readonly<Record<string, number>>, columnId: string): TableColumnSizingState {\n\tconst nextState: TableColumnSizingState = {};\n\n\tfor (const [key, width] of Object.entries(state)) {\n\t\tif (key !== columnId) {\n\t\t\tnextState[key] = width;\n\t\t}\n\t}\n\n\treturn nextState;\n}\n","import type { FormattersPipelineTypedValueContext } from \"../formatters/pipeline\";\nimport type { EntityColumnProperty } from \"../odata-service\";\nimport type { TableColumnAlign, TableColumnDef, TableColumnFormattingMetaInput } from \"./types\";\n\n/**\n * Опции генерации колонок по OData metadata.\n */\nexport interface CreateTableColumnsFromODataMetadataOptions {\n\t/**\n\t * Позволяет переопределить заголовок колонки.\n\t */\n\tresolveHeader?: (column: EntityColumnProperty) => string;\n\t/**\n\t * Позволяет сразу навесить formatting-конфиг на generated column.\n\t *\n\t * `id`, `role` и `type` будут автоматически получены из metadata.\n\t */\n\tresolveFormatting?: (column: EntityColumnProperty) => TableColumnFormattingMetaInput | undefined;\n}\n\n/**\n * Опции обогащения уже существующих колонок данными OData metadata.\n */\nexport interface EnrichTableColumnsWithODataFormattingOptions {\n\t/**\n\t * Позволяет подменить стратегию поиска metadata по колонке.\n\t */\n\tresolveMetadataColumn?: (args: {\n\t\tcolumnId: string;\n\t\tmetadataColumnsById: ReadonlyMap<string, EntityColumnProperty>;\n\t}) => EntityColumnProperty | undefined;\n}\n\n/**\n * Опции построения стартовой карты видимости колонок по OData metadata.\n */\nexport interface CreateTableColumnVisibilityFromODataMetadataOptions {\n\t/**\n\t * Позволяет явно переопределить стартовую видимость колонки.\n\t */\n\tresolveVisible?: (column: EntityColumnProperty) => boolean;\n}\n\n/**\n * Возвращает typed-formatting контекст по metadata OData-колонки.\n */\nexport function resolveTableColumnFormattingContextFromODataColumn(column: EntityColumnProperty): FormattersPipelineTypedValueContext {\n\tconst { role, type } = column;\n\tconst forceMeasure = type === \"decimal\" || type === \"float\" || type === \"double\" || type === \"int\" || type === \"byte\";\n\n\treturn {\n\t\trole: forceMeasure ? \"measure\" : role,\n\t\ttype: type\n\t};\n}\n\n/**\n * Создаёт базовое выравнивание по семантике OData-колонки.\n *\n * NOTE: \"не аналитические\" все поля возвращают как \"dimension\"...\n */\nfunction resolveTableColumnAlignFromODataColumn(columnContext: FormattersPipelineTypedValueContext): TableColumnAlign {\n\treturn columnContext.role === \"measure\" ? \"right\" : \"left\";\n}\n\n/**\n * Определяет видимость metadata-колонки по умолчанию.\n *\n * Кодовые поля скрываются только если у них есть явная text-пара,\n * чтобы экран по умолчанию показывал человекочитаемое значение.\n */\nfunction resolveMetadataColumnVisibleByDefault(\n\tcolumn: EntityColumnProperty,\n\tmetadataColumnsById: ReadonlyMap<string, EntityColumnProperty>\n): boolean {\n\tif (column.semanticType !== \"code\" || !column.linkedColumnId) {\n\t\treturn true;\n\t}\n\n\tconst linkedColumn = metadataColumnsById.get(column.linkedColumnId);\n\n\treturn linkedColumn?.semanticType !== \"text\";\n}\n\n/**\n * Определяет стабильный идентификатор колонки для сопоставления с metadata.\n */\nexport function resolveStableColumnId<TData extends object>(column: TableColumnDef<TData>): string | undefined {\n\tif (typeof column.id === \"string\" && column.id.trim()) {\n\t\treturn column.id;\n\t}\n\n\tif (\"accessorKey\" in column && typeof column.accessorKey === \"string\" && column.accessorKey.trim()) {\n\t\treturn column.accessorKey;\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Рекурсивно обходит только leaf-колонки.\n */\nfunction mapLeafTableColumns<TData extends object>(\n\tcolumns: readonly TableColumnDef<TData>[],\n\tmapper: (column: TableColumnDef<TData>) => TableColumnDef<TData>\n): TableColumnDef<TData>[] {\n\treturn columns.map((column) => {\n\t\tif (\"columns\" in column && Array.isArray(column.columns) && column.columns.length > 0) {\n\t\t\treturn {\n\t\t\t\t...column,\n\t\t\t\tcolumns: mapLeafTableColumns(column.columns as TableColumnDef<TData>[], mapper)\n\t\t\t};\n\t\t}\n\n\t\treturn mapper(column);\n\t});\n}\n\n/**\n * Строит стартовую карту видимости колонок из metadata OData-сущности.\n *\n * Возвращает visibility-state для TanStack Table, не удаляя сами колонки\n * из результата build-режима.\n */\nexport function createTableColumnVisibilityFromODataMetadata(\n\tmetadataColumns: readonly EntityColumnProperty[],\n\toptions?: CreateTableColumnVisibilityFromODataMetadataOptions\n): Record<string, boolean> {\n\tconst metadataColumnsById = new Map(metadataColumns.map((column) => [column.id, column]));\n\n\treturn Object.fromEntries(\n\t\tmetadataColumns.map((column) => {\n\t\t\tconst visible = options?.resolveVisible?.(column) ?? resolveMetadataColumnVisibleByDefault(column, metadataColumnsById);\n\n\t\t\treturn [column.id, visible];\n\t\t})\n\t);\n}\n\n/**\n * Создаёт базовые generic-колонки из metadata OData-сущности.\n */\nexport function createTableColumnsFromODataMetadata<TData extends object = Record<string, unknown>>(\n\tmetadataColumns: readonly EntityColumnProperty[],\n\toptions?: CreateTableColumnsFromODataMetadataOptions\n): TableColumnDef<TData>[] {\n\treturn metadataColumns.map((metadataColumn) => {\n\t\t// NOTE: Ячейка сама определит выравнивание, `align` только для переопределения\n\t\tconst columnContext = resolveTableColumnFormattingContextFromODataColumn(metadataColumn);\n\t\tconst align = resolveTableColumnAlignFromODataColumn(columnContext);\n\t\tconst resolvedFormatting = options?.resolveFormatting?.(metadataColumn);\n\n\t\treturn {\n\t\t\tid: metadataColumn.id,\n\t\t\taccessorKey: metadataColumn.id,\n\t\t\theader: options?.resolveHeader?.(metadataColumn) ?? metadataColumn.label ?? metadataColumn.id,\n\t\t\tmeta: {\n\t\t\t\talign,\n\t\t\t\tformatting: {\n\t\t\t\t\t...resolvedFormatting,\n\t\t\t\t\tid: metadataColumn.id,\n\t\t\t\t\trole: columnContext.role,\n\t\t\t\t\ttype: columnContext.type\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}) as TableColumnDef<TData>[];\n}\n\n/**\n * Дозаполняет уже существующие колонки typed-formatting контекстом из OData metadata.\n */\nexport function enrichTableColumnsWithODataFormatting<TData extends object>(\n\tcolumns: readonly TableColumnDef<TData>[],\n\tmetadataColumns: readonly EntityColumnProperty[],\n\toptions?: EnrichTableColumnsWithODataFormattingOptions\n): TableColumnDef<TData>[] {\n\tconst metadataColumnsById = new Map(metadataColumns.map((column) => [column.id, column]));\n\n\treturn mapLeafTableColumns(columns, (column) => {\n\t\tconst columnId = resolveStableColumnId(column);\n\t\tif (!columnId) {\n\t\t\treturn column;\n\t\t}\n\n\t\tconst metadataColumn =\n\t\t\toptions?.resolveMetadataColumn?.({\n\t\t\t\tcolumnId,\n\t\t\t\tmetadataColumnsById\n\t\t\t}) ?? metadataColumnsById.get(columnId);\n\n\t\tif (!metadataColumn) {\n\t\t\treturn column;\n\t\t}\n\n\t\tif (column.meta?.formatting?.role && column.meta.formatting.type) {\n\t\t\treturn column;\n\t\t}\n\t\tconst columnContext = resolveTableColumnFormattingContextFromODataColumn(metadataColumn);\n\n\t\treturn {\n\t\t\t...column,\n\t\t\tmeta: {\n\t\t\t\t...column.meta,\n\t\t\t\tformatting: {\n\t\t\t\t\t...column.meta?.formatting,\n\t\t\t\t\tid: columnId,\n\t\t\t\t\trole: columnContext.role,\n\t\t\t\t\ttype: columnContext.type\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t});\n}\n","import { RowSelectionState } from \"@tanstack/react-table\";\n\nimport { TableSelectionMode } from \"./types\";\n\n/**\n * Возвращает только активные идентификаторы выбранных строк.\n */\nfunction getSelectedRowIds(selection: RowSelectionState): string[] {\n\treturn Object.keys(selection).filter((rowId) => selection[rowId]);\n}\n\n/**\n * Нормализует состояние выбора относительно списка доступных строк и текущего режима.\n */\nexport function pruneTableRowSelection(\n\tselection: RowSelectionState,\n\tavailableRowIds: readonly string[],\n\tselectionMode: TableSelectionMode\n): RowSelectionState {\n\tif (selectionMode === \"none\") {\n\t\treturn {};\n\t}\n\n\tconst availableRowIdSet = new Set(availableRowIds);\n\tconst selectedRowIds = getSelectedRowIds(selection).filter((rowId) => availableRowIdSet.has(rowId));\n\n\tif (selectionMode === \"single\") {\n\t\tconst lastSelectedRowId = selectedRowIds.at(-1);\n\n\t\treturn lastSelectedRowId ? { [lastSelectedRowId]: true } : {};\n\t}\n\n\treturn Object.fromEntries(selectedRowIds.map((rowId) => [rowId, true]));\n}\n\n/**\n * Вычисляет следующее состояние выбора после активации строки пользователем.\n *\n * Семантика:\n * - `none` не хранит выбор;\n * - `single` всегда оставляет только одну строку и не снимает выбор повторным кликом;\n * - `multi` переключает строку как toggle.\n */\nexport function toggleTableRowSelection(selection: RowSelectionState, rowId: string, selectionMode: TableSelectionMode): RowSelectionState {\n\tif (selectionMode === \"none\") {\n\t\treturn {};\n\t}\n\n\tif (selectionMode === \"single\") {\n\t\treturn selection[rowId] ? selection : { [rowId]: true };\n\t}\n\n\tif (selection[rowId]) {\n\t\tconst nextSelection = { ...selection };\n\n\t\tdelete nextSelection[rowId];\n\n\t\treturn nextSelection;\n\t}\n\n\treturn {\n\t\t...selection,\n\t\t[rowId]: true\n\t};\n}\n","import { useCallback, useEffect, useEffectEvent, useRef, useState } from \"react\";\n\nimport { RowSelectionState } from \"@tanstack/react-table\";\n\nimport { pruneTableRowSelection, toggleTableRowSelection } from \"./selection\";\nimport { TableSelectionMode } from \"./types\";\n\ninterface UseTableRowSelectionParams<TData extends object> {\n\tavailableRowIds: readonly string[];\n\trowById: Map<string, TData>;\n\tselectionMode: TableSelectionMode;\n\tselectedRowIds?: readonly string[];\n\tonRowSelectionChange?: (rows: TData[]) => void;\n}\n\n/**\n * Общая модель выбора строк для плоской и древовидной таблиц.\n */\nexport function useTableRowSelection<TData extends object>({\n\tavailableRowIds,\n\trowById,\n\tselectionMode,\n\tselectedRowIds,\n\tonRowSelectionChange\n}: UseTableRowSelectionParams<TData>) {\n\tconst [rowSelection, setRowSelection] = useState<RowSelectionState>({});\n\tconst shouldSkipNextSelectionEmitRef = useRef(false);\n\n\t/**\n\t * Сравнивает состояния выбора без лишних перерисовок.\n\t */\n\tconst areSelectionsEqual = useCallback((left: RowSelectionState, right: RowSelectionState) => {\n\t\tconst leftRowIds = Object.keys(left).filter((rowId) => left[rowId]);\n\t\tconst rightRowIds = Object.keys(right).filter((rowId) => right[rowId]);\n\n\t\tif (leftRowIds.length !== rightRowIds.length) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn leftRowIds.every((rowId) => right[rowId]);\n\n\t\t/*\n\n Более оптимизированная версия, но для текущего случая может быть практически незначительной\n\n let leftCount = 0;\n\t\tlet rightCount = 0;\n\n\t\tfor (const rowId of Object.keys(left)) {\n\t\t\tif (!left[rowId]) continue;\n\t\t\tleftCount += 1;\n\t\t\tif (!right[rowId]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\tfor (const rowId of Object.keys(right)) {\n\t\t\tif (right[rowId]) {\n\t\t\t\trightCount += 1;\n\t\t\t}\n\t\t}\n\n\t\tif (leftCount !== rightCount) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\n */\n\t}, []);\n\n\tconst syncRowSelectionState = useEffectEvent((nextAvailableRowIds: readonly string[]) => {\n\t\tsetRowSelection((currentSelection) => pruneTableRowSelection(currentSelection, nextAvailableRowIds, selectionMode));\n\t});\n\n\tconst syncControlledRowSelectionState = useEffectEvent(\n\t\t(nextSelectedRowIds: readonly string[], nextAvailableRowIds: readonly string[]) => {\n\t\t\tconst nextSelection = pruneTableRowSelection(\n\t\t\t\tObject.fromEntries(nextSelectedRowIds.map((rowId) => [rowId, true])),\n\t\t\t\tnextAvailableRowIds,\n\t\t\t\tselectionMode\n\t\t\t);\n\n\t\t\tsetRowSelection((currentSelection) => {\n\t\t\t\tif (areSelectionsEqual(currentSelection, nextSelection)) {\n\t\t\t\t\treturn currentSelection;\n\t\t\t\t}\n\n\t\t\t\t// Пропускаем следующий emit, потому что это синхронизация от внешнего controlled-состояния,\n\t\t\t\t// а не пользовательское изменение выбора внутри таблицы.\n\t\t\t\tshouldSkipNextSelectionEmitRef.current = true;\n\n\t\t\t\treturn nextSelection;\n\t\t\t});\n\t\t}\n\t);\n\n\tconst emitRowSelectionChange = useEffectEvent((currentSelection: RowSelectionState) => {\n\t\tif (!onRowSelectionChange) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst selectedRows: TData[] = [];\n\n\t\tfor (const rowId of Object.keys(currentSelection)) {\n\t\t\tif (!currentSelection[rowId]) continue;\n\n\t\t\tconst row = rowById.get(rowId);\n\t\t\tif (row) {\n\t\t\t\tselectedRows.push(row);\n\t\t\t}\n\t\t}\n\n\t\tonRowSelectionChange(selectedRows);\n\t});\n\n\tuseEffect(() => {\n\t\tif (selectedRowIds !== undefined) {\n\t\t\tsyncControlledRowSelectionState(selectedRowIds, availableRowIds);\n\t\t\treturn;\n\t\t}\n\n\t\tsyncRowSelectionState(availableRowIds);\n\t}, [availableRowIds, selectedRowIds, selectionMode]);\n\n\tuseEffect(() => {\n\t\tif (shouldSkipNextSelectionEmitRef.current) {\n\t\t\tshouldSkipNextSelectionEmitRef.current = false;\n\t\t\treturn;\n\t\t}\n\n\t\temitRowSelectionChange(rowSelection);\n\t}, [rowSelection]);\n\n\t/**\n\t * Применяет изменение выбора при активации конкретной строки.\n\t */\n\tconst activateRowSelection = useCallback(\n\t\t(rowId: string, canSelect: boolean) => {\n\t\t\tif (selectionMode === \"none\" || !canSelect) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetRowSelection((currentSelection) => toggleTableRowSelection(currentSelection, rowId, selectionMode));\n\t\t},\n\t\t[selectionMode]\n\t);\n\n\treturn {\n\t\trowSelection,\n\t\tsetRowSelection,\n\t\tactivateRowSelection\n\t};\n}\n","import { TableColumnDef, TableColumnFormattingMeta, TableColumnMeta } from \"./types\";\n\n/**\n * Возвращает метаданные колонки таблицы.\n */\nexport function getTableColumnMeta<TData extends object>(column: TableColumnDef<TData>): TableColumnMeta | undefined {\n\treturn column.meta;\n}\n\n/**\n * Возвращает форматирующий блок метаданных колонки.\n */\nexport function getTableColumnFormattingMeta<TData extends object>(column: TableColumnDef<TData>): TableColumnFormattingMeta | undefined {\n\treturn column.meta?.formatting;\n}\n\n/**\n * Нормализует размер таблицы.\n *\n * Числовое значение трактуется как `em`, чтобы разметка масштабировалась вместе со шрифтом.\n */\nexport function resolveTableLength(length: string | number): string {\n\tif (typeof length === \"number\") {\n\t\treturn `${length}em`;\n\t}\n\n\treturn length;\n}\n\n/**\n * Определяет, пришёл ли клик из интерактивного элемента внутри таблицы.\n */\nexport function isTableInteractiveElement(target: EventTarget | null, extraSelector?: string): boolean {\n\tif (!(target instanceof HTMLElement)) {\n\t\treturn false;\n\t}\n\n\tconst baseSelector = 'button, a, input, select, textarea, [role=\"button\"], [role=\"link\"]';\n\tconst selector = extraSelector ? `${baseSelector}, ${extraSelector}` : baseSelector;\n\n\treturn Boolean(target.closest(selector));\n}\n"],"mappings":"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"}
|
package/dist/tree-table/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryuzaki13/react-foundation-lib",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "Reusable React and TypeScript foundation utilities, hooks, and primitives.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryuzaki13",
|
|
@@ -210,53 +210,53 @@
|
|
|
210
210
|
"registry": "https://registry.npmjs.org/"
|
|
211
211
|
},
|
|
212
212
|
"peerDependencies": {
|
|
213
|
-
"@dnd-kit/core": "
|
|
214
|
-
"@dnd-kit/sortable": "
|
|
215
|
-
"@fingerprintjs/fingerprintjs": "
|
|
216
|
-
"@floating-ui/react": "
|
|
217
|
-
"@tanstack/query-broadcast-client-experimental": "
|
|
218
|
-
"@tanstack/query-persist-client-core": "
|
|
219
|
-
"@tanstack/react-query": "
|
|
220
|
-
"@tanstack/react-router": "
|
|
221
|
-
"@tanstack/react-table": "
|
|
222
|
-
"@tanstack/react-virtual": "
|
|
223
|
-
"immer": "
|
|
224
|
-
"react": "
|
|
225
|
-
"write-excel-file": "
|
|
226
|
-
"zod": "
|
|
227
|
-
"zustand": "
|
|
213
|
+
"@dnd-kit/core": ">=6.3.0 <7.0.0",
|
|
214
|
+
"@dnd-kit/sortable": ">=10.0.0 <11.0.0",
|
|
215
|
+
"@fingerprintjs/fingerprintjs": ">=5.0.0 <6.0.0",
|
|
216
|
+
"@floating-ui/react": ">=0.27.0 <0.28.0",
|
|
217
|
+
"@tanstack/query-broadcast-client-experimental": ">=5.101.1 <6.0.0",
|
|
218
|
+
"@tanstack/query-persist-client-core": ">=5.101.1 <6.0.0",
|
|
219
|
+
"@tanstack/react-query": ">=5.101.1 <6.0.0",
|
|
220
|
+
"@tanstack/react-router": ">=1.170.0 <2.0.0",
|
|
221
|
+
"@tanstack/react-table": ">=8.21.0 <9.0.0",
|
|
222
|
+
"@tanstack/react-virtual": ">=3.14.0 <4.0.0",
|
|
223
|
+
"immer": ">=11.0.0 <12.0.0",
|
|
224
|
+
"react": ">=19.2.0 <20.0.0",
|
|
225
|
+
"write-excel-file": ">=4.0.0 <5.0.0",
|
|
226
|
+
"zod": ">=4.0.0 <5.0.0",
|
|
227
|
+
"zustand": ">=5.0.0 <6.0.0"
|
|
228
228
|
},
|
|
229
229
|
"devDependencies": {
|
|
230
230
|
"@dnd-kit/core": "^6.3.1",
|
|
231
231
|
"@dnd-kit/sortable": "^10.0.0",
|
|
232
232
|
"@fingerprintjs/fingerprintjs": "^5.2.0",
|
|
233
|
-
"@floating-ui/react": "^0.27.
|
|
234
|
-
"@tanstack/query-broadcast-client-experimental": "^5.101.
|
|
235
|
-
"@tanstack/query-persist-client-core": "^5.101.
|
|
236
|
-
"@tanstack/react-query": "^5.101.
|
|
237
|
-
"@tanstack/react-router": "^1.170.
|
|
233
|
+
"@floating-ui/react": "^0.27.20",
|
|
234
|
+
"@tanstack/query-broadcast-client-experimental": "^5.101.4",
|
|
235
|
+
"@tanstack/query-persist-client-core": "^5.101.4",
|
|
236
|
+
"@tanstack/react-query": "^5.101.4",
|
|
237
|
+
"@tanstack/react-router": "^1.170.18",
|
|
238
238
|
"@tanstack/react-table": "^8.21.3",
|
|
239
|
-
"@tanstack/react-virtual": "^3.14.
|
|
240
|
-
"@types/node": "^22.
|
|
241
|
-
"@types/react": "^19.2.
|
|
242
|
-
"@types/react-dom": "^19.2.
|
|
243
|
-
"eslint": "^10.
|
|
239
|
+
"@tanstack/react-virtual": "^3.14.9",
|
|
240
|
+
"@types/node": "^22.20.1",
|
|
241
|
+
"@types/react": "^19.2.18",
|
|
242
|
+
"@types/react-dom": "^19.2.4",
|
|
243
|
+
"eslint": "^10.8.0",
|
|
244
244
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
245
245
|
"eslint-plugin-import-x": "^4.17.1",
|
|
246
246
|
"eslint-plugin-prettier": "^5.5.6",
|
|
247
247
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
248
248
|
"eslint-plugin-react-refresh": "^0.5.3",
|
|
249
249
|
"fake-indexeddb": "^6.2.5",
|
|
250
|
-
"globals": "^17.
|
|
251
|
-
"immer": "^11.1.
|
|
250
|
+
"globals": "^17.8.0",
|
|
251
|
+
"immer": "^11.1.15",
|
|
252
252
|
"jiti": "^2.7.0",
|
|
253
253
|
"jsdom": "^29.1.1",
|
|
254
|
-
"react": "^19.2.
|
|
255
|
-
"react-dom": "^19.2.
|
|
254
|
+
"react": "^19.2.8",
|
|
255
|
+
"react-dom": "^19.2.8",
|
|
256
256
|
"typescript": "^6.0.3",
|
|
257
|
-
"typescript-eslint": "^8.
|
|
258
|
-
"vite": "^8.
|
|
259
|
-
"vitest": "^4.1.
|
|
257
|
+
"typescript-eslint": "^8.65.0",
|
|
258
|
+
"vite": "^8.2.0",
|
|
259
|
+
"vitest": "^4.1.10",
|
|
260
260
|
"write-excel-file": "^4.1.1",
|
|
261
261
|
"zod": "^4.4.3",
|
|
262
262
|
"zustand": "^5.0.14"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation Lib/Browser и React API" />
|
|
4
|
+
|
|
5
|
+
# Browser и React API
|
|
6
|
+
|
|
7
|
+
Эти entrypoint работают только после монтирования в браузере. Вызывайте hooks на верхнем уровне React-компонента или custom hook, а императивные действия (скачивание, clipboard, открытие меню) — из event handler. Они не заменяют UI primitives из `@ryuzaki13/react-foundation-ui`.
|
|
8
|
+
|
|
9
|
+
## Общие hooks
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
useDebounce,
|
|
14
|
+
useDebouncedCallback,
|
|
15
|
+
useDndSortableSensors,
|
|
16
|
+
useFloatingListbox,
|
|
17
|
+
useSearchLink,
|
|
18
|
+
useSearchParams,
|
|
19
|
+
useThrottledCallback
|
|
20
|
+
} from "@ryuzaki13/react-foundation-lib/hooks";
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| API | Назначение и lifecycle |
|
|
24
|
+
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `useDebounce(value, delay)` | Возвращает отложенное значение. Не запускает network запрос сам — используйте результат в owner Query/UI flow. |
|
|
26
|
+
| `useDebouncedCallback(callback, delay, { flushOnUnmount? })` | Возвращает debounced callback. Опция `flushOnUnmount` нужна только когда финальный вызов действительно обязателен. |
|
|
27
|
+
| `useThrottledCallback(callback, interval, { flushOnUnmount? })` | Возвращает `{ call, flush, cancel }`; стратегия last-write-wins. Вызывайте `flush` на границе сохранения, `cancel` при смене identity. |
|
|
28
|
+
| `useDndSortableSensors(distanceOrOptions?)` | Готовые Pointer/Keyboard sensors для `@dnd-kit/sortable`; default distance — 6 px. |
|
|
29
|
+
| `useLazyComponent(loader, ...)` | Используйте для lazy UI по устойчивому loader/cache key, а не вызывайте dynamic import во время render. |
|
|
30
|
+
| `useReferenceTrace(name, value)` | Development diagnostic для отслеживания смены ссылок; не часть бизнес-логики. |
|
|
31
|
+
| `useBlockBrowserZoom(enabled)` | Подписывается на browser events для блокировки zoom. Используйте только в явном специализированном UX, не как default. |
|
|
32
|
+
|
|
33
|
+
### Search и floating listbox
|
|
34
|
+
|
|
35
|
+
`useSearchParams<K>(allowedKeys?)` и `useSearchLink<K>()` требуют `@tanstack/react-router`. Первый возвращает `{ params, filteredParams, setParams }` и выполняет navigation с `replace: true`; второй возвращает `buildLink(params)`. Оба работают с URL state, а не с произвольным Zustand/local store.
|
|
36
|
+
|
|
37
|
+
`useFloatingListbox<T>(options)` требует `@floating-ui/react` и объединяет ref, ARIA-interactions, keyboard navigation и placement. Передавайте:
|
|
38
|
+
|
|
39
|
+
- `options`, `selectedIndex`, `getOptionDisabled` — данные и доступность опций;
|
|
40
|
+
- optional controlled `open` + `onOpenChange`;
|
|
41
|
+
- `onSelect`, `closeOnSelect`, `focusFloatingOnOpen`, `restoreFocusOnClose` — interaction policy;
|
|
42
|
+
- `placement` и `placementStrategy: "flip" | "auto"` — placement;
|
|
43
|
+
- `resolveFloatingSize(context)` — только layout/CSS size policy.
|
|
44
|
+
|
|
45
|
+
Hook возвращает Floating UI props/refs и их нужно распространять на reference, floating element и options строго по его контракту. Не заменяйте эти props самодельными keyboard handlers: это нарушит `aria-activedescendant`, focus restore и cleanup `autoUpdate`.
|
|
46
|
+
|
|
47
|
+
## DOM и overlay helpers
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import {
|
|
51
|
+
downloadFileFromBlob,
|
|
52
|
+
getOrCreatePortalRoot,
|
|
53
|
+
useClickOutside,
|
|
54
|
+
useEscapeDismiss,
|
|
55
|
+
useFocusTrap,
|
|
56
|
+
useIntersectionObserver,
|
|
57
|
+
useOverlayFocus
|
|
58
|
+
} from "@ryuzaki13/react-foundation-lib/dom";
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| API | Контракт |
|
|
62
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
|
|
63
|
+
| `downloadFileFromObjectURL(filename, objectUrl)` | Инициирует download по готовому object URL; не владеет временем `URL.revokeObjectURL`. |
|
|
64
|
+
| `downloadFileFromBlob(filename, blob)` | Создаёт временный object URL и инициирует download. |
|
|
65
|
+
| `downloadFileFromJson(filename, payload)` | Сериализует payload в JSON blob и скачивает. |
|
|
66
|
+
| `getOrCreatePortalRoot(id)` | Возвращает/создаёт DOM root или `null`, если `document` недоступен. |
|
|
67
|
+
| `useClickOutside(refOrRefs, handler)` | Вызывает handler при внешнем pointer interaction; передавайте стабильные refs. |
|
|
68
|
+
| `useEscapeDismiss(options)` | Dismiss только для active/enabled overlay; поддерживает container и целевой document. |
|
|
69
|
+
| `useFocusTrap(isOpen)` | Возвращает ref для trapping focus внутри открытого overlay. |
|
|
70
|
+
| `useOverlayFocus(options)` | Управляет initial focus и restore для overlay; применяйте вместе с accessible dialog/menu. |
|
|
71
|
+
| `useElementHeightObserver(ref)` | Следит за высотой через `ResizeObserver`; результат используйте для layout, не для копирования props в state. |
|
|
72
|
+
| `useIntersectionObserver(ref, options?)` | Возвращает intersection state DOM element. |
|
|
73
|
+
| `useIsTouchDevice()` | Реактивно сообщает touch-capability браузера. |
|
|
74
|
+
|
|
75
|
+
Hooks сами подписываются и очищают observers/listeners при unmount. Не вызывайте `getOrCreatePortalRoot` или browser API в server render; hook-проверки не делают UI SSR-safe автоматически.
|
|
76
|
+
|
|
77
|
+
## Clipboard, media и context menu
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { useCopyText, useCopyElementTextWithFeedback } from "@ryuzaki13/react-foundation-lib/copy";
|
|
81
|
+
import { useMediaQuery, useMatchMedia, resolveResponsiveValue } from "@ryuzaki13/react-foundation-lib/media";
|
|
82
|
+
import { createVirtualAnchor, openMenu } from "@ryuzaki13/react-foundation-lib/context-menu";
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `copy`
|
|
86
|
+
|
|
87
|
+
| API | Результат |
|
|
88
|
+
| ------------------------------------- | ----------------------------------------------------------------- |
|
|
89
|
+
| `useCopyText()` | Возвращает copy action для строки через Clipboard API с fallback. |
|
|
90
|
+
| `useElementText(ref)` | Читает текст из DOM element. |
|
|
91
|
+
| `useCopyElementText(ref)` | Комбинирует чтение элемента и copy. |
|
|
92
|
+
| `useCopyFeedback()` | Локальное временное состояние feedback после успешного copy. |
|
|
93
|
+
| `useCopyElementTextWithFeedback(ref)` | Удобная композиция copy element + feedback. |
|
|
94
|
+
|
|
95
|
+
Copy может быть отклонён browser permissions; показывайте ожидаемую UI-ошибку, но не считайте clipboard источником истины для сохранения данных.
|
|
96
|
+
|
|
97
|
+
### `media`
|
|
98
|
+
|
|
99
|
+
`BREAKPOINTS_EM` и `Breakpoint` задают стандартные breakpoints; `useMatchMedia()` возвращает `MediaMatches`. Для конкретного media query используйте `useMediaQuery(query, { ssrFallback? })`; `ssrFallback` определяет значение только до browser hydration/в окружении без `matchMedia`.
|
|
100
|
+
|
|
101
|
+
`ResponsiveValue<T>`, `isResponsiveMap`, `resolveResponsiveValue` и `resolveProps` — pure helpers для responsive props. `getCurrentFontSize`, `getCurrentLineHeight`, `getControlHeight`, `pxToEm` читают layout/DOM: не вызывайте их в render, если результат влияет на state без external synchronization.
|
|
102
|
+
|
|
103
|
+
### `context-menu`
|
|
104
|
+
|
|
105
|
+
`MenuAnchor`, `MenuState`, `MenuPoint`, `OpenMenuPayload` — serializable contracts состояния меню. `openMenu(payload)`, `closeMenu(state)` и `toggleMenu(state, payload)` — pure reducers; их можно держать в local/store owner.
|
|
106
|
+
|
|
107
|
+
Для pointer menu используйте `getMenuPointFromEvent(event)` и `createVirtualAnchor(point, contextElement?)`. Для keyboard anchor используйте `getMenuPointFromRect(rect)`. API лишь описывает состояние/anchor; отображение и accessibility menu остаются у UI-компонента.
|
|
108
|
+
|
|
109
|
+
## Virtualizer, PWA и Three.js
|
|
110
|
+
|
|
111
|
+
### Infinite virtualizer
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { useFetchNextPageEffect } from "@ryuzaki13/react-foundation-lib/virtualizer";
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Hook принимает `{ virtualItems, currentItemsCount, hasNextPage, fetchNextPage }`; при достижении последней виртуальной строки запускает один следующий fetch и не создаёт параллельные запросы. `fetchNextPage` должен быть стабильным Query action. Не передавайте raw `fetch` и не подменяйте backend state локальным массивом.
|
|
118
|
+
|
|
119
|
+
### Service Worker
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { parseSwCachePolicy, buildSwCachePolicyValue, useServiceWorkerUpdate } from "@ryuzaki13/react-foundation-lib/pwa";
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`useServiceWorkerUpdate()` возвращает `{ hasUpdate, applyUpdate, dismiss }`. Его задача — UI-поверхность для уже настроенного приложения с Service Worker; hook не публикует и не настраивает private registration API.
|
|
126
|
+
|
|
127
|
+
`pwa/sw-cache-policy` — отдельный точечный import того же pure policy API:
|
|
128
|
+
|
|
129
|
+
| API | Контракт |
|
|
130
|
+
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `parseSwCachePolicy(value)` | Парсит `off`, `ttl=<duration>` или `bust=<duration>` с `max`/`name`; invalid input даёт `undefined`. |
|
|
132
|
+
| `buildSwCachePolicyValue(draft)` | Нормализует `SwCachePolicyDraft` в canonical transport string. |
|
|
133
|
+
| `normalizeSwCacheName(value)` | Делает безопасное lower-case cache name или `undefined`. |
|
|
134
|
+
| `resolveSwCacheCacheNameByPolicy(value)` | Возвращает вычисленное имя cache только для `ttl`/`bust`. |
|
|
135
|
+
|
|
136
|
+
Пример: `ttl=24h;max=200;name=reference`. Это policy для transport/Service Worker, а не замена TanStack Query cache. Не включайте в SW cache auth-sensitive responses без отдельной security policy.
|
|
137
|
+
|
|
138
|
+
### Доступность Three.js
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { useThreeSceneAvailability, disableThreeSceneForSession } from "@ryuzaki13/react-foundation-lib/three-scene";
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
| API | Назначение |
|
|
145
|
+
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
146
|
+
| `isThreeSceneSupported(options?)` | Проверяет browser WebGL/WebGL2 support. |
|
|
147
|
+
| `isThreeSceneDisabledForSession(key?)`, `disableThreeSceneForSession(key?)` | Читают/устанавливают opt-out текущей tab session. |
|
|
148
|
+
| `useThreeSceneAvailability(options?)` | React state с `pending`/`available`/`unavailable` и причиной (`sessionDisabled`, `reducedMotion`, `webglUnsupported`, `ssr`). |
|
|
149
|
+
|
|
150
|
+
Проверка support не заменяет обработку ошибки renderer. При `unavailable` всегда рендерите семантический non-Three fallback.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation Lib/Файлы и Excel" />
|
|
4
|
+
|
|
5
|
+
# Файлы, бинарные данные и Excel
|
|
6
|
+
|
|
7
|
+
Эта страница описывает browser-only API для чтения пользовательских файлов, преобразования base64 и формирования `.xlsx`. Подготовка бизнес-полей, проверка прав доступа и отправка файла на сервер остаются ответственностью host-приложения.
|
|
8
|
+
|
|
9
|
+
## `file`: чтение файлов и HTTP-заголовки
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
createContentDispositionHeader,
|
|
14
|
+
readFile,
|
|
15
|
+
readImageFile,
|
|
16
|
+
resolveMimeTypeByExtension,
|
|
17
|
+
type ReadFileError,
|
|
18
|
+
type ReadImageError
|
|
19
|
+
} from "@ryuzaki13/react-foundation-lib/file";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Когда использовать
|
|
23
|
+
|
|
24
|
+
- `readFile` — получить `dataUrl` для preview или `ArrayBuffer` для дальнейшей обработки;
|
|
25
|
+
- `readImageFile` — то же для изображений с проверкой image MIME и размером изображения;
|
|
26
|
+
- helpers Content-Disposition — сформировать безопасный заголовок в browser-compatible/edge runtime коде;
|
|
27
|
+
- `resolveMimeTypeByExtension` — определить MIME по расширению, если содержимого для sniffing нет.
|
|
28
|
+
|
|
29
|
+
### `readFile`
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
readFile(file, options?): Promise<ReadFileResult>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Опция | Назначение |
|
|
36
|
+
| --------------------------------- | ------------------------------------------------- |
|
|
37
|
+
| `mode?: "data-url"` | Режим по умолчанию. Результат содержит `dataUrl`. |
|
|
38
|
+
| `mode: "array-buffer"` | Результат содержит `buffer`. |
|
|
39
|
+
| `allowedMime?: readonly string[]` | Точный список разрешённых MIME. |
|
|
40
|
+
| `maxBytes?: number` | Максимальный размер файла в байтах. |
|
|
41
|
+
| `signal?: AbortSignal` | Отменяет чтение через `FileReader.abort()`. |
|
|
42
|
+
|
|
43
|
+
Возвращаемый discriminated union всегда содержит `file` и `meta` (`name`, `mime`, `size`, `lastModified`). После проверки `result.mode` TypeScript раскрывает либо `dataUrl`, либо `buffer`.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const result = await readFile(file, {
|
|
47
|
+
mode: "data-url",
|
|
48
|
+
allowedMime: ["application/pdf"],
|
|
49
|
+
maxBytes: 10 * 1024 * 1024,
|
|
50
|
+
signal
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (result.mode === "data-url") {
|
|
54
|
+
previewUrl = result.dataUrl;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`ReadFileError` имеет предсказуемый `code`: `NO_FILE`, `MIME_NOT_ALLOWED`, `FILE_TOO_LARGE`, `READ_ABORTED` или `READ_FAILED`. Обрабатывайте его как ожидаемую ошибку выбора файла, а остальные ошибки не скрывайте.
|
|
59
|
+
|
|
60
|
+
### `readImageFile`
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
readImageFile(image, options?): Promise<ReadImageResult>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Поддерживаемые `ImageMime`: `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `image/svg+xml`, `image/avif`. В режиме `data-url` результат дополнительно включает `dimensions` (`width`, `height`). В режиме `array-buffer` декодирование изображения не выполняется.
|
|
67
|
+
|
|
68
|
+
`ReadImageError` расширяет коды `ReadFileError` значениями `NOT_AN_IMAGE` и `IMAGE_DECODE_FAILED`.
|
|
69
|
+
|
|
70
|
+
### Дополнительный API `file`
|
|
71
|
+
|
|
72
|
+
| API | Контракт |
|
|
73
|
+
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
74
|
+
| `buildMeta(file)` | Возвращает serializable метаданные `FileMeta`. |
|
|
75
|
+
| `attachAbort(signal, reader)` | Привязывает отмену к `FileReader`; возвращённую функцию cleanup вызывают только custom consumers низкоуровневого API. |
|
|
76
|
+
| `assertNotAborted(signal?)` | Бросает `ReadFileError("READ_ABORTED")`, если сигнал уже отменён. |
|
|
77
|
+
| `assertValidAllowedMime`, `assertValidAccept` | Проверяют конфигурацию accept/MIME до чтения. |
|
|
78
|
+
| `sanitizeFileName(value, fallback?)` | Удаляет unsafe символы из имени. |
|
|
79
|
+
| `encodeFilenameRFC5987(value)` | Кодирует Unicode filename для HTTP header. |
|
|
80
|
+
| `createAsciiFilenameFallback(value, fallback?)` | Создаёт ASCII fallback для старых consumers заголовка. |
|
|
81
|
+
| `createContentDispositionHeader(options)` | Создаёт `inline`/`attachment` Content-Disposition с `filename` и `filename*`. |
|
|
82
|
+
|
|
83
|
+
## `binary`: base64 → `Blob`
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { binaryToBlob, detectMimeType, useBinaryFile } from "@ryuzaki13/react-foundation-lib/binary";
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| API | Вход и результат |
|
|
90
|
+
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
91
|
+
| `binaryToBlob(binaryData, mimeType?, isBase64?)` | Создаёт `Blob` из base64/data URL (по умолчанию) или raw binary string (`false`). Пустые или некорректные данные бросают `Error`. |
|
|
92
|
+
| `detectMimeType(fileName?, binaryData?)` | Ищет MIME по расширению, data URL, затем сигнатуре base64. Неопределённый документ трактует как `application/pdf`. |
|
|
93
|
+
| `useBinaryFile(bin)` | React hook, возвращает `{ data: { blob, mime } \| null, error: string \| null }`. |
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const mime = detectMimeType("report.pdf", base64);
|
|
97
|
+
const blob = binaryToBlob(base64, mime);
|
|
98
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
99
|
+
|
|
100
|
+
// Владелец UI обязан освободить URL при смене файла/размонтировании.
|
|
101
|
+
URL.revokeObjectURL(objectUrl);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`useBinaryFile` не создаёт object URL и не освобождает его: это lifecycle конкретного preview-компонента. Hook использует `Blob` и `atob`, поэтому не вызывайте его в SSR или worker без совместимого polyfill.
|
|
105
|
+
|
|
106
|
+
## `excel`: скачивание `.xlsx`
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import {
|
|
110
|
+
buildExcelAutoFilterRef,
|
|
111
|
+
buildExcelSheetData,
|
|
112
|
+
createTableExcelColumn,
|
|
113
|
+
downloadExcelFile,
|
|
114
|
+
downloadExcelWorkbook,
|
|
115
|
+
downloadResolvedExcelTable,
|
|
116
|
+
downloadResolvedExcelWorkbook,
|
|
117
|
+
resolveExcelCellStyleFromState,
|
|
118
|
+
resolveExcelSheetNames
|
|
119
|
+
} from "@ryuzaki13/react-foundation-lib/excel";
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Базовый экспорт
|
|
123
|
+
|
|
124
|
+
`downloadExcelFile({ fileName, columns, rows, autoFilter?, getCellStyle? })` создаёт один лист; `downloadExcelWorkbook({ fileName, sheets })` — книгу из нескольких листов. Это asynchronous browser API, который инициирует скачивание через `write-excel-file`.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
await downloadExcelFile({
|
|
128
|
+
fileName: "users.xlsx",
|
|
129
|
+
columns: [
|
|
130
|
+
{ id: "name", header: "Имя", type: String, width: 28 },
|
|
131
|
+
{ id: "createdAt", header: "Дата", type: Date, format: "dd.mm.yyyy" }
|
|
132
|
+
],
|
|
133
|
+
rows: [{ name: "Анна", createdAt: new Date("2026-07-30") }],
|
|
134
|
+
autoFilter: true
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
| Public type/API | Назначение |
|
|
139
|
+
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
140
|
+
| `ExcelColumn`, `ExcelRow`, `ExcelCellStyle` | Контракты столбца, строки и стиля ячейки. `type` передаёт в файл истинные `Number`/`Date`/`Boolean`, а не UI-строку. |
|
|
141
|
+
| `buildExcelSheetData(args)` | Строит данные листа без запуска download; подходит для тестируемой подготовки. |
|
|
142
|
+
| `buildExcelAutoFilterRef(columns, rows)` | Даёт Excel-диапазон или `undefined` для пустой сетки. |
|
|
143
|
+
| `resolveExcelSheetNames(names)` | Нормализует ограничения Excel (31 символ, forbidden chars) и делает имена уникальными. |
|
|
144
|
+
| `resolveExcelCellStyleFromState(state)` | Преобразует `State` в базовый цвет текста Excel. |
|
|
145
|
+
|
|
146
|
+
### Экспорт подготовленной таблицы
|
|
147
|
+
|
|
148
|
+
`TableExcelColumnDescriptor` связывает generic Excel column с OData type/role и необязательным `formattersPipeline`. Используйте `createTableExcelColumn(descriptor)` для получения типизированного столбца, затем передавайте значения строк без предварительного UI-форматирования.
|
|
149
|
+
|
|
150
|
+
| API | Назначение |
|
|
151
|
+
| -------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
152
|
+
| `resolveTableExcelColumnType(descriptor)` | Выбирает `String`/`Number`/`Date`/`Boolean` по OData metadata и роли. |
|
|
153
|
+
| `createTableExcelColumn(descriptor)` | Создаёт `ExcelColumn` с Excel-маской числа/даты из published formatter contracts. |
|
|
154
|
+
| `downloadResolvedExcelTable({ fileName, table, autoFilter? })` | Скачивает одну уже подготовленную таблицу. |
|
|
155
|
+
| `downloadResolvedExcelWorkbook({ fileName, sheets })` | Скачивает несколько `ResolvedExcelTable` как листы книги. |
|
|
156
|
+
|
|
157
|
+
Не передавайте в `rows` отформатированные через `Intl` строки для числовых и date-столбцов: Excel потеряет сортировку, формулы и формат. Строки создавайте только для действительно текстовых значений.
|
|
158
|
+
|
|
159
|
+
## Ограничения
|
|
160
|
+
|
|
161
|
+
- Все три entrypoint ориентированы на DOM/browser; не вызывайте их в server-only коде.
|
|
162
|
+
- MIME по имени/сигнатуре не является проверкой безопасности содержимого. Сервер обязан валидировать загружаемые файлы повторно.
|
|
163
|
+
- `download*` имеет пользовательский side effect. Вызывайте его из event handler, а не из render/effect при монтировании.
|