4ce22d6d05
CI / commits (pull_request) Successful in 3m31s
CI / scan (pull_request) Successful in 4m28s
CI / check (pull_request) Failing after 8m9s
CI / a11y (pull_request) Successful in 3m56s
Docs site / build (pull_request) Successful in 4m59s
CI / perf (pull_request) Successful in 10m8s
Implementation of ADR-0023 (PR 1). The next chantier wires these
components into the audit-log page; this PR delivers the lib + the
three starter chart types + the a11y contract.
Deps:
* d3 + @types/d3 (top-level)
* d3-shape + @types/d3-shape (donut's pie/arc generators)
* d3-scale-chromatic + @types/d3-scale-chromatic (palettes)
* @observablehq/plot (bar + stacked-bar)
Lib `libs/shared/charts/`:
* `_internal/palette.ts` — single source of palettes. Sequential
(Viridis / Cividis for dark mode) + categorical (ColorBrewer
Set2). Colour-blind-safe per the ColorBrewer review.
* `_internal/a11y.ts` — chartId generator, SVG <title>/<desc>
injector, `findChartSvg` for Plot's legend-wrapped output,
prefers-reduced-motion guard, theme resolver.
* `_internal/chart-envelope.scss` — shared figure/caption/
fallback rules consumed via `@use` by each chart's .scss.
Extracted at the third consumer per CLAUDE.md's "three similar
things" rule.
* `_internal/chart-types.ts` — common `ChartBaseInputs<T>`
interface every component extends.
Three Plot-or-D3-backed components:
* `<lib-bar-chart>` — Plot.barY, categorical-by-default palette.
* `<lib-donut-chart>` — raw d3-shape (pie + arc); Plot has no
donut mark so we reach for D3 directly. Each slice carries its
own <title> child for SVG-tooltip accessibility.
* `<lib-stacked-bar-chart>` — Plot.barY with `fill` set to the
series key (auto-stacked), legend on.
Each component honours ADR-0023's six commitments:
1. <figure role="img" aria-labelledby aria-describedby>
2. SVG <title> + <desc> as first two children (injected post-
render because Plot doesn't emit them)
3. <details> tabular fallback rendering every data point
4. Palette from the canonical list (no ad-hoc colours)
5. AA-contrast text on axis ticks (theme-aware grayscale via
the .chart-canvas svg text rule)
6. prefers-reduced-motion → data-no-transitions marker on the
SVG, CSS strips animations.
A custom ESLint rule in `libs/shared/charts/eslint.config.mjs`
bans direct imports of `d3-scale-chromatic` outside
`_internal/palette.ts`, so a future contributor can't bypass the
colour-blind-safe contract.
Tests: 13 specs across the three components. Each component covers
the figure ARIA contract, the SVG title/desc injection, the
tabular fallback, and data-reactivity. Donut spec additionally
asserts per-slice <title> labels for screen readers.
Verification:
* `pnpm nx lint test build --projects=shared-charts,portal-shell,portal-admin`
— 12/12 tasks green.
* `pnpm nx build portal-shell --configuration=production` —
i18n-strict prod build clean (the lib ships no i18n marks
since axis labels are caller-supplied).
192 lines
6.2 KiB
JSON
192 lines
6.2 KiB
JSON
{
|
|
"name": "apf-portal",
|
|
"version": "0.0.0",
|
|
"license": "MIT",
|
|
"packageManager": "pnpm@10.33.4",
|
|
"scripts": {
|
|
"prepare": "husky",
|
|
"ci:check": "pnpm exec nx affected -t format:check lint test build",
|
|
"ci:audit": "pnpm audit --audit-level=moderate",
|
|
"ci:commits": "pnpm exec commitlint --from ${COMMIT_LINT_FROM:-origin/main} --to HEAD --verbose",
|
|
"ci:gzip-budgets": "node scripts/check-gzip-budgets.mjs",
|
|
"ci:perf": "pnpm exec nx build portal-shell --configuration=production && pnpm ci:gzip-budgets && pnpm exec lhci autorun --config=./lighthouserc.js",
|
|
"docs:dev": "vitepress dev docs",
|
|
"docs:build": "vitepress build docs",
|
|
"docs:preview": "vitepress preview docs"
|
|
},
|
|
"private": true,
|
|
"lint-staged": {
|
|
"!(pnpm-lock).{ts,tsx,js,jsx,html,scss,css,md,json,yml,yaml}": [
|
|
"prettier --write"
|
|
]
|
|
},
|
|
"pnpm": {
|
|
"onlyBuiltDependencies": [
|
|
"@nestjs/core",
|
|
"@parcel/watcher",
|
|
"@prisma/client",
|
|
"@prisma/engines",
|
|
"@swc/core",
|
|
"esbuild",
|
|
"less",
|
|
"lmdb",
|
|
"msgpackr-extract",
|
|
"nx",
|
|
"prisma",
|
|
"unrs-resolver"
|
|
],
|
|
"overrides": {
|
|
"axios@<1.15.2": ">=1.15.2",
|
|
"@angular-devkit/core>ajv@<8.18.0": ">=8.18.0",
|
|
"brace-expansion@<5.0.5": ">=5.0.5",
|
|
"esbuild@<0.25.0": ">=0.25.0",
|
|
"follow-redirects@<1.16.0": ">=1.16.0",
|
|
"ip-address@<10.1.1": ">=10.1.1",
|
|
"protobufjs@<8.0.2": ">=8.0.2",
|
|
"tmp@<0.2.4": ">=0.2.4",
|
|
"vitepress>vite": ">=6.4.2 <7",
|
|
"yaml@<2.8.3": ">=2.8.3",
|
|
"@types/express": "^5.0.6"
|
|
}
|
|
},
|
|
"prisma": {
|
|
"schema": "apps/portal-bff/prisma/schema.prisma"
|
|
},
|
|
"devDependencies": {
|
|
"@analogjs/vite-plugin-angular": "~2.5.0",
|
|
"@analogjs/vitest-angular": "~2.5.0",
|
|
"@angular-devkit/core": "~21.2.0",
|
|
"@angular-devkit/schematics": "~21.2.0",
|
|
"@angular/build": "~21.2.0",
|
|
"@angular/cli": "~21.2.0",
|
|
"@angular/compiler-cli": "~21.2.0",
|
|
"@angular/language-service": "~21.2.0",
|
|
"@braintree/sanitize-url": "^7.1.2",
|
|
"@commitlint/cli": "^20.5.3",
|
|
"@commitlint/config-conventional": "^20.5.3",
|
|
"@eslint/js": "^9.8.0",
|
|
"@lhci/cli": "^0.15.1",
|
|
"@nestjs/schematics": "^11.0.0",
|
|
"@nestjs/testing": "^11.0.0",
|
|
"@nx/angular": "^22.7.1",
|
|
"@nx/devkit": "22.7.1",
|
|
"@nx/eslint": "^22.7.1",
|
|
"@nx/eslint-plugin": "22.7.1",
|
|
"@nx/jest": "22.7.1",
|
|
"@nx/js": "22.7.1",
|
|
"@nx/nest": "^22.7.1",
|
|
"@nx/node": "22.7.1",
|
|
"@nx/playwright": "22.7.1",
|
|
"@nx/vite": "^22.7.1",
|
|
"@nx/vitest": "22.7.1",
|
|
"@nx/web": "22.7.1",
|
|
"@nx/webpack": "22.7.1",
|
|
"@oxc-project/runtime": "^0.129.0",
|
|
"@playwright/test": "^1.36.0",
|
|
"@schematics/angular": "~21.2.0",
|
|
"@swc-node/register": "1.11.1",
|
|
"@swc/core": "1.15.33",
|
|
"@swc/helpers": "0.5.21",
|
|
"@tailwindcss/postcss": "^4.2.4",
|
|
"@types/cookie-parser": "^1.4.10",
|
|
"@types/d3": "^7.4.3",
|
|
"@types/d3-scale-chromatic": "^3.1.0",
|
|
"@types/d3-shape": "^3.1.8",
|
|
"@types/express": "^5.0.6",
|
|
"@types/express-session": "^1.19.0",
|
|
"@types/jest": "^30.0.0",
|
|
"@types/node": "24.12.4",
|
|
"@typescript-eslint/utils": "^8.40.0",
|
|
"@vitest/coverage-v8": "~4.1.0",
|
|
"@vitest/ui": "~4.1.0",
|
|
"angular-eslint": "^21.2.0",
|
|
"cytoscape": "^3.33.3",
|
|
"cytoscape-cose-bilkent": "^4.1.0",
|
|
"dayjs": "^1.11.20",
|
|
"debug": "^4.4.3",
|
|
"eslint": "^9.8.0",
|
|
"eslint-config-prettier": "^10.0.0",
|
|
"eslint-plugin-playwright": "^1.6.2",
|
|
"husky": "^9.1.7",
|
|
"jest": "^30.0.2",
|
|
"jest-environment-node": "^30.0.2",
|
|
"jest-util": "^30.0.2",
|
|
"jsdom": "^29.0.0",
|
|
"jsonc-eslint-parser": "^2.1.0",
|
|
"lint-staged": "^17.0.0",
|
|
"mermaid": "^11.15.0",
|
|
"nx": "22.7.1",
|
|
"pino-pretty": "^13.1.3",
|
|
"postcss": "^8.5.12",
|
|
"prettier": "^3.8.1",
|
|
"prisma": "^6.19.3",
|
|
"tailwindcss": "^4.2.4",
|
|
"ts-jest": "^29.4.0",
|
|
"ts-node": "10.9.2",
|
|
"tslib": "^2.3.0",
|
|
"typescript": "~5.9.2",
|
|
"typescript-eslint": "^8.40.0",
|
|
"vite": "^8.0.0",
|
|
"vitepress": "^1.6.4",
|
|
"vitepress-plugin-mermaid": "^2.0.17",
|
|
"vitest": "^4.0.8",
|
|
"webpack-cli": "^5.1.4"
|
|
},
|
|
"dependencies": {
|
|
"@angular/cdk": "~21.2.10",
|
|
"@angular/common": "~21.2.0",
|
|
"@angular/compiler": "~21.2.0",
|
|
"@angular/core": "~21.2.0",
|
|
"@angular/forms": "~21.2.0",
|
|
"@angular/localize": "~21.2.12",
|
|
"@angular/platform-browser": "~21.2.0",
|
|
"@angular/router": "~21.2.0",
|
|
"@azure/msal-node": "^5.2.1",
|
|
"@nestjs/common": "^11.0.0",
|
|
"@nestjs/core": "^11.0.0",
|
|
"@nestjs/platform-express": "^11.0.0",
|
|
"@nestjs/swagger": "^11.4.3",
|
|
"@observablehq/plot": "^0.6.17",
|
|
"@opentelemetry/api": "^1.9.1",
|
|
"@opentelemetry/exporter-trace-otlp-http": "^0.217.0",
|
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.217.0",
|
|
"@opentelemetry/instrumentation": "^0.217.0",
|
|
"@opentelemetry/instrumentation-document-load": "^0.62.0",
|
|
"@opentelemetry/instrumentation-express": "^0.65.0",
|
|
"@opentelemetry/instrumentation-fetch": "^0.217.0",
|
|
"@opentelemetry/instrumentation-http": "^0.217.0",
|
|
"@opentelemetry/instrumentation-ioredis": "^0.65.0",
|
|
"@opentelemetry/instrumentation-nestjs-core": "^0.63.0",
|
|
"@opentelemetry/instrumentation-pg": "^0.69.0",
|
|
"@opentelemetry/instrumentation-pino": "^0.63.0",
|
|
"@opentelemetry/instrumentation-user-interaction": "^0.61.0",
|
|
"@opentelemetry/resources": "^2.7.1",
|
|
"@opentelemetry/sdk-node": "^0.217.0",
|
|
"@opentelemetry/sdk-trace-web": "^2.7.1",
|
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
"@prisma/client": "^6.19.3",
|
|
"@scalar/nestjs-api-reference": "^1.1.14",
|
|
"axios": "^1.6.0",
|
|
"class-transformer": "^0.5.1",
|
|
"class-validator": "^0.15.1",
|
|
"connect-redis": "^9.0.0",
|
|
"cookie-parser": "^1.4.7",
|
|
"d3": "^7.9.0",
|
|
"d3-scale-chromatic": "^3.1.0",
|
|
"d3-shape": "^3.2.0",
|
|
"express-rate-limit": "^8.5.1",
|
|
"express-session": "^1.19.0",
|
|
"helmet": "^8.1.0",
|
|
"ioredis": "^5.10.1",
|
|
"jose": "^6.2.3",
|
|
"lucide-angular": "^1.0.0",
|
|
"nestjs-cls": "^6.2.0",
|
|
"nestjs-pino": "^4.6.1",
|
|
"nestjs-prisma": "^0.27.0",
|
|
"pino": "^10.3.1",
|
|
"pino-http": "^11.0.0",
|
|
"reflect-metadata": "^0.2.0",
|
|
"rxjs": "~7.8.0"
|
|
}
|
|
}
|