@ruby-native/react 0.14.2 → 0.15.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.
Files changed (3) hide show
  1. package/index.d.ts +11 -0
  2. package/index.js +53 -5
  3. package/package.json +14 -5
package/index.d.ts CHANGED
@@ -216,6 +216,17 @@ export function NativeOverscroll({ top, bottom }: {
216
216
  top: string;
217
217
  bottom?: string | undefined;
218
218
  }): import("react").ReactElement;
219
+ /**
220
+ * Hides the iOS keyboard toolbar (the previous/next/Done bar) on this page.
221
+ * Page-level: the toolbar belongs to the web view, not to individual fields.
222
+ *
223
+ * @param {object} props
224
+ * @param {boolean} [props.toolbar]
225
+ * @returns {import("react").ReactElement | null}
226
+ */
227
+ export function NativeKeyboard({ toolbar }: {
228
+ toolbar?: boolean | undefined;
229
+ }): import("react").ReactElement | null;
219
230
  /**
220
231
  * @param {object} props
221
232
  * @param {string} [props.title]
package/index.js CHANGED
@@ -33,17 +33,46 @@ function rubyNativePlatform() {
33
33
  return null
34
34
  }
35
35
 
36
+ const ACTIONS = ["push", "replace"]
37
+ const PRESENTATION_INTENTS = ["root"]
38
+ const TOAST_APPEARANCES = ["inverted", "system"]
39
+
40
+ /**
41
+ * @param {string} value
42
+ * @returns {string}
43
+ */
44
+ function validateAction(value) {
45
+ if (!ACTIONS.includes(value)) {
46
+ throw new Error(`action must be "push" or "replace", got ${JSON.stringify(value)}`)
47
+ }
48
+ return value
49
+ }
50
+
36
51
  /**
37
52
  * @param {string} [icon]
38
53
  * @param {NativeIcons} [icons]
39
54
  * @returns {string | undefined}
40
55
  */
41
56
  function resolveIcon(icon, icons) {
57
+ if (icons != null && (typeof icons !== "object" || Array.isArray(icons))) {
58
+ throw new Error(`icons must be an object like { ios: "square.and.arrow.up", android: "share" }`)
59
+ }
42
60
  if (icons) {
43
61
  const platform = rubyNativePlatform()
44
62
  if (platform && icons[platform]) return icons[platform]
45
63
  }
46
- return icon
64
+ return icon || fallbackIcon(icons)
65
+ }
66
+
67
+ // A web render has no platform, so `icons` alone must still resolve (ios then
68
+ // android, like the Rails helper) or NativeFab would throw on pages that are fine in the app.
69
+ /**
70
+ * @param {NativeIcons} [icons]
71
+ * @returns {string | undefined}
72
+ */
73
+ function fallbackIcon(icons) {
74
+ if (!icons) return undefined
75
+ return icons.ios || icons.android
47
76
  }
48
77
 
49
78
  /**
@@ -119,6 +148,9 @@ export function NativeForm() {
119
148
  * @returns {import("react").ReactElement}
120
149
  */
121
150
  export function NativePresentation({ intent = "root" } = {}) {
151
+ if (!PRESENTATION_INTENTS.includes(intent)) {
152
+ throw new Error(`intent must be "root", got ${JSON.stringify(intent)}`)
153
+ }
122
154
  return createElement("div", { "data-native-presentation": intent, hidden: true })
123
155
  }
124
156
 
@@ -143,6 +175,9 @@ export function NativeReview() {
143
175
  */
144
176
  export function NativeToast({ message, icon, icons, duration = 4, appearance = "inverted" }) {
145
177
  if (!message) return null
178
+ if (!TOAST_APPEARANCES.includes(appearance)) {
179
+ throw new Error(`appearance must be "inverted" or "system", got ${JSON.stringify(appearance)}`)
180
+ }
146
181
  /** @type {Record<string, any>} */
147
182
  const props = {
148
183
  "data-native-toast": "",
@@ -223,7 +258,7 @@ export function NativeMenuItem({ title, href, click, icon, icons, selected, dest
223
258
  if (resolved) props["data-native-icon"] = resolved
224
259
  if (selected) props["data-native-selected"] = ""
225
260
  if (destructive) props["data-native-destructive"] = ""
226
- if (action) props["data-native-action"] = action
261
+ if (action) props["data-native-action"] = validateAction(action)
227
262
  return createElement("div", props)
228
263
  }
229
264
 
@@ -273,7 +308,7 @@ export function NativeSegment({ title, href, click, selected }) {
273
308
  * @param {string} [props.url]
274
309
  * @returns {import("react").ReactElement}
275
310
  */
276
- export function NativeShareButton({ position = "trailing", title = "Share", icon = "square.and.arrow.up", icons, url }) {
311
+ export function NativeShareButton({ position = "trailing", title = "Share", icon = "square.and.arrow.up", icons = { android: "share" }, url }) {
277
312
  const resolved = resolveIcon(icon, icons)
278
313
  /** @type {Record<string, any>} */
279
314
  const props = { "data-native-button": true, "data-native-share": "" }
@@ -293,7 +328,7 @@ export function NativeShareButton({ position = "trailing", title = "Share", icon
293
328
  * @param {boolean} [props.selected]
294
329
  * @returns {import("react").ReactElement}
295
330
  */
296
- export function NativeShareMenuItem({ title = "Share", url, icon = "square.and.arrow.up", icons, selected }) {
331
+ export function NativeShareMenuItem({ title = "Share", url, icon = "square.and.arrow.up", icons = { android: "share" }, selected }) {
297
332
  const resolved = resolveIcon(icon, icons)
298
333
  /** @type {Record<string, any>} */
299
334
  const props = { "data-native-menu-item": true, "data-native-share": "", "data-native-title": title }
@@ -337,6 +372,19 @@ export function NativeOverscroll({ top, bottom }) {
337
372
  })
338
373
  }
339
374
 
375
+ /**
376
+ * Hides the iOS keyboard toolbar (the previous/next/Done bar) on this page.
377
+ * Page-level: the toolbar belongs to the web view, not to individual fields.
378
+ *
379
+ * @param {object} props
380
+ * @param {boolean} [props.toolbar]
381
+ * @returns {import("react").ReactElement | null}
382
+ */
383
+ export function NativeKeyboard({ toolbar = false }) {
384
+ if (toolbar) return null
385
+ return createElement("div", { "data-native-keyboard-toolbar": "false", hidden: true })
386
+ }
387
+
340
388
  /**
341
389
  * @param {object} props
342
390
  * @param {string} [props.title]
@@ -398,5 +446,5 @@ export function NativeBackButton({ children, className, onClick, ...rest }) {
398
446
  * @returns {Record<string, unknown>}
399
447
  */
400
448
  export function nativeHaptic(feedback = "success", data = {}) {
401
- return { ...data, "data-native-haptic": feedback }
449
+ return { ...data, "data-native-haptic": feedback || "success" }
402
450
  }
package/package.json CHANGED
@@ -1,23 +1,32 @@
1
1
  {
2
2
  "name": "@ruby-native/react",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "description": "React components for Ruby Native",
5
+ "type": "module",
5
6
  "main": "index.js",
6
7
  "types": "index.d.ts",
7
- "files": ["index.js", "index.d.ts", "README.md"],
8
+ "files": [
9
+ "index.js",
10
+ "index.d.ts",
11
+ "README.md"
12
+ ],
8
13
  "scripts": {
14
+ "test": "node --test test/*.test.mjs",
9
15
  "types": "tsc -p .",
10
16
  "prepublishOnly": "npm run types"
11
17
  },
12
18
  "peerDependencies": {
13
- "react": ">=18",
14
- "@inertiajs/react": ">=2"
19
+ "@inertiajs/react": ">=2",
20
+ "react": ">=18"
15
21
  },
16
22
  "peerDependenciesMeta": {
17
- "@inertiajs/react": { "optional": true }
23
+ "@inertiajs/react": {
24
+ "optional": true
25
+ }
18
26
  },
19
27
  "devDependencies": {
20
28
  "@types/react": "^18.3.0",
29
+ "react-dom": "^19.2.8",
21
30
  "typescript": "^5.9.0"
22
31
  },
23
32
  "repository": {