Building or modifying components in the RomM v2 frontend (frontend/src/v2/). Use when creating/editing v2 primitives (R* components in src/v2/lib/), shared composites, or feature composites — covers the three-tier model, file/folder conventions, SFC structure, import order, barrels, Storybook requirements, and v2 anti-patterns. Trigger on any work under frontend/src/v2/.
Scanned 9/20/2026
Install to Claude Code
npx -y skills add rommapp/romm --skill frontend-v2-components --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Frontend V2 Components?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/rommapp-frontend-v2-components)More formats (shields.io, HTML) on the badges page.
---
name: frontend-v2-components
description: Building or modifying components in the RomM v2 frontend (frontend/src/v2/). Use when creating/editing v2 primitives (R* components in src/v2/lib/), shared composites, or feature composites — covers the three-tier model, file/folder conventions, SFC structure, import order, barrels, Storybook requirements, and v2 anti-patterns. Trigger on any work under frontend/src/v2/.
---
# RomM Frontend v2 — Component Constitution
This governs work inside `frontend/src/v2/`. **v1 is frozen** (`src/views/`, `src/components/`, `src/console/`, `src/layouts/`) — never refactor it; it will be deleted wholesale in a final wave. v2 is gated by `user.ui_settings.uiVersion`.
> Official language for all code, comments, identifiers, `.md`, and commit/PR messages: **English**.
Related skills: `frontend-v2-theming` (tokens/colors), `frontend-v2-input` (focus/gamepad/responsive), `frontend-v2-patterns` (errors/loading/forms/permissions/confirmations), `frontend-i18n`, `review-polish`.
---
## Premises (stable)
1. **v1 is frozen.** Don't touch `src/views/`, `src/components/`, `src/console/`, `src/layouts/`. When coexistence forces a v2 fork of a store/composable/util, annotate the v1 export with `@deprecated` pointing at the v2 replacement.
2. **Three component tiers** (below).
3. **Shared resources are canonical.** Pinia stores, API services, OpenAPI types (`src/__generated__/`), locales, utils — v2 _imports_ them, never forks them. Additive changes to shared resources are allowed; changing a shared store API to work around a v2 call-site issue is not.
4. **TypeScript strict.** Zero `any` (justify with a comment if unavoidable). No `as unknown as ...`; fix the source or define an intermediate type.
5. **Universal substitution.** When an `R*` primitive exists, use it. If it doesn't, create or extend it. Never drop to raw HTML when a primitive applies.
6. **Attribute forwarding contract.** A primitive whose root is polymorphic (`<component :is>`) or that forwards to an inner element uses `defineOptions({ inheritAttrs: false })` + `v-bind="$attrs"` + slot passthrough, so call-site attrs and listeners land on the intended node instead of vanishing silently.
7. **Layout in scoped CSS against tokens.** v2 uses no utility-class framework and no component library: no Tailwind, no Vuetify. Write `display: flex` / `gap: var(--r-space-*)` in the component's own `<style scoped>`. Recurring structure becomes a primitive (`RList`, `RToolbar`, `RCollapsible`, `RVirtualScroller`), not a repeated block of classes.
8. **Accessibility & performance** are requirements: semantic HTML, focus management, contrast, ARIA on icon-only controls; lazy-load heavy views, virtualize large lists, stable `:key` on every `v-for`.
---
## The three tiers
| Tier | Path | Prefix | Stores/services/router/emitter/i18n | Story | Domain knowledge |
| --------------------- | ------------------------------ | -------------- | ----------------------------------- | --------- | --------------------------------- |
| **Primitive** | `src/v2/lib/` | `R*` mandatory | **No** | Mandatory | None |
| **Shared composite** | `src/v2/components/shared/` | no prefix | Yes | Optional | Cross-feature, no specific domain |
| **Feature composite** | `src/v2/components/<feature>/` | no prefix | Yes | Optional | Feature-specific |
### A component is a primitive only if all three hold
1. Does not depend on stores, services, router, or emitter.
2. No knowledge of a product domain (ROM, Platform, Collection, User…). `RAvatar` yes, `UserAvatar` no.
3. Its API can be described without naming features — generic props/slots/events.
If any fails: **shared composite** if generic across features, **feature composite** if owned by one feature. Edge cases get raised to the user, not auto-decided. Consumer count never demotes a primitive.
### Primitive boundaries
- **Can use**: tokens, other primitives, Vue, generic composables (`useInput*`, `useFocus*`).
- **Cannot use**: Pinia stores, API services, `emitter`, `router` (a `RouterLink` may be accepted as a prop), `i18n` directly. **No `$t()` in primitives** — text comes via props or slots.
- **Chrome labels** are the exception to "via props": the accessible name
of a control the primitive renders for itself (a dialog's close button, a
chip's remove X, a date field's steppers, a stepper's "Step 2 of 5") is
not caller-supplied content, and a per-instance prop for it has to be
passed at every call site to have any effect. Read them from
`useChromeLabels()` (`lib/a11y/chromeLabels.ts`), which the app fills with
translations via `app.provide` and which falls back to English when
unprovided. Never hard-code an `aria-label` string (including a bound
literal like `:aria-label="'Close'"`) or an English prop default in
`lib/`. A label prop may still exist as a per-instance override,
defaulting to `undefined` so it resolves through the bundle.
---
## File & folder conventions
- **Primitive**: one per folder — `RFoo/RFoo.vue`, `RFoo/RFoo.stories.ts`, `RFoo/index.ts`, optional `RFoo/types.ts`.
- **Composite**: flat `.vue` if one file suffices; a folder with the same internal structure (no story required) if it has sub-pieces.
- **Barrel**: `src/v2/lib/index.ts` re-exports every primitive — update it when a new primitive ships. Composites are imported directly by path; no barrel. No single-file `index.ts` that just re-exports to shorten a path.
### SFC structure
- `<script setup lang="ts">` always.
- `defineOptions({ inheritAttrs: false })` on every wrapper, paired with `v-bind="$attrs"` and slot passthrough (without the bind, attrs vanish silently).
- Props via `defineProps<Props>()` (interface), never runtime declarations. Emits via `defineEmits<{...}>()`. Slots with payload via `defineSlots<{}>()`.
- Order: `<script setup>` → `<template>` → `<style scoped>`. Unscoped `<style>` (teleport overrides only) goes after the scoped block.
### Import order & aliases
```ts
// 1. External
// 2. v2 primitives
import { RBtn, RDialog } from "@v2/lib";
import { computed, ref } from "vue";
import type { SimpleRom } from "@/__generated__";
// 5. Canonical shared resources
import storeAuth from "@/stores/auth";
// 4. v2 feature siblings
import GameCard from "@/v2/components/GameCard.vue";
// 3. v2 composables / shared
import { useCan } from "@/v2/composables/useCan";
```
- `@v2/lib` — primitives barrel. `@/v2/...` — anything else under v2. `@/...` — canonical shared resources. Never relative paths (`../../foo`) when an alias exists.
- Shared v2 types live in `src/v2/types/`; backend types come from `src/__generated__/`; not `src/types/` (legacy).
### Composables
- `use` prefix; single named export from `composables/useFoo/index.ts`; fully typed args/return; no side effects on module load (init on first call). Creating a v2-only composable when a v1 equivalent exists is allowed.
### Console logging
- `console.error` allowed for production-visible errors. `console.log`/`console.warn` must not ship. `console.debug` is dev-only — remove before PR.
---
## Storybook (mandatory for `/lib`)
- Every primitive ships at least one story with controls and at least one variant per theme.
- A new interactive primitive that warrants gamepad navigation ships a `play()` interaction.
- Modified primitive: existing story must still render and its interactions still pass.
- `npm run test` runs Vitest **and** every `/lib` story's `play()` via `composeStories`. Don't duplicate coverage between Vitest (pure logic) and Storybook `play()` (components).
---
## Anti-patterns (beyond what the premises already say)
1. Don't change shared store APIs to work around a v2 call-site issue. (Fix the call site; the Gallery lesson was calling `romsStore.reset()` from the view, not adding `_fetchSeq` to the store.)
2. Don't drop to inline role checks — always go through `useCan` (see `frontend-v2-patterns`).
3. Don't reinvent a surface — dialog/menu/popover/card all go through their primitive; special cases become a new prop, not a parallel surface.
4. Don't hand-roll a `<form>`; use `RForm`.
5. Don't add backwards-compat shims inside v2: delete removed code; no `// removed`, no renamed-but-unused exports, no deprecated wrappers that just call the new function.
6. Don't write redundant tests; don't touch v1; never `--no-verify` on commits.
**Allowed (often misread):** modifying shared stores/services/utils _additively_; creating v2-only composables; importing from `src/__generated__/`.
---
## Known debt (focused follow-ups)
- **When v1 dies**: move `uiVersion` into `UI_SETTINGS_KEYS`; drop `.r-v2-*` scope classes (tokens move to `:root`); simplify `useUISettings` sync; delete `useGameAnimation`; drop the color-string→tone collapser in `NotificationHost`; remove the Vuetify rule arrays in `stores/users.ts`.
Full reference: `docs/FRONTEND_ARCHITECTURE.md`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!