@ruby-native/react 0.10.14 → 0.10.22

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.
Files changed (4) hide show
  1. package/README.md +25 -0
  2. package/index.d.ts +180 -0
  3. package/index.js +166 -1
  4. package/package.json +11 -2
package/README.md CHANGED
@@ -39,18 +39,43 @@ Each component renders a hidden `data-native-*` signal element that the Ruby Nat
39
39
  - `NativeNavbar` - native navigation bar with title and buttons
40
40
  - `NativeButton` - native nav bar button (icon, title, href, or click target)
41
41
  - `NativeMenuItem` - item inside a native menu
42
+ - `NativeShareButton` - native nav bar button that opens the share sheet
43
+ - `NativeShareMenuItem` - menu item that opens the share sheet
42
44
  - `NativeSubmitButton` - native "Save" button that submits a form
45
+ - `NativeFab` - floating action button
43
46
  - `NativeOverscroll` - per-page overscroll colors
44
47
  - `NativeBadge` - set the badge count on a home screen or tab bar icon
48
+ - `NativeBackButton` - visible button that pops the native navigation stack
49
+
50
+ `NativeBackButton` renders a chevron unless you pass children, and forwards extra props to the underlying `<button>`. An `onClick` handler runs first and can call `preventDefault()` to cancel the back:
51
+
52
+ ```jsx
53
+ <NativeBackButton className="mr-2" aria-label="Go back" />
54
+ ```
45
55
 
46
56
  ## Helpers
47
57
 
58
+ - `nativePlatform()` - returns `"ios"`, `"android"`, or `null` on the web. The counterpart of the `native_platform` Rails helper.
48
59
  - `nativeHaptic(feedback = "success", data = {})` - returns props to spread onto a clickable element so tapping it triggers native haptic feedback:
49
60
 
50
61
  ```jsx
51
62
  <button {...nativeHaptic("success")}>Save</button>
52
63
  ```
53
64
 
65
+ ## TypeScript
66
+
67
+ Type declarations ship with the package. There is nothing to install and no `@types` companion package. Importing a component gives you autocomplete and prop checking:
68
+
69
+ ```tsx
70
+ import { NativeNavbar, NativeButton } from "@ruby-native/react"
71
+
72
+ <NativeNavbar title="Books" pullToRefresh={false}>
73
+ <NativeButton position="leading" icon="chevron.left" href="/books" />
74
+ </NativeNavbar>
75
+ ```
76
+
77
+ The `NativeIcons`, `NativeButtonPosition`, and `NativeHapticFeedback` types are exported too.
78
+
54
79
  ## Docs
55
80
 
56
81
  Full guides at [rubynative.com/docs](https://rubynative.com/docs).
package/index.d.ts ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Returns "ios" or "android" inside a Ruby Native app, and null on the web.
3
+ * The counterpart of the `native_platform` Rails helper.
4
+ * @returns {"ios" | "android" | null}
5
+ */
6
+ export function nativePlatform(): "ios" | "android" | null;
7
+ /**
8
+ * @param {object} props
9
+ * @param {boolean} [props.enabled]
10
+ * @returns {import("react").ReactElement | null}
11
+ */
12
+ export function NativeTabs({ enabled }: {
13
+ enabled?: boolean | undefined;
14
+ }): import("react").ReactElement | null;
15
+ /**
16
+ * @returns {import("react").ReactElement}
17
+ */
18
+ export function NativePush(): import("react").ReactElement;
19
+ /**
20
+ * @returns {import("react").ReactElement}
21
+ */
22
+ export function NativeForm(): import("react").ReactElement;
23
+ /**
24
+ * @returns {import("react").ReactElement}
25
+ */
26
+ export function NativeReview(): import("react").ReactElement;
27
+ /**
28
+ * @param {object} props
29
+ * @param {string} [props.title]
30
+ * @param {boolean} [props.pullToRefresh]
31
+ * @param {import("react").ReactNode} [props.children]
32
+ * @returns {import("react").ReactElement}
33
+ */
34
+ export function NativeNavbar({ title, pullToRefresh, children }: {
35
+ title?: string | undefined;
36
+ pullToRefresh?: boolean | undefined;
37
+ children?: import("react").ReactNode;
38
+ }): import("react").ReactElement;
39
+ /**
40
+ * @param {object} props
41
+ * @param {NativeButtonPosition} [props.position]
42
+ * @param {string} [props.icon]
43
+ * @param {NativeIcons} [props.icons]
44
+ * @param {string} [props.title]
45
+ * @param {string} [props.href]
46
+ * @param {string} [props.click]
47
+ * @param {boolean} [props.selected]
48
+ * @param {import("react").ReactNode} [props.children]
49
+ * @returns {import("react").ReactElement}
50
+ */
51
+ export function NativeButton({ position, icon, icons, title, href, click, selected, children }: {
52
+ position?: NativeButtonPosition | undefined;
53
+ icon?: string | undefined;
54
+ icons?: NativeIcons | undefined;
55
+ title?: string | undefined;
56
+ href?: string | undefined;
57
+ click?: string | undefined;
58
+ selected?: boolean | undefined;
59
+ children?: import("react").ReactNode;
60
+ }): import("react").ReactElement;
61
+ /**
62
+ * @param {object} props
63
+ * @param {string} [props.title]
64
+ * @param {string} [props.href]
65
+ * @param {string} [props.click]
66
+ * @param {string} [props.icon]
67
+ * @param {NativeIcons} [props.icons]
68
+ * @param {boolean} [props.selected]
69
+ * @returns {import("react").ReactElement}
70
+ */
71
+ export function NativeMenuItem({ title, href, click, icon, icons, selected }: {
72
+ title?: string | undefined;
73
+ href?: string | undefined;
74
+ click?: string | undefined;
75
+ icon?: string | undefined;
76
+ icons?: NativeIcons | undefined;
77
+ selected?: boolean | undefined;
78
+ }): import("react").ReactElement;
79
+ /**
80
+ * @param {object} props
81
+ * @param {NativeButtonPosition} [props.position]
82
+ * @param {string} [props.title]
83
+ * @param {string} [props.icon]
84
+ * @param {NativeIcons} [props.icons]
85
+ * @param {string} [props.url]
86
+ * @returns {import("react").ReactElement}
87
+ */
88
+ export function NativeShareButton({ position, title, icon, icons, url }: {
89
+ position?: NativeButtonPosition | undefined;
90
+ title?: string | undefined;
91
+ icon?: string | undefined;
92
+ icons?: NativeIcons | undefined;
93
+ url?: string | undefined;
94
+ }): import("react").ReactElement;
95
+ /**
96
+ * @param {object} props
97
+ * @param {string} [props.title]
98
+ * @param {string} [props.url]
99
+ * @param {string} [props.icon]
100
+ * @param {NativeIcons} [props.icons]
101
+ * @param {boolean} [props.selected]
102
+ * @returns {import("react").ReactElement}
103
+ */
104
+ export function NativeShareMenuItem({ title, url, icon, icons, selected }: {
105
+ title?: string | undefined;
106
+ url?: string | undefined;
107
+ icon?: string | undefined;
108
+ icons?: NativeIcons | undefined;
109
+ selected?: boolean | undefined;
110
+ }): import("react").ReactElement;
111
+ /**
112
+ * @param {object} props
113
+ * @param {string} [props.icon]
114
+ * @param {NativeIcons} [props.icons]
115
+ * @param {string} [props.href]
116
+ * @param {string} [props.click]
117
+ * @returns {import("react").ReactElement}
118
+ */
119
+ export function NativeFab({ icon, icons, href, click }: {
120
+ icon?: string | undefined;
121
+ icons?: NativeIcons | undefined;
122
+ href?: string | undefined;
123
+ click?: string | undefined;
124
+ }): import("react").ReactElement;
125
+ /**
126
+ * @param {object} props
127
+ * @param {string} props.top
128
+ * @param {string} [props.bottom]
129
+ * @returns {import("react").ReactElement}
130
+ */
131
+ export function NativeOverscroll({ top, bottom }: {
132
+ top: string;
133
+ bottom?: string | undefined;
134
+ }): import("react").ReactElement;
135
+ /**
136
+ * @param {object} props
137
+ * @param {string} [props.title]
138
+ * @param {string} [props.click]
139
+ * @returns {import("react").ReactElement}
140
+ */
141
+ export function NativeSubmitButton({ title, click }: {
142
+ title?: string | undefined;
143
+ click?: string | undefined;
144
+ }): import("react").ReactElement;
145
+ /**
146
+ * @param {object} props
147
+ * @param {number} [props.count] Sets both `home` and `tab` unless they are given explicitly.
148
+ * @param {number} [props.home]
149
+ * @param {number} [props.tab]
150
+ * @returns {import("react").ReactElement}
151
+ */
152
+ export function NativeBadge({ count, home, tab }: {
153
+ count?: number | undefined;
154
+ home?: number | undefined;
155
+ tab?: number | undefined;
156
+ }): import("react").ReactElement;
157
+ /**
158
+ * A visible button that pops the native navigation stack on tap. Renders a
159
+ * chevron unless you pass children. Extra props are forwarded to the button.
160
+ * An `onClick` handler runs first and may `preventDefault()` to cancel the pop.
161
+ * The counterpart of the `native_back_button_tag` Rails helper.
162
+ * @param {import("react").ButtonHTMLAttributes<HTMLButtonElement>} props
163
+ * @returns {import("react").ReactElement}
164
+ */
165
+ export function NativeBackButton({ children, className, onClick, ...rest }: import("react").ButtonHTMLAttributes<HTMLButtonElement>): import("react").ReactElement;
166
+ /**
167
+ * @param {NativeHapticFeedback} [feedback]
168
+ * @param {Record<string, unknown>} [data]
169
+ * @returns {Record<string, unknown>}
170
+ */
171
+ export function nativeHaptic(feedback?: NativeHapticFeedback, data?: Record<string, unknown>): Record<string, unknown>;
172
+ /**
173
+ * Per-platform icon names. iOS uses SF Symbols, Android uses Material icons.
174
+ */
175
+ export type NativeIcons = {
176
+ ios?: string | undefined;
177
+ android?: string | undefined;
178
+ };
179
+ export type NativeHapticFeedback = "success" | "warning" | "error" | "impact" | "selection";
180
+ export type NativeButtonPosition = "leading" | "trailing";
package/index.js CHANGED
@@ -2,6 +2,21 @@ import { createElement } from "react"
2
2
 
3
3
  import("@inertiajs/react").then(m => { window.__inertiaRouter = m.router }).catch(() => {})
4
4
 
5
+ /**
6
+ * Per-platform icon names. iOS uses SF Symbols, Android uses Material icons.
7
+ * @typedef {object} NativeIcons
8
+ * @property {string} [ios]
9
+ * @property {string} [android]
10
+ */
11
+
12
+ /**
13
+ * @typedef {"success" | "warning" | "error" | "impact" | "selection"} NativeHapticFeedback
14
+ */
15
+
16
+ /**
17
+ * @typedef {"leading" | "trailing"} NativeButtonPosition
18
+ */
19
+
5
20
  function rubyNativePlatform() {
6
21
  if (typeof navigator === "undefined") return null
7
22
  const ua = navigator.userAgent || ""
@@ -10,6 +25,11 @@ function rubyNativePlatform() {
10
25
  return null
11
26
  }
12
27
 
28
+ /**
29
+ * @param {string} [icon]
30
+ * @param {NativeIcons} [icons]
31
+ * @returns {string | undefined}
32
+ */
13
33
  function resolveIcon(icon, icons) {
14
34
  if (icons) {
15
35
  const platform = rubyNativePlatform()
@@ -18,31 +38,89 @@ function resolveIcon(icon, icons) {
18
38
  return icon
19
39
  }
20
40
 
41
+ /**
42
+ * Sends a message over the native bridge. Deliberately not exported: app code
43
+ * talks to the app through signal elements, not the raw message protocol.
44
+ * @param {Record<string, unknown>} message
45
+ * @returns {boolean}
46
+ */
47
+ function postMessage(message) {
48
+ if (typeof window === "undefined") return false
49
+ const bridge = window.RubyNative
50
+ if (!bridge || typeof bridge.postMessage !== "function") return false
51
+ bridge.postMessage(message)
52
+ return true
53
+ }
54
+
55
+ /**
56
+ * Returns "ios" or "android" inside a Ruby Native app, and null on the web.
57
+ * The counterpart of the `native_platform` Rails helper.
58
+ * @returns {"ios" | "android" | null}
59
+ */
60
+ export function nativePlatform() {
61
+ return rubyNativePlatform()
62
+ }
63
+
64
+ /**
65
+ * @param {object} props
66
+ * @param {boolean} [props.enabled]
67
+ * @returns {import("react").ReactElement | null}
68
+ */
21
69
  export function NativeTabs({ enabled = true }) {
22
70
  if (!enabled) return null
23
71
  return createElement("div", { "data-native-tabs": true, hidden: true })
24
72
  }
25
73
 
74
+ /**
75
+ * @returns {import("react").ReactElement}
76
+ */
26
77
  export function NativePush() {
27
78
  return createElement("div", { "data-native-push": true, hidden: true })
28
79
  }
29
80
 
81
+ /**
82
+ * @returns {import("react").ReactElement}
83
+ */
30
84
  export function NativeForm() {
31
85
  return createElement("div", { "data-native-form": true, hidden: true })
32
86
  }
33
87
 
88
+ /**
89
+ * @returns {import("react").ReactElement}
90
+ */
34
91
  export function NativeReview() {
35
92
  return createElement("div", { "data-native-review": true, hidden: true })
36
93
  }
37
94
 
95
+ /**
96
+ * @param {object} props
97
+ * @param {string} [props.title]
98
+ * @param {boolean} [props.pullToRefresh]
99
+ * @param {import("react").ReactNode} [props.children]
100
+ * @returns {import("react").ReactElement}
101
+ */
38
102
  export function NativeNavbar({ title = "", pullToRefresh = true, children }) {
103
+ /** @type {Record<string, any>} */
39
104
  const props = { "data-native-navbar": title, hidden: true }
40
105
  if (!pullToRefresh) props["data-native-pull-to-refresh"] = "false"
41
106
  return createElement("div", props, children)
42
107
  }
43
108
 
109
+ /**
110
+ * @param {object} props
111
+ * @param {NativeButtonPosition} [props.position]
112
+ * @param {string} [props.icon]
113
+ * @param {NativeIcons} [props.icons]
114
+ * @param {string} [props.title]
115
+ * @param {string} [props.href]
116
+ * @param {string} [props.click]
117
+ * @param {boolean} [props.selected]
118
+ * @param {import("react").ReactNode} [props.children]
119
+ * @returns {import("react").ReactElement}
120
+ */
44
121
  export function NativeButton({ position = "trailing", icon, icons, title, href, click, selected, children }) {
45
122
  const resolved = resolveIcon(icon, icons)
123
+ /** @type {Record<string, any>} */
46
124
  const props = { "data-native-button": true }
47
125
  if (resolved) props["data-native-icon"] = resolved
48
126
  if (title) props["data-native-title"] = title
@@ -53,8 +131,19 @@ export function NativeButton({ position = "trailing", icon, icons, title, href,
53
131
  return createElement("div", props, children)
54
132
  }
55
133
 
134
+ /**
135
+ * @param {object} props
136
+ * @param {string} [props.title]
137
+ * @param {string} [props.href]
138
+ * @param {string} [props.click]
139
+ * @param {string} [props.icon]
140
+ * @param {NativeIcons} [props.icons]
141
+ * @param {boolean} [props.selected]
142
+ * @returns {import("react").ReactElement}
143
+ */
56
144
  export function NativeMenuItem({ title, href, click, icon, icons, selected }) {
57
145
  const resolved = resolveIcon(icon, icons)
146
+ /** @type {Record<string, any>} */
58
147
  const props = { "data-native-menu-item": true }
59
148
  if (title) props["data-native-title"] = title
60
149
  if (href) props["data-native-href"] = href
@@ -64,8 +153,18 @@ export function NativeMenuItem({ title, href, click, icon, icons, selected }) {
64
153
  return createElement("div", props)
65
154
  }
66
155
 
156
+ /**
157
+ * @param {object} props
158
+ * @param {NativeButtonPosition} [props.position]
159
+ * @param {string} [props.title]
160
+ * @param {string} [props.icon]
161
+ * @param {NativeIcons} [props.icons]
162
+ * @param {string} [props.url]
163
+ * @returns {import("react").ReactElement}
164
+ */
67
165
  export function NativeShareButton({ position = "trailing", title = "Share", icon = "square.and.arrow.up", icons, url }) {
68
166
  const resolved = resolveIcon(icon, icons)
167
+ /** @type {Record<string, any>} */
69
168
  const props = { "data-native-button": true, "data-native-share": "" }
70
169
  if (title) props["data-native-title"] = title
71
170
  if (resolved) props["data-native-icon"] = resolved
@@ -74,8 +173,18 @@ export function NativeShareButton({ position = "trailing", title = "Share", icon
74
173
  return createElement("div", props)
75
174
  }
76
175
 
176
+ /**
177
+ * @param {object} props
178
+ * @param {string} [props.title]
179
+ * @param {string} [props.url]
180
+ * @param {string} [props.icon]
181
+ * @param {NativeIcons} [props.icons]
182
+ * @param {boolean} [props.selected]
183
+ * @returns {import("react").ReactElement}
184
+ */
77
185
  export function NativeShareMenuItem({ title = "Share", url, icon = "square.and.arrow.up", icons, selected }) {
78
186
  const resolved = resolveIcon(icon, icons)
187
+ /** @type {Record<string, any>} */
79
188
  const props = { "data-native-menu-item": true, "data-native-share": "", "data-native-title": title }
80
189
  if (url) props["data-native-share-url"] = url
81
190
  if (resolved) props["data-native-icon"] = resolved
@@ -83,15 +192,30 @@ export function NativeShareMenuItem({ title = "Share", url, icon = "square.and.a
83
192
  return createElement("div", props)
84
193
  }
85
194
 
195
+ /**
196
+ * @param {object} props
197
+ * @param {string} [props.icon]
198
+ * @param {NativeIcons} [props.icons]
199
+ * @param {string} [props.href]
200
+ * @param {string} [props.click]
201
+ * @returns {import("react").ReactElement}
202
+ */
86
203
  export function NativeFab({ icon, icons, href, click }) {
87
204
  const resolved = resolveIcon(icon, icons)
88
205
  if (!resolved) throw new Error("NativeFab requires `icon` or `icons`")
206
+ /** @type {Record<string, any>} */
89
207
  const props = { "data-native-fab": true, "data-native-icon": resolved, hidden: true }
90
208
  if (href) props["data-native-href"] = href
91
209
  if (click) props["data-native-click"] = click
92
210
  return createElement("div", props)
93
211
  }
94
212
 
213
+ /**
214
+ * @param {object} props
215
+ * @param {string} props.top
216
+ * @param {string} [props.bottom]
217
+ * @returns {import("react").ReactElement}
218
+ */
95
219
  export function NativeOverscroll({ top, bottom }) {
96
220
  return createElement("div", {
97
221
  "data-native-overscroll-top": top,
@@ -100,6 +224,12 @@ export function NativeOverscroll({ top, bottom }) {
100
224
  })
101
225
  }
102
226
 
227
+ /**
228
+ * @param {object} props
229
+ * @param {string} [props.title]
230
+ * @param {string} [props.click]
231
+ * @returns {import("react").ReactElement}
232
+ */
103
233
  export function NativeSubmitButton({ title = "Save", click = "[type='submit']" }) {
104
234
  return createElement("div", {
105
235
  "data-native-submit-button": true,
@@ -109,16 +239,51 @@ export function NativeSubmitButton({ title = "Save", click = "[type='submit']" }
109
239
  })
110
240
  }
111
241
 
242
+ /**
243
+ * @param {object} props
244
+ * @param {number} [props.count] Sets both `home` and `tab` unless they are given explicitly.
245
+ * @param {number} [props.home]
246
+ * @param {number} [props.tab]
247
+ * @returns {import("react").ReactElement}
248
+ */
112
249
  export function NativeBadge({ count, home, tab }) {
113
250
  if (count != null && home == null) home = count
114
251
  if (count != null && tab == null) tab = count
252
+ /** @type {Record<string, any>} */
115
253
  const props = { "data-native-badge": "", hidden: true }
116
254
  if (home != null) props["data-native-badge-home"] = home
117
255
  if (tab != null) props["data-native-badge-tab"] = tab
118
256
  return createElement("div", props)
119
257
  }
120
258
 
259
+ /**
260
+ * A visible button that pops the native navigation stack on tap. Renders a
261
+ * chevron unless you pass children. Extra props are forwarded to the button.
262
+ * An `onClick` handler runs first and may `preventDefault()` to cancel the pop.
263
+ * The counterpart of the `native_back_button_tag` Rails helper.
264
+ * @param {import("react").ButtonHTMLAttributes<HTMLButtonElement>} props
265
+ * @returns {import("react").ReactElement}
266
+ */
267
+ export function NativeBackButton({ children, className, onClick, ...rest }) {
268
+ const classes = ["native-back-button", className].filter(Boolean).join(" ")
269
+ /** @param {import("react").MouseEvent<HTMLButtonElement>} event */
270
+ const handleClick = event => {
271
+ if (onClick) onClick(event)
272
+ if (!event.defaultPrevented) postMessage({ action: "back" })
273
+ }
274
+ const content = children != null ? children : createElement(
275
+ "svg",
276
+ { width: 24, height: 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, "aria-hidden": true },
277
+ createElement("path", { d: "M15.75 19.5L8.25 12l7.5-7.5", strokeLinecap: "round", strokeLinejoin: "round" })
278
+ )
279
+ return createElement("button", { type: "button", ...rest, className: classes, onClick: handleClick }, content)
280
+ }
281
+
282
+ /**
283
+ * @param {NativeHapticFeedback} [feedback]
284
+ * @param {Record<string, unknown>} [data]
285
+ * @returns {Record<string, unknown>}
286
+ */
121
287
  export function nativeHaptic(feedback = "success", data = {}) {
122
288
  return { ...data, "data-native-haptic": feedback }
123
289
  }
124
-
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@ruby-native/react",
3
- "version": "0.10.14",
3
+ "version": "0.10.22",
4
4
  "description": "React components for Ruby Native",
5
5
  "main": "index.js",
6
- "files": ["index.js", "README.md"],
6
+ "types": "index.d.ts",
7
+ "files": ["index.js", "index.d.ts", "README.md"],
8
+ "scripts": {
9
+ "types": "tsc -p .",
10
+ "prepublishOnly": "npm run types"
11
+ },
7
12
  "peerDependencies": {
8
13
  "react": ">=18",
9
14
  "@inertiajs/react": ">=2"
@@ -11,6 +16,10 @@
11
16
  "peerDependenciesMeta": {
12
17
  "@inertiajs/react": { "optional": true }
13
18
  },
19
+ "devDependencies": {
20
+ "@types/react": "^18.3.0",
21
+ "typescript": "^5.9.0"
22
+ },
14
23
  "repository": {
15
24
  "type": "git",
16
25
  "url": "https://github.com/ruby-native/gem.git",