refactor(model): extract dashboard model package

This commit is contained in:
vince 2026-06-20 06:26:19 +02:00
parent 22f50d64f9
commit 7a6ad8ab0e
35 changed files with 164 additions and 57 deletions

View file

@ -5,13 +5,16 @@ reusable React component library.
This project is not a Homepage customization and does not depend on Homepage This project is not a Homepage customization and does not depend on Homepage
runtime, frontend code, or configuration. The dashboard will be model-driven: runtime, frontend code, or configuration. The dashboard will be model-driven:
the reusable UI package stays content-free, while environment-specific data the reusable UI package stays content-free, the reusable dashboard model
lives in validated dashboard model state inside the web app. package owns schema and validation, and environment-specific data lives in
validated dashboard model state inside the web app.
## Workspace Layout ## Workspace Layout
- `apps/web`: Vite React website, Bun API server, model fixtures, Drizzle - `apps/web`: Vite React website, Bun API server, model fixtures, Drizzle
persistence, Playwright e2e checks, and container build. persistence, Playwright e2e checks, and container build.
- `packages/dashboard-model`: reusable dashboard schema, validation, and
generic model fixtures shared by apps and tooling.
- `packages/ui`: reusable dashboard React components, design tokens, - `packages/ui`: reusable dashboard React components, design tokens,
shadcn/radix primitives, generic fixtures, and Storybook. shadcn/radix primitives, generic fixtures, and Storybook.
- `docs/superpowers`: migration specs and execution plans used for this repo. - `docs/superpowers`: migration specs and execution plans used for this repo.
@ -65,7 +68,7 @@ with an explicit migration error.
## Seed Data ## Seed Data
The initial Dimension Lab dashboard lives in The initial Dimension Lab dashboard lives in
`apps/web/src/lib/model/fixtures/dimensionlab.ts` as validated model data. It `apps/web/src/lib/dashboard-seed/dimensionlab.ts` as validated model data. It
includes the first-screen telemetry, service groups, status strip, weather includes the first-screen telemetry, service groups, status strip, weather
module, Iconify icon identifiers, links, and datasource references. Values that module, Iconify icon identifiers, links, and datasource references. Values that
are not live yet are labeled as fallback values in the data so later datasource are not live yet are labeled as fallback values in the data so later datasource

View file

@ -15,6 +15,7 @@
"db:check": "drizzle-kit check" "db:check": "drizzle-kit check"
}, },
"dependencies": { "dependencies": {
"@dimensionlab/dashboard-model": "workspace:*",
"@dimensionlab/ui": "workspace:*", "@dimensionlab/ui": "workspace:*",
"@sinclair/typebox": "^0.34.49", "@sinclair/typebox": "^0.34.49",
"ajv": "^8.20.0", "ajv": "^8.20.0",

View file

@ -7,7 +7,7 @@ import type {
DatasourceReference, DatasourceReference,
ServiceEntry, ServiceEntry,
TelemetryCard, TelemetryCard,
} from "../schema"; } from "@dimensionlab/dashboard-model";
describe("Dimension Lab dashboard seed", () => { describe("Dimension Lab dashboard seed", () => {
test("defines the primary first-screen sections as model data", () => { test("defines the primary first-screen sections as model data", () => {

View file

@ -7,7 +7,7 @@ import {
type ServiceGroup, type ServiceGroup,
type Severity, type Severity,
type TelemetryCard, type TelemetryCard,
} from "../schema"; } from "@dimensionlab/dashboard-model";
type NumericValueKind = "percent" | "bytes" | "temperature" | "latency" | "number"; type NumericValueKind = "percent" | "bytes" | "temperature" | "latency" | "number";

View file

@ -1,2 +0,0 @@
export { dimensionLabDashboardFixture } from "./dimensionlab";
export { genericDashboardFixture } from "./generic";

View file

@ -74,5 +74,7 @@ function findFiles(path: string, extension: string): string[] {
} }
function withoutInternalPackageScope(source: string): string { function withoutInternalPackageScope(source: string): string {
return source.replaceAll("@dimensionlab/ui", "@internal/ui"); return source
.replaceAll("@dimensionlab/dashboard-model", "@internal/dashboard-model")
.replaceAll("@dimensionlab/ui", "@internal/ui");
} }

View file

@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest"; import { afterEach, describe, expect, test } from "vitest";
import type { DashboardDocument } from "$lib/model"; import type { DashboardDocument } from "@dimensionlab/dashboard-model";
import { genericDashboardFixture } from "$lib/model/fixtures/generic"; import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { createDashboardStore, type DashboardStore } from "$lib/server/db/dashboard-store"; import { createDashboardStore, type DashboardStore } from "$lib/server/db/dashboard-store";
import { import {
AgentConfigAuthorizationError, AgentConfigAuthorizationError,

View file

@ -16,7 +16,7 @@ import {
type ServiceGroup, type ServiceGroup,
type StatusItem, type StatusItem,
type TelemetryCard, type TelemetryCard,
} from "$lib/model"; } from "@dimensionlab/dashboard-model";
import { import {
createDashboardStore, createDashboardStore,
DashboardRevisionNotFoundError, DashboardRevisionNotFoundError,

View file

@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest"; import { afterEach, describe, expect, test } from "vitest";
import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import { genericDashboardFixture } from "$lib/model/fixtures/generic"; import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { createDashboardStore, type DashboardStore } from "./db/dashboard-store"; import { createDashboardStore, type DashboardStore } from "./db/dashboard-store";
import { loadDashboardRuntime } from "./dashboard"; import { loadDashboardRuntime } from "./dashboard";

View file

@ -1,5 +1,5 @@
import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import type { DashboardDocument } from "$lib/model"; import type { DashboardDocument } from "@dimensionlab/dashboard-model";
import { import {
createDashboardStore, createDashboardStore,
DashboardPersistenceValidationError, DashboardPersistenceValidationError,

View file

@ -2,7 +2,7 @@ import { describe, expect, test, vi } from "vitest";
import { import {
DASHBOARD_SCHEMA_VERSION, DASHBOARD_SCHEMA_VERSION,
type DashboardDocument, type DashboardDocument,
} from "$lib/model"; } from "@dimensionlab/dashboard-model";
import { resolveDashboardDatasources } from "."; import { resolveDashboardDatasources } from ".";
describe("dashboard datasource resolution", () => { describe("dashboard datasource resolution", () => {

View file

@ -8,7 +8,7 @@ import type {
StatusItem, StatusItem,
StatusStrip, StatusStrip,
TelemetryCard, TelemetryCard,
} from "$lib/model"; } from "@dimensionlab/dashboard-model";
export interface DatasourceResolutionOptions { export interface DatasourceResolutionOptions {
fetch?: DatasourceFetch; fetch?: DatasourceFetch;

View file

@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { afterEach, describe, expect, test, vi } from "vitest"; import { afterEach, describe, expect, test, vi } from "vitest";
import { genericDashboardFixture } from "$lib/model/fixtures/generic"; import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import type { DashboardDocument } from "$lib/model"; import type { DashboardDocument } from "@dimensionlab/dashboard-model";
import { import {
DashboardPersistenceValidationError, DashboardPersistenceValidationError,
createDashboardStore, createDashboardStore,

View file

@ -3,7 +3,7 @@ import { desc, eq } from "drizzle-orm";
import { import {
type DashboardDocument, type DashboardDocument,
type DashboardValidationFailure, type DashboardValidationFailure,
} from "$lib/model"; } from "@dimensionlab/dashboard-model";
import { import {
type DashboardDatabaseConnection, type DashboardDatabaseConnection,
openDashboardDatabase, openDashboardDatabase,

View file

@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { DASHBOARD_SCHEMA_VERSION } from "$lib/model"; import { DASHBOARD_SCHEMA_VERSION } from "@dimensionlab/dashboard-model";
import { genericDashboardFixture } from "$lib/model/fixtures/generic"; import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { import {
UnsupportedDashboardModelVersionError, UnsupportedDashboardModelVersionError,
migrateDashboardDocumentForPersistence, migrateDashboardDocumentForPersistence,

View file

@ -3,7 +3,7 @@ import {
validateDashboardDocument, validateDashboardDocument,
type DashboardDocument, type DashboardDocument,
type DashboardValidationFailure, type DashboardValidationFailure,
} from "$lib/model"; } from "@dimensionlab/dashboard-model";
export interface DashboardModelMigrationSuccess { export interface DashboardModelMigrationSuccess {
valid: true; valid: true;

View file

@ -1,4 +1,4 @@
import type { DashboardDocument } from "$lib/model"; import type { DashboardDocument } from "@dimensionlab/dashboard-model";
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const dashboardDocuments = sqliteTable("dashboard_documents", { export const dashboardDocuments = sqliteTable("dashboard_documents", {

View file

@ -1,9 +1,9 @@
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { type DashboardDocument } from "$lib/model"; import { type DashboardDocument } from "@dimensionlab/dashboard-model";
import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import { genericDashboardFixture } from "$lib/model/fixtures/generic"; import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { dashboardDocumentToUiDashboard } from "./model-renderer"; import { dashboardDocumentToUiDashboard } from "./model-renderer";
const appRoot = process.cwd().endsWith(`${join("apps", "web")}`) const appRoot = process.cwd().endsWith(`${join("apps", "web")}`)
@ -93,6 +93,7 @@ describe("dashboard model renderer", () => {
"utf8", "utf8",
) )
.toLowerCase() .toLowerCase()
.replaceAll("@dimensionlab/dashboard-model", "@internal/dashboard-model")
.replaceAll("@dimensionlab/ui", "@internal/ui"); .replaceAll("@dimensionlab/ui", "@internal/ui");
expect(source).not.toContain("dimension"); expect(source).not.toContain("dimension");

View file

@ -6,7 +6,7 @@ import type {
StatusItem, StatusItem,
StatusStrip, StatusStrip,
TelemetryCard, TelemetryCard,
} from "$lib/model"; } from "@dimensionlab/dashboard-model";
import type { import type {
UiDashboardPreview, UiDashboardPreview,
UiModuleBlock, UiModuleBlock,

View file

@ -88,4 +88,29 @@ describe("workspace boundaries", () => {
expect(uiPackage.exports).toHaveProperty("."); expect(uiPackage.exports).toHaveProperty(".");
expect(uiPackage.exports).toHaveProperty("./styles.css"); expect(uiPackage.exports).toHaveProperty("./styles.css");
}); });
test("keeps the dashboard model in a reusable internal package", () => {
const webPackage = JSON.parse(
readFileSync(join(root, "apps/web/package.json"), "utf8"),
) as { dependencies?: Record<string, string> };
const modelPackage = JSON.parse(
readFileSync(join(root, "packages/dashboard-model/package.json"), "utf8"),
) as { exports?: Record<string, unknown>; name?: string };
const tsconfig = JSON.parse(
readFileSync(join(root, "tsconfig.json"), "utf8"),
) as { references?: Array<{ path: string }> };
expect(webPackage.dependencies?.["@dimensionlab/dashboard-model"]).toBe(
"workspace:*",
);
expect(modelPackage.name).toBe("@dimensionlab/dashboard-model");
expect(modelPackage.exports).toHaveProperty(".");
expect(modelPackage.exports).toHaveProperty("./fixtures");
expect(tsconfig.references).toEqual(
expect.arrayContaining([{ path: "./packages/dashboard-model" }]),
);
expect(existsSync(join(root, "apps/web/src/lib/model/index.ts"))).toBe(
false,
);
});
}); });

View file

@ -1,6 +1,6 @@
import { renderToString } from "react-dom/server"; import { renderToString } from "react-dom/server";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { genericDashboardFixture } from "$lib/model/fixtures/generic"; import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { AppStateView, resolveDocumentMetadata } from "./App"; import { AppStateView, resolveDocumentMetadata } from "./App";
describe("home page model renderer", () => { describe("home page model renderer", () => {

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import { loadDashboardResponse } from "./dashboard"; import { loadDashboardResponse } from "./dashboard";
describe("dashboard API route", () => { describe("dashboard API route", () => {

View file

@ -3,6 +3,10 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@dimensionlab/dashboard-model": ["../../packages/dashboard-model/src/index.ts"],
"@dimensionlab/dashboard-model/fixtures": [
"../../packages/dashboard-model/src/fixtures/index.ts"
],
"@dimensionlab/ui": ["../../packages/ui/src/index.ts"], "@dimensionlab/ui": ["../../packages/ui/src/index.ts"],
"@dimensionlab/ui/styles.css": ["../../packages/ui/src/styles.css"], "@dimensionlab/ui/styles.css": ["../../packages/ui/src/styles.css"],
"$lib/*": ["src/lib/*"] "$lib/*": ["src/lib/*"]

View file

@ -30,6 +30,12 @@ export default defineConfig({
"@dimensionlab/ui": fileURLToPath( "@dimensionlab/ui": fileURLToPath(
new URL("../../packages/ui/src/index.ts", import.meta.url), new URL("../../packages/ui/src/index.ts", import.meta.url),
), ),
"@dimensionlab/dashboard-model/fixtures": fileURLToPath(
new URL("../../packages/dashboard-model/src/fixtures/index.ts", import.meta.url),
),
"@dimensionlab/dashboard-model": fileURLToPath(
new URL("../../packages/dashboard-model/src/index.ts", import.meta.url),
),
$lib: fileURLToPath(new URL("./src/lib", import.meta.url)), $lib: fileURLToPath(new URL("./src/lib", import.meta.url)),
}, },
}, },

View file

@ -12,6 +12,7 @@
"name": "@dimensionlab/web", "name": "@dimensionlab/web",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@dimensionlab/dashboard-model": "workspace:*",
"@dimensionlab/ui": "workspace:*", "@dimensionlab/ui": "workspace:*",
"@sinclair/typebox": "^0.34.49", "@sinclair/typebox": "^0.34.49",
"ajv": "^8.20.0", "ajv": "^8.20.0",
@ -40,6 +41,22 @@
"vitest": "^4.1.9", "vitest": "^4.1.9",
}, },
}, },
"packages/dashboard-model": {
"name": "@dimensionlab/dashboard-model",
"version": "0.0.1",
"dependencies": {
"@sinclair/typebox": "^0.34.49",
"ajv": "^8.20.0",
"ajv-formats": "^3.0.1",
},
"devDependencies": {
"@types/bun": "^1.3.14",
"@types/node": "^25.9.3",
"bun-types": "^1.3.14",
"typescript": "^6.0.3",
"vitest": "^4.1.9",
},
},
"packages/ui": { "packages/ui": {
"name": "@dimensionlab/ui", "name": "@dimensionlab/ui",
"version": "0.0.1", "version": "0.0.1",
@ -137,6 +154,8 @@
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
"@dimensionlab/dashboard-model": ["@dimensionlab/dashboard-model@workspace:packages/dashboard-model"],
"@dimensionlab/ui": ["@dimensionlab/ui@workspace:packages/ui"], "@dimensionlab/ui": ["@dimensionlab/ui@workspace:packages/ui"],
"@dimensionlab/web": ["@dimensionlab/web@workspace:apps/web"], "@dimensionlab/web": ["@dimensionlab/web@workspace:apps/web"],
@ -671,7 +690,7 @@
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="],
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="],
"@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="],
@ -681,7 +700,7 @@
"@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="],
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="],
"@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="],
@ -745,7 +764,7 @@
"caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
@ -873,7 +892,7 @@
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
@ -1479,6 +1498,8 @@
"@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="],
@ -1495,14 +1516,6 @@
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
"@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
"@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="],
"@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@ -1549,14 +1562,14 @@
"shadcn/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "shadcn/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
"storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
"storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"vitest/@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="],
"vitest/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="],
"wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@ -1625,8 +1638,6 @@
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], "@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@ -1689,10 +1700,16 @@
"shadcn/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], "shadcn/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
"storybook/@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"storybook/@vitest/expect/chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
"storybook/@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"storybook/@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
} }
} }

View file

@ -0,0 +1,34 @@
{
"name": "@dimensionlab/dashboard-model",
"version": "0.0.1",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./fixtures": {
"types": "./dist/fixtures/index.d.ts",
"default": "./dist/fixtures/index.js"
}
},
"scripts": {
"build": "rm -rf dist && tsc -p tsconfig.build.json",
"check": "tsc --noEmit",
"test": "vitest run",
"test:unit": "vitest run"
},
"dependencies": {
"@sinclair/typebox": "^0.34.49",
"ajv": "^8.20.0",
"ajv-formats": "^3.0.1"
},
"devDependencies": {
"@types/bun": "^1.3.14",
"@types/node": "^25.9.3",
"bun-types": "^1.3.14",
"typescript": "^6.0.3",
"vitest": "^4.1.9"
}
}

View file

@ -0,0 +1 @@
export { genericDashboardFixture } from "./generic";

View file

@ -5,7 +5,6 @@ import {
validateDashboardDocument, validateDashboardDocument,
} from "."; } from ".";
import { import {
dimensionLabDashboardFixture,
genericDashboardFixture, genericDashboardFixture,
} from "./fixtures"; } from "./fixtures";
@ -19,12 +18,6 @@ describe("dashboard model validation", () => {
} }
}); });
it("accepts the Dimension Lab dashboard fixture", () => {
const result = validateDashboardDocument(dimensionLabDashboardFixture);
expect(result.valid).toBe(true);
});
it("rejects documents with an unsupported schema version", () => { it("rejects documents with an unsupported schema version", () => {
const invalid = { const invalid = {
...genericDashboardFixture, ...genericDashboardFixture,

View file

@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": false,
"noEmit": false,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules", "src/**/*.test.ts"]
}

View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"types": ["node", "bun-types"]
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}

View file

@ -1,6 +1,7 @@
{ {
"files": [], "files": [],
"references": [ "references": [
{ "path": "./packages/dashboard-model" },
{ "path": "./apps/web" }, { "path": "./apps/web" },
{ "path": "./packages/ui" } { "path": "./packages/ui" }
] ]