agentsclimarketplace

Hotwire native bridge

Skill davidteren/hotwire-codex-skills/skills/hotwire-native-bridge

Create and validate Strada / Hotwire Native bridge components across web (Stimulus), iOS (Swift), and Android (Kotlin). Use when adding native UI driven by the web — a native menu, share button, toolbar, native form submit — to a Hotwire Native app, or when a bridge component "works on web but not in the app", a native control is missing/duplicated, or a value sent from the web never arrives natively. Generates the three platform halves from one component name and lints the cross-platform contract for name mismatches and silently-dropped payload fields.From its SKILL.md

Install
npx -y skills add davidteren/hotwire-codex-skills --skill hotwire-native-bridge

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 2 commands, including `scripts/new_bridge_component.sh <name-in-kebab> \ --web path/to/piazza-web \ --ios path/to/piazza-ios \ --android path/to/piazza-android \ --package com.yourco.app` and 1 more.

SKILL.md

5.1 KB, ~1.1k tokens by cl100k_base, as published. Nobody here has run it

Hotwire Native bridge components (Strada)

A Strada bridge component has three halves that must agree: a web Stimulus controller (BridgeComponent), an iOS BridgeComponent (Swift), and an Android BridgeComponent (Kotlin). They are bound by an implicit contract:

  1. A component name string (e.g. "nav-menu") — identical on all three sides.
  2. Message eventsconnect / disconnect plus any custom events.
  3. A JSON payload shape — every key the web sends must be decoded on both native sides, or it is silently dropped (no error).

Getting any of these subtly wrong is the #1 time-sink: a name typo makes the component invisible; a payload key missing from one native struct vanishes with no crash. This skill generates the three halves consistently and lints the contract.

See references/bridge-contract.md for the full contract + the real Piazza nav-menu example, and the gotcha catalogue.

When to use

  • Adding a native component backed by web markup (menu, toolbar, share sheet, native submit button, native picker).
  • Debugging "works on web, broken/missing in the native app", a duplicated native control after navigation, or a value that doesn't reach the native side.
  • Reviewing a PR that touches any *Component.swift / *Component.kt / controllers/bridge/*.js — run the linter.

Generate a new component

scripts/new_bridge_component.sh <name-in-kebab> \
  --web   path/to/piazza-web \
  --ios   path/to/piazza-ios \
  --android path/to/piazza-android \
  --package com.yourco.app

Writes (refuses to overwrite):

  • web/app/javascript/controllers/bridge/<snake>_controller.js
  • ios/<App>/Bridge/<Pascal>Component.swift
  • android/.../<Pascal>Component.kt

Then register it (the generator prints these — it does NOT edit registries). Hotwire Native 1.x (default — templates target this):

  • iOS: Hotwire.registerBridgeComponents([<Pascal>Component.self]) in AppDelegate, before any Navigator is created (make the navigator lazy, else the component never attaches — hotwire-native-ios #35). import HotwireNative.
  • Android: Hotwire.registerBridgeComponents(BridgeComponentFactory("<name>", ::<Pascal>Component)) in your Application subclass (also set Hotwire.config.jsonConverter = KotlinXJsonConverter()). Imports from dev.hotwire.core.bridge; destination type HotwireDestination.
  • Web: install @hotwired/hotwire-native-bridge; Stimulus auto-registers the controller as bridge--<name> if it lives under app/javascript/controllers/bridge/.

Strada-beta apps (the Piazza example repos still use this): iOS adds <Pascal>Component.self to a BridgeComponent.allTypes extension; Android adds the factory to a bridgeComponentFactories list passed to the WebFragment; web imports @hotwired/strada. Same contract, older wiring. Full mapping: references/bridge-contract.md.

  • Markup: data-controller="bridge--<name>", item targets data-bridge--<name>-target="item", payload attrs via data-bridge-<attr> (read in JS with bridgeElement.bridgeAttribute("<attr>")).

Fill in the TODOs in each native half (render UI from data, reply on selection).

Lint the contract

scripts/lint_bridge_contract.sh --root <dir-with-the-three-repos>
# or: --web W --ios I --android A

Checks, and exits non-zero on drift:

  • Name parity — every component name present on web, iOS, and Android (catches typos / unregistered = invisible components).
  • iOS registration — a component declared but absent from allTypes.
  • Payload field parity — a field decoded on one native side but not the other (the silent-drop class — e.g. Piazza's icon, sent by web + decoded on iOS, has no field on Android).

Heuristic text scan (grep/awk), not a parser — conservative, good enough to gate a PR. It is the check that pays for itself: payload drift produces no runtime error.

The rules that matter (least astonishment)

  • The component name is an API across three repos — renaming means three edits.
  • Add a payload key → add it to the Swift Decodable and the Kotlin @Serializable in the same change, or it silently disappears on the side you forgot.
  • disconnect() must undo what connect() rendered, or native controls duplicate on the next Turbo navigation.
  • Components degrade to plain web when no native bridge is present — keep the web markup usable on its own (the JS hides it only when the bridge connects).

What ships with it: 6 files

22.8 KB alongside SKILL.md, 2 of them executable

references/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.