From 829cbd578c6748e03193989eaab598603952d6d5 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 3 Aug 2026 08:22:41 +0100 Subject: [PATCH] chore: cleanup standards & skills --- .agents/skills/api-module/SKILL.md | 23 ++ .agents/skills/api-module/agents/openai.yaml | 4 + .agents/skills/cross-platform-pages/SKILL.md | 38 +++ .../cross-platform-pages/agents/openai.yaml | 4 + .agents/skills/figma-mcp/SKILL.md | 28 ++ .agents/skills/figma-mcp/agents/openai.yaml | 4 + .agents/skills/i18n-pass/SKILL.md | 32 ++ .agents/skills/i18n-pass/agents/openai.yaml | 4 + .agents/skills/review-changelog/SKILL.md | 40 +++ .../review-changelog/agents/openai.yaml | 4 + .agents/skills/tanstack-query/SKILL.md | 39 +++ .../skills/tanstack-query/agents/openai.yaml | 4 + .claude/skills/api-module/SKILL.md | 18 -- .claude/skills/cross-platform-pages/SKILL.md | 26 -- .claude/skills/figma-mcp/SKILL.md | 22 -- .claude/skills/i18n-pass/SKILL.md | 24 -- .claude/skills/review-changelog/SKILL.md | 36 --- .claude/skills/tanstack-query/SKILL.md | 27 -- .gitignore | 3 +- AGENTS.md | 94 +++++- CLAUDE.md | 110 ------- apps/frontend/AGENTS.md | 43 ++- apps/frontend/CLAUDE.md | 42 --- apps/labrinth/CLAUDE.md | 1 - packages/api-client/AGENTS.md | 209 +++++++++++- packages/api-client/CLAUDE.md | 208 ------------ packages/ui/AGENTS.md | 97 +++++- packages/ui/CLAUDE.md | 96 ------ standards/README.md | 6 +- standards/frontend/ADDING_API_MODULES.md | 98 +++--- standards/frontend/COMPONENT_STRUCTURE.md | 66 ++-- standards/frontend/CROSS_PLATFORM_PAGES.md | 124 +++++--- standards/frontend/DEPENDENCY_INJECTION.md | 143 +++++---- standards/frontend/FETCHING_DATA.md | 60 ++-- standards/frontend/FIGMA_MCP_USAGE.md | 40 +-- standards/frontend/INTERNATIONALIZATION.md | 129 +++++--- standards/frontend/MODALS.md | 297 ++++++++++-------- standards/frontend/SURFACE_SYSTEM.md | 30 +- standards/maintaining/CHANGELOG.md | 161 ++++++---- 39 files changed, 1308 insertions(+), 1126 deletions(-) create mode 100644 .agents/skills/api-module/SKILL.md create mode 100644 .agents/skills/api-module/agents/openai.yaml create mode 100644 .agents/skills/cross-platform-pages/SKILL.md create mode 100644 .agents/skills/cross-platform-pages/agents/openai.yaml create mode 100644 .agents/skills/figma-mcp/SKILL.md create mode 100644 .agents/skills/figma-mcp/agents/openai.yaml create mode 100644 .agents/skills/i18n-pass/SKILL.md create mode 100644 .agents/skills/i18n-pass/agents/openai.yaml create mode 100644 .agents/skills/review-changelog/SKILL.md create mode 100644 .agents/skills/review-changelog/agents/openai.yaml create mode 100644 .agents/skills/tanstack-query/SKILL.md create mode 100644 .agents/skills/tanstack-query/agents/openai.yaml delete mode 100644 .claude/skills/api-module/SKILL.md delete mode 100644 .claude/skills/cross-platform-pages/SKILL.md delete mode 100644 .claude/skills/figma-mcp/SKILL.md delete mode 100644 .claude/skills/i18n-pass/SKILL.md delete mode 100644 .claude/skills/review-changelog/SKILL.md delete mode 100644 .claude/skills/tanstack-query/SKILL.md mode change 120000 => 100644 AGENTS.md delete mode 100644 CLAUDE.md mode change 120000 => 100644 apps/frontend/AGENTS.md delete mode 100644 apps/frontend/CLAUDE.md delete mode 100644 apps/labrinth/CLAUDE.md mode change 120000 => 100644 packages/api-client/AGENTS.md delete mode 100644 packages/api-client/CLAUDE.md mode change 120000 => 100644 packages/ui/AGENTS.md delete mode 100644 packages/ui/CLAUDE.md diff --git a/.agents/skills/api-module/SKILL.md b/.agents/skills/api-module/SKILL.md new file mode 100644 index 0000000000..32cc16756b --- /dev/null +++ b/.agents/skills/api-module/SKILL.md @@ -0,0 +1,23 @@ +--- +name: api-module +description: Add an API endpoint module to packages/api-client from an OpenAPI schema. Use for new backend endpoints, API client modules, or tasks that provide an OpenAPI schema. +--- + +# Add an API Module + +Read the applicable `AGENTS.md` files before you edit code. + +Read [the API module standard](../../../standards/frontend/ADDING_API_MODULES.md) in full. + +1. Identify the OpenAPI schema from the request. If more than one schema is possible, ask the user to select one. +2. Read the schema. Identify each endpoint, HTTP method, request type, response type, and path parameter. +3. Get the service and version from the URL prefix. For example, map `/v3/projects` to `labrinth/v3/`. +4. Define the API types in `types.ts`. Make each type match the schema exactly. +5. Do not change, rename, or remove API fields. +6. Make a module class that extends `AbstractModule`. Implement each endpoint with `this.client.request()` or `this.client.upload()`. +7. Use the request-option pattern from the standard. Do not call `$fetch`, `fetch`, or another HTTP client directly. +8. Add the module to `MODULE_REGISTRY` so the client can instantiate it. +9. Export new service types from the applicable barrel `index.ts`. +10. Check the module paths, registry key, public type exports, and endpoint types. + +Run only the checks that the user or the applicable `AGENTS.md` permits. diff --git a/.agents/skills/api-module/agents/openai.yaml b/.agents/skills/api-module/agents/openai.yaml new file mode 100644 index 0000000000..c8dd768afe --- /dev/null +++ b/.agents/skills/api-module/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Add API Module" + short_description: "Add typed API client modules from OpenAPI" + default_prompt: "Use $api-module to add an API client module from this OpenAPI schema." diff --git a/.agents/skills/cross-platform-pages/SKILL.md b/.agents/skills/cross-platform-pages/SKILL.md new file mode 100644 index 0000000000..2399604821 --- /dev/null +++ b/.agents/skills/cross-platform-pages/SKILL.md @@ -0,0 +1,38 @@ +--- +name: cross-platform-pages +description: Convert a page to the shared Modrinth page system for the website and desktop app. Use for shared layouts, wrapped layouts, or platform dependency-injection contracts. +--- + +# Convert a Cross-Platform Page + +Read the applicable `AGENTS.md` files before you edit code. + +Read these standards in full: + +- [Cross-platform pages](../../../standards/frontend/CROSS_PLATFORM_PAGES.md) +- [Dependency injection](../../../standards/frontend/DEPENDENCY_INJECTION.md) + +1. Identify the target page from the request. +2. Read the page and its route shell. Identify data sources, mutations, navigation, and platform APIs. +3. Use a wrapped layout when both platforms use the same API source and page logic. +4. Use a shared layout when platform data or operations have different implementations. + +For a shared layout: + +1. Define a provider contract for all platform operations. +2. Put common UI and state logic in the shared layout. +3. Put reusable search, filter, and selection logic in local composables. +4. Implement the contract in `apps/frontend/` and `apps/app-frontend/`. +5. Use optional contract fields only for capabilities that are not available on both platforms. + +For a wrapped layout: + +1. Move the page to `packages/ui/src/layouts/wrapped/` and preserve its route structure. +2. Replace platform-only imports with common utilities or provider calls. +3. Make each frontend route shell render the wrapped component. +4. Match primary query options in both route shells when the layout uses `ReadyTransition` and `useReadyState`. +5. Prefetch these queries with `ensureQueryData`, as the standard specifies. + +Check that both route shells resolve their imports. Check that all required provider fields have implementations. + +Run only the checks that the user or the applicable `AGENTS.md` permits. diff --git a/.agents/skills/cross-platform-pages/agents/openai.yaml b/.agents/skills/cross-platform-pages/agents/openai.yaml new file mode 100644 index 0000000000..ad15d4df68 --- /dev/null +++ b/.agents/skills/cross-platform-pages/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Convert Cross-Platform Page" + short_description: "Share pages across the web and desktop app" + default_prompt: "Use $cross-platform-pages to convert this page for the website and desktop app." diff --git a/.agents/skills/figma-mcp/SKILL.md b/.agents/skills/figma-mcp/SKILL.md new file mode 100644 index 0000000000..57d4b2914b --- /dev/null +++ b/.agents/skills/figma-mcp/SKILL.md @@ -0,0 +1,28 @@ +--- +name: figma-mcp +description: Convert a Figma design into a Modrinth Vue page or component. Use when a request provides a Figma URL or asks to implement a Figma layout. +--- + +# Implement a Figma Design + +Read the applicable `AGENTS.md` files before you edit code. + +Read these files in full: + +- [Figma MCP usage](../../../standards/frontend/FIGMA_MCP_USAGE.md) +- `packages/ui/AGENTS.md` + +1. Get the Figma URL from the request. +2. Extract the `fileKey` and `nodeId`. Replace each `-` in the node ID with `:`. +3. Load any available Figma design-to-code instructions before you call `get_design_context`. +4. Call `get_design_context` first. Use `clientLanguages: "typescript,html,css"` and `clientFrameworks: "vue"`. +5. Map Figma color variables to the applicable `surface-*` and `text-*` tokens. +6. Do not use aliased Figma names directly. +7. Search `packages/ui/src/components/` for applicable components before you make new components. +8. Read `packages/assets/styles/variables.scss` when Figma does not supply a required token. +9. Use exact spacing values from the design. +10. Use `get_screenshot` for more visual detail when necessary. +11. Use `get_variable_defs` when a token is not clear. +12. Implement the result as a Vue SFC with Tailwind classes and the existing component library. + +Run only the checks that the user or the applicable `AGENTS.md` permits. diff --git a/.agents/skills/figma-mcp/agents/openai.yaml b/.agents/skills/figma-mcp/agents/openai.yaml new file mode 100644 index 0000000000..1a7c61d304 --- /dev/null +++ b/.agents/skills/figma-mcp/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Implement Figma Design" + short_description: "Build Modrinth Vue UI from Figma designs" + default_prompt: "Use $figma-mcp to implement this Figma design as a Modrinth Vue component." diff --git a/.agents/skills/i18n-pass/SKILL.md b/.agents/skills/i18n-pass/SKILL.md new file mode 100644 index 0000000000..05f69de695 --- /dev/null +++ b/.agents/skills/i18n-pass/SKILL.md @@ -0,0 +1,32 @@ +--- +name: i18n-pass +description: Convert hard-coded English text in changed Vue components to the @modrinth/ui localization system. Use for an i18n pass, untranslated-string review, pull request, or component migration. +--- + +# Do an Internationalization Pass + +Read the applicable `AGENTS.md` files before you edit code. + +Read [the internationalization standard](../../../standards/frontend/INTERNATIONALIZATION.md) in full. + +1. Identify the scope from the request. +2. For a pull request, use `gh pr diff ` to identify changed files. +3. For a file path, inspect that file. +4. When the request gives no scope, inspect the current uncommitted diff. +5. Limit the pass to changed `.vue` files. +6. Find user-visible text in templates and scripts. + +Check inner text, `alt`, `placeholder`, `aria-label`, buttons, tooltips, notifications, dropdown labels, and error messages. + +Do not change dynamic expressions, HTML tag names, CSS classes, internal identifiers, or log messages. + +1. Define stable message IDs with `defineMessage` or `defineMessages`. +2. Replace simple text with `formatMessage()` calls. +3. Use `` for text that contains links or markup. +4. Use ICU selections and plurals when grammar depends on a value. +5. Add a space before `}}` when an ICU placeholder ends at the Vue delimiter. +6. Do not change component logic, layout, or reactivity. +7. Do not edit localization JSON files. The user maintains those files. +8. Check the changed templates again for hard-coded English text. + +Run only the checks that the user or the applicable `AGENTS.md` permits. diff --git a/.agents/skills/i18n-pass/agents/openai.yaml b/.agents/skills/i18n-pass/agents/openai.yaml new file mode 100644 index 0000000000..347ef98b2d --- /dev/null +++ b/.agents/skills/i18n-pass/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Run Internationalization Pass" + short_description: "Localize user-visible text in Vue files" + default_prompt: "Use $i18n-pass to localize the user-visible text in these changed Vue files." diff --git a/.agents/skills/review-changelog/SKILL.md b/.agents/skills/review-changelog/SKILL.md new file mode 100644 index 0000000000..5ab041c1c9 --- /dev/null +++ b/.agents/skills/review-changelog/SKILL.md @@ -0,0 +1,40 @@ +--- +name: review-changelog +description: Review the latest packages/blog/changelog.ts entry against the Modrinth changelog standard. Use before a pull request or when asked to review or lint a changelog entry. +--- + +# Review a Changelog Entry + +Read [the changelog standard](../../../standards/maintaining/CHANGELOG.md) in full before the review. + +1. Open `packages/blog/changelog.ts`. +2. Find the first entry in the `VERSIONS` array. +3. If the request names `web`, `hosting`, or `app`, review the latest entry for that product. +4. Otherwise, review the latest entry and all adjacent entries with the same date. + +Check the entry structure: + +- `date` contains a valid ISO 8601 timestamp. +- `product` is `web`, `hosting`, or `app`. +- An `app` entry has a `version` value. +- A `web` or `hosting` entry does not have a `version` value. +- Standard headings are `## Added`, `## Changed`, `## Fixed`, and `## Security`. +- A featured release can use a linked heading. +- Flag the legacy `## Improvements` heading. + +Check each bullet: + +- The voice and tense agree with the section. +- The first verb agrees with the section. +- The bullet describes user-visible behavior, not implementation. +- The bullet identifies the applicable page, tab, modal, or feature. +- The bullet contains one sentence, uses sentence case, and ends with a period. +- Product and UI names use the public labels. +- The bullet does not contain filler, vague intensifiers, apologies, or internal references. +- The bullet is not a duplicate detail of a larger listed change. + +Group findings by entry. For each finding, show the original bullet and a proposed replacement. + +If the entry has no findings, state this result. Do not edit the changelog unless the user asks you to apply fixes. + +When the user asks for fixes, preserve tab indentation and template-literal formatting. diff --git a/.agents/skills/review-changelog/agents/openai.yaml b/.agents/skills/review-changelog/agents/openai.yaml new file mode 100644 index 0000000000..421afc3587 --- /dev/null +++ b/.agents/skills/review-changelog/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Review Changelog" + short_description: "Review changelog entries for style problems" + default_prompt: "Use $review-changelog to review the latest changelog entry." diff --git a/.agents/skills/tanstack-query/SKILL.md b/.agents/skills/tanstack-query/SKILL.md new file mode 100644 index 0000000000..71c3ecc71b --- /dev/null +++ b/.agents/skills/tanstack-query/SKILL.md @@ -0,0 +1,39 @@ +--- +name: tanstack-query +description: Convert Vue server-state code to TanStack Query. Use for useQuery, useMutation, cache invalidation, optimistic updates, or replacement of useAsyncData and manual ref patterns. +--- + +# Convert Data Code to TanStack Query + +Read the applicable `AGENTS.md` files before you edit code. + +Read [the TanStack Query standard](../../../standards/frontend/FETCHING_DATA.md) in full. + +1. Identify the target file from the request. +2. Find `useAsyncData`, `useFetch`, manual API refs, and fetch calls in `onMounted`. +3. Identify mutations that use manual loading, error, or result refs. + +For queries: + +1. Replace manual fetch logic with `useQuery`. +2. Get `api-client` with `injectModrinthClient()`. +3. Use a hierarchical query key with the resource, qualifier, and parameters. +4. Use a computed query key for reactive parameters. +5. Use a computed `enabled` option when the query depends on other data. +6. Use a shared query-option factory when multiple components use the query. + +For mutations: + +1. Replace manual mutation state with `useMutation`. +2. Invalidate or update related query data after success. +3. Use an optimistic update only when the UI needs an immediate response. +4. Cancel the applicable query and save its prior data before an optimistic update. +5. Restore the prior data after an error. Invalidate the query after settlement. + +Remove manual loading and error refs that TanStack Query replaces. Remove obsolete `onMounted` fetch calls. + +Keep Nuxt SSR behavior. Match route-shell prefetch options when `ReadyTransition` and `useReadyState` depend on the query. + +Check query keys, invalidation prefixes, reactive values, and rollback data. + +Run only the checks that the user or the applicable `AGENTS.md` permits. diff --git a/.agents/skills/tanstack-query/agents/openai.yaml b/.agents/skills/tanstack-query/agents/openai.yaml new file mode 100644 index 0000000000..2db5ece878 --- /dev/null +++ b/.agents/skills/tanstack-query/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Migrate to TanStack Query" + short_description: "Migrate Vue server state to TanStack Query" + default_prompt: "Use $tanstack-query to migrate this Vue component to TanStack Query." diff --git a/.claude/skills/api-module/SKILL.md b/.claude/skills/api-module/SKILL.md deleted file mode 100644 index 37b4ac7788..0000000000 --- a/.claude/skills/api-module/SKILL.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -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: ---- - -Refer to the standard: @standards/frontend/ADDING_API_MODULES.md - -## Steps - -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 deleted file mode 100644 index 6558eed3c3..0000000000 --- a/.claude/skills/cross-platform-pages/SKILL.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -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: ---- - -Refer to the standards: @standards/frontend/CROSS_PLATFORM_PAGES.md and @standards/frontend/DEPENDENCY_INJECTION.md - -## Steps - -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. - - If the layout uses TanStack Query for initial route paint with `ReadyTransition` / `useReadyState`, each platform route shell must call `ensureQueryData` for those queries with matching keys and fetchers — see **Platform route shells: prefetch with `ensureQueryData`** in `standards/frontend/CROSS_PLATFORM_PAGES.md`. -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 deleted file mode 100644 index defabf8d2e..0000000000 --- a/.claude/skills/figma-mcp/SKILL.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -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: ---- - -Refer to the standard: @standards/frontend/FIGMA_MCP_USAGE.md -Also read @packages/ui/CLAUDE.md for color token mapping and component conventions. - -## Steps - -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-pass/SKILL.md b/.claude/skills/i18n-pass/SKILL.md deleted file mode 100644 index 7edd69996c..0000000000 --- a/.claude/skills/i18n-pass/SKILL.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: i18n-pass -description: Perform an i18n localization pass on changed files or a pull request, converting hard-coded English strings to the @modrinth/ui i18n system. Use when internationalizing a set of changes, reviewing a PR for untranslated strings, or converting a specific component. -argument-hint: [file-path-or-pr-number] ---- - -Refer to the standard: @standards/frontend/INTERNATIONALIZATION.md - -## Steps - -1. **Identify the scope of changes:** - - If `$ARGUMENTS` is a PR number, run `gh pr diff $ARGUMENTS` to get the changed files. - - If `$ARGUMENTS` is a file path, use that directly. - - If no argument, check `git diff` for uncommitted changes. -2. **Read the standard above** for the message definition pattern, ICU format rules, and `IntlFormatted` usage. -3. **Filter to Vue SFCs** — only `.vue` files need i18n passes. Skip non-component files. -4. **For each file, scan for hard-coded strings:** - - ` ``` -**App** — uses Tauri `invoke`: +The app uses Tauri `invoke`: ```vue @@ -83,14 +91,14 @@ import { provideContentManager, ContentPageLayout } from '@modrinth/ui' import { invoke } from '@tauri-apps/api/core' const items = ref([]) -await invoke('get_instance_content', { instanceId }).then(/* map to ContentItem[] */) +await invoke('get_instance_content', { instanceId }).then(/* Map the result to ContentItem[]. */) provideContentManager({ items, deleteItem: async (item) => { await invoke('delete_content', { instanceId, path: item.file_path }) }, - // ... rest of the contract + // Implement the remaining contract fields. }) @@ -99,29 +107,33 @@ provideContentManager({ ``` -### Optional capabilities +### Optional Capabilities -The DI contract uses optional fields for features that not every platform supports. The layout checks for them before rendering the corresponding UI: +Use optional contract fields for capabilities that are not available on all platforms. + +Check that an optional field exists before you show its UI: ```ts -// Contract +// Contract fields. bulkUpdateItems?: (items: ContentItem[]) => Promise shareItems?: (items: ContentItem[], format: string) => void -// Layout checks before showing UI +// Show the UI only when the capability exists. v-if="ctx.bulkUpdateItems && hasOutdatedProjects" ``` -### Props vs DI +### Props and DI -| Use | When | -| --------- | ------------------------------------------------------------------------------------------ | -| **DI** | Data depends on _how_ it's fetched — API calls, file operations, navigation (per-platform) | -| **Props** | Data is the same regardless of platform — configuration flags, display options | +| Use | Condition | +| ----- | -------------------------------------------------------------------------- | +| DI | Use when API calls, file operations, or navigation differ by platform. | +| Props | Use when configuration and display data are the same on all platforms. | ## Wrapped Pages (`layouts/wrapped/`) -For pages where the **logic is identical** on both platforms — same API source, same data fetching, same state management. These are full page-level Vue components that directly implement routes: +Use a wrapped page when both platforms use the same API source, data logic, and state logic. + +A wrapped page is a complete page-level Vue component. Its directory structure matches the route structure: ``` wrapped/hosting/manage/ @@ -132,7 +144,9 @@ wrapped/hosting/manage/ └── [id]/onboarding.vue ``` -Wrapped pages handle their own data fetching (typically via TanStack Query and `api-client`) and are consumed as simple component imports in both frontends: +Wrapped pages get their own data. They usually use TanStack Query and `api-client`. + +Import the wrapped page as a simple component in both frontends: ```vue @@ -145,32 +159,48 @@ import { ServersManageContentPage } from '@modrinth/ui' ``` -### Platform route shells: prefetch with `ensureQueryData` +### Prefetch Data in Platform Route Shells + +#### `ReadyTransition` and `useReadyState` -#### Wrapped layout: `ReadyTransition` and `useReadyState` +Many wrapped pages put the main UI in [`ReadyTransition`](../../packages/ui/src/components/base/ReadyTransition.vue). -Many wrapped pages wrap the main UI in [`ReadyTransition`](../../packages/ui/src/components/base/ReadyTransition.vue) with `:pending` driven by [`useReadyState`](../../packages/ui/src/composables/use-ready-state.ts) on the **primary** TanStack query (true only on the first load while that query has no cached data yet—background refetches stay “ready”). That avoids flashing empty content before data exists. +The `:pending` prop usually comes from [`useReadyState`](../../packages/ui/src/composables/use-ready-state.ts) for the primary TanStack query. + +The state is true only during the first load when the cache has no data. Background refetches keep the page ready. + +This behavior prevents empty content from appearing before the data exists. ```vue - + ``` ```ts -const primaryQuery = useQuery({ /* ... */ }) +const primaryQuery = useQuery({ /* Query options. */ }) const readyPending = useReadyState(primaryQuery) -// or useReadyState({ isLoading, data }) when not using the full query object + +// Use this form when the complete query object is not available. +const readyPendingFromState = useReadyState({ isLoading, data }) ``` -Shell prefetch (below) warms the cache so that on navigation the query often **already has data** when the layout mounts; `pending` stays false and `ReadyTransition` can skip the enter animation on that fast path (see `ReadyTransition` docs and stories). +Shell prefetch adds data to the cache before the layout mounts. On this fast path, `pending` stays false. + +`ReadyTransition` can then omit its enter animation. Refer to the `ReadyTransition` documentation and stories for details. -#### Rule: `ensureQueryData` in each platform route shell +#### Use `ensureQueryData` in Each Route Shell -When a wrapped layout uses that pattern, the **thin platform page** that imports the layout must **prefetch the same primary query** in ` @@ -41,50 +42,54 @@ const modal = ref | null>(null) -

Modal content here.

+

Modal content.

``` -Call `show(event?)` to open the modal. Passing the `MouseEvent` triggers an animation originating from the click position. Call `hide()` to close it programmatically. +Call `show(event?)` to open the modal. A `MouseEvent` starts the animation at the click position. + +Call `hide()` to close the modal from code. ## Props -| Prop | Type | Default | Description | -| --------------------- | ------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------ | -| `header` | `string` | — | Title text displayed in the header bar | -| `hideHeader` | `boolean` | `false` | Hides the entire header (title + close button) | -| `mergeHeader` | `boolean` | `false` | Removes the header bar; renders a floating close button over the content | -| `closable` | `boolean` | `true` | Shows the close button and enables ESC / click-outside dismissal | -| `disableClose` | `boolean` | `false` | Disables all close actions (close button, ESC, click-outside). The close button appears disabled | -| `closeOnEsc` | `boolean` | `true` | Allow closing with the Escape key | -| `closeOnClickOutside` | `boolean` | `true` | Allow closing by clicking the overlay | -| `scrollable` | `boolean` | `false` | Enables scroll tracking with top/bottom fade indicators | -| `maxContentHeight` | `string` | `’70vh’` | Max height of the scrollable content area (only applies when `scrollable`) | -| `noPadding` | `boolean` | `false` | Removes padding from the content area for edge-to-edge layouts | -| `maxWidth` | `string` | `’60rem’` | Maximum width of the modal | -| `width` | `string` | `fit-content` | Width of the modal body | -| `noblur` | `boolean` | — | Disables backdrop blur. Defaults to the value from `injectModalBehavior` | -| `fade` | `’standard’ \| ‘warning’ \| ‘danger’` | `’standard’` | Overlay color variant | -| `danger` | `boolean` | `false` | **Deprecated** — use `fade="danger"` instead | -| `onShow` | `() => void` | — | Called when the modal opens | -| `onHide` | `() => void` | — | Called when the modal closes | +| Prop | Type | Default | Description | +| --------------------- | ----------------------------------------- | ------------- | ------------------------------------------------------------------ | +| `header` | `string` | None | Sets the title in the header bar. | +| `hideHeader` | `boolean` | `false` | Hides the title and close button. | +| `mergeHeader` | `boolean` | `false` | Replaces the header bar with a floating close button. | +| `closable` | `boolean` | `true` | Enables the close button, Escape key, and overlay click. | +| `disableClose` | `boolean` | `false` | Disables all close actions and shows a disabled close button. | +| `closeOnEsc` | `boolean` | `true` | Enables the Escape key as a close action. | +| `closeOnClickOutside` | `boolean` | `true` | Enables an overlay click as a close action. | +| `scrollable` | `boolean` | `false` | Enables scroll tracking and edge-fade indicators. | +| `maxContentHeight` | `string` | `'70vh'` | Sets the maximum scrollable-content height. | +| `noPadding` | `boolean` | `false` | Removes content padding for edge-to-edge layouts. | +| `maxWidth` | `string` | `'60rem'` | Sets the maximum modal width. | +| `width` | `string` | `fit-content` | Sets the modal-body width. | +| `noblur` | `boolean` | None | Disables the backdrop blur. The DI behavior supplies the default. | +| `fade` | `'standard' \| 'warning' \| 'danger'` | `'standard'` | Sets the overlay color variant. | +| `danger` | `boolean` | `false` | Deprecated. Use `fade="danger"`. | +| `onShow` | `() => void` | None | Runs when the modal opens. | +| `onHide` | `() => void` | None | Runs when the modal closes. | + +`maxContentHeight` has an effect only when `scrollable` is true. ## Slots -### Default slot +### Default Slot -The main content area. Rendered inside a padded, optionally scrollable container. +The default slot contains the main content. `NewModal` puts it in a padded container that can scroll. ```vue -

Are you sure you want to proceed?

+

Are you sure that you want to continue?

``` -### `title` slot +### `title` Slot -Replaces the default header text. Use this when you need custom markup in the header (e.g. an icon next to the title or a badge). +The `title` slot replaces the default header text. Use it for custom header markup, such as an icon or badge. ```vue @@ -92,17 +97,19 @@ Replaces the default header text. Use this when you need custom markup in the he Custom Title -

Content here.

+

Content.

``` -### `actions` slot +### `actions` Slot + +The `actions` slot makes an action bar below the content. The bar uses `p-4 pt-0` padding. -Renders a bottom action bar below the content area (with `p-4 pt-0` padding). Use this for confirm/cancel buttons. +Use this slot for confirmation and cancellation buttons: ```vue -

This action cannot be undone.

+

You cannot reverse this action.

``` -### 4. Create the wrapper component +### 4. Create the Wrapper Component -The wrapper provides context and renders `MultiStageModal`: +Provide the context from the wrapper. Then, render `MultiStageModal`: ```vue @@ -323,20 +340,20 @@ defineExpose({ show: () => modal.value?.show() }) ## Modal API -`MultiStageModal` exposes via ref: +`MultiStageModal` exposes these methods and properties through its reference: -| Method/Property | Description | -| --------------------- | ----------------------------------- | -| `show()` | Open the modal | -| `hide()` | Close the modal | -| `setStage(indexOrId)` | Jump to stage by index or string id | -| `nextStage()` | Advance to next non-skipped stage | -| `prevStage()` | Go back to previous stage | -| `currentStageIndex` | Ref to current stage index | +| Method or property | Description | +| ---------------------- | -------------------------------------------- | +| `show()` | Opens the modal. | +| `hide()` | Closes the modal. | +| `setStage(indexOrId)` | Goes to a stage by index or string ID. | +| `nextStage()` | Goes to the next applicable stage. | +| `prevStage()` | Goes to the previous stage. | +| `currentStageIndex` | Contains the current stage index as a `Ref`. | -## Non-Progress Stages (Edit Sub-Flows) +## Non-Progress Stages -For stages that shouldn't appear in the progress bar (e.g. editing a specific field from a summary page): +Use a non-progress stage for an edit flow that must not appear in the progress bar: ```ts export const editLoadersStageConfig: StageConfigInput = { @@ -355,16 +372,18 @@ export const editLoadersStageConfig: StageConfigInput = { } ``` -Navigate to it with `modal.value?.setStage('edit-loaders')` — it won't affect the progress indicator. +Call `modal.value?.setStage('edit-loaders')` to open the stage. This stage does not change the progress indicator. ## Reference Implementation -The version creation/edit modal is the most complete example: +The version create-and-edit modal is the most complete example: + +| File | Purpose | +| ------------------------------------------------------------- | -------------------------------------- | +| `apps/frontend/src/providers/version/manage-version-modal.ts` | Contains context and application logic. | +| `apps/frontend/src/providers/version/stages/index.ts` | Exports all stage configurations. | +| `apps/frontend/src/providers/version/stages/*-stage.ts` | Contains each stage configuration. | -| File | Purpose | -| ------------------------------------------------------------- | --------------------------------- | -| `apps/frontend/src/providers/version/manage-version-modal.ts` | Context creation + business logic | -| `apps/frontend/src/providers/version/stages/index.ts` | Stage config barrel export | -| `apps/frontend/src/providers/version/stages/*-stage.ts` | Individual stage configs | +The context has computed properties for conditional UI. It also has dependency watchers and granular button loading states. -The context includes computed properties for conditional UI, watchers for auto-fetching dependencies, loading states for granular button disabling, and both "create" and "edit" flows sharing the same stages with different button configs. +The create and edit flows use the same stages with different button configurations. diff --git a/standards/frontend/SURFACE_SYSTEM.md b/standards/frontend/SURFACE_SYSTEM.md index 79b656a21a..8aa3aa2383 100644 --- a/standards/frontend/SURFACE_SYSTEM.md +++ b/standards/frontend/SURFACE_SYSTEM.md @@ -1,25 +1,31 @@ # Surface System -Use `surface-*` variables to describe UI elevation and separation. The scale is ordered from the page base up through stronger raised surfaces and strokes. +Use `surface-*` variables to show UI elevation and separation. The scale starts at the page base and ends at strong strokes. ## Layers -| Token | Use | -| ----------- | ------------------------------------------------------------------- | -| `surface-1` | Page background. | -| `surface-2` | Default raised surfaces, table rows, and standard card backgrounds. | -| `surface-3` | Header bands, inputs, dropdown surfaces, and card hover states. | -| `surface-4` | Standard strokes and outlines, including table outlines. | -| `surface-5` | Strong strokes for surfaces that need extra separation. | +| Token | Use | +| ----------- | ----------------------------------------------------------------- | +| `surface-1` | Use for the page background. | +| `surface-2` | Use for raised surfaces, table rows, and standard card backgrounds. | +| `surface-3` | Use for header bands, inputs, dropdowns, and card hover states. | +| `surface-4` | Use for standard strokes, outlines, and table outlines. | +| `surface-5` | Use for strong strokes that need more separation. | ## Strokes -Use `surface-4` for normal outlines and dividers. Tables should use `surface-4` for their outer border and row separators. +Use `surface-4` for standard outlines and dividers. Use it for table borders and row separators. -Reserve `surface-5` for stronger outlines, such as modal frames, high-emphasis separators, or hover states on elements that already sit on `surface-4`. +Use `surface-5` for modal frames, strong separators, and hover states above `surface-4`. ## Backgrounds -Use `surface-1` for page backgrounds and `surface-2` for ordinary raised content. Use `surface-3` for header strips, inputs, and temporary elevation such as hover states. Use `surface-4` sparingly as a stronger raised background, usually for controls or badges that need to sit above nearby content. +Use `surface-1` for page backgrounds. Use `surface-2` for standard raised content. -Avoid using legacy aliased background variables for new UI. Prefer explicit `bg-surface-*` and `border-surface-*` utilities so the layer intent is visible in the component. +Use `surface-3` for header strips, inputs, and temporary elevation. A hover state is an example of temporary elevation. + +Use `surface-4` only for controls or badges that must appear above adjacent content. + +Do not use legacy aliased background variables in new UI. Use explicit `bg-surface-*` and `border-surface-*` utilities. + +These utilities show the intended layer in the component. diff --git a/standards/maintaining/CHANGELOG.md b/standards/maintaining/CHANGELOG.md index 421aaf2098..9d72bb8868 100644 --- a/standards/maintaining/CHANGELOG.md +++ b/standards/maintaining/CHANGELOG.md @@ -1,111 +1,136 @@ # Changelog Style Guide -## The core rule +## Core Rule -**Each bullet describes one user-visible change, written from the user's perspective, in plain language, as a single sentence.** +Each bullet describes one user-visible change. Write one plain-language sentence from the perspective of the user. -If you can't explain the change without referencing internal code, components, or refactors, it probably doesn't belong in the changelog. +Do not add a change that you can explain only with internal code, component, or refactor details. -## Voice and tense +## Voice and Tense -- **Past tense, implied subject.** The section heading (`## Added`, `## Fixed`, `## Changed`) supplies the verb's mood - bullets read as a continuation of it. - - Good: `Fixed a missing gap between the project filter tabs and the project list.` - - Good: `Added support for Java 25.` - - Avoid: `We fixed...`, `This fixes...`, `Fixes...` (present tense), `Will fix...` -- **No first person.** Don't say "we" or "our" inside a bullet. The exception is featured release callouts that link to a blog post (`We've overhauled the Content tab...`). -- **No second person except for direct user actions.** "You" is fine when describing what the user can now do (`Joining a server from the app downloads the required content and launches you directly into the server.`), but don't address the user gratuitously. +- Use the past tense with an implicit subject. The section heading supplies the context for the bullet. + - Correct: `Fixed a missing gap between the project filter tabs and the project list.` + - Correct: `Added support for Java 25.` + - Incorrect: `We fixed...`, `This fixes...`, `Fixes...`, or `Will fix...`. +- Do not use the first person. A featured release that links to a blog post is an exception. +- Use the second person only for a direct user action. -## Section/verb agreement +Example of a direct action: `Joining a server downloads the required content and opens the server.` -The opening verb must match the section it lives under. Don't put "Fixed X" bullets inside `## Added`. +## Section and Verb Agreement -| Section | Typical opening verbs | -| ------------- | ------------------------------------------------------------------------------- | -| `## Added` | Added, Introduced, New | -| `## Changed` | Refreshed, Redesigned, Moved, Renamed, Updated, Consolidated, Improved, Rebuilt | -| `## Fixed` | Fixed | -| `## Security` | Fixed (security framing) | +Make the first verb agree with its section. Do not put a `Fixed` bullet in `## Added`. -In `## Added`, the leading "Added" is often dropped because it's redundant with the heading: +| Section | Typical first words | +| ------------- | -------------------------------------------------------------------------- | +| `## Added` | Added, Introduced, New | +| `## Changed` | Refreshed, Redesigned, Moved, Renamed, Updated, Consolidated, Improved | +| `## Fixed` | Fixed | +| `## Security` | Fixed, with a clear security context | -- `- Server stats inside server settings modal, in info card.` -- `- Confirmation modal for resubscribing to a server.` +You can omit `Added` in the `## Added` section because the heading supplies it: -In `## Fixed`, the leading "Fixed" is **kept** in most entries - it reads more clearly. Be consistent within a single entry. +- `Server statistics in an information card inside the server settings modal.` +- `Confirmation modal for server resubscription.` -## What to write about +Keep `Fixed` in most `## Fixed` bullets because it makes the text clear. Use one pattern in each entry. -Describe the **observable behavior**, not the implementation. +## Content -- Good: `Server CPU and memory graphs no longer freeze on the last value after a hard crash or out-of-memory kill.` -- Bad: `Refactored the metrics polling hook to clear stale state on socket disconnect.` +Describe the result that the user can see. Do not describe the implementation. -- Good: `Historical log files are now fetched in the background when opening the Logs page, so switching between them is instant.` -- Bad: `Moved log file fetching into a background worker.` +- Correct: `Server CPU and memory graphs no longer freeze after a hard crash or out-of-memory termination.` +- Incorrect: `Refactored the metrics polling hook to clear stale state after a socket disconnection.` -If a refactor has no user-visible effect, **don't list it**. Internal cleanup, dependency bumps, and code moves don't belong in the changelog unless they produce a noticeable difference (perf, reliability, consistency). +- Correct: `Historical log files now load in the background, so selection between files is immediate.` +- Incorrect: `Moved log file fetching into a background worker.` -## Specificity +Do not list a refactor that has no user-visible result. -Be specific enough that a user reading the changelog can recognize the thing you're talking about. +You can list an internal change when it gives a visible improvement in performance, reliability, or consistency. + +## Specific Terms + +Give sufficient detail for the user to identify the applicable item. - Vague: `Fixed a bug on the project page.` -- Better: `Fixed project versions table overflowing outside of table. Version tags will now truncate.` +- Specific: `Fixed project version rows that extended past the table. Version tags now truncate.` - Vague: `Improved the UI.` -- Better: `Refreshed the server cards UI for consistency.` +- Specific: `Refreshed the server cards for visual consistency.` -Name the page, tab, modal, or feature you're talking about. "The Content tab", "the server panel header", "the Worlds tab", "the project page" - these give the reader a concrete anchor. +Name the applicable page, tab, modal, or feature. Examples include the Content tab, server panel header, Worlds tab, and project page. ## Length -- **One sentence per bullet.** If you need two sentences, you probably have two bullets, or one bullet plus a sub-bullet. -- Aim for under ~25 words. Long bullets are usually a sign that the change is being over-explained or is actually multiple changes. -- Sub-bullets (indented with a tab) are allowed when one change has several facets - see the `## Added` section in the v0.12.0 app release for a good example. +- Write one sentence in each bullet. +- Use a second bullet when the change needs a second sentence. +- Use fewer than 25 words when possible. +- Use tab-indented sub-bullets when one change has multiple related parts. + +Refer to the `## Added` section in the v0.12.0 app release for a sub-bullet example. ## Punctuation -- **End every bullet with a period.** This is inconsistent in the historical file, but periods are the more common pattern and the one to follow going forward. -- Use sentence case, not Title Case. -- Use straight quotes, not curly quotes (`"foo"` not `"foo"`). -- Use proper code formatting for filenames, flags, and literal strings: `` `.log` ``, `` `Restart` ``. +- End each bullet with a period. +- Use sentence case, not title case. +- Use straight quotation marks, not curly quotation marks: `"foo"`. +- Use code formatting for filenames, flags, and literal strings: `.log` and `Restart`. + +Historical entries do not always use periods. Use periods in all new entries. + +## Product and UI Names + +- Use the public names `Modrinth App`, `Modrinth Hosting`, and `Modrinth`. +- Do not use deprecated names, such as `Modrinth Servers`. +- Use the labels that appear in the UI. +- Capitalize a tab or page name when you refer to its label. +- Use lowercase when you refer to a generic action, such as `browse content`. -## Naming things +Examples of UI labels include Content tab, Worlds tab, Files tab, Logs page, server panel, project page, and Discover page. -- Use the public, branded name: **Modrinth App**, **Modrinth Hosting**, **Modrinth** - not "the app", "servers", "Modrinth Servers" (deprecated). Capitalize product names. -- Refer to UI surfaces by the label the user sees: **Content tab**, **Worlds tab**, **Files tab**, **Logs page**, **server panel**, **project page**, **Discover page**. -- Capitalize tab and page names when referring to them by name (`the Content tab`), but not when used generically (`browse content`). +## Prohibited Content -## Don't +- Do not assign blame. Describe the correction without the release that caused the problem. +- Do not refer to pull requests, issues, or commits. +- Do not refer to internal team members or processes. +- Do not apologize or add an opinion about the change. +- Do not use vague intensifiers. Give a measurement when possible, or remove the adverb. +- Do not list each small correction from one larger change. +- Do not use `issue with` or `issue where` as filler. -- **Don't blame.** Avoid "fixed a regression introduced in v0.12.0" - just describe the fix. -- **Don't reference PRs, issues, or commits.** The changelog is for users, not contributors - the exception is notable third-party contributions, where you should credit the contributor by linking their GitHub profile (e.g. `Added support for Java 25. Thanks to [@username](https://github.com/username)!`). Sharing credit for community contributions is encouraged. -- **Don't reference internal team members or processes.** No "as requested by support", no "per the design review". -- **Don't apologize or editorialize.** Skip "unfortunately", "finally", "long-awaited", "we know this has been a pain point". State the change. -- **Don't use vague intensifiers.** "Significantly improved", "much better", "vastly faster" - quantify if you can, otherwise drop the adverb. -- **Don't list every sub-fix of a bigger change separately.** If you redesigned the server panel header, write one bullet about the redesign rather than six bullets about each moved element. -- **Don't use "issue with" / "issue where" as filler.** `Fixed an issue where buttons were misaligned` → `Fixed misaligned buttons.` +You can credit a notable community contribution with a link to the contributor's GitHub profile. -## Examples - rewriting weak bullets +Example: `Added support for Java 25. Thanks to [@username](https://github.com/username)!` + +Replace `Fixed an issue with misaligned buttons` with `Fixed misaligned buttons.` + +## Weak-Bullet Rewrites | Weak | Better | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- | -| `Fixed a bug.` | `Fixed project icons becoming extremely bright on hover.` | -| `Various improvements to the server panel.` | Split into specific bullets, or drop entirely. | +| `Fixed a bug.` | `Fixed excessive brightness on project icons during hover.` | +| `Various improvements to the server panel.` | Divide it into specific bullets, or remove it. | | `Refactored the logs page to use a new component.` | `Redesigned the Logs page to match the Modrinth Hosting server panel.` | -| `Fixed an issue where the server address wasn't copyable.` | `Server address in the panel header can now be clicked to copy it to your clipboard.` | -| `Made some changes to the content tab.` | Either drop, or list each user-visible change as its own bullet. | -| `Fixed UX issues.` | Name the specific UX issue. | +| `Fixed an issue where the server address was not copyable.` | `The server address in the panel header now copies to the clipboard when selected.` | +| `Made some changes to the Content tab.` | List each user-visible change, or remove the bullet. | +| `Fixed UX issues.` | Name the specific user-experience problem. | + +## Featured Release Bullets + +A featured release has a linked blog-post heading, such as `## [Introducing Server Projects](/news/article/...)`. + +Use one to four lines below the heading to summarize the primary changes. Then, link to the blog post. -## Featured release bullets +The bullets do not need to contain all details. The blog post contains the complete information. -When an entry has a linked blog post heading (e.g. `## [Introducing Server Projects](/news/article/...)`), the bullets underneath summarize the *highlights* in 1–4 lines, then link out. They don't need to be exhaustive - that's what the blog post is for. +## Bullet Checklist -## Quick checklist before committing a bullet +Before you commit a bullet, make sure that it meets these requirements: -1. Would a non-developer user understand it? -2. Does it describe behavior, not implementation? -3. Is the verb in the right tense for its section? -4. Does it name the specific surface (tab/page/modal)? -5. Is it one sentence, ending in a period? -6. Is there a vague word ("issue", "bug", "various", "some") I can replace with something concrete? +1. A user who is not a developer can understand it. +2. It describes behavior, not implementation. +3. Its verb uses the correct tense for the section. +4. It identifies the applicable tab, page, modal, or feature. +5. It contains one sentence and ends with a period. +6. It replaces vague words with specific terms.