feat: integrate AG Grid with Mantine theme and add showcase component
This commit is contained in:
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user