RomM v2: Universal Input & Universal Viewport
SkillDev toolsGuides your agent in adding keyboard, mouse, touch, and gamepad input plus responsive layout to the RomM v2 frontend.
Use RomM v2: Universal Input & Universal Viewport in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add RomM v2: Universal Input & Universal Viewport and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the RomM v2: Universal Input & Universal Viewport 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
Universal input (mouse, touch, keyboard, gamepad) and responsive/universal-viewport layout in the RomM v2 frontend. Use when adding interactive v2 components, focus management, spatial navigation, gamepad/keyboard handling, modality-gated focus rings, breakpoints, or responsive layout. Covers useInp
What this skill tells your AI
The instructions your AI receives, as published by rommapp/romm in .claude/skills/frontend-v2-input/SKILL.md and read by ahel’s review.
Premise: all v2 UI works with mouse, touch, keyboard, and gamepad; and every surface reads cleanly from a 320px phone to a 4K display. Both mechanisms are fixed; don't invent a parallel one.
The input system lives in src/v2/composables/useInput/ (bus, keyboard, gamepad, actions, scope). It generalises the original gamepad-only src/console/ system. There are no /console/* routes in v2.
Modality
useInputModality sets data-input="mouse|touch|key|pad" on <html> from the most recent input.
- Focus rings appear only with
keyandpad(CSS inglobal.css). Never use bare:focusin styles; use the modality-gated selectors, or focus rings flash on mouse click. - Modality gates appearance and behaviour, never size: focus rings, hover-reveal, autofocus. A tap is followed by compatibility mouse events, so anything sized off
data-inputresizes under the finger. Hit targets scale by breakpoint instead (below).
Coverage: every interactive primitive participates
Buttons, list items, tabs, menu items, focusable cards, toggleable chips: all participate in spatial navigation (not optional). A new interactive primitive must:
- be focusable (a natively focusable element, or a proper
tabindex); - react to logical actions (confirm/cancel) from
useInput, in addition to native click; - show a modality-gated focus state.
Storybook play() covering gamepad input is required only when applicable (the primitive is interactive enough that gamepad navigation matters).
Focus geometry
Each view declares its layout with focus primitives: RFocusZone, RFocusGrid, RFocusRow, RFocusColumn. Multiple regions = multiple zones. Predictable up/down/left/right movement is the view's responsibility.
Element-level global shortcuts
Above per-view geometry, some elements bind globally regardless of focus location:
UserMenuopens on Start.- Navbar tabs cycle with LB/RB (plus D-pad).
- Context menu opens on X or Y.
Scope (overlay stack)
When a dialog opens, push a scope; when it closes, pop. This stops Escape from closing two things at once and stops confirm leaking to controls beneath an overlay. RDialog and RMenu manage their scope automatically; custom overlays are an anti-pattern; go through the primitives.
Responsive layout (universal viewport)
- Single breakpoint source:
useBreakpoint(src/v2/composables/useBreakpoint/). Material thresholds:xs <600,sm 600–959,md 960–1279,lg 1280–1919,xl ≥1920.installBreakpointAttribute()mirrors the active set onto<html data-bp="…">(fromAppLayoutandAuthLayoutin the app, and from.storybook/preview.tsso stories match production CSS). - Layout switches live in CSS via the attribute selector:
html[data-bp~="xs"] .foo { … },html[data-bp~="sm-and-down"] .foo { … }. No raw@mediafor layout: the only allowed@mediaareprefers-reduced-motionand print. The attribute is on<html>so it reaches teleported overlays.romm/no-layout-media-query(ESLint) enforces this in SFC styles. - Conditional rendering (mount/unmount a different component per tier) uses the
useBreakpoint()refs in<script>, e.g.v-if="xs". Prefer mount-gating overdisplay:nonefor focusable chrome so hidden controls never sit in the tab/spatial-nav order. --r-row-padis the global horizontal gutter, already re-scoped responsive inglobal.css(36 → 20 → 14px). Consumevar(--r-row-pad); don't hard-code a smallerxspadding per component.- Touch targets scale with the breakpoint, never with
data-input:--r-touch-target(44px) atsm-and-down, less only where the row would not fit. - Overlays go full-bleed on
xs.RDialogrenders full-screen / bottom-sheet on phones (fullscreenOnMobile, default on);RMenubottom-sheets large menus. Never float a 600px dialog on a 360px screen. - Label→icon collapse (the AppNav precedent) is the canonical way to compress chrome; the four primary destinations relocate to
BottomNavonsm-and-down. - Grids size via
useResponsiveColumns(ResizeObserver), never a fixed column count.
Verification adds a breakpoint sweep; see review-polish. In Storybook, use the viewport toolbar presets in .storybook/rommViewports.ts (RomM xs/sm/md tiers, Steam Deck for lg, AYN Thor top/bottom screens). A story that needs a tier sets globals: { viewport: { value: "rommPhoneXs" } } instead of writing data-bp itself. Layout that depends on html[data-bp~="xs"] needs a canvas under 600px wide (e.g. 390×844 · RomM phone), not just useBreakpoint() switching subtabs at tablet widths.
Signals
- GitHub stars
- 13k
- Forks
- 747
- Last commit
- Oct 2026
Advanced
- Item type
- skill
- Key
frontend-v2-input- Source
- github.com/rommapp/romm