melta-app 0.2.3 → 0.4.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/LICENSE +2 -2
- package/README.md +39 -0
- package/docs/patterns.md +263 -0
- package/lib/module/components/Radio.js +11 -7
- package/lib/module/components/Radio.js.map +1 -1
- package/lib/module/components/TextField.js +31 -5
- package/lib/module/components/TextField.js.map +1 -1
- package/lib/module/safe-area/index.js +29 -24
- package/lib/module/safe-area/index.js.map +1 -1
- package/lib/module/theme/ThemeProvider.js +45 -6
- package/lib/module/theme/ThemeProvider.js.map +1 -1
- package/lib/module/theme/define-theme.js +360 -0
- package/lib/module/theme/define-theme.js.map +1 -0
- package/lib/module/theme/index.js +2 -0
- package/lib/module/theme/index.js.map +1 -1
- package/lib/typescript/src/components/Radio.d.ts.map +1 -1
- package/lib/typescript/src/components/TextField.d.ts +21 -4
- package/lib/typescript/src/components/TextField.d.ts.map +1 -1
- package/lib/typescript/src/safe-area/index.d.ts +7 -3
- package/lib/typescript/src/safe-area/index.d.ts.map +1 -1
- package/lib/typescript/src/theme/ThemeProvider.d.ts +26 -5
- package/lib/typescript/src/theme/ThemeProvider.d.ts.map +1 -1
- package/lib/typescript/src/theme/define-theme.d.ts +113 -0
- package/lib/typescript/src/theme/define-theme.d.ts.map +1 -0
- package/lib/typescript/src/theme/index.d.ts +3 -0
- package/lib/typescript/src/theme/index.d.ts.map +1 -1
- package/lib/typescript/src/theme/types.d.ts +5 -0
- package/lib/typescript/src/theme/types.d.ts.map +1 -1
- package/llms.txt +62 -0
- package/package.json +5 -2
- package/src/components/Radio.tsx +9 -6
- package/src/components/TextField.tsx +48 -6
- package/src/theme/ThemeProvider.tsx +74 -12
- package/src/theme/define-theme.ts +426 -0
- package/src/theme/index.ts +16 -0
- package/src/theme/types.ts +6 -0
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 tsubotax
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -36,6 +36,44 @@ export default function App() {
|
|
|
36
36
|
|
|
37
37
|
テーマは `ThemeProvider` が OS の light / dark に自動追従(`forcedMode` で固定も可)。トークンは `useTheme()` / `nativeTheme` から取れる。Icon だけは subpath([後述](#icon-だけ-subpath-エントリmelta-appicons))。
|
|
38
38
|
|
|
39
|
+
### ブランドテーマを注入する
|
|
40
|
+
|
|
41
|
+
`theme` を渡すと自分のブランドトークンで塗り替わる。未指定なら melta 既定(`nativeTheme`)。
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { defineTheme, ThemeProvider } from "melta-app";
|
|
45
|
+
|
|
46
|
+
// module スコープで1回だけ組み立てる(render 中に作ると毎レンダー参照が変わり全体が再レンダーする)
|
|
47
|
+
const theme = defineTheme({
|
|
48
|
+
id: "acme",
|
|
49
|
+
color: { /* primary / body / semantic / status */ },
|
|
50
|
+
typography: { /* … */ },
|
|
51
|
+
spacing: { /* … */ }, radius: { /* … */ },
|
|
52
|
+
elevation: { /* … */ }, motion: { /* … */ }, zIndex: { /* … */ },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export default function App() {
|
|
56
|
+
return <ThemeProvider theme={theme}>{/* … */}</ThemeProvider>;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**単一の配色しか持たないブランド**(例: dark しか作らない)は、持っていない mode を **書かない**。
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
const theme = defineTheme({
|
|
64
|
+
id: "acme",
|
|
65
|
+
color: { /* … */ semantic: { dark: darkColors } }, // light は書かない
|
|
66
|
+
/* … */
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- `useTheme().capabilities.colorScheme` が `"single-dark"` として導出される(宣言する欄は無い。`color.semantic` のキー集合がそのまま能力になる)
|
|
71
|
+
- OS が light を返しても **dark で描画する**(警告は出さない。light を作らないのは設計判断で、OS 設定は事故ではない)
|
|
72
|
+
- `forcedMode="light"` のように**対応していない mode を明示指定**した場合も描画は止めず clamp するが、開発時は `console.error` で1回報告する。light/dark トグル UI を出すなら `capabilities.colorScheme` を見て出し分けること
|
|
73
|
+
- `theme.color.semantic.light` を直接読むと、原因を名指しするエラーで落ちる(値を捏造して静かに間違った色を返すことはしない)。現在 mode の色は `useTheme().colors` から取る
|
|
74
|
+
|
|
75
|
+
> 現状 `color.primary` / `text-accent` / `elevation` / status の light 値は、単一 dark のテーマでも**必須のまま**。省略できるのは `color.semantic` の mode だけで、他の軸を「持たない」と宣言する仕組みは後続で入れる。
|
|
76
|
+
|
|
39
77
|
## 設計の核
|
|
40
78
|
|
|
41
79
|
- **契約は共有、実装は各最適**: tokens / 禁止ルール / component 契約は `melta-contracts`(JSON)が SSOT。melta-app に token は持たない(二重化を物理防止)。
|
|
@@ -219,6 +257,7 @@ enableSafeAreaContext({ edges: ["top"] });
|
|
|
219
257
|
- Screen の SafeArea は adapter registry 化済み: default は RN core SafeAreaView(依存ゼロ維持)、`melta-app/safe-area` の `enableSafeAreaContext()` で react-native-safe-area-context に差し替え可(optional peer)
|
|
220
258
|
- ✅ form / feedback 10 個(TextField / Toggle / Checkbox / Radio / Alert / Toast / Progress / Modal / ActionSheet / BottomSheet)— checkbox / radio は Pressable + 描画(svg 非依存)、ActionSheet / BottomSheet は select / dropdown の adapted 変換先の受け皿
|
|
221
259
|
- ✅ showcase(https://app.melta.tsubotax.com — melta-ui 様式シェル + 実 RN カタログの Live 埋め込み。表・統計は契約からビルド時生成)
|
|
260
|
+
- ✅ AI 入口: [llms.txt](https://app.melta.tsubotax.com/llms.txt)(契約から生成・drift 検査対象)+ [docs/patterns.md](docs/patterns.md)(フォームの組み方規範。スニペットは実コードと機械同期)
|
|
222
261
|
- ✅ [npm publish(0.1.0)](https://www.npmjs.com/package/melta-app)
|
|
223
262
|
- ⬜ React Native Directory 登録([PR #2606](https://github.com/react-native-community/directory/pull/2606) レビュー待ち)
|
|
224
263
|
|
package/docs/patterns.md
ADDED
|
@@ -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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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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
|
|
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 相当が無いため、
|
|
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
|
-
|
|
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","
|
|
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
|
|
21
|
-
*
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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","
|
|
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":[]}
|
|
@@ -8,24 +8,63 @@
|
|
|
8
8
|
* useColorScheme() を Provider 内で1回だけ読む(RN コア、追加依存なし)。
|
|
9
9
|
*
|
|
10
10
|
* context value は useMemo 必須(§3): 毎レンダで新オブジェクトを配ると全 consumer が再レンダする。
|
|
11
|
+
*
|
|
12
|
+
* theme 注入(Step 2-①): `theme` を渡すと消費者ブランドの theme で塗り替わる。未指定なら
|
|
13
|
+
* 従来どおり codegen 済みの `nativeTheme`(=既存消費者に破壊的変更なし)。渡せるのは
|
|
14
|
+
* `defineTheme()` の戻り値だけ(define-theme.ts 参照)。
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import { createContext, useContext, useMemo } from "react";
|
|
14
18
|
import { useColorScheme } from "react-native";
|
|
15
19
|
import { nativeTheme } from "./native-theme.js";
|
|
20
|
+
import { defineTheme, isDev, resolveMode } from "./define-theme.js";
|
|
16
21
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
17
22
|
const ThemeContext = /*#__PURE__*/createContext(null);
|
|
23
|
+
|
|
24
|
+
/** 既定 theme。melta-contracts 由来の codegen 結果を注入経路と同じ形に通す(自分でも dogfood する)。 */
|
|
25
|
+
const DEFAULT_THEME = defineTheme({
|
|
26
|
+
...nativeTheme,
|
|
27
|
+
id: "melta"
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** 同じ矛盾を毎レンダー報告しないためのラッチ(StrictMode の二重 render も1回に畳む)。 */
|
|
31
|
+
const reportedViolations = new Set();
|
|
32
|
+
function reportViolation(themeId, violation) {
|
|
33
|
+
const key = `${themeId ?? "(id 未設定)"}:${violation.requested}`;
|
|
34
|
+
if (reportedViolations.has(key)) return;
|
|
35
|
+
reportedViolations.add(key);
|
|
36
|
+
console.error(`melta: forcedMode="${violation.requested}" が指定されたが、この theme は ` + `colorScheme=${violation.colorScheme} なので "${violation.resolved}" で描画する。` + `useTheme().capabilities.colorScheme を見て、対応していない mode は UI 側で出さないこと。`);
|
|
37
|
+
}
|
|
18
38
|
export function ThemeProvider({
|
|
19
39
|
children,
|
|
20
|
-
forcedMode
|
|
40
|
+
forcedMode,
|
|
41
|
+
theme
|
|
21
42
|
}) {
|
|
22
43
|
const system = useColorScheme();
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
theme: nativeTheme,
|
|
44
|
+
const activeTheme = theme ?? DEFAULT_THEME;
|
|
45
|
+
const {
|
|
26
46
|
mode,
|
|
27
|
-
|
|
28
|
-
}
|
|
47
|
+
violation
|
|
48
|
+
} = resolveMode(activeTheme.capabilities.colorScheme, forcedMode, system === "dark" ? "dark" : "light");
|
|
49
|
+
if (isDev && violation) reportViolation(activeTheme.id, violation);
|
|
50
|
+
const value = useMemo(() => {
|
|
51
|
+
const colors = activeTheme.color.semantic[mode];
|
|
52
|
+
if (!colors) {
|
|
53
|
+
// resolveMode が capability に従って選んだ mode なので、ここに来るのは theme が壊れている場合だけ。
|
|
54
|
+
// 色が undefined のまま下流に流れると全 component が別々の場所で死ぬので、ここで落とす。
|
|
55
|
+
throw new Error(`melta: theme "${activeTheme.id ?? "(id 未設定)"}" に mode="${mode}" の semantic 色が無い。`);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
// 配布時の型は NativeTheme のまま(既存の全 style resolver の署名を維持するため)。
|
|
59
|
+
// 単一 colorScheme の theme では持たない mode のキーが実体に無く、型が実体より広い。
|
|
60
|
+
// 到達しうる読みは resolveMode が保証した現在 mode だけで、他 mode を直接読んだ場合は
|
|
61
|
+
// define-theme.ts の guard getter が原因を名指しして落とす。
|
|
62
|
+
theme: activeTheme,
|
|
63
|
+
mode,
|
|
64
|
+
colors,
|
|
65
|
+
capabilities: activeTheme.capabilities
|
|
66
|
+
};
|
|
67
|
+
}, [activeTheme, mode]);
|
|
29
68
|
return /*#__PURE__*/_jsx(ThemeContext.Provider, {
|
|
30
69
|
value: value,
|
|
31
70
|
children: children
|