melta-app 0.2.3 → 0.3.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.
package/README.md CHANGED
@@ -219,6 +219,7 @@ enableSafeAreaContext({ edges: ["top"] });
219
219
  - Screen の SafeArea は adapter registry 化済み: default は RN core SafeAreaView(依存ゼロ維持)、`melta-app/safe-area` の `enableSafeAreaContext()` で react-native-safe-area-context に差し替え可(optional peer)
220
220
  - ✅ form / feedback 10 個(TextField / Toggle / Checkbox / Radio / Alert / Toast / Progress / Modal / ActionSheet / BottomSheet)— checkbox / radio は Pressable + 描画(svg 非依存)、ActionSheet / BottomSheet は select / dropdown の adapted 変換先の受け皿
221
221
  - ✅ showcase(https://app.melta.tsubotax.com — melta-ui 様式シェル + 実 RN カタログの Live 埋め込み。表・統計は契約からビルド時生成)
222
+ - ✅ AI 入口: [llms.txt](https://app.melta.tsubotax.com/llms.txt)(契約から生成・drift 検査対象)+ [docs/patterns.md](docs/patterns.md)(フォームの組み方規範。スニペットは実コードと機械同期)
222
223
  - ✅ [npm publish(0.1.0)](https://www.npmjs.com/package/melta-app)
223
224
  - ⬜ React Native Directory 登録([PR #2606](https://github.com/react-native-community/directory/pull/2606) レビュー待ち)
224
225
 
@@ -0,0 +1,263 @@
1
+ # melta-app Form Patterns(フォームの組み方)
2
+
3
+ > フォーム構成の規範 + コピペ可能スニペット。スニペットは example/screens/FormValidationScreen.tsx
4
+ > からの抜粋で、`npm run check:drift` が実コードとの一致を機械検査する(手で fence を編集しない。
5
+ > 実装を変えたら fence を実装に合わせる)。web 版の規範は melta-ui patterns/form.md。
6
+
7
+ ## 1. 原則
8
+
9
+ フォームは「入力者を信頼し、間違えたときだけ静かに助ける」方向で組む。melta-app の公開 API だけで成立する形を規範とし、form ライブラリは前提にしない。
10
+
11
+ | 原則 | 理由 |
12
+ |---|---|
13
+ | フィールドは縦積み(`Stack gap="4"`) | モバイル幅で横並びフィールドは折返し事故の温床。1 カラムが読む順序 = 入力順序になる |
14
+ | ラベルは入力欄の上 | `TextField` の `label`(必須 prop)が構造で強制する。placeholder をラベル代わりにしない |
15
+ | エラーはフィールド直下 | `errorText` / `Radio.error` が描画位置を固定する。離れた場所のサマリーだけに頼らない |
16
+ | 主要アクションは full-width | モバイルでは web の右寄せボタンを持ち込まない。`Stack` 直下の `Button` は stretch される |
17
+ | フォーム全体は `Stack gap="4"` | フィールド間隔はトークン経由。raw spacing は lint が落とす |
18
+
19
+ ## 2. 検証タイミング — blur で検証し、change で赦し、submit で全量
20
+
21
+ 1. **blur で初回検証**(touched フラグを立てる)— 入力途中に叱らない。
22
+ 2. **エラー表示中は change で即時再評価** — errors は values からの `useMemo` 導出なので、修正した瞬間にエラーが消える(punish late, reward early)。
23
+ 3. **submit で全量検証** — `submitAttempted` を立てて未 touch フィールドのエラーも表示し、フォームレベルのサマリーを Alert で出す。
24
+ 4. **送信ボタンは disable しない** — 「なぜ押せないか」を伝えられないため。二重送信だけ `loading` で防ぎ、**送信中は全フィールドを `disabled={submitting}` でロックする**(後出し編集で成功表示が嘘になるのを防ぐ)。編集が再開されたら前回の成功表示は破棄する(`setField` に集約)。
25
+ 5. **エラーはスクリーンリーダーにも伝える** — TextField / Radio の errorText は liveRegion(Android)+ accessibilityLabel 合成(再フォーカス時読み上げ)を内蔵。submit 失敗のサマリーは `AccessibilityInfo.announceForAccessibility` で iOS にも通知する(スニペット参照)。
26
+ 5. **エラーの自動非表示はしない** — 修正されるまで表示し続ける(web 版 form.md の禁止パターンと同一規範)。
27
+
28
+ 検証は state に持たず、values からの pure function 導出にする。これが規範 2 を無料にする。
29
+
30
+ <!-- snippet:validators source=example/screens/FormValidationScreen.tsx -->
31
+ ```tsx
32
+ interface FormValues {
33
+ name: string;
34
+ email: string;
35
+ capacity: string; // TextInput は文字列で保持し、検証時に数値解釈する
36
+ visibility: string | undefined; // Radio 未選択は undefined
37
+ notifyOnDone: boolean;
38
+ agreed: boolean;
39
+ }
40
+
41
+ type FieldKey = "name" | "email" | "capacity" | "visibility" | "agreed";
42
+ type FieldErrors = Partial<Record<FieldKey, string>>;
43
+
44
+ // 軽量ヒューリスティック(RFC 完全準拠はしない)。正規化 + サーバー側検証を前提にする
45
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
46
+
47
+ /** 全フィールドの検証(pure)。エラーが無いフィールドはキーごと持たない。 */
48
+ function validate(values: FormValues): FieldErrors {
49
+ const errors: FieldErrors = {};
50
+ if (values.name.trim() === "") errors.name = "プロジェクト名を入力してください";
51
+ const email = values.email.trim(); // 前後空白は入力ミス扱いせず、正規化してから判定する
52
+ if (email === "") errors.email = "通知先メールアドレスを入力してください";
53
+ else if (!EMAIL_RE.test(email)) errors.email = "メールアドレスの形式が正しくありません";
54
+ const capacity = values.capacity.trim();
55
+ if (capacity === "") errors.capacity = "定員を入力してください";
56
+ // number-pad は paste・物理キーボードを制約しないので digits-only を正面から検査する
57
+ // (Number() 変換だけだと "1e1" や "0x10" が通ってしまう)
58
+ else if (!/^\d+$/.test(capacity) || Number(capacity) < 1 || Number(capacity) > 99)
59
+ errors.capacity = "定員は 1〜99 の整数で入力してください";
60
+ if (values.visibility == null) errors.visibility = "公開範囲を選択してください";
61
+ if (!values.agreed) errors.agreed = "利用規約への同意が必要です";
62
+ return errors;
63
+ }
64
+ ```
65
+
66
+ フィールド側は `onBlur` で touched を立て、`shownError(key)`(blur 済み or 送信試行後だけエラーを返す)で表示を制御する。「通った」ことを積極的に伝えたいフィールド(この見本ではメールアドレスのみ)は、valid かつ touched のとき `variant="success"` にする — 全フィールドに付ける必要はない。`helperText` と `errorText` は排他表示(TextField が内部で制御)。
67
+
68
+ <!-- snippet:field-blur source=example/screens/FormValidationScreen.tsx -->
69
+ ```tsx
70
+ <TextField
71
+ label="プロジェクト名(必須)"
72
+ value={values.name}
73
+ onChangeText={(text) => setField("name", text)}
74
+ placeholder="例: 道東撮影プロジェクト"
75
+ variant={shownError("name") != null ? "error" : "default"}
76
+ errorText={shownError("name")}
77
+ onBlur={() => touch("name")}
78
+ disabled={submitting}
79
+ />
80
+ <TextField
81
+ label="通知先メールアドレス(必須)"
82
+ value={values.email}
83
+ onChangeText={(text) => setField("email", text)}
84
+ placeholder="user@example.com"
85
+ keyboardType="email-address"
86
+ autoCapitalize="none"
87
+ autoCorrect={false}
88
+ variant={
89
+ shownError("email") != null
90
+ ? "error"
91
+ : touched.email === true && errors.email == null && values.email !== ""
92
+ ? "success"
93
+ : "default"
94
+ }
95
+ helperText="進捗の通知に使う。プロフィールには表示されない"
96
+ errorText={shownError("email")}
97
+ onBlur={() => touch("email")}
98
+ disabled={submitting}
99
+ />
100
+ ```
101
+
102
+ ## 3. required の示し方
103
+
104
+ label 文字列に「(必須)」「(任意)」を明記する。web の赤アスタリスクは RN の `label: string` では表現できず、スクリーンリーダーの読み上げでも意味を成さないため、文字で書く。色だけの必須表示は禁止。任意フィールドが少数派なら「(任意)」だけを付ける形でもよいが、1 フォーム内で流儀を混ぜない。
105
+
106
+ ## 4. エラーの伝え方(FORM_NO_COLOR_ONLY_ERROR)
107
+
108
+ 色だけでエラーを伝えない。コンポーネントごとにテキスト表示の経路が違う:
109
+
110
+ | コンポーネント | エラー表示 |
111
+ |---|---|
112
+ | `TextField` | `variant="error"` + `errorText`(テキスト必須) |
113
+ | `Radio` | `error?: string` — 枠色 + メッセージ表示まで内蔵 |
114
+ | `Checkbox` | `error: boolean` のみ — **隣接 `Text` で文言を必ず併記**(errorText prop は無い) |
115
+ | フォームレベル | `Alert variant="error"`(サマリー)/ 成功は `variant="success"` |
116
+
117
+ エラーの自動非表示(数秒で消す等)は禁止。修正されるまで表示する。
118
+
119
+ <!-- snippet:choice-errors source=example/screens/FormValidationScreen.tsx -->
120
+ ```tsx
121
+ <Radio
122
+ label="公開範囲(必須)"
123
+ options={VISIBILITY_OPTIONS}
124
+ value={values.visibility}
125
+ onChange={(value) => setField("visibility", value)}
126
+ error={shownError("visibility")}
127
+ disabled={submitting}
128
+ />
129
+ <Toggle
130
+ value={values.notifyOnDone}
131
+ onValueChange={(value) => setField("notifyOnDone", value)}
132
+ label="完了時に通知する(任意)"
133
+ disabled={submitting}
134
+ />
135
+ <Stack gap="1">
136
+ <Checkbox
137
+ label="利用規約に同意する(必須)"
138
+ checked={values.agreed}
139
+ onChange={(checked) => setField("agreed", checked)}
140
+ error={shownError("agreed") != null}
141
+ disabled={submitting}
142
+ />
143
+ {/* Checkbox は errorText を持たないため、隣接 Text で必ず文言でも伝える
144
+ (FORM_NO_COLOR_ONLY_ERROR = 色だけで伝えない)。View の liveRegion で
145
+ 出現も通知する(melta Text は a11y props を透過しない) */}
146
+ {shownError("agreed") != null && (
147
+ <View accessibilityLiveRegion="polite">
148
+ <Text variant="xs" style={{ color: theme.color.status.danger.base }}>
149
+ {shownError("agreed")}
150
+ </Text>
151
+ </View>
152
+ )}
153
+ </Stack>
154
+ ```
155
+
156
+ ## 5. keyboardType の選び方
157
+
158
+ 入力内容に合ったキーボードを必ず指定する(`default` のまま数値やメールを打たせない):
159
+
160
+ | 用途 | keyboardType | 併用 |
161
+ |---|---|---|
162
+ | 自由テキスト | `default` | — |
163
+ | メール | `email-address` | `autoCapitalize="none"` `autoCorrect={false}` |
164
+ | 整数(個数・回数) | `number-pad` | 値は string で保持し検証で数値化 |
165
+ | 小数(距離・金額) | `decimal-pad` | 同上 |
166
+ | 電話番号 | `phone-pad` | — |
167
+
168
+ ※ `TextField` が RN `TextInput` へ透過するのは入力メソッド系 props(`keyboardType` / `autoCapitalize` / `autoCorrect` / `maxLength` / `returnKeyType` / `onSubmitEditing` / `onBlur` / `onFocus`)のみ。style 系 props は透過しない(token 純度維持)。
169
+
170
+ ## 6. キーボード回避
171
+
172
+ 現行公開 API での正攻法は **`KeyboardAvoidingView`(iOS のみ `behavior="padding"`)+ `Screen variant="fixed" padding="none"` + 自前 `ScrollView`** の組み合わせ。`Screen variant="scroll"` は `keyboardShouldPersistTaps` を公開していないため、キーボード表示中の送信ボタンが 1 タップ目で反応しない(タップがキーボード dismiss に食われる)。フォーム画面では自前 ScrollView に `keyboardShouldPersistTaps="handled"` を必ず付ける。padding は `theme.spacing` から取る(raw 値は lint が落とす)。
173
+
174
+ <!-- snippet:keyboard-shell source=example/screens/FormValidationScreen.tsx -->
175
+ ```tsx
176
+ export function FormValidationScreen() {
177
+ const { theme } = useTheme();
178
+ return (
179
+ <KeyboardAvoidingView
180
+ style={{ flex: 1 }}
181
+ behavior={Platform.OS === "ios" ? "padding" : undefined}
182
+ >
183
+ {/* Screen(variant="scroll") は keyboardShouldPersistTaps を公開していないため、
184
+ フォーム画面は fixed + 自前 ScrollView で組む。"handled" にしないと
185
+ キーボード表示中の送信ボタンが 1 タップ目で反応しない(dismiss に食われる) */}
186
+ <Screen variant="fixed" padding="none" header={<Header title="新規プロジェクト" />}>
187
+ <ScrollView
188
+ contentContainerStyle={{ padding: theme.spacing["4"] }}
189
+ keyboardShouldPersistTaps="handled"
190
+ >
191
+ <FormValidationForm />
192
+ </ScrollView>
193
+ </Screen>
194
+ </KeyboardAvoidingView>
195
+ );
196
+ }
197
+ ```
198
+
199
+ ## 7. 送信ボタンの制御
200
+
201
+ 送信ボタンは**常時活性**にする。disabled-until-valid は「なぜ押せないか」を伝えられず、a11y 的にも不利。押下時に全量検証してサマリー Alert で理由を見せる方が誠実。二重送信だけ `Button` の `loading`(Spinner + disabled 相当)で防ぐ。
202
+
203
+ <!-- snippet:submit-handler source=example/screens/FormValidationScreen.tsx -->
204
+ ```tsx
205
+ const handleSubmit = () => {
206
+ setSubmitAttempted(true); // 以後は未 blur フィールドのエラーも表示する
207
+ if (Object.keys(errors).length > 0) {
208
+ // 視覚はサマリー Alert(variant="error")。liveRegion の無い iOS にも announce で伝える
209
+ AccessibilityInfo.announceForAccessibility("入力内容を確認してください");
210
+ return;
211
+ }
212
+ setSubmitted(false);
213
+ setSubmitting(true); // 送信中は全フィールドを disabled にする(後出し編集で成功表示が嘘になるのを防ぐ)
214
+ // 実アプリではここで API 呼び出し。見本ではダミー遅延で loading を見せる
215
+ setTimeout(() => {
216
+ setSubmitting(false);
217
+ setSubmitted(true);
218
+ }, 800);
219
+ };
220
+ ```
221
+
222
+ <!-- snippet:submit-button source=example/screens/FormValidationScreen.tsx -->
223
+ ```tsx
224
+ {submitAttempted && Object.keys(errors).length > 0 && (
225
+ <Alert
226
+ variant="error"
227
+ title="入力内容を確認してください"
228
+ message={Object.values(errors).join("\n")}
229
+ />
230
+ )}
231
+ {submitted && (
232
+ <Alert
233
+ variant="success"
234
+ message="プロジェクトを作成しました"
235
+ onClose={() => setSubmitted(false)}
236
+ />
237
+ )}
238
+ {/* 送信は常時活性(なぜ押せないかを伝えられない disabled-until-valid にしない)。
239
+ 二重送信だけ loading で防ぐ */}
240
+ <Button label="プロジェクトを作成" onPress={handleSubmit} loading={submitting} />
241
+ ```
242
+
243
+ ## 8. state 設計の最小形
244
+
245
+ form ライブラリを DS として強制しない。素の React で必要十分な最小形:
246
+
247
+ - state は 5 つ: `values` / `touched: Partial<Record<FieldKey, boolean>>` / `submitAttempted` / `submitting` / `submitted`
248
+ - `errors = useMemo(() => validate(values), [values])` — 検証結果は state に持たず、values からの純導出にする(エラー中の修正が即座に画面へ反映される)
249
+ - `shownError(key) = (touched[key] || submitAttempted) ? errors[key] : undefined` — 表示制御はこの 1 関数に集約
250
+ - 数値入力も `TextInput` の値は string で保持し、検証・送信時に数値解釈する
251
+
252
+ プロジェクト側で form ライブラリを使うのは自由だが、このパターン文書とカタログ見本は素の React を正とする。
253
+
254
+ ## 9. 禁止パターン
255
+
256
+ | 禁止 | 理由 / 代替 |
257
+ |---|---|
258
+ | ラベル省略 | `TextField` は `label` 必須で型的に不可能。他コンポーネントでも省略しない |
259
+ | 色だけのエラー表示 | FORM_NO_COLOR_ONLY_ERROR。必ずテキストを併記(§4 の対応表) |
260
+ | エラーの自動非表示 | 修正されるまで表示する。トースト等で数秒だけ見せるのは不可 |
261
+ | disabled-until-valid の常用 | 「なぜ押せないか」を伝えられない。常時活性 + submit 時全量検証 + サマリー Alert |
262
+ | placeholder をラベル代わりにする | 入力を始めた瞬間に文脈が消える。placeholder は例示専用 |
263
+ | 生値スタイル(hex / 数値 spacing 直書き) | lint(--max-warnings 0)が落とす。`theme.color.*` / `theme.spacing[*]` 経由のみ |
@@ -118,13 +118,17 @@ export function Radio({
118
118
  }), hasError ?
119
119
  /*#__PURE__*/
120
120
  // danger.text-light は SemanticColors 外なので style で上書き(Button の textColor と同じ扱い)。
121
- _jsx(Text, {
122
- variant: "xs",
123
- style: {
124
- color: group.errorTextStyle.color,
125
- marginTop: group.errorTextStyle.marginTop
126
- },
127
- children: error
121
+ // View の liveRegion で出現を通知(melta Text は a11y props を透過しないため)。
122
+ _jsx(View, {
123
+ accessibilityLiveRegion: "polite",
124
+ children: /*#__PURE__*/_jsx(Text, {
125
+ variant: "xs",
126
+ style: {
127
+ color: group.errorTextStyle.color,
128
+ marginTop: group.errorTextStyle.marginTop
129
+ },
130
+ children: error
131
+ })
128
132
  }) : null]
129
133
  });
130
134
  }
@@ -1 +1 @@
1
- {"version":3,"names":["useMemo","Pressable","View","Text","useTheme","CONTRACTS","RADIO_SPEC","resolveRadioCircleStyle","resolveRadioGroupStyle","jsx","_jsx","jsxs","_jsxs","Radio","label","options","value","onChange","variant","error","disabled","testID","theme","mode","hasError","group","circles","unselected","selected","style","opacity","disabledOpacity","undefined","children","weight","color","marginBottom","groupLabelStyle","accessibilityRole","containerStyle","map","option","optionDisabled","onPress","accessibilityState","checked","accessibilityLabel","cardStyle","cardSelectedStyle","flexDirection","alignItems","description","gap","optionGap","dotStyle","flexShrink","errorTextStyle","marginTop","__contract","radio"],"sourceRoot":"../../../src","sources":["components/Radio.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["useMemo","Pressable","View","Text","useTheme","CONTRACTS","RADIO_SPEC","resolveRadioCircleStyle","resolveRadioGroupStyle","jsx","_jsx","jsxs","_jsxs","Radio","label","options","value","onChange","variant","error","disabled","testID","theme","mode","hasError","group","circles","unselected","selected","style","opacity","disabledOpacity","undefined","children","weight","color","marginBottom","groupLabelStyle","accessibilityRole","containerStyle","map","option","optionDisabled","onPress","accessibilityState","checked","accessibilityLabel","cardStyle","cardSelectedStyle","flexDirection","alignItems","description","gap","optionGap","dotStyle","flexShrink","accessibilityLiveRegion","errorTextStyle","marginTop","__contract","radio"],"sourceRoot":"../../../src","sources":["components/Radio.tsx"],"mappings":"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","ignoreList":[]}
@@ -7,11 +7,18 @@
7
7
  * - variant は検証状態(default/error/success)。disabled=true で variant を "disabled" に上書きし、
8
8
  * editable=false + accessibilityState.disabled を併せて適用する(recipe description の規約)。
9
9
  * - error 時は errorText を必ずテキストで表示する(FORM_NO_COLOR_ONLY_ERROR = 色だけで伝えない)。
10
- * RN には aria-describedby 相当が無いため、accessibilityState ではなく Text の実表示で伝える。
10
+ * RN には aria-describedby 相当が無いため、(a) errorText input の accessibilityLabel に合成
11
+ * (再フォーカス時に読み上げ)、(b) errorText 側に accessibilityLiveRegion="polite"(Android は
12
+ * 出現時に通知。iOS の即時通知が要る画面は submit 時に announceForAccessibility を併用する —
13
+ * docs/patterns.md 参照)。
11
14
  * - focus は内部 state(onFocus/onBlur)で recipe states.focus の inputStyle 差分
12
- * (borderColor=primary.500)を重ねる。disabled 時は focus させない。
15
+ * (borderColor=primary.500)を重ねる。disabled 時は focus スタイルを付けない。
16
+ * 外部の onFocus/onBlur は RN の event ごと常に透過する(内部の視覚 state と分離)。
13
17
  * - 色・寸法の決定は pure resolver(textfield.styles.ts)に分離。
14
18
  * recipes/app/textfield.recipe.json との機械照合は scripts/lib/textfield-conformance.test.ts が行う。
19
+ * - RN TextInput へ透過するのは入力メソッド系 props(keyboardType / autoCapitalize / autoCorrect /
20
+ * maxLength / returnKeyType / onSubmitEditing / onBlur / onFocus)のみ。style 系 props は
21
+ * 透過しない(token 純度維持)。
15
22
  */
16
23
 
17
24
  import { useMemo, useState } from "react";
@@ -57,6 +64,14 @@ export function TextField({
57
64
  size = "medium",
58
65
  helperText,
59
66
  errorText,
67
+ keyboardType,
68
+ autoCapitalize,
69
+ autoCorrect,
70
+ maxLength,
71
+ returnKeyType,
72
+ onSubmitEditing,
73
+ onBlur,
74
+ onFocus,
60
75
  style,
61
76
  testID
62
77
  }) {
@@ -87,19 +102,30 @@ export function TextField({
87
102
  placeholder: placeholder,
88
103
  placeholderTextColor: colors["text-muted"],
89
104
  editable: !disabled,
90
- accessibilityLabel: label,
105
+ keyboardType: keyboardType,
106
+ autoCapitalize: autoCapitalize,
107
+ autoCorrect: autoCorrect,
108
+ maxLength: maxLength,
109
+ returnKeyType: returnKeyType,
110
+ onSubmitEditing: onSubmitEditing,
111
+ accessibilityLabel: showError ? `${label}。エラー: ${errorText}` : label,
91
112
  accessibilityState: {
92
113
  disabled
93
114
  },
94
- onFocus: () => {
115
+ onFocus: e => {
95
116
  if (!disabled) setFocused(true);
117
+ onFocus?.(e);
118
+ },
119
+ onBlur: e => {
120
+ setFocused(false);
121
+ onBlur?.(e);
96
122
  },
97
- onBlur: () => setFocused(false),
98
123
  style: [resolved.input, INPUT_VERTICAL_FIX, focused && !disabled ? focusStyle : null]
99
124
  }), helperText != null && !showError && /*#__PURE__*/_jsx(RNText, {
100
125
  style: resolved.helperText,
101
126
  children: helperText
102
127
  }), showError && /*#__PURE__*/_jsx(RNText, {
128
+ accessibilityLiveRegion: "polite",
103
129
  style: resolved.errorText,
104
130
  children: errorText
105
131
  })]
@@ -1 +1 @@
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":[]}
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","keyboardType","autoCapitalize","autoCorrect","maxLength","returnKeyType","onSubmitEditing","onBlur","onFocus","style","testID","theme","mode","colors","focused","setFocused","effectiveVariant","resolved","focusStyle","showError","children","placeholderTextColor","editable","accessibilityLabel","accessibilityState","e","input","accessibilityLiveRegion","__contract","textfield"],"sourceRoot":"../../../src","sources":["components/TextField.tsx"],"mappings":"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","ignoreList":[]}
@@ -17,13 +17,17 @@
17
17
  * Provider の context 値を render 中に同期参照して padding を付けるため、native
18
18
  * SafeAreaView の layout/state 更新を待たず、初回 render から inset が反映される。
19
19
  *
20
- * ⚠️ 前提: アプリの root に SafeAreaProvider が必要。初回 render から正しい inset を
21
- * 使うには Provider に initialMetrics も渡す。React Navigation / Expo Router 利用時は
22
- * Provider が設置済みのことが多い。
20
+ * ⚠️ 前提: アプリの root に SafeAreaProvider が必要(無いと useSafeAreaInsets throw
21
+ * する)。初回 render から正しい inset を使うには Provider に initialMetrics も渡す。
22
+ * React Navigation / Expo Router 利用時は Provider が設置済みのことが多い。
23
+ *
24
+ * ⚠️ 公開契約: safe-area と合成する padding は数値のみサポート。"5%" 等の非数値は
25
+ * 基底値として扱えず、対象 edge の padding は inset 値に置き換わる(RNSAC native も
26
+ * percentage padding 未サポートのため、より弱い保証を契約化はしない)。
23
27
  */
24
28
 
25
29
  import { createElement, useMemo } from "react";
26
- import { StyleSheet, View } from "react-native";
30
+ import { I18nManager, StyleSheet, View } from "react-native";
27
31
  import { useSafeAreaInsets } from "react-native-safe-area-context";
28
32
  import { setSafeAreaView } from "../components/safe-area-registry.js";
29
33
  const ALL_EDGES = ["top", "right", "bottom", "left"];
@@ -32,26 +36,27 @@ const ALL_EDGES = ["top", "right", "bottom", "left"];
32
36
 
33
37
  function additivePadding(style, insets, edges) {
34
38
  const flat = StyleSheet.flatten(style) ?? {};
35
- const base = edge => {
36
- const edgeValue = flat[`padding${edge[0].toUpperCase()}${edge.slice(1)}`];
37
- const axisValue = edge === "top" || edge === "bottom" ? flat.paddingVertical : flat.paddingHorizontal;
38
- const value = edgeValue ?? axisValue ?? flat.padding;
39
- return typeof value === "number" ? value : 0;
40
- };
41
- return {
42
- ...(edges.has("top") ? {
43
- paddingTop: base("top") + insets.top
44
- } : null),
45
- ...(edges.has("right") ? {
46
- paddingRight: base("right") + insets.right
47
- } : null),
48
- ...(edges.has("bottom") ? {
49
- paddingBottom: base("bottom") + insets.bottom
50
- } : null),
51
- ...(edges.has("left") ? {
52
- paddingLeft: base("left") + insets.left
53
- } : null)
54
- };
39
+ const num = value => typeof value === "number" ? value : null;
40
+
41
+ // 基底 padding の解決は Yoga の優先順位(論理 > 物理 > > 全体)に合わせる。
42
+ // 論理キー(paddingStart/End)を読まないと、基底が論理指定のとき取りこぼす。
43
+ const isRTL = I18nManager.isRTL;
44
+ const baseVertical = edge => num(edge === "top" ? flat.paddingTop : flat.paddingBottom) ?? num(flat.paddingVertical) ?? num(flat.padding) ?? 0;
45
+ const baseHorizontal = edge => num(flat[edge === "left" === !isRTL ? "paddingStart" : "paddingEnd"]) ?? num(edge === "left" ? flat.paddingLeft : flat.paddingRight) ?? num(flat.paddingHorizontal) ?? num(flat.padding) ?? 0;
46
+
47
+ // 水平方向は論理キーで出力する。物理キー(paddingLeft/Right)で上書きすると、
48
+ // 基底 style 側に paddingStart/End があるとき論理キーが Yoga 上で勝ち、
49
+ // inset の加算結果ごと無視されるため。
50
+ const out = {};
51
+ if (edges.has("top")) out.paddingTop = baseVertical("top") + insets.top;
52
+ if (edges.has("bottom")) out.paddingBottom = baseVertical("bottom") + insets.bottom;
53
+ if (edges.has("left")) {
54
+ out[isRTL ? "paddingEnd" : "paddingStart"] = baseHorizontal("left") + insets.left;
55
+ }
56
+ if (edges.has("right")) {
57
+ out[isRTL ? "paddingStart" : "paddingEnd"] = baseHorizontal("right") + insets.right;
58
+ }
59
+ return out;
55
60
  }
56
61
  function createContextSafeAreaView(selectedEdges) {
57
62
  const edges = new Set(selectedEdges);
@@ -1 +1 @@
1
- {"version":3,"names":["createElement","useMemo","StyleSheet","View","useSafeAreaInsets","setSafeAreaView","ALL_EDGES","additivePadding","style","insets","edges","flat","flatten","base","edge","edgeValue","toUpperCase","slice","axisValue","paddingVertical","paddingHorizontal","value","padding","has","paddingTop","top","paddingRight","right","paddingBottom","bottom","paddingLeft","left","createContextSafeAreaView","selectedEdges","Set","ContextSafeAreaView","props","paddingStyle","displayName","enableSafeAreaContext","options"],"sourceRoot":"../../../src","sources":["safe-area/index.ts"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["createElement","useMemo","I18nManager","StyleSheet","View","useSafeAreaInsets","setSafeAreaView","ALL_EDGES","additivePadding","style","insets","edges","flat","flatten","num","value","isRTL","baseVertical","edge","paddingTop","paddingBottom","paddingVertical","padding","baseHorizontal","paddingLeft","paddingRight","paddingHorizontal","out","has","top","bottom","left","right","createContextSafeAreaView","selectedEdges","Set","ContextSafeAreaView","props","paddingStyle","displayName","enableSafeAreaContext","options"],"sourceRoot":"../../../src","sources":["safe-area/index.ts"],"mappings":"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","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"Radio.d.ts","sourceRoot":"","sources":["../../../../src/components/Radio.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sBAAsB;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,OAAO,EACP,KAAK,EACL,QAAQ,EACR,OAAoB,EACpB,KAAK,EACL,QAAgB,EAChB,MAAM,GACP,EAAE,UAAU,2CAmFZ;yBA5Fe,KAAK"}
1
+ {"version":3,"file":"Radio.d.ts","sourceRoot":"","sources":["../../../../src/components/Radio.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sBAAsB;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,OAAO,EACP,KAAK,EACL,QAAQ,EACR,OAAoB,EACpB,KAAK,EACL,QAAgB,EAChB,MAAM,GACP,EAAE,UAAU,2CAsFZ;yBA/Fe,KAAK"}
@@ -5,13 +5,20 @@
5
5
  * - variant は検証状態(default/error/success)。disabled=true で variant を "disabled" に上書きし、
6
6
  * editable=false + accessibilityState.disabled を併せて適用する(recipe description の規約)。
7
7
  * - error 時は errorText を必ずテキストで表示する(FORM_NO_COLOR_ONLY_ERROR = 色だけで伝えない)。
8
- * RN には aria-describedby 相当が無いため、accessibilityState ではなく Text の実表示で伝える。
8
+ * RN には aria-describedby 相当が無いため、(a) errorText input の accessibilityLabel に合成
9
+ * (再フォーカス時に読み上げ)、(b) errorText 側に accessibilityLiveRegion="polite"(Android は
10
+ * 出現時に通知。iOS の即時通知が要る画面は submit 時に announceForAccessibility を併用する —
11
+ * docs/patterns.md 参照)。
9
12
  * - focus は内部 state(onFocus/onBlur)で recipe states.focus の inputStyle 差分
10
- * (borderColor=primary.500)を重ねる。disabled 時は focus させない。
13
+ * (borderColor=primary.500)を重ねる。disabled 時は focus スタイルを付けない。
14
+ * 外部の onFocus/onBlur は RN の event ごと常に透過する(内部の視覚 state と分離)。
11
15
  * - 色・寸法の決定は pure resolver(textfield.styles.ts)に分離。
12
16
  * recipes/app/textfield.recipe.json との機械照合は scripts/lib/textfield-conformance.test.ts が行う。
17
+ * - RN TextInput へ透過するのは入力メソッド系 props(keyboardType / autoCapitalize / autoCorrect /
18
+ * maxLength / returnKeyType / onSubmitEditing / onBlur / onFocus)のみ。style 系 props は
19
+ * 透過しない(token 純度維持)。
13
20
  */
14
- import { Platform, type StyleProp, type TextStyle, type ViewStyle } from "react-native";
21
+ import { Platform, type KeyboardTypeOptions, type ReturnKeyTypeOptions, type TextInputProps, type StyleProp, type TextStyle, type ViewStyle } from "react-native";
15
22
  import { type TextFieldSize } from "./textfield.styles";
16
23
  /**
17
24
  * Android の EditText は固定 height と組み合わさると既定の縦 padding で文字が
@@ -42,10 +49,20 @@ interface TextFieldProps {
42
49
  helperText?: string;
43
50
  /** variant="error" 時に表示するエラーメッセージ(色だけでなくテキストで伝える)。 */
44
51
  errorText?: string;
52
+ /** キーボード種別(RN TextInput へ透過)。 */
53
+ keyboardType?: KeyboardTypeOptions;
54
+ autoCapitalize?: "none" | "sentences" | "words" | "characters";
55
+ autoCorrect?: boolean;
56
+ maxLength?: number;
57
+ returnKeyType?: ReturnKeyTypeOptions;
58
+ onSubmitEditing?: TextInputProps["onSubmitEditing"];
59
+ /** blur 時コールバック(blur 検証用)。RN の event ごと透過し、内部の focus スタイル制御と分離。 */
60
+ onBlur?: TextInputProps["onBlur"];
61
+ onFocus?: TextInputProps["onFocus"];
45
62
  style?: StyleProp<ViewStyle>;
46
63
  testID?: string;
47
64
  }
48
- export declare function TextField({ label, value, onChangeText, placeholder, variant, disabled, size, helperText, errorText, style, testID, }: TextFieldProps): import("react/jsx-runtime").JSX.Element;
65
+ export declare function TextField({ label, value, onChangeText, placeholder, variant, disabled, size, helperText, errorText, keyboardType, autoCapitalize, autoCorrect, maxLength, returnKeyType, onSubmitEditing, onBlur, onFocus, style, testID, }: TextFieldProps): import("react/jsx-runtime").JSX.Element;
49
66
  export declare namespace TextField {
50
67
  var __contract: {
51
68
  readonly id: "textfield";
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../../../../src/components/TextField.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EACL,QAAQ,EAIR,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,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,iCAAiC;IACjC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,YAAY,CAAC;IAC/D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC,eAAe,CAAC,EAAE,cAAc,CAAC,iBAAiB,CAAC,CAAC;IACpD,mEAAmE;IACnE,MAAM,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;IAClC,OAAO,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IACpC,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,YAAY,EACZ,cAAc,EACd,WAAW,EACX,SAAS,EACT,aAAa,EACb,eAAe,EACf,MAAM,EACN,OAAO,EACP,KAAK,EACL,MAAM,GACP,EAAE,cAAc,2CAmDhB;yBAvEe,SAAS"}
@@ -15,9 +15,13 @@
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
  import { type Edge } from "react-native-safe-area-context";
23
27
  /** enableSafeAreaContext() の設定。edges 省略時は従来相当の全 edge。 */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/safe-area/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,EAEL,KAAK,IAAI,EAEV,MAAM,gCAAgC,CAAC;AASxC,yDAAyD;AACzD,MAAM,WAAW,4BAA4B;IAC3C,mDAAmD;IACnD,KAAK,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;CACzB;AA0CD,6DAA6D;AAC7D,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,4BAAiC,GAAG,IAAI,CAEtF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/safe-area/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,EAEL,KAAK,IAAI,EAEV,MAAM,gCAAgC,CAAC;AASxC,yDAAyD;AACzD,MAAM,WAAW,4BAA4B;IAC3C,mDAAmD;IACnD,KAAK,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;CACzB;AAyDD,6DAA6D;AAC7D,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,4BAAiC,GAAG,IAAI,CAEtF"}
package/llms.txt ADDED
@@ -0,0 +1,62 @@
1
+ # melta-app
2
+
3
+ > melta デザインシステムの React Native(Expo)UI kit。web 版 melta-ui と同じデザイン契約(melta-contracts、全 40 契約)を SSOT に、RN 実装 25 個で契約を満たす(planned 9 / not-planned 6 / adapted 形 4)。アプリ本体ではなく `npm install melta-app` で使うライブラリ。必須 peerDependencies は react / react-native のみ(optional peer: react-native-svg = Icon 用 / react-native-safe-area-context = SafeArea 差替用)。
4
+
5
+ UI を組む前に docs/patterns.md(組み方の規範)を読むこと。exact value(トークン・契約・禁止ルール)の SSOT は melta-contracts。melta-app にトークンの手書きは存在しない(theme は契約からの生成物)。デザイン言語の一次資料は web 版 https://melta.tsubotax.com/llms.txt。
6
+
7
+ ## Docs
8
+
9
+ - [README.md](https://github.com/tsubotax/melta-app#readme): 全体像(Install / 設計の核 / トークン正規化 / コンポーネント表)
10
+ - [patterns.md](https://app.melta.tsubotax.com/docs/patterns.md): フォーム等「組み方」の規範 + コピペ可能スニペット(実コードと drift 検査済み)
11
+ - [showcase](https://app.melta.tsubotax.com/): Live Catalog(実 RN コンポーネントの web export)+ 実装状態の全量表
12
+ - [tokens.json](https://melta.tsubotax.com/design/contracts/tokens.json): デザイントークン(契約 SSOT の配信面)
13
+ - [rules.json](https://melta.tsubotax.com/design/contracts/rules.json): 禁止ルール
14
+
15
+ ## Components
16
+
17
+ - [action-sheet](https://melta.tsubotax.com/design/contracts/components/action-sheet.contract.json): ✅ `ActionSheet` — 画面下から出るアクション/選択リスト。web の select / dropdown を APP に adapted 変換する際の受け皿(構造的 P0)。destructive アクションは色で区別しラベルでも伝える。
18
+ - [alert](https://melta.tsubotax.com/design/contracts/components/alert.contract.json): ✅ `Alert` — インラインアラート通知。Info / Success / Warning / Error の4タイプ。
19
+ - [avatar](https://melta.tsubotax.com/design/contracts/components/avatar.contract.json): ✅ `Avatar` — ユーザーアバター。Image / Initials / Status / Group。
20
+ - [bottom-sheet](https://melta.tsubotax.com/design/contracts/components/bottom-sheet.contract.json): ✅ `BottomSheet` — 画面下から出る汎用シート container(content は自由 slot)。フィルタ・詳細・複雑な選択 UI の受け皿。ActionSheet がリスト特化なのに対しこちらは容器。
21
+ - [button](https://melta.tsubotax.com/design/contracts/components/button.contract.json): ✅ `Button` — CTA / secondary / icon button。3階層ヒエラルキー。
22
+ - [card](https://melta.tsubotax.com/design/contracts/components/card.contract.json): ✅ `Card` — コンテンツコンテナ。Basic / Metrics / Media / Action / Link の5バリエーション。
23
+ - [checkbox](https://melta.tsubotax.com/design/contracts/components/checkbox.contract.json): ✅ `Checkbox` — 複数選択フォームコントロール。CSS 18px サイズ。
24
+ - [empty-state](https://melta.tsubotax.com/design/contracts/components/empty-state.contract.json): ✅ `EmptyState` — データ無し時のプレースホルダー(アイコン + タイトル + 説明 + 任意のアクション)。Text + Button の compose。
25
+ - [header](https://melta.tsubotax.com/design/contracts/components/header.contract.json): ✅ `Header` — 画面ヘッダー primitive(title + leading? / trailing? slot、下 border)。Screen の header slot に差して使う想定。title は text-heading の見出しとして読
26
+ - [icon](https://melta.tsubotax.com/design/contracts/components/icon.contract.json): ✅ `Icon` — 統一アイコン primitive。name prop でグリフを選択し(グリフ集合は実装側の公開面 = assets/icons の Charcoal セット由来)、色は semantic token キー限定の prop(default
27
+ - [image](https://melta.tsubotax.com/design/contracts/components/image.contract.json): ✅ `Image` — 画像表示 primitive。aspectRatio / radius / contentFit / onError fallback を持つ。Phase1 は RN コア Image、expo-image は Phase2。
28
+ - [metric](https://melta.tsubotax.com/design/contracts/components/metric.contract.json): ✅ `Metric` — 数値 + 単位 + ラベルの指標表示 primitive(距離 / 時間 / 獲得標高 等の走行サマリー表示)。tabular-nums で桁揃え。sizes の height は数値テキストの想定行高(px)で、実 fontSize は実
29
+ - [modal](https://melta.tsubotax.com/design/contracts/components/modal.contract.json): ✅ `Modal` — フォーカストラップとバックドロップを備えたモーダルダイアログ。
30
+ - [progress](https://melta.tsubotax.com/design/contracts/components/progress.contract.json): ✅ `Progress` — リニアプログレスバー。aria-valuenow/min/max必須。
31
+ - [radio](https://melta.tsubotax.com/design/contracts/components/radio.contract.json): ✅ `Radio` — 単一選択フォームコントロール。fieldsetラッパー必須。
32
+ - [row](https://melta.tsubotax.com/design/contracts/components/row.contract.json): ✅ `Row` — 子要素を横に並べる layout primitive。gap は spacing token キー限定の prop。折返し(wrap)と主軸配置(justify)を持ち、タグ列・メトリクス列・ヘッダー行の生 flexbox 手書きを置き換え
33
+ - [screen](https://melta.tsubotax.com/design/contracts/components/screen.contract.json): ✅ `Screen` — 画面骨格 primitive(SafeArea + bg-page + content padding)。header slot を scroll 外に固定し、毎画面手書きされる SafeAreaView + ScrollView + ヘッ
34
+ - [skeleton](https://melta.tsubotax.com/design/contracts/components/skeleton.contract.json): ✅ `Skeleton` — ローディングプレースホルダー。aria-busy="true"必須。
35
+ - [stack](https://melta.tsubotax.com/design/contracts/components/stack.contract.json): ✅ `Stack` — 子要素を縦に積む layout primitive。gap は spacing token キー限定の prop(省略時 gap なし)。画面の縦リズムを生 flexbox 手書きから置き換える(dogfood 不足-1)。
36
+ - [surface](https://melta.tsubotax.com/design/contracts/components/surface.contract.json): ✅ `Surface` — Card / Skeleton / EmptyState の共通土台となる箱 primitive(bg / radius / elevation / padding)。melta-app では internal(未 export)。
37
+ - [tag](https://melta.tsubotax.com/design/contracts/components/tag.contract.json): ✅ `Tag` — 削除可能なタグ / フィルターチップ。
38
+ - [text](https://melta.tsubotax.com/design/contracts/components/text.contract.json): ✅ `Text` — 本文・見出し・キャプションの基本テキスト primitive。fontSize variant と semantic color、heading/body で letterSpacing を切替。
39
+ - [textfield](https://melta.tsubotax.com/design/contracts/components/textfield.contract.json): ✅ `TextField` — テキスト入力フィールド。Default / Error / Disabled / Success の4状態。
40
+ - [toast](https://melta.tsubotax.com/design/contracts/components/toast.contract.json): ✅ `Toast` — 一時的な通知メッセージ。4タイプ(success/error/warning/info)。
41
+ - [toggle](https://melta.tsubotax.com/design/contracts/components/toggle.contract.json): ✅ `Toggle` — ON/OFFスイッチコンポーネント。role="switch"必須。
42
+ - [accordion](https://melta.tsubotax.com/design/contracts/components/accordion.contract.json): ⬜ planned
43
+ - [badge](https://melta.tsubotax.com/design/contracts/components/badge.contract.json): ⬜ planned
44
+ - [datepicker](https://melta.tsubotax.com/design/contracts/components/datepicker.contract.json): ⬜ planned(adapted: カレンダー自作はしない。OS 標準の日付ピッカー(@react-native-community/datetimepicker 等)への委譲でトークンのみ供給)
45
+ - [divider](https://melta.tsubotax.com/design/contracts/components/divider.contract.json): ⬜ planned
46
+ - [dropdown](https://melta.tsubotax.com/design/contracts/components/dropdown.contract.json): ⬜ planned(adapted: hover 起動は存在しない。タップ起動の Menu(アンカー付き)or ActionSheet に変換)
47
+ - [list](https://melta.tsubotax.com/design/contracts/components/list.contract.json): ⬜ planned
48
+ - [select](https://melta.tsubotax.com/design/contracts/components/select.contract.json): ⬜ planned(adapted: web のドロップリストは持ち込まない。ActionSheet / BottomSheet / OS Picker で開く選択 UI に変換(gluestack/Tamagui も native では Sheet 化))
49
+ - [stepper](https://melta.tsubotax.com/design/contracts/components/stepper.contract.json): ⬜ planned
50
+ - [tabs](https://melta.tsubotax.com/design/contracts/components/tabs.contract.json): ⬜ planned(adapted: トップタブ / SegmentedControl 型として実装(web の下線タブ意味論を M3 segmented / iOS UISegmentedControl に写像))
51
+ - [breadcrumb](https://melta.tsubotax.com/design/contracts/components/breadcrumb.contract.json): 🚫 not-planned — RN 主要ライブラリ提供 0/5。Apple HIG が multisegment breadcrumb を明示的に否定。back ボタン + nav bar タイトルで代替
52
+ - [copy-button](https://melta.tsubotax.com/design/contracts/components/copy-button.contract.json): 🚫 not-planned — DS プリミティブでなくアプリ層のコンポジット(button + clipboard + toast)
53
+ - [pagination](https://melta.tsubotax.com/design/contracts/components/pagination.contract.json): 🚫 not-planned — モバイルは無限スクロール(FlatList onEndReached)で代替。カルーセル文脈は PageControl の領分
54
+ - [sidebar](https://melta.tsubotax.com/design/contracts/components/sidebar.contract.json): 🚫 not-planned — Navigation Drawer の領分=react-navigation 側の責務。DS はトークン供給のみ
55
+ - [table](https://melta.tsubotax.com/design/contracts/components/table.contract.json): 🚫 not-planned — モバイルは単列リスト / Card への再構成が定石(M3 に data table なし)。list + metric で分解表現する
56
+ - [tooltip](https://melta.tsubotax.com/design/contracts/components/tooltip.contract.json): 🚫 not-planned — hover 前提のため。iPhone HIG に tooltip 概念なし。必要になれば長押し Hint として別契約を切る
57
+
58
+ ## Optional
59
+
60
+ - [melta-ui llms.txt](https://melta.tsubotax.com/llms.txt): web 版デザインシステムの AI 入口(デザイン言語の一次資料)
61
+ - [npm](https://www.npmjs.com/package/melta-app): パッケージ(Icon は melta-app/icons、SafeArea 差替は melta-app/safe-area サブパス)
62
+ - [THIRD_PARTY_LICENSES.md](https://github.com/tsubotax/melta-app/blob/main/THIRD_PARTY_LICENSES.md): Icon グリフの帰属表示
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "melta-app",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
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",
@@ -43,6 +43,8 @@
43
43
  "lib",
44
44
  "src",
45
45
  "!src/**/*.test.*",
46
+ "llms.txt",
47
+ "docs",
46
48
  "THIRD_PARTY_LICENSES.md"
47
49
  ],
48
50
  "sideEffects": false,
@@ -100,7 +102,8 @@
100
102
  "typecheck": "tsc --noEmit && tsc --noEmit -p example/tsconfig.json && tsc --noEmit -p scripts/tsconfig.json",
101
103
  "lint": "eslint . --max-warnings 0",
102
104
  "check:drift": "tsx scripts/check-drift.ts",
103
- "check:installability": "bash scripts/check-installability.sh"
105
+ "check:installability": "bash scripts/check-installability.sh",
106
+ "build:llms": "tsx scripts/build-llms-txt.ts"
104
107
  },
105
108
  "jest": {
106
109
  "preset": "@react-native/jest-preset",
@@ -136,12 +136,15 @@ export function Radio({
136
136
  </View>
137
137
  {hasError ? (
138
138
  // danger.text-light は SemanticColors 外なので style で上書き(Button の textColor と同じ扱い)。
139
- <Text
140
- variant="xs"
141
- style={{ color: group.errorTextStyle.color, marginTop: group.errorTextStyle.marginTop }}
142
- >
143
- {error}
144
- </Text>
139
+ // View の liveRegion で出現を通知(melta Text は a11y props を透過しないため)。
140
+ <View accessibilityLiveRegion="polite">
141
+ <Text
142
+ variant="xs"
143
+ style={{ color: group.errorTextStyle.color, marginTop: group.errorTextStyle.marginTop }}
144
+ >
145
+ {error}
146
+ </Text>
147
+ </View>
145
148
  ) : null}
146
149
  </View>
147
150
  );
@@ -5,11 +5,18 @@
5
5
  * - variant は検証状態(default/error/success)。disabled=true で variant を "disabled" に上書きし、
6
6
  * editable=false + accessibilityState.disabled を併せて適用する(recipe description の規約)。
7
7
  * - error 時は errorText を必ずテキストで表示する(FORM_NO_COLOR_ONLY_ERROR = 色だけで伝えない)。
8
- * RN には aria-describedby 相当が無いため、accessibilityState ではなく Text の実表示で伝える。
8
+ * RN には aria-describedby 相当が無いため、(a) errorText input の accessibilityLabel に合成
9
+ * (再フォーカス時に読み上げ)、(b) errorText 側に accessibilityLiveRegion="polite"(Android は
10
+ * 出現時に通知。iOS の即時通知が要る画面は submit 時に announceForAccessibility を併用する —
11
+ * docs/patterns.md 参照)。
9
12
  * - focus は内部 state(onFocus/onBlur)で recipe states.focus の inputStyle 差分
10
- * (borderColor=primary.500)を重ねる。disabled 時は focus させない。
13
+ * (borderColor=primary.500)を重ねる。disabled 時は focus スタイルを付けない。
14
+ * 外部の onFocus/onBlur は RN の event ごと常に透過する(内部の視覚 state と分離)。
11
15
  * - 色・寸法の決定は pure resolver(textfield.styles.ts)に分離。
12
16
  * recipes/app/textfield.recipe.json との機械照合は scripts/lib/textfield-conformance.test.ts が行う。
17
+ * - RN TextInput へ透過するのは入力メソッド系 props(keyboardType / autoCapitalize / autoCorrect /
18
+ * maxLength / returnKeyType / onSubmitEditing / onBlur / onFocus)のみ。style 系 props は
19
+ * 透過しない(token 純度維持)。
13
20
  */
14
21
 
15
22
  import { useMemo, useState } from "react";
@@ -18,6 +25,9 @@ import {
18
25
  TextInput,
19
26
  View,
20
27
  Text as RNText,
28
+ type KeyboardTypeOptions,
29
+ type ReturnKeyTypeOptions,
30
+ type TextInputProps,
21
31
  type StyleProp,
22
32
  type TextStyle,
23
33
  type ViewStyle,
@@ -70,6 +80,16 @@ interface TextFieldProps {
70
80
  helperText?: string;
71
81
  /** variant="error" 時に表示するエラーメッセージ(色だけでなくテキストで伝える)。 */
72
82
  errorText?: string;
83
+ /** キーボード種別(RN TextInput へ透過)。 */
84
+ keyboardType?: KeyboardTypeOptions;
85
+ autoCapitalize?: "none" | "sentences" | "words" | "characters";
86
+ autoCorrect?: boolean;
87
+ maxLength?: number;
88
+ returnKeyType?: ReturnKeyTypeOptions;
89
+ onSubmitEditing?: TextInputProps["onSubmitEditing"];
90
+ /** blur 時コールバック(blur 検証用)。RN の event ごと透過し、内部の focus スタイル制御と分離。 */
91
+ onBlur?: TextInputProps["onBlur"];
92
+ onFocus?: TextInputProps["onFocus"];
73
93
  style?: StyleProp<ViewStyle>;
74
94
  testID?: string;
75
95
  }
@@ -84,6 +104,14 @@ export function TextField({
84
104
  size = "medium",
85
105
  helperText,
86
106
  errorText,
107
+ keyboardType,
108
+ autoCapitalize,
109
+ autoCorrect,
110
+ maxLength,
111
+ returnKeyType,
112
+ onSubmitEditing,
113
+ onBlur,
114
+ onFocus,
87
115
  style,
88
116
  testID,
89
117
  }: TextFieldProps) {
@@ -109,18 +137,32 @@ export function TextField({
109
137
  placeholder={placeholder}
110
138
  placeholderTextColor={colors["text-muted"]}
111
139
  editable={!disabled}
112
- accessibilityLabel={label}
140
+ keyboardType={keyboardType}
141
+ autoCapitalize={autoCapitalize}
142
+ autoCorrect={autoCorrect}
143
+ maxLength={maxLength}
144
+ returnKeyType={returnKeyType}
145
+ onSubmitEditing={onSubmitEditing}
146
+ accessibilityLabel={showError ? `${label}。エラー: ${errorText}` : label}
113
147
  accessibilityState={{ disabled }}
114
- onFocus={() => {
148
+ onFocus={(e) => {
115
149
  if (!disabled) setFocused(true);
150
+ onFocus?.(e);
151
+ }}
152
+ onBlur={(e) => {
153
+ setFocused(false);
154
+ onBlur?.(e);
116
155
  }}
117
- onBlur={() => setFocused(false)}
118
156
  style={[resolved.input, INPUT_VERTICAL_FIX, focused && !disabled ? focusStyle : null]}
119
157
  />
120
158
  {helperText != null && !showError && (
121
159
  <RNText style={resolved.helperText}>{helperText}</RNText>
122
160
  )}
123
- {showError && <RNText style={resolved.errorText}>{errorText}</RNText>}
161
+ {showError && (
162
+ <RNText accessibilityLiveRegion="polite" style={resolved.errorText}>
163
+ {errorText}
164
+ </RNText>
165
+ )}
124
166
  </View>
125
167
  );
126
168
  }