From aa9bd8272b63424064330f7d310354a54361d6b7 Mon Sep 17 00:00:00 2001 From: fabric0de Date: Thu, 13 Aug 2026 22:32:30 +0900 Subject: [PATCH] feat: add grid preference migrations --- CHANGELOG.md | 4 ++ README.md | 1 + api-contract/primitives/index.d.ts | 15 ++++- api-contract/react-ui/index.d.ts | 2 +- api-contract/svelte-ui/index.d.ts | 2 +- api-contract/vue-ui/index.d.ts | 2 +- benchmarks/bundle-budgets.json | 2 +- docs/.vitepress/config.mts | 1 + docs/README.md | 1 + docs/architecture.md | 2 +- docs/preferences.md | 73 +++++++++++++++++++++ docs/react-ui.md | 3 +- docs/roadmap.md | 2 +- docs/svelte-ui.md | 3 +- docs/vue-ui.md | 3 +- packages/primitives/src/index.ts | 72 +++++++++++++++++++- packages/primitives/test/primitives.test.ts | 55 ++++++++++++++++ packages/react-ui/src/index.tsx | 3 +- packages/svelte-ui/src/index.ts | 3 +- packages/vue-ui/src/index.ts | 3 +- scripts/release-consumer-smoke.mjs | 4 +- 21 files changed, 237 insertions(+), 19 deletions(-) create mode 100644 docs/preferences.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 95acd6e..81d0874 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ All notable Open Grid release changes should be documented here before a package and rebaseline the Svelte-to-Vue document-node delta to 80 after measurement. - Rebaseline the required Svelte UI gzip ceiling to 21,000 bytes after measuring the component-renderer addition. +- Add explicit forward-only persisted preference migrations with guarded failure + handling and task-oriented customization, composition, and upgrade guides. +- Rebaseline the required primitives gzip ceiling from 14,500 to 15,000 bytes after + measuring the preference migration addition. - Add generated public export and declaration contracts, including CSS entry points and removal detection, plus a documented pre-1.0 compatibility and deprecation policy enforced by CI and the protected publish workflow. diff --git a/README.md b/README.md index 5b280ca..99ca158 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ Open Grid aims to be: - consistent across React, Vue, and Svelte - localizable per grid instance through one typed cross-framework contract - composable with typed framework-native cells, headers, toolbars, and query states +- explicit about persisted preference versions and product-owned migrations - friendly to design systems through CSS variables and unstyled primitives - suitable for server-side data workflows as a first-class use case diff --git a/api-contract/primitives/index.d.ts b/api-contract/primitives/index.d.ts index ed2bedb..d1a6c1f 100644 --- a/api-contract/primitives/index.d.ts +++ b/api-contract/primitives/index.d.ts @@ -76,6 +76,14 @@ interface GridPreferences { interface GridPreferencesOptions { validColumnIds?: readonly string[] | undefined; } +interface GridPreferencesMigration { + fromVersion: number; + toVersion: number; + migrate: (preferences: Readonly>) => unknown; +} +interface GridPreferencesParseOptions extends GridPreferencesOptions { + migrations?: readonly GridPreferencesMigration[] | undefined; +} interface GridPreferencesStorageLike { getItem: (key: string) => string | null; setItem: (key: string, value: string) => void; @@ -741,11 +749,12 @@ declare function getColumnVisibilityEmptyText(localization?: GridLocalization): declare function getColumnVisibilityResetButtonProps(hiddenCount: number, localization?: GridLocalization): PrimitiveProps; declare function getColumnVisibilityResetButtonText(localization?: GridLocalization): string; declare function normalizeGridDensity(value: unknown, fallback?: GridDensity): GridDensity; +declare function migrateGridPreferences(value: unknown, migrations?: readonly GridPreferencesMigration[]): Record | null; declare function createGridPreferences(state?: Partial, density?: GridDensity, options?: GridPreferencesOptions): GridPreferences; declare function serializeGridPreferences(preferences: GridPreferences, options?: GridPreferencesOptions): string; -declare function parseGridPreferences(value: string, options?: GridPreferencesOptions): GridPreferences | null; +declare function parseGridPreferences(value: string, options?: GridPreferencesParseOptions): GridPreferences | null; declare function getBrowserGridPreferencesStorage(environment?: GridPreferencesStorageEnvironmentLike | null | undefined): GridPreferencesStorageLike | null; -declare function readGridPreferences(storage: GridPreferencesStorageLike | null | undefined, key: string, options?: GridPreferencesOptions): GridPreferences | null; +declare function readGridPreferences(storage: GridPreferencesStorageLike | null | undefined, key: string, options?: GridPreferencesParseOptions): GridPreferences | null; declare function writeGridPreferences(storage: GridPreferencesStorageLike | null | undefined, key: string, state: Partial, density: GridDensity, options?: GridPreferencesOptions): boolean; declare function removeGridPreferences(storage: GridPreferencesStorageLike | null | undefined, key: string): boolean; declare function getGridDensityProps(density: GridDensity): PrimitiveProps; @@ -986,4 +995,4 @@ declare function getPinnedColumnOffsetStyleText(layout: PinnedColumnOffsetStyleL declare function isCellCoordinateEqual(left: CellCoordinate | null | undefined, right: CellCoordinate | null | undefined): boolean; declare function isFocusedCell(focusedCell: CellCoordinate | null, rowId: string, columnId: string): boolean; -export { type BrowserDownloadAnchorLike, type BrowserDownloadBlobConstructorLike, type BrowserDownloadDocumentLike, type BrowserDownloadEnvironment, type BrowserDownloadParentLike, type BrowserDownloadUrlLike, type BrowserExportFileLike, type CellEditValidationEventLike, type CellEditorKeyboardAction, type CellEditorOptionPrimitiveOptions, type CellEditorPrimitiveOptions, type CellEditorValueEventLike, type CellEditorValueTargetLike, type CellFillHandleVisibilityOptions, type CellLayoutPrimitiveOptions, type CellPrimitiveOptions, type CheckboxIndeterminateTargetLike, type ClickSuppressionController, type ClipboardTextAreaLike, type ClipboardTextDocumentLike, type ClipboardTextEnvironment, type ClipboardTextNavigatorLike, type ClipboardTextParentLike, type ColumnFilterCellPrimitiveOptions, type ColumnFilterInputPrimitiveOptions, type ColumnIdItemLike, type ColumnLayoutTotalWidthItemLike, type ColumnMoveDirection, type ColumnMoveGrid, type ColumnPinningButtonPrimitiveOptions, type ColumnResizeDefaultPreventableEventLike, type ColumnResizeFinalSizeEventLike, type ColumnResizeHandlePrimitiveOptions, type ColumnResizeKeyboardEventLike, type ColumnResizeKeyboardSizeOptions, type ColumnResizePointerEventLike, type ColumnVirtualizationPrimitiveOptions, type ColumnVisibilityCheckboxPrimitiveOptions, DEFAULT_GRID_LOCALIZATION, DEFAULT_PAGINATION_PAGE_SIZE_OPTIONS, type DatasetElementLike, type DefaultPreventableEventLike, type DefaultPreventablePropagationEventLike, type DefaultPreventedEventLike, type ElementIdLookupLike, type FocusableElementLike, type FocusableSelectableElementLike, type FocusedCellContainerLike, type FocusedCellRestoreFrameSyncOptions, type FocusedCellRestoreOptions, type FocusedCellScrollElementOptions, type FocusedCellScrollOptions, type FocusedCellScrollPosition, type FocusedRowSelectionTarget, GRID_DENSITIES, GRID_FOCUSED_CELL_SELECTOR, GRID_INTERACTIVE_KEYBOARD_TARGET_SELECTOR, GRID_PREFERENCES_VERSION, GROUPING_PANEL_EMPTY_MESSAGE, type GridBodyPrimitiveOptions, type GridDensity, type GridEmptyCellPrimitiveOptions, type GridEmptyRowPrimitiveOptions, type GridHeaderRowPrimitiveOptions, type GridKeyboardEditAction, type GridKeyboardFocusMove, type GridKeyboardShortcutAction, type GridKeyboardShortcutEventLike, type GridLocalization, type GridLocalizationOverrides, type GridPreferences, type GridPreferencesOptions, type GridPreferencesState, type GridPreferencesStorageEnvironmentLike, type GridPreferencesStorageLike, type GridRowCoordinateOptions, type GroupCellIndentOptions, type GroupCellIndentStyleProps, type GroupRowLabelOptions, type GroupingColumnMoveGrid, type GroupingPanelMoveButtonPrimitiveOptions, type GroupingPanelPrimitiveOptions, HEADER_ACTION_MENU_ENABLED_ITEM_SELECTOR, HEADER_ACTION_MENU_TRIGGER_TEXT, type HeaderActionMenuActiveElementDocumentLike, type HeaderActionMenuActiveElementTargetLike, type HeaderActionMenuDefaultItemDescriptor, type HeaderActionMenuDefaultItemDescriptorOptions, type HeaderActionMenuDefaultItemId, type HeaderActionMenuDefaultItemLabelOptions, type HeaderActionMenuFocusTargetEventLike, type HeaderActionMenuFocusTargetLike, type HeaderActionMenuItemFocusPosition, type HeaderActionMenuItemFocusTargetLike, type HeaderActionMenuItemPrimitiveOptions, type HeaderActionMenuKeyboardAction, type HeaderActionMenuLabelPrimitiveOptions, type HeaderActionMenuPrimitiveCustomItem, type HeaderActionMenuPrimitiveItem, type HeaderActionMenuPrimitiveLabelItem, type HeaderActionMenuPrimitiveSeparatorItem, type HeaderActionMenuTriggerPrimitiveOptions, type HeaderButtonPrimitiveOptions, type HeaderCellLayoutPrimitiveOptions, type HeaderCellPrimitiveOptions, type HeaderDragEndAction, type HeaderDragEndActionOptions, type HeaderDragEndColumnLike, type HeaderDragEndLike, type HeaderKeyboardMoveEventLike, type HeaderSortIndicatorTextOptions, type InlineSizeStyleProps, type KeyboardTargetEventLike, type MeasuredBlockElementLike, type MeasuredBlockRectElementLike, type MeasuredBlockRectElementTargetLike, type MeasuredInlineDatasetElementTargetLike, type MeasuredInlineElementLike, type MeasuredSizeCacheLike, type ObservedElementMapLike, type OffsetBlockElementLike, type PaginationAction, type PaginationButtonPrimitiveOptions, type PinnedColumnOffsetStyleLayoutLike, type PinnedColumnOffsetStyleProps, type PointerButtonEventLike, type PointerButtonsEventLike, type PointerCaptureEventLike, type PointerCaptureTargetLike, type PointerClientCoordinateLike, type PointerDragMovementLike, type PointerDragSessionLike, type PointerDragStartLike, type PointerHitTestElementLike, type PointerIdEventLike, type PointerListener, type PointerMoveUpCancelListenerTargetLike, type PointerMoveUpCancelListeners, type PointerMoveUpListenerTargetLike, type PointerMoveUpListeners, type PointerUpCancelListenerTargetLike, type PointerUpCancelListeners, type PrimitiveProps, type PropagationEventLike, type QuickFilterInputPrimitiveOptions, type ResizeObserverBlockSizeEntryLike, type ResizeObserverConstructorLike, type ResizeObserverDatasetEntryLike, type ResizeObserverDatasetTargetLike, type ResizeObserverDisconnectLike, type ResizeObserverEntryDatasetKey, type ResizeObserverInlineSizeEntryLike, type ResizeObserverObserveLike, type ResizeObserverUnobserveLike, type ResolvedColumnVirtualizationOptions, type ResolvedRowVirtualizationOptions, type RowExpansionTogglePrimitiveOptions, type RowIdItemLike, type RowLayoutPrimitiveOptions, type RowPrimitiveOptions, type RowSelectionCheckboxPrimitiveOptions, type RowSelectionToggleEventLike, type RowVirtualizationPrimitiveOptions, type ScrollFrameElementLike, type ScrollFrameElementOptions, type ScrollFrameOptionsLike, type ScrollListener, type ScrollListenerTargetLike, type ScrollPositionElementLike, type VirtualBodyStyleProps, type VirtualBodyStyleRangeLike, type VirtualRowStyleItemLike, type VirtualRowStyleProps, type VirtualizedInlineSizeStyleProps, addPassiveScrollListener, addPointerMoveUpCancelListeners, addPointerMoveUpListeners, addPointerUpCancelListeners, applyResizeObserverMeasuredSizes, createClickSuppressionController, createGridLocalization, createGridPreferences, createResizeObserver, disconnectResizeObserver, downloadBrowserExportFile, escapeGridHtml, focusCellEditorElement, focusElement, focusElementById, focusFocusedCellInGrid, focusHeaderActionMenuItem, focusHeaderActionMenuItemById, focusHeaderActionMenuTrigger, focusHeaderActionMenuTriggerById, getBrowserGridPreferencesStorage, getCanApplyColumnResize, getCanCancelCellEdit, getCanContinuePointerDrag, getCanEndPointerDrag, getCanRestoreFocusedCell, getCanRunGridKeyboardShortcut, getCanStartCellEdit, getCanStartCellPointerDrag, getCanStartColumnResize, getCanStartFocusedCellEdit, getCanToggleRowSelection, getCellDisplayText, getCellEditText, getCellEditValidationMessage, getCellEditorEventValue, getCellEditorKeyboardAction, getCellEditorOptionProps, getCellEditorOptionText, getCellEditorProps, getCellEditorTargetValue, getCellFillHandleProps, getCellFillHandleVisible, getCellLayoutProps, getCellProps, getCellValidationMessageProps, getColumnById, getColumnFilterCellProps, getColumnFilterInputProps, getColumnFilterText, getColumnHeaderText, getColumnIndexById, getColumnLayoutById, getColumnLayoutTotalWidth, getColumnPinningButtonProps, getColumnPinningButtonText, getColumnPinningControlsProps, getColumnResizeFinalSize, getColumnResizeHandleProps, getColumnResizeKeyboardSize, getColumnResizePointerSize, getColumnResizeStartSize, getColumnSpacerProps, getColumnVisibilityCheckboxProps, getColumnVisibilityControlsProps, getColumnVisibilityEmptyText, getColumnVisibilityListProps, getColumnVisibilityResetButtonProps, getColumnVisibilityResetButtonText, getColumnVisibilitySearchInputProps, getColumnVisibilityStatusProps, getColumnVisibilityStatusText, getColumnVisibilitySummaryProps, getColumnVisibilitySummaryText, getDefaultBrowserDownloadEnvironment, getDefaultClipboardTextEnvironment, getDefaultPointerMoveUpCancelListenerTarget, getDefaultPointerMoveUpListenerTarget, getDefaultPointerUpCancelListenerTarget, getDensityButtonProps, getDensityButtonText, getDensityControlsProps, getElementOffsetBlockSize, getEnabledHeaderActionMenuItems, getFilteredColumnVisibilityColumns, getFocusedCellScrollOptionsFromElement, getFocusedRowSelectionTarget, getGridBodyProps, getGridBodyRowIndexOffset, getGridDensityProps, getGridDensityRowHeight, getGridEmptyCellProps, getGridEmptyRowProps, getGridErrorOverlayProps, getGridErrorRetryButtonProps, getGridErrorRetryButtonText, getGridErrorText, getGridHeaderProps, getGridHeaderRowCount, getGridHeaderRowProps, getGridKeyboardEditAction, getGridKeyboardFocusMove, getGridKeyboardShortcutAction, getGridLoadingOverlayProps, getGridLoadingText, getGridProps, getGroupCellIndent, getGroupCellIndentStyle, getGroupCellIndentStyleText, getGroupRowCountText, getGroupRowLabel, getGroupValueText, getGroupingPanelChipProps, getGroupingPanelMoveButtonProps, getGroupingPanelPlaceholderProps, getGroupingPanelProps, getGroupingPanelRemoveButtonProps, getHeaderActionMenuActiveElement, getHeaderActionMenuCustomItemProps, getHeaderActionMenuCustomItemUserProps, getHeaderActionMenuDefaultItemDescriptors, getHeaderActionMenuDefaultItemLabel, getHeaderActionMenuFocusTarget, getHeaderActionMenuId, getHeaderActionMenuItemProps, getHeaderActionMenuKeyboardAction, getHeaderActionMenuLabelProps, getHeaderActionMenuProps, getHeaderActionMenuSeparatorProps, getHeaderActionMenuTriggerFocusPosition, getHeaderActionMenuTriggerId, getHeaderActionMenuTriggerProps, getHeaderButtonProps, getHeaderCellLayoutProps, getHeaderCellProps, getHeaderDragEndAction, getHeaderKeyboardMoveDirection, getHeaderPlaceholderCellProps, getHeaderSortIndicatorProps, getHeaderSortIndicatorText, getInlineSizeStyle, getInlineSizeStyleText, getIsGroupLabelCell, getMeasuredElementBlockSize, getMeasuredElementBlockSizeFromRect, getMeasuredElementBlockSizeFromTarget, getMeasuredElementInlineSize, getNextColumnFilters, getPaginationButtonProps, getPaginationButtonText, getPaginationPageSizeOptionText, getPaginationPageSizeOptions, getPaginationPageSizeSelectProps, getPaginationPageText, getPaginationProps, getPaginationStatusProps, getPinnedColumnOffsetStyle, getPinnedColumnOffsetStyleText, getPointerCaptureTarget, getQuickFilterClearButtonProps, getQuickFilterClearButtonText, getQuickFilterInputProps, getQuickFilterProps, getResizeObserverEntryBlockSize, getResizeObserverEntryDatasetId, getResizeObserverEntryInlineSize, getRowById, getRowExpansionSpacerProps, getRowExpansionToggleProps, getRowExpansionToggleText, getRowLayoutProps, getRowProps, getRowSelectionCheckboxProps, getRowSelectionCheckboxText, getRowSelectionClearButtonProps, getRowSelectionClearButtonText, getRowSelectionControlsProps, getRowSelectionStatusProps, getRowSelectionStatusText, getScrollForFocusedCell, getScrollFrameOptionsFromElement, getShouldCancelFocusedCellRestoreOnFrameSync, getShouldPreventEventDefault, getSizeOffset, getVirtualBodyStyle, getVirtualBodyStyleText, getVirtualRowStyle, getVirtualRowStyleText, getVirtualizedInlineSizeStyle, getVirtualizedInlineSizeStyleText, hasPointerMovedPastDragThreshold, isCellCoordinateEqual, isFocusableElement, isFocusedCell, isGridInteractiveKeyboardEventTarget, isGridInteractiveKeyboardTarget, isHeaderActionMenuActionItem, isHeaderActionMenuCloseAction, isHeaderActionMenuCustomItem, isHeaderActionMenuFocusAction, isHeaderActionMenuFocusTarget, isHeaderActionMenuItem, isHeaderActionMenuLabelItem, isHeaderActionMenuSeparatorItem, isHeaderActionMenuTabCloseAction, isHeaderDragGroupAction, isHeaderDragMoveAction, isMeasuredBlockElement, isMeasuredBlockRectElement, isMeasuredBlockRectElementTarget, isMeasuredInlineDatasetElementTarget, isMeasuredInlineElement, isPointerCaptureTarget, isPointerInsideElement, isPrimaryPointerButton, isPrimaryPointerDragActive, isResizeObserverDatasetTarget, moveColumnToTarget, moveGroupedColumn, moveVisibleColumn, normalizeGridDensity, observeElements, parseCellEditValue, parseGridPreferences, preventDefaultAndStopPropagation, preventEventDefault, readClipboardText, readGridPreferences, removeGridPreferences, removeObservedElement, replaceObservedElement, resolveColumnVirtualizationOptions, resolveRowVirtualizationOptions, scrollElementToPosition, serializeGridPreferences, setCheckboxIndeterminate, setElementDatasetId, stopEventPropagation, tryReleasePointerCapture, trySetPointerCapture, updatePointerDragDidMovePastThreshold, writeClipboardText, writeGridPreferences }; +export { type BrowserDownloadAnchorLike, type BrowserDownloadBlobConstructorLike, type BrowserDownloadDocumentLike, type BrowserDownloadEnvironment, type BrowserDownloadParentLike, type BrowserDownloadUrlLike, type BrowserExportFileLike, type CellEditValidationEventLike, type CellEditorKeyboardAction, type CellEditorOptionPrimitiveOptions, type CellEditorPrimitiveOptions, type CellEditorValueEventLike, type CellEditorValueTargetLike, type CellFillHandleVisibilityOptions, type CellLayoutPrimitiveOptions, type CellPrimitiveOptions, type CheckboxIndeterminateTargetLike, type ClickSuppressionController, type ClipboardTextAreaLike, type ClipboardTextDocumentLike, type ClipboardTextEnvironment, type ClipboardTextNavigatorLike, type ClipboardTextParentLike, type ColumnFilterCellPrimitiveOptions, type ColumnFilterInputPrimitiveOptions, type ColumnIdItemLike, type ColumnLayoutTotalWidthItemLike, type ColumnMoveDirection, type ColumnMoveGrid, type ColumnPinningButtonPrimitiveOptions, type ColumnResizeDefaultPreventableEventLike, type ColumnResizeFinalSizeEventLike, type ColumnResizeHandlePrimitiveOptions, type ColumnResizeKeyboardEventLike, type ColumnResizeKeyboardSizeOptions, type ColumnResizePointerEventLike, type ColumnVirtualizationPrimitiveOptions, type ColumnVisibilityCheckboxPrimitiveOptions, DEFAULT_GRID_LOCALIZATION, DEFAULT_PAGINATION_PAGE_SIZE_OPTIONS, type DatasetElementLike, type DefaultPreventableEventLike, type DefaultPreventablePropagationEventLike, type DefaultPreventedEventLike, type ElementIdLookupLike, type FocusableElementLike, type FocusableSelectableElementLike, type FocusedCellContainerLike, type FocusedCellRestoreFrameSyncOptions, type FocusedCellRestoreOptions, type FocusedCellScrollElementOptions, type FocusedCellScrollOptions, type FocusedCellScrollPosition, type FocusedRowSelectionTarget, GRID_DENSITIES, GRID_FOCUSED_CELL_SELECTOR, GRID_INTERACTIVE_KEYBOARD_TARGET_SELECTOR, GRID_PREFERENCES_VERSION, GROUPING_PANEL_EMPTY_MESSAGE, type GridBodyPrimitiveOptions, type GridDensity, type GridEmptyCellPrimitiveOptions, type GridEmptyRowPrimitiveOptions, type GridHeaderRowPrimitiveOptions, type GridKeyboardEditAction, type GridKeyboardFocusMove, type GridKeyboardShortcutAction, type GridKeyboardShortcutEventLike, type GridLocalization, type GridLocalizationOverrides, type GridPreferences, type GridPreferencesMigration, type GridPreferencesOptions, type GridPreferencesParseOptions, type GridPreferencesState, type GridPreferencesStorageEnvironmentLike, type GridPreferencesStorageLike, type GridRowCoordinateOptions, type GroupCellIndentOptions, type GroupCellIndentStyleProps, type GroupRowLabelOptions, type GroupingColumnMoveGrid, type GroupingPanelMoveButtonPrimitiveOptions, type GroupingPanelPrimitiveOptions, HEADER_ACTION_MENU_ENABLED_ITEM_SELECTOR, HEADER_ACTION_MENU_TRIGGER_TEXT, type HeaderActionMenuActiveElementDocumentLike, type HeaderActionMenuActiveElementTargetLike, type HeaderActionMenuDefaultItemDescriptor, type HeaderActionMenuDefaultItemDescriptorOptions, type HeaderActionMenuDefaultItemId, type HeaderActionMenuDefaultItemLabelOptions, type HeaderActionMenuFocusTargetEventLike, type HeaderActionMenuFocusTargetLike, type HeaderActionMenuItemFocusPosition, type HeaderActionMenuItemFocusTargetLike, type HeaderActionMenuItemPrimitiveOptions, type HeaderActionMenuKeyboardAction, type HeaderActionMenuLabelPrimitiveOptions, type HeaderActionMenuPrimitiveCustomItem, type HeaderActionMenuPrimitiveItem, type HeaderActionMenuPrimitiveLabelItem, type HeaderActionMenuPrimitiveSeparatorItem, type HeaderActionMenuTriggerPrimitiveOptions, type HeaderButtonPrimitiveOptions, type HeaderCellLayoutPrimitiveOptions, type HeaderCellPrimitiveOptions, type HeaderDragEndAction, type HeaderDragEndActionOptions, type HeaderDragEndColumnLike, type HeaderDragEndLike, type HeaderKeyboardMoveEventLike, type HeaderSortIndicatorTextOptions, type InlineSizeStyleProps, type KeyboardTargetEventLike, type MeasuredBlockElementLike, type MeasuredBlockRectElementLike, type MeasuredBlockRectElementTargetLike, type MeasuredInlineDatasetElementTargetLike, type MeasuredInlineElementLike, type MeasuredSizeCacheLike, type ObservedElementMapLike, type OffsetBlockElementLike, type PaginationAction, type PaginationButtonPrimitiveOptions, type PinnedColumnOffsetStyleLayoutLike, type PinnedColumnOffsetStyleProps, type PointerButtonEventLike, type PointerButtonsEventLike, type PointerCaptureEventLike, type PointerCaptureTargetLike, type PointerClientCoordinateLike, type PointerDragMovementLike, type PointerDragSessionLike, type PointerDragStartLike, type PointerHitTestElementLike, type PointerIdEventLike, type PointerListener, type PointerMoveUpCancelListenerTargetLike, type PointerMoveUpCancelListeners, type PointerMoveUpListenerTargetLike, type PointerMoveUpListeners, type PointerUpCancelListenerTargetLike, type PointerUpCancelListeners, type PrimitiveProps, type PropagationEventLike, type QuickFilterInputPrimitiveOptions, type ResizeObserverBlockSizeEntryLike, type ResizeObserverConstructorLike, type ResizeObserverDatasetEntryLike, type ResizeObserverDatasetTargetLike, type ResizeObserverDisconnectLike, type ResizeObserverEntryDatasetKey, type ResizeObserverInlineSizeEntryLike, type ResizeObserverObserveLike, type ResizeObserverUnobserveLike, type ResolvedColumnVirtualizationOptions, type ResolvedRowVirtualizationOptions, type RowExpansionTogglePrimitiveOptions, type RowIdItemLike, type RowLayoutPrimitiveOptions, type RowPrimitiveOptions, type RowSelectionCheckboxPrimitiveOptions, type RowSelectionToggleEventLike, type RowVirtualizationPrimitiveOptions, type ScrollFrameElementLike, type ScrollFrameElementOptions, type ScrollFrameOptionsLike, type ScrollListener, type ScrollListenerTargetLike, type ScrollPositionElementLike, type VirtualBodyStyleProps, type VirtualBodyStyleRangeLike, type VirtualRowStyleItemLike, type VirtualRowStyleProps, type VirtualizedInlineSizeStyleProps, addPassiveScrollListener, addPointerMoveUpCancelListeners, addPointerMoveUpListeners, addPointerUpCancelListeners, applyResizeObserverMeasuredSizes, createClickSuppressionController, createGridLocalization, createGridPreferences, createResizeObserver, disconnectResizeObserver, downloadBrowserExportFile, escapeGridHtml, focusCellEditorElement, focusElement, focusElementById, focusFocusedCellInGrid, focusHeaderActionMenuItem, focusHeaderActionMenuItemById, focusHeaderActionMenuTrigger, focusHeaderActionMenuTriggerById, getBrowserGridPreferencesStorage, getCanApplyColumnResize, getCanCancelCellEdit, getCanContinuePointerDrag, getCanEndPointerDrag, getCanRestoreFocusedCell, getCanRunGridKeyboardShortcut, getCanStartCellEdit, getCanStartCellPointerDrag, getCanStartColumnResize, getCanStartFocusedCellEdit, getCanToggleRowSelection, getCellDisplayText, getCellEditText, getCellEditValidationMessage, getCellEditorEventValue, getCellEditorKeyboardAction, getCellEditorOptionProps, getCellEditorOptionText, getCellEditorProps, getCellEditorTargetValue, getCellFillHandleProps, getCellFillHandleVisible, getCellLayoutProps, getCellProps, getCellValidationMessageProps, getColumnById, getColumnFilterCellProps, getColumnFilterInputProps, getColumnFilterText, getColumnHeaderText, getColumnIndexById, getColumnLayoutById, getColumnLayoutTotalWidth, getColumnPinningButtonProps, getColumnPinningButtonText, getColumnPinningControlsProps, getColumnResizeFinalSize, getColumnResizeHandleProps, getColumnResizeKeyboardSize, getColumnResizePointerSize, getColumnResizeStartSize, getColumnSpacerProps, getColumnVisibilityCheckboxProps, getColumnVisibilityControlsProps, getColumnVisibilityEmptyText, getColumnVisibilityListProps, getColumnVisibilityResetButtonProps, getColumnVisibilityResetButtonText, getColumnVisibilitySearchInputProps, getColumnVisibilityStatusProps, getColumnVisibilityStatusText, getColumnVisibilitySummaryProps, getColumnVisibilitySummaryText, getDefaultBrowserDownloadEnvironment, getDefaultClipboardTextEnvironment, getDefaultPointerMoveUpCancelListenerTarget, getDefaultPointerMoveUpListenerTarget, getDefaultPointerUpCancelListenerTarget, getDensityButtonProps, getDensityButtonText, getDensityControlsProps, getElementOffsetBlockSize, getEnabledHeaderActionMenuItems, getFilteredColumnVisibilityColumns, getFocusedCellScrollOptionsFromElement, getFocusedRowSelectionTarget, getGridBodyProps, getGridBodyRowIndexOffset, getGridDensityProps, getGridDensityRowHeight, getGridEmptyCellProps, getGridEmptyRowProps, getGridErrorOverlayProps, getGridErrorRetryButtonProps, getGridErrorRetryButtonText, getGridErrorText, getGridHeaderProps, getGridHeaderRowCount, getGridHeaderRowProps, getGridKeyboardEditAction, getGridKeyboardFocusMove, getGridKeyboardShortcutAction, getGridLoadingOverlayProps, getGridLoadingText, getGridProps, getGroupCellIndent, getGroupCellIndentStyle, getGroupCellIndentStyleText, getGroupRowCountText, getGroupRowLabel, getGroupValueText, getGroupingPanelChipProps, getGroupingPanelMoveButtonProps, getGroupingPanelPlaceholderProps, getGroupingPanelProps, getGroupingPanelRemoveButtonProps, getHeaderActionMenuActiveElement, getHeaderActionMenuCustomItemProps, getHeaderActionMenuCustomItemUserProps, getHeaderActionMenuDefaultItemDescriptors, getHeaderActionMenuDefaultItemLabel, getHeaderActionMenuFocusTarget, getHeaderActionMenuId, getHeaderActionMenuItemProps, getHeaderActionMenuKeyboardAction, getHeaderActionMenuLabelProps, getHeaderActionMenuProps, getHeaderActionMenuSeparatorProps, getHeaderActionMenuTriggerFocusPosition, getHeaderActionMenuTriggerId, getHeaderActionMenuTriggerProps, getHeaderButtonProps, getHeaderCellLayoutProps, getHeaderCellProps, getHeaderDragEndAction, getHeaderKeyboardMoveDirection, getHeaderPlaceholderCellProps, getHeaderSortIndicatorProps, getHeaderSortIndicatorText, getInlineSizeStyle, getInlineSizeStyleText, getIsGroupLabelCell, getMeasuredElementBlockSize, getMeasuredElementBlockSizeFromRect, getMeasuredElementBlockSizeFromTarget, getMeasuredElementInlineSize, getNextColumnFilters, getPaginationButtonProps, getPaginationButtonText, getPaginationPageSizeOptionText, getPaginationPageSizeOptions, getPaginationPageSizeSelectProps, getPaginationPageText, getPaginationProps, getPaginationStatusProps, getPinnedColumnOffsetStyle, getPinnedColumnOffsetStyleText, getPointerCaptureTarget, getQuickFilterClearButtonProps, getQuickFilterClearButtonText, getQuickFilterInputProps, getQuickFilterProps, getResizeObserverEntryBlockSize, getResizeObserverEntryDatasetId, getResizeObserverEntryInlineSize, getRowById, getRowExpansionSpacerProps, getRowExpansionToggleProps, getRowExpansionToggleText, getRowLayoutProps, getRowProps, getRowSelectionCheckboxProps, getRowSelectionCheckboxText, getRowSelectionClearButtonProps, getRowSelectionClearButtonText, getRowSelectionControlsProps, getRowSelectionStatusProps, getRowSelectionStatusText, getScrollForFocusedCell, getScrollFrameOptionsFromElement, getShouldCancelFocusedCellRestoreOnFrameSync, getShouldPreventEventDefault, getSizeOffset, getVirtualBodyStyle, getVirtualBodyStyleText, getVirtualRowStyle, getVirtualRowStyleText, getVirtualizedInlineSizeStyle, getVirtualizedInlineSizeStyleText, hasPointerMovedPastDragThreshold, isCellCoordinateEqual, isFocusableElement, isFocusedCell, isGridInteractiveKeyboardEventTarget, isGridInteractiveKeyboardTarget, isHeaderActionMenuActionItem, isHeaderActionMenuCloseAction, isHeaderActionMenuCustomItem, isHeaderActionMenuFocusAction, isHeaderActionMenuFocusTarget, isHeaderActionMenuItem, isHeaderActionMenuLabelItem, isHeaderActionMenuSeparatorItem, isHeaderActionMenuTabCloseAction, isHeaderDragGroupAction, isHeaderDragMoveAction, isMeasuredBlockElement, isMeasuredBlockRectElement, isMeasuredBlockRectElementTarget, isMeasuredInlineDatasetElementTarget, isMeasuredInlineElement, isPointerCaptureTarget, isPointerInsideElement, isPrimaryPointerButton, isPrimaryPointerDragActive, isResizeObserverDatasetTarget, migrateGridPreferences, moveColumnToTarget, moveGroupedColumn, moveVisibleColumn, normalizeGridDensity, observeElements, parseCellEditValue, parseGridPreferences, preventDefaultAndStopPropagation, preventEventDefault, readClipboardText, readGridPreferences, removeGridPreferences, removeObservedElement, replaceObservedElement, resolveColumnVirtualizationOptions, resolveRowVirtualizationOptions, scrollElementToPosition, serializeGridPreferences, setCheckboxIndeterminate, setElementDatasetId, stopEventPropagation, tryReleasePointerCapture, trySetPointerCapture, updatePointerDragDidMovePastThreshold, writeClipboardText, writeGridPreferences }; diff --git a/api-contract/react-ui/index.d.ts b/api-contract/react-ui/index.d.ts index bf1a47a..bf08c92 100644 --- a/api-contract/react-ui/index.d.ts +++ b/api-contract/react-ui/index.d.ts @@ -3,7 +3,7 @@ import { CSSProperties, ReactNode } from 'react'; import { GridOptions, Grid, Row, Column, HeaderContext, CellContext, CellFillOptions, ClipboardPasteOptions, ClipboardPasteResult, ExportFile } from '@open-grid/core'; export { AccessorColumnOptions, AccessorFnColumnDef, AccessorKeyColumnDef, AnyColumnDef, CellContext, CellEditEvent, CellEditEventParams, CellEditHistoryAction, CellEditHistoryState, CellEditOption, CellEditParserContext, CellEditPhase, CellEditValidationContext, CellEditValidationResult, CellEditValidationState, CellEditingState, CellFillOptions, CellInteractionEvent, CellInteractionEventParams, CellRange, CellRangeSelectionState, ClipboardCellContext, ClipboardCopyOptions, ClipboardPasteCellContext, ClipboardPasteCommittedCell, ClipboardPasteOptions, ClipboardPasteResult, ClipboardPasteSkippedCell, ClipboardPasteSkippedReason, ClipboardPasteValidationError, ColumnDef, ColumnFiltersState, ColumnHelper, ColumnMovePosition, ColumnOrderState, ColumnPinningPosition, ColumnResizeEvent, ColumnResizeEventParams, ColumnResizePhase, DisplayColumnDef, ExpandedState, ExportFile, ExportFileOptions, FitColumnsToWidthOptions, GridCacheDiagnostics, GridCacheDiagnosticsEntry, GridCacheKey, GridOptions, GridState, GroupColumnDef, GroupingState, Header, HeaderContext, HeaderGroup, MoveFocusOptions, PaginationState, RowInteractionEvent, RowInteractionEventParams, RowSelectionCleanupScope, SortingState, createColumnHelper, fitColumnsToWidth } from '@open-grid/core'; import { ColumnVirtualizationPrimitiveOptions, GridLocalizationOverrides, GridDensity, RowVirtualizationPrimitiveOptions } from '@open-grid/primitives'; -export { DEFAULT_GRID_LOCALIZATION, GRID_PREFERENCES_VERSION, GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike, createGridLocalization, createGridPreferences, getBrowserGridPreferencesStorage, parseGridPreferences, readGridPreferences, removeGridPreferences, serializeGridPreferences, writeGridPreferences } from '@open-grid/primitives'; +export { DEFAULT_GRID_LOCALIZATION, GRID_PREFERENCES_VERSION, GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesMigration, GridPreferencesOptions, GridPreferencesParseOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike, createGridLocalization, createGridPreferences, getBrowserGridPreferencesStorage, migrateGridPreferences, parseGridPreferences, readGridPreferences, removeGridPreferences, serializeGridPreferences, writeGridPreferences } from '@open-grid/primitives'; type RowVirtualizationOptions = RowVirtualizationPrimitiveOptions; type ColumnVirtualizationOptions = ColumnVirtualizationPrimitiveOptions; diff --git a/api-contract/svelte-ui/index.d.ts b/api-contract/svelte-ui/index.d.ts index 30bac2a..60e6762 100644 --- a/api-contract/svelte-ui/index.d.ts +++ b/api-contract/svelte-ui/index.d.ts @@ -2,7 +2,7 @@ import { ExportFile } from '@open-grid/core'; export { AccessorColumnOptions, AccessorFnColumnDef, AccessorKey, AccessorKeyColumnDef, AnyColumnDef, CellContext, CellCoordinate, CellEditEvent, CellEditEventParams, CellEditHistoryAction, CellEditHistoryState, CellEditOption, CellEditParserContext, CellEditPhase, CellEditValidationContext, CellEditValidationResult, CellEditValidationState, CellEditingState, CellFillOptions, CellInteractionEvent, CellInteractionEventParams, CellRange, CellRangeSelectionState, ClipboardCellContext, ClipboardCopyOptions, ClipboardPasteCellContext, ClipboardPasteCommittedCell, ClipboardPasteOptions, ClipboardPasteResult, ClipboardPasteSkippedCell, ClipboardPasteSkippedReason, ClipboardPasteValidationError, Column, ColumnDef, ColumnFilter, ColumnFiltersState, ColumnHelper, ColumnId, ColumnMovePosition, ColumnOrderState, ColumnPinningPosition, ColumnPinningState, ColumnResizeEvent, ColumnResizeEventParams, ColumnResizePhase, ColumnSizingState, ColumnVisibilityState, DisplayColumnDef, ExpandedState, ExportFile, ExportFileOptions, FilterFn, FitColumnsToWidthOptions, Grid, GridCacheDiagnostics, GridCacheDiagnosticsEntry, GridCacheKey, GridOptions, GridState, GroupColumnDef, GroupingState, Header, HeaderContext, HeaderGroup, MoveFocusOptions, PaginationState, Row, RowId, RowInteractionEvent, RowInteractionEventParams, RowModel, RowSelectionCleanupScope, RowSelectionState, SortFn, SortingRule, SortingState, Updater } from '@open-grid/core'; export { ColumnVirtualizationOptions, default as DataGrid, DataGridErrorRenderContext, DataGridProps, DataGridRenderContext, GridReadyHandler, HeaderActionMenuActionItem, HeaderActionMenuContext, HeaderActionMenuCustomItem, HeaderActionMenuItem, HeaderActionMenuItems, HeaderActionMenuLabelItem, HeaderActionMenuSeparatorItem, RowVirtualizationOptions, SvelteDataGridRenderValue } from './DataGrid.svelte'; export { createColumnHelper, createGridStore, fitColumnsToWidth } from '@open-grid/svelte'; -export { DEFAULT_GRID_LOCALIZATION, GRID_PREFERENCES_VERSION, GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike, createGridLocalization, createGridPreferences, getBrowserGridPreferencesStorage, parseGridPreferences, readGridPreferences, removeGridPreferences, serializeGridPreferences, writeGridPreferences } from '@open-grid/primitives'; +export { DEFAULT_GRID_LOCALIZATION, GRID_PREFERENCES_VERSION, GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesMigration, GridPreferencesOptions, GridPreferencesParseOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike, createGridLocalization, createGridPreferences, getBrowserGridPreferencesStorage, migrateGridPreferences, parseGridPreferences, readGridPreferences, removeGridPreferences, serializeGridPreferences, writeGridPreferences } from '@open-grid/primitives'; interface SvelteDataGridRenderer { type: "open-grid:svelte-renderer"; diff --git a/api-contract/vue-ui/index.d.ts b/api-contract/vue-ui/index.d.ts index b6e4fb6..7eb7366 100644 --- a/api-contract/vue-ui/index.d.ts +++ b/api-contract/vue-ui/index.d.ts @@ -3,7 +3,7 @@ import { DefineComponent, PropType, VNodeChild, VNode } from 'vue'; import { GridOptions, Grid, Row, Column, HeaderContext, CellContext, CellFillOptions, ClipboardPasteResult, ClipboardPasteOptions, ExportFile } from '@open-grid/core'; export { AccessorColumnOptions, AccessorFnColumnDef, AccessorKey, AccessorKeyColumnDef, AnyColumnDef, CellContext, CellCoordinate, CellEditEvent, CellEditEventParams, CellEditHistoryAction, CellEditHistoryState, CellEditOption, CellEditParserContext, CellEditPhase, CellEditValidationContext, CellEditValidationResult, CellEditValidationState, CellEditingState, CellFillOptions, CellInteractionEvent, CellInteractionEventParams, CellRange, CellRangeSelectionState, ClipboardCellContext, ClipboardCopyOptions, ClipboardPasteCellContext, ClipboardPasteCommittedCell, ClipboardPasteOptions, ClipboardPasteResult, ClipboardPasteSkippedCell, ClipboardPasteSkippedReason, ClipboardPasteValidationError, Column, ColumnDef, ColumnFilter, ColumnFiltersState, ColumnHelper, ColumnId, ColumnMovePosition, ColumnOrderState, ColumnPinningPosition, ColumnPinningState, ColumnResizeEvent, ColumnResizeEventParams, ColumnResizePhase, ColumnSizingState, ColumnVisibilityState, DisplayColumnDef, ExpandedState, ExportFile, ExportFileOptions, FilterFn, FitColumnsToWidthOptions, Grid, GridCacheDiagnostics, GridCacheDiagnosticsEntry, GridCacheKey, GridOptions, GridState, GroupColumnDef, GroupingState, Header, HeaderContext, HeaderGroup, MoveFocusOptions, PaginationState, Row, RowId, RowInteractionEvent, RowInteractionEventParams, RowModel, RowSelectionCleanupScope, RowSelectionState, SortFn, SortingRule, SortingState, Updater, fitColumnsToWidth } from '@open-grid/core'; import { ColumnVirtualizationPrimitiveOptions, GridLocalizationOverrides, GridDensity, RowVirtualizationPrimitiveOptions, GridLocalization } from '@open-grid/primitives'; -export { DEFAULT_GRID_LOCALIZATION, GRID_PREFERENCES_VERSION, GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike, createGridLocalization, createGridPreferences, getBrowserGridPreferencesStorage, parseGridPreferences, readGridPreferences, removeGridPreferences, serializeGridPreferences, writeGridPreferences } from '@open-grid/primitives'; +export { DEFAULT_GRID_LOCALIZATION, GRID_PREFERENCES_VERSION, GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesMigration, GridPreferencesOptions, GridPreferencesParseOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike, createGridLocalization, createGridPreferences, getBrowserGridPreferencesStorage, migrateGridPreferences, parseGridPreferences, readGridPreferences, removeGridPreferences, serializeGridPreferences, writeGridPreferences } from '@open-grid/primitives'; export { createColumnHelper, useGrid } from '@open-grid/vue'; interface DataGridRenderContext { diff --git a/benchmarks/bundle-budgets.json b/benchmarks/bundle-budgets.json index 4b9b6da..066a515 100644 --- a/benchmarks/bundle-budgets.json +++ b/benchmarks/bundle-budgets.json @@ -2,7 +2,7 @@ "schemaVersion": 2, "applications": [ { "id": "package-core", "distDirectory": "packages/core/dist", "includeExtensions": [".js", ".css", ".svelte"], "limits": { "totalGzipBytes": 23000 } }, - { "id": "package-primitives", "distDirectory": "packages/primitives/dist", "includeExtensions": [".js", ".css", ".svelte"], "limits": { "totalGzipBytes": 14500 } }, + { "id": "package-primitives", "distDirectory": "packages/primitives/dist", "includeExtensions": [".js", ".css", ".svelte"], "limits": { "totalGzipBytes": 15000 } }, { "id": "package-virtual", "distDirectory": "packages/virtual/dist", "includeExtensions": [".js", ".css", ".svelte"], "limits": { "totalGzipBytes": 3500 } }, { "id": "package-theme", "distDirectory": "packages/theme/dist", "includeExtensions": [".js", ".css", ".svelte"], "limits": { "totalGzipBytes": 2000 } }, { "id": "package-react", "distDirectory": "packages/react/dist", "includeExtensions": [".js", ".css", ".svelte"], "limits": { "totalGzipBytes": 768 } }, diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index ee1dba4..70dd7f6 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -45,6 +45,7 @@ export default defineConfig({ { text: "Localization", link: "/localization" }, { text: "Custom rendering", link: "/custom-rendering" }, { text: "Product composition", link: "/composition" }, + { text: "Persisted preferences", link: "/preferences" }, ], }, { diff --git a/docs/README.md b/docs/README.md index 936a4a7..8f61254 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,7 @@ as the source-tree index when browsing the repository directly. - [Localization](localization.md) - [Custom rendering](custom-rendering.md) - [Product composition](composition.md) +- [Persisted preferences](preferences.md) - [Project overview](https://github.com/Goatshave/open-grid#readme) - [React UI](react-ui.md) - [Vue UI](vue-ui.md) diff --git a/docs/architecture.md b/docs/architecture.md index 5a96e2c..9e1cff4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -179,7 +179,7 @@ React, Vue, and Svelte UI packages can render optional searchable visibility con React, Vue, and Svelte UI packages also share an optional density contract. `densityControl` owns only the compact/standard/comfortable segmented control, while `defaultDensity` supports uncontrolled initialization and `density` plus `onDensityChange` supports product-controlled preferences. Density remains renderer presentation state rather than core grid data state. Shared primitives own labels, pressed state, root markers, and row-height values; renderers clear measured virtual-row caches when the value changes so CSS sizing and virtual offsets remain aligned. Grids that do not opt into a density prop or control retain their existing CSS variables and virtualization estimates. -Persisted grid preferences remain product-owned rather than core state side effects. `@open-grid/primitives` exposes a versioned `GridPreferences` JSON contract and guarded browser-storage helpers, re-exported by each UI package. The contract includes only column visibility, sizing, order, pinning, and renderer density. Parsing filters unknown column ids through a product allowlist, removes duplicate ids and left/right pinning conflicts, rejects invalid sizes and unsupported schema versions, normalizes invalid density values, prevents a restored allowlist from hiding every column, and treats unavailable or denied browser storage as a no-op. Sorting, filters, grouping, expansion, pagination, selection, focus, editing, and cell ranges are deliberately excluded because they describe a working session rather than a durable product preference. Future shape changes require a new schema version and an explicit migration policy. +Persisted grid preferences remain product-owned rather than core state side effects. `@open-grid/primitives` exposes a versioned `GridPreferences` JSON contract and guarded browser-storage helpers, re-exported by each UI package. The contract includes only column visibility, sizing, order, pinning, and renderer density. Parsing filters unknown column ids through a product allowlist, removes duplicate ids and left/right pinning conflicts, rejects invalid sizes and unsupported schema versions, normalizes invalid density values, prevents a restored allowlist from hiding every column, and treats unavailable or denied browser storage as a no-op. Sorting, filters, grouping, expansion, pagination, selection, focus, editing, and cell ranges are deliberately excluded because they describe a working session rather than a durable product preference. Product-provided migrations are sequential and forward-only; missing, duplicate, malformed, future, or throwing steps reject the payload before current-schema normalization. Column sizing updates are clamped in core against each column definition's `minSize` and `maxSize`. Renderers should call core sizing APIs instead of duplicating sizing constraints. diff --git a/docs/preferences.md b/docs/preferences.md new file mode 100644 index 0000000..15caeec --- /dev/null +++ b/docs/preferences.md @@ -0,0 +1,73 @@ +# Persisted preferences + +Grid preferences are product-owned. Open Grid provides a narrow, versioned JSON +contract for column visibility, sizing, order, pinning, and density while excluding +transient sorting, filters, pagination, selection, focus, and edit state. + +## Read and write safely + +```ts +import { + getBrowserGridPreferencesStorage, + readGridPreferences, + writeGridPreferences, +} from "@open-grid/react-ui"; + +const storage = getBrowserGridPreferencesStorage(); +const columnIds = ["invoice", "customer", "status", "amount"]; +const saved = readGridPreferences(storage, "invoice-grid", { + validColumnIds: columnIds, +}); + +writeGridPreferences(storage, "invoice-grid", grid.getState(), density, { + validColumnIds: columnIds, +}); +``` + +Use the equivalent re-exports from Vue UI, Svelte UI, or primitives. Storage access +failures return `null` or `false`; unknown columns and malformed values are removed +during normalization. + +## Migrate an older product schema + +Register every required forward step when reading an older payload: + +```ts +import type { GridPreferencesMigration } from "@open-grid/react-ui"; + +const migrations: readonly GridPreferencesMigration[] = [{ + fromVersion: 0, + toVersion: 1, + migrate: (legacy) => ({ + version: 1, + density: legacy.compact === true ? "compact" : "standard", + state: { + columnVisibility: legacy.hiddenCustomer === true ? { customer: false } : {}, + columnSizing: {}, + columnOrder: [], + columnPinning: { left: [], right: [] }, + }, + }), +}]; + +const saved = readGridPreferences(storage, "invoice-grid", { + validColumnIds: columnIds, + migrations, +}); +``` + +Migrations are explicit, sequential, and forward-only. A missing step, duplicate +`fromVersion`, invalid target version, malformed result, future schema, or thrown +error rejects the payload safely. A migration must return an object whose `version` +equals its declared `toVersion`. + +`migrateGridPreferences(value, migrations)` is available when the product needs to +inspect a parsed object before normal Open Grid normalization. Prefer +`parseGridPreferences` or `readGridPreferences` for ordinary use. + +## Upgrade policy + +Keep the storage key stable when old values should migrate. Change the key when the +old preference meaning is incompatible and should reset. Always pass the current +leaf-column allowlist, and retain migration steps while supported old application +versions may still have written their payloads. diff --git a/docs/react-ui.md b/docs/react-ui.md index 1464a17..86f3832 100644 --- a/docs/react-ui.md +++ b/docs/react-ui.md @@ -6,7 +6,8 @@ Built-in text and accessibility labels use the shared typed [localization contract](localization.md). Product components and commands use the shared [custom rendering](custom-rendering.md) -and [composition](composition.md) contracts. +and [composition](composition.md) contracts. Durable column and density state uses +the guarded [persisted preferences](preferences.md) contract. ## Install diff --git a/docs/roadmap.md b/docs/roadmap.md index 7ff537b..513237a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -42,7 +42,7 @@ regression safety, and dependable releases. - [x] Expand typed custom renderer parity across React, Vue, and Svelte. - [x] Add product toolbar and loading, error, and empty-state composition points without coupling product UI to the grid core. -- Version persisted preference migrations and document upgrade behavior. +- [x] Version persisted preference migrations and document upgrade behavior. - [x] Expand task-oriented API reference and integration examples from user reports. - Continue keyboard, screen-reader, forced-colors, mobile reflow, server-data, and export improvements where real integration evidence identifies a gap. diff --git a/docs/svelte-ui.md b/docs/svelte-ui.md index 3ff2ad7..40a3670 100644 --- a/docs/svelte-ui.md +++ b/docs/svelte-ui.md @@ -6,7 +6,8 @@ Built-in text and accessibility labels use the shared typed [localization contract](localization.md). Product components and commands use the shared [custom rendering](custom-rendering.md) -and [composition](composition.md) contracts. +and [composition](composition.md) contracts. Durable column and density state uses +the guarded [persisted preferences](preferences.md) contract. ## Install diff --git a/docs/vue-ui.md b/docs/vue-ui.md index eb31ec1..7bab8f1 100644 --- a/docs/vue-ui.md +++ b/docs/vue-ui.md @@ -6,7 +6,8 @@ Built-in text and accessibility labels use the shared typed [localization contract](localization.md). Product components and commands use the shared [custom rendering](custom-rendering.md) -and [composition](composition.md) contracts. +and [composition](composition.md) contracts. Durable column and density state uses +the guarded [persisted preferences](preferences.md) contract. ## Install diff --git a/packages/primitives/src/index.ts b/packages/primitives/src/index.ts index 6f6c06b..0db2af8 100644 --- a/packages/primitives/src/index.ts +++ b/packages/primitives/src/index.ts @@ -153,6 +153,16 @@ export interface GridPreferencesOptions { validColumnIds?: readonly string[] | undefined; } +export interface GridPreferencesMigration { + fromVersion: number; + toVersion: number; + migrate: (preferences: Readonly>) => unknown; +} + +export interface GridPreferencesParseOptions extends GridPreferencesOptions { + migrations?: readonly GridPreferencesMigration[] | undefined; +} + export interface GridPreferencesStorageLike { getItem: (key: string) => string | null; setItem: (key: string, value: string) => void; @@ -1281,6 +1291,62 @@ function getGridPreferenceRecord(value: unknown): Record | null : null; } +function getGridPreferenceVersion(value: unknown): number | null { + return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null; +} + +export function migrateGridPreferences( + value: unknown, + migrations: readonly GridPreferencesMigration[] = [], +): Record | null { + let preferences = getGridPreferenceRecord(value); + const initialVersion = getGridPreferenceVersion(preferences?.version); + + if (!preferences || initialVersion === null || initialVersion > GRID_PREFERENCES_VERSION) { + return null; + } + + const migrationsByVersion = new Map(); + + for (const migration of migrations) { + if ( + getGridPreferenceVersion(migration.fromVersion) === null + || getGridPreferenceVersion(migration.toVersion) === null + || migration.toVersion <= migration.fromVersion + || migration.toVersion > GRID_PREFERENCES_VERSION + || migrationsByVersion.has(migration.fromVersion) + ) { + return null; + } + + migrationsByVersion.set(migration.fromVersion, migration); + } + + let version = initialVersion; + + while (version < GRID_PREFERENCES_VERSION) { + const migration = migrationsByVersion.get(version); + + if (!migration) { + return null; + } + + try { + preferences = getGridPreferenceRecord(migration.migrate(preferences)); + } catch { + return null; + } + + if (!preferences || preferences.version !== migration.toVersion) { + return null; + } + + version = migration.toVersion; + } + + return preferences; +} + function getGridPreferenceColumnIds(value: unknown, validColumnIds?: readonly string[]): string[] { if (!Array.isArray(value)) { return []; @@ -1339,9 +1405,9 @@ export function serializeGridPreferences(preferences: GridPreferences, options: return JSON.stringify(createGridPreferences(preferences.state, preferences.density, options)); } -export function parseGridPreferences(value: string, options: GridPreferencesOptions = {}): GridPreferences | null { +export function parseGridPreferences(value: string, options: GridPreferencesParseOptions = {}): GridPreferences | null { try { - const parsed = getGridPreferenceRecord(JSON.parse(value)); + const parsed = migrateGridPreferences(JSON.parse(value), options.migrations); if (!parsed || parsed.version !== GRID_PREFERENCES_VERSION) { return null; @@ -1382,7 +1448,7 @@ export function getBrowserGridPreferencesStorage( export function readGridPreferences( storage: GridPreferencesStorageLike | null | undefined, key: string, - options: GridPreferencesOptions = {}, + options: GridPreferencesParseOptions = {}, ): GridPreferences | null { if (!storage) { return null; diff --git a/packages/primitives/test/primitives.test.ts b/packages/primitives/test/primitives.test.ts index 6dffe73..a4a9dd4 100644 --- a/packages/primitives/test/primitives.test.ts +++ b/packages/primitives/test/primitives.test.ts @@ -141,6 +141,7 @@ import { getRowSelectionStatusText, normalizeGridDensity, createGridPreferences, + migrateGridPreferences, parseGridPreferences, readGridPreferences, removeGridPreferences, @@ -606,6 +607,60 @@ describe("primitives", () => { }); }); + it("migrates legacy preferences through an explicit version chain", () => { + const migrations = [{ + fromVersion: 0, + toVersion: 1, + migrate: (preferences: Readonly>) => ({ + version: 1, + density: preferences.compact ? "compact" : "standard", + state: { + columnVisibility: preferences.hiddenColumn === "owner" ? { owner: false } : {}, + columnSizing: {}, + columnOrder: ["id", "owner"], + columnPinning: { left: [], right: [] }, + }, + }), + }] as const; + const legacy = JSON.stringify({ version: 0, compact: true, hiddenColumn: "owner" }); + + expect(parseGridPreferences(legacy, { migrations, validColumnIds: ["id", "owner"] })).toEqual({ + version: 1, + density: "compact", + state: { + columnVisibility: { owner: false }, + columnSizing: {}, + columnOrder: ["id", "owner"], + columnPinning: { left: [], right: [] }, + }, + }); + expect(migrateGridPreferences(JSON.parse(legacy), migrations)?.version).toBe(1); + }); + + it("rejects incomplete, ambiguous, invalid, and failing preference migrations", () => { + const legacy = JSON.stringify({ version: 0 }); + const validMigration = { + fromVersion: 0, + toVersion: 1, + migrate: () => ({ version: 1, state: {} }), + } as const; + + expect(parseGridPreferences(legacy)).toBeNull(); + expect(parseGridPreferences(legacy, { migrations: [validMigration, validMigration] })).toBeNull(); + expect(parseGridPreferences(legacy, { + migrations: [{ ...validMigration, migrate: () => ({ version: 0, state: {} }) }], + })).toBeNull(); + expect(parseGridPreferences(legacy, { + migrations: [{ fromVersion: 0, toVersion: 2, migrate: () => ({ version: 2 }) }], + })).toBeNull(); + expect(parseGridPreferences(legacy, { + migrations: [{ ...validMigration, migrate: () => { throw new Error("migration failed"); } }], + })).toBeNull(); + expect(migrateGridPreferences(JSON.parse(legacy), [ + { ...validMigration, migrate: () => { throw new Error("migration failed"); } }, + ])).toBeNull(); + }); + it("guards browser preference storage reads, writes, removals, and access failures", () => { const values = new Map(); const storage = { diff --git a/packages/react-ui/src/index.tsx b/packages/react-ui/src/index.tsx index 357b773..ec0fb40 100644 --- a/packages/react-ui/src/index.tsx +++ b/packages/react-ui/src/index.tsx @@ -2232,13 +2232,14 @@ export { createGridLocalization, createGridPreferences, getBrowserGridPreferencesStorage, + migrateGridPreferences, parseGridPreferences, readGridPreferences, removeGridPreferences, serializeGridPreferences, writeGridPreferences, } from "@open-grid/primitives"; -export type { GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike } from "@open-grid/primitives"; +export type { GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesMigration, GridPreferencesOptions, GridPreferencesParseOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike } from "@open-grid/primitives"; export type { AccessorColumnOptions, AccessorFnColumnDef, diff --git a/packages/svelte-ui/src/index.ts b/packages/svelte-ui/src/index.ts index 209c565..266c63f 100644 --- a/packages/svelte-ui/src/index.ts +++ b/packages/svelte-ui/src/index.ts @@ -46,13 +46,14 @@ export { createGridLocalization, createGridPreferences, getBrowserGridPreferencesStorage, + migrateGridPreferences, parseGridPreferences, readGridPreferences, removeGridPreferences, serializeGridPreferences, writeGridPreferences, } from "@open-grid/primitives"; -export type { GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike } from "@open-grid/primitives"; +export type { GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesMigration, GridPreferencesOptions, GridPreferencesParseOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike } from "@open-grid/primitives"; export function downloadExportFile(file: ExportFile): boolean { return downloadBrowserExportFile(file); diff --git a/packages/vue-ui/src/index.ts b/packages/vue-ui/src/index.ts index 40dac8f..00a7ae7 100644 --- a/packages/vue-ui/src/index.ts +++ b/packages/vue-ui/src/index.ts @@ -2218,13 +2218,14 @@ export { createGridLocalization, createGridPreferences, getBrowserGridPreferencesStorage, + migrateGridPreferences, parseGridPreferences, readGridPreferences, removeGridPreferences, serializeGridPreferences, writeGridPreferences, } from "@open-grid/primitives"; -export type { GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike } from "@open-grid/primitives"; +export type { GridDensity, GridLocalization, GridLocalizationOverrides, GridPreferences, GridPreferencesMigration, GridPreferencesOptions, GridPreferencesParseOptions, GridPreferencesState, GridPreferencesStorageEnvironmentLike, GridPreferencesStorageLike } from "@open-grid/primitives"; export type { AccessorKey, AccessorColumnOptions, diff --git a/scripts/release-consumer-smoke.mjs b/scripts/release-consumer-smoke.mjs index b07a417..ec65785 100644 --- a/scripts/release-consumer-smoke.mjs +++ b/scripts/release-consumer-smoke.mjs @@ -168,7 +168,7 @@ function writeConsumerFiles(directory) { writeFileSync(join(directory, "src", "consumer.tsx"), ` import { createColumnHelper, createGrid, type CellContext } from "@open-grid/core"; import { getGridProps } from "@open-grid/primitives"; -import { DataGrid as ReactDataGrid } from "@open-grid/react-ui"; +import { DataGrid as ReactDataGrid, parseGridPreferences, type GridPreferencesMigration } from "@open-grid/react-ui"; import { createSvelteDataGridRenderer, type DataGridProps as SvelteDataGridProps } from "@open-grid/svelte-ui"; import { createOpenGridThemeStyle } from "@open-grid/theme/tokens"; import { createDataGrid, type DataGridProps as VueDataGridProps } from "@open-grid/vue-ui"; @@ -180,6 +180,8 @@ const columns = [column.accessor("name", { header: "Name" })]; const grid = createGrid({ columns, data: rows, getRowId: (row) => row.id }); getGridProps(grid, { ariaLabel: "External consumer" }); createOpenGridThemeStyle({ accent: "#087f5b" }); +const migrations: readonly GridPreferencesMigration[] = [{ fromVersion: 0, toVersion: 1, migrate: () => ({ version: 1, density: "standard", state: {} }) }]; +parseGridPreferences('{"version":0}', { migrations }); export const reactGrid = row.id} localization={{ noRows: "No external rows" }} renderToolbar={({ rows }) => {rows.length}} renderCell={({ value }) => {String(value)}} />; export const VueGrid = createDataGrid(); export const vueGridProps: VueDataGridProps = { options: { columns, data: rows, getRowId: (row) => row.id }, localization: { noRows: "No external rows" }, renderToolbar: ({ rows }) => String(rows.length), renderCell: ({ value }) => String(value) };