From a429ed0601534f87fc4cd0d627f2d6a7e067f065 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 11:55:24 -0400 Subject: [PATCH 1/9] Run ESLint for the website and calculator in CI The Lint and format job ran ESLint only in app/. The two Next.js apps that are now deployed, website/ and calculator-app/, were never linted in CI. - pr.yaml and push.yaml run `bun run lint` in website/ and calculator-app/ after app/'s checks. - website: `settings.next.rootDir` was "website/", which matched no folder when ESLint ran from website/. @next/next/no-html-link-for-pages then printed "Pages directory cannot be found" and switched itself off. rootDir is now the config's own folder. With the rule back on, the only finding was PE84Banner, which nothing has imported since the banner left the homepage (c09c0885b), so it is removed. - calculator-app had no ESLint setup. It gets the website's Next.js config, plus app/'s settings for `any` and unused variables, since its routes are written in app/'s style. Fixes for the findings: next.config.ts takes webpack from the options Next passes instead of require("webpack"); postcss.config.mjs names its export; the two error pages keep their plain (a full page load) with a disable comment saying why. - website and calculator lint, and app's eslint, run with --max-warnings 0, so warnings fail CI as CLAUDE.md already says. - @eslint/eslintrc, which both eslint configs import, is declared in each app instead of relying on hoisting. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr.yaml | 12 ++++- .github/workflows/push.yaml | 12 ++++- app/package.json | 2 +- bun.lock | 4 ++ calculator-app/eslint.config.mjs | 44 +++++++++++++++++++ calculator-app/next.config.ts | 3 +- calculator-app/package.json | 5 +++ calculator-app/postcss.config.mjs | 3 +- .../app/[countryId]/(calculator)/error.tsx | 3 ++ calculator-app/src/app/error.tsx | 3 ++ website/eslint.config.mjs | 5 ++- website/package.json | 3 +- website/src/components/home/PE84Banner.tsx | 26 ----------- 13 files changed, 89 insertions(+), 36 deletions(-) create mode 100644 calculator-app/eslint.config.mjs delete mode 100644 website/src/components/home/PE84Banner.tsx diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index fc1c1ab3d..1e76a6423 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -27,14 +27,22 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Check formatting with Prettier + - name: Check formatting with Prettier (app) working-directory: ./app run: bun run prettier - - name: Run ESLint + - name: Run ESLint (app) working-directory: ./app run: bun run eslint + - name: Run ESLint (website) + working-directory: ./website + run: bun run lint + + - name: Run ESLint (calculator) + working-directory: ./calculator-app + run: bun run lint + typecheck: name: Type checking runs-on: ubuntu-latest diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 186d2a21a..2ae1e165a 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -20,14 +20,22 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Check formatting with Prettier + - name: Check formatting with Prettier (app) working-directory: ./app run: bun run prettier - - name: Run ESLint + - name: Run ESLint (app) working-directory: ./app run: bun run eslint + - name: Run ESLint (website) + working-directory: ./website + run: bun run lint + + - name: Run ESLint (calculator) + working-directory: ./calculator-app + run: bun run lint + typecheck: name: Type checking runs-on: ubuntu-latest diff --git a/app/package.json b/app/package.json index 6b95a83aa..adcbe10a0 100644 --- a/app/package.json +++ b/app/package.json @@ -9,7 +9,7 @@ "build-parameter-dependencies": "python3 scripts/build-parameter-dependency-map.py", "typecheck": "tsc --noEmit", "lint": "bun run eslint && bun run stylelint", - "eslint": "eslint . --cache", + "eslint": "eslint . --cache --max-warnings 0", "stylelint": "stylelint '**/*.css' --cache", "prettier": "prettier --check \"**/*.{ts,tsx}\"", "prettier:write": "prettier --write \"**/*.{ts,tsx}\"", diff --git a/bun.lock b/bun.lock index 7da0225ac..7f451212c 100644 --- a/bun.lock +++ b/bun.lock @@ -122,12 +122,15 @@ "tailwindcss": "^4.2.0", }, "devDependencies": { + "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4.1.8", "@types/node": "^22.15.0", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", "@types/react-plotly.js": "^2.6.3", "@types/react-simple-maps": "^3.0.6", + "eslint": "^9.29.0", + "eslint-config-next": "^15.3.3", "postcss": "^8.5.4", "typescript": "^5.8.0", }, @@ -155,6 +158,7 @@ "tailwind-merge": "^3.5.0", }, "devDependencies": { + "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4.1.8", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", diff --git a/calculator-app/eslint.config.mjs b/calculator-app/eslint.config.mjs new file mode 100644 index 000000000..97f68d912 --- /dev/null +++ b/calculator-app/eslint.config.mjs @@ -0,0 +1,44 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { FlatCompat } from "@eslint/eslintrc"; +import { defineConfig, globalIgnores } from "eslint/config"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +// The Next.js rules the website uses. ESLint lints only this folder; the +// shared code it imports from ../app/src is linted by app/'s own config. +export default defineConfig([ + ...compat.config({ + extends: ["next/core-web-vitals", "next/typescript"], + settings: { + next: { + // Absolute, so @next/next/no-html-link-for-pages finds src/app + // whichever folder ESLint runs from. + rootDir: __dirname, + }, + }, + }), + { + // The calculator's routes are written in app/'s TypeScript style, so + // they follow app/'s rules for `any` and unused variables. + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + caughtErrors: "none", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + }, + }, + globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]), +]); diff --git a/calculator-app/next.config.ts b/calculator-app/next.config.ts index 6a6c7ad82..e0f20f38f 100644 --- a/calculator-app/next.config.ts +++ b/calculator-app/next.config.ts @@ -39,10 +39,9 @@ const nextConfig: NextConfig = { ]; }, - webpack: (config, { dev }) => { + webpack: (config, { dev, webpack }) => { // Polyfill import.meta.env for shared code written for Vite. // These are replaced at build time — no runtime cost. - const webpack = require("webpack"); config.plugins.push( new webpack.DefinePlugin({ "import.meta.env.DEV": JSON.stringify(dev), diff --git a/calculator-app/package.json b/calculator-app/package.json index 4265aa079..df6c7dee2 100644 --- a/calculator-app/package.json +++ b/calculator-app/package.json @@ -6,6 +6,8 @@ "dev": "sh -c 'next dev --port ${PORT:-3001}'", "build": "next build", "start": "next start", + "lint": "eslint . --max-warnings 0", + "lint:fix": "eslint . --fix", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -38,12 +40,15 @@ "tailwindcss": "^4.2.0" }, "devDependencies": { + "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4.1.8", "@types/node": "^22.15.0", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", "@types/react-plotly.js": "^2.6.3", "@types/react-simple-maps": "^3.0.6", + "eslint": "^9.29.0", + "eslint-config-next": "^15.3.3", "postcss": "^8.5.4", "typescript": "^5.8.0" } diff --git a/calculator-app/postcss.config.mjs b/calculator-app/postcss.config.mjs index c2ddf7482..7059fe95a 100644 --- a/calculator-app/postcss.config.mjs +++ b/calculator-app/postcss.config.mjs @@ -1,5 +1,6 @@ -export default { +const config = { plugins: { "@tailwindcss/postcss": {}, }, }; +export default config; diff --git a/calculator-app/src/app/[countryId]/(calculator)/error.tsx b/calculator-app/src/app/[countryId]/(calculator)/error.tsx index f91446d05..6e319e92e 100644 --- a/calculator-app/src/app/[countryId]/(calculator)/error.tsx +++ b/calculator-app/src/app/[countryId]/(calculator)/error.tsx @@ -33,6 +33,9 @@ export default function CountryError({ > Try again + {/* A full page load rather than a client-side navigation, so the + client state that may have caused the error is discarded. */} + {/* eslint-disable-next-line @next/next/no-html-link-for-pages */} Try again + {/* A full page load rather than a client-side navigation, so the + client state that may have caused the error is discarded. */} + {/* eslint-disable-next-line @next/next/no-html-link-for-pages */} - Introducing the PE-84 — The graphing microsimulator.{" "} - - Learn more → - - - ); -} From 3d225e517d91964d50cc65d20b1422dab46e6c98 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 11:56:27 -0400 Subject: [PATCH 2/9] Pin Prettier's default style for the website and calculator Neither app had a Prettier config, so Prettier used its defaults unless it found a config in a parent folder (a developer's home folder, say). Each app now has an empty prettier.config.mjs, which keeps the defaults and stops that search, plus `format` and `format:check` scripts over ts, tsx, mjs and css files. public/ is ignored: in both apps it links to app/public, which holds built assets. Co-Authored-By: Claude Opus 5.5 --- bun.lock | 2 ++ calculator-app/.prettierignore | 2 ++ calculator-app/package.json | 3 +++ calculator-app/prettier.config.mjs | 6 ++++++ website/.prettierignore | 2 ++ website/package.json | 3 +++ website/prettier.config.mjs | 6 ++++++ 7 files changed, 24 insertions(+) create mode 100644 calculator-app/.prettierignore create mode 100644 calculator-app/prettier.config.mjs create mode 100644 website/.prettierignore create mode 100644 website/prettier.config.mjs diff --git a/bun.lock b/bun.lock index 7f451212c..abb341c13 100644 --- a/bun.lock +++ b/bun.lock @@ -132,6 +132,7 @@ "eslint": "^9.29.0", "eslint-config-next": "^15.3.3", "postcss": "^8.5.4", + "prettier": "^3.5.3", "typescript": "^5.8.0", }, }, @@ -173,6 +174,7 @@ "fast-check": "^4.10.2", "jsdom": "^26.1.0", "postcss": "^8.5.4", + "prettier": "^3.5.3", "tailwindcss": "^4.1.8", "typescript": "^5.8.0", "vite-tsconfig-paths": "^4.3.2", diff --git a/calculator-app/.prettierignore b/calculator-app/.prettierignore new file mode 100644 index 000000000..a0241161d --- /dev/null +++ b/calculator-app/.prettierignore @@ -0,0 +1,2 @@ +# Symlinks into app/public, which holds built assets. +public/ diff --git a/calculator-app/package.json b/calculator-app/package.json index df6c7dee2..6a531f392 100644 --- a/calculator-app/package.json +++ b/calculator-app/package.json @@ -8,6 +8,8 @@ "start": "next start", "lint": "eslint . --max-warnings 0", "lint:fix": "eslint . --fix", + "format": "prettier --write \"**/*.{ts,tsx,mjs,css}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,mjs,css}\"", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -50,6 +52,7 @@ "eslint": "^9.29.0", "eslint-config-next": "^15.3.3", "postcss": "^8.5.4", + "prettier": "^3.5.3", "typescript": "^5.8.0" } } diff --git a/calculator-app/prettier.config.mjs b/calculator-app/prettier.config.mjs new file mode 100644 index 000000000..8857869d6 --- /dev/null +++ b/calculator-app/prettier.config.mjs @@ -0,0 +1,6 @@ +// The calculator uses Prettier's default style. This file stops Prettier from +// looking in parent folders for a config; app/ has its own. +/** @type {import("prettier").Config} */ +const config = {}; + +export default config; diff --git a/website/.prettierignore b/website/.prettierignore new file mode 100644 index 000000000..a0241161d --- /dev/null +++ b/website/.prettierignore @@ -0,0 +1,2 @@ +# Symlinks into app/public, which holds built assets. +public/ diff --git a/website/package.json b/website/package.json index 252e17859..65fa09062 100644 --- a/website/package.json +++ b/website/package.json @@ -9,6 +9,8 @@ "start": "next start", "lint": "eslint . --max-warnings 0", "lint:fix": "eslint . --fix", + "format": "prettier --write \"**/*.{ts,tsx,mjs,css}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,mjs,css}\"", "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest" @@ -48,6 +50,7 @@ "fast-check": "^4.10.2", "jsdom": "^26.1.0", "postcss": "^8.5.4", + "prettier": "^3.5.3", "tailwindcss": "^4.1.8", "typescript": "^5.8.0", "vite-tsconfig-paths": "^4.3.2", diff --git a/website/prettier.config.mjs b/website/prettier.config.mjs new file mode 100644 index 000000000..539474558 --- /dev/null +++ b/website/prettier.config.mjs @@ -0,0 +1,6 @@ +// The website uses Prettier's default style. This file stops Prettier from +// looking in parent folders for a config; app/ has its own. +/** @type {import("prettier").Config} */ +const config = {}; + +export default config; From e8d04db611dc231e6c27fe178893f55e3bfc0435 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 11:59:02 -0400 Subject: [PATCH 3/9] Format the website and calculator with Prettier `bun run format` in website/ and calculator-app/, nothing else. 74 website files and 4 calculator files had drifted from Prettier's default style; most were copied from app/ with app/'s single quotes. Every changed ts, tsx and mjs file parses to the same syntax tree before and after, and keeps the same comments. The css changes are quote style and one wrapped custom property. Co-Authored-By: Claude Opus 5.5 --- .../app/[countryId]/(calculator)/error.tsx | 5 +- .../app/[countryId]/(calculator)/loading.tsx | 9 +- .../report/[userReportId]/page.tsx | 9 +- calculator-app/src/app/error.tsx | 5 +- website/eslint.config.mjs | 5 +- .../__tests__/app/api/geolocate/route.test.ts | 8 +- .../src/__tests__/components/header.test.tsx | 4 +- .../src/app/[countryId]/brand/assets/page.tsx | 14 +- .../brand/design/BrandDesignClient.tsx | 71 ++-- website/src/app/[countryId]/brand/page.tsx | 6 +- .../app/[countryId]/brand/writing/page.tsx | 55 +-- .../dev-tools/api-status/ApiStatusClient.tsx | 23 +- .../[countryId]/dev-tools/api-status/page.tsx | 3 +- .../events/iariw-2026/WorkshopContent.tsx | 7 +- .../[countryId]/research/ResearchClient.tsx | 359 +++++++++--------- .../research/[slug]/ArticleClient.tsx | 19 +- website/src/app/api/betterstack/route.ts | 8 +- website/src/app/globals.css | 15 +- website/src/app/releases/[version]/page.tsx | 6 +- website/src/components/Footer.tsx | 161 ++++---- website/src/components/blog/blogStyles.ts | 6 +- .../home/FeaturedResearchBanner.tsx | 6 +- website/src/components/home/HeroCTA.tsx | 6 +- website/src/components/home/HeroSection.tsx | 6 +- .../components/home/HomeTrackerPreview.tsx | 6 +- website/src/components/home/OrgLogos.tsx | 6 +- .../src/components/home/TypewriterPrompt.tsx | 6 +- .../src/components/static/ActionButton.tsx | 6 +- website/src/components/static/BulletList.tsx | 6 +- website/src/components/static/CTASection.tsx | 10 +- .../src/components/static/ContentSection.tsx | 6 +- .../src/components/static/LegalPageLayout.tsx | 25 +- .../components/static/SupportedProject.tsx | 18 +- .../src/components/static/SupporterCard.tsx | 18 +- .../src/components/static/TeamMemberCard.tsx | 28 +- website/src/components/static/TeamSection.tsx | 6 +- website/src/components/ui/Container.tsx | 38 +- website/src/components/ui/Group.tsx | 71 ++-- website/src/components/ui/Spinner.tsx | 20 +- website/src/components/ui/Stack.tsx | 50 +-- website/src/components/ui/Text.tsx | 55 ++- website/src/components/ui/Title.tsx | 20 +- website/src/components/ui/accordion.tsx | 22 +- website/src/components/ui/alert.tsx | 33 +- website/src/components/ui/badge.tsx | 35 +- website/src/components/ui/button.tsx | 56 +-- website/src/components/ui/card.tsx | 57 ++- website/src/components/ui/checkbox.tsx | 17 +- website/src/components/ui/collapsible.tsx | 14 +- website/src/components/ui/command.tsx | 67 ++-- .../src/components/ui/date-picker.story.tsx | 8 +- website/src/components/ui/date-picker.tsx | 64 ++-- website/src/components/ui/dialog.tsx | 58 +-- website/src/components/ui/dropdown-menu.tsx | 87 +++-- website/src/components/ui/index.ts | 110 +++--- website/src/components/ui/input.tsx | 14 +- website/src/components/ui/label.tsx | 17 +- website/src/components/ui/popover.tsx | 45 ++- website/src/components/ui/progress.tsx | 12 +- website/src/components/ui/radio-group.tsx | 14 +- website/src/components/ui/scroll-area.tsx | 20 +- .../components/ui/segmented-control.story.tsx | 33 +- .../src/components/ui/segmented-control.tsx | 24 +- website/src/components/ui/select.tsx | 73 ++-- website/src/components/ui/separator.tsx | 14 +- website/src/components/ui/sheet.tsx | 68 ++-- website/src/components/ui/skeleton.tsx | 6 +- website/src/components/ui/switch.tsx | 18 +- website/src/components/ui/table.tsx | 65 ++-- website/src/components/ui/tabs.tsx | 51 +-- website/src/components/ui/textarea.tsx | 10 +- website/src/components/ui/tooltip.tsx | 18 +- .../src/components/ui/year-picker.story.tsx | 8 +- website/src/components/ui/year-picker.tsx | 34 +- website/src/data/supporters.ts | 5 +- website/src/lib/betterstack.ts | 14 +- website/src/lib/utils.ts | 4 +- .../src/styles/stylesheets/RichTextBlock.css | 2 +- 78 files changed, 1306 insertions(+), 1102 deletions(-) diff --git a/calculator-app/src/app/[countryId]/(calculator)/error.tsx b/calculator-app/src/app/[countryId]/(calculator)/error.tsx index 6e319e92e..e33e75f1d 100644 --- a/calculator-app/src/app/[countryId]/(calculator)/error.tsx +++ b/calculator-app/src/app/[countryId]/(calculator)/error.tsx @@ -39,7 +39,10 @@ export default function CountryError({ Go to reports diff --git a/calculator-app/src/app/[countryId]/(calculator)/loading.tsx b/calculator-app/src/app/[countryId]/(calculator)/loading.tsx index 42f177637..ad8ee7d82 100644 --- a/calculator-app/src/app/[countryId]/(calculator)/loading.tsx +++ b/calculator-app/src/app/[countryId]/(calculator)/loading.tsx @@ -1,6 +1,13 @@ export default function CountryLoading() { return ( -
+
import("@/pages/flagship/Report.page"), { - ssr: false, -}); +const FlagshipReportPage = dynamic( + () => import("@/pages/flagship/Report.page"), + { + ssr: false, + }, +); export default function FlagshipReportRoute({ params, diff --git a/calculator-app/src/app/error.tsx b/calculator-app/src/app/error.tsx index d696cf41e..9e911421b 100644 --- a/calculator-app/src/app/error.tsx +++ b/calculator-app/src/app/error.tsx @@ -39,7 +39,10 @@ export default function GlobalError({ Go to reports diff --git a/website/eslint.config.mjs b/website/eslint.config.mjs index e8849355e..837a5ec26 100644 --- a/website/eslint.config.mjs +++ b/website/eslint.config.mjs @@ -22,10 +22,7 @@ export default defineConfig([ }, }), { - files: [ - "src/app/layout.tsx", - "src/components/ui/OptimisedImage.tsx", - ], + files: ["src/app/layout.tsx", "src/components/ui/OptimisedImage.tsx"], rules: { "@next/next/no-img-element": "off", "@next/next/no-page-custom-font": "off", diff --git a/website/src/__tests__/app/api/geolocate/route.test.ts b/website/src/__tests__/app/api/geolocate/route.test.ts index df347933d..3ef988fb2 100644 --- a/website/src/__tests__/app/api/geolocate/route.test.ts +++ b/website/src/__tests__/app/api/geolocate/route.test.ts @@ -99,9 +99,11 @@ describe("GET /api/geolocate", () => { test("given upstream 5xx then 502 with empty body (no key leak)", async () => { // Given process.env.IPAPI_CO_KEY = "secret-test-key"; - global.fetch = vi.fn().mockResolvedValue( - new Response("Internal Server Error", { status: 500 }), - ); + global.fetch = vi + .fn() + .mockResolvedValue( + new Response("Internal Server Error", { status: 500 }), + ); // When const response = await GET(); diff --git a/website/src/__tests__/components/header.test.tsx b/website/src/__tests__/components/header.test.tsx index 39e6f9ca4..659b837b0 100644 --- a/website/src/__tests__/components/header.test.tsx +++ b/website/src/__tests__/components/header.test.tsx @@ -23,7 +23,9 @@ describe("Header", () => { test("does not force desktop navigation to display via inline styles", () => { render(
); - expect(screen.getByTestId("desktop-nav")).not.toHaveStyle({ display: "flex" }); + expect(screen.getByTestId("desktop-nav")).not.toHaveStyle({ + display: "flex", + }); expect(screen.getByTestId("mobile-controls")).toBeInTheDocument(); }); diff --git a/website/src/app/[countryId]/brand/assets/page.tsx b/website/src/app/[countryId]/brand/assets/page.tsx index fcc41dd36..140ac5944 100644 --- a/website/src/app/[countryId]/brand/assets/page.tsx +++ b/website/src/app/[countryId]/brand/assets/page.tsx @@ -2,11 +2,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import { IconCheck, IconX } from "@tabler/icons-react"; import { Button, Text, Title } from "@/components/ui"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; export const metadata: Metadata = { title: "Assets", @@ -81,13 +77,7 @@ function LogoCard({ ); } -function UsageCard({ - type, - items, -}: { - type: "do" | "dont"; - items: string[]; -}) { +function UsageCard({ type, items }: { type: "do" | "dont"; items: string[] }) { const isDo = type === "do"; return (
- - - - - {copied ? "Copied!" : "Click to copy"} - + + + + + {copied ? "Copied!" : "Click to copy"} + ); } @@ -296,9 +297,7 @@ export default function BrandDesignClient() { Tabler Icons . Import from{" "} - - @tabler/icons-react - + @tabler/icons-react .

diff --git a/website/src/app/[countryId]/brand/page.tsx b/website/src/app/[countryId]/brand/page.tsx index b8253fe6d..bab6667d6 100644 --- a/website/src/app/[countryId]/brand/page.tsx +++ b/website/src/app/[countryId]/brand/page.tsx @@ -2,11 +2,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import HeroSection from "@/components/static/HeroSection"; import { Text, Title } from "@/components/ui"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; export const metadata: Metadata = { title: "Brand", diff --git a/website/src/app/[countryId]/brand/writing/page.tsx b/website/src/app/[countryId]/brand/writing/page.tsx index 4a080ef27..fbf978d74 100644 --- a/website/src/app/[countryId]/brand/writing/page.tsx +++ b/website/src/app/[countryId]/brand/writing/page.tsx @@ -2,11 +2,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import { IconCheck, IconX } from "@tabler/icons-react"; import { Text, Title } from "@/components/ui"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; export const metadata: Metadata = { title: "Writing guide", @@ -74,9 +70,7 @@ function ExampleBox({
- How PolicyEngine communicates. Voice, tone, and content guidelines - for research-oriented writing. + How PolicyEngine communicates. Voice, tone, and content guidelines for + research-oriented writing.
@@ -255,16 +243,16 @@ export default function BrandWritingPage() { lineHeight: typography.lineHeight.relaxed, }} > - Always use sentence case for headings, not title case. This - follows the modern standard used by Apple, Google, Slack, Notion, - and GOV.UK. + Always use sentence case for headings, not title case. This follows + the modern standard used by Apple, Google, Slack, Notion, and + GOV.UK.
The rule - Capitalize only the first word of a heading and any proper - nouns. Everything else stays lowercase. + Capitalize only the first word of a heading and any proper nouns. + Everything else stays lowercase.
@@ -295,8 +283,7 @@ export default function BrandWritingPage() {
Proper nouns stay capitalized - Brand names, acronyms, and proper nouns keep their - capitalization. + Brand names, acronyms, and proper nouns keep their capitalization.
@@ -342,9 +329,7 @@ export default function BrandWritingPage() {
  • Navigation labels
  • Button text
  • Form labels
  • -
  • - Documentation titles -
  • +
  • Documentation titles
  • Blog post titles
  • Error messages
  • @@ -371,8 +356,8 @@ export default function BrandWritingPage() {
    Use active voice - Write in active voice. The subject should perform the action, - not receive it. + Write in active voice. The subject should perform the action, not + receive it.
    @@ -438,9 +423,9 @@ export default function BrandWritingPage() {
    Present numbers dispassionately - When presenting results from PolicyEngine, let the data speak - for itself. Avoid adjectives or adverbs that aren't backed - by the numbers themselves. + When presenting results from PolicyEngine, let the data speak for + itself. Avoid adjectives or adverbs that aren't backed by the + numbers themselves.
    @@ -454,13 +439,11 @@ export default function BrandWritingPage() {
    - The policy dramatically slashes poverty by an impressive - 15%. + The policy dramatically slashes poverty by an impressive 15%.
    Benefits skyrocket by a remarkable $2,400/year.
    - The reform has a surprisingly modest cost of just $80 - billion. + The reform has a surprisingly modest cost of just $80 billion.
    diff --git a/website/src/app/[countryId]/dev-tools/api-status/ApiStatusClient.tsx b/website/src/app/[countryId]/dev-tools/api-status/ApiStatusClient.tsx index 2a8ab5526..4dd0ca02b 100644 --- a/website/src/app/[countryId]/dev-tools/api-status/ApiStatusClient.tsx +++ b/website/src/app/[countryId]/dev-tools/api-status/ApiStatusClient.tsx @@ -6,7 +6,13 @@ import { IconCircleCheck, IconCircleX, } from "@tabler/icons-react"; -import { Alert, AlertDescription, AlertTitle, Spinner, Text } from "@/components/ui"; +import { + Alert, + AlertDescription, + AlertTitle, + Spinner, + Text, +} from "@/components/ui"; import { colors, spacing, typography } from "@/designTokens"; import type { DayRecord, @@ -149,9 +155,7 @@ function formatDowntime(minutes: number): string { const hours = Math.floor(minutes / 60); const remainingMinutes = Math.round(minutes % 60); - return remainingMinutes > 0 - ? `${hours}h ${remainingMinutes}m` - : `${hours}h`; + return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`; } function formatDate(dateStr: string): string { @@ -175,9 +179,10 @@ function formatLastUpdated(timestamp: string): string { }); } -function formatMonitorStatus( - status: MonitorStatus, -): { label: string; color: string } { +function formatMonitorStatus(status: MonitorStatus): { + label: string; + color: string; +} { if (status === "up") { return { label: "Operational", color: colors.primary[500] }; } @@ -457,7 +462,9 @@ function useStatusData(): { const response = await fetch( `/api/betterstack?monitors=${MONITOR_IDS.join(",")}`, ); - const data = (await response.json()) as StatusPageData | { error: string }; + const data = (await response.json()) as + | StatusPageData + | { error: string }; if (!response.ok) { throw new Error( diff --git a/website/src/app/[countryId]/dev-tools/api-status/page.tsx b/website/src/app/[countryId]/dev-tools/api-status/page.tsx index c84b89dfa..b1b1b4d3b 100644 --- a/website/src/app/[countryId]/dev-tools/api-status/page.tsx +++ b/website/src/app/[countryId]/dev-tools/api-status/page.tsx @@ -5,7 +5,8 @@ import ApiStatusClient from "./ApiStatusClient"; export const metadata: Metadata = { title: "API status", - description: "Monitor the current status and availability of PolicyEngine API services.", + description: + "Monitor the current status and availability of PolicyEngine API services.", }; export default function ApiStatusPage() { diff --git a/website/src/app/[countryId]/events/iariw-2026/WorkshopContent.tsx b/website/src/app/[countryId]/events/iariw-2026/WorkshopContent.tsx index 7850586bd..a9c642d4a 100644 --- a/website/src/app/[countryId]/events/iariw-2026/WorkshopContent.tsx +++ b/website/src/app/[countryId]/events/iariw-2026/WorkshopContent.tsx @@ -121,9 +121,10 @@ export default function WorkshopContent() { description={ <> A free half-day workshop on open microsimulation, simulation-ready - microdata, and executable law, co-organized by PolicyEngine, the Axiom - Foundation, CAPE, and BEAMM at UCLouvain Saint-Louis, alongside the 39th IARIW General - Conference. Thursday 27 August 2026, 13:00–17:00, Brussels. + microdata, and executable law, co-organized by PolicyEngine, the + Axiom Foundation, CAPE, and BEAMM at UCLouvain Saint-Louis, + alongside the 39th IARIW General Conference. Thursday 27 August + 2026, 13:00–17:00, Brussels. } /> diff --git a/website/src/app/[countryId]/research/ResearchClient.tsx b/website/src/app/[countryId]/research/ResearchClient.tsx index ba1d4ce4e..25274a49d 100644 --- a/website/src/app/[countryId]/research/ResearchClient.tsx +++ b/website/src/app/[countryId]/research/ResearchClient.tsx @@ -32,11 +32,7 @@ import { IconChevronDown, IconSearch, } from "@tabler/icons-react"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; import { Button, Checkbox, @@ -150,170 +146,178 @@ export function BlogPostCard({ // to force a full server request instead of client-side routing. const cardClassName = "tw:no-underline tw:text-inherit tw:group"; const cardContent = ( -
    - {/* Image */} -
    - {item.image && ( - { - e.currentTarget.style.display = "none"; - }} - /> - )} - {/* Gradient overlay at bottom of image for depth */} -
    + {/* Image */} +
    + {item.image && ( + { + e.currentTarget.style.display = "none"; }} /> -
    + )} + {/* Gradient overlay at bottom of image for depth */} +
    +
    - {/* Content */} + {/* Content */} +
    + {/* Tags + Date row */}
    - {/* Tags + Date row */} -
    -
    - {displayTags.map((tag, i) => ( - - {tag} - {i < displayTags.length - 1 && ( - - ● - - )} - - ))} -
    - {formattedDate} +
    + {displayTags.map((tag, i) => ( + + {tag} + {i < displayTags.length - 1 && ( + + ● + + )} + + ))}
    + {formattedDate} +
    - {/* Title */} -

    - {item.title} -

    + {/* Title */} +

    + {item.title} +

    - {/* Description or body-search excerpt */} - - {item.searchExcerpt ? ( - <> - “ - {item.searchExcerpt.text.slice( - 0, - item.searchExcerpt.highlightStart, - )} - - {item.searchExcerpt.highlightedText} - - {item.searchExcerpt.text.slice(item.searchExcerpt.highlightEnd)}” - - ) : ( - item.description - )} - + {/* Description or body-search excerpt */} + + {item.searchExcerpt ? ( + <> + “ + {item.searchExcerpt.text.slice( + 0, + item.searchExcerpt.highlightStart, + )} + + {item.searchExcerpt.highlightedText} + + {item.searchExcerpt.text.slice(item.searchExcerpt.highlightEnd)}” + + ) : ( + item.description + )} + - {/* CTA */} -
    - {item.isApp ? "Open" : "Read more"} - -
    + {/* CTA */} +
    + {item.isApp ? "Open" : "Read more"} +
    +
    ); if (item.isApp) { - return
    {cardContent}; + return ( + + {cardContent} + + ); } - return {cardContent}; + return ( + + {cardContent} + + ); } /* ─── BlogPostGrid ─── */ @@ -475,9 +479,7 @@ function FilterSection({ fw={typography.fontWeight.semibold} size="sm" style={{ - color: isExpanded - ? colors.primary[700] - : colors.secondary[800], + color: isExpanded ? colors.primary[700] : colors.secondary[800], }} > {label} @@ -517,9 +519,7 @@ function FilterSection({ style={{ flex: isExpanded ? "1 1 auto" : "0 1 auto", minHeight: 0, - maxHeight: isExpanded - ? `${Math.min(height, maxHeight)}px` - : "0px", + maxHeight: isExpanded ? `${Math.min(height, maxHeight)}px` : "0px", opacity: isExpanded ? 1 : 0, transition: "max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease", @@ -555,7 +555,12 @@ function CheckboxRow({ role="button" tabIndex={0} onClick={onChange} - onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onChange(); } }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onChange(); + } + }} style={{ display: "flex", alignItems: "center", @@ -635,8 +640,7 @@ export function ResearchFilters({ availableAuthors, countryId = "us", }: ResearchFiltersProps) { - const [expandedSection, setExpandedSection] = - useState(null); + const [expandedSection, setExpandedSection] = useState(null); const [usStatesExpanded, setUsStatesExpanded] = useState(false); const [availableHeight, setAvailableHeight] = useState(400); const filterContainerRef = useRef(null); @@ -833,8 +837,7 @@ export function ResearchFilters({ colors.primary[50]; }} onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = - "transparent"; + e.currentTarget.style.backgroundColor = "transparent"; }} > {usStatesExpanded ? "Hide states" : "Show states"} @@ -885,11 +888,7 @@ export function ResearchFilters({ /* ─── Main Client Component ─── */ -export default function ResearchClient({ - countryId, -}: { - countryId: string; -}) { +export default function ResearchClient({ countryId }: { countryId: string }) { const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname(); @@ -899,10 +898,7 @@ export default function ResearchClient({ const allItems = useMemo(() => getResearchItems(), []); // Default locations based on country - const defaultLocations = useMemo( - () => [countryId, "global"], - [countryId], - ); + const defaultLocations = useMemo(() => [countryId, "global"], [countryId]); // Filter state - initialize from URL params const [searchQuery, setSearchQuery] = useState( @@ -1081,25 +1077,19 @@ export default function ResearchClient({
    {/* Sidebar Filters */}
    ) : filteredItems.length > 0 ? ( <> - + {/* Sentinel element for infinite scroll */} {hasMore && ( diff --git a/website/src/app/[countryId]/research/[slug]/ArticleClient.tsx b/website/src/app/[countryId]/research/[slug]/ArticleClient.tsx index 84d100d1a..fd8f68221 100644 --- a/website/src/app/[countryId]/research/[slug]/ArticleClient.tsx +++ b/website/src/app/[countryId]/research/[slug]/ArticleClient.tsx @@ -27,11 +27,7 @@ import { import { extractMarkdownFromNotebook } from "@/lib/notebookUtils"; import { formatPostDate } from "@/lib/postDate"; import authorsData from "@/data/posts/authors.json"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; const authors = authorsData as AuthorsCollection; @@ -229,11 +225,7 @@ function PostHeading({ > {post.title} - + {post.description}
    ; diff --git a/website/src/components/Footer.tsx b/website/src/components/Footer.tsx index 195dd3ccf..3274c1b9d 100644 --- a/website/src/components/Footer.tsx +++ b/website/src/components/Footer.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from 'react'; +import { useState } from "react"; import { IconBrandFacebook, IconBrandGithub, @@ -9,13 +9,13 @@ import { IconBrandTwitter, IconBrandYoutube, IconMail, -} from '@tabler/icons-react'; -import jsonp from 'jsonp'; -import OptimisedImage from '@/components/ui/OptimisedImage'; -import { Button, Container, Input, Spinner, Stack } from '@/components/ui'; -import { colors, spacing, typography } from '@/designTokens'; -import { cn } from '@/lib/utils'; -import { useCountryId } from '@/hooks/useCountryId'; +} from "@tabler/icons-react"; +import jsonp from "jsonp"; +import OptimisedImage from "@/components/ui/OptimisedImage"; +import { Button, Container, Input, Spinner, Stack } from "@/components/ui"; +import { colors, spacing, typography } from "@/designTokens"; +import { cn } from "@/lib/utils"; +import { useCountryId } from "@/hooks/useCountryId"; /* ── GA4 helpers (inlined; no app-only deps) ── */ @@ -26,17 +26,17 @@ declare global { } function trackEvent(eventName: string, params?: Record) { - if (typeof window !== 'undefined' && window.gtag) { - window.gtag('event', eventName, params); + if (typeof window !== "undefined" && window.gtag) { + window.gtag("event", eventName, params); } } function trackContactClicked() { - trackEvent('contact_clicked'); + trackEvent("contact_clicked"); } function trackNewsletterSignup() { - trackEvent('newsletter_signup'); + trackEvent("newsletter_signup"); } /* ── Mailchimp helper (inlined; uses jsonp from deps) ── */ @@ -47,33 +47,39 @@ interface SubscriptionResult { } const MAILCHIMP_URL = - 'https://policyengine.us5.list-manage.com/subscribe/post-json?u=e5ad35332666289a0f48013c5&id=71ed1f89d8&f_id=00f173e6f0'; + "https://policyengine.us5.list-manage.com/subscribe/post-json?u=e5ad35332666289a0f48013c5&id=71ed1f89d8&f_id=00f173e6f0"; function submitToMailchimp(email: string): Promise { return new Promise((resolve, reject) => { const encodedEmail = encodeURIComponent(email); - jsonp(`${MAILCHIMP_URL}&EMAIL=${encodedEmail}`, { param: 'c' }, (error, data) => { - if (error) { - reject( - new Error('There was an issue processing your subscription; please try again later.') - ); - return; - } + jsonp( + `${MAILCHIMP_URL}&EMAIL=${encodedEmail}`, + { param: "c" }, + (error, data) => { + if (error) { + reject( + new Error( + "There was an issue processing your subscription; please try again later.", + ), + ); + return; + } - if (data) { - const { msg } = data; - resolve({ - isSuccessful: data.result !== 'error', - message: msg, - }); - } - }); + if (data) { + const { msg } = data; + resolve({ + isSuccessful: data.result !== "error", + message: msg, + }); + } + }, + ); }); } /* ── Constants ── */ -const PolicyEngineLogo = '/assets/logos/policyengine/white.svg'; +const PolicyEngineLogo = "/assets/logos/policyengine/white.svg"; const getContactLinks = (countryId: string) => ({ about: `/${countryId}/team`, @@ -84,57 +90,75 @@ const getContactLinks = (countryId: string) => ({ }); const SOCIAL_LINKS = [ - { icon: IconMail, href: 'mailto:hello@policyengine.org', label: 'Email' }, - { icon: IconBrandTwitter, href: 'https://twitter.com/ThePolicyEngine', label: 'Twitter' }, - { icon: IconBrandFacebook, href: 'https://www.facebook.com/PolicyEngine', label: 'Facebook' }, + { icon: IconMail, href: "mailto:hello@policyengine.org", label: "Email" }, + { + icon: IconBrandTwitter, + href: "https://twitter.com/ThePolicyEngine", + label: "Twitter", + }, + { + icon: IconBrandFacebook, + href: "https://www.facebook.com/PolicyEngine", + label: "Facebook", + }, { icon: IconBrandLinkedin, - href: 'https://www.linkedin.com/company/thepolicyengine', - label: 'LinkedIn', + href: "https://www.linkedin.com/company/thepolicyengine", + label: "LinkedIn", + }, + { + icon: IconBrandYoutube, + href: "https://www.youtube.com/@policyengine", + label: "YouTube", }, - { icon: IconBrandYoutube, href: 'https://www.youtube.com/@policyengine', label: 'YouTube' }, { icon: IconBrandInstagram, - href: 'https://www.instagram.com/PolicyEngine/', - label: 'Instagram', + href: "https://www.instagram.com/PolicyEngine/", + label: "Instagram", + }, + { + icon: IconBrandGithub, + href: "https://github.com/PolicyEngine", + label: "GitHub", }, - { icon: IconBrandGithub, href: 'https://github.com/PolicyEngine', label: 'GitHub' }, ]; /* ── FooterSubscribe ── */ function FooterSubscribe() { - const [email, setEmail] = useState(''); - const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); - const [message, setMessage] = useState(''); + const [email, setEmail] = useState(""); + const [status, setStatus] = useState< + "idle" | "loading" | "success" | "error" + >("idle"); + const [message, setMessage] = useState(""); const handleSubscribe = async () => { if (!email) { - setStatus('error'); - setMessage('Please enter a valid email address.'); + setStatus("error"); + setMessage("Please enter a valid email address."); return; } - setStatus('loading'); - setMessage(''); + setStatus("loading"); + setMessage(""); try { const result = await submitToMailchimp(email); if (result.isSuccessful) { trackNewsletterSignup(); - setStatus('success'); + setStatus("success"); setMessage(result.message); - setEmail(''); + setEmail(""); } else { - setStatus('error'); + setStatus("error"); setMessage(result.message); } } catch (error) { - setStatus('error'); + setStatus("error"); setMessage( error instanceof Error ? error.message - : 'There was an issue processing your subscription; please try again later.' + : "There was an issue processing your subscription; please try again later.", ); } }; @@ -143,37 +167,37 @@ function FooterSubscribe() {

    Subscribe to PolicyEngine

    Get the latest posts delivered right to your inbox.

    - + setEmail(event.currentTarget.value)} - disabled={status === 'loading'} + disabled={status === "loading"} /> {message && (

    {message} @@ -194,7 +218,7 @@ export default function Footer() { data-testid="site-footer" className="tw:w-full" style={{ - padding: `${spacing['4xl']} ${spacing['5xl']}`, + padding: `${spacing["4xl"]} ${spacing["5xl"]}`, background: `linear-gradient(to right, ${colors.primary[800]}, ${colors.primary[600]})`, }} > @@ -206,16 +230,16 @@ export default function Footer() { />

    {[ - { href: CONTACT_LINKS.about, text: 'About us' }, - { href: CONTACT_LINKS.donate, text: 'Donate' }, - { href: CONTACT_LINKS.devTools, text: 'Developer tools' }, - { href: CONTACT_LINKS.privacy, text: 'Privacy policy' }, - { href: CONTACT_LINKS.terms, text: 'Terms and conditions' }, + { href: CONTACT_LINKS.about, text: "About us" }, + { href: CONTACT_LINKS.donate, text: "Donate" }, + { href: CONTACT_LINKS.devTools, text: "Developer tools" }, + { href: CONTACT_LINKS.privacy, text: "Privacy policy" }, + { href: CONTACT_LINKS.terms, text: "Terms and conditions" }, ].map(({ href, text }) => ( ))}

    - © {new Date().getFullYear()} PolicyEngine. All rights reserved. + © {new Date().getFullYear()} PolicyEngine. All rights + reserved.

    diff --git a/website/src/components/blog/blogStyles.ts b/website/src/components/blog/blogStyles.ts index 3b3b702a5..1b4aff346 100644 --- a/website/src/components/blog/blogStyles.ts +++ b/website/src/components/blog/blogStyles.ts @@ -4,11 +4,7 @@ * for markdown rendering in blog posts. */ -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; export const blogColors = { primary: colors.primary[600], diff --git a/website/src/components/home/FeaturedResearchBanner.tsx b/website/src/components/home/FeaturedResearchBanner.tsx index 2c600697f..300eac522 100644 --- a/website/src/components/home/FeaturedResearchBanner.tsx +++ b/website/src/components/home/FeaturedResearchBanner.tsx @@ -2,11 +2,7 @@ import { useState } from "react"; import { IconArrowRight, IconX } from "@tabler/icons-react"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; const BANNER_DISMISSED_KEY = "featured-research-banner-dismissed"; diff --git a/website/src/components/home/HeroCTA.tsx b/website/src/components/home/HeroCTA.tsx index b79475e84..b77cda8ff 100644 --- a/website/src/components/home/HeroCTA.tsx +++ b/website/src/components/home/HeroCTA.tsx @@ -3,11 +3,7 @@ import { motion } from "framer-motion"; const CALCULATOR_URL = process.env.NEXT_PUBLIC_CALCULATOR_URL || "https://app.policyengine.org"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; const ctaVariant = { hidden: { opacity: 0, y: 20 }, diff --git a/website/src/components/home/HeroSection.tsx b/website/src/components/home/HeroSection.tsx index fb56d7b40..4f1ba53ba 100644 --- a/website/src/components/home/HeroSection.tsx +++ b/website/src/components/home/HeroSection.tsx @@ -2,11 +2,7 @@ import { useCallback, useMemo, useRef, useState } from "react"; import { motion, useReducedMotion } from "framer-motion"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; import HeroCTA from "./HeroCTA"; import HouseholdGraph, { generateGraph, diff --git a/website/src/components/home/HomeTrackerPreview.tsx b/website/src/components/home/HomeTrackerPreview.tsx index 030042d37..e63c90158 100644 --- a/website/src/components/home/HomeTrackerPreview.tsx +++ b/website/src/components/home/HomeTrackerPreview.tsx @@ -1,11 +1,7 @@ "use client"; import Link from "next/link"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; export default function HomeTrackerPreview({ countryId, diff --git a/website/src/components/home/OrgLogos.tsx b/website/src/components/home/OrgLogos.tsx index a1b0c7c5b..61657bfcd 100644 --- a/website/src/components/home/OrgLogos.tsx +++ b/website/src/components/home/OrgLogos.tsx @@ -8,11 +8,7 @@ import { getOrgsForCountry, Organization, } from "@/data/organizations"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; const LOGO_WIDTH = 140; const LOGO_GAP = 64; diff --git a/website/src/components/home/TypewriterPrompt.tsx b/website/src/components/home/TypewriterPrompt.tsx index acb71e7bb..731bb705e 100644 --- a/website/src/components/home/TypewriterPrompt.tsx +++ b/website/src/components/home/TypewriterPrompt.tsx @@ -1,11 +1,7 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; export interface PromptData { text: string; diff --git a/website/src/components/static/ActionButton.tsx b/website/src/components/static/ActionButton.tsx index edb8f026c..e4e79313f 100644 --- a/website/src/components/static/ActionButton.tsx +++ b/website/src/components/static/ActionButton.tsx @@ -2,11 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { Text } from "@/components/ui"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; export interface ActionButtonProps { text: string; diff --git a/website/src/components/static/BulletList.tsx b/website/src/components/static/BulletList.tsx index a73251ad9..851322dd0 100644 --- a/website/src/components/static/BulletList.tsx +++ b/website/src/components/static/BulletList.tsx @@ -1,9 +1,5 @@ import { Text } from "@/components/ui"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; export interface BulletItem { title: string; diff --git a/website/src/components/static/CTASection.tsx b/website/src/components/static/CTASection.tsx index a4f3e87fb..c5385d3ea 100644 --- a/website/src/components/static/CTASection.tsx +++ b/website/src/components/static/CTASection.tsx @@ -1,10 +1,6 @@ import { ReactNode } from "react"; import { Container, Title } from "@/components/ui"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; import ActionButton, { ActionButtonProps } from "./ActionButton"; export interface CTASectionProps { @@ -63,9 +59,7 @@ export default function CTASection({ )}
    -
    - {content} -
    +
    {content}
    {title} {sections.map((section, index) => ( -
    +
    {project.title} @@ -73,8 +73,8 @@ export default function SupportedProject({ project }: SupportedProjectProps) { fontFamily: typography.fontFamily.body, }} > - <strong>Award:</strong> {project.currency === 'GBP' ? '\u00a3' : '$'} - {project.amount.toLocaleString('en-US')} + <strong>Award:</strong> {project.currency === "GBP" ? "\u00a3" : "$"} + {project.amount.toLocaleString("en-US")} </span> <span style={{ diff --git a/website/src/components/static/SupporterCard.tsx b/website/src/components/static/SupporterCard.tsx index 8926e5df4..db2c371cc 100644 --- a/website/src/components/static/SupporterCard.tsx +++ b/website/src/components/static/SupporterCard.tsx @@ -1,5 +1,7 @@ -import OptimisedImage from '@/components/ui/OptimisedImage'; -import SupportedProject, { SupportedProject as SupportedProjectType } from './SupportedProject'; +import OptimisedImage from "@/components/ui/OptimisedImage"; +import SupportedProject, { + SupportedProject as SupportedProjectType, +} from "./SupportedProject"; export interface Supporter { id: string; @@ -14,7 +16,10 @@ export interface SupporterCardProps { projects: SupportedProjectType[]; } -export default function SupporterCard({ supporter, projects }: SupporterCardProps) { +export default function SupporterCard({ + supporter, + projects, +}: SupporterCardProps) { return ( <div className="tw:mb-4xl tw:p-2xl tw:border tw:border-border-light tw:rounded-element tw:shadow-[0_2px_4px_rgba(0,0,0,0.05)]"> <div className="tw:flex tw:flex-col tw:sm:flex-row tw:sm:items-center tw:gap-lg tw:mb-2xl"> @@ -34,7 +39,10 @@ export default function SupporterCard({ supporter, projects }: SupporterCardProp </a> ) : ( // Keep the text column aligned with cards that have a logo. - <div aria-hidden="true" className="tw:hidden tw:sm:block tw:shrink-0 tw:w-[200px]" /> + <div + aria-hidden="true" + className="tw:hidden tw:sm:block tw:shrink-0 tw:w-[200px]" + /> )} <p className="tw:flex-1 tw:min-w-0 tw:text-base tw:leading-relaxed"> {supporter.websiteUrl ? ( @@ -48,7 +56,7 @@ export default function SupporterCard({ supporter, projects }: SupporterCardProp </a> ) : ( <span className="tw:font-bold">{supporter.name}</span> - )}{' '} + )}{" "} — {supporter.description} </p> </div> diff --git a/website/src/components/static/TeamMemberCard.tsx b/website/src/components/static/TeamMemberCard.tsx index c54edf1b8..573791a6f 100644 --- a/website/src/components/static/TeamMemberCard.tsx +++ b/website/src/components/static/TeamMemberCard.tsx @@ -1,5 +1,5 @@ -import OptimisedImage from '@/components/ui/OptimisedImage'; -import { cn } from '@/lib/utils'; +import OptimisedImage from "@/components/ui/OptimisedImage"; +import { cn } from "@/lib/utils"; export interface TeamMember { name: string; @@ -9,11 +9,14 @@ export interface TeamMember { export interface TeamMemberCardProps { member: TeamMember; - variant?: 'default' | 'inverted'; + variant?: "default" | "inverted"; } -export default function TeamMemberCard({ member, variant = 'default' }: TeamMemberCardProps) { - const isInverted = variant === 'inverted'; +export default function TeamMemberCard({ + member, + variant = "default", +}: TeamMemberCardProps) { + const isInverted = variant === "inverted"; return ( <div className="tw:grid tw:grid-cols-1 tw:sm:grid-cols-[auto_1fr] tw:items-stretch tw:gap-[5vw] tw:mt-[50px]"> @@ -27,19 +30,22 @@ export default function TeamMemberCard({ member, variant = 'default' }: TeamMemb <div className={cn( - 'tw:h-full tw:pb-[50px] tw:border-b', - isInverted ? 'tw:border-white' : 'tw:border-black' + "tw:h-full tw:pb-[50px] tw:border-b", + isInverted ? "tw:border-white" : "tw:border-black", )} > <p className={cn( - 'tw:text-base tw:leading-relaxed', - isInverted ? 'tw:text-text-inverse' : 'tw:text-text-primary' + "tw:text-base tw:leading-relaxed", + isInverted ? "tw:text-text-inverse" : "tw:text-text-primary", )} > - <span className="tw:font-semibold tw:uppercase" style={{ letterSpacing: '0.05em' }}> + <span + className="tw:font-semibold tw:uppercase" + style={{ letterSpacing: "0.05em" }} + > {member.name} - </span>{' '} + </span>{" "} {member.bio} </p> </div> diff --git a/website/src/components/static/TeamSection.tsx b/website/src/components/static/TeamSection.tsx index a75147257..6a9e1d9e6 100644 --- a/website/src/components/static/TeamSection.tsx +++ b/website/src/components/static/TeamSection.tsx @@ -1,9 +1,5 @@ import { Container, Title } from "@/components/ui"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; import TeamMemberCard, { TeamMember } from "./TeamMemberCard"; export interface TeamSectionProps { diff --git a/website/src/components/ui/Container.tsx b/website/src/components/ui/Container.tsx index 9118de63f..a65c20fb4 100644 --- a/website/src/components/ui/Container.tsx +++ b/website/src/components/ui/Container.tsx @@ -1,28 +1,28 @@ -import React from 'react'; -import { spacing } from '@/designTokens'; -import { cn } from '@/lib/utils'; +import React from "react"; +import { spacing } from "@/designTokens"; +import { cn } from "@/lib/utils"; const sizeMap = { - xs: 'tw:max-w-screen-xs', - sm: 'tw:max-w-screen-sm', - md: 'tw:max-w-screen-md', - lg: 'tw:max-w-screen-lg', - xl: 'tw:max-w-screen-xl', - '2xl': 'tw:max-w-screen-2xl', + xs: "tw:max-w-screen-xs", + sm: "tw:max-w-screen-sm", + md: "tw:max-w-screen-md", + lg: "tw:max-w-screen-lg", + xl: "tw:max-w-screen-xl", + "2xl": "tw:max-w-screen-2xl", } as const; interface ContainerProps extends React.HTMLAttributes<HTMLDivElement> { size?: keyof typeof sizeMap; - variant?: 'guttered'; + variant?: "guttered"; } const Container = React.forwardRef<HTMLDivElement, ContainerProps>( ({ className, size, variant, children, style, ...props }, ref) => { const gutteredStyle = - variant === 'guttered' + variant === "guttered" ? { - paddingLeft: spacing.container['2xl'], - paddingRight: spacing.container['2xl'], + paddingLeft: spacing.container["2xl"], + paddingRight: spacing.container["2xl"], paddingTop: spacing.container.lg, paddingBottom: spacing.container.lg, } @@ -32,10 +32,10 @@ const Container = React.forwardRef<HTMLDivElement, ContainerProps>( <div ref={ref} className={cn( - 'tw:mx-auto tw:w-full', - variant !== 'guttered' && 'tw:px-lg', - size ? sizeMap[size] : 'tw:max-w-[976px]', - className + "tw:mx-auto tw:w-full", + variant !== "guttered" && "tw:px-lg", + size ? sizeMap[size] : "tw:max-w-[976px]", + className, )} style={{ ...gutteredStyle, ...style }} {...props} @@ -43,9 +43,9 @@ const Container = React.forwardRef<HTMLDivElement, ContainerProps>( {children} </div> ); - } + }, ); -Container.displayName = 'Container'; +Container.displayName = "Container"; export { Container }; export type { ContainerProps }; diff --git a/website/src/components/ui/Group.tsx b/website/src/components/ui/Group.tsx index 94770f9df..b6e36c1b8 100644 --- a/website/src/components/ui/Group.tsx +++ b/website/src/components/ui/Group.tsx @@ -1,67 +1,76 @@ -import React from 'react'; -import { cn } from '@/lib/utils'; +import React from "react"; +import { cn } from "@/lib/utils"; const gapMap = { - xs: 'tw:gap-1', - sm: 'tw:gap-2', - md: 'tw:gap-3', - lg: 'tw:gap-4', - xl: 'tw:gap-5', - '2xl': 'tw:gap-6', - '3xl': 'tw:gap-8', - '4xl': 'tw:gap-12', - '5xl': 'tw:gap-16', + xs: "tw:gap-1", + sm: "tw:gap-2", + md: "tw:gap-3", + lg: "tw:gap-4", + xl: "tw:gap-5", + "2xl": "tw:gap-6", + "3xl": "tw:gap-8", + "4xl": "tw:gap-12", + "5xl": "tw:gap-16", } as const; const justifyMap = { - start: 'tw:justify-start', - center: 'tw:justify-center', - end: 'tw:justify-end', - 'space-between': 'tw:justify-between', - apart: 'tw:justify-between', + start: "tw:justify-start", + center: "tw:justify-center", + end: "tw:justify-end", + "space-between": "tw:justify-between", + apart: "tw:justify-between", } as const; const alignMap = { - start: 'tw:items-start', - center: 'tw:items-center', - end: 'tw:items-end', - stretch: 'tw:items-stretch', - baseline: 'tw:items-baseline', + start: "tw:items-start", + center: "tw:items-center", + end: "tw:items-end", + stretch: "tw:items-stretch", + baseline: "tw:items-baseline", } as const; interface GroupProps extends React.HTMLAttributes<HTMLDivElement> { gap?: keyof typeof gapMap; justify?: keyof typeof justifyMap; align?: keyof typeof alignMap; - wrap?: 'wrap' | 'nowrap'; + wrap?: "wrap" | "nowrap"; grow?: boolean; } const Group = React.forwardRef<HTMLDivElement, GroupProps>( ( - { className, gap = 'md', justify, align = 'center', wrap = 'wrap', grow, children, ...props }, - ref + { + className, + gap = "md", + justify, + align = "center", + wrap = "wrap", + grow, + children, + ...props + }, + ref, ) => { return ( <div ref={ref} className={cn( - 'tw:flex tw:flex-row', + "tw:flex tw:flex-row", gapMap[gap], - justifyMap[justify ?? 'start'], + justifyMap[justify ?? "start"], alignMap[align], - wrap === 'wrap' ? 'tw:flex-wrap' : 'tw:flex-nowrap', - grow && 'tw:[&>*]:flex-1', - className + wrap === "wrap" ? "tw:flex-wrap" : "tw:flex-nowrap", + grow && "tw:[&>*]:flex-1", + className, )} {...props} > {children} </div> ); - } + }, ); -Group.displayName = 'Group'; +Group.displayName = "Group"; export { Group }; export type { GroupProps }; diff --git a/website/src/components/ui/Spinner.tsx b/website/src/components/ui/Spinner.tsx index a07d57466..dfefaff8b 100644 --- a/website/src/components/ui/Spinner.tsx +++ b/website/src/components/ui/Spinner.tsx @@ -1,10 +1,10 @@ -import React from 'react'; -import { cn } from '@/lib/utils'; +import React from "react"; +import { cn } from "@/lib/utils"; const sizeMap = { - sm: 'tw:h-4 tw:w-4', - md: 'tw:h-8 tw:w-8', - lg: 'tw:h-12 tw:w-12', + sm: "tw:h-4 tw:w-4", + md: "tw:h-8 tw:w-8", + lg: "tw:h-12 tw:w-12", } as const; interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> { @@ -12,24 +12,24 @@ interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> { } const Spinner = React.forwardRef<HTMLDivElement, SpinnerProps>( - ({ className, size = 'md', ...props }, ref) => { + ({ className, size = "md", ...props }, ref) => { return ( <div ref={ref} role="status" className={cn( - 'tw:animate-spin tw:rounded-full tw:border-2 tw:border-current tw:border-t-transparent tw:text-primary-500', + "tw:animate-spin tw:rounded-full tw:border-2 tw:border-current tw:border-t-transparent tw:text-primary-500", sizeMap[size], - className + className, )} {...props} > <span className="tw:sr-only">Loading...</span> </div> ); - } + }, ); -Spinner.displayName = 'Spinner'; +Spinner.displayName = "Spinner"; export { Spinner }; export type { SpinnerProps }; diff --git a/website/src/components/ui/Stack.tsx b/website/src/components/ui/Stack.tsx index bf8b5f013..47b75d17d 100644 --- a/website/src/components/ui/Stack.tsx +++ b/website/src/components/ui/Stack.tsx @@ -1,31 +1,31 @@ -import React from 'react'; -import { cn } from '@/lib/utils'; +import React from "react"; +import { cn } from "@/lib/utils"; const gapMap = { - xs: 'tw:gap-1', - sm: 'tw:gap-2', - md: 'tw:gap-3', - lg: 'tw:gap-4', - xl: 'tw:gap-5', - '2xl': 'tw:gap-6', - '3xl': 'tw:gap-8', - '4xl': 'tw:gap-12', - '5xl': 'tw:gap-16', + xs: "tw:gap-1", + sm: "tw:gap-2", + md: "tw:gap-3", + lg: "tw:gap-4", + xl: "tw:gap-5", + "2xl": "tw:gap-6", + "3xl": "tw:gap-8", + "4xl": "tw:gap-12", + "5xl": "tw:gap-16", } as const; const alignMap = { - start: 'tw:items-start', - center: 'tw:items-center', - end: 'tw:items-end', - stretch: 'tw:items-stretch', + start: "tw:items-start", + center: "tw:items-center", + end: "tw:items-end", + stretch: "tw:items-stretch", } as const; const justifyMap = { - start: 'tw:justify-start', - center: 'tw:justify-center', - end: 'tw:justify-end', - 'space-between': 'tw:justify-between', - 'space-around': 'tw:justify-around', + start: "tw:justify-start", + center: "tw:justify-center", + end: "tw:justify-end", + "space-between": "tw:justify-between", + "space-around": "tw:justify-around", } as const; interface StackProps extends React.HTMLAttributes<HTMLDivElement> { @@ -35,25 +35,25 @@ interface StackProps extends React.HTMLAttributes<HTMLDivElement> { } const Stack = React.forwardRef<HTMLDivElement, StackProps>( - ({ className, gap = 'md', align, justify, children, ...props }, ref) => { + ({ className, gap = "md", align, justify, children, ...props }, ref) => { return ( <div ref={ref} className={cn( - 'tw:flex tw:flex-col', + "tw:flex tw:flex-col", gapMap[gap], align && alignMap[align], justify && justifyMap[justify], - className + className, )} {...props} > {children} </div> ); - } + }, ); -Stack.displayName = 'Stack'; +Stack.displayName = "Stack"; export { Stack }; export type { StackProps }; diff --git a/website/src/components/ui/Text.tsx b/website/src/components/ui/Text.tsx index 60b2ca88d..382d82ee9 100644 --- a/website/src/components/ui/Text.tsx +++ b/website/src/components/ui/Text.tsx @@ -1,34 +1,35 @@ -import React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from '@/lib/utils'; +import React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; // Mantine color aliases — "dimmed" was the most common const COLOR_ALIASES: Record<string, string> = { - dimmed: '#9CA3AF', + dimmed: "#9CA3AF", }; -const textVariants = cva('', { +const textVariants = cva("", { variants: { size: { - xs: 'tw:text-xs', - sm: 'tw:text-sm', - md: 'tw:text-base', - lg: 'tw:text-lg', - xl: 'tw:text-xl', + xs: "tw:text-xs", + sm: "tw:text-sm", + md: "tw:text-base", + lg: "tw:text-lg", + xl: "tw:text-xl", }, weight: { - normal: 'tw:font-normal', - medium: 'tw:font-medium', - semibold: 'tw:font-semibold', - bold: 'tw:font-bold', + normal: "tw:font-normal", + medium: "tw:font-medium", + semibold: "tw:font-semibold", + bold: "tw:font-bold", }, }, defaultVariants: { - size: 'md', + size: "md", }, }); -interface TextProps extends React.HTMLAttributes<HTMLElement>, VariantProps<typeof textVariants> { +interface TextProps + extends React.HTMLAttributes<HTMLElement>, VariantProps<typeof textVariants> { span?: boolean; fw?: number; c?: string; @@ -36,8 +37,22 @@ interface TextProps extends React.HTMLAttributes<HTMLElement>, VariantProps<type } const Text = React.forwardRef<HTMLElement, TextProps>( - ({ className, size, weight, span, fw, c, component, style, children, ...props }, ref) => { - const Component = component || (span ? 'span' : 'p'); + ( + { + className, + size, + weight, + span, + fw, + c, + component, + style, + children, + ...props + }, + ref, + ) => { + const Component = component || (span ? "span" : "p"); const fontWeightStyle = fw ? { fontWeight: fw } : undefined; const colorStyle = c ? { color: COLOR_ALIASES[c] || c } : undefined; @@ -51,9 +66,9 @@ const Text = React.forwardRef<HTMLElement, TextProps>( {children} </Component> ); - } + }, ); -Text.displayName = 'Text'; +Text.displayName = "Text"; export { Text, textVariants }; export type { TextProps }; diff --git a/website/src/components/ui/Title.tsx b/website/src/components/ui/Title.tsx index 93ef0a43f..60bdbd37e 100644 --- a/website/src/components/ui/Title.tsx +++ b/website/src/components/ui/Title.tsx @@ -1,13 +1,13 @@ -import React from 'react'; -import { cn } from '@/lib/utils'; +import React from "react"; +import { cn } from "@/lib/utils"; const orderStyles = { - 1: 'tw:text-4xl tw:font-bold', - 2: 'tw:text-3xl tw:font-bold', - 3: 'tw:text-2xl tw:font-semibold', - 4: 'tw:text-xl tw:font-semibold', - 5: 'tw:text-lg tw:font-medium', - 6: 'tw:text-base tw:font-medium', + 1: "tw:text-4xl tw:font-bold", + 2: "tw:text-3xl tw:font-bold", + 3: "tw:text-2xl tw:font-semibold", + 4: "tw:text-xl tw:font-semibold", + 5: "tw:text-lg tw:font-medium", + 6: "tw:text-base tw:font-medium", } as const; interface TitleProps extends React.HTMLAttributes<HTMLHeadingElement> { @@ -27,9 +27,9 @@ const Title = React.forwardRef<HTMLHeadingElement, TitleProps>( {children} </Component> ); - } + }, ); -Title.displayName = 'Title'; +Title.displayName = "Title"; export { Title }; export type { TitleProps }; diff --git a/website/src/components/ui/accordion.tsx b/website/src/components/ui/accordion.tsx index a7211fa51..674fca83a 100644 --- a/website/src/components/ui/accordion.tsx +++ b/website/src/components/ui/accordion.tsx @@ -1,11 +1,13 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { ChevronDownIcon } from 'lucide-react'; -import { Accordion as AccordionPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { ChevronDownIcon } from "lucide-react"; +import { Accordion as AccordionPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; -function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) { +function Accordion({ + ...props +}: React.ComponentProps<typeof AccordionPrimitive.Root>) { return <AccordionPrimitive.Root data-slot="accordion" {...props} />; } @@ -16,7 +18,7 @@ function AccordionItem({ return ( <AccordionPrimitive.Item data-slot="accordion-item" - className={cn('tw:border-b tw:last:border-b-0', className)} + className={cn("tw:border-b tw:last:border-b-0", className)} {...props} /> ); @@ -32,8 +34,8 @@ function AccordionTrigger({ <AccordionPrimitive.Trigger data-slot="accordion-trigger" className={cn( - 'tw:bg-transparent tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:flex tw:flex-1 tw:items-start tw:justify-between tw:gap-4 tw:rounded-md tw:py-4 tw:text-left tw:text-sm tw:font-medium tw:transition-all tw:outline-none tw:hover:underline tw:focus-visible:ring-[3px] tw:disabled:pointer-events-none tw:disabled:opacity-50 tw:[&[data-state=open]>svg]:rotate-180', - className + "tw:bg-transparent tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:flex tw:flex-1 tw:items-start tw:justify-between tw:gap-4 tw:rounded-md tw:py-4 tw:text-left tw:text-sm tw:font-medium tw:transition-all tw:outline-none tw:hover:underline tw:focus-visible:ring-[3px] tw:disabled:pointer-events-none tw:disabled:opacity-50 tw:[&[data-state=open]>svg]:rotate-180", + className, )} {...props} > @@ -55,7 +57,7 @@ function AccordionContent({ className="tw:data-[state=closed]:animate-accordion-up tw:data-[state=open]:animate-accordion-down tw:overflow-hidden tw:text-sm" {...props} > - <div className={cn('tw:pt-0 tw:pb-4', className)}>{children}</div> + <div className={cn("tw:pt-0 tw:pb-4", className)}>{children}</div> </AccordionPrimitive.Content> ); } diff --git a/website/src/components/ui/alert.tsx b/website/src/components/ui/alert.tsx index 426f5968b..0ffafa51a 100644 --- a/website/src/components/ui/alert.tsx +++ b/website/src/components/ui/alert.tsx @@ -1,28 +1,28 @@ -import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; const alertVariants = cva( - 'tw:relative tw:w-full tw:rounded-lg tw:border tw:px-4 tw:py-3 tw:text-sm tw:grid tw:has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] tw:grid-cols-[0_1fr] tw:has-[>svg]:gap-x-3 tw:gap-y-0.5 tw:items-start tw:[&>svg]:size-4 tw:[&>svg]:translate-y-0.5 tw:[&>svg]:text-current', + "tw:relative tw:w-full tw:rounded-lg tw:border tw:px-4 tw:py-3 tw:text-sm tw:grid tw:has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] tw:grid-cols-[0_1fr] tw:has-[>svg]:gap-x-3 tw:gap-y-0.5 tw:items-start tw:[&>svg]:size-4 tw:[&>svg]:translate-y-0.5 tw:[&>svg]:text-current", { variants: { variant: { - default: 'tw:bg-card tw:text-card-foreground', + default: "tw:bg-card tw:text-card-foreground", destructive: - 'tw:text-destructive tw:bg-card tw:[&>svg]:text-current tw:*:data-[slot=alert-description]:text-destructive/90', + "tw:text-destructive tw:bg-card tw:[&>svg]:text-current tw:*:data-[slot=alert-description]:text-destructive/90", }, }, defaultVariants: { - variant: 'default', + variant: "default", }, - } + }, ); function Alert({ className, variant, ...props -}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) { +}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) { return ( <div data-slot="alert" @@ -33,26 +33,29 @@ function Alert({ ); } -function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="alert-title" className={cn( - 'tw:col-start-2 tw:line-clamp-1 tw:min-h-4 tw:font-medium tw:tracking-tight', - className + "tw:col-start-2 tw:line-clamp-1 tw:min-h-4 tw:font-medium tw:tracking-tight", + className, )} {...props} /> ); } -function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) { +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { return ( <div data-slot="alert-description" className={cn( - 'tw:text-muted-foreground tw:col-start-2 tw:grid tw:justify-items-start tw:gap-1 tw:text-sm tw:[&_p]:leading-relaxed', - className + "tw:text-muted-foreground tw:col-start-2 tw:grid tw:justify-items-start tw:gap-1 tw:text-sm tw:[&_p]:leading-relaxed", + className, )} {...props} /> diff --git a/website/src/components/ui/badge.tsx b/website/src/components/ui/badge.tsx index c40a3b7c0..c75ed981f 100644 --- a/website/src/components/ui/badge.tsx +++ b/website/src/components/ui/badge.tsx @@ -1,37 +1,40 @@ -import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { Slot } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; +import { cn } from "@/lib/utils"; const badgeVariants = cva( - 'tw:inline-flex tw:items-center tw:justify-center tw:rounded-full tw:border tw:border-transparent tw:px-2 tw:py-0.5 tw:text-xs tw:font-medium tw:w-fit tw:whitespace-nowrap tw:shrink-0 tw:[&>svg]:size-3 tw:gap-1 tw:[&>svg]:pointer-events-none tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:focus-visible:ring-[3px] tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive tw:transition-[color,box-shadow] tw:overflow-hidden', + "tw:inline-flex tw:items-center tw:justify-center tw:rounded-full tw:border tw:border-transparent tw:px-2 tw:py-0.5 tw:text-xs tw:font-medium tw:w-fit tw:whitespace-nowrap tw:shrink-0 tw:[&>svg]:size-3 tw:gap-1 tw:[&>svg]:pointer-events-none tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:focus-visible:ring-[3px] tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive tw:transition-[color,box-shadow] tw:overflow-hidden", { variants: { variant: { - default: 'tw:bg-primary tw:text-primary-foreground tw:[a&]:hover:bg-primary/90', - secondary: 'tw:bg-secondary tw:text-secondary-foreground tw:[a&]:hover:bg-secondary/90', + default: + "tw:bg-primary tw:text-primary-foreground tw:[a&]:hover:bg-primary/90", + secondary: + "tw:bg-secondary tw:text-secondary-foreground tw:[a&]:hover:bg-secondary/90", destructive: - 'tw:bg-destructive tw:text-white tw:[a&]:hover:bg-destructive/90 tw:focus-visible:ring-destructive/20', + "tw:bg-destructive tw:text-white tw:[a&]:hover:bg-destructive/90 tw:focus-visible:ring-destructive/20", outline: - 'tw:border-border tw:text-foreground tw:[a&]:hover:bg-accent tw:[a&]:hover:text-accent-foreground', - ghost: 'tw:[a&]:hover:bg-accent tw:[a&]:hover:text-accent-foreground', - link: 'tw:text-primary tw:underline-offset-4 tw:[a&]:hover:underline', + "tw:border-border tw:text-foreground tw:[a&]:hover:bg-accent tw:[a&]:hover:text-accent-foreground", + ghost: "tw:[a&]:hover:bg-accent tw:[a&]:hover:text-accent-foreground", + link: "tw:text-primary tw:underline-offset-4 tw:[a&]:hover:underline", }, }, defaultVariants: { - variant: 'default', + variant: "default", }, - } + }, ); function Badge({ className, - variant = 'default', + variant = "default", asChild = false, ...props -}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) { +}: React.ComponentProps<"span"> & + VariantProps<typeof badgeVariants> & { asChild?: boolean }) { // Cast needed: dual csstype versions in monorepo cause SlotProps style mismatch - const Comp = (asChild ? Slot.Root : 'span') as React.ElementType; + const Comp = (asChild ? Slot.Root : "span") as React.ElementType; return ( <Comp diff --git a/website/src/components/ui/button.tsx b/website/src/components/ui/button.tsx index 0c41f7795..437fc2d00 100644 --- a/website/src/components/ui/button.tsx +++ b/website/src/components/ui/button.tsx @@ -1,52 +1,56 @@ -import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { Slot } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; +import { cn } from "@/lib/utils"; const buttonVariants = cva( - 'tw:inline-flex tw:items-center tw:justify-center tw:gap-2 tw:whitespace-nowrap tw:rounded-md tw:text-sm tw:font-medium tw:cursor-pointer tw:transition-all tw:disabled:pointer-events-none tw:disabled:opacity-50 tw:[&_svg]:pointer-events-none tw:[&_svg:not([class*=size-])]:size-4 tw:shrink-0 tw:[&_svg]:shrink-0 tw:outline-none tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:focus-visible:ring-[3px] tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive', + "tw:inline-flex tw:items-center tw:justify-center tw:gap-2 tw:whitespace-nowrap tw:rounded-md tw:text-sm tw:font-medium tw:cursor-pointer tw:transition-all tw:disabled:pointer-events-none tw:disabled:opacity-50 tw:[&_svg]:pointer-events-none tw:[&_svg:not([class*=size-])]:size-4 tw:shrink-0 tw:[&_svg]:shrink-0 tw:outline-none tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:focus-visible:ring-[3px] tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive", { variants: { variant: { - default: 'tw:bg-primary tw:text-primary-foreground tw:hover:bg-primary/90', + default: + "tw:bg-primary tw:text-primary-foreground tw:hover:bg-primary/90", destructive: - 'tw:bg-destructive tw:text-white tw:hover:bg-destructive/90 tw:focus-visible:ring-destructive/20', + "tw:bg-destructive tw:text-white tw:hover:bg-destructive/90 tw:focus-visible:ring-destructive/20", outline: - 'tw:border tw:bg-background tw:shadow-xs tw:hover:bg-accent tw:hover:text-accent-foreground', - secondary: 'tw:bg-secondary tw:text-secondary-foreground tw:hover:bg-secondary/80', - ghost: 'tw:bg-transparent tw:hover:bg-accent tw:hover:text-accent-foreground', - link: 'tw:bg-transparent tw:text-primary tw:underline-offset-4 tw:hover:underline', + "tw:border tw:bg-background tw:shadow-xs tw:hover:bg-accent tw:hover:text-accent-foreground", + secondary: + "tw:bg-secondary tw:text-secondary-foreground tw:hover:bg-secondary/80", + ghost: + "tw:bg-transparent tw:hover:bg-accent tw:hover:text-accent-foreground", + link: "tw:bg-transparent tw:text-primary tw:underline-offset-4 tw:hover:underline", }, size: { - default: 'tw:h-9 tw:px-4 tw:py-2 tw:has-[>svg]:px-3', - xs: 'tw:h-6 tw:gap-1 tw:rounded-md tw:px-2 tw:text-xs tw:has-[>svg]:px-1.5 tw:[&_svg:not([class*=size-])]:size-3', - sm: 'tw:h-8 tw:rounded-md tw:gap-1.5 tw:px-3 tw:has-[>svg]:px-2.5', - lg: 'tw:h-10 tw:rounded-md tw:px-6 tw:has-[>svg]:px-4', - icon: 'tw:size-9', - 'icon-xs': 'tw:size-6 tw:rounded-md tw:[&_svg:not([class*=size-])]:size-3', - 'icon-sm': 'tw:size-8', - 'icon-lg': 'tw:size-10', + default: "tw:h-9 tw:px-4 tw:py-2 tw:has-[>svg]:px-3", + xs: "tw:h-6 tw:gap-1 tw:rounded-md tw:px-2 tw:text-xs tw:has-[>svg]:px-1.5 tw:[&_svg:not([class*=size-])]:size-3", + sm: "tw:h-8 tw:rounded-md tw:gap-1.5 tw:px-3 tw:has-[>svg]:px-2.5", + lg: "tw:h-10 tw:rounded-md tw:px-6 tw:has-[>svg]:px-4", + icon: "tw:size-9", + "icon-xs": + "tw:size-6 tw:rounded-md tw:[&_svg:not([class*=size-])]:size-3", + "icon-sm": "tw:size-8", + "icon-lg": "tw:size-10", }, }, defaultVariants: { - variant: 'default', - size: 'default', + variant: "default", + size: "default", }, - } + }, ); function Button({ className, - variant = 'default', - size = 'default', + variant = "default", + size = "default", asChild = false, ...props -}: React.ComponentProps<'button'> & +}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean; }) { // Cast needed: dual csstype versions in monorepo cause SlotProps style mismatch - const Comp = (asChild ? Slot.Root : 'button') as React.ElementType; + const Comp = (asChild ? Slot.Root : "button") as React.ElementType; return ( <Comp diff --git a/website/src/components/ui/card.tsx b/website/src/components/ui/card.tsx index e4ba307ea..8c0c64928 100644 --- a/website/src/components/ui/card.tsx +++ b/website/src/components/ui/card.tsx @@ -1,77 +1,94 @@ -import * as React from 'react'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { cn } from "@/lib/utils"; -function Card({ className, ...props }: React.ComponentProps<'div'>) { +function Card({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card" className={cn( - 'tw:bg-card tw:text-card-foreground tw:flex tw:flex-col tw:gap-6 tw:rounded-xl tw:border tw:py-6 tw:shadow-sm', - className + "tw:bg-card tw:text-card-foreground tw:flex tw:flex-col tw:gap-6 tw:rounded-xl tw:border tw:py-6 tw:shadow-sm", + className, )} {...props} /> ); } -function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-header" className={cn( - 'tw:@container/card-header tw:grid tw:auto-rows-min tw:grid-rows-[auto_auto] tw:items-start tw:gap-2 tw:px-6 tw:has-data-[slot=card-action]:grid-cols-[1fr_auto] tw:[.border-b]:pb-6', - className + "tw:@container/card-header tw:grid tw:auto-rows-min tw:grid-rows-[auto_auto] tw:items-start tw:gap-2 tw:px-6 tw:has-data-[slot=card-action]:grid-cols-[1fr_auto] tw:[.border-b]:pb-6", + className, )} {...props} /> ); } -function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-title" - className={cn('tw:leading-none tw:font-semibold', className)} + className={cn("tw:leading-none tw:font-semibold", className)} {...props} /> ); } -function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-description" - className={cn('tw:text-muted-foreground tw:text-sm', className)} + className={cn("tw:text-muted-foreground tw:text-sm", className)} {...props} /> ); } -function CardAction({ className, ...props }: React.ComponentProps<'div'>) { +function CardAction({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-action" className={cn( - 'tw:col-start-2 tw:row-span-2 tw:row-start-1 tw:self-start tw:justify-self-end', - className + "tw:col-start-2 tw:row-span-2 tw:row-start-1 tw:self-start tw:justify-self-end", + className, )} {...props} /> ); } -function CardContent({ className, ...props }: React.ComponentProps<'div'>) { - return <div data-slot="card-content" className={cn('tw:px-6', className)} {...props} />; +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-content" + className={cn("tw:px-6", className)} + {...props} + /> + ); } -function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-footer" - className={cn('tw:flex tw:items-center tw:px-6 tw:[.border-t]:pt-6', className)} + className={cn( + "tw:flex tw:items-center tw:px-6 tw:[.border-t]:pt-6", + className, + )} {...props} /> ); } -export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}; diff --git a/website/src/components/ui/checkbox.tsx b/website/src/components/ui/checkbox.tsx index 20452f814..8c8d318da 100644 --- a/website/src/components/ui/checkbox.tsx +++ b/website/src/components/ui/checkbox.tsx @@ -1,15 +1,18 @@ -import * as React from 'react'; -import { CheckIcon } from 'lucide-react'; -import { Checkbox as CheckboxPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { CheckIcon } from "lucide-react"; +import { Checkbox as CheckboxPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; -function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) { +function Checkbox({ + className, + ...props +}: React.ComponentProps<typeof CheckboxPrimitive.Root>) { return ( <CheckboxPrimitive.Root data-slot="checkbox" className={cn( - 'tw:peer tw:border-input tw:data-[state=checked]:bg-primary tw:data-[state=checked]:text-primary-foreground tw:data-[state=checked]:border-primary tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive tw:size-4 tw:shrink-0 tw:rounded-[4px] tw:border tw:shadow-xs tw:transition-shadow tw:outline-none tw:focus-visible:ring-[3px] tw:disabled:cursor-not-allowed tw:disabled:opacity-50', - className + "tw:peer tw:border-input tw:data-[state=checked]:bg-primary tw:data-[state=checked]:text-primary-foreground tw:data-[state=checked]:border-primary tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive tw:size-4 tw:shrink-0 tw:rounded-[4px] tw:border tw:shadow-xs tw:transition-shadow tw:outline-none tw:focus-visible:ring-[3px] tw:disabled:cursor-not-allowed tw:disabled:opacity-50", + className, )} {...props} > diff --git a/website/src/components/ui/collapsible.tsx b/website/src/components/ui/collapsible.tsx index f68af59ff..d0d49dc48 100644 --- a/website/src/components/ui/collapsible.tsx +++ b/website/src/components/ui/collapsible.tsx @@ -1,19 +1,25 @@ -import { Collapsible as CollapsiblePrimitive } from 'radix-ui'; +import { Collapsible as CollapsiblePrimitive } from "radix-ui"; -function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { +function Collapsible({ + ...props +}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />; } function CollapsibleTrigger({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Trigger>) { - return <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />; + return ( + <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} /> + ); } function CollapsibleContent({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Content>) { - return <CollapsiblePrimitive.Content data-slot="collapsible-content" {...props} />; + return ( + <CollapsiblePrimitive.Content data-slot="collapsible-content" {...props} /> + ); } export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/website/src/components/ui/command.tsx b/website/src/components/ui/command.tsx index 5233a3cb1..33be61b2e 100644 --- a/website/src/components/ui/command.tsx +++ b/website/src/components/ui/command.tsx @@ -1,22 +1,25 @@ -import * as React from 'react'; -import { Command as CommandPrimitive } from 'cmdk'; -import { SearchIcon } from 'lucide-react'; +import * as React from "react"; +import { Command as CommandPrimitive } from "cmdk"; +import { SearchIcon } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, -} from '@/components/ui/dialog'; -import { cn } from '@/lib/utils'; +} from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; -function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) { +function Command({ + className, + ...props +}: React.ComponentProps<typeof CommandPrimitive>) { return ( <CommandPrimitive data-slot="command" className={cn( - 'tw:bg-popover tw:text-popover-foreground tw:flex tw:h-full tw:w-full tw:flex-col tw:overflow-hidden tw:rounded-md', - className + "tw:bg-popover tw:text-popover-foreground tw:flex tw:h-full tw:w-full tw:flex-col tw:overflow-hidden tw:rounded-md", + className, )} {...props} /> @@ -24,8 +27,8 @@ function Command({ className, ...props }: React.ComponentProps<typeof CommandPri } function CommandDialog({ - title = 'Command palette', - description = 'Search for a command to run...', + title = "Command palette", + description = "Search for a command to run...", children, className, showCloseButton = true, @@ -43,7 +46,7 @@ function CommandDialog({ <DialogDescription>{description}</DialogDescription> </DialogHeader> <DialogContent - className={cn('tw:overflow-hidden tw:p-0', className)} + className={cn("tw:overflow-hidden tw:p-0", className)} showCloseButton={showCloseButton} > <Command className="tw:[&_[cmdk-group-heading]]:text-muted-foreground tw:**:data-[slot=command-input-wrapper]:h-12 tw:[&_[cmdk-group-heading]]:px-2 tw:[&_[cmdk-group-heading]]:font-medium tw:[&_[cmdk-group]]:px-2 tw:[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 tw:[&_[cmdk-input-wrapper]_svg]:h-5 tw:[&_[cmdk-input-wrapper]_svg]:w-5 tw:[&_[cmdk-input]]:h-12 tw:[&_[cmdk-item]]:px-2 tw:[&_[cmdk-item]]:py-3 tw:[&_[cmdk-item]_svg]:h-5 tw:[&_[cmdk-item]_svg]:w-5"> @@ -67,8 +70,8 @@ function CommandInput({ <CommandPrimitive.Input data-slot="command-input" className={cn( - 'tw:placeholder:text-muted-foreground tw:flex tw:h-10 tw:w-full tw:rounded-md tw:bg-transparent tw:py-3 tw:text-sm tw:outline-hidden tw:disabled:cursor-not-allowed tw:disabled:opacity-50', - className + "tw:placeholder:text-muted-foreground tw:flex tw:h-10 tw:w-full tw:rounded-md tw:bg-transparent tw:py-3 tw:text-sm tw:outline-hidden tw:disabled:cursor-not-allowed tw:disabled:opacity-50", + className, )} {...props} /> @@ -76,20 +79,25 @@ function CommandInput({ ); } -function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) { +function CommandList({ + className, + ...props +}: React.ComponentProps<typeof CommandPrimitive.List>) { return ( <CommandPrimitive.List data-slot="command-list" className={cn( - 'tw:max-h-[300px] tw:scroll-py-1 tw:overflow-x-hidden tw:overflow-y-auto', - className + "tw:max-h-[300px] tw:scroll-py-1 tw:overflow-x-hidden tw:overflow-y-auto", + className, )} {...props} /> ); } -function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) { +function CommandEmpty({ + ...props +}: React.ComponentProps<typeof CommandPrimitive.Empty>) { return ( <CommandPrimitive.Empty data-slot="command-empty" @@ -107,8 +115,8 @@ function CommandGroup({ <CommandPrimitive.Group data-slot="command-group" className={cn( - 'tw:text-foreground tw:[&_[cmdk-group-heading]]:text-muted-foreground tw:overflow-hidden tw:p-1 tw:[&_[cmdk-group-heading]]:px-2 tw:[&_[cmdk-group-heading]]:py-1.5 tw:[&_[cmdk-group-heading]]:text-xs tw:[&_[cmdk-group-heading]]:font-medium', - className + "tw:text-foreground tw:[&_[cmdk-group-heading]]:text-muted-foreground tw:overflow-hidden tw:p-1 tw:[&_[cmdk-group-heading]]:px-2 tw:[&_[cmdk-group-heading]]:py-1.5 tw:[&_[cmdk-group-heading]]:text-xs tw:[&_[cmdk-group-heading]]:font-medium", + className, )} {...props} /> @@ -122,30 +130,39 @@ function CommandSeparator({ return ( <CommandPrimitive.Separator data-slot="command-separator" - className={cn('tw:bg-border tw:-mx-1 tw:h-px', className)} + className={cn("tw:bg-border tw:-mx-1 tw:h-px", className)} {...props} /> ); } -function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) { +function CommandItem({ + className, + ...props +}: React.ComponentProps<typeof CommandPrimitive.Item>) { return ( <CommandPrimitive.Item data-slot="command-item" className={cn( - 'tw:data-[selected=true]:bg-accent tw:data-[selected=true]:text-accent-foreground tw:[&_svg:not([class*=text-])]:text-muted-foreground tw:relative tw:flex tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:px-2 tw:py-1.5 tw:text-sm tw:outline-hidden tw:select-none tw:data-[disabled=true]:pointer-events-none tw:data-[disabled=true]:opacity-50 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4', - className + "tw:data-[selected=true]:bg-accent tw:data-[selected=true]:text-accent-foreground tw:[&_svg:not([class*=text-])]:text-muted-foreground tw:relative tw:flex tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:px-2 tw:py-1.5 tw:text-sm tw:outline-hidden tw:select-none tw:data-[disabled=true]:pointer-events-none tw:data-[disabled=true]:opacity-50 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4", + className, )} {...props} /> ); } -function CommandShortcut({ className, ...props }: React.ComponentProps<'span'>) { +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { return ( <span data-slot="command-shortcut" - className={cn('tw:text-muted-foreground tw:ml-auto tw:text-xs tw:tracking-widest', className)} + className={cn( + "tw:text-muted-foreground tw:ml-auto tw:text-xs tw:tracking-widest", + className, + )} {...props} /> ); diff --git a/website/src/components/ui/date-picker.story.tsx b/website/src/components/ui/date-picker.story.tsx index 1d7cf06e8..4dc6564d7 100644 --- a/website/src/components/ui/date-picker.story.tsx +++ b/website/src/components/ui/date-picker.story.tsx @@ -1,10 +1,10 @@ "use client"; -import { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; -import { DatePicker } from './date-picker'; +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { DatePicker } from "./date-picker"; const meta: Meta<typeof DatePicker> = { - title: 'UI/DatePicker', + title: "UI/DatePicker", component: DatePicker, decorators: [ (Story) => ( diff --git a/website/src/components/ui/date-picker.tsx b/website/src/components/ui/date-picker.tsx index 1805644e9..14a380652 100644 --- a/website/src/components/ui/date-picker.tsx +++ b/website/src/components/ui/date-picker.tsx @@ -1,11 +1,11 @@ "use client"; -import dayjs from 'dayjs'; -import * as React from 'react'; -import { useState } from 'react'; -import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'; -import { cn } from '@/lib/utils'; -import { Button } from './button'; -import { Popover, PopoverContent, PopoverTrigger } from './popover'; +import dayjs from "dayjs"; +import * as React from "react"; +import { useState } from "react"; +import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react"; +import { cn } from "@/lib/utils"; +import { Button } from "./button"; +import { Popover, PopoverContent, PopoverTrigger } from "./popover"; interface DatePickerProps { value: Date | null | undefined; @@ -17,40 +17,40 @@ interface DatePickerProps { className?: string; } -const DAYS_OF_WEEK = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; +const DAYS_OF_WEEK = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; function DatePicker({ value, onChange, minDate, maxDate, - placeholder = 'Select date', - displayFormat = 'MMM. D, YYYY', + placeholder = "Select date", + displayFormat = "MMM. D, YYYY", className, }: DatePickerProps) { const [open, setOpen] = useState(false); const initialMonth = value ? dayjs(value) : dayjs(); - const [viewMonth, setViewMonth] = useState(initialMonth.startOf('month')); + const [viewMonth, setViewMonth] = useState(initialMonth.startOf("month")); const displayText = value ? dayjs(value).format(displayFormat) : placeholder; // Generate calendar days for the current view month - const startOfMonth = viewMonth.startOf('month'); - const endOfMonth = viewMonth.endOf('month'); + const startOfMonth = viewMonth.startOf("month"); + const endOfMonth = viewMonth.endOf("month"); const startDay = startOfMonth.day(); // 0 = Sunday const daysInMonth = viewMonth.daysInMonth(); // Build 6 rows x 7 cols grid const calendarDays: (dayjs.Dayjs | null)[] = []; for (let i = 0; i < startDay; i++) { - calendarDays.push(startOfMonth.subtract(startDay - i, 'day')); + calendarDays.push(startOfMonth.subtract(startDay - i, "day")); } for (let i = 1; i <= daysInMonth; i++) { calendarDays.push(viewMonth.date(i)); } const remaining = 42 - calendarDays.length; for (let i = 1; i <= remaining; i++) { - calendarDays.push(endOfMonth.add(i, 'day')); + calendarDays.push(endOfMonth.add(i, "day")); } const handleSelect = (day: dayjs.Dayjs) => { @@ -59,10 +59,10 @@ function DatePicker({ }; const isDateDisabled = (day: dayjs.Dayjs) => { - if (minDate && day.isBefore(dayjs(minDate), 'day')) { + if (minDate && day.isBefore(dayjs(minDate), "day")) { return true; } - if (maxDate && day.isAfter(dayjs(maxDate), 'day')) { + if (maxDate && day.isAfter(dayjs(maxDate), "day")) { return true; } return false; @@ -74,9 +74,9 @@ function DatePicker({ <button type="button" className={cn( - 'tw:border-input tw:flex tw:h-9 tw:w-full tw:items-center tw:justify-between tw:rounded-md tw:border tw:px-3 tw:py-2 tw:text-sm tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none', - !value && 'tw:text-muted-foreground', - className + "tw:border-input tw:flex tw:h-9 tw:w-full tw:items-center tw:justify-between tw:rounded-md tw:border tw:px-3 tw:py-2 tw:text-sm tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none", + !value && "tw:text-muted-foreground", + className, )} > {displayText} @@ -89,15 +89,17 @@ function DatePicker({ <Button variant="ghost" size="icon-xs" - onClick={() => setViewMonth((m) => m.subtract(1, 'month'))} + onClick={() => setViewMonth((m) => m.subtract(1, "month"))} > <IconChevronLeft size={14} /> </Button> - <span className="tw:text-sm tw:font-medium">{viewMonth.format('MMMM YYYY')}</span> + <span className="tw:text-sm tw:font-medium"> + {viewMonth.format("MMMM YYYY")} + </span> <Button variant="ghost" size="icon-xs" - onClick={() => setViewMonth((m) => m.add(1, 'month'))} + onClick={() => setViewMonth((m) => m.add(1, "month"))} > <IconChevronRight size={14} /> </Button> @@ -120,9 +122,9 @@ function DatePicker({ return <div key={i} />; } const isCurrentMonth = day.month() === viewMonth.month(); - const isSelected = value && day.isSame(dayjs(value), 'day'); + const isSelected = value && day.isSame(dayjs(value), "day"); const isDisabled = isDateDisabled(day); - const isToday = day.isSame(dayjs(), 'day'); + const isToday = day.isSame(dayjs(), "day"); return ( <button @@ -131,13 +133,13 @@ function DatePicker({ disabled={isDisabled} onClick={() => handleSelect(day)} className={cn( - 'tw:h-8 tw:w-full tw:rounded-md tw:text-sm tw:transition-colors', + "tw:h-8 tw:w-full tw:rounded-md tw:text-sm tw:transition-colors", isSelected - ? 'tw:bg-primary tw:text-primary-foreground tw:font-medium' - : 'tw:hover:bg-accent tw:hover:text-accent-foreground', - !isCurrentMonth && !isSelected && 'tw:text-muted-foreground', - isToday && !isSelected && 'tw:font-semibold', - isDisabled && 'tw:opacity-30 tw:pointer-events-none' + ? "tw:bg-primary tw:text-primary-foreground tw:font-medium" + : "tw:hover:bg-accent tw:hover:text-accent-foreground", + !isCurrentMonth && !isSelected && "tw:text-muted-foreground", + isToday && !isSelected && "tw:font-semibold", + isDisabled && "tw:opacity-30 tw:pointer-events-none", )} > {day.date()} diff --git a/website/src/components/ui/dialog.tsx b/website/src/components/ui/dialog.tsx index 530f074fb..1f8b9163d 100644 --- a/website/src/components/ui/dialog.tsx +++ b/website/src/components/ui/dialog.tsx @@ -1,24 +1,32 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { XIcon } from 'lucide-react'; -import { Dialog as DialogPrimitive } from 'radix-ui'; -import { Button } from '@/components/ui/button'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { XIcon } from "lucide-react"; +import { Dialog as DialogPrimitive } from "radix-ui"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; -function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) { +function Dialog({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Root>) { return <DialogPrimitive.Root data-slot="dialog" {...props} />; } -function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { +function DialogTrigger({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Trigger>) { return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; } -function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) { +function DialogPortal({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Portal>) { return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; } -function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) { +function DialogClose({ + ...props +}: React.ComponentProps<typeof DialogPrimitive.Close>) { return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; } @@ -30,8 +38,8 @@ function DialogOverlay({ <DialogPrimitive.Overlay data-slot="dialog-overlay" className={cn( - 'tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:fixed tw:inset-0 tw:z-50 tw:bg-black/50', - className + "tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:fixed tw:inset-0 tw:z-50 tw:bg-black/50", + className, )} {...props} /> @@ -52,8 +60,8 @@ function DialogContent({ <DialogPrimitive.Content data-slot="dialog-content" className={cn( - 'tw:bg-background tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:data-[state=closed]:zoom-out-95 tw:data-[state=open]:zoom-in-95 tw:fixed tw:top-[50%] tw:left-[50%] tw:z-50 tw:grid tw:w-full tw:max-w-[calc(100%-2rem)] tw:translate-x-[-50%] tw:translate-y-[-50%] tw:gap-4 tw:rounded-lg tw:border tw:p-6 tw:shadow-lg tw:duration-200 tw:outline-none tw:sm:max-w-[32rem]', - className + "tw:bg-background tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:data-[state=closed]:zoom-out-95 tw:data-[state=open]:zoom-in-95 tw:fixed tw:top-[50%] tw:left-[50%] tw:z-50 tw:grid tw:w-full tw:max-w-[calc(100%-2rem)] tw:translate-x-[-50%] tw:translate-y-[-50%] tw:gap-4 tw:rounded-lg tw:border tw:p-6 tw:shadow-lg tw:duration-200 tw:outline-none tw:sm:max-w-[32rem]", + className, )} {...props} > @@ -72,11 +80,14 @@ function DialogContent({ ); } -function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-header" - className={cn('tw:flex tw:flex-col tw:gap-2 tw:text-center tw:sm:text-left', className)} + className={cn( + "tw:flex tw:flex-col tw:gap-2 tw:text-center tw:sm:text-left", + className, + )} {...props} /> ); @@ -87,15 +98,15 @@ function DialogFooter({ showCloseButton = false, children, ...props -}: React.ComponentProps<'div'> & { +}: React.ComponentProps<"div"> & { showCloseButton?: boolean; }) { return ( <div data-slot="dialog-footer" className={cn( - 'tw:flex tw:flex-col-reverse tw:gap-2 tw:sm:flex-row tw:sm:justify-end', - className + "tw:flex tw:flex-col-reverse tw:gap-2 tw:sm:flex-row tw:sm:justify-end", + className, )} {...props} > @@ -109,11 +120,14 @@ function DialogFooter({ ); } -function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) { +function DialogTitle({ + className, + ...props +}: React.ComponentProps<typeof DialogPrimitive.Title>) { return ( <DialogPrimitive.Title data-slot="dialog-title" - className={cn('tw:text-lg tw:leading-none tw:font-semibold', className)} + className={cn("tw:text-lg tw:leading-none tw:font-semibold", className)} {...props} /> ); @@ -126,7 +140,7 @@ function DialogDescription({ return ( <DialogPrimitive.Description data-slot="dialog-description" - className={cn('tw:text-muted-foreground tw:text-sm', className)} + className={cn("tw:text-muted-foreground tw:text-sm", className)} {...props} /> ); diff --git a/website/src/components/ui/dropdown-menu.tsx b/website/src/components/ui/dropdown-menu.tsx index 49e1713f4..061e79e22 100644 --- a/website/src/components/ui/dropdown-menu.tsx +++ b/website/src/components/ui/dropdown-menu.tsx @@ -1,22 +1,31 @@ -import * as React from 'react'; -import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; -import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; -function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { +function DropdownMenu({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />; } function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { - return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />; + return ( + <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> + ); } function DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { - return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />; + return ( + <DropdownMenuPrimitive.Trigger + data-slot="dropdown-menu-trigger" + {...props} + /> + ); } function DropdownMenuContent({ @@ -30,8 +39,8 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - 'tw:bg-popover tw:text-popover-foreground tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:data-[state=closed]:zoom-out-95 tw:data-[state=open]:zoom-in-95 tw:data-[side=bottom]:slide-in-from-top-2 tw:data-[side=left]:slide-in-from-right-2 tw:data-[side=right]:slide-in-from-left-2 tw:data-[side=top]:slide-in-from-bottom-2 tw:z-50 tw:max-h-(--radix-dropdown-menu-content-available-height) tw:min-w-[8rem] tw:origin-(--radix-dropdown-menu-content-transform-origin) tw:overflow-x-hidden tw:overflow-y-auto tw:rounded-md tw:border tw:p-1 tw:shadow-md', - className + "tw:bg-popover tw:text-popover-foreground tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:data-[state=closed]:zoom-out-95 tw:data-[state=open]:zoom-in-95 tw:data-[side=bottom]:slide-in-from-top-2 tw:data-[side=left]:slide-in-from-right-2 tw:data-[side=right]:slide-in-from-left-2 tw:data-[side=top]:slide-in-from-bottom-2 tw:z-50 tw:max-h-(--radix-dropdown-menu-content-available-height) tw:min-w-[8rem] tw:origin-(--radix-dropdown-menu-content-transform-origin) tw:overflow-x-hidden tw:overflow-y-auto tw:rounded-md tw:border tw:p-1 tw:shadow-md", + className, )} {...props} /> @@ -39,18 +48,22 @@ function DropdownMenuContent({ ); } -function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { - return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />; +function DropdownMenuGroup({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { + return ( + <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> + ); } function DropdownMenuItem({ className, inset, - variant = 'default', + variant = "default", ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { inset?: boolean; - variant?: 'default' | 'destructive'; + variant?: "default" | "destructive"; }) { return ( <DropdownMenuPrimitive.Item @@ -58,8 +71,8 @@ function DropdownMenuItem({ data-inset={inset} data-variant={variant} className={cn( - 'tw:focus:bg-accent tw:focus:text-accent-foreground tw:data-[variant=destructive]:text-destructive tw:data-[variant=destructive]:focus:bg-destructive/10 tw:data-[variant=destructive]:focus:text-destructive tw:data-[variant=destructive]:*:[svg]:!text-destructive tw:[&_svg:not([class*=text-])]:text-muted-foreground tw:relative tw:flex tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:px-2 tw:py-1.5 tw:text-sm tw:outline-hidden tw:select-none tw:data-[disabled]:pointer-events-none tw:data-[disabled]:opacity-50 tw:data-[inset]:pl-8 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4', - className + "tw:focus:bg-accent tw:focus:text-accent-foreground tw:data-[variant=destructive]:text-destructive tw:data-[variant=destructive]:focus:bg-destructive/10 tw:data-[variant=destructive]:focus:text-destructive tw:data-[variant=destructive]:*:[svg]:!text-destructive tw:[&_svg:not([class*=text-])]:text-muted-foreground tw:relative tw:flex tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:px-2 tw:py-1.5 tw:text-sm tw:outline-hidden tw:select-none tw:data-[disabled]:pointer-events-none tw:data-[disabled]:opacity-50 tw:data-[inset]:pl-8 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4", + className, )} {...props} /> @@ -76,8 +89,8 @@ function DropdownMenuCheckboxItem({ <DropdownMenuPrimitive.CheckboxItem data-slot="dropdown-menu-checkbox-item" className={cn( - 'tw:focus:bg-accent tw:focus:text-accent-foreground tw:relative tw:flex tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:py-1.5 tw:pr-2 tw:pl-8 tw:text-sm tw:outline-hidden tw:select-none tw:data-[disabled]:pointer-events-none tw:data-[disabled]:opacity-50 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4', - className + "tw:focus:bg-accent tw:focus:text-accent-foreground tw:relative tw:flex tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:py-1.5 tw:pr-2 tw:pl-8 tw:text-sm tw:outline-hidden tw:select-none tw:data-[disabled]:pointer-events-none tw:data-[disabled]:opacity-50 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4", + className, )} checked={checked} {...props} @@ -95,7 +108,12 @@ function DropdownMenuCheckboxItem({ function DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { - return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />; + return ( + <DropdownMenuPrimitive.RadioGroup + data-slot="dropdown-menu-radio-group" + {...props} + /> + ); } function DropdownMenuRadioItem({ @@ -107,8 +125,8 @@ function DropdownMenuRadioItem({ <DropdownMenuPrimitive.RadioItem data-slot="dropdown-menu-radio-item" className={cn( - 'tw:focus:bg-accent tw:focus:text-accent-foreground tw:relative tw:flex tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:py-1.5 tw:pr-2 tw:pl-8 tw:text-sm tw:outline-hidden tw:select-none tw:data-[disabled]:pointer-events-none tw:data-[disabled]:opacity-50 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4', - className + "tw:focus:bg-accent tw:focus:text-accent-foreground tw:relative tw:flex tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:py-1.5 tw:pr-2 tw:pl-8 tw:text-sm tw:outline-hidden tw:select-none tw:data-[disabled]:pointer-events-none tw:data-[disabled]:opacity-50 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4", + className, )} {...props} > @@ -133,7 +151,10 @@ function DropdownMenuLabel({ <DropdownMenuPrimitive.Label data-slot="dropdown-menu-label" data-inset={inset} - className={cn('tw:px-2 tw:py-1.5 tw:text-sm tw:font-medium tw:data-[inset]:pl-8', className)} + className={cn( + "tw:px-2 tw:py-1.5 tw:text-sm tw:font-medium tw:data-[inset]:pl-8", + className, + )} {...props} /> ); @@ -146,23 +167,31 @@ function DropdownMenuSeparator({ return ( <DropdownMenuPrimitive.Separator data-slot="dropdown-menu-separator" - className={cn('tw:bg-border tw:-mx-1 tw:my-1 tw:h-px', className)} + className={cn("tw:bg-border tw:-mx-1 tw:my-1 tw:h-px", className)} {...props} /> ); } -function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { return ( <span data-slot="dropdown-menu-shortcut" - className={cn('tw:text-muted-foreground tw:ml-auto tw:text-xs tw:tracking-widest', className)} + className={cn( + "tw:text-muted-foreground tw:ml-auto tw:text-xs tw:tracking-widest", + className, + )} {...props} /> ); } -function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { +function DropdownMenuSub({ + ...props +}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />; } @@ -179,8 +208,8 @@ function DropdownMenuSubTrigger({ data-slot="dropdown-menu-sub-trigger" data-inset={inset} className={cn( - 'tw:focus:bg-accent tw:focus:text-accent-foreground tw:data-[state=open]:bg-accent tw:data-[state=open]:text-accent-foreground tw:[&_svg:not([class*=text-])]:text-muted-foreground tw:flex tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:px-2 tw:py-1.5 tw:text-sm tw:outline-hidden tw:select-none tw:data-[inset]:pl-8 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4', - className + "tw:focus:bg-accent tw:focus:text-accent-foreground tw:data-[state=open]:bg-accent tw:data-[state=open]:text-accent-foreground tw:[&_svg:not([class*=text-])]:text-muted-foreground tw:flex tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:px-2 tw:py-1.5 tw:text-sm tw:outline-hidden tw:select-none tw:data-[inset]:pl-8 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4", + className, )} {...props} > @@ -198,8 +227,8 @@ function DropdownMenuSubContent({ <DropdownMenuPrimitive.SubContent data-slot="dropdown-menu-sub-content" className={cn( - 'tw:bg-popover tw:text-popover-foreground tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:data-[state=closed]:zoom-out-95 tw:data-[state=open]:zoom-in-95 tw:data-[side=bottom]:slide-in-from-top-2 tw:data-[side=left]:slide-in-from-right-2 tw:data-[side=right]:slide-in-from-left-2 tw:data-[side=top]:slide-in-from-bottom-2 tw:z-50 tw:min-w-[8rem] tw:origin-(--radix-dropdown-menu-content-transform-origin) tw:overflow-hidden tw:rounded-md tw:border tw:p-1 tw:shadow-lg', - className + "tw:bg-popover tw:text-popover-foreground tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:data-[state=closed]:zoom-out-95 tw:data-[state=open]:zoom-in-95 tw:data-[side=bottom]:slide-in-from-top-2 tw:data-[side=left]:slide-in-from-right-2 tw:data-[side=right]:slide-in-from-left-2 tw:data-[side=top]:slide-in-from-bottom-2 tw:z-50 tw:min-w-[8rem] tw:origin-(--radix-dropdown-menu-content-transform-origin) tw:overflow-hidden tw:rounded-md tw:border tw:p-1 tw:shadow-lg", + className, )} {...props} /> diff --git a/website/src/components/ui/index.ts b/website/src/components/ui/index.ts index 41507b0d1..bfc8776e3 100644 --- a/website/src/components/ui/index.ts +++ b/website/src/components/ui/index.ts @@ -1,21 +1,21 @@ // Custom PE components -export { Container } from './Container'; -export type { ContainerProps } from './Container'; +export { Container } from "./Container"; +export type { ContainerProps } from "./Container"; -export { Group } from './Group'; -export type { GroupProps } from './Group'; +export { Group } from "./Group"; +export type { GroupProps } from "./Group"; -export { Spinner } from './Spinner'; -export type { SpinnerProps } from './Spinner'; +export { Spinner } from "./Spinner"; +export type { SpinnerProps } from "./Spinner"; -export { Stack } from './Stack'; -export type { StackProps } from './Stack'; +export { Stack } from "./Stack"; +export type { StackProps } from "./Stack"; -export { Text, textVariants } from './Text'; -export type { TextProps } from './Text'; +export { Text, textVariants } from "./Text"; +export type { TextProps } from "./Text"; -export { Title } from './Title'; -export type { TitleProps } from './Title'; +export { Title } from "./Title"; +export type { TitleProps } from "./Title"; // shadcn/ui components export { @@ -34,13 +34,25 @@ export { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup, -} from './dropdown-menu'; -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from './accordion'; -export { Alert, AlertTitle, AlertDescription } from './alert'; -export { Badge, badgeVariants } from './badge'; -export { Button, buttonVariants } from './button'; -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './card'; -export { Checkbox } from './checkbox'; +} from "./dropdown-menu"; +export { + Accordion, + AccordionItem, + AccordionTrigger, + AccordionContent, +} from "./accordion"; +export { Alert, AlertTitle, AlertDescription } from "./alert"; +export { Badge, badgeVariants } from "./badge"; +export { Button, buttonVariants } from "./button"; +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +} from "./card"; +export { Checkbox } from "./checkbox"; export { Dialog, DialogPortal, @@ -52,12 +64,12 @@ export { DialogFooter, DialogTitle, DialogDescription, -} from './dialog'; -export { Input } from './input'; -export { Label } from './label'; -export { Progress } from './progress'; -export { RadioGroup, RadioGroupItem } from './radio-group'; -export { ScrollArea, ScrollBar } from './scroll-area'; +} from "./dialog"; +export { Input } from "./input"; +export { Label } from "./label"; +export { Progress } from "./progress"; +export { RadioGroup, RadioGroupItem } from "./radio-group"; +export { ScrollArea, ScrollBar } from "./scroll-area"; export { Select, SelectGroup, @@ -69,8 +81,8 @@ export { SelectSeparator, SelectScrollUpButton, SelectScrollDownButton, -} from './select'; -export { Separator } from './separator'; +} from "./select"; +export { Separator } from "./separator"; export { Sheet, SheetPortal, @@ -82,9 +94,9 @@ export { SheetFooter, SheetTitle, SheetDescription, -} from './sheet'; -export { Skeleton } from './skeleton'; -export { Switch } from './switch'; +} from "./sheet"; +export { Skeleton } from "./skeleton"; +export { Switch } from "./switch"; export { Table as ShadcnTable, TableHeader, @@ -94,13 +106,25 @@ export { TableRow, TableCell, TableCaption, -} from './table'; -export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs'; -export { Textarea } from './textarea'; -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './tooltip'; -export { Collapsible, CollapsibleTrigger, CollapsibleContent } from './collapsible'; -export { SegmentedControl } from './segmented-control'; -export type { SegmentedControlOption, SegmentedControlProps } from './segmented-control'; +} from "./table"; +export { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs"; +export { Textarea } from "./textarea"; +export { + Tooltip, + TooltipTrigger, + TooltipContent, + TooltipProvider, +} from "./tooltip"; +export { + Collapsible, + CollapsibleTrigger, + CollapsibleContent, +} from "./collapsible"; +export { SegmentedControl } from "./segmented-control"; +export type { + SegmentedControlOption, + SegmentedControlProps, +} from "./segmented-control"; export { Popover, PopoverTrigger, @@ -109,7 +133,7 @@ export { PopoverHeader, PopoverTitle, PopoverDescription, -} from './popover'; +} from "./popover"; export { Command, CommandDialog, @@ -120,8 +144,8 @@ export { CommandItem, CommandShortcut, CommandSeparator, -} from './command'; -export { YearPicker } from './year-picker'; -export type { YearPickerProps } from './year-picker'; -export { DatePicker } from './date-picker'; -export type { DatePickerProps } from './date-picker'; +} from "./command"; +export { YearPicker } from "./year-picker"; +export type { YearPickerProps } from "./year-picker"; +export { DatePicker } from "./date-picker"; +export type { DatePickerProps } from "./date-picker"; diff --git a/website/src/components/ui/input.tsx b/website/src/components/ui/input.tsx index d99d6e1f6..ba674d227 100644 --- a/website/src/components/ui/input.tsx +++ b/website/src/components/ui/input.tsx @@ -1,16 +1,16 @@ -import * as React from 'react'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { cn } from "@/lib/utils"; -function Input({ className, type, ...props }: React.ComponentProps<'input'>) { +function Input({ className, type, ...props }: React.ComponentProps<"input">) { return ( <input type={type} data-slot="input" className={cn( - 'tw:file:text-foreground tw:placeholder:text-muted-foreground tw:selection:bg-primary tw:selection:text-primary-foreground tw:border-input tw:h-9 tw:w-full tw:min-w-0 tw:rounded-md tw:border tw:px-3 tw:py-1 tw:text-base tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none tw:file:inline-flex tw:file:h-7 tw:file:border-0 tw:file:bg-transparent tw:file:text-sm tw:file:font-medium tw:disabled:pointer-events-none tw:disabled:cursor-not-allowed tw:disabled:opacity-50 tw:md:text-sm', - 'tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:focus-visible:ring-[3px]', - 'tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive', - className + "tw:file:text-foreground tw:placeholder:text-muted-foreground tw:selection:bg-primary tw:selection:text-primary-foreground tw:border-input tw:h-9 tw:w-full tw:min-w-0 tw:rounded-md tw:border tw:px-3 tw:py-1 tw:text-base tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none tw:file:inline-flex tw:file:h-7 tw:file:border-0 tw:file:bg-transparent tw:file:text-sm tw:file:font-medium tw:disabled:pointer-events-none tw:disabled:cursor-not-allowed tw:disabled:opacity-50 tw:md:text-sm", + "tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:focus-visible:ring-[3px]", + "tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive", + className, )} {...props} /> diff --git a/website/src/components/ui/label.tsx b/website/src/components/ui/label.tsx index e281ad07b..e73cf929b 100644 --- a/website/src/components/ui/label.tsx +++ b/website/src/components/ui/label.tsx @@ -1,16 +1,19 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { Label as LabelPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { Label as LabelPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; -function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) { +function Label({ + className, + ...props +}: React.ComponentProps<typeof LabelPrimitive.Root>) { return ( <LabelPrimitive.Root data-slot="label" className={cn( - 'tw:flex tw:items-center tw:gap-2 tw:text-sm tw:leading-none tw:font-normal tw:select-none tw:group-data-[disabled=true]:pointer-events-none tw:group-data-[disabled=true]:opacity-50 tw:peer-disabled:cursor-not-allowed tw:peer-disabled:opacity-50', - className + "tw:flex tw:items-center tw:gap-2 tw:text-sm tw:leading-none tw:font-normal tw:select-none tw:group-data-[disabled=true]:pointer-events-none tw:group-data-[disabled=true]:opacity-50 tw:peer-disabled:cursor-not-allowed tw:peer-disabled:opacity-50", + className, )} {...props} /> diff --git a/website/src/components/ui/popover.tsx b/website/src/components/ui/popover.tsx index 3db0f3b42..8fc8818be 100644 --- a/website/src/components/ui/popover.tsx +++ b/website/src/components/ui/popover.tsx @@ -1,18 +1,22 @@ -import * as React from 'react'; -import { Popover as PopoverPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { Popover as PopoverPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; -function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) { +function Popover({ + ...props +}: React.ComponentProps<typeof PopoverPrimitive.Root>) { return <PopoverPrimitive.Root data-slot="popover" {...props} />; } -function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { +function PopoverTrigger({ + ...props +}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />; } function PopoverContent({ className, - align = 'center', + align = "center", sideOffset = 4, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>) { @@ -23,8 +27,8 @@ function PopoverContent({ align={align} sideOffset={sideOffset} className={cn( - 'tw:bg-popover tw:text-popover-foreground tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:data-[state=closed]:zoom-out-95 tw:data-[state=open]:zoom-in-95 tw:data-[side=bottom]:slide-in-from-top-2 tw:data-[side=left]:slide-in-from-right-2 tw:data-[side=right]:slide-in-from-left-2 tw:data-[side=top]:slide-in-from-bottom-2 tw:z-50 tw:w-72 tw:origin-(--radix-popover-content-transform-origin) tw:rounded-md tw:border tw:p-4 tw:shadow-md tw:outline-hidden', - className + "tw:bg-popover tw:text-popover-foreground tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:data-[state=closed]:zoom-out-95 tw:data-[state=open]:zoom-in-95 tw:data-[side=bottom]:slide-in-from-top-2 tw:data-[side=left]:slide-in-from-right-2 tw:data-[side=right]:slide-in-from-left-2 tw:data-[side=top]:slide-in-from-bottom-2 tw:z-50 tw:w-72 tw:origin-(--radix-popover-content-transform-origin) tw:rounded-md tw:border tw:p-4 tw:shadow-md tw:outline-hidden", + className, )} {...props} /> @@ -32,29 +36,40 @@ function PopoverContent({ ); } -function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { +function PopoverAnchor({ + ...props +}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />; } -function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) { +function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="popover-header" - className={cn('tw:flex tw:flex-col tw:gap-1 tw:text-sm', className)} + className={cn("tw:flex tw:flex-col tw:gap-1 tw:text-sm", className)} {...props} /> ); } -function PopoverTitle({ className, ...props }: React.ComponentProps<'h2'>) { - return <div data-slot="popover-title" className={cn('tw:font-medium', className)} {...props} />; +function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) { + return ( + <div + data-slot="popover-title" + className={cn("tw:font-medium", className)} + {...props} + /> + ); } -function PopoverDescription({ className, ...props }: React.ComponentProps<'p'>) { +function PopoverDescription({ + className, + ...props +}: React.ComponentProps<"p">) { return ( <p data-slot="popover-description" - className={cn('tw:text-muted-foreground', className)} + className={cn("tw:text-muted-foreground", className)} {...props} /> ); diff --git a/website/src/components/ui/progress.tsx b/website/src/components/ui/progress.tsx index 9b0d01c78..a503d223e 100644 --- a/website/src/components/ui/progress.tsx +++ b/website/src/components/ui/progress.tsx @@ -1,8 +1,8 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { Progress as ProgressPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { Progress as ProgressPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; function Progress({ className, @@ -13,8 +13,8 @@ function Progress({ <ProgressPrimitive.Root data-slot="progress" className={cn( - 'tw:bg-primary/20 tw:relative tw:h-2 tw:w-full tw:overflow-hidden tw:rounded-full', - className + "tw:bg-primary/20 tw:relative tw:h-2 tw:w-full tw:overflow-hidden tw:rounded-full", + className, )} {...props} > diff --git a/website/src/components/ui/radio-group.tsx b/website/src/components/ui/radio-group.tsx index 2fbbd367b..27a5eabf2 100644 --- a/website/src/components/ui/radio-group.tsx +++ b/website/src/components/ui/radio-group.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; -import { CircleIcon } from 'lucide-react'; -import { RadioGroup as RadioGroupPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { CircleIcon } from "lucide-react"; +import { RadioGroup as RadioGroupPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; function RadioGroup({ className, @@ -10,7 +10,7 @@ function RadioGroup({ return ( <RadioGroupPrimitive.Root data-slot="radio-group" - className={cn('tw:grid tw:gap-3', className)} + className={cn("tw:grid tw:gap-3", className)} {...props} /> ); @@ -24,8 +24,8 @@ function RadioGroupItem({ <RadioGroupPrimitive.Item data-slot="radio-group-item" className={cn( - 'tw:border-input tw:text-primary tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive tw:aspect-square tw:size-4 tw:shrink-0 tw:rounded-full tw:border tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none tw:focus-visible:ring-[3px] tw:disabled:cursor-not-allowed tw:disabled:opacity-50', - className + "tw:border-input tw:text-primary tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive tw:aspect-square tw:size-4 tw:shrink-0 tw:rounded-full tw:border tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none tw:focus-visible:ring-[3px] tw:disabled:cursor-not-allowed tw:disabled:opacity-50", + className, )} {...props} > diff --git a/website/src/components/ui/scroll-area.tsx b/website/src/components/ui/scroll-area.tsx index f1ceedfd5..550f70f7a 100644 --- a/website/src/components/ui/scroll-area.tsx +++ b/website/src/components/ui/scroll-area.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; -import { ScrollArea as ScrollAreaPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; function ScrollArea({ className, @@ -10,7 +10,7 @@ function ScrollArea({ return ( <ScrollAreaPrimitive.Root data-slot="scroll-area" - className={cn('tw:relative', className)} + className={cn("tw:relative", className)} {...props} > <ScrollAreaPrimitive.Viewport @@ -27,7 +27,7 @@ function ScrollArea({ function ScrollBar({ className, - orientation = 'vertical', + orientation = "vertical", ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) { return ( @@ -35,10 +35,12 @@ function ScrollBar({ data-slot="scroll-area-scrollbar" orientation={orientation} className={cn( - 'tw:flex tw:touch-none tw:p-px tw:transition-colors tw:select-none', - orientation === 'vertical' && 'tw:h-full tw:w-2.5 tw:border-l tw:border-l-transparent', - orientation === 'horizontal' && 'tw:h-2.5 tw:flex-col tw:border-t tw:border-t-transparent', - className + "tw:flex tw:touch-none tw:p-px tw:transition-colors tw:select-none", + orientation === "vertical" && + "tw:h-full tw:w-2.5 tw:border-l tw:border-l-transparent", + orientation === "horizontal" && + "tw:h-2.5 tw:flex-col tw:border-t tw:border-t-transparent", + className, )} {...props} > diff --git a/website/src/components/ui/segmented-control.story.tsx b/website/src/components/ui/segmented-control.story.tsx index 7db3de434..af33ff286 100644 --- a/website/src/components/ui/segmented-control.story.tsx +++ b/website/src/components/ui/segmented-control.story.tsx @@ -1,10 +1,10 @@ "use client"; -import { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; -import { SegmentedControl } from './segmented-control'; +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { SegmentedControl } from "./segmented-control"; const meta: Meta<typeof SegmentedControl> = { - title: 'UI/SegmentedControl', + title: "UI/SegmentedControl", component: SegmentedControl, }; @@ -16,20 +16,27 @@ function ControlledSegmented({ options, defaultValue, }: { - size?: 'xs' | 'sm'; + size?: "xs" | "sm"; options: { label: string; value: string; disabled?: boolean }[]; defaultValue: string; }) { const [value, setValue] = useState(defaultValue); - return <SegmentedControl value={value} onValueChange={setValue} options={options} size={size} />; + return ( + <SegmentedControl + value={value} + onValueChange={setValue} + options={options} + size={size} + /> + ); } export const Default: Story = { render: () => ( <ControlledSegmented options={[ - { label: 'Absolute', value: 'absolute' }, - { label: 'Relative', value: 'relative' }, + { label: "Absolute", value: "absolute" }, + { label: "Relative", value: "relative" }, ]} defaultValue="absolute" /> @@ -41,8 +48,8 @@ export const ExtraSmall: Story = { <ControlledSegmented size="xs" options={[ - { label: 'Geographic', value: 'geographic' }, - { label: 'Hex grid', value: 'hex' }, + { label: "Geographic", value: "geographic" }, + { label: "Hex grid", value: "hex" }, ]} defaultValue="geographic" /> @@ -53,9 +60,9 @@ export const ThreeOptions: Story = { render: () => ( <ControlledSegmented options={[ - { label: 'Income', value: 'income' }, - { label: 'Wealth', value: 'wealth' }, - { label: 'Consumption', value: 'consumption' }, + { label: "Income", value: "income" }, + { label: "Wealth", value: "wealth" }, + { label: "Consumption", value: "consumption" }, ]} defaultValue="income" /> diff --git a/website/src/components/ui/segmented-control.tsx b/website/src/components/ui/segmented-control.tsx index d1297bd16..d7132b40e 100644 --- a/website/src/components/ui/segmented-control.tsx +++ b/website/src/components/ui/segmented-control.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; -import { Tabs as TabsPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { Tabs as TabsPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; interface SegmentedControlOption { label: string; @@ -12,7 +12,7 @@ interface SegmentedControlProps { value: string; onValueChange: (value: string) => void; options: SegmentedControlOption[]; - size?: 'xs' | 'sm'; + size?: "xs" | "sm"; className?: string; } @@ -20,7 +20,7 @@ function SegmentedControl({ value, onValueChange, options, - size = 'sm', + size = "sm", className, }: SegmentedControlProps) { return ( @@ -28,9 +28,9 @@ function SegmentedControl({ <TabsPrimitive.List data-slot="segmented-control" className={cn( - 'tw:inline-flex tw:items-center tw:gap-1 tw:rounded-lg tw:bg-muted', - size === 'xs' ? 'tw:h-7 tw:p-1' : 'tw:h-9 tw:p-1', - className + "tw:inline-flex tw:items-center tw:gap-1 tw:rounded-lg tw:bg-muted", + size === "xs" ? "tw:h-7 tw:p-1" : "tw:h-9 tw:p-1", + className, )} > {options.map((option) => ( @@ -39,10 +39,10 @@ function SegmentedControl({ value={option.value} disabled={option.disabled} className={cn( - 'tw:inline-flex tw:h-full tw:items-center tw:justify-center tw:rounded-md tw:px-2 tw:font-medium tw:whitespace-nowrap tw:transition-all', - 'tw:text-muted-foreground tw:data-[state=active]:bg-background tw:data-[state=active]:text-foreground tw:data-[state=active]:shadow-sm', - 'tw:disabled:pointer-events-none tw:disabled:opacity-50', - size === 'xs' ? 'tw:text-xs' : 'tw:text-sm' + "tw:inline-flex tw:h-full tw:items-center tw:justify-center tw:rounded-md tw:px-2 tw:font-medium tw:whitespace-nowrap tw:transition-all", + "tw:text-muted-foreground tw:data-[state=active]:bg-background tw:data-[state=active]:text-foreground tw:data-[state=active]:shadow-sm", + "tw:disabled:pointer-events-none tw:disabled:opacity-50", + size === "xs" ? "tw:text-xs" : "tw:text-sm", )} > {option.label} diff --git a/website/src/components/ui/select.tsx b/website/src/components/ui/select.tsx index 0f519d18d..a1b2443f9 100644 --- a/website/src/components/ui/select.tsx +++ b/website/src/components/ui/select.tsx @@ -1,35 +1,41 @@ -import * as React from 'react'; -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; -import { Select as SelectPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; +import { Select as SelectPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; -function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) { +function Select({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Root>) { return <SelectPrimitive.Root data-slot="select" {...props} />; } -function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) { +function SelectGroup({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Group>) { return <SelectPrimitive.Group data-slot="select-group" {...props} />; } -function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) { +function SelectValue({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Value>) { return <SelectPrimitive.Value data-slot="select-value" {...props} />; } function SelectTrigger({ className, - size = 'default', + size = "default", children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger> & { - size?: 'sm' | 'default'; + size?: "sm" | "default"; }) { return ( <SelectPrimitive.Trigger data-slot="select-trigger" data-size={size} className={cn( - 'tw:border-input tw:data-[placeholder]:text-muted-foreground tw:[&_svg:not([class*=text-])]:text-muted-foreground tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive tw:flex tw:w-full tw:items-center tw:justify-between tw:gap-2 tw:rounded-md tw:border tw:px-3 tw:py-2 tw:text-sm tw:whitespace-nowrap tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none tw:focus-visible:ring-[3px] tw:disabled:cursor-not-allowed tw:disabled:opacity-50 tw:data-[size=default]:h-9 tw:data-[size=sm]:h-8 tw:*:data-[slot=select-value]:line-clamp-1 tw:*:data-[slot=select-value]:flex tw:*:data-[slot=select-value]:items-center tw:*:data-[slot=select-value]:gap-2 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4', - className + "tw:border-input tw:data-[placeholder]:text-muted-foreground tw:[&_svg:not([class*=text-])]:text-muted-foreground tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive tw:flex tw:w-full tw:items-center tw:justify-between tw:gap-2 tw:rounded-md tw:border tw:px-3 tw:py-2 tw:text-sm tw:whitespace-nowrap tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none tw:focus-visible:ring-[3px] tw:disabled:cursor-not-allowed tw:disabled:opacity-50 tw:data-[size=default]:h-9 tw:data-[size=sm]:h-8 tw:*:data-[slot=select-value]:line-clamp-1 tw:*:data-[slot=select-value]:flex tw:*:data-[slot=select-value]:items-center tw:*:data-[slot=select-value]:gap-2 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4", + className, )} {...props} > @@ -44,8 +50,8 @@ function SelectTrigger({ function SelectContent({ className, children, - position = 'item-aligned', - align = 'center', + position = "item-aligned", + align = "center", ...props }: React.ComponentProps<typeof SelectPrimitive.Content>) { return ( @@ -53,10 +59,10 @@ function SelectContent({ <SelectPrimitive.Content data-slot="select-content" className={cn( - 'tw:bg-popover tw:text-popover-foreground tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:data-[state=closed]:zoom-out-95 tw:data-[state=open]:zoom-in-95 tw:data-[side=bottom]:slide-in-from-top-2 tw:data-[side=left]:slide-in-from-right-2 tw:data-[side=right]:slide-in-from-left-2 tw:data-[side=top]:slide-in-from-bottom-2 tw:relative tw:z-50 tw:max-h-(--radix-select-content-available-height) tw:min-w-[8rem] tw:origin-(--radix-select-content-transform-origin) tw:overflow-x-hidden tw:overflow-y-auto tw:rounded-md tw:border tw:shadow-md', - position === 'popper' && - 'tw:data-[side=bottom]:translate-y-1 tw:data-[side=left]:-translate-x-1 tw:data-[side=right]:translate-x-1 tw:data-[side=top]:-translate-y-1', - className + "tw:bg-popover tw:text-popover-foreground tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:data-[state=closed]:zoom-out-95 tw:data-[state=open]:zoom-in-95 tw:data-[side=bottom]:slide-in-from-top-2 tw:data-[side=left]:slide-in-from-right-2 tw:data-[side=right]:slide-in-from-left-2 tw:data-[side=top]:slide-in-from-bottom-2 tw:relative tw:z-50 tw:max-h-(--radix-select-content-available-height) tw:min-w-[8rem] tw:origin-(--radix-select-content-transform-origin) tw:overflow-x-hidden tw:overflow-y-auto tw:rounded-md tw:border tw:shadow-md", + position === "popper" && + "tw:data-[side=bottom]:translate-y-1 tw:data-[side=left]:-translate-x-1 tw:data-[side=right]:translate-x-1 tw:data-[side=top]:-translate-y-1", + className, )} position={position} align={align} @@ -65,9 +71,9 @@ function SelectContent({ <SelectScrollUpButton /> <SelectPrimitive.Viewport className={cn( - 'tw:p-1', - position === 'popper' && - 'tw:h-[var(--radix-select-trigger-height)] tw:w-full tw:min-w-[var(--radix-select-trigger-width)] tw:scroll-my-1' + "tw:p-1", + position === "popper" && + "tw:h-[var(--radix-select-trigger-height)] tw:w-full tw:min-w-[var(--radix-select-trigger-width)] tw:scroll-my-1", )} > {children} @@ -78,11 +84,17 @@ function SelectContent({ ); } -function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) { +function SelectLabel({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Label>) { return ( <SelectPrimitive.Label data-slot="select-label" - className={cn('tw:text-muted-foreground tw:px-2 tw:py-1.5 tw:text-xs', className)} + className={cn( + "tw:text-muted-foreground tw:px-2 tw:py-1.5 tw:text-xs", + className, + )} {...props} /> ); @@ -97,8 +109,8 @@ function SelectItem({ <SelectPrimitive.Item data-slot="select-item" className={cn( - 'tw:focus:bg-accent tw:focus:text-accent-foreground tw:[&_svg:not([class*=text-])]:text-muted-foreground tw:relative tw:flex tw:w-full tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:py-1.5 tw:pr-8 tw:pl-2 tw:text-sm tw:outline-hidden tw:select-none tw:data-[disabled]:pointer-events-none tw:data-[disabled]:opacity-50 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4 tw:*:[span]:last:flex tw:*:[span]:last:items-center tw:*:[span]:last:gap-2', - className + "tw:focus:bg-accent tw:focus:text-accent-foreground tw:[&_svg:not([class*=text-])]:text-muted-foreground tw:relative tw:flex tw:w-full tw:cursor-default tw:items-center tw:gap-2 tw:rounded-sm tw:py-1.5 tw:pr-8 tw:pl-2 tw:text-sm tw:outline-hidden tw:select-none tw:data-[disabled]:pointer-events-none tw:data-[disabled]:opacity-50 tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4 tw:*:[span]:last:flex tw:*:[span]:last:items-center tw:*:[span]:last:gap-2", + className, )} {...props} > @@ -122,7 +134,10 @@ function SelectSeparator({ return ( <SelectPrimitive.Separator data-slot="select-separator" - className={cn('tw:bg-border tw:pointer-events-none tw:-mx-1 tw:my-1 tw:h-px', className)} + className={cn( + "tw:bg-border tw:pointer-events-none tw:-mx-1 tw:my-1 tw:h-px", + className, + )} {...props} /> ); @@ -136,8 +151,8 @@ function SelectScrollUpButton({ <SelectPrimitive.ScrollUpButton data-slot="select-scroll-up-button" className={cn( - 'tw:flex tw:cursor-default tw:items-center tw:justify-center tw:py-1', - className + "tw:flex tw:cursor-default tw:items-center tw:justify-center tw:py-1", + className, )} {...props} > @@ -154,8 +169,8 @@ function SelectScrollDownButton({ <SelectPrimitive.ScrollDownButton data-slot="select-scroll-down-button" className={cn( - 'tw:flex tw:cursor-default tw:items-center tw:justify-center tw:py-1', - className + "tw:flex tw:cursor-default tw:items-center tw:justify-center tw:py-1", + className, )} {...props} > diff --git a/website/src/components/ui/separator.tsx b/website/src/components/ui/separator.tsx index 62ec24a0c..422e2f57b 100644 --- a/website/src/components/ui/separator.tsx +++ b/website/src/components/ui/separator.tsx @@ -1,12 +1,12 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { Separator as SeparatorPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { Separator as SeparatorPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; function Separator({ className, - orientation = 'horizontal', + orientation = "horizontal", decorative = true, ...props }: React.ComponentProps<typeof SeparatorPrimitive.Root>) { @@ -16,8 +16,8 @@ function Separator({ decorative={decorative} orientation={orientation} className={cn( - 'tw:bg-border tw:shrink-0 tw:data-[orientation=horizontal]:h-px tw:data-[orientation=horizontal]:w-full tw:data-[orientation=vertical]:h-full tw:data-[orientation=vertical]:w-px', - className + "tw:bg-border tw:shrink-0 tw:data-[orientation=horizontal]:h-px tw:data-[orientation=horizontal]:w-full tw:data-[orientation=vertical]:h-full tw:data-[orientation=vertical]:w-px", + className, )} {...props} /> diff --git a/website/src/components/ui/sheet.tsx b/website/src/components/ui/sheet.tsx index 64de61f5c..8744d076a 100644 --- a/website/src/components/ui/sheet.tsx +++ b/website/src/components/ui/sheet.tsx @@ -1,21 +1,27 @@ -import * as React from 'react'; -import { XIcon } from 'lucide-react'; -import { Dialog as SheetPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { XIcon } from "lucide-react"; +import { Dialog as SheetPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { return <SheetPrimitive.Root data-slot="sheet" {...props} />; } -function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) { +function SheetTrigger({ + ...props +}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />; } -function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) { +function SheetClose({ + ...props +}: React.ComponentProps<typeof SheetPrimitive.Close>) { return <SheetPrimitive.Close data-slot="sheet-close" {...props} />; } -function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) { +function SheetPortal({ + ...props +}: React.ComponentProps<typeof SheetPrimitive.Portal>) { return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />; } @@ -27,8 +33,8 @@ function SheetOverlay({ <SheetPrimitive.Overlay data-slot="sheet-overlay" className={cn( - 'tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:fixed tw:inset-0 tw:z-50 tw:bg-black/50', - className + "tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=open]:fade-in-0 tw:fixed tw:inset-0 tw:z-50 tw:bg-black/50", + className, )} {...props} /> @@ -38,11 +44,11 @@ function SheetOverlay({ function SheetContent({ className, children, - side = 'right', + side = "right", showCloseButton = true, ...props }: React.ComponentProps<typeof SheetPrimitive.Content> & { - side?: 'top' | 'right' | 'bottom' | 'left'; + side?: "top" | "right" | "bottom" | "left"; showCloseButton?: boolean; }) { return ( @@ -51,16 +57,16 @@ function SheetContent({ <SheetPrimitive.Content data-slot="sheet-content" className={cn( - 'tw:bg-background tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:fixed tw:z-50 tw:flex tw:flex-col tw:gap-4 tw:shadow-lg tw:transition tw:ease-in-out tw:data-[state=closed]:duration-300 tw:data-[state=open]:duration-500', - side === 'right' && - 'tw:data-[state=closed]:slide-out-to-right tw:data-[state=open]:slide-in-from-right tw:inset-y-0 tw:right-0 tw:h-full tw:w-3/4 tw:border-l tw:sm:max-w-sm', - side === 'left' && - 'tw:data-[state=closed]:slide-out-to-left tw:data-[state=open]:slide-in-from-left tw:inset-y-0 tw:left-0 tw:h-full tw:w-3/4 tw:border-r tw:sm:max-w-sm', - side === 'top' && - 'tw:data-[state=closed]:slide-out-to-top tw:data-[state=open]:slide-in-from-top tw:inset-x-0 tw:top-0 tw:h-auto tw:border-b', - side === 'bottom' && - 'tw:data-[state=closed]:slide-out-to-bottom tw:data-[state=open]:slide-in-from-bottom tw:inset-x-0 tw:bottom-0 tw:h-auto tw:border-t', - className + "tw:bg-background tw:data-[state=open]:animate-in tw:data-[state=closed]:animate-out tw:fixed tw:z-50 tw:flex tw:flex-col tw:gap-4 tw:shadow-lg tw:transition tw:ease-in-out tw:data-[state=closed]:duration-300 tw:data-[state=open]:duration-500", + side === "right" && + "tw:data-[state=closed]:slide-out-to-right tw:data-[state=open]:slide-in-from-right tw:inset-y-0 tw:right-0 tw:h-full tw:w-3/4 tw:border-l tw:sm:max-w-sm", + side === "left" && + "tw:data-[state=closed]:slide-out-to-left tw:data-[state=open]:slide-in-from-left tw:inset-y-0 tw:left-0 tw:h-full tw:w-3/4 tw:border-r tw:sm:max-w-sm", + side === "top" && + "tw:data-[state=closed]:slide-out-to-top tw:data-[state=open]:slide-in-from-top tw:inset-x-0 tw:top-0 tw:h-auto tw:border-b", + side === "bottom" && + "tw:data-[state=closed]:slide-out-to-bottom tw:data-[state=open]:slide-in-from-bottom tw:inset-x-0 tw:bottom-0 tw:h-auto tw:border-t", + className, )} {...props} > @@ -76,31 +82,37 @@ function SheetContent({ ); } -function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) { +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sheet-header" - className={cn('tw:flex tw:flex-col tw:gap-1.5 tw:p-4', className)} + className={cn("tw:flex tw:flex-col tw:gap-1.5 tw:p-4", className)} {...props} /> ); } -function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) { +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sheet-footer" - className={cn('tw:mt-auto tw:flex tw:flex-col tw:gap-2 tw:p-4', className)} + className={cn( + "tw:mt-auto tw:flex tw:flex-col tw:gap-2 tw:p-4", + className, + )} {...props} /> ); } -function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) { +function SheetTitle({ + className, + ...props +}: React.ComponentProps<typeof SheetPrimitive.Title>) { return ( <SheetPrimitive.Title data-slot="sheet-title" - className={cn('tw:text-foreground tw:font-semibold', className)} + className={cn("tw:text-foreground tw:font-semibold", className)} {...props} /> ); @@ -113,7 +125,7 @@ function SheetDescription({ return ( <SheetPrimitive.Description data-slot="sheet-description" - className={cn('tw:text-muted-foreground tw:text-sm', className)} + className={cn("tw:text-muted-foreground tw:text-sm", className)} {...props} /> ); diff --git a/website/src/components/ui/skeleton.tsx b/website/src/components/ui/skeleton.tsx index c5dce1ca8..7416cd1ad 100644 --- a/website/src/components/ui/skeleton.tsx +++ b/website/src/components/ui/skeleton.tsx @@ -1,10 +1,10 @@ -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils"; -function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="skeleton" - className={cn('tw:bg-accent tw:animate-pulse tw:rounded-md', className)} + className={cn("tw:bg-accent tw:animate-pulse tw:rounded-md", className)} {...props} /> ); diff --git a/website/src/components/ui/switch.tsx b/website/src/components/ui/switch.tsx index f0626d3fb..1aca633e7 100644 --- a/website/src/components/ui/switch.tsx +++ b/website/src/components/ui/switch.tsx @@ -1,30 +1,30 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { Switch as SwitchPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { Switch as SwitchPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; function Switch({ className, - size = 'default', + size = "default", ...props }: React.ComponentProps<typeof SwitchPrimitive.Root> & { - size?: 'sm' | 'default'; + size?: "sm" | "default"; }) { return ( <SwitchPrimitive.Root data-slot="switch" data-size={size} className={cn( - 'tw:peer tw:data-[state=checked]:bg-primary tw:data-[state=unchecked]:bg-input tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:group/switch tw:inline-flex tw:shrink-0 tw:items-center tw:rounded-full tw:border tw:border-transparent tw:shadow-xs tw:transition-all tw:outline-none tw:focus-visible:ring-[3px] tw:disabled:cursor-not-allowed tw:disabled:opacity-50 tw:data-[size=default]:h-[1.15rem] tw:data-[size=default]:w-8 tw:data-[size=sm]:h-3.5 tw:data-[size=sm]:w-6', - className + "tw:peer tw:data-[state=checked]:bg-primary tw:data-[state=unchecked]:bg-input tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:group/switch tw:inline-flex tw:shrink-0 tw:items-center tw:rounded-full tw:border tw:border-transparent tw:shadow-xs tw:transition-all tw:outline-none tw:focus-visible:ring-[3px] tw:disabled:cursor-not-allowed tw:disabled:opacity-50 tw:data-[size=default]:h-[1.15rem] tw:data-[size=default]:w-8 tw:data-[size=sm]:h-3.5 tw:data-[size=sm]:w-6", + className, )} {...props} > <SwitchPrimitive.Thumb data-slot="switch-thumb" className={cn( - 'tw:bg-background tw:pointer-events-none tw:block tw:rounded-full tw:ring-0 tw:transition-transform tw:group-data-[size=default]/switch:size-4 tw:group-data-[size=sm]/switch:size-3 tw:data-[state=checked]:translate-x-[calc(100%-2px)] tw:data-[state=unchecked]:translate-x-0' + "tw:bg-background tw:pointer-events-none tw:block tw:rounded-full tw:ring-0 tw:transition-transform tw:group-data-[size=default]/switch:size-4 tw:group-data-[size=sm]/switch:size-3 tw:data-[state=checked]:translate-x-[calc(100%-2px)] tw:data-[state=unchecked]:translate-x-0", )} /> </SwitchPrimitive.Root> diff --git a/website/src/components/ui/table.tsx b/website/src/components/ui/table.tsx index d134553cb..4b76c3f72 100644 --- a/website/src/components/ui/table.tsx +++ b/website/src/components/ui/table.tsx @@ -1,100 +1,115 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { cn } from "@/lib/utils"; -function Table({ className, ...props }: React.ComponentProps<'table'>) { +function Table({ className, ...props }: React.ComponentProps<"table">) { return ( - <div data-slot="table-container" className="tw:relative tw:w-full tw:overflow-x-auto"> + <div + data-slot="table-container" + className="tw:relative tw:w-full tw:overflow-x-auto" + > <table data-slot="table" - className={cn('tw:w-full tw:caption-bottom tw:text-sm', className)} + className={cn("tw:w-full tw:caption-bottom tw:text-sm", className)} {...props} /> </div> ); } -function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) { +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { return ( <thead data-slot="table-header" - className={cn('tw:[&_tr]:border-b tw:[&_tr]:border-border', className)} + className={cn("tw:[&_tr]:border-b tw:[&_tr]:border-border", className)} {...props} /> ); } -function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) { +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { return ( <tbody data-slot="table-body" - className={cn('tw:[&_tr:last-child]:border-0', className)} + className={cn("tw:[&_tr:last-child]:border-0", className)} {...props} /> ); } -function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) { +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { return ( <tfoot data-slot="table-footer" className={cn( - 'tw:bg-muted/50 tw:border-t tw:border-border tw:font-medium tw:[&>tr]:last:border-b-0', - className + "tw:bg-muted/50 tw:border-t tw:border-border tw:font-medium tw:[&>tr]:last:border-b-0", + className, )} {...props} /> ); } -function TableRow({ className, ...props }: React.ComponentProps<'tr'>) { +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { return ( <tr data-slot="table-row" className={cn( - 'tw:hover:bg-muted/50 tw:data-[state=selected]:bg-muted tw:border-b tw:border-border tw:transition-colors', - className + "tw:hover:bg-muted/50 tw:data-[state=selected]:bg-muted tw:border-b tw:border-border tw:transition-colors", + className, )} {...props} /> ); } -function TableHead({ className, ...props }: React.ComponentProps<'th'>) { +function TableHead({ className, ...props }: React.ComponentProps<"th">) { return ( <th data-slot="table-head" className={cn( - 'tw:text-foreground tw:h-10 tw:px-2 tw:text-left tw:align-middle tw:font-medium tw:[&:has([role=checkbox])]:pr-0 tw:[&>[role=checkbox]]:translate-y-[2px]', - className + "tw:text-foreground tw:h-10 tw:px-2 tw:text-left tw:align-middle tw:font-medium tw:[&:has([role=checkbox])]:pr-0 tw:[&>[role=checkbox]]:translate-y-[2px]", + className, )} {...props} /> ); } -function TableCell({ className, ...props }: React.ComponentProps<'td'>) { +function TableCell({ className, ...props }: React.ComponentProps<"td">) { return ( <td data-slot="table-cell" className={cn( - 'tw:p-2 tw:align-middle tw:[&:has([role=checkbox])]:pr-0 tw:[&>[role=checkbox]]:translate-y-[2px]', - className + "tw:p-2 tw:align-middle tw:[&:has([role=checkbox])]:pr-0 tw:[&>[role=checkbox]]:translate-y-[2px]", + className, )} {...props} /> ); } -function TableCaption({ className, ...props }: React.ComponentProps<'caption'>) { +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { return ( <caption data-slot="table-caption" - className={cn('tw:text-muted-foreground tw:mt-4 tw:text-sm', className)} + className={cn("tw:text-muted-foreground tw:mt-4 tw:text-sm", className)} {...props} /> ); } -export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }; +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/website/src/components/ui/tabs.tsx b/website/src/components/ui/tabs.tsx index 3efa13000..7c1c60f85 100644 --- a/website/src/components/ui/tabs.tsx +++ b/website/src/components/ui/tabs.tsx @@ -1,11 +1,11 @@ -import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { Tabs as TabsPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Tabs as TabsPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; function Tabs({ className, - orientation = 'horizontal', + orientation = "horizontal", ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) { return ( @@ -14,8 +14,8 @@ function Tabs({ data-orientation={orientation} orientation={orientation} className={cn( - 'tw:group/tabs tw:flex tw:gap-2 tw:data-[orientation=horizontal]:flex-col', - className + "tw:group/tabs tw:flex tw:gap-2 tw:data-[orientation=horizontal]:flex-col", + className, )} {...props} /> @@ -23,25 +23,26 @@ function Tabs({ } const tabsListVariants = cva( - 'tw:rounded-lg tw:p-[3px] tw:group-data-[orientation=horizontal]/tabs:h-9 tw:data-[variant=line]:rounded-none tw:group/tabs-list tw:text-muted-foreground tw:inline-flex tw:w-fit tw:items-center tw:justify-center tw:group-data-[orientation=vertical]/tabs:h-fit tw:group-data-[orientation=vertical]/tabs:flex-col', + "tw:rounded-lg tw:p-[3px] tw:group-data-[orientation=horizontal]/tabs:h-9 tw:data-[variant=line]:rounded-none tw:group/tabs-list tw:text-muted-foreground tw:inline-flex tw:w-fit tw:items-center tw:justify-center tw:group-data-[orientation=vertical]/tabs:h-fit tw:group-data-[orientation=vertical]/tabs:flex-col", { variants: { variant: { - default: 'tw:bg-muted', - line: 'tw:gap-1 tw:bg-transparent', + default: "tw:bg-muted", + line: "tw:gap-1 tw:bg-transparent", }, }, defaultVariants: { - variant: 'default', + variant: "default", }, - } + }, ); function TabsList({ className, - variant = 'default', + variant = "default", ...props -}: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) { +}: React.ComponentProps<typeof TabsPrimitive.List> & + VariantProps<typeof tabsListVariants>) { return ( <TabsPrimitive.List data-slot="tabs-list" @@ -52,27 +53,33 @@ function TabsList({ ); } -function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) { +function TabsTrigger({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Trigger>) { return ( <TabsPrimitive.Trigger data-slot="tabs-trigger" className={cn( - 'tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:focus-visible:outline-ring tw:text-foreground/60 tw:hover:text-foreground tw:relative tw:inline-flex tw:h-[calc(100%-1px)] tw:flex-1 tw:items-center tw:justify-center tw:gap-1.5 tw:rounded-md tw:border tw:border-transparent tw:px-2 tw:py-1 tw:text-sm tw:font-medium tw:whitespace-nowrap tw:transition-all tw:group-data-[orientation=vertical]/tabs:w-full tw:group-data-[orientation=vertical]/tabs:justify-start tw:focus-visible:ring-[3px] tw:focus-visible:outline-1 tw:disabled:pointer-events-none tw:disabled:opacity-50 tw:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm tw:group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4', - 'tw:group-data-[variant=line]/tabs-list:bg-transparent tw:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent', - 'tw:data-[state=active]:bg-background tw:data-[state=active]:text-foreground', - 'tw:after:bg-foreground tw:after:absolute tw:after:opacity-0 tw:after:transition-opacity tw:group-data-[orientation=horizontal]/tabs:after:inset-x-0 tw:group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] tw:group-data-[orientation=horizontal]/tabs:after:h-0.5 tw:group-data-[orientation=vertical]/tabs:after:inset-y-0 tw:group-data-[orientation=vertical]/tabs:after:-right-1 tw:group-data-[orientation=vertical]/tabs:after:w-0.5 tw:group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100', - className + "tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:focus-visible:outline-ring tw:text-foreground/60 tw:hover:text-foreground tw:relative tw:inline-flex tw:h-[calc(100%-1px)] tw:flex-1 tw:items-center tw:justify-center tw:gap-1.5 tw:rounded-md tw:border tw:border-transparent tw:px-2 tw:py-1 tw:text-sm tw:font-medium tw:whitespace-nowrap tw:transition-all tw:group-data-[orientation=vertical]/tabs:w-full tw:group-data-[orientation=vertical]/tabs:justify-start tw:focus-visible:ring-[3px] tw:focus-visible:outline-1 tw:disabled:pointer-events-none tw:disabled:opacity-50 tw:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm tw:group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none tw:[&_svg]:pointer-events-none tw:[&_svg]:shrink-0 tw:[&_svg:not([class*=size-])]:size-4", + "tw:group-data-[variant=line]/tabs-list:bg-transparent tw:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", + "tw:data-[state=active]:bg-background tw:data-[state=active]:text-foreground", + "tw:after:bg-foreground tw:after:absolute tw:after:opacity-0 tw:after:transition-opacity tw:group-data-[orientation=horizontal]/tabs:after:inset-x-0 tw:group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] tw:group-data-[orientation=horizontal]/tabs:after:h-0.5 tw:group-data-[orientation=vertical]/tabs:after:inset-y-0 tw:group-data-[orientation=vertical]/tabs:after:-right-1 tw:group-data-[orientation=vertical]/tabs:after:w-0.5 tw:group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", + className, )} {...props} /> ); } -function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) { +function TabsContent({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Content>) { return ( <TabsPrimitive.Content data-slot="tabs-content" - className={cn('tw:flex-1 tw:outline-none', className)} + className={cn("tw:flex-1 tw:outline-none", className)} {...props} /> ); diff --git a/website/src/components/ui/textarea.tsx b/website/src/components/ui/textarea.tsx index d81ce41b1..5a9e6ceb9 100644 --- a/website/src/components/ui/textarea.tsx +++ b/website/src/components/ui/textarea.tsx @@ -1,13 +1,13 @@ -import * as React from 'react'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { cn } from "@/lib/utils"; -function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { +function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { return ( <textarea data-slot="textarea" className={cn( - 'tw:border-input tw:placeholder:text-muted-foreground tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive tw:flex tw:field-sizing-content tw:min-h-16 tw:w-full tw:rounded-md tw:border tw:px-3 tw:py-2 tw:text-base tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none tw:focus-visible:ring-[3px] tw:disabled:cursor-not-allowed tw:disabled:opacity-50 tw:md:text-sm', - className + "tw:border-input tw:placeholder:text-muted-foreground tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:aria-invalid:ring-destructive/20 tw:aria-invalid:border-destructive tw:flex tw:field-sizing-content tw:min-h-16 tw:w-full tw:rounded-md tw:border tw:px-3 tw:py-2 tw:text-base tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none tw:focus-visible:ring-[3px] tw:disabled:cursor-not-allowed tw:disabled:opacity-50 tw:md:text-sm", + className, )} {...props} /> diff --git a/website/src/components/ui/tooltip.tsx b/website/src/components/ui/tooltip.tsx index 026fb6568..0fc5d8628 100644 --- a/website/src/components/ui/tooltip.tsx +++ b/website/src/components/ui/tooltip.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; -import { Tooltip as TooltipPrimitive } from 'radix-ui'; -import { cn } from '@/lib/utils'; +import * as React from "react"; +import { Tooltip as TooltipPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; function TooltipProvider({ delayDuration = 0, @@ -15,11 +15,15 @@ function TooltipProvider({ ); } -function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) { +function Tooltip({ + ...props +}: React.ComponentProps<typeof TooltipPrimitive.Root>) { return <TooltipPrimitive.Root data-slot="tooltip" {...props} />; } -function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { +function TooltipTrigger({ + ...props +}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />; } @@ -35,8 +39,8 @@ function TooltipContent({ data-slot="tooltip-content" sideOffset={sideOffset} className={cn( - 'tw:bg-foreground tw:text-background tw:animate-in tw:fade-in-0 tw:zoom-in-95 tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=closed]:zoom-out-95 tw:data-[side=bottom]:slide-in-from-top-2 tw:data-[side=left]:slide-in-from-right-2 tw:data-[side=right]:slide-in-from-left-2 tw:data-[side=top]:slide-in-from-bottom-2 tw:z-50 tw:w-fit tw:origin-(--radix-tooltip-content-transform-origin) tw:rounded-md tw:px-3 tw:py-1.5 tw:text-xs tw:text-balance', - className + "tw:bg-foreground tw:text-background tw:animate-in tw:fade-in-0 tw:zoom-in-95 tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0 tw:data-[state=closed]:zoom-out-95 tw:data-[side=bottom]:slide-in-from-top-2 tw:data-[side=left]:slide-in-from-right-2 tw:data-[side=right]:slide-in-from-left-2 tw:data-[side=top]:slide-in-from-bottom-2 tw:z-50 tw:w-fit tw:origin-(--radix-tooltip-content-transform-origin) tw:rounded-md tw:px-3 tw:py-1.5 tw:text-xs tw:text-balance", + className, )} {...props} > diff --git a/website/src/components/ui/year-picker.story.tsx b/website/src/components/ui/year-picker.story.tsx index 9f19f8ba4..07923e170 100644 --- a/website/src/components/ui/year-picker.story.tsx +++ b/website/src/components/ui/year-picker.story.tsx @@ -1,10 +1,10 @@ "use client"; -import { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; -import { YearPicker } from './year-picker'; +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { YearPicker } from "./year-picker"; const meta: Meta<typeof YearPicker> = { - title: 'UI/YearPicker', + title: "UI/YearPicker", component: YearPicker, decorators: [ (Story) => ( diff --git a/website/src/components/ui/year-picker.tsx b/website/src/components/ui/year-picker.tsx index b305656eb..92e0ebaa4 100644 --- a/website/src/components/ui/year-picker.tsx +++ b/website/src/components/ui/year-picker.tsx @@ -1,10 +1,10 @@ "use client"; -import * as React from 'react'; -import { useState } from 'react'; -import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'; -import { cn } from '@/lib/utils'; -import { Button } from './button'; -import { Popover, PopoverContent, PopoverTrigger } from './popover'; +import * as React from "react"; +import { useState } from "react"; +import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react"; +import { cn } from "@/lib/utils"; +import { Button } from "./button"; +import { Popover, PopoverContent, PopoverTrigger } from "./popover"; interface YearPickerProps { value: Date | null | undefined; @@ -20,12 +20,14 @@ function YearPicker({ onChange, minDate, maxDate, - placeholder = 'Select year', + placeholder = "Select year", className, }: YearPickerProps) { const [open, setOpen] = useState(false); const currentYear = value ? value.getFullYear() : new Date().getFullYear(); - const [decadeStart, setDecadeStart] = useState(Math.floor(currentYear / 10) * 10); + const [decadeStart, setDecadeStart] = useState( + Math.floor(currentYear / 10) * 10, + ); const minYear = minDate ? minDate.getFullYear() : 1900; const maxYear = maxDate ? maxDate.getFullYear() : 2100; @@ -45,9 +47,9 @@ function YearPicker({ <button type="button" className={cn( - 'tw:border-input tw:flex tw:h-9 tw:w-full tw:items-center tw:justify-between tw:rounded-md tw:border tw:px-3 tw:py-2 tw:text-sm tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none', - !value && 'tw:text-muted-foreground', - className + "tw:border-input tw:flex tw:h-9 tw:w-full tw:items-center tw:justify-between tw:rounded-md tw:border tw:px-3 tw:py-2 tw:text-sm tw:shadow-xs tw:transition-[color,box-shadow] tw:outline-none", + !value && "tw:text-muted-foreground", + className, )} > {displayText} @@ -90,12 +92,12 @@ function YearPicker({ disabled={isDisabled} onClick={() => handleSelect(year)} className={cn( - 'tw:h-9 tw:rounded-md tw:text-sm tw:transition-colors', + "tw:h-9 tw:rounded-md tw:text-sm tw:transition-colors", isSelected - ? 'tw:bg-primary tw:text-primary-foreground tw:font-medium' - : 'tw:hover:bg-accent tw:hover:text-accent-foreground', - isOutOfDecade && !isSelected && 'tw:text-muted-foreground', - isDisabled && 'tw:opacity-30 tw:pointer-events-none' + ? "tw:bg-primary tw:text-primary-foreground tw:font-medium" + : "tw:hover:bg-accent tw:hover:text-accent-foreground", + isOutOfDecade && !isSelected && "tw:text-muted-foreground", + isDisabled && "tw:opacity-30 tw:pointer-events-none", )} > {year} diff --git a/website/src/data/supporters.ts b/website/src/data/supporters.ts index 3810b13bb..9299f3107 100644 --- a/website/src/data/supporters.ts +++ b/website/src/data/supporters.ts @@ -123,7 +123,7 @@ export const projects: SupportedProjectData[] = [ currency: "USD", awardDate: "2026-10", description: - "Modelling the revenue and distributional effects of a UK tax on AI inference, for the project \"Token taxes as a mechanism for reducing AI-driven power concentration\". The project is funded through Manifund by grantmaking.ai.", + 'Modelling the revenue and distributional effects of a UK tax on AI inference, for the project "Token taxes as a mechanism for reducing AI-driven power concentration". The project is funded through Manifund by grantmaking.ai.', supporterId: "manifund", }, { @@ -136,7 +136,8 @@ export const projects: SupportedProjectData[] = [ supporterId: "ses", }, { - title: "Advancing evidence-based policy with AI & tax-benefit microsimulation", + title: + "Advancing evidence-based policy with AI & tax-benefit microsimulation", projectUrl: "https://www.nuffieldfoundation.org/project/advancing-evidence-based-policy-with-ai-tax-benefit-microsimulation", amount: 300000, diff --git a/website/src/lib/betterstack.ts b/website/src/lib/betterstack.ts index 920056d3c..2f9088cb4 100644 --- a/website/src/lib/betterstack.ts +++ b/website/src/lib/betterstack.ts @@ -97,7 +97,10 @@ async function fetchAllIncidents( `/api/v3/incidents?monitor_id=${monitorId}&from=${from}&to=${to}&per_page=50`; while (path) { - const data: BetterStackIncidentResponse = await fetchBetterStack(path, token); + const data: BetterStackIncidentResponse = await fetchBetterStack( + path, + token, + ); if (data.data) { incidents.push(...data.data); } @@ -170,7 +173,10 @@ async function fetchMonitorData( token: string, ): Promise<MonitorData> { const [monitorRes, slaRes, incidents] = await Promise.all([ - fetchBetterStack<BetterStackMonitorResponse>(`/api/v2/monitors/${monitorId}`, token), + fetchBetterStack<BetterStackMonitorResponse>( + `/api/v2/monitors/${monitorId}`, + token, + ), fetchBetterStack<BetterStackSlaResponse>( `/api/v2/monitors/${monitorId}/sla?from=${from}&to=${to}`, token, @@ -207,7 +213,9 @@ export async function fetchStatusPageData( const { from, to, dates } = getDateRange(); const monitors = await Promise.all( - monitorIds.map((monitorId) => fetchMonitorData(monitorId, from, to, dates, token)), + monitorIds.map((monitorId) => + fetchMonitorData(monitorId, from, to, dates, token), + ), ); return { diff --git a/website/src/lib/utils.ts b/website/src/lib/utils.ts index 2819a830d..a5ef19350 100644 --- a/website/src/lib/utils.ts +++ b/website/src/lib/utils.ts @@ -1,5 +1,5 @@ -import { clsx, type ClassValue } from 'clsx'; -import { twMerge } from 'tailwind-merge'; +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); diff --git a/website/src/styles/stylesheets/RichTextBlock.css b/website/src/styles/stylesheets/RichTextBlock.css index 6211fa9d7..074a8c356 100644 --- a/website/src/styles/stylesheets/RichTextBlock.css +++ b/website/src/styles/stylesheets/RichTextBlock.css @@ -6,7 +6,7 @@ Inter, -apple-system, BlinkMacSystemFont, - 'Segoe UI', + "Segoe UI", Roboto, sans-serif; font-size: 16px; From 34adfda656ef07e95e45957c44867c24b2b0fb5c Mon Sep 17 00:00:00 2001 From: Max Ghenis <mghenis@gmail.com> Date: Tue, 20 Oct 2026 11:59:56 -0400 Subject: [PATCH 4/9] Check website and calculator formatting in CI - pr.yaml and push.yaml run `bun run format:check` in website/ and calculator-app/, next to each app's ESLint step. - `make lint` and `make format` cover website/ and calculator-app/ as well as app/, and the before-committing guidance says so. - Both ESLint configs ignore public/. It links to app/public, and website/scripts/build.sh swaps in a real copy of those assets while it builds; ESLint run during a build then linted a bundled .js file in that copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .github/workflows/pr.yaml | 8 ++++++++ .github/workflows/push.yaml | 8 ++++++++ Makefile | 4 ++++ calculator-app/eslint.config.mjs | 9 ++++++++- docs/engineering/skills/repository-guidance.md | 6 +++--- website/eslint.config.mjs | 3 +++ 6 files changed, 34 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 1e76a6423..5e5ec70e4 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -35,10 +35,18 @@ jobs: working-directory: ./app run: bun run eslint + - name: Check formatting with Prettier (website) + working-directory: ./website + run: bun run format:check + - name: Run ESLint (website) working-directory: ./website run: bun run lint + - name: Check formatting with Prettier (calculator) + working-directory: ./calculator-app + run: bun run format:check + - name: Run ESLint (calculator) working-directory: ./calculator-app run: bun run lint diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 2ae1e165a..0c4a24bfb 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -28,10 +28,18 @@ jobs: working-directory: ./app run: bun run eslint + - name: Check formatting with Prettier (website) + working-directory: ./website + run: bun run format:check + - name: Run ESLint (website) working-directory: ./website run: bun run lint + - name: Check formatting with Prettier (calculator) + working-directory: ./calculator-app + run: bun run format:check + - name: Run ESLint (calculator) working-directory: ./calculator-app run: bun run lint diff --git a/Makefile b/Makefile index 395940fda..7cfd0a8fd 100644 --- a/Makefile +++ b/Makefile @@ -32,9 +32,13 @@ test: lint: cd app && bun run lint + cd website && bun run lint + cd calculator-app && bun run lint format: cd app && bun run prettier:write + cd website && bun run format + cd calculator-app && bun run format clean: rm -rf website/.next calculator-app/.next app/node_modules node_modules packages/*/node_modules diff --git a/calculator-app/eslint.config.mjs b/calculator-app/eslint.config.mjs index 97f68d912..3819b2ca6 100644 --- a/calculator-app/eslint.config.mjs +++ b/calculator-app/eslint.config.mjs @@ -40,5 +40,12 @@ export default defineConfig([ ], }, }, - globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]), + globalIgnores([ + ".next/**", + "out/**", + "build/**", + // A link to app/public, which holds built assets. + "public/**", + "next-env.d.ts", + ]), ]); diff --git a/docs/engineering/skills/repository-guidance.md b/docs/engineering/skills/repository-guidance.md index ccf333947..5b4077899 100644 --- a/docs/engineering/skills/repository-guidance.md +++ b/docs/engineering/skills/repository-guidance.md @@ -123,6 +123,6 @@ No route embeds a GitHub Pages site as a whole page any more: the 2025 year in r ## Before committing -1. Run `cd app && bun run prettier -- --write .` to format -2. Run `bun run lint` to check for errors -3. CI uses `--max-warnings 0` so fix all warnings +1. Run `make format` to format `app/`, `website/` and `calculator-app/` with Prettier. `app/` has its own Prettier config; `website/` and `calculator-app/` use Prettier's defaults. +2. Run `make lint` to run ESLint (and Stylelint in `app/`) in all three. +3. CI fails on ESLint warnings as well as errors (`--max-warnings 0`), and on files Prettier would reformat, so fix all of them. diff --git a/website/eslint.config.mjs b/website/eslint.config.mjs index 837a5ec26..b74907c8d 100644 --- a/website/eslint.config.mjs +++ b/website/eslint.config.mjs @@ -33,6 +33,9 @@ export default defineConfig([ "out/**", "build/**", "coverage/**", + // A link to app/public; scripts/build.sh swaps in a copy of its built + // assets during a build. + "public/**", "next-env.d.ts", ]), ]); From 68a407d8757aae278bfa890fd2f02b5869e20515 Mon Sep 17 00:00:00 2001 From: Max Ghenis <mghenis@gmail.com> Date: Tue, 20 Oct 2026 12:00:19 -0400 Subject: [PATCH 5/9] Have git blame skip the Prettier reformat Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .git-blame-ignore-revs | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..076b30810 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# Commits that only reformat code. To have `git blame` skip them, run +# git config blame.ignoreRevsFile .git-blame-ignore-revs + +# Format the website and calculator with Prettier +e8d04db611dc231e6c27fe178893f55e3bfc0435 From ccc108c389a42088e8d4b5484a37b392fe97e791 Mon Sep 17 00:00:00 2001 From: Max Ghenis <mghenis@gmail.com> Date: Tue, 20 Oct 2026 12:00:28 -0400 Subject: [PATCH 6/9] Typecheck and test the website and calculator on pushes to main push.yaml's typecheck job checked only app/ and its test job ran only app/'s tests, while pr.yaml also typechecks website/ and calculator-app/ and runs website/'s tests. Every job in push.yaml now has the same steps as in pr.yaml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .github/workflows/push.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 0c4a24bfb..cabe44e29 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -58,10 +58,18 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Run TypeScript type checks + - name: Run TypeScript type checks (app) working-directory: ./app run: bun run typecheck + - name: Run TypeScript type checks (website) + working-directory: ./website + run: bun run typecheck + + - name: Typecheck calculator + working-directory: ./calculator-app + run: bun run typecheck + test: name: Tests runs-on: ubuntu-latest @@ -80,6 +88,10 @@ jobs: working-directory: ./app run: bun run test + - name: Run website tests + working-directory: ./website + run: bun run test + check-embeds: name: Check embedded site URLs runs-on: ubuntu-latest From 0183ac7ad5d243bb540e78a96dccf3b266f64800 Mon Sep 17 00:00:00 2001 From: Max Ghenis <mghenis@gmail.com> Date: Tue, 20 Oct 2026 12:16:55 -0400 Subject: [PATCH 7/9] Keep the link rule and Prettier style independent of parent folders From review: - @next/eslint-plugin-next globs settings.next.rootDir, so an absolute path under a folder named like "Projects (1)" or "a{b,c}" matched nothing and @next/next/no-html-link-for-pages switched itself off again, without failing lint. Both configs now bracket (, ), { and } in the path. A new website test fails if the rule stops reporting a plain <a> link to a website page; it fails with the old "website/". - Prettier merges .editorconfig settings under its config, and it stops looking for them only at a .git folder. A worktree's .git is a file, so a home-folder .editorconfig could change indentation, quotes, width or line endings. The website and calculator configs now set Prettier's defaults for the five options EditorConfig can set, and app/'s config sets the two it left open (useTabs, tabWidth). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- app/.prettierrc.mjs | 3 +++ calculator-app/eslint.config.mjs | 12 ++++++++--- calculator-app/prettier.config.mjs | 15 ++++++++++--- website/eslint.config.mjs | 13 ++++++++---- website/prettier.config.mjs | 15 ++++++++++--- .../__tests__/config/eslint-config.test.ts | 21 +++++++++++++++++++ 6 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 website/src/__tests__/config/eslint-config.test.ts diff --git a/app/.prettierrc.mjs b/app/.prettierrc.mjs index 9a3ddde31..f2da58a96 100644 --- a/app/.prettierrc.mjs +++ b/app/.prettierrc.mjs @@ -1,5 +1,8 @@ /** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */ const config = { + // Set explicitly so an .editorconfig in a parent folder can't change them. + useTabs: false, + tabWidth: 2, endOfLine: 'lf', printWidth: 100, singleQuote: true, diff --git a/calculator-app/eslint.config.mjs b/calculator-app/eslint.config.mjs index 3819b2ca6..d9c919415 100644 --- a/calculator-app/eslint.config.mjs +++ b/calculator-app/eslint.config.mjs @@ -5,6 +5,12 @@ import { defineConfig, globalIgnores } from "eslint/config"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// @next/eslint-plugin-next reads settings.next.rootDir as a glob. Brackets +// make (, ), { and } literal, so a parent folder named like "Projects (1)" +// doesn't stop the path matching. When rootDir matches nothing, +// @next/next/no-html-link-for-pages switches itself off without failing lint. +const rootDir = __dirname.replace(/[(){}]/g, "[$&]"); + const compat = new FlatCompat({ baseDirectory: __dirname, }); @@ -16,9 +22,9 @@ export default defineConfig([ extends: ["next/core-web-vitals", "next/typescript"], settings: { next: { - // Absolute, so @next/next/no-html-link-for-pages finds src/app - // whichever folder ESLint runs from. - rootDir: __dirname, + // Absolute, so the rule finds src/app whichever folder ESLint runs + // from. + rootDir, }, }, }), diff --git a/calculator-app/prettier.config.mjs b/calculator-app/prettier.config.mjs index 8857869d6..89d239f1c 100644 --- a/calculator-app/prettier.config.mjs +++ b/calculator-app/prettier.config.mjs @@ -1,6 +1,15 @@ -// The calculator uses Prettier's default style. This file stops Prettier from -// looking in parent folders for a config; app/ has its own. +// The calculator uses Prettier's default style; app/ has its own config. +// Having a config file here stops Prettier looking for one in parent +// folders. The options below are Prettier's defaults, written out because +// an .editorconfig in a parent folder (one outside this repo, such as a +// home folder) can otherwise change them. /** @type {import("prettier").Config} */ -const config = {}; +const config = { + useTabs: false, + tabWidth: 2, + printWidth: 80, + singleQuote: false, + endOfLine: "lf", +}; export default config; diff --git a/website/eslint.config.mjs b/website/eslint.config.mjs index b74907c8d..071764e20 100644 --- a/website/eslint.config.mjs +++ b/website/eslint.config.mjs @@ -5,6 +5,12 @@ import { defineConfig, globalIgnores } from "eslint/config"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// @next/eslint-plugin-next reads settings.next.rootDir as a glob. Brackets +// make (, ), { and } literal, so a parent folder named like "Projects (1)" +// doesn't stop the path matching. When rootDir matches nothing, +// @next/next/no-html-link-for-pages switches itself off without failing lint. +const rootDir = __dirname.replace(/[(){}]/g, "[$&]"); + const compat = new FlatCompat({ baseDirectory: __dirname, }); @@ -14,10 +20,9 @@ export default defineConfig([ extends: ["next/core-web-vitals", "next/typescript"], settings: { next: { - // Absolute, so @next/next/no-html-link-for-pages finds src/app - // whichever folder ESLint runs from. A relative "website/" matched - // nothing when run from website/, which turned the rule off. - rootDir: __dirname, + // Absolute, so the rule finds src/app whichever folder ESLint runs + // from. A relative "website/" matched nothing when run from website/. + rootDir, }, }, }), diff --git a/website/prettier.config.mjs b/website/prettier.config.mjs index 539474558..e8f3eccc7 100644 --- a/website/prettier.config.mjs +++ b/website/prettier.config.mjs @@ -1,6 +1,15 @@ -// The website uses Prettier's default style. This file stops Prettier from -// looking in parent folders for a config; app/ has its own. +// The website uses Prettier's default style; app/ has its own config. +// Having a config file here stops Prettier looking for one in parent +// folders. The options below are Prettier's defaults, written out because +// an .editorconfig in a parent folder (one outside this repo, such as a +// home folder) can otherwise change them. /** @type {import("prettier").Config} */ -const config = {}; +const config = { + useTabs: false, + tabWidth: 2, + printWidth: 80, + singleQuote: false, + endOfLine: "lf", +}; export default config; diff --git a/website/src/__tests__/config/eslint-config.test.ts b/website/src/__tests__/config/eslint-config.test.ts new file mode 100644 index 000000000..1e06d1f96 --- /dev/null +++ b/website/src/__tests__/config/eslint-config.test.ts @@ -0,0 +1,21 @@ +// @vitest-environment node +import { fileURLToPath } from "node:url"; +import { ESLint } from "eslint"; +import { describe, expect, test } from "vitest"; + +const websiteDir = fileURLToPath(new URL("../../..", import.meta.url)); + +// @next/next/no-html-link-for-pages switches itself off, without failing +// lint, when settings.next.rootDir in eslint.config.mjs matches no folder. +describe("website ESLint config", () => { + test("reports a plain <a> link to a website page", async () => { + const eslint = new ESLint({ cwd: websiteDir }); + const [result] = await eslint.lintText( + 'export default function Probe() {\n return <a href="/us/research">Research</a>;\n}\n', + { filePath: `${websiteDir}src/app/probe.tsx` }, + ); + expect(result.messages.map((message) => message.ruleId)).toContain( + "@next/next/no-html-link-for-pages", + ); + }, 60_000); +}); From 9fb4375b53d53a92a50e684b55399882087a7acf Mon Sep 17 00:00:00 2001 From: Max Ghenis <mghenis@gmail.com> Date: Tue, 20 Oct 2026 13:06:20 -0400 Subject: [PATCH 8/9] Also bracket | and + in the ESLint rootDir path From review round 2. A parent folder named like "p|q" or "x (y)+" still turned the link rule off: | becomes regex alternation and + after a bracketed ) becomes a quantifier. Checked by globbing through the plugin's getRootDirs under folder names with (, ), {, }, [, ], |, +, !, @, * and ?, and by linting from a copy under "c (d)|e+/". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- calculator-app/eslint.config.mjs | 9 +++++---- website/eslint.config.mjs | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/calculator-app/eslint.config.mjs b/calculator-app/eslint.config.mjs index d9c919415..3fad05d1a 100644 --- a/calculator-app/eslint.config.mjs +++ b/calculator-app/eslint.config.mjs @@ -6,10 +6,11 @@ import { defineConfig, globalIgnores } from "eslint/config"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // @next/eslint-plugin-next reads settings.next.rootDir as a glob. Brackets -// make (, ), { and } literal, so a parent folder named like "Projects (1)" -// doesn't stop the path matching. When rootDir matches nothing, -// @next/next/no-html-link-for-pages switches itself off without failing lint. -const rootDir = __dirname.replace(/[(){}]/g, "[$&]"); +// make (, ), {, }, | and + literal, so a parent folder named like +// "Projects (1)" doesn't stop the path matching. When rootDir matches +// nothing, @next/next/no-html-link-for-pages switches itself off without +// failing lint. +const rootDir = __dirname.replace(/[(){}|+]/g, "[$&]"); const compat = new FlatCompat({ baseDirectory: __dirname, diff --git a/website/eslint.config.mjs b/website/eslint.config.mjs index 071764e20..f83c36d49 100644 --- a/website/eslint.config.mjs +++ b/website/eslint.config.mjs @@ -6,10 +6,11 @@ import { defineConfig, globalIgnores } from "eslint/config"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // @next/eslint-plugin-next reads settings.next.rootDir as a glob. Brackets -// make (, ), { and } literal, so a parent folder named like "Projects (1)" -// doesn't stop the path matching. When rootDir matches nothing, -// @next/next/no-html-link-for-pages switches itself off without failing lint. -const rootDir = __dirname.replace(/[(){}]/g, "[$&]"); +// make (, ), {, }, | and + literal, so a parent folder named like +// "Projects (1)" doesn't stop the path matching. When rootDir matches +// nothing, @next/next/no-html-link-for-pages switches itself off without +// failing lint. +const rootDir = __dirname.replace(/[(){}|+]/g, "[$&]"); const compat = new FlatCompat({ baseDirectory: __dirname, From 53ce3c851cfead7797fd4e6d5424a3b04845f6ad Mon Sep 17 00:00:00 2001 From: Max Ghenis <mghenis@gmail.com> Date: Tue, 20 Oct 2026 13:15:08 -0400 Subject: [PATCH 9/9] Bracket [ in the ESLint rootDir path too From the delta review: an unmatched [ anywhere in the path, together with a + that is now bracketed, made the glob match nothing ("a[b+c"). Bracketing [ as well fixed those and names such as "[a-z]" and "[a+b]", and kept "Backup [old]", "x [1]", "a]b[c" and "a[]b" working. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- calculator-app/eslint.config.mjs | 4 ++-- website/eslint.config.mjs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/calculator-app/eslint.config.mjs b/calculator-app/eslint.config.mjs index 3fad05d1a..db99e8084 100644 --- a/calculator-app/eslint.config.mjs +++ b/calculator-app/eslint.config.mjs @@ -6,11 +6,11 @@ import { defineConfig, globalIgnores } from "eslint/config"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // @next/eslint-plugin-next reads settings.next.rootDir as a glob. Brackets -// make (, ), {, }, | and + literal, so a parent folder named like +// make (, ), {, }, |, + and [ literal, so a parent folder named like // "Projects (1)" doesn't stop the path matching. When rootDir matches // nothing, @next/next/no-html-link-for-pages switches itself off without // failing lint. -const rootDir = __dirname.replace(/[(){}|+]/g, "[$&]"); +const rootDir = __dirname.replace(/[(){}|+[]/g, "[$&]"); const compat = new FlatCompat({ baseDirectory: __dirname, diff --git a/website/eslint.config.mjs b/website/eslint.config.mjs index f83c36d49..6be06c078 100644 --- a/website/eslint.config.mjs +++ b/website/eslint.config.mjs @@ -6,11 +6,11 @@ import { defineConfig, globalIgnores } from "eslint/config"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // @next/eslint-plugin-next reads settings.next.rootDir as a glob. Brackets -// make (, ), {, }, | and + literal, so a parent folder named like +// make (, ), {, }, |, + and [ literal, so a parent folder named like // "Projects (1)" doesn't stop the path matching. When rootDir matches // nothing, @next/next/no-html-link-for-pages switches itself off without // failing lint. -const rootDir = __dirname.replace(/[(){}|+]/g, "[$&]"); +const rootDir = __dirname.replace(/[(){}|+[]/g, "[$&]"); const compat = new FlatCompat({ baseDirectory: __dirname,