diff --git a/changelog.d/legacy-compat.added.md b/changelog.d/legacy-compat.added.md new file mode 100644 index 0000000..b49d0a9 --- /dev/null +++ b/changelog.d/legacy-compat.added.md @@ -0,0 +1 @@ +`@policyengine/ui-kit/legacy` compatibility shim that mirrors the API surface of the deprecated `@policyengine/design-system` package. Migrating from design-system is now a pure import-path rename: `@policyengine/design-system` → `@policyengine/ui-kit/legacy`, with matching subpath exports for `/tokens`, `/tokens/colors`, `/tokens/typography`, `/tokens/spacing`, and `/charts`. All legacy exports carry `@deprecated` JSDoc pointing at the canonical ui-kit equivalents (`palette`, `semanticFills`, `typography`, `namedSpacing`, `chartPalette`, etc.). diff --git a/package.json b/package.json index 7720d11..67c5b36 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,36 @@ "import": "./dist/assets.js", "require": "./dist/assets.cjs" }, + "./legacy": { + "types": "./dist/legacy/index.d.ts", + "import": "./dist/legacy.js", + "require": "./dist/legacy.cjs" + }, + "./legacy/tokens": { + "types": "./dist/legacy/tokens/index.d.ts", + "import": "./dist/legacy/tokens.js", + "require": "./dist/legacy/tokens.cjs" + }, + "./legacy/tokens/colors": { + "types": "./dist/legacy/tokens/colors.d.ts", + "import": "./dist/legacy/tokens/colors.js", + "require": "./dist/legacy/tokens/colors.cjs" + }, + "./legacy/tokens/typography": { + "types": "./dist/legacy/tokens/typography.d.ts", + "import": "./dist/legacy/tokens/typography.js", + "require": "./dist/legacy/tokens/typography.cjs" + }, + "./legacy/tokens/spacing": { + "types": "./dist/legacy/tokens/spacing.d.ts", + "import": "./dist/legacy/tokens/spacing.js", + "require": "./dist/legacy/tokens/spacing.cjs" + }, + "./legacy/charts": { + "types": "./dist/legacy/charts/index.d.ts", + "import": "./dist/legacy/charts.js", + "require": "./dist/legacy/charts.cjs" + }, "./styles.css": "./dist/styles.css", "./theme.css": "./src/theme/tokens.css", "./quarto.scss": "./src/theme/quarto.scss", diff --git a/src/legacy/charts/index.ts b/src/legacy/charts/index.ts new file mode 100644 index 0000000..de87fc2 --- /dev/null +++ b/src/legacy/charts/index.ts @@ -0,0 +1,212 @@ +/** + * @deprecated Compatibility shim mirroring `@policyengine/design-system/charts`. + * These are Plotly-flavored helpers. For new code, use the canonical Recharts + * components (`PEBarChart`, `PELineChart`, `PEAreaChart`, `PEWaterfallChart`) + * and chart defaults (`AXIS_STYLE`, `chartColors`) from + * `@policyengine/ui-kit/charts`, plus `chartPalette` (resolved hex per + * theme) from `@policyengine/ui-kit`. + * + * PolicyEngine Chart Utilities + * Shared chart formatting for Plotly.js charts + */ + +import { colors, TEAL_PRIMARY } from "../tokens/colors"; +import { typography } from "../tokens/typography"; + +/** + * Standard chart colors for PolicyEngine visualizations + */ +export const chartColors = { + primary: TEAL_PRIMARY, + secondary: colors.gray[400], + baseline: colors.gray[300], + positive: TEAL_PRIMARY, + negative: colors.error, + neutral: colors.gray[500], + // For multi-series charts + series: [ + TEAL_PRIMARY, + colors.gray[500], + colors.primary[700], + colors.gray[700], + colors.primary[300], + ], +} as const; + +/** + * Standard Plotly layout configuration for PolicyEngine charts + */ +export const chartLayout = { + font: { + family: typography.fontFamily.chart, + color: colors.text.primary, + size: 14, + }, + paper_bgcolor: colors.white, + plot_bgcolor: colors.white, + margin: { + l: 60, + r: 40, + t: 40, + b: 60, + }, + showlegend: true, + legend: { + orientation: "h" as const, + yanchor: "bottom" as const, + y: 1.02, + xanchor: "right" as const, + x: 1, + }, + xaxis: { + gridcolor: colors.border.light, + zerolinecolor: colors.border.medium, + }, + yaxis: { + gridcolor: colors.border.light, + zerolinecolor: colors.border.medium, + }, +} as const; + +/** + * Standard chart dimensions + */ +export const chartDimensions = { + default: { + width: 800, + height: 600, + }, + compact: { + width: 600, + height: 400, + }, + wide: { + width: 1000, + height: 500, + }, + square: { + width: 600, + height: 600, + }, +} as const; + +/** + * PolicyEngine logo image configuration for chart watermarks + */ +export const chartLogo = { + source: "/assets/logos/policyengine/teal-square.png", + xref: "paper" as const, + yref: "paper" as const, + x: 1, + y: 0, + sizex: 0.1, + sizey: 0.1, + xanchor: "right" as const, + yanchor: "bottom" as const, + opacity: 0.8, +} as const; + +/** + * Format configuration object for creating Plotly charts + * Compatible with both React (react-plotly.js) and Python (plotly.py) + */ +export interface ChartConfig { + layout: typeof chartLayout; + config: { + displayModeBar: boolean; + responsive: boolean; + }; + style: { + width: string; + height: string; + }; +} + +export function getChartConfig( + dimensions: keyof typeof chartDimensions = "default", +): ChartConfig { + const dims = chartDimensions[dimensions]; + return { + layout: chartLayout, + config: { + displayModeBar: false, + responsive: true, + }, + style: { + width: `${dims.width}px`, + height: `${dims.height}px`, + }, + }; +} + +/** + * Currency formatter for chart axis labels + */ +export function formatCurrency(value: number): string { + if (Math.abs(value) >= 1e9) { + return `$${(value / 1e9).toFixed(1)}B`; + } + if (Math.abs(value) >= 1e6) { + return `$${(value / 1e6).toFixed(1)}M`; + } + if (Math.abs(value) >= 1e3) { + return `$${(value / 1e3).toFixed(0)}K`; + } + return `$${value.toFixed(0)}`; +} + +/** + * Percentage formatter for chart axis labels + */ +export function formatPercent(value: number, decimals: number = 0): string { + return `${(value * 100).toFixed(decimals)}%`; +} + +// --------------------------------------------------------------------------- +// Nice tick generation (D3-style) +// --------------------------------------------------------------------------- + +const NICE_STEPS = [1, 2, 2.5, 5, 10]; + +/** + * Compute a "nice" step size for axis ticks by snapping to {1, 2, 2.5, 5} + * multiples at each order of magnitude. + */ +function niceStep(roughStep: number): number { + if (roughStep <= 0) { + return 1; + } + const exponent = Math.floor(Math.log10(roughStep)); + const magnitude = 10 ** exponent; + const normalized = roughStep / magnitude; + const nice = NICE_STEPS.find((s) => s >= normalized - 1e-10) ?? 10; + return nice * magnitude; +} + +/** + * Generate nice tick values for a given domain and approximate tick count. + * Ticks are rounded to human-friendly numbers (multiples of 1, 2, 2.5, 5). + * + * @param domain - [min, max] data range + * @param count - Approximate number of ticks desired (default: 5) + * @returns Array of tick values + */ +export function getNiceTicks(domain: [number, number], count = 5): number[] { + const [dMin, dMax] = domain; + if (dMin === dMax) { + return [dMin]; + } + + const rawStep = (dMax - dMin) / Math.max(count - 1, 1); + const step = niceStep(rawStep); + const start = Math.floor(dMin / step) * step; + const ticks: number[] = []; + + for (let v = start; v <= dMax + step * 0.01; v += step) { + // Round to avoid floating-point noise and negative zero + const rounded = Math.round(v * 1e10) / 1e10 || 0; + ticks.push(rounded); + } + + return ticks; +} diff --git a/src/legacy/index.ts b/src/legacy/index.ts new file mode 100644 index 0000000..392bd3b --- /dev/null +++ b/src/legacy/index.ts @@ -0,0 +1,32 @@ +/** + * @policyengine/ui-kit/legacy + * + * @deprecated Backwards-compatibility shim that re-exports the API surface of + * the legacy `@policyengine/design-system` package. Use the canonical exports + * from `@policyengine/ui-kit` (root) and the `tokens` / `theme.css` / + * `quarto.scss` subpath exports for new code. + * + * This module exists to make the migration from `@policyengine/design-system` + * a pure import-path rename: + * + * - import … from '@policyengine/design-system' → '@policyengine/ui-kit/legacy' + * - import … from '@policyengine/design-system/tokens' → '@policyengine/ui-kit/legacy/tokens' + * - import … from '@policyengine/design-system/charts' → '@policyengine/ui-kit/legacy/charts' + * - import … from '@policyengine/design-system/tokens/colors' → '@policyengine/ui-kit/legacy/tokens/colors' + * + * Mapping to canonical ui-kit exports for net-new code: + * + * colors.primary[N] → palette.teal[N] + * colors.gray[N] → palette.gray[N] + * colors.warning → semanticFills.warning + * colors.error → semanticFills.error + * colors.text.warning → rootColorsLight['--text-warning'] (or `var(--text-warning)`) + * typography.fontFamily.primary → typography.fontFamily.sans + * spacing.{layout,…} → namedSpacing (only `header`, `sidebar`, `content` exposed today) + * chartColors / chartLayout → see charts/chartDefaults.ts (Recharts) or chartPalette (resolved hex) + * + * This module will be removed in a future major release once consumers migrate. + */ + +export * from "./tokens"; +export * from "./charts"; diff --git a/src/legacy/tokens/colors.ts b/src/legacy/tokens/colors.ts new file mode 100644 index 0000000..eae21e2 --- /dev/null +++ b/src/legacy/tokens/colors.ts @@ -0,0 +1,128 @@ +/** + * @deprecated Compatibility shim mirroring `@policyengine/design-system/tokens/colors`. + * Use the canonical `palette`, `semanticFills`, and `rootColorsLight` / + * `rootColorsDark` exports from `@policyengine/ui-kit` for new code. + */ + +export const colors = { + // Primary brand colors - teal + primary: { + 50: "#E6FFFA", + 100: "#B2F5EA", + 200: "#81E6D9", + 300: "#4FD1C5", + 400: "#38B2AC", + 500: "#319795", // Main brand color + 600: "#2C7A7B", + 700: "#285E61", + 800: "#234E52", + 900: "#1D4044", + alpha: { + 40: "#31979566", + 50: "#31979580", + 60: "#31979599", + }, + }, + + // Secondary colors - gray scale + secondary: { + 50: "#F0F9FF", + 100: "#F2F4F7", + 200: "#E2E8F0", + 300: "#CBD5E1", + 400: "#94A3B8", + 500: "#64748B", + 600: "#475569", + 700: "#344054", + 800: "#1E293B", + 900: "#101828", + }, + + // Semantic colors + warning: "#FEC601", + error: "#EF4444", + info: "#2C7A7B", + + // Neutral colors + white: "#FFFFFF", + black: "#000000", + + // Gray scale (alias for secondary) + gray: { + 50: "#F9FAFB", + 100: "#F2F4F7", + 200: "#E2E8F0", + 300: "#D1D5DB", + 400: "#9CA3AF", + 500: "#6B7280", + 600: "#4B5563", + 700: "#344054", + 800: "#1F2937", + 900: "#101828", + }, + + // Background colors + background: { + primary: "#FFFFFF", + secondary: "#F5F9FF", + tertiary: "#F1F5F9", + sider: "#FFFFFF", + }, + + // Text colors + text: { + primary: "#000000", + secondary: "#5A5A5A", + tertiary: "#9CA3AF", + inverse: "#FFFFFF", + title: "#000000", + link: "#2C7A7B", + linkHover: "#285E61", + warning: "#d9480f", // Mantine orange.9 — WCAG AA compliant (~4.8:1 contrast) + error: "#B91C1C", // Tailwind red-700 — WCAG AA compliant on white (5.94:1) and on a 12% --pe-color-error tint + success: "#285E61", // primary[700] — WCAG AA compliant on white (7.07:1) and on the success-soft tint + }, + + // Teal alias (for convenience) + teal: { + 500: "#319795", + }, + + // Border colors + border: { + light: "#E2E8F0", + medium: "#CBD5E1", + dark: "#94A3B8", + }, + + // Shadow colors + shadow: { + light: "rgba(16, 24, 40, 0.05)", + medium: "rgba(16, 24, 40, 0.1)", + dark: "rgba(16, 24, 40, 0.2)", + }, +} as const; + +// Convenience exports for common colors +export const TEAL_PRIMARY = colors.primary[500]; +export const TEAL_ACCENT = "#39C6C0"; // Legacy accent + +// Semantic color exports (for givecalc compatibility) +export const WARNING_YELLOW = colors.warning; +export const ERROR_RED = colors.error; +export const INFO_COLOR = colors.info; + +// Background exports (for givecalc compatibility) +export const BACKGROUND_PRIMARY = colors.background.primary; +export const BACKGROUND_SIDEBAR = colors.background.secondary; +export const BACKGROUND_TERTIARY = colors.background.tertiary; + +// Text exports (for givecalc compatibility) +export const TEXT_PRIMARY = colors.text.primary; +export const TEXT_SECONDARY = colors.text.secondary; +export const TEXT_TERTIARY = colors.text.tertiary; + +// Border exports (for givecalc compatibility) +export const BORDER_LIGHT = colors.border.light; + +export type Colors = typeof colors; diff --git a/src/legacy/tokens/index.ts b/src/legacy/tokens/index.ts new file mode 100644 index 0000000..e3e29e6 --- /dev/null +++ b/src/legacy/tokens/index.ts @@ -0,0 +1,26 @@ +/** + * @deprecated Compatibility shim mirroring the API of the legacy + * `@policyengine/design-system/tokens` module. Use the canonical + * `@policyengine/ui-kit` exports (`palette`, `semanticFills`, `typography`, + * `namedSpacing`, `tokens`) for new code. + * + * PolicyEngine Design Tokens + * Re-exports all design tokens for convenient access + */ + +export * from './colors'; +export * from './typography'; +export * from './spacing'; + +// Combined tokens object for JSON/YAML export +import { colors } from './colors'; +import { typography } from './typography'; +import { spacing } from './spacing'; + +export const tokens = { + colors, + typography, + spacing, +} as const; + +export type Tokens = typeof tokens; diff --git a/src/legacy/tokens/spacing.ts b/src/legacy/tokens/spacing.ts new file mode 100644 index 0000000..1646c00 --- /dev/null +++ b/src/legacy/tokens/spacing.ts @@ -0,0 +1,101 @@ +/** + * @deprecated Compatibility shim mirroring `@policyengine/design-system/tokens/spacing`. + * For new code, prefer `namedSpacing` (layout sizes) from `@policyengine/ui-kit` + * or Tailwind's spacing utilities. + * + * PolicyEngine spacing system + * Source of truth for spacing, layout, and border radius + */ + +export const spacing = { + // Base spacing scale + xs: '4px', + sm: '8px', + md: '12px', + lg: '16px', + xl: '20px', + '2xl': '24px', + '3xl': '32px', + '4xl': '48px', + '5xl': '64px', + + // Component-specific spacing + component: { + button: { + padding: '8px 14px', + height: '36px', + }, + input: { + padding: '8px 12px', + height: '40px', + compactWidth: '120px', + }, + badge: { + padding: '4px 12px', + }, + menu: { + itemPadding: '6px 24px', + itemHeight: '40px', + }, + tab: { + padding: '12px 16px', + }, + }, + + // Layout spacing + layout: { + sidebar: '79px', + sidebarWidth: '280px', + header: '58px', + content: '1361px', + container: '976px', + sideGutter: '200px', + }, + + // AppShell design tokens + appShell: { + header: { + height: '58px', + padding: '8px 16px', + }, + navbar: { + width: '300px', + padding: '0px', + breakpoint: 'sm', + }, + aside: { + width: '300px', + padding: '16px', + breakpoint: 'md', + }, + footer: { + height: '60px', + padding: '12px 24px', + }, + main: { + padding: '24px', + minHeight: '100dvh', + }, + }, + + // Container padding + container: { + xs: '16px', + sm: '24px', + md: '32px', + lg: '48px', + xl: '64px', + '2xl': '80px', + }, + + // Border radius — semantic scale + radius: { + none: '0px', + chip: '2px', + element: '4px', + container: '8px', + feature: '12px', + }, +} as const; + +export type Spacing = typeof spacing; diff --git a/src/legacy/tokens/typography.ts b/src/legacy/tokens/typography.ts new file mode 100644 index 0000000..a43a99c --- /dev/null +++ b/src/legacy/tokens/typography.ts @@ -0,0 +1,102 @@ +/** + * @deprecated Compatibility shim mirroring `@policyengine/design-system/tokens/typography`. + * Use the canonical `typography` export from `@policyengine/ui-kit` for new code. + * + * PolicyEngine typography system + * Source of truth for fonts, sizes, and text styles + * + * Two font families only: Inter (everything) + JetBrains Mono (code). + * Legacy aliases (secondary, body, chart, prose) all resolve to the + * primary Inter stack for backward compatibility. + */ + +const INTER = 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; + +export const typography = { + fontFamily: { + primary: INTER, + // Legacy aliases — all resolve to Inter + secondary: INTER, + body: INTER, + chart: INTER, + prose: INTER, + // Code + mono: 'JetBrains Mono, "Fira Code", Consolas, monospace', + }, + + fontWeight: { + thin: 100, + light: 300, + normal: 400, + medium: 500, + semibold: 600, + bold: 700, + extrabold: 800, + black: 900, + }, + + fontSize: { + xs: '12px', + sm: '14px', + base: '16px', + lg: '18px', + xl: '20px', + '2xl': '24px', + '3xl': '28px', + '4xl': '32px', + }, + + lineHeight: { + none: '1', + tight: '1.25', + snug: '1.375', + normal: '1.5', + relaxed: '1.625', + loose: '2', + '20': '20px', + '22': '22px', + '24': '24px', + }, + + // Pre-defined text styles — all use Inter + textStyles: { + 'sm-medium': { + fontFamily: 'Inter', + fontSize: '14px', + fontWeight: 500, + lineHeight: '20px', + }, + 'sm-semibold': { + fontFamily: 'Inter', + fontSize: '14px', + fontWeight: 600, + lineHeight: '20px', + }, + 'md-normal': { + fontFamily: 'Inter', + fontSize: '16px', + fontWeight: 400, + lineHeight: '24px', + }, + 'body-regular': { + fontFamily: 'Inter', + fontSize: '14px', + fontWeight: 400, + lineHeight: '22px', + }, + 'h5-regular': { + fontFamily: 'Inter', + fontSize: '16px', + fontWeight: 400, + lineHeight: '24px', + }, + }, +} as const; + +// Convenience exports for common fonts +export const FONT_UI = typography.fontFamily.primary; +export const FONT_CHART = typography.fontFamily.chart; +export const FONT_PROSE = typography.fontFamily.prose; +export const FONT_MONO = typography.fontFamily.mono; + +export type Typography = typeof typography; diff --git a/tests/legacy/charts.test.ts b/tests/legacy/charts.test.ts new file mode 100644 index 0000000..0a6bd81 --- /dev/null +++ b/tests/legacy/charts.test.ts @@ -0,0 +1,170 @@ +import { describe, it, expect } from "vitest"; +import { + chartColors, + chartLayout, + chartDimensions, + chartLogo, + getChartConfig, + formatCurrency, + formatPercent, +} from "../../src/legacy/charts"; +import { colors } from "../../src/legacy/tokens/colors"; +import { typography } from "../../src/legacy/tokens/typography"; + +describe("charts", () => { + describe("chartColors", () => { + it("should have primary as teal", () => { + expect(chartColors.primary).toBe(colors.primary[500]); + expect(chartColors.primary).toBe("#319795"); + }); + + it("should have semantic colors", () => { + expect(chartColors.positive).toBe(colors.primary[500]); + expect(chartColors.negative).toBe(colors.error); + }); + + it("should have a series array for multi-line charts", () => { + expect(chartColors.series).toBeInstanceOf(Array); + expect(chartColors.series.length).toBeGreaterThanOrEqual(5); + expect(chartColors.series[0]).toBe(chartColors.primary); + expect(chartColors.series).toContain(colors.gray[500]); + expect(chartColors.series).toContain(colors.primary[700]); + }); + }); + + describe("chartLayout", () => { + it("should use Inter font for charts", () => { + expect(chartLayout.font.family).toBe(typography.fontFamily.chart); + expect(chartLayout.font.family).toContain("Inter"); + }); + + it("should have white background", () => { + expect(chartLayout.paper_bgcolor).toBe(colors.white); + expect(chartLayout.plot_bgcolor).toBe(colors.white); + }); + + it("should have sensible margins", () => { + expect(chartLayout.margin.l).toBeGreaterThan(0); + expect(chartLayout.margin.r).toBeGreaterThan(0); + expect(chartLayout.margin.t).toBeGreaterThan(0); + expect(chartLayout.margin.b).toBeGreaterThan(0); + }); + + it("should have horizontal legend at top", () => { + expect(chartLayout.legend.orientation).toBe("h"); + expect(chartLayout.legend.y).toBeGreaterThan(1); + }); + }); + + describe("chartDimensions", () => { + it("should have default dimensions", () => { + expect(chartDimensions.default.width).toBe(800); + expect(chartDimensions.default.height).toBe(600); + }); + + it("should have compact dimensions", () => { + expect(chartDimensions.compact.width).toBeLessThan( + chartDimensions.default.width, + ); + expect(chartDimensions.compact.height).toBeLessThan( + chartDimensions.default.height, + ); + }); + + it("should have wide dimensions", () => { + expect(chartDimensions.wide.width).toBeGreaterThan( + chartDimensions.default.width, + ); + }); + + it("should have square dimensions", () => { + expect(chartDimensions.square.width).toBe(chartDimensions.square.height); + }); + }); + + describe("chartLogo", () => { + it("should reference the teal-square logo", () => { + expect(chartLogo.source).toContain("teal-square.png"); + }); + + it("should be positioned in bottom-right", () => { + expect(chartLogo.x).toBe(1); + expect(chartLogo.y).toBe(0); + expect(chartLogo.xanchor).toBe("right"); + expect(chartLogo.yanchor).toBe("bottom"); + }); + }); + + describe("getChartConfig", () => { + it("should return default config when called without arguments", () => { + const config = getChartConfig(); + expect(config.style.width).toBe("800px"); + expect(config.style.height).toBe("600px"); + }); + + it("should return compact config when specified", () => { + const config = getChartConfig("compact"); + expect(config.style.width).toBe("600px"); + expect(config.style.height).toBe("400px"); + }); + + it("should disable mode bar by default", () => { + const config = getChartConfig(); + expect(config.config.displayModeBar).toBe(false); + }); + + it("should enable responsive mode", () => { + const config = getChartConfig(); + expect(config.config.responsive).toBe(true); + }); + }); + + describe("formatCurrency", () => { + it("should format small values without suffix", () => { + expect(formatCurrency(500)).toBe("$500"); + expect(formatCurrency(999)).toBe("$999"); + }); + + it("should format thousands with K suffix", () => { + expect(formatCurrency(1000)).toBe("$1K"); + expect(formatCurrency(5000)).toBe("$5K"); + expect(formatCurrency(50000)).toBe("$50K"); + }); + + it("should format millions with M suffix", () => { + expect(formatCurrency(1000000)).toBe("$1.0M"); + expect(formatCurrency(5500000)).toBe("$5.5M"); + }); + + it("should format billions with B suffix", () => { + expect(formatCurrency(1000000000)).toBe("$1.0B"); + expect(formatCurrency(2500000000)).toBe("$2.5B"); + }); + + it("should handle negative values", () => { + expect(formatCurrency(-5000)).toBe("$-5K"); + expect(formatCurrency(-1000000)).toBe("$-1.0M"); + }); + }); + + describe("formatPercent", () => { + it("should format decimals as percentages", () => { + expect(formatPercent(0.5)).toBe("50%"); + expect(formatPercent(0.25)).toBe("25%"); + expect(formatPercent(1)).toBe("100%"); + }); + + it("should support decimal places", () => { + expect(formatPercent(0.256, 1)).toBe("25.6%"); + expect(formatPercent(0.2567, 2)).toBe("25.67%"); + }); + + it("should handle values over 100%", () => { + expect(formatPercent(1.5)).toBe("150%"); + }); + + it("should handle zero", () => { + expect(formatPercent(0)).toBe("0%"); + }); + }); +}); diff --git a/tests/legacy/colors.test.ts b/tests/legacy/colors.test.ts new file mode 100644 index 0000000..29f4a84 --- /dev/null +++ b/tests/legacy/colors.test.ts @@ -0,0 +1,143 @@ +import { describe, it, expect } from "vitest"; +import { + colors, + TEAL_PRIMARY, + TEAL_ACCENT, + WARNING_YELLOW, + ERROR_RED, + INFO_COLOR, + BACKGROUND_PRIMARY, + BACKGROUND_SIDEBAR, + TEXT_PRIMARY, + TEXT_SECONDARY, + BORDER_LIGHT, +} from "../../src/legacy/tokens/colors"; + +describe("colors", () => { + describe("primary (teal) colors", () => { + it("should have primary.500 as the main brand color", () => { + expect(colors.primary[500]).toBe("#319795"); + }); + + it("should have a complete primary scale from 50-900", () => { + const expectedShades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]; + expectedShades.forEach((shade) => { + expect( + colors.primary[shade as keyof typeof colors.primary], + ).toBeDefined(); + expect(colors.primary[shade as keyof typeof colors.primary]).toMatch( + /^#[0-9A-Fa-f]{6}$/, + ); + }); + }); + + it("should have alpha variants for transparency", () => { + expect(colors.primary.alpha[40]).toBe("#31979566"); + expect(colors.primary.alpha[50]).toBe("#31979580"); + expect(colors.primary.alpha[60]).toBe("#31979599"); + }); + }); + + describe("convenience exports", () => { + it("should export TEAL_PRIMARY as primary.500", () => { + expect(TEAL_PRIMARY).toBe(colors.primary[500]); + expect(TEAL_PRIMARY).toBe("#319795"); + }); + + it("should export TEAL_ACCENT for legacy compatibility", () => { + expect(TEAL_ACCENT).toBe("#39C6C0"); + }); + }); + + describe("semantic colors", () => { + it("should have warning, error, info colors", () => { + expect(colors.warning).toBe("#FEC601"); + expect(colors.error).toBe("#EF4444"); + expect(colors.info).toBe("#2C7A7B"); + }); + + it("should export semantic colors as constants", () => { + expect(WARNING_YELLOW).toBe(colors.warning); + expect(ERROR_RED).toBe(colors.error); + expect(INFO_COLOR).toBe(colors.info); + }); + }); + + describe("background colors", () => { + it("should have background variants", () => { + expect(colors.background.primary).toBe("#FFFFFF"); + expect(colors.background.secondary).toBe("#F5F9FF"); + expect(colors.background.tertiary).toBe("#F1F5F9"); + }); + + it("should export background colors for givecalc compatibility", () => { + expect(BACKGROUND_PRIMARY).toBe(colors.background.primary); + expect(BACKGROUND_SIDEBAR).toBe(colors.background.secondary); + }); + }); + + describe("text colors", () => { + it("should have text variants", () => { + expect(colors.text.primary).toBe("#000000"); + expect(colors.text.secondary).toBe("#5A5A5A"); + expect(colors.text.tertiary).toBe("#9CA3AF"); + expect(colors.text.inverse).toBe("#FFFFFF"); + expect(colors.text.link).toBe("#2C7A7B"); + expect(colors.text.linkHover).toBe("#285E61"); + expect(colors.text.warning).toBe("#d9480f"); + expect(colors.text.error).toBe("#B91C1C"); + expect(colors.text.success).toBe("#285E61"); + }); + + it("should export text colors for givecalc compatibility", () => { + expect(TEXT_PRIMARY).toBe(colors.text.primary); + expect(TEXT_SECONDARY).toBe(colors.text.secondary); + }); + }); + + describe("border colors", () => { + it("should have border variants", () => { + expect(colors.border.light).toBe("#E2E8F0"); + expect(colors.border.medium).toBe("#CBD5E1"); + expect(colors.border.dark).toBe("#94A3B8"); + }); + + it("should export BORDER_LIGHT for givecalc compatibility", () => { + expect(BORDER_LIGHT).toBe(colors.border.light); + }); + }); + + describe("gray scale", () => { + it("should have gray scale from 50-900", () => { + expect(colors.gray[50]).toBeDefined(); + expect(colors.gray[100]).toBe("#F2F4F7"); + expect(colors.gray[200]).toBe("#E2E8F0"); + expect(colors.gray[700]).toBe("#344054"); + expect(colors.gray[900]).toBe("#101828"); + }); + }); + + describe("color format validation", () => { + it("should have all hex colors in valid format", () => { + const hexRegex = /^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/; + const rgbaRegex = /^rgba\(\d+,\s*\d+,\s*\d+,\s*[\d.]+\)$/; + + // Check primary colors + Object.values(colors.primary).forEach((value) => { + if (typeof value === "string") { + expect(value).toMatch(hexRegex); + } + }); + + // Check semantic colors + expect(colors.warning).toMatch(hexRegex); + expect(colors.error).toMatch(hexRegex); + expect(colors.info).toMatch(hexRegex); + + // Check shadow colors (rgba format) + Object.values(colors.shadow).forEach((value) => { + expect(value).toMatch(rgbaRegex); + }); + }); + }); +}); diff --git a/tests/legacy/spacing.test.ts b/tests/legacy/spacing.test.ts new file mode 100644 index 0000000..65bd079 --- /dev/null +++ b/tests/legacy/spacing.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect } from 'vitest'; +import { spacing } from '../../src/legacy/tokens/spacing'; + +describe('spacing', () => { + describe('base spacing scale', () => { + it('should have a complete spacing scale', () => { + expect(spacing.xs).toBe('4px'); + expect(spacing.sm).toBe('8px'); + expect(spacing.md).toBe('12px'); + expect(spacing.lg).toBe('16px'); + expect(spacing.xl).toBe('20px'); + expect(spacing['2xl']).toBe('24px'); + expect(spacing['3xl']).toBe('32px'); + expect(spacing['4xl']).toBe('48px'); + expect(spacing['5xl']).toBe('64px'); + }); + + it('should follow a consistent progression', () => { + // xs -> sm should double + const xs = parseInt(spacing.xs); + const sm = parseInt(spacing.sm); + expect(sm).toBe(xs * 2); + }); + }); + + describe('component spacing', () => { + it('should have button spacing', () => { + expect(spacing.component.button.padding).toBe('8px 14px'); + expect(spacing.component.button.height).toBe('36px'); + }); + + it('should have input spacing', () => { + expect(spacing.component.input.padding).toBe('8px 12px'); + expect(spacing.component.input.height).toBe('40px'); + expect(spacing.component.input.compactWidth).toBe('120px'); + }); + + it('should have menu spacing', () => { + expect(spacing.component.menu.itemPadding).toBeDefined(); + expect(spacing.component.menu.itemHeight).toBe('40px'); + }); + }); + + describe('layout spacing', () => { + it('should have consistent header height', () => { + expect(spacing.layout.header).toBe('58px'); + expect(spacing.appShell.header.height).toBe('58px'); + }); + + it('should have sidebar dimensions', () => { + expect(spacing.layout.sidebarWidth).toBe('280px'); + expect(spacing.appShell.navbar.width).toBe('300px'); + }); + + it('should have container width', () => { + expect(spacing.layout.container).toBe('976px'); + }); + }); + + describe('appShell tokens', () => { + it('should have header config', () => { + expect(spacing.appShell.header.height).toBe('58px'); + expect(spacing.appShell.header.padding).toBe('8px 16px'); + }); + + it('should have navbar config', () => { + expect(spacing.appShell.navbar.width).toBe('300px'); + expect(spacing.appShell.navbar.breakpoint).toBe('sm'); + }); + + it('should have footer config', () => { + expect(spacing.appShell.footer.height).toBe('60px'); + expect(spacing.appShell.footer.padding).toBe('12px 24px'); + }); + + it('should have main content config', () => { + expect(spacing.appShell.main.padding).toBe('24px'); + expect(spacing.appShell.main.minHeight).toBe('100dvh'); + }); + }); + + describe('border radius', () => { + it('should have a complete semantic radius scale', () => { + expect(spacing.radius.none).toBe('0px'); + expect(spacing.radius.chip).toBe('2px'); + expect(spacing.radius.element).toBe('4px'); + expect(spacing.radius.container).toBe('8px'); + expect(spacing.radius.feature).toBe('12px'); + }); + }); + + describe('container padding', () => { + it('should have responsive container padding', () => { + expect(spacing.container.xs).toBe('16px'); + expect(spacing.container.sm).toBe('24px'); + expect(spacing.container.md).toBe('32px'); + expect(spacing.container.lg).toBe('48px'); + expect(spacing.container.xl).toBe('64px'); + expect(spacing.container['2xl']).toBe('80px'); + }); + }); +}); diff --git a/tests/legacy/typography.test.ts b/tests/legacy/typography.test.ts new file mode 100644 index 0000000..c7ffd40 --- /dev/null +++ b/tests/legacy/typography.test.ts @@ -0,0 +1,123 @@ +import { describe, it, expect } from 'vitest'; +import { typography, FONT_UI, FONT_CHART, FONT_PROSE, FONT_MONO } from '../../src/legacy/tokens/typography'; + +describe('typography', () => { + describe('font families', () => { + it('should use Inter for all non-mono font families', () => { + expect(typography.fontFamily.primary).toContain('Inter'); + expect(typography.fontFamily.secondary).toContain('Inter'); + expect(typography.fontFamily.body).toContain('Inter'); + expect(typography.fontFamily.chart).toContain('Inter'); + expect(typography.fontFamily.prose).toContain('Inter'); + }); + + it('should have all non-mono families resolve to the same value', () => { + const inter = typography.fontFamily.primary; + expect(typography.fontFamily.secondary).toBe(inter); + expect(typography.fontFamily.body).toBe(inter); + expect(typography.fontFamily.chart).toBe(inter); + expect(typography.fontFamily.prose).toBe(inter); + }); + + it('should have monospace font', () => { + expect(typography.fontFamily.mono).toContain('JetBrains Mono'); + }); + + it('should include fallback fonts', () => { + expect(typography.fontFamily.primary).toContain('sans-serif'); + expect(typography.fontFamily.mono).toContain('monospace'); + }); + }); + + describe('convenience exports', () => { + it('should export FONT_UI as primary font family', () => { + expect(FONT_UI).toBe(typography.fontFamily.primary); + }); + + it('should export FONT_CHART as Inter', () => { + expect(FONT_CHART).toBe(typography.fontFamily.chart); + expect(FONT_CHART).toContain('Inter'); + }); + + it('should export FONT_PROSE as Inter (same as primary)', () => { + expect(FONT_PROSE).toBe(typography.fontFamily.primary); + expect(FONT_PROSE).toContain('Inter'); + }); + + it('should export FONT_MONO for code', () => { + expect(FONT_MONO).toBe(typography.fontFamily.mono); + }); + }); + + describe('font weights', () => { + it('should have standard weight scale', () => { + expect(typography.fontWeight.normal).toBe(400); + expect(typography.fontWeight.medium).toBe(500); + expect(typography.fontWeight.semibold).toBe(600); + expect(typography.fontWeight.bold).toBe(700); + }); + + it('should have all weights from thin to black', () => { + expect(typography.fontWeight.thin).toBe(100); + expect(typography.fontWeight.black).toBe(900); + }); + }); + + describe('font sizes', () => { + it('should have base size of 16px', () => { + expect(typography.fontSize.base).toBe('16px'); + }); + + it('should have a complete size scale', () => { + expect(typography.fontSize.xs).toBe('12px'); + expect(typography.fontSize.sm).toBe('14px'); + expect(typography.fontSize.lg).toBe('18px'); + expect(typography.fontSize.xl).toBe('20px'); + expect(typography.fontSize['2xl']).toBe('24px'); + expect(typography.fontSize['4xl']).toBe('32px'); + }); + + it('should have all sizes in px format', () => { + Object.values(typography.fontSize).forEach((size) => { + expect(size).toMatch(/^\d+px$/); + }); + }); + }); + + describe('line heights', () => { + it('should have semantic line height values', () => { + expect(typography.lineHeight.none).toBe('1'); + expect(typography.lineHeight.tight).toBe('1.25'); + expect(typography.lineHeight.normal).toBe('1.5'); + expect(typography.lineHeight.loose).toBe('2'); + }); + + it('should have pixel-based line heights for specific uses', () => { + expect(typography.lineHeight['20']).toBe('20px'); + expect(typography.lineHeight['22']).toBe('22px'); + expect(typography.lineHeight['24']).toBe('24px'); + }); + }); + + describe('text styles', () => { + it('should have predefined text styles', () => { + expect(typography.textStyles['sm-medium']).toBeDefined(); + expect(typography.textStyles['sm-semibold']).toBeDefined(); + expect(typography.textStyles['body-regular']).toBeDefined(); + }); + + it('should use Inter for all text styles', () => { + Object.values(typography.textStyles).forEach((style) => { + expect(style.fontFamily).toBe('Inter'); + }); + }); + + it('should have complete text style definitions', () => { + const smMedium = typography.textStyles['sm-medium']; + expect(smMedium.fontFamily).toBe('Inter'); + expect(smMedium.fontSize).toBe('14px'); + expect(smMedium.fontWeight).toBe(500); + expect(smMedium.lineHeight).toBe('20px'); + }); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index ba78c68..c15303f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,6 +22,14 @@ export default defineConfig({ display: resolve(__dirname, 'src/display/index.ts'), utils: resolve(__dirname, 'src/utils/index.ts'), assets: resolve(__dirname, 'src/assets/index.ts'), + // Legacy compat surface mirroring @policyengine/design-system. See + // src/legacy/index.ts for the migration map. + legacy: resolve(__dirname, 'src/legacy/index.ts'), + 'legacy/tokens': resolve(__dirname, 'src/legacy/tokens/index.ts'), + 'legacy/tokens/colors': resolve(__dirname, 'src/legacy/tokens/colors.ts'), + 'legacy/tokens/typography': resolve(__dirname, 'src/legacy/tokens/typography.ts'), + 'legacy/tokens/spacing': resolve(__dirname, 'src/legacy/tokens/spacing.ts'), + 'legacy/charts': resolve(__dirname, 'src/legacy/charts/index.ts'), }, formats: ['es', 'cjs'], fileName: (format, entryName) => `${entryName}.${format === 'es' ? 'js' : 'cjs'}`,