Agent skill

Tv Remote UI

by ventic in ventic/ventic

How this app stays usable from a TV remote (Android TV / Google TV).

MITAuto-check passedFrontend & Design

Install Tv Remote UI

skills CLI
$ npx skills add ventic/ventic --skill tv-remote-ui -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install ventic/ventic tv-remote-ui --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/ventic/ventic.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/tv-remote-ui .claude/skills/tv-remote-ui && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
tv-remote-ui
GitHub stars
174
Token cost
~4.2k tokens
SKILL.md length
2,582 words
Files
1
Skills in repo
1
Repo updated
First seen
Licence
MIT

At a glance

How this app stays usable from a TV remote (Android TV / Google TV).

  • Works in 7 steps: Everything actionable is an or a . Not a… → Hover state must also be focus state.… → Decorative or duplicate controls get… → …
  • Keyboard navigation
  • SKILL.md covers How it already works, Rules for new UI, Android TV specifics and What has deliberately been…
  • Calls bun

What it does

Tv Remote UI is an agent skill from ventic/ventic. How this app stays usable from a TV remote (Android TV / Google TV). Read before adding or changing any interactive UI — a new page, dialog, control bar, list, card, or overlay — and before touching plugins/dpad.client.ts, utils/dpad.ts, or the Android manifest/activity. Triggers on "remote", "d-pad", "dpad", "Android TV", "smart TV", "leanback", "focus", "keyboard navigation", "arrow keys", "back button".

Its SKILL.md is about 4.2k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Frontend & Design, covering Accessibility. It works with Android, Tauri, Nuxt and Vue.js. The repository describes itself as: A media library and BitTorrent player for desktop and Android - phone, tablet and TV. Streams torrents into a real embedded mpv window. Built with Nuxt and Tauri. Ships with no… The licence is MIT.

When your agent uses it

  • Keyboard navigation
  • Tasks that involve Accessibility

Example prompts

  • “remote”
  • “Android TV”
  • “smart TV”
  • “/tv-remote-ui”

Workflow steps

7 steps, taken from the first numbered list in SKILL.md.

  1. Everything actionable is an or a . Not a div with
  2. Hover state must also be focus state. @mouseenter needs a matching
  3. Decorative or duplicate controls get tabindex="-1". Buttons layered on
  4. One obvious first stop. The layout marks the page region with
  5. Don't rely on typing, and don't focus a text field on the way past one.
  6. Test at 10 feet. Body text no smaller than text-body-medium, targets no
  7. Keep dialogs modal. Vuetify overlays trap the d-pad automatically

What it can do on your machine

Read from SKILL.md and the folder at commit 393d796. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • bun

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Tv Remote UI loads about 4.2k tokens when it runs. Until then it costs about 106 tokens; SKILL.md has 2,582 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~106
When it runs · the whole SKILL.md, loaded when a task matches
~4.2k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from ventic/ventic at commit 393d796, republished under its MIT licence (© ventic). 2,582 words, ~4,177 tokens.

Download SKILL.mdSave it as .claude/skills/tv-remote-ui/SKILL.md (or your agent's skills folder).
name
tv-remote-ui
description
How this app stays usable from a TV remote (Android TV / Google TV). Read before adding or changing any interactive UI — a new page, dialog, control bar, list, card, or overlay — and before touching plugins/dpad.client.ts, utils/dpad.ts, or the Android manifest/activity. Triggers on "remote", "d-pad", "dpad", "Android TV", "smart TV", "leanback", "focus", "keyboard navigation", "arrow keys", "back button".

Remote-friendly UI

Ventic runs on desktop and on Android TV, where the only input is a d-pad: up, down, left, right, OK, back. No pointer, no hover, no text entry worth having. Anything you add has to be reachable and legible with those six keys.

How it already works

app/plugins/dpad.client.ts is the whole mechanism, and it is generic — new pages get remote support for free as long as they follow the rules below.

  • Arrows move focus to the nearest focusable element in that direction, scored by pickDirection() in app/utils/dpad.ts. Sideways drift is measured between the boxes' edges, not their centres — boxes that face each other at all count as level, or a 44px nav link never reaches the 361px poster beside it. Anything level like that beats everything that isn't, however much closer: a run along the toolbar has to reach Downloads 264px away rather than fall into the grid 74px below. Drift costs double only among equals, which is what keeps a grid walking straight down its column.
  • A rect is the document's, not the screen's, so every candidate is put through clipped() first: each edge pulled inside every scroller it sits in, collapsing onto the edge it went past rather than being dropped. Without it a poster three rows above the fold still measured level with the toolbar — right off the search box landed on it instead of on Settings, and up out of the drawer landed on one 350px above the screen, after which every further press was measured from somewhere nobody could see. Collapsing and not dropping is what keeps a horizontal row walking: its next card is off the right edge by design, and sat on that edge it is still the nearest thing that way. It is handed a real DOMRect, whose edges are prototype getters — destructure it, never spread it, or every edge is NaN and the page answers no arrow at all.
  • A press never leaves a scroll region that still has content that way; it scrolls the region instead (scroller(), which nudge shares). A title page's synopsis is prose, so nothing above the Play row is a target and up escaped straight to the toolbar — leaving the title, poster and overview unreachable for the rest of the visit, there being nothing to move to that would bring them back.
  • The scroll is animated, and both halves of that are ours. heading holds where each scroller is going and step() eases it there on a frame loop, because the platform's animation cannot be retargeted: scrollTo with a smooth behaviour cancels whatever is running and eases in from a standstill, so a press landing mid-scroll stopped the page dead and started again — measured on the set at eight presses 120ms apart, the page crawled at 5px a frame for three seconds and then covered 2000px in half of one, once the presses stopped. Easing toward a moving target has no standstill in it, and costs nothing extra: the same per-frame durations as the platform's, at 60fps. The loop stops when it arrives, and lets go the moment a wheel or a finger moves the scroller instead.
  • Every decision about that scroll is taken against the page at rest. A rect read mid-animation describes a page on its way somewhere, and scrollIntoView is no better — its nearest is measured from where the scroll has got to, so it asks for the whole remaining distance plus a row and each press of a burst aims further than the last. show() works the destination out itself from the resting geometry; move() measures candidates the same way, because mid-flight every row below the fold clips onto the same edge and only document order tells them apart, which walked the grid backwards; and focusFirst() likewise, or it lands on a row already on its way off the screen. A heading is clamped to where the scroller can stop, and re-clamped every frame — an unreachable one is answered with for ever, and every later press is then judged against a page that does not exist. nudge steps on from where the scroller is heading, and keeps focus unless the element it was on really did scroll out of view.
  • The handler sits on document in the bubble phase and bails on e.defaultPrevented, so any component that already handles arrows keeps them: Vuetify sliders, lists, selects, and the player's seek keys.
  • A component may own an axis but never the one that leads out of it, or the d-pad has no exit but Back. A second handler in the capture phase takes the key back for the ones that would: up/down off a [role="slider"] (whose value is left/right, and which otherwise changes as you try to leave), up/down off the first/last item of a .v-list (which otherwise wraps, so the drawer cycles for ever instead of letting go), and left/right off the ends of a .v-slide-group — chip groups and tabs wrap the same way, which is why the category chips circled instead of reaching the genre filter beside them.
  • Only tabindex="-1" opts out. Vuetify's lists rove the tabindex across their items and park -2 on the ones that aren't current — every drawer link carries it, and they are all real targets. A wrapper that merely contains other targets (the .v-list div is focusable itself) is not one.
  • Nothing that way? The page scrolls by 80% of a screen instead (nudge()), then focus is dropped so the next press lands on the freshly revealed row. This is what makes lazily-mounted content (v-lazy rows) reachable at all.
  • OK is plain Enter — the browser clicks a focused <a>/<button> itself. Vuetify's list items and chips handle Enter/Space too.
  • A held OK is window.__tvHold(), which fires a contextmenu on whatever has focus — the same event a right-click and a finger held on a card produce, so a card answers all three with one handler (MediaMenu). The press itself is MainActivity's to hold back: the WebView clicks a focused link on the key's way down (measured on the set: click 8ms after keydown, 600ms before keyup), so an OK the WebView had seen would already have opened the card. The first DOWN is kept, a repeat of it (about half a second in) is the hold, and a release with no repeat replays the kept DOWN — the click lands on release, as a tap's does. A hold nothing claims is swallowed, not clicked.
  • Back is back() in the plugin: close the top dialog, else let the page claim Escape (the player's menus, then leaving playback), else router.back(), else return false — at which point the app goes behind whatever is next (backgroundApp(), the bridge's leave()). On Android it arrives through tauri's back-button plugin event, which the plugin registers a listener for at boot, and that registration is the whole fix for "BACK left the film instead of closing the subtitle panel": tauri's own AppPlugin sits above MainActivity on the back dispatcher and, with no listener, pops the WebView's history itself whenever there is any — so the page was only ever asked at the root. MainActivity's backToPage (which calls window.__tvBack()) is now the fallback for a press that lands before the page has loaded. Three things still have to be true for any of that to run. handleBackNavigation must be false — WryActivity.setWebView otherwise adds a callback of its own that does nothing but webView.goBack(), and it wins, because the dispatcher runs the last callback added and ours goes on in onCreate while wry's goes on when the webview is created. That is a silent failure with a plausible face: popping a history entry looks exactly like a page-level back until something is open in front of it, and at the root canGoBack() is false, so the one case that ever reached __tvBack was the one that backgrounds the app. And the override has to sit on OnBackPressedDispatcher, not on onKeyDown. An app targeting API 35+ gets predictive back, where BACK arrives through OnBackInvokedDispatcher and onKeyDown is never called at all; the manifest declares enableOnBackInvokedCallback so 33 and 34 take that path too, and the dispatcher is what both mechanisms feed. Catching the keycode instead is what made Android 15 phones close the app out of dialogs and out of a film.
  • And the WebView answers BACK on its own account, before any of that. It pops its own history whenever canGoBack() is true, and it sits below the activity in the view hierarchy — super.dispatchKeyEvent walks the views before onBackPressedDispatcher is ever consulted. So turning wry's callback off was only half the job: the key was still spent on a history pop, and __tvBack ran only at the root, where there is no history to pop. Measured on the box: BACK on a film with the subtitle panel open left the film and never closed the panel, and no dialog anywhere could be closed with it. MainActivity therefore takes KEYCODE_BACK in dispatchKeyEvent and hands it to onBackPressedDispatcher itself. Predictive back never calls dispatchKeyEvent, so that is the older path being routed to the same callback, not a second rule.
  • Back at the root backgrounds the app, it does not finish it. Finishing the activity leaves the process alive, and wry starts the Rust side exactly once per process (a ProcessLifecycleOwner observer that ignores being added twice), so the next launch built a fresh activity onto an event loop whose webview was already gone — and aborted. moveTaskToBack(true) avoids all of it; onDestroy kills the process when the activity is genuinely finishing, so a real close is always followed by a cold start. bun run check:dpad holds both ends of this.
  • Focus is visible whenever <html> carries .dpad, added on the first arrow key and removed on the first mouse move. The ring lives in the vuetify-final layer in assets/css/layers.css.

Run bun run check:dpad after touching the geometry.

Show full SKILL.md (945 more words)Show less

Rules for new UI

  1. Everything actionable is an <a href> or a <button>. Not a div with a click handler — that is unreachable by remote. A custom control that must be a div needs tabindex="0", a role, and its own Enter/Space handling.
  2. Hover state must also be focus state. @mouseenter needs a matching @focus, hover: needs focus-visible: or group-focus-within:. A remote never hovers, so hover-only affordances are invisible on a TV.
  3. Decorative or duplicate controls get tabindex="-1". Buttons layered on top of a card (favourite, watched, ⋮) would otherwise swallow a d-pad moving across the grid. The card is the target; the extras are for pointers.
  4. One obvious first stop. The layout marks the page region with data-dpad-start; focus lands on the first thing inside it after a navigation. Put the primary action (Play, the first card) early in the DOM.
  5. Don't rely on typing, and don't focus a text field on the way past one. Every screen has to be reachable by moving and pressing OK. In a text field left/right belong to the caret and up/down leave it — but on a TV a field that merely has focus puts the on-screen keyboard over the whole screen, and a remote crosses the app bar's search box to reach Downloads and Settings. So a field a d-pad can pass sits inert under a transparent <button> — TvField, which every <v-text-field> in the app sits inside (check:dpad holds that), and OK on that button is what focuses it. It has to be a button: the WebView drops OK for an <input> it thinks can't be edited, and Android only raises the keyboard for a focus that happens inside a real press — nextTick after the click still counts, a timer seconds later does not. router.afterEach leaves focus alone while a caret is in play, or the debounced search navigation snatches it away mid-word.
  6. Test at 10 feet. Body text no smaller than text-body-medium, targets no smaller than ~40px, and never signal state with colour alone at that distance.
  7. Keep dialogs modal. Vuetify overlays trap the d-pad automatically (.v-overlay--active, tooltips excepted). A hand-rolled overlay has to either be one, or accept that the d-pad will walk out of it.

Android TV specifics

  • gen/android/app/src/main/AndroidManifest.xml declares android.software.leanback and android.hardware.touchscreen as required="false" (one APK for phones and TVs), the LEANBACK_LAUNCHER category, and android:banner — a TV launcher shows a blank tile without one (res/drawable-xhdpi/tv_banner.png, 320×180).
  • gen/android is committed, so edits there survive; regenerating the project will clobber them, so re-apply the BACK callback and the handleBackNavigation = false beside it, the OK forward and hold (dispatchKeyEvent → window.__tvOk, window.__tvHold), the VenticScreen JS interface (fullscreen, orientation, metered network, tv()), the wide viewport settings, mediaPlaybackRequiresUserGesture = false, Downloads.kt with its onResume/onPause/onDestroy hooks, the process kill in onDestroy, the windowBackground in res/values*/themes.xml, and the manifest lines — the leanback ones, enableOnBackInvokedCallback, the service and its permissions — if that ever happens. bun run check:android-downloads fails loudly when the download half is gone, and check:dpad when the BACK half is.
  • A TV is 960dp wide, which is a small laptop as far as any breakpoint is concerned: below every lg: rule the desktop layout is built on, and at twice the size anything wants to be across a room. plugins/tv.client.ts asks for a 1280 viewport when isTv() (UiModeManager, through the bridge — a TV's user agent is a phone's), which needs useWideViewPort and loadWithOverviewMode in MainActivity: the first lets the page ask for more width than the screen has, the second scales it down to fit. With only the first, the right quarter of the layout is off the side of the screen. It also puts .tv on <html> for anything that follows from being a TV rather than from a width.
  • OK is not a key the page always sees. The WebView turns DPAD_CENTER into a click on a link or a button, and drops it for the readonly <input> behind a Vuetify select — and it claims the key before the activity's onKeyDown runs, so the forward has to sit in dispatchKeyEvent. window.__tvOk opens what it can and answers false for everything else, and the key is passed on either way. Vuetify opens a select on mousedown, not on click().
  • Volume is the TV's own remote, not the app's — and the phone's own buttons. The player hides its volume slider on any coarse pointer for that reason.
  • Playback there is the webview's <video>, not mpv (app/utils/htmlvideo.ts), so the player's bars stack in CSS rather than being punched out of a native window. A control added to MpvPlayer.vue works on both without extra work; anything reaching for player_ipc directly does not.
  • A set answers hover: none and means it, but the webview still fires a pointerenter at whatever appears under its idea of where a pointer last was — the middle of the screen. One phantom enter with no leave behind it pinned the player's chrome open for a whole film, so anything that lets hover hold UI up has to check it can hover at all (hoverable in MpvPlayer.vue).
  • Play/pause sits in the bottom bar on a TV (barTransport), not in the middle of the picture where a thumb would want it: a remote arrives at the bar anyway, and the centre cluster is up the longest of anything on screen.

What has deliberately been left out

  • No overscan padding. Modern Android TV doesn't overscan. Add a safe-area inset only if a real set clips the edges.
  • No focus-scale animation on cards. The ring plus the existing hover overlay is enough; per-card transforms cost frames on TV silicon.
  • No spatial-navigation dependency. pickDirection is ~30 lines and the polyfills are far larger than what a poster grid needs.

© ventic, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .claude/skills/tv-remote-ui of ventic/ventic.

Open the folder on GitHubat commit 393d796

Compare with similar skills

Tv Remote UI next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Tv Remote UI compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Tv Remote UI this skillventic/ventic174—~4.2kAutomated safety check: PassMIT
Tauri Config Ipcifer47/markeron1.2k—~1kAutomated safety check: PassMIT
Rgsm Gui Debugmcthesw/game-save-manager1.1k—~267Automated safety check: PassAGPL-3.0
Create Docsvictorgarciaesgi/nuxt-typed-router4132 repos~2.8kAutomated safety check: PassMIT
Cross Platform Tauri UIifer47/markeron1.2k—~2kAutomated safety check: PassMIT
Worklog Designregisx001/Worklog258—~3.3kAutomated safety check: PassMIT

Similar skills

  • Tauri Config Ipc

    ifer47/markeron

    Extend MarkerOn settings, persisted config, or Tauri IPC commands.

    1.2k GitHub stars~1k tokensUpdated 8 days ago
    Frontend & DesignAuto-check passed
  • Rgsm Gui Debug

    mcthesw/game-save-manager

    Debug the RGSM frontend against the real Rust backend in a normal browser, without Tauri IPC or WebView tooling.

    1.1k GitHub stars~267 tokensUpdated yesterday
    Frontend & DesignAuto-check passed
  • Create Docs

    victorgarciaesgi/nuxt-typed-router

    Create complete documentation sites for projects. An agent skill from victorgarciaesgi/nuxt-typed-router.

    413 GitHub starsUsed in 2 repos~2.8k tokens
    Frontend & DesignAuto-check passed
  • Cross-platform UI styling for MarkerOn Tauri app (macOS WKWebView vs Windows WebView2).

    1.2k GitHub stars~2k tokensUpdated 8 days ago
    Frontend & DesignAuto-check passed
  • Worklog Design

    regisx001/Worklog

    Design and UI skill for the Worklog desktop project manager.

    258 GitHub stars~3.3k tokensUpdated 1 mo ago
    Frontend & DesignAuto-check passed
  • Web Design

    drewnekota/cetus

    A skill your agent uses when building any web page, HTML artifact, landing page, dashboard, slide deck, report, email, or UI component the user will look at.

    146 GitHub stars~2.1k tokensUpdated 2 days ago
    Frontend & DesignAuto-check passed

Questions about Tv Remote UI

What does Tv Remote UI do?

How this app stays usable from a TV remote (Android TV / Google TV). Tv Remote UI is an agent skill from ventic/ventic. How this app stays usable from a TV remote (Android TV / Google TV).

When should I use Tv Remote UI?

Tv Remote UI fits situations like: keyboard navigation; tasks that involve Accessibility.

How do I install Tv Remote UI in Claude Code?

Run `npx skills add ventic/ventic --skill tv-remote-ui -a claude-code`. Or copy the skill folder (.claude/skills/tv-remote-ui in ventic/ventic) into .claude/skills/tv-remote-ui in your project. Claude Code loads it when a task matches its description.

How do I install Tv Remote UI in Codex?

Run `npx skills add ventic/ventic --skill tv-remote-ui -a codex`. Or copy the skill folder (.claude/skills/tv-remote-ui in ventic/ventic) into .agents/skills/tv-remote-ui in your project. Codex loads it when a task matches its description.

Can I use Tv Remote UI in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add ventic/ventic --skill tv-remote-ui -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/tv-remote-ui, .gemini/skills/tv-remote-ui, .github/skills/tv-remote-ui and .opencode/skills/tv-remote-ui in your project.

What does Tv Remote UI need to run?

Going by SKILL.md and its folder, Tv Remote UI needs the command-line tools its instructions call (bun).

Does Tv Remote UI access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Tv Remote UI safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Tv Remote UI use?

Tv Remote UI is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Tv Remote UI use?

About 4.2k tokens (SKILL.md is roughly 17k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Tv Remote UI?

Skills that share tags, products or a category with Tv Remote UI: Tauri Config Ipc (ifer47/markeron, 1.2k stars), Rgsm Gui Debug (mcthesw/game-save-manager, 1.1k stars), Create Docs (victorgarciaesgi/nuxt-typed-router, 413 stars) and Cross Platform Tauri UI (ifer47/markeron, 1.2k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Tv Remote UI?

ventic (a GitHub organization) maintains it in ventic/ventic, which has 174 GitHub stars. The repository was last updated on October 4, 2026.

Source: ventic/ventic on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.