From d0c7575a23aecd1d8af3a9fdffe01038e46ffb41 Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Mon, 16 Mar 2026 17:30:05 +0000 Subject: [PATCH] feat: move notion docs to standards folder (#5590) * feat: move notion docs to standards folder * fix: remove skills mention (automatic now) --- .claude/skills/api-module/SKILL.md | 166 +------- .claude/skills/cross-platform-pages/SKILL.md | 163 ++------ .claude/skills/figma-mcp/SKILL.md | 61 +-- .claude/skills/i18n-convert/SKILL.md | 105 ----- .claude/skills/i18n-pass/SKILL.md | 24 ++ .claude/skills/multistage-modals/SKILL.md | 215 ---------- .claude/skills/tanstack-query/SKILL.md | 175 ++------- CLAUDE.md | 24 +- standards/README.md | 5 + standards/frontend/ADDING_API_MODULES.md | 167 ++++++++ standards/frontend/CROSS_PLATFORM_PAGES.md | 157 ++++++++ .../frontend/DEPENDENCY_INJECTION.md | 46 ++- standards/frontend/FETCHING_DATA.md | 164 ++++++++ standards/frontend/FIGMA_MCP_USAGE.md | 32 ++ standards/frontend/INTERNATIONALIZATION.md | 112 ++++++ standards/frontend/MODALS.md | 370 ++++++++++++++++++ 16 files changed, 1143 insertions(+), 843 deletions(-) delete mode 100644 .claude/skills/i18n-convert/SKILL.md create mode 100644 .claude/skills/i18n-pass/SKILL.md delete mode 100644 .claude/skills/multistage-modals/SKILL.md create mode 100644 standards/README.md create mode 100644 standards/frontend/ADDING_API_MODULES.md create mode 100644 standards/frontend/CROSS_PLATFORM_PAGES.md rename .claude/skills/dependency-injection/SKILL.md => standards/frontend/DEPENDENCY_INJECTION.md (68%) create mode 100644 standards/frontend/FETCHING_DATA.md create mode 100644 standards/frontend/FIGMA_MCP_USAGE.md create mode 100644 standards/frontend/INTERNATIONALIZATION.md create mode 100644 standards/frontend/MODALS.md diff --git a/.claude/skills/api-module/SKILL.md b/.claude/skills/api-module/SKILL.md index 6cecf0b21..37b4ac778 100644 --- a/.claude/skills/api-module/SKILL.md +++ b/.claude/skills/api-module/SKILL.md @@ -1,156 +1,18 @@ -# Adding a New API Module +--- +name: api-module +description: Add a new API endpoint module to packages/api-client from an OpenAPI schema. Use when adding new backend endpoints, creating API client modules, or when an openapi.yml is provided. +argument-hint: +--- -How to add a new API endpoint module to `packages/api-client`. +Refer to the standard: @standards/frontend/ADDING_API_MODULES.md ## Steps -### 1. Define types in the module's `types.ts` - -Types must match 1:1 with the backend API response. Do not reshape, rename, or omit fields. - -Add to an existing namespace or create a new one: - -```ts -// modules/labrinth/types.ts (existing namespace) -export namespace Labrinth { - export namespace MyDomain { - export namespace v3 { - export type Thing = { - id: string - name: string - created: string - // ... matches API response exactly - } - - export type CreateThingRequest = { - name: string - } - } - } -} -``` - -For a new API service, create `modules//types.ts` with a new top-level namespace and re-export it from `modules/types.ts`. - -### 2. Create the module class - -Create `modules///v.ts`: - -```ts -// modules/labrinth/things/v3.ts -import { AbstractModule } from '../../../core/abstract-module' -import type { Labrinth } from '../types' - -export class LabrinthThingsV3Module extends AbstractModule { - public getModuleID(): string { - return 'labrinth_things_v3' - } - - public async get(id: string): Promise { - return this.client.request(`/thing/${id}`, { - api: 'labrinth', - version: 3, - method: 'GET', - }) - } - - public async create(data: Labrinth.MyDomain.v3.CreateThingRequest): Promise { - return this.client.request(`/thing`, { - api: 'labrinth', - version: 3, - method: 'POST', - body: data, - }) - } - - public async delete(id: string): Promise { - return this.client.request(`/thing/${id}`, { - api: 'labrinth', - version: 3, - method: 'DELETE', - }) - } -} -``` - -#### Request options - -| Field | Values | Purpose | -|-------|--------|---------| -| `api` | `'labrinth'`, `'archon'`, or a full URL | Which base URL to use | -| `version` | `2`, `3`, `'internal'`, `'modrinth/v0'`, etc. | URL version segment | -| `method` | `'GET'`, `'POST'`, `'PUT'`, `'PATCH'`, `'DELETE'` | HTTP method | -| `body` | object | JSON request body | -| `params` | `Record` | Query parameters | -| `skipAuth` | `boolean` | Skip auth feature for this request | -| `useNodeAuth` | `boolean` | Use node-level auth (kyros) | -| `timeout` | `number` | Request timeout in ms | -| `retry` | `boolean \| number` | Override retry behavior | - -#### For uploads - -Return an `UploadHandle` instead of a `Promise`: - -```ts -public uploadThing(id: string, file: File): UploadHandle { - return this.client.upload(`/thing/${id}/file`, { - api: 'labrinth', - version: 3, - file, - }) -} - -// Or with FormData for multipart: -public createWithFiles(data: CreateRequest, files: File[]): UploadHandle { - const formData = new FormData() - formData.append('data', JSON.stringify(data)) - files.forEach((f, i) => formData.append(`file-${i}`, f, f.name)) - - return this.client.upload(`/thing`, { - api: 'labrinth', - version: 3, - formData, - timeout: 60 * 5 * 1000, // longer timeout for uploads - }) -} -``` - -### 3. Register in the MODULE_REGISTRY - -Add to `modules/index.ts`: - -```ts -import { LabrinthThingsV3Module } from './labrinth/things/v3' - -export const MODULE_REGISTRY = { - // ... existing modules - labrinth_things_v3: LabrinthThingsV3Module, -} as const -``` - -The naming convention is `__`. This flat key gets transformed into nested access: `client.labrinth.things_v3`. - -### 4. Export types - -If you added to an existing namespace, types are already re-exported. If you created a new `types.ts`, add it to `modules/types.ts`: - -```ts -export * from './/types' -``` - -## Naming Conventions - -| Convention | Example | -|-----------|---------| -| Module class | `LabrinthThingsV3Module` — `{Api}{Domain}V{N}Module` | -| Module ID | `labrinth_things_v3` — `{api}_{domain}_v{n}` | -| Type namespace | `Labrinth.MyDomain.v3.Thing` | -| File path | `modules/labrinth/things/v3.ts` | - -## Key Files - -- `src/core/abstract-module.ts` — base class all modules extend -- `src/core/abstract-client.ts` — `request()` and `upload()` methods -- `src/modules/index.ts` — `MODULE_REGISTRY` and `buildModuleStructure()` -- `src/modules//types.ts` — type definitions per API -- `src/types/upload.ts` — `UploadHandle`, `UploadProgress`, `UploadRequestOptions` +1. **Read the OpenAPI schema** at `$ARGUMENTS` — identify the endpoints, request/response shapes, and path parameters. +2. **Read the standard above** for naming conventions, type rules, and the module registration pattern. +3. **Determine the service and version** — the URL path prefix tells you which service directory and version namespace to use (e.g. `/v3/projects` → `labrinth/v3/`). +4. **Define types in `types.ts`** — types must match the API response 1:1. Use the OpenAPI schema as the source of truth. Do not reshape or rename fields. +5. **Create the module class** — extend `BaseModule`, implement each endpoint as a method. Use the correct HTTP verb and request options pattern from the standard. +6. **Register in `MODULE_REGISTRY`** — add the module entry so it's auto-instantiated on the client. +7. **Export types** from the service's barrel `index.ts`. +8. **Verify** — check that the module compiles and the types are accessible from `@modrinth/api-client`. diff --git a/.claude/skills/cross-platform-pages/SKILL.md b/.claude/skills/cross-platform-pages/SKILL.md index 05b596d43..b3dfd9e8a 100644 --- a/.claude/skills/cross-platform-pages/SKILL.md +++ b/.claude/skills/cross-platform-pages/SKILL.md @@ -1,144 +1,25 @@ -# Cross-Platform Page System +--- +name: cross-platform-pages +description: Convert a page to the cross-platform page system so it works in both the website and the desktop app. Use when moving a page into packages/ui/src/layouts/, creating shared or wrapped layouts, or setting up DI contracts for platform abstraction. +argument-hint: +--- -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. +Refer to the standards: @standards/frontend/CROSS_PLATFORM_PAGES.md and @standards/frontend/DEPENDENCY_INJECTION.md -## How It Works +## Steps -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 +1. **Read the target page** at `$ARGUMENTS` and understand its data sources, mutations, and navigation. +2. **Read the standards above** to understand the shared vs wrapped distinction and the DI pattern. +3. **Decide the category:** + - **Wrapped** (`layouts/wrapped/`) — if the page uses the same API source on both platforms (e.g. web requests, not Tauri plugins). Just move the page component into `packages/ui` and import it from both frontends. + - **Shared** (`layouts/shared/`) — if the page has different data-fetching logic per platform (e.g. website uses `api-client`, app uses Tauri `invoke`). Requires a DI contract. +4. **For shared layouts:** + - Define a DI contract interface in `providers/` capturing all platform-specific operations. + - Create the layout component that injects the context and handles all UI logic. + - Extract reusable stateful logic (search, filtering, selection) into `composables/`. + - Implement the contract separately in each frontend (`apps/frontend/`, `apps/app-frontend/`). +5. **For wrapped pages:** + - Move the page component into `packages/ui/src/layouts/wrapped/` matching the route structure. + - Replace any platform-specific imports with shared utilities. + - Import and render the wrapped page from both frontends as a simple component. +6. **Verify** the page renders correctly by checking for missing imports and that all DI contracts are satisfied. diff --git a/.claude/skills/figma-mcp/SKILL.md b/.claude/skills/figma-mcp/SKILL.md index cd1bf60a2..defabf8d2 100644 --- a/.claude/skills/figma-mcp/SKILL.md +++ b/.claude/skills/figma-mcp/SKILL.md @@ -1,45 +1,22 @@ -# Figma MCP Usage +--- +name: figma-mcp +description: Use the Figma MCP server to translate a Figma design into a Vue page or component layout. Use when the user provides a Figma URL, asks to implement a design, or wants to draft a page layout from Figma. +argument-hint: +--- -When the Figma MCP server is connected, use it to translate Figma designs into production-ready Vue components for this monorepo. +Refer to the standard: @standards/frontend/FIGMA_MCP_USAGE.md +Also read @packages/ui/CLAUDE.md for color token mapping and component conventions. -## Workflow +## Steps -### 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. +1. **Parse the Figma URL** from `$ARGUMENTS` — extract the `fileKey` and `nodeId`. Convert `-` to `:` in the node ID. +2. **Read the standards above** for the available tools, adaptation rules, and color usage. +3. **Call `get_design_context`** with the extracted `nodeId` and `fileKey`, using `clientLanguages: "typescript,html,css"` and `clientFrameworks: "vue"`. This is always the first tool to call. +5. **Adapt the output to the Modrinth codebase:** + - Map Figma color variables to `surface-*` / `text-*` tokens — never use Figma's aliased names directly. + - Check `packages/ui/src/components/` for existing components that match elements in the design (buttons, cards, modals, inputs, etc.). + - Check `packages/assets/styles/variables.scss` for tokens not exposed in Figma. + - Match spacing values exactly from the design. +6. **Use `get_screenshot`** if you need a closer visual reference of specific nodes. +7. **Use `get_variable_defs`** to verify which design tokens are applied to ambiguous elements. +8. **Build the component** as a Vue SFC using Tailwind classes and the project's existing component library. diff --git a/.claude/skills/i18n-convert/SKILL.md b/.claude/skills/i18n-convert/SKILL.md deleted file mode 100644 index 80fa2c127..000000000 --- a/.claude/skills/i18n-convert/SKILL.md +++ /dev/null @@ -1,105 +0,0 @@ -# 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 `