ruby_native 0.0.6 → 0.1.1

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: 964767ba60eef5d5f816a3644ea585e029fbf6e46f196c8882a0bfa391d4a649
4
- data.tar.gz: 5e0a6be4f2692a36911c2641fabcf50fb85676bbf351fb603c561f909753931e
3
+ metadata.gz: c9cc3f159d5a1cdb53f843060574ad40fa6023854633df6ab08c23628246f926
4
+ data.tar.gz: bfc390268391502cc285da1011e2b17263829d1a92daf33bf40256ebe90f57a3
5
5
  SHA512:
6
- metadata.gz: 3011427d8748de602bdb099b2247c52c346eb956b2f4300157efad9df75556fa77740ef45629e08cbe7aabe4e31963d5d2f93a69263ad398826b3f1a2c8bed65
7
- data.tar.gz: d9e25c1151c74c9bcddab33fb4125f6af3f388319f4eab2afe5f7d2d7b9be7fa51943a26eab833ddb4449700c3767535ee5ce1d5b6a118aa1a2a746599cc28bc
6
+ metadata.gz: 7638386535366c20bb57eac4b339d1c6df136d172efbebbda30acf5a1de8d12fdf4b5bb1bb8c8cb0b3a0a8fdd8f43792dfa9fbee6831450efe4aa67f50e7fc52
7
+ data.tar.gz: 4bbd9674f45483760079c09476650c8bec90f693bdca2632d59fd8a2865a154c9fd26174110ad7a248d36cd20320e7cffbd73dcab71f07b278c3468a6353c32b
data/README.md CHANGED
@@ -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"
@@ -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
+ ## Normal and Advanced Modes
82
+
83
+ Normal 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 Normal 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 Normal 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
+ ### Normal 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"
@@ -30,13 +30,17 @@ rails generate ruby_native:install
30
30
  <%= native_tabs_tag %>
31
31
  ```
32
32
 
33
- 6. Preview on your phone:
33
+ 6. Start your Rails server and the preview tunnel in separate terminals:
34
+
35
+ ```bash
36
+ bin/rails server
37
+ ```
34
38
 
35
39
  ```bash
36
40
  bundle exec ruby_native preview
37
41
  ```
38
42
 
39
- Scan the QR code with the Ruby Native Preview app from the App Store.
43
+ Scan the QR code with the Ruby Native Preview app from the App Store. Keep both the server and tunnel running.
40
44
 
41
45
  ## Configuration
42
46
 
@@ -100,7 +104,7 @@ Signal elements are hidden `<div>` tags. Place them in the `<body>`, not the `<h
100
104
 
101
105
  ## Preview
102
106
 
103
- `bundle exec ruby_native preview` starts a Cloudflare tunnel and displays a QR code. Requires `cloudflared`:
107
+ `bundle exec ruby_native preview` starts a Cloudflare tunnel and displays a QR code. Your Rails server must be running separately (e.g., `bin/rails server` in another terminal). Requires `cloudflared`:
104
108
 
105
109
  ```bash
106
110
  brew install cloudflare/cloudflare/cloudflared
@@ -139,16 +143,14 @@ The gem auto-mounts at `/native`. No route configuration needed.
139
143
  <%= native_tabs_tag if user_signed_in? %>
140
144
  ```
141
145
 
142
- ### Add a native back button
146
+ ### Add a native back button (Normal Mode only)
143
147
 
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.
148
+ 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
149
 
146
150
  ```erb
147
151
  <%= stylesheet_link_tag :ruby_native %>
148
152
  ```
149
153
 
150
154
  ```erb
151
- <button class="native-back-button" onclick="webkit.messageHandlers.rubyNative.postMessage({action: 'back'})">
152
- Back
153
- </button>
155
+ <%= native_back_button_tag %>
154
156
  ```
@@ -45,6 +45,8 @@ module RubyNative
45
45
 
46
46
  def start_tunnel
47
47
  puts "Starting tunnel to http://localhost:#{@port}..."
48
+ puts "Make sure your Rails server is running on port #{@port} in another terminal."
49
+ puts ""
48
50
 
49
51
  stdin, stdout_err, wait_thread = Open3.popen2e(
50
52
  "cloudflared", "tunnel", "--url", "http://localhost:#{@port}"
@@ -108,6 +110,7 @@ module RubyNative
108
110
  puts url
109
111
  puts ""
110
112
  puts "Scan with the Ruby Native preview app."
113
+ puts "Keep this running and your Rails server on port #{@port} in another terminal."
111
114
  puts "Press Ctrl+C to stop."
112
115
  end
113
116
 
@@ -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,83 @@ 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
+ default_content = tag.svg(
36
+ tag.path(d: "M15.75 19.5L8.25 12l7.5-7.5", stroke_linecap: "round", stroke_linejoin: "round"),
37
+ width: 24, height: 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: 2.5
38
+ )
39
+ tag.button(text || default_content, onclick: "webkit.messageHandlers.rubyNative.postMessage({action: 'back'})", **options)
40
+ end
41
+
42
+ def native_search_tag
43
+ tag.div(data: { controller: "bridge--search" })
44
+ end
45
+
46
+ def native_button_tag(title, url, ios_image: nil, side: :right, **options)
47
+ data = options.delete(:data) || {}
48
+ data[:controller] = "bridge--button"
49
+ data[:bridge_side] = side.to_s
50
+ data[:bridge_ios_image] = ios_image if ios_image
51
+
52
+ link_to title, url, **options, data: data
53
+ end
54
+
55
+ def native_menu_tag(title:, side: :right, &block)
56
+ builder = MenuBuilder.new(self)
57
+ capture(builder, &block)
58
+
59
+ tag.div(style: "display:none", data: {
60
+ controller: "bridge--menu",
61
+ bridge__menu_title_value: title,
62
+ bridge__menu_side_value: side.to_s
63
+ }) { builder.to_html }
64
+ end
65
+
66
+ class MenuBuilder
67
+ def initialize(context)
68
+ @context = context
69
+ @items = []
70
+ end
71
+
72
+ def item(title, url, method: nil, destructive: false, **options)
73
+ data = options.delete(:data) || {}
74
+ data[:bridge__menu_target] = "item"
75
+ data[:turbo_method] = method if method
76
+ data[:destructive] = "" if destructive
77
+
78
+ @items << @context.link_to(title, url, **options, data: data, hidden: true)
79
+ end
80
+
81
+ def to_html
82
+ @context.safe_join(@items)
83
+ end
17
84
  end
18
85
  end
19
86
  end
@@ -1,3 +1,3 @@
1
1
  module RubyNative
2
- VERSION = "0.0.6"
2
+ VERSION = "0.1.1"
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.6
4
+ version: 0.1.1
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