ruby_everywhere 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0bb96376a66f4202388542212bc1d03b2dea1bb6519ccc4a8339cc1b3cda3e43
4
- data.tar.gz: be5f8b69e0157a93dfc1837445faa8f4e6326678741b88ddd76c9bff0521ddf5
3
+ metadata.gz: b255975aa8724cb4980606ddbde415eea3a238c193ffce8530028ef5fdc63c24
4
+ data.tar.gz: 386145d42d750984da4fa800ea9b63417371937dbf67d49642a229cd92cfbb71
5
5
  SHA512:
6
- metadata.gz: 29c23a79521c57f81a0881e663debc4e2c9a505a424dbe8b615c1a1c5eccba3d93c1dfd7ee0281ea04c8121d2c4ba6ec4c0de8276453ab5ca0d348d6d8afdf00
7
- data.tar.gz: 7a626876bb0820701922e23990541e149360b35246b2456be3b2d5f7729743f2c5e3772d6e9e2d94d7ea898b85d6e528ff5e9ad85e919653357463073cfbdcea
6
+ metadata.gz: 6af24db6e60c693ae855fa47832564074d887c18248a63be23c343c5e85c7d84cf4210017ba6bb26dae9950bb6b1394255f02cbea4d278f9407952a22f37dd6c
7
+ data.tar.gz: 368f12504a2a5748aa30eaf641eadf5b85bbd54b99e0f372015b9621510f01dc5779ca6e3e37cfab594083768815e40e48200e2c3649195c5b5e800cc7cf5557
data/bridge/README.md CHANGED
@@ -33,6 +33,11 @@ Everywhere.confirm("Sure?") // Promise<boolean>, native dialog when pos
33
33
  Everywhere.on("menu", handler) // shell events; returns an unsubscribe fn
34
34
  Everywhere.visit("/settings") // Turbo.visit with location fallback
35
35
 
36
+ Everywhere.menu({ title, items }) // Promise<item | null> — native action
37
+ // sheet in the shell, a styled bottom
38
+ // sheet everywhere else. items:
39
+ // [{ title, id?, style?, disabled? }]
40
+
36
41
  Everywhere.clipboard.write(text) // Promise<void>
37
42
  Everywhere.clipboard.read() // Promise<string | null>
38
43
 
@@ -61,6 +66,49 @@ Everywhere.storage.clear() // Promise<boolean> — clears app keys onl
61
66
  // desktop shell. Settings, not secrets — tokens go in biometrics.credential.
62
67
  ```
63
68
 
69
+ ### Native chrome (nav bar buttons, menus, action sheets)
70
+
71
+ Tag ordinary markup, and inside the mobile shell the bridge lifts it into real
72
+ native controls — a nav-bar button, a pull-down / overflow menu, or an action
73
+ sheet. Tapping the native control just `.click()`s the element it mirrors, so a
74
+ link navigates, a submit submits, and behavior stays defined once in the DOM. In
75
+ a browser the same markup is the plain HTML it already is. No JavaScript, no
76
+ Stimulus — CSP-safe.
77
+
78
+ ```html
79
+ <!-- becomes a nav-bar button (right side); icons are per-platform like tabs -->
80
+ <a href="/notes/new" data-everywhere-nav-button
81
+ data-everywhere-nav-title="New"
82
+ data-everywhere-nav-icon-ios="plus"
83
+ data-everywhere-nav-icon-android="add">New</a>
84
+
85
+ <!-- a submit button in the nav bar -->
86
+ <button type="submit" data-everywhere-nav-button
87
+ data-everywhere-nav-title="Save" data-everywhere-nav-style="done">Save</button>
88
+
89
+ <!-- a nav-bar pull-down / overflow menu (defaults to the ⋯ icon) -->
90
+ <div data-everywhere-nav-menu data-everywhere-nav-title="More">
91
+ <a href="/share" data-everywhere-menu-item data-everywhere-menu-title="Share">Share</a>
92
+ <button form="delete_1" data-everywhere-menu-item
93
+ data-everywhere-menu-title="Delete"
94
+ data-everywhere-menu-style="destructive">Delete</button>
95
+ </div>
96
+
97
+ <!-- an in-content action sheet: a trigger and the items it opens -->
98
+ <div data-everywhere-menu>
99
+ <button data-everywhere-menu-trigger>Options</button>
100
+ <div data-everywhere-menu-items>
101
+ <a href="/x/edit" data-everywhere-menu-item>Edit</a>
102
+ </div>
103
+ </div>
104
+ ```
105
+
106
+ Rails ships helpers that emit exactly this — `everywhere_nav_button`,
107
+ `everywhere_submit_button`, `everywhere_nav_menu` / `everywhere_menu_item`,
108
+ `everywhere_menu`, and `everywhere_fab` (a floating action button). Include
109
+ `everywhere/native.css` for the FAB, the action-sheet fallback, and the
110
+ safe-area utilities.
111
+
64
112
  ### Multi-instance apps
65
113
 
66
114
  Apps whose users pick a server — one hosted platform, many instances — opt in
@@ -103,6 +103,50 @@
103
103
  // notes is the markdown source; notesHtml is the same notes pre-rendered to
104
104
  // HTML (from the signed update feed — your own content), ready for a
105
105
  // changelog modal: el.innerHTML = notesHtml.
106
+ //
107
+ // Everywhere.menu({ title, message, items }) // Promise<item|null>
108
+ // items: [{ title, id?, style?, disabled? }] style: "destructive" | "cancel"
109
+ // Native action sheet in the mobile shell; a styled bottom sheet everywhere
110
+ // else. Resolves the chosen item (null on cancel/dismiss).
111
+ //
112
+ // Native chrome, declarative (no JS) — the bridge watches every page and,
113
+ // inside the mobile shell, lifts these into real native controls; in a
114
+ // browser (and the desktop shell) they stay as the plain HTML they already
115
+ // are, so the same markup works everywhere. Tapping a native control just
116
+ // .click()s the element it mirrors — a link navigates, a submit submits, a
117
+ // button fires its handler — so behavior is defined once, in the DOM.
118
+ //
119
+ // Nav bar button (right side by default):
120
+ // <a href="/notes/new" data-everywhere-nav-button
121
+ // data-everywhere-nav-title="New" data-everywhere-nav-icon="plus">New</a>
122
+ // Nav bar submit button (submits the form when tapped):
123
+ // <button type="submit" data-everywhere-nav-button
124
+ // data-everywhere-nav-title="Save" data-everywhere-nav-style="done">Save</button>
125
+ // Nav bar pull-down / overflow menu (defaults to the ⋯ ellipsis icon):
126
+ // <div data-everywhere-nav-menu data-everywhere-nav-title="More">
127
+ // <a href="/share" data-everywhere-menu-item data-everywhere-menu-title="Share"
128
+ // data-everywhere-menu-icon-ios="square.and.arrow.up"
129
+ // data-everywhere-menu-icon-android="share">Share</a>
130
+ // <button form="delete_1" data-everywhere-menu-item data-everywhere-menu-title="Delete"
131
+ // data-everywhere-menu-style="destructive">Delete</button>
132
+ // </div>
133
+ // In-content action sheet (a trigger + the items it opens):
134
+ // <div data-everywhere-menu>
135
+ // <button data-everywhere-menu-trigger>Options</button>
136
+ // <div data-everywhere-menu-items>
137
+ // <a href="/x/edit" data-everywhere-menu-item>Edit</a>
138
+ // <button form="del" data-everywhere-menu-item
139
+ // data-everywhere-menu-style="destructive">Delete</button>
140
+ // </div>
141
+ // </div>
142
+ // Icons are per-platform, like tabs: -icon-ios is an SF Symbol, -icon-android
143
+ // a Material name, and a plain -icon is the shared fallback; the bridge picks
144
+ // by the running os, so one page serves both shells. Icons are ignored on the
145
+ // web. side is "left" or "right" (default). Nav-bar sources are hidden in the
146
+ // shell (they live in the bar now) but shown in a browser; the action-sheet
147
+ // trigger toggles its items as an inline menu when there's no native shell.
148
+ // These are the everywhere_nav_button / everywhere_nav_menu /
149
+ // everywhere_submit_button / everywhere_menu Rails helpers.
106
150
 
107
151
  const config =
108
152
  (typeof window !== "undefined" && window.__EVERYWHERE_CONFIG__) || {}
@@ -395,6 +439,7 @@ const HAPTICS_COMPONENT = "everywhere--haptics"
395
439
  const PERMISSIONS_COMPONENT = "everywhere--permissions"
396
440
  const BIOMETRICS_COMPONENT = "everywhere--biometrics"
397
441
  const STORAGE_COMPONENT = "everywhere--storage"
442
+ const MENU_COMPONENT = "everywhere--menu"
398
443
 
399
444
  function installWebBridge() {
400
445
  if (window.HotwireNative) return
@@ -633,6 +678,27 @@ const mobile = {
633
678
  }
634
679
  })
635
680
  })
681
+ },
682
+
683
+ // Native chrome (nav bar items + action sheets) via the shell's
684
+ // "everywhere--menu" component. Unlike the other components these replies are
685
+ // not one-shot: native re-replies to the SAME message on every tap, so the
686
+ // caller owns the callback's lifetime (menuRemove) rather than clearing it on
687
+ // first reply.
688
+ menuSupported() {
689
+ const web = window.HotwireNative && window.HotwireNative.web
690
+ return !!(web && web.supportsComponent(MENU_COMPONENT))
691
+ },
692
+
693
+ menuSend(event, data, callback) {
694
+ const web = window.HotwireNative && window.HotwireNative.web
695
+ if (!web || !web.supportsComponent(MENU_COMPONENT)) return null
696
+ return web.send({ component: MENU_COMPONENT, event, data, callback })
697
+ },
698
+
699
+ menuRemove(id) {
700
+ const web = window.HotwireNative && window.HotwireNative.web
701
+ if (web && id !== null && id !== undefined && web.removeCallbackFor) web.removeCallbackFor(id)
636
702
  }
637
703
  }
638
704
 
@@ -689,6 +755,26 @@ export const Everywhere = {
689
755
  else window.location.assign(path)
690
756
  },
691
757
 
758
+ // A native action sheet in the mobile shell; a styled bottom sheet in the
759
+ // browser and desktop shell. Resolves the chosen item (the same object you
760
+ // passed), or null on cancel/dismiss:
761
+ //
762
+ // const choice = await Everywhere.menu({
763
+ // title: "Post",
764
+ // items: [{ id: "share", title: "Share" },
765
+ // { id: "delete", title: "Delete", style: "destructive" }]
766
+ // })
767
+ // if (choice?.id === "delete") …
768
+ menu(options = {}) {
769
+ const items = Array.isArray(options.items) ? options.items : []
770
+ return presentMenu({
771
+ title: options.title,
772
+ message: options.message,
773
+ items,
774
+ source: options.source
775
+ }).then((id) => (id == null ? null : items.find((item) => String(item.id) === String(id)) || null))
776
+ },
777
+
692
778
  // Refresh the native tab bar / navigation config now (mobile only; a no-op
693
779
  // in the browser and desktop shell, where there's nothing to refresh).
694
780
  reloadTabs() {
@@ -1032,10 +1118,237 @@ async function wireBiometricToggles() {
1032
1118
  })
1033
1119
  }
1034
1120
 
1121
+ // --- native chrome (menus, nav bar items) -----------------------------------
1122
+ // Picks the native action sheet in the mobile shell and a DOM bottom sheet
1123
+ // (native.css .everywhere-sheet) everywhere else. Resolves the chosen item's
1124
+ // id, or null on cancel/dismiss.
1125
+ function presentMenu({ title, message, items, source }) {
1126
+ const list = (items || []).map((item, i) => ({
1127
+ id: item.id != null ? String(item.id) : String(i),
1128
+ title: item.title || "",
1129
+ image: item.image || null,
1130
+ style: item.style || "default",
1131
+ disabled: !!item.disabled
1132
+ }))
1133
+
1134
+ if (platform === "mobile" && adapter.menuSupported && adapter.menuSupported()) {
1135
+ return new Promise((resolve) => {
1136
+ let settled = false
1137
+ const id = adapter.menuSend("menu", { title, message, items: list, source }, (message) => {
1138
+ if (settled) return
1139
+ settled = true
1140
+ adapter.menuRemove(id)
1141
+ const data = (message && message.data) || {}
1142
+ resolve(data.action === "select" && data.id != null ? String(data.id) : null)
1143
+ })
1144
+ if (id === null) resolve(domMenu({ title, message, items: list }))
1145
+ })
1146
+ }
1147
+ return domMenu({ title, message, items: list })
1148
+ }
1149
+
1150
+ // The honest web/desktop fallback for Everywhere.menu(): a modal bottom sheet
1151
+ // built from the same item list. No inline scripts — pure DOM — so it clears CSP.
1152
+ function domMenu({ title, message, items }) {
1153
+ if (typeof document === "undefined") return Promise.resolve(null)
1154
+
1155
+ return new Promise((resolve) => {
1156
+ const overlay = document.createElement("div")
1157
+ overlay.className = "everywhere-sheet-overlay"
1158
+ const sheet = document.createElement("div")
1159
+ sheet.className = "everywhere-sheet"
1160
+ sheet.setAttribute("role", "menu")
1161
+
1162
+ if (title || message) {
1163
+ const header = document.createElement("div")
1164
+ header.className = "everywhere-sheet-header"
1165
+ if (title) {
1166
+ const el = document.createElement("div")
1167
+ el.className = "everywhere-sheet-title"
1168
+ el.textContent = title
1169
+ header.appendChild(el)
1170
+ }
1171
+ if (message) {
1172
+ const el = document.createElement("div")
1173
+ el.className = "everywhere-sheet-message"
1174
+ el.textContent = message
1175
+ header.appendChild(el)
1176
+ }
1177
+ sheet.appendChild(header)
1178
+ }
1179
+
1180
+ let done = false
1181
+ const close = (value) => {
1182
+ if (done) return
1183
+ done = true
1184
+ document.removeEventListener("keydown", onKey)
1185
+ overlay.remove()
1186
+ resolve(value)
1187
+ }
1188
+ const onKey = (event) => { if (event.key === "Escape") close(null) }
1189
+
1190
+ items.forEach((item) => {
1191
+ const button = document.createElement("button")
1192
+ button.type = "button"
1193
+ button.className = "everywhere-sheet-item" +
1194
+ (item.style === "destructive" ? " everywhere-sheet-item-destructive" : "")
1195
+ button.textContent = item.title
1196
+ button.setAttribute("role", "menuitem")
1197
+ if (item.disabled) button.disabled = true
1198
+ else button.addEventListener("click", () => close(item.id))
1199
+ sheet.appendChild(button)
1200
+ })
1201
+
1202
+ const cancel = document.createElement("button")
1203
+ cancel.type = "button"
1204
+ cancel.className = "everywhere-sheet-item everywhere-sheet-cancel"
1205
+ cancel.textContent = "Cancel"
1206
+ cancel.addEventListener("click", () => close(null))
1207
+ sheet.appendChild(cancel)
1208
+
1209
+ overlay.addEventListener("click", (event) => { if (event.target === overlay) close(null) })
1210
+ document.addEventListener("keydown", onKey)
1211
+ overlay.appendChild(sheet)
1212
+ document.body.appendChild(overlay)
1213
+ })
1214
+ }
1215
+
1216
+ // Per-platform icon, resolved like tabs: `<base>-<os>` (SF Symbol on iOS, a
1217
+ // Material name on Android) then the shared `<base>`. One page serves both shells.
1218
+ function resolveNativeIcon(el, base) {
1219
+ return el.getAttribute(`${base}-${os}`) || el.getAttribute(base) || null
1220
+ }
1221
+
1222
+ function navText(el) {
1223
+ return (el.getAttribute("data-everywhere-nav-title") || el.textContent || "").trim()
1224
+ }
1225
+
1226
+ function navSide(el) {
1227
+ return el.getAttribute("data-everywhere-nav-side") === "left" ? "left" : "right"
1228
+ }
1229
+
1230
+ function menuItemTitle(el) {
1231
+ return (el.getAttribute("data-everywhere-menu-title") || el.textContent || "").trim()
1232
+ }
1233
+
1234
+ // The nav bar items the current page declares, plus a fresh id→element map so
1235
+ // native taps can .click() the source element. Menu children get ids like
1236
+ // "n2.0" so a pull-down item routes back to the right element.
1237
+ let navMessageId = null
1238
+ const navElements = new Map()
1239
+
1240
+ function collectNavItems() {
1241
+ navElements.clear()
1242
+ const items = []
1243
+ let n = 0
1244
+
1245
+ document.querySelectorAll("[data-everywhere-nav-button]").forEach((el) => {
1246
+ const id = "n" + (n++)
1247
+ navElements.set(id, el)
1248
+ items.push({
1249
+ id,
1250
+ kind: "button",
1251
+ title: navText(el),
1252
+ image: resolveNativeIcon(el, "data-everywhere-nav-icon"),
1253
+ side: navSide(el),
1254
+ style: el.getAttribute("data-everywhere-nav-style") || "plain",
1255
+ disabled: el.disabled === true || el.hasAttribute("data-everywhere-nav-disabled")
1256
+ })
1257
+ })
1258
+
1259
+ document.querySelectorAll("[data-everywhere-nav-menu]").forEach((menuEl) => {
1260
+ const id = "n" + (n++)
1261
+ const children = []
1262
+ menuEl.querySelectorAll("[data-everywhere-menu-item]").forEach((child, i) => {
1263
+ const cid = id + "." + i
1264
+ navElements.set(cid, child)
1265
+ children.push({
1266
+ id: cid,
1267
+ title: menuItemTitle(child),
1268
+ image: resolveNativeIcon(child, "data-everywhere-menu-icon"),
1269
+ style: child.getAttribute("data-everywhere-menu-style") || "default"
1270
+ })
1271
+ })
1272
+ items.push({
1273
+ id,
1274
+ kind: "menu",
1275
+ title: navText(menuEl),
1276
+ image: resolveNativeIcon(menuEl, "data-everywhere-nav-icon") || "ellipsis.circle",
1277
+ side: navSide(menuEl),
1278
+ children
1279
+ })
1280
+ })
1281
+
1282
+ return items
1283
+ }
1284
+
1285
+ // Send this page's nav bar items to the shell. Native re-replies on every tap,
1286
+ // so the callback lives until the next page swaps it out (menuRemove first).
1287
+ function applyNavItems() {
1288
+ if (platform !== "mobile" || !adapter.menuSupported || !adapter.menuSupported()) return
1289
+ if (navMessageId !== null) {
1290
+ adapter.menuRemove(navMessageId)
1291
+ navMessageId = null
1292
+ }
1293
+ const items = collectNavItems()
1294
+ if (!items.length) return
1295
+
1296
+ navMessageId = adapter.menuSend("navItems", { items }, (message) => {
1297
+ const data = (message && message.data) || {}
1298
+ if (data.action === "tap" && data.id != null) {
1299
+ const el = navElements.get(String(data.id))
1300
+ if (el) el.click()
1301
+ }
1302
+ })
1303
+ }
1304
+
1305
+ // A tap on an in-content [data-everywhere-menu-trigger]: a native action sheet
1306
+ // in the shell, an inline disclosure (toggling [data-everywhere-menu-open]) in
1307
+ // a browser. Either way, choosing an item .click()s the element it maps to.
1308
+ function handleMenuTrigger(event, trigger) {
1309
+ const container = trigger.closest("[data-everywhere-menu]") || trigger.parentElement
1310
+ if (!container) return
1311
+ const holder = container.querySelector("[data-everywhere-menu-items]") || container
1312
+ const elements = Array.from(holder.querySelectorAll("[data-everywhere-menu-item]"))
1313
+
1314
+ if (platform === "mobile" && adapter.menuSupported && adapter.menuSupported()) {
1315
+ event.preventDefault()
1316
+ const rect = trigger.getBoundingClientRect()
1317
+ const items = elements.map((el, i) => ({
1318
+ id: String(i),
1319
+ title: menuItemTitle(el),
1320
+ image: resolveNativeIcon(el, "data-everywhere-menu-icon"),
1321
+ style: el.getAttribute("data-everywhere-menu-style") || "default",
1322
+ disabled: el.disabled === true
1323
+ }))
1324
+ presentMenu({
1325
+ title: trigger.getAttribute("data-everywhere-menu-title") ||
1326
+ container.getAttribute("data-everywhere-menu-title") || null,
1327
+ items,
1328
+ source: { x: rect.left, y: rect.top, width: rect.width, height: rect.height }
1329
+ }).then((id) => {
1330
+ if (id == null) return
1331
+ const el = elements[parseInt(id, 10)]
1332
+ if (el) el.click()
1333
+ })
1334
+ } else {
1335
+ event.preventDefault()
1336
+ const open = container.hasAttribute("data-everywhere-menu-open")
1337
+ closeInlineMenus()
1338
+ if (!open) container.setAttribute("data-everywhere-menu-open", "")
1339
+ }
1340
+ }
1341
+
1342
+ function closeInlineMenus() {
1343
+ document.querySelectorAll("[data-everywhere-menu-open]")
1344
+ .forEach((el) => el.removeAttribute("data-everywhere-menu-open"))
1345
+ }
1346
+
1035
1347
  function applyDeclarative() {
1036
1348
  applyBadgeMetas()
1037
1349
  applyBiometricGates()
1038
1350
  wireBiometricToggles()
1351
+ applyNavItems()
1039
1352
  }
1040
1353
 
1041
1354
  if (typeof document !== "undefined") {
@@ -1061,6 +1374,13 @@ if (typeof document !== "undefined") {
1061
1374
  if (content) content.hidden = true
1062
1375
  if (overlay) overlay.hidden = true
1063
1376
  })
1377
+ // Never snapshot an open inline menu; the next page's applyNavItems()
1378
+ // replaces the nav callback, so drop this page's before it's cached.
1379
+ closeInlineMenus()
1380
+ if (navMessageId !== null) {
1381
+ adapter.menuRemove(navMessageId)
1382
+ navMessageId = null
1383
+ }
1064
1384
  })
1065
1385
 
1066
1386
  document.addEventListener("click", (event) => {
@@ -1071,6 +1391,14 @@ if (typeof document !== "undefined") {
1071
1391
  const unlock = event.target.closest("[data-everywhere-biometric-unlock]")
1072
1392
  const gate = unlock && unlock.closest("[data-everywhere-biometric-lock]")
1073
1393
  if (gate) resolveBiometricGate(gate, { prompt: true })
1394
+
1395
+ const trigger = event.target.closest("[data-everywhere-menu-trigger]")
1396
+ if (trigger) {
1397
+ handleMenuTrigger(event, trigger)
1398
+ } else if (!event.target.closest("[data-everywhere-menu-open]")) {
1399
+ // A click anywhere outside an open inline menu dismisses it.
1400
+ closeInlineMenus()
1401
+ }
1074
1402
  })
1075
1403
 
1076
1404
  document.addEventListener("change", async (event) => {
@@ -0,0 +1,203 @@
1
+ /* @rubyeverywhere/bridge — safe-area utilities for the native shells.
2
+ *
3
+ * Pair with <meta name="viewport" content="…, viewport-fit=cover"> so the
4
+ * browser exposes the device's safe-area insets. These classes pad content
5
+ * clear of the status bar / Dynamic Island / home indicator without
6
+ * hardcoding device sizes. On the desktop shell and plain browsers the insets
7
+ * are 0, so the classes are inert — safe to apply unconditionally.
8
+ *
9
+ * Include in Rails: <%= stylesheet_link_tag "everywhere/native" %>
10
+ * (Sinatra/Hanami: it's vendored to public/ by `every install`.)
11
+ */
12
+
13
+ :root {
14
+ --everywhere-inset-top: env(safe-area-inset-top, 0px);
15
+ --everywhere-inset-right: env(safe-area-inset-right, 0px);
16
+ --everywhere-inset-bottom: env(safe-area-inset-bottom, 0px);
17
+ --everywhere-inset-left: env(safe-area-inset-left, 0px);
18
+ }
19
+
20
+ /* Padding utilities — add safe-area room on top of any existing padding. */
21
+ .native-inset {
22
+ padding-top: calc(var(--everywhere-inset-top) + var(--native-inset-pad, 0px));
23
+ padding-bottom: calc(var(--everywhere-inset-bottom) + var(--native-inset-pad, 0px));
24
+ }
25
+ .native-inset-top { padding-top: calc(var(--everywhere-inset-top) + var(--native-inset-pad, 0px)); }
26
+ .native-inset-bottom { padding-bottom: calc(var(--everywhere-inset-bottom) + var(--native-inset-pad, 0px)); }
27
+ .native-inset-left { padding-left: calc(var(--everywhere-inset-left) + var(--native-inset-pad, 0px)); }
28
+ .native-inset-right { padding-right: calc(var(--everywhere-inset-right) + var(--native-inset-pad, 0px)); }
29
+
30
+ /* Margin variants, when padding would fight an existing background. */
31
+ .native-inset-mt { margin-top: var(--everywhere-inset-top); }
32
+ .native-inset-mb { margin-bottom: var(--everywhere-inset-bottom); }
33
+
34
+ /* A bar pinned to the bottom edge that stays above the home indicator. */
35
+ .native-safe-bottom {
36
+ padding-bottom: max(var(--everywhere-inset-bottom), var(--native-safe-min, 0.5rem));
37
+ }
38
+
39
+ /* Fixed/absolute elements anchored near the bottom (toasts, nudges, FABs):
40
+ * lifts them clear of the home indicator AND anything overlaying the webview
41
+ * from below — on iOS the floating tab bar contributes to the bottom inset.
42
+ * --native-bottom-gap is the visual gap above that (default 1rem). */
43
+ .native-bottom {
44
+ bottom: calc(var(--everywhere-inset-bottom) + var(--native-bottom-gap, 1rem));
45
+ }
46
+
47
+ /* Full-height element that respects both vertical insets. */
48
+ .native-safe-height {
49
+ min-height: calc(100dvh - var(--everywhere-inset-top) - var(--everywhere-inset-bottom));
50
+ }
51
+
52
+ /* ---------------------------------------------------------------------------
53
+ * Native chrome — styling for the everywhere_fab / everywhere_menu helpers and
54
+ * the bridge's DOM action-sheet fallback. Set --everywhere-tint in your own CSS
55
+ * to brand the accent (it defaults to the platform accent color).
56
+ * ------------------------------------------------------------------------- */
57
+
58
+ :root {
59
+ --everywhere-tint: AccentColor;
60
+ }
61
+
62
+ /* Floating action button (everywhere_fab): fixed, safe-area-aware, above the
63
+ * home indicator and iOS floating tab bar. A real link/button, so it shows in
64
+ * a browser too. */
65
+ .everywhere-fab {
66
+ position: fixed;
67
+ right: max(var(--everywhere-inset-right), 1rem);
68
+ bottom: calc(var(--everywhere-inset-bottom) + var(--native-bottom-gap, 1rem));
69
+ z-index: 1000;
70
+ display: inline-flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ gap: 0.5rem;
74
+ width: 3.5rem;
75
+ height: 3.5rem;
76
+ padding: 0;
77
+ border: 0;
78
+ border-radius: 9999px;
79
+ background: var(--everywhere-fab-bg, var(--everywhere-tint, #1a73e8));
80
+ color: var(--everywhere-fab-color, #fff);
81
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.28);
82
+ cursor: pointer;
83
+ -webkit-tap-highlight-color: transparent;
84
+ transition: transform 0.1s ease, box-shadow 0.1s ease;
85
+ }
86
+ .everywhere-fab:active {
87
+ transform: scale(0.94);
88
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
89
+ }
90
+ .everywhere-fab-left {
91
+ right: auto;
92
+ left: max(var(--everywhere-inset-left), 1rem);
93
+ }
94
+ /* Extended FAB: pill with a visible label beside the icon. */
95
+ .everywhere-fab-extended {
96
+ width: auto;
97
+ height: 3.25rem;
98
+ padding: 0 1.25rem;
99
+ font: inherit;
100
+ font-weight: 600;
101
+ }
102
+ .everywhere-fab-icon {
103
+ width: 1.5rem;
104
+ height: 1.5rem;
105
+ flex: none;
106
+ }
107
+ .everywhere-fab-label {
108
+ white-space: nowrap;
109
+ }
110
+
111
+ /* In-content action sheet (everywhere_menu) inline fallback: the items stay
112
+ * hidden until the trigger opens them. In the native shell the bridge shows a
113
+ * real action sheet instead and these never appear. */
114
+ [data-everywhere-menu-items] {
115
+ display: none;
116
+ }
117
+ [data-everywhere-menu-open] > [data-everywhere-menu-items],
118
+ [data-everywhere-menu-open] [data-everywhere-menu-items] {
119
+ display: block;
120
+ }
121
+
122
+ /* Nav-bar sources (everywhere_nav_button / everywhere_nav_menu) live in the
123
+ * top bar inside the shell, so hide the in-page copy there — but keep it in a
124
+ * browser, where there is no native bar. Keyed on the bridge's platform marker
125
+ * (set only by the mobile web bridge). */
126
+ html[data-bridge-platform] [data-everywhere-nav-button],
127
+ html[data-bridge-platform] [data-everywhere-nav-menu] {
128
+ display: none !important;
129
+ }
130
+
131
+ /* DOM action-sheet fallback rendered by the bridge (Everywhere.menu / the
132
+ * everywhere_menu trigger) when there is no native shell. */
133
+ .everywhere-sheet-overlay {
134
+ position: fixed;
135
+ inset: 0;
136
+ z-index: 2000;
137
+ display: flex;
138
+ align-items: flex-end;
139
+ justify-content: center;
140
+ padding: 0.75rem;
141
+ padding-bottom: calc(var(--everywhere-inset-bottom) + 0.75rem);
142
+ background: rgba(0, 0, 0, 0.4);
143
+ animation: everywhere-sheet-fade 0.15s ease;
144
+ }
145
+ .everywhere-sheet {
146
+ width: 100%;
147
+ max-width: 30rem;
148
+ display: flex;
149
+ flex-direction: column;
150
+ gap: 1px;
151
+ border-radius: 0.9rem;
152
+ overflow: hidden;
153
+ background: rgba(120, 120, 128, 0.2);
154
+ animation: everywhere-sheet-rise 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
155
+ }
156
+ .everywhere-sheet-header {
157
+ padding: 0.85rem 1rem;
158
+ text-align: center;
159
+ background: Canvas;
160
+ color: color-mix(in srgb, CanvasText 60%, transparent);
161
+ }
162
+ .everywhere-sheet-title {
163
+ font-weight: 600;
164
+ color: CanvasText;
165
+ }
166
+ .everywhere-sheet-message {
167
+ font-size: 0.85rem;
168
+ margin-top: 0.15rem;
169
+ }
170
+ .everywhere-sheet-item {
171
+ appearance: none;
172
+ border: 0;
173
+ width: 100%;
174
+ padding: 1rem;
175
+ font: inherit;
176
+ font-size: 1.05rem;
177
+ text-align: center;
178
+ background: Canvas;
179
+ color: var(--everywhere-tint, #1a73e8);
180
+ cursor: pointer;
181
+ }
182
+ .everywhere-sheet-item:active {
183
+ background: color-mix(in srgb, CanvasText 8%, Canvas);
184
+ }
185
+ .everywhere-sheet-item:disabled {
186
+ color: color-mix(in srgb, CanvasText 30%, transparent);
187
+ cursor: default;
188
+ }
189
+ .everywhere-sheet-item-destructive {
190
+ color: #e5484d;
191
+ }
192
+ .everywhere-sheet-cancel {
193
+ margin-top: 0.5rem;
194
+ border-radius: 0.9rem;
195
+ font-weight: 600;
196
+ color: CanvasText;
197
+ }
198
+ @keyframes everywhere-sheet-fade {
199
+ from { opacity: 0; }
200
+ }
201
+ @keyframes everywhere-sheet-rise {
202
+ from { transform: translateY(1rem); opacity: 0; }
203
+ }
data/bridge/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@rubyeverywhere/bridge",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "One API for browser, desktop, and mobile — the JS half of RubyEverywhere",
5
5
  "type": "module",
6
6
  "main": "everywhere/bridge.js",
7
- "exports": { ".": "./everywhere/bridge.js" },
8
- "files": ["everywhere/bridge.js", "README.md", "LICENSE"],
7
+ "exports": {
8
+ ".": "./everywhere/bridge.js",
9
+ "./native.css": "./everywhere/native.css"
10
+ },
11
+ "files": ["everywhere/bridge.js", "everywhere/native.css", "README.md", "LICENSE"],
9
12
  "sideEffects": false,
10
13
  "keywords": ["rubyeverywhere", "tauri", "hotwire", "turbo", "rails", "desktop", "native", "bridge"],
11
14
  "homepage": "https://rubyeverywhere.com",