@multiplatform.one/forms 8.6.0 → 9.0.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.
- package/README.md +20 -0
- package/dist/cjs/fields/Barcode/BarcodeScanner.native.js +53 -5
- package/dist/cjs/fields/Barcode/BarcodeScanner.native.js.map +1 -1
- package/dist/cjs/fields/Barcode/expoCameraLoader.cjs +54 -0
- package/dist/cjs/fields/Barcode/expoCameraLoader.native.js +58 -0
- package/dist/cjs/fields/Barcode/expoCameraLoader.native.js.map +1 -0
- package/dist/cjs/fields/ColorPicker/expoLinearGradientLoader.cjs +53 -0
- package/dist/cjs/fields/ColorPicker/expoLinearGradientLoader.native.js +57 -0
- package/dist/cjs/fields/ColorPicker/expoLinearGradientLoader.native.js.map +1 -0
- package/dist/cjs/fields/ColorPicker/gradients.native.js +5 -5
- package/dist/cjs/fields/ColorPicker/gradients.native.js.map +1 -1
- package/dist/cjs/fields/ColorPicker/nativeLinearGradient.cjs +93 -0
- package/dist/cjs/fields/ColorPicker/nativeLinearGradient.native.js +101 -0
- package/dist/cjs/fields/ColorPicker/nativeLinearGradient.native.js.map +1 -0
- package/dist/cjs/shared/expoNativeModule.cjs +37 -0
- package/dist/cjs/shared/expoNativeModule.native.js +40 -0
- package/dist/cjs/shared/expoNativeModule.native.js.map +1 -0
- package/dist/cjs/shared/i18n.cjs +1 -1
- package/dist/cjs/shared/i18n.native.js +1 -1
- package/dist/cjs/shared/i18n.native.js.map +1 -1
- package/dist/esm/fields/Barcode/BarcodeScanner.native.js +51 -3
- package/dist/esm/fields/Barcode/BarcodeScanner.native.js.map +1 -1
- package/dist/esm/fields/Barcode/expoCameraLoader.mjs +28 -0
- package/dist/esm/fields/Barcode/expoCameraLoader.mjs.map +1 -0
- package/dist/esm/fields/Barcode/expoCameraLoader.native.js +29 -0
- package/dist/esm/fields/Barcode/expoCameraLoader.native.js.map +1 -0
- package/dist/esm/fields/ColorPicker/expoLinearGradientLoader.mjs +27 -0
- package/dist/esm/fields/ColorPicker/expoLinearGradientLoader.mjs.map +1 -0
- package/dist/esm/fields/ColorPicker/expoLinearGradientLoader.native.js +28 -0
- package/dist/esm/fields/ColorPicker/expoLinearGradientLoader.native.js.map +1 -0
- package/dist/esm/fields/ColorPicker/gradients.native.js +1 -1
- package/dist/esm/fields/ColorPicker/nativeLinearGradient.mjs +65 -0
- package/dist/esm/fields/ColorPicker/nativeLinearGradient.mjs.map +1 -0
- package/dist/esm/fields/ColorPicker/nativeLinearGradient.native.js +70 -0
- package/dist/esm/fields/ColorPicker/nativeLinearGradient.native.js.map +1 -0
- package/dist/esm/shared/expoNativeModule.mjs +12 -0
- package/dist/esm/shared/expoNativeModule.mjs.map +1 -0
- package/dist/esm/shared/expoNativeModule.native.js +12 -0
- package/dist/esm/shared/expoNativeModule.native.js.map +1 -0
- package/dist/esm/shared/i18n.mjs +1 -1
- package/dist/esm/shared/i18n.mjs.map +1 -1
- package/dist/esm/shared/i18n.native.js +1 -1
- package/dist/esm/shared/i18n.native.js.map +1 -1
- package/dist/jsx/fields/Barcode/BarcodeScanner.native.js +53 -5
- package/dist/jsx/fields/Barcode/BarcodeScanner.native.js.map +1 -1
- package/dist/jsx/fields/Barcode/expoCameraLoader.mjs +28 -0
- package/dist/jsx/fields/Barcode/expoCameraLoader.mjs.map +1 -0
- package/dist/jsx/fields/Barcode/expoCameraLoader.native.js +58 -0
- package/dist/jsx/fields/Barcode/expoCameraLoader.native.js.map +1 -0
- package/dist/jsx/fields/ColorPicker/expoLinearGradientLoader.mjs +27 -0
- package/dist/jsx/fields/ColorPicker/expoLinearGradientLoader.mjs.map +1 -0
- package/dist/jsx/fields/ColorPicker/expoLinearGradientLoader.native.js +57 -0
- package/dist/jsx/fields/ColorPicker/expoLinearGradientLoader.native.js.map +1 -0
- package/dist/jsx/fields/ColorPicker/gradients.native.js +5 -5
- package/dist/jsx/fields/ColorPicker/gradients.native.js.map +1 -1
- package/dist/jsx/fields/ColorPicker/nativeLinearGradient.mjs +65 -0
- package/dist/jsx/fields/ColorPicker/nativeLinearGradient.mjs.map +1 -0
- package/dist/jsx/fields/ColorPicker/nativeLinearGradient.native.js +101 -0
- package/dist/jsx/fields/ColorPicker/nativeLinearGradient.native.js.map +1 -0
- package/dist/jsx/shared/expoNativeModule.mjs +12 -0
- package/dist/jsx/shared/expoNativeModule.mjs.map +1 -0
- package/dist/jsx/shared/expoNativeModule.native.js +40 -0
- package/dist/jsx/shared/expoNativeModule.native.js.map +1 -0
- package/dist/jsx/shared/i18n.mjs +1 -1
- package/dist/jsx/shared/i18n.mjs.map +1 -1
- package/dist/jsx/shared/i18n.native.js +1 -1
- package/dist/jsx/shared/i18n.native.js.map +1 -1
- package/metro/index.js +92 -0
- package/metro/phosphor-icon-base.js +129 -0
- package/package.json +24 -20
- package/src/fields/Barcode/BarcodeScanner.native.spec.tsx +59 -0
- package/src/fields/Barcode/BarcodeScanner.native.tsx +49 -4
- package/src/fields/Barcode/expoCameraLoader.spec.ts +45 -0
- package/src/fields/Barcode/expoCameraLoader.ts +46 -0
- package/src/fields/ColorPicker/ColorPicker.spec.tsx +18 -16
- package/src/fields/ColorPicker/expoLinearGradientLoader.ts +44 -0
- package/src/fields/ColorPicker/gradients.native.tsx +1 -1
- package/src/fields/ColorPicker/nativeLinearGradient.spec.tsx +109 -0
- package/src/fields/ColorPicker/nativeLinearGradient.tsx +84 -0
- package/src/metroPreset.spec.ts +132 -0
- package/src/shared/expoNativeModule.ts +20 -0
- package/src/shared/i18n.ts +7 -10
- package/src/shared/i18nPeers.spec.ts +49 -0
- package/types/fields/Barcode/BarcodeScanner.native.d.ts +1 -14
- package/types/fields/Barcode/BarcodeScanner.native.d.ts.map +1 -1
- package/types/fields/Barcode/expoCameraLoader.d.ts +15 -0
- package/types/fields/Barcode/expoCameraLoader.d.ts.map +1 -0
- package/types/fields/ColorPicker/expoLinearGradientLoader.d.ts +14 -0
- package/types/fields/ColorPicker/expoLinearGradientLoader.d.ts.map +1 -0
- package/types/fields/ColorPicker/nativeLinearGradient.d.ts +36 -0
- package/types/fields/ColorPicker/nativeLinearGradient.d.ts.map +1 -0
- package/types/shared/expoNativeModule.d.ts +10 -0
- package/types/shared/expoNativeModule.d.ts.map +1 -0
- package/types/shared/i18n.d.ts +6 -8
- package/types/shared/i18n.d.ts.map +1 -1
package/dist/jsx/shared/i18n.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import * as ReactI18next from "react-i18next";
|
|
|
2
2
|
const useTranslation = (...args) => {
|
|
3
3
|
const impl = ReactI18next.useTranslation;
|
|
4
4
|
if (typeof impl !== "function") {
|
|
5
|
-
throw new Error("[@multiplatform.one/forms] react-i18next is not installed, but a component that translates its built-in copy was rendered. Add react-i18next and i18next to your app's dependencies
|
|
5
|
+
throw new Error("[@multiplatform.one/forms] react-i18next is not installed, but a component that translates its built-in copy was rendered. Add react-i18next and i18next to your app's dependencies.");
|
|
6
6
|
}
|
|
7
7
|
return impl(...args);
|
|
8
8
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["ReactI18next","useTranslation","args","impl","Error"],"sources":["../../../src/shared/i18n.ts"],"sourcesContent":[null],"mappings":"AAAA,YAAYA,YAAA,MAAkB;
|
|
1
|
+
{"version":3,"names":["ReactI18next","useTranslation","args","impl","Error"],"sources":["../../../src/shared/i18n.ts"],"sourcesContent":[null],"mappings":"AAAA,YAAYA,YAAA,MAAkB;AAUvB,MAAMC,cAAA,GAAkBA,CAAA,GAAIC,IAAA,KAAoB;EACrD,MAAMC,IAAA,GAAQH,YAAA,CACXC,cAAA;EACH,IAAI,OAAOE,IAAA,KAAS,YAAY;IAC9B,MAAM,IAAIC,KAAA,CACR,sLAGF;EACF;EACA,OAAOD,IAAA,CAAK,GAAGD,IAAI;AACrB","ignoreList":[]}
|
|
@@ -45,7 +45,7 @@ var useTranslation = function useTranslation2() {
|
|
|
45
45
|
}
|
|
46
46
|
var impl = ReactI18next.useTranslation;
|
|
47
47
|
if (typeof impl !== "function") {
|
|
48
|
-
throw new Error("[@multiplatform.one/forms] react-i18next is not installed, but a component that translates its built-in copy was rendered. Add react-i18next and i18next to your app's dependencies
|
|
48
|
+
throw new Error("[@multiplatform.one/forms] react-i18next is not installed, but a component that translates its built-in copy was rendered. Add react-i18next and i18next to your app's dependencies.");
|
|
49
49
|
}
|
|
50
50
|
return impl(...args);
|
|
51
51
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["__create","Object","create","__defProp","defineProperty","__getOwnPropDesc","getOwnPropertyDescriptor","__getOwnPropNames","getOwnPropertyNames","__getProtoOf","getPrototypeOf","__hasOwnProp","prototype","hasOwnProperty","__export","target","all","name","get","enumerable"],"sources":["../../../src/shared/i18n.ts"],"sourcesContent":[null],"mappings":"AAAA,YAAY;;
|
|
1
|
+
{"version":3,"names":["__create","Object","create","__defProp","defineProperty","__getOwnPropDesc","getOwnPropertyDescriptor","__getOwnPropNames","getOwnPropertyNames","__getProtoOf","getPrototypeOf","__hasOwnProp","prototype","hasOwnProperty","__export","target","all","name","get","enumerable"],"sources":["../../../src/shared/i18n.ts"],"sourcesContent":[null],"mappings":"AAAA,YAAY;;AAUL,IAAAA,QAAM,GAAAC,MAAA,CAAAC,MAAsB;AACjC,IAAAC,SAAM,GAAQF,MAAA,CAAAG,cACX;AACH,IAAAC,gBAAW,GAASJ,MAAA,CAAAK,wBAAY;AAC9B,IAAAC,iBAAU,GAAAN,MAAA,CAAAO,mBAAA;AAAA,IAAAC,YACR,GAAAR,MAAA,CAAAS,cAAA;AAAA,IAGFC,YAAA,GAAAV,MAAA,CAAAW,SAAA,CAAAC,cAAA;AAAA,IACFC,QAAA,GAAAA,CAAAC,MAAA,EAAAC,GAAA;EACA,SAAOC,IAAK,IAAGD,GAAI,EACrBb,SAAA,CAAAY,MAAA,EAAAE,IAAA;IAAAC,GAAA,EAAAF,GAAA,CAAAC,IAAA;IAAAE,UAAA;EAAA","ignoreList":[]}
|
package/metro/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metro preset for an Expo app that hosts MPO (FZ-10629, FZ-10634).
|
|
3
|
+
*
|
|
4
|
+
* const { withMultiplatformOne } = require("@multiplatform.one/forms/metro");
|
|
5
|
+
* module.exports = withMultiplatformOne(getDefaultConfig(__dirname));
|
|
6
|
+
*
|
|
7
|
+
* Plain CommonJS with no build step, because a metro.config.js is loaded by
|
|
8
|
+
* Node before any transform exists. It touches no `sourceExts`, no
|
|
9
|
+
* `blockList` and no `transformer`. Those belong to the app.
|
|
10
|
+
*
|
|
11
|
+
* It does two things.
|
|
12
|
+
*
|
|
13
|
+
* 1. On iOS and Android, `@phosphor-icons/react`'s IconBase resolves to
|
|
14
|
+
* `phosphor-icon-base.js` beside this file. MPO draws its icons with
|
|
15
|
+
* phosphor, whose IconBase renders DOM `<svg>`; the swap draws the same
|
|
16
|
+
* glyph with react-native-svg and gives it the theme's ink. Web keeps
|
|
17
|
+
* phosphor's own IconBase.
|
|
18
|
+
*
|
|
19
|
+
* 2. With `{ router: "expo-router" }`, a web bundle resolves the `one` that
|
|
20
|
+
* `@multiplatform.one/router` imports to the package's Expo Router binding.
|
|
21
|
+
* The router's web entry is written for One, which builds with Vite. A
|
|
22
|
+
* phone bundle needs none of this: there the app selects the binding with
|
|
23
|
+
* `import "@multiplatform.one/router/expo-router"` and Metro never sees
|
|
24
|
+
* `one`.
|
|
25
|
+
*
|
|
26
|
+
* It CHAINS any `resolveRequest` the app already set, and applying it twice
|
|
27
|
+
* is a no-op, marked with a well-known symbol on the config.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
"use strict";
|
|
31
|
+
|
|
32
|
+
const path = require("node:path");
|
|
33
|
+
|
|
34
|
+
const metroMarker = Symbol.for("@multiplatform.one/forms/metro");
|
|
35
|
+
const phosphorIconBase = path.join(__dirname, "phosphor-icon-base.js");
|
|
36
|
+
const routerPackage = /[\\/]@multiplatform\.one[\\/]router[\\/]/;
|
|
37
|
+
const routerBindings = { "expo-router": "@multiplatform.one/router/expo-router" };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {{ originModulePath?: string }} context Metro's resolution context
|
|
41
|
+
* @param {string} moduleName the specifier being resolved
|
|
42
|
+
* @param {string | null} platform
|
|
43
|
+
*/
|
|
44
|
+
function isPhosphorIconBase(context, moduleName, platform) {
|
|
45
|
+
return (
|
|
46
|
+
platform !== "web" &&
|
|
47
|
+
moduleName.includes("IconBase") &&
|
|
48
|
+
Boolean(context.originModulePath?.includes("@phosphor-icons/react"))
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @template {{ resolver?: { resolveRequest?: Function } }} T
|
|
54
|
+
* @param {T} config the app's Metro config
|
|
55
|
+
* @param {{ router?: "expo-router" }} [options]
|
|
56
|
+
* @returns {T}
|
|
57
|
+
*/
|
|
58
|
+
function withMultiplatformOne(config, options = {}) {
|
|
59
|
+
if (config[metroMarker]) return config;
|
|
60
|
+
const binding = options.router === undefined ? undefined : routerBindings[options.router];
|
|
61
|
+
if (options.router !== undefined && !binding) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`[@multiplatform.one/forms/metro] unknown router "${options.router}". Known: ${Object.keys(routerBindings).join(", ")}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const upstream = config.resolver?.resolveRequest;
|
|
67
|
+
const next = (context, moduleName, platform) =>
|
|
68
|
+
upstream
|
|
69
|
+
? upstream(context, moduleName, platform)
|
|
70
|
+
: context.resolveRequest(context, moduleName, platform);
|
|
71
|
+
const resolveRequest = (context, moduleName, platform) => {
|
|
72
|
+
if (isPhosphorIconBase(context, moduleName, platform)) {
|
|
73
|
+
return { type: "sourceFile", filePath: phosphorIconBase };
|
|
74
|
+
}
|
|
75
|
+
if (
|
|
76
|
+
binding &&
|
|
77
|
+
platform === "web" &&
|
|
78
|
+
moduleName === "one" &&
|
|
79
|
+
routerPackage.test(context.originModulePath ?? "")
|
|
80
|
+
) {
|
|
81
|
+
return next(context, binding, platform);
|
|
82
|
+
}
|
|
83
|
+
return next(context, moduleName, platform);
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
...config,
|
|
87
|
+
resolver: { ...config.resolver, resolveRequest },
|
|
88
|
+
[metroMarker]: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { phosphorIconBase, withMultiplatformOne };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native-compatible shim for @phosphor-icons/react IconBase.
|
|
3
|
+
* Replaces web SVG elements (svg, path, rect, circle, line, polyline, polygon)
|
|
4
|
+
* with react-native-svg equivalents (Svg, Path, Rect, Circle, Line, Polyline, Polygon).
|
|
5
|
+
*/
|
|
6
|
+
const React = require("react");
|
|
7
|
+
const RNSvg = require("react-native-svg");
|
|
8
|
+
const { useTheme } = require("@tamagui/web");
|
|
9
|
+
|
|
10
|
+
const SVG_ELEMENT_MAP = {
|
|
11
|
+
svg: RNSvg.Svg,
|
|
12
|
+
path: RNSvg.Path,
|
|
13
|
+
rect: RNSvg.Rect,
|
|
14
|
+
circle: RNSvg.Circle,
|
|
15
|
+
line: RNSvg.Line,
|
|
16
|
+
polyline: RNSvg.Polyline,
|
|
17
|
+
polygon: RNSvg.Polygon,
|
|
18
|
+
ellipse: RNSvg.Ellipse,
|
|
19
|
+
g: RNSvg.G,
|
|
20
|
+
defs: RNSvg.Defs,
|
|
21
|
+
clipPath: RNSvg.ClipPath,
|
|
22
|
+
mask: RNSvg.Mask,
|
|
23
|
+
use: RNSvg.Use,
|
|
24
|
+
text: RNSvg.Text,
|
|
25
|
+
tspan: RNSvg.TSpan,
|
|
26
|
+
linearGradient: RNSvg.LinearGradient,
|
|
27
|
+
radialGradient: RNSvg.RadialGradient,
|
|
28
|
+
stop: RNSvg.Stop,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function transformChildren(children) {
|
|
32
|
+
return React.Children.map(children, (child) => {
|
|
33
|
+
if (!React.isValidElement(child)) return child;
|
|
34
|
+
const type = child.type;
|
|
35
|
+
if (typeof type === "string" && SVG_ELEMENT_MAP[type]) {
|
|
36
|
+
const newProps = { ...child.props };
|
|
37
|
+
if (newProps.children) {
|
|
38
|
+
newProps.children = transformChildren(newProps.children);
|
|
39
|
+
}
|
|
40
|
+
return React.createElement(SVG_ELEMENT_MAP[type], newProps);
|
|
41
|
+
}
|
|
42
|
+
if (child.props && child.props.children) {
|
|
43
|
+
return React.cloneElement(child, {
|
|
44
|
+
...child.props,
|
|
45
|
+
children: transformChildren(child.props.children),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return child;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveToken(theme, value) {
|
|
53
|
+
if (typeof value !== "string" || value.charAt(0) !== "$") return value;
|
|
54
|
+
const entry = theme && theme[value.slice(1)];
|
|
55
|
+
if (!entry) return undefined;
|
|
56
|
+
return typeof entry.get === "function" ? entry.get() : entry.val || undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* FZ-7680. A glyph handed no colour, `currentColor`, or a token the theme
|
|
61
|
+
* lacks would reach react-native-svg as `currentColor`, which is nil there
|
|
62
|
+
* and paints black on the dark scheme. It takes the theme's ink instead.
|
|
63
|
+
*/
|
|
64
|
+
function themeInk(theme) {
|
|
65
|
+
for (const key of ["color", "color12", "color11"]) {
|
|
66
|
+
const value = resolveToken(theme, `$${key}`);
|
|
67
|
+
if (typeof value === "string" && value && value !== "currentColor") return value;
|
|
68
|
+
}
|
|
69
|
+
return "currentColor";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolvePaint(theme, value) {
|
|
73
|
+
const resolved = resolveToken(theme, value);
|
|
74
|
+
return typeof resolved === "string" && resolved && resolved !== "currentColor"
|
|
75
|
+
? resolved
|
|
76
|
+
: themeInk(theme);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const IconBase = React.forwardRef((props, ref) => {
|
|
80
|
+
const {
|
|
81
|
+
alt: _alt,
|
|
82
|
+
color,
|
|
83
|
+
size,
|
|
84
|
+
weight,
|
|
85
|
+
mirrored: _mirrored,
|
|
86
|
+
children: _children,
|
|
87
|
+
weights,
|
|
88
|
+
...rest
|
|
89
|
+
} = props;
|
|
90
|
+
|
|
91
|
+
/*
|
|
92
|
+
* MPO-67. Every caller passes a Tamagui token ("$color11",
|
|
93
|
+
* "$accentBackground"); react-native-svg does not know tokens, so the token
|
|
94
|
+
* string fell through to `currentColor`, which on native resolves to nil and
|
|
95
|
+
* paints the glyph OPAQUE BLACK. On the dark scheme that is a glyph you
|
|
96
|
+
* cannot see — the invisible selected tick in Combobox is exactly this.
|
|
97
|
+
* Web never showed it because there the token is a CSS variable the cascade
|
|
98
|
+
* resolves. Resolve the token here, where the value becomes a paint.
|
|
99
|
+
*/
|
|
100
|
+
const theme = useTheme();
|
|
101
|
+
const resolvedColor = resolvePaint(theme, color);
|
|
102
|
+
const resolvedSize = size || 24;
|
|
103
|
+
const resolvedWeight = weight || "regular";
|
|
104
|
+
|
|
105
|
+
const weightContent = weights ? weights.get(resolvedWeight) : null;
|
|
106
|
+
const svgChildren = transformChildren(weightContent);
|
|
107
|
+
|
|
108
|
+
return React.createElement(
|
|
109
|
+
RNSvg.Svg,
|
|
110
|
+
{
|
|
111
|
+
ref,
|
|
112
|
+
width: resolvedSize,
|
|
113
|
+
height: resolvedSize,
|
|
114
|
+
fill: resolvedColor,
|
|
115
|
+
viewBox: "0 0 256 256",
|
|
116
|
+
// Decorative glyph parity with web: react-native-svg views are touch
|
|
117
|
+
// responders by default and swallow taps meant for a wrapping Pressable
|
|
118
|
+
// (web <svg> inside <button> passes clicks through).
|
|
119
|
+
pointerEvents: "none",
|
|
120
|
+
...rest,
|
|
121
|
+
},
|
|
122
|
+
svgChildren,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
IconBase.displayName = "IconBase";
|
|
127
|
+
|
|
128
|
+
module.exports = IconBase;
|
|
129
|
+
module.exports.default = IconBase;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/forms",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.1",
|
|
4
4
|
"description": "Cross-platform form components with URL state management",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cross-platform",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"source": "src/index.ts",
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
30
|
+
"metro",
|
|
30
31
|
"src",
|
|
31
32
|
"types",
|
|
32
33
|
"!types/.tsbuildinfo"
|
|
@@ -63,17 +64,18 @@
|
|
|
63
64
|
"import": "./dist/esm/nativeTwins.mjs",
|
|
64
65
|
"require": "./dist/cjs/nativeTwins.cjs",
|
|
65
66
|
"default": "./dist/cjs/nativeTwins.cjs"
|
|
66
|
-
}
|
|
67
|
+
},
|
|
68
|
+
"./metro": "./metro/index.js"
|
|
67
69
|
},
|
|
68
70
|
"publishConfig": {
|
|
69
71
|
"access": "public"
|
|
70
72
|
},
|
|
71
73
|
"dependencies": {
|
|
72
74
|
"@floating-ui/react": "^0.27.20",
|
|
73
|
-
"@multiplatform.one/platform": "^
|
|
74
|
-
"@multiplatform.one/router": "^
|
|
75
|
-
"@multiplatform.one/table-primitives": "^
|
|
76
|
-
"@multiplatform.one/theme": "^
|
|
75
|
+
"@multiplatform.one/platform": "^9.0.1",
|
|
76
|
+
"@multiplatform.one/router": "^9.0.1",
|
|
77
|
+
"@multiplatform.one/table-primitives": "^9.0.1",
|
|
78
|
+
"@multiplatform.one/theme": "^9.0.1",
|
|
77
79
|
"@phosphor-icons/react": "^2.1.10",
|
|
78
80
|
"@tamagui/get-font-sized": "2.7.6",
|
|
79
81
|
"@tamagui/get-token": "2.7.6",
|
|
@@ -84,17 +86,15 @@
|
|
|
84
86
|
"@tanstack/form-core": "^1.33.5",
|
|
85
87
|
"@tanstack/react-form": "^1.33.5",
|
|
86
88
|
"@tanstack/react-store": "^0.11.0",
|
|
87
|
-
"expo-camera": "~55.0.18",
|
|
88
|
-
"expo-linear-gradient": "~55.0.13",
|
|
89
89
|
"html5-qrcode": "^2.3.8",
|
|
90
90
|
"perfect-freehand": "^1.2.3",
|
|
91
91
|
"tamagui": "2.7.6"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"@expo/ui": "~55.0.17",
|
|
95
|
-
"@multiplatform.one/config": "
|
|
96
|
-
"@multiplatform.one/storybook": "
|
|
97
|
-
"@multiplatform.one/test-utils": "
|
|
95
|
+
"@multiplatform.one/config": "9.0.1",
|
|
96
|
+
"@multiplatform.one/storybook": "9.0.1",
|
|
97
|
+
"@multiplatform.one/test-utils": "9.0.1",
|
|
98
98
|
"@tamagui/build": "2.7.6",
|
|
99
99
|
"@tamagui/config": "2.7.6",
|
|
100
100
|
"@tamagui/vite-plugin": "2.7.6",
|
|
@@ -103,8 +103,10 @@
|
|
|
103
103
|
"@types/react-dom": "^19.2.3",
|
|
104
104
|
"@vitejs/plugin-react": "^6.1.2",
|
|
105
105
|
"@vitest/coverage-v8": "^4.1.12",
|
|
106
|
+
"expo-camera": "~55.0.18",
|
|
106
107
|
"expo-document-picker": "~55.0.16",
|
|
107
108
|
"expo-image-picker": "~55.0.23",
|
|
109
|
+
"expo-linear-gradient": "~55.0.13",
|
|
108
110
|
"expo-maps": "~55.0.21",
|
|
109
111
|
"happy-dom": "^21.0.3",
|
|
110
112
|
"i18next": "^25.10.10",
|
|
@@ -121,13 +123,15 @@
|
|
|
121
123
|
"@react-native-community/datetimepicker": ">=8.0.1",
|
|
122
124
|
"@react-native-community/slider": ">=4.5.0",
|
|
123
125
|
"@react-native-segmented-control/segmented-control": ">=2.5.0",
|
|
126
|
+
"expo-camera": "*",
|
|
124
127
|
"expo-document-picker": "*",
|
|
125
128
|
"expo-glass-effect": "*",
|
|
126
129
|
"expo-haptics": "55.0.17",
|
|
127
130
|
"expo-image-picker": "*",
|
|
131
|
+
"expo-linear-gradient": "*",
|
|
128
132
|
"expo-maps": "*",
|
|
129
133
|
"expo-modules-core": "*",
|
|
130
|
-
"i18next": "
|
|
134
|
+
"i18next": ">=25.0.0",
|
|
131
135
|
"react": "^19.1.0",
|
|
132
136
|
"react-dom": "^19.1.0",
|
|
133
137
|
"react-i18next": ">=14",
|
|
@@ -148,28 +152,28 @@
|
|
|
148
152
|
"@react-native-segmented-control/segmented-control": {
|
|
149
153
|
"optional": true
|
|
150
154
|
},
|
|
151
|
-
"expo-
|
|
155
|
+
"expo-camera": {
|
|
152
156
|
"optional": true
|
|
153
157
|
},
|
|
154
|
-
"expo-
|
|
158
|
+
"expo-document-picker": {
|
|
155
159
|
"optional": true
|
|
156
160
|
},
|
|
157
|
-
"expo-
|
|
161
|
+
"expo-glass-effect": {
|
|
158
162
|
"optional": true
|
|
159
163
|
},
|
|
160
|
-
"expo-
|
|
164
|
+
"expo-haptics": {
|
|
161
165
|
"optional": true
|
|
162
166
|
},
|
|
163
|
-
"expo-
|
|
167
|
+
"expo-image-picker": {
|
|
164
168
|
"optional": true
|
|
165
169
|
},
|
|
166
|
-
"expo-
|
|
170
|
+
"expo-linear-gradient": {
|
|
167
171
|
"optional": true
|
|
168
172
|
},
|
|
169
|
-
"
|
|
173
|
+
"expo-maps": {
|
|
170
174
|
"optional": true
|
|
171
175
|
},
|
|
172
|
-
"
|
|
176
|
+
"expo-modules-core": {
|
|
173
177
|
"optional": true
|
|
174
178
|
},
|
|
175
179
|
"react-native": {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import type { KnobProps } from "@multiplatform.one/theme";
|
|
3
|
+
import { fireEvent } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { BarcodeScanner, preflightCamera } from "./BarcodeScanner.native";
|
|
6
|
+
|
|
7
|
+
vi.mock("./expoCameraLoader", () => ({
|
|
8
|
+
loadExpoCamera: vi.fn(() => null),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const knobProps = {
|
|
12
|
+
borderRadius: {},
|
|
13
|
+
inputSurface: {},
|
|
14
|
+
body: {},
|
|
15
|
+
sizeToken: "$4",
|
|
16
|
+
} as unknown as KnobProps;
|
|
17
|
+
|
|
18
|
+
// FZ-10637. An app without expo-camera: the Barcode field fails soft.
|
|
19
|
+
describe("BarcodeScanner (native) without expo-camera", () => {
|
|
20
|
+
it("fails the camera pre-flight with a reason, so the scanner is never presented (LC-19)", async () => {
|
|
21
|
+
await expect(preflightCamera()).resolves.toEqual({
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: "Camera unavailable: this app does not include the camera module.",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("renders the reason and a close action when a caller mounts it anyway", () => {
|
|
28
|
+
const onClose = vi.fn();
|
|
29
|
+
const result = renderWithProviders(
|
|
30
|
+
<BarcodeScanner
|
|
31
|
+
active
|
|
32
|
+
onScan={() => {}}
|
|
33
|
+
onError={() => {}}
|
|
34
|
+
onClose={onClose}
|
|
35
|
+
knobProps={knobProps}
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
expect(result.getByTestId("barcode-scanner-unavailable").textContent).toContain(
|
|
39
|
+
"Camera unavailable: this app does not include the camera module.",
|
|
40
|
+
);
|
|
41
|
+
fireEvent.click(result.getByLabelText("Close scanner"));
|
|
42
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("renders nothing while inactive", () => {
|
|
46
|
+
const result = renderWithProviders(
|
|
47
|
+
<BarcodeScanner
|
|
48
|
+
active={false}
|
|
49
|
+
onScan={() => {}}
|
|
50
|
+
onError={() => {}}
|
|
51
|
+
onClose={() => {}}
|
|
52
|
+
knobProps={knobProps}
|
|
53
|
+
/>,
|
|
54
|
+
);
|
|
55
|
+
expect(
|
|
56
|
+
result.container.querySelector("[data-testid='barcode-scanner-unavailable']"),
|
|
57
|
+
).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Camera, CameraView, useCameraPermissions } from "expo-camera";
|
|
2
1
|
import { useEffect, useRef, useState } from "react";
|
|
3
2
|
import { StyleSheet } from "react-native";
|
|
4
3
|
import { Text, View, XStack, YStack } from "tamagui";
|
|
@@ -6,6 +5,7 @@ import { Button, IconButton } from "../../Button";
|
|
|
6
5
|
import { standardActions } from "../../standardActions";
|
|
7
6
|
import { formCommonColors, formInputColors } from "../../shared/colorRamps";
|
|
8
7
|
import { t } from "../../shared/t";
|
|
8
|
+
import { type ExpoCameraModule, loadExpoCamera } from "./expoCameraLoader";
|
|
9
9
|
import type { BarcodeScannerProps, CameraPreflightResult } from "./scannerTypes";
|
|
10
10
|
import { resolveNativeBarcodeTypes, scannerViewportHeight } from "./scannerTypes";
|
|
11
11
|
|
|
@@ -18,8 +18,10 @@ import { resolveNativeBarcodeTypes, scannerViewportHeight } from "./scannerTypes
|
|
|
18
18
|
* stable scanner surface, never as a present-fail-dismiss flash.
|
|
19
19
|
*/
|
|
20
20
|
export async function preflightCamera(): Promise<CameraPreflightResult> {
|
|
21
|
+
const camera = loadExpoCamera();
|
|
22
|
+
if (!camera) return { ok: false, reason: cameraModuleMissing() };
|
|
21
23
|
try {
|
|
22
|
-
const status = await Camera.getCameraPermissionsAsync();
|
|
24
|
+
const status = await camera.Camera.getCameraPermissionsAsync();
|
|
23
25
|
if (!status.granted && !status.canAskAgain) {
|
|
24
26
|
return {
|
|
25
27
|
ok: false,
|
|
@@ -47,14 +49,16 @@ export async function preflightCamera(): Promise<CameraPreflightResult> {
|
|
|
47
49
|
* Note: the iOS simulator has no camera, so live scanning can only be verified
|
|
48
50
|
* on a physical device; in the simulator the camera surface renders empty.
|
|
49
51
|
*/
|
|
50
|
-
|
|
52
|
+
function CameraScanner({
|
|
53
|
+
camera,
|
|
51
54
|
active,
|
|
52
55
|
onScan,
|
|
53
56
|
onError,
|
|
54
57
|
onClose,
|
|
55
58
|
knobProps,
|
|
56
59
|
formats,
|
|
57
|
-
}: BarcodeScannerProps) {
|
|
60
|
+
}: BarcodeScannerProps & { camera: ExpoCameraModule }) {
|
|
61
|
+
const { CameraView, useCameraPermissions } = camera;
|
|
58
62
|
const barcodeTypes = resolveNativeBarcodeTypes(formats);
|
|
59
63
|
const viewportPx = scannerViewportHeight(knobProps.sizeToken);
|
|
60
64
|
const [permission, requestPermission] = useCameraPermissions();
|
|
@@ -135,3 +139,44 @@ export function BarcodeScanner({
|
|
|
135
139
|
</YStack>
|
|
136
140
|
);
|
|
137
141
|
}
|
|
142
|
+
|
|
143
|
+
function cameraModuleMissing(): string {
|
|
144
|
+
return t("Camera unavailable: this app does not include the camera module.");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The scanner an app without `expo-camera` gets: the reason and a way out,
|
|
149
|
+
* never a crash. `preflightCamera()` already refuses to present the scanner
|
|
150
|
+
* in that app, so this is the guard for a caller that mounts it anyway.
|
|
151
|
+
*/
|
|
152
|
+
function CameraUnavailable({
|
|
153
|
+
onClose,
|
|
154
|
+
knobProps,
|
|
155
|
+
}: Pick<BarcodeScannerProps, "onClose" | "knobProps">) {
|
|
156
|
+
return (
|
|
157
|
+
<YStack
|
|
158
|
+
testID="barcode-scanner-unavailable"
|
|
159
|
+
{...knobProps.borderRadius}
|
|
160
|
+
{...knobProps.inputSurface}
|
|
161
|
+
borderColor={formInputColors.border.base}
|
|
162
|
+
padding="$4"
|
|
163
|
+
gap="$3"
|
|
164
|
+
alignItems="center"
|
|
165
|
+
>
|
|
166
|
+
<Text {...knobProps.body} color={formCommonColors.error} textAlign="center">
|
|
167
|
+
{cameraModuleMissing()}
|
|
168
|
+
</Text>
|
|
169
|
+
<IconButton
|
|
170
|
+
icon={standardActions.close.icon}
|
|
171
|
+
onPress={onClose}
|
|
172
|
+
aria-label={t("Close scanner")}
|
|
173
|
+
/>
|
|
174
|
+
</YStack>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function BarcodeScanner(props: BarcodeScannerProps) {
|
|
179
|
+
const camera = loadExpoCamera();
|
|
180
|
+
if (!camera) return props.active ? <CameraUnavailable {...props} /> : null;
|
|
181
|
+
return <CameraScanner camera={camera} {...props} />;
|
|
182
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { resolveExpoCamera } from "./expoCameraLoader";
|
|
5
|
+
|
|
6
|
+
const camera = {
|
|
7
|
+
Camera: { getCameraPermissionsAsync: async () => ({ granted: true, canAskAgain: true }) },
|
|
8
|
+
CameraView: () => null,
|
|
9
|
+
useCameraPermissions: () => [null, async () => ({ granted: true })],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// FZ-10637. expo-camera is an optional peer the host installs at its own SDK
|
|
13
|
+
// version, so the Barcode field has to survive an app that leaves it out.
|
|
14
|
+
describe("resolveExpoCamera", () => {
|
|
15
|
+
it("returns the module when the package and the native module are both present", () => {
|
|
16
|
+
expect(resolveExpoCamera({ hasNativeModule: () => true, camera: () => camera })).toBe(camera);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns null when the app does not install expo-camera", () => {
|
|
20
|
+
expect(resolveExpoCamera({ hasNativeModule: () => true, camera: () => null })).toBeNull();
|
|
21
|
+
const missing = () => {
|
|
22
|
+
throw new Error("Cannot find module 'expo-camera'");
|
|
23
|
+
};
|
|
24
|
+
expect(resolveExpoCamera({ hasNativeModule: () => true, camera: missing })).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns null without loading the package when the binary lacks the native module", () => {
|
|
28
|
+
const load = vi.fn(() => camera);
|
|
29
|
+
expect(resolveExpoCamera({ hasNativeModule: () => false, camera: load })).toBeNull();
|
|
30
|
+
expect(load).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("asks for the ExpoCamera native module", () => {
|
|
34
|
+
const hasNativeModule = vi.fn(() => true);
|
|
35
|
+
resolveExpoCamera({ hasNativeModule, camera: () => camera });
|
|
36
|
+
expect(hasNativeModule).toHaveBeenCalledWith("ExpoCamera");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("loads expo-camera from inside a try block, never a static import, so Metro bundles without it", () => {
|
|
40
|
+
const loader = readFileSync(join(__dirname, "expoCameraLoader.ts"), "utf8");
|
|
41
|
+
expect(loader).toMatch(/try \{\s*return require\("expo-camera"\);/);
|
|
42
|
+
const scanner = readFileSync(join(__dirname, "BarcodeScanner.native.tsx"), "utf8");
|
|
43
|
+
expect(scanner).not.toMatch(/from "expo-camera"/);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { hasExpoNativeModule } from "../../shared/expoNativeModule";
|
|
2
|
+
|
|
3
|
+
export type ExpoCameraModule = typeof import("expo-camera");
|
|
4
|
+
|
|
5
|
+
export interface ExpoCameraSources {
|
|
6
|
+
hasNativeModule(name: string): boolean;
|
|
7
|
+
camera(): unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const appSources: ExpoCameraSources = {
|
|
11
|
+
hasNativeModule: hasExpoNativeModule,
|
|
12
|
+
camera: () => {
|
|
13
|
+
try {
|
|
14
|
+
return require("expo-camera");
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `expo-camera` is an optional peer: the host app installs it at the version
|
|
23
|
+
* its Expo SDK ships, and an app that never scans a barcode leaves it out.
|
|
24
|
+
* Returns null when the package is not installed or the binary lacks the
|
|
25
|
+
* native module, and the Barcode field then reports the camera as unavailable
|
|
26
|
+
* instead of presenting the scanner (FZ-10637).
|
|
27
|
+
*/
|
|
28
|
+
export function resolveExpoCamera(
|
|
29
|
+
sources: ExpoCameraSources = appSources,
|
|
30
|
+
): ExpoCameraModule | null {
|
|
31
|
+
try {
|
|
32
|
+
if (!sources.hasNativeModule("ExpoCamera")) return null;
|
|
33
|
+
const camera = sources.camera() as Partial<ExpoCameraModule> | null | undefined;
|
|
34
|
+
if (!camera?.CameraView || !camera.Camera || !camera.useCameraPermissions) return null;
|
|
35
|
+
return camera as ExpoCameraModule;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let cached: ExpoCameraModule | null | undefined;
|
|
42
|
+
|
|
43
|
+
export function loadExpoCamera(): ExpoCameraModule | null {
|
|
44
|
+
if (cached === undefined) cached = resolveExpoCamera();
|
|
45
|
+
return cached;
|
|
46
|
+
}
|
|
@@ -28,22 +28,24 @@ vi.mock("@tamagui/lucide-icons-2", () => ({
|
|
|
28
28
|
Pipette: () => null,
|
|
29
29
|
}));
|
|
30
30
|
|
|
31
|
-
vi.mock("
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
vi.mock("./expoLinearGradientLoader", () => ({
|
|
32
|
+
loadExpoLinearGradient: () => ({
|
|
33
|
+
LinearGradient: (props: {
|
|
34
|
+
colors?: string[];
|
|
35
|
+
locations?: number[];
|
|
36
|
+
start?: { x: number; y: number };
|
|
37
|
+
end?: { x: number; y: number };
|
|
38
|
+
style?: unknown;
|
|
39
|
+
testID?: string;
|
|
40
|
+
}) =>
|
|
41
|
+
createElement("div", {
|
|
42
|
+
"data-testid": props.testID ?? "expo-linear-gradient",
|
|
43
|
+
"data-colors": JSON.stringify(props.colors ?? []),
|
|
44
|
+
"data-locations": JSON.stringify(props.locations ?? null),
|
|
45
|
+
"data-start": JSON.stringify(props.start ?? null),
|
|
46
|
+
"data-end": JSON.stringify(props.end ?? null),
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
47
49
|
}));
|
|
48
50
|
|
|
49
51
|
async function loadColorPicker() {
|