diff --git a/docs/web/docs/.vitepress/config.ts b/docs/web/docs/.vitepress/config.ts
index bbf9d3d3..84e13772 100644
--- a/docs/web/docs/.vitepress/config.ts
+++ b/docs/web/docs/.vitepress/config.ts
@@ -75,6 +75,10 @@ export default defineConfig({
text: "Integration",
items: [
{ text: "Component Libraries", link: "/featured/libraries" },
+ {
+ text: "AG Grid (No Adapter)",
+ link: "/featured/ag-grid-no-adapter",
+ },
{ text: "Web Components", link: "/featured/web-components" },
{ text: "JSX Support", link: "/featured/jsx" },
{ text: "Vue Templates", link: "/featured/vue-templates" },
diff --git a/docs/web/docs/featured/ag-grid-no-adapter.md b/docs/web/docs/featured/ag-grid-no-adapter.md
new file mode 100644
index 00000000..c26f7629
--- /dev/null
+++ b/docs/web/docs/featured/ag-grid-no-adapter.md
@@ -0,0 +1,97 @@
+---
+title: AG Grid (No Adapter)
+description: Integrate AG Grid directly in Inglorious Web without a dedicated adapter package.
+---
+
+# AG Grid (No Adapter)
+
+If your team prefers zero abstraction, you can mount AG Grid directly from an Inglorious Web type.
+
+## When this is enough
+
+- You only have one or two grid screens.
+- You want full direct access to AG Grid API and options.
+- You do not need a reusable adapter layer.
+
+## Minimal direct integration
+
+```js
+import { html, ref } from "@inglorious/web"
+import {
+ AllCommunityModule,
+ createGrid,
+ ModuleRegistry,
+} from "ag-grid-community"
+
+ModuleRegistry.registerModules([AllCommunityModule])
+
+const gridInstances = new Map()
+
+export const gridType = {
+ create(entity) {
+ entity.themeClass ??= "ag-theme-quartz"
+ entity.height ??= 520
+ entity.rowData ??= []
+ entity.columnDefs ??= []
+ },
+
+ render(entity) {
+ const height =
+ typeof entity.height === "number" ? `${entity.height}px` : entity.height
+
+ return html`
+
{
+ if (!el) return
+
+ const current = gridInstances.get(entity.id)
+ if (!current) {
+ const api = createGrid(el, {
+ rowData: entity.rowData,
+ columnDefs: entity.columnDefs,
+ })
+ gridInstances.set(entity.id, { api, el })
+ return
+ }
+
+ if (current.el !== el) {
+ current.api.destroy()
+ const api = createGrid(el, {
+ rowData: entity.rowData,
+ columnDefs: entity.columnDefs,
+ })
+ gridInstances.set(entity.id, { api, el })
+ return
+ }
+
+ current.api.updateGridOptions({
+ rowData: entity.rowData,
+ columnDefs: entity.columnDefs,
+ })
+ })}
+ >
+ `
+ },
+
+ destroy(entity) {
+ const current = gridInstances.get(entity.id)
+ if (!current) return
+ current.api.destroy()
+ gridInstances.delete(entity.id)
+ },
+}
+```
+
+## CSS imports
+
+```js
+import "ag-grid-community/styles/ag-grid.css"
+import "ag-grid-community/styles/ag-theme-quartz.css"
+```
+
+## Trade-off vs adapter package
+
+- Direct recipe: less code upfront, maximum control.
+- Adapter package: reusable lifecycle patterns and consistent event surface across apps.
diff --git a/docs/web/docs/featured/overview.md b/docs/web/docs/featured/overview.md
index fc1c09e5..7bfb09c7 100644
--- a/docs/web/docs/featured/overview.md
+++ b/docs/web/docs/featured/overview.md
@@ -184,8 +184,10 @@ Inglorious Web plays nicely with:
- **Web Components** — Shoelace, Material Web Components, etc.
- **Design Systems** — Any CSS framework
- **Specialized Libraries** — Date pickers, rich editors, etc.
+- **Data Grids** — AG Grid can be integrated directly (no adapter required)
**[Learn more →](./web-components.md)**
+**[AG Grid recipe (no adapter) →](./ag-grid-no-adapter.md)**
## Ecosystem Packages
diff --git a/examples/apps/web-ag-grid/eslint.config.js b/examples/apps/web-ag-grid/eslint.config.js
new file mode 100644
index 00000000..3f045e7e
--- /dev/null
+++ b/examples/apps/web-ag-grid/eslint.config.js
@@ -0,0 +1 @@
+export { default } from "@inglorious/eslint-config/browser"
diff --git a/examples/apps/web-ag-grid/index.html b/examples/apps/web-ag-grid/index.html
new file mode 100644
index 00000000..676f50ef
--- /dev/null
+++ b/examples/apps/web-ag-grid/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ web-ag-grid
+
+
+
+
+
+
+
diff --git a/examples/apps/web-ag-grid/package.json b/examples/apps/web-ag-grid/package.json
new file mode 100644
index 00000000..2b5e66bd
--- /dev/null
+++ b/examples/apps/web-ag-grid/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "web-ag-grid",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "author": "IceOnFire (https://ingloriouscoderz.it)",
+ "license": "MIT",
+ "scripts": {
+ "format": "prettier --write .",
+ "lint": "eslint .",
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@inglorious/ag-grid": "workspace:*",
+ "@inglorious/web": "workspace:*",
+ "ag-grid-community": "^34.3.1"
+ },
+ "devDependencies": {
+ "@inglorious/eslint-config": "workspace:*",
+ "eslint": "^9.39.1",
+ "prettier": "^3.6.2",
+ "rollup-plugin-minify-template-literals": "^1.1.7",
+ "vite": "^7.1.6"
+ }
+}
diff --git a/examples/apps/web-ag-grid/public/vite.svg b/examples/apps/web-ag-grid/public/vite.svg
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/examples/apps/web-ag-grid/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/apps/web-ag-grid/src/app.js b/examples/apps/web-ag-grid/src/app.js
new file mode 100644
index 00000000..7fe99421
--- /dev/null
+++ b/examples/apps/web-ag-grid/src/app.js
@@ -0,0 +1,42 @@
+import { html } from "@inglorious/web"
+
+export const app = {
+ render(api) {
+ const grid = api.getEntity("agGrid")
+
+ return html`
+
+
+
+
+
+
+
+
+
+
+
+
+ Entity: ${grid.id}
+ Render Tick: ${grid.tickCount}
+ Grid API ID: ${grid.gridApiId ?? "pending"}
+ Status: ${grid.gridStatus}
+
+ ${api.render("agGrid")}
+
+
+ `
+ },
+}
diff --git a/examples/apps/web-ag-grid/src/main.js b/examples/apps/web-ag-grid/src/main.js
new file mode 100644
index 00000000..67dd33e2
--- /dev/null
+++ b/examples/apps/web-ag-grid/src/main.js
@@ -0,0 +1,22 @@
+import "ag-grid-community/styles/ag-grid.css"
+import "ag-grid-community/styles/ag-theme-quartz.css"
+
+import { configureAgGrid } from "@inglorious/ag-grid"
+import { mount } from "@inglorious/web"
+import {
+ AllCommunityModule,
+ createGrid,
+ ModuleRegistry,
+} from "ag-grid-community"
+
+import { app } from "./app.js"
+import { store } from "./store/index.js"
+
+configureAgGrid({
+ createGrid,
+ registerModules() {
+ ModuleRegistry.registerModules([AllCommunityModule])
+ },
+})
+
+mount(store, app.render, document.getElementById("root"))
diff --git a/examples/apps/web-ag-grid/src/store/entities.js b/examples/apps/web-ag-grid/src/store/entities.js
new file mode 100644
index 00000000..c552a1bc
--- /dev/null
+++ b/examples/apps/web-ag-grid/src/store/entities.js
@@ -0,0 +1,104 @@
+class MyCellComponent {
+ eGui
+ eButton
+ eventListener
+
+ init(params) {
+ this.eGui = document.createElement("div")
+ this.eGui.innerHTML =
+ `
+
+ ` + params.value
+ this.eButton = this.eGui.querySelector("button")
+ this.eventListener = () => alert(`Value: ${params.value}`)
+ this.eButton.addEventListener("click", this.eventListener)
+ }
+
+ getGui() {
+ return this.eGui
+ }
+
+ refresh() {
+ return true
+ }
+
+ destroy() {
+ this.eButton.removeEventListener("click", this.eventListener)
+ }
+}
+
+export const entities = {
+ agGrid: {
+ type: "grid",
+ title: "Sales Data",
+ tickCount: 0,
+ rowIdField: "id",
+ themeClass: "ag-theme-quartz",
+ height: 520,
+ columnDefs: [
+ { field: "id", maxWidth: 100 },
+ { field: "product", cellRenderer: MyCellComponent },
+ { field: "category" },
+ { field: "country" },
+ {
+ field: "revenue",
+ valueFormatter: (p) => "€" + p.value.toLocaleString(),
+ },
+ { field: "price", valueFormatter: (p) => "€" + p.value.toLocaleString() },
+ { field: "rating" },
+ { field: "growth", valueFormatter: ({ value }) => `${value}%` },
+ ],
+ rowData: [
+ {
+ id: 1,
+ product: "Forge Cloud",
+ category: "Software",
+ country: "Italy",
+ revenue: 120000,
+ price: 120,
+ rating: 4.7,
+ growth: 8.1,
+ },
+ {
+ id: 2,
+ product: "Forge Beam",
+ category: "Hardware",
+ country: "Brazil",
+ revenue: 98000,
+ price: 98,
+ rating: 4.4,
+ growth: 5.4,
+ },
+ {
+ id: 3,
+ product: "Forge Shield",
+ category: "Software",
+ country: "United States",
+ revenue: 154000,
+ price: 154,
+ rating: 4.9,
+ growth: 10.2,
+ },
+ {
+ id: 4,
+ product: "Forge Arc",
+ category: "Hardware",
+ country: "Germany",
+ revenue: 76000,
+ price: 76,
+ rating: 4.1,
+ growth: 3.6,
+ },
+ {
+ id: 5,
+ product: "Forge Loop",
+ category: "Software",
+ country: "Japan",
+ revenue: 143000,
+ price: 143,
+ rating: 4.8,
+ growth: 9.4,
+ },
+ ],
+ },
+}
diff --git a/examples/apps/web-ag-grid/src/store/index.js b/examples/apps/web-ag-grid/src/store/index.js
new file mode 100644
index 00000000..34cfeaac
--- /dev/null
+++ b/examples/apps/web-ag-grid/src/store/index.js
@@ -0,0 +1,12 @@
+import { agGrid } from "@inglorious/ag-grid"
+import { createStore } from "@inglorious/web"
+
+import { gridDemo } from "../types/grid-demo/index.js"
+import { entities } from "./entities.js"
+import { middlewares } from "./middlewares.js"
+
+export const store = createStore({
+ types: { grid: [agGrid, gridDemo] },
+ entities,
+ middlewares,
+})
diff --git a/examples/apps/web-ag-grid/src/store/middlewares.js b/examples/apps/web-ag-grid/src/store/middlewares.js
new file mode 100644
index 00000000..3fd34bab
--- /dev/null
+++ b/examples/apps/web-ag-grid/src/store/middlewares.js
@@ -0,0 +1,7 @@
+import { createDevtools } from "@inglorious/web"
+
+export const middlewares = []
+
+if (import.meta.env.DEV) {
+ middlewares.push(createDevtools().middleware)
+}
diff --git a/examples/apps/web-ag-grid/src/style.css b/examples/apps/web-ag-grid/src/style.css
new file mode 100644
index 00000000..3609c0e3
--- /dev/null
+++ b/examples/apps/web-ag-grid/src/style.css
@@ -0,0 +1,69 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ background: #f2f3f6;
+ color: #121826;
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
+ Arial, sans-serif;
+}
+
+.app-shell {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 24px;
+}
+
+.app-header {
+ background: #fff;
+ border: 1px solid #e5e7eb;
+ border-radius: 10px;
+ padding: 20px;
+ margin-bottom: 16px;
+}
+
+.app-header h1 {
+ margin: 0 0 8px;
+}
+
+.app-header p {
+ margin: 0;
+ color: #4b5563;
+}
+
+.controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-bottom: 16px;
+}
+
+.controls button {
+ border: 1px solid #c7ced8;
+ background: #fff;
+ border-radius: 8px;
+ padding: 8px 12px;
+ cursor: pointer;
+}
+
+.controls button:hover {
+ background: #f9fafb;
+}
+
+.demo-main {
+ background: #fff;
+ border: 1px solid #e5e7eb;
+ border-radius: 10px;
+ padding: 12px;
+}
+
+.iw-ag-grid-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ margin-bottom: 10px;
+ font-size: 14px;
+}
diff --git a/examples/apps/web-ag-grid/src/types/grid-demo/handlers.js b/examples/apps/web-ag-grid/src/types/grid-demo/handlers.js
new file mode 100644
index 00000000..000cc63b
--- /dev/null
+++ b/examples/apps/web-ag-grid/src/types/grid-demo/handlers.js
@@ -0,0 +1,98 @@
+/* eslint-disable no-magic-numbers */
+const PRICE_BUMP = 15
+
+/**
+ * @typedef {Record & {
+ * tickCount?: number
+ * rowIdField?: string
+ * rowData: Record[]
+ * }} GridDemoEntity
+ */
+
+/**
+ * Initializes demo-specific counters.
+ * @param {GridDemoEntity} entity
+ */
+export function create(entity) {
+ entity.tickCount ??= 0
+}
+
+/**
+ * Increments the render tick counter for the demo UI.
+ * @param {GridDemoEntity} entity
+ */
+export function tick(entity) {
+ entity.tickCount += 1
+}
+
+/**
+ * Shuffles rows to showcase incremental row updates.
+ * @param {GridDemoEntity} entity
+ */
+export function shuffleRows(entity) {
+ const nextRows = [...entity.rowData]
+ for (let i = nextRows.length - 1; i > 0; i -= 1) {
+ const j = Math.floor(Math.random() * (i + 1))
+ const current = nextRows[i]
+ nextRows[i] = nextRows[j]
+ nextRows[j] = current
+ }
+ entity.rowData = nextRows
+}
+
+/**
+ * Applies a simple price/rating mutation used by demo controls.
+ * @param {GridDemoEntity} entity
+ */
+export function bumpPrices(entity) {
+ entity.rowData = entity.rowData.map((row) => ({
+ ...row,
+ price: typeof row.price === "number" ? row.price + PRICE_BUMP : row.price,
+ rating:
+ typeof row.rating === "number"
+ ? Number((row.rating + Math.random()).toFixed(1))
+ : row.rating,
+ }))
+}
+
+/**
+ * Appends a synthetic row based on the last row shape.
+ * @param {GridDemoEntity} entity
+ */
+export function addRow(entity) {
+ const idField = entity.rowIdField || "id"
+ const nextId = entity.rowData.length + 1
+ const previousRow = entity.rowData.at(-1) || {}
+ const nextRow = {}
+
+ const entries = Object.entries(previousRow)
+ if (!entries.length) {
+ nextRow[idField] = nextId
+ }
+
+ for (const [key, value] of entries) {
+ if (key === idField) {
+ nextRow[key] = nextId
+ continue
+ }
+
+ if (typeof value === "number") {
+ const randomFactor = 0.9 + Math.random() * 0.2
+ nextRow[key] = Number((value * randomFactor).toFixed(2))
+ continue
+ }
+
+ if (typeof value === "string") {
+ nextRow[key] = `${value} ${nextId}`
+ continue
+ }
+
+ nextRow[key] = value
+ }
+
+ if (!(idField in nextRow)) {
+ nextRow[idField] = nextId
+ }
+
+ entity.rowData = [...entity.rowData, nextRow]
+}
diff --git a/examples/apps/web-ag-grid/src/types/grid-demo/index.js b/examples/apps/web-ag-grid/src/types/grid-demo/index.js
new file mode 100644
index 00000000..3797ad93
--- /dev/null
+++ b/examples/apps/web-ag-grid/src/types/grid-demo/index.js
@@ -0,0 +1,6 @@
+import * as handlers from "./handlers.js"
+
+/** Demo-only behavior layered on top of the base `agGrid` adapter. */
+export const gridDemo = {
+ ...handlers,
+}
diff --git a/examples/apps/web-ag-grid/vite.config.js b/examples/apps/web-ag-grid/vite.config.js
new file mode 100644
index 00000000..0d05b7dd
--- /dev/null
+++ b/examples/apps/web-ag-grid/vite.config.js
@@ -0,0 +1,22 @@
+import path from "node:path"
+import { fileURLToPath } from "node:url"
+
+import { minifyTemplateLiterals } from "rollup-plugin-minify-template-literals"
+import { defineConfig } from "vite"
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+
+export default defineConfig({
+ build: {
+ rollupOptions: {
+ plugins: [minifyTemplateLiterals()],
+ },
+ },
+
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "src"),
+ },
+ },
+})
diff --git a/packages/ag-grid/CHANGELOG.md b/packages/ag-grid/CHANGELOG.md
new file mode 100644
index 00000000..92dbcb1b
--- /dev/null
+++ b/packages/ag-grid/CHANGELOG.md
@@ -0,0 +1,5 @@
+# @inglorious/ag-grid
+
+## 0.1.0
+
+- Initial extraction from `@inglorious/web` as a standalone package.
diff --git a/packages/ag-grid/README.md b/packages/ag-grid/README.md
new file mode 100644
index 00000000..5252bc51
--- /dev/null
+++ b/packages/ag-grid/README.md
@@ -0,0 +1,86 @@
+# @inglorious/ag-grid
+
+AG Grid integration for Inglorious Web.
+
+This package is intentionally thin: it provides a generic `agGrid` type and minimal adapter CSS.
+Keep app-specific data mutations and domain handlers in your app types via composition.
+
+## Install
+
+```bash
+pnpm add @inglorious/ag-grid @inglorious/web ag-grid-community
+```
+
+## Usage
+
+```js
+import { createStore } from "@inglorious/web"
+import { agGrid, configureAgGrid } from "@inglorious/ag-grid"
+import {
+ AllCommunityModule,
+ createGrid,
+ ModuleRegistry,
+} from "ag-grid-community"
+
+configureAgGrid({
+ createGrid,
+ registerModules() {
+ ModuleRegistry.registerModules([AllCommunityModule])
+ },
+})
+
+const store = createStore({
+ types: {
+ grid: agGrid,
+ },
+ entities: {
+ agGrid: {
+ type: "grid",
+ columnDefs: [],
+ rowData: [],
+ },
+ },
+})
+```
+
+```js
+import "ag-grid-community/styles/ag-grid.css"
+import "ag-grid-community/styles/ag-theme-quartz.css"
+```
+
+Set sizing from entity state:
+
+```js
+{
+ type: "grid",
+ themeClass: "ag-theme-quartz",
+ height: 520, // number (px) or CSS string like "70vh"
+ gridOptions: {
+ suppressCellFocus: true,
+ },
+ columnDefs: [],
+ rowData: [],
+}
+```
+
+For AG Grid Enterprise, import enterprise modules and license setup in your app, then pass the enterprise `createGrid` and module registration via `configureAgGrid(...)`.
+
+## Event handlers
+
+The adapter keeps a small event surface:
+
+- `mounted` updates runtime status when the grid instance is ready.
+- `rowDataChange` replaces `entity.rowData`.
+- `columnDefsChange` replaces `entity.columnDefs`.
+- `gridOptionsChange` replaces `entity.gridOptions`.
+- `apiCall` calls a grid API method by name:
+
+```js
+api.notify("#agGrid:apiCall", { method: "sizeColumnsToFit" })
+api.notify("#agGrid:apiCall", {
+ method: "setFilterModel",
+ args: [{ id: null }],
+})
+```
+
+Backward-compatible aliases still exist (`gridMounted`, `setRowData`, `setColumnDefs`, `setGridOptions`).
diff --git a/packages/ag-grid/eslint.config.js b/packages/ag-grid/eslint.config.js
new file mode 100644
index 00000000..3f045e7e
--- /dev/null
+++ b/packages/ag-grid/eslint.config.js
@@ -0,0 +1 @@
+export { default } from "@inglorious/eslint-config/browser"
diff --git a/packages/ag-grid/package.json b/packages/ag-grid/package.json
new file mode 100644
index 00000000..9f569282
--- /dev/null
+++ b/packages/ag-grid/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@inglorious/ag-grid",
+ "version": "0.1.0",
+ "description": "AG Grid integration for Inglorious Web.",
+ "author": "IceOnFire (https://ingloriouscoderz.it)",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/IngloriousCoderz/inglorious-forge.git",
+ "directory": "packages/ag-grid"
+ },
+ "bugs": {
+ "url": "https://github.com/IngloriousCoderz/inglorious-forge/issues"
+ },
+ "keywords": [
+ "ag-grid",
+ "data-grid",
+ "inglorious-web"
+ ],
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./types/index.d.ts",
+ "import": "./src/index.js"
+ }
+ },
+ "files": [
+ "src",
+ "types"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "scripts": {
+ "format": "prettier --write .",
+ "lint": "eslint .",
+ "test:watch": "vitest",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "@inglorious/web": "workspace:*"
+ },
+ "peerDependencies": {
+ "@inglorious/web": "workspace:*",
+ "ag-grid-community": "^34.3.1"
+ },
+ "devDependencies": {
+ "@inglorious/eslint-config": "workspace:*",
+ "prettier": "^3.6.2",
+ "vitest": "^4.0.15"
+ },
+ "packageManager": "pnpm@10.15.0",
+ "engines": {
+ "node": ">= 22"
+ }
+}
diff --git a/packages/ag-grid/src/defaults.js b/packages/ag-grid/src/defaults.js
new file mode 100644
index 00000000..6a785960
--- /dev/null
+++ b/packages/ag-grid/src/defaults.js
@@ -0,0 +1,12 @@
+/** Default AG Grid theme class used by the adapter. */
+export const DEFAULT_THEME_CLASS = "ag-theme-quartz"
+
+/** Default column definition merged into `entity.defaultColDef`. */
+export const DEFAULT_COL_DEF = {
+ filter: true,
+ floatingFilter: true,
+ sortable: true,
+ resizable: true,
+ flex: 1,
+ minWidth: 120,
+}
diff --git a/packages/ag-grid/src/handlers.js b/packages/ag-grid/src/handlers.js
new file mode 100644
index 00000000..cd0ee447
--- /dev/null
+++ b/packages/ag-grid/src/handlers.js
@@ -0,0 +1,96 @@
+import { DEFAULT_COL_DEF, DEFAULT_THEME_CLASS } from "./defaults.js"
+import { callGridApi, destroyGrid } from "./runtime.js"
+
+/**
+ * @typedef {Record & {
+ * id: string
+ * rowIdField?: string
+ * themeClass?: string
+ * height?: number | string
+ * defaultColDef?: Record
+ * rowData?: any[]
+ * columnDefs?: any[]
+ * gridOptions?: Record
+ * gridApiId?: string | number | null
+ * gridStatus?: string
+ * }} AgGridEntity
+ */
+
+/**
+ * Initializes an AG Grid entity with adapter defaults.
+ * @param {AgGridEntity} entity
+ */
+export function create(entity) {
+ entity.rowIdField ??= "id"
+ entity.themeClass ??= DEFAULT_THEME_CLASS
+ entity.height ??= 520
+ entity.defaultColDef = {
+ ...DEFAULT_COL_DEF,
+ ...(entity.defaultColDef || {}),
+ }
+ entity.gridApiId ??= null
+ entity.gridStatus ??= "mounting"
+ entity.rowData ??= []
+ entity.columnDefs ??= []
+ entity.gridOptions ??= {}
+}
+
+/**
+ * Marks runtime mount completion and stores a stable API id.
+ * @param {AgGridEntity} entity
+ * @param {{ gridApiId?: string | number }} [payload]
+ */
+export function mounted(entity, payload) {
+ entity.gridApiId = payload?.gridApiId ?? entity.gridApiId
+ entity.gridStatus = "mounted"
+}
+
+/**
+ * Replaces the grid row data.
+ * @param {AgGridEntity} entity
+ * @param {any[]} rowData
+ */
+export function rowDataChange(entity, rowData) {
+ entity.rowData = rowData
+}
+
+/**
+ * Replaces the grid column definitions.
+ * @param {AgGridEntity} entity
+ * @param {any[]} columnDefs
+ */
+export function columnDefsChange(entity, columnDefs) {
+ entity.columnDefs = columnDefs
+}
+
+/**
+ * Replaces passthrough AG Grid options.
+ * @param {AgGridEntity} entity
+ * @param {Record} gridOptions
+ */
+export function gridOptionsChange(entity, gridOptions) {
+ entity.gridOptions = gridOptions
+}
+
+/**
+ * Invokes a grid API method by name.
+ * @param {AgGridEntity} entity
+ * @param {string | { method: string, args?: any[] }} payload
+ */
+export function apiCall(entity, payload) {
+ callGridApi(entity.id, payload)
+}
+
+// Backward-compatible aliases.
+export const gridMounted = mounted
+export const setRowData = rowDataChange
+export const setColumnDefs = columnDefsChange
+export const setGridOptions = gridOptionsChange
+
+/**
+ * Disposes runtime resources for this entity.
+ * @param {AgGridEntity} entity
+ */
+export function destroy(entity) {
+ destroyGrid(entity.id)
+}
diff --git a/packages/ag-grid/src/handlers.test.js b/packages/ag-grid/src/handlers.test.js
new file mode 100644
index 00000000..b135d5c6
--- /dev/null
+++ b/packages/ag-grid/src/handlers.test.js
@@ -0,0 +1,91 @@
+import { describe, expect, it } from "vitest"
+
+import {
+ apiCall,
+ columnDefsChange,
+ create,
+ gridMounted,
+ gridOptionsChange,
+ mounted,
+ rowDataChange,
+ setColumnDefs,
+ setGridOptions,
+ setRowData,
+} from "./handlers.js"
+
+describe("handlers", () => {
+ it("initializes default entity values", () => {
+ const entity = { id: "grid-1" }
+
+ create(entity)
+
+ expect(entity.rowIdField).toBe("id")
+ expect(entity.themeClass).toBe("ag-theme-quartz")
+ expect(entity.height).toBe(520)
+ expect(entity.gridStatus).toBe("mounting")
+ expect(entity.rowData).toEqual([])
+ expect(entity.columnDefs).toEqual([])
+ expect(entity.gridOptions).toEqual({})
+ expect(entity.defaultColDef).toEqual(
+ expect.objectContaining({
+ filter: true,
+ floatingFilter: true,
+ sortable: true,
+ }),
+ )
+ })
+
+ it("keeps custom defaults when provided", () => {
+ const entity = {
+ id: "grid-1",
+ rowIdField: "customId",
+ themeClass: "ag-theme-alpine",
+ height: "70vh",
+ defaultColDef: { sortable: false, minWidth: 200 },
+ rowData: [{ id: 1 }],
+ columnDefs: [{ field: "id" }],
+ gridOptions: { suppressCellFocus: true },
+ }
+
+ create(entity)
+
+ expect(entity.rowIdField).toBe("customId")
+ expect(entity.themeClass).toBe("ag-theme-alpine")
+ expect(entity.height).toBe("70vh")
+ expect(entity.defaultColDef.sortable).toBe(false)
+ expect(entity.defaultColDef.minWidth).toBe(200)
+ expect(entity.rowData).toEqual([{ id: 1 }])
+ expect(entity.columnDefs).toEqual([{ field: "id" }])
+ expect(entity.gridOptions).toEqual({ suppressCellFocus: true })
+ })
+
+ it("applies mounted and data change handlers", () => {
+ const entity = { id: "grid-1", gridStatus: "mounting" }
+ const rowData = [{ id: 1 }]
+ const columnDefs = [{ field: "id" }]
+ const gridOptions = { suppressMovableColumns: true }
+
+ mounted(entity, { gridApiId: 7 })
+ rowDataChange(entity, rowData)
+ columnDefsChange(entity, columnDefs)
+ gridOptionsChange(entity, gridOptions)
+
+ expect(entity.gridApiId).toBe(7)
+ expect(entity.gridStatus).toBe("mounted")
+ expect(entity.rowData).toBe(rowData)
+ expect(entity.columnDefs).toBe(columnDefs)
+ expect(entity.gridOptions).toBe(gridOptions)
+ })
+
+ it("exposes backward-compatible aliases", () => {
+ expect(gridMounted).toBe(mounted)
+ expect(setRowData).toBe(rowDataChange)
+ expect(setColumnDefs).toBe(columnDefsChange)
+ expect(setGridOptions).toBe(gridOptionsChange)
+ })
+
+ it("apiCall does not throw when grid does not exist", () => {
+ const entity = { id: "missing-grid" }
+ expect(() => apiCall(entity, { method: "sizeColumnsToFit" })).not.toThrow()
+ })
+})
diff --git a/packages/ag-grid/src/index.js b/packages/ag-grid/src/index.js
new file mode 100644
index 00000000..81cced4b
--- /dev/null
+++ b/packages/ag-grid/src/index.js
@@ -0,0 +1,12 @@
+import * as handlers from "./handlers.js"
+export { configureAgGrid } from "./runtime-config.js"
+import { render } from "./template.js"
+
+/**
+ * Thin AG Grid adapter type for Inglorious Web.
+ * Compose app-specific behaviors on top of this base type.
+ */
+export const agGrid = {
+ ...handlers,
+ render,
+}
diff --git a/packages/ag-grid/src/runtime-config.js b/packages/ag-grid/src/runtime-config.js
new file mode 100644
index 00000000..50c7d569
--- /dev/null
+++ b/packages/ag-grid/src/runtime-config.js
@@ -0,0 +1,49 @@
+let runtimeConfig = null
+
+/**
+ * @typedef {{
+ * createGrid: (element: HTMLElement, options: object) => any
+ * registerModules?: () => void
+ * }} AgGridRuntimeConfig
+ */
+
+/**
+ * Configures the AG Grid runtime used by the adapter.
+ *
+ * @param {AgGridRuntimeConfig} config
+ */
+export function configureAgGrid(config) {
+ if (!config || typeof config.createGrid !== "function") {
+ throw new TypeError(
+ "configureAgGrid(config) requires a createGrid function.",
+ )
+ }
+
+ if (
+ config.registerModules !== undefined &&
+ typeof config.registerModules !== "function"
+ ) {
+ throw new TypeError(
+ "configureAgGrid(config) registerModules must be a function when provided.",
+ )
+ }
+
+ runtimeConfig = {
+ createGrid: config.createGrid,
+ registerModules: config.registerModules || null,
+ }
+}
+
+/**
+ * Returns the configured AG Grid runtime.
+ * Throws when runtime has not been configured yet.
+ *
+ * @returns {{ createGrid: (element: HTMLElement, options: object) => any, registerModules: (() => void) | null }}
+ */
+export function getAgGridRuntimeConfig() {
+ if (runtimeConfig) return runtimeConfig
+
+ throw new Error(
+ "AG Grid runtime is not configured. Call configureAgGrid({ createGrid, registerModules? }) before mounting your app.",
+ )
+}
diff --git a/packages/ag-grid/src/runtime-config.test.js b/packages/ag-grid/src/runtime-config.test.js
new file mode 100644
index 00000000..9fd13942
--- /dev/null
+++ b/packages/ag-grid/src/runtime-config.test.js
@@ -0,0 +1,34 @@
+import { describe, expect, it } from "vitest"
+
+import { configureAgGrid, getAgGridRuntimeConfig } from "./runtime-config.js"
+
+describe("runtime-config", () => {
+ it("validates required createGrid function", () => {
+ expect(() => configureAgGrid()).toThrow(/requires a createGrid function/)
+ expect(() => configureAgGrid({})).toThrow(/requires a createGrid function/)
+ })
+
+ it("validates optional registerModules function", () => {
+ expect(() =>
+ configureAgGrid({
+ createGrid: () => ({}),
+ registerModules: "nope",
+ }),
+ ).toThrow(/registerModules must be a function/)
+ })
+
+ it("stores runtime configuration", () => {
+ const createGrid = () => ({})
+ const registerModules = () => {}
+
+ configureAgGrid({
+ createGrid,
+ registerModules,
+ })
+
+ expect(getAgGridRuntimeConfig()).toEqual({
+ createGrid,
+ registerModules,
+ })
+ })
+})
diff --git a/packages/ag-grid/src/runtime.js b/packages/ag-grid/src/runtime.js
new file mode 100644
index 00000000..bd2413cb
--- /dev/null
+++ b/packages/ag-grid/src/runtime.js
@@ -0,0 +1,164 @@
+/* eslint-disable no-magic-numbers */
+import { getAgGridRuntimeConfig } from "./runtime-config.js"
+
+const gridInstances = new Map()
+const apiIds = new WeakMap()
+let nextApiId = 1
+let modulesRegistered = false
+
+/**
+ * @typedef {Record & {
+ * id: string
+ * rowIdField?: string
+ * defaultColDef?: Record
+ * rowData?: any[]
+ * columnDefs?: any[]
+ * gridOptions?: Record
+ * }} AgGridEntity
+ */
+
+/**
+ * @typedef {{
+ * api: any
+ * element: HTMLElement
+ * lastColumnDefs: any[] | undefined
+ * lastGridOptions: Record | undefined
+ * lastRowData: any[] | undefined
+ * lastDefaultColDef: Record | undefined
+ * }} GridInstance
+ */
+
+/**
+ * Mounts a grid instance for an entity, or updates the existing instance.
+ * @param {AgGridEntity} entity
+ * @param {HTMLElement} element
+ * @param {{ notify: (event: string, payload?: any) => void }} api
+ */
+export function mountOrUpdateGrid(entity, element, api) {
+ registerModulesOnce()
+
+ const existing = gridInstances.get(entity.id)
+
+ if (!existing || existing.element !== element) {
+ if (existing) {
+ existing.api.destroy()
+ gridInstances.delete(entity.id)
+ }
+
+ const gridApi = getAgGridRuntimeConfig().createGrid(
+ element,
+ buildGridOptions(entity),
+ )
+
+ gridInstances.set(entity.id, {
+ api: gridApi,
+ element,
+ lastColumnDefs: entity.columnDefs,
+ lastGridOptions: entity.gridOptions,
+ lastRowData: entity.rowData,
+ lastDefaultColDef: entity.defaultColDef,
+ })
+
+ const entityId = entity.id
+ queueMicrotask(() => {
+ api.notify(`#${entityId}:mounted`, {
+ gridApiId: getApiId(gridApi),
+ })
+ })
+ return
+ }
+
+ syncGrid(existing, entity)
+}
+
+/**
+ * Destroys grid runtime state for the provided entity id.
+ * @param {string} entityId
+ */
+export function destroyGrid(entityId) {
+ const instance = gridInstances.get(entityId)
+ if (!instance) return
+ instance.api.destroy()
+ gridInstances.delete(entityId)
+}
+
+function registerModulesOnce() {
+ if (modulesRegistered) return
+ getAgGridRuntimeConfig().registerModules?.()
+ modulesRegistered = true
+}
+
+/**
+ * Builds AG Grid options from entity state and passthrough options.
+ * @param {AgGridEntity} entity
+ * @returns {Record}
+ */
+function buildGridOptions(entity) {
+ const options = entity.gridOptions || {}
+
+ return {
+ ...options,
+ defaultColDef: entity.defaultColDef,
+ animateRows: options.animateRows ?? true,
+ rowData: entity.rowData,
+ columnDefs: entity.columnDefs,
+ getRowId:
+ options.getRowId ||
+ ((params) => String(params.data?.[entity.rowIdField])),
+ }
+}
+
+/**
+ * Applies incremental grid updates when tracked entity fields change.
+ * @param {GridInstance} instance
+ * @param {AgGridEntity} entity
+ */
+function syncGrid(instance, entity) {
+ const hasChanges =
+ instance.lastDefaultColDef !== entity.defaultColDef ||
+ instance.lastGridOptions !== entity.gridOptions ||
+ instance.lastRowData !== entity.rowData ||
+ instance.lastColumnDefs !== entity.columnDefs
+
+ if (!hasChanges) return
+
+ instance.api.updateGridOptions(buildGridOptions(entity))
+
+ instance.lastDefaultColDef = entity.defaultColDef
+ instance.lastGridOptions = entity.gridOptions
+ instance.lastRowData = entity.rowData
+ instance.lastColumnDefs = entity.columnDefs
+}
+
+/**
+ * Invokes a method on the underlying AG Grid API.
+ * @param {string} entityId
+ * @param {string | { method: string, args?: any[] }} payload
+ */
+export function callGridApi(entityId, payload) {
+ const instance = gridInstances.get(entityId)
+ if (!instance || !instance.api) return
+
+ const method = typeof payload === "string" ? payload : payload?.method
+ if (!method || typeof instance.api[method] !== "function") return
+
+ const args =
+ typeof payload === "object" && Array.isArray(payload.args)
+ ? payload.args
+ : []
+ instance.api[method](...args)
+}
+
+/**
+ * Returns a stable numeric identifier for a grid API instance.
+ * @param {any} api
+ * @returns {number}
+ */
+function getApiId(api) {
+ if (!apiIds.has(api)) {
+ apiIds.set(api, nextApiId)
+ nextApiId += 1
+ }
+
+ return apiIds.get(api)
+}
diff --git a/packages/ag-grid/src/runtime.test.js b/packages/ag-grid/src/runtime.test.js
new file mode 100644
index 00000000..29b8057b
--- /dev/null
+++ b/packages/ag-grid/src/runtime.test.js
@@ -0,0 +1,123 @@
+import { describe, expect, it, vi } from "vitest"
+
+import { callGridApi, destroyGrid, mountOrUpdateGrid } from "./runtime.js"
+import { configureAgGrid } from "./runtime-config.js"
+
+function createEntity(id) {
+ return {
+ id,
+ rowIdField: "id",
+ defaultColDef: { sortable: true },
+ rowData: [{ id: 1, name: "Alpha" }],
+ columnDefs: [{ field: "name" }],
+ gridOptions: { suppressCellFocus: true },
+ }
+}
+
+describe("runtime", () => {
+ it("mounts grid and emits mounted event", async () => {
+ const createGrid = vi.fn(() => ({
+ destroy: vi.fn(),
+ updateGridOptions: vi.fn(),
+ }))
+ const registerModules = vi.fn()
+ const notify = vi.fn()
+ const entity = createEntity("grid-runtime-1")
+ const element = {}
+
+ configureAgGrid({ createGrid, registerModules })
+ mountOrUpdateGrid(entity, element, { notify })
+ await Promise.resolve()
+
+ expect(registerModules).toHaveBeenCalledTimes(1)
+ expect(createGrid).toHaveBeenCalledTimes(1)
+ expect(createGrid).toHaveBeenCalledWith(
+ element,
+ expect.objectContaining({
+ defaultColDef: entity.defaultColDef,
+ rowData: entity.rowData,
+ columnDefs: entity.columnDefs,
+ suppressCellFocus: true,
+ }),
+ )
+ expect(notify).toHaveBeenCalledWith("#grid-runtime-1:mounted", {
+ gridApiId: expect.any(Number),
+ })
+ })
+
+ it("updates grid options only when tracked values change", async () => {
+ const updateGridOptions = vi.fn()
+ const createGrid = vi.fn(() => ({
+ destroy: vi.fn(),
+ updateGridOptions,
+ }))
+ const notify = vi.fn()
+ const entity = createEntity("grid-runtime-2")
+ const element = {}
+
+ configureAgGrid({ createGrid })
+
+ mountOrUpdateGrid(entity, element, { notify })
+ await Promise.resolve()
+ mountOrUpdateGrid(entity, element, { notify })
+
+ expect(updateGridOptions).not.toHaveBeenCalled()
+
+ entity.rowData = [...entity.rowData, { id: 2, name: "Beta" }]
+ mountOrUpdateGrid(entity, element, { notify })
+
+ expect(updateGridOptions).toHaveBeenCalledTimes(1)
+ expect(updateGridOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ rowData: entity.rowData,
+ columnDefs: entity.columnDefs,
+ }),
+ )
+ })
+
+ it("calls underlying grid api methods via apiCall payload", async () => {
+ const sizeColumnsToFit = vi.fn()
+ const setFilterModel = vi.fn()
+ const createGrid = vi.fn(() => ({
+ destroy: vi.fn(),
+ updateGridOptions: vi.fn(),
+ sizeColumnsToFit,
+ setFilterModel,
+ }))
+ const entity = createEntity("grid-runtime-3")
+ const element = {}
+
+ configureAgGrid({ createGrid })
+ mountOrUpdateGrid(entity, element, { notify: vi.fn() })
+ await Promise.resolve()
+
+ callGridApi(entity.id, "sizeColumnsToFit")
+ callGridApi(entity.id, {
+ method: "setFilterModel",
+ args: [{ id: null }],
+ })
+
+ expect(sizeColumnsToFit).toHaveBeenCalledTimes(1)
+ expect(setFilterModel).toHaveBeenCalledWith({ id: null })
+ })
+
+ it("destroys mounted grid instances", async () => {
+ const destroy = vi.fn()
+ const createGrid = vi.fn(() => ({
+ destroy,
+ updateGridOptions: vi.fn(),
+ }))
+ const entity = createEntity("grid-runtime-4")
+ const element = {}
+
+ configureAgGrid({ createGrid })
+ mountOrUpdateGrid(entity, element, { notify: vi.fn() })
+ await Promise.resolve()
+
+ destroyGrid(entity.id)
+ expect(destroy).toHaveBeenCalledTimes(1)
+
+ callGridApi(entity.id, "sizeColumnsToFit")
+ expect(destroy).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/packages/ag-grid/src/template.js b/packages/ag-grid/src/template.js
new file mode 100644
index 00000000..157be00d
--- /dev/null
+++ b/packages/ag-grid/src/template.js
@@ -0,0 +1,52 @@
+import { html, ref } from "@inglorious/web"
+
+import { DEFAULT_THEME_CLASS } from "./defaults.js"
+import { mountOrUpdateGrid } from "./runtime.js"
+import { warnOnce } from "./warnings.js"
+
+/**
+ * @typedef {Record & {
+ * themeClass?: string
+ * height?: number | string
+ * }} AgGridRenderableEntity
+ */
+
+/**
+ * Renders the AG Grid host element and delegates lifecycle to runtime helpers.
+ * @param {AgGridRenderableEntity} entity
+ * @param {{ notify: (event: string, payload?: any) => void }} api
+ * @returns {import("lit-html").TemplateResult}
+ */
+export function render(entity, api) {
+ const height =
+ typeof entity.height === "number" ? `${entity.height}px` : entity.height
+ const themeClass = resolveThemeClass(entity.themeClass)
+
+ return html`
+ {
+ if (!el) return
+ mountOrUpdateGrid(entity, el, api)
+ })}
+ >
+ `
+}
+
+/**
+ * Ensures a non-empty AG Grid theme class and warns once in dev when invalid.
+ * @param {unknown} themeClass
+ * @returns {string}
+ */
+function resolveThemeClass(themeClass) {
+ if (typeof themeClass === "string" && themeClass.trim()) {
+ return themeClass
+ }
+
+ warnOnce(
+ "empty-theme-class",
+ `Missing or empty "themeClass". Falling back to "${DEFAULT_THEME_CLASS}".`,
+ )
+ return DEFAULT_THEME_CLASS
+}
diff --git a/packages/ag-grid/src/warnings.js b/packages/ag-grid/src/warnings.js
new file mode 100644
index 00000000..aa593595
--- /dev/null
+++ b/packages/ag-grid/src/warnings.js
@@ -0,0 +1,26 @@
+const emittedWarnings = new Set()
+
+function isDevEnvironment() {
+ if (
+ typeof import.meta !== "undefined" &&
+ import.meta.env &&
+ typeof import.meta.env.DEV === "boolean"
+ ) {
+ return import.meta.env.DEV
+ }
+
+ return false
+}
+
+/**
+ * Emits a warning once in development environments.
+ * @param {string} key
+ * @param {string} message
+ */
+export function warnOnce(key, message) {
+ if (!isDevEnvironment()) return
+ if (emittedWarnings.has(key)) return
+ emittedWarnings.add(key)
+
+ console.warn(`[inglorious/ag-grid] ${message}`)
+}
diff --git a/packages/ag-grid/types/index.d.ts b/packages/ag-grid/types/index.d.ts
new file mode 100644
index 00000000..1cacda69
--- /dev/null
+++ b/packages/ag-grid/types/index.d.ts
@@ -0,0 +1,32 @@
+export type AgGridRuntimeConfig = {
+ createGrid: (element: HTMLElement, options: Record) => any
+ registerModules?: () => void
+}
+
+export function configureAgGrid(config: AgGridRuntimeConfig): void
+
+export type AgGridEntity = Record & {
+ height?: number | string
+ gridOptions?: Record
+}
+
+export const agGrid: {
+ create(entity: AgGridEntity): void
+ render(entity: AgGridEntity): any
+ mounted(entity: AgGridEntity, payload?: any): void
+ rowDataChange(entity: AgGridEntity, rowData: any[]): void
+ columnDefsChange(entity: AgGridEntity, columnDefs: any[]): void
+ gridOptionsChange(
+ entity: AgGridEntity,
+ gridOptions: Record,
+ ): void
+ apiCall(
+ entity: AgGridEntity,
+ payload: string | { method: string; args?: any[] },
+ ): void
+ gridMounted(entity: AgGridEntity, payload?: any): void
+ setRowData(entity: AgGridEntity, rowData: any[]): void
+ setColumnDefs(entity: AgGridEntity, columnDefs: any[]): void
+ setGridOptions(entity: AgGridEntity, gridOptions: Record): void
+ destroy(entity: AgGridEntity): void
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 53ed03cb..f4ca1a34 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -824,6 +824,28 @@ importers:
specifier: ^3.2.4
version: 3.2.4(@types/node@24.8.0)(happy-dom@20.0.11)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.91.0)(terser@5.44.1)
+ examples/apps/web-ag-grid:
+ dependencies:
+ "@inglorious/web":
+ specifier: workspace:*
+ version: link:../../../packages/web
+ devDependencies:
+ "@inglorious/eslint-config":
+ specifier: workspace:*
+ version: link:../../../packages/eslint-config
+ eslint:
+ specifier: ^9.39.1
+ version: 9.39.2(jiti@2.5.1)
+ prettier:
+ specifier: ^3.6.2
+ version: 3.8.1
+ rollup-plugin-minify-template-literals:
+ specifier: ^1.1.7
+ version: 1.1.7(rollup@4.50.0)
+ vite:
+ specifier: ^7.1.6
+ version: 7.3.0(@types/node@24.8.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.91.0)(terser@5.44.1)
+
examples/apps/web-charts:
dependencies:
"@inglorious/charts":
@@ -1743,6 +1765,9 @@ importers:
"@lit-labs/ssr-client":
specifier: ^1.1.8
version: 1.1.8
+ ag-grid-community:
+ specifier: ^34.3.1
+ version: 34.3.1
d3-array:
specifier: ^3.2.4
version: 3.2.4
@@ -5542,6 +5567,18 @@ packages:
engines: { node: ">=0.4.0" }
hasBin: true
+ ag-charts-types@12.3.1:
+ resolution:
+ {
+ integrity: sha512-5216xYoawnvMXDFI6kTpPku+mH0Csiwu/FE7lsAm8Z22HEN6ciSG/V7g+IrpLWncELqksgENebCTP75PZ3CsHA==,
+ }
+
+ ag-grid-community@34.3.1:
+ resolution:
+ {
+ integrity: sha512-PwlrPudsFOzGumphi2y9ihWeaUlIwKhOra/MXu2LjeV2U8DgLLcYS8CartE5Hszhn1poJHawwI9HWrxlKliwdw==,
+ }
+
agent-base@7.1.4:
resolution:
{
@@ -14156,6 +14193,12 @@ snapshots:
acorn@8.15.0: {}
+ ag-charts-types@12.3.1: {}
+
+ ag-grid-community@34.3.1:
+ dependencies:
+ ag-charts-types: 12.3.1
+
agent-base@7.1.4:
optional: true