RomM Frontend v2 — Component Constitution
SkillFiles & storageGuides your agent to build and edit Vue components in the RomM v2 frontend following its three-tier component rules.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the RomM Frontend v2 — Component Constitution skill
About this capability
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 requir
What this skill tells your AI
The instructions your AI receives, as published by rommapp/romm in .claude/skills/frontend-v2-components/SKILL.md and read by ahel’s review.
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)
- 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@deprecatedpointing at the v2 replacement. - Three component tiers (below).
- 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. - TypeScript strict. Zero
any(justify with a comment if unavoidable). Noas unknown as ...; fix the source or define an intermediate type. - 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. - Attribute forwarding contract. A primitive whose root is polymorphic (
<component :is>) or that forwards to an inner element usesdefineOptions({ inheritAttrs: false })+v-bind="$attrs"+ slot passthrough, so call-site attrs and listeners land on the intended node instead of vanishing silently. - 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. - Accessibility & performance are requirements: semantic HTML, focus management, contrast, ARIA on icon-only controls; lazy-load heavy views, virtualize large lists, stable
:keyon everyv-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
- Does not depend on stores, services, router, or emitter.
- No knowledge of a product domain (ROM, Platform, Collection, User…).
RAvataryes,UserAvatarno. - 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(aRouterLinkmay be accepted as a prop),i18ndirectly. No$t()in primitives — text comes via props or slots.
File & folder conventions
- Primitive: one per folder —
RFoo/RFoo.vue,RFoo/RFoo.stories.ts,RFoo/index.ts, optionalRFoo/types.ts. - Composite: flat
.vueif one file suffices; a folder with the same internal structure (no story required) if it has sub-pieces. - Barrel:
src/v2/lib/index.tsre-exports every primitive — update it when a new primitive ships. Composites are imported directly by path; no barrel. No single-fileindex.tsthat just re-exports to shorten a path.
SFC structure
<script setup lang="ts">always.defineOptions({ inheritAttrs: false })on every wrapper, paired withv-bind="$attrs"and slot passthrough (without the bind, attrs vanish silently).- Props via
defineProps<Props>()(interface), never runtime declarations. Emits viadefineEmits<{...}>(). Slots with payload viadefineSlots<{}>(). - Order:
<script setup>→<template>→<style scoped>. Unscoped<style>(teleport overrides only) goes after the scoped block.
Import order & aliases
// 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 fromsrc/__generated__/; notsrc/types/(legacy).
Composables
useprefix; single named export fromcomposables/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.errorallowed for production-visible errors.console.log/console.warnmust not ship.console.debugis 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 testruns Vitest and every/libstory'splay()viacomposeStories. Don't duplicate coverage between Vitest (pure logic) and Storybookplay()(components).
Anti-patterns (beyond what the premises already say)
- 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_fetchSeqto the store.) - Don't drop to inline role checks — always go through
useCan(seefrontend-v2-patterns). - Don't reinvent a surface — dialog/menu/popover/card all go through their primitive; special cases become a new prop, not a parallel surface.
- Don't hand-roll a
<form>; useRForm. - 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. - Don't write redundant tests; don't touch v1; never
--no-verifyon 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
uiVersionintoUI_SETTINGS_KEYS; drop.r-v2-*scope classes (tokens move to:root); simplifyuseUISettingssync; deleteuseGameAnimation; drop the color-string→tone collapser inNotificationHost; remove the Vuetify rule arrays instores/users.ts.
Full reference: docs/FRONTEND_ARCHITECTURE.md.
Signals
- GitHub stars
- 13k
- Forks
- 735
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
frontend-v2-components- Source
- github.com/rommapp/romm