RomM Frontend v2: Component Constitution

SkillFiles & storage

Guides 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.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

RomM Frontend v2: Component ConstitutionStart free
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)

  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

TierPathPrefixStores/services/router/emitter/i18nStoryDomain knowledge
Primitivesrc/v2/lib/R* mandatoryNoMandatoryNone
Shared compositesrc/v2/components/shared/no prefixYesOptionalCross-feature, no specific domain
Feature compositesrc/v2/components/<feature>/no prefixYesOptionalFeature-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. ESLint enforces the import side for src/v2/lib (no-restricted-imports for packages, import-x/no-restricted-paths for 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 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.
  • ESLint enforces lang="ts", <script setup>, block order, and type-based defineProps/defineEmits on src/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 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 story play() functions via composeStories. Don't duplicate coverage between Vitest (pure logic) and Storybook play() (components).
  • Responsive QA: .storybook/rommViewports.ts + viewport globals in preview.ts; see frontend-v2-input for data-bp vs iframe width.

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.

Signals

GitHub stars
13k
Forks
747
Last commit
Oct 2026
Advanced
Item type
skill
Key
frontend-v2-components
Source
github.com/rommapp/romm