ruby_native 0.0.6 → 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 +4 -4
- data/README.md +78 -6
- data/app/assets/stylesheets/ruby_native.css +11 -0
- data/app/javascript/ruby_native/back.js +3 -0
- data/app/javascript/ruby_native/bridge/button_controller.js +30 -0
- data/app/javascript/ruby_native/bridge/form_controller.js +27 -0
- data/app/javascript/ruby_native/bridge/index.js +14 -0
- data/app/javascript/ruby_native/bridge/menu_controller.js +22 -0
- data/app/javascript/ruby_native/bridge/push_controller.js +10 -0
- data/app/javascript/ruby_native/bridge/search_controller.js +16 -0
- data/app/javascript/ruby_native/bridge/tabs_controller.js +11 -0
- data/config/importmap.rb +2 -0
- data/lib/generators/ruby_native/templates/CLAUDE.md +3 -5
- data/lib/ruby_native/engine.rb +10 -1
- data/lib/ruby_native/helper.rb +66 -3
- data/lib/ruby_native/version.rb +1 -1
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6157407a95aecdb39c9f05de070e3268e6d0910162fcd3b50e54b75f52f4093
|
|
4
|
+
data.tar.gz: 66c9cc2caa92638cbb52f56cae855c16ffd956de4ad1e8db843e990a7197cc90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a510f3343561161004f5a4104a28c79846f224e4d30335fcd33617d18f57c35179aedced255e9876fdb704c6cdd7e5b26727614dbda9bb0e5c95470fbd029a5
|
|
7
|
+
data.tar.gz: 0a177fca9a18e66d623c18a97412e4ccc923ca3f97c750f90c246f6f7eb4c7124bd279a41fa6a91f3c3e627deabf7b83584ad74f5fc731a0703311c5042b8451
|
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
|
+
## 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` -
|
|
87
|
-
- `
|
|
88
|
-
- `
|
|
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
|
-
|
|
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,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,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
|
+
}
|
data/config/importmap.rb
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
152
|
-
Back
|
|
153
|
-
</button>
|
|
151
|
+
<%= native_back_button_tag %>
|
|
154
152
|
```
|
data/lib/ruby_native/engine.rb
CHANGED
|
@@ -10,7 +10,16 @@ module RubyNative
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
initializer "ruby_native.assets" do |app|
|
|
13
|
-
|
|
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
|
data/lib/ruby_native/helper.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
data/lib/ruby_native/version.rb
CHANGED
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
|
|
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
|