melta-app 0.2.1 → 0.2.3

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 CHANGED
@@ -193,11 +193,21 @@ npx expo install react-native-safe-area-context
193
193
  // アプリの entry(Screen の初回 render より前)で一度だけ
194
194
  import { enableSafeAreaContext } from "melta-app/safe-area";
195
195
  enableSafeAreaContext();
196
+
197
+ // ボトムタブバー等が bottom inset を自前処理するアプリは edge を絞る
198
+ // (全 edge のまま使うとタブ内 Screen の bottom が二重余白になる)
199
+ enableSafeAreaContext({ edges: ["top"] });
196
200
  ```
197
201
 
198
- ⚠️ react-native-safe-area-context の SafeAreaView は祖先に `SafeAreaProvider` が必要
199
- (React Navigation / Expo Router を使っていれば設置済みのことが多い。無い場合は root を
200
- `<SafeAreaProvider>` で包むこと 無いと警告は消えるが inset が効かない)。
202
+ ⚠️ 前提と契約:
203
+
204
+ - 祖先に `SafeAreaProvider` が**必須**(無いと `useSafeAreaInsets` **throw する**。
205
+ React Navigation / Expo Router を使っていれば設置済みのことが多い)。初回 render から
206
+ 正しい inset を使うには Provider に `initialMetrics` を渡す
207
+ - adapter は `useSafeAreaInsets()` を render 中に同期参照して View padding に加算する方式
208
+ (native SafeAreaView の初回フレーム inset 未適用によるフラッシュを避けるため)
209
+ - safe-area と合成する padding は**数値のみ**サポート(`"5%"` 等の非数値は基底として扱えず、
210
+ 対象 edge は inset 値に置き換わる)。RTL / `paddingStart`・`paddingEnd` は対応済み
201
211
 
202
212
  ## ステータス
203
213
 
@@ -15,11 +15,38 @@
15
15
  */
16
16
 
17
17
  import { useMemo, useState } from "react";
18
- import { TextInput, View, Text as RNText } from "react-native";
18
+ import { Platform, TextInput, View, Text as RNText } from "react-native";
19
19
  import { useTheme } from "../theme/index.js";
20
20
  import { CONTRACTS } from "../contracts/contract-types.js";
21
21
  import { resolveTextFieldStyle, resolveTextFieldFocusStyle } from "./textfield.styles.js";
22
+
23
+ /**
24
+ * Android の EditText は固定 height と組み合わさると既定の縦 padding で文字が
25
+ * 上寄り・下端切れになるため、縦 padding を殺して縦センターに揃える。
26
+ * textAlignVertical は Android 専用、paddingVertical: 0 は iOS の見た目を変えない
27
+ * (iOS の単一行入力は元々縦センター)。
28
+ * さらに Android では Noto CJK のフォントメトリクス由来で文字インクが
29
+ * 1dp 強下寄りになる(Pixel 実機ピクセル実測 +2.7px/density2.5。
30
+ * includeFontPadding: false は gravity center 下では無効なことも実測済み)ため、
31
+ * paddingBottom の光学ナッジで打ち消す(gravity center は残り高さの中央に置くので
32
+ * paddingBottom p で p/2 上がる)。
33
+ * recipe(styleRefs)の写像ではなく RN 実装の補正なので resolver には置かない。
34
+ */
35
+ /* eslint-disable melta/no-raw-spacing -- spacing の選択ではなく EditText 既定 padding の打ち消しと CJK 光学ナッジ(実測由来の固定値) */
36
+ /** OS 毎の補正値を返す純関数(jest は Platform=ios 固定のため、Android 分岐はこれを直接検証する)。 */
22
37
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
38
+ export function resolveInputVerticalFix(os) {
39
+ return {
40
+ paddingVertical: 0,
41
+ ...(os === "android" ? {
42
+ paddingBottom: 2
43
+ } : null),
44
+ textAlignVertical: "center"
45
+ };
46
+ }
47
+ /* eslint-enable melta/no-raw-spacing */
48
+
49
+ const INPUT_VERTICAL_FIX = resolveInputVerticalFix(Platform.OS);
23
50
  export function TextField({
24
51
  label,
25
52
  value,
@@ -68,7 +95,7 @@ export function TextField({
68
95
  if (!disabled) setFocused(true);
69
96
  },
70
97
  onBlur: () => setFocused(false),
71
- style: [resolved.input, focused && !disabled ? focusStyle : null]
98
+ style: [resolved.input, INPUT_VERTICAL_FIX, focused && !disabled ? focusStyle : null]
72
99
  }), helperText != null && !showError && /*#__PURE__*/_jsx(RNText, {
73
100
  style: resolved.helperText,
74
101
  children: helperText
@@ -1 +1 @@
1
- {"version":3,"names":["useMemo","useState","TextInput","View","Text","RNText","useTheme","CONTRACTS","resolveTextFieldStyle","resolveTextFieldFocusStyle","jsx","_jsx","jsxs","_jsxs","TextField","label","value","onChangeText","placeholder","variant","disabled","size","helperText","errorText","style","testID","theme","mode","colors","focused","setFocused","effectiveVariant","resolved","focusStyle","showError","children","placeholderTextColor","editable","accessibilityLabel","accessibilityState","onFocus","onBlur","input","__contract","textfield"],"sourceRoot":"../../../src","sources":["components/TextField.tsx"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAASA,OAAO,EAAEC,QAAQ,QAAQ,OAAO;AACzC,SACEC,SAAS,EACTC,IAAI,EACJC,IAAI,IAAIC,MAAM,QAGT,cAAc;AACrB,SAASC,QAAQ,QAAQ,mBAAU;AACnC,SAASC,SAAS,QAAQ,gCAA6B;AACvD,SACEC,qBAAqB,EACrBC,0BAA0B,QAErB,uBAAoB;AAAC,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAqB5B,OAAO,SAASC,SAASA,CAAC;EACxBC,KAAK;EACLC,KAAK;EACLC,YAAY;EACZC,WAAW;EACXC,OAAO,GAAG,SAAS;EACnBC,QAAQ,GAAG,KAAK;EAChBC,IAAI,GAAG,QAAQ;EACfC,UAAU;EACVC,SAAS;EACTC,KAAK;EACLC;AACc,CAAC,EAAE;EACjB,MAAM;IAAEC,KAAK;IAAEC,IAAI;IAAEC;EAAO,CAAC,GAAGtB,QAAQ,CAAC,CAAC;EAC1C,MAAM,CAACuB,OAAO,EAAEC,UAAU,CAAC,GAAG7B,QAAQ,CAAC,KAAK,CAAC;;EAE7C;EACA,MAAM8B,gBAAgB,GAAGX,QAAQ,GAAG,UAAU,GAAGD,OAAO;EACxD,MAAMa,QAAQ,GAAGhC,OAAO,CACtB,MAAMQ,qBAAqB,CAACkB,KAAK,EAAEC,IAAI,EAAE;IAAER,OAAO,EAAEY,gBAAgB;IAAEV;EAAK,CAAC,CAAC,EAC7E,CAACK,KAAK,EAAEC,IAAI,EAAEI,gBAAgB,EAAEV,IAAI,CACtC,CAAC;EACD,MAAMY,UAAU,GAAGjC,OAAO,CAAC,MAAMS,0BAA0B,CAACiB,KAAK,CAAC,EAAE,CAACA,KAAK,CAAC,CAAC;EAE5E,MAAMQ,SAAS,GAAGH,gBAAgB,KAAK,OAAO,IAAIR,SAAS,IAAI,IAAI;EAEnE,oBACEV,KAAA,CAACV,IAAI;IAACqB,KAAK,EAAEA,KAAM;IAACC,MAAM,EAAEA,MAAO;IAAAU,QAAA,gBACjCxB,IAAA,CAACN,MAAM;MAACmB,KAAK,EAAEQ,QAAQ,CAACjB,KAAM;MAAAoB,QAAA,EAAEpB;IAAK,CAAS,CAAC,eAC/CJ,IAAA,CAACT,SAAS;MACRc,KAAK,EAAEA,KAAM;MACbC,YAAY,EAAEA,YAAa;MAC3BC,WAAW,EAAEA,WAAY;MACzBkB,oBAAoB,EAAER,MAAM,CAAC,YAAY,CAAE;MAC3CS,QAAQ,EAAE,CAACjB,QAAS;MACpBkB,kBAAkB,EAAEvB,KAAM;MAC1BwB,kBAAkB,EAAE;QAAEnB;MAAS,CAAE;MACjCoB,OAAO,EAAEA,CAAA,KAAM;QACb,IAAI,CAACpB,QAAQ,EAAEU,UAAU,CAAC,IAAI,CAAC;MACjC,CAAE;MACFW,MAAM,EAAEA,CAAA,KAAMX,UAAU,CAAC,KAAK,CAAE;MAChCN,KAAK,EAAE,CAACQ,QAAQ,CAACU,KAAK,EAAEb,OAAO,IAAI,CAACT,QAAQ,GAAGa,UAAU,GAAG,IAAI;IAAE,CACnE,CAAC,EACDX,UAAU,IAAI,IAAI,IAAI,CAACY,SAAS,iBAC/BvB,IAAA,CAACN,MAAM;MAACmB,KAAK,EAAEQ,QAAQ,CAACV,UAAW;MAAAa,QAAA,EAAEb;IAAU,CAAS,CACzD,EACAY,SAAS,iBAAIvB,IAAA,CAACN,MAAM;MAACmB,KAAK,EAAEQ,QAAQ,CAACT,SAAU;MAAAY,QAAA,EAAEZ;IAAS,CAAS,CAAC;EAAA,CACjE,CAAC;AAEX;;AAEA;AACAT,SAAS,CAAC6B,UAAU,GAAGpC,SAAS,CAACqC,SAAS","ignoreList":[]}
1
+ {"version":3,"names":["useMemo","useState","Platform","TextInput","View","Text","RNText","useTheme","CONTRACTS","resolveTextFieldStyle","resolveTextFieldFocusStyle","jsx","_jsx","jsxs","_jsxs","resolveInputVerticalFix","os","paddingVertical","paddingBottom","textAlignVertical","INPUT_VERTICAL_FIX","OS","TextField","label","value","onChangeText","placeholder","variant","disabled","size","helperText","errorText","style","testID","theme","mode","colors","focused","setFocused","effectiveVariant","resolved","focusStyle","showError","children","placeholderTextColor","editable","accessibilityLabel","accessibilityState","onFocus","onBlur","input","__contract","textfield"],"sourceRoot":"../../../src","sources":["components/TextField.tsx"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAASA,OAAO,EAAEC,QAAQ,QAAQ,OAAO;AACzC,SACEC,QAAQ,EACRC,SAAS,EACTC,IAAI,EACJC,IAAI,IAAIC,MAAM,QAIT,cAAc;AACrB,SAASC,QAAQ,QAAQ,mBAAU;AACnC,SAASC,SAAS,QAAQ,gCAA6B;AACvD,SACEC,qBAAqB,EACrBC,0BAA0B,QAErB,uBAAoB;;AAE3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AACA,OAAO,SAASC,uBAAuBA,CAACC,EAAsB,EAAa;EACzE,OAAO;IACLC,eAAe,EAAE,CAAC;IAClB,IAAID,EAAE,KAAK,SAAS,GAAG;MAAEE,aAAa,EAAE;IAAE,CAAC,GAAG,IAAI,CAAC;IACnDC,iBAAiB,EAAE;EACrB,CAAC;AACH;AACA;;AAEA,MAAMC,kBAAkB,GAAGL,uBAAuB,CAACb,QAAQ,CAACmB,EAAE,CAAC;AAqB/D,OAAO,SAASC,SAASA,CAAC;EACxBC,KAAK;EACLC,KAAK;EACLC,YAAY;EACZC,WAAW;EACXC,OAAO,GAAG,SAAS;EACnBC,QAAQ,GAAG,KAAK;EAChBC,IAAI,GAAG,QAAQ;EACfC,UAAU;EACVC,SAAS;EACTC,KAAK;EACLC;AACc,CAAC,EAAE;EACjB,MAAM;IAAEC,KAAK;IAAEC,IAAI;IAAEC;EAAO,CAAC,GAAG7B,QAAQ,CAAC,CAAC;EAC1C,MAAM,CAAC8B,OAAO,EAAEC,UAAU,CAAC,GAAGrC,QAAQ,CAAC,KAAK,CAAC;;EAE7C;EACA,MAAMsC,gBAAgB,GAAGX,QAAQ,GAAG,UAAU,GAAGD,OAAO;EACxD,MAAMa,QAAQ,GAAGxC,OAAO,CACtB,MAAMS,qBAAqB,CAACyB,KAAK,EAAEC,IAAI,EAAE;IAAER,OAAO,EAAEY,gBAAgB;IAAEV;EAAK,CAAC,CAAC,EAC7E,CAACK,KAAK,EAAEC,IAAI,EAAEI,gBAAgB,EAAEV,IAAI,CACtC,CAAC;EACD,MAAMY,UAAU,GAAGzC,OAAO,CAAC,MAAMU,0BAA0B,CAACwB,KAAK,CAAC,EAAE,CAACA,KAAK,CAAC,CAAC;EAE5E,MAAMQ,SAAS,GAAGH,gBAAgB,KAAK,OAAO,IAAIR,SAAS,IAAI,IAAI;EAEnE,oBACEjB,KAAA,CAACV,IAAI;IAAC4B,KAAK,EAAEA,KAAM;IAACC,MAAM,EAAEA,MAAO;IAAAU,QAAA,gBACjC/B,IAAA,CAACN,MAAM;MAAC0B,KAAK,EAAEQ,QAAQ,CAACjB,KAAM;MAAAoB,QAAA,EAAEpB;IAAK,CAAS,CAAC,eAC/CX,IAAA,CAACT,SAAS;MACRqB,KAAK,EAAEA,KAAM;MACbC,YAAY,EAAEA,YAAa;MAC3BC,WAAW,EAAEA,WAAY;MACzBkB,oBAAoB,EAAER,MAAM,CAAC,YAAY,CAAE;MAC3CS,QAAQ,EAAE,CAACjB,QAAS;MACpBkB,kBAAkB,EAAEvB,KAAM;MAC1BwB,kBAAkB,EAAE;QAAEnB;MAAS,CAAE;MACjCoB,OAAO,EAAEA,CAAA,KAAM;QACb,IAAI,CAACpB,QAAQ,EAAEU,UAAU,CAAC,IAAI,CAAC;MACjC,CAAE;MACFW,MAAM,EAAEA,CAAA,KAAMX,UAAU,CAAC,KAAK,CAAE;MAChCN,KAAK,EAAE,CAACQ,QAAQ,CAACU,KAAK,EAAE9B,kBAAkB,EAAEiB,OAAO,IAAI,CAACT,QAAQ,GAAGa,UAAU,GAAG,IAAI;IAAE,CACvF,CAAC,EACDX,UAAU,IAAI,IAAI,IAAI,CAACY,SAAS,iBAC/B9B,IAAA,CAACN,MAAM;MAAC0B,KAAK,EAAEQ,QAAQ,CAACV,UAAW;MAAAa,QAAA,EAAEb;IAAU,CAAS,CACzD,EACAY,SAAS,iBAAI9B,IAAA,CAACN,MAAM;MAAC0B,KAAK,EAAEQ,QAAQ,CAACT,SAAU;MAAAY,QAAA,EAAEZ;IAAS,CAAS,CAAC;EAAA,CACjE,CAAC;AAEX;;AAEA;AACAT,SAAS,CAAC6B,UAAU,GAAG3C,SAAS,CAAC4C,SAAS","ignoreList":[]}
@@ -11,8 +11,22 @@
11
11
  * - 色・寸法の決定は pure resolver(textfield.styles.ts)に分離。
12
12
  * recipes/app/textfield.recipe.json との機械照合は scripts/lib/textfield-conformance.test.ts が行う。
13
13
  */
14
- import { type StyleProp, type ViewStyle } from "react-native";
14
+ import { Platform, type StyleProp, type TextStyle, type ViewStyle } from "react-native";
15
15
  import { type TextFieldSize } from "./textfield.styles";
16
+ /**
17
+ * Android の EditText は固定 height と組み合わさると既定の縦 padding で文字が
18
+ * 上寄り・下端切れになるため、縦 padding を殺して縦センターに揃える。
19
+ * textAlignVertical は Android 専用、paddingVertical: 0 は iOS の見た目を変えない
20
+ * (iOS の単一行入力は元々縦センター)。
21
+ * さらに Android では Noto CJK のフォントメトリクス由来で文字インクが
22
+ * 1dp 強下寄りになる(Pixel 実機ピクセル実測 +2.7px/density2.5。
23
+ * includeFontPadding: false は gravity center 下では無効なことも実測済み)ため、
24
+ * paddingBottom の光学ナッジで打ち消す(gravity center は残り高さの中央に置くので
25
+ * paddingBottom p で p/2 上がる)。
26
+ * recipe(styleRefs)の写像ではなく RN 実装の補正なので resolver には置かない。
27
+ */
28
+ /** OS 毎の補正値を返す純関数(jest は Platform=ios 固定のため、Android 分岐はこれを直接検証する)。 */
29
+ export declare function resolveInputVerticalFix(os: typeof Platform.OS): TextStyle;
16
30
  interface TextFieldProps {
17
31
  /** ラベル(必須。placeholder だけの入力欄を作らせない = FORM_NO_LABEL_OMIT)。 */
18
32
  label: string;
@@ -1 +1 @@
1
- {"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../../../../src/components/TextField.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAGtB,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAE5B,UAAU,cAAc;IACtB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IAC1C,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,KAAK,EACL,YAAY,EACZ,WAAW,EACX,OAAmB,EACnB,QAAgB,EAChB,IAAe,EACf,UAAU,EACV,SAAS,EACT,KAAK,EACL,MAAM,GACP,EAAE,cAAc,2CAqChB;yBAjDe,SAAS"}
1
+ {"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../../../../src/components/TextField.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EACL,QAAQ,EAIR,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAGtB,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;;;;GAWG;AAEH,sEAAsE;AACtE,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,OAAO,QAAQ,CAAC,EAAE,GAAG,SAAS,CAMzE;AAKD,UAAU,cAAc;IACtB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IAC1C,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,KAAK,EACL,YAAY,EACZ,WAAW,EACX,OAAmB,EACnB,QAAgB,EAChB,IAAe,EACf,UAAU,EACV,SAAS,EACT,KAAK,EACL,MAAM,GACP,EAAE,cAAc,2CAqChB;yBAjDe,SAAS"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "melta-app",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "melta for APP — melta デザインシステムの React Native UI kit。melta-contracts(デザイン契約)を single source of truth に RN 実装でその契約を満たす。アプリ本体ではなくライブラリ。",
5
5
  "license": "MIT",
6
6
  "homepage": "https://app.melta.tsubotax.com",
@@ -14,10 +14,12 @@
14
14
 
15
15
  import { useMemo, useState } from "react";
16
16
  import {
17
+ Platform,
17
18
  TextInput,
18
19
  View,
19
20
  Text as RNText,
20
21
  type StyleProp,
22
+ type TextStyle,
21
23
  type ViewStyle,
22
24
  } from "react-native";
23
25
  import { useTheme } from "../theme";
@@ -28,6 +30,31 @@ import {
28
30
  type TextFieldSize,
29
31
  } from "./textfield.styles";
30
32
 
33
+ /**
34
+ * Android の EditText は固定 height と組み合わさると既定の縦 padding で文字が
35
+ * 上寄り・下端切れになるため、縦 padding を殺して縦センターに揃える。
36
+ * textAlignVertical は Android 専用、paddingVertical: 0 は iOS の見た目を変えない
37
+ * (iOS の単一行入力は元々縦センター)。
38
+ * さらに Android では Noto CJK のフォントメトリクス由来で文字インクが
39
+ * 1dp 強下寄りになる(Pixel 実機ピクセル実測 +2.7px/density2.5。
40
+ * includeFontPadding: false は gravity center 下では無効なことも実測済み)ため、
41
+ * paddingBottom の光学ナッジで打ち消す(gravity center は残り高さの中央に置くので
42
+ * paddingBottom p で p/2 上がる)。
43
+ * recipe(styleRefs)の写像ではなく RN 実装の補正なので resolver には置かない。
44
+ */
45
+ /* eslint-disable melta/no-raw-spacing -- spacing の選択ではなく EditText 既定 padding の打ち消しと CJK 光学ナッジ(実測由来の固定値) */
46
+ /** OS 毎の補正値を返す純関数(jest は Platform=ios 固定のため、Android 分岐はこれを直接検証する)。 */
47
+ export function resolveInputVerticalFix(os: typeof Platform.OS): TextStyle {
48
+ return {
49
+ paddingVertical: 0,
50
+ ...(os === "android" ? { paddingBottom: 2 } : null),
51
+ textAlignVertical: "center",
52
+ };
53
+ }
54
+ /* eslint-enable melta/no-raw-spacing */
55
+
56
+ const INPUT_VERTICAL_FIX = resolveInputVerticalFix(Platform.OS);
57
+
31
58
  interface TextFieldProps {
32
59
  /** ラベル(必須。placeholder だけの入力欄を作らせない = FORM_NO_LABEL_OMIT)。 */
33
60
  label: string;
@@ -88,7 +115,7 @@ export function TextField({
88
115
  if (!disabled) setFocused(true);
89
116
  }}
90
117
  onBlur={() => setFocused(false)}
91
- style={[resolved.input, focused && !disabled ? focusStyle : null]}
118
+ style={[resolved.input, INPUT_VERTICAL_FIX, focused && !disabled ? focusStyle : null]}
92
119
  />
93
120
  {helperText != null && !showError && (
94
121
  <RNText style={resolved.helperText}>{helperText}</RNText>
@@ -15,13 +15,17 @@
15
15
  * Provider の context 値を render 中に同期参照して padding を付けるため、native
16
16
  * SafeAreaView の layout/state 更新を待たず、初回 render から inset が反映される。
17
17
  *
18
- * ⚠️ 前提: アプリの root に SafeAreaProvider が必要。初回 render から正しい inset を
19
- * 使うには Provider に initialMetrics も渡す。React Navigation / Expo Router 利用時は
20
- * Provider が設置済みのことが多い。
18
+ * ⚠️ 前提: アプリの root に SafeAreaProvider が必要(無いと useSafeAreaInsets throw
19
+ * する)。初回 render から正しい inset を使うには Provider に initialMetrics も渡す。
20
+ * React Navigation / Expo Router 利用時は Provider が設置済みのことが多い。
21
+ *
22
+ * ⚠️ 公開契約: safe-area と合成する padding は数値のみサポート。"5%" 等の非数値は
23
+ * 基底値として扱えず、対象 edge の padding は inset 値に置き換わる(RNSAC native も
24
+ * percentage padding 未サポートのため、より弱い保証を契約化はしない)。
21
25
  */
22
26
 
23
27
  import { createElement, useMemo } from "react";
24
- import { StyleSheet, View, type ViewStyle } from "react-native";
28
+ import { I18nManager, StyleSheet, View, type ViewStyle } from "react-native";
25
29
  import {
26
30
  useSafeAreaInsets,
27
31
  type Edge,
@@ -47,21 +51,36 @@ function additivePadding(
47
51
  edges: ReadonlySet<Edge>,
48
52
  ): ViewStyle {
49
53
  const flat = StyleSheet.flatten(style) ?? {};
54
+ const num = (value: unknown): number | null => (typeof value === "number" ? value : null);
50
55
 
51
- const base = (edge: Edge): number => {
52
- const edgeValue = flat[`padding${edge[0].toUpperCase()}${edge.slice(1)}` as keyof ViewStyle];
53
- const axisValue =
54
- edge === "top" || edge === "bottom" ? flat.paddingVertical : flat.paddingHorizontal;
55
- const value = edgeValue ?? axisValue ?? flat.padding;
56
- return typeof value === "number" ? value : 0;
57
- };
56
+ // 基底 padding の解決は Yoga の優先順位(論理 > 物理 > 軸 > 全体)に合わせる。
57
+ // 論理キー(paddingStart/End)を読まないと、基底が論理指定のとき取りこぼす。
58
+ const isRTL = I18nManager.isRTL;
59
+ const baseVertical = (edge: "top" | "bottom"): number =>
60
+ num(edge === "top" ? flat.paddingTop : flat.paddingBottom) ??
61
+ num(flat.paddingVertical) ??
62
+ num(flat.padding) ??
63
+ 0;
64
+ const baseHorizontal = (edge: "left" | "right"): number =>
65
+ num(flat[(edge === "left") === !isRTL ? "paddingStart" : "paddingEnd"]) ??
66
+ num(edge === "left" ? flat.paddingLeft : flat.paddingRight) ??
67
+ num(flat.paddingHorizontal) ??
68
+ num(flat.padding) ??
69
+ 0;
58
70
 
59
- return {
60
- ...(edges.has("top") ? { paddingTop: base("top") + insets.top } : null),
61
- ...(edges.has("right") ? { paddingRight: base("right") + insets.right } : null),
62
- ...(edges.has("bottom") ? { paddingBottom: base("bottom") + insets.bottom } : null),
63
- ...(edges.has("left") ? { paddingLeft: base("left") + insets.left } : null),
64
- };
71
+ // 水平方向は論理キーで出力する。物理キー(paddingLeft/Right)で上書きすると、
72
+ // 基底 style 側に paddingStart/End があるとき論理キーが Yoga 上で勝ち、
73
+ // inset の加算結果ごと無視されるため。
74
+ const out: ViewStyle = {};
75
+ if (edges.has("top")) out.paddingTop = baseVertical("top") + insets.top;
76
+ if (edges.has("bottom")) out.paddingBottom = baseVertical("bottom") + insets.bottom;
77
+ if (edges.has("left")) {
78
+ out[isRTL ? "paddingEnd" : "paddingStart"] = baseHorizontal("left") + insets.left;
79
+ }
80
+ if (edges.has("right")) {
81
+ out[isRTL ? "paddingStart" : "paddingEnd"] = baseHorizontal("right") + insets.right;
82
+ }
83
+ return out;
65
84
  }
66
85
 
67
86
  function createContextSafeAreaView(selectedEdges: readonly Edge[]): SafeAreaViewLike {