@opencorestack/opengridx 3.8.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/ai.cjs.map +1 -1
  3. package/dist/ai.es.js.map +1 -1
  4. package/dist/index.d.ts +342 -3
  5. package/dist/opengridx.css +1 -1
  6. package/dist/opengridx.es.js +6952 -6697
  7. package/dist/opengridx.es.js.map +1 -1
  8. package/dist/opengridx.umd.js +17 -17
  9. package/dist/opengridx.umd.js.map +1 -1
  10. package/docs/API_REFERENCE.md +121 -4
  11. package/docs/customization/theming.md +11 -2
  12. package/docs/features/ai-toolkit.md +1 -1
  13. package/lib/components/AiAssistant/GridAiAssistantPanel.css +2 -2
  14. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +16 -14
  15. package/lib/components/Cell/Cell.css +1 -3
  16. package/lib/components/Cell/CellErrorBoundary.tsx +17 -10
  17. package/lib/components/CellSelectionStats/CellSelectionStats.tsx +5 -3
  18. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +1 -2
  19. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +3 -1
  20. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +6 -4
  21. package/lib/components/DataGrid/DataGrid.css +1 -28
  22. package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -1
  23. package/lib/components/DataGrid/GridErrorOverlay.tsx +4 -2
  24. package/lib/components/DataGrid/GridListView.tsx +7 -5
  25. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -3
  26. package/lib/components/DataGrid/GridLoadingOverlay.tsx +3 -1
  27. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  28. package/lib/components/DataGrid/GridRoot.tsx +8 -1
  29. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +3 -1
  30. package/lib/components/DataGrid/GridViewport.tsx +3 -1
  31. package/lib/components/DataGrid/GridVirtualRows.tsx +3 -2
  32. package/lib/components/DataGrid/gridLocaleContext.ts +13 -0
  33. package/lib/components/FilterPanel/FilterPanel.tsx +25 -56
  34. package/lib/components/Header/ColumnMenu.css +5 -5
  35. package/lib/components/Header/ColumnMenu.tsx +17 -15
  36. package/lib/components/Header/Header.css +2 -11
  37. package/lib/components/Header/Header.tsx +8 -6
  38. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +6 -37
  39. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +3 -1
  40. package/lib/components/ListView/ListView.css +0 -16
  41. package/lib/components/ListView/ListViewRow.tsx +3 -1
  42. package/lib/components/Pagination/Pagination.tsx +15 -18
  43. package/lib/components/QuickFilter/QuickFilter.tsx +5 -2
  44. package/lib/components/Row/Row.tsx +9 -4
  45. package/lib/components/Toolbar/GlobalSearch.tsx +6 -3
  46. package/lib/components/Toolbar/GridToolbar.tsx +35 -29
  47. package/lib/components/Toolbar/PivotPanel.tsx +26 -24
  48. package/lib/components/ui/Checkbox.css +5 -5
  49. package/lib/components/ui/DragHandleIcon.tsx +3 -1
  50. package/lib/components/ui/ExpandIcon.css +3 -3
  51. package/lib/components/ui/ExpandIcon.tsx +3 -1
  52. package/lib/hooks/core/useGridRowInteractions.ts +13 -3
  53. package/lib/hooks/core/useGridToolbarStack.ts +2 -2
  54. package/lib/hooks/core/useGridVirtualization.ts +40 -25
  55. package/lib/hooks/core/useShallowStableValue.ts +29 -0
  56. package/lib/hooks/features/useGridAiAssistant.ts +52 -22
  57. package/lib/hooks/features/useGridSpanning.ts +1 -1
  58. package/lib/styles/base/reset.css +6 -7
  59. package/lib/theme/DataGridThemeProvider.tsx +9 -0
  60. package/lib/theme/__baseline__/default-theme.darwin.chromium.json +1 -1503
  61. package/lib/theme/__baseline__/default-theme.darwin.firefox.json +1 -1503
  62. package/lib/theme/__baseline__/default-theme.darwin.webkit.json +1 -1503
  63. package/lib/theme/types.ts +18 -0
  64. package/lib/types/locale.ts +344 -0
  65. package/lib/types/rows.ts +0 -2
  66. package/lib/utils/cellSelection.ts +0 -2
  67. package/lib/utils/editing/index.ts +1 -1
  68. package/lib/utils/filtering/filterInput.ts +45 -0
  69. package/lib/utils/filtering/index.ts +0 -28
  70. package/lib/utils/gridLocale.ts +241 -0
  71. package/lib/utils/pinning/index.ts +0 -107
  72. package/lib/utils/sorting/index.ts +42 -33
  73. package/lib/utils/values.ts +5 -1
  74. package/llms.txt +9 -3
  75. package/package.json +1 -1
  76. package/lib/theme/defaultStyles.ts +0 -0
@@ -243,7 +243,7 @@ Public CSS: `ogx__cell-fill-handle` (24 × 24 px hit area in the corner cell; th
243
243
 
244
244
  | Prop | Type | Default | Description |
245
245
  | :--- | :--- | :--- | :--- |
246
- | `localeText` | `GridLocaleText` | — | Override user-visible strings: the Pagination labels, the empty-state label (`noRowsLabel`, which wins over the `noRowsLabel` prop), the cell-range announcement, the Ask AI button (`aiAssistantButton`) and the header filter row (`headerFilter*`, v3.6). See [`GridLocaleText`](#gridlocaletext). |
246
+ | `localeText` | `GridLocaleText` | — | Override user-visible strings: the Pagination labels, the empty-state label (`noRowsLabel`, which wins over the `noRowsLabel` prop), the cell-range announcement, the Ask AI button (`aiAssistantButton`), the header filter row (`headerFilter*`, v3.6) and, since v3.9, every other built-in string (toolbar, column menu, filter, columns, pivot and AI panels, rows, overlays, live region). See [`GridLocaleText`](#gridlocaletext). |
247
247
 
248
248
  #### Imperative Ref
249
249
 
@@ -546,6 +546,21 @@ In every exporter, a non-empty `selectedRows` takes precedence over `groupedRows
546
546
 
547
547
  Every exporter takes `columns` typed for your rows (`GridColDef<R>[]`) or untyped (`GridColDef[]`), and its options also accept `getRowId?: (row: R) => GridRowId`. Pass the grid's `getRowId` when you use one: `selectedRows` holds those ids, and since v3.0 the grid no longer copies them onto `row.id`.
548
548
 
549
+ ### Export option types
550
+
551
+ Each exporter's options are exported as a type. Fields common to all of them: `fileName`, `selectedRows`, `getRowId`, `aggregationResult`, `aggregationModel`, `groupedRows`.
552
+
553
+ | Type | For | Its own fields |
554
+ | :--- | :--- | :--- |
555
+ | `CsvExportOptions<R>` | `exportToCsv` | `includeHeaders`, `delimiter`, `escapeFormulas`, `bom` |
556
+ | `JsonExportOptions<R>` | `exportToJson` | `pretty` |
557
+ | `ExcelExportOptions<R>` | `exportToExcel` (HTML `.xls`) | `sheetName`, `includeHeaders`, `escapeFormulas` |
558
+ | `ExcelAdvancedExportOptions<R>` | `exportToExcelAdvanced` (`.xlsx`) | `sheets`, `columnStyles`, `headerFillColor`, `headerTextColor`, `headerFontSize`, `bodyFontSize` |
559
+ | `ExcelSheetDefinition` | one entry of `sheets` | `name`, `rows` (`'all'` / `'selected'`), `includeHeaders`, `includeSummary`, `autoFilter`, `frozenHeader`, `alternateRowColor`; `{ type: 'summary', name? }` adds a totals sheet |
560
+ | `ExcelColumnStyle` | a value of `columnStyles` (by field) | `numFmt` (e.g. `'$#,##0.00'`), `width`, `alignment`, `embedImage`, `imageWidth`, `imageHeight` |
561
+
562
+ `PdfExportOptions` and `PrintOptions` are described in [PDF Export](features/pdf-export.md) and the [Export Guide](features/export-guide.md).
563
+
549
564
  ---
550
565
 
551
566
  ## 🤖 AI Toolkit (`@opencorestack/opengridx/ai`)
@@ -648,10 +663,14 @@ Context provider for overriding the grid's visual system.
648
663
  - **`colors`**: `primary`, `primaryDark`, `primaryLight`, `primaryFocus`, `secondary*`, `success`, `warning`, `error`, `info`, `white`, `black`, `gray` (`{ 50 … 900 }`). Toolbar, menu, selection and focus accents derive from `primary*` unless set directly.
649
664
  - **`typography`**: `fontFamily`, `fontFamilyMono`, `fontSizeXs` … `fontSizeXl`.
650
665
  - **`spacing`**: `xs` … `xxl`. **`borders`**: `widthThin/Medium/Thick`, `radiusSm` … `radiusXl`, `color`, `colorHover`. **`shadows`**: `sm` … `xl`.
651
- - **`grid`**: surfaces (`background`, `borderColor`, `headerBackground`, `headerText`, `headerHoverBackground`, `headerSortedBackground`, `rowText`, `rowHoverBackground`, `rowAlternateBackground`, `rowSelectedBackground`, `rowSelectedHoverBackground`, `cellFocusBorder`, `rangeBackground`, `rangeBorder` (v3.3), `pinnedLeftShadow`, `pinnedRightShadow`, `checkboxBg`, `checkboxBorder`), sizing (`rowHeightCompact`, `rowHeightStandard`, `rowHeightComfortable`, `headerHeight` in px, read by the grid's layout; the `rowHeight` / `headerHeight` props win), `cellPaddingX`, `cellPaddingY`, `cellFontSize`, `headerFontSize`; v3.7: fonts (`headerFontFamily`, `headerFontWeight`, `cellFontFamily`, `cellFontWeight`), `headerPaddingX`, outer `borderWidth` and `borderRadius`, dividers (`columnDividerWidth`, `headerColumnDividerWidth`, `rowDividerWidth`, `columnDividerColor`, `rowDividerColor`; `0` hides a divider).
652
- - **`overlays`**: colours (`background`, `text`, `border`, `shadow`, `item*`) and, v3.7, `fontFamily`, `fontWeight`, `fontSize` for menus and panels.
666
+ - **`grid`**: surfaces (`background`, `borderColor`, `headerBackground`, `headerText`, `headerHoverBackground`, `headerSortedBackground`, `rowText`, `rowHoverBackground`, `rowAlternateBackground`, `rowSelectedBackground`, `rowSelectedHoverBackground`, `cellFocusBorder`, `rangeBackground`, `rangeBorder` (v3.3), `pinnedLeftShadow`, `pinnedRightShadow`, `checkboxBg`, `checkboxBorder`; v3.9: `checkboxCheckedBackground`, `checkboxCheckmark`, `expandIconColor`, `expandIconHoverColor`), sizing (`rowHeightCompact`, `rowHeightStandard`, `rowHeightComfortable`, `headerHeight` in px, read by the grid's layout; the `rowHeight` / `headerHeight` props win), `cellPaddingX`, `cellPaddingY`, `cellFontSize`, `headerFontSize`; v3.7: fonts (`headerFontFamily`, `headerFontWeight`, `cellFontFamily`, `cellFontWeight`), `headerPaddingX`, outer `borderWidth` and `borderRadius`, dividers (`columnDividerWidth`, `headerColumnDividerWidth`, `rowDividerWidth`, `columnDividerColor`, `rowDividerColor`; `0` hides a divider).
667
+ - **`overlays`**: colours (`background`, `text`, `border`, `shadow`, `item*`) and, v3.7, `fontFamily`, `fontWeight`, `fontSize` for menus and panels; v3.9: `itemIndicatorColor` (the selection dot), `itemSelectedIcon`, `iconColor`, `sectionLabelText`, `dividerColor`.
653
668
  - **`toolbar`**, **`scrollbar`** (`thumbColor`, `trackColor`, `size`), **`skeleton`** (`baseColor`, `highlightColor`), **`transitions`**.
654
669
 
670
+ #### Theme types
671
+
672
+ `DataGridThemeProviderProps`: `theme: GridTheme`, `children`, `className?`, `style?` (both on the wrapper element). Each key of `GridTheme` has its own exported type, for building a theme in parts: `GridThemeColors` (with `GridThemeGrayScale` for `gray`), `GridThemeTypography`, `GridThemeSpacing`, `GridThemeBorders`, `GridThemeShadows`, `GridThemeGrid`, `GridThemeOverlays`, `GridThemeToolbar`, `GridThemeScrollbar`, `GridThemeSkeleton`, `GridThemeTransitions`. Every field is optional.
673
+
655
674
  Presets: `darkTheme`, `roseTheme`, `emeraldTheme`, `amberTheme`, `compactTheme`. See [Theming](customization/theming.md).
656
675
 
657
676
  ---
@@ -836,6 +855,15 @@ If the key can change while the grid stays mounted, remount the grid with it (`<
836
855
 
837
856
  ## ⚙️ Interfaces & Types
838
857
 
858
+ ### Small shared types
859
+
860
+ | Type | Definition | Used by |
861
+ | :--- | :--- | :--- |
862
+ | `GridAlignment` | `'left' \| 'center' \| 'right'` | `GridColDef.align`, `headerAlign` |
863
+ | `GridSortDirection` | `'asc' \| 'desc' \| null` | a column's sort state (`null`: unsorted) |
864
+ | `GridSortModel` | `{ field: string; sort: GridSortDirection }`: like `GridSortItem`, but `sort` may be `null` | a single column's sort state |
865
+ | `GridPinnedPosition` | `'left' \| 'right'` (logical: inline start / end in RTL) | column pinning |
866
+
839
867
  ### `GridSortItem`
840
868
  ```typescript
841
869
  interface GridSortItem {
@@ -878,7 +906,7 @@ interface GridPivotValueField {
878
906
 
879
907
  ### `GridLocaleText`
880
908
 
881
- Override user-visible strings in the Pagination component and the empty state. All fields are optional.
909
+ Override the grid's user-visible strings (visible text, `aria-label`, `title`, placeholder). All fields are optional; each one left out keeps its English default. Strings that contain a value (a header, a row id, a count) are functions.
882
910
 
883
911
  ```typescript
884
912
  interface GridLocaleText {
@@ -903,9 +931,98 @@ interface GridLocaleText {
903
931
  headerFilterTrue?: string; // "Yes"
904
932
  headerFilterFalse?: string; // "No"
905
933
  headerFilterOperators?: Partial<Record<GridFilterOperator, string>>; // operator names
934
+
935
+ // ── v3.9: every other UI string (default English shown) ──
936
+ // Grid, list view, overlays, live region
937
+ gridLabel; listViewLabel; // "Data grid", "Data grid list view" (the aria-label prop wins)
938
+ listViewItemCount(n); listViewPage(p, pc); listViewSelectedCount(n); // "3 items", "page 1 of 2", "2 selected"
939
+ loadingLabel; liveRegionLoading; // "Loading data", "Loading data..."
940
+ liveRegionError(message); liveRegionRowsFound(n); // "Error: …", "4 rows found"
941
+ errorOverlayTitle; errorOverlayRetry; // "Oops! Something went wrong", "Retry"
942
+ aggregationFooterLabel; // "Aggregation totals"
943
+ cellErrorLabel(field); cellErrorTooltip; // "Error in cell: name", "Render error"
944
+ // Header
945
+ headerRowReorderLabel; headerRowReorderTooltip; headerDetailPanelLabel;
946
+ headerSelectAllLabel; headerDeselectAllLabel; // "Select all rows", "Deselect all rows"
947
+ headerSortDescription(direction, priority, total); // "Sorted ascending, sort priority 1 of 2"
948
+ headerColumnMenuButtonLabel(header); // "Open column menu for Name"
949
+ headerColumnResizeLabel(field); headerFilterNoFilterLabel(header);
950
+ // Rows
951
+ rowSelectLabel(id); rowDeselectLabel(id); // "Select row 1", "Deselect row 1"
952
+ rowReorderLabel; rowDetailPanelExpandLabel; rowDetailPanelCollapseLabel; rowExpandLabel; rowCollapseLabel;
953
+ // Column menu
954
+ columnMenuLabel(header); columnMenuSortAscending; columnMenuSortDescending; columnMenuUnsort;
955
+ columnMenuPinColumn; columnMenuPinLeft; columnMenuPinLeftLabel; columnMenuPinRight; columnMenuPinRightLabel;
956
+ columnMenuUnpin; columnMenuUnpinLabel; columnMenuHideColumn; columnMenuHideColumnLabel(header);
957
+ columnMenuManageColumns; columnMenuManageColumnsLabel;
958
+ // Columns panel
959
+ columnsPanelSearchPlaceholder; columnsPanelDragToReorder; columnsPanelShowHideAll; columnsPanelReset; columnsPanelClose;
960
+ // Filter panel
961
+ filterPanelValueLabel(header); filterPanelOperatorLabel(header); filterPanelClearLabel(header); filterPanelClearTooltip;
962
+ filterPanelAny; filterPanelTrue; filterPanelFalse; filterPanelNumberPlaceholder; filterPanelValuePlaceholder;
963
+ filterPanelMatch; filterPanelAnd; filterPanelOr; filterPanelHiddenConditions(n); filterPanelNoColumns;
964
+ filterPanelOperators; // Partial<Record<GridFilterOperator, string>>, default: the operator id
965
+ // Toolbar and search (toolbarSearchPlaceholder also for QuickFilter)
966
+ toolbarLabel; toolbarSearchPlaceholder; toolbarSearchLabel; toolbarSearchClear;
967
+ toolbarColumns; toolbarColumnsLabel; toolbarColumnsPanelLabel;
968
+ toolbarFilters; toolbarFiltersLabel; toolbarFiltersActive; toolbarFiltersClearAll; toolbarFiltersClearAllTooltip;
969
+ toolbarFiltersClose; toolbarFiltersCloseTooltip; toolbarFiltersCloseLabel;
970
+ toolbarPivot; toolbarPivotLabel; toolbarPivotActive;
971
+ toolbarSummaries; toolbarSummariesLabel; toolbarSummariesActive(n); toolbarSummariesPanelLabel;
972
+ toolbarSummariesClearAll; toolbarSummariesClearAllTooltip; toolbarSummariesEmpty;
973
+ toolbarSummariesEmptyHintBefore; toolbarSummariesEmptyHintAfter;
974
+ toolbarAggregationFunctions; // Partial<Record<string, string>>, default: the function id
975
+ // Pivot panel
976
+ pivotPanelLabel; pivotMode; pivotReset; pivotResetTooltip;
977
+ pivotRowFields; pivotColumnFields; pivotValueFields;
978
+ pivotRowFieldsPlaceholder; pivotColumnFieldsPlaceholder; pivotValueFieldsPlaceholder;
979
+ pivotRemoveTooltip(header); pivotRemoveRowLabel(header); pivotRemoveColumnLabel(header); pivotRemoveValueLabel(header, fn);
980
+ pivotAggregationTooltip; pivotAggregationLabel(header);
981
+ pivotAggregationFunctions; // Partial<Record<string, string>>, default: the id in upper case
982
+ pivotAvailableFields; pivotAllFieldsInUse; pivotAddRow; pivotAddColumn; pivotAddValue;
983
+ pivotAddRowTooltip; pivotAddColumnTooltip; pivotAddValueTooltip;
984
+ pivotAddRowLabel(header); pivotAddColumnLabel(header); pivotAddValueLabel(header);
985
+ // AI assistant panel (aiAssistant.placeholder wins over aiAssistantPlaceholder)
986
+ aiAssistantTitle; aiAssistantClose; aiAssistantPromptLabel; aiAssistantPlaceholder;
987
+ aiAssistantStartDictation; aiAssistantStopDictation; aiAssistantAsk; aiAssistantStop; aiAssistantSuggestions;
988
+ aiAssistantAppliedChanges; aiAssistantRemoveChange(label); aiAssistantUndo; aiAssistantIgnored; aiAssistantIgnoredItem(message);
989
+ aiAssistantEarlier; aiAssistantThinking; aiAssistantStopped; aiAssistantUndone; aiAssistantChangeRemoved;
990
+ aiAssistantApplied(applied, ignored); aiAssistantNotValidated; aiAssistantFailed(message); aiAssistantUnknownError;
991
+ // Pagination buttons
992
+ paginationLabel; paginationFirstPageLabel; paginationFirstPageTooltip; paginationPreviousPageLabel;
993
+ paginationPreviousPageTooltip; paginationNextPageLabel; paginationNextPageTooltip;
994
+ paginationLastPageLabel; paginationLastPageTooltip;
995
+ // Cell selection stats bar
996
+ cellSelectionStatsCount; cellSelectionStatsSum; cellSelectionStatsAverage;
906
997
  }
907
998
  ```
908
999
 
1000
+ Each key's English default and parameter types are documented on `GridLocaleText` in `lib/types/locale.ts` (shown by editors on hover).
1001
+
1002
+ ### Locale text for every UI string (v3.9)
1003
+
1004
+ Since v3.9 every built-in string of the grid goes through `localeText`: the toolbar and its panels, the column menu, the filter, columns, pivot and AI assistant panels, the row and select-all checkboxes, the pagination buttons, the loading and error overlays, the aggregation footer and the live-region announcements. Pass only the keys you translate; the rest stay English.
1005
+
1006
+ ```tsx
1007
+ <DataGrid
1008
+ rows={rows}
1009
+ columns={columns}
1010
+ slots={{ toolbar: GridToolbar }}
1011
+ localeText={{
1012
+ toolbarSearchPlaceholder: 'Rechercher…',
1013
+ columnMenuSortAscending: 'Tri croissant',
1014
+ columnMenuHideColumnLabel: (header) => `Masquer la colonne ${header}`,
1015
+ filterPanelOperators: { contains: 'contient' },
1016
+ rowSelectLabel: (id) => `Sélectionner la ligne ${id}`,
1017
+ }}
1018
+ />
1019
+ ```
1020
+
1021
+ - The strings reach portalled menus and panels through React context, so a custom toolbar slot that renders the built-in `GridToolbar` is translated too.
1022
+ - Components used outside a grid (`ColumnVisibilityPanel`, `GridToolbar`, `FilterPanel`, `Pagination`, `QuickFilter`) show English. Their own props (`placeholder`, `aiAssistantLabel`, `Pagination`'s `localeText`) still win.
1023
+ - Not covered: the grouping column's default header (`'Group'`, set `groupingColDef.headerName`), pivot's `'Grand Total'` row label, export file contents, the AI change chips' labels, and developer console warnings.
1024
+ - An inline object of strings is fine: while it holds the same entries as on the previous render (records such as `filterPanelOperators` compared one level deep), the grid keeps the resolved strings and the rows are not re-rendered. Functions are compared by identity, so define function entries (`rowSelectLabel: id => …`) outside the render, or keep the whole object in a module constant or `useMemo`.
1025
+
909
1026
  ### `GridRowMeta`
910
1027
 
911
1028
  Available via `params.rowMeta` in `renderCell`. Undefined for non-hierarchy rows.
@@ -262,8 +262,12 @@ For visible gaps between cells instead of lines, set the divider colour to the g
262
262
  | `--ogx-fill-handle-color` | `var(--ogx-range-border)` | `var(--ogx-range-border)` | The fill handle square (`ogx__cell-fill-handle`, v3.5). Unset in the stylesheet, where it falls back to `--ogx-range-border`; every theme of `DataGridThemeProvider` sets it to `var(--ogx-range-border)`. Theme key `grid.fillHandleColor` |
263
263
  | `--ogx-grid-pinned-left-shadow` | `4px 0 24px...` | (darker) | Shadow on left-pinned columns |
264
264
  | `--ogx-grid-pinned-right-shadow` | `-4px 0 24px...` | (darker) | Shadow on right-pinned columns |
265
- | `--ogx-checkbox-bg` | — | `#1e293b` | Checkbox background (dark only) |
266
- | `--ogx-checkbox-border` | — | `#64748b` | Checkbox border (dark only) |
265
+ | `--ogx-checkbox-bg` | — | `#1e293b` | Checkbox background (dark only). Theme key `grid.checkboxBg` |
266
+ | `--ogx-checkbox-border` | — | `#64748b` | Checkbox border (dark only). Theme key `grid.checkboxBorder` |
267
+ | `--ogx-checkbox-checked-bg` | primary | primary | Fill and border of a checked or indeterminate checkbox (v3.9). Theme key `grid.checkboxCheckedBackground` |
268
+ | `--ogx-checkbox-checkmark` | `white` | `white` | Check mark and indeterminate dash (v3.9). Theme key `grid.checkboxCheckmark` |
269
+ | `--ogx-expand-icon-color` | header text | header text | Expand / collapse chevron of group and tree rows (v3.9). Theme key `grid.expandIconColor` |
270
+ | `--ogx-expand-icon-hover-color` | row text | row text | The chevron on hover (v3.9). Theme key `grid.expandIconHoverColor` |
267
271
 
268
272
  ### Toolbar, Overlay and Scrollbar Colors
269
273
 
@@ -282,6 +286,11 @@ Read with a fallback, so a grid without a provider looks the same as before. Ins
282
286
  | `--ogx-overlay-*` | `overlays.*` | white surface, gray border | Menus, panels, tooltips |
283
287
  | `--ogx-overlay-item-selected-bg` / `-text` | `overlays.itemSelected*` | 8% primary / primary-dark | Selected menu item |
284
288
  | `--ogx-overlay-item-danger-bg` / `-text` | `overlays.itemDanger*` | red tints | Remove-condition button in the filter panel |
289
+ | `--ogx-overlay-item-indicator` | `overlays.itemIndicatorColor` (v3.9) | the selected item text | Dot before the selected option of a menu group (sort, pin) |
290
+ | `--ogx-overlay-item-selected-icon` | `overlays.itemSelectedIcon` (v3.9) | primary | Icon of the selected menu option |
291
+ | `--ogx-overlay-icon` | `overlays.iconColor` (v3.9) | gray-400 | Menu item icons |
292
+ | `--ogx-overlay-section-label-text` | `overlays.sectionLabelText` (v3.9) | gray-400 | Menu section labels ("Pin column") |
293
+ | `--ogx-overlay-divider` | `overlays.dividerColor` (v3.9) | overlay border | Lines between menu sections |
285
294
  | `--ogx-scrollbar-thumb` / `-track` / `-size` | `scrollbar.thumbColor` / `trackColor` / `size` | gray-300 / transparent / 8px | Grid viewport scrollbars |
286
295
  | `--ogx-skeleton-base` / `-highlight` | `skeleton.baseColor` / `highlightColor` | light grays | Loading skeleton rows |
287
296
 
@@ -1,6 +1,6 @@
1
1
  # AI Toolkit: Schema, Validator, Assistant & Agent Tools
2
2
 
3
- *Since v3.4.0 (schema and validator); v3.5.0 (assistant panel, prompt handler, agent tools)* · Demos: [AI: Grid Schema & Validator](https://opencorestack.github.io/OpenGridX/ai-schema), [AI Assistant](https://opencorestack.github.io/OpenGridX/ai-assistant)
3
+ *Since v3.4.0 (schema and validator); v3.5.0 (assistant panel, prompt handler, agent tools)* · Demos: [Grid Schema & Validator](https://opencorestack.github.io/OpenGridX/ai-schema), [AI Assistant](https://opencorestack.github.io/OpenGridX/ai-assistant)
4
4
 
5
5
  Let users drive the grid in plain language ("paid orders over $10k in the North, biggest first") with the model your app already uses. The `@opencorestack/opengridx/ai` entry point gives you the pieces the grid can own:
6
6
 
@@ -71,7 +71,7 @@
71
71
  .ogx-ai-panel__suggestion {
72
72
  padding: 3px 10px;
73
73
  border: 1px solid var(--ogx-border-color, var(--ogx-grid-border-color));
74
- border-radius: var(--ogx-border-radius-round, 999px);
74
+ border-radius: 999px;
75
75
  background: var(--ogx-grid-header-background);
76
76
  color: inherit;
77
77
  font: inherit;
@@ -119,7 +119,7 @@
119
119
  align-items: center;
120
120
  gap: 4px;
121
121
  padding: 2px 4px 2px 10px;
122
- border-radius: var(--ogx-border-radius-round, 999px);
122
+ border-radius: 999px;
123
123
  background: var(--ogx-color-primary-light, #e0e7ff);
124
124
  color: var(--ogx-color-primary-dark, #1e3a8a);
125
125
  }
@@ -3,6 +3,7 @@ import type { GridAiAssistantPanelProps } from '../../types';
3
3
  import { Button } from '../ui/Button';
4
4
  import { Input } from '../ui/Input';
5
5
  import { useSpeechInput } from './useSpeechInput';
6
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
6
7
 
7
8
  /** The sparkle of the Ask AI button and the panel title. */
8
9
  export function AiSparkleIcon() {
@@ -33,6 +34,7 @@ export function GridAiAssistantPanel(props: GridAiAssistantPanelProps) {
33
34
  status, statusText, message, errors, chips, history, suggestions, placeholder, voiceAvailable, canUndo,
34
35
  submit, stop, undo, removeChip, close,
35
36
  } = props;
37
+ const locale = useGridLocale();
36
38
  const [text, setText] = useState('');
37
39
  const inputRef = useRef<HTMLInputElement>(null);
38
40
  const speech = useSpeechInput(setText);
@@ -59,10 +61,10 @@ export function GridAiAssistantPanel(props: GridAiAssistantPanelProps) {
59
61
  };
60
62
 
61
63
  return (
62
- <div className="ogx-ai-panel" role="dialog" aria-label="Ask AI">
64
+ <div className="ogx-ai-panel" role="dialog" aria-label={locale.aiAssistantTitle}>
63
65
  <div className="ogx-ai-panel__header">
64
- <span className="ogx-ai-panel__title"><AiSparkleIcon />Ask AI</span>
65
- <Button size="small" className="ogx-ai-panel__close" aria-label="Close" onClick={close}>×</Button>
66
+ <span className="ogx-ai-panel__title"><AiSparkleIcon />{locale.aiAssistantTitle}</span>
67
+ <Button size="small" className="ogx-ai-panel__close" aria-label={locale.aiAssistantClose} onClick={close}>×</Button>
66
68
  </div>
67
69
 
68
70
  <form className="ogx-ai-panel__form" onSubmit={handleSubmit}>
@@ -71,7 +73,7 @@ export function GridAiAssistantPanel(props: GridAiAssistantPanelProps) {
71
73
  className="ogx-ai-panel__input"
72
74
  value={text}
73
75
  placeholder={placeholder}
74
- aria-label="Prompt"
76
+ aria-label={locale.aiAssistantPromptLabel}
75
77
  autoComplete="off"
76
78
  onChange={(event) => setText(event.target.value)}
77
79
  />
@@ -80,7 +82,7 @@ export function GridAiAssistantPanel(props: GridAiAssistantPanelProps) {
80
82
  size="small"
81
83
  variant="outlined"
82
84
  className={`ogx-ai-panel__mic${speech.listening ? ' ogx-ai-panel__mic--on' : ''}`}
83
- aria-label={speech.listening ? 'Stop dictation' : 'Dictate a prompt'}
85
+ aria-label={speech.listening ? locale.aiAssistantStopDictation : locale.aiAssistantStartDictation}
84
86
  aria-pressed={speech.listening}
85
87
  onClick={speech.toggle}
86
88
  >
@@ -89,12 +91,12 @@ export function GridAiAssistantPanel(props: GridAiAssistantPanelProps) {
89
91
  )}
90
92
  {/* Separate keys: one reused <button> would turn into the submit button during the Stop click and submit again. */}
91
93
  {running
92
- ? <Button key="stop" size="small" variant="outlined" onClick={thenFocusInput(stop)}>Stop</Button>
93
- : <Button key="ask" size="small" variant="contained" color="primary" type="submit" disabled={text.trim() === ''}>Ask</Button>}
94
+ ? <Button key="stop" size="small" variant="outlined" onClick={thenFocusInput(stop)}>{locale.aiAssistantStop}</Button>
95
+ : <Button key="ask" size="small" variant="contained" color="primary" type="submit" disabled={text.trim() === ''}>{locale.aiAssistantAsk}</Button>}
94
96
  </form>
95
97
 
96
98
  {suggestions.length > 0 && (
97
- <div className="ogx-ai-panel__suggestions" aria-label="Suggestions" role="group">
99
+ <div className="ogx-ai-panel__suggestions" aria-label={locale.aiAssistantSuggestions} role="group">
98
100
  {suggestions.map((suggestion) => (
99
101
  <button key={suggestion} type="button" className="ogx-ai-panel__suggestion" disabled={running} onClick={() => send(suggestion)}>
100
102
  {suggestion}
@@ -108,29 +110,29 @@ export function GridAiAssistantPanel(props: GridAiAssistantPanelProps) {
108
110
 
109
111
  {(chips.length > 0 || canUndo) && (
110
112
  <div className="ogx-ai-panel__changes">
111
- <ul className="ogx-ai-panel__chips" aria-label="Applied changes">
113
+ <ul className="ogx-ai-panel__chips" aria-label={locale.aiAssistantAppliedChanges}>
112
114
  {chips.map((chip) => (
113
115
  <li key={chip.id} className="ogx-ai-chip" data-part={chip.part}>
114
116
  <span className="ogx-ai-chip__label">{chip.label}</span>
115
- <button type="button" className="ogx-ai-chip__remove" aria-label={`Remove ${chip.label}`} onClick={thenFocusInput(() => removeChip(chip.id))}>×</button>
117
+ <button type="button" className="ogx-ai-chip__remove" aria-label={locale.aiAssistantRemoveChange(chip.label)} onClick={thenFocusInput(() => removeChip(chip.id))}>×</button>
116
118
  </li>
117
119
  ))}
118
120
  </ul>
119
- {canUndo && <Button size="small" variant="outlined" className="ogx-ai-panel__undo" onClick={thenFocusInput(undo)}>Undo</Button>}
121
+ {canUndo && <Button size="small" variant="outlined" className="ogx-ai-panel__undo" onClick={thenFocusInput(undo)}>{locale.aiAssistantUndo}</Button>}
120
122
  </div>
121
123
  )}
122
124
 
123
125
  {errors.length > 0 && (
124
- <ul className="ogx-ai-panel__errors" aria-label="Ignored">
126
+ <ul className="ogx-ai-panel__errors" aria-label={locale.aiAssistantIgnored}>
125
127
  {errors.map((error, i) => (
126
- <li key={`${error.path}:${i}`}>{error.path ? `Ignored: ${error.message}` : error.message}</li>
128
+ <li key={`${error.path}:${i}`}>{error.path ? locale.aiAssistantIgnoredItem(error.message) : error.message}</li>
127
129
  ))}
128
130
  </ul>
129
131
  )}
130
132
 
131
133
  {history.length > 0 && (
132
134
  <div className="ogx-ai-panel__history">
133
- <div className="ogx-ai-panel__history-title">Earlier</div>
135
+ <div className="ogx-ai-panel__history-title">{locale.aiAssistantEarlier}</div>
134
136
  <ul>
135
137
  {history.map((entry, i) => (
136
138
  <li key={`${i}:${entry.prompt}`}>
@@ -27,9 +27,7 @@
27
27
  outline: 2px solid var(--ogx-grid-cell-focus-border, var(--ogx-color-primary, #2563eb));
28
28
  outline-offset: -2px;
29
29
  }
30
- .ogx__cell--editable {
31
- cursor: text;
32
- }
30
+
33
31
  /*
34
32
  * Cell range selection (cellSelection, v3.3). The tint is a background image, so it also covers
35
33
  * pinned cells' own background; the outline is an inset box-shadow, so the layout never shifts.
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
2
3
 
3
4
  // Must live at module scope (not inline) so React sees a stable component type.
4
5
  // Calling renderFn() here puts the throw one level BELOW CellErrorBoundary,
@@ -10,6 +11,21 @@ function CellRenderTarget({ renderFn }: { renderFn: () => React.ReactNode }) {
10
11
  return <>{renderFn()}</>;
11
12
  }
12
13
 
14
+ /** The cell content after `renderCell` threw: a warning sign labelled with the field. */
15
+ function CellErrorMarker({ field, message }: { field: string; message: string | undefined }) {
16
+ const locale = useGridLocale();
17
+ return (
18
+ <div
19
+ className="ogx__cell-error"
20
+ role="img"
21
+ aria-label={locale.cellErrorLabel(field)}
22
+ title={message ?? locale.cellErrorTooltip}
23
+ >
24
+ ⚠
25
+ </div>
26
+ );
27
+ }
28
+
13
29
  interface CellErrorBoundaryProps {
14
30
  renderFn: () => React.ReactNode;
15
31
  field: string;
@@ -59,16 +75,7 @@ export class CellErrorBoundary extends React.Component<CellErrorBoundaryProps, C
59
75
 
60
76
  override render() {
61
77
  if (this.state.hasError) {
62
- return (
63
- <div
64
- className="ogx__cell-error"
65
- role="img"
66
- aria-label={`Error in cell: ${this.props.field}`}
67
- title={this.state.error?.message ?? 'Render error'}
68
- >
69
- ⚠
70
- </div>
71
- );
78
+ return <CellErrorMarker field={this.props.field} message={this.state.error?.message} />;
72
79
  }
73
80
  return <CellRenderTarget renderFn={this.props.renderFn} />;
74
81
  }
@@ -1,5 +1,6 @@
1
1
  import { formatAggregationValue } from '../../utils/aggregation';
2
2
  import type { GridCellSelectionStatsSlotProps } from '../../types';
3
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
3
4
 
4
5
  export type CellSelectionStatsProps = GridCellSelectionStatsSlotProps & Record<string, unknown>;
5
6
 
@@ -9,21 +10,22 @@ export type CellSelectionStatsProps = GridCellSelectionStatsSlotProps & Record<s
9
10
  * @since v3.3
10
11
  */
11
12
  export function CellSelectionStats({ count, sum, average }: CellSelectionStatsProps) {
13
+ const locale = useGridLocale();
12
14
  return (
13
15
  <div className="ogx__cell-selection-stats">
14
16
  <span className="ogx__cell-selection-stat">
15
- <span className="ogx__cell-selection-stat-label">Count</span>
17
+ <span className="ogx__cell-selection-stat-label">{locale.cellSelectionStatsCount}</span>
16
18
  <span className="ogx__cell-selection-stat-value" data-stat="count">{count.toLocaleString()}</span>
17
19
  </span>
18
20
  {sum !== null && (
19
21
  <span className="ogx__cell-selection-stat">
20
- <span className="ogx__cell-selection-stat-label">Sum</span>
22
+ <span className="ogx__cell-selection-stat-label">{locale.cellSelectionStatsSum}</span>
21
23
  <span className="ogx__cell-selection-stat-value" data-stat="sum">{formatAggregationValue(sum, 'sum')}</span>
22
24
  </span>
23
25
  )}
24
26
  {average !== null && (
25
27
  <span className="ogx__cell-selection-stat">
26
- <span className="ogx__cell-selection-stat-label">Average</span>
28
+ <span className="ogx__cell-selection-stat-label">{locale.cellSelectionStatsAverage}</span>
27
29
  <span className="ogx__cell-selection-stat-value" data-stat="average">{formatAggregationValue(average, 'avg')}</span>
28
30
  </span>
29
31
  )}
@@ -67,7 +67,6 @@
67
67
  }
68
68
 
69
69
  /* ─── Dark-mode overrides ────────────────────────────────────────────────────*/
70
- [data-theme="dark"] .ogx-col-group-cell__label,
71
- .ogx--dark .ogx-col-group-cell__label {
70
+ [data-theme="dark"] .ogx-col-group-cell__label {
72
71
  color: var(--ogx-color-gray-300, #cbd5e1);
73
72
  }
@@ -1,6 +1,7 @@
1
1
  import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { clampResizeWidth } from './clampResizeWidth';
3
3
  import { isRtl, logicalArrowKey } from '../../utils/direction';
4
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
4
5
 
5
6
 
6
7
  export interface ColumnResizeHandleProps {
@@ -49,6 +50,7 @@ function suppressNextClick(): void {
49
50
 
50
51
  export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
51
52
  const { field, currentWidth, onResize, onAutosize, minWidth, maxWidth, edge = 'end' } = props;
53
+ const locale = useGridLocale();
52
54
  const [isDragging, setIsDragging] = useState(false);
53
55
  const activeRef = useRef<ActiveResize | null>(null);
54
56
 
@@ -164,7 +166,7 @@ export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
164
166
  onKeyDown={handleKeyDown}
165
167
  role="separator"
166
168
  aria-orientation="vertical"
167
- aria-label={`Resize ${field} column`}
169
+ aria-label={locale.headerColumnResizeLabel(field)}
168
170
  aria-valuenow={Math.round(currentWidth)}
169
171
  aria-valuemin={Math.round(Math.min(minWidth ?? 50, currentWidth))}
170
172
  aria-valuemax={maxWidth}
@@ -1,6 +1,7 @@
1
1
  import { useState, useMemo, useRef, useId } from 'react';
2
2
  import { Checkbox } from '../ui/Checkbox';
3
3
  import { Input } from '../ui/Input';
4
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
4
5
  import type { GridColDef, GridRowModel, GridValidRowModel } from '../../types';
5
6
 
6
7
 
@@ -48,6 +49,7 @@ export function ColumnVisibilityPanel<R extends GridValidRowModel = GridRowModel
48
49
  props: ColumnVisibilityPanelProps<R>
49
50
  ) {
50
51
  const { columns, visibleColumns, onVisibilityChange, onShowAll, onHideAll, onColumnReorder, onColumnOrderReset, showNonHideableColumns } = props;
52
+ const locale = useGridLocale();
51
53
  const [searchQuery, setSearchQuery] = useState('');
52
54
  // Each checkbox is named by a <label htmlFor> holding the column name. (Wrapping Checkbox,
53
55
  // which renders its own <label>, in another <label> nested labels and left the name to a
@@ -119,7 +121,7 @@ export function ColumnVisibilityPanel<R extends GridValidRowModel = GridRowModel
119
121
  <Input
120
122
  autoComplete="off"
121
123
  inputClassName="ogx-column-visibility-panel__search-input"
122
- placeholder="Search"
124
+ placeholder={locale.columnsPanelSearchPlaceholder}
123
125
  value={searchQuery}
124
126
  onChange={(e) => setSearchQuery(e.target.value)}
125
127
  />
@@ -153,7 +155,7 @@ export function ColumnVisibilityPanel<R extends GridValidRowModel = GridRowModel
153
155
  className="ogx-column-visibility-panel__drag-handle-wrapper"
154
156
  draggable
155
157
  onDragStart={handleDragStart(col.field)}
156
- title="Drag to reorder"
158
+ title={locale.columnsPanelDragToReorder}
157
159
  >
158
160
  <DragHandleIcon />
159
161
  </span>
@@ -185,7 +187,7 @@ export function ColumnVisibilityPanel<R extends GridValidRowModel = GridRowModel
185
187
  onChange={handleToggleAll}
186
188
  />
187
189
  <label htmlFor={`${idBase}-all`} className="ogx-column-visibility-panel__label">
188
- Show/Hide All
190
+ {locale.columnsPanelShowHideAll}
189
191
  </label>
190
192
  </div>
191
193
  <button
@@ -193,7 +195,7 @@ export function ColumnVisibilityPanel<R extends GridValidRowModel = GridRowModel
193
195
  className="ogx-column-visibility-panel__reset-btn"
194
196
  onClick={() => { onShowAll(); onColumnOrderReset?.(); }}
195
197
  >
196
- Reset
198
+ {locale.columnsPanelReset}
197
199
  </button>
198
200
  </div>
199
201
  </div>
@@ -67,24 +67,7 @@
67
67
  background: var(--ogx-grid-background);
68
68
  box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.05);
69
69
  }
70
- .ogx--loading {
71
- opacity: 0.6;
72
- pointer-events: none;
73
- }
74
- .ogx--loading::after {
75
- content: '';
76
- position: absolute;
77
- top: 0;
78
- left: 0;
79
- right: 0;
80
- bottom: 0;
81
- background: color-mix(in srgb, var(--ogx-grid-background) 60%, transparent);
82
- backdrop-filter: blur(2px);
83
- display: flex;
84
- align-items: center;
85
- justify-content: center;
86
- z-index: var(--ogx-z-index-modal);
87
- }
70
+
88
71
  /* No height prop: fill the container, as a block child of a sized box (height: 100%) or as a
89
72
  flex item (flex: 1 with min-height: 0). In an auto-height parent 100% resolves to auto. */
90
73
  .ogx--fill {
@@ -312,16 +295,6 @@
312
295
  text-overflow: ellipsis;
313
296
  }
314
297
 
315
- .ogx--density-compact .ogx__cell,
316
- .ogx--density-compact .ogx__header-cell {
317
- padding: var(--ogx-spacing-xs) var(--ogx-spacing-sm);
318
- font-size: var(--ogx-font-size-xs);
319
- }
320
- .ogx--density-comfortable .ogx__cell,
321
- .ogx--density-comfortable .ogx__header-cell {
322
- padding: var(--ogx-spacing-md) var(--ogx-spacing-lg);
323
- font-size: var(--ogx-font-size-md);
324
- }
325
298
  .ogx__group-cell-content {
326
299
  font-weight: 500;
327
300
  text-align: start;
@@ -3,6 +3,7 @@ import { formatAggregateForColumn, isAggregationAllowed } from '../../utils/aggr
3
3
  import type { GridColDef, GridAggregationModel, GridAggregationResult, GridColumnPinning } from '../../types';
4
4
  import { calculatePinnedPositions, getPinnedEdgeFields, isColumnPinned } from '../../utils/pinning';
5
5
  import { getRenderedColumnWidth } from '../../utils/columnWidth';
6
+ import { useGridLocale } from './gridLocaleContext';
6
7
 
7
8
  const SYSTEM_COLUMN_WIDTH = 48;
8
9
 
@@ -41,6 +42,7 @@ export function GridAggregationFooter({
41
42
  pinnedColumns,
42
43
  loading = false,
43
44
  }: GridAggregationFooterProps) {
45
+ const locale = useGridLocale();
44
46
  // Same offsets as Row, so pinned totals sit exactly under their pinned body cells.
45
47
  const pinnedOffsets = useMemo(
46
48
  () => calculatePinnedPositions(
@@ -77,7 +79,7 @@ export function GridAggregationFooter({
77
79
  <div
78
80
  className={`ogx__aggregation-footer${loading ? ' ogx__aggregation-footer--loading' : ''}`}
79
81
  role="row"
80
- aria-label="Aggregation totals"
82
+ aria-label={locale.aggregationFooterLabel}
81
83
  aria-live="polite"
82
84
  aria-busy={loading || undefined}
83
85
  style={{ minHeight: `${rowHeight}px` }}
@@ -1,4 +1,5 @@
1
1
  import { getDataSourceErrorMessage } from '../../utils/dataSource';
2
+ import { useGridLocale } from './gridLocaleContext';
2
3
 
3
4
  interface GridErrorOverlayProps {
4
5
  error: unknown;
@@ -7,6 +8,7 @@ interface GridErrorOverlayProps {
7
8
  }
8
9
 
9
10
  export function GridErrorOverlay({ error, onRetry }: GridErrorOverlayProps) {
11
+ const locale = useGridLocale();
10
12
  if (!error) return null;
11
13
  const message = getDataSourceErrorMessage(error);
12
14
  return (
@@ -17,7 +19,7 @@ export function GridErrorOverlay({ error, onRetry }: GridErrorOverlayProps) {
17
19
  <line x1="12" y1="16" x2="12.01" y2="16" />
18
20
  </svg>
19
21
  <div className="ogx-error-overlay__title">
20
- Oops! Something went wrong
22
+ {locale.errorOverlayTitle}
21
23
  </div>
22
24
  <div className="ogx-error-overlay__message">
23
25
  {message}
@@ -28,7 +30,7 @@ export function GridErrorOverlay({ error, onRetry }: GridErrorOverlayProps) {
28
30
  onClick={onRetry}
29
31
  className="ogx-button ogx-button--primary"
30
32
  >
31
- Retry
33
+ {locale.errorOverlayRetry}
32
34
  </button>
33
35
  )}
34
36
  </div>