RomM Frontend v2: Component Constitution
SkillFiles & storageGuides your agent to build and edit components in the RomM v2 frontend following its conventions.
Use RomM Frontend v2: Component Constitution in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add RomM Frontend v2: Component Constitution and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the RomM Frontend v2: Component Constitution skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
About this skill
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. ESLint enforces the import side forsrc/v2/lib(no-restricted-importsfor packages,import-x/no-restricted-pathsfor app modules). Domain knowledge that is not an import (a hardcoded/assets/...path, domain-named props) still needs review. - 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 viaapp.provideand which falls back to English when unprovided. Never hard-code anaria-labelstring (including a bound literal like:aria-label="'Close'") or an English prop default inlib/. A label prop may still exist as a per-instance override, defaulting toundefinedso it resolves through the bundle.
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. - ESLint enforces
lang="ts",<script setup>, block order, and type-baseddefineProps/defineEmitsonsrc/v2/**/*.vue.
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 storyplay()functions viacomposeStories. Don't duplicate coverage between Vitest (pure logic) and Storybookplay()(components).- Responsive QA:
.storybook/rommViewports.ts+ viewport globals inpreview.ts; seefrontend-v2-inputfordata-bpvs iframe width.
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
- 747
- Last commit
- Oct 2026
Advanced
- Item type
- skill
- Key
frontend-v2-components- Source
- github.com/rommapp/romm