diff --git a/.github/prompts/assets/pull-request-template.md b/.claude/commands/assets/pull-request-template.md similarity index 100% rename from .github/prompts/assets/pull-request-template.md rename to .claude/commands/assets/pull-request-template.md diff --git a/.claude/commands/open-pr.md b/.claude/commands/open-pr.md new file mode 100644 index 00000000..fdfd3daf --- /dev/null +++ b/.claude/commands/open-pr.md @@ -0,0 +1,24 @@ +Open a pull request from the current branch changes. + +$ARGUMENTS is an optional mode. Use `draft` to open a draft pull request. + +Workflow: + +1. Collect context from git: + - Branch name. + - Commits ahead of `dev`. + - Changed files. +2. Set base branch to `dev`. +3. Summarise what changed, why, context, risks, and manual testing evidence. +4. Produce a pull request body using this exact template and section order: + `.claude/commands/assets/pull-request-template.md` +5. If any section details are unknown, write "N/A". +6. Do not remove sections. +7. Ensure the branch is pushed to origin. +8. Decide draft mode from $ARGUMENTS: + - If `draft`, create a draft pull request. + - Otherwise, create a ready-for-review pull request. +9. Verify `gh` is installed and authenticated. +10. Open the pull request using `gh pr create` with explicit title and body, + always with `--base dev`. Use `--draft` only when mode is `draft`. +11. Return only the created pull request URL. diff --git a/.claude/commands/upgrade-deps.md b/.claude/commands/upgrade-deps.md new file mode 100644 index 00000000..42e1d489 --- /dev/null +++ b/.claude/commands/upgrade-deps.md @@ -0,0 +1,36 @@ +Upgrade the provided dependencies. + +$ARGUMENTS is a list of package names to upgrade, one per line or +comma-separated. + +Workflow: + +1. Read `pnpm-workspace.yaml`. +2. Run `pnpm -r outdated` and keep only selected packages. +3. For each selected package, fetch the changelog or release notes and extract + only relevant changes between current and target versions. +4. Present the changelog summary in this exact format for every package: + + ``` + Package: ( -> ) + + - Changelog: + - Relevant fixes: + - + - Relevant features: + - + - Breaking changes: + - | + - Risk level: - + - Recommendation: + ``` + + If an item has no fixes or features, write `- `. + +5. Ask for explicit confirmation before any file edit. +6. Only if confirmed, update `pnpm-workspace.yaml` for the selected packages and + run `pnpm i`. +7. Report bumped packages, changed files, and install warnings. + +Follow the dependency upgrade guidelines in +`.github/instructions/dependency-upgrades.instructions.md`. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..4ef867c8 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,30 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "dir=\"$(dirname \"$CLAUDE_TOOL_INPUT_FILE_PATH\")\"; while [[ ! -f \"$dir/package.json\" ]] && [[ \"$dir\" != \"/\" ]]; do dir=\"$(dirname \"$dir\")\"; done; [[ -f \"$dir/package.json\" ]] && cd \"$dir\" && pnpm exec prettier --write --log-level warn \"$CLAUDE_TOOL_INPUT_FILE_PATH\"" + }, + { + "type": "command", + "command": "if [[ \"$CLAUDE_TOOL_INPUT_FILE_PATH\" =~ \\.(js|ts|tsx)$ ]]; then dir=\"$(dirname \"$CLAUDE_TOOL_INPUT_FILE_PATH\")\"; while [[ ! -f \"$dir/package.json\" ]] && [[ \"$dir\" != \"/\" ]]; do dir=\"$(dirname \"$dir\")\"; done; [[ -f \"$dir/package.json\" ]] && cd \"$dir\" && pnpm exec eslint --cache --cache-location ./node_modules/.cache/eslint --fix --max-warnings 0 \"$CLAUDE_TOOL_INPUT_FILE_PATH\"; fi" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "if [[ \"$CLAUDE_TOOL_INPUT_FILE_PATH\" == *\".env\"* ]]; then echo 'Editing .env files is not allowed. Ask the user to update environment variables manually.' && exit 2; fi" + } + ] + } + ] + } +} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 404d0001..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,16 +0,0 @@ -# Project guidelines - -## General writing style - -- Prefer British English spelling in prose (for example: "synchronise", - "synchronisation"). -- Keep language simple, direct, and concise. - -## Pull request drafting - -- When asked to open, prepare, or draft a pull request, always use the template - in `.github/prompts/assets/pull-request-template.md`. -- Keep section headings and order exactly as written in that template. -- If information is missing, write "N/A" rather than removing sections. -- This template is for agent output only and is not a mandatory developer - template. diff --git a/.github/instructions/architecture.instructions.md b/.github/instructions/architecture.instructions.md deleted file mode 100644 index b613a731..00000000 --- a/.github/instructions/architecture.instructions.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -description: - "Use when editing app or lib source files. Covers the main workspace structure - and package responsibilities." -applyTo: "apps/**, libs/**, scripts/**" ---- - -# Architecture guidelines - -## Workspace structure - -- `apps/*` contains deployable applications. -- `libs/*` contains shared workspace packages used by the apps. -- `scripts/` contains workspace-level tooling. - -## Package responsibilities - -- Keep app-specific entry points, routes, and composition inside `apps/*`. -- Keep reusable domain logic, UI primitives, and shared utilities inside - `libs/*`. -- `libs/prisma` owns Prisma-related code and generated client integration for - the workspace. - -## Build model - -- Shared libs are generally consumed from their built output. -- Changes to shared libs may require synchronisation into injected workspace - packages. -- See: docs/decisions/002-workspace-package-injection.md diff --git a/.github/instructions/code-comments.instructions.md b/.github/instructions/code-comments.instructions.md deleted file mode 100644 index f81e8589..00000000 --- a/.github/instructions/code-comments.instructions.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -description: - "Use when creating or editing source code files. Covers the required style for - See reference lines in code comments." -applyTo: "**/*.{js,ts,tsx}, **/Dockerfile, **/*.yml" ---- - -# Code comment guidelines - -## See reference lines - -- For "See" reference lines in code comments, use this style: - - `See: path/to/file` -- In "See" reference lines, do not wrap the path in backticks. diff --git a/.github/instructions/dependency-upgrades.instructions.md b/.github/instructions/dependency-upgrades.instructions.md deleted file mode 100644 index 955fef98..00000000 --- a/.github/instructions/dependency-upgrades.instructions.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -description: - "Use when editing dependencies, pnpm-workspace.yaml, package.json dependency - fields. Covers workspace package injection and safe step-by-step dependency - changes." -applyTo: "pnpm-workspace.yaml, **/package.json" ---- - -# Dependency upgrade guidelines - -## Workspace dependency model - -- Workspace packages are injected, not symlinked. -- Keep `injectWorkspacePackages: true`. -- Keep `dedupeInjectedDeps: false`. -- Keep `syncInjectedDepsAfterScripts` pointing to `sync-workspace`. -- See: docs/decisions/002-workspace-package-injection.md - -## Package manifest rules - -- Use `workspace:*` for internal dependencies. -- Use `catalog:` for third-party dependencies managed at workspace level. -- For libs that are consumed from `build/`, keep the `files` field aligned with - the published build output. -- For injected libs, keep the `sync-workspace` hook and the scripts that trigger - it. - -## Upgrade strategy - -- Prefer step-by-step dependency changes over broad workspace-wide upgrades, - unless the upgrade is a patch release or a clearly simple minor release. -- Keep peer dependency changes scoped and explicit. -- When changing shared lib dependencies, consider the consuming apps before - aligning versions. diff --git a/.github/instructions/docs-writing.instructions.md b/.github/instructions/docs-writing.instructions.md deleted file mode 100644 index f01be488..00000000 --- a/.github/instructions/docs-writing.instructions.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -description: - "Use when creating or editing documentation and markdown files. Covers heading - case, prose style, shorthand policy, and markdown formatting conventions." -applyTo: "**/*.md" ---- - -# Documentation writing guidelines - -## Writing style - -- Use sentence case for document and section headings. - -## Naming and shorthand - -- Avoid shortened words in prose when a clear full word is available. -- Allowed shorthand in prose: "apps" and "libs". - -## Markdown formatting - -- Wrap file paths and script names in backticks in prose and markdown text. -- Prefer reference-style markdown links, with link references defined at the - bottom of the file. -- Sort link references alphabetically by reference name. - -## Documentation changes - -- When updating ADRs or docs, preserve existing meaning and improve clarity. -- Prefer fluid paragraphs over fragmented bullet points unless a list improves - readability. diff --git a/.github/instructions/package-scripts.instructions.md b/.github/instructions/package-scripts.instructions.md deleted file mode 100644 index 5bf4bfa3..00000000 --- a/.github/instructions/package-scripts.instructions.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -description: - "Use when creating or editing package.json scripts. Covers the script naming - conventions shared by apps, libs, and workspace packages." -applyTo: "**/package.json" ---- - -# Package script guidelines - -## Script naming - -- Keep the existing script prefixes and meanings. -- Use `build` for the production build entry point. -- Use `build:*` for build subtasks. -- Use `prebuild` for cleanup or generation that must happen before `build`. -- Use `dev` for the long-running local workflow. -- Use `dev:*` for long-running subtasks started by `dev`. -- Use `generate` for one-off asset or code generation. -- Use `generate:*` for generation subtasks. -- Use `clean` to remove generated outputs. -- Use `lint` for read-only validation checks. -- Use `lint-fix` for safe automatic fixes. -- Use `start` for the production runtime entry point. -- Use `docker-build` and `docker-start` for local container workflows when - needed. - -## Script composition - -- Prefer `run-s` for sequential orchestration and `run-p` for parallel - orchestration. -- When a script runs multiple subtasks, keep `--print-label` so output stays - readable. -- For long-running parallel `dev` or `generate` workflows, keep - `--continue-on-error` unless a task must fail fast. - -## Workspace injection hooks - -- For libs that publish only `build/`, keep `postbuild` calling - `pnpm sync-workspace`. -- For watch mode, use `dev:sync-workspace` to watch `build/` and trigger - `sync-workspace`. -- Keep `sync-workspace` as the dedicated hook script used by - `pnpm-workspace.yaml`. diff --git a/.github/instructions/source-editing.instructions.md b/.github/instructions/source-editing.instructions.md deleted file mode 100644 index c0b3b6d1..00000000 --- a/.github/instructions/source-editing.instructions.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -description: - "Use when creating or editing source code files. Covers common-sense editing - constraints, safe defaults, and refactor expectations for this workspace." -applyTo: "**/*.{js,ts,tsx}, **/Dockerfile, **/*.yml" ---- - -# Source editing guidelines - -## General approach - -- Prefer small, focused changes that solve the root cause. -- Preserve existing behaviour unless the task explicitly asks for a behaviour - change. -- Preserve public APIs and script names unless the task requires changing them. - -## Opportunistic cleanups - -- Opportunistic cleanups are allowed. -- Keep them small, local, and low risk. -- Limit them to the code you are already touching or to directly related dead - code. -- Do not mix broad refactors with functional changes unless the task asks for - both. - -## Generated outputs - -- Do not edit generated outputs by hand. -- Treat `build/`, `public/build/`, and `src/generated/` as generated unless the - task is specifically about generation. -- Prefer changing the source, generator, or script that produces generated - files. - -## Shared code - -- When a change affects multiple apps, prefer fixing the shared lib instead of - duplicating changes in each app. -- Keep app-specific composition inside `apps/*` and reusable logic inside - `libs/*`. diff --git a/.github/prompts/open-pr.prompt.md b/.github/prompts/open-pr.prompt.md deleted file mode 100644 index e8cf7666..00000000 --- a/.github/prompts/open-pr.prompt.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: open-pr -description: Open a pull request using the stable agent template -agent: agent -model: GPT-5.3-Codex -tools: - - execute/runInTerminal -argument-hint: Optional mode, use `draft` to open a draft pull request ---- - -Open a pull request from the current branch changes. - -Input: - -- ${input:mode:Optional mode. Use `draft` for a draft pull request} - -Workflow: - -1. Collect context from git: - - Branch name. - - Commits ahead of `dev`. - - Changed files. -2. Set base branch to `dev`. -3. Summarise what changed, why, context, risks, and manual testing evidence. -4. Produce a pull request body using this exact template and section order: - `.github/prompts/assets/pull-request-template.md` -5. If any section details are unknown, write "N/A". -6. Do not remove sections. -7. Ensure the branch is pushed to origin. -8. Decide draft mode from input: - - If `mode` is `draft`, create a draft pull request. - - If `mode` is missing or any other value, create a ready-for-review pull - request. -9. Open the pull request with GitHub CLI using the generated title and body. -10. Return only the created pull request URL. - -Command requirements: - -1. Verify `gh` is installed and authenticated before creating the pull request. -2. Create the pull request using `gh pr create` with explicit title and body. -3. Always use `--base dev`. -4. Use `--draft` only when `mode` is `draft`. - -Output format: - -```md -PR URL: -``` diff --git a/.github/prompts/upgrade-deps.prompt.md b/.github/prompts/upgrade-deps.prompt.md deleted file mode 100644 index b72f2c20..00000000 --- a/.github/prompts/upgrade-deps.prompt.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: upgrade-deps -description: - Upgrade selected dependencies after changelog review and confirmation -agent: agent -model: GPT-5.3-Codex -tools: - - web/fetch - - execute/runInTerminal - - edit/editFiles -argument-hint: List packages to upgrade ---- - -Upgrade the provided dependencies. - -Input: - -- ${input:packagesToUpgrade:Package names, one per line or comma-separated} - -Workflow: - -1. Read [`pnpm-workspace.yaml`][pnpm-workspace]. -2. Run `pnpm -r outdated` and keep only selected packages. -3. For each selected package, fetch changelog or release notes with - #tool:web/fetch and extract only relevant changes between current and target - versions. -4. Present the changelog summary in this exact format for every package: - - ```md - Package: ( -> ) - - - Changelog: - - Relevant fixes: - - - - - - Relevant features: - - - - - - Breaking changes: - - | - - Risk level: - - - Recommendation: - ``` - - If an item has no fixes or features, write `- `. - -5. Ask for explicit confirmation before any file edit, for example: "Proceed - with these version bumps?" -6. Only if confirmed, update [`pnpm-workspace.yaml`][pnpm-workspace] for the - selected packages and run `pnpm i`. -7. Report bumped packages, changed files, and install warnings. - -Reference: - -- [Dependency upgrade guidelines][dependency-upgrade-guidelines] - -[dependency-upgrade-guidelines]: - ../instructions/dependency-upgrades.instructions.md -[pnpm-workspace]: ../../pnpm-workspace.yaml diff --git a/.github/workflows/admin.deploy.yml b/.github/workflows/admin.deploy.yml index 3b8013f0..23e21a85 100644 --- a/.github/workflows/admin.deploy.yml +++ b/.github/workflows/admin.deploy.yml @@ -63,7 +63,7 @@ jobs: - name: βŽ” Setup node uses: actions/setup-node@v4 with: - node-version: 22.12 + node-version: 22.13 cache: "pnpm" - name: πŸ“₯ Download deps diff --git a/.github/workflows/show.deploy.yml b/.github/workflows/show.deploy.yml index 6ad3cecd..797934ff 100644 --- a/.github/workflows/show.deploy.yml +++ b/.github/workflows/show.deploy.yml @@ -63,7 +63,7 @@ jobs: - name: βŽ” Setup node uses: actions/setup-node@v4 with: - node-version: 22.12 + node-version: 22.13 cache: "pnpm" - name: πŸ“₯ Download deps diff --git a/.github/workflows/website.deploy.yml b/.github/workflows/website.deploy.yml index 559b6996..f3ca7aca 100644 --- a/.github/workflows/website.deploy.yml +++ b/.github/workflows/website.deploy.yml @@ -63,7 +63,7 @@ jobs: - name: βŽ” Setup node uses: actions/setup-node@v4 with: - node-version: 22.12 + node-version: 22.13 cache: "pnpm" - name: πŸ“₯ Download deps diff --git a/.github/workflows/workspace.checks.yml b/.github/workflows/workspace.checks.yml index b5a3c7bf..f36586c2 100644 --- a/.github/workflows/workspace.checks.yml +++ b/.github/workflows/workspace.checks.yml @@ -25,7 +25,7 @@ jobs: - name: βŽ” Setup node uses: actions/setup-node@v4 with: - node-version: 22.12 + node-version: 22.13 cache: "pnpm" - name: πŸ“₯ Download deps diff --git a/.gitignore b/.gitignore index 8bf7e5db..67132b6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Claude Code worktrees created for agentic tasks. +/.claude/worktrees/ + # Environment variable files with local overrides. /.env*.local diff --git a/.nvmrc b/.nvmrc index 35d2d08e..fb0a1355 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.12 +22.13 diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c5cd83d..629745e6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,9 @@ { "pattern": "./apps/*/" }, { "pattern": "./libs/*/" } ], + "files.associations": { + "*.css": "tailwindcss" + }, "js/ts.tsdk.path": "node_modules/typescript/lib", "prettier.ignorePath": ".prettierignore.ide", "search.exclude": { @@ -16,8 +19,17 @@ }, "tailwindCSS.classFunctions": ["cn"], "tailwindCSS.experimental.configFile": { - "apps/admin/tailwind.config.ts": "apps/admin/**", + "apps/admin/src/styles/main.css": "apps/admin/**", "apps/website/tailwind.config.ts": "apps/website/**", - "apps/show/tailwind.config.ts": "apps/show/**" - } + "apps/show/src/styles/main.css": "apps/show/**", + "libs/tailwindcss-peekaboo/storybook/styles.css": "libs/tailwindcss-peekaboo/**" + }, + + // To avoid confusion with ESLint rule `no-conflicting-classes`. + // https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-conflicting-classes.md + "tailwindCSS.lint.cssConflict": "ignore", + + // To avoid confusion with ESLint rule `enforce-canonical-classes`. + // https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/enforce-canonical-classes.md + "tailwindCSS.lint.suggestCanonicalClasses": "ignore" } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..6fa9ee7f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,98 @@ +# Project guidelines + +## General writing style + +- Prefer British English spelling in prose (for example: "synchronise", + "synchronisation"). +- Keep language simple, direct, and concise. + +## Commit messages + +Follow `docs/commit-message-guidelines.md` for all commits. + +Agents must always include a `body`. + +## Pull requests + +When asked to open, prepare, or draft a pull request, use `/open-pr`. + +## Workspace structure + +- `apps/*` β€” deployable applications. +- `libs/*` β€” shared workspace packages used by the apps. +- `scripts/` β€” workspace-level tooling. + +Keep app-specific entry points, routes, and composition inside `apps/*`. Keep +reusable domain logic, UI primitives, and shared utilities inside `libs/*`. +`libs/prisma` owns Prisma-related code and generated client integration for the +workspace. + +Shared libs are consumed from their built output. Changes to shared libs may +require synchronisation into injected workspace packages. + +See: docs/decisions/002-workspace-package-injection.md + +## Source editing + +- Prefer small, focused changes that solve the root cause. +- Preserve existing behaviour unless the task explicitly asks for a behaviour + change. +- Preserve public APIs and script names unless the task requires changing them. +- Do not edit generated outputs by hand. Treat `build/`, `public/build/`, and + `src/generated/` as generated unless the task is specifically about + generation. +- When a change affects multiple apps, prefer fixing the shared lib instead of + duplicating changes in each app. +- Opportunistic cleanups are allowed. Keep them small, local, and low risk. + Limit them to the code you are already touching or to directly related dead + code. Do not mix broad refactors with functional changes unless the task asks + for both. + +## Code comments + +For "See" reference lines in code comments, use this style: + +- `See: path/to/file` + +Do not wrap the path in backticks. + +## Package scripts + +- Use `build` for the production build entry point, `build:*` for subtasks, + `prebuild` for pre-build cleanup or generation. +- Use `dev` for the long-running local workflow, `dev:*` for subtasks. +- Use `generate` / `generate:*` for one-off asset or code generation. +- Use `clean` to remove generated outputs. +- Use `lint` for read-only validation, `lint-fix` for safe automatic fixes. +- Use `start` for the production runtime entry point. +- Prefer `run-s` for sequential orchestration and `run-p` for parallel + orchestration. Keep `--print-label` when running multiple subtasks. +- For libs that publish only `build/`, keep `postbuild` calling + `pnpm sync-workspace`. For watch mode, use `dev:sync-workspace`. + +## Dependency upgrades + +- Use `workspace:*` for internal dependencies. +- Use `catalog:` for third-party dependencies managed at workspace level. +- Keep `injectWorkspacePackages: true`, `dedupeInjectedDeps: false`, and + `syncInjectedDepsAfterScripts` pointing to `sync-workspace`. +- Prefer step-by-step dependency changes over broad workspace-wide upgrades, + unless the upgrade is a patch or a clearly simple minor release. +- When changing shared lib dependencies, consider the consuming apps before + aligning versions. + +See: docs/decisions/002-workspace-package-injection.md + +## Documentation + +- Use sentence case for document and section headings. +- Avoid shortened words in prose when a clear full word is available. Allowed + shorthand: "apps" and "libs". +- Wrap file paths and script names in backticks in prose and markdown text. +- Prefer reference-style markdown links. Define link references at the bottom of + the file, sorted alphabetically by reference name. +- For internal section links in long planning documents, define each reference + directly below the target heading. +- When updating ADRs or docs, preserve existing meaning and improve clarity. +- Prefer fluid paragraphs over fragmented bullet points unless a list improves + readability. diff --git a/apps/admin/Dockerfile b/apps/admin/Dockerfile index ffff74bc..3460a502 100644 --- a/apps/admin/Dockerfile +++ b/apps/admin/Dockerfile @@ -1,5 +1,5 @@ # Base stage for all subsequent stages. -FROM node:22.12-bullseye-slim AS base +FROM node:22.13-bullseye-slim AS base # Needed for Prisma generation and runtime. RUN apt-get update && apt-get install -y openssl @@ -15,6 +15,9 @@ RUN corepack enable # See https://github.com/nodejs/corepack/issues/612#issuecomment-2630469508 RUN npm i -g corepack@latest +# Pin the pnpm version to match `packageManager` in package.json. +RUN corepack prepare pnpm@11.1.2 --activate + WORKDIR /workspace # Populate pnpm store with all dependencies (prod and dev). @@ -32,17 +35,22 @@ RUN rm -rf node_modules FROM base AS packages-json COPY package.json ./ +COPY apps/admin/package.json apps/admin/ +COPY apps/show/package.json apps/show/ +COPY apps/website/package.json apps/website/ COPY libs/core/package.json libs/core/ COPY libs/dev-tools/package.json libs/dev-tools/ COPY libs/file-storage/package.json libs/file-storage/ +COPY libs/files-io/package.json libs/files-io/ COPY libs/form-data/package.json libs/form-data/ COPY libs/password/package.json libs/password/ COPY libs/prisma/package.json libs/prisma/ COPY libs/react-primitives/package.json libs/react-primitives/ COPY libs/search-params-io/package.json libs/search-params-io/ COPY libs/tailwind-animation/package.json libs/tailwind-animation/ +COPY libs/tailwindcss-peekaboo/package.json libs/tailwindcss-peekaboo/ COPY libs/zod-utils/package.json libs/zod-utils/ -COPY apps/admin/package.json apps/admin/ +COPY scripts/package.json scripts/ # App production build. FROM base AS build @@ -61,13 +69,13 @@ COPY libs/password libs/password COPY libs/prisma libs/prisma COPY libs/react-primitives libs/react-primitives COPY libs/search-params-io libs/search-params-io -COPY libs/tailwind-animation libs/tailwind-animation +COPY libs/tailwindcss-peekaboo libs/tailwindcss-peekaboo COPY libs/zod-utils libs/zod-utils COPY apps/admin apps/admin # Synchronise injected workspace packages after copying source code. # See: docs/decisions/002-workspace-package-injection.md -RUN pnpm install --frozen-lockfile --offline +RUN pnpm -r sync-workspace # Build libs and app. RUN pnpm --filter admin... build @@ -105,6 +113,6 @@ COPY libs/prisma/schema.prisma libs/prisma/schema.prisma # Synchronise injected workspace packages after copying build artifacts. # See: docs/decisions/002-workspace-package-injection.md -RUN pnpm install --prod --frozen-lockfile --offline +RUN pnpm -r sync-workspace CMD ["pnpm", "--filter", "admin", "start"] diff --git a/apps/admin/eslint.config.js b/apps/admin/eslint.config.js index c28c06bb..b6765fe1 100644 --- a/apps/admin/eslint.config.js +++ b/apps/admin/eslint.config.js @@ -1 +1,5 @@ -export { default } from "@animeaux/dev-tools/eslint" +import { createConfig } from "@animeaux/dev-tools/eslint" + +export default createConfig({ + tailwindStylesheet: "./src/styles/main.css", +}) diff --git a/apps/admin/package.json b/apps/admin/package.json index faeaad50..ed854707 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -57,9 +57,9 @@ "croner": "catalog:", "d3-dsv": "catalog:", "downshift": "catalog:", + "es-toolkit": "catalog:", "history": "catalog:", "isbot": "catalog:", - "es-toolkit": "catalog:", "luxon": "catalog:", "metascraper": "catalog:", "metascraper-date": "catalog:", @@ -80,10 +80,11 @@ }, "devDependencies": { "@animeaux/dev-tools": "workspace:*", - "@animeaux/tailwind-animation": "workspace:*", + "@animeaux/tailwindcss-peekaboo": "workspace:*", "@faker-js/faker": "catalog:", "@remix-run/dev": "catalog:", - "@tailwindcss/container-queries": "catalog:", + "@tailwindcss/node": "catalog:tailwindcss4", + "@tailwindcss/vite": "catalog:tailwindcss4", "@types/autosize": "catalog:", "@types/cookie-signature": "catalog:", "@types/d3-dsv": "catalog:", @@ -91,7 +92,6 @@ "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", - "autoprefixer": "catalog:", "chokidar": "catalog:", "dotenv-flow": "catalog:", "eslint": "catalog:", @@ -99,8 +99,7 @@ "node-html-parser": "catalog:", "npm-run-all": "catalog:", "prettier": "catalog:", - "prettier-plugin-tailwindcss": "catalog:", - "tailwindcss": "catalog:", + "tailwindcss": "catalog:tailwindcss4", "tsx": "catalog:", "type-fest": "catalog:", "typescript": "catalog:", diff --git a/apps/admin/postcss.config.js b/apps/admin/postcss.config.js deleted file mode 100644 index d9bc7d09..00000000 --- a/apps/admin/postcss.config.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - plugins: { - "tailwindcss/nesting": {}, - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/apps/admin/prettier.config.js b/apps/admin/prettier.config.js index 9bcbf4e4..1a6f250b 100644 --- a/apps/admin/prettier.config.js +++ b/apps/admin/prettier.config.js @@ -1,5 +1 @@ -import { createConfig } from "@animeaux/dev-tools/prettier" - -export default createConfig({ - tailwindConfig: "./tailwind.config.ts", -}) +export { default } from "@animeaux/dev-tools/prettier" diff --git a/apps/admin/scripts/generate-theme.ts b/apps/admin/scripts/generate-theme.ts index ac0f350d..760e8f6e 100644 --- a/apps/admin/scripts/generate-theme.ts +++ b/apps/admin/scripts/generate-theme.ts @@ -1,37 +1,51 @@ -import { mkdir, writeFile } from "node:fs/promises" +import { mkdir, readFile, writeFile } from "node:fs/promises" import { dirname, resolve } from "node:path" import { fileURLToPath } from "node:url" -import { colors, screens, spacing } from "../tailwind.config.js" +import { __unstable__loadDesignSystem as loadDesignSystem } from "@tailwindcss/node" +import { orderBy } from "es-toolkit/array" +import type { __unstable__loadDesignSystem as loadDesignSystemBase } from "tailwindcss" +import invariant from "tiny-invariant" + import { relativeToCwd, safelyReadFile } from "./shared.js" const FILENAME = fileURLToPath(import.meta.url) const DIRNAME = dirname(FILENAME) const SCRIPT_NAME = relativeToCwd(FILENAME) +const STYLES_DIRECTORY = resolve(DIRNAME, "../src/styles") +const MAIN_STYLE_FILE = resolve(STYLES_DIRECTORY, "main.css") const DEST_DIRECTORY = resolve(DIRNAME, "../src/generated") const DEST_FILE = resolve(DEST_DIRECTORY, "theme.ts") +/** + * Matches values for the `` CSS type. + * We only support unitless `0`, rem and px. + * + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length + */ +const CSS_LENGTH_REGEXP = /(?-?\d*\.?\d+)(?.*)/ + console.log("Building...") +const mainCss = await readFile(MAIN_STYLE_FILE, "utf-8") + +// The type cast is required because @tailwindcss/node's `loadDesignSystem` +// types are broken (`design.theme: any`). We use tailwindcss's +// `loadDesignSystem` types because it's used internally and its types are fine. +const design = (await loadDesignSystem(mainCss, { + base: STYLES_DIRECTORY, +})) as Awaited> + // Ensure the destination directory exists. await mkdir(DEST_DIRECTORY, { recursive: true }) -// Add here values we need in code. -// Only add what is actualy needed to keep a minimal size. -const theme = { - screens, - colors, - spacing: Object.fromEntries( - Object.entries(spacing).map(([key, value]) => [ - key, - Number(value.replace("px", "")), - ]), - ), -} - const content = `// This file is generated by ${SCRIPT_NAME} -export type ScreenSize = keyof typeof theme.screens -export const theme = ${JSON.stringify(theme, null, 2)}` +${getBreakpoints()} + +${getColors()} + +${getSpacing()} +` const currentContent = await safelyReadFile(DEST_FILE) if (currentContent === content) { @@ -44,3 +58,93 @@ if (currentContent === content) { await writeFile(DEST_FILE, content) console.info(`Built theme (${relativeToCwd(DEST_FILE)})`) + +function getBreakpoints() { + const breakpoints = design.theme.namespace("--breakpoint") + + const sortedEntries = orderBy( + Array.from(breakpoints.entries()) + // `null` is used as key for a property with the namespace's name (e.g. + // `--breakpoint: `). + // For breakpoints this shouldn't happen. + .filter((entry): entry is [string, string] => entry[0] != null) + .map( + ([breakpoint, length]) => + [breakpoint, getLengthValuePx(length)] as const, + ), + [([_breakpoint, valuePx]) => valuePx], + ["desc"], + ) + + return `export type Breakpoint = typeof Breakpoint.names[number] + +export namespace Breakpoint { + /** Breakpoint names sorted by descending value. */ + export const names = ${JSON.stringify( + sortedEntries.map(([breakpoint]) => breakpoint), + )} as const + + /** Breakpoint values in pixels. */ + export const value = ${JSON.stringify(Object.fromEntries(sortedEntries), null, 2)} as const +}` +} + +function getColors() { + const colors = design.theme.namespace("--color") + + return `export namespace Color { + export const primary = ${JSON.stringify(colors.get("blue-500"))} + export const white = ${JSON.stringify(colors.get("white"))} +}` +} + +function getSpacing() { + const spacings = design.theme.namespace("--spacing") + + // A property with the namespace's name has `null` as key. (e.g. + // `--spacing: `). + const spacing = spacings.get(null) + + invariant(spacing != null, "A `--spacing` definition is missing in the theme") + + return `export namespace Spacing { + export const unitPx = ${getLengthValuePx(spacing)} +}` +} + +/** + * Returns the given CSS length in pixels. + * + * @param length expressed with the `` CSS type. + * @returns The value in pixels + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length + */ +function getLengthValuePx(length: string) { + length = length.trim() + + const match = length.match(CSS_LENGTH_REGEXP) + + if (match == null || match?.groups?.value == null) { + throw new Error(`Invalid CSS length from theme: "${length}"`) + } + + const value = Number(match.groups.value) + + // The unit is optional when the value is `0`. + // See https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length#syntax + const unit = match.groups.unit || "px" + + switch (unit) { + case "px": { + return value + } + + case "rem": { + return 16 * value + } + + default: { + throw new Error(`Unsupported length unit: "${unit}" in "${length}"`) + } + } +} diff --git a/apps/admin/src/animals/agreements.tsx b/apps/admin/src/animals/agreements.tsx index 9b41147c..5bb0eadd 100644 --- a/apps/admin/src/animals/agreements.tsx +++ b/apps/admin/src/animals/agreements.tsx @@ -39,7 +39,7 @@ export function AgreementItem({ AGREEMENT_CLASS_NAMES[agreement], )} > - + {AGREEMENT_TRANSLATION[agreement]} @@ -48,9 +48,9 @@ export function AgreementItem({ } const AGREEMENT_CLASS_NAMES: Record = { - [AgreementValue.TRUE]: "bg-green-50 text-green-600", - [AgreementValue.FALSE]: "bg-red-50 text-red-500", - [AgreementValue.UNKNOWN]: "bg-gray-100 text-gray-700", + [AgreementValue.TRUE]: cn("bg-green-50 text-green-600"), + [AgreementValue.FALSE]: cn("bg-red-50 text-red-500"), + [AgreementValue.UNKNOWN]: cn("bg-gray-100 text-gray-700"), } const ICONS: Record> = { diff --git a/apps/admin/src/animals/item.tsx b/apps/admin/src/animals/item.tsx index 84216460..43930c44 100644 --- a/apps/admin/src/animals/item.tsx +++ b/apps/admin/src/animals/item.tsx @@ -94,7 +94,7 @@ export function AnimalItem({ to={Routes.animals.id(animal.id).toString()} className={cn( className, - "flex flex-col gap-0.5 rounded-1.5 bg-white p-0.5 focus-visible:z-10 focus-visible:focus-compact-blue-400 hover:bg-gray-100 md:rounded-2 md:p-1", + "flex flex-col gap-0.5 rounded-1.5 bg-white p-0.5 hover:bg-gray-100 focus-visible:z-10 focus-visible:focus-ring md:rounded-2 md:p-1", )} > @@ -139,13 +139,13 @@ export function AnimalItem({ - + {getAnimalDisplayName(animal)}

{animal.manager != null ? ( -

+

{animal.manager.displayName}

) : null} @@ -172,7 +172,7 @@ export function AnimalSmallItem({ to={Routes.animals.id(animal.id).toString()} className={cn( className, - "grid grid-cols-[auto_minmax(0px,1fr)_auto] items-center gap-1 rounded-0.5 bg-white px-0.5 py-1 focus-visible:z-10 focus-visible:focus-compact-blue-400 hover:bg-gray-100 md:px-1", + "grid grid-cols-[auto_minmax(0px,1fr)_auto] items-center gap-1 rounded-0.5 bg-white px-0.5 py-1 hover:bg-gray-100 focus-visible:z-10 focus-visible:focus-ring md:px-1", )} > @@ -200,8 +200,8 @@ export function AnimalSmallItem({ className={cn( "flex", hasError - ? "text-red-500 text-caption-emphasis" - : "text-gray-500 text-caption-default", + ? "text-caption-emphasis text-red-500" + : "text-caption-default text-gray-500", )} > {secondaryLabel} diff --git a/apps/admin/src/animals/pictures/form.tsx b/apps/admin/src/animals/pictures/form.tsx index 48f069bd..ac63a0c6 100644 --- a/apps/admin/src/animals/pictures/form.tsx +++ b/apps/admin/src/animals/pictures/form.tsx @@ -254,11 +254,11 @@ function ImageItem({
) : null} diff --git a/apps/admin/src/core/actions.tsx b/apps/admin/src/core/actions.tsx index adeb39f0..160ba570 100644 --- a/apps/admin/src/core/actions.tsx +++ b/apps/admin/src/core/actions.tsx @@ -34,7 +34,7 @@ export const Action = Object.assign( {...rest} ref={ref} className={cn( - "relative flex flex-none items-center justify-center gap-0.5 duration-100 ease-in-out active:scale-95 disabled:opacity-50 focus-visible:focus-spaced-blue-400", + "relative flex flex-none items-center justify-center gap-0.5 ease-in-out focus-ring-spaced focus-visible:focus-ring active:scale-95 disabled:opacity-disabled", VARIANT_CLASS_NAME[variant]({ isIconOnly }), COLOR_CLASS_NAMES[variant][color], className, @@ -48,11 +48,11 @@ export const Action = Object.assign( return ( - + ) }, @@ -61,7 +61,7 @@ export const Action = Object.assign( className, ...props }: React.ComponentPropsWithoutRef) { - return + return }, }, ) @@ -72,21 +72,19 @@ const VARIANT_CLASS_NAME: Record< > = { primary: ({ isIconOnly }) => cn( - "h-4 min-w-[40px] rounded-0.5 transition-[background-color,transform] text-body-emphasis", + "h-4 min-w-4 rounded-0.5 text-body-emphasis transition-[background-color,scale]", isIconOnly ? "px-1" : "px-2", ), secondary: ({ isIconOnly }) => cn( - "h-4 min-w-[40px] rounded-0.5 transition-[background-color,transform] text-body-emphasis", + "h-4 min-w-4 rounded-0.5 text-body-emphasis transition-[background-color,scale]", isIconOnly ? "px-1" : "px-2", ), text: () => - cn( - "h-2 min-w-[20px] rounded-0.5 transition-[color,transform] text-body-emphasis", - ), + cn("h-2 min-w-2 rounded-0.5 text-body-emphasis transition-[color,scale]"), translucid: ({ isIconOnly }) => cn( - "h-4 min-w-[40px] rounded-0.5 bg-opacity-50 transition-[background-color,transform] text-body-emphasis hover:bg-opacity-70", + "h-4 min-w-4 rounded-0.5 text-body-emphasis transition-[background-color,scale]", isIconOnly ? "px-1" : "px-2", ), } @@ -117,7 +115,7 @@ const COLOR_CLASS_NAMES: Record> = { red: cn("text-red-500 hover:text-red-600"), }, translucid: { - black: cn("bg-gray-700 text-white"), + black: cn("bg-gray-700/50 text-white hover:bg-gray-700/70"), blue: cn(""), gray: cn(""), green: cn(""), @@ -137,7 +135,7 @@ export const ProseInlineAction = forwardRef< {...rest} ref={ref} className={cn( - "relative after:absolute after:bottom-0 after:left-0 after:w-full after:border-b after:border-blue-500 focus-visible:focus-spaced-blue-400", + "relative focus-ring-spaced after:absolute after:bottom-0 after:left-0 after:w-full after:border-b after:border-blue-500 focus-visible:focus-ring", CLASS_NAME_BY_VARIANT[variant], className, )} diff --git a/apps/admin/src/core/controllers/filters.tsx b/apps/admin/src/core/controllers/filters.tsx index 29b4340b..27f7b396 100644 --- a/apps/admin/src/core/controllers/filters.tsx +++ b/apps/admin/src/core/controllers/filters.tsx @@ -89,13 +89,13 @@ Filters.Filter = function Filter({ openedFilter === value ? null : value, ) } - className="group/filter grid w-full grid-flow-col grid-cols-1 items-start rounded-0.5 text-left focus:z-10 focus-visible:focus-spaced-blue-400" + className="group/filter grid w-full grid-flow-col grid-cols-1 items-start rounded-0.5 text-left focus-ring-spaced focus:z-10 focus-visible:focus-ring" > diff --git a/apps/admin/src/core/controllers/paginator.tsx b/apps/admin/src/core/controllers/paginator.tsx index f1f457ba..a098d8a9 100644 --- a/apps/admin/src/core/controllers/paginator.tsx +++ b/apps/admin/src/core/controllers/paginator.tsx @@ -72,7 +72,7 @@ function PaginatorItem({ - - + + @@ -26,12 +26,12 @@ export function SortAndFiltersFloatingAction({ // Use absolute instead of fixed to avoid performances issues when // mobile browser's height change due to scroll. "absolute", - "bottom-0 left-0 right-0 top-0 z-30 overscroll-none bg-black/20", + "inset-0 z-30 overscroll-none bg-black/20", )} /> - -
+ +
{hasSort ? "Trier et filtrer" : "Filtrer"} @@ -47,7 +47,7 @@ export function SortAndFiltersFloatingAction({ {children} -