From 7b8072d049eda78c1acd368d24056f7834929da1 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Fri, 31 Jul 2026 04:21:34 +0200 Subject: [PATCH] feat: complete shared platform UI contracts --- README.md | 3 + docs/DATAGRID_SIZING_CONTRACT.md | 63 ++++ src/govoplan_core/core/modules.py | 16 + src/govoplan_core/core/registry.py | 22 ++ tests/test_documentation_topic_contract.py | 34 ++ webui/package-lock.json | 20 ++ webui/package.json | 1 + webui/scripts/test-module-permutations.mjs | 5 +- webui/src/components/WizardDirectory.tsx | 67 ++++ webui/src/components/table/DataGrid.tsx | 309 +++++-------------- webui/src/components/table/dataGridSizing.ts | 252 +++++++++++++-- webui/src/index.ts | 3 + webui/src/platform/wizards.ts | 31 ++ webui/src/styles/layout.css | 15 + webui/src/styles/tables.css | 9 - webui/src/types.ts | 32 ++ webui/tests/data-grid-actions.test.tsx | 18 ++ webui/tests/data-grid-sizing.test.ts | 147 ++++++++- webui/vite.config.ts | 2 + 19 files changed, 764 insertions(+), 285 deletions(-) create mode 100644 docs/DATAGRID_SIZING_CONTRACT.md create mode 100644 webui/src/components/WizardDirectory.tsx create mode 100644 webui/src/platform/wizards.ts diff --git a/README.md b/README.md index 64d7f2e..0c7d65a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ Core owns: - kernel APIs for platform metadata, module lifecycle, health, and development diagnostics - `@govoplan/core-webui`, including login, CSRF/API helpers, shell layout, generic UI components, IconRail, DataGrid, access boundaries, and module route/nav contracts +The shared DataGrid sizing and resize invariants are specified in +[`docs/DATAGRID_SIZING_CONTRACT.md`](docs/DATAGRID_SIZING_CONTRACT.md). + Platform and feature modules own their backend routers, models, migrations, permissions, frontend packages, nav items, and route contributions. Access, tenancy, policy, audit, and admin behavior live in their owning platform diff --git a/docs/DATAGRID_SIZING_CONTRACT.md b/docs/DATAGRID_SIZING_CONTRACT.md new file mode 100644 index 0000000..94d9f47 --- /dev/null +++ b/docs/DATAGRID_SIZING_CONTRACT.md @@ -0,0 +1,63 @@ +# DataGrid Sizing Contract + +`DataGrid` turns every declared track into a deterministic pixel layout after +its container has a measurable width. The same contract is used on initial +layout, container resize, persisted-layout restore, and pointer resize. + +## Column Declarations + +- `width: number` or `Npx` is the preferred pixel width. +- `width: N%` is a preferred share of the measured container. +- `width: Nfr` shares residual width by fraction weight. +- `width: minmax(Npx, preferred)` combines a hard lower bound with any + supported preferred width. +- An omitted width and the legacy `fill` flag are one-fraction flexible tracks. +- `minWidth` is a hard floor. Header sort, filter, and resize controls may raise + the effective accessible floor. +- `maxWidth` bounds direct user growth and free/constrained compensation. In a + cover layout it is a preferred maximum: passive tracks may exceed it when + that is necessary to keep the table flush with its container. + +## Layout Modes + +| `initialFit` | `resizeBehavior` | Initial layout | Pointer resize | +| --- | --- | --- | --- | +| `container` | `cover` | Real columns fill the container. Hard-minimum excess scrolls horizontally. | The active change is compensated by ordinary peer columns. Passive maxima are soft; no blank filler column is created. | +| `content` | `cover` | After measurement, the same cover invariant applies. | Same as cover above. | +| `container` | `constrained` | Real columns fill the container. | Peer compensation respects every hard min/max and stops the active resize when capacity is exhausted. | +| `content` | `free` | Declared content widths are retained. | Only the active column changes, so underflow or horizontal overflow is allowed. | +| `container` | `free` | The initial layout fills the container. | Later user resizing is free. A right-sticky column promotes this mode to cover so its edge remains stable. | + +Sticky columns do not absorb ordinary cover residuals and are not resize +compensation targets. A last resizable column compensates against columns to +its left, so it follows the same right-edge invariant as earlier columns. + +## Persistence + +Only the pixel layout resulting from an explicit user resize is persisted. +Persisted widths are keyed by a signature containing column IDs, declared +widths and bounds, resize affordances, sticky placement, initial fit, and resize +behavior. A changed signature discards the old override and recomputes the +declared layout. + +Container reconciliation is suspended while a pointer drag is active. On +release, the already-rendered pixel layout becomes the persisted preference; +there is no second fit pass and therefore no drag-end snap. + +## Regression Matrix + +`webui/tests/data-grid-sizing.test.ts` covers: + +- pixel, percentage, fraction, `minmax`, omitted, and legacy-fill tracks; +- preferred max exhaustion without a synthetic filler column; +- hard-minimum horizontal overflow; +- fixed-only cover grids; +- persisted overrides under growth and viewport pressure; +- stale layout signatures; +- first, middle, and last-column peer compensation; +- free, cover, and constrained resizing; +- cover-expanded tracks that already exceed preferred maxima; and +- preservation of the pointer layout across the commit fit. + +`webui/tests/data-grid-actions.test.tsx` also verifies the rendered fixed-cover +shape and guards against reintroducing a synthetic buffer cell. diff --git a/src/govoplan_core/core/modules.py b/src/govoplan_core/core/modules.py index ff80e2a..a1e01d9 100644 --- a/src/govoplan_core/core/modules.py +++ b/src/govoplan_core/core/modules.py @@ -222,11 +222,13 @@ DocumentationSourceKind = Literal[ "route", "capability", "policy", + "release_catalog", "configuration_package", "wiki", "repository", ] DocumentationSourceState = Literal["configured", "disabled", "unavailable"] +CapabilityStability = Literal["experimental", "stable", "deprecated"] @dataclass(frozen=True, slots=True) @@ -336,12 +338,25 @@ class DocumentationSourceDefinition: documentation_types: tuple[DocumentationType, ...] = ("admin",) condition: DocumentationCondition = field(default_factory=DocumentationCondition) state: DocumentationSourceState = "configured" + state_reason: str | None = None provenance: Mapping[str, Any] = field(default_factory=dict) inspection: Mapping[str, Any] = field(default_factory=dict) link: DocumentationLink | None = None configuration_key: str | None = None +@dataclass(frozen=True, slots=True) +class CapabilityDocumentation: + """Provider-owned capability metadata safe for generic platform consumers.""" + + label: str + summary: str + contract_version: str | None = None + stability: CapabilityStability = "stable" + documentation_types: tuple[DocumentationType, ...] = ("admin",) + audience: tuple[str, ...] = () + + class ResourceAclProvider(Protocol): resource_type: str @@ -412,6 +427,7 @@ class ModuleManifest: delete_veto_providers: Mapping[str, Sequence[DeleteVetoProvider]] = field(default_factory=dict) uninstall_guard_providers: tuple[UninstallGuardProvider, ...] = () capability_factories: Mapping[str, CapabilityFactory] = field(default_factory=dict) + capability_documentation: Mapping[str, CapabilityDocumentation] = field(default_factory=dict) search_providers: tuple["SearchProviderRegistration", ...] = () search_sources: tuple["SearchSourceProviderRegistration", ...] = () compatibility: ModuleCompatibility = field(default_factory=ModuleCompatibility) diff --git a/src/govoplan_core/core/registry.py b/src/govoplan_core/core/registry.py index 74cc406..b9f8770 100644 --- a/src/govoplan_core/core/registry.py +++ b/src/govoplan_core/core/registry.py @@ -614,6 +614,28 @@ def _validate_manifest_shape(manifest: ModuleManifest) -> None: def _validate_documentation_extensions(manifest: ModuleManifest) -> None: + for capability, metadata in manifest.capability_documentation.items(): + if capability not in manifest.capability_factories: + raise RegistryError( + f"Module {manifest.id!r} documents capability {capability!r} " + "but does not provide it" + ) + if not metadata.label.strip(): + raise RegistryError( + f"Module {manifest.id!r} capability {capability!r} " + "documentation must have a label" + ) + if not metadata.summary.strip(): + raise RegistryError( + f"Module {manifest.id!r} capability {capability!r} " + "documentation must have a summary" + ) + if metadata.contract_version is not None and not metadata.contract_version.strip(): + raise RegistryError( + f"Module {manifest.id!r} capability {capability!r} " + "documentation contract version must not be empty" + ) + provider_keys: set[str] = set() for registration in manifest.documentation_configuration_providers: if not registration.keys: diff --git a/tests/test_documentation_topic_contract.py b/tests/test_documentation_topic_contract.py index f9a7af7..612a440 100644 --- a/tests/test_documentation_topic_contract.py +++ b/tests/test_documentation_topic_contract.py @@ -3,6 +3,7 @@ from __future__ import annotations import unittest from govoplan_core.core.modules import ( + CapabilityDocumentation, DocumentationConfigurationDecision, DocumentationConfigurationProviderRegistration, DocumentationCondition, @@ -116,6 +117,39 @@ class DocumentationTopicContractTests(unittest.TestCase): with self.assertRaisesRegex(RegistryError, "duplicate documentation configuration key"): duplicate.validate() + def test_capability_documentation_is_typed_and_must_match_a_provider(self) -> None: + registry = PlatformRegistry() + registry.register(ModuleManifest( + id="example", + name="Example", + version="1.0.0", + capability_factories={"example.lookup": lambda _context: object()}, + capability_documentation={ + "example.lookup": CapabilityDocumentation( + label="Example lookup", + summary="Resolves example records without exposing provider internals.", + contract_version="2", + audience=("module_admin",), + ), + }, + )) + registry.validate() + + missing_provider = PlatformRegistry() + missing_provider.register(ModuleManifest( + id="example", + name="Example", + version="1.0.0", + capability_documentation={ + "example.lookup": CapabilityDocumentation( + label="Example lookup", + summary="Resolves example records.", + ), + }, + )) + with self.assertRaisesRegex(RegistryError, "does not provide it"): + missing_provider.validate() + def registry_for(*topics: DocumentationTopic) -> PlatformRegistry: registry = PlatformRegistry() diff --git a/webui/package-lock.json b/webui/package-lock.json index e0aa541..be0ab35 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -29,6 +29,7 @@ "@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui", "@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui", "@govoplan/search-webui": "file:../../govoplan-search/webui", + "@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui", "@govoplan/views-webui": "file:../../govoplan-views/webui", "@govoplan/workflow-webui": "file:../../govoplan-workflow/webui", "@tiptap/core": "^3.29.2", @@ -435,6 +436,21 @@ } } }, + "../../govoplan-tenancy/webui": { + "name": "@govoplan/tenancy-webui", + "version": "0.1.8", + "peerDependencies": { + "@govoplan/core-webui": "^0.1.14", + "lucide-react": "^1.23.0", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20" + }, + "peerDependenciesMeta": { + "@govoplan/core-webui": { + "optional": true + } + } + }, "../../govoplan-views/webui": { "name": "@govoplan/views-webui", "version": "0.1.0", @@ -1305,6 +1321,10 @@ "resolved": "../../govoplan-search/webui", "link": true }, + "node_modules/@govoplan/tenancy-webui": { + "resolved": "../../govoplan-tenancy/webui", + "link": true + }, "node_modules/@govoplan/views-webui": { "resolved": "../../govoplan-views/webui", "link": true diff --git a/webui/package.json b/webui/package.json index 1b7cbe5..0e7c31a 100644 --- a/webui/package.json +++ b/webui/package.json @@ -68,6 +68,7 @@ "@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui", "@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui", "@govoplan/search-webui": "file:../../govoplan-search/webui", + "@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui", "@govoplan/views-webui": "file:../../govoplan-views/webui", "@govoplan/workflow-webui": "file:../../govoplan-workflow/webui", "@tiptap/core": "^3.29.2", diff --git a/webui/scripts/test-module-permutations.mjs b/webui/scripts/test-module-permutations.mjs index 26d59be..f3519d4 100644 --- a/webui/scripts/test-module-permutations.mjs +++ b/webui/scripts/test-module-permutations.mjs @@ -23,6 +23,7 @@ const packageByModule = { risk_compliance: "@govoplan/risk-compliance-webui", scheduling: "@govoplan/scheduling-webui", search: "@govoplan/search-webui", + tenancy: "@govoplan/tenancy-webui", views: "@govoplan/views-webui", workflow: "@govoplan/workflow-webui" }; @@ -30,6 +31,8 @@ const packageByModule = { const cases = [ { name: "core-only", modules: [] }, { name: "access-only", modules: ["access"] }, + { name: "tenancy-only", modules: ["tenancy"] }, + { name: "access-with-tenancy", modules: ["access", "tenancy"] }, { name: "admin-only", modules: ["admin"] }, { name: "addresses-only", modules: ["addresses"] }, { name: "access-with-admin", modules: ["access", "admin"] }, @@ -58,7 +61,7 @@ const cases = [ { name: "search-only", modules: ["search"] }, { name: "risk-compliance-only", modules: ["risk_compliance"] }, { name: "docs-and-ops", modules: ["access", "docs", "ops"] }, - { name: "full-product", modules: ["access", "admin", "addresses", "policy", "audit", "dashboard", "datasources", "dataflow", "workflow", "views", "organizations", "idm", "campaigns", "files", "mail", "notifications", "docs", "ops", "calendar", "scheduling", "postbox", "risk_compliance", "search"] } + { name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "policy", "audit", "dashboard", "datasources", "dataflow", "workflow", "views", "organizations", "idm", "campaigns", "files", "mail", "notifications", "docs", "ops", "calendar", "scheduling", "postbox", "risk_compliance", "search"] } ]; const npmExec = process.env.npm_execpath; diff --git a/webui/src/components/WizardDirectory.tsx b/webui/src/components/WizardDirectory.tsx new file mode 100644 index 0000000..d9f0bb9 --- /dev/null +++ b/webui/src/components/WizardDirectory.tsx @@ -0,0 +1,67 @@ +import { Link } from "react-router"; + +import type { AuthInfo, WizardDirectoryEntry } from "../types"; +import { usePlatformLanguage } from "../i18n/LanguageContext"; +import { hasAnyScope, hasScope } from "../utils/permissions"; +import Card from "./Card"; + +export type WizardDirectoryProps = { + entries: WizardDirectoryEntry[]; + auth: AuthInfo; + emptyText?: string; +}; + +function canOpenWizard(auth: AuthInfo, entry: WizardDirectoryEntry): boolean { + if (entry.available === false) return false; + if (entry.allOf?.some((scope) => !hasScope(auth, scope))) return false; + if (entry.anyOf?.length && !hasAnyScope(auth, entry.anyOf)) return false; + return true; +} + +export default function WizardDirectory({ + entries, + auth, + emptyText = "No guided workflows are available." +}: WizardDirectoryProps) { + const { translateText } = usePlatformLanguage(); + const visibleEntries = entries.filter( + (entry) => + !entry.anyOf?.length + || hasAnyScope(auth, entry.anyOf) + || entry.available === false + ); + + if (!visibleEntries.length) { + return

{translateText(emptyText)}

; + } + + return ( +
+ {visibleEntries.map((entry) => { + const available = canOpenWizard(auth, entry); + return ( + + {translateText(entry.actionLabel ?? "Open")} + + ) + : null + } + > + {entry.description &&

{translateText(entry.description)}

} + {!available && entry.unavailableReason && ( +

+ {translateText(entry.unavailableReason)} +

+ )} +
+ ); + })} +
+ ); +} diff --git a/webui/src/components/table/DataGrid.tsx b/webui/src/components/table/DataGrid.tsx index dcd0832..4512a6b 100644 --- a/webui/src/components/table/DataGrid.tsx +++ b/webui/src/components/table/DataGrid.tsx @@ -5,20 +5,17 @@ import StatusBadge from "../StatusBadge"; import TableActionGroup from "./TableActionGroup"; import { usePlatformLanguage, i18nMessage } from "../../i18n/LanguageContext"; import { - DATA_GRID_MAX_TRACK_WIDTH as BUFFER_MAX_WIDTH, + DATA_GRID_MAX_TRACK_WIDTH, dataGridColumnPixelWidth as columnPixelWidth, - dataGridColumnResizeWeight, dataGridColumnTrackWithMinimum as columnTrackWithMinimum, dataGridLayoutSignature, dataGridWidthsForLayout, - distributeDataGridResizeAmount as distributeResizeAmount, effectiveDataGridColumnMaxWidth as effectiveColumnMaxWidth, effectiveDataGridColumnMinWidth as effectiveColumnMinWidth, fitDataGridColumns, isFlexibleDataGridWidth as isFlexibleWidth, - roundDataGridWidths as roundWidthRecord, - totalDataGridColumnWidth as totalColumnWidth, - type DataGridResizeTarget as ResizeTarget + resizeDataGridColumn, + roundDataGridWidths as roundWidthRecord } from "./dataGridSizing"; export type DataGridSortDirection = "asc" | "desc"; @@ -89,7 +86,10 @@ export type DataGridColumn = { width?: number | string; /** Hard lower bound. Header controls may require a larger accessible minimum. */ minWidth?: number; - /** Hard upper bound. Space left after all maxima is assigned to the neutral buffer track. */ + /** + * Upper bound for direct resizing. A cover layout may exceed preferred + * maxima only when that is required to consume the complete container. + */ maxWidth?: number; resizable?: boolean; /** @deprecated Kept for source compatibility. Resize space is now distributed across resizable columns. */ @@ -119,8 +119,6 @@ type DataGridState = { layoutSignature?: string; /** Legacy layout state; removed when persisted state is sanitized. */ fillColumnId?: string | null; - /** Width of the synthetic, handle-less coverage track. */ - bufferWidth?: number; }; type DataGridBaseProps = { @@ -136,7 +134,11 @@ type DataGridBaseProps = { initialFit?: DataGridInitialFit; /** @deprecated Use initialFit instead. */ fit?: DataGridInitialFit; - /** Defaults to cover. Free is automatically promoted to cover when a right-sticky column exists. */ + /** + * Cover keeps the right edge fixed by redistributing every resize across + * real columns. Free permits underflow and horizontal growth. Free is + * automatically promoted to cover when a right-sticky column exists. + */ resizeBehavior?: DataGridResizeBehavior; className?: string; rowClassName?: (row: T, index: number) => string | undefined; @@ -190,28 +192,14 @@ type FilterPosition = { type ColumnResizeState = { columnId: string; startX: number; - startWidth: number; - minWidth: number; - maxWidth: number; baseWidths: Record; - rightResizeTargets: ResizeTarget[]; - bufferTarget?: ResizeTarget; - bufferStartWidth: number; - shrinkRoomWithoutScroll: number; - /** - * The last resizable column may consume overflow on either side of the - * current viewport. As its track shrinks, the browser clamps scrollLeft and - * moves the viewport left, while the grid still covers the complete surface. - */ - lastResizableShrinkRoom: number; - isLastResizableColumn: boolean; + uncompensatedShrinkRoom: number; behavior: DataGridResizeBehavior; }; const STORAGE_PREFIX = "govoplan.datagrid."; const FILTER_POPOVER_WIDTH = 320; const FILTER_POPOVER_MARGIN = 12; -const BUFFER_COLUMN_ID = "__data_grid_resize_buffer__"; export default function DataGrid({ id, @@ -241,8 +229,6 @@ export default function DataGrid({ "cover" : resizeBehavior; const layoutSignature = dataGridLayoutSignature(columns, resolvedInitialFit, effectiveResizeBehavior); - const bufferInsertIndex = findBufferInsertIndex(columns); - const containerResizeColumns = columns.filter(isContainerResizeColumn); const localStorageKey = storageKey ?? `${STORAGE_PREFIX}${id}`; const initialFiltersKey = JSON.stringify(initialFilters); @@ -368,31 +354,33 @@ export default function DataGrid({ const userWidths = signatureMatches ? current.userWidths ?? {} : {}; const mustFit = effectiveResizeBehavior !== "free" || resolvedInitialFit === "container"; if (!mustFit) { - if (signatureMatches && current.widths === undefined && current.bufferWidth === undefined) return current; + if (signatureMatches && current.widths === undefined) return current; return { ...current, widths: undefined, userWidths, layoutSignature, - fillColumnId: undefined, - bufferWidth: undefined + fillColumnId: undefined }; } - const layout = fitDataGridColumns(columns, nextContainerWidth, measuredWidths, userWidths); - const nextBufferWidth = normalizeBufferWidth(layout.bufferWidth, containerResizeColumns.length === 0); + const layout = fitDataGridColumns( + columns, + nextContainerWidth, + measuredWidths, + userWidths, + effectiveResizeBehavior + ); if ( signatureMatches && shallowEqualNumberRecords(current.widths ?? {}, layout.widths) - && nextBufferWidth === current.bufferWidth ) return current; return { ...current, widths: layout.widths, userWidths, layoutSignature, - fillColumnId: undefined, - bufferWidth: nextBufferWidth + fillColumnId: undefined }; }); }); @@ -421,57 +409,25 @@ export default function DataGrid({ function onMove(event: MouseEvent) { const rawDelta = event.clientX - activeResize.startX; - const activeDelta = Math.min( - activeResize.maxWidth - activeResize.startWidth, - Math.max(activeResize.minWidth - activeResize.startWidth, rawDelta) + const resized = resizeDataGridColumn( + columns, + activeResize.baseWidths, + activeResize.columnId, + rawDelta, + activeResize.behavior, + activeResize.uncompensatedShrinkRoom ); - const nextWidths = { ...activeResize.baseWidths }; - let nextBufferWidth = activeResize.bufferStartWidth; - let appliedActiveDelta = activeDelta; - - if (activeResize.behavior === "cover" && activeDelta < 0) { - const requestedShrink = -activeDelta; - const availableUncompensatedShrink = activeResize.isLastResizableColumn ? - activeResize.lastResizableShrinkRoom : - activeResize.shrinkRoomWithoutScroll; - const freeShrink = Math.min(requestedShrink, availableUncompensatedShrink); - const compensationNeeded = requestedShrink - freeShrink; - const distribution = distributeWithFallback( - compensationNeeded, - activeResize.rightResizeTargets, - activeResize.bufferTarget - ); - appliedActiveDelta = -(freeShrink + distribution.applied); - const applied = applyResizeAmounts(nextWidths, nextBufferWidth, distribution.amounts); - nextBufferWidth = applied.bufferWidth; - } else if (activeResize.behavior === "cover" && activeDelta > 0 && activeResize.bufferTarget) { - // The buffer is residual cover space, not user-sized content. Growing a - // real column consumes that track before the table itself gets wider. - const distribution = distributeResizeAmount(-activeDelta, [activeResize.bufferTarget]); - const applied = applyResizeAmounts(nextWidths, nextBufferWidth, distribution.amounts); - nextBufferWidth = applied.bufferWidth; - } else if (activeResize.behavior === "constrained") { - const distribution = distributeWithFallback( - -activeDelta, - activeResize.rightResizeTargets, - activeResize.bufferTarget - ); - appliedActiveDelta = -distribution.applied; - const applied = applyResizeAmounts(nextWidths, nextBufferWidth, distribution.amounts); - nextBufferWidth = applied.bufferWidth; - } - - nextWidths[activeResize.columnId] = activeResize.startWidth + appliedActiveDelta; setState((current) => ({ ...current, - widths: roundWidthRecord(nextWidths), - userWidths: { - ...(current.layoutSignature === layoutSignature ? current.userWidths ?? {} : {}), - [activeResize.columnId]: Math.round(nextWidths[activeResize.columnId] * 100) / 100 - }, + widths: resized.widths, + userWidths: activeResize.behavior === "free" + ? { + ...(current.layoutSignature === layoutSignature ? current.userWidths ?? {} : {}), + [activeResize.columnId]: resized.widths[activeResize.columnId] + } + : resized.widths, layoutSignature, - fillColumnId: undefined, - bufferWidth: normalizeBufferWidth(nextBufferWidth, containerResizeColumns.length === 0) + fillColumnId: undefined })); } @@ -491,7 +447,7 @@ export default function DataGrid({ window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; - }, [resizeState, columns, effectiveResizeBehavior, containerResizeColumns.length, layoutSignature]); + }, [resizeState, columns, effectiveResizeBehavior, layoutSignature]); useEffect(() => { if (!openFilterColumnId) return undefined; @@ -594,13 +550,8 @@ export default function DataGrid({ if (pagination && pagination.page !== paginationPage) pagination.onPageChange(paginationPage); }, [pagination, paginationPage]); - const bufferWidth = Math.max(0, state.bufferWidth ?? 0); - const shouldRenderBuffer = effectiveResizeBehavior !== "free" && ( - containerResizeColumns.length === 0 || bufferWidth > 0.01); const actualTracks = columns.map((column) => widthForColumn(column, state.widths?.[column.id])); - const templateTracks = [...actualTracks]; - if (shouldRenderBuffer) templateTracks.splice(bufferInsertIndex, 0, `${bufferWidth}px`); - const templateColumns = templateTracks.join(" "); + const templateColumns = actualTracks.join(" "); const hasFlexibleColumns = columns.some((column) => !state.widths?.[column.id] && isFlexibleColumn(column)); const stickyOffsets = useMemo(() => computeStickyOffsets(columns, state.widths, measuredWidths), [columns, state.widths, measuredWidths]); const gridClassName = [ @@ -655,17 +606,6 @@ export default function DataGrid({ }); } - function bufferCell(key: string, classNames: string, role: "columnheader" | "cell") { - return ( -