@rubyeverywhere/bridge
v0.4.0
Published
One API for browser, desktop, and mobile — the JS half of RubyEverywhere
Maintainers
Readme
@rubyeverywhere/bridge
One API for the browser, the RubyEverywhere desktop shell, and (soon) Hotwire Native mobile apps — the JavaScript half of RubyEverywhere.
App code writes to this surface only; platform differences live in adapters inside the package. Everything degrades gracefully: the same page works in a plain browser tab and gains native powers inside a RubyEverywhere app.
Install
npm install @rubyeverywhere/bridgeUsing RubyEverywhere? You don't need npm at all: the ruby_everywhere gem
ships this exact package. Rails apps get it served and importmap-pinned by the
gem's engine (updates arrive with bundle update ruby_everywhere); Sinatra and
Hanami apps get it vendored to public/bridge.js by every install.
Usage
import Everywhere from "@rubyeverywhere/bridge"
Everywhere.platform // "desktop" | "mobile" | "browser"
Everywhere.os // "macos" | "windows" | "linux" | "ios" | "android" | "chromeos" | "unknown"
Everywhere.native // true inside a RubyEverywhere app
Everywhere.version // app version; null in a plain browser tab
Everywhere.notify({ title, body }) // native notification / web Notification / console
Everywhere.confirm("Sure?") // Promise<boolean>, native dialog when possible
Everywhere.on("menu", handler) // shell events; returns an unsubscribe fn
Everywhere.visit("/settings") // Turbo.visit with location fallback
Everywhere.menu({ title, items }) // Promise<item | null> — native action
// sheet in the shell, a styled bottom
// sheet everywhere else. items:
// [{ title, id?, style?, disabled? }]
Everywhere.clipboard.write(text) // Promise<void>
Everywhere.clipboard.read() // Promise<string | null>
Everywhere.haptics.impact("medium") // real haptics in the mobile shell;
Everywhere.haptics.notification("success") // Vibration API in browsers, no-op elsewhere
Everywhere.haptics.selection()
Everywhere.badge.set(3) // app icon badge (mobile / installed PWAs)
Everywhere.badge.clear()
Everywhere.badge.setTab("/inbox", 3) // native tab bar badge (mobile)
Everywhere.badge.clearTab("/inbox")
Everywhere.permissions.query("camera") // Promise<{name, status}>
Everywhere.permissions.request("camera") // prompts when possible
Everywhere.permissions.openSettings() // for the denied dead-end (iOS asks once)
// status: "granted" | "denied" | "prompt" | "undeclared" | "unsupported"
// mobile permissions must be declared in everywhere.yml (camera/location
// declarations carry the usage string iOS shows) — undeclared never prompts
Everywhere.storage.get("key") // Promise<any | null> — device-local settings
Everywhere.storage.set("key", value) // Promise<boolean>; any JSON value round-trips
Everywhere.storage.remove("key") // Promise<boolean>
Everywhere.storage.clear() // Promise<boolean> — clears app keys only
// UserDefaults in the mobile shell (survives web view resets and cache
// clears, unlike localStorage); localStorage-backed in browsers and the
// desktop shell. Settings, not secrets — tokens go in biometrics.credential.Native chrome (nav bar buttons, menus, action sheets)
Tag ordinary markup, and inside the mobile shell the bridge lifts it into real
native controls — a nav-bar button, a pull-down / overflow menu, or an action
sheet. Tapping the native control just .click()s the element it mirrors, so a
link navigates, a submit submits, and behavior stays defined once in the DOM. In
a browser the same markup is the plain HTML it already is. No JavaScript, no
Stimulus — CSP-safe.
<!-- becomes a nav-bar button (right side); icons are per-platform like tabs -->
<a href="/notes/new" data-everywhere-nav-button
data-everywhere-nav-title="New"
data-everywhere-nav-icon-ios="plus"
data-everywhere-nav-icon-android="add">New</a>
<!-- a submit button in the nav bar -->
<button type="submit" data-everywhere-nav-button
data-everywhere-nav-title="Save" data-everywhere-nav-style="done">Save</button>
<!-- a nav-bar pull-down / overflow menu (defaults to the ⋯ icon) -->
<div data-everywhere-nav-menu data-everywhere-nav-title="More">
<a href="/share" data-everywhere-menu-item data-everywhere-menu-title="Share">Share</a>
<button form="delete_1" data-everywhere-menu-item
data-everywhere-menu-title="Delete"
data-everywhere-menu-style="destructive">Delete</button>
</div>
<!-- an in-content action sheet: a trigger and the items it opens -->
<div data-everywhere-menu>
<button data-everywhere-menu-trigger>Options</button>
<div data-everywhere-menu-items>
<a href="/x/edit" data-everywhere-menu-item>Edit</a>
</div>
</div>Rails ships helpers that emit exactly this — everywhere_nav_button,
everywhere_submit_button, everywhere_nav_menu / everywhere_menu_item,
everywhere_menu, and everywhere_fab (a floating action button). Include
everywhere/native.css for the FAB, the action-sheet fallback, and the
safe-area utilities.
Multi-instance apps
Apps whose users pick a server — one hosted platform, many instances — opt in
with remote.instances: true in everywhere.yml. The mobile shell then boots
into remote.url (your hosted picker page) until that page picks an instance:
Everywhere.instance.supported // true in a shell built with remote.instances
Everywhere.instance.current // active instance URL, or null (= built-in root)
Everywhere.instance.set("https://acme.example.com")
// validates, persists, and fully resets the app onto the instance — every
// later launch boots straight there. {to: "/path"} lands somewhere specific.
Everywhere.instance.clear() // back to the picker (and resets again)In a plain browser set() simply navigates to the URL — going there is what
picking an instance means on the web.
Declarative forms need no JS: elements with data-everywhere-haptic
("light", "impact:heavy", "notification:error", "selection") play on
tap, and <meta name="everywhere:badge"> / <meta name="everywhere:tab-badge">
tags (the everywhere_badge / everywhere_tab_badge Rails helpers) are
applied on every Turbo visit.
Auto-updates
Inside the desktop shell, apps with an update feed configured can check, install, and switch channels at runtime:
Everywhere.updates.supported // true when the shell has an update feed
Everywhere.updates.channel // effective channel ("stable", "beta", …)
const result = await Everywhere.updates.check()
// { available: true, version, notes, notesHtml } or { available: false }
Everywhere.updates.install() // download / verify / swap / relaunch
Everywhere.updates.setChannel("beta") // Promise<{ channel }>, persisted by the shell
Everywhere.updates.on("available", handler)
// events: "available" | "none" | "progress" | "ready" | "error" | "channel"notes is the release-notes markdown source; notesHtml is the same notes
pre-rendered to HTML from your signed update feed, ready for a changelog
modal: el.innerHTML = notesHtml.
In a plain browser tab the updates API is inert: check() resolves
{ available: false, unsupported: true } and nothing throws.
Platforms
- desktop — the Tauri-based RubyEverywhere shell. Notifications, dialogs, clipboard, shell events, and auto-updates are native.
- browser — honest web fallbacks: the Notification API,
window.confirm,navigator.clipboard, DOM CustomEvents. - mobile — Hotwire Native (iOS shell).
notify()delivers a native local notification via the shell'severywhere--notificationbridge component (web-bridge handshake is installed automatically unless the app already loads@hotwired/hotwire-native-bridge— first one wins, like the official package).confirm()/clipboard use the browser fallbacks, which are native in a WKWebView anyway. Everything degrades to browser behavior outside the shell.
License
MIT
