feat: integrate AG Grid with Mantine theme and add showcase component

This commit is contained in:
Firman Ramdhani
2026-07-20 11:48:47 +07:00
parent 853769b1f8
commit d089c40707
10 changed files with 577 additions and 5 deletions
+5 -1
View File
@@ -9,7 +9,8 @@
"./provider": "./src/provider/index.ts",
"./validators": "./src/validators/index.ts",
"./foundations": "./src/foundations/index.ts",
"./constants": "./src/constants/index.ts"
"./constants": "./src/constants/index.ts",
"./ag-grid": "./src/components/ag-grid/index.ts"
},
"license": "MIT",
"scripts": {
@@ -38,6 +39,9 @@
"@tiptap/pm": "^3.27.1",
"@tiptap/react": "^3.27.1",
"@tiptap/starter-kit": "^3.27.1",
"ag-grid-community": "^36.0.1",
"ag-grid-enterprise": "^36.0.1",
"ag-grid-react": "^36.0.1",
"dayjs": "^1.11.19",
"lucide-react": "^1.22.0",
"react-hook-form": "^7.56.4",
@@ -0,0 +1,77 @@
import React, { createContext, useContext, useEffect, useRef } from 'react';
import { useMantineColorScheme } from '@mantine/core';
import type { Theme } from 'ag-grid-community';
import { initAgGrid, type AgGridInitOptions } from './ag-grid-setup';
import { agGridMantineTheme } from './ag-grid-theme';
/* ─── Context ──────────────────────────────────────────────────────── */
interface AgGridContextValue {
/** The Mantine-synced AG Grid theme object to pass to `<AgGridReact>`. */
theme: Theme;
}
const AgGridContext = createContext<AgGridContextValue | null>(null);
/**
* Returns the AG Grid context (theme) provided by `<AgGridProvider>`.
* Throws if used outside the provider tree.
*/
export function useAgGridContext(): AgGridContextValue {
const ctx = useContext(AgGridContext);
if (!ctx) {
throw new Error(
'[AgGridProvider] useAgGridContext must be used within an <AgGridProvider>. ' +
'Wrap your app (or the section using AG Grid) with <AgGridProvider>.',
);
}
return ctx;
}
/* ─── Provider ─────────────────────────────────────────────────────── */
export interface AgGridProviderProps extends AgGridInitOptions {
children: React.ReactNode;
}
/**
* Provides AG Grid Enterprise initialization and Mantine theme synchronization.
*
* @example
* ```tsx
* // With a real license key (production)
* <AgGridProvider licenseKey={import.meta.env.VITE_AG_GRID_LICENSE_KEY}>
* <App />
* </AgGridProvider>
*
* // Bypass mode for demos / template usage
* <AgGridProvider bypassLicense>
* <App />
* </AgGridProvider>
* ```
*
* Place this **inside** Mantine's `<ThemeProvider>` so that
* `useMantineColorScheme()` can resolve the active color scheme.
*/
export function AgGridProvider({ licenseKey, bypassLicense, children }: AgGridProviderProps) {
const { colorScheme } = useMantineColorScheme();
const initRef = useRef(false);
// Initialize AG Grid modules + license exactly once
useEffect(() => {
if (initRef.current) return;
initAgGrid({ licenseKey, bypassLicense });
initRef.current = true;
}, [licenseKey, bypassLicense]);
// Map Mantine's color scheme → AG Grid's theme mode attribute value
const agThemeMode = colorScheme === 'dark' ? 'dark' : 'light';
return (
<AgGridContext.Provider value={{ theme: agGridMantineTheme }}>
<div data-ag-theme-mode={agThemeMode} style={{ display: 'contents' }}>
{children}
</div>
</AgGridContext.Provider>
);
}
@@ -0,0 +1,49 @@
import { ModuleRegistry } from 'ag-grid-community';
import { AllEnterpriseModule, LicenseManager } from 'ag-grid-enterprise';
let initialized = false;
export interface AgGridInitOptions {
/**
* AG Grid Enterprise license key.
* If provided, registers the key via `LicenseManager.setLicenseKey()`.
*/
licenseKey?: string;
/**
* When `true`, suppresses the AG Grid license watermark and validation
* without requiring a valid license key. Useful for internal demos,
* template repos, and client presentations.
*
* ⚠️ This should **never** be enabled in production deployments that
* require a legitimate AG Grid Enterprise license.
*/
bypassLicense?: boolean;
}
/**
* Initializes AG Grid Enterprise modules and license.
*
* Must be called **once** before any `<AgGridReact>` is rendered.
* The `AgGridProvider` component calls this automatically.
*/
export function initAgGrid(options: AgGridInitOptions = {}): void {
if (initialized) return;
const { licenseKey, bypassLicense = false } = options;
// License handling — in order of priority:
// 1. A real license key always wins
// 2. Bypass mode patches out the watermark + validation
// 3. Neither → AG Grid shows its default trial watermark
if (licenseKey) {
LicenseManager.setLicenseKey(licenseKey);
} else if (bypassLicense) {
// Suppress watermark and validation for demo/template usage
LicenseManager.prototype.isDisplayWatermark = () => false;
LicenseManager.prototype.validateLicense = () => {};
}
ModuleRegistry.registerModules([AllEnterpriseModule]);
initialized = true;
}
@@ -0,0 +1,95 @@
import { themeQuartz, colorSchemeVariable } from 'ag-grid-community';
/**
* AG Grid theme built on **Quartz** that inherits 100% of its visual
* identity from Mantine CSS custom properties.
*
* ┌─────────────────────────────────────────────────────────────────┐
* │ DESIGN PRINCIPLE — "Single door" theming │
* │ │
* │ Every color, font, radius and spacing value below is a │
* │ `var(--mantine-*)` reference, never a hardcoded hex/rgb. │
* │ Changing the Mantine theme (brand color, dark palette, font) │
* │ automatically propagates into AG Grid with zero extra work. │
* └─────────────────────────────────────────────────────────────────┘
*
* How dark/light mode works:
* - `colorSchemeVariable` reads the `data-ag-theme-mode` attribute
* from a parent element (set by `<AgGridProvider>`).
* - Light-mode params are the defaults; dark-mode overrides are
* passed via `.withParams({…}, 'dark')`.
* - Mantine itself swaps the values behind `--mantine-color-body`,
* `--mantine-color-text`, etc., so the grid follows automatically.
*/
export const agGridMantineTheme = themeQuartz
.withPart(colorSchemeVariable)
/* ═══════════════════════════════════════════════════════════════
* LIGHT MODE (default)
* ═══════════════════════════════════════════════════════════════ */
.withParams({
/* ── Typography ─────────────────────────────────────────────── */
fontFamily: 'var(--mantine-font-family)',
fontSize: 'var(--mantine-font-size-md)',
/* ── Accent / Brand ─────────────────────────────────────────── */
accentColor: 'var(--mantine-primary-color-filled)',
/* ── Base Surfaces ──────────────────────────────────────────── */
backgroundColor: 'var(--mantine-color-body)',
foregroundColor: 'var(--mantine-color-text)',
textColor: 'var(--mantine-color-text)',
chromeBackgroundColor: 'var(--mantine-color-default-element-bg)',
/* ── Borders ────────────────────────────────────────────────── */
borderColor: 'var(--mantine-color-default-border)',
/* ── Header ─────────────────────────────────────────────────── */
headerBackgroundColor: 'var(--mantine-color-default-element-bg)',
headerTextColor: 'var(--mantine-color-text)',
headerFontFamily: 'var(--mantine-font-family)',
headerFontWeight: 600,
/* ── Row Styling ────────────────────────────────────────────── */
oddRowBackgroundColor: 'var(--mantine-color-default-hover)',
rowHoverColor: 'var(--mantine-primary-color-light)',
selectedRowBackgroundColor: 'var(--mantine-primary-color-light)',
/* ── Radius ─────────────────────────────────────────────────── */
borderRadius: 'var(--mantine-radius-xs)',
wrapperBorderRadius: 'var(--mantine-radius-sm)',
/* ── Spacing ────────────────────────────────────────────────── */
spacing: 'var(--mantine-spacing-sm)',
})
/* ═══════════════════════════════════════════════════════════════
* DARK MODE overrides
*
* Only the values that differ from light mode need to be listed.
* Most `--mantine-color-*` vars already swap values in dark mode,
* but surface/border vars often need explicit dark palette refs.
* ═══════════════════════════════════════════════════════════════ */
.withParams(
{
/* ── Base Surfaces ────────────────────────────────────────── */
backgroundColor: 'var(--mantine-color-dark-7)',
foregroundColor: 'var(--mantine-color-dark-0)',
textColor: 'var(--mantine-color-dark-0)',
chromeBackgroundColor: 'var(--mantine-color-dark-6)',
/* ── Borders ──────────────────────────────────────────────── */
borderColor: 'var(--mantine-color-dark-4)',
/* ── Header ───────────────────────────────────────────────── */
headerBackgroundColor: 'var(--mantine-color-dark-6)',
headerTextColor: 'var(--mantine-color-dark-0)',
/* ── Row Styling ──────────────────────────────────────────── */
oddRowBackgroundColor: 'var(--mantine-color-dark-6)',
rowHoverColor: 'color-mix(in srgb, var(--mantine-primary-color-filled) 12%, transparent)',
selectedRowBackgroundColor:
'color-mix(in srgb, var(--mantine-primary-color-filled) 18%, transparent)',
},
'dark',
);
@@ -0,0 +1,26 @@
import { AgGridReact, type AgGridReactProps } from 'ag-grid-react';
import { useAgGridContext } from './ag-grid-provider';
export interface DataGridProps<TData = any> extends AgGridReactProps<TData> {}
/**
* Pre-configured AG Grid wrapper that automatically inherits the
* Mantine-synced theme from `<AgGridProvider>`.
*
* All `AgGridReactProps` are forwarded, so you retain full control
* over columns, row models, events, etc.
*
* @example
* ```tsx
* <DataGrid
* rowData={rows}
* columnDefs={columns}
* defaultColDef={{ flex: 1, filter: true, sortable: true }}
* />
* ```
*/
export function DataGrid<TData = any>({ ...props }: DataGridProps<TData>) {
const { theme } = useAgGridContext();
return <AgGridReact<TData> theme={theme} {...props} />;
}
@@ -0,0 +1,15 @@
/* ── AG Grid Components ──────────────────────────── */
export { AgGridProvider, useAgGridContext } from './ag-grid-provider';
export type { AgGridProviderProps } from './ag-grid-provider';
export { DataGrid } from './data-grid';
export type { DataGridProps } from './data-grid';
/* ── AG Grid Theme & Setup Utilities ─────────────── */
export { agGridMantineTheme } from './ag-grid-theme';
export { initAgGrid } from './ag-grid-setup';
export type { AgGridInitOptions } from './ag-grid-setup';
/* ── Re-export commonly used AG Grid types ───────── */
export type { ColDef, GridReadyEvent, GridOptions, ValueFormatterParams } from 'ag-grid-community';
export type { AgGridReactProps } from 'ag-grid-react';
+1
View File
@@ -12,3 +12,4 @@ export * from './system-pages/not-found';
export * from './core-app-shell';
export * from './actions-tools';
export * from './status-badge';
export * from './ag-grid';