ruby_native 0.0.5 → 0.1.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: 836bc807774628d28c27eb1a6dfbff6e3c0290ba6c741f6cd94a275c74e7b922
4
- data.tar.gz: 9f1b0a41befb2052118ce36d7000a804fb08d0b8e32ea0c259db8430fb9f4045
3
+ metadata.gz: d6157407a95aecdb39c9f05de070e3268e6d0910162fcd3b50e54b75f52f4093
4
+ data.tar.gz: 66c9cc2caa92638cbb52f56cae855c16ffd956de4ad1e8db843e990a7197cc90
5
5
  SHA512:
6
- metadata.gz: 9d80810078f702591e54cd8443918ae3d90b21276d96cc4b5b5a39f6b951d8ef49197b0b4cc0ebc8b7c015de128fad8de4790dc2a3805b912029ce113a979236
7
- data.tar.gz: 27d4d742993fb0b42aeeae37ba03a9a339ec5080cc7bb658e5828833de0129b0e03c11c78dfb8b38dd1188649249b0c7620a83b598b433f925833fbc6f56d2ad
6
+ metadata.gz: 1a510f3343561161004f5a4104a28c79846f224e4d30335fcd33617d18f57c35179aedced255e9876fdb704c6cdd7e5b26727614dbda9bb0e5c95470fbd029a5
7
+ data.tar.gz: 0a177fca9a18e66d623c18a97412e4ccc923ca3f97c750f90c246f6f7eb4c7124bd279a41fa6a91f3c3e627deabf7b83584ad74f5fc731a0703311c5042b8451
data/README.md CHANGED
@@ -23,7 +23,7 @@ This creates `config/ruby_native.yml` with sensible defaults. If you have a `.cl
23
23
  1. Edit the config with your app name, colors, and tabs
24
24
  2. Add `<%= stylesheet_link_tag :ruby_native %>` to your layout `<head>`
25
25
  3. Add `<%= native_tabs_tag %>` to your layout `<body>`
26
- 4. Run `ruby_native preview` to see it on your phone
26
+ 4. Run `bundle exec ruby_native preview` to see it on your phone
27
27
 
28
28
  Using Claude Code? Open it in your project and ask "what do I need to do next?" for guided setup.
29
29
 
@@ -32,8 +32,6 @@ Using Claude Code? Open it in your project and ask "what do I need to do next?"
32
32
  Edit `config/ruby_native.yml`:
33
33
 
34
34
  ```yaml
35
- app:
36
- name: My App
37
35
  appearance:
38
36
  tint_color: "#4F46E5"
39
37
  background_color: "#FFFFFF"
@@ -60,7 +58,7 @@ background_color:
60
58
  Preview your app on a real device without deploying. This starts a Cloudflare tunnel and displays a QR code for the companion app to scan.
61
59
 
62
60
  ```bash
63
- ruby_native preview
61
+ bundle exec ruby_native preview
64
62
  ```
65
63
 
66
64
  Options:
@@ -80,14 +78,88 @@ The companion app persists the scanned URL across launches. Long-press the app i
80
78
  - `GET /native/config` - returns the YAML config as JSON
81
79
  - `POST /native/push/devices` - registers a push notification token (requires `current_user` from host app)
82
80
 
81
+ ## Standard and Advanced Modes
82
+
83
+ Standard Mode works with any frontend framework and requires no JavaScript. You get tabs, form page marking, push notifications, and history management.
84
+
85
+ Advanced Mode adds native navigation bar buttons, submit buttons, action menus, and search bars. It requires Stimulus and a small JavaScript setup step (see [Advanced Mode setup](#advanced-mode-setup) below). Migration is additive. Start with Standard and add Advanced helpers one page at a time.
86
+
83
87
  ## View helpers
84
88
 
89
+ Place helpers in the `<body>`, not the `<head>`.
90
+
91
+ ### Any mode
92
+
85
93
  - `native_app?` - true when the request comes from a Ruby Native app (checks user agent)
86
- - `native_tabs_tag` - renders a hidden signal element for tab bar detection
87
- - `native_form_tag` - renders a hidden signal element marking the page as a form
88
- - `native_push_tag` - renders a hidden signal element requesting push permission
94
+ - `native_tabs_tag(enabled: true)` - shows the native tab bar.
95
+ - `native_push_tag` - requests push notification permission.
96
+ - `native_back_button_tag(text = nil, **options)` - renders a back button for Standard Mode. Hidden by default, shown when the native app sets `body.can-go-back`. Not needed in [Advanced Mode](https://rubynative.dev/docs/advanced-mode) where the system provides a native back button.
97
+
98
+ ### Standard Mode
99
+
100
+ - `native_form_tag` - marks the page as a form. The app skips form pages when navigating back.
101
+
102
+ ### Advanced Mode
103
+
104
+ These require the JavaScript setup described in [Advanced Mode setup](#advanced-mode-setup).
105
+
106
+ - `native_form_data` - returns the data hash for the native form submit button. Pass to `form_with`'s `data:` option.
107
+ - `native_submit_data` - returns the data hash for the native submit target. Pass to `form.submit`'s `data:` option.
108
+ - `native_button_tag(title, url, ios_image:, side: :right, **options)` - adds a native navigation bar button.
109
+ - `native_menu_tag(title:, side: :right, &block)` - displays a native action sheet menu.
110
+ - `native_search_tag` - adds a native search bar.
111
+
112
+ ## Advanced Mode setup
113
+
114
+ 1. Install the JavaScript dependency:
115
+
116
+ ```bash
117
+ yarn add @hotwired/hotwire-native-bridge
118
+ # or
119
+ bin/importmap pin @hotwired/hotwire-native-bridge
120
+ ```
121
+
122
+ 2. Import the controllers in your JavaScript entrypoint:
123
+
124
+ ```js
125
+ import "ruby_native/bridge"
126
+ ```
127
+
128
+ ### `native_form_data` / `native_submit_data`
129
+
130
+ ```erb
131
+ <%= form_with model: @link, data: native_form_data do |f| %>
132
+ <%= f.text_field :url %>
133
+ <%= f.submit "Save", data: native_submit_data %>
134
+ <% end %>
135
+ ```
136
+
137
+ ### `native_button_tag`
138
+
139
+ ```erb
140
+ <%= native_button_tag "Add a link", new_link_path, ios_image: "plus", class: "btn btn-primary" %>
141
+ ```
142
+
143
+ Options:
144
+ - `ios_image:` - SF Symbol icon name (falls back to the title text)
145
+ - `side:` - `:left` or `:right` (default). Left supplements the back button.
146
+
147
+ ### `native_menu_tag`
148
+
149
+ ```erb
150
+ <%= native_menu_tag(title: "Actions") do |menu| %>
151
+ <%= menu.item "Edit", edit_link_path(@link) %>
152
+ <%= menu.item "Delete", link_path(@link), method: :delete, destructive: true %>
153
+ <% end %>
154
+ ```
155
+
156
+ Options on `native_menu_tag`:
157
+ - `title:` - action sheet title
158
+ - `side:` - `:left` or `:right` (default)
89
159
 
90
- Signal elements are hidden `<div>` tags with data attributes (e.g., `<div data-native-tabs hidden>`). Place them in the `<body>`, not the `<head>`.
160
+ Options on `menu.item`:
161
+ - `method:` - Turbo method (e.g., `:delete`)
162
+ - `destructive: true` - red styling
91
163
 
92
164
  ## Stylesheet
93
165
 
@@ -5,3 +5,14 @@
5
5
  body.can-go-back .native-back-button {
6
6
  display: inline;
7
7
  }
8
+
9
+ [data-bridge-components~="form"]
10
+ [data-controller~="bridge--form"]
11
+ [type="submit"] {
12
+ display: none;
13
+ }
14
+
15
+ [data-bridge-components~="button"]
16
+ [data-controller~="bridge--button"] {
17
+ display: none;
18
+ }
@@ -0,0 +1,3 @@
1
+ export function goBack() {
2
+ webkit.messageHandlers.rubyNative.postMessage({ action: "back" })
3
+ }
@@ -0,0 +1,30 @@
1
+ import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
2
+
3
+ export default class extends BridgeComponent {
4
+ static component = "button"
5
+
6
+ connect() {
7
+ super.connect()
8
+ this.#addButton()
9
+ }
10
+
11
+ disconnect() {
12
+ super.disconnect()
13
+ this.#removeButton()
14
+ }
15
+
16
+ #addButton() {
17
+ const element = this.bridgeElement
18
+ const side = element.bridgeAttribute("side") || "right"
19
+ const image = element.bridgeAttribute("ios-image")
20
+ const data = { title: element.title, image }
21
+
22
+ this.send(side, data, () => {
23
+ this.element.click()
24
+ })
25
+ }
26
+
27
+ #removeButton() {
28
+ this.send("disconnect")
29
+ }
30
+ }
@@ -0,0 +1,27 @@
1
+ import { BridgeComponent, BridgeElement } from "@hotwired/hotwire-native-bridge"
2
+
3
+ export default class extends BridgeComponent {
4
+ static component = "form"
5
+ static targets = ["submit"]
6
+
7
+ connect() {
8
+ super.connect()
9
+
10
+ const title = new BridgeElement(this.submitTarget).title
11
+ this.send("connect", { submitTitle: title.trim() }, () => {
12
+ this.submitTarget.click()
13
+ })
14
+
15
+ this.element.addEventListener("turbo:submit-start", this.submitStarted)
16
+ this.element.addEventListener("turbo:submit-end", this.submitEnded)
17
+ }
18
+
19
+ disconnect() {
20
+ super.disconnect()
21
+ this.element.removeEventListener("turbo:submit-start", this.submitStarted)
22
+ this.element.removeEventListener("turbo:submit-end", this.submitEnded)
23
+ }
24
+
25
+ submitStarted = () => this.send("submitDisabled")
26
+ submitEnded = () => this.send("submitEnabled")
27
+ }
@@ -0,0 +1,14 @@
1
+ import { application } from "controllers/application"
2
+ import TabsController from "ruby_native/bridge/tabs_controller"
3
+ import FormController from "ruby_native/bridge/form_controller"
4
+ import PushController from "ruby_native/bridge/push_controller"
5
+ import MenuController from "ruby_native/bridge/menu_controller"
6
+ import SearchController from "ruby_native/bridge/search_controller"
7
+ import ButtonController from "ruby_native/bridge/button_controller"
8
+
9
+ application.register("bridge--tabs", TabsController)
10
+ application.register("bridge--form", FormController)
11
+ application.register("bridge--push", PushController)
12
+ application.register("bridge--menu", MenuController)
13
+ application.register("bridge--search", SearchController)
14
+ application.register("bridge--button", ButtonController)
@@ -0,0 +1,22 @@
1
+ import { BridgeComponent, BridgeElement } from "@hotwired/hotwire-native-bridge"
2
+
3
+ export default class extends BridgeComponent {
4
+ static component = "menu"
5
+ static targets = ["item"]
6
+ static values = { title: String, side: { type: String, default: "right" } }
7
+
8
+ connect() {
9
+ super.connect()
10
+
11
+ const items = this.itemTargets.map((el, index) => ({
12
+ title: new BridgeElement(el).title,
13
+ index,
14
+ destructive: el.hasAttribute("data-destructive")
15
+ }))
16
+
17
+ this.send("connect", { title: this.titleValue, items, side: this.sideValue }, (message) => {
18
+ const { selectedIndex } = message.data
19
+ this.itemTargets[selectedIndex]?.click()
20
+ })
21
+ }
22
+ }
@@ -0,0 +1,10 @@
1
+ import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
2
+
3
+ export default class extends BridgeComponent {
4
+ static component = "push"
5
+
6
+ connect() {
7
+ super.connect()
8
+ this.send("connect")
9
+ }
10
+ }
@@ -0,0 +1,16 @@
1
+ import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
2
+
3
+ export default class extends BridgeComponent {
4
+ static component = "search"
5
+
6
+ connect() {
7
+ super.connect()
8
+
9
+ this.send("connect", {}, (message) => {
10
+ const query = message.data.query
11
+ const detail = {query}
12
+
13
+ this.dispatch("queried", {detail})
14
+ })
15
+ }
16
+ }
@@ -0,0 +1,11 @@
1
+ import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
2
+
3
+ export default class extends BridgeComponent {
4
+ static component = "tabs"
5
+ static values = { enabled: Boolean }
6
+
7
+ connect() {
8
+ super.connect()
9
+ this.send("connect", { enabled: this.enabledValue })
10
+ }
11
+ }
@@ -0,0 +1,2 @@
1
+ pin_all_from RubyNative::Engine.root.join("app/javascript/ruby_native/bridge"), under: "ruby_native/bridge"
2
+ pin "ruby_native/back", to: "ruby_native/back.js"
@@ -35,7 +35,7 @@ module RubyNative
35
35
  say " 3. Add to your layout <body>:"
36
36
  say " <%= native_tabs_tag %>"
37
37
  say " 4. Preview on your device:"
38
- say " ruby_native preview"
38
+ say " bundle exec ruby_native preview"
39
39
  say ""
40
40
  if File.directory?(File.join(destination_root, ".claude"))
41
41
  say " Tip: .claude/ruby_native.md was added with setup instructions."
@@ -33,7 +33,7 @@ rails generate ruby_native:install
33
33
  6. Preview on your phone:
34
34
 
35
35
  ```bash
36
- ruby_native preview
36
+ bundle exec ruby_native preview
37
37
  ```
38
38
 
39
39
  Scan the QR code with the Ruby Native Preview app from the App Store.
@@ -100,7 +100,7 @@ Signal elements are hidden `<div>` tags. Place them in the `<body>`, not the `<h
100
100
 
101
101
  ## Preview
102
102
 
103
- `ruby_native preview` starts a Cloudflare tunnel and displays a QR code. Requires `cloudflared`:
103
+ `bundle exec ruby_native preview` starts a Cloudflare tunnel and displays a QR code. Requires `cloudflared`:
104
104
 
105
105
  ```bash
106
106
  brew install cloudflare/cloudflare/cloudflared
@@ -122,7 +122,7 @@ The Preview app remembers the scanned URL. Long-press the app icon and tap "Swit
122
122
 
123
123
  The gem auto-mounts at `/native`. No route configuration needed.
124
124
 
125
- - `GET /native/config.json` returns the YAML config as JSON
125
+ - `GET /native/config` returns the YAML config as JSON
126
126
  - `POST /native/push/devices` registers a push notification device token
127
127
 
128
128
  ## Common tasks
@@ -139,16 +139,14 @@ The gem auto-mounts at `/native`. No route configuration needed.
139
139
  <%= native_tabs_tag if user_signed_in? %>
140
140
  ```
141
141
 
142
- ### Add a native back button
142
+ ### Add a native back button (Standard Mode only)
143
143
 
144
- Add an element with the `native-back-button` class. The gem's stylesheet handles showing it only when there's history to go back to.
144
+ Use the `native_back_button_tag` helper. The gem's stylesheet handles showing it only when there's history to go back to. Not needed in Advanced Mode where the system provides a native back button.
145
145
 
146
146
  ```erb
147
147
  <%= stylesheet_link_tag :ruby_native %>
148
148
  ```
149
149
 
150
150
  ```erb
151
- <button class="native-back-button" onclick="webkit.messageHandlers.rubyNative.postMessage({action: 'back'})">
152
- Back
153
- </button>
151
+ <%= native_back_button_tag %>
154
152
  ```
@@ -5,10 +5,10 @@ module RubyNative
5
5
  class Preview
6
6
  TUNNEL_URL_PATTERN = %r{https://[a-z0-9-]+\.trycloudflare\.com}
7
7
 
8
- BLACK_BG = "\033[48;2;0;0;0m"
9
- WHITE_BG = "\033[48;2;255;255;255m"
10
- BLACK_FG = "\033[38;2;0;0;0m"
11
- WHITE_FG = "\033[38;2;255;255;255m"
8
+ BLACK_BG = "\033[40m"
9
+ WHITE_BG = "\033[107m"
10
+ BLACK_FG = "\033[30m"
11
+ WHITE_FG = "\033[97m"
12
12
  RESET = "\033[0m"
13
13
 
14
14
  def initialize(argv)
@@ -10,7 +10,16 @@ module RubyNative
10
10
  end
11
11
 
12
12
  initializer "ruby_native.assets" do |app|
13
- app.config.assets.paths << root.join("app/assets/stylesheets") if app.config.respond_to?(:assets)
13
+ if app.config.respond_to?(:assets)
14
+ app.config.assets.paths << root.join("app/assets/stylesheets")
15
+ app.config.assets.paths << root.join("app/javascript")
16
+ end
17
+ end
18
+
19
+ initializer "ruby_native.importmap", before: "importmap" do |app|
20
+ if app.config.respond_to?(:importmap)
21
+ app.config.importmap.paths << root.join("config/importmap.rb")
22
+ end
14
23
  end
15
24
 
16
25
  initializer "ruby_native.config" do
@@ -4,16 +4,79 @@ module RubyNative
4
4
  request.user_agent.to_s.include?("Ruby Native")
5
5
  end
6
6
 
7
- def native_tabs_tag
8
- tag.div(data: { native_tabs: true }, hidden: true)
7
+ def native_tabs_tag(enabled: true)
8
+ safe_join([
9
+ (tag.div(data: { native_tabs: true }, hidden: true) if enabled),
10
+ tag.div(data: { controller: "bridge--tabs", bridge__tabs_enabled_value: enabled })
11
+ ].compact)
9
12
  end
10
13
 
11
14
  def native_form_tag
12
15
  tag.div(data: { native_form: true }, hidden: true)
13
16
  end
14
17
 
18
+ def native_form_data
19
+ { controller: "bridge--form" }
20
+ end
21
+
22
+ def native_submit_data
23
+ { bridge__form_target: "submit" }
24
+ end
25
+
15
26
  def native_push_tag
16
- tag.div(data: { native_push: true }, hidden: true)
27
+ safe_join([
28
+ tag.div(data: { native_push: true }, hidden: true),
29
+ tag.div(data: { controller: "bridge--push" })
30
+ ])
31
+ end
32
+
33
+ def native_back_button_tag(text = nil, **options)
34
+ options[:class] = [options[:class], "native-back-button"].compact.join(" ")
35
+ tag.button(text || "Back", onclick: "webkit.messageHandlers.rubyNative.postMessage({action: 'back'})", **options)
36
+ end
37
+
38
+ def native_search_tag
39
+ tag.div(data: { controller: "bridge--search" })
40
+ end
41
+
42
+ def native_button_tag(title, url, ios_image: nil, side: :right, **options)
43
+ data = options.delete(:data) || {}
44
+ data[:controller] = "bridge--button"
45
+ data[:bridge_side] = side.to_s
46
+ data[:bridge_ios_image] = ios_image if ios_image
47
+
48
+ link_to title, url, **options, data: data
49
+ end
50
+
51
+ def native_menu_tag(title:, side: :right, &block)
52
+ builder = MenuBuilder.new(self)
53
+ capture(builder, &block)
54
+
55
+ tag.div(style: "display:none", data: {
56
+ controller: "bridge--menu",
57
+ bridge__menu_title_value: title,
58
+ bridge__menu_side_value: side.to_s
59
+ }) { builder.to_html }
60
+ end
61
+
62
+ class MenuBuilder
63
+ def initialize(context)
64
+ @context = context
65
+ @items = []
66
+ end
67
+
68
+ def item(title, url, method: nil, destructive: false, **options)
69
+ data = options.delete(:data) || {}
70
+ data[:bridge__menu_target] = "item"
71
+ data[:turbo_method] = method if method
72
+ data[:destructive] = "" if destructive
73
+
74
+ @items << @context.link_to(title, url, **options, data: data, hidden: true)
75
+ end
76
+
77
+ def to_html
78
+ @context.safe_join(@items)
79
+ end
17
80
  end
18
81
  end
19
82
  end
@@ -1,3 +1,3 @@
1
1
  module RubyNative
2
- VERSION = "0.0.5"
2
+ VERSION = "0.1.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_native
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Masilotti
@@ -51,6 +51,15 @@ files:
51
51
  - app/assets/stylesheets/ruby_native.css
52
52
  - app/controllers/ruby_native/config_controller.rb
53
53
  - app/controllers/ruby_native/push/devices_controller.rb
54
+ - app/javascript/ruby_native/back.js
55
+ - app/javascript/ruby_native/bridge/button_controller.js
56
+ - app/javascript/ruby_native/bridge/form_controller.js
57
+ - app/javascript/ruby_native/bridge/index.js
58
+ - app/javascript/ruby_native/bridge/menu_controller.js
59
+ - app/javascript/ruby_native/bridge/push_controller.js
60
+ - app/javascript/ruby_native/bridge/search_controller.js
61
+ - app/javascript/ruby_native/bridge/tabs_controller.js
62
+ - config/importmap.rb
54
63
  - config/routes.rb
55
64
  - exe/ruby_native
56
65
  - lib/generators/ruby_native/install_generator.rb