From 39a4c75f8b04daef4fcc2c08f95d36572fd10cbe Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 27 Feb 2026 14:57:32 +0000 Subject: [PATCH 1/6] feat: start on agents.md/claude.md --- AGENTS.md | 1 + CLAUDE.md | 107 ++++++++++++++++++++++++---------------- apps/frontend/CLAUDE.md | 0 apps/labrinth/CLAUDE.md | 30 +++++++++++ 4 files changed, 96 insertions(+), 42 deletions(-) create mode 100644 AGENTS.md create mode 100644 apps/frontend/CLAUDE.md create mode 100644 apps/labrinth/CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..59f6a8dbe6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +See [CLAUDE.md](./CLAUDE.md) for all project instructions and guidelines. diff --git a/CLAUDE.md b/CLAUDE.md index 42161ddf3e..0c3adf0b43 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,63 +1,86 @@ -# Architecture +# Modrinth Monorepo -Use TAB instead of spaces. +This is the Modrinth monorepo — it contains all Modrinth projects, both frontend and backend. -## Frontend +## Architecture -There are two similar frontends in the Modrinth monorepo, the website (apps/frontend) and the app frontend (apps/app-frontend). +- **Monorepo tooling:** [Turborepo](https://turbo.build/) (`turbo.jsonc`) + [pnpm workspaces](https://pnpm.io/workspaces) (`pnpm-workspace.yaml`) +- **Frontend:** Vue 3 / Nuxt 3, Tailwind CSS v3 +- **Backend:** Rust (Labrinth API), Postgres, Clickhouse +- **Indentation:** Use TAB everywhere, never spaces -Both use Tailwind v3, and their respective configs can be seen at `tailwind.config.ts` and `tailwind.config.js` respectively. +### Apps (`apps/`) -Both utilize shared and common components from `@modrinth/ui` which can be found at `packages/ui`, and stylings from `@modrinth/assets` which can be found at `packages/assets`. +| App | Description | +| ----------------- | ----------------------------------- | +| `frontend` | Main Modrinth website (Nuxt 3) | +| `app-frontend` | Desktop/mobile app frontend (Vue 3) | +| `app` | Desktop/mobile app shell (Tauri) | +| `app-playground` | Testing playground for app | +| `labrinth` | Backend API service (Rust) | +| `daedalus_client` | Daedalus client implementation | +| `docs` | Documentation site (Nuxt) | -Both can utilize icons from `@modrinth/assets`, which are automatically generated based on what's available within the `icons` folder of the `packages/assets` directory. You can see the generated icons list in `generated-icons.ts`. +### Packages (`packages/`) -Both have access to our dependency injection framework, examples as seen in `packages/ui/src/providers/`. Ideally any state which is shared between a page and it's subpages should be shared using this dependency injection framework. +| Package | Description | +| ------------------ | ----------------------------------------------------- | +| `ui` | Shared Vue component library (`@modrinth/ui`) | +| `assets` | Styling and auto-generated icons (`@modrinth/assets`) | +| `api-client` | API client for Nuxt, Tauri, and Node/browser | +| `app-lib` | Shared app library | +| `blog` | Blog system and changelog data | +| `utils` | Shared utility functions | +| `moderation` | Moderation utilities | +| `daedalus` | Daedalus protocol (JS) | +| `tooling-config` | ESLint, Prettier, TypeScript configs | +| `ariadne` | Analytics library (Rust) | +| `modrinth-log` | Logging utilities (Rust) | +| `modrinth-maxmind` | MaxMind GeoIP (Rust) | +| `modrinth-util` | General utilities (Rust) | +| `muralpay` | Payment processing (Rust) | +| `path-util` | Path utilities (Rust) | +| `sqlx-tracing` | SQLx query tracing (Rust) | -### Website (apps/frontend) +## Pre-PR Commands -Before a pull request can be opened for the website, run `pnpm prepr:frontend:web` from the root folder, otherwise CI will fail. +Run these from the **root** folder before opening a pull request - do not run these after each prompt the user gives you, only run when asked, ask the user a question if they want to run it if the user indicates that they are about to create a pull request. -To run a development version of the frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within the `apps/frontend` folder into `apps/frontend/.env`. Then you can run the frontend by running `pnpm web:dev` in the root folder. +- **Website:** `pnpm prepr:frontend:web` +- **App frontend:** `pnpm prepr:frontend:app` +- **Frontend libs:** `pnpm prepr:frontend:lib` +- **All frontend (app+web):** `pnpm prepr` +- **Labrinth (backend):** See `apps/labrinth/CLAUDE.md` -### App Frontend (apps/app-frontend) +The website and app `prepr` commands -Before a pull request can be opened for the app frontend, run `pnpm prepr:frontend:app` from the root folder, otherwise CI will fail. +## Dev Commands -To run a development version of the app frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within `packages/app-lib` into `packages/app-lib/.env`. Then you must run the app itself by running `pnpm app:dev` in the root folder. +- **Website:** `pnpm web:dev` (copy `.env` template in `apps/frontend/` first) +- **App:** `pnpm app:dev` (copy `.env` template in `packages/app-lib/` first) +- **Storybook (packages/ui):** `pnpm storybook` -### Localization +## Project-Specific Instructions -Refer to `.github/instructions/i18n-convert.instructions.md` if the user asks you to perform any i18n conversion work on a component, set of components, pages or sets of pages. +Each project may have its own `CLAUDE.md` with detailed instructions: -## Labrinth +- [`apps/labrinth/CLAUDE.md`](apps/labrinth/CLAUDE.md) — Backend API (Rust) +- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website -Labrinth is the backend API service for Modrinth. +## Code Guidelines -### Testing +### Comments +- DO NOT use "heading" comments like: // === Helper methods === . +- Use doc comments, but avoid inline comments unless ABSOLUTELY necessary for clarity. Code should aim to be self documenting! -Before a pull request can be opened, run `cargo clippy -p labrinth --all-targets` and make sure there are ZERO warnings, otherwise CI will fail. +## Bash Guidelines -Use `cargo test -p labrinth --all-targets` to test your changes. All tests must pass, otherwise CI will fail. +### Output handling +- DO NOT pipe output through `head`, `tail`, `less`, or `more` +- NEVER use `| head -n X` or `| tail -n X` to truncate output +- Run commands directly without pipes when possible +- If you need to limit output, use command-specific flags (e.g. `git log -n 10` instead of `git log | head -10`) +- ALWAYS read the full output — never pipe through filters -To prepare the sqlx cache, cd into `apps/labrinth` and run `cargo sqlx prepare`. Make sure to NEVER run `cargo sqlx prepare --workspace`. - -Read the root `docker-compose.yml` to see what running services are available while developing. Use `docker exec` to access these services. - -When the user refers to "performing pre-PR checks", do the following: - -- Run clippy as described above -- DO NOT run tests unless explicitly requested (they take a long time) -- Prepare the sqlx cache - -### Clickhouse - -Use `docker exec labrinth-clickhouse clickhouse-client` to access the Clickhouse instance. We use the `staging_ariadne` database to store data in testing. - -### Postgres - -Use `docker exec labrinth-postgres psql -U labrinth -d labrinth -c "SELECT 1"` to access the PostgreSQL instance, replacing the `SELECT 1` with your query. - -# Guidelines - -- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to. +### General +- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to diff --git a/apps/frontend/CLAUDE.md b/apps/frontend/CLAUDE.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/labrinth/CLAUDE.md b/apps/labrinth/CLAUDE.md new file mode 100644 index 0000000000..2d496b028d --- /dev/null +++ b/apps/labrinth/CLAUDE.md @@ -0,0 +1,30 @@ +# Labrinth + +Labrinth is the backend API service for Modrinth, written in Rust. + +## Pre-PR Checks + +When the user refers to "perform[ing] pre-PR checks", do the following: + +- Run `cargo clippy -p labrinth --all-targets` — there must be ZERO warnings, otherwise CI will fail +- DO NOT run tests unless explicitly requested (they take a long time) +- Prepare the sqlx cache: cd into `apps/labrinth` and run `cargo sqlx prepare` + - NEVER run `cargo sqlx prepare --workspace` + +## Testing + +- Run `cargo test -p labrinth --all-targets` to test your changes — all tests must pass + +## Local Services + +- Read the root `docker-compose.yml` to see what running services are available while developing +- Use `docker exec` to access these services + +### Clickhouse + +- Access: `docker exec labrinth-clickhouse clickhouse-client` +- Database: `staging_ariadne` + +### Postgres + +- Access: `docker exec labrinth-postgres psql -U labrinth -d labrinth -c ""` From e7d190b5dbfe12215fb73b46d8207aa2f1049828 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 27 Feb 2026 15:33:47 +0000 Subject: [PATCH 2/6] feat: set up --- .claude/skills/cross-platform-pages/SKILL.md | 144 +++++++++++++ .claude/skills/dependency-injection/SKILL.md | 174 +++++++++++++++ .claude/skills/figma-mcp/SKILL.md | 45 ++++ .claude/skills/i18n-convert/SKILL.md | 105 +++++++++ .claude/skills/multistage-modals/SKILL.md | 215 +++++++++++++++++++ .gitignore | 3 +- AGENTS.md | 10 + packages/ui/CLAUDE.md | 67 ++++++ 8 files changed, 762 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/cross-platform-pages/SKILL.md create mode 100644 .claude/skills/dependency-injection/SKILL.md create mode 100644 .claude/skills/figma-mcp/SKILL.md create mode 100644 .claude/skills/i18n-convert/SKILL.md create mode 100644 .claude/skills/multistage-modals/SKILL.md create mode 100644 packages/ui/CLAUDE.md diff --git a/.claude/skills/cross-platform-pages/SKILL.md b/.claude/skills/cross-platform-pages/SKILL.md new file mode 100644 index 0000000000..05b596d43c --- /dev/null +++ b/.claude/skills/cross-platform-pages/SKILL.md @@ -0,0 +1,144 @@ +# Cross-Platform Page System + +When a page needs to exist in both the Modrinth App (`apps/app-frontend`) and the Modrinth Website (`apps/frontend`), use the cross-platform page system. + +## How It Works + +1. **Pages live as Vue SFCs in `packages/ui`** — either in `src/pages/` or `src/layout/` (if `src/pages/` doesn't exist, it's been renamed to `src/layout/`). +2. **Platform-dependent data flows via DI** — the app uses Tauri `invoke` commands, the website uses `api-client` or the legacy `useBaseFetch` composable. The shared page never knows which. See the `dependency-injection` skill for full DI docs. +3. **Non-platform-dependent data flows via props** — if data doesn't change based on _how_ it's fetched, just pass it as a prop. + +## Example: Content Page + +`ContentPageLayout` demonstrates the full pattern. + +### 1. Define a DI contract in `packages/ui/src/providers/` + +The provider interface abstracts all platform-specific operations: + +```ts +// packages/ui/src/providers/content-manager.ts +export interface ContentManagerContext { + items: Ref + loading: Ref + error: Ref + contentTypeLabel: Ref + + // These are the platform-abstracted operations: + // App uses invoke(), website uses api-client + toggleEnabled: (item: ContentItem) => Promise + deleteItem: (item: ContentItem) => Promise + refresh: () => Promise + browse: () => void + uploadFiles: () => void + + // Optional capabilities — not every platform supports everything + hasUpdateSupport: boolean + updateItem?: (item: ContentItem) => Promise + bulkUpdateItem?: (items: ContentItem[]) => Promise + + mapToTableItem: (item: ContentItem) => ContentCardTableItem +} + +export const [injectContentManager, provideContentManager] = + createContext('ContentManager') +``` + +### 2. Build the shared page in `packages/ui` + +The page component injects the context and handles all UI logic (search, filtering, selection, bulk operations, empty states, modals) without knowing the platform: + +```vue + + + + +``` + +### 3. Each platform provides its implementation + +**Website (Nuxt)** — uses `api-client` or `useBaseFetch`: + +```vue + + + + +``` + +**App (Tauri)** — uses `invoke`: + +```vue + + + + +``` + +## When to Use Props vs DI + +| Use | When | +| --------- | -------------------------------------------------------------------------------------------------------- | +| **DI** | The data depends on _how_ it's fetched (different per platform) — API calls, file operations, navigation | +| **Props** | The data is the same regardless of platform — configuration flags, display options | + +## Composables for Shared Logic + +Extract reusable stateful logic into composables in `packages/ui/src/composables/`. The shared page orchestrates them internally: + +- Search (Fuse.js fuzzy search over items) +- Filtering (dynamic filter pills) +- Selection (multi-select with bulk operations) +- Bulk operations (sequential execution with progress tracking) + +## Key Files + +- `packages/ui/src/pages/` (or `src/layout/`) — shared page components +- `packages/ui/src/providers/` — DI contracts +- `packages/ui/src/composables/` — shared stateful logic +- `apps/frontend/src/app.vue` — website root provider setup +- `apps/app-frontend/src/App.vue` — app root provider setup +- `apps/app-frontend/src/routes.js` — app route definitions diff --git a/.claude/skills/dependency-injection/SKILL.md b/.claude/skills/dependency-injection/SKILL.md new file mode 100644 index 0000000000..9c83b974f9 --- /dev/null +++ b/.claude/skills/dependency-injection/SKILL.md @@ -0,0 +1,174 @@ +# Dependency Injection + +Modrinth uses a lightweight DI layer built on Vue's `provide`/`inject` for sharing platform-specific capabilities and page-level state across shared UI components. + +## The `createContext` Factory + +All providers are defined using `createContext` from `packages/ui/src/providers/index.ts` (adapted from Reka UI). It produces a typed `[inject, provide]` tuple: + +```ts +import { createContext } from '@modrinth/ui' + +interface MyContext { + someValue: Ref + doSomething: () => void +} + +export const [injectMyContext, provideMyContext] = createContext('MyComponent') +``` + +- **`provideMyContext(value)`** — call in a parent component's `setup()`. +- **`injectMyContext()`** — call in any descendant's `setup()`. Throws if never provided. +- **`injectMyContext(null)`** — returns `null` instead of throwing (for optional contexts). + +## When to Use DI + +Use DI when: +- **The same interface needs different implementations** depending on the platform (web vs desktop app). +- **Deeply nested components** need access to shared page-level state without prop drilling through 3+ levels. + +### Platform Abstraction (Primary Use Case) + +`packages/ui` components need capabilities that each frontend fulfils differently: + +| Provider | App Frontend | Website Frontend | +|----------|-------------|-----------------| +| API client | Tauri IPC client | REST fetch client | +| Notifications | `ref()` state + app window mgmt | `useState()` for SSR hydration | +| File picker | Native Tauri dialogs | Browser file inputs | +| Tags | Tauri commands | Nuxt server state | +| Page context | `sidebar: true`, ad window hooks | `sidebar: false`, no ads | + +### Page-Level Context + +Sharing data between a page and deeply nested children — e.g. project page data consumed by sidebar, header, and version components. + +## Creating a New Provider + +### 1. Define the interface in `packages/ui/src/providers/` + +```ts +// packages/ui/src/providers/my-feature.ts +import type { Ref } from 'vue' +import { createContext } from '.' + +export interface MyFeatureContext { + items: Ref + addItem: (item: Item) => Promise + removeItem: (id: string) => Promise +} + +export const [injectMyFeature, provideMyFeature] = createContext('MyFeature') +``` + +Re-export from the barrel file (`packages/ui/src/providers/index.ts`). + +### 2. For complex platform-specific logic, use an abstract class + +```ts +export abstract class AbstractMyFeatureManager { + abstract items: Ref + abstract addItem(item: Item): Promise + + // Shared logic lives on the base class + handleError(err: unknown) { + console.error(err) + } +} + +export const [injectMyFeature, provideMyFeature] = + createContext('MyFeature') +``` + +See `AbstractWebNotificationManager` in `packages/ui/src/providers/web-notifications.ts` for a real example. + +## Wiring Up Providers + +### App Frontend (Tauri) + +Create a setup function in `apps/app-frontend/src/providers/setup/`: + +```ts +// apps/app-frontend/src/providers/setup/my-feature.ts +import { ref } from 'vue' +import { provideMyFeature } from '@modrinth/ui' + +export function setupMyFeatureProvider() { + const items = ref([]) + + provideMyFeature({ + items, + addItem: async (item) => { + await invoke('add_item', { item }) + items.value.push(item) + }, + removeItem: async (id) => { + await invoke('remove_item', { id }) + items.value = items.value.filter(i => i.id !== id) + }, + }) +} +``` + +Register it in `apps/app-frontend/src/providers/setup.ts`, which is called from `App.vue`'s `setup()`. + +### Website Frontend (Nuxt) + +Provide directly in `apps/frontend/src/app.vue`, using Nuxt's `useState()` where SSR hydration is needed: + +```ts +provideMyFeature({ + items: useState('my-feature-items', () => []), + addItem: async (item) => { + await $fetch('/api/items', { method: 'POST', body: item }) + }, + removeItem: async (id) => { + await $fetch(`/api/items/${id}`, { method: 'DELETE' }) + }, +}) +``` + +## Consuming Providers + +In any component across `packages/ui`, `apps/frontend`, or `apps/app-frontend`: + +```vue + + + +``` + +## When NOT to Use DI + +Default to props and emits. DI adds indirection — only use it with a concrete reason. + +- **Parent to direct child** — use props. +- **Data only exists in one frontend** — keep context local to that app, not in `packages/ui`. +- **Shallow prop drilling (1–2 levels)** — passing through one intermediate is fine. +- **Component-local state** — use `ref()` / `reactive()` locally. + +## Existing Providers + +| Provider | File | Purpose | +|----------|------|---------| +| `provideModrinthClient` | `providers/api-client.ts` | API client instance | +| `provideNotificationManager` | `providers/web-notifications.ts` | Notification management | +| `providePageContext` | `providers/page-context.ts` | Page config (sidebar, ads) | +| `provideProjectPageContext` | `providers/project-page.ts` | Project page state + mutations | +| `provideServerContext` | `providers/server-context.ts` | Server hosting state | +| `provideUserPageContext` | `providers/user-page.ts` | User page state | + +## Key Files + +- `packages/ui/src/providers/index.ts` — `createContext` factory + barrel exports +- `packages/ui/src/providers/*.ts` — Provider definitions +- `apps/frontend/src/app.vue` — Nuxt root provider setup +- `apps/app-frontend/src/App.vue` — Tauri root provider setup +- `apps/app-frontend/src/providers/setup/` — App provider setup functions diff --git a/.claude/skills/figma-mcp/SKILL.md b/.claude/skills/figma-mcp/SKILL.md new file mode 100644 index 0000000000..cd1bf60a27 --- /dev/null +++ b/.claude/skills/figma-mcp/SKILL.md @@ -0,0 +1,45 @@ +# Figma MCP Usage + +When the Figma MCP server is connected, use it to translate Figma designs into production-ready Vue components for this monorepo. + +## Workflow + +### 1. Get the design context + +Use `get_design_context` with the node ID from a Figma URL. If the URL is `https://figma.com/design/:fileKey/:fileName?node-id=1-2`, the node ID is `1:2`. + +``` +get_design_context(nodeId: "1:2", clientLanguages: "typescript,html,css", clientFrameworks: "vue") +``` + +This returns reference code, a screenshot, and metadata. Always start here. + +### 2. Get a screenshot for visual reference + +Use `get_screenshot` if you need to see the design without full code context: + +``` +get_screenshot(nodeId: "1:2") +``` + +### 3. Get variable definitions + +Use `get_variable_defs` to see what design tokens are applied to a node: + +``` +get_variable_defs(nodeId: "1:2") +``` + +### 4. Get metadata for structure overview + +Use `get_metadata` to get an XML overview of node IDs, layer types, names, positions and sizes — useful for understanding the structure of a complex frame before diving into individual nodes. + +## Adapting Figma Output + +The Figma MCP returns generic reference code. Adapt it to match the Modrinth codebase: + +1. **Read `packages/ui/CLAUDE.md`** for color usage rules, surface token mapping, and component patterns. +2. **Map Figma color variables to `surface-*` tokens** — never use Figma's aliased names like `bg/default` or `bg/raised` directly. The CLAUDE.md has the full mapping table. +3. **Check `packages/assets/styles/variables.scss`** for tokens not exposed in Figma (brand highlights, semantic backgrounds, shadows). +4. **Check for existing components** in `packages/ui/src/components/` before building from scratch. +5. **Match spacing exactly** — do not approximate values from the design. diff --git a/.claude/skills/i18n-convert/SKILL.md b/.claude/skills/i18n-convert/SKILL.md new file mode 100644 index 0000000000..80fa2c127f --- /dev/null +++ b/.claude/skills/i18n-convert/SKILL.md @@ -0,0 +1,105 @@ +# i18n String Conversion + +Convert hard-coded natural-language strings in Vue SFCs into the localization system using utilities from `@modrinth/ui`. + +## Rules + +### 1. Identify translatable strings + +- Scan `